tools/geo/src/modules/geojson.js

/**
* @module @svizzle/geo/geojson
*/

import bbox from '@turf/bbox';
import centroid from '@turf/centroid';
import {featureCollection} from '@turf/helpers';
import truncate from '@turf/truncate';
import * as _ from 'lamb';

/**
 * Return or create the {@link https://tools.ietf.org/html/rfc7946#section-5|bbox} of the provided geojson
 *
 * @function
 * @arg {object} geojson - Geojson object
 * @return {array}
 *
 * @example
> getOrMakeBBox({
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[1, -1], [1, 1], [-1, 1], [-1, -1], [1, -1]]
			]
		}
	}, {
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[2, -1], [2, 1], [0, 1], [0, -1], [2, -1]]
			]
		}
	}]
})
[-1, -1, 2, 1]

> // no calculation involved here
> getOrMakeBBox({
	type: 'FeatureCollection',
	bbox: [-10.0, -10.0, 10.0, 10.0],
	geometry: {
	type: 'Polygon',
	coordinates: [
		[
			[-10.0, -10.0],
			[10.0, -10.0],
			[10.0, 10.0],
			[-10.0, -10.0]
		]
	]
	}
})
[-10.0, -10.0, 10.0, 10.0]
 * @since 0.1.0
 */
export const getOrMakeBBox = json => json.bbox ? json.bbox : bbox(json);

/**
 * Return a function expecting a geojson and creating or updating the provided property of all features using the provided map.
 * Note that you can pass a `key or an alternative key `key_alt` e.g. when you use ISO Alpha 2 codes and you need to identify unrecognized territories with another key.
 *
 * @function
 * @arg {object} args - Geojson object
 * @arg {string} args.key_alt - Alternative key to be found in properties in `key` is not found.
 * @arg {string} args.key - Key to be found in properties
 * @arg {object} args.map - Mapping key (string) -> string
 * @arg {function} args.mapFn - Function key (string) -> string
 * @arg {string} args.propName - Name of the property to be added to `properties`
 * @return {function} - Object -> Object
 *
 * @example
> geojson = {
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[1, -1], [1, 1], [-1, 1], [-1, -1], [1, -1]]
			]
		},
		properties: {iso_a2: 'BF'}
	}, {
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[2, -1], [2, 1], [0, 1], [0, -1], [2, -1]]
			]
		},
		properties: {name: 'Kosovo'}
	}, {
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[4, -1], [2, 7], [0, 5], [0, -4], [4, -1]]
			]
		},
		properties: {iso_a2: 'FR'}
	}]
}
> keyToColor = {BF: 'red', Kosovo: 'yellow'}
> addColor = makeAddFeaturesProperty({
	propName: 'color',
	map: keyToColor,
	key: 'iso_a2',
	key_alt: 'name'
})
> coloredFeatures = addColor(geojson)
{
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[1, -1], [1, 1], [-1, 1], [-1, -1], [1, -1]]
			]
		},
		properties: {iso_a2: 'BF', color: 'red'}
	}, {
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[2, -1], [2, 1], [0, 1], [0, -1], [2, -1]]
			]
		},
		properties: {name: 'Kosovo', color: 'yellow'}
	}, {
		type: 'Feature',
		geometry: {
			type: 'Polygon',
			coordinates: [
				[[4, -1], [2, 7], [0, 5], [0, -4], [4, -1]]
			]
		},
		properties: {iso_a2: 'FR', color: undefined}
	}]
}
 * @since 0.5.0
 */
export const makeUpdateFeaturesProperty = ({
	key_alt,
	key,
	map,
	mapFn,
	propName,
}) =>
	_.updateKey('features', _.mapWith(
		_.updateKey('properties', properties => {
			let propValue;

			if (map) {
				propValue = _.has(map, properties[key])
					? map[properties[key]]
					: _.has(map, properties[key_alt])
						? map[properties[key_alt]]
						: undefined
			} else if (mapFn) {
				propValue = properties[key]
					? mapFn(properties[key])
					: properties[key_alt]
						? mapFn(properties[key_alt])
						: undefined
			}

			return {
				...properties,
				[propName]: propValue
			}
		})
	));

/**
 * Return the a collection of centroids of the provided features, each having the correspondent feature properties.
 *
 * @function
 * @arg {array} features - Array of features
 * @return {object} collection - FeatureCollection of Point features
 *
 * @example
> makeCentroids([
	{
		type: 'Feature',
		properties: {foo: 'a'},
		geometry: {type: 'LineString', coordinates: [
			[[1, -1], [1, 1], [-1, 1], [-1, -1], [1, -1]]
		]}
	},
	{
		type: 'Feature',
		properties: {foo: 'b'},
		geometry: {type: 'LineString', coordinates: [
			[[2, -1], [2, 1], [0, 1], [0, -1], [2, -1]]
		]}
	}
])
{
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [0.2, -0.2]},
		properties: {foo: 'a'}
	}, {
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [1.2, -0.2]},
		properties: {foo: 'b'}
	}]
}
 * @since 0.1.0
 */
export const makeCentroids = _.pipe([
	_.mapWith(feature => centroid(feature, {properties: feature.properties})),
	featureCollection
]);

/**
 * Return a function expecting an object and returning it as a Point feature.
 * You can define a coordPicker using {@link makeKeysGetter}:
 * const getCoordinates = makeKeysGetter(['lng', 'lat'])
 *
 * @function
 * @arg {function} coordPicker - The function to create the point coordinates ([longitude, latitude]) from the provided feature
 * @arg {function} propsTransformer - The function to create the properties of the resulting point from the provided feature
 * @return {object} point - Geojson Point feature.
 *
 * @example
> coordPicker = _.collect([_.getKey('lng'), _.getKey('lat')])
> toPointFeature = makeToPointFeature(coordPicker)
> toPointFeature({foo: 'a', lng: 0.1, lat: 0.1})
{
	type: 'Feature',
	geometry: {type: 'Point', coordinates: [0.1, 0.1]},
	properties: {foo: 'a', lng: 0.1, lat: 0.1}
}

> const propsTransformer = applyFnMap({name: _.getKey('foo')})
> const toPointFeature = makeToPointFeature(coordPicker, propsTransformer)
> toPointFeature({foo: 'a', lng: 0.1, lat: 0.1})
{
	type: 'Feature',
	geometry: {type: 'Point', coordinates: [0.1, 0.1]},
	properties: {name: 'a'}
}
 * @since 0.1.0
 */
export const makeToPointFeature = (coordPicker, propsTransformer = null) =>
	object => ({
		type: 'Feature',
		geometry: {
			type: 'Point',
			coordinates: coordPicker(object)
		},
		properties: propsTransformer ? propsTransformer(object) : object
	});

/**
 * Return a function expecting an array of objects and returning them as a FeatureCollection of Point features.
 * You can define a coordPicker using {@link makeKeysGetter}:
 * const getCoordinates = makeKeysGetter(['lng', 'lat'])
 *
 * @function
 * @arg {function} coordPicker - The function to create the point coordinates ([longitude, latitude]) from the provided features
 * @arg {function} propsTransformer - The function to create the properties of the resulting points from the provided features
 * @return {object} collection - FeatureCollection of Point features
 *
 * @example
> coordPicker = _.collect([_.getKey('lng'), _.getKey('lat')])
> toGeoPoints = makeToGeoPoints(coordPicker)
> toGeoPoints([
	{foo: 'a', lng: 0.1, lat: 0.1},
	{foo: 'b', lng: 0.2, lat: 0.2}
])
{
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [0.1, 0.1]},
		properties: {foo: 'a', lng: 0.1, lat: 0.1}
	}, {
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [0.2, 0.2]},
		properties: {foo: 'b', lng: 0.2, lat: 0.2}
	}]
}

> propsTransformer = applyFnMap({name: _.getKey('foo')})
> toGeoPoints = makeToGeoPoints(coordPicker, propsTransformer)
> toGeoPoints([
	{foo: 'a', lng: 0.1, lat: 0.1},
	{foo: 'b', lng: 0.2, lat: 0.2}
])
{
	type: 'FeatureCollection',
	features: [{
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [0.1, 0.1]},
		properties: {name: 'a'}
	}, {
		type: 'Feature',
		geometry: {type: 'Point', coordinates: [0.2, 0.2]},
		properties: {name: 'b'}
	}]
}
 * @since 0.1.0
 */
export const makeToGeoPoints = (coordPicker, propsTransformer) => _.pipe([
	_.mapWith(makeToPointFeature(coordPicker, propsTransformer)),
	featureCollection
]);

// TODO use a reduce to include only items with lat/lng as defined by coordPicker

/**
 * Return a function returning a copy of the provided geojson having the geometry coordinates rounded to the given precision.
 *
 * @function
 * @arg {number} precision - coordinate decimal precision
 * @return {function} - Geojson -> Geojson
 *
 * @example
> truncateGeometry = setGeometryPrecision(4)
> point = {
	type: 'Feature',
	geometry: {type: 'Point', coordinates: [0.1234567, 0.12341]},
	properties: {name: 'a'}
}
> truncateGeometry(point)
{
	type: 'Feature',
	geometry: {type: 'Point', coordinates: [0.1234, 0.1234]},
	properties: {name: 'a'}
}
 * @since 0.1.0
 */
export const setGeometryPrecision = precision =>
	geojson => truncate(geojson, {precision, mutate: false});

// convenience function
export const truncateGeojson = setGeometryPrecision(4);

// TODO DOC: define FeatureCollection type