@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +487 -2
- package/dist/core/basemap.svelte.d.ts +38 -0
- package/dist/core/basemap.svelte.js +163 -0
- package/dist/core/context.d.ts +5 -0
- package/dist/core/context.js +13 -0
- package/dist/core/create-map.svelte.d.ts +207 -0
- package/dist/core/create-map.svelte.js +376 -0
- package/dist/core/gl.d.ts +92 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +42 -0
- package/dist/core/interactions.svelte.js +213 -0
- package/dist/core/plugin-host.svelte.d.ts +46 -0
- package/dist/core/plugin-host.svelte.js +366 -0
- package/dist/core/plugin.d.ts +148 -0
- package/dist/core/plugin.js +1 -0
- package/dist/core/scoped-context.d.ts +7 -0
- package/dist/core/scoped-context.js +34 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +24 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +51 -0
- package/dist/data/messages.d.ts +10 -0
- package/dist/data/messages.js +17 -0
- package/dist/data/metadata.d.ts +22 -0
- package/dist/data/metadata.js +110 -0
- package/dist/data/refs.d.ts +18 -0
- package/dist/data/refs.js +61 -0
- package/dist/data/scale-types.d.ts +7 -0
- package/dist/data/scale-types.js +1 -0
- package/dist/data/scale.d.ts +13 -0
- package/dist/data/scale.js +27 -0
- package/dist/data/selection.d.ts +7 -0
- package/dist/data/selection.js +31 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +69 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +41 -0
- package/dist/plugins/icons.d.ts +21 -0
- package/dist/plugins/icons.js +73 -0
- package/dist/plugins/points-clusters.d.ts +60 -0
- package/dist/plugins/points-clusters.js +93 -0
- package/dist/plugins/points.svelte.d.ts +93 -0
- package/dist/plugins/points.svelte.js +473 -0
- package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
- package/dist/plugins/scale-resolution.svelte.js +111 -0
- package/dist/plugins/shapes.svelte.d.ts +29 -0
- package/dist/plugins/shapes.svelte.js +307 -0
- package/dist/plugins/shared.d.ts +6 -0
- package/dist/plugins/shared.js +18 -0
- package/dist/plugins/style.d.ts +57 -0
- package/dist/plugins/style.js +193 -0
- package/dist/presets/CollocationTooltip.svelte +132 -0
- package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorClusterTooltip.svelte +149 -0
- package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
- package/dist/presets/MonitorReadingBox.svelte +58 -0
- package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
- package/dist/presets/MonitorTooltip.svelte +88 -0
- package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
- package/dist/presets/calheatscore.d.ts +24 -0
- package/dist/presets/calheatscore.js +22 -0
- package/dist/presets/collocation-sites.d.ts +18 -0
- package/dist/presets/collocation-sites.js +67 -0
- package/dist/presets/fire-groups.d.ts +26 -0
- package/dist/presets/fire-groups.js +51 -0
- package/dist/presets/forecast-zones.d.ts +25 -0
- package/dist/presets/forecast-zones.js +23 -0
- package/dist/presets/hms-fire.d.ts +26 -0
- package/dist/presets/hms-fire.js +87 -0
- package/dist/presets/hms-smoke.d.ts +19 -0
- package/dist/presets/hms-smoke.js +20 -0
- package/dist/presets/monitors.d.ts +48 -0
- package/dist/presets/monitors.js +243 -0
- package/dist/presets/reactive-props.svelte.d.ts +4 -0
- package/dist/presets/reactive-props.svelte.js +7 -0
- package/dist/presets/regions.d.ts +24 -0
- package/dist/presets/regions.js +22 -0
- package/dist/stores/abort.d.ts +16 -0
- package/dist/stores/abort.js +52 -0
- package/dist/stores/collocation-store.svelte.d.ts +25 -0
- package/dist/stores/collocation-store.svelte.js +72 -0
- package/dist/stores/hms-stores.svelte.d.ts +35 -0
- package/dist/stores/hms-stores.svelte.js +97 -0
- package/dist/stores/index.d.ts +4 -0
- package/dist/stores/index.js +4 -0
- package/dist/stores/load-once.svelte.d.ts +25 -0
- package/dist/stores/load-once.svelte.js +93 -0
- package/dist/stores/monitors-store.svelte.d.ts +85 -0
- package/dist/stores/monitors-store.svelte.js +340 -0
- package/dist/theme.css +34 -0
- package/dist/ui/FeatureList.svelte +501 -0
- package/dist/ui/FeatureList.svelte.d.ts +14 -0
- package/dist/ui/Legend.svelte +172 -0
- package/dist/ui/Legend.svelte.d.ts +13 -0
- package/dist/ui/LegendBar.svelte +161 -0
- package/dist/ui/LegendBar.svelte.d.ts +11 -0
- package/dist/ui/MapControls.svelte +135 -0
- package/dist/ui/MapControls.svelte.d.ts +11 -0
- package/dist/ui/MapTooltip.svelte +158 -0
- package/dist/ui/MapTooltip.svelte.d.ts +28 -0
- package/dist/ui/SelectionBar.svelte +123 -0
- package/dist/ui/SelectionBar.svelte.d.ts +10 -0
- package/dist/ui/TooltipItem.svelte +66 -0
- package/dist/ui/TooltipItem.svelte.d.ts +11 -0
- package/dist/ui/ViewSummary.svelte +97 -0
- package/dist/ui/ViewSummary.svelte.d.ts +7 -0
- package/dist/ui/controls.css +45 -0
- package/dist/ui/data-credits.svelte.d.ts +9 -0
- package/dist/ui/data-credits.svelte.js +52 -0
- package/dist/ui/feature-list.d.ts +31 -0
- package/dist/ui/feature-list.js +80 -0
- package/dist/ui/legend-data.d.ts +41 -0
- package/dist/ui/legend-data.js +40 -0
- package/dist/ui/level-chip.d.ts +11 -0
- package/dist/ui/level-chip.js +15 -0
- package/dist/ui/value-format.d.ts +7 -0
- package/dist/ui/value-format.js +13 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +373 -0
- package/dist/view/MapView.svelte.d.ts +43 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +182 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +103 -5
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "@maptiler/sdk/dist/maptiler-sdk.css";
|
|
3
|
+
import "../ui/controls.css";
|
|
4
|
+
import { DEV } from "esm-env";
|
|
5
|
+
import type { Snippet } from "svelte";
|
|
6
|
+
import { untrack } from "svelte";
|
|
7
|
+
import { setMapContext } from "../core/context.js";
|
|
8
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
9
|
+
import type { GlMapLike } from "../core/gl.js";
|
|
10
|
+
import FeatureList from "../ui/FeatureList.svelte";
|
|
11
|
+
import type { SortBy } from "../ui/feature-list.js";
|
|
12
|
+
import MapControls from "../ui/MapControls.svelte";
|
|
13
|
+
import MapTooltip from "../ui/MapTooltip.svelte";
|
|
14
|
+
import SelectionBar from "../ui/SelectionBar.svelte";
|
|
15
|
+
import ViewSummary from "../ui/ViewSummary.svelte";
|
|
16
|
+
import { createDataCredits } from "../ui/data-credits.svelte.js";
|
|
17
|
+
import { createGlMap, setDataCredits, watchTheme } from "./create-gl-map.js";
|
|
18
|
+
import {
|
|
19
|
+
cornerSides,
|
|
20
|
+
resolveFeatureList,
|
|
21
|
+
type FeatureListOptions,
|
|
22
|
+
type ListPosition
|
|
23
|
+
} from "./list-placement.js";
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** The map to show. Fixed for the component's life. */
|
|
27
|
+
map: MapContext;
|
|
28
|
+
/** `false` releases the WebGL context; `true` rebuilds it with the same camera. */
|
|
29
|
+
active?: boolean;
|
|
30
|
+
class?: string;
|
|
31
|
+
/** The region's accessible name. Defaults to the `mapsdk_map_label` message. */
|
|
32
|
+
label?: string;
|
|
33
|
+
/** On-screen pieces drawn over the map; they get the map through `getMapContext()`. */
|
|
34
|
+
overlays?: Snippet;
|
|
35
|
+
/**
|
|
36
|
+
* The List button and its feature list panel: `false` leaves them out; `true` (default) or
|
|
37
|
+
* options place them. `position` picks the corner (default `"top-left"`); the button stacks
|
|
38
|
+
* below (or, in a bottom corner, above) the controls in that corner, and the open panel stops
|
|
39
|
+
* short of the controls and credits at the far end of its side. `reveal: "focus"` shows the
|
|
40
|
+
* button only while it or its open panel has keyboard focus (and the whole time the panel is
|
|
41
|
+
* open); otherwise it's visually hidden but stays in the Tab order, like a skip link.
|
|
42
|
+
*/
|
|
43
|
+
featureList?: boolean | FeatureListOptions;
|
|
44
|
+
/** `false` leaves out the selection bar. */
|
|
45
|
+
selectionBar?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* The zoom controls: `"mapsdk"` (default) draws map-sdk's zoom buttons, `"engine"` the
|
|
48
|
+
* MapTiler SDK's default zoom/compass and locate-me controls, `"none"` none. The credits
|
|
49
|
+
* always show. Choose it up front: changing it after mount recreates the map (the engine
|
|
50
|
+
* controls are part of how the map is built), like `active`.
|
|
51
|
+
*/
|
|
52
|
+
controls?: "mapsdk" | "engine" | "none";
|
|
53
|
+
/**
|
|
54
|
+
* `"pinned"` (default) anchors the tooltip to its feature. `"pointer"` shows it at the
|
|
55
|
+
* pointer, like sjvair.com's map, and hides it on zoom until the pointer moves again.
|
|
56
|
+
* Keyboard focus from the feature list always shows it pinned.
|
|
57
|
+
*/
|
|
58
|
+
tooltip?: "pinned" | "pointer";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let {
|
|
62
|
+
map,
|
|
63
|
+
active = true,
|
|
64
|
+
class: className,
|
|
65
|
+
label,
|
|
66
|
+
overlays,
|
|
67
|
+
featureList = true,
|
|
68
|
+
selectionBar = true,
|
|
69
|
+
controls = "mapsdk",
|
|
70
|
+
tooltip = "pinned"
|
|
71
|
+
}: Props = $props();
|
|
72
|
+
|
|
73
|
+
const list = $derived(resolveFeatureList(featureList));
|
|
74
|
+
const sides = $derived(list ? cornerSides(list.position) : null);
|
|
75
|
+
|
|
76
|
+
const uid = $props.id();
|
|
77
|
+
const listId = `${uid}-list`;
|
|
78
|
+
let listOpen = $state(false);
|
|
79
|
+
// Kept here: the list is unmounted while closed, so it doesn't sort every feature on each move.
|
|
80
|
+
let listSort = $state<SortBy>("name");
|
|
81
|
+
let listButton: HTMLButtonElement | undefined = $state();
|
|
82
|
+
let listBox: HTMLDivElement | undefined = $state();
|
|
83
|
+
let listWidth = $state(0);
|
|
84
|
+
|
|
85
|
+
// The open panel's right edge, so focusing a feature pans it clear of the list.
|
|
86
|
+
$effect(() => {
|
|
87
|
+
const box = listOpen ? listBox : undefined;
|
|
88
|
+
void listWidth;
|
|
89
|
+
if (!box) {
|
|
90
|
+
map.setListInset(0);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const panel = box.getBoundingClientRect();
|
|
94
|
+
const mapBox = root.getBoundingClientRect();
|
|
95
|
+
if (sides?.horizontal === "right") map.setListInset(mapBox.right - panel.left, "right");
|
|
96
|
+
else map.setListInset(panel.right - mapBox.left, "left");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// How far the controls in the button's corner, and in the other corner on its side, reach in
|
|
100
|
+
// from the map's top or bottom edge (px, 0 when there are none), so the button stacks clear of
|
|
101
|
+
// the first and the open panel stops short of the second. Measured, since the engine's
|
|
102
|
+
// controls change size (the credits switch between compact and full; MapTiler's logo).
|
|
103
|
+
let nearExtent = $state(0);
|
|
104
|
+
let farExtent = $state(0);
|
|
105
|
+
|
|
106
|
+
function extent(corner: ListPosition): number {
|
|
107
|
+
const mapBox = root.getBoundingClientRect();
|
|
108
|
+
const top = corner.startsWith("top");
|
|
109
|
+
const sources: Element[] = [];
|
|
110
|
+
const engine = glContainer.querySelector(`.maplibregl-ctrl-${corner}`);
|
|
111
|
+
if (engine) sources.push(engine);
|
|
112
|
+
if (corner === "top-right" && controls === "mapsdk") {
|
|
113
|
+
const zoom = root.querySelector(".mapsdk-zoom");
|
|
114
|
+
if (zoom) sources.push(zoom);
|
|
115
|
+
}
|
|
116
|
+
let reach = 0;
|
|
117
|
+
for (const el of sources) {
|
|
118
|
+
const r = el.getBoundingClientRect();
|
|
119
|
+
if (r.height === 0) continue;
|
|
120
|
+
reach = Math.max(reach, top ? r.bottom - mapBox.top : mapBox.bottom - r.top);
|
|
121
|
+
}
|
|
122
|
+
return Math.max(0, reach);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$effect(() => {
|
|
126
|
+
const s = sides;
|
|
127
|
+
void projector;
|
|
128
|
+
void controls;
|
|
129
|
+
if (!s) return;
|
|
130
|
+
const measure = () => {
|
|
131
|
+
nearExtent = extent(`${s.vertical}-${s.horizontal}`);
|
|
132
|
+
farExtent = extent(s.far);
|
|
133
|
+
};
|
|
134
|
+
const observer = new ResizeObserver(measure);
|
|
135
|
+
observer.observe(root);
|
|
136
|
+
const corners = [`${s.vertical}-${s.horizontal}`, s.far].map((c) =>
|
|
137
|
+
glContainer.querySelector(`.maplibregl-ctrl-${c}`)
|
|
138
|
+
);
|
|
139
|
+
for (const el of [...corners, root.querySelector(".mapsdk-zoom")]) {
|
|
140
|
+
if (el) observer.observe(el);
|
|
141
|
+
}
|
|
142
|
+
measure();
|
|
143
|
+
return () => observer.disconnect();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const inset = (reach: number) => (reach > 0 ? `calc(${reach}px + 0.5rem)` : undefined);
|
|
147
|
+
|
|
148
|
+
function onListKeydown(e: KeyboardEvent) {
|
|
149
|
+
if (e.key !== "Escape") return;
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
listOpen = false;
|
|
152
|
+
listButton?.focus();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The map is fixed for the component's life, so its first value is the one to provide.
|
|
156
|
+
// svelte-ignore state_referenced_locally
|
|
157
|
+
setMapContext(map);
|
|
158
|
+
// svelte-ignore state_referenced_locally
|
|
159
|
+
const firstMap = map;
|
|
160
|
+
if (DEV) {
|
|
161
|
+
$effect(() => {
|
|
162
|
+
if (map !== firstMap) {
|
|
163
|
+
console.warn(
|
|
164
|
+
"map-sdk: MapView's `map` prop changed after mount. It is fixed for the component's life (overlays keep the first map); wrap MapView in {#key map} to show another map."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dataCredits = createDataCredits(firstMap);
|
|
171
|
+
// The dataset credits go in the engine's attribution control; it is replaced when they change.
|
|
172
|
+
$effect(() => {
|
|
173
|
+
const credits = dataCredits.list;
|
|
174
|
+
if (projector) untrack(() => setDataCredits(projector!, credits));
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let root: HTMLDivElement;
|
|
178
|
+
let glContainer: HTMLDivElement;
|
|
179
|
+
// The live GL map and a counter bumped on every camera move or resize: the tooltip's `project` reads
|
|
180
|
+
// both, so it re-runs (and follows its feature) as the map moves.
|
|
181
|
+
let projector = $state.raw<GlMapLike | null>(null);
|
|
182
|
+
let moves = $state(0);
|
|
183
|
+
let width = $state(0);
|
|
184
|
+
let height = $state(0);
|
|
185
|
+
|
|
186
|
+
function project(lngLat: [number, number]) {
|
|
187
|
+
void moves;
|
|
188
|
+
return projector ? projector.project(lngLat) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
$effect(() => {
|
|
192
|
+
if (!active) return;
|
|
193
|
+
const ctx = map;
|
|
194
|
+
const engine = controls === "engine";
|
|
195
|
+
const gl = untrack(() => {
|
|
196
|
+
const gl = createGlMap(glContainer, ctx, engine);
|
|
197
|
+
try {
|
|
198
|
+
ctx.attach(gl);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
gl.remove();
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
return gl;
|
|
204
|
+
});
|
|
205
|
+
const onMove = () => moves++;
|
|
206
|
+
gl.on("move", onMove);
|
|
207
|
+
gl.on("resize", onMove);
|
|
208
|
+
projector = gl;
|
|
209
|
+
// Ready is the first idle after each style load (the first style, and every basemap
|
|
210
|
+
// switch). A wait still pending when the style is replaced or the map detached is dropped,
|
|
211
|
+
// so a stale idle never marks the context ready.
|
|
212
|
+
let stopWaiting: (() => void) | null = null;
|
|
213
|
+
const waitForIdle = () => {
|
|
214
|
+
stopWaiting?.();
|
|
215
|
+
const onIdle = () => {
|
|
216
|
+
stopWaiting?.();
|
|
217
|
+
if (ctx.gl === gl) ctx.markReady();
|
|
218
|
+
};
|
|
219
|
+
stopWaiting = () => {
|
|
220
|
+
gl.off("idle", onIdle);
|
|
221
|
+
stopWaiting = null;
|
|
222
|
+
};
|
|
223
|
+
gl.on("idle", onIdle);
|
|
224
|
+
};
|
|
225
|
+
gl.on("style.load", waitForIdle);
|
|
226
|
+
waitForIdle();
|
|
227
|
+
// Plugins bake theme colors into paint and images, so they set up again on a change.
|
|
228
|
+
const stopTheme = watchTheme(root, () => ctx.themeChanged());
|
|
229
|
+
return () => {
|
|
230
|
+
stopTheme();
|
|
231
|
+
gl.off("style.load", waitForIdle);
|
|
232
|
+
stopWaiting?.();
|
|
233
|
+
projector = null;
|
|
234
|
+
gl.off("move", onMove);
|
|
235
|
+
gl.off("resize", onMove);
|
|
236
|
+
try {
|
|
237
|
+
ctx.detach();
|
|
238
|
+
} finally {
|
|
239
|
+
gl.remove();
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
});
|
|
243
|
+
</script>
|
|
244
|
+
|
|
245
|
+
<div
|
|
246
|
+
bind:this={root}
|
|
247
|
+
bind:clientWidth={width}
|
|
248
|
+
bind:clientHeight={height}
|
|
249
|
+
class={["mapsdk-map mapsdk-focus-outline", className]}
|
|
250
|
+
role="region"
|
|
251
|
+
aria-label={label ?? map.messages("mapsdk_map_label")}
|
|
252
|
+
data-mapsdk-map={map.uid}
|
|
253
|
+
>
|
|
254
|
+
<div class="mapsdk-gl" bind:this={glContainer}></div>
|
|
255
|
+
<MapTooltip {map} {project} {width} {height} mode={tooltip} gl={projector} />
|
|
256
|
+
{#if list && sides}
|
|
257
|
+
<div
|
|
258
|
+
class="mapsdk-list-panel"
|
|
259
|
+
data-position={list.position}
|
|
260
|
+
data-reveal={list.reveal}
|
|
261
|
+
data-open={listOpen}
|
|
262
|
+
style:--mapsdk-list-near={inset(nearExtent)}
|
|
263
|
+
style:--mapsdk-list-far={inset(farExtent)}
|
|
264
|
+
>
|
|
265
|
+
<button
|
|
266
|
+
bind:this={listButton}
|
|
267
|
+
type="button"
|
|
268
|
+
class="mapsdk-list-button mapsdk-focus mapsdk-motion text-sm"
|
|
269
|
+
aria-label={map.messages("mapsdk_list_button_label")}
|
|
270
|
+
aria-expanded={listOpen}
|
|
271
|
+
aria-controls={listOpen ? listId : undefined}
|
|
272
|
+
onclick={() => (listOpen = !listOpen)}
|
|
273
|
+
>
|
|
274
|
+
{map.messages("mapsdk_list_button")}
|
|
275
|
+
</button>
|
|
276
|
+
<!-- Escape anywhere in the panel closes it. -->
|
|
277
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
278
|
+
{#if listOpen}
|
|
279
|
+
<div bind:this={listBox} bind:clientWidth={listWidth} onkeydown={onListKeydown}>
|
|
280
|
+
<FeatureList {map} id={listId} bind:sortBy={listSort} />
|
|
281
|
+
</div>
|
|
282
|
+
{/if}
|
|
283
|
+
</div>
|
|
284
|
+
{/if}
|
|
285
|
+
<!-- After the list, so Tab reaches the List button first. -->
|
|
286
|
+
{#if selectionBar}
|
|
287
|
+
<SelectionBar {map} clearRight={list?.position === "top-right" && controls === "none"} />
|
|
288
|
+
{/if}
|
|
289
|
+
<ViewSummary {map} />
|
|
290
|
+
{#if controls === "mapsdk"}
|
|
291
|
+
<MapControls {map} gl={projector} />
|
|
292
|
+
{/if}
|
|
293
|
+
{@render overlays?.()}
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<style>
|
|
297
|
+
.mapsdk-map {
|
|
298
|
+
position: relative;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* The region takes focus only from script (the selection bar's Clear), never from Tab: the
|
|
302
|
+
outline alone, from .mapsdk-focus-outline. */
|
|
303
|
+
.mapsdk-gl {
|
|
304
|
+
position: absolute;
|
|
305
|
+
inset: 0;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* In its corner: 0.75rem from the edges, or 0.5rem past the controls already there. The open
|
|
309
|
+
panel also stops 0.5rem short of the controls at the far end of the side. */
|
|
310
|
+
.mapsdk-list-panel {
|
|
311
|
+
position: absolute;
|
|
312
|
+
z-index: 2;
|
|
313
|
+
display: flex;
|
|
314
|
+
flex-direction: column;
|
|
315
|
+
align-items: flex-start;
|
|
316
|
+
gap: 0.5rem;
|
|
317
|
+
max-height: calc(100% - var(--mapsdk-list-near, 0.75rem) - var(--mapsdk-list-far, 0.75rem));
|
|
318
|
+
pointer-events: none;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
.mapsdk-list-panel[data-position^="top"] {
|
|
322
|
+
top: var(--mapsdk-list-near, 0.75rem);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.mapsdk-list-panel[data-position^="bottom"] {
|
|
326
|
+
bottom: var(--mapsdk-list-near, 0.75rem);
|
|
327
|
+
flex-direction: column-reverse;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.mapsdk-list-panel[data-position$="left"] {
|
|
331
|
+
left: 0.75rem;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.mapsdk-list-panel[data-position$="right"] {
|
|
335
|
+
right: 0.75rem;
|
|
336
|
+
align-items: flex-end;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/* reveal: "focus": hidden like a skip link (still in the Tab order and the accessibility tree)
|
|
340
|
+
until it or its panel has focus, and shown the whole time the panel is open. No transition. */
|
|
341
|
+
.mapsdk-list-panel[data-reveal="focus"][data-open="false"]:not(:focus-within)
|
|
342
|
+
.mapsdk-list-button {
|
|
343
|
+
position: absolute;
|
|
344
|
+
width: 1px;
|
|
345
|
+
height: 1px;
|
|
346
|
+
padding: 0;
|
|
347
|
+
border: 0;
|
|
348
|
+
overflow: hidden;
|
|
349
|
+
clip-path: inset(50%);
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
.mapsdk-list-panel > div {
|
|
354
|
+
display: flex;
|
|
355
|
+
min-height: 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.mapsdk-list-button {
|
|
359
|
+
padding: 0.375rem 0.75rem;
|
|
360
|
+
border: 1px solid var(--card-border, var(--border));
|
|
361
|
+
border-radius: var(--radius-control, var(--radius));
|
|
362
|
+
background: var(--background);
|
|
363
|
+
color: var(--foreground);
|
|
364
|
+
box-shadow: var(--shadow-card, none);
|
|
365
|
+
font-family: inherit;
|
|
366
|
+
cursor: pointer;
|
|
367
|
+
pointer-events: auto;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.mapsdk-list-button:hover {
|
|
371
|
+
background: var(--accent, var(--muted));
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import "@maptiler/sdk/dist/maptiler-sdk.css";
|
|
2
|
+
import "../ui/controls.css";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
5
|
+
import { type FeatureListOptions } from "./list-placement.js";
|
|
6
|
+
interface Props {
|
|
7
|
+
/** The map to show. Fixed for the component's life. */
|
|
8
|
+
map: MapContext;
|
|
9
|
+
/** `false` releases the WebGL context; `true` rebuilds it with the same camera. */
|
|
10
|
+
active?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
/** The region's accessible name. Defaults to the `mapsdk_map_label` message. */
|
|
13
|
+
label?: string;
|
|
14
|
+
/** On-screen pieces drawn over the map; they get the map through `getMapContext()`. */
|
|
15
|
+
overlays?: Snippet;
|
|
16
|
+
/**
|
|
17
|
+
* The List button and its feature list panel: `false` leaves them out; `true` (default) or
|
|
18
|
+
* options place them. `position` picks the corner (default `"top-left"`); the button stacks
|
|
19
|
+
* below (or, in a bottom corner, above) the controls in that corner, and the open panel stops
|
|
20
|
+
* short of the controls and credits at the far end of its side. `reveal: "focus"` shows the
|
|
21
|
+
* button only while it or its open panel has keyboard focus (and the whole time the panel is
|
|
22
|
+
* open); otherwise it's visually hidden but stays in the Tab order, like a skip link.
|
|
23
|
+
*/
|
|
24
|
+
featureList?: boolean | FeatureListOptions;
|
|
25
|
+
/** `false` leaves out the selection bar. */
|
|
26
|
+
selectionBar?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The zoom controls: `"mapsdk"` (default) draws map-sdk's zoom buttons, `"engine"` the
|
|
29
|
+
* MapTiler SDK's default zoom/compass and locate-me controls, `"none"` none. The credits
|
|
30
|
+
* always show. Choose it up front: changing it after mount recreates the map (the engine
|
|
31
|
+
* controls are part of how the map is built), like `active`.
|
|
32
|
+
*/
|
|
33
|
+
controls?: "mapsdk" | "engine" | "none";
|
|
34
|
+
/**
|
|
35
|
+
* `"pinned"` (default) anchors the tooltip to its feature. `"pointer"` shows it at the
|
|
36
|
+
* pointer, like sjvair.com's map, and hides it on zoom until the pointer moves again.
|
|
37
|
+
* Keyboard focus from the feature list always shows it pinned.
|
|
38
|
+
*/
|
|
39
|
+
tooltip?: "pinned" | "pointer";
|
|
40
|
+
}
|
|
41
|
+
declare const MapView: import("svelte").Component<Props, {}, "">;
|
|
42
|
+
type MapView = ReturnType<typeof MapView>;
|
|
43
|
+
export default MapView;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { BasemapConfig } from "../core/basemap.svelte.js";
|
|
2
|
+
import type { MapContext } from "../core/create-map.svelte.js";
|
|
3
|
+
import type { GlMapLike } from "../core/gl.js";
|
|
4
|
+
/**
|
|
5
|
+
* Calls `onChange` when the host's theme changes the tokens map colors are read from: on a
|
|
6
|
+
* `prefers-color-scheme` change, or a `data-theme`, `class`, or `style` change on `<html>`.
|
|
7
|
+
* Changes that leave the tokens on `container` as they were (e.g. a scroll lock's inline style)
|
|
8
|
+
* are ignored. Returns the stop function.
|
|
9
|
+
*/
|
|
10
|
+
export declare function watchTheme(container: Element, onChange: () => void): () => void;
|
|
11
|
+
/** Today's live map: MapTiler Streets v4 and its variants, without the busy POI label layers. */
|
|
12
|
+
export declare function defaultBasemaps(): BasemapConfig;
|
|
13
|
+
/**
|
|
14
|
+
* A `transformRequest` that adds `key` to MapTiler API requests that have none. The SDK's own
|
|
15
|
+
* transform runs after it and only adds its global `config.apiKey` when `key` is missing, so
|
|
16
|
+
* each map's requests carry its own key, and two maps with different keys don't clash.
|
|
17
|
+
*/
|
|
18
|
+
export declare function maptilerKeyTransform(key: string): (url: string) => {
|
|
19
|
+
url: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Creates the MapTiler map for `ctx` in `container`, at `ctx`'s camera with its current basemap.
|
|
23
|
+
* Reads `ctx` untracked, so calling this in an effect doesn't make it depend on the camera or
|
|
24
|
+
* basemap. With `engineControls`, the MapTiler SDK adds its default zoom/compass and locate-me
|
|
25
|
+
* controls at the top right; they go with the map.
|
|
26
|
+
* Records the camera and bounds once the map loads and after every move.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createGlMap(container: HTMLElement, ctx: MapContext, engineControls?: boolean): GlMapLike;
|
|
29
|
+
/**
|
|
30
|
+
* Shows `credits` (plain text, one per dataset) in the map's attribution control, after the
|
|
31
|
+
* style's own credits. The control is replaced, not edited: MapLibre reads its custom
|
|
32
|
+
* attribution once. Does nothing for a map that isn't one of `createGlMap`'s or has no change.
|
|
33
|
+
*/
|
|
34
|
+
export declare function setDataCredits(gl: GlMapLike, credits: string[]): void;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import { AttributionControl, AttributionControlMLGL, Map as MaptilerMap, MapStyle, config as maptilerConfig } from "@maptiler/sdk";
|
|
3
|
+
import { themeSignature } from "../plugins/style.js";
|
|
4
|
+
/**
|
|
5
|
+
* Calls `onChange` when the host's theme changes the tokens map colors are read from: on a
|
|
6
|
+
* `prefers-color-scheme` change, or a `data-theme`, `class`, or `style` change on `<html>`.
|
|
7
|
+
* Changes that leave the tokens on `container` as they were (e.g. a scroll lock's inline style)
|
|
8
|
+
* are ignored. Returns the stop function.
|
|
9
|
+
*/
|
|
10
|
+
export function watchTheme(container, onChange) {
|
|
11
|
+
let last = themeSignature(container);
|
|
12
|
+
const check = () => {
|
|
13
|
+
const next = themeSignature(container);
|
|
14
|
+
if (next === last)
|
|
15
|
+
return;
|
|
16
|
+
last = next;
|
|
17
|
+
onChange();
|
|
18
|
+
};
|
|
19
|
+
const media = matchMedia("(prefers-color-scheme: dark)");
|
|
20
|
+
media.addEventListener("change", check);
|
|
21
|
+
const observer = new MutationObserver(check);
|
|
22
|
+
observer.observe(document.documentElement, {
|
|
23
|
+
attributes: true,
|
|
24
|
+
attributeFilter: ["data-theme", "class", "style"]
|
|
25
|
+
});
|
|
26
|
+
return () => {
|
|
27
|
+
media.removeEventListener("change", check);
|
|
28
|
+
observer.disconnect();
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Today's live map: MapTiler Streets v4 and its variants, without the busy POI label layers. */
|
|
32
|
+
export function defaultBasemaps() {
|
|
33
|
+
const streets = MapStyle.STREETS_V4;
|
|
34
|
+
return {
|
|
35
|
+
// Style URLs, not variant objects: the basemap fallback matches a failed style by its URL.
|
|
36
|
+
styles: streets.getVariants().map((v) => ({ id: v.getId(), style: v.getExpandedStyleURL() })),
|
|
37
|
+
defaultId: streets.getDefaultVariant().getId(),
|
|
38
|
+
excludedLayers: ["Sport", "Tourism", "Culture", "Shopping", "Food", "Transport"]
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A `transformRequest` that adds `key` to MapTiler API requests that have none. The SDK's own
|
|
43
|
+
* transform runs after it and only adds its global `config.apiKey` when `key` is missing, so
|
|
44
|
+
* each map's requests carry its own key, and two maps with different keys don't clash.
|
|
45
|
+
*/
|
|
46
|
+
export function maptilerKeyTransform(key) {
|
|
47
|
+
return (url) => {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = new URL(url);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { url };
|
|
54
|
+
}
|
|
55
|
+
if (parsed.host !== maptilerConfig.apiHost || parsed.searchParams.has("key"))
|
|
56
|
+
return { url };
|
|
57
|
+
parsed.searchParams.append("key", key);
|
|
58
|
+
return { url: parsed.href };
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/** MapLibre's own `setStyle`, which MapTiler's `Map` overrides. */
|
|
62
|
+
const clearStyle = Object.getPrototypeOf(MaptilerMap.prototype)
|
|
63
|
+
.setStyle;
|
|
64
|
+
/**
|
|
65
|
+
* MapLibre's `remove()` clears the style with `setStyle(null)`. MapTiler's `setStyle` turns
|
|
66
|
+
* `null` into its default Streets style when no style has loaded yet, so removing a map that is
|
|
67
|
+
* still loading (hiding it quickly) would request that style for nothing; and once one has
|
|
68
|
+
* loaded it keeps it, so the style's requests and worker sources outlive the map. `null` goes to
|
|
69
|
+
* MapLibre's `setStyle` instead (nothing else here passes `null`), and MapTiler's pending
|
|
70
|
+
* space and halo setup, which would read the cleared style, is marked done.
|
|
71
|
+
*/
|
|
72
|
+
class GlMap extends MaptilerMap {
|
|
73
|
+
/** Notes the SDK's own attribution control, which it adds while constructing the map. */
|
|
74
|
+
addControl(...args) {
|
|
75
|
+
const [control] = args;
|
|
76
|
+
if (control instanceof AttributionControlMLGL)
|
|
77
|
+
this.attribution = { control, credits: "[]" };
|
|
78
|
+
return super.addControl(...args);
|
|
79
|
+
}
|
|
80
|
+
setStyle(...args) {
|
|
81
|
+
if (args[0] !== null)
|
|
82
|
+
return super.setStyle(...args);
|
|
83
|
+
const pending = this
|
|
84
|
+
.spaceboxLoadingState;
|
|
85
|
+
if (pending)
|
|
86
|
+
pending.styleLoadedCallbackFired = true;
|
|
87
|
+
return clearStyle.apply(this, args);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* MapTiler's telemetry timer calls this 2 s after the map is created, even when it has been
|
|
91
|
+
* removed since (a map hidden quickly), and a removed map has no style to ask.
|
|
92
|
+
*/
|
|
93
|
+
isGlobeProjection() {
|
|
94
|
+
if (!this.style)
|
|
95
|
+
return false;
|
|
96
|
+
return super.isGlobeProjection();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** The style of `ctx`'s current basemap. Throws when there is none, rather than letting MapTiler fall back to its own default style. */
|
|
100
|
+
function styleOf(ctx) {
|
|
101
|
+
const id = ctx.basemap.current;
|
|
102
|
+
const entry = ctx.config.basemaps?.styles.find((s) => s.id === id);
|
|
103
|
+
if (!entry)
|
|
104
|
+
throw new Error(`map-sdk: no basemap style for "${id}"`);
|
|
105
|
+
return entry.style;
|
|
106
|
+
}
|
|
107
|
+
function recordView(gl, ctx) {
|
|
108
|
+
const center = gl.getCenter();
|
|
109
|
+
const bounds = gl.getBounds();
|
|
110
|
+
ctx.updateView({
|
|
111
|
+
center: [center.lng, center.lat],
|
|
112
|
+
zoom: gl.getZoom(),
|
|
113
|
+
bearing: gl.getBearing(),
|
|
114
|
+
pitch: gl.getPitch()
|
|
115
|
+
}, {
|
|
116
|
+
west: bounds.getWest(),
|
|
117
|
+
south: bounds.getSouth(),
|
|
118
|
+
east: bounds.getEast(),
|
|
119
|
+
north: bounds.getNorth()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Creates the MapTiler map for `ctx` in `container`, at `ctx`'s camera with its current basemap.
|
|
124
|
+
* Reads `ctx` untracked, so calling this in an effect doesn't make it depend on the camera or
|
|
125
|
+
* basemap. With `engineControls`, the MapTiler SDK adds its default zoom/compass and locate-me
|
|
126
|
+
* controls at the top right; they go with the map.
|
|
127
|
+
* Records the camera and bounds once the map loads and after every move.
|
|
128
|
+
*/
|
|
129
|
+
export function createGlMap(container, ctx, engineControls = false) {
|
|
130
|
+
const { camera, style } = untrack(() => ({ camera: ctx.camera, style: styleOf(ctx) }));
|
|
131
|
+
const gl = new GlMap({
|
|
132
|
+
container,
|
|
133
|
+
style,
|
|
134
|
+
center: camera.center,
|
|
135
|
+
zoom: camera.zoom,
|
|
136
|
+
bearing: camera.bearing,
|
|
137
|
+
pitch: camera.pitch,
|
|
138
|
+
// The key goes on each request, not on the SDK's global config (the `apiKey` option
|
|
139
|
+
// sets that global).
|
|
140
|
+
transformRequest: maptilerKeyTransform(ctx.config.maptilerKey),
|
|
141
|
+
cooperativeGestures: ctx.config.cooperativeGestures ?? false,
|
|
142
|
+
keyboard: true,
|
|
143
|
+
// The engine's attribution control (and MapTiler's logo, when the key's plan requires it),
|
|
144
|
+
// at its automatic size: full on wide maps, collapsed on narrow ones. The SDK's default is
|
|
145
|
+
// always full, so ask for its automatic mode.
|
|
146
|
+
attributionControl: { compact: "auto" },
|
|
147
|
+
// `controls: "engine"` gets the SDK's own zoom/compass and locate-me controls, with its
|
|
148
|
+
// defaults (as monitor-map has today); otherwise MapView draws the controls.
|
|
149
|
+
navigationControl: engineControls ? "top-right" : false,
|
|
150
|
+
geolocateControl: engineControls ? "top-right" : false,
|
|
151
|
+
logSDKVersion: false,
|
|
152
|
+
// Only when set, so a map without them gets today's constructor options.
|
|
153
|
+
...(ctx.config.projection ? { projection: ctx.config.projection } : {}),
|
|
154
|
+
...(ctx.config.space ? { space: ctx.config.space } : {})
|
|
155
|
+
});
|
|
156
|
+
gl.on("load", () => recordView(gl, ctx));
|
|
157
|
+
gl.on("moveend", () => recordView(gl, ctx));
|
|
158
|
+
return gl;
|
|
159
|
+
}
|
|
160
|
+
const escapeHtml = (text) => text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
161
|
+
/**
|
|
162
|
+
* Shows `credits` (plain text, one per dataset) in the map's attribution control, after the
|
|
163
|
+
* style's own credits. The control is replaced, not edited: MapLibre reads its custom
|
|
164
|
+
* attribution once. Does nothing for a map that isn't one of `createGlMap`'s or has no change.
|
|
165
|
+
*/
|
|
166
|
+
export function setDataCredits(gl, credits) {
|
|
167
|
+
const map = gl;
|
|
168
|
+
const current = map.attribution;
|
|
169
|
+
if (!current || !(map instanceof GlMap))
|
|
170
|
+
return;
|
|
171
|
+
const key = JSON.stringify(credits);
|
|
172
|
+
if (key === current.credits)
|
|
173
|
+
return;
|
|
174
|
+
map.removeControl(current.control);
|
|
175
|
+
map.attribution = null;
|
|
176
|
+
const next = new AttributionControl({
|
|
177
|
+
compact: undefined,
|
|
178
|
+
customAttribution: credits.map(escapeHtml)
|
|
179
|
+
});
|
|
180
|
+
map.addControl(next);
|
|
181
|
+
map.attribution = { control: next, credits: key };
|
|
182
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A corner of the map. */
|
|
2
|
+
export type ListPosition = "top-left" | "top-right" | "bottom-left" | "bottom-right";
|
|
3
|
+
/** `MapView`'s `featureList` options. */
|
|
4
|
+
export interface FeatureListOptions {
|
|
5
|
+
/** The corner the List button sits in; its panel opens from there. Default `"top-left"`. */
|
|
6
|
+
position?: ListPosition;
|
|
7
|
+
/**
|
|
8
|
+
* `"always"` (default) shows the button. `"focus"` shows it only while it, or its open panel,
|
|
9
|
+
* has keyboard focus, and the whole time the panel is open; otherwise it's visually hidden but
|
|
10
|
+
* stays in the Tab order and the accessibility tree, like a skip link.
|
|
11
|
+
*/
|
|
12
|
+
reveal?: "always" | "focus";
|
|
13
|
+
}
|
|
14
|
+
/** The `featureList` prop with its defaults filled in, or `null` when the list is off. */
|
|
15
|
+
export declare function resolveFeatureList(value: boolean | FeatureListOptions | undefined): Required<FeatureListOptions> | null;
|
|
16
|
+
/** A corner's edges, and the other corner on the same side (where the panel must stop). */
|
|
17
|
+
export declare function cornerSides(position: ListPosition): {
|
|
18
|
+
vertical: "top" | "bottom";
|
|
19
|
+
horizontal: "left" | "right";
|
|
20
|
+
far: ListPosition;
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** The `featureList` prop with its defaults filled in, or `null` when the list is off. */
|
|
2
|
+
export function resolveFeatureList(value) {
|
|
3
|
+
if (value === false)
|
|
4
|
+
return null;
|
|
5
|
+
const options = value === true || value === undefined ? {} : value;
|
|
6
|
+
return { position: options.position ?? "top-left", reveal: options.reveal ?? "always" };
|
|
7
|
+
}
|
|
8
|
+
/** A corner's edges, and the other corner on the same side (where the panel must stop). */
|
|
9
|
+
export function cornerSides(position) {
|
|
10
|
+
const [vertical, horizontal] = position.split("-");
|
|
11
|
+
const far = `${vertical === "top" ? "bottom" : "top"}-${horizontal}`;
|
|
12
|
+
return { vertical, horizontal, far };
|
|
13
|
+
}
|