diff --git a/packages/core-data/src/utils/Typesense.js b/packages/core-data/src/utils/Typesense.js index 77af2bd7..51a31e90 100644 --- a/packages/core-data/src/utils/Typesense.js +++ b/packages/core-data/src/utils/Typesense.js @@ -2,7 +2,7 @@ import { ObjectJs as ObjectUtils } from '@performant-software/shared-components'; import { Map as MapUtils } from '@performant-software/geospatial'; -import { feature, featureCollection, truncate } from '@turf/turf'; +import { feature, featureCollection } from '@turf/turf'; import { history } from 'instantsearch.js/es/lib/routers'; import TypesenseInstantsearchAdapter from 'typesense-instantsearch-adapter'; import _ from 'underscore'; @@ -179,6 +179,16 @@ const getGeometry = (place, path) => { return _.get(place, path); }; +/** + * Returns the properties object for the passed place/path. + * + * @param place + * @param path + */ +const getProperties = (place, path) => { + return _.get(place, path) || {}; +}; + /** * Returns the geometry URL for the passed place. * @@ -230,7 +240,8 @@ const toFeature = (record: any, item: any, geometry: any) => { type: record.type, items: [item], url: record.url, - layerId: record.layerId + layerId: record.layerId, + originalProperties: record.properties }; const id = parseInt(record.record_id, 10); @@ -310,11 +321,20 @@ const toFeatureCollection = (results: Array, path: string, options: Options * * @returns {*} */ -const getFeatures = (features, results, path, options = {}) => { +const getFeatures = ( + features, + results, + geometryPath, + propertiesPath, + options = {} +) => { const newFeatures = [...features]; - const objectPath = path.substring(0, path.lastIndexOf(ATTRIBUTE_DELIMITER)); - const geometryPath = path.substring(path.lastIndexOf(ATTRIBUTE_DELIMITER) + 1, path.length); + const objectPath = geometryPath.substring(0, geometryPath.lastIndexOf(ATTRIBUTE_DELIMITER)); + const geoJsonPath = geometryPath.substring(geometryPath.lastIndexOf(ATTRIBUTE_DELIMITER) + 1, geometryPath.length); + const originalPropertiesPath = propertiesPath + ? propertiesPath.substring(propertiesPath.lastIndexOf(ATTRIBUTE_DELIMITER) + 1, propertiesPath.length) + : null; const placeIds = []; const recordIds = []; @@ -334,9 +354,13 @@ const getFeatures = (features, results, path, options = {}) => { if (options.geometries) { geometryUrl = getGeometryUrl(place, options.geometries); } else { - geometry = getGeometry(place, geometryPath); + geometry = getGeometry(place, geoJsonPath); } + const properties = originalPropertiesPath + ? getProperties(place, originalPropertiesPath) + : null; + const include = geometryUrl || (geometry && (!options.type || geometry.type === options.type)); if (include) { @@ -350,7 +374,11 @@ const getFeatures = (features, results, path, options = {}) => { record.properties?.items.push(trimmedResult); } } else { - newFeatures.push(MapUtils.toFeature({ ...place, layerId, url: geometryUrl }, trimmedResult, geometry)); + newFeatures.push(MapUtils.toFeature({ + ...place, + layerId, + url: geometryUrl + }, trimmedResult, geometry, properties)); } } }); diff --git a/packages/geospatial/src/components/CertaintyLayer.js b/packages/geospatial/src/components/CertaintyLayer.js new file mode 100644 index 00000000..10e503a5 --- /dev/null +++ b/packages/geospatial/src/components/CertaintyLayer.js @@ -0,0 +1,36 @@ +// @flow + +import React from 'react'; +import { Layer, Source } from 'react-map-gl/maplibre'; +import MapStyles from '../utils/MapStyles'; +import MapUtils from '../utils/Map'; + +type Props = { + geometry?: any, + certaintyRadius: number +}; + +/** + * Renders circles with the given certainty_radius circumference around all points in a new layer. + */ +const CertaintyLayer = (props: Props) => { + const circles = MapUtils.getCertaintyCircles([props.geometry], () => props.certaintyRadius); + + return ( + + + + ); +}; + +export default CertaintyLayer; + diff --git a/packages/geospatial/src/index.js b/packages/geospatial/src/index.js index e6ab844a..60e4ff86 100644 --- a/packages/geospatial/src/index.js +++ b/packages/geospatial/src/index.js @@ -1,6 +1,7 @@ // @flow // Components +export { default as CertaintyLayer } from './components/CertaintyLayer'; export { default as DrawControl } from './components/DrawControl'; export { default as GeoJsonLayer } from './components/GeoJsonLayer'; export { default as GeocodingControl } from './components/GeocodingControl'; diff --git a/packages/geospatial/src/utils/Map.js b/packages/geospatial/src/utils/Map.js index add9aa43..cdfb9167 100644 --- a/packages/geospatial/src/utils/Map.js +++ b/packages/geospatial/src/utils/Map.js @@ -9,12 +9,55 @@ import { featureCollection } from '@turf/turf'; import _ from 'underscore'; +import circle from '@turf/circle'; const MIN_LATITUDE = -90; const MAX_LATITUDE = 90; const MIN_LONGITUDE = -180; const MAX_LONGITUDE = 180; +/** + * Returns a GeoJSON circle feature with the given center point and radius. + * @param point - The center point of the circle. + * @param radius - The radius of the circle in kilometers. + * @returns {Feature} - The GeoJSON circle feature. + */ +const buildCircle = (point, radius) => ( + circle(point.coordinates, radius, { units: 'kilometers', steps: 32 }) +); + +/** + * Returns a GeoJSON feature collection containing circles for each item in the given array. + */ +const getCertaintyCircles = ( + items, + getCertaintyRadius: (item: any) => number | undefined +) => { + const features = []; + + for (const item of items) { + if (getCertaintyRadius(item)) { + if (item.geometry?.type === 'FeatureCollection') { + for (const childFeature of item.geometry.features) { + if (childFeature.geometry?.type === 'Point') { + features.push(buildCircle(childFeature.geometry, getCertaintyRadius(item))); + } + } + } else if (item.geometry.type === 'GeometryCollection') { + for (const geometry of item.geometry.geometries) { + if (geometry.type === 'Point') { + features.push(buildCircle(geometry, getCertaintyRadius(item))); + } + } + } else if (item.geometry?.type === 'Point') { + features.push(buildCircle(item.geometry, getCertaintyRadius(item))); + } + } + } + + return featureCollection(features); +}; + /** * Adds the geo-referenced image layer to the passed map. * @@ -85,6 +128,7 @@ const removeLayer = (map, layerId) => map && map.removeLayer(layerId); * @param record * @param item * @param geometry + * @param originalProperties * * @returns {Feature map && map.removeLayer(layerId); * url: * * }>} */ -const toFeature = (record: any, item: any, geometry: any) => { +const toFeature = (record: any, item: any, geometry: any, originalProperties?: any) => { const properties = { id: record.record_id, ccode: [], @@ -110,7 +154,8 @@ const toFeature = (record: any, item: any, geometry: any) => { names: record.names?.map((toponym: string) => ({ toponym })), type: record.type, items: [item], - url: record.url + url: record.url, + originalProperties: originalProperties || {} }; const id = parseInt(record.record_id, 10); @@ -159,6 +204,7 @@ const validateCoordinates = (coordinates) => { export default { addGeoreferenceLayer, + getCertaintyCircles, getBoundingBox, removeLayer, toFeature,