@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,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
|
+
}
|