@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,591 @@
|
|
|
1
|
+
import { untrack } from "svelte";
|
|
2
|
+
import { SvelteMap } from "svelte/reactivity";
|
|
3
|
+
import { createDefaultFormat } from "../data/format.js";
|
|
4
|
+
import { defaultMessages } from "../data/messages.js";
|
|
5
|
+
import { createDefaultMetadata } from "../data/metadata.js";
|
|
6
|
+
import { applyPick, selectionKeys, toggleInSelection } from "../data/selection.js";
|
|
7
|
+
import { BasemapState } from "./basemap.svelte.js";
|
|
8
|
+
import { InteractionRouter } from "./interactions.svelte.js";
|
|
9
|
+
import { PluginHost } from "./plugin-host.svelte.js";
|
|
10
|
+
import { intersectsBounds, shapeAnchor } from "./shape-view.js";
|
|
11
|
+
/** Monitor-map's start view. */
|
|
12
|
+
const DEFAULT_VIEW = {
|
|
13
|
+
center: [-119.7987626619462, 36.76272050981146],
|
|
14
|
+
zoom: 7,
|
|
15
|
+
bearing: 0,
|
|
16
|
+
pitch: 0
|
|
17
|
+
};
|
|
18
|
+
/** One plugin's selection state. */
|
|
19
|
+
class PluginSelection {
|
|
20
|
+
mode = $state("none");
|
|
21
|
+
selection = $state.raw(null);
|
|
22
|
+
onselectionchange;
|
|
23
|
+
pickWith = $state("click");
|
|
24
|
+
canSelect;
|
|
25
|
+
}
|
|
26
|
+
function copyView(view) {
|
|
27
|
+
return { ...view, center: [view.center[0], view.center[1]] };
|
|
28
|
+
}
|
|
29
|
+
function copySelection(sel) {
|
|
30
|
+
return sel && { kind: sel.kind, ids: [...sel.ids] };
|
|
31
|
+
}
|
|
32
|
+
/** 8 hex characters. `crypto.randomUUID` is missing on insecure origins (a dev server on a LAN IP). */
|
|
33
|
+
function randomUid() {
|
|
34
|
+
const bytes = crypto.getRandomValues(new Uint8Array(4));
|
|
35
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
36
|
+
}
|
|
37
|
+
function sameSelection(a, b) {
|
|
38
|
+
if (!a || !b)
|
|
39
|
+
return a === b;
|
|
40
|
+
return (a.kind === b.kind && a.ids.length === b.ids.length && a.ids.every((id, i) => id === b.ids[i]));
|
|
41
|
+
}
|
|
42
|
+
const inRect = (p, b) => p[0] >= b.west && p[0] <= b.east && p[1] >= b.south && p[1] <= b.north;
|
|
43
|
+
function prefersReducedMotion() {
|
|
44
|
+
return globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;
|
|
45
|
+
}
|
|
46
|
+
class MapContextImpl {
|
|
47
|
+
uid = randomUid();
|
|
48
|
+
config;
|
|
49
|
+
metadata;
|
|
50
|
+
messages;
|
|
51
|
+
format;
|
|
52
|
+
basemap;
|
|
53
|
+
#camera = $state.raw(DEFAULT_VIEW);
|
|
54
|
+
#bounds = $state.raw(null);
|
|
55
|
+
#attached = $state(false);
|
|
56
|
+
#ready = $state(false);
|
|
57
|
+
/** `markReady` came while plugin setups were queued; the queued run sets `ready`. */
|
|
58
|
+
#readyRequested = false;
|
|
59
|
+
#glState = $state.raw(null);
|
|
60
|
+
#focusedKey = $state(null);
|
|
61
|
+
#focusedPluginId = $state(null);
|
|
62
|
+
#focusedAnchor = $state.raw(null);
|
|
63
|
+
#pinned = $state.raw(null);
|
|
64
|
+
#notice = $state.raw(null);
|
|
65
|
+
#noticeCount = 0;
|
|
66
|
+
#cardEnabled = false;
|
|
67
|
+
#selections = new SvelteMap();
|
|
68
|
+
#featureClickHandlers = new Set();
|
|
69
|
+
#router = new InteractionRouter();
|
|
70
|
+
#host;
|
|
71
|
+
#gl = null;
|
|
72
|
+
#destroyed = false;
|
|
73
|
+
#listInset = 0;
|
|
74
|
+
#listInsetSide = "left";
|
|
75
|
+
/** An animated ease that `focusFeature` started is still running (cleared on `moveend`). */
|
|
76
|
+
#focusEasing = false;
|
|
77
|
+
#onMoveEnd = () => {
|
|
78
|
+
this.#focusEasing = false;
|
|
79
|
+
};
|
|
80
|
+
constructor(options) {
|
|
81
|
+
const initialView = copyView(options.config.initialView ?? DEFAULT_VIEW);
|
|
82
|
+
this.config = {
|
|
83
|
+
...options.config,
|
|
84
|
+
initialView,
|
|
85
|
+
cooperativeGestures: options.config.cooperativeGestures ?? false
|
|
86
|
+
};
|
|
87
|
+
this.metadata = options.metadata ?? createDefaultMetadata();
|
|
88
|
+
this.messages = options.messages ?? defaultMessages;
|
|
89
|
+
this.format = options.format ?? createDefaultFormat();
|
|
90
|
+
this.basemap = new BasemapState(options.config.basemaps);
|
|
91
|
+
this.#camera = copyView(initialView);
|
|
92
|
+
this.#host = new PluginHost({
|
|
93
|
+
mapUid: this.uid,
|
|
94
|
+
providers: { metadata: this.metadata, messages: this.messages, format: this.format },
|
|
95
|
+
router: this.#router,
|
|
96
|
+
selectedKeys: (pluginId) => selectionKeys(this.getSelection(pluginId)),
|
|
97
|
+
onSettled: () => {
|
|
98
|
+
// A setup that added a plugin queued another run; that run settles it.
|
|
99
|
+
if (this.#host.pending)
|
|
100
|
+
return;
|
|
101
|
+
if (this.#readyRequested && this.#gl)
|
|
102
|
+
this.#ready = true;
|
|
103
|
+
this.#readyRequested = false;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
this.#router.onPick((e) => this.#pick(e));
|
|
107
|
+
this.#router.onMiss(() => this.unpin());
|
|
108
|
+
}
|
|
109
|
+
get camera() {
|
|
110
|
+
return this.#camera;
|
|
111
|
+
}
|
|
112
|
+
get bounds() {
|
|
113
|
+
return this.#bounds;
|
|
114
|
+
}
|
|
115
|
+
get plugins() {
|
|
116
|
+
return this.#host.plugins;
|
|
117
|
+
}
|
|
118
|
+
get attached() {
|
|
119
|
+
return this.#attached;
|
|
120
|
+
}
|
|
121
|
+
get ready() {
|
|
122
|
+
return this.#ready;
|
|
123
|
+
}
|
|
124
|
+
get gl() {
|
|
125
|
+
return this.#glState;
|
|
126
|
+
}
|
|
127
|
+
markReady() {
|
|
128
|
+
if (!this.#gl)
|
|
129
|
+
return;
|
|
130
|
+
if (this.#host.pending)
|
|
131
|
+
this.#readyRequested = true;
|
|
132
|
+
else
|
|
133
|
+
this.#ready = true;
|
|
134
|
+
}
|
|
135
|
+
get focusedKey() {
|
|
136
|
+
return this.#focusedKey;
|
|
137
|
+
}
|
|
138
|
+
get focusedPluginId() {
|
|
139
|
+
return this.#focusedPluginId;
|
|
140
|
+
}
|
|
141
|
+
get focusedAnchor() {
|
|
142
|
+
return this.#focusedAnchor;
|
|
143
|
+
}
|
|
144
|
+
get hovered() {
|
|
145
|
+
return this.#router.hovered;
|
|
146
|
+
}
|
|
147
|
+
get pinned() {
|
|
148
|
+
return this.#pinned;
|
|
149
|
+
}
|
|
150
|
+
get notice() {
|
|
151
|
+
return this.#notice;
|
|
152
|
+
}
|
|
153
|
+
clearNotice() {
|
|
154
|
+
if (untrack(() => this.#notice) !== null)
|
|
155
|
+
this.#notice = null;
|
|
156
|
+
}
|
|
157
|
+
getPickWith(pluginId) {
|
|
158
|
+
return this.#selections.get(pluginId)?.pickWith ?? "click";
|
|
159
|
+
}
|
|
160
|
+
togglePick(pluginId, ref) {
|
|
161
|
+
if (this.#destroyed)
|
|
162
|
+
return;
|
|
163
|
+
this.#applyPick(pluginId, ref, { toggle: true });
|
|
164
|
+
}
|
|
165
|
+
unpin() {
|
|
166
|
+
if (untrack(() => this.#pinned) !== null)
|
|
167
|
+
this.#pinned = null;
|
|
168
|
+
}
|
|
169
|
+
setCardEnabled(on) {
|
|
170
|
+
this.#cardEnabled = on;
|
|
171
|
+
if (!on)
|
|
172
|
+
this.unpin();
|
|
173
|
+
}
|
|
174
|
+
addPlugin(plugin, options = {}) {
|
|
175
|
+
this.#assertAlive();
|
|
176
|
+
const previous = untrack(() => this.#selections.get(plugin.id));
|
|
177
|
+
const state = new PluginSelection();
|
|
178
|
+
// Untracked: addPlugin may be called from inside an effect (e.g. a host component's).
|
|
179
|
+
untrack(() => {
|
|
180
|
+
state.mode = options.selectionMode ?? previous?.mode ?? "none";
|
|
181
|
+
state.selection =
|
|
182
|
+
options.selection !== undefined
|
|
183
|
+
? copySelection(options.selection)
|
|
184
|
+
: (previous?.selection ?? null);
|
|
185
|
+
});
|
|
186
|
+
state.onselectionchange = options.onselectionchange ?? previous?.onselectionchange;
|
|
187
|
+
untrack(() => {
|
|
188
|
+
state.pickWith = options.pickWith ?? previous?.pickWith ?? "click";
|
|
189
|
+
});
|
|
190
|
+
state.canSelect = options.canSelect ?? previous?.canSelect;
|
|
191
|
+
// Stored before the host adds it, so a setup that runs at once sees its selection.
|
|
192
|
+
this.#selections.set(plugin.id, state);
|
|
193
|
+
try {
|
|
194
|
+
this.#host.add(plugin, options.before);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (previous)
|
|
198
|
+
this.#selections.set(plugin.id, previous);
|
|
199
|
+
else
|
|
200
|
+
this.#selections.delete(plugin.id);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
movePlugin(id, options = {}) {
|
|
205
|
+
if (this.#destroyed)
|
|
206
|
+
return;
|
|
207
|
+
this.#host.move(id, options.before);
|
|
208
|
+
}
|
|
209
|
+
removePlugin(id) {
|
|
210
|
+
this.#host.remove(id);
|
|
211
|
+
this.#selections.delete(id);
|
|
212
|
+
if (untrack(() => this.#pinned)?.hit.pluginId === id)
|
|
213
|
+
this.unpin();
|
|
214
|
+
}
|
|
215
|
+
setSelection(pluginId, selection) {
|
|
216
|
+
if (this.#destroyed)
|
|
217
|
+
return;
|
|
218
|
+
this.#stateFor(pluginId).selection = copySelection(selection);
|
|
219
|
+
}
|
|
220
|
+
getSelection(pluginId) {
|
|
221
|
+
return this.#selections.get(pluginId)?.selection ?? null;
|
|
222
|
+
}
|
|
223
|
+
getSelectionMode(pluginId) {
|
|
224
|
+
return this.#selections.get(pluginId)?.mode ?? "none";
|
|
225
|
+
}
|
|
226
|
+
pickFromList(pluginId, refKey, additive) {
|
|
227
|
+
if (this.#destroyed)
|
|
228
|
+
return;
|
|
229
|
+
const found = untrack(() => this.#findItem(refKey, pluginId));
|
|
230
|
+
if (!found)
|
|
231
|
+
return;
|
|
232
|
+
const { item } = found;
|
|
233
|
+
// Where the row's tooltip pins: the focused anchor for the focused row, else a point of a
|
|
234
|
+
// shape in view (a point's own position for points and on-screen label points).
|
|
235
|
+
const focused = untrack(() => this.#focusedKey) === refKey &&
|
|
236
|
+
(untrack(() => this.#focusedPluginId) ?? pluginId) === pluginId;
|
|
237
|
+
const anchor = (focused && untrack(() => this.#focusedAnchor)) || this.anchorFor(item);
|
|
238
|
+
this.#emitClick(pluginId, item.ref, anchor);
|
|
239
|
+
if (item.selectable)
|
|
240
|
+
this.#applyPick(pluginId, item.ref, { additive, modifier: additive });
|
|
241
|
+
}
|
|
242
|
+
clearSelection(pluginId) {
|
|
243
|
+
if (this.#destroyed)
|
|
244
|
+
return;
|
|
245
|
+
const state = untrack(() => this.#selections.get(pluginId));
|
|
246
|
+
if (!state || !untrack(() => state.selection))
|
|
247
|
+
return;
|
|
248
|
+
state.selection = null;
|
|
249
|
+
state.onselectionchange?.(null);
|
|
250
|
+
}
|
|
251
|
+
setSelectionMode(pluginId, mode) {
|
|
252
|
+
if (this.#destroyed)
|
|
253
|
+
return;
|
|
254
|
+
this.#stateFor(pluginId).mode = mode;
|
|
255
|
+
}
|
|
256
|
+
on(event, handler) {
|
|
257
|
+
if (this.#destroyed)
|
|
258
|
+
return () => { };
|
|
259
|
+
this.#featureClickHandlers.add(handler);
|
|
260
|
+
return () => {
|
|
261
|
+
this.#featureClickHandlers.delete(handler);
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
featuresAt(point) {
|
|
265
|
+
return this.#router.hitsAt([point.x, point.y]);
|
|
266
|
+
}
|
|
267
|
+
featureForElement(element) {
|
|
268
|
+
const row = element.closest("[data-mapsdk-ref]");
|
|
269
|
+
if (!row || row.closest("[data-mapsdk-map]")?.getAttribute("data-mapsdk-map") !== this.uid) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const refKey = row.getAttribute("data-mapsdk-ref") ?? "";
|
|
273
|
+
const pluginId = row.getAttribute("data-mapsdk-plugin") ?? undefined;
|
|
274
|
+
const found = untrack(() => this.#findItem(refKey, pluginId));
|
|
275
|
+
if (!found)
|
|
276
|
+
return null;
|
|
277
|
+
const { plugin, item } = found;
|
|
278
|
+
return { pluginId: plugin.id, ref: item.ref, refKey: item.refKey, name: item.name };
|
|
279
|
+
}
|
|
280
|
+
focusFeature(refKey, pluginId) {
|
|
281
|
+
if (this.#destroyed)
|
|
282
|
+
return;
|
|
283
|
+
this.#focusedKey = refKey;
|
|
284
|
+
this.#focusedPluginId = refKey === null ? null : (pluginId ?? null);
|
|
285
|
+
this.#focusedAnchor = null;
|
|
286
|
+
const gl = this.#gl;
|
|
287
|
+
if (!gl || refKey === null)
|
|
288
|
+
return;
|
|
289
|
+
const found = untrack(() => this.#findItem(refKey, pluginId));
|
|
290
|
+
if (!found)
|
|
291
|
+
return;
|
|
292
|
+
const { plugin, item } = found;
|
|
293
|
+
const place = this.#placeFor(item, gl);
|
|
294
|
+
this.#focusedAnchor = place.anchor;
|
|
295
|
+
if (this.#cardEnabled) {
|
|
296
|
+
this.#pinned = {
|
|
297
|
+
hit: { pluginId: plugin.id, ref: item.ref, refKey: item.refKey, name: item.name },
|
|
298
|
+
lngLat: place.anchor
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
if (!place.pan) {
|
|
302
|
+
// Stays where it is now: an earlier focus's ease that is still running stops here.
|
|
303
|
+
// With nothing running, no move at all.
|
|
304
|
+
if (this.#focusEasing) {
|
|
305
|
+
const { lng, lat } = gl.getCenter();
|
|
306
|
+
gl.easeTo({ center: [lng, lat], duration: 0 });
|
|
307
|
+
this.#focusEasing = false;
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const reduce = prefersReducedMotion();
|
|
312
|
+
gl.easeTo({
|
|
313
|
+
center: place.anchor,
|
|
314
|
+
// The middle of the part the panel leaves clear. An offset, not padding: MapLibre keeps
|
|
315
|
+
// padding for later moves (zooms, the reported center), an offset applies to this one.
|
|
316
|
+
...(this.#listInset > 0 && { offset: [this.#listOffsetX(), 0] }),
|
|
317
|
+
...(reduce && { duration: 0 })
|
|
318
|
+
});
|
|
319
|
+
// Set after the call: stopping an earlier move fires its `moveend` inside `easeTo`.
|
|
320
|
+
this.#focusEasing = !reduce;
|
|
321
|
+
}
|
|
322
|
+
anchorFor(item) {
|
|
323
|
+
const gl = this.#gl;
|
|
324
|
+
if (!gl || !item.geometry)
|
|
325
|
+
return item.lngLat;
|
|
326
|
+
const { live, clear, liveCenter, clearCenter } = this.#views(gl);
|
|
327
|
+
if (inRect(item.lngLat, live) || !intersectsBounds(item, live))
|
|
328
|
+
return item.lngLat;
|
|
329
|
+
return (shapeAnchor(item.geometry, clear, clearCenter) ??
|
|
330
|
+
shapeAnchor(item.geometry, live, liveCenter) ??
|
|
331
|
+
item.lngLat);
|
|
332
|
+
}
|
|
333
|
+
/** The live view, and the part of it the list panel leaves clear, with their centers. */
|
|
334
|
+
#views(gl) {
|
|
335
|
+
const b = gl.getBounds();
|
|
336
|
+
const live = {
|
|
337
|
+
west: b.getWest(),
|
|
338
|
+
south: b.getSouth(),
|
|
339
|
+
east: b.getEast(),
|
|
340
|
+
north: b.getNorth()
|
|
341
|
+
};
|
|
342
|
+
const center = gl.getCenter();
|
|
343
|
+
const width = untrack(() => gl.getCanvas().clientWidth) || 0;
|
|
344
|
+
const share = width > 0 ? Math.min(Math.max(this.#listInset / width, 0), 0.99) : 0;
|
|
345
|
+
const cut = share * (live.east - live.west);
|
|
346
|
+
const clear = this.#listInsetSide === "left"
|
|
347
|
+
? { ...live, west: live.west + cut }
|
|
348
|
+
: { ...live, east: live.east - cut };
|
|
349
|
+
return {
|
|
350
|
+
live,
|
|
351
|
+
clear,
|
|
352
|
+
liveCenter: [center.lng, center.lat],
|
|
353
|
+
clearCenter: [(clear.west + clear.east) / 2, center.lat]
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Where focus puts `item`: `pan: false` keeps the camera (a shape in view whose `lngLat` is
|
|
358
|
+
* off-screen, with a point in the clear part); otherwise ease to center `anchor`.
|
|
359
|
+
*/
|
|
360
|
+
#placeFor(item, gl) {
|
|
361
|
+
const { geometry, lngLat } = item;
|
|
362
|
+
if (!geometry)
|
|
363
|
+
return { anchor: lngLat, pan: true };
|
|
364
|
+
const { live, clear, liveCenter, clearCenter } = this.#views(gl);
|
|
365
|
+
if (inRect(lngLat, live))
|
|
366
|
+
return { anchor: lngLat, pan: true };
|
|
367
|
+
if (intersectsBounds(item, live)) {
|
|
368
|
+
const inClear = shapeAnchor(geometry, clear, clearCenter);
|
|
369
|
+
if (inClear)
|
|
370
|
+
return { anchor: inClear, pan: false };
|
|
371
|
+
const inLive = shapeAnchor(geometry, live, liveCenter);
|
|
372
|
+
if (inLive)
|
|
373
|
+
return { anchor: inLive, pan: true };
|
|
374
|
+
}
|
|
375
|
+
const settled = untrack(() => this.#bounds);
|
|
376
|
+
if (settled && intersectsBounds(item, settled)) {
|
|
377
|
+
const middle = [
|
|
378
|
+
(settled.west + settled.east) / 2,
|
|
379
|
+
(settled.south + settled.north) / 2
|
|
380
|
+
];
|
|
381
|
+
const inSettled = shapeAnchor(geometry, settled, middle);
|
|
382
|
+
if (inSettled)
|
|
383
|
+
return { anchor: inSettled, pan: true };
|
|
384
|
+
}
|
|
385
|
+
return { anchor: lngLat, pan: true };
|
|
386
|
+
}
|
|
387
|
+
#cameraOptions(target, gl) {
|
|
388
|
+
const zoom = target.zoom ??
|
|
389
|
+
(target.minZoom === undefined ? undefined : Math.max(gl.getZoom(), target.minZoom));
|
|
390
|
+
return {
|
|
391
|
+
center: target.center,
|
|
392
|
+
...(zoom !== undefined && { zoom }),
|
|
393
|
+
...(prefersReducedMotion() && { duration: 0 })
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
easeTo(target) {
|
|
397
|
+
const gl = this.#destroyed ? null : this.#gl;
|
|
398
|
+
if (gl)
|
|
399
|
+
gl.easeTo(this.#cameraOptions(target, gl));
|
|
400
|
+
}
|
|
401
|
+
flyTo(target) {
|
|
402
|
+
const gl = this.#destroyed ? null : this.#gl;
|
|
403
|
+
if (gl)
|
|
404
|
+
gl.flyTo(this.#cameraOptions(target, gl));
|
|
405
|
+
}
|
|
406
|
+
fitBounds(bounds, options = {}) {
|
|
407
|
+
const gl = this.#destroyed ? null : this.#gl;
|
|
408
|
+
if (!gl)
|
|
409
|
+
return;
|
|
410
|
+
gl.fitBounds([
|
|
411
|
+
[bounds.west, bounds.south],
|
|
412
|
+
[bounds.east, bounds.north]
|
|
413
|
+
], {
|
|
414
|
+
...(options.padding !== undefined && { padding: options.padding }),
|
|
415
|
+
...(options.maxZoom !== undefined && { maxZoom: options.maxZoom }),
|
|
416
|
+
...(prefersReducedMotion() && { duration: 0 })
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
attach(gl) {
|
|
420
|
+
this.#assertAlive();
|
|
421
|
+
if (this.#gl === gl)
|
|
422
|
+
return;
|
|
423
|
+
if (this.#gl)
|
|
424
|
+
this.detach();
|
|
425
|
+
this.#gl = gl;
|
|
426
|
+
this.#glState = gl;
|
|
427
|
+
this.#host.attach(gl);
|
|
428
|
+
this.#router.attach(gl);
|
|
429
|
+
gl.on("moveend", this.#onMoveEnd);
|
|
430
|
+
this.basemap.attach(gl,
|
|
431
|
+
// Called from style.load: the style is loaded, though its tiles may not be.
|
|
432
|
+
() => this.#host.restyle(true), () => {
|
|
433
|
+
this.#ready = false;
|
|
434
|
+
this.#readyRequested = false;
|
|
435
|
+
this.#host.suspend();
|
|
436
|
+
});
|
|
437
|
+
this.#attached = true;
|
|
438
|
+
}
|
|
439
|
+
detach() {
|
|
440
|
+
if (!this.#gl)
|
|
441
|
+
return;
|
|
442
|
+
this.#gl.off("moveend", this.#onMoveEnd);
|
|
443
|
+
this.#focusEasing = false;
|
|
444
|
+
this.unpin();
|
|
445
|
+
this.clearNotice();
|
|
446
|
+
this.#router.detach();
|
|
447
|
+
this.basemap.detach();
|
|
448
|
+
this.#host.detach();
|
|
449
|
+
this.#gl = null;
|
|
450
|
+
this.#glState = null;
|
|
451
|
+
this.#attached = false;
|
|
452
|
+
this.#ready = false;
|
|
453
|
+
this.#readyRequested = false;
|
|
454
|
+
}
|
|
455
|
+
themeChanged() {
|
|
456
|
+
if (this.#gl)
|
|
457
|
+
this.#host.restyle();
|
|
458
|
+
}
|
|
459
|
+
/** Half the panel's width, toward the part of the map it leaves clear. */
|
|
460
|
+
#listOffsetX() {
|
|
461
|
+
return ((this.#listInsetSide === "left" ? 1 : -1) * this.#listInset) / 2;
|
|
462
|
+
}
|
|
463
|
+
setListInset(width, side = "left") {
|
|
464
|
+
this.#listInset = Math.max(0, width);
|
|
465
|
+
this.#listInsetSide = side;
|
|
466
|
+
}
|
|
467
|
+
updateView(camera, bounds) {
|
|
468
|
+
this.#camera = copyView(camera);
|
|
469
|
+
this.#bounds = { ...bounds };
|
|
470
|
+
}
|
|
471
|
+
destroy() {
|
|
472
|
+
if (this.#destroyed)
|
|
473
|
+
return;
|
|
474
|
+
this.detach();
|
|
475
|
+
for (const plugin of untrack(() => this.plugins))
|
|
476
|
+
this.#host.remove(plugin.id);
|
|
477
|
+
this.#selections.clear();
|
|
478
|
+
this.#featureClickHandlers.clear();
|
|
479
|
+
this.#focusedKey = null;
|
|
480
|
+
this.#focusedPluginId = null;
|
|
481
|
+
this.#focusedAnchor = null;
|
|
482
|
+
this.#destroyed = true;
|
|
483
|
+
}
|
|
484
|
+
/** A click on a feature: report it, then apply it to the top hit's plugin's selection. */
|
|
485
|
+
#pick(e) {
|
|
486
|
+
if (this.#destroyed)
|
|
487
|
+
return;
|
|
488
|
+
const [hit] = e.hits;
|
|
489
|
+
const { pluginId, ref } = hit;
|
|
490
|
+
// A box-zoom Shift-click only counts for a plugin that asked for modifier picks.
|
|
491
|
+
if (e.boxZoom && untrack(() => this.#selections.get(pluginId)?.pickWith) !== "modifier") {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
this.#emitClick(pluginId, ref, e.lngLat);
|
|
495
|
+
this.#applyPick(pluginId, ref, { additive: e.additive, modifier: e.modifier });
|
|
496
|
+
if (!this.#cardEnabled || this.#destroyed)
|
|
497
|
+
return;
|
|
498
|
+
// A feature with no hover card (e.g. a smoke plume) gets no pinned card either.
|
|
499
|
+
const plugin = untrack(() => this.plugins).find((p) => p.id === pluginId);
|
|
500
|
+
if (!plugin || plugin.hoverable === false)
|
|
501
|
+
this.unpin();
|
|
502
|
+
else
|
|
503
|
+
this.#pinned = { hit, lngLat: e.lngLat };
|
|
504
|
+
}
|
|
505
|
+
#emitClick(pluginId, ref, lngLat) {
|
|
506
|
+
for (const handler of [...this.#featureClickHandlers]) {
|
|
507
|
+
try {
|
|
508
|
+
handler({ pluginId, ref, lngLat });
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.error("map-sdk: a featureclick handler failed", error);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** The one place a user's pick (map click or list key) changes a plugin's selection. */
|
|
516
|
+
/**
|
|
517
|
+
* The one place a user's pick (map click, list key, or the card's toggle) changes a plugin's
|
|
518
|
+
* selection: the plugin's `pickWith` rule, then its `canSelect`.
|
|
519
|
+
*/
|
|
520
|
+
#applyPick(pluginId, ref, gesture) {
|
|
521
|
+
// A featureclick handler may have removed the plugin.
|
|
522
|
+
const state = untrack(() => this.#selections.get(pluginId));
|
|
523
|
+
if (!state)
|
|
524
|
+
return;
|
|
525
|
+
const current = untrack(() => state.selection);
|
|
526
|
+
const mode = untrack(() => state.mode);
|
|
527
|
+
let next;
|
|
528
|
+
if (gesture.toggle)
|
|
529
|
+
next = toggleInSelection(current, ref, mode);
|
|
530
|
+
else if (untrack(() => state.pickWith) === "modifier") {
|
|
531
|
+
// A plain click or tap only pins the card.
|
|
532
|
+
next = gesture.modifier ? toggleInSelection(current, ref, mode) : current;
|
|
533
|
+
}
|
|
534
|
+
else
|
|
535
|
+
next = applyPick(current, ref, mode, { additive: gesture.additive ?? false });
|
|
536
|
+
if (sameSelection(current, next)) {
|
|
537
|
+
this.clearNotice();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (state.canSelect) {
|
|
541
|
+
let verdict;
|
|
542
|
+
try {
|
|
543
|
+
verdict = state.canSelect(copySelection(next), ref);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
console.error("map-sdk: canSelect failed", error);
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (verdict !== true) {
|
|
550
|
+
// An empty (or, from untyped JS, false) verdict refuses quietly.
|
|
551
|
+
if (typeof verdict === "string" && verdict !== "") {
|
|
552
|
+
this.#notice = { message: verdict, id: ++this.#noticeCount };
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
this.clearNotice();
|
|
558
|
+
state.selection = next;
|
|
559
|
+
state.onselectionchange?.(copySelection(next));
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* A listed item and its plugin: in `pluginId` when given (`null` if that plugin doesn't list
|
|
563
|
+
* it), else the first plugin listing `refKey`. Reads `features()`, so call it untracked.
|
|
564
|
+
*/
|
|
565
|
+
#findItem(refKey, pluginId) {
|
|
566
|
+
for (const plugin of this.plugins) {
|
|
567
|
+
if (pluginId !== undefined && plugin.id !== pluginId)
|
|
568
|
+
continue;
|
|
569
|
+
const item = plugin.features?.().find((f) => f.refKey === refKey);
|
|
570
|
+
if (item)
|
|
571
|
+
return { plugin, item };
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
#stateFor(pluginId) {
|
|
576
|
+
let state = untrack(() => this.#selections.get(pluginId));
|
|
577
|
+
if (!state) {
|
|
578
|
+
state = new PluginSelection();
|
|
579
|
+
this.#selections.set(pluginId, state);
|
|
580
|
+
}
|
|
581
|
+
return state;
|
|
582
|
+
}
|
|
583
|
+
#assertAlive() {
|
|
584
|
+
if (this.#destroyed)
|
|
585
|
+
throw new Error("map destroyed");
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/** Creates a map's state and services. `MapView` attaches a GL map to it. */
|
|
589
|
+
export function createMap(options) {
|
|
590
|
+
return new MapContextImpl(options);
|
|
591
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { CustomLayerInterface, LayerSpecification, SourceSpecification } from "@maptiler/sdk";
|
|
2
|
+
import type { Geometry } from "geojson";
|
|
3
|
+
/**
|
|
4
|
+
* The subset of MapLibre's `Map` the core and plugins use. Types only, so nothing in core/
|
|
5
|
+
* pulls the map engine at runtime, and tests can pass an in-memory fake.
|
|
6
|
+
*/
|
|
7
|
+
export interface GlMapLike {
|
|
8
|
+
addSource(id: string, source: SourceSpecification): void;
|
|
9
|
+
removeSource(id: string): void;
|
|
10
|
+
getSource(id: string): unknown;
|
|
11
|
+
addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string): void;
|
|
12
|
+
/** Moves a layer below `beforeId`, or to the top without it. */
|
|
13
|
+
moveLayer(id: string, beforeId?: string): void;
|
|
14
|
+
removeLayer(id: string): void;
|
|
15
|
+
getLayer(id: string): unknown;
|
|
16
|
+
addImage(id: string, image: HTMLImageElement | ImageData, options?: {
|
|
17
|
+
pixelRatio?: number;
|
|
18
|
+
}): void;
|
|
19
|
+
hasImage(id: string): boolean;
|
|
20
|
+
removeImage(id: string): void;
|
|
21
|
+
setFeatureState(target: {
|
|
22
|
+
source: string;
|
|
23
|
+
id: string | number;
|
|
24
|
+
}, state: Record<string, unknown>): void;
|
|
25
|
+
setLayoutProperty(layerId: string, name: string, value: unknown): void;
|
|
26
|
+
removeFeatureState(target: {
|
|
27
|
+
source: string;
|
|
28
|
+
id?: string | number;
|
|
29
|
+
}, key?: string): void;
|
|
30
|
+
queryRenderedFeatures(point: [number, number] | [[number, number], [number, number]], options?: {
|
|
31
|
+
layers?: string[];
|
|
32
|
+
}): Array<{
|
|
33
|
+
layer: {
|
|
34
|
+
id: string;
|
|
35
|
+
};
|
|
36
|
+
source: string;
|
|
37
|
+
id?: string | number;
|
|
38
|
+
properties: Record<string, unknown>;
|
|
39
|
+
geometry: Geometry;
|
|
40
|
+
}>;
|
|
41
|
+
isStyleLoaded(): boolean | void;
|
|
42
|
+
setStyle(style: unknown, options?: {
|
|
43
|
+
diff?: boolean;
|
|
44
|
+
}): void;
|
|
45
|
+
on(type: string, listener: (e: any) => void): void;
|
|
46
|
+
on(type: string, layerId: string, listener: (e: any) => void): void;
|
|
47
|
+
off(type: string, listener: (e: any) => void): void;
|
|
48
|
+
off(type: string, layerId: string, listener: (e: any) => void): void;
|
|
49
|
+
getBounds(): {
|
|
50
|
+
getWest(): number;
|
|
51
|
+
getSouth(): number;
|
|
52
|
+
getEast(): number;
|
|
53
|
+
getNorth(): number;
|
|
54
|
+
};
|
|
55
|
+
getCenter(): {
|
|
56
|
+
lng: number;
|
|
57
|
+
lat: number;
|
|
58
|
+
};
|
|
59
|
+
getZoom(): number;
|
|
60
|
+
getBearing(): number;
|
|
61
|
+
getPitch(): number;
|
|
62
|
+
easeTo(options: {
|
|
63
|
+
center?: [number, number];
|
|
64
|
+
zoom?: number;
|
|
65
|
+
duration?: number;
|
|
66
|
+
/** Where `center` ends up, in pixels from the container's center (positive x is right). */
|
|
67
|
+
offset?: [number, number];
|
|
68
|
+
}): void;
|
|
69
|
+
flyTo(options: {
|
|
70
|
+
center?: [number, number];
|
|
71
|
+
zoom?: number;
|
|
72
|
+
duration?: number;
|
|
73
|
+
}): void;
|
|
74
|
+
fitBounds(bounds: [[number, number], [number, number]], options?: {
|
|
75
|
+
padding?: number;
|
|
76
|
+
maxZoom?: number;
|
|
77
|
+
duration?: number;
|
|
78
|
+
}): void;
|
|
79
|
+
jumpTo(options: {
|
|
80
|
+
center?: [number, number];
|
|
81
|
+
zoom?: number;
|
|
82
|
+
bearing?: number;
|
|
83
|
+
pitch?: number;
|
|
84
|
+
}): void;
|
|
85
|
+
/** Canvas pixel for a position. */
|
|
86
|
+
project(lngLat: [number, number]): {
|
|
87
|
+
x: number;
|
|
88
|
+
y: number;
|
|
89
|
+
};
|
|
90
|
+
/** Position at a canvas pixel. */
|
|
91
|
+
unproject(point: [number, number]): {
|
|
92
|
+
lng: number;
|
|
93
|
+
lat: number;
|
|
94
|
+
};
|
|
95
|
+
getCanvas(): HTMLCanvasElement;
|
|
96
|
+
remove(): void;
|
|
97
|
+
}
|
package/dist/core/gl.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|