@sjvair/map-sdk 0.0.0-stage → 0.1.0-beta.10
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 +713 -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 +274 -0
- package/dist/core/create-map.svelte.js +591 -0
- package/dist/core/gl.d.ts +97 -0
- package/dist/core/gl.js +1 -0
- package/dist/core/interactions.svelte.d.ts +50 -0
- package/dist/core/interactions.svelte.js +330 -0
- package/dist/core/plugin-host.svelte.d.ts +71 -0
- package/dist/core/plugin-host.svelte.js +445 -0
- package/dist/core/plugin.d.ts +190 -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/core/shape-view.d.ts +27 -0
- package/dist/core/shape-view.js +323 -0
- package/dist/data/format.d.ts +10 -0
- package/dist/data/format.js +37 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.js +9 -0
- package/dist/data/messages/en.json +61 -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 +13 -0
- package/dist/data/selection.js +52 -0
- package/dist/data/static-source.d.ts +4 -0
- package/dist/data/static-source.js +3 -0
- package/dist/data/types.d.ts +74 -0
- package/dist/data/types.js +1 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +42 -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 +105 -0
- package/dist/plugins/points.svelte.js +502 -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 +34 -0
- package/dist/plugins/shapes.svelte.js +325 -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/FireTooltip.svelte +70 -0
- package/dist/presets/FireTooltip.svelte.d.ts +13 -0
- package/dist/presets/MonitorClusterTooltip.svelte +168 -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/cluster-area.d.ts +13 -0
- package/dist/presets/cluster-area.js +34 -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 +30 -0
- package/dist/presets/hms-fire.js +86 -0
- package/dist/presets/hms-smoke.d.ts +20 -0
- package/dist/presets/hms-smoke.js +22 -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 +38 -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 +214 -0
- package/dist/ui/Legend.svelte.d.ts +22 -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 +397 -0
- package/dist/ui/MapTooltip.svelte.d.ts +37 -0
- package/dist/ui/PickNotice.svelte +56 -0
- package/dist/ui/PickNotice.svelte.d.ts +7 -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/card.d.ts +29 -0
- package/dist/ui/card.js +1 -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 +46 -0
- package/dist/ui/feature-list.js +115 -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 +8 -0
- package/dist/ui/value-format.js +20 -0
- package/dist/ui/view-summary.d.ts +11 -0
- package/dist/ui/view-summary.js +33 -0
- package/dist/view/MapView.svelte +443 -0
- package/dist/view/MapView.svelte.d.ts +51 -0
- package/dist/view/create-gl-map.d.ts +34 -0
- package/dist/view/create-gl-map.js +186 -0
- package/dist/view/list-placement.d.ts +21 -0
- package/dist/view/list-placement.js +13 -0
- package/package.json +104 -5
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
/** Setup runs on whichever of these fires first. */
|
|
3
|
+
const STYLE_READY_EVENTS = ["style.load", "idle"];
|
|
4
|
+
/**
|
|
5
|
+
* Marks a plugin object with the host it is set up on, while it is. A plugin object keeps
|
|
6
|
+
* per-map state (its source and layer ids, its features), so it belongs to one map at a time.
|
|
7
|
+
* A property on the plugin, not a registry: nothing here is shared between maps.
|
|
8
|
+
*/
|
|
9
|
+
const OWNER = Symbol("map-sdk plugin host");
|
|
10
|
+
function ownedElsewhere(plugin, host) {
|
|
11
|
+
const owner = plugin[OWNER];
|
|
12
|
+
return owner !== undefined && owner !== host;
|
|
13
|
+
}
|
|
14
|
+
function ownershipError(plugin) {
|
|
15
|
+
return new Error(`map-sdk: plugin "${plugin.id}" is already set up on another map; create one plugin per map`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Runs plugins against a GL map. Plugins are set up only when the map's style is loaded, and
|
|
19
|
+
* everything they add through their `PluginContext` is removed on teardown, whatever their own
|
|
20
|
+
* teardown does. The plugin list outlives any one GL map.
|
|
21
|
+
*
|
|
22
|
+
* Setup never runs inside the caller's code. When `add` or `attach` would set plugins up, they
|
|
23
|
+
* are queued and set up in one microtask, where no effect is active: Svelte cuts loose the
|
|
24
|
+
* `$effect.root`s created inside an effect when that effect re-runs, and their effects can stop
|
|
25
|
+
* (see docs/superpowers/specs/2026-11-07-plugin-setup-outside-effects-design.md). Setup on a
|
|
26
|
+
* style load or `restyle` comes from map events, not host code, and stays synchronous.
|
|
27
|
+
*
|
|
28
|
+
* Plugins draw in list order, later on top: each plugin's layers stay together, below the first
|
|
29
|
+
* layer of the next plugin in the list that has any.
|
|
30
|
+
*/
|
|
31
|
+
export class PluginHost {
|
|
32
|
+
#opts;
|
|
33
|
+
#plugins = $state.raw([]);
|
|
34
|
+
// Bookkeeping only; nothing reads it reactively.
|
|
35
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
|
36
|
+
#running = new Map();
|
|
37
|
+
#gl = null;
|
|
38
|
+
/** True once the current gl's style has loaded and plugins are set up on it. */
|
|
39
|
+
#live = false;
|
|
40
|
+
/**
|
|
41
|
+
* The pending one-shot listener on `#gl` for `style.load` and `idle`, if waiting. `idle` covers
|
|
42
|
+
* a style that already loaded while `isStyleLoaded()` is false because sources are loading.
|
|
43
|
+
*/
|
|
44
|
+
#onStyleLoad = null;
|
|
45
|
+
/** True from scheduling a queued setup run until it starts. */
|
|
46
|
+
#setupQueued = false;
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
this.#opts = opts;
|
|
49
|
+
}
|
|
50
|
+
/** In draw order, bottom first. Reactive. */
|
|
51
|
+
get plugins() {
|
|
52
|
+
return this.#plugins;
|
|
53
|
+
}
|
|
54
|
+
/** True while a setup run is queued and has not started. Not reactive. */
|
|
55
|
+
get pending() {
|
|
56
|
+
return this.#setupQueued;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The host's own reads are untracked, so calling `add`, `attach`, etc. from inside an effect
|
|
60
|
+
* doesn't make that effect depend on the plugin list.
|
|
61
|
+
*/
|
|
62
|
+
get #list() {
|
|
63
|
+
return untrack(() => this.#plugins);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Adds `plugin` on top, or just below the plugin `before` names. It is in `plugins` at once;
|
|
67
|
+
* on a live map its setup runs in a microtask. Throws when the same plugin object is set up on
|
|
68
|
+
* another map at the moment (one plugin object per map).
|
|
69
|
+
*/
|
|
70
|
+
add(plugin, before) {
|
|
71
|
+
if (this.#list.some((p) => p.id === plugin.id)) {
|
|
72
|
+
throw new Error(`map-sdk: a plugin with id "${plugin.id}" is already added`);
|
|
73
|
+
}
|
|
74
|
+
if (ownedElsewhere(plugin, this))
|
|
75
|
+
throw ownershipError(plugin);
|
|
76
|
+
this.#plugins = this.#inserted(this.#list, plugin, before);
|
|
77
|
+
if (this.#live)
|
|
78
|
+
this.#queueSetup();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Moves a plugin to the top, or just below the plugin `before` names, with its layers. Does
|
|
82
|
+
* nothing for an unknown `id` or when `before` is `id`.
|
|
83
|
+
*/
|
|
84
|
+
move(id, before) {
|
|
85
|
+
const plugin = this.#list.find((p) => p.id === id);
|
|
86
|
+
if (!plugin || before === id)
|
|
87
|
+
return;
|
|
88
|
+
this.#plugins = this.#inserted(this.#list.filter((p) => p !== plugin), plugin, before);
|
|
89
|
+
const running = this.#running.get(id);
|
|
90
|
+
if (!running || running.gl !== this.#gl)
|
|
91
|
+
return;
|
|
92
|
+
const { gl } = running;
|
|
93
|
+
const beforeId = this.#layerAbove(id);
|
|
94
|
+
for (const layerId of running.layers) {
|
|
95
|
+
try {
|
|
96
|
+
if (gl.getLayer(layerId))
|
|
97
|
+
gl.moveLayer(layerId, beforeId);
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
console.error(`map-sdk: plugin "${id}" moving layer ${layerId} failed`, error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** `list` with `plugin` inserted before `before` (at the end without it). */
|
|
105
|
+
#inserted(list, plugin, before) {
|
|
106
|
+
if (before === undefined)
|
|
107
|
+
return [...list, plugin];
|
|
108
|
+
const at = list.findIndex((p) => p.id === before);
|
|
109
|
+
if (at < 0)
|
|
110
|
+
throw new Error(`map-sdk: no plugin with id "${before}"`);
|
|
111
|
+
return [...list.slice(0, at), plugin, ...list.slice(at)];
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The bottom layer of the plugins drawn above `pluginId` on the current gl: where its layers
|
|
115
|
+
* go. `undefined` (the top) when none above has a layer.
|
|
116
|
+
*/
|
|
117
|
+
#layerAbove(pluginId) {
|
|
118
|
+
const gl = this.#gl;
|
|
119
|
+
if (!gl)
|
|
120
|
+
return undefined;
|
|
121
|
+
const list = this.#list;
|
|
122
|
+
for (const plugin of list.slice(list.findIndex((p) => p.id === pluginId) + 1)) {
|
|
123
|
+
const running = this.#running.get(plugin.id);
|
|
124
|
+
if (running?.gl !== gl)
|
|
125
|
+
continue;
|
|
126
|
+
const layerId = running.layers.find((l) => gl.getLayer(l));
|
|
127
|
+
if (layerId !== undefined)
|
|
128
|
+
return layerId;
|
|
129
|
+
}
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
remove(id) {
|
|
133
|
+
if (!this.#list.some((p) => p.id === id))
|
|
134
|
+
return;
|
|
135
|
+
this.#teardown(id);
|
|
136
|
+
const plugin = this.#list.find((p) => p.id === id);
|
|
137
|
+
if (plugin)
|
|
138
|
+
this.#release(plugin);
|
|
139
|
+
this.#plugins = this.#list.filter((p) => p.id !== id);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Uses `gl` from now on, setting up every plugin once its style is loaded (in a microtask when
|
|
143
|
+
* it already is).
|
|
144
|
+
*/
|
|
145
|
+
attach(gl) {
|
|
146
|
+
if (this.#gl === gl)
|
|
147
|
+
return;
|
|
148
|
+
if (this.#gl)
|
|
149
|
+
this.detach();
|
|
150
|
+
this.#gl = gl;
|
|
151
|
+
this.#whenStyleLoaded(false);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Tears every plugin down and stops waiting for a style, before the map's style is replaced:
|
|
155
|
+
* nothing may touch a style on its way out. `restyle` sets them up again.
|
|
156
|
+
*/
|
|
157
|
+
suspend() {
|
|
158
|
+
if (!this.#gl)
|
|
159
|
+
return;
|
|
160
|
+
this.#cancelStyleWait();
|
|
161
|
+
this.#teardownAll();
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Tears down and sets up every plugin again (after a basemap or theme change), once the style
|
|
165
|
+
* loads. Pass `styleLoaded` from a `style.load` listener: `isStyleLoaded()` stays false there
|
|
166
|
+
* until every tile has loaded, and `style.load` has already fired, so waiting would last until
|
|
167
|
+
* `idle`. A live host's style is loaded too (`suspend` comes before every `setStyle`), so a theme
|
|
168
|
+
* change sets up at once as well. On a loaded style the setup is synchronous, so only map events
|
|
169
|
+
* and theme watching call this, never code inside an effect: roots created there can stop (see
|
|
170
|
+
* the class comment).
|
|
171
|
+
*/
|
|
172
|
+
restyle(styleLoaded = false) {
|
|
173
|
+
if (!this.#gl)
|
|
174
|
+
return;
|
|
175
|
+
const loaded = styleLoaded || this.#live;
|
|
176
|
+
this.#teardownAll();
|
|
177
|
+
if (loaded) {
|
|
178
|
+
this.#cancelStyleWait();
|
|
179
|
+
this.#setupAll();
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
this.#whenStyleLoaded(true);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Tears down every plugin. The gl is not touched after this returns. */
|
|
186
|
+
detach() {
|
|
187
|
+
const gl = this.#gl;
|
|
188
|
+
if (!gl)
|
|
189
|
+
return;
|
|
190
|
+
this.#cancelStyleWait();
|
|
191
|
+
this.#teardownAll();
|
|
192
|
+
this.#gl = null;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Sets every plugin up once the style is loaded. On a style that already is, `now` sets them up
|
|
196
|
+
* synchronously (only for callers that never run inside an effect); otherwise they are queued.
|
|
197
|
+
*/
|
|
198
|
+
#whenStyleLoaded(now) {
|
|
199
|
+
const gl = this.#gl;
|
|
200
|
+
this.#cancelStyleWait();
|
|
201
|
+
if (gl.isStyleLoaded()) {
|
|
202
|
+
if (now) {
|
|
203
|
+
this.#setupAll();
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
// Live already, so an `add` before the microtask joins the queued run.
|
|
207
|
+
this.#live = true;
|
|
208
|
+
this.#queueSetup();
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const onStyleLoad = () => {
|
|
213
|
+
// Stale if the host was detached, re-attached, or restyled since.
|
|
214
|
+
if (this.#onStyleLoad !== onStyleLoad || this.#gl !== gl)
|
|
215
|
+
return;
|
|
216
|
+
this.#cancelStyleWait();
|
|
217
|
+
this.#setupAll();
|
|
218
|
+
};
|
|
219
|
+
this.#onStyleLoad = onStyleLoad;
|
|
220
|
+
for (const type of STYLE_READY_EVENTS)
|
|
221
|
+
gl.on(type, onStyleLoad);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Sets every plugin that isn't running up in a microtask, on whichever gl is live by then.
|
|
225
|
+
* Detach, suspend, and restyle clear `#live`, and only a loaded current gl sets it again, so
|
|
226
|
+
* the run never touches a gl that left. One run covers every request made before it starts.
|
|
227
|
+
*/
|
|
228
|
+
#queueSetup() {
|
|
229
|
+
// Claim the queued plugins now, so another map can't take one before the run.
|
|
230
|
+
for (const plugin of this.#list)
|
|
231
|
+
this.#claim(plugin);
|
|
232
|
+
if (this.#setupQueued)
|
|
233
|
+
return;
|
|
234
|
+
this.#setupQueued = true;
|
|
235
|
+
queueMicrotask(() => {
|
|
236
|
+
this.#setupQueued = false;
|
|
237
|
+
if (this.#live)
|
|
238
|
+
this.#setupAll();
|
|
239
|
+
this.#opts.onSettled?.();
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/** Marks `plugin` as this host's, unless another map holds it. */
|
|
243
|
+
#claim(plugin) {
|
|
244
|
+
if (!ownedElsewhere(plugin, this) && Object.isExtensible(plugin)) {
|
|
245
|
+
plugin[OWNER] = this;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Releases a plugin this host claimed but isn't running (queued, then removed or left). */
|
|
249
|
+
#release(plugin) {
|
|
250
|
+
const owned = plugin;
|
|
251
|
+
if (owned[OWNER] === this && !this.#running.has(plugin.id))
|
|
252
|
+
delete owned[OWNER];
|
|
253
|
+
}
|
|
254
|
+
#cancelStyleWait() {
|
|
255
|
+
const listener = this.#onStyleLoad;
|
|
256
|
+
if (listener && this.#gl) {
|
|
257
|
+
for (const type of STYLE_READY_EVENTS)
|
|
258
|
+
this.#gl.off(type, listener);
|
|
259
|
+
}
|
|
260
|
+
this.#onStyleLoad = null;
|
|
261
|
+
}
|
|
262
|
+
#setupAll() {
|
|
263
|
+
const gl = this.#gl;
|
|
264
|
+
this.#live = true;
|
|
265
|
+
for (const plugin of this.#list) {
|
|
266
|
+
// A plugin's setup may add, remove, or detach; follow the host's current state.
|
|
267
|
+
if (this.#gl !== gl || !this.#live)
|
|
268
|
+
return;
|
|
269
|
+
if (!this.#list.includes(plugin) || this.#running.has(plugin.id))
|
|
270
|
+
continue;
|
|
271
|
+
// Another map set this plugin object up after it was added here.
|
|
272
|
+
if (ownedElsewhere(plugin, this)) {
|
|
273
|
+
console.error(ownershipError(plugin));
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
this.#setup(plugin);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#teardownAll() {
|
|
280
|
+
this.#live = false;
|
|
281
|
+
for (const plugin of this.#list) {
|
|
282
|
+
this.#teardown(plugin.id);
|
|
283
|
+
this.#release(plugin);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
#setup(plugin) {
|
|
287
|
+
const gl = this.#gl;
|
|
288
|
+
const running = {
|
|
289
|
+
plugin,
|
|
290
|
+
gl,
|
|
291
|
+
dead: false,
|
|
292
|
+
teardown: null,
|
|
293
|
+
effects: [],
|
|
294
|
+
layers: [],
|
|
295
|
+
sources: [],
|
|
296
|
+
images: [],
|
|
297
|
+
listeners: [],
|
|
298
|
+
unregisters: []
|
|
299
|
+
};
|
|
300
|
+
this.#running.set(plugin.id, running);
|
|
301
|
+
if (Object.isExtensible(plugin))
|
|
302
|
+
plugin[OWNER] = this;
|
|
303
|
+
const ctx = this.#context(plugin.id, running);
|
|
304
|
+
let teardown;
|
|
305
|
+
try {
|
|
306
|
+
// Untracked, in case a plugin's setup reads state (setup runs from a microtask or a map
|
|
307
|
+
// event, never inside the caller's effect).
|
|
308
|
+
teardown = untrack(() => plugin.setup(ctx));
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.error(`map-sdk: plugin "${plugin.id}" setup failed`, error);
|
|
312
|
+
if (this.#running.get(plugin.id) === running)
|
|
313
|
+
this.#teardown(plugin.id);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (!running.dead) {
|
|
317
|
+
running.teardown = teardown;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// Torn down during its own setup (it removed itself or detached the host).
|
|
321
|
+
try {
|
|
322
|
+
untrack(teardown);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error(`map-sdk: plugin "${plugin.id}" teardown failed`, error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
#teardown(pluginId) {
|
|
329
|
+
const running = this.#running.get(pluginId);
|
|
330
|
+
if (!running)
|
|
331
|
+
return;
|
|
332
|
+
this.#running.delete(pluginId);
|
|
333
|
+
running.dead = true;
|
|
334
|
+
const owned = running.plugin;
|
|
335
|
+
if (owned[OWNER] === this)
|
|
336
|
+
delete owned[OWNER];
|
|
337
|
+
const { gl } = running;
|
|
338
|
+
const attempt = (what, fn) => {
|
|
339
|
+
try {
|
|
340
|
+
fn();
|
|
341
|
+
}
|
|
342
|
+
catch (error) {
|
|
343
|
+
console.error(`map-sdk: plugin "${pluginId}" ${what} failed`, error);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
// Effects stop first so none re-runs against a half-removed plugin.
|
|
347
|
+
for (const stop of running.effects)
|
|
348
|
+
attempt("effect cleanup", stop);
|
|
349
|
+
if (running.teardown)
|
|
350
|
+
attempt("teardown", untrack.bind(null, running.teardown));
|
|
351
|
+
// A style change may already have dropped these, so check before removing.
|
|
352
|
+
for (const id of running.layers.toReversed()) {
|
|
353
|
+
attempt(`removing layer ${id}`, () => gl.getLayer(id) && gl.removeLayer(id));
|
|
354
|
+
}
|
|
355
|
+
for (const id of running.sources.toReversed()) {
|
|
356
|
+
attempt(`removing source ${id}`, () => gl.getSource(id) && gl.removeSource(id));
|
|
357
|
+
}
|
|
358
|
+
for (const id of running.images) {
|
|
359
|
+
attempt(`removing image ${id}`, () => gl.hasImage(id) && gl.removeImage(id));
|
|
360
|
+
}
|
|
361
|
+
for (const { type, layerId, listener } of running.listeners) {
|
|
362
|
+
attempt(`removing ${type} listener`, () => gl.off(type, layerId, listener));
|
|
363
|
+
}
|
|
364
|
+
for (const unregister of running.unregisters)
|
|
365
|
+
attempt("unregistering", unregister);
|
|
366
|
+
}
|
|
367
|
+
#context(pluginId, running) {
|
|
368
|
+
const { mapUid, providers, router, selectedKeys } = this.#opts;
|
|
369
|
+
const { gl } = running;
|
|
370
|
+
const layerAbove = () => this.#layerAbove(pluginId);
|
|
371
|
+
const id = (local) => `${mapUid}:${pluginId}:${local}`;
|
|
372
|
+
const addTracked = (full, layer) => {
|
|
373
|
+
if (running.dead)
|
|
374
|
+
return full;
|
|
375
|
+
gl.addLayer(layer, layerAbove());
|
|
376
|
+
// A re-added layer (a plugin rebuilding its layers) is now its plugin's top layer.
|
|
377
|
+
const at = running.layers.indexOf(full);
|
|
378
|
+
if (at >= 0)
|
|
379
|
+
running.layers.splice(at, 1);
|
|
380
|
+
running.layers.push(full);
|
|
381
|
+
return full;
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
gl,
|
|
385
|
+
id,
|
|
386
|
+
metadata: providers.metadata,
|
|
387
|
+
messages: providers.messages,
|
|
388
|
+
format: providers.format,
|
|
389
|
+
addSource(local, spec) {
|
|
390
|
+
const full = id(local);
|
|
391
|
+
if (running.dead)
|
|
392
|
+
return full;
|
|
393
|
+
gl.addSource(full, spec);
|
|
394
|
+
running.sources.push(full);
|
|
395
|
+
return full;
|
|
396
|
+
},
|
|
397
|
+
addLayer(local, spec) {
|
|
398
|
+
return addTracked(id(local), { ...spec, id: id(local) });
|
|
399
|
+
},
|
|
400
|
+
addCustomLayer(local, create) {
|
|
401
|
+
const full = id(local);
|
|
402
|
+
if (running.dead)
|
|
403
|
+
return full;
|
|
404
|
+
const layer = create(full);
|
|
405
|
+
if (layer.id !== full) {
|
|
406
|
+
throw new Error(`map-sdk: custom layer "${local}" must use the id it is given (${full})`);
|
|
407
|
+
}
|
|
408
|
+
return addTracked(full, layer);
|
|
409
|
+
},
|
|
410
|
+
addImage(local, image) {
|
|
411
|
+
if (running.dead)
|
|
412
|
+
return;
|
|
413
|
+
const full = id(local);
|
|
414
|
+
gl.addImage(full, image);
|
|
415
|
+
running.images.push(full);
|
|
416
|
+
},
|
|
417
|
+
interactive(layerIds, toHit) {
|
|
418
|
+
if (running.dead)
|
|
419
|
+
return;
|
|
420
|
+
running.unregisters.push(router.register(pluginId, layerIds, toHit, running.plugin.hoverPriority ?? 0, running.plugin.hoverable ?? true));
|
|
421
|
+
},
|
|
422
|
+
blocking(layerIds, toHover, onClick) {
|
|
423
|
+
if (running.dead)
|
|
424
|
+
return;
|
|
425
|
+
running.unregisters.push(router.registerBlocking(pluginId, layerIds, toHover, onClick, running.plugin.hoverPriority ?? 0, running.plugin.hoverable ?? true));
|
|
426
|
+
},
|
|
427
|
+
on(type, layerId, listener) {
|
|
428
|
+
if (running.dead)
|
|
429
|
+
return;
|
|
430
|
+
gl.on(type, layerId, listener);
|
|
431
|
+
running.listeners.push({ type, layerId, listener });
|
|
432
|
+
},
|
|
433
|
+
get selectedKeys() {
|
|
434
|
+
return selectedKeys(pluginId);
|
|
435
|
+
},
|
|
436
|
+
effect(fn) {
|
|
437
|
+
if (running.dead)
|
|
438
|
+
return;
|
|
439
|
+
running.effects.push($effect.root(() => {
|
|
440
|
+
$effect(fn);
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { CustomLayerInterface, LayerSpecification, SourceSpecification } from "@maptiler/sdk";
|
|
2
|
+
import type { Geometry } from "geojson";
|
|
3
|
+
import type { Snippet } from "svelte";
|
|
4
|
+
import type { FormatProvider } from "../data/format.js";
|
|
5
|
+
import type { MessageProvider } from "../data/messages.js";
|
|
6
|
+
import type { MetadataProvider } from "../data/metadata.js";
|
|
7
|
+
import type { NO_LEVEL } from "../data/scale.js";
|
|
8
|
+
import type { Level, Scale } from "../data/scale-types.js";
|
|
9
|
+
import type { DataStatus, FeatureRef } from "../data/types.js";
|
|
10
|
+
import type { GlMapLike } from "./gl.js";
|
|
11
|
+
export type Providers = {
|
|
12
|
+
metadata: MetadataProvider;
|
|
13
|
+
messages: MessageProvider;
|
|
14
|
+
format: FormatProvider;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A layer spec without its id (the host assigns it). Distributive: `Omit` over the
|
|
18
|
+
* `LayerSpecification` union would keep only the keys every layer type shares, losing `source`.
|
|
19
|
+
*/
|
|
20
|
+
export type LayerSpecWithoutId = LayerSpecification extends infer L ? L extends unknown ? Omit<L, "id"> : never : never;
|
|
21
|
+
export interface FeatureHit {
|
|
22
|
+
pluginId: string;
|
|
23
|
+
ref: FeatureRef;
|
|
24
|
+
refKey: string;
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* The rendered feature's properties, for hover content of a hit no plugin lists (e.g. a
|
|
28
|
+
* cluster bubble's count and aggregates). Unset for ordinary hits.
|
|
29
|
+
*/
|
|
30
|
+
properties?: Readonly<Record<string, unknown>>;
|
|
31
|
+
}
|
|
32
|
+
/** The topmost feature of a blocking layer that was clicked. */
|
|
33
|
+
export interface BlockingClick {
|
|
34
|
+
properties: Record<string, unknown>;
|
|
35
|
+
geometry: Geometry;
|
|
36
|
+
}
|
|
37
|
+
export interface FeatureListItem {
|
|
38
|
+
refKey: string;
|
|
39
|
+
ref: FeatureRef;
|
|
40
|
+
name: string;
|
|
41
|
+
value: number | string | null;
|
|
42
|
+
level: Level | typeof NO_LEVEL;
|
|
43
|
+
units: string;
|
|
44
|
+
/**
|
|
45
|
+
* Decimal places to show a numeric `value` with (the built-in plugins take it from the
|
|
46
|
+
* metadata's display hint). Without it the value is formatted as given.
|
|
47
|
+
*/
|
|
48
|
+
precision?: number;
|
|
49
|
+
/**
|
|
50
|
+
* Cut a numeric `value` off at `precision` instead of rounding it, so the number never reads
|
|
51
|
+
* as the next level (e.g. fire power: 9.96 MW shows "9.9", not "10.0", beside "<10"). Default
|
|
52
|
+
* false.
|
|
53
|
+
*/
|
|
54
|
+
truncate?: boolean;
|
|
55
|
+
lngLat: [number, number];
|
|
56
|
+
/**
|
|
57
|
+
* The feature's shape. When given, the item is in view (the feature list, the view summary)
|
|
58
|
+
* when the shape overlaps the view, not only when `lngLat` is inside it, and focusing or
|
|
59
|
+
* hovering it while `lngLat` is off-screen pins its tooltip at a point of the shape in view
|
|
60
|
+
* (focus then leaves the camera where it is).
|
|
61
|
+
*/
|
|
62
|
+
geometry?: Geometry;
|
|
63
|
+
/**
|
|
64
|
+
* `geometry`'s bounding box, `[west, south, east, north]` (`geometryBbox`). Optional; computed
|
|
65
|
+
* from `geometry` when missing. A plugin listing many shapes should measure it once per data
|
|
66
|
+
* load, since the list and summary test every item on every move.
|
|
67
|
+
*/
|
|
68
|
+
bbox?: [number, number, number, number];
|
|
69
|
+
selectable: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* A short text about a feature with no level (e.g. "Level 2 charger"), shown where its level
|
|
72
|
+
* would be: in the feature list, its row's accessible name, and the generic tooltip. Ignored
|
|
73
|
+
* when the item has a level.
|
|
74
|
+
*/
|
|
75
|
+
detail?: string;
|
|
76
|
+
}
|
|
77
|
+
export type LegendData = {
|
|
78
|
+
pluginId: string;
|
|
79
|
+
title: string;
|
|
80
|
+
units: string;
|
|
81
|
+
scale: Scale;
|
|
82
|
+
/**
|
|
83
|
+
* `"gradient"` or `"blocks"` (labeled swatches with the levels' own labels). Absent: a
|
|
84
|
+
* gradient for range scales and blocks for category scales.
|
|
85
|
+
*/
|
|
86
|
+
display?: "gradient" | "blocks";
|
|
87
|
+
} | {
|
|
88
|
+
pluginId: string;
|
|
89
|
+
status: DataStatus;
|
|
90
|
+
};
|
|
91
|
+
export interface ClickEvent {
|
|
92
|
+
hits: FeatureHit[];
|
|
93
|
+
/** `modifier || touch`: today's "add to the selection" in multiple mode. */
|
|
94
|
+
additive: boolean;
|
|
95
|
+
/** Shift, ⌘, or Ctrl was held. */
|
|
96
|
+
modifier: boolean;
|
|
97
|
+
/** A touch tap. */
|
|
98
|
+
touch: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* A Shift-click that MapLibre's box zoom swallowed (it fires `boxzoomcancel` instead of
|
|
101
|
+
* `click`). Only `"modifier"` mode acts on it, so other maps behave as before.
|
|
102
|
+
*/
|
|
103
|
+
boxZoom?: boolean;
|
|
104
|
+
lngLat: [number, number];
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* What a plugin's `setup` gets. Everything added through it is removed automatically when the
|
|
108
|
+
* plugin is torn down, whether or not the plugin's own teardown cleans up.
|
|
109
|
+
*/
|
|
110
|
+
export interface PluginContext {
|
|
111
|
+
/**
|
|
112
|
+
* The GL map. Don't use it after teardown: do late async work (an image that finishes
|
|
113
|
+
* loading, a fetch) through the context's methods, which do nothing once torn down.
|
|
114
|
+
*/
|
|
115
|
+
readonly gl: GlMapLike;
|
|
116
|
+
/** `${mapUid}:${pluginId}:${local}` */
|
|
117
|
+
id(local: string): string;
|
|
118
|
+
readonly metadata: MetadataProvider;
|
|
119
|
+
readonly messages: MessageProvider;
|
|
120
|
+
readonly format: FormatProvider;
|
|
121
|
+
/** Returns the namespaced id; removed on teardown. */
|
|
122
|
+
addSource(local: string, spec: SourceSpecification): string;
|
|
123
|
+
/** Returns the namespaced id; removed on teardown, before sources. */
|
|
124
|
+
addLayer(local: string, spec: LayerSpecWithoutId): string;
|
|
125
|
+
/**
|
|
126
|
+
* Adds a MapLibre custom layer (e.g. a wind layer from a library the host adds) in the
|
|
127
|
+
* plugin's draw-order slot, with the same lifecycle as `addLayer`. `create` gets the
|
|
128
|
+
* namespaced id, which the layer must use; the object it returns is added as is, never copied,
|
|
129
|
+
* and `create` runs again on each restyle, so build a fresh layer every time. Returns the id.
|
|
130
|
+
*/
|
|
131
|
+
addCustomLayer(local: string, create: (id: string) => CustomLayerInterface): string;
|
|
132
|
+
/** Adds the image as `id(local)`; removed on teardown. */
|
|
133
|
+
addImage(local: string, image: HTMLImageElement | ImageData): void;
|
|
134
|
+
/** Registers layers for click, hover, tooltip, and `featuresAt`. */
|
|
135
|
+
interactive(layerIds: string[], toHit: (props: Record<string, unknown>) => FeatureHit | null): void;
|
|
136
|
+
/**
|
|
137
|
+
* Layers that hide what is beneath them (e.g. cluster bubbles): where one is topmost, a click
|
|
138
|
+
* picks nothing and calls `onClick` with that feature, and the hover is `toHover`'s hit for it
|
|
139
|
+
* (for its tooltip), or nothing. `toHover` must have no side effects (lookups call it too).
|
|
140
|
+
*/
|
|
141
|
+
blocking(layerIds: string[], toHover?: (props: Record<string, unknown>) => FeatureHit | null, onClick?: (feature: BlockingClick) => void): void;
|
|
142
|
+
/** A layer event listener (e.g. a cluster click), removed on teardown. */
|
|
143
|
+
on(type: string, layerId: string, listener: (e: any) => void): void;
|
|
144
|
+
/** Reactive. */
|
|
145
|
+
readonly selectedKeys: ReadonlySet<string>;
|
|
146
|
+
/** An `$effect.root` torn down with the plugin. */
|
|
147
|
+
effect(fn: () => void | (() => void)): void;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* A layer of data on a map. A plugin object keeps per-map state, so it belongs to one map: create
|
|
151
|
+
* one plugin per map (adding an object that is set up on another map throws).
|
|
152
|
+
*/
|
|
153
|
+
export interface MapPlugin {
|
|
154
|
+
id: string;
|
|
155
|
+
/** Already-translated layer name, for the selection bar and view summary. */
|
|
156
|
+
label?: string;
|
|
157
|
+
/** The metadata dataset the plugin's data comes from (for attribution); may change with its source. */
|
|
158
|
+
readonly dataset?: string;
|
|
159
|
+
/** Adds sources, layers, and icons; returns its teardown. Runs only once a style is loaded. */
|
|
160
|
+
setup(ctx: PluginContext): () => void;
|
|
161
|
+
legend?(): LegendData | null;
|
|
162
|
+
features?(): FeatureListItem[];
|
|
163
|
+
selectable?: boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Which plugin's feature the hover tooltip shows where several are under the pointer: the
|
|
166
|
+
* highest wins, the topmost on a tie. Default 0. Over a blocking layer (a cluster bubble), a
|
|
167
|
+
* feature beneath shows only if its plugin's priority is higher than the blocking plugin's.
|
|
168
|
+
* Clicks ignore this (topmost first), and a blocking layer on top still blocks them.
|
|
169
|
+
*/
|
|
170
|
+
hoverPriority?: number;
|
|
171
|
+
/**
|
|
172
|
+
* False: the plugin's features never take the hover (so no hover tooltip and no pointer
|
|
173
|
+
* cursor) and never hide a feature beneath from it, as if they weren't rendered. Clicks,
|
|
174
|
+
* `featuresAt`, and keyboard focus from the list are unchanged. Default true.
|
|
175
|
+
*/
|
|
176
|
+
hoverable?: boolean;
|
|
177
|
+
displayOptions?: Snippet;
|
|
178
|
+
/** Richer tooltip content for one of this plugin's features, below its name. */
|
|
179
|
+
tooltip?(item: FeatureListItem): Snippet | undefined;
|
|
180
|
+
/**
|
|
181
|
+
* Set when this plugin's `tooltip` content shows the feature's name itself, so the generic
|
|
182
|
+
* tooltip leaves its name header out. Default false.
|
|
183
|
+
*/
|
|
184
|
+
tooltipShowsName?: boolean;
|
|
185
|
+
/**
|
|
186
|
+
* Tooltip content for a hovered hit that isn't a listed feature (e.g. a cluster bubble's hover
|
|
187
|
+
* hit from `PluginContext.blocking`).
|
|
188
|
+
*/
|
|
189
|
+
hoverTooltip?(hit: FeatureHit): Snippet | undefined;
|
|
190
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginContext } from "./plugin.js";
|
|
2
|
+
/**
|
|
3
|
+
* A view of `ctx` whose local ids all start with `prefix`, so several plugins can be set up
|
|
4
|
+
* inside one (one id, one teardown) without their sources, layers, and images colliding.
|
|
5
|
+
* Everything else is the parent's, read live (`selectedKeys` stays reactive).
|
|
6
|
+
*/
|
|
7
|
+
export declare function scopedContext(ctx: PluginContext, prefix: string): PluginContext;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A view of `ctx` whose local ids all start with `prefix`, so several plugins can be set up
|
|
3
|
+
* inside one (one id, one teardown) without their sources, layers, and images colliding.
|
|
4
|
+
* Everything else is the parent's, read live (`selectedKeys` stays reactive).
|
|
5
|
+
*/
|
|
6
|
+
export function scopedContext(ctx, prefix) {
|
|
7
|
+
const local = (name) => `${prefix}${name}`;
|
|
8
|
+
return {
|
|
9
|
+
get gl() {
|
|
10
|
+
return ctx.gl;
|
|
11
|
+
},
|
|
12
|
+
get metadata() {
|
|
13
|
+
return ctx.metadata;
|
|
14
|
+
},
|
|
15
|
+
get messages() {
|
|
16
|
+
return ctx.messages;
|
|
17
|
+
},
|
|
18
|
+
get format() {
|
|
19
|
+
return ctx.format;
|
|
20
|
+
},
|
|
21
|
+
get selectedKeys() {
|
|
22
|
+
return ctx.selectedKeys;
|
|
23
|
+
},
|
|
24
|
+
id: (name) => ctx.id(local(name)),
|
|
25
|
+
addSource: (name, spec) => ctx.addSource(local(name), spec),
|
|
26
|
+
addLayer: (name, spec) => ctx.addLayer(local(name), spec),
|
|
27
|
+
addCustomLayer: (name, create) => ctx.addCustomLayer(local(name), create),
|
|
28
|
+
addImage: (name, image) => ctx.addImage(local(name), image),
|
|
29
|
+
interactive: (layerIds, toHit) => ctx.interactive(layerIds, toHit),
|
|
30
|
+
blocking: (layerIds, toHover, onClick) => ctx.blocking(layerIds, toHover, onClick),
|
|
31
|
+
on: (type, layerId, listener) => ctx.on(type, layerId, listener),
|
|
32
|
+
effect: (fn) => ctx.effect(fn)
|
|
33
|
+
};
|
|
34
|
+
}
|