Skip to content

Commit 5831ed7

Browse files
authored
Merge pull request #569 from performant-software/feature/cdp428_preload_map
CDP #428 - Preload map data
2 parents b1f896c + 16b91fa commit 5831ed7

File tree

13 files changed

+559
-172
lines changed

13 files changed

+559
-172
lines changed

packages/core-data/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@
5050
"peerDependencies": {
5151
"@performant-software/geospatial": "^3.1.12",
5252
"@performant-software/shared-components": "^3.1.12",
53-
"@peripleo/maplibre": "^0.8.7",
54-
"@peripleo/peripleo": "^0.8.7",
53+
"@peripleo/maplibre": "^0.8.9",
54+
"@peripleo/peripleo": "^0.8.9",
5555
"react": ">= 16.13.1 < 19.0.0",
5656
"react-dom": ">= 16.13.1 < 19.0.0",
5757
"tailwindcss": "^4.1.4"

packages/core-data/src/utils/Typesense.js

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
// @flow
22

33
import { ObjectJs as ObjectUtils } from '@performant-software/shared-components';
4-
import { feature, featureCollection } from '@turf/turf';
4+
import { Map as MapUtils } from '@performant-software/geospatial';
5+
import { feature, featureCollection, truncate } from '@turf/turf';
56
import { history } from 'instantsearch.js/es/lib/routers';
67
import TypesenseInstantsearchAdapter from 'typesense-instantsearch-adapter';
78
import _ from 'underscore';
89
import type { Event as EventType } from '../types/Event';
910
import type { TypesenseSearchResult } from '../types/typesense/SearchResult';
1011

1112
type Options = {
13+
geometries: {
14+
[uuid: string]: any
15+
},
1216
type?: string
1317
};
1418

@@ -163,6 +167,31 @@ const getFieldId = (attribute: string) => {
163167
return value;
164168
};
165169

170+
/**
171+
* Returns the geometry object for the passed place/path.
172+
*
173+
* @param place
174+
* @param path
175+
*
176+
* @returns {*}
177+
*/
178+
const getGeometry = (place, path) => {
179+
return _.get(place, path);
180+
};
181+
182+
/**
183+
* Returns the geometry URL for the passed place.
184+
*
185+
* @param place
186+
* @param hash
187+
*
188+
* @returns {*}
189+
*/
190+
const getGeometryUrl = (place, hash) => {
191+
const object = hash[place?.uuid];
192+
return object?.url;
193+
};
194+
166195
/**
167196
* Takes a <relationship-uuid>.<field-uuid>_facet formatted attribute and returns the parsed relationship UUID.
168197
*
@@ -186,6 +215,8 @@ const getRelationshipId = (attribute: string) => {
186215
* @param geometry
187216
*
188217
* @returns {Feature<*, {ccode: [], record_id: *, names: *, name: *, id: *, title: *, type: *, uuid: *, items: [*]}>}
218+
*
219+
* @deprecated
189220
*/
190221
const toFeature = (record: any, item: any, geometry: any) => {
191222
const properties = {
@@ -197,7 +228,9 @@ const toFeature = (record: any, item: any, geometry: any) => {
197228
name: record.name,
198229
names: record.names?.map((toponym: string) => ({ toponym })),
199230
type: record.type,
200-
items: [item]
231+
items: [item],
232+
url: record.url,
233+
layerId: record.layerId
201234
};
202235

203236
const id = parseInt(record.record_id, 10);
@@ -212,6 +245,8 @@ const toFeature = (record: any, item: any, geometry: any) => {
212245
* @param options
213246
*
214247
* @returns {FeatureCollection<Geometry, Properties>}
248+
*
249+
* @deprecated
215250
*/
216251
const toFeatureCollection = (results: Array<any>, path: string, options: Options = {}) => {
217252
const features = [];
@@ -265,11 +300,87 @@ const toFeatureCollection = (results: Array<any>, path: string, options: Options
265300
return featureCollection(features);
266301
};
267302

303+
/**
304+
* Returns a set of GeoJSON features for the passed results.
305+
*
306+
* @param features
307+
* @param results
308+
* @param path
309+
* @param options
310+
*
311+
* @returns {*}
312+
*/
313+
const getFeatures = (features, results, path, options = {}) => {
314+
const newFeatures = [...features];
315+
316+
const objectPath = path.substring(0, path.lastIndexOf(ATTRIBUTE_DELIMITER));
317+
const geometryPath = path.substring(path.lastIndexOf(ATTRIBUTE_DELIMITER) + 1, path.length);
318+
319+
const placeIds = [];
320+
const recordIds = [];
321+
322+
_.each(results, (result) => {
323+
recordIds.push(result.uuid);
324+
325+
const places = _.isEmpty(objectPath) ? [result] : ObjectUtils.getNestedValue(result, objectPath);
326+
327+
_.each(places, (place) => {
328+
placeIds.push(place.uuid);
329+
330+
let geometry;
331+
let geometryUrl;
332+
let layerId;
333+
334+
if (options.geometries) {
335+
geometryUrl = getGeometryUrl(place, options.geometries);
336+
} else {
337+
geometry = getGeometry(place, geometryPath);
338+
}
339+
340+
const include = geometryUrl || (geometry && (!options.type || geometry.type === options.type));
341+
342+
if (include) {
343+
const record = _.find(newFeatures, (f) => f.properties?.uuid === place.uuid);
344+
const trimmedResult = trimResult(result, objectPath);
345+
346+
if (record) {
347+
const item = _.find(record.properties?.items, (item) => item.uuid === trimmedResult.uuid);
348+
349+
if (!item) {
350+
record.properties?.items.push(trimmedResult);
351+
}
352+
} else {
353+
newFeatures.push(MapUtils.toFeature({ ...place, layerId, url: geometryUrl }, trimmedResult, geometry));
354+
}
355+
}
356+
});
357+
});
358+
359+
return _.map(newFeatures, (feature) => ({
360+
...feature,
361+
properties: {
362+
...feature.properties,
363+
visible: placeIds.includes(feature.properties.uuid),
364+
items: _.filter(feature.properties.items, (item) => recordIds.includes(item.uuid))
365+
}
366+
}));
367+
};
368+
369+
/**
370+
* Trims the Typesense document to only include data needed for map visualizations.
371+
*
372+
* @param result
373+
*
374+
* @returns {*}
375+
*/
376+
const trimResult = (result) => _.pick(result, 'id', 'uuid', 'record_id', 'name', 'names');
377+
268378
export default {
269379
createCachedHits,
270380
createRouting,
271381
createTypesenseAdapter,
272382
getDate,
383+
getFeatures,
273384
getFieldId,
274385
getRelationshipId,
275386
toFeature,

packages/geospatial/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"underscore": "^1.13.7"
3030
},
3131
"peerDependencies": {
32-
"@peripleo/maplibre": "^0.8.7",
32+
"@peripleo/maplibre": "^0.8.9",
3333
"react": ">= 16.13.1 < 19.0.0",
3434
"react-dom": ">= 16.13.1 < 19.0.0"
3535
},

packages/geospatial/src/components/LocationMarkers.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ type Props = {
8686
/**
8787
* GeoJSON layer stroke style
8888
*/
89-
strokeStyle?: { [key: string]: any }
89+
strokeStyle?: { [key: string]: any },
90+
91+
/**
92+
* If true, the layer will be visible on the map.
93+
*/
94+
visible?: boolean
9095
};
9196

9297
const DEFAULT_BUFFER = 2;
@@ -141,6 +146,7 @@ const LocationMarkers = (props: Props) => {
141146
interactive={props.interactive}
142147
strokeStyle={props.strokeStyle}
143148
pointStyle={props.pointStyle}
149+
visible={props.visible}
144150
/>
145151
</>
146152
);

packages/geospatial/src/utils/Map.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
// @flow
22

33
import { WarpedMapLayer } from '@allmaps/maplibre';
4-
import { bbox, bboxPolygon, buffer } from '@turf/turf';
4+
import {
5+
bbox,
6+
bboxPolygon,
7+
buffer,
8+
feature,
9+
featureCollection,
10+
truncate
11+
} from '@turf/turf';
512
import _ from 'underscore';
613

714
const MIN_LATITUDE = -90;
@@ -73,6 +80,55 @@ const getBoundingBox = (data, bufferDistance = null) => {
7380
*/
7481
const removeLayer = (map, layerId) => map && map.removeLayer(layerId);
7582

83+
/**
84+
* Wraps the passed record in a feature.
85+
*
86+
* @param record
87+
* @param item
88+
* @param geometry
89+
*
90+
* @returns {Feature<Geometry, {
91+
* id: *,
92+
* ccode: [],
93+
* title: *,
94+
* uuid: *,
95+
* record_id: *,
96+
* name: *,
97+
* names: *,
98+
* type: *,
99+
* items: [*],
100+
* url: *
101+
* }>}
102+
*/
103+
const toFeature = (record: any, item: any, geometry: any) => {
104+
const properties = {
105+
id: record.record_id,
106+
ccode: [],
107+
title: record.name,
108+
uuid: record.uuid,
109+
record_id: record.record_id,
110+
name: record.name,
111+
names: record.names?.map((toponym: string) => ({ toponym })),
112+
type: record.type,
113+
items: [item],
114+
url: record.url
115+
};
116+
117+
const id = parseInt(record.record_id, 10);
118+
const data = geometry ? truncate(geometry, { precision: 3, coordinates: 2 }) : geometry;
119+
120+
return feature(data, properties, { id });
121+
};
122+
123+
/**
124+
* Returns a feature collection for the passed set of features.
125+
*
126+
* @param features
127+
*
128+
* @returns {FeatureCollection<Geometry, GeoJsonProperties>}
129+
*/
130+
const toFeatureCollection = (features: Array<any>) => featureCollection(features);
131+
76132
/**
77133
* Validates that the passed bounding box contains finite coordinates.
78134
*
@@ -108,6 +164,8 @@ export default {
108164
addGeoreferenceLayer,
109165
getBoundingBox,
110166
removeLayer,
167+
toFeature,
168+
toFeatureCollection,
111169
validateBoundingBox,
112170
validateCoordinates
113171
};

packages/storybook/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
"@bunchtogether/vite-plugin-flow": "^1.0.2",
1818
"@faker-js/faker": "^8.0.2",
1919
"@headlessui/react": "^1.7.18",
20-
"@peripleo/maplibre": "^0.4.2",
21-
"@peripleo/peripleo": "^0.4.2",
20+
"@peripleo/maplibre": "^0.8.9",
21+
"@peripleo/peripleo": "^0.8.9",
2222
"@storybook/addon-a11y": "9.0.17",
2323
"@storybook/addon-docs": "9.0.17",
2424
"@storybook/addon-links": "9.0.17",

packages/storybook/src/core-data/EventDetails.stories.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
// @flow
22

33
import { faker } from '@faker-js/faker';
4-
import { Map, Zoom } from '@peripleo/maplibre';
5-
import { Controls, Peripleo } from '@peripleo/peripleo';
4+
import { Map } from '@peripleo/maplibre';
5+
import { Peripleo } from '@peripleo/peripleo';
66
import { action } from 'storybook/actions';
77
import React from 'react';
88
import _ from 'underscore';
@@ -48,11 +48,6 @@ export const RelatedRecords = withCoreDataContextProvider(() => {
4848
<Map
4949
style={mapStyle}
5050
>
51-
<Controls
52-
position='topright'
53-
>
54-
<Zoom />
55-
</Controls>
5651
<div
5752
style={{
5853
width: '100%',

packages/storybook/src/core-data/PlaceLayersSelector.stories.js

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22

3-
import { Map, Zoom } from '@peripleo/maplibre';
4-
import { Controls, Peripleo } from '@peripleo/peripleo';
3+
import { Map } from '@peripleo/maplibre';
4+
import { Peripleo } from '@peripleo/peripleo';
55
import React from 'react';
66
import LocationMarkers from '../../../geospatial/src/components/LocationMarkers';
77
import mapStyle from '../data/MapStyles.json';
@@ -18,11 +18,6 @@ export const Default = () => (
1818
<Map
1919
style={mapStyle}
2020
>
21-
<Controls
22-
position='topright'
23-
>
24-
<Zoom />
25-
</Controls>
2621
<div
2722
style={{
2823
width: '100%',

packages/storybook/src/core-data/PlaceMarkers.stories.js

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// @flow
22

3-
import { Map, Zoom } from '@peripleo/maplibre';
4-
import { Controls, Peripleo } from '@peripleo/peripleo';
3+
import { Map } from '@peripleo/maplibre';
4+
import { Peripleo } from '@peripleo/peripleo';
55
import React from 'react';
66
import _ from 'underscore';
77
import mapStyle from '../data/MapStyles.json';
@@ -17,11 +17,6 @@ export const Default = () => (
1717
<Map
1818
style={mapStyle}
1919
>
20-
<Controls
21-
position='topright'
22-
>
23-
<Zoom />
24-
</Controls>
2520
<div
2621
style={{
2722
width: '100%',
@@ -43,11 +38,6 @@ export const MultiplePlaces = () => (
4338
<Map
4439
style={mapStyle}
4540
>
46-
<Controls
47-
position='topright'
48-
>
49-
<Zoom />
50-
</Controls>
5141
<div
5242
style={{
5343
width: '100%',
@@ -67,11 +57,6 @@ export const LargerBuffer = () => (
6757
<Map
6858
style={mapStyle}
6959
>
70-
<Controls
71-
position='topright'
72-
>
73-
<Zoom />
74-
</Controls>
7560
<div
7661
style={{
7762
width: '100%',

0 commit comments

Comments
 (0)