@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,130 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import type { Marker as MapLibreMarker } from 'maplibre-gl';
|
|
4
|
+
import { Marker, cx } from '@loidolt/theme-svelte';
|
|
5
|
+
import type { LngLat } from '../core/types.js';
|
|
6
|
+
import { requireMapContext } from '../internal/context.js';
|
|
7
|
+
import { safely } from '../internal/safely.js';
|
|
8
|
+
import MapPopup from './MapPopup.svelte';
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
/** Where the marker stands. */
|
|
12
|
+
lngLat: LngLat;
|
|
13
|
+
/** Names the marker — its button's accessible name. */
|
|
14
|
+
label: string;
|
|
15
|
+
/** A number printed in the marker, e.g. a stop's order. */
|
|
16
|
+
number?: number;
|
|
17
|
+
/** `pin` points at its spot from above; `dot` sits centred on it. */
|
|
18
|
+
shape?: 'pin' | 'dot';
|
|
19
|
+
/** Draws the marker in its selected state. */
|
|
20
|
+
active?: boolean;
|
|
21
|
+
/** Let the marker be dragged to a new spot (pointer only). */
|
|
22
|
+
draggable?: boolean;
|
|
23
|
+
/** The marker was pressed — by pointer, Enter or Space. */
|
|
24
|
+
onSelect?: () => void;
|
|
25
|
+
/** A drag ended, at this spot. */
|
|
26
|
+
onDragEnd?: (lngLat: LngLat) => void;
|
|
27
|
+
/** Content for a popup the marker opens when pressed. Focus moves in, and back on close. */
|
|
28
|
+
popup?: Snippet;
|
|
29
|
+
/** Names the popup. Defaults to `label`. */
|
|
30
|
+
popupLabel?: string;
|
|
31
|
+
/** Whether the popup is showing. Bindable. */
|
|
32
|
+
popupOpen?: boolean;
|
|
33
|
+
/** Custom marker content in place of the loidolt pin. */
|
|
34
|
+
children?: Snippet;
|
|
35
|
+
class?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let {
|
|
39
|
+
lngLat,
|
|
40
|
+
label,
|
|
41
|
+
number,
|
|
42
|
+
shape = 'pin',
|
|
43
|
+
active = false,
|
|
44
|
+
draggable = false,
|
|
45
|
+
onSelect,
|
|
46
|
+
onDragEnd,
|
|
47
|
+
popup,
|
|
48
|
+
popupLabel,
|
|
49
|
+
popupOpen = $bindable(false),
|
|
50
|
+
children,
|
|
51
|
+
class: className,
|
|
52
|
+
}: Props = $props();
|
|
53
|
+
|
|
54
|
+
const context = requireMapContext('MapMarker');
|
|
55
|
+
let element = $state<HTMLElement | null>(null);
|
|
56
|
+
// Something to do makes it a button; otherwise it is a named picture, not a dead tab stop.
|
|
57
|
+
const interactive = $derived(Boolean(popup || onSelect || draggable));
|
|
58
|
+
let marker = $state.raw<MapLibreMarker | null>(null);
|
|
59
|
+
|
|
60
|
+
$effect(() => {
|
|
61
|
+
const target = context.map;
|
|
62
|
+
const lib = context.lib;
|
|
63
|
+
const node = element;
|
|
64
|
+
if (!target || !lib || !node) return;
|
|
65
|
+
// The pin's point is its bottom-left corner; a dot is centred on its spot.
|
|
66
|
+
const created = new lib.Marker({
|
|
67
|
+
element: node,
|
|
68
|
+
anchor: untrack(() => (shape === 'pin' ? 'bottom-left' : 'center')),
|
|
69
|
+
draggable: untrack(() => draggable),
|
|
70
|
+
})
|
|
71
|
+
.setLngLat(untrack(() => lngLat))
|
|
72
|
+
.addTo(target);
|
|
73
|
+
created.on('dragend', () => {
|
|
74
|
+
const { lng, lat } = created.getLngLat();
|
|
75
|
+
onDragEnd?.([lng, lat]);
|
|
76
|
+
});
|
|
77
|
+
marker = created;
|
|
78
|
+
return () => {
|
|
79
|
+
marker = null;
|
|
80
|
+
safely(() => created.remove());
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
$effect(() => {
|
|
85
|
+
marker?.setLngLat(lngLat);
|
|
86
|
+
});
|
|
87
|
+
$effect(() => {
|
|
88
|
+
marker?.setDraggable(draggable);
|
|
89
|
+
});
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<div hidden>
|
|
93
|
+
{#if interactive}
|
|
94
|
+
<button
|
|
95
|
+
bind:this={element}
|
|
96
|
+
type="button"
|
|
97
|
+
class={cx('ldt-map-marker', className)}
|
|
98
|
+
aria-label={label}
|
|
99
|
+
aria-haspopup={popup ? 'dialog' : undefined}
|
|
100
|
+
aria-expanded={popup ? popupOpen : undefined}
|
|
101
|
+
onclick={(event) => {
|
|
102
|
+
// The map must not also take this as a click on the map, which closes popups.
|
|
103
|
+
event.stopPropagation();
|
|
104
|
+
if (popup) popupOpen = !popupOpen;
|
|
105
|
+
onSelect?.();
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{@render face()}
|
|
109
|
+
</button>
|
|
110
|
+
{:else}
|
|
111
|
+
<span bind:this={element} class={cx('ldt-map-marker', className)} role="img" aria-label={label}>
|
|
112
|
+
{@render face()}
|
|
113
|
+
</span>
|
|
114
|
+
{/if}
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{#snippet face()}
|
|
118
|
+
{#if children}{@render children()}{:else}<Marker {shape} {number} {active} />{/if}
|
|
119
|
+
{/snippet}
|
|
120
|
+
|
|
121
|
+
{#if popup}
|
|
122
|
+
<MapPopup
|
|
123
|
+
{lngLat}
|
|
124
|
+
label={popupLabel ?? label}
|
|
125
|
+
bind:open={popupOpen}
|
|
126
|
+
offset={shape === 'pin' ? 28 : 14}
|
|
127
|
+
>
|
|
128
|
+
{@render popup()}
|
|
129
|
+
</MapPopup>
|
|
130
|
+
{/if}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { LngLat } from '../core/types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Where the marker stands. */
|
|
5
|
+
lngLat: LngLat;
|
|
6
|
+
/** Names the marker — its button's accessible name. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** A number printed in the marker, e.g. a stop's order. */
|
|
9
|
+
number?: number;
|
|
10
|
+
/** `pin` points at its spot from above; `dot` sits centred on it. */
|
|
11
|
+
shape?: 'pin' | 'dot';
|
|
12
|
+
/** Draws the marker in its selected state. */
|
|
13
|
+
active?: boolean;
|
|
14
|
+
/** Let the marker be dragged to a new spot (pointer only). */
|
|
15
|
+
draggable?: boolean;
|
|
16
|
+
/** The marker was pressed — by pointer, Enter or Space. */
|
|
17
|
+
onSelect?: () => void;
|
|
18
|
+
/** A drag ended, at this spot. */
|
|
19
|
+
onDragEnd?: (lngLat: LngLat) => void;
|
|
20
|
+
/** Content for a popup the marker opens when pressed. Focus moves in, and back on close. */
|
|
21
|
+
popup?: Snippet;
|
|
22
|
+
/** Names the popup. Defaults to `label`. */
|
|
23
|
+
popupLabel?: string;
|
|
24
|
+
/** Whether the popup is showing. Bindable. */
|
|
25
|
+
popupOpen?: boolean;
|
|
26
|
+
/** Custom marker content in place of the loidolt pin. */
|
|
27
|
+
children?: Snippet;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
declare const MapMarker: import("svelte").Component<Props, {}, "popupOpen">;
|
|
31
|
+
type MapMarker = ReturnType<typeof MapMarker>;
|
|
32
|
+
export default MapMarker;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import type { Popup } from 'maplibre-gl';
|
|
4
|
+
import { IconButton, cx } from '@loidolt/theme-svelte';
|
|
5
|
+
import type { LngLat } from '../core/types.js';
|
|
6
|
+
import { requireMapContext } from '../internal/context.js';
|
|
7
|
+
import { safely } from '../internal/safely.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
/** Where the popup points. */
|
|
11
|
+
lngLat: LngLat;
|
|
12
|
+
/** Bindable. */
|
|
13
|
+
open?: boolean;
|
|
14
|
+
onOpenChange?: (open: boolean) => void;
|
|
15
|
+
/** Names the popup for assistive tech, e.g. the place it describes. */
|
|
16
|
+
label: string;
|
|
17
|
+
closeLabel?: string;
|
|
18
|
+
/** Close when the map itself is clicked. */
|
|
19
|
+
closeOnClick?: boolean;
|
|
20
|
+
/** Distance from `lngLat`, in pixels. */
|
|
21
|
+
offset?: number;
|
|
22
|
+
children?: Snippet;
|
|
23
|
+
class?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
lngLat,
|
|
28
|
+
open = $bindable(false),
|
|
29
|
+
onOpenChange,
|
|
30
|
+
label,
|
|
31
|
+
closeLabel = 'Close',
|
|
32
|
+
closeOnClick = true,
|
|
33
|
+
offset = 12,
|
|
34
|
+
children,
|
|
35
|
+
class: className,
|
|
36
|
+
}: Props = $props();
|
|
37
|
+
|
|
38
|
+
const context = requireMapContext('MapPopup');
|
|
39
|
+
let content = $state<HTMLDivElement | null>(null);
|
|
40
|
+
let popup = $state.raw<Popup | null>(null);
|
|
41
|
+
let returnTo: HTMLElement | null = null;
|
|
42
|
+
|
|
43
|
+
const setOpen = (next: boolean) => {
|
|
44
|
+
if (open === next) return;
|
|
45
|
+
open = next;
|
|
46
|
+
onOpenChange?.(next);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Focus goes back where it came from — unless the user has already moved it elsewhere. */
|
|
50
|
+
const restoreFocus = () => {
|
|
51
|
+
const active = document.activeElement;
|
|
52
|
+
const inside = !active || active === document.body || content?.contains(active);
|
|
53
|
+
if (inside && returnTo?.isConnected) returnTo.focus();
|
|
54
|
+
returnTo = null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
$effect(() => {
|
|
58
|
+
const target = context.map;
|
|
59
|
+
const lib = context.lib;
|
|
60
|
+
const element = content;
|
|
61
|
+
if (!target || !lib || !element) return;
|
|
62
|
+
const created = new lib.Popup({
|
|
63
|
+
closeButton: false,
|
|
64
|
+
closeOnClick: untrack(() => closeOnClick),
|
|
65
|
+
focusAfterOpen: false,
|
|
66
|
+
maxWidth: 'none',
|
|
67
|
+
offset: untrack(() => offset),
|
|
68
|
+
className: 'ldt-map-popup',
|
|
69
|
+
}).setDOMContent(element);
|
|
70
|
+
created.on('close', () => {
|
|
71
|
+
setOpen(false);
|
|
72
|
+
restoreFocus();
|
|
73
|
+
});
|
|
74
|
+
popup = created;
|
|
75
|
+
return () => {
|
|
76
|
+
popup = null;
|
|
77
|
+
safely(() => created.remove());
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
$effect(() => {
|
|
82
|
+
const target = context.map;
|
|
83
|
+
const current = popup;
|
|
84
|
+
if (!target || !current) return;
|
|
85
|
+
current.setLngLat(lngLat);
|
|
86
|
+
if (open && !current.isOpen()) {
|
|
87
|
+
returnTo = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
88
|
+
current.addTo(target);
|
|
89
|
+
// Non-modal: focus moves in so its content is read and reachable, and Escape or the
|
|
90
|
+
// close button brings it back.
|
|
91
|
+
queueMicrotask(() => content?.focus());
|
|
92
|
+
} else if (!open && current.isOpen()) {
|
|
93
|
+
current.remove();
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
</script>
|
|
97
|
+
|
|
98
|
+
<div hidden>
|
|
99
|
+
<div
|
|
100
|
+
bind:this={content}
|
|
101
|
+
class={cx('ldt-map-popup__content', className)}
|
|
102
|
+
role="dialog"
|
|
103
|
+
aria-label={label}
|
|
104
|
+
tabindex="-1"
|
|
105
|
+
onkeydown={(event) => {
|
|
106
|
+
if (event.key !== 'Escape') return;
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
<div class="ldt-map-popup__body">{@render children?.()}</div>
|
|
112
|
+
<IconButton
|
|
113
|
+
label={closeLabel}
|
|
114
|
+
size="sm"
|
|
115
|
+
class="ldt-map-popup__close"
|
|
116
|
+
onclick={() => setOpen(false)}
|
|
117
|
+
>
|
|
118
|
+
<svg viewBox="0 0 10 10" width="10" height="10" aria-hidden="true" focusable="false"
|
|
119
|
+
><path d="m2 2 6 6M8 2 2 8" stroke="currentColor" stroke-width="1.4" /></svg
|
|
120
|
+
>
|
|
121
|
+
</IconButton>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { LngLat } from '../core/types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** Where the popup points. */
|
|
5
|
+
lngLat: LngLat;
|
|
6
|
+
/** Bindable. */
|
|
7
|
+
open?: boolean;
|
|
8
|
+
onOpenChange?: (open: boolean) => void;
|
|
9
|
+
/** Names the popup for assistive tech, e.g. the place it describes. */
|
|
10
|
+
label: string;
|
|
11
|
+
closeLabel?: string;
|
|
12
|
+
/** Close when the map itself is clicked. */
|
|
13
|
+
closeOnClick?: boolean;
|
|
14
|
+
/** Distance from `lngLat`, in pixels. */
|
|
15
|
+
offset?: number;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
declare const MapPopup: import("svelte").Component<Props, {}, "open">;
|
|
20
|
+
type MapPopup = ReturnType<typeof MapPopup>;
|
|
21
|
+
export default MapPopup;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack, type Snippet } from 'svelte';
|
|
3
|
+
import type { GeoJSONSource } from 'maplibre-gl';
|
|
4
|
+
import type { GeoJSON } from '../core/types.js';
|
|
5
|
+
import { requireMapContext, setSourceContext } from '../internal/context.js';
|
|
6
|
+
import { safely } from '../internal/safely.js';
|
|
7
|
+
|
|
8
|
+
interface ClusterOptions {
|
|
9
|
+
/** Pixel radius points gather within. */
|
|
10
|
+
radius?: number;
|
|
11
|
+
/** The zoom at which clusters stop forming. */
|
|
12
|
+
maxZoom?: number;
|
|
13
|
+
/** The fewest points that make a cluster. */
|
|
14
|
+
minPoints?: number;
|
|
15
|
+
/** Aggregates kept on each cluster, as MapLibre `clusterProperties`. */
|
|
16
|
+
properties?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Unique on the map; layers name it as their `source`. */
|
|
21
|
+
id: string;
|
|
22
|
+
/** GeoJSON, or a URL to fetch it from. Replace the object to update the map. */
|
|
23
|
+
data?: GeoJSON | string;
|
|
24
|
+
/** Any other MapLibre source — vector or raster tiles, an image — as its specification. */
|
|
25
|
+
spec?: Record<string, unknown>;
|
|
26
|
+
/** Gather nearby points into clusters. */
|
|
27
|
+
cluster?: boolean | ClusterOptions;
|
|
28
|
+
/** A feature property to use as its id, for hover state and `LayerManager`. */
|
|
29
|
+
promoteId?: string;
|
|
30
|
+
/** Credit shown in the attribution control. */
|
|
31
|
+
attribution?: string;
|
|
32
|
+
/** Layers drawn from this source. They pick up its id without naming it. */
|
|
33
|
+
children?: Snippet;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let { id, data, spec, cluster = false, promoteId, attribution, children }: Props = $props();
|
|
37
|
+
|
|
38
|
+
const context = requireMapContext('MapSource');
|
|
39
|
+
let ready = $state(false);
|
|
40
|
+
let applied: unknown = undefined;
|
|
41
|
+
|
|
42
|
+
setSourceContext({
|
|
43
|
+
get id() {
|
|
44
|
+
return id;
|
|
45
|
+
},
|
|
46
|
+
get ready() {
|
|
47
|
+
return ready;
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
function specification(): Record<string, unknown> {
|
|
52
|
+
if (spec) return { ...spec, ...(attribution ? { attribution } : {}) };
|
|
53
|
+
const options = typeof cluster === 'object' ? cluster : {};
|
|
54
|
+
return {
|
|
55
|
+
type: 'geojson',
|
|
56
|
+
data: data ?? { type: 'FeatureCollection', features: [] },
|
|
57
|
+
...(cluster
|
|
58
|
+
? {
|
|
59
|
+
cluster: true,
|
|
60
|
+
clusterRadius: options.radius ?? 50,
|
|
61
|
+
clusterMaxZoom: options.maxZoom ?? 14,
|
|
62
|
+
clusterMinPoints: options.minPoints ?? 2,
|
|
63
|
+
...(options.properties ? { clusterProperties: options.properties } : {}),
|
|
64
|
+
}
|
|
65
|
+
: {}),
|
|
66
|
+
...(promoteId ? { promoteId } : {}),
|
|
67
|
+
...(attribution ? { attribution } : {}),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Added once per style: a basemap swap drops every source, and `styleVersion` says so.
|
|
72
|
+
$effect(() => {
|
|
73
|
+
const target = context.map;
|
|
74
|
+
const sourceId = id;
|
|
75
|
+
void context.styleVersion;
|
|
76
|
+
if (!target || !context.loaded) return;
|
|
77
|
+
untrack(() => {
|
|
78
|
+
if (!target.getSource(sourceId)) target.addSource(sourceId, specification() as never);
|
|
79
|
+
applied = data;
|
|
80
|
+
});
|
|
81
|
+
ready = true;
|
|
82
|
+
// A style swap re-runs this after dropping the source, and layers waiting on it pick it up
|
|
83
|
+
// as soon as it is back; only unmounting (below) takes `ready` away.
|
|
84
|
+
return () => {
|
|
85
|
+
safely(() => {
|
|
86
|
+
// Layers go first: MapLibre refuses to remove a source that is still drawn from.
|
|
87
|
+
for (const layer of target.getStyle()?.layers ?? []) {
|
|
88
|
+
if ('source' in layer && layer.source === sourceId) target.removeLayer(layer.id);
|
|
89
|
+
}
|
|
90
|
+
if (target.getSource(sourceId)) target.removeSource(sourceId);
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
$effect(() => () => {
|
|
96
|
+
ready = false;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// New data replaces the old in place; the layers drawing it are untouched.
|
|
100
|
+
$effect(() => {
|
|
101
|
+
const next = data;
|
|
102
|
+
if (!ready || next === applied || next === undefined) return;
|
|
103
|
+
applied = next;
|
|
104
|
+
const source = context.map?.getSource(id) as GeoJSONSource | undefined;
|
|
105
|
+
source?.setData?.(next as never);
|
|
106
|
+
});
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
{#if ready}{@render children?.()}{/if}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Snippet } from 'svelte';
|
|
2
|
+
import type { GeoJSON } from '../core/types.js';
|
|
3
|
+
interface ClusterOptions {
|
|
4
|
+
/** Pixel radius points gather within. */
|
|
5
|
+
radius?: number;
|
|
6
|
+
/** The zoom at which clusters stop forming. */
|
|
7
|
+
maxZoom?: number;
|
|
8
|
+
/** The fewest points that make a cluster. */
|
|
9
|
+
minPoints?: number;
|
|
10
|
+
/** Aggregates kept on each cluster, as MapLibre `clusterProperties`. */
|
|
11
|
+
properties?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
interface Props {
|
|
14
|
+
/** Unique on the map; layers name it as their `source`. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** GeoJSON, or a URL to fetch it from. Replace the object to update the map. */
|
|
17
|
+
data?: GeoJSON | string;
|
|
18
|
+
/** Any other MapLibre source — vector or raster tiles, an image — as its specification. */
|
|
19
|
+
spec?: Record<string, unknown>;
|
|
20
|
+
/** Gather nearby points into clusters. */
|
|
21
|
+
cluster?: boolean | ClusterOptions;
|
|
22
|
+
/** A feature property to use as its id, for hover state and `LayerManager`. */
|
|
23
|
+
promoteId?: string;
|
|
24
|
+
/** Credit shown in the attribution control. */
|
|
25
|
+
attribution?: string;
|
|
26
|
+
/** Layers drawn from this source. They pick up its id without naming it. */
|
|
27
|
+
children?: Snippet;
|
|
28
|
+
}
|
|
29
|
+
declare const MapSource: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type MapSource = ReturnType<typeof MapSource>;
|
|
31
|
+
export default MapSource;
|