Skip to content

Commit

Permalink
Merge pull request #273 from performant-software/feature/cdc121_geoco…
Browse files Browse the repository at this point in the history
…ding

CDC #121 - Geocoding
  • Loading branch information
dleadbetter authored Apr 12, 2024
2 parents ea6ab01 + b8fe435 commit 1225737
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 44 deletions.
6 changes: 3 additions & 3 deletions packages/controlled-vocabulary/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/controlled-vocabulary",
"version": "2.1.1",
"version": "2.1.2",
"description": "A package of components to allow user to configure dropdown elements. Use with the \"controlled_vocabulary\" gem.",
"license": "MIT",
"main": "./dist/index.cjs.js",
Expand All @@ -23,8 +23,8 @@
"underscore": "^1.13.2"
},
"peerDependencies": {
"@performant-software/semantic-components": "^2.1.1",
"@performant-software/shared-components": "^2.1.1",
"@performant-software/semantic-components": "^2.1.2",
"@performant-software/shared-components": "^2.1.2",
"react": ">= 16.13.1 < 19.0.0",
"react-dom": ">= 16.13.1 < 19.0.0"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/core-data/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/core-data",
"version": "2.1.1",
"version": "2.1.2",
"description": "A package of components used with the Core Data platform.",
"license": "MIT",
"main": "./dist/index.cjs.js",
Expand Down Expand Up @@ -38,7 +38,7 @@
"underscore": "^1.13.2"
},
"peerDependencies": {
"@performant-software/geospatial": "^2.1.1",
"@performant-software/geospatial": "^2.1.2",
"@peripleo/maplibre": "^0.5.2",
"@peripleo/peripleo": "^0.5.2",
"react": ">= 16.13.1 < 19.0.0",
Expand Down
7 changes: 5 additions & 2 deletions packages/core-data/src/components/SearchResultsLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ const SearchResultsLayer = (props: Props) => {
useEffect(() => {
if (props.fitBoundingBox && data && mapLoaded && searchCompleted) {
// Set the bounding box on the map
const boundingBox = MapUtils.getBoundingBox(data, props.buffer);
map.fitBounds(boundingBox, props.boundingBoxOptions, props.boundingBoxData);
const bbox = MapUtils.getBoundingBox(data, props.buffer);

if (bbox) {
map.fitBounds(bbox, props.boundingBoxOptions, props.boundingBoxData);
}

// Reset search completed
setSearchCompleted(false);
Expand Down
3 changes: 2 additions & 1 deletion packages/geospatial/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/geospatial",
"version": "2.1.1",
"version": "2.1.2",
"description": "A package of components for all things map-related.",
"license": "MIT",
"main": "./dist/index.cjs.js",
Expand All @@ -19,6 +19,7 @@
},
"dependencies": {
"@mapbox/mapbox-gl-draw": "^1.4.3",
"@maptiler/geocoding-control": "^1.2.2",
"@turf/turf": "^6.5.0",
"mapbox-gl": "npm:[email protected]",
"maplibre-gl": "^3.6.2",
Expand Down
33 changes: 33 additions & 0 deletions packages/geospatial/src/components/GeocodingControl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// @flow

import { GeocodingControl as MapTilerGeocoding } from '@maptiler/geocoding-control/maplibregl';
import maplibregl from 'maplibre-gl';
import { forwardRef, useImperativeHandle } from 'react';
import { useControl, type ControlPosition } from 'react-map-gl';

type Props = {
apiKey: string,
onSelection: () => void,
position?: ControlPosition
};

const GeocodingControl = forwardRef(({ position, ...props }: Props, ref) => {
/**
* Creates the drawer ref using MapboxDraw.
*/
const geocodingRef = useControl(() => {
const control = new MapTilerGeocoding({ ...props, maplibregl });
control.addEventListener('pick', props.onSelection);

return control;
}, { position });

/**
* Exposes the ref for the MapboxDraw object.
*/
useImperativeHandle(ref, () => geocodingRef, [geocodingRef]);

return null;
});

export default GeocodingControl;
7 changes: 5 additions & 2 deletions packages/geospatial/src/components/LocationMarkers.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,11 @@ const LocationMarkers = (props: Props) => {
*/
useEffect(() => {
if (map && data && props.fitBoundingBox) {
const boundingBox = MapUtils.getBoundingBox(props.data, props.buffer);
map.fitBounds(boundingBox, props.boundingBoxOptions, props.boundingBoxData);
const bbox = MapUtils.getBoundingBox(data, props.buffer);

if (bbox) {
map.fitBounds(bbox, props.boundingBoxOptions, props.boundingBoxData);
}
}
}, [map, props.buffer, props.data, props.boundingBoxData, props.boundingBoxOptions, props.fitBoundingBox]);

Expand Down
3 changes: 2 additions & 1 deletion packages/geospatial/src/components/MapDraw.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
@import 'maplibre-gl/dist/maplibre-gl.css';
@import '@maptiler/geocoding-control/style.css';
@import 'maplibre-gl/dist/maplibre-gl.css';
91 changes: 78 additions & 13 deletions packages/geospatial/src/components/MapDraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
import Map, { type MapboxMap } from 'react-map-gl';
import _ from 'underscore';
import DrawControl from './DrawControl';
import GeocodingControl from './GeocodingControl';
import MapUtils from '../utils/Map';
import './MapDraw.css';

Expand All @@ -23,6 +24,11 @@ MapboxDraw.constants.classes.CONTROL_PREFIX = 'maplibregl-ctrl-';
MapboxDraw.constants.classes.CONTROL_GROUP = 'maplibregl-ctrl-group';

type Props = {
/**
* MapTiler API key.
*/
apiKey?: string,

/**
* The number of miles to buffer the GeoJSON data.
*/
Expand All @@ -38,6 +44,11 @@ type Props = {
*/
data: GeometryCollection | FeatureCollection,

/**
* Controls the type of GeoJSON data returned from the MapTiler Geocoding API.
*/
geocoding?: undefined | 'point' | 'polygon',

/**
* URL of the map style to render. This URL should contain any necessary API keys.
*/
Expand All @@ -50,6 +61,11 @@ type Props = {
*/
onChange: (features: Array<any>) => void,

/**
* Callback fired when an item is selected from the geocoding dropdown.
*/
onGeocodingSelection?: (data: any) => void,

/**
* Map style object.
*/
Expand All @@ -66,7 +82,8 @@ const DEFAULT_ZOOM_DELAY = 1000;

const GeometryTypes = {
geometryCollection: 'GeometryCollection',
point: 'Point'
point: 'Point',
polygon: 'Polygon'
};

/**
Expand All @@ -79,17 +96,59 @@ const MapDraw = (props: Props) => {
const drawRef = useRef<MapboxDraw>();
const mapRef = useRef<MapboxMap>();

/**
* Returns true if the passed geometry type is valid. MapTiler fires the onSelection callback twice: Once after
* selecting the record from the list (with a point geometry), and once after making a call to the server for the
* full record (polygon geometry). We should on fire the onGeocodingSelection callback and add the geometry to the
* map once.
*
* @type {function({geometry: {type: *}}): *}
*/
const isValid = useCallback((detail) => {
if (!detail) {
return false;
}

const { geometry: { type } } = detail;

return (props.geocoding === 'point' && type === GeometryTypes.point)
|| (props.geocoding === 'polygon' && type === GeometryTypes.polygon);
}, [props.geocoding]);

/**
* Calls the onChange prop with all of the geometries in the current drawer.
*
* @type {(function(): void)|*}
*/
const onChange = useCallback(() => {
props.onChange(drawRef.current.getAll());
}, [props.onChange]);
const onChange = useCallback(() => props.onChange(drawRef.current.getAll()), [props.onChange]);

/**
* Adds the selected geometry to the map.
*
* @type {(function({detail: *}): void)|*}
*/
const onSelection = useCallback(({ detail }) => {
if (isValid(detail)) {
// Add the geometry to the map
drawRef.current.add(detail.geometry);

// Trigger the onChange prop
onChange();

// Call the onGeocoding selection callback
props.onGeocodingSelection(detail);
}
}, [isValid, onChange, props.onGeocodingSelection]);

/**
* Sets the map style URL.
*
* @type {string}
*/
const mapStyleUrl = useMemo(() => `${props.mapStyle}?key=${props.apiKey}`, [props.apiKey, props.mapStyle]);

/**
* Sets the map style.
* Sets the element map style.
*
* @type {{width: string, height: number}}
*/
Expand All @@ -100,15 +159,11 @@ const MapDraw = (props: Props) => {
*/
useEffect(() => {
if (loaded && props.data) {
// Get the bounding box for the passed data
const boundingBox = MapUtils.getBoundingBox(props.data, props.buffer);

// Sets the bounding box for the current geometry
if (_.every(boundingBox, _.isFinite)) {
const [minLng, minLat, maxLng, maxLat] = boundingBox;
const bounds = [[minLng, minLat], [maxLng, maxLat]];
const bbox = MapUtils.getBoundingBox(props.data, props.buffer);

mapRef.current.fitBounds(bounds, { duration: props.zoomDuration });
if (bbox) {
mapRef.current.fitBounds(bbox, { duration: props.zoomDuration });
}

// Handle special cases for geometry collection (not supported by mabox-gl-draw) and point
Expand All @@ -129,7 +184,7 @@ const MapDraw = (props: Props) => {
mapLib={maplibregl}
ref={mapRef}
style={style}
mapStyle={props.mapStyle}
mapStyle={mapStyleUrl}
>
<DrawControl
ref={drawRef}
Expand All @@ -145,6 +200,16 @@ const MapDraw = (props: Props) => {
onDelete={onChange}
position='bottom-left'
/>
{ props.geocoding && (
<GeocodingControl
apiKey={props.apiKey}
marker={false}
position='top-left'
onSelection={onSelection}
showFullGeometry={props.geocoding === 'polygon'}
showResultMarkers={false}
/>
)}
{ props.children }
</Map>
);
Expand Down
1 change: 1 addition & 0 deletions packages/geospatial/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// Components
export { default as DrawControl } from './components/DrawControl';
export { default as GeoJsonLayer } from './components/GeoJsonLayer';
export { default as GeocodingControl } from './components/GeocodingControl';
export { default as LayerMenu } from './components/LayerMenu';
export { default as LocationMarkers } from './components/LocationMarkers';
export { default as MapControl } from './components/MapControl';
Expand Down
21 changes: 18 additions & 3 deletions packages/geospatial/src/utils/Map.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import { bbox, bboxPolygon, buffer } from '@turf/turf';
import _ from 'underscore';

const MIN_LATITUDE = -90;
const MAX_LATITUDE = 90;
Expand All @@ -10,14 +11,18 @@ const MAX_LONGITUDE = 180;
/**
* Returns a bounding box for the passed geometry (with optional buffer).
*
* @param geometry
* @param data
* @param bufferDistance
*
* @returns {BBox}
*/
const getBoundingBox = (geometry, bufferDistance = null) => {
const getBoundingBox = (data, bufferDistance = null) => {
// Convert the GeoJSON into a bounding box
const box = bbox(geometry);
const box = bbox(data);

if (!validateBoundingBox(box)) {
return null;
}

// Convert the bounding box to a polygon
const polygon = bboxPolygon(box);
Expand All @@ -35,6 +40,15 @@ const getBoundingBox = (geometry, bufferDistance = null) => {
return bbox(polygonBuffer);
};

/**
* Validates that the passed bounding box contains finite coordinates.
*
* @param boundingBox
*
* @returns {*}
*/
const validateBoundingBox = (boundingBox: Array<number>) => _.every(boundingBox, _.isFinite);

/**
* Returns true if the passed coordinates are valid.
*
Expand All @@ -59,5 +73,6 @@ const validateCoordinates = (coordinates) => {

export default {
getBoundingBox,
validateBoundingBox,
validateCoordinates
};
4 changes: 2 additions & 2 deletions packages/semantic-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/semantic-components",
"version": "2.1.1",
"version": "2.1.2",
"description": "A package of shared components based on the Semantic UI Framework.",
"license": "MIT",
"main": "./dist/index.cjs.js",
Expand Down Expand Up @@ -35,7 +35,7 @@
"zotero-translation-client": "^5.0.1"
},
"peerDependencies": {
"@performant-software/shared-components": "^2.1.1",
"@performant-software/shared-components": "^2.1.2",
"@samvera/clover-iiif": "^2.3.2",
"react": ">= 16.13.1 < 19.0.0",
"react-dnd": "^11.1.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@performant-software/shared-components",
"version": "2.1.1",
"version": "2.1.2",
"description": "A package of shared, framework agnostic, components.",
"license": "MIT",
"main": "./dist/index.cjs.js",
Expand Down
Loading

0 comments on commit 1225737

Please sign in to comment.