Skip to content

Commit

Permalink
CDC #121 - Adding geocoding capabilities to MapDraw component
Browse files Browse the repository at this point in the history
  • Loading branch information
dleadbetter committed Apr 10, 2024
1 parent ea6ab01 commit 910d05f
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 17 deletions.
3 changes: 2 additions & 1 deletion packages/geospatial/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 All @@ -38,4 +39,4 @@
"react-dom": "^18.2.0",
"vite": "^5.1.4"
}
}
}
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;
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';
75 changes: 69 additions & 6 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,53 @@ 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(({ geometry: { type } }) => (
(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 Down Expand Up @@ -129,7 +182,7 @@ const MapDraw = (props: Props) => {
mapLib={maplibregl}
ref={mapRef}
style={style}
mapStyle={props.mapStyle}
mapStyle={mapStyleUrl}
>
<DrawControl
ref={drawRef}
Expand All @@ -145,6 +198,16 @@ const MapDraw = (props: Props) => {
onDelete={onChange}
position='bottom-left'
/>
{ props.geocoding && (
<GeocodingControl
apiKey={props.apiKey}
collapsed
marker={false}
position='top-left'
onSelection={onSelection}
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
47 changes: 38 additions & 9 deletions packages/storybook/src/geospatial/MapDraw.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,39 @@ export default {

export const Default = () => (
<MapDraw
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
apiKey={mapTilerKey}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
/>
);

export const GeoJSON = () => (
<MapDraw
apiKey={mapTilerKey}
data={bostonBoundaryData}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
/>
);

export const Point = () => (
<MapDraw
apiKey={mapTilerKey}
data={{
type: 'Point',
coordinates: [
-81.2653727,
31.4252249
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
/>
);

export const GeoJSONFillLayer = () => (
<MapDraw
apiKey={mapTilerKey}
data={{
type: 'GeometryCollection',
geometries: [
Expand All @@ -62,7 +66,7 @@ export const GeoJSONFillLayer = () => (
}
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<LayerMenu
Expand All @@ -79,6 +83,7 @@ export const GeoJSONFillLayer = () => (

export const GeoJSONCircleLayer = () => (
<MapDraw
apiKey={mapTilerKey}
data={{
type: 'GeometryCollection',
geometries: [
Expand All @@ -91,7 +96,7 @@ export const GeoJSONCircleLayer = () => (
}
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<LayerMenu
Expand All @@ -108,6 +113,7 @@ export const GeoJSONCircleLayer = () => (

export const GeoJSONLayerStyles = () => (
<MapDraw
apiKey={mapTilerKey}
data={{
type: 'GeometryCollection',
geometries: [
Expand All @@ -120,7 +126,7 @@ export const GeoJSONLayerStyles = () => (
}
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<LayerMenu
Expand All @@ -141,6 +147,7 @@ export const GeoJSONLayerStyles = () => (

export const RasterLayer = () => (
<MapDraw
apiKey={mapTilerKey}
buffer={120}
data={{
type: 'GeometryCollection',
Expand All @@ -154,7 +161,7 @@ export const RasterLayer = () => (
}
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<LayerMenu
Expand All @@ -176,14 +183,15 @@ export const RasterLayer = () => (

export const EmptyLayerMenu = () => (
<MapDraw
apiKey={mapTilerKey}
data={{
type: 'Point',
coordinates: [
-81.2653727,
31.4252249
]
}}
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<LayerMenu names={[]} />
Expand All @@ -192,7 +200,8 @@ export const EmptyLayerMenu = () => (

export const CustomControl = () => (
<MapDraw
mapStyle={`https://api.maptiler.com/maps/basic-v2/style.json?key=${mapTilerKey}`}
apiKey={mapTilerKey}
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
>
<MapControl
Expand All @@ -208,3 +217,23 @@ export const CustomControl = () => (
</MapControl>
</MapDraw>
);

export const GeocodingPoints = () => (
<MapDraw
apiKey={mapTilerKey}
geocoding='point'
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
onGeocodingSelection={action('onGeocodingSelection')}
/>
);

export const GeocodingPolygons = () => (
<MapDraw
apiKey={mapTilerKey}
geocoding='polygon'
mapStyle='https://api.maptiler.com/maps/basic-v2/style.json'
onChange={action('onChange')}
onGeocodingSelection={action('onGeocodingSelection')}
/>
);
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2089,6 +2089,13 @@
rw "^1.3.3"
sort-object "^3.0.3"

"@maptiler/geocoding-control@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@maptiler/geocoding-control/-/geocoding-control-1.2.2.tgz#319b1b2abaa2b4de6cc91e1a2990e58b18557960"
integrity sha512-w0JH0MOWN/z4l5t89LPinn9P9CGUg+L9R0WslXSVFP8gX9SYUMaqmGB28txXlSJsckA5/oyYfOe5UOBgXK7sbw==
dependencies:
geo-coordinates-parser "^1.6.4"

"@mdx-js/react@^2.1.5":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@mdx-js/react/-/react-2.3.0.tgz#4208bd6d70f0d0831def28ef28c26149b03180b3"
Expand Down Expand Up @@ -9056,6 +9063,11 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==

geo-coordinates-parser@^1.6.4:
version "1.6.6"
resolved "https://registry.yarnpkg.com/geo-coordinates-parser/-/geo-coordinates-parser-1.6.6.tgz#856ea86639b5fb4ea20208418b7cfcf465d55fc2"
integrity sha512-+zmVBzbTrC/LyFUMcYrvUqi+XUYkJ6bWqPHywfCsMYLa9BEGHEzLsBgltwXS9Ul5oJcFbrdt2y/CjjxNtTTQ+w==

[email protected]:
version "0.1.6"
resolved "https://registry.yarnpkg.com/geojson-equality/-/geojson-equality-0.1.6.tgz#a171374ef043e5d4797995840bae4648e0752d72"
Expand Down

0 comments on commit 910d05f

Please sign in to comment.