diff --git a/components/src/index.js b/components/src/index.js index 06dd7691..a3e4ba1d 100644 --- a/components/src/index.js +++ b/components/src/index.js @@ -29,6 +29,7 @@ export { default as Map } from './maplibre/Map/Map.svelte'; export { SWRDataLabLight, SWRDataLabDark } from './maplibre/MapStyle'; export { default as MapControl } from './maplibre/MapControl/MapControl.svelte'; export { default as AttributionControl } from './maplibre/AttributionControl/AttributionControl.svelte'; +export { default as Geocoder } from './maplibre/Geocoder/Geocoder.svelte'; export { default as GeocoderControl } from './maplibre/GeocoderControl/GeocoderControl.svelte'; export { default as NavigationControl } from './maplibre/NavigationControl/NavigationControl.svelte'; export { default as ScaleControl } from './maplibre/ScaleControl/ScaleControl.svelte'; diff --git a/components/src/maplibre/Geocoder/Geocoder.mdx b/components/src/maplibre/Geocoder/Geocoder.mdx new file mode 100644 index 00000000..29f5e713 --- /dev/null +++ b/components/src/maplibre/Geocoder/Geocoder.mdx @@ -0,0 +1,10 @@ +import { Story, Meta, Primary, Controls, Stories } from '@storybook/addon-docs/blocks'; + +import * as GeocoderStories from './Geocoder.stories.svelte'; + + + +# Geocoder + + + diff --git a/components/src/maplibre/Geocoder/Geocoder.stories.svelte b/components/src/maplibre/Geocoder/Geocoder.stories.svelte new file mode 100644 index 00000000..54d9b6d9 --- /dev/null +++ b/components/src/maplibre/Geocoder/Geocoder.stories.svelte @@ -0,0 +1,51 @@ + + + { + }} +> + +
+ +
+ +
+
+
+
+ + diff --git a/components/src/maplibre/Geocoder/Geocoder.svelte b/components/src/maplibre/Geocoder/Geocoder.svelte new file mode 100644 index 00000000..7e3aac5a --- /dev/null +++ b/components/src/maplibre/Geocoder/Geocoder.svelte @@ -0,0 +1,60 @@ + + +
+ + diff --git a/components/src/maplibre/Geocoder/GeocoderAPIs.ts b/components/src/maplibre/Geocoder/GeocoderAPIs.ts new file mode 100644 index 00000000..5cd82931 --- /dev/null +++ b/components/src/maplibre/Geocoder/GeocoderAPIs.ts @@ -0,0 +1,62 @@ +import { + type CarmenGeojsonFeature, + type MaplibreGeocoderApi, + type MaplibreGeocoderApiConfig, + type MaplibreGeocoderFeatureResults +} from '@maplibre/maplibre-gl-geocoder'; + +export class MaptilerGeocoderAPI implements MaplibreGeocoderApi { + key: string; + + constructor(key: string) { + this.key = key; + } + + async forwardGeocode(config: MaplibreGeocoderApiConfig): Promise { + const result: MaplibreGeocoderFeatureResults = { + type: 'FeatureCollection', + features: [] + }; + try { + const params = new URLSearchParams( + Object.fromEntries( + Object.entries({ + country: config.countries || '', + language: config.language || '', + types: config.types || '', + key: this.key || '' + }).filter(([_, value]) => { + return value !== ''; + }) + ) + ); + + const response = await fetch( + `https://api.maptiler.com/geocoding/${config.query}.json?${params.toString()}` + ); + const geojson = await response.json(); + for (const feature of geojson.features) { + const res: CarmenGeojsonFeature = { + type: 'Feature', + id: feature.id, + place_type: ['place'], + place_name: feature.place_name, + language: feature.properties.display_name, + text: feature.text, + bbox: feature.bbox, + geometry: { + type: 'Point', + coordinates: feature.center + }, + properties: { + text: feature.text + } + }; + result.features.push(res); + } + } catch (e) { + console.error(`Failed to forwardGeocode with error: ${e}`); + } + return result; + } +} diff --git a/components/src/maplibre/Geocoder/GeocoderProps.ts b/components/src/maplibre/Geocoder/GeocoderProps.ts new file mode 100644 index 00000000..c19633f4 --- /dev/null +++ b/components/src/maplibre/Geocoder/GeocoderProps.ts @@ -0,0 +1,37 @@ +import { Map } from "maplibre-gl"; +import { + type GeocodingCountry, + type GeocodingLanguage, + type GeocodingPlaceType, + type GeocodingService + } from '../types'; + + +export default interface GeocoderProps { + map?: Map | null + service: GeocodingService; + /** + * API key for selected geocoding `service` + */ + key: string; + /** + * Limit the number of returned suggestions + */ + limit?: number; + /** + * Limit search to one or more countries + */ + countries?: GeocodingCountry | GeocodingCountry[]; + /** + * Limit search to one or more result type. + */ + types?: GeocodingPlaceType | GeocodingPlaceType[]; + /** + * Limit search to one or more languages. The UI is localised to the first language specified if [available](https://github.com/maplibre/maplibre-gl-geocoder/blob/main/lib/localization.ts). + */ + languages?: GeocodingLanguage | GeocodingLanguage[]; + /** + * Overwrite the default input placeholder + */ + placeholder?: string; + } diff --git a/components/src/maplibre/Geocoder/geocoder.scss b/components/src/maplibre/Geocoder/geocoder.scss new file mode 100644 index 00000000..91b7cd69 --- /dev/null +++ b/components/src/maplibre/Geocoder/geocoder.scss @@ -0,0 +1,162 @@ + :global { + .maplibregl-ctrl-geocoder { + background-color: var(--color-pageFill); + position: relative; + width: 100%; + z-index: 1; + font-family: var(--swr-sans); + font-size: var(--fs-base); + border: 1px solid var(--color-textSecondary); + + button { + cursor: pointer; + } + * { + box-sizing: border-box; + } + } + + .maplibre-gl-geocoder--error { + font-size: var(--fs-small-2); + color: var(--color-textSecondary); + padding: 0.4em 0.65em; + } + + .maplibregl-ctrl-geocoder--input { + height: 100%; + font-family: inherit; + font-size: inherit; + background-color: transparent; + color: var(--color-textPrimary); + border: 0; + height: 2em; + padding: 0 1.85em; + width: 100%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + &::-webkit-search-cancel-button, + &::-webkit-search-decoration { + appearance: none; + } + } + + .maplibregl-ctrl-geocoder--input::placeholder { + opacity: 1; + color: var(--color-textSecondary); + } + .maplibregl-ctrl-geocoder--input:focus { + color: var(--color-textPrimary); + outline: 0; + } + + .maplibregl-ctrl-geocoder .maplibregl-ctrl-geocoder--pin-right > * { + display: none; + position: absolute; + z-index: 2; + right: 0.5em; + top: 50%; + transform: translateY(-50%); + border: 0; + } + + /* Suggestions */ + .maplibregl-ctrl-geocoder .suggestions { + background: var(--color-pageFill); + border: 1px solid var(--color-textSecondary); + left: -1px; + list-style: none; + position: absolute; + width: calc(100% + 2px); + top: 100%; + z-index: 1000; + overflow: hidden; + } + + .maplibregl-ctrl-bottom-left .suggestions, + .maplibregl-ctrl-bottom-right .suggestions { + top: auto; + bottom: 100%; + } + + .maplibregl-ctrl-geocoder .suggestions > li > a { + cursor: default; + display: block; + font-size: var(--fs-small-1); + padding: 0.7em 0.75em; + padding-bottom: 0.65em; + border-bottom: 1px solid var(--color-surfaceBorder); + } + .maplibregl-ctrl-geocoder .suggestions > li:last-child > a { + border-bottom: 0; + } + + .maplibregl-ctrl-geocoder .suggestions > .active > a, + .maplibregl-ctrl-geocoder .suggestions > li > a:hover { + background: var(--color-surfaceHover); + text-decoration: none; + cursor: pointer; + } + .maplibregl-ctrl-geocoder .suggestions > .active .maplibregl-ctrl-geocoder--result-title, + .maplibregl-ctrl-geocoder .suggestions > li > a:hover .maplibregl-ctrl-geocoder--result-title { + text-decoration: underline; + } + + .maplibregl-ctrl-geocoder--suggestion { + display: flex; + flex-direction: row; + align-items: center; + } + + .maplibregl-ctrl-geocoder--suggestion-info { + display: flex; + flex-direction: column; + } + + .maplibregl-ctrl-geocoder--suggestion-title, + .maplibregl-ctrl-geocoder--suggestion-address, + .maplibregl-ctrl-geocoder--result-address { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + + .maplibregl-ctrl-geocoder--result-icon { + display: none; + } + .maplibregl-ctrl-geocoder--result-title { + color: var(--color-textPrimary); + font-weight: 600; + letter-spacing: 0; + } + .maplibregl-ctrl-geocoder--result-address { + font-size: var(--fs-small-2); + color: var(--color-textSecondary); + line-height: 1.3; + } + + .maplibregl-ctrl-geocoder--icon { + display: inline-block; + position: absolute; + top: 50%; + transform: translateY(-50%); + path { + fill: var(--color-textPrimary); + } + } + + .maplibregl-ctrl-geocoder--icon-close { + right: .25em; + height: 1.15em; + opacity: 0.65; + &:hover, &:focus-visible { + opacity: 1; + } + } + .maplibregl-ctrl-geocoder--icon-search { + left: 0.4em; + height: 1.25em; + top: 51%; // optical correction + width: auto; + } + } diff --git a/components/src/maplibre/Geocoder/index.js b/components/src/maplibre/Geocoder/index.js new file mode 100644 index 00000000..ef694f5f --- /dev/null +++ b/components/src/maplibre/Geocoder/index.js @@ -0,0 +1,2 @@ +import Geocoder from './Geocoder.svelte'; +export default Geocoder; diff --git a/components/src/maplibre/GeocoderControl/GeocoderControl.mdx b/components/src/maplibre/GeocoderControl/GeocoderControl.mdx index b4d13201..4527fefb 100644 --- a/components/src/maplibre/GeocoderControl/GeocoderControl.mdx +++ b/components/src/maplibre/GeocoderControl/GeocoderControl.mdx @@ -14,11 +14,13 @@ import * as GeocoderControlStories from './GeocoderControl.stories.svelte'; Maptiler doesn't allow filtering by German admin boundaries, but its `types` seem to map as follows: - + + - + +
type boundary
countyKreis
municipalityGemeinde
regionBundesland
diff --git a/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte b/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte index 31c2f53f..36862a7c 100644 --- a/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte +++ b/components/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte @@ -35,7 +35,6 @@
{ position?: ControlPosition; } @@ -73,162 +43,5 @@ diff --git a/components/src/maplibre/Map/Map.stories.svelte b/components/src/maplibre/Map/Map.stories.svelte index a4b1a42b..f92648b4 100644 --- a/components/src/maplibre/Map/Map.stories.svelte +++ b/components/src/maplibre/Map/Map.stories.svelte @@ -26,7 +26,8 @@ component: Map }); - let mapContext: MapContext; + let mapContext: MapContext | undefined = $state(); + const onMoveStart = fn(); let mapOptions = $state({ @@ -115,13 +116,13 @@ { await userEvent.click(toggleZoomButton); - expect(mapContext.map?.scrollZoom.isEnabled()).toBe(false); + expect(mapContext?.map?.scrollZoom.isEnabled()).toBe(false); }); await step('toggle zoom on', async () => { await userEvent.click(toggleZoomButton); - expect(mapContext.map?.scrollZoom.isEnabled()).toBe(true); + expect(mapContext?.map?.scrollZoom.isEnabled()).toBe(true); }); await step('toggle pan off', async () => { await userEvent.click(togglePanButton); - expect(mapContext.map?.dragPan.isEnabled()).toBe(false); + expect(mapContext?.map?.dragPan.isEnabled()).toBe(false); }); await step('toggle pan on', async () => { await userEvent.click(togglePanButton); - expect(mapContext.map?.dragPan.isEnabled()).toBe(true); + expect(mapContext?.map?.dragPan.isEnabled()).toBe(true); }); }} > diff --git a/components/src/maplibre/Map/Map.svelte b/components/src/maplibre/Map/Map.svelte index 530b1397..6693e7b0 100644 --- a/components/src/maplibre/Map/Map.svelte +++ b/components/src/maplibre/Map/Map.svelte @@ -1,6 +1,7 @@ diff --git a/components/src/maplibre/WithLinkLocation/WithLinkLocation.svelte b/components/src/maplibre/WithLinkLocation/WithLinkLocation.svelte index 30df757f..f6afc615 100644 --- a/components/src/maplibre/WithLinkLocation/WithLinkLocation.svelte +++ b/components/src/maplibre/WithLinkLocation/WithLinkLocation.svelte @@ -43,14 +43,10 @@ children }: WithLinkLocationProps = $props(); - const countriesArr = Array.isArray(countries) ? countries : [countries]; - const languagesArr = Array.isArray(languages) ? languages : [languages]; + const countriesArr = $derived(Array.isArray(countries) ? countries : [countries]); + const languagesArr = $derived(Array.isArray(languages) ? languages : [languages]); - let geocoder: MaplibreGeocoderApi; - - if (service === 'maptiler') { - geocoder = new MaptilerGeocoderAPI(key); - } + const geocoder: MaplibreGeocoderApi = $derived(new MaptilerGeocoderAPI(key)); let location: any = $state({}); diff --git a/package-lock.json b/package-lock.json index 0acb1002..a1fd68da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,7 +42,7 @@ "@sveltejs/vite-plugin-svelte": "7.1.2", "@types/geojson": "7946.0.16", "@versatiles/style": "5.7.0", - "@vitest/browser": "^4.1.9", + "@vitest/browser": "4.1.9", "@vitest/browser-playwright": "4.1.7", "@vitest/coverage-v8": "4.1.7", "@vueless/storybook-dark-mode": "9.0.8",