diff --git a/package-lock.json b/package-lock.json index 67480474..1efe13a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@deck.gl/react": "^9.2.2", "@geoarrow/deck.gl-layers": "^0.4.0-beta.1", "@geoarrow/geoarrow-js": "^0.3.2", + "@maplibre/maplibre-gl-geocoder": "^1.9.1", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", "apache-arrow": "^21.1.0", @@ -3067,6 +3068,25 @@ "node": ">=6.0.0" } }, + "node_modules/@maplibre/maplibre-gl-geocoder": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-geocoder/-/maplibre-gl-geocoder-1.9.1.tgz", + "integrity": "sha512-tLd93wQWwr9l/svNTYG25WT5Bo1KQMXwUl21Y0OOB8bV0TMhmbEzs1OUUBuWmo6Xn07lhgm5Y5GZvn7eMfmz1A==", + "license": "ISC", + "dependencies": { + "events": "^3.3.0", + "lodash.debounce": "^4.0.6", + "subtag": "^0.5.0", + "suggestions-list": "^0.0.2", + "xtend": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "maplibre-gl": ">=4.0.0" + } + }, "node_modules/@maplibre/maplibre-gl-style-spec": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-24.3.0.tgz", @@ -10387,6 +10407,15 @@ "url": "https://github.com/sponsors/mysticatea" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/expect-type": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", @@ -10738,6 +10767,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuzzy": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", + "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/generator-function": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", @@ -12378,6 +12415,7 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.10.0.tgz", "integrity": "sha512-eLhlX8Fnpaoo7+uGqggZmXmZld6WrbzOJUPB7G8JB8XpminlTnrQTtXilMHduR8fsNVxrzD8yRRqEoajONc8LQ==", "license": "BSD-3-Clause", + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -15623,6 +15661,12 @@ ], "license": "MIT" }, + "node_modules/subtag": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz", + "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==", + "license": "ISC" + }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -15645,6 +15689,16 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/suggestions-list": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/suggestions-list/-/suggestions-list-0.0.2.tgz", + "integrity": "sha512-Yw0fdq14c6RQWQIfE1/8WEi9Dp8rjyCD6FhYA/Tit2/ADbE9Y4ADG4ezlvivsa8Civ5nz++pyVVBMjOMlgIUJw==", + "license": "ISC", + "dependencies": { + "fuzzy": "^0.1.1", + "xtend": "^4.0.0" + } + }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", @@ -16952,6 +17006,15 @@ "url": "https://opencollective.com/xstate" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y-protocols": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", diff --git a/package.json b/package.json index a09bbf43..6854e9ba 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@deck.gl/react": "^9.2.2", "@geoarrow/deck.gl-layers": "^0.4.0-beta.1", "@geoarrow/geoarrow-js": "^0.3.2", + "@maplibre/maplibre-gl-geocoder": "^1.9.1", "@nextui-org/react": "^2.4.8", "@xstate/react": "^6.0.0", "apache-arrow": "^21.1.0", diff --git a/src/model/map-control.tsx b/src/model/map-control.tsx index 0b42009b..70188dbe 100644 --- a/src/model/map-control.tsx +++ b/src/model/map-control.tsx @@ -1,15 +1,29 @@ +import { ControlPosition } from "@deck.gl/mapbox/dist/types"; import { CompassWidget, FullscreenWidget, _ScaleWidget as ScaleWidget, ZoomWidget, } from "@deck.gl/react"; +import { + _GoogleGeocoder as GoogleGeocoder, + _MapboxGeocoder as MapboxGeocoder, + _OpenCageGeocoder as OpenCageGeocoder, + _CoordinatesGeocoder as CoordinatesGeocoder, +} from "@deck.gl/widgets"; +import type { Geocoder } from "@deck.gl/widgets"; import type { WidgetModel } from "@jupyter-widgets/base"; +import MaplibreGeocoder, { + CarmenGeojsonFeature, + MaplibreGeocoderApi, + MaplibreGeocoderOptions, +} from "@maplibre/maplibre-gl-geocoder"; import React from "react"; import { FullscreenControl, NavigationControl, ScaleControl, + useControl, } from "react-map-gl/maplibre"; import { isDefined } from "../util"; @@ -134,6 +148,93 @@ export class ScaleControlModel extends BaseMapControlModel { } } +function MaplibreGeocoderControl( + api: MaplibreGeocoderApi, + props: MaplibreGeocoderOptions, + opts?: ControlOptions, +) { + useControl(() => new MaplibreGeocoder(api, props), opts); + + return null; +} + +export class GeocoderControlModel extends BaseMapControlModel { + static controlType = "geocoder"; + + protected provider?: string; + protected apiKey?: string; + + constructor(model: WidgetModel, updateStateCallback: () => void) { + super(model, updateStateCallback); + } + + providerInstance(): Geocoder | null { + switch (this.provider) { + case "google": + return GoogleGeocoder; + case "mapbox": + return MapboxGeocoder; + case "opencage": + return OpenCageGeocoder; + case "coordinates": + return CoordinatesGeocoder; + default: + return null; + } + } + + maplibreApi(): MaplibreGeocoderApi | null { + const provider = this.providerInstance(); + if (provider instanceof MaplibreGeocoder) { + return { + forwardGeocode: async (config) => { + const queryString = config.query?.toString() || ""; + const result = await provider.geocode(queryString, this.apiKey || ""); + const feature: CarmenGeojsonFeature = { + id: "", + text: queryString, + place_name: "", + place_type: [], + type: "Feature", + geometry: { + type: "Point", + coordinates: [result?.longitude || 0, result?.longitude || 0], + }, + properties: {}, + }; + return { + type: "FeatureCollection", + features: [feature], + }; + }, + }; + } + return null; + } + + renderDeck() { + return null; + } + + renderMaplibre() { + const api = this.maplibreApi(); + if (api) { + return ( + + ); + } + return null; + } +} + +type ControlOptions = { + position?: ControlPosition; +}; + export async function initializeControl( model: WidgetModel, updateStateCallback: () => void,