Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
10 changes: 10 additions & 0 deletions components/src/maplibre/Geocoder/Geocoder.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Story, Meta, Primary, Controls, Stories } from '@storybook/addon-docs/blocks';

import * as GeocoderStories from './Geocoder.stories.svelte';

<Meta of={GeocoderStories} />

# Geocoder

<Primary/>
<Controls/>
51 changes: 51 additions & 0 deletions components/src/maplibre/Geocoder/Geocoder.stories.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<script module lang="ts">
import { defineMeta } from '@storybook/addon-svelte-csf';
import { within, expect } from 'storybook/test';

import Geocoder from './Geocoder.svelte';
import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
import { SWRDataLabLight } from '../MapStyle';
import Map from '../Map/Map.svelte';
import type { MapContext } from '../context.svelte';

const { Story } = defineMeta({
title: 'Maplibre/Extras/Geocoder',
component: Geocoder
});

let mapContext: MapContext | undefined = $state(undefined)
</script>

<Story
asChild
name="Default"
play={async ({ canvasElement, step }) => {
}}
>
<DesignTokens theme="light">
<div class="container">
<Geocoder
languages="de"
service="maptiler"
key="V32kPHZjMa0Mkn6YvSzA"
map={mapContext?.map}
/>
<div class="map-container">
<Map bind:mapContext style={SWRDataLabLight()} initialLocation={{ lat: 51, lng: 10, zoom: 20 }}/>
</div>
</div>
</DesignTokens>
</Story>

<style>
.container {
display: flex;
flex-flow: column;
gap: 1rem;
width: 800px;
}
.map-container {
height: 300px;
border: 1px solid darkgrey;
}
</style>
60 changes: 60 additions & 0 deletions components/src/maplibre/Geocoder/Geocoder.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script lang="ts">
import maplibre from 'maplibre-gl';
import MaplibreGeocoder, { type CarmenGeojsonFeature, type MaplibreGeocoderApi } from '@maplibre/maplibre-gl-geocoder';
import { MaptilerGeocoderAPI } from './GeocoderAPIs';

import type { Attachment } from 'svelte/attachments';
import type GeocoderProps from './GeocoderProps';

const {
key,
service = 'maptiler',
countries = 'de',
languages = 'en',
types = [],
placeholder,
limit = 3,
map,
}: GeocoderProps = $props();

const countriesArr = $derived(Array.isArray(countries) ? countries : [countries]);
const languagesArr = $derived(Array.isArray(languages) ? languages : [languages]);
const typesArr = $derived(Array.isArray(types) ? types : [types]);

// Future: initialise a different GeocoderAPI depending on "service"
let geocoderAPI: MaplibreGeocoderApi = $derived(new MaptilerGeocoderAPI(key));

const geocoder = $derived(new MaplibreGeocoder(geocoderAPI, {
maplibregl: maplibre,
language: languagesArr.join(','),
countries: countriesArr.join(','),
types: typesArr.join(','),
showResultsWhileTyping: true,
showResultMarkers: false,
debounceSearch: 25,
placeholder,
limit
}));

const handleResult = $derived((r: {result: CarmenGeojsonFeature})=>{
const res = r.result
if(res.bbox && map){
map.fitBounds([
[res.bbox[0], res.bbox[1]],
[res.bbox[2], res.bbox[3]],
])
}

})

const attachGeocoder: Attachment = (el) => {
geocoder.addTo(el as HTMLElement);
geocoder.on("result", handleResult)
}
</script>

<div class="container" {@attach attachGeocoder}></div>

<style lang="scss">
@use "geocoder.scss"
</style>
62 changes: 62 additions & 0 deletions components/src/maplibre/Geocoder/GeocoderAPIs.ts
Original file line number Diff line number Diff line change
@@ -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<MaplibreGeocoderFeatureResults> {
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;
}
}
37 changes: 37 additions & 0 deletions components/src/maplibre/Geocoder/GeocoderProps.ts
Original file line number Diff line number Diff line change
@@ -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;
}
162 changes: 162 additions & 0 deletions components/src/maplibre/Geocoder/geocoder.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 2 additions & 0 deletions components/src/maplibre/Geocoder/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Geocoder from './Geocoder.svelte';
export default Geocoder;
Loading
Loading