@swr-data-lab/components 1.11.3 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.storybook/blocks/Mermaid.jsx +9 -0
  2. package/.storybook/main.ts +23 -17
  3. package/.storybook/preview.ts +42 -13
  4. package/package.json +72 -68
  5. package/src/DesignTokens/Tokens.ts +15 -12
  6. package/src/Select/Select.stories.svelte +3 -3
  7. package/src/Switcher/Switcher.svelte +1 -0
  8. package/src/index.js +12 -0
  9. package/src/maplibre/AttributionControl/AttributionControl.stories.svelte +29 -0
  10. package/src/maplibre/AttributionControl/AttributionControl.svelte +45 -0
  11. package/src/maplibre/AttributionControl/index.js +2 -0
  12. package/src/maplibre/GeocoderControl/GeocoderAPIs.ts +49 -0
  13. package/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte +78 -0
  14. package/src/maplibre/GeocoderControl/GeocoderControl.svelte +207 -0
  15. package/src/maplibre/GeocoderControl/index.js +2 -0
  16. package/src/maplibre/Map/FallbackStyle.ts +18 -0
  17. package/src/maplibre/Map/Map.stories.svelte +118 -0
  18. package/src/maplibre/Map/Map.svelte +283 -0
  19. package/src/maplibre/Map/index.js +2 -0
  20. package/src/maplibre/MapControl/MapControl.mdx +12 -0
  21. package/src/maplibre/MapControl/MapControl.stories.svelte +56 -0
  22. package/src/maplibre/MapControl/MapControl.svelte +41 -0
  23. package/src/maplibre/MapControl/index.js +2 -0
  24. package/src/maplibre/MapStyle/SWRDataLabLight.mdx +86 -0
  25. package/src/maplibre/MapStyle/SWRDataLabLight.stories.svelte +41 -0
  26. package/src/maplibre/MapStyle/SWRDataLabLight.ts +72 -0
  27. package/src/maplibre/MapStyle/components/Admin.ts +173 -0
  28. package/src/maplibre/MapStyle/components/Buildings.ts +23 -0
  29. package/src/maplibre/MapStyle/components/Landuse.ts +499 -0
  30. package/src/maplibre/MapStyle/components/Natural.ts +1 -0
  31. package/src/maplibre/MapStyle/components/PlaceLabels.ts +199 -0
  32. package/src/maplibre/MapStyle/components/Roads.ts +2345 -0
  33. package/src/maplibre/MapStyle/components/Transit.ts +507 -0
  34. package/src/maplibre/MapStyle/components/Walking.ts +1538 -0
  35. package/src/maplibre/MapStyle/index.js +2 -0
  36. package/src/maplibre/MapStyle/tokens.ts +21 -0
  37. package/src/maplibre/Maplibre.mdx +91 -0
  38. package/src/maplibre/NavigationControl/NavigationControl.stories.svelte +39 -0
  39. package/src/maplibre/NavigationControl/NavigationControl.svelte +36 -0
  40. package/src/maplibre/NavigationControl/index.js +2 -0
  41. package/src/maplibre/ScaleControl/ScaleControl.stories.svelte +71 -0
  42. package/src/maplibre/ScaleControl/ScaleControl.svelte +25 -0
  43. package/src/maplibre/ScaleControl/index.js +2 -0
  44. package/src/maplibre/Source/MapSource.stories.svelte +9 -0
  45. package/src/maplibre/Source/MapSource.svelte +61 -0
  46. package/src/maplibre/Source/index.js +2 -0
  47. package/src/maplibre/Source/source.ts +89 -0
  48. package/src/maplibre/Tooltip/Tooltip.stories.svelte +192 -0
  49. package/src/maplibre/Tooltip/Tooltip.svelte +175 -0
  50. package/src/maplibre/Tooltip/index.js +2 -0
  51. package/src/maplibre/VectorLayer/VectorLayer.stories.svelte +65 -0
  52. package/src/maplibre/VectorLayer/VectorLayer.svelte +142 -0
  53. package/src/maplibre/VectorLayer/index.js +2 -0
  54. package/src/maplibre/VectorTileSource/VectorTileSource.mdx +19 -0
  55. package/src/maplibre/VectorTileSource/VectorTileSource.stories.svelte +46 -0
  56. package/src/maplibre/VectorTileSource/VectorTileSource.svelte +24 -0
  57. package/src/maplibre/VectorTileSource/index.js +2 -0
  58. package/src/maplibre/WithLinkLocation/WithLinkLocation.mdx +11 -0
  59. package/src/maplibre/WithLinkLocation/WithLinkLocation.stories.svelte +29 -0
  60. package/src/maplibre/WithLinkLocation/WithLinkLocation.svelte +83 -0
  61. package/src/maplibre/WithLinkLocation/index.js +2 -0
  62. package/src/maplibre/context.svelte.ts +89 -0
  63. package/src/maplibre/types.ts +12 -0
  64. package/src/maplibre/utils.ts +52 -0
  65. package/tsconfig.json +1 -0
@@ -0,0 +1,2 @@
1
+ import SWRDataLabLight from './SWRDataLabLight';
2
+ export { SWRDataLabLight };
@@ -0,0 +1,21 @@
1
+ const tokens = {
2
+ sans_regular: ['SWR Sans Regular'],
3
+ sans_medium: ['SWR Sans Medium'],
4
+ sans_bold: ['SWR Sans Bold'],
5
+ background: 'hsl(0, 0%, 100%)',
6
+ water: 'hsl(210, 41%, 87%)',
7
+ water_light: 'hsl(210, 41%, 90%)',
8
+ marsh: 'hsl(200, 14%, 97%)',
9
+ grass: 'hsl(149, 37%, 97%)',
10
+ grass_dark: 'hsl(149, 37%, 93%)',
11
+ street_primary: 'hsl(0, 0%, 95%)',
12
+ street_primary_outline: 'hsla(0, 0%, 0%, 20%)',
13
+ street_secondary: 'hsl(0, 0%, 95%)',
14
+ street_secondary_outline: 'hsl(0, 0%, 50%)',
15
+ street_tertiary: 'hsl(0, 0%, 95%)',
16
+ street_tertiary_outline: 'hsl(0, 0%, 50%)',
17
+ label_primary: 'rgb(10, 10, 11)',
18
+ label_secondary: 'hsl(240, 2%, 20%)'
19
+ };
20
+
21
+ export default tokens;
@@ -0,0 +1,91 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+ import { Story, Primary, Controls, Stories } from '@storybook/addon-docs/blocks';
3
+ import * as MapStories from './Map/Map.stories.svelte';
4
+ import * as MapStyleStories from './MapStyle/SWRDataLabLight.stories.svelte';
5
+
6
+ <Meta title="Maplibre" />
7
+
8
+ # Maplibre
9
+
10
+ Lightweight svelte components and basemaps for rendering custom slippy maps using the [versatiles](https://docs.versatiles.org/) stack.
11
+
12
+ Based on [prior work](https://github.com/SWRdata/frontend_svelte_p012_spritpreise/blob/main/src/lib/components/Map/Map.svelte) from [Jakob Bauer](https://github.com/AgricolaJKB), [dimfield](https://github.com/dimfeld/svelte-maplibre) and [MIRUNE](https://github.com/MIERUNE/svelte-maplibre-gl).
13
+
14
+ <Story of={MapStyleStories.Default} />
15
+
16
+ <br />
17
+
18
+ ## Usage
19
+
20
+ This example initialises a map using the SWRDataLabLight style, adds an additional vector tile source and renders it as a vector layer.
21
+
22
+ ```jsx
23
+ <script>
24
+ import {
25
+ Map,
26
+ VectorLayer,
27
+ AttributionControl,
28
+ VectorTileSource,
29
+ SWRDataLabLight,
30
+ DesignTokens
31
+ } from '@swr-data-lab/components';
32
+ </script>
33
+
34
+ <DesignTokens>
35
+ <div class='container'>
36
+ <Map style={SWRDataLabLight}>
37
+ <VectorTileSource
38
+ id='ev-infra-source'
39
+ url='https://static.datenhub.net/data/p108_e_auto_check/ev_infra_merged.versatiles?tiles/{z}/{x}/{y}'>
40
+ </VectorTileSource>
41
+ <VectorLayer
42
+ sourceId='ev-infra-source'
43
+ sourceLayer='coverage'
44
+ id='ev-infra-outline'
45
+ type='line'
46
+ paint={{'line-width': 1, 'line-color': 'red'}}>
47
+ </VectorLayer>
48
+ <AttributionControl />
49
+ </Map>
50
+ </div>
51
+ </DesignTokens>
52
+
53
+ <style>
54
+ .container {
55
+ width: 100%;
56
+ height: 600px;
57
+ }
58
+ </style>
59
+ ```
60
+
61
+ ## Available components
62
+
63
+ ### Sources
64
+
65
+ - **[VectorTileSource](?path=/docs/maplibre-source-vectortilesource--docs)**: Loads a vector tile source supplied by a tileserver
66
+
67
+ ### Layers
68
+
69
+ - **[VectorLayer](?path=/docs/maplibre-layer-vectorlayer--docs)**: Renders a layer from a vector tile source.
70
+
71
+ ### Controls
72
+
73
+ - **[ScaleControl](?path=/docs/maplibre-control-scalecontrol--docs)**: Renders a dynamic scalebar
74
+ - **[NavigationControl](?path=/docs/maplibre-control-navigationcontrol--docs)**: Renders zoom buttons and optional compass
75
+ - **[GeocoderControl](?path=/docs/maplibre-control-geocodercontrol--docs)**: Renders a search input using [maplibre-gl-geocoder](https://github.com/maplibre/maplibre-gl-geocoder) and any supported forward geocoding service (currently [maptiler](https://www.maptiler.com/) only)
76
+ - **[AttributionControl](?path=/docs/maplibre-control-attributioncontrol--docs)**: Renders maplibre's default attribution control
77
+ - **[Custom controls](?path=/docs/maplibre-control-mapcontrol--docs)**: Renders arbitrary HTML inside maplibre's layout
78
+
79
+ ### Misc
80
+
81
+ - **[Tooltip](?path=/docs/maplibre-extras-tooltip--docs)**
82
+ - **[WithLinkLocation](?path=/docs/maplibre-extras-withlinklocation--docs)**: Derive the maps `initialLocation` from a URL parameter via forward geocoding
83
+
84
+ ### Styles
85
+
86
+ - **[SWR Data Lab Light](?path=/docs/maplibre-style-swr-data-lab-light--docs)**: Light-themed basemap using SWR colours and typefaces
87
+ - Any valid MapLibre style specification, such as [versatiles-style](https://github.com/versatiles-org/versatiles-style) (see [demo](?path=/story/map-map--alternate-style))
88
+
89
+ ## Known issues
90
+
91
+ - Firefox throws warning `WebGL warning: texImage: Alpha-premult and y-flip are deprecated for non-DOM-Element uploads`: Known [upstream mapligre-gl issue](https://github.com/maplibre/maplibre-gl-js/issues/2030) (safe to ignore)
@@ -0,0 +1,39 @@
1
+ <script context="module" lang="ts">
2
+ import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import NavigationControl from './NavigationControl.svelte';
4
+ import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
5
+ import { SWRDataLabLight } from '../MapStyle';
6
+ import Map from '../Map/Map.svelte';
7
+
8
+ const { Story } = defineMeta({
9
+ title: 'Maplibre/Control/NavigationControl',
10
+ component: NavigationControl
11
+ });
12
+ </script>
13
+
14
+ <Story asChild name="Default">
15
+ <DesignTokens>
16
+ <div class="container">
17
+ <Map style={SWRDataLabLight} initialLocation={{ lat: 51, lng: 10, zoom: 20 }}>
18
+ <NavigationControl />
19
+ </Map>
20
+ </div>
21
+ </DesignTokens>
22
+ </Story>
23
+
24
+ <Story asChild name="With compass">
25
+ <DesignTokens>
26
+ <div class="container">
27
+ <Map style={SWRDataLabLight} initialLocation={{ lat: 51, lng: 10, zoom: 20 }}>
28
+ <NavigationControl showCompass visualizePitch />
29
+ </Map>
30
+ </div>
31
+ </DesignTokens>
32
+ </Story>
33
+
34
+ <style>
35
+ .container {
36
+ width: 500px;
37
+ height: 200px;
38
+ }
39
+ </style>
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { type ControlPosition, NavigationControl } from 'maplibre-gl';
3
+ import MapControl from '../MapControl/MapControl.svelte';
4
+
5
+ interface NavigationControlProps {
6
+ showCompass?: boolean;
7
+ /**
8
+ * Show 3d tilt in the compass control. Requires `showCompass` to be `true`
9
+ */
10
+ visualizePitch?: boolean;
11
+ position?: ControlPosition;
12
+ }
13
+ const {
14
+ showCompass = false,
15
+ visualizePitch = false,
16
+ position = 'bottom-left'
17
+ }: NavigationControlProps = $props();
18
+ </script>
19
+
20
+ <MapControl control={new NavigationControl({ showCompass, visualizePitch })} {position} />
21
+
22
+ <style>
23
+ :global {
24
+ button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon {
25
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E");
26
+ }
27
+
28
+ button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon {
29
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E");
30
+ }
31
+
32
+ button.maplibregl-ctrl-compass .maplibregl-ctrl-icon {
33
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg width='29' height='29' viewBox='0 0 29 29' xmlns='http://www.w3.org/2000/svg' fill='%23333'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath d='m10.5 16 4 8 4-8h-8z' fill='%23ccc'/%3E%3C/svg%3E");
34
+ }
35
+ }
36
+ </style>
@@ -0,0 +1,2 @@
1
+ import NavigationControl from './NavigationControl.svelte';
2
+ export default NavigationControl;
@@ -0,0 +1,71 @@
1
+ <script context="module" lang="ts">
2
+ import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import { within, expect } from 'storybook/test';
4
+
5
+ import ScaleControl from './ScaleControl.svelte';
6
+ import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
7
+ import Map from '../Map/Map.svelte';
8
+ import { SWRDataLabLight } from '../MapStyle';
9
+
10
+ const { Story } = defineMeta({
11
+ title: 'Maplibre/Control/ScaleControl',
12
+ component: ScaleControl
13
+ });
14
+ </script>
15
+
16
+ <Story
17
+ asChild
18
+ name="Default"
19
+ play={async ({ canvasElement, step }) => {
20
+ const canvas = within(canvasElement);
21
+ const containerEl = canvas.getByTestId('map-container');
22
+ await step('Scale control renders', async () => {
23
+ const el = containerEl.querySelector('.maplibregl-ctrl-scale');
24
+ expect(el).toBeTruthy();
25
+ });
26
+ await step('Distance is rendered in metric units', async () => {
27
+ const el = containerEl.querySelector('.maplibregl-ctrl-scale');
28
+ expect(el).toHaveTextContent('100 m');
29
+ });
30
+ }}
31
+ >
32
+ <DesignTokens>
33
+ <div class="container">
34
+ <Map style={SWRDataLabLight} initialLocation={{ lat: 51, lng: 10, zoom: 20 }}>
35
+ <ScaleControl />
36
+ </Map>
37
+ </div>
38
+ </DesignTokens>
39
+ </Story>
40
+
41
+ <Story
42
+ asChild
43
+ name="Imperial units"
44
+ play={async ({ canvasElement, step }) => {
45
+ const canvas = within(canvasElement);
46
+ const containerEl = canvas.getByTestId('map-container');
47
+ await step('Scale control renders', async () => {
48
+ const el = containerEl.querySelector('.maplibregl-ctrl-scale');
49
+ expect(el).toBeTruthy();
50
+ });
51
+ await step('Distance is rendered in imperial units', async () => {
52
+ const el = containerEl.querySelector('.maplibregl-ctrl-scale');
53
+ expect(el).toHaveTextContent('300 ft');
54
+ });
55
+ }}
56
+ >
57
+ <DesignTokens>
58
+ <div class="container">
59
+ <Map style={SWRDataLabLight} initialLocation={{ lat: 51, lng: 10, zoom: 20 }}>
60
+ <ScaleControl unit="imperial" />
61
+ </Map>
62
+ </div>
63
+ </DesignTokens>
64
+ </Story>
65
+
66
+ <style>
67
+ .container {
68
+ width: 500px;
69
+ height: 200px;
70
+ }
71
+ </style>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ import maplibre, { type ControlPosition, type Unit } from 'maplibre-gl';
3
+ import MapControl from '../MapControl/MapControl.svelte';
4
+
5
+ interface ScaleControlProps {
6
+ maxWidth?: number | undefined;
7
+ position?: ControlPosition;
8
+ unit?: Unit;
9
+ }
10
+ let { maxWidth = 100, unit = 'metric', position = 'bottom-left' }: ScaleControlProps = $props();
11
+ </script>
12
+
13
+ <MapControl control={new maplibre.ScaleControl({ maxWidth, unit })} {position} />
14
+
15
+ <style>
16
+ :global {
17
+ .maplibregl-ctrl-scale {
18
+ color: inherit;
19
+ border-bottom: 1px solid currentColor;
20
+ font-weight: 400;
21
+ font-size: var(--fs-small-3);
22
+ font-family: var(--swr-text);
23
+ }
24
+ }
25
+ </style>
@@ -0,0 +1,2 @@
1
+ import ScaleControl from './ScaleControl.svelte';
2
+ export default ScaleControl;
@@ -0,0 +1,9 @@
1
+ <script context="module" lang="ts">
2
+ import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import MapSource from './MapSource.svelte';
4
+
5
+ const { Story } = defineMeta({
6
+ title: 'Maplibre/MapSource',
7
+ component: MapSource
8
+ });
9
+ </script>
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import { onDestroy, type Snippet } from 'svelte';
3
+ import { type Map, type SourceSpecification } from 'maplibre-gl';
4
+ import { getMapContext, createSourceContext } from '../context.svelte.ts';
5
+
6
+ type Source = maplibregl.VectorTileSource;
7
+
8
+ interface MapSourceProps {
9
+ id: string;
10
+ sourceSpec: SourceSpecification;
11
+ source?: Source;
12
+ onLoad?: (map: Map, url?: string, data?: string) => undefined;
13
+ children?: Snippet;
14
+ }
15
+
16
+ let { id, sourceSpec, source = $bindable(), children }: MapSourceProps = $props();
17
+ let firstRun = true;
18
+
19
+ // Get map context
20
+ const { map, styleLoaded } = $derived(getMapContext());
21
+
22
+ // Create source context
23
+ const sourceContext = createSourceContext();
24
+
25
+ // 1. Add the source to the map using the spec, then get the
26
+ // actual source object back from the map instance
27
+ $effect(() => {
28
+ if (map && styleLoaded) {
29
+ // See: https://svelte.dev/docs/svelte/$state#$state.snapshot
30
+ map.addSource(id, $state.snapshot(sourceSpec) as SourceSpecification);
31
+ source = map.getSource(id);
32
+ firstRun = true;
33
+ }
34
+ });
35
+
36
+ // 2. Do extra stuff with the source object
37
+ $effect(() => {
38
+ if (source) {
39
+ }
40
+ });
41
+
42
+ $effect(() => {
43
+ source;
44
+ firstRun = false;
45
+ });
46
+
47
+ onDestroy(() => {
48
+ if (map && styleLoaded) {
49
+ const layers = map?.getStyle().layers;
50
+ layers
51
+ .filter((l) => l.type !== 'background' && l.source == id)
52
+ .forEach((l) => {
53
+ map?.removeLayer(l.id);
54
+ });
55
+ map.removeSource(id);
56
+ source = undefined;
57
+ }
58
+ });
59
+ </script>
60
+
61
+ {@render children?.()}
@@ -0,0 +1,2 @@
1
+ import MapSource from './MapSource.svelte';
2
+ export default MapSource;
@@ -0,0 +1,89 @@
1
+ import type { Map, SourceSpecification } from 'maplibre-gl';
2
+ import { tick } from 'svelte';
3
+
4
+ /**
5
+ * Add a source to the map.
6
+ *
7
+ * @param map - The map instance
8
+ * @param sourceId - The ID of the source to add
9
+ * @param source - The source specification object
10
+ * @param okToAdd - Callback to check if the source should still be added
11
+ * @param cb - Callback when the source has been added
12
+ *
13
+ * This properly handles the case where an old source with the same ID is still being removed.
14
+ */
15
+ export function addSource(
16
+ map: Map,
17
+ sourceId: string,
18
+ source: SourceSpecification,
19
+ okToAdd: (sourceId: string) => boolean,
20
+ cb: () => void
21
+ ) {
22
+ // If there was an old source with the same ID, then remove it. This can happen when removing a source and adding a new source in quick succession.
23
+ let removed = false;
24
+ if (map.getSource(sourceId)) {
25
+ removed = true;
26
+ map.removeSource(sourceId);
27
+ }
28
+
29
+ const doAddSource = () => {
30
+ if (!okToAdd(sourceId)) {
31
+ // in case the component was destroyed or the id changed while waiting to call this
32
+ return;
33
+ }
34
+
35
+ map.addSource(sourceId, source);
36
+ cb();
37
+ };
38
+
39
+ if (removed) {
40
+ // Source removal happens quickly but asynchronously, and we have no way to really interlock on when it happens, so just loop until it does.
41
+ const waitForRemoval = () => {
42
+ if (!sourceId) {
43
+ return;
44
+ }
45
+
46
+ if (map.getSource(sourceId)) {
47
+ // The source hasn't been removed yet, so keep waiting.
48
+ setTimeout(waitForRemoval, 1);
49
+ } else {
50
+ doAddSource();
51
+ }
52
+ };
53
+
54
+ waitForRemoval();
55
+ } else {
56
+ // If we don't have an existing source to remove (i.e. the normal case) then
57
+ // just add the source right away.
58
+ doAddSource();
59
+ }
60
+ }
61
+
62
+ /**
63
+ * A helper function that removes a source from the map after all of the layers inside it have
64
+ * had a chance to remove themselves.
65
+ *
66
+ * @param {Readable<Map|undefined>} mapStore - The store containing the Map instance
67
+ * @param {string} sourceId - The ID of the source to remove
68
+ * @param {unknown} sourceObj - The source object that was originally added
69
+ *
70
+ * Waits one tick to ensure layers have a chance to be removed, then checks if the
71
+ * source with the given ID is still the same object as was originally added.
72
+ *
73
+ * If so, it removes the source from the map. This avoids removing a source that was
74
+ * already replaced by another source reusing the same ID.
75
+ */
76
+ export function removeSource(map: Map, sourceId: string, sourceObj: unknown) {
77
+ tick().then(() => {
78
+ // Wait a tick so that the layers inside this source can all be removed.
79
+ // But make sure that the source wasn't already replaced with another source with the same ID.
80
+ if (!map.loaded()) {
81
+ return;
82
+ }
83
+
84
+ let remainingSource = map.getSource(sourceId);
85
+ if (remainingSource === sourceObj) {
86
+ map.removeSource(sourceId);
87
+ }
88
+ });
89
+ }
@@ -0,0 +1,192 @@
1
+ <script module lang="ts">
2
+ import { defineMeta } from '@storybook/addon-svelte-csf';
3
+ import type { LngLatLike, MapGeoJSONFeature } from 'maplibre-gl';
4
+
5
+ import DesignTokens from '../../DesignTokens/DesignTokens.svelte';
6
+ import Map from '../Map/Map.svelte';
7
+ import VectorTileSource from '../VectorTileSource/VectorTileSource.svelte';
8
+ import VectorLayer from '../VectorLayer/VectorLayer.svelte';
9
+ import AttributionControl from '../AttributionControl/AttributionControl.svelte';
10
+ import Tooltip from './Tooltip.svelte';
11
+ import { SWRDataLabLight } from '../MapStyle';
12
+
13
+ const { Story } = defineMeta({
14
+ title: 'Maplibre/Extras/Tooltip',
15
+ component: Tooltip
16
+ });
17
+
18
+ let hovered = $state() as MapGeoJSONFeature | undefined;
19
+ let hovered2 = $state() as MapGeoJSONFeature | undefined;
20
+ let hoverCoords = $state([0, 0]) as LngLatLike;
21
+
22
+ let selected = $state() as MapGeoJSONFeature | undefined;
23
+ let selectCoords = $state([0, 0]) as LngLatLike;
24
+ </script>
25
+
26
+ <Story asChild name="Default">
27
+ <DesignTokens>
28
+ <div class="container">
29
+ <Map
30
+ style={SWRDataLabLight}
31
+ initialLocation={{ lat: 51, lng: 10, zoom: 8 }}
32
+ allowZoom={false}
33
+ >
34
+ <VectorTileSource
35
+ id="ev-infra-source"
36
+ url={`https://static.datenhub.net/data/p108_e_auto_check/ev_infra_merged.versatiles?tiles/{z}/{x}/{y}`}
37
+ />
38
+ <VectorLayer
39
+ sourceId="ev-infra-source"
40
+ type="fill"
41
+ id="coverage-fill"
42
+ sourceLayer="coverage"
43
+ placeBelow="street-residential"
44
+ onmousemove={(e) => {
45
+ hovered = e.features?.[0];
46
+ hoverCoords = e.lngLat;
47
+ }}
48
+ onmouseleave={() => (hovered = undefined)}
49
+ paint={{
50
+ 'fill-color': ['step', ['get', 'coverage_2025'], 'white', 1, '#ffcfcc', 1.3, '#FF4D20']
51
+ }}
52
+ />
53
+ <VectorLayer
54
+ {hovered}
55
+ sourceId="ev-infra-source"
56
+ sourceLayer="coverage"
57
+ id="ev-infra-outline"
58
+ placeBelow="label-place-city"
59
+ type="line"
60
+ layout={{
61
+ 'line-join': 'round'
62
+ }}
63
+ paint={{
64
+ 'line-width': [
65
+ 'case',
66
+ ['any', ['boolean', ['feature-state', 'hovered'], false]],
67
+ 1.5,
68
+ 0.5
69
+ ],
70
+ 'line-color': [
71
+ 'case',
72
+ ['any', ['boolean', ['feature-state', 'hovered'], false]],
73
+ '#000',
74
+ '#555'
75
+ ],
76
+ 'line-opacity': 1
77
+ }}
78
+ />
79
+
80
+ {#if hovered}
81
+ <Tooltip
82
+ position={hoverCoords}
83
+ mouseEvents={false}
84
+ showCloseButton={false}
85
+ closeOnClick={false}
86
+ >
87
+ <pre>{Object.entries(hovered.properties)
88
+ .map(([key, val]) => `${key}: ${val}`)
89
+ .join('\n')}</pre>
90
+ </Tooltip>
91
+ {/if}
92
+ <AttributionControl position="bottom-left" />
93
+ </Map>
94
+ </div>
95
+ </DesignTokens>
96
+ </Story>
97
+
98
+ <Story asChild name="Show on Click">
99
+ <DesignTokens>
100
+ <div class="container">
101
+ <Map
102
+ style={SWRDataLabLight}
103
+ initialLocation={{ lat: 51, lng: 10, zoom: 8 }}
104
+ allowZoom={false}
105
+ >
106
+ <VectorTileSource
107
+ id="ev-infra-source"
108
+ url={`https://static.datenhub.net/data/p108_e_auto_check/ev_infra_merged.versatiles?tiles/{z}/{x}/{y}`}
109
+ />
110
+ <VectorLayer
111
+ sourceId="ev-infra-source"
112
+ type="fill"
113
+ id="coverage-fill"
114
+ sourceLayer="coverage"
115
+ placeBelow="street-residential"
116
+ onmousemove={(e) => {
117
+ hovered2 = e.features?.[0];
118
+ }}
119
+ onclick={(e) => {
120
+ selected = e.features?.[0];
121
+ selectCoords = e.lngLat;
122
+ }}
123
+ onmouseleave={() => (hovered2 = undefined)}
124
+ paint={{
125
+ 'fill-color': ['step', ['get', 'coverage_2025'], 'white', 1, '#CCDCFF', 1.3, '#6280E5']
126
+ }}
127
+ />
128
+ <VectorLayer
129
+ hovered={hovered2}
130
+ {selected}
131
+ sourceId="ev-infra-source"
132
+ sourceLayer="coverage"
133
+ id="ev-infra-outline"
134
+ placeBelow="label-place-city"
135
+ type="line"
136
+ layout={{
137
+ 'line-join': 'round'
138
+ }}
139
+ paint={{
140
+ 'line-width': [
141
+ 'case',
142
+ [
143
+ 'any',
144
+ ['boolean', ['feature-state', 'hovered'], false],
145
+ ['boolean', ['feature-state', 'selected'], false]
146
+ ],
147
+ 2,
148
+ 0.5
149
+ ],
150
+ 'line-color': [
151
+ 'case',
152
+ [
153
+ 'any',
154
+ ['boolean', ['feature-state', 'hovered'], false],
155
+ ['boolean', ['feature-state', 'selected'], false]
156
+ ],
157
+ '#000',
158
+ '#555'
159
+ ],
160
+ 'line-opacity': 1
161
+ }}
162
+ />
163
+ {#if selected}
164
+ <Tooltip
165
+ position={selectCoords}
166
+ mouseEvents={true}
167
+ showCloseButton={true}
168
+ onClose={() => {
169
+ selected = undefined;
170
+ }}
171
+ >
172
+ <pre class="padRight">{Object.entries(selected.properties)
173
+ .map(([key, val]) => `${key}: ${val}`)
174
+ .join('\n')}</pre>
175
+ </Tooltip>
176
+ {/if}
177
+ <AttributionControl position="bottom-left" />
178
+ </Map>
179
+ </div>
180
+ </DesignTokens>
181
+ </Story>
182
+
183
+ <style>
184
+ .container {
185
+ width: 100%;
186
+ height: 600px;
187
+ }
188
+
189
+ .padRight {
190
+ padding-right: 2.5em;
191
+ }
192
+ </style>