@loidolt/theme-maps 0.7.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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/components/BasemapSwitcher.svelte +53 -0
- package/dist/components/BasemapSwitcher.svelte.d.ts +20 -0
- package/dist/components/CircleLayer.svelte +22 -0
- package/dist/components/CircleLayer.svelte.d.ts +4 -0
- package/dist/components/CoordinateDisplay.svelte +68 -0
- package/dist/components/CoordinateDisplay.svelte.d.ts +13 -0
- package/dist/components/DeckOverlay.svelte +58 -0
- package/dist/components/DeckOverlay.svelte.d.ts +18 -0
- package/dist/components/FillExtrusionLayer.svelte +17 -0
- package/dist/components/FillExtrusionLayer.svelte.d.ts +4 -0
- package/dist/components/FillLayer.svelte +21 -0
- package/dist/components/FillLayer.svelte.d.ts +4 -0
- package/dist/components/HeatmapLayer.svelte +36 -0
- package/dist/components/HeatmapLayer.svelte.d.ts +4 -0
- package/dist/components/LayerManager.svelte +234 -0
- package/dist/components/LayerManager.svelte.d.ts +24 -0
- package/dist/components/LineLayer.svelte +18 -0
- package/dist/components/LineLayer.svelte.d.ts +4 -0
- package/dist/components/MapControl.svelte +47 -0
- package/dist/components/MapControl.svelte.d.ts +13 -0
- package/dist/components/MapFeatureList.svelte +67 -0
- package/dist/components/MapFeatureList.svelte.d.ts +20 -0
- package/dist/components/MapLegend.svelte +68 -0
- package/dist/components/MapLegend.svelte.d.ts +19 -0
- package/dist/components/MapLegendGroup.svelte +72 -0
- package/dist/components/MapLegendGroup.svelte.d.ts +19 -0
- package/dist/components/MapMarker.svelte +130 -0
- package/dist/components/MapMarker.svelte.d.ts +32 -0
- package/dist/components/MapPopup.svelte +123 -0
- package/dist/components/MapPopup.svelte.d.ts +21 -0
- package/dist/components/MapSource.svelte +109 -0
- package/dist/components/MapSource.svelte.d.ts +31 -0
- package/dist/components/MapView.svelte +454 -0
- package/dist/components/MapView.svelte.d.ts +76 -0
- package/dist/components/RasterLayer.svelte +14 -0
- package/dist/components/RasterLayer.svelte.d.ts +4 -0
- package/dist/components/SymbolLayer.svelte +22 -0
- package/dist/components/SymbolLayer.svelte.d.ts +4 -0
- package/dist/core/accessibility.d.ts +24 -0
- package/dist/core/accessibility.js +33 -0
- package/dist/core/bounds.d.ts +18 -0
- package/dist/core/bounds.js +91 -0
- package/dist/core/cluster.d.ts +35 -0
- package/dist/core/cluster.js +44 -0
- package/dist/core/data.d.ts +33 -0
- package/dist/core/data.js +157 -0
- package/dist/core/equality.d.ts +7 -0
- package/dist/core/equality.js +33 -0
- package/dist/core/format.d.ts +20 -0
- package/dist/core/format.js +47 -0
- package/dist/core/index.d.ts +16 -0
- package/dist/core/index.js +18 -0
- package/dist/core/lod.d.ts +27 -0
- package/dist/core/lod.js +49 -0
- package/dist/core/route.d.ts +58 -0
- package/dist/core/route.js +182 -0
- package/dist/core/simplify.d.ts +9 -0
- package/dist/core/simplify.js +102 -0
- package/dist/core/spatial.d.ts +21 -0
- package/dist/core/spatial.js +88 -0
- package/dist/core/style.d.ts +43 -0
- package/dist/core/style.js +205 -0
- package/dist/core/types.d.ts +110 -0
- package/dist/core/types.js +5 -0
- package/dist/core/workers/client.d.ts +26 -0
- package/dist/core/workers/client.js +88 -0
- package/dist/core/workers/handler.d.ts +6 -0
- package/dist/core/workers/handler.js +53 -0
- package/dist/core/workers/protocol.d.ts +32 -0
- package/dist/core/workers/protocol.js +1 -0
- package/dist/core/workers/spatial.worker.d.ts +1 -0
- package/dist/core/workers/spatial.worker.js +12 -0
- package/dist/deck.d.ts +18 -0
- package/dist/deck.js +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +23 -0
- package/dist/internal/context.d.ts +39 -0
- package/dist/internal/context.js +15 -0
- package/dist/internal/safely.d.ts +5 -0
- package/dist/internal/safely.js +12 -0
- package/dist/internal/use-layer.svelte.d.ts +45 -0
- package/dist/internal/use-layer.svelte.js +179 -0
- package/dist/maplibre.d.ts +19 -0
- package/dist/maplibre.js +47 -0
- package/dist/styles.css +10 -0
- package/package.json +76 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
IconButton,
|
|
5
|
+
LiveRegion,
|
|
6
|
+
Slider,
|
|
7
|
+
Switch,
|
|
8
|
+
createAnnouncer,
|
|
9
|
+
cx,
|
|
10
|
+
} from '@loidolt/theme-svelte';
|
|
11
|
+
import type { LayerState } from '../core/types.js';
|
|
12
|
+
import { requireMapContext, type LayerEntry } from '../internal/context.js';
|
|
13
|
+
import MapControl from './MapControl.svelte';
|
|
14
|
+
|
|
15
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** The layers to offer, by id. Defaults to every layer placed with a layer component. */
|
|
19
|
+
layers?: string[];
|
|
20
|
+
title?: string;
|
|
21
|
+
position?: Corner;
|
|
22
|
+
/** Offer an opacity slider per layer. */
|
|
23
|
+
showOpacity?: boolean;
|
|
24
|
+
/** Offer moving layers up and down (Alt + ↑ and ↓ on a row, too). */
|
|
25
|
+
allowReorder?: boolean;
|
|
26
|
+
/** Show the list. Bindable, and toggled by the heading. */
|
|
27
|
+
open?: boolean;
|
|
28
|
+
/** Every change: visibility, opacity or order. Top of the list first. */
|
|
29
|
+
onChange?: (layers: LayerState[]) => void;
|
|
30
|
+
opacityLabel?: (label: string) => string;
|
|
31
|
+
moveUpLabel?: (label: string) => string;
|
|
32
|
+
moveDownLabel?: (label: string) => string;
|
|
33
|
+
movedMessage?: (label: string, position: number, total: number) => string;
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
layers: only,
|
|
39
|
+
title = 'Layers',
|
|
40
|
+
position = 'top-right',
|
|
41
|
+
showOpacity = true,
|
|
42
|
+
allowReorder = true,
|
|
43
|
+
open = $bindable(true),
|
|
44
|
+
onChange,
|
|
45
|
+
opacityLabel = (label) => `${label} opacity`,
|
|
46
|
+
moveUpLabel = (label) => `Move ${label} up`,
|
|
47
|
+
moveDownLabel = (label) => `Move ${label} down`,
|
|
48
|
+
movedMessage = (label, place, total) => `${label} moved to position ${place} of ${total}.`,
|
|
49
|
+
class: className,
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
const context = requireMapContext('LayerManager');
|
|
53
|
+
const announcer = createAnnouncer();
|
|
54
|
+
const id = $props.id();
|
|
55
|
+
let list = $state<HTMLUListElement | null>(null);
|
|
56
|
+
let orderVersion = $state(0);
|
|
57
|
+
let states = $state<Record<string, { visible: boolean; opacity: number }>>({});
|
|
58
|
+
|
|
59
|
+
const OPACITY: Record<string, string[]> = {
|
|
60
|
+
fill: ['fill-opacity'],
|
|
61
|
+
line: ['line-opacity'],
|
|
62
|
+
circle: ['circle-opacity', 'circle-stroke-opacity'],
|
|
63
|
+
symbol: ['text-opacity', 'icon-opacity'],
|
|
64
|
+
'fill-extrusion': ['fill-extrusion-opacity'],
|
|
65
|
+
heatmap: ['heatmap-opacity'],
|
|
66
|
+
raster: ['raster-opacity'],
|
|
67
|
+
background: ['background-opacity'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Managed layers in draw order, topmost first — the order the list shows. */
|
|
71
|
+
const rows = $derived.by((): LayerEntry[] => {
|
|
72
|
+
void orderVersion;
|
|
73
|
+
void context.styleVersion;
|
|
74
|
+
const map = context.map;
|
|
75
|
+
const entries = context.layers.filter((entry) => !only || only.includes(entry.id));
|
|
76
|
+
if (!map) return entries;
|
|
77
|
+
const drawn = (map.getStyle()?.layers ?? []).map((layer) => layer.id);
|
|
78
|
+
return entries
|
|
79
|
+
.filter((entry) => drawn.includes(entry.id))
|
|
80
|
+
.sort((a, b) => drawn.indexOf(b.id) - drawn.indexOf(a.id));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Read each layer's current state off the map as it arrives.
|
|
84
|
+
$effect(() => {
|
|
85
|
+
const map = context.map;
|
|
86
|
+
const current = rows;
|
|
87
|
+
if (!map) return;
|
|
88
|
+
untrack(() => {
|
|
89
|
+
for (const entry of current) {
|
|
90
|
+
if (states[entry.id]) continue;
|
|
91
|
+
const property = OPACITY[entry.type]?.[0];
|
|
92
|
+
const opacity = property ? map.getPaintProperty(entry.id, property as never) : undefined;
|
|
93
|
+
states[entry.id] = {
|
|
94
|
+
visible: map.getLayoutProperty(entry.id, 'visibility') !== 'none',
|
|
95
|
+
opacity: typeof opacity === 'number' ? opacity : 1,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const snapshot = (): LayerState[] =>
|
|
102
|
+
rows.map((entry) => ({
|
|
103
|
+
id: entry.id,
|
|
104
|
+
label: entry.label,
|
|
105
|
+
visible: states[entry.id]?.visible ?? true,
|
|
106
|
+
opacity: states[entry.id]?.opacity ?? 1,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
function setVisible(entry: LayerEntry, visible: boolean) {
|
|
110
|
+
context.map?.setLayoutProperty(entry.id, 'visibility', visible ? 'visible' : 'none');
|
|
111
|
+
states[entry.id] = { ...states[entry.id], visible };
|
|
112
|
+
onChange?.(snapshot());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function setOpacity(entry: LayerEntry, opacity: number) {
|
|
116
|
+
for (const property of OPACITY[entry.type] ?? []) {
|
|
117
|
+
context.map?.setPaintProperty(entry.id, property as never, opacity as never);
|
|
118
|
+
}
|
|
119
|
+
states[entry.id] = { ...states[entry.id], opacity };
|
|
120
|
+
onChange?.(snapshot());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Moves a layer one place up (drawn above its neighbour) or down, keeping focus with it. */
|
|
124
|
+
async function move(entry: LayerEntry, direction: 'up' | 'down') {
|
|
125
|
+
const map = context.map;
|
|
126
|
+
const index = rows.indexOf(entry);
|
|
127
|
+
const neighbour = rows[direction === 'up' ? index - 1 : index + 1];
|
|
128
|
+
if (!map || !neighbour) return;
|
|
129
|
+
if (direction === 'down') {
|
|
130
|
+
map.moveLayer(entry.id, neighbour.id);
|
|
131
|
+
} else {
|
|
132
|
+
// Above the neighbour means before whatever is drawn right after it, or on top.
|
|
133
|
+
const drawn = (map.getStyle()?.layers ?? []).map((layer) => layer.id);
|
|
134
|
+
map.moveLayer(entry.id, drawn[drawn.indexOf(neighbour.id) + 1]);
|
|
135
|
+
}
|
|
136
|
+
orderVersion += 1;
|
|
137
|
+
const place = rows.indexOf(entry) + 1;
|
|
138
|
+
announcer.announce(movedMessage(entry.label, place, rows.length));
|
|
139
|
+
onChange?.(snapshot());
|
|
140
|
+
// Reordering moves the row's DOM, which drops focus; put it back on the same control.
|
|
141
|
+
await tick();
|
|
142
|
+
const row = list?.querySelector<HTMLElement>(`[data-layer="${CSS.escape(entry.id)}"]`);
|
|
143
|
+
const preferred = row?.querySelector<HTMLButtonElement>(
|
|
144
|
+
`[data-move="${direction}"]:not(:disabled)`
|
|
145
|
+
);
|
|
146
|
+
(preferred ?? row?.querySelector<HTMLButtonElement>('[data-move]:not(:disabled)'))?.focus();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function onRowKeydown(event: KeyboardEvent, entry: LayerEntry) {
|
|
150
|
+
if (!allowReorder || !event.altKey) return;
|
|
151
|
+
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
void move(entry, event.key === 'ArrowUp' ? 'up' : 'down');
|
|
154
|
+
}
|
|
155
|
+
</script>
|
|
156
|
+
|
|
157
|
+
<MapControl {position} class={cx('ldt-layer-manager', className)}>
|
|
158
|
+
<section aria-labelledby={`${id}-title`}>
|
|
159
|
+
<h3 class="ldt-layer-manager__title" id={`${id}-title`}>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="ldt-layer-manager__toggle"
|
|
163
|
+
aria-expanded={open}
|
|
164
|
+
aria-controls={`${id}-list`}
|
|
165
|
+
onclick={() => (open = !open)}>{title}</button
|
|
166
|
+
>
|
|
167
|
+
</h3>
|
|
168
|
+
<ul bind:this={list} class="ldt-layer-manager__list" id={`${id}-list`} hidden={!open}>
|
|
169
|
+
{#each rows as entry, index (entry.id)}
|
|
170
|
+
{@const state = states[entry.id] ?? { visible: true, opacity: 1 }}
|
|
171
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
172
|
+
<li
|
|
173
|
+
class="ldt-layer-manager__row"
|
|
174
|
+
data-layer={entry.id}
|
|
175
|
+
onkeydown={(event) => onRowKeydown(event, entry)}
|
|
176
|
+
>
|
|
177
|
+
<Switch checked={state.visible} onCheckedChange={(visible) => setVisible(entry, visible)}
|
|
178
|
+
>{entry.label}</Switch
|
|
179
|
+
>
|
|
180
|
+
{#if allowReorder}
|
|
181
|
+
<span class="ldt-layer-manager__move">
|
|
182
|
+
<IconButton
|
|
183
|
+
size="sm"
|
|
184
|
+
label={moveUpLabel(entry.label)}
|
|
185
|
+
data-move="up"
|
|
186
|
+
disabled={index === 0}
|
|
187
|
+
onclick={() => move(entry, 'up')}
|
|
188
|
+
>
|
|
189
|
+
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true" focusable="false"
|
|
190
|
+
><path
|
|
191
|
+
d="M5 8V2M2 5l3-3 3 3"
|
|
192
|
+
fill="none"
|
|
193
|
+
stroke="currentColor"
|
|
194
|
+
stroke-width="1.4"
|
|
195
|
+
/></svg
|
|
196
|
+
>
|
|
197
|
+
</IconButton>
|
|
198
|
+
<IconButton
|
|
199
|
+
size="sm"
|
|
200
|
+
label={moveDownLabel(entry.label)}
|
|
201
|
+
data-move="down"
|
|
202
|
+
disabled={index === rows.length - 1}
|
|
203
|
+
onclick={() => move(entry, 'down')}
|
|
204
|
+
>
|
|
205
|
+
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true" focusable="false"
|
|
206
|
+
><path
|
|
207
|
+
d="M5 2v6M2 5l3 3 3-3"
|
|
208
|
+
fill="none"
|
|
209
|
+
stroke="currentColor"
|
|
210
|
+
stroke-width="1.4"
|
|
211
|
+
/></svg
|
|
212
|
+
>
|
|
213
|
+
</IconButton>
|
|
214
|
+
</span>
|
|
215
|
+
{/if}
|
|
216
|
+
{#if showOpacity && OPACITY[entry.type]}
|
|
217
|
+
<Slider
|
|
218
|
+
class="ldt-layer-manager__opacity"
|
|
219
|
+
label={opacityLabel(entry.label)}
|
|
220
|
+
min={0}
|
|
221
|
+
max={1}
|
|
222
|
+
step={0.05}
|
|
223
|
+
value={state.opacity}
|
|
224
|
+
disabled={!state.visible}
|
|
225
|
+
formatValue={(value) => `${Math.round(value * 100)}%`}
|
|
226
|
+
onValueChange={(value) => setOpacity(entry, value as number)}
|
|
227
|
+
/>
|
|
228
|
+
{/if}
|
|
229
|
+
</li>
|
|
230
|
+
{/each}
|
|
231
|
+
</ul>
|
|
232
|
+
<LiveRegion message={announcer.message} politeness={announcer.politeness} />
|
|
233
|
+
</section>
|
|
234
|
+
</MapControl>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { LayerState } from '../core/types.js';
|
|
2
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The layers to offer, by id. Defaults to every layer placed with a layer component. */
|
|
5
|
+
layers?: string[];
|
|
6
|
+
title?: string;
|
|
7
|
+
position?: Corner;
|
|
8
|
+
/** Offer an opacity slider per layer. */
|
|
9
|
+
showOpacity?: boolean;
|
|
10
|
+
/** Offer moving layers up and down (Alt + ↑ and ↓ on a row, too). */
|
|
11
|
+
allowReorder?: boolean;
|
|
12
|
+
/** Show the list. Bindable, and toggled by the heading. */
|
|
13
|
+
open?: boolean;
|
|
14
|
+
/** Every change: visibility, opacity or order. Top of the list first. */
|
|
15
|
+
onChange?: (layers: LayerState[]) => void;
|
|
16
|
+
opacityLabel?: (label: string) => string;
|
|
17
|
+
moveUpLabel?: (label: string) => string;
|
|
18
|
+
moveDownLabel?: (label: string) => string;
|
|
19
|
+
movedMessage?: (label: string, position: number, total: number) => string;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
declare const LayerManager: import("svelte").Component<Props, {}, "open">;
|
|
23
|
+
type LayerManager = ReturnType<typeof LayerManager>;
|
|
24
|
+
export default LayerManager;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { MapColors } from '@loidolt/theme-tokens';
|
|
3
|
+
import { useLayer, type LayerProps } from '../internal/use-layer.svelte.js';
|
|
4
|
+
|
|
5
|
+
/* Lines — routes, boundaries, outlines. Defaults to the accent colour. */
|
|
6
|
+
type Props = LayerProps;
|
|
7
|
+
|
|
8
|
+
let props: Props = $props();
|
|
9
|
+
|
|
10
|
+
useLayer(
|
|
11
|
+
'line',
|
|
12
|
+
() => props,
|
|
13
|
+
(colors: MapColors) => ({
|
|
14
|
+
paint: { 'line-color': colors.accent, 'line-width': 2.5 },
|
|
15
|
+
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
|
16
|
+
})
|
|
17
|
+
);
|
|
18
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { IControl } from 'maplibre-gl';
|
|
5
|
+
import { cx } from '@loidolt/theme-svelte';
|
|
6
|
+
import { requireMapContext } from '../internal/context.js';
|
|
7
|
+
import { safely } from '../internal/safely.js';
|
|
8
|
+
|
|
9
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
10
|
+
|
|
11
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
/** The corner it sits in, stacked with MapLibre's own controls there. */
|
|
13
|
+
position?: Corner;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
class?: string;
|
|
16
|
+
ref?: HTMLDivElement | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
position = 'top-left',
|
|
21
|
+
children,
|
|
22
|
+
class: className,
|
|
23
|
+
ref = $bindable(null),
|
|
24
|
+
...rest
|
|
25
|
+
}: Props = $props();
|
|
26
|
+
|
|
27
|
+
const context = requireMapContext('MapControl');
|
|
28
|
+
|
|
29
|
+
$effect(() => {
|
|
30
|
+
const target = context.map;
|
|
31
|
+
const node = ref;
|
|
32
|
+
const corner = position;
|
|
33
|
+
if (!target || !node) return;
|
|
34
|
+
const control: IControl = {
|
|
35
|
+
onAdd: () => node,
|
|
36
|
+
onRemove: () => node.remove(),
|
|
37
|
+
};
|
|
38
|
+
target.addControl(control, corner);
|
|
39
|
+
return () => safely(() => target.removeControl(control));
|
|
40
|
+
});
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<div hidden>
|
|
44
|
+
<div bind:this={ref} class={cx('maplibregl-ctrl ldt-map-control', className)} {...rest}>
|
|
45
|
+
{@render children?.()}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
/** The corner it sits in, stacked with MapLibre's own controls there. */
|
|
6
|
+
position?: Corner;
|
|
7
|
+
children?: Snippet;
|
|
8
|
+
class?: string;
|
|
9
|
+
ref?: HTMLDivElement | null;
|
|
10
|
+
}
|
|
11
|
+
declare const MapControl: import("svelte").Component<Props, {}, "ref">;
|
|
12
|
+
type MapControl = ReturnType<typeof MapControl>;
|
|
13
|
+
export default MapControl;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { Map as MapLibreMap } from 'maplibre-gl';
|
|
4
|
+
import { cx } from '@loidolt/theme-svelte';
|
|
5
|
+
import type { MapFeatureItem } from '../core/types.js';
|
|
6
|
+
import { getMapContext } from '../internal/context.js';
|
|
7
|
+
|
|
8
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
9
|
+
/** The places on the map, in reading order. */
|
|
10
|
+
items: MapFeatureItem[];
|
|
11
|
+
/** Names the list, e.g. "Stops on this route". */
|
|
12
|
+
label: string;
|
|
13
|
+
/** The map to move. Found automatically inside a `MapView`; pass it when the list sits beside one. */
|
|
14
|
+
map?: MapLibreMap | null;
|
|
15
|
+
/** Zoom level to fly to. */
|
|
16
|
+
zoom?: number;
|
|
17
|
+
/** The chosen item, highlighted. Bindable. */
|
|
18
|
+
selected?: string | number | null;
|
|
19
|
+
onSelect?: (item: MapFeatureItem) => void;
|
|
20
|
+
class?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
items,
|
|
25
|
+
label,
|
|
26
|
+
map = null,
|
|
27
|
+
zoom = 14,
|
|
28
|
+
selected = $bindable(null),
|
|
29
|
+
onSelect,
|
|
30
|
+
class: className,
|
|
31
|
+
...rest
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
const context = getMapContext();
|
|
35
|
+
const reducedMotion =
|
|
36
|
+
typeof window !== 'undefined' &&
|
|
37
|
+
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
38
|
+
|
|
39
|
+
function choose(item: MapFeatureItem) {
|
|
40
|
+
selected = item.id;
|
|
41
|
+
const target = map ?? context?.map;
|
|
42
|
+
const move = { center: item.lngLat, zoom: Math.max(zoom, target?.getZoom() ?? 0) };
|
|
43
|
+
if (context?.reducedMotion ?? reducedMotion) target?.jumpTo(move);
|
|
44
|
+
else target?.flyTo(move);
|
|
45
|
+
onSelect?.(item);
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<!-- Every place on the map, as a list: the non-visual way to find and go to one. -->
|
|
50
|
+
<nav class={cx('ldt-map-features', className)} aria-label={label} {...rest}>
|
|
51
|
+
<ul class="ldt-map-features__list">
|
|
52
|
+
{#each items as item (item.id)}
|
|
53
|
+
<li>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
class="ldt-map-features__item"
|
|
57
|
+
aria-current={selected === item.id ? 'true' : undefined}
|
|
58
|
+
onclick={() => choose(item)}
|
|
59
|
+
>
|
|
60
|
+
<span class="ldt-map-features__label">{item.label}</span>
|
|
61
|
+
{#if item.description}<span class="ldt-map-features__description">{item.description}</span
|
|
62
|
+
>{/if}
|
|
63
|
+
</button>
|
|
64
|
+
</li>
|
|
65
|
+
{/each}
|
|
66
|
+
</ul>
|
|
67
|
+
</nav>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { Map as MapLibreMap } from 'maplibre-gl';
|
|
3
|
+
import type { MapFeatureItem } from '../core/types.js';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** The places on the map, in reading order. */
|
|
6
|
+
items: MapFeatureItem[];
|
|
7
|
+
/** Names the list, e.g. "Stops on this route". */
|
|
8
|
+
label: string;
|
|
9
|
+
/** The map to move. Found automatically inside a `MapView`; pass it when the list sits beside one. */
|
|
10
|
+
map?: MapLibreMap | null;
|
|
11
|
+
/** Zoom level to fly to. */
|
|
12
|
+
zoom?: number;
|
|
13
|
+
/** The chosen item, highlighted. Bindable. */
|
|
14
|
+
selected?: string | number | null;
|
|
15
|
+
onSelect?: (item: MapFeatureItem) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const MapFeatureList: import("svelte").Component<Props, {}, "selected">;
|
|
19
|
+
type MapFeatureList = ReturnType<typeof MapFeatureList>;
|
|
20
|
+
export default MapFeatureList;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cx } from '@loidolt/theme-svelte';
|
|
4
|
+
import type { LegendGradient, LegendItem } from '../core/types.js';
|
|
5
|
+
import { getMapContext } from '../internal/context.js';
|
|
6
|
+
import MapControl from './MapControl.svelte';
|
|
7
|
+
|
|
8
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
9
|
+
|
|
10
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
11
|
+
title: string;
|
|
12
|
+
/** Categories, each with its colour. */
|
|
13
|
+
items?: LegendItem[];
|
|
14
|
+
/** A continuous scale instead: its colours from least to most, and the two end labels. */
|
|
15
|
+
gradient?: LegendGradient;
|
|
16
|
+
/**
|
|
17
|
+
* Where it sits. Inside a `MapView` it becomes a map control in this corner; outside one it
|
|
18
|
+
* renders in the page flow, beside the map.
|
|
19
|
+
*/
|
|
20
|
+
position?: Corner;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let {
|
|
25
|
+
title,
|
|
26
|
+
items = [],
|
|
27
|
+
gradient,
|
|
28
|
+
position = 'bottom-left',
|
|
29
|
+
class: className,
|
|
30
|
+
...rest
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
const inMap = getMapContext() !== null;
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
{#snippet legend()}
|
|
37
|
+
<section class={cx('ldt-map-legend', className)} aria-label={title} {...rest}>
|
|
38
|
+
<h3 class="ldt-map-legend__title">{title}</h3>
|
|
39
|
+
{#if gradient}
|
|
40
|
+
<div
|
|
41
|
+
class="ldt-map-legend__ramp"
|
|
42
|
+
style:background-image={`linear-gradient(to right, ${gradient.colors.join(', ')})`}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
></div>
|
|
45
|
+
<p class="ldt-map-legend__ends"><span>{gradient.min}</span><span>{gradient.max}</span></p>
|
|
46
|
+
{/if}
|
|
47
|
+
{#if items.length}
|
|
48
|
+
<ul class="ldt-map-legend__items">
|
|
49
|
+
{#each items as item (item.label)}
|
|
50
|
+
<li class="ldt-map-legend__item">
|
|
51
|
+
<span
|
|
52
|
+
class={cx('ldt-map-legend__key', `ldt-map-legend__key--${item.shape ?? 'fill'}`)}
|
|
53
|
+
style:--ldt-legend-color={item.color}
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
></span>
|
|
56
|
+
{item.label}
|
|
57
|
+
</li>
|
|
58
|
+
{/each}
|
|
59
|
+
</ul>
|
|
60
|
+
{/if}
|
|
61
|
+
</section>
|
|
62
|
+
{/snippet}
|
|
63
|
+
|
|
64
|
+
{#if inMap}
|
|
65
|
+
<MapControl {position}>{@render legend()}</MapControl>
|
|
66
|
+
{:else}
|
|
67
|
+
{@render legend()}
|
|
68
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { LegendGradient, LegendItem } from '../core/types.js';
|
|
3
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
title: string;
|
|
6
|
+
/** Categories, each with its colour. */
|
|
7
|
+
items?: LegendItem[];
|
|
8
|
+
/** A continuous scale instead: its colours from least to most, and the two end labels. */
|
|
9
|
+
gradient?: LegendGradient;
|
|
10
|
+
/**
|
|
11
|
+
* Where it sits. Inside a `MapView` it becomes a map control in this corner; outside one it
|
|
12
|
+
* renders in the page flow, beside the map.
|
|
13
|
+
*/
|
|
14
|
+
position?: Corner;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const MapLegend: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type MapLegend = ReturnType<typeof MapLegend>;
|
|
19
|
+
export default MapLegend;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cx } from '@loidolt/theme-svelte';
|
|
4
|
+
import type { LegendGradient, LegendItem } from '../core/types.js';
|
|
5
|
+
import { getMapContext } from '../internal/context.js';
|
|
6
|
+
import MapControl from './MapControl.svelte';
|
|
7
|
+
|
|
8
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
9
|
+
|
|
10
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
11
|
+
/** Names the group, e.g. "Layers shown". */
|
|
12
|
+
title: string;
|
|
13
|
+
/** One collapsible legend per layer. */
|
|
14
|
+
legends: Array<{
|
|
15
|
+
title: string;
|
|
16
|
+
items?: LegendItem[];
|
|
17
|
+
gradient?: LegendGradient;
|
|
18
|
+
open?: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
position?: Corner;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let { title, legends, position = 'bottom-left', class: className, ...rest }: Props = $props();
|
|
25
|
+
|
|
26
|
+
const inMap = getMapContext() !== null;
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
{#snippet group()}
|
|
30
|
+
<section
|
|
31
|
+
class={cx('ldt-map-legend ldt-map-legend--group', className)}
|
|
32
|
+
aria-label={title}
|
|
33
|
+
{...rest}
|
|
34
|
+
>
|
|
35
|
+
<h3 class="ldt-map-legend__title">{title}</h3>
|
|
36
|
+
{#each legends as legend (legend.title)}
|
|
37
|
+
<details class="ldt-map-legend__section" open={legend.open ?? true}>
|
|
38
|
+
<summary class="ldt-map-legend__summary">{legend.title}</summary>
|
|
39
|
+
{#if legend.gradient}
|
|
40
|
+
<div
|
|
41
|
+
class="ldt-map-legend__ramp"
|
|
42
|
+
style:background-image={`linear-gradient(to right, ${legend.gradient.colors.join(', ')})`}
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
></div>
|
|
45
|
+
<p class="ldt-map-legend__ends">
|
|
46
|
+
<span>{legend.gradient.min}</span><span>{legend.gradient.max}</span>
|
|
47
|
+
</p>
|
|
48
|
+
{/if}
|
|
49
|
+
{#if legend.items?.length}
|
|
50
|
+
<ul class="ldt-map-legend__items">
|
|
51
|
+
{#each legend.items as item (item.label)}
|
|
52
|
+
<li class="ldt-map-legend__item">
|
|
53
|
+
<span
|
|
54
|
+
class={cx('ldt-map-legend__key', `ldt-map-legend__key--${item.shape ?? 'fill'}`)}
|
|
55
|
+
style:--ldt-legend-color={item.color}
|
|
56
|
+
aria-hidden="true"
|
|
57
|
+
></span>
|
|
58
|
+
{item.label}
|
|
59
|
+
</li>
|
|
60
|
+
{/each}
|
|
61
|
+
</ul>
|
|
62
|
+
{/if}
|
|
63
|
+
</details>
|
|
64
|
+
{/each}
|
|
65
|
+
</section>
|
|
66
|
+
{/snippet}
|
|
67
|
+
|
|
68
|
+
{#if inMap}
|
|
69
|
+
<MapControl {position}>{@render group()}</MapControl>
|
|
70
|
+
{:else}
|
|
71
|
+
{@render group()}
|
|
72
|
+
{/if}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { LegendGradient, LegendItem } from '../core/types.js';
|
|
3
|
+
type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
5
|
+
/** Names the group, e.g. "Layers shown". */
|
|
6
|
+
title: string;
|
|
7
|
+
/** One collapsible legend per layer. */
|
|
8
|
+
legends: Array<{
|
|
9
|
+
title: string;
|
|
10
|
+
items?: LegendItem[];
|
|
11
|
+
gradient?: LegendGradient;
|
|
12
|
+
open?: boolean;
|
|
13
|
+
}>;
|
|
14
|
+
position?: Corner;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const MapLegendGroup: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type MapLegendGroup = ReturnType<typeof MapLegendGroup>;
|
|
19
|
+
export default MapLegendGroup;
|