diff --git a/packages/geospatial/package.json b/packages/geospatial/package.json index 3aa827bb..571e9cd3 100644 --- a/packages/geospatial/package.json +++ b/packages/geospatial/package.json @@ -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:empty-npm-package@1.0.0", "maplibre-gl": "^3.6.2", @@ -38,4 +39,4 @@ "react-dom": "^18.2.0", "vite": "^5.1.4" } -} \ No newline at end of file +} diff --git a/packages/geospatial/src/components/GeocodingControl.js b/packages/geospatial/src/components/GeocodingControl.js new file mode 100644 index 00000000..a32943df --- /dev/null +++ b/packages/geospatial/src/components/GeocodingControl.js @@ -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; diff --git a/packages/geospatial/src/components/MapDraw.css b/packages/geospatial/src/components/MapDraw.css index eae7435e..e629ea80 100644 --- a/packages/geospatial/src/components/MapDraw.css +++ b/packages/geospatial/src/components/MapDraw.css @@ -1,2 +1,3 @@ @import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; -@import 'maplibre-gl/dist/maplibre-gl.css'; \ No newline at end of file +@import '@maptiler/geocoding-control/style.css'; +@import 'maplibre-gl/dist/maplibre-gl.css'; diff --git a/packages/geospatial/src/components/MapDraw.js b/packages/geospatial/src/components/MapDraw.js index 6b82ab34..16618211 100644 --- a/packages/geospatial/src/components/MapDraw.js +++ b/packages/geospatial/src/components/MapDraw.js @@ -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'; @@ -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. */ @@ -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. */ @@ -50,6 +61,11 @@ type Props = { */ onChange: (features: Array) => void, + /** + * Callback fired when an item is selected from the geocoding dropdown. + */ + onGeocodingSelection?: (data: any) => void, + /** * Map style object. */ @@ -66,7 +82,8 @@ const DEFAULT_ZOOM_DELAY = 1000; const GeometryTypes = { geometryCollection: 'GeometryCollection', - point: 'Point' + point: 'Point', + polygon: 'Polygon' }; /** @@ -79,17 +96,53 @@ const MapDraw = (props: Props) => { const drawRef = useRef(); const mapRef = useRef(); + /** + * 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}} */ @@ -129,7 +182,7 @@ const MapDraw = (props: Props) => { mapLib={maplibregl} ref={mapRef} style={style} - mapStyle={props.mapStyle} + mapStyle={mapStyleUrl} > { onDelete={onChange} position='bottom-left' /> + { props.geocoding && ( + + )} { props.children } ); diff --git a/packages/geospatial/src/index.js b/packages/geospatial/src/index.js index bad0508d..99263ae7 100644 --- a/packages/geospatial/src/index.js +++ b/packages/geospatial/src/index.js @@ -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'; diff --git a/packages/storybook/src/geospatial/MapDraw.stories.js b/packages/storybook/src/geospatial/MapDraw.stories.js index abdb5999..241c74ea 100644 --- a/packages/storybook/src/geospatial/MapDraw.stories.js +++ b/packages/storybook/src/geospatial/MapDraw.stories.js @@ -21,21 +21,24 @@ export default { export const Default = () => ( ); export const GeoJSON = () => ( ); export const Point = () => ( ( 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 = () => ( ( } ] }} - 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 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')} > ( 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')} > ( 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')} > ( export const EmptyLayerMenu = () => ( ( 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')} > @@ -192,7 +200,8 @@ export const EmptyLayerMenu = () => ( export const CustomControl = () => ( ( ); + +export const GeocodingPoints = () => ( + +); + +export const GeocodingPolygons = () => ( + +); diff --git a/yarn.lock b/yarn.lock index 3ca7fe03..027ac099 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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== + geojson-equality@0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/geojson-equality/-/geojson-equality-0.1.6.tgz#a171374ef043e5d4797995840bae4648e0752d72"