diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 60721e93b..ede3afdde 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -50,8 +50,8 @@ "peerDependencies": { "@performant-software/geospatial": "^3.1.12", "@performant-software/shared-components": "^3.1.12", - "@peripleo/maplibre": "^0.8.7", - "@peripleo/peripleo": "^0.8.7", + "@peripleo/maplibre": "^0.8.9", + "@peripleo/peripleo": "^0.8.9", "react": ">= 16.13.1 < 19.0.0", "react-dom": ">= 16.13.1 < 19.0.0", "tailwindcss": "^4.1.4" diff --git a/packages/core-data/src/utils/Typesense.js b/packages/core-data/src/utils/Typesense.js index 6318b0888..77af2bd7d 100644 --- a/packages/core-data/src/utils/Typesense.js +++ b/packages/core-data/src/utils/Typesense.js @@ -1,7 +1,8 @@ // @flow import { ObjectJs as ObjectUtils } from '@performant-software/shared-components'; -import { feature, featureCollection } from '@turf/turf'; +import { Map as MapUtils } from '@performant-software/geospatial'; +import { feature, featureCollection, truncate } from '@turf/turf'; import { history } from 'instantsearch.js/es/lib/routers'; import TypesenseInstantsearchAdapter from 'typesense-instantsearch-adapter'; import _ from 'underscore'; @@ -9,6 +10,9 @@ import type { Event as EventType } from '../types/Event'; import type { TypesenseSearchResult } from '../types/typesense/SearchResult'; type Options = { + geometries: { + [uuid: string]: any + }, type?: string }; @@ -163,6 +167,31 @@ const getFieldId = (attribute: string) => { return value; }; +/** + * Returns the geometry object for the passed place/path. + * + * @param place + * @param path + * + * @returns {*} + */ +const getGeometry = (place, path) => { + return _.get(place, path); +}; + +/** + * Returns the geometry URL for the passed place. + * + * @param place + * @param hash + * + * @returns {*} + */ +const getGeometryUrl = (place, hash) => { + const object = hash[place?.uuid]; + return object?.url; +}; + /** * Takes a ._facet formatted attribute and returns the parsed relationship UUID. * @@ -186,6 +215,8 @@ const getRelationshipId = (attribute: string) => { * @param geometry * * @returns {Feature<*, {ccode: [], record_id: *, names: *, name: *, id: *, title: *, type: *, uuid: *, items: [*]}>} + * + * @deprecated */ const toFeature = (record: any, item: any, geometry: any) => { const properties = { @@ -197,7 +228,9 @@ const toFeature = (record: any, item: any, geometry: any) => { name: record.name, names: record.names?.map((toponym: string) => ({ toponym })), type: record.type, - items: [item] + items: [item], + url: record.url, + layerId: record.layerId }; const id = parseInt(record.record_id, 10); @@ -212,6 +245,8 @@ const toFeature = (record: any, item: any, geometry: any) => { * @param options * * @returns {FeatureCollection} + * + * @deprecated */ const toFeatureCollection = (results: Array, path: string, options: Options = {}) => { const features = []; @@ -265,11 +300,87 @@ const toFeatureCollection = (results: Array, path: string, options: Options return featureCollection(features); }; +/** + * Returns a set of GeoJSON features for the passed results. + * + * @param features + * @param results + * @param path + * @param options + * + * @returns {*} + */ +const getFeatures = (features, results, path, 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 placeIds = []; + const recordIds = []; + + _.each(results, (result) => { + recordIds.push(result.uuid); + + const places = _.isEmpty(objectPath) ? [result] : ObjectUtils.getNestedValue(result, objectPath); + + _.each(places, (place) => { + placeIds.push(place.uuid); + + let geometry; + let geometryUrl; + let layerId; + + if (options.geometries) { + geometryUrl = getGeometryUrl(place, options.geometries); + } else { + geometry = getGeometry(place, geometryPath); + } + + const include = geometryUrl || (geometry && (!options.type || geometry.type === options.type)); + + if (include) { + const record = _.find(newFeatures, (f) => f.properties?.uuid === place.uuid); + const trimmedResult = trimResult(result, objectPath); + + if (record) { + const item = _.find(record.properties?.items, (item) => item.uuid === trimmedResult.uuid); + + if (!item) { + record.properties?.items.push(trimmedResult); + } + } else { + newFeatures.push(MapUtils.toFeature({ ...place, layerId, url: geometryUrl }, trimmedResult, geometry)); + } + } + }); + }); + + return _.map(newFeatures, (feature) => ({ + ...feature, + properties: { + ...feature.properties, + visible: placeIds.includes(feature.properties.uuid), + items: _.filter(feature.properties.items, (item) => recordIds.includes(item.uuid)) + } + })); +}; + +/** + * Trims the Typesense document to only include data needed for map visualizations. + * + * @param result + * + * @returns {*} + */ +const trimResult = (result) => _.pick(result, 'id', 'uuid', 'record_id', 'name', 'names'); + export default { createCachedHits, createRouting, createTypesenseAdapter, getDate, + getFeatures, getFieldId, getRelationshipId, toFeature, diff --git a/packages/geospatial/package.json b/packages/geospatial/package.json index 2e2a66475..f92a9c515 100644 --- a/packages/geospatial/package.json +++ b/packages/geospatial/package.json @@ -29,7 +29,7 @@ "underscore": "^1.13.7" }, "peerDependencies": { - "@peripleo/maplibre": "^0.8.7", + "@peripleo/maplibre": "^0.8.9", "react": ">= 16.13.1 < 19.0.0", "react-dom": ">= 16.13.1 < 19.0.0" }, diff --git a/packages/geospatial/src/components/LocationMarkers.js b/packages/geospatial/src/components/LocationMarkers.js index 8b5d5b8a2..ebfadc7f2 100644 --- a/packages/geospatial/src/components/LocationMarkers.js +++ b/packages/geospatial/src/components/LocationMarkers.js @@ -86,7 +86,12 @@ type Props = { /** * GeoJSON layer stroke style */ - strokeStyle?: { [key: string]: any } + strokeStyle?: { [key: string]: any }, + + /** + * If true, the layer will be visible on the map. + */ + visible?: boolean }; const DEFAULT_BUFFER = 2; @@ -141,6 +146,7 @@ const LocationMarkers = (props: Props) => { interactive={props.interactive} strokeStyle={props.strokeStyle} pointStyle={props.pointStyle} + visible={props.visible} /> ); diff --git a/packages/geospatial/src/utils/Map.js b/packages/geospatial/src/utils/Map.js index bd039578a..fdd055b37 100644 --- a/packages/geospatial/src/utils/Map.js +++ b/packages/geospatial/src/utils/Map.js @@ -1,7 +1,14 @@ // @flow import { WarpedMapLayer } from '@allmaps/maplibre'; -import { bbox, bboxPolygon, buffer } from '@turf/turf'; +import { + bbox, + bboxPolygon, + buffer, + feature, + featureCollection, + truncate +} from '@turf/turf'; import _ from 'underscore'; const MIN_LATITUDE = -90; @@ -73,6 +80,55 @@ const getBoundingBox = (data, bufferDistance = null) => { */ const removeLayer = (map, layerId) => map && map.removeLayer(layerId); +/** + * Wraps the passed record in a feature. + * + * @param record + * @param item + * @param geometry + * + * @returns {Feature} + */ +const toFeature = (record: any, item: any, geometry: any) => { + const properties = { + id: record.record_id, + ccode: [], + title: record.name, + uuid: record.uuid, + record_id: record.record_id, + name: record.name, + names: record.names?.map((toponym: string) => ({ toponym })), + type: record.type, + items: [item], + url: record.url + }; + + const id = parseInt(record.record_id, 10); + const data = geometry ? truncate(geometry, { precision: 3, coordinates: 2 }) : geometry; + + return feature(data, properties, { id }); +}; + +/** + * Returns a feature collection for the passed set of features. + * + * @param features + * + * @returns {FeatureCollection} + */ +const toFeatureCollection = (features: Array) => featureCollection(features); + /** * Validates that the passed bounding box contains finite coordinates. * @@ -108,6 +164,8 @@ export default { addGeoreferenceLayer, getBoundingBox, removeLayer, + toFeature, + toFeatureCollection, validateBoundingBox, validateCoordinates }; diff --git a/packages/storybook/package.json b/packages/storybook/package.json index 338001670..a9833519f 100644 --- a/packages/storybook/package.json +++ b/packages/storybook/package.json @@ -17,8 +17,8 @@ "@bunchtogether/vite-plugin-flow": "^1.0.2", "@faker-js/faker": "^8.0.2", "@headlessui/react": "^1.7.18", - "@peripleo/maplibre": "^0.4.2", - "@peripleo/peripleo": "^0.4.2", + "@peripleo/maplibre": "^0.8.9", + "@peripleo/peripleo": "^0.8.9", "@storybook/addon-a11y": "9.0.17", "@storybook/addon-docs": "9.0.17", "@storybook/addon-links": "9.0.17", diff --git a/packages/storybook/src/core-data/EventDetails.stories.js b/packages/storybook/src/core-data/EventDetails.stories.js index b223c340b..876b4ed1d 100644 --- a/packages/storybook/src/core-data/EventDetails.stories.js +++ b/packages/storybook/src/core-data/EventDetails.stories.js @@ -1,8 +1,8 @@ // @flow import { faker } from '@faker-js/faker'; -import { Map, Zoom } from '@peripleo/maplibre'; -import { Controls, Peripleo } from '@peripleo/peripleo'; +import { Map } from '@peripleo/maplibre'; +import { Peripleo } from '@peripleo/peripleo'; import { action } from 'storybook/actions'; import React from 'react'; import _ from 'underscore'; @@ -48,11 +48,6 @@ export const RelatedRecords = withCoreDataContextProvider(() => { - - -
( - - -
( - - -
( - - -
( - - -
{ - - -
( - - -
( - - -
( - - -
( - - -
( - - -
( - - -
( - - -
( ]} style={mapStyle} > - - -