@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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +713 -2
  3. package/dist/core/basemap.svelte.d.ts +38 -0
  4. package/dist/core/basemap.svelte.js +163 -0
  5. package/dist/core/context.d.ts +5 -0
  6. package/dist/core/context.js +13 -0
  7. package/dist/core/create-map.svelte.d.ts +274 -0
  8. package/dist/core/create-map.svelte.js +591 -0
  9. package/dist/core/gl.d.ts +97 -0
  10. package/dist/core/gl.js +1 -0
  11. package/dist/core/interactions.svelte.d.ts +50 -0
  12. package/dist/core/interactions.svelte.js +330 -0
  13. package/dist/core/plugin-host.svelte.d.ts +71 -0
  14. package/dist/core/plugin-host.svelte.js +445 -0
  15. package/dist/core/plugin.d.ts +190 -0
  16. package/dist/core/plugin.js +1 -0
  17. package/dist/core/scoped-context.d.ts +7 -0
  18. package/dist/core/scoped-context.js +34 -0
  19. package/dist/core/shape-view.d.ts +27 -0
  20. package/dist/core/shape-view.js +323 -0
  21. package/dist/data/format.d.ts +10 -0
  22. package/dist/data/format.js +37 -0
  23. package/dist/data/index.d.ts +9 -0
  24. package/dist/data/index.js +9 -0
  25. package/dist/data/messages/en.json +61 -0
  26. package/dist/data/messages.d.ts +10 -0
  27. package/dist/data/messages.js +17 -0
  28. package/dist/data/metadata.d.ts +22 -0
  29. package/dist/data/metadata.js +110 -0
  30. package/dist/data/refs.d.ts +18 -0
  31. package/dist/data/refs.js +61 -0
  32. package/dist/data/scale-types.d.ts +7 -0
  33. package/dist/data/scale-types.js +1 -0
  34. package/dist/data/scale.d.ts +13 -0
  35. package/dist/data/scale.js +27 -0
  36. package/dist/data/selection.d.ts +13 -0
  37. package/dist/data/selection.js +52 -0
  38. package/dist/data/static-source.d.ts +4 -0
  39. package/dist/data/static-source.js +3 -0
  40. package/dist/data/types.d.ts +74 -0
  41. package/dist/data/types.js +1 -0
  42. package/dist/index.d.ts +59 -0
  43. package/dist/index.js +42 -0
  44. package/dist/plugins/icons.d.ts +21 -0
  45. package/dist/plugins/icons.js +73 -0
  46. package/dist/plugins/points-clusters.d.ts +60 -0
  47. package/dist/plugins/points-clusters.js +93 -0
  48. package/dist/plugins/points.svelte.d.ts +105 -0
  49. package/dist/plugins/points.svelte.js +502 -0
  50. package/dist/plugins/scale-resolution.svelte.d.ts +27 -0
  51. package/dist/plugins/scale-resolution.svelte.js +111 -0
  52. package/dist/plugins/shapes.svelte.d.ts +34 -0
  53. package/dist/plugins/shapes.svelte.js +325 -0
  54. package/dist/plugins/shared.d.ts +6 -0
  55. package/dist/plugins/shared.js +18 -0
  56. package/dist/plugins/style.d.ts +57 -0
  57. package/dist/plugins/style.js +193 -0
  58. package/dist/presets/CollocationTooltip.svelte +132 -0
  59. package/dist/presets/CollocationTooltip.svelte.d.ts +16 -0
  60. package/dist/presets/FireTooltip.svelte +70 -0
  61. package/dist/presets/FireTooltip.svelte.d.ts +13 -0
  62. package/dist/presets/MonitorClusterTooltip.svelte +168 -0
  63. package/dist/presets/MonitorClusterTooltip.svelte.d.ts +16 -0
  64. package/dist/presets/MonitorReadingBox.svelte +58 -0
  65. package/dist/presets/MonitorReadingBox.svelte.d.ts +12 -0
  66. package/dist/presets/MonitorTooltip.svelte +88 -0
  67. package/dist/presets/MonitorTooltip.svelte.d.ts +20 -0
  68. package/dist/presets/calheatscore.d.ts +24 -0
  69. package/dist/presets/calheatscore.js +22 -0
  70. package/dist/presets/cluster-area.d.ts +13 -0
  71. package/dist/presets/cluster-area.js +34 -0
  72. package/dist/presets/collocation-sites.d.ts +18 -0
  73. package/dist/presets/collocation-sites.js +67 -0
  74. package/dist/presets/fire-groups.d.ts +26 -0
  75. package/dist/presets/fire-groups.js +51 -0
  76. package/dist/presets/forecast-zones.d.ts +25 -0
  77. package/dist/presets/forecast-zones.js +23 -0
  78. package/dist/presets/hms-fire.d.ts +30 -0
  79. package/dist/presets/hms-fire.js +86 -0
  80. package/dist/presets/hms-smoke.d.ts +20 -0
  81. package/dist/presets/hms-smoke.js +22 -0
  82. package/dist/presets/monitors.d.ts +48 -0
  83. package/dist/presets/monitors.js +243 -0
  84. package/dist/presets/reactive-props.svelte.d.ts +4 -0
  85. package/dist/presets/reactive-props.svelte.js +7 -0
  86. package/dist/presets/regions.d.ts +24 -0
  87. package/dist/presets/regions.js +22 -0
  88. package/dist/stores/abort.d.ts +16 -0
  89. package/dist/stores/abort.js +52 -0
  90. package/dist/stores/collocation-store.svelte.d.ts +25 -0
  91. package/dist/stores/collocation-store.svelte.js +72 -0
  92. package/dist/stores/hms-stores.svelte.d.ts +38 -0
  93. package/dist/stores/hms-stores.svelte.js +97 -0
  94. package/dist/stores/index.d.ts +4 -0
  95. package/dist/stores/index.js +4 -0
  96. package/dist/stores/load-once.svelte.d.ts +25 -0
  97. package/dist/stores/load-once.svelte.js +93 -0
  98. package/dist/stores/monitors-store.svelte.d.ts +85 -0
  99. package/dist/stores/monitors-store.svelte.js +340 -0
  100. package/dist/theme.css +34 -0
  101. package/dist/ui/FeatureList.svelte +501 -0
  102. package/dist/ui/FeatureList.svelte.d.ts +14 -0
  103. package/dist/ui/Legend.svelte +214 -0
  104. package/dist/ui/Legend.svelte.d.ts +22 -0
  105. package/dist/ui/LegendBar.svelte +161 -0
  106. package/dist/ui/LegendBar.svelte.d.ts +11 -0
  107. package/dist/ui/MapControls.svelte +135 -0
  108. package/dist/ui/MapControls.svelte.d.ts +11 -0
  109. package/dist/ui/MapTooltip.svelte +397 -0
  110. package/dist/ui/MapTooltip.svelte.d.ts +37 -0
  111. package/dist/ui/PickNotice.svelte +56 -0
  112. package/dist/ui/PickNotice.svelte.d.ts +7 -0
  113. package/dist/ui/SelectionBar.svelte +123 -0
  114. package/dist/ui/SelectionBar.svelte.d.ts +10 -0
  115. package/dist/ui/TooltipItem.svelte +66 -0
  116. package/dist/ui/TooltipItem.svelte.d.ts +11 -0
  117. package/dist/ui/ViewSummary.svelte +97 -0
  118. package/dist/ui/ViewSummary.svelte.d.ts +7 -0
  119. package/dist/ui/card.d.ts +29 -0
  120. package/dist/ui/card.js +1 -0
  121. package/dist/ui/controls.css +45 -0
  122. package/dist/ui/data-credits.svelte.d.ts +9 -0
  123. package/dist/ui/data-credits.svelte.js +52 -0
  124. package/dist/ui/feature-list.d.ts +46 -0
  125. package/dist/ui/feature-list.js +115 -0
  126. package/dist/ui/legend-data.d.ts +41 -0
  127. package/dist/ui/legend-data.js +40 -0
  128. package/dist/ui/level-chip.d.ts +11 -0
  129. package/dist/ui/level-chip.js +15 -0
  130. package/dist/ui/value-format.d.ts +8 -0
  131. package/dist/ui/value-format.js +20 -0
  132. package/dist/ui/view-summary.d.ts +11 -0
  133. package/dist/ui/view-summary.js +33 -0
  134. package/dist/view/MapView.svelte +443 -0
  135. package/dist/view/MapView.svelte.d.ts +51 -0
  136. package/dist/view/create-gl-map.d.ts +34 -0
  137. package/dist/view/create-gl-map.js +186 -0
  138. package/dist/view/list-placement.d.ts +21 -0
  139. package/dist/view/list-placement.js +13 -0
  140. package/package.json +104 -5
@@ -0,0 +1,50 @@
1
+ import type { GlMapLike } from "./gl.js";
2
+ import type { BlockingClick, ClickEvent, FeatureHit } from "./plugin.js";
3
+ export interface Hovered {
4
+ hit: FeatureHit;
5
+ lngLat: [number, number];
6
+ }
7
+ /**
8
+ * Routes click and hover events on plugin layers to feature hits. One per map.
9
+ *
10
+ * Listens on the map itself (`click`, `mousemove`, `mouseout`) and queries only the registered
11
+ * layers, so registrations can come and go without re-binding listeners. Layers nobody registered
12
+ * are never hit. A blocking layer (e.g. a cluster bubble, which zooms instead) is never hit either,
13
+ * and when it is topmost at a point nothing beneath it is hit; only a feature whose plugin
14
+ * outranks it in `hoverPriority` is hovered through it. A non-hoverable plugin's features (e.g.
15
+ * smoke plumes) are left out of the hover and cursor as if they weren't rendered, but are still
16
+ * clicked and returned by lookups.
17
+ */
18
+ export declare class InteractionRouter {
19
+ #private;
20
+ /** The top hit under the pointer and where it is. Reactive, for the tooltip. */
21
+ get hovered(): Hovered | null;
22
+ /** Records the layers whose features `toHit` turns into hits. Returns an unregister function. */
23
+ register(pluginId: string, layerIds: string[], toHit: (props: Record<string, unknown>) => FeatureHit | null, priority?: number, hoverable?: boolean): () => void;
24
+ /**
25
+ * Records layers that hide what is beneath them: when one is topmost at a point, clicks pick
26
+ * nothing (calling `onClick` with that feature instead) and the hover is `toHover`'s hit for
27
+ * it (or nothing). `toHover` must have no side effects: lookups call it too. Returns an
28
+ * unregister function.
29
+ */
30
+ registerBlocking(pluginId: string, layerIds: string[], toHover?: (props: Record<string, unknown>) => FeatureHit | null, onClick?: (feature: BlockingClick) => void, priority?: number, hoverable?: boolean): () => void;
31
+ /** Listens to `gl` from now on. */
32
+ attach(gl: GlMapLike): void;
33
+ /** Stops listening and clears `hovered`. */
34
+ detach(): void;
35
+ /**
36
+ * Hits under a canvas point, topmost first, de-duplicated by `refKey`. `[]` when detached or
37
+ * when a blocking layer is topmost.
38
+ */
39
+ hitsAt(point: [number, number]): FeatureHit[];
40
+ /**
41
+ * Called for every click with at least one hit. `additive` is true for a shift, ⌘, or Ctrl
42
+ * click, or a touch tap; selection rules decide what it means for the plugin's mode.
43
+ */
44
+ onPick(handler: (e: ClickEvent) => void): () => void;
45
+ /**
46
+ * Called for a click that picks nothing: one with no hits, or one on a blocking layer (after
47
+ * that layer's own click handler). Returns an unsubscribe function.
48
+ */
49
+ onMiss(handler: () => void): () => void;
50
+ }
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Routes click and hover events on plugin layers to feature hits. One per map.
3
+ *
4
+ * Listens on the map itself (`click`, `mousemove`, `mouseout`) and queries only the registered
5
+ * layers, so registrations can come and go without re-binding listeners. Layers nobody registered
6
+ * are never hit. A blocking layer (e.g. a cluster bubble, which zooms instead) is never hit either,
7
+ * and when it is topmost at a point nothing beneath it is hit; only a feature whose plugin
8
+ * outranks it in `hoverPriority` is hovered through it. A non-hoverable plugin's features (e.g.
9
+ * smoke plumes) are left out of the hover and cursor as if they weren't rendered, but are still
10
+ * clicked and returned by lookups.
11
+ */
12
+ export class InteractionRouter {
13
+ // Bookkeeping only; nothing reads these reactively.
14
+ /* eslint-disable svelte/prefer-svelte-reactivity */
15
+ #registrations = new Set();
16
+ #pickHandlers = new Set();
17
+ #missHandlers = new Set();
18
+ /* eslint-enable svelte/prefer-svelte-reactivity */
19
+ #gl = null;
20
+ #canvas = null;
21
+ /**
22
+ * The `pointerType` of the latest `pointerdown` on the canvas, for clicks whose original event
23
+ * has none (Safari before 17 dispatches `click` as a `MouseEvent`). Cleared after each click.
24
+ */
25
+ #lastPointerType = null;
26
+ #hovered = $state.raw(null);
27
+ /** Whether the canvas cursor is set to a pointer. */
28
+ #pointer = false;
29
+ /** The top hit under the pointer and where it is. Reactive, for the tooltip. */
30
+ get hovered() {
31
+ return this.#hovered;
32
+ }
33
+ /** Records the layers whose features `toHit` turns into hits. Returns an unregister function. */
34
+ register(pluginId, layerIds, toHit, priority = 0, hoverable = true) {
35
+ return this.#add({
36
+ pluginId,
37
+ layerIds: [...layerIds],
38
+ toHit,
39
+ blocking: false,
40
+ priority,
41
+ hoverable
42
+ });
43
+ }
44
+ /**
45
+ * Records layers that hide what is beneath them: when one is topmost at a point, clicks pick
46
+ * nothing (calling `onClick` with that feature instead) and the hover is `toHover`'s hit for
47
+ * it (or nothing). `toHover` must have no side effects: lookups call it too. Returns an
48
+ * unregister function.
49
+ */
50
+ registerBlocking(pluginId, layerIds, toHover, onClick, priority = 0, hoverable = true) {
51
+ return this.#add({
52
+ pluginId,
53
+ layerIds: [...layerIds],
54
+ toHit: toHover ?? (() => null),
55
+ blocking: true,
56
+ onClick,
57
+ priority,
58
+ hoverable
59
+ });
60
+ }
61
+ #add(registration) {
62
+ const { pluginId } = registration;
63
+ this.#registrations.add(registration);
64
+ return () => {
65
+ if (!this.#registrations.delete(registration))
66
+ return;
67
+ if (this.#hovered?.hit.pluginId === pluginId)
68
+ this.#hovered = null;
69
+ // The pointer may have been over this plugin; the next move sets it again.
70
+ this.#setPointer(false);
71
+ };
72
+ }
73
+ /** Listens to `gl` from now on. */
74
+ attach(gl) {
75
+ if (this.#gl === gl)
76
+ return;
77
+ this.detach();
78
+ this.#gl = gl;
79
+ gl.on("click", this.#onClick);
80
+ gl.on("mousemove", this.#onMove);
81
+ gl.on("mouseout", this.#onOut);
82
+ gl.on("boxzoomstart", this.#onBoxZoomStart);
83
+ gl.on("boxzoomcancel", this.#onBoxZoomCancel);
84
+ this.#canvas = gl.getCanvas();
85
+ this.#canvas.addEventListener("pointerdown", this.#onPointerDown);
86
+ }
87
+ /** Stops listening and clears `hovered`. */
88
+ detach() {
89
+ const gl = this.#gl;
90
+ if (!gl)
91
+ return;
92
+ gl.off("click", this.#onClick);
93
+ gl.off("mousemove", this.#onMove);
94
+ gl.off("mouseout", this.#onOut);
95
+ gl.off("boxzoomstart", this.#onBoxZoomStart);
96
+ gl.off("boxzoomcancel", this.#onBoxZoomCancel);
97
+ this.#boxDrawn = false;
98
+ this.#canvas?.removeEventListener("pointerdown", this.#onPointerDown);
99
+ this.#setPointer(false);
100
+ this.#canvas = null;
101
+ this.#lastPointerType = null;
102
+ this.#gl = null;
103
+ this.#hovered = null;
104
+ }
105
+ /**
106
+ * Hits under a canvas point, topmost first, de-duplicated by `refKey`. `[]` when detached or
107
+ * when a blocking layer is topmost.
108
+ */
109
+ hitsAt(point) {
110
+ const found = this.#lookup(point);
111
+ return found.blocked ? [] : found.hits;
112
+ }
113
+ /** What is under one point; with `forHover`, non-hoverable features are left out first. */
114
+ #lookup(point, forHover = false) {
115
+ const none = { hits: [], hover: null, blocked: false, blocker: null, blockedBy: null };
116
+ const gl = this.#gl;
117
+ if (!gl)
118
+ return none;
119
+ // Locals for one lookup; nothing reads them reactively.
120
+ /* eslint-disable svelte/prefer-svelte-reactivity */
121
+ const byLayer = new Map();
122
+ for (const registration of this.#registrations) {
123
+ if (forHover && !registration.hoverable)
124
+ continue;
125
+ for (const id of registration.layerIds) {
126
+ // MapLibre errors on querying a missing layer (e.g. mid basemap change).
127
+ if (!byLayer.has(id) && gl.getLayer(id))
128
+ byLayer.set(id, registration);
129
+ }
130
+ }
131
+ if (byLayer.size === 0)
132
+ return none;
133
+ const hits = [];
134
+ let hover = null;
135
+ let hoverPriority = -Infinity;
136
+ const seen = new Set();
137
+ /* eslint-enable svelte/prefer-svelte-reactivity */
138
+ const features = gl.queryRenderedFeatures(point, { layers: [...byLayer.keys()] });
139
+ const top = features[0] && byLayer.get(features[0].layer.id);
140
+ if (top?.blocking) {
141
+ const properties = features[0].properties ?? {};
142
+ const hit = top.toHit(properties);
143
+ // Clicks stay blocked, but the hover goes to the highest-priority feature beneath that
144
+ // outranks the blocking plugin.
145
+ let beneath = null;
146
+ let beneathPriority = top.priority;
147
+ for (const feature of features.slice(1)) {
148
+ const registration = byLayer.get(feature.layer.id);
149
+ if (!registration || registration.blocking)
150
+ continue;
151
+ if (registration.priority <= beneathPriority)
152
+ continue;
153
+ const below = registration.toHit(feature.properties ?? {});
154
+ if (!below)
155
+ continue;
156
+ beneath = { ...below, pluginId: registration.pluginId };
157
+ beneathPriority = registration.priority;
158
+ }
159
+ return {
160
+ hits: [],
161
+ hover: beneath,
162
+ blocked: true,
163
+ blocker: hit && { ...hit, pluginId: top.pluginId },
164
+ blockedBy: { registration: top, feature: { properties, geometry: features[0].geometry } }
165
+ };
166
+ }
167
+ for (const feature of features) {
168
+ const registration = byLayer.get(feature.layer.id);
169
+ if (!registration || registration.blocking)
170
+ continue;
171
+ const hit = registration.toHit(feature.properties ?? {});
172
+ if (!hit || seen.has(hit.refKey))
173
+ continue;
174
+ seen.add(hit.refKey);
175
+ const withPlugin = { ...hit, pluginId: registration.pluginId };
176
+ hits.push(withPlugin);
177
+ // Strictly greater, so a tie keeps the topmost.
178
+ if (registration.priority > hoverPriority) {
179
+ hover = withPlugin;
180
+ hoverPriority = registration.priority;
181
+ }
182
+ }
183
+ return { hits, hover, blocked: false, blocker: null, blockedBy: null };
184
+ }
185
+ /**
186
+ * Called for every click with at least one hit. `additive` is true for a shift, ⌘, or Ctrl
187
+ * click, or a touch tap; selection rules decide what it means for the plugin's mode.
188
+ */
189
+ onPick(handler) {
190
+ this.#pickHandlers.add(handler);
191
+ return () => {
192
+ this.#pickHandlers.delete(handler);
193
+ };
194
+ }
195
+ /**
196
+ * Called for a click that picks nothing: one with no hits, or one on a blocking layer (after
197
+ * that layer's own click handler). Returns an unsubscribe function.
198
+ */
199
+ onMiss(handler) {
200
+ this.#missHandlers.add(handler);
201
+ return () => {
202
+ this.#missHandlers.delete(handler);
203
+ };
204
+ }
205
+ #emitMiss() {
206
+ for (const handler of [...this.#missHandlers]) {
207
+ try {
208
+ handler();
209
+ }
210
+ catch (error) {
211
+ console.error("map-sdk: a miss handler failed", error);
212
+ }
213
+ }
214
+ }
215
+ #onPointerDown = (e) => {
216
+ this.#lastPointerType = e.pointerType || null;
217
+ };
218
+ #onClick = (e) => {
219
+ const recorded = this.#lastPointerType;
220
+ this.#lastPointerType = null;
221
+ const found = this.#lookup([e.point.x, e.point.y]);
222
+ if (found.blockedBy) {
223
+ const { registration, feature } = found.blockedBy;
224
+ try {
225
+ registration.onClick?.(feature);
226
+ }
227
+ catch (error) {
228
+ console.error("map-sdk: a blocking layer's click handler failed", error);
229
+ }
230
+ this.#emitMiss();
231
+ return;
232
+ }
233
+ const { hits } = found;
234
+ if (hits.length === 0) {
235
+ this.#emitMiss();
236
+ return;
237
+ }
238
+ const o = e.originalEvent;
239
+ const pointerType = o?.pointerType || recorded;
240
+ const modifier = Boolean(o?.shiftKey || o?.metaKey || o?.ctrlKey);
241
+ const touch = pointerType === "touch";
242
+ const event = {
243
+ hits,
244
+ additive: modifier || touch,
245
+ modifier,
246
+ touch,
247
+ lngLat: [e.lngLat.lng, e.lngLat.lat]
248
+ };
249
+ for (const handler of [...this.#pickHandlers]) {
250
+ try {
251
+ handler(event);
252
+ }
253
+ catch (error) {
254
+ console.error("map-sdk: a pick handler failed", error);
255
+ }
256
+ }
257
+ };
258
+ /**
259
+ * MapLibre's box zoom starts on a Shift press and, released in place, suppresses the click and
260
+ * fires `boxzoomcancel`. A release with a pointer position is reported as a Shift-click
261
+ * (`boxZoom: true`) to the pick handlers only: no miss, no blocking layer's click.
262
+ */
263
+ /** A box was drawn since the Shift press: its cancel is a drag back to the start, not a click. */
264
+ #boxDrawn = false;
265
+ #onBoxZoomStart = () => {
266
+ this.#boxDrawn = true;
267
+ };
268
+ #onBoxZoomCancel = (e) => {
269
+ const drawn = this.#boxDrawn;
270
+ this.#boxDrawn = false;
271
+ if (drawn)
272
+ return;
273
+ const o = e.originalEvent;
274
+ const gl = this.#gl;
275
+ if (!gl || typeof o?.clientX !== "number" || typeof o.clientY !== "number")
276
+ return;
277
+ const point = this.#canvasPoint(o.clientX, o.clientY);
278
+ const found = this.#lookup(point);
279
+ if (found.blockedBy || found.hits.length === 0)
280
+ return;
281
+ const { lng, lat } = gl.unproject(point);
282
+ const event = {
283
+ hits: found.hits,
284
+ additive: true,
285
+ modifier: true,
286
+ touch: false,
287
+ boxZoom: true,
288
+ lngLat: [lng, lat]
289
+ };
290
+ for (const handler of [...this.#pickHandlers]) {
291
+ try {
292
+ handler(event);
293
+ }
294
+ catch (error) {
295
+ console.error("map-sdk: a pick handler failed", error);
296
+ }
297
+ }
298
+ };
299
+ /** A page position as a canvas pixel, as MapLibre's `DOM.mousePos` does (CSS scale included). */
300
+ #canvasPoint(clientX, clientY) {
301
+ const canvas = this.#canvas;
302
+ const rect = canvas?.getBoundingClientRect?.();
303
+ if (!canvas || !rect)
304
+ return [clientX, clientY];
305
+ const scaleX = canvas.offsetWidth ? rect.width / canvas.offsetWidth : 1;
306
+ const scaleY = canvas.offsetHeight ? rect.height / canvas.offsetHeight : 1;
307
+ return [
308
+ (clientX - rect.left) / scaleX - (canvas.clientLeft || 0),
309
+ (clientY - rect.top) / scaleY - (canvas.clientTop || 0)
310
+ ];
311
+ }
312
+ #onMove = (e) => {
313
+ const found = this.#lookup([e.point.x, e.point.y], true);
314
+ const hit = found.blocked ? (found.hover ?? found.blocker) : found.hover;
315
+ this.#hovered = hit ? { hit, lngLat: [e.lngLat.lng, e.lngLat.lat] } : null;
316
+ this.#setPointer(found.blocked || hit !== null);
317
+ };
318
+ #onOut = () => {
319
+ this.#hovered = null;
320
+ this.#setPointer(false);
321
+ };
322
+ /** Sets the canvas cursor only when it changes, so the engine's own cursors are left alone. */
323
+ #setPointer(on) {
324
+ if (on === this.#pointer)
325
+ return;
326
+ this.#pointer = on;
327
+ if (this.#canvas)
328
+ this.#canvas.style.cursor = on ? "pointer" : "";
329
+ }
330
+ }
@@ -0,0 +1,71 @@
1
+ import type { GlMapLike } from "./gl.js";
2
+ import type { InteractionRouter } from "./interactions.svelte.js";
3
+ import type { MapPlugin, Providers } from "./plugin.js";
4
+ export interface PluginHostOptions {
5
+ mapUid: string;
6
+ providers: Providers;
7
+ router: InteractionRouter;
8
+ /** Reactive: read inside a plugin's effects. */
9
+ selectedKeys(pluginId: string): ReadonlySet<string>;
10
+ /**
11
+ * Called after each queued setup run finishes (also when it had nothing to do), never after a
12
+ * synchronous one.
13
+ */
14
+ onSettled?: () => void;
15
+ }
16
+ /**
17
+ * Runs plugins against a GL map. Plugins are set up only when the map's style is loaded, and
18
+ * everything they add through their `PluginContext` is removed on teardown, whatever their own
19
+ * teardown does. The plugin list outlives any one GL map.
20
+ *
21
+ * Setup never runs inside the caller's code. When `add` or `attach` would set plugins up, they
22
+ * are queued and set up in one microtask, where no effect is active: Svelte cuts loose the
23
+ * `$effect.root`s created inside an effect when that effect re-runs, and their effects can stop
24
+ * (see docs/superpowers/specs/2026-11-07-plugin-setup-outside-effects-design.md). Setup on a
25
+ * style load or `restyle` comes from map events, not host code, and stays synchronous.
26
+ *
27
+ * Plugins draw in list order, later on top: each plugin's layers stay together, below the first
28
+ * layer of the next plugin in the list that has any.
29
+ */
30
+ export declare class PluginHost {
31
+ #private;
32
+ constructor(opts: PluginHostOptions);
33
+ /** In draw order, bottom first. Reactive. */
34
+ get plugins(): readonly MapPlugin[];
35
+ /** True while a setup run is queued and has not started. Not reactive. */
36
+ get pending(): boolean;
37
+ /**
38
+ * Adds `plugin` on top, or just below the plugin `before` names. It is in `plugins` at once;
39
+ * on a live map its setup runs in a microtask. Throws when the same plugin object is set up on
40
+ * another map at the moment (one plugin object per map).
41
+ */
42
+ add(plugin: MapPlugin, before?: string): void;
43
+ /**
44
+ * Moves a plugin to the top, or just below the plugin `before` names, with its layers. Does
45
+ * nothing for an unknown `id` or when `before` is `id`.
46
+ */
47
+ move(id: string, before?: string): void;
48
+ remove(id: string): void;
49
+ /**
50
+ * Uses `gl` from now on, setting up every plugin once its style is loaded (in a microtask when
51
+ * it already is).
52
+ */
53
+ attach(gl: GlMapLike): void;
54
+ /**
55
+ * Tears every plugin down and stops waiting for a style, before the map's style is replaced:
56
+ * nothing may touch a style on its way out. `restyle` sets them up again.
57
+ */
58
+ suspend(): void;
59
+ /**
60
+ * Tears down and sets up every plugin again (after a basemap or theme change), once the style
61
+ * loads. Pass `styleLoaded` from a `style.load` listener: `isStyleLoaded()` stays false there
62
+ * until every tile has loaded, and `style.load` has already fired, so waiting would last until
63
+ * `idle`. A live host's style is loaded too (`suspend` comes before every `setStyle`), so a theme
64
+ * change sets up at once as well. On a loaded style the setup is synchronous, so only map events
65
+ * and theme watching call this, never code inside an effect: roots created there can stop (see
66
+ * the class comment).
67
+ */
68
+ restyle(styleLoaded?: boolean): void;
69
+ /** Tears down every plugin. The gl is not touched after this returns. */
70
+ detach(): void;
71
+ }