@swr-data-lab/components 1.11.3 → 1.12.1
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.
- package/.storybook/blocks/Mermaid.jsx +9 -0
- package/.storybook/main.ts +23 -17
- package/.storybook/preview.ts +42 -13
- package/package.json +72 -68
- package/src/DesignTokens/Tokens.ts +15 -12
- package/src/Select/Select.stories.svelte +3 -3
- package/src/Switcher/Switcher.svelte +1 -0
- package/src/index.js +12 -0
- package/src/maplibre/AttributionControl/AttributionControl.stories.svelte +29 -0
- package/src/maplibre/AttributionControl/AttributionControl.svelte +45 -0
- package/src/maplibre/AttributionControl/index.js +2 -0
- package/src/maplibre/GeocoderControl/GeocoderAPIs.ts +49 -0
- package/src/maplibre/GeocoderControl/GeocoderControl.stories.svelte +78 -0
- package/src/maplibre/GeocoderControl/GeocoderControl.svelte +207 -0
- package/src/maplibre/GeocoderControl/index.js +2 -0
- package/src/maplibre/Map/FallbackStyle.ts +18 -0
- package/src/maplibre/Map/Map.stories.svelte +118 -0
- package/src/maplibre/Map/Map.svelte +283 -0
- package/src/maplibre/Map/index.js +2 -0
- package/src/maplibre/MapControl/MapControl.mdx +12 -0
- package/src/maplibre/MapControl/MapControl.stories.svelte +56 -0
- package/src/maplibre/MapControl/MapControl.svelte +41 -0
- package/src/maplibre/MapControl/index.js +2 -0
- package/src/maplibre/MapStyle/SWRDataLabLight.mdx +86 -0
- package/src/maplibre/MapStyle/SWRDataLabLight.stories.svelte +41 -0
- package/src/maplibre/MapStyle/SWRDataLabLight.ts +72 -0
- package/src/maplibre/MapStyle/components/Admin.ts +173 -0
- package/src/maplibre/MapStyle/components/Buildings.ts +23 -0
- package/src/maplibre/MapStyle/components/Landuse.ts +499 -0
- package/src/maplibre/MapStyle/components/Natural.ts +1 -0
- package/src/maplibre/MapStyle/components/PlaceLabels.ts +199 -0
- package/src/maplibre/MapStyle/components/Roads.ts +2345 -0
- package/src/maplibre/MapStyle/components/Transit.ts +507 -0
- package/src/maplibre/MapStyle/components/Walking.ts +1538 -0
- package/src/maplibre/MapStyle/index.js +2 -0
- package/src/maplibre/MapStyle/tokens.ts +21 -0
- package/src/maplibre/Maplibre.mdx +91 -0
- package/src/maplibre/NavigationControl/NavigationControl.stories.svelte +39 -0
- package/src/maplibre/NavigationControl/NavigationControl.svelte +36 -0
- package/src/maplibre/NavigationControl/index.js +2 -0
- package/src/maplibre/ScaleControl/ScaleControl.stories.svelte +71 -0
- package/src/maplibre/ScaleControl/ScaleControl.svelte +25 -0
- package/src/maplibre/ScaleControl/index.js +2 -0
- package/src/maplibre/Source/MapSource.stories.svelte +9 -0
- package/src/maplibre/Source/MapSource.svelte +61 -0
- package/src/maplibre/Source/index.js +2 -0
- package/src/maplibre/Source/source.ts +89 -0
- package/src/maplibre/Tooltip/Tooltip.stories.svelte +192 -0
- package/src/maplibre/Tooltip/Tooltip.svelte +175 -0
- package/src/maplibre/Tooltip/index.js +2 -0
- package/src/maplibre/VectorLayer/VectorLayer.stories.svelte +65 -0
- package/src/maplibre/VectorLayer/VectorLayer.svelte +142 -0
- package/src/maplibre/VectorLayer/index.js +2 -0
- package/src/maplibre/VectorTileSource/VectorTileSource.mdx +19 -0
- package/src/maplibre/VectorTileSource/VectorTileSource.stories.svelte +46 -0
- package/src/maplibre/VectorTileSource/VectorTileSource.svelte +24 -0
- package/src/maplibre/VectorTileSource/index.js +2 -0
- package/src/maplibre/WithLinkLocation/WithLinkLocation.mdx +11 -0
- package/src/maplibre/WithLinkLocation/WithLinkLocation.stories.svelte +29 -0
- package/src/maplibre/WithLinkLocation/WithLinkLocation.svelte +83 -0
- package/src/maplibre/WithLinkLocation/index.js +2 -0
- package/src/maplibre/context.svelte.ts +89 -0
- package/src/maplibre/types.ts +12 -0
- package/src/maplibre/utils.ts +52 -0
|
@@ -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 maplibre, { type ControlPosition } 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 maplibre.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,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,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.js';
|
|
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,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>
|