diff --git a/packages/controlled-vocabulary/package.json b/packages/controlled-vocabulary/package.json index f3617ae6..071e7d42 100644 --- a/packages/controlled-vocabulary/package.json +++ b/packages/controlled-vocabulary/package.json @@ -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", @@ -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" }, diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 622abf69..ff04a6a9 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -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", @@ -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", diff --git a/packages/core-data/src/components/SearchResultsLayer.js b/packages/core-data/src/components/SearchResultsLayer.js index 733fcfe6..4b7c33bb 100644 --- a/packages/core-data/src/components/SearchResultsLayer.js +++ b/packages/core-data/src/components/SearchResultsLayer.js @@ -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); diff --git a/packages/geospatial/package.json b/packages/geospatial/package.json index 3aa827bb..68508c4f 100644 --- a/packages/geospatial/package.json +++ b/packages/geospatial/package.json @@ -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", @@ -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", 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/LocationMarkers.js b/packages/geospatial/src/components/LocationMarkers.js index 04c1ee71..a4bb3acb 100644 --- a/packages/geospatial/src/components/LocationMarkers.js +++ b/packages/geospatial/src/components/LocationMarkers.js @@ -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]); 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..8fdba145 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,59 @@ 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((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}} */ @@ -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 @@ -129,7 +184,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/geospatial/src/utils/Map.js b/packages/geospatial/src/utils/Map.js index a1cccb0f..fda49489 100644 --- a/packages/geospatial/src/utils/Map.js +++ b/packages/geospatial/src/utils/Map.js @@ -1,6 +1,7 @@ // @flow import { bbox, bboxPolygon, buffer } from '@turf/turf'; +import _ from 'underscore'; const MIN_LATITUDE = -90; const MAX_LATITUDE = 90; @@ -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); @@ -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) => _.every(boundingBox, _.isFinite); + /** * Returns true if the passed coordinates are valid. * @@ -59,5 +73,6 @@ const validateCoordinates = (coordinates) => { export default { getBoundingBox, + validateBoundingBox, validateCoordinates }; diff --git a/packages/semantic-ui/package.json b/packages/semantic-ui/package.json index 8655b142..61c35a79 100644 --- a/packages/semantic-ui/package.json +++ b/packages/semantic-ui/package.json @@ -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", @@ -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", diff --git a/packages/shared/package.json b/packages/shared/package.json index 914cadc1..a02f62de 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -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", 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/packages/user-defined-fields/package.json b/packages/user-defined-fields/package.json index 8d4467c4..7ece16ff 100644 --- a/packages/user-defined-fields/package.json +++ b/packages/user-defined-fields/package.json @@ -1,6 +1,6 @@ { "name": "@performant-software/user-defined-fields", - "version": "2.1.1", + "version": "2.1.2", "description": "A package of components used for allowing end users to define fields on models. Use with the \"user_defined_fields\" gem.", "license": "MIT", "main": "./dist/index.cjs.js", @@ -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" }, diff --git a/packages/visualize/package.json b/packages/visualize/package.json index 4bb589f5..9fd57587 100644 --- a/packages/visualize/package.json +++ b/packages/visualize/package.json @@ -1,6 +1,6 @@ { "name": "@performant-software/visualize", - "version": "2.1.1", + "version": "2.1.2", "description": "A package of components used for data visualization", "license": "MIT", "main": "./dist/index.cjs.js", diff --git a/react-components.json b/react-components.json index 17410dd2..e1fff23d 100644 --- a/react-components.json +++ b/react-components.json @@ -8,5 +8,5 @@ "packages/user-defined-fields", "packages/visualize" ], - "version": "2.1.1" + "version": "2.1.2" } 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"