@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.
Files changed (88) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/components/BasemapSwitcher.svelte +53 -0
  4. package/dist/components/BasemapSwitcher.svelte.d.ts +20 -0
  5. package/dist/components/CircleLayer.svelte +22 -0
  6. package/dist/components/CircleLayer.svelte.d.ts +4 -0
  7. package/dist/components/CoordinateDisplay.svelte +68 -0
  8. package/dist/components/CoordinateDisplay.svelte.d.ts +13 -0
  9. package/dist/components/DeckOverlay.svelte +58 -0
  10. package/dist/components/DeckOverlay.svelte.d.ts +18 -0
  11. package/dist/components/FillExtrusionLayer.svelte +17 -0
  12. package/dist/components/FillExtrusionLayer.svelte.d.ts +4 -0
  13. package/dist/components/FillLayer.svelte +21 -0
  14. package/dist/components/FillLayer.svelte.d.ts +4 -0
  15. package/dist/components/HeatmapLayer.svelte +36 -0
  16. package/dist/components/HeatmapLayer.svelte.d.ts +4 -0
  17. package/dist/components/LayerManager.svelte +234 -0
  18. package/dist/components/LayerManager.svelte.d.ts +24 -0
  19. package/dist/components/LineLayer.svelte +18 -0
  20. package/dist/components/LineLayer.svelte.d.ts +4 -0
  21. package/dist/components/MapControl.svelte +47 -0
  22. package/dist/components/MapControl.svelte.d.ts +13 -0
  23. package/dist/components/MapFeatureList.svelte +67 -0
  24. package/dist/components/MapFeatureList.svelte.d.ts +20 -0
  25. package/dist/components/MapLegend.svelte +68 -0
  26. package/dist/components/MapLegend.svelte.d.ts +19 -0
  27. package/dist/components/MapLegendGroup.svelte +72 -0
  28. package/dist/components/MapLegendGroup.svelte.d.ts +19 -0
  29. package/dist/components/MapMarker.svelte +130 -0
  30. package/dist/components/MapMarker.svelte.d.ts +32 -0
  31. package/dist/components/MapPopup.svelte +123 -0
  32. package/dist/components/MapPopup.svelte.d.ts +21 -0
  33. package/dist/components/MapSource.svelte +109 -0
  34. package/dist/components/MapSource.svelte.d.ts +31 -0
  35. package/dist/components/MapView.svelte +454 -0
  36. package/dist/components/MapView.svelte.d.ts +76 -0
  37. package/dist/components/RasterLayer.svelte +14 -0
  38. package/dist/components/RasterLayer.svelte.d.ts +4 -0
  39. package/dist/components/SymbolLayer.svelte +22 -0
  40. package/dist/components/SymbolLayer.svelte.d.ts +4 -0
  41. package/dist/core/accessibility.d.ts +24 -0
  42. package/dist/core/accessibility.js +33 -0
  43. package/dist/core/bounds.d.ts +18 -0
  44. package/dist/core/bounds.js +91 -0
  45. package/dist/core/cluster.d.ts +35 -0
  46. package/dist/core/cluster.js +44 -0
  47. package/dist/core/data.d.ts +33 -0
  48. package/dist/core/data.js +157 -0
  49. package/dist/core/equality.d.ts +7 -0
  50. package/dist/core/equality.js +33 -0
  51. package/dist/core/format.d.ts +20 -0
  52. package/dist/core/format.js +47 -0
  53. package/dist/core/index.d.ts +16 -0
  54. package/dist/core/index.js +18 -0
  55. package/dist/core/lod.d.ts +27 -0
  56. package/dist/core/lod.js +49 -0
  57. package/dist/core/route.d.ts +58 -0
  58. package/dist/core/route.js +182 -0
  59. package/dist/core/simplify.d.ts +9 -0
  60. package/dist/core/simplify.js +102 -0
  61. package/dist/core/spatial.d.ts +21 -0
  62. package/dist/core/spatial.js +88 -0
  63. package/dist/core/style.d.ts +43 -0
  64. package/dist/core/style.js +205 -0
  65. package/dist/core/types.d.ts +110 -0
  66. package/dist/core/types.js +5 -0
  67. package/dist/core/workers/client.d.ts +26 -0
  68. package/dist/core/workers/client.js +88 -0
  69. package/dist/core/workers/handler.d.ts +6 -0
  70. package/dist/core/workers/handler.js +53 -0
  71. package/dist/core/workers/protocol.d.ts +32 -0
  72. package/dist/core/workers/protocol.js +1 -0
  73. package/dist/core/workers/spatial.worker.d.ts +1 -0
  74. package/dist/core/workers/spatial.worker.js +12 -0
  75. package/dist/deck.d.ts +18 -0
  76. package/dist/deck.js +27 -0
  77. package/dist/index.d.ts +27 -0
  78. package/dist/index.js +23 -0
  79. package/dist/internal/context.d.ts +39 -0
  80. package/dist/internal/context.js +15 -0
  81. package/dist/internal/safely.d.ts +5 -0
  82. package/dist/internal/safely.js +12 -0
  83. package/dist/internal/use-layer.svelte.d.ts +45 -0
  84. package/dist/internal/use-layer.svelte.js +179 -0
  85. package/dist/maplibre.d.ts +19 -0
  86. package/dist/maplibre.js +47 -0
  87. package/dist/styles.css +10 -0
  88. package/package.json +76 -0
@@ -0,0 +1,454 @@
1
+ <script lang="ts">
2
+ import { untrack, type Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { Map as MapLibreMap, MapMouseEvent } from 'maplibre-gl';
5
+ import { mapRoles } from '@loidolt/theme-tokens';
6
+ import {
7
+ Alert,
8
+ LiveRegion,
9
+ createAnnouncer,
10
+ createMediaQuery,
11
+ createTokenColors,
12
+ cx,
13
+ } from '@loidolt/theme-svelte';
14
+ import { formatMoveAnnouncement, describeMapKeys } from '../core/accessibility.js';
15
+ import {
16
+ basemapPaint,
17
+ createBasemapStyle,
18
+ createBlankStyle,
19
+ OPENFREEMAP_ATTRIBUTION,
20
+ OPENFREEMAP_GLYPHS,
21
+ OPENFREEMAP_TILES,
22
+ } from '../core/style.js';
23
+ import type {
24
+ LngLat,
25
+ LngLatBounds,
26
+ MapClickEvent,
27
+ MapViewState,
28
+ StyleSpecification,
29
+ } from '../core/types.js';
30
+ import { setMapContext, type LayerEntry } from '../internal/context.js';
31
+ import { loadMapLibre, type MapLibreModule } from '../maplibre.js';
32
+
33
+ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
34
+
35
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onclick' | 'onerror'> {
36
+ /** Names the map for assistive tech — say what it shows, e.g. "Delivery stops". */
37
+ label: string;
38
+ /**
39
+ * What the map shows, read after the label. The keyboard controls are appended, so a
40
+ * screen-reader user knows the map can be moved.
41
+ */
42
+ description?: string;
43
+ /** `[longitude, latitude]`. Bindable: follows the map as it moves. */
44
+ center?: LngLat;
45
+ /** Bindable: follows the map as it zooms. */
46
+ zoom?: number;
47
+ bearing?: number;
48
+ pitch?: number;
49
+ /** Fit the view to this box on load and whenever it changes. Wins over `center`/`zoom`. */
50
+ bounds?: LngLatBounds;
51
+ /** Space kept around `bounds`, in pixels. */
52
+ boundsPadding?: number;
53
+ minZoom?: number;
54
+ maxZoom?: number;
55
+ /** Keep the view inside this box. */
56
+ maxBounds?: LngLatBounds;
57
+ /**
58
+ * `default` draws the loidolt basemap from OpenMapTiles-schema tiles (OpenFreeMap's unless
59
+ * `tiles` says otherwise); `blank` draws land colour only, with no network requests.
60
+ * Bindable, so a `BasemapSwitcher` can drive it.
61
+ */
62
+ basemap?: string;
63
+ /** A style URL or document of your own. Overrides `basemap`, and is not recoloured. */
64
+ mapStyle?: string | StyleSpecification;
65
+ /** OpenMapTiles-schema tiles for the default basemap: a TileJSON URL or `{z}/{x}/{y}` template. */
66
+ tiles?: string;
67
+ /** Glyph URL template for labels. */
68
+ glyphs?: string;
69
+ /** Attribution for `tiles`. Required by most tile providers — OpenFreeMap's is the default. */
70
+ attribution?: string;
71
+ /** Pan and zoom by pointer and keyboard. */
72
+ interactive?: boolean;
73
+ /** Require two fingers or Ctrl + wheel to move the map, so it cannot trap page scrolling. */
74
+ cooperativeGestures?: boolean;
75
+ navigationControl?: boolean;
76
+ scaleControl?: boolean;
77
+ scaleUnit?: 'metric' | 'imperial';
78
+ fullscreenControl?: boolean;
79
+ geolocateControl?: boolean;
80
+ /** Corner for the built-in controls. */
81
+ controlPosition?: Corner;
82
+ /** Map height: pixels, or any CSS length. */
83
+ height?: number | string;
84
+ /**
85
+ * Announce where the map lands after a keyboard pan or zoom — the non-visual answer to
86
+ * "where am I now?".
87
+ */
88
+ announceMoves?: boolean;
89
+ /** Shown when the map cannot be drawn at all (no WebGL, or MapLibre failed to load). */
90
+ unavailableText?: string;
91
+ errorTitle?: string;
92
+ onLoad?: (map: MapLibreMap) => void;
93
+ onClick?: (event: MapClickEvent) => void;
94
+ onMoveEnd?: (view: MapViewState) => void;
95
+ onError?: (error: Error) => void;
96
+ /** Sources, layers, markers and controls. Rendered once the style has loaded. */
97
+ children?: Snippet;
98
+ /** The live MapLibre map. Bindable; `null` until created. */
99
+ map?: MapLibreMap | null;
100
+ class?: string;
101
+ ref?: HTMLDivElement | null;
102
+ }
103
+
104
+ const id = $props.id();
105
+
106
+ let {
107
+ label,
108
+ description,
109
+ center = $bindable([0, 20]),
110
+ zoom = $bindable(1.5),
111
+ bearing = 0,
112
+ pitch = 0,
113
+ bounds,
114
+ boundsPadding = 32,
115
+ minZoom,
116
+ maxZoom,
117
+ maxBounds,
118
+ basemap = $bindable('default'),
119
+ mapStyle,
120
+ tiles = OPENFREEMAP_TILES,
121
+ glyphs = OPENFREEMAP_GLYPHS,
122
+ attribution = OPENFREEMAP_ATTRIBUTION,
123
+ interactive = true,
124
+ cooperativeGestures = false,
125
+ navigationControl = true,
126
+ scaleControl = false,
127
+ scaleUnit = 'metric',
128
+ fullscreenControl = false,
129
+ geolocateControl = false,
130
+ controlPosition = 'top-right',
131
+ height = 400,
132
+ announceMoves = true,
133
+ unavailableText = 'The map could not be shown in this browser.',
134
+ errorTitle = 'Map unavailable',
135
+ onLoad,
136
+ onClick,
137
+ onMoveEnd,
138
+ onError,
139
+ children,
140
+ map = $bindable(null),
141
+ class: className,
142
+ ref = $bindable(null),
143
+ ...rest
144
+ }: Props = $props();
145
+
146
+ let container = $state<HTMLDivElement | null>(null);
147
+ let instance = $state.raw<MapLibreMap | null>(null);
148
+ let lib = $state.raw<MapLibreModule | null>(null);
149
+ let loaded = $state(false);
150
+ let styleVersion = $state(0);
151
+ let failure = $state<string | null>(null);
152
+ let layers = $state.raw<LayerEntry[]>([]);
153
+ /** A style chosen through `setBasemap` with a document of its own. */
154
+ let customBasemap = $state.raw<string | StyleSpecification | null>(null);
155
+
156
+ const palette = createTokenColors(mapRoles, { element: () => ref });
157
+ const reducedMotion = createMediaQuery('(prefers-reduced-motion: reduce)');
158
+ const announcer = createAnnouncer();
159
+
160
+ const descriptionId = `${id}-description`;
161
+ const cssHeight = $derived(typeof height === 'number' ? `${height}px` : height);
162
+ const fullDescription = $derived([description, describeMapKeys()].filter(Boolean).join(' '));
163
+
164
+ /** The style to show, before colours: recomputed only when the choice itself changes. */
165
+ const styleChoice = $derived.by(() => {
166
+ if (mapStyle) return { kind: 'custom' as const, value: mapStyle };
167
+ if (customBasemap) return { kind: 'custom' as const, value: customBasemap };
168
+ return {
169
+ kind: basemap === 'blank' ? ('blank' as const) : ('default' as const),
170
+ tiles,
171
+ glyphs,
172
+ attribution,
173
+ };
174
+ });
175
+
176
+ function buildStyle(): string | StyleSpecification {
177
+ const choice = styleChoice;
178
+ const colors = untrack(() => palette.colors);
179
+ if (choice.kind === 'custom') return choice.value;
180
+ if (choice.kind === 'blank') return createBlankStyle({ colors, glyphs: choice.glyphs });
181
+ return createBasemapStyle({
182
+ colors,
183
+ tiles: choice.tiles,
184
+ glyphs: choice.glyphs,
185
+ attribution: choice.attribution,
186
+ });
187
+ }
188
+
189
+ const viewOf = (target: MapLibreMap): MapViewState => {
190
+ const box = target.getBounds();
191
+ const middle = target.getCenter();
192
+ return {
193
+ center: [middle.lng, middle.lat],
194
+ zoom: target.getZoom(),
195
+ bearing: target.getBearing(),
196
+ pitch: target.getPitch(),
197
+ bounds: [
198
+ [box.getWest(), box.getSouth()],
199
+ [box.getEast(), box.getNorth()],
200
+ ],
201
+ };
202
+ };
203
+
204
+ setMapContext({
205
+ get map() {
206
+ return instance;
207
+ },
208
+ get lib() {
209
+ return lib;
210
+ },
211
+ get loaded() {
212
+ return loaded;
213
+ },
214
+ get styleVersion() {
215
+ return styleVersion;
216
+ },
217
+ get colors() {
218
+ return palette.colors;
219
+ },
220
+ get colorVersion() {
221
+ return palette.version;
222
+ },
223
+ get reducedMotion() {
224
+ return reducedMotion.matches;
225
+ },
226
+ get basemap() {
227
+ return basemap;
228
+ },
229
+ setBasemap(next, spec) {
230
+ customBasemap = spec ?? null;
231
+ basemap = next;
232
+ },
233
+ get layers() {
234
+ return layers;
235
+ },
236
+ registerLayer(entry) {
237
+ layers = [...layers.filter((layer) => layer.id !== entry.id), entry];
238
+ return () => {
239
+ layers = layers.filter((layer) => layer !== entry);
240
+ };
241
+ },
242
+ });
243
+
244
+ $effect(() => {
245
+ if (ref) untrack(() => palette.refresh());
246
+ });
247
+
248
+ // One MapLibre map per mounted container. Everything it is created with is read untracked:
249
+ // later changes flow through the effects below instead of rebuilding the map.
250
+ $effect(() => {
251
+ const element = container;
252
+ if (!element) return;
253
+ let disposed = false;
254
+ let current: MapLibreMap | null = null;
255
+ let announceTimer: ReturnType<typeof setTimeout> | undefined;
256
+
257
+ void (async () => {
258
+ const maplibre = await loadMapLibre();
259
+ if (disposed) return;
260
+ if (!maplibre) {
261
+ failure = unavailableText;
262
+ return;
263
+ }
264
+ const settings = untrack(() => ({
265
+ container: element,
266
+ style: buildStyle() as never,
267
+ center,
268
+ zoom,
269
+ bearing,
270
+ pitch,
271
+ minZoom,
272
+ maxZoom,
273
+ maxBounds,
274
+ interactive,
275
+ cooperativeGestures,
276
+ attributionControl: false as const,
277
+ fadeDuration: reducedMotion.matches ? 0 : 300,
278
+ locale: { 'Map.Title': label },
279
+ }));
280
+ try {
281
+ current = new maplibre.Map(settings);
282
+ } catch (error) {
283
+ failure = unavailableText;
284
+ untrack(() => onError?.(error instanceof Error ? error : new Error(String(error))));
285
+ return;
286
+ }
287
+ const created = current;
288
+ untrack(() => {
289
+ created.addControl(new maplibre.AttributionControl({ compact: true }), 'bottom-right');
290
+ if (navigationControl) {
291
+ created.addControl(
292
+ new maplibre.NavigationControl({ visualizePitch: true }),
293
+ controlPosition
294
+ );
295
+ }
296
+ if (fullscreenControl) {
297
+ created.addControl(
298
+ new maplibre.FullscreenControl({ container: ref ?? undefined }),
299
+ controlPosition
300
+ );
301
+ }
302
+ if (geolocateControl) {
303
+ created.addControl(
304
+ new maplibre.GeolocateControl({ positionOptions: { enableHighAccuracy: true } }),
305
+ controlPosition
306
+ );
307
+ }
308
+ if (scaleControl) {
309
+ created.addControl(new maplibre.ScaleControl({ unit: scaleUnit }), 'bottom-left');
310
+ }
311
+ });
312
+ created.getCanvas().setAttribute('aria-describedby', descriptionId);
313
+
314
+ created.on('style.load', () => {
315
+ styleVersion += 1;
316
+ if (loaded) return;
317
+ loaded = true;
318
+ const box = untrack(() => bounds);
319
+ if (box) created.fitBounds(box, { padding: boundsPadding, animate: false });
320
+ untrack(() => onLoad?.(created));
321
+ });
322
+ created.on('moveend', (event) => {
323
+ const view = viewOf(created);
324
+ center = view.center;
325
+ zoom = view.zoom;
326
+ untrack(() => onMoveEnd?.(view));
327
+ // Only moves made from the keyboard are spoken; pointer users can see where they went.
328
+ if (!(event as { originalEvent?: Event }).originalEvent || !announceMoves) return;
329
+ if (!((event as { originalEvent?: Event }).originalEvent instanceof KeyboardEvent)) return;
330
+ clearTimeout(announceTimer);
331
+ announceTimer = setTimeout(
332
+ () => announcer.announce(formatMoveAnnouncement(view.center, view.zoom)),
333
+ 400
334
+ );
335
+ });
336
+ created.on('click', (event: MapMouseEvent) =>
337
+ onClick?.({
338
+ lngLat: [event.lngLat.lng, event.lngLat.lat],
339
+ point: [event.point.x, event.point.y],
340
+ features: created.queryRenderedFeatures(event.point),
341
+ })
342
+ );
343
+ created.on('error', (event) => {
344
+ const error =
345
+ event.error instanceof Error
346
+ ? event.error
347
+ : new Error(event.error?.message ?? 'Map error');
348
+ // Before the style loads nothing can be drawn, so that is fatal. After it, a failed
349
+ // tile or image is not: the rest of the map keeps working.
350
+ if (!untrack(() => loaded)) failure = error.message || unavailableText;
351
+ untrack(() => onError?.(error));
352
+ });
353
+
354
+ lib = maplibre;
355
+ instance = created;
356
+ map = created;
357
+ })();
358
+
359
+ return () => {
360
+ disposed = true;
361
+ clearTimeout(announceTimer);
362
+ current?.remove();
363
+ instance = null;
364
+ map = null;
365
+ loaded = false;
366
+ layers = [];
367
+ };
368
+ });
369
+
370
+ // A new basemap or style: swap it in. Sources and layers re-add themselves on `style.load`.
371
+ let appliedChoice: unknown = null;
372
+ $effect(() => {
373
+ const choice = styleChoice;
374
+ const target = instance;
375
+ if (!target) return;
376
+ if (appliedChoice === null) {
377
+ appliedChoice = choice;
378
+ return;
379
+ }
380
+ if (appliedChoice === choice) return;
381
+ appliedChoice = choice;
382
+ target.setStyle(untrack(() => buildStyle()) as never);
383
+ });
384
+
385
+ // A theme change repaints the generated basemap in place: no reload, and data layers stay.
386
+ $effect(() => {
387
+ const colors = palette.colors;
388
+ const target = instance;
389
+ if (!target || !loaded || untrack(() => styleChoice.kind === 'custom')) return;
390
+ for (const [layerId, paint] of Object.entries(basemapPaint(colors))) {
391
+ if (!target.getLayer(layerId)) continue;
392
+ for (const [property, value] of Object.entries(paint)) {
393
+ target.setPaintProperty(layerId, property as never, value as never);
394
+ }
395
+ }
396
+ });
397
+
398
+ // Props into the map. A move the map made itself comes back equal, so it is a no-op.
399
+ $effect(() => {
400
+ const target = instance;
401
+ if (!target || !loaded) return;
402
+ const next = { center, zoom, bearing, pitch };
403
+ const now = target.getCenter();
404
+ const moved =
405
+ Math.abs(now.lng - next.center[0]) > 1e-7 ||
406
+ Math.abs(now.lat - next.center[1]) > 1e-7 ||
407
+ Math.abs(target.getZoom() - next.zoom) > 1e-4 ||
408
+ Math.abs(target.getBearing() - next.bearing) > 1e-4 ||
409
+ Math.abs(target.getPitch() - next.pitch) > 1e-4;
410
+ if (!moved) return;
411
+ if (untrack(() => reducedMotion.matches)) target.jumpTo(next);
412
+ else target.easeTo(next);
413
+ });
414
+
415
+ let fittedBounds: string | null = null;
416
+ $effect(() => {
417
+ const target = instance;
418
+ const key = bounds ? JSON.stringify(bounds) : null;
419
+ if (!target || !loaded || !bounds) return;
420
+ if (fittedBounds === null) {
421
+ // The load handler made the first fit.
422
+ fittedBounds = key;
423
+ return;
424
+ }
425
+ if (key === fittedBounds) return;
426
+ fittedBounds = key;
427
+ target.fitBounds(bounds, {
428
+ padding: boundsPadding,
429
+ animate: !untrack(() => reducedMotion.matches),
430
+ });
431
+ });
432
+
433
+ $effect(() => {
434
+ instance?.getCanvas().setAttribute('aria-label', label);
435
+ });
436
+ </script>
437
+
438
+ <div
439
+ bind:this={ref}
440
+ class={cx('ldt-map', className)}
441
+ style:--ldt-map-height={cssHeight}
442
+ data-loaded={loaded || undefined}
443
+ {...rest}
444
+ >
445
+ {#if failure}
446
+ <Alert variant="warning" title={errorTitle} class="ldt-map__alert">{failure}</Alert>
447
+ {/if}
448
+ <div bind:this={container} class="ldt-map__canvas" hidden={Boolean(failure)}></div>
449
+ <p id={descriptionId} class="ldt-sr-only">{fullDescription}</p>
450
+ <LiveRegion message={announcer.message} politeness={announcer.politeness} />
451
+ {#if loaded && instance}
452
+ <div class="ldt-map__children" hidden>{@render children?.()}</div>
453
+ {/if}
454
+ </div>
@@ -0,0 +1,76 @@
1
+ import { type Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { Map as MapLibreMap } from 'maplibre-gl';
4
+ import type { LngLat, LngLatBounds, MapClickEvent, MapViewState, StyleSpecification } from '../core/types.js';
5
+ type Corner = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'onclick' | 'onerror'> {
7
+ /** Names the map for assistive tech — say what it shows, e.g. "Delivery stops". */
8
+ label: string;
9
+ /**
10
+ * What the map shows, read after the label. The keyboard controls are appended, so a
11
+ * screen-reader user knows the map can be moved.
12
+ */
13
+ description?: string;
14
+ /** `[longitude, latitude]`. Bindable: follows the map as it moves. */
15
+ center?: LngLat;
16
+ /** Bindable: follows the map as it zooms. */
17
+ zoom?: number;
18
+ bearing?: number;
19
+ pitch?: number;
20
+ /** Fit the view to this box on load and whenever it changes. Wins over `center`/`zoom`. */
21
+ bounds?: LngLatBounds;
22
+ /** Space kept around `bounds`, in pixels. */
23
+ boundsPadding?: number;
24
+ minZoom?: number;
25
+ maxZoom?: number;
26
+ /** Keep the view inside this box. */
27
+ maxBounds?: LngLatBounds;
28
+ /**
29
+ * `default` draws the loidolt basemap from OpenMapTiles-schema tiles (OpenFreeMap's unless
30
+ * `tiles` says otherwise); `blank` draws land colour only, with no network requests.
31
+ * Bindable, so a `BasemapSwitcher` can drive it.
32
+ */
33
+ basemap?: string;
34
+ /** A style URL or document of your own. Overrides `basemap`, and is not recoloured. */
35
+ mapStyle?: string | StyleSpecification;
36
+ /** OpenMapTiles-schema tiles for the default basemap: a TileJSON URL or `{z}/{x}/{y}` template. */
37
+ tiles?: string;
38
+ /** Glyph URL template for labels. */
39
+ glyphs?: string;
40
+ /** Attribution for `tiles`. Required by most tile providers — OpenFreeMap's is the default. */
41
+ attribution?: string;
42
+ /** Pan and zoom by pointer and keyboard. */
43
+ interactive?: boolean;
44
+ /** Require two fingers or Ctrl + wheel to move the map, so it cannot trap page scrolling. */
45
+ cooperativeGestures?: boolean;
46
+ navigationControl?: boolean;
47
+ scaleControl?: boolean;
48
+ scaleUnit?: 'metric' | 'imperial';
49
+ fullscreenControl?: boolean;
50
+ geolocateControl?: boolean;
51
+ /** Corner for the built-in controls. */
52
+ controlPosition?: Corner;
53
+ /** Map height: pixels, or any CSS length. */
54
+ height?: number | string;
55
+ /**
56
+ * Announce where the map lands after a keyboard pan or zoom — the non-visual answer to
57
+ * "where am I now?".
58
+ */
59
+ announceMoves?: boolean;
60
+ /** Shown when the map cannot be drawn at all (no WebGL, or MapLibre failed to load). */
61
+ unavailableText?: string;
62
+ errorTitle?: string;
63
+ onLoad?: (map: MapLibreMap) => void;
64
+ onClick?: (event: MapClickEvent) => void;
65
+ onMoveEnd?: (view: MapViewState) => void;
66
+ onError?: (error: Error) => void;
67
+ /** Sources, layers, markers and controls. Rendered once the style has loaded. */
68
+ children?: Snippet;
69
+ /** The live MapLibre map. Bindable; `null` until created. */
70
+ map?: MapLibreMap | null;
71
+ class?: string;
72
+ ref?: HTMLDivElement | null;
73
+ }
74
+ declare const MapView: import("svelte").Component<Props, {}, "ref" | "map" | "zoom" | "center" | "basemap">;
75
+ type MapView = ReturnType<typeof MapView>;
76
+ export default MapView;
@@ -0,0 +1,14 @@
1
+ <script lang="ts">
2
+ import { useLayer, type LayerProps } from '../internal/use-layer.svelte.js';
3
+
4
+ /* Raster tiles or an image from a raster source. */
5
+ type Props = LayerProps;
6
+
7
+ let props: Props = $props();
8
+
9
+ useLayer(
10
+ 'raster',
11
+ () => props,
12
+ () => ({ paint: { 'raster-opacity': 1 } })
13
+ );
14
+ </script>
@@ -0,0 +1,4 @@
1
+ import { type LayerProps } from '../internal/use-layer.svelte.js';
2
+ declare const RasterLayer: import("svelte").Component<LayerProps, {}, "">;
3
+ type RasterLayer = ReturnType<typeof RasterLayer>;
4
+ export default RasterLayer;
@@ -0,0 +1,22 @@
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
+ /* Text and icons placed on features. Labels default to the map label colour and halo. */
6
+ type Props = LayerProps;
7
+
8
+ let props: Props = $props();
9
+
10
+ useLayer(
11
+ 'symbol',
12
+ () => props,
13
+ (colors: MapColors) => ({
14
+ paint: {
15
+ 'text-color': colors.label,
16
+ 'text-halo-color': colors.labelHalo,
17
+ 'text-halo-width': 1.4,
18
+ },
19
+ layout: { 'text-font': ['Noto Sans Regular'], 'text-size': 12 },
20
+ })
21
+ );
22
+ </script>
@@ -0,0 +1,4 @@
1
+ import { type LayerProps } from '../internal/use-layer.svelte.js';
2
+ declare const SymbolLayer: import("svelte").Component<LayerProps, {}, "">;
3
+ type SymbolLayer = ReturnType<typeof SymbolLayer>;
4
+ export default SymbolLayer;
@@ -0,0 +1,24 @@
1
+ import type { LngLat } from './types.js';
2
+ /** The keyboard controls MapLibre actually implements, for a help panel or a description. */
3
+ export declare const MAP_KEYS: readonly [{
4
+ readonly keys: "Arrow keys";
5
+ readonly action: "Pan";
6
+ }, {
7
+ readonly keys: "+ or =";
8
+ readonly action: "Zoom in";
9
+ }, {
10
+ readonly keys: "−";
11
+ readonly action: "Zoom out";
12
+ }, {
13
+ readonly keys: "Shift + ← or →";
14
+ readonly action: "Rotate";
15
+ }, {
16
+ readonly keys: "Shift + ↑ or ↓";
17
+ readonly action: "Tilt";
18
+ }];
19
+ /** One sentence naming the keys, for a map's description. */
20
+ export declare const describeMapKeys: () => string;
21
+ /** Zoom as words, since "zoom 12.4" means little aloud. */
22
+ export declare function describeZoom(zoom: number): string;
23
+ /** "Map centred on 45.52° N, 122.68° W at city level." — announced after a move. */
24
+ export declare function formatMoveAnnouncement(center: LngLat, zoom: number): string;
@@ -0,0 +1,33 @@
1
+ import { formatCoordinate } from './format.js';
2
+ /*
3
+ * Text for the non-visual side of a map. MapLibre's own keyboard handler does the work — the
4
+ * canvas takes focus, arrows pan, + and − zoom — and these describe what it did.
5
+ */
6
+ /** The keyboard controls MapLibre actually implements, for a help panel or a description. */
7
+ export const MAP_KEYS = [
8
+ { keys: 'Arrow keys', action: 'Pan' },
9
+ { keys: '+ or =', action: 'Zoom in' },
10
+ { keys: '−', action: 'Zoom out' },
11
+ { keys: 'Shift + ← or →', action: 'Rotate' },
12
+ { keys: 'Shift + ↑ or ↓', action: 'Tilt' },
13
+ ];
14
+ /** One sentence naming the keys, for a map's description. */
15
+ export const describeMapKeys = () => 'Focus the map to move it: arrow keys pan, plus and minus zoom, Shift with the arrows rotates and tilts.';
16
+ /** Zoom as words, since "zoom 12.4" means little aloud. */
17
+ export function describeZoom(zoom) {
18
+ if (zoom < 3)
19
+ return 'world';
20
+ if (zoom < 6)
21
+ return 'continent';
22
+ if (zoom < 9)
23
+ return 'region';
24
+ if (zoom < 12)
25
+ return 'city';
26
+ if (zoom < 15)
27
+ return 'neighbourhood';
28
+ return 'street';
29
+ }
30
+ /** "Map centred on 45.52° N, 122.68° W at city level." — announced after a move. */
31
+ export function formatMoveAnnouncement(center, zoom) {
32
+ return `Map centred on ${formatCoordinate(center, { precision: 2 })} at ${describeZoom(zoom)} level.`;
33
+ }
@@ -0,0 +1,18 @@
1
+ import type { GeoJSON, LngLat, LngLatBounds, Position } from './types.js';
2
+ /** Whether a pair is a real longitude/latitude: finite, within ±180 and ±90. */
3
+ export declare function isLngLat(value: unknown): value is LngLat;
4
+ /** Every coordinate in any GeoJSON object. */
5
+ export declare function coordinatesOf(data: GeoJSON): Generator<Position>;
6
+ /**
7
+ * The box around GeoJSON or a list of points, or `null` when there is nothing to box. Does
8
+ * not handle data that crosses the antimeridian.
9
+ */
10
+ export declare function boundsOf(data: GeoJSON | LngLat[]): LngLatBounds | null;
11
+ /** The middle of a box. */
12
+ export declare function boundsCenter([[west, south], [east, north]]: LngLatBounds): LngLat;
13
+ /** Grows a box by a fraction of its size on every side (`0.1` adds 10%). */
14
+ export declare function padBounds([[west, south], [east, north]]: LngLatBounds, ratio: number): LngLatBounds;
15
+ /** Whether a point lies in a box, edges included. */
16
+ export declare function boundsContain([[west, south], [east, north]]: LngLatBounds, [lng, lat]: LngLat): boolean;
17
+ /** Whether two boxes overlap. */
18
+ export declare function boundsIntersect(a: LngLatBounds, b: LngLatBounds): boolean;