@tileflow/interactions 0.0.0-bootstrap.0
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/GENERATED_OUTPUT_LICENSE.md +21 -0
- package/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +95 -0
- package/TRADEMARKS.md +10 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +639 -0
- package/dist/maplibre-entry.d.ts +298 -0
- package/dist/maplibre-entry.js +2892 -0
- package/dist/validation-Cy6Tz3lz.d.ts +506 -0
- package/package.json +65 -0
|
@@ -0,0 +1,2892 @@
|
|
|
1
|
+
// src/maplibre.ts
|
|
2
|
+
function createTileflowAnnotationRegistry(adapter) {
|
|
3
|
+
let currentEntries = [];
|
|
4
|
+
let currentEntriesByKey = /* @__PURE__ */ new Map();
|
|
5
|
+
const retiredEntries = /* @__PURE__ */ new Set();
|
|
6
|
+
let disposed = false;
|
|
7
|
+
let mutating = false;
|
|
8
|
+
const publicEntries = () => snapshotEntries(currentEntries);
|
|
9
|
+
const reconcile = (definitions) => {
|
|
10
|
+
assertRegistryCanMutate(disposed, mutating);
|
|
11
|
+
mutating = true;
|
|
12
|
+
try {
|
|
13
|
+
const requested = collectRequestedDefinitions(definitions, adapter.getKey);
|
|
14
|
+
const previousEntries = currentEntries;
|
|
15
|
+
const nextEntries = [];
|
|
16
|
+
const nextEntriesByKey = /* @__PURE__ */ new Map();
|
|
17
|
+
const createdEntries = [];
|
|
18
|
+
const completedUpdates = [];
|
|
19
|
+
let failureAlreadyRolledBack = false;
|
|
20
|
+
try {
|
|
21
|
+
for (const { definition, key } of requested) {
|
|
22
|
+
const previous = currentEntriesByKey.get(key);
|
|
23
|
+
if (previous) {
|
|
24
|
+
const next2 = { definition, instance: previous.instance, key };
|
|
25
|
+
nextEntries.push(next2);
|
|
26
|
+
nextEntriesByKey.set(key, next2);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const instance = adapter.create(adapter.context, definition, key);
|
|
30
|
+
const next = { definition, instance, key };
|
|
31
|
+
createdEntries.push(next);
|
|
32
|
+
nextEntries.push(next);
|
|
33
|
+
nextEntriesByKey.set(key, next);
|
|
34
|
+
}
|
|
35
|
+
for (const entry of nextEntries) {
|
|
36
|
+
const previous = currentEntriesByKey.get(entry.key);
|
|
37
|
+
if (!previous || !shouldUpdateDefinition(adapter, previous.definition, entry.definition, entry.key)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const completed = {
|
|
41
|
+
entry: previous,
|
|
42
|
+
next: entry.definition
|
|
43
|
+
};
|
|
44
|
+
completedUpdates.push(completed);
|
|
45
|
+
completed.rollback = adapter.update(
|
|
46
|
+
previous.instance,
|
|
47
|
+
adapter.context,
|
|
48
|
+
entry.definition,
|
|
49
|
+
previous.definition,
|
|
50
|
+
entry.key
|
|
51
|
+
) ?? void 0;
|
|
52
|
+
}
|
|
53
|
+
if (adapter.reorder && !haveSameKeyOrder(previousEntries, nextEntries)) {
|
|
54
|
+
const nextSnapshot = snapshotEntries(nextEntries);
|
|
55
|
+
const previousSnapshot = snapshotEntries(previousEntries);
|
|
56
|
+
try {
|
|
57
|
+
adapter.reorder(adapter.context, nextSnapshot, previousSnapshot);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const rollbackErrors = safelyRun(
|
|
60
|
+
() => adapter.reorder?.(adapter.context, previousSnapshot, nextSnapshot)
|
|
61
|
+
);
|
|
62
|
+
rollbackErrors.push(...rollbackCompletedUpdates(adapter, completedUpdates));
|
|
63
|
+
rollbackErrors.push(...retireAndClean(adapter, retiredEntries, createdEntries));
|
|
64
|
+
failureAlreadyRolledBack = true;
|
|
65
|
+
throwRollbackFailure(error, rollbackErrors);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (failureAlreadyRolledBack) throw error;
|
|
70
|
+
const rollbackErrors = rollbackCompletedUpdates(adapter, completedUpdates);
|
|
71
|
+
rollbackErrors.push(...retireAndClean(adapter, retiredEntries, createdEntries));
|
|
72
|
+
throwRollbackFailure(error, rollbackErrors);
|
|
73
|
+
}
|
|
74
|
+
currentEntries = nextEntries;
|
|
75
|
+
currentEntriesByKey = nextEntriesByKey;
|
|
76
|
+
for (const entry of previousEntries) {
|
|
77
|
+
if (!nextEntriesByKey.has(entry.key)) retiredEntries.add(entry);
|
|
78
|
+
}
|
|
79
|
+
throwCleanupFailures(cleanRetiredEntries(adapter, retiredEntries));
|
|
80
|
+
} finally {
|
|
81
|
+
mutating = false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const dispose = () => {
|
|
85
|
+
if (mutating) {
|
|
86
|
+
throw new Error("Tileflow annotation registry mutation is already in progress.");
|
|
87
|
+
}
|
|
88
|
+
if (disposed && retiredEntries.size === 0) return;
|
|
89
|
+
mutating = true;
|
|
90
|
+
try {
|
|
91
|
+
if (!disposed) {
|
|
92
|
+
disposed = true;
|
|
93
|
+
for (const entry of currentEntries) retiredEntries.add(entry);
|
|
94
|
+
currentEntries = [];
|
|
95
|
+
currentEntriesByKey = /* @__PURE__ */ new Map();
|
|
96
|
+
}
|
|
97
|
+
throwCleanupFailures(cleanRetiredEntries(adapter, retiredEntries));
|
|
98
|
+
} finally {
|
|
99
|
+
mutating = false;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
get size() {
|
|
104
|
+
return currentEntries.length;
|
|
105
|
+
},
|
|
106
|
+
clear() {
|
|
107
|
+
if (disposed) return;
|
|
108
|
+
reconcile([]);
|
|
109
|
+
},
|
|
110
|
+
dispose,
|
|
111
|
+
entries: publicEntries,
|
|
112
|
+
get(key) {
|
|
113
|
+
const entry = currentEntriesByKey.get(key);
|
|
114
|
+
return entry ? freezeEntry(entry) : void 0;
|
|
115
|
+
},
|
|
116
|
+
reconcile
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function collectRequestedDefinitions(definitions, getKey) {
|
|
120
|
+
const keys = /* @__PURE__ */ new Set();
|
|
121
|
+
const requested = [];
|
|
122
|
+
for (const definition of definitions) {
|
|
123
|
+
const key = getKey(definition);
|
|
124
|
+
if (keys.has(key)) {
|
|
125
|
+
throw new TypeError("Duplicate Tileflow annotation key.");
|
|
126
|
+
}
|
|
127
|
+
keys.add(key);
|
|
128
|
+
requested.push({ definition, key });
|
|
129
|
+
}
|
|
130
|
+
return requested;
|
|
131
|
+
}
|
|
132
|
+
function shouldUpdateDefinition(adapter, previous, next, key) {
|
|
133
|
+
return adapter.shouldUpdate?.(previous, next, key) ?? !Object.is(previous, next);
|
|
134
|
+
}
|
|
135
|
+
function rollbackCompletedUpdates(adapter, updates) {
|
|
136
|
+
const errors = [];
|
|
137
|
+
for (const update of [...updates].reverse()) {
|
|
138
|
+
errors.push(
|
|
139
|
+
...safelyRun(() => {
|
|
140
|
+
if (update.rollback) {
|
|
141
|
+
update.rollback();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
adapter.update(
|
|
145
|
+
update.entry.instance,
|
|
146
|
+
adapter.context,
|
|
147
|
+
update.entry.definition,
|
|
148
|
+
update.next,
|
|
149
|
+
update.entry.key
|
|
150
|
+
);
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return errors;
|
|
155
|
+
}
|
|
156
|
+
function retireAndClean(adapter, retiredEntries, entries) {
|
|
157
|
+
for (const entry of entries) retiredEntries.add(entry);
|
|
158
|
+
return cleanRetiredEntries(adapter, retiredEntries);
|
|
159
|
+
}
|
|
160
|
+
function cleanRetiredEntries(adapter, retiredEntries) {
|
|
161
|
+
const errors = [];
|
|
162
|
+
for (const entry of retiredEntries) {
|
|
163
|
+
try {
|
|
164
|
+
adapter.remove(entry.instance, adapter.context, entry.definition, entry.key);
|
|
165
|
+
retiredEntries.delete(entry);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
errors.push(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return errors;
|
|
171
|
+
}
|
|
172
|
+
function haveSameKeyOrder(previous, next) {
|
|
173
|
+
return previous.length === next.length && previous.every((entry, index) => sameValueZero(entry.key, next[index]?.key));
|
|
174
|
+
}
|
|
175
|
+
function sameValueZero(left, right) {
|
|
176
|
+
return left === right || left !== left && right !== right;
|
|
177
|
+
}
|
|
178
|
+
function snapshotEntries(entries) {
|
|
179
|
+
return Object.freeze(entries.map(freezeEntry));
|
|
180
|
+
}
|
|
181
|
+
function freezeEntry(entry) {
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
definition: entry.definition,
|
|
184
|
+
instance: entry.instance,
|
|
185
|
+
key: entry.key
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function assertRegistryCanMutate(disposed, mutating) {
|
|
189
|
+
if (disposed) throw new Error("Tileflow annotation registry is disposed.");
|
|
190
|
+
if (mutating) throw new Error("Tileflow annotation registry mutation is already in progress.");
|
|
191
|
+
}
|
|
192
|
+
function throwRollbackFailure(error, rollbackErrors) {
|
|
193
|
+
if (rollbackErrors.length === 0) throw error;
|
|
194
|
+
throw new AggregateError(
|
|
195
|
+
[error, ...rollbackErrors],
|
|
196
|
+
"Tileflow annotation reconciliation failed and rollback reported additional errors.",
|
|
197
|
+
{ cause: error }
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
function throwCleanupFailures(errors) {
|
|
201
|
+
if (errors.length === 0) return;
|
|
202
|
+
throw new AggregateError(errors, "Unable to remove one or more Tileflow annotation instances.");
|
|
203
|
+
}
|
|
204
|
+
function safelyRun(operation) {
|
|
205
|
+
try {
|
|
206
|
+
operation();
|
|
207
|
+
return [];
|
|
208
|
+
} catch (error) {
|
|
209
|
+
return [error];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function createTileflowOverlayStateController(options = {}) {
|
|
213
|
+
const areTargetsEqual = options.areTargetsEqual ?? Object.is;
|
|
214
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
215
|
+
if (options.onChange) listeners.add(options.onChange);
|
|
216
|
+
const initialPopup = options.initialPopup === void 0 ? null : options.initialPopup;
|
|
217
|
+
let initialTooltip = options.initialTooltip === void 0 ? null : options.initialTooltip;
|
|
218
|
+
if (initialPopup !== null && initialTooltip !== null && areTargetsEqual(initialPopup, initialTooltip)) {
|
|
219
|
+
initialTooltip = null;
|
|
220
|
+
}
|
|
221
|
+
let state = overlayState(initialPopup, initialTooltip);
|
|
222
|
+
let disposed = false;
|
|
223
|
+
const commit = (popup, tooltip, reason) => {
|
|
224
|
+
if (disposed || sameTarget(state.popup, popup, areTargetsEqual) && sameTarget(state.tooltip, tooltip, areTargetsEqual)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
const previous = state;
|
|
228
|
+
state = overlayState(popup, tooltip);
|
|
229
|
+
notifyOverlayListeners(listeners, state, previous, reason);
|
|
230
|
+
return true;
|
|
231
|
+
};
|
|
232
|
+
return {
|
|
233
|
+
clearTarget(target) {
|
|
234
|
+
const popup = targetMatches(state.popup, target, areTargetsEqual) ? null : state.popup;
|
|
235
|
+
const tooltip = targetMatches(state.tooltip, target, areTargetsEqual) ? null : state.tooltip;
|
|
236
|
+
return commit(popup, tooltip, "target:remove");
|
|
237
|
+
},
|
|
238
|
+
dispose() {
|
|
239
|
+
if (disposed) return;
|
|
240
|
+
const previous = state;
|
|
241
|
+
disposed = true;
|
|
242
|
+
state = overlayState(null, null);
|
|
243
|
+
try {
|
|
244
|
+
if (previous.popup !== null || previous.tooltip !== null) {
|
|
245
|
+
notifyOverlayListeners(listeners, state, previous, "dispose");
|
|
246
|
+
}
|
|
247
|
+
} finally {
|
|
248
|
+
listeners.clear();
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
getState() {
|
|
252
|
+
return state;
|
|
253
|
+
},
|
|
254
|
+
reconcileTargets(isAvailable) {
|
|
255
|
+
if (disposed) return false;
|
|
256
|
+
const popup = state.popup !== null && !isAvailable(state.popup) ? null : state.popup;
|
|
257
|
+
const tooltip = state.tooltip !== null && !isAvailable(state.tooltip) ? null : state.tooltip;
|
|
258
|
+
return commit(popup, tooltip, "targets:reconcile");
|
|
259
|
+
},
|
|
260
|
+
setPopup(target) {
|
|
261
|
+
const tooltip = target !== null && targetMatches(state.tooltip, target, areTargetsEqual) ? null : state.tooltip;
|
|
262
|
+
return commit(target, tooltip, target === null ? "popup:close" : "popup:open");
|
|
263
|
+
},
|
|
264
|
+
setTooltip(target) {
|
|
265
|
+
if (target !== null && targetMatches(state.popup, target, areTargetsEqual)) return false;
|
|
266
|
+
return commit(state.popup, target, target === null ? "tooltip:close" : "tooltip:open");
|
|
267
|
+
},
|
|
268
|
+
subscribe(listener) {
|
|
269
|
+
if (disposed) return () => {
|
|
270
|
+
};
|
|
271
|
+
listeners.add(listener);
|
|
272
|
+
let subscribed = true;
|
|
273
|
+
return () => {
|
|
274
|
+
if (!subscribed) return;
|
|
275
|
+
subscribed = false;
|
|
276
|
+
listeners.delete(listener);
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function overlayState(popup, tooltip) {
|
|
282
|
+
return Object.freeze({ popup, tooltip });
|
|
283
|
+
}
|
|
284
|
+
function targetMatches(current, target, areTargetsEqual) {
|
|
285
|
+
return current !== null && areTargetsEqual(current, target);
|
|
286
|
+
}
|
|
287
|
+
function sameTarget(left, right, areTargetsEqual) {
|
|
288
|
+
if (left === null || right === null) return left === right;
|
|
289
|
+
return areTargetsEqual(left, right);
|
|
290
|
+
}
|
|
291
|
+
function notifyOverlayListeners(listeners, state, previous, reason) {
|
|
292
|
+
const errors = [];
|
|
293
|
+
for (const listener of [...listeners]) {
|
|
294
|
+
try {
|
|
295
|
+
listener(state, previous, reason);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
errors.push(error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (errors.length === 1) throw errors[0];
|
|
301
|
+
if (errors.length > 1) {
|
|
302
|
+
throw new AggregateError(errors, "Tileflow overlay state listeners failed.");
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/maplibre-dom.ts
|
|
307
|
+
function normalizeTileflowLegacyMarkers(markers) {
|
|
308
|
+
const titles = /* @__PURE__ */ new Map();
|
|
309
|
+
const annotations = markers.map((marker) => {
|
|
310
|
+
const title = marker.label ?? marker.id;
|
|
311
|
+
titles.set(marker.id, title);
|
|
312
|
+
return {
|
|
313
|
+
ariaLabel: title.trim().length > 0 ? title : marker.id,
|
|
314
|
+
coordinate: marker.coordinates,
|
|
315
|
+
id: marker.id,
|
|
316
|
+
kind: "marker",
|
|
317
|
+
...marker.color === void 0 ? {} : { marker: { color: marker.color } }
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
return Object.freeze({ annotations: Object.freeze(annotations), titles });
|
|
321
|
+
}
|
|
322
|
+
var runtimeSequence = 0;
|
|
323
|
+
function createTileflowMapLibreDomRuntime(options) {
|
|
324
|
+
const document = options.document;
|
|
325
|
+
const idPrefix = options.idPrefix ?? `tileflow-interactions-${++runtimeSequence}`;
|
|
326
|
+
assertIdPrefix(idPrefix);
|
|
327
|
+
const controlled = options.interactionState !== void 0;
|
|
328
|
+
let customRenderers = Object.freeze({
|
|
329
|
+
marker: options.customMarker ?? false,
|
|
330
|
+
popup: options.customPopup ?? false,
|
|
331
|
+
tooltip: options.customTooltip ?? false
|
|
332
|
+
});
|
|
333
|
+
let interactionState = options.interactionState ?? options.defaultInteractionState ?? createInteractionState(null);
|
|
334
|
+
let pendingStateRequest = null;
|
|
335
|
+
let missingControlledPopupNotified = null;
|
|
336
|
+
const eventListeners = /* @__PURE__ */ new Set();
|
|
337
|
+
const diagnosticListeners = /* @__PURE__ */ new Set();
|
|
338
|
+
const renderTargetListeners = /* @__PURE__ */ new Set();
|
|
339
|
+
const activeDiagnostics = /* @__PURE__ */ new Map();
|
|
340
|
+
let diagnosticSnapshot = Object.freeze([]);
|
|
341
|
+
let renderTargets = Object.freeze([]);
|
|
342
|
+
let activeTooltip = null;
|
|
343
|
+
let activePopup = null;
|
|
344
|
+
let reconciling = false;
|
|
345
|
+
let disposeStarted = false;
|
|
346
|
+
let disposeComplete = false;
|
|
347
|
+
let overlaySequence = 0;
|
|
348
|
+
const emit = (type, annotation, inputModality2) => {
|
|
349
|
+
const event = Object.freeze({
|
|
350
|
+
coordinate: annotation.coordinate,
|
|
351
|
+
...inputModality2 === void 0 ? {} : { inputModality: inputModality2 },
|
|
352
|
+
target: Object.freeze({
|
|
353
|
+
annotation,
|
|
354
|
+
coordinate: annotation.coordinate,
|
|
355
|
+
kind: "annotation"
|
|
356
|
+
}),
|
|
357
|
+
type
|
|
358
|
+
});
|
|
359
|
+
notifyListeners(eventListeners, event, "Tileflow interaction event listeners failed.");
|
|
360
|
+
};
|
|
361
|
+
const publishRenderTargets = () => {
|
|
362
|
+
if (reconciling || disposeComplete) return;
|
|
363
|
+
renderTargets = buildRenderTargets();
|
|
364
|
+
notifyListeners(
|
|
365
|
+
renderTargetListeners,
|
|
366
|
+
renderTargets,
|
|
367
|
+
"Tileflow render target listeners failed."
|
|
368
|
+
);
|
|
369
|
+
};
|
|
370
|
+
const publishDiagnostics = () => {
|
|
371
|
+
if (reconciling || disposeComplete) return;
|
|
372
|
+
diagnosticSnapshot = Object.freeze([...activeDiagnostics.values()]);
|
|
373
|
+
notifyListeners(
|
|
374
|
+
diagnosticListeners,
|
|
375
|
+
diagnosticSnapshot,
|
|
376
|
+
"Tileflow diagnostic listeners failed."
|
|
377
|
+
);
|
|
378
|
+
};
|
|
379
|
+
const closeTooltipVisual = (annotationId) => {
|
|
380
|
+
const current = activeTooltip;
|
|
381
|
+
if (!current || annotationId !== void 0 && current.annotation.id !== annotationId) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
activeTooltip = null;
|
|
385
|
+
clearSurfaceDiagnostics(current.annotation.id, "tooltip");
|
|
386
|
+
const marker = registry.get(current.annotation.id)?.instance;
|
|
387
|
+
marker?.element.removeAttribute("aria-describedby");
|
|
388
|
+
const errors = safelyRunAll([() => current.overlay.remove(), () => current.shell.remove()]);
|
|
389
|
+
publishRenderTargets();
|
|
390
|
+
throwCollectedErrors(errors, "Unable to close the Tileflow tooltip.");
|
|
391
|
+
return true;
|
|
392
|
+
};
|
|
393
|
+
const closePopupVisual = (returnFocus, inputModality2, annotationId) => {
|
|
394
|
+
const current = activePopup;
|
|
395
|
+
if (!current || annotationId !== void 0 && current.annotation.id !== annotationId) {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
398
|
+
activePopup = null;
|
|
399
|
+
clearSurfaceDiagnostics(current.annotation.id, "popup");
|
|
400
|
+
const marker = registry.get(current.annotation.id)?.instance;
|
|
401
|
+
if (current.keydownListener) {
|
|
402
|
+
current.shell.removeEventListener("keydown", current.keydownListener);
|
|
403
|
+
}
|
|
404
|
+
marker?.element.setAttribute("aria-expanded", "false");
|
|
405
|
+
marker?.element.removeAttribute("aria-controls");
|
|
406
|
+
const errors = safelyRunAll([() => current.overlay.remove(), () => current.shell.remove()]);
|
|
407
|
+
publishRenderTargets();
|
|
408
|
+
emit("popup:close", current.annotation, inputModality2);
|
|
409
|
+
if (returnFocus && marker && !marker.removed) {
|
|
410
|
+
errors.push(...safelyRunAll([() => marker.element.focus({ preventScroll: true })]));
|
|
411
|
+
}
|
|
412
|
+
throwCollectedErrors(errors, "Unable to close the Tileflow popup.");
|
|
413
|
+
return true;
|
|
414
|
+
};
|
|
415
|
+
const updateActiveOverlay = (current, annotation) => {
|
|
416
|
+
const previousAnnotation = current.annotation;
|
|
417
|
+
current.annotation = annotation;
|
|
418
|
+
current.overlay.setLngLat(copyCoordinate(annotation.coordinate));
|
|
419
|
+
options.updateOverlay?.(current.overlay, {
|
|
420
|
+
annotation,
|
|
421
|
+
container: current.shell,
|
|
422
|
+
kind: current.kind,
|
|
423
|
+
previousAnnotation
|
|
424
|
+
});
|
|
425
|
+
configureOverlayContent(current, annotation);
|
|
426
|
+
};
|
|
427
|
+
const openTooltipVisual = (annotationId) => {
|
|
428
|
+
const entry = registry.get(annotationId);
|
|
429
|
+
if (!entry?.definition.tooltip || activePopup?.annotation.id === annotationId) return false;
|
|
430
|
+
if (activeTooltip?.annotation.id === annotationId) {
|
|
431
|
+
if (!Object.is(activeTooltip.annotation, entry.definition)) {
|
|
432
|
+
try {
|
|
433
|
+
updateActiveOverlay(activeTooltip, entry.definition);
|
|
434
|
+
clearOverlayFailure(entry.definition.id, "tooltip");
|
|
435
|
+
} catch {
|
|
436
|
+
reportOverlayFailure(entry.definition, "tooltip");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
closeTooltipVisual();
|
|
442
|
+
const shell = document.createElement("div");
|
|
443
|
+
const container = document.createElement("div");
|
|
444
|
+
shell.className = "tileflow-interaction-tooltip";
|
|
445
|
+
container.className = "tileflow-interaction-tooltip-content";
|
|
446
|
+
shell.id = domId(idPrefix, "tooltip", ++overlaySequence);
|
|
447
|
+
shell.setAttribute("data-tileflow-interaction", "tooltip");
|
|
448
|
+
shell.setAttribute("data-tileflow-target-kind", "annotation");
|
|
449
|
+
shell.setAttribute("role", "tooltip");
|
|
450
|
+
shell.style.pointerEvents = "none";
|
|
451
|
+
configureSurfaceStyles(shell);
|
|
452
|
+
shell.append(container);
|
|
453
|
+
let overlay;
|
|
454
|
+
try {
|
|
455
|
+
overlay = options.createOverlay({
|
|
456
|
+
annotation: entry.definition,
|
|
457
|
+
container: shell,
|
|
458
|
+
kind: "tooltip"
|
|
459
|
+
});
|
|
460
|
+
overlay.setLngLat(copyCoordinate(entry.definition.coordinate));
|
|
461
|
+
overlay.addTo(options.map);
|
|
462
|
+
} catch {
|
|
463
|
+
safelyRunAll([() => overlay?.remove(), () => shell.remove()]);
|
|
464
|
+
reportOverlayFailure(entry.definition, "tooltip");
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
activeTooltip = {
|
|
468
|
+
annotation: entry.definition,
|
|
469
|
+
container,
|
|
470
|
+
kind: "tooltip",
|
|
471
|
+
overlay,
|
|
472
|
+
shell
|
|
473
|
+
};
|
|
474
|
+
configureOverlayContent(activeTooltip, entry.definition);
|
|
475
|
+
clearOverlayFailure(entry.definition.id, "tooltip");
|
|
476
|
+
entry.instance.element.setAttribute("aria-describedby", shell.id);
|
|
477
|
+
publishRenderTargets();
|
|
478
|
+
return true;
|
|
479
|
+
};
|
|
480
|
+
const openPopupVisual = (annotationId, inputModality2) => {
|
|
481
|
+
const entry = registry.get(annotationId);
|
|
482
|
+
if (!entry?.definition.popup) return false;
|
|
483
|
+
if (activePopup?.annotation.id === annotationId) {
|
|
484
|
+
if (!Object.is(activePopup.annotation, entry.definition)) {
|
|
485
|
+
try {
|
|
486
|
+
updateActiveOverlay(activePopup, entry.definition);
|
|
487
|
+
clearOverlayFailure(entry.definition.id, "popup");
|
|
488
|
+
} catch {
|
|
489
|
+
reportOverlayFailure(entry.definition, "popup");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
closePopupVisual(false, inputModality2);
|
|
495
|
+
closeTooltipVisual(annotationId);
|
|
496
|
+
const shell = document.createElement("div");
|
|
497
|
+
const container = document.createElement("div");
|
|
498
|
+
const closeButton = document.createElement("button");
|
|
499
|
+
shell.className = "tileflow-interaction-popup";
|
|
500
|
+
container.className = "tileflow-interaction-popup-content";
|
|
501
|
+
closeButton.className = "tileflow-interaction-popup-close";
|
|
502
|
+
closeButton.type = "button";
|
|
503
|
+
closeButton.textContent = "Close";
|
|
504
|
+
shell.id = domId(idPrefix, "popup", ++overlaySequence);
|
|
505
|
+
shell.setAttribute("data-tileflow-interaction", "popup");
|
|
506
|
+
shell.setAttribute("data-tileflow-target-kind", "annotation");
|
|
507
|
+
shell.setAttribute("aria-label", entry.definition.ariaLabel);
|
|
508
|
+
shell.setAttribute("aria-modal", "false");
|
|
509
|
+
shell.setAttribute("role", "dialog");
|
|
510
|
+
closeButton.setAttribute("aria-label", `Close ${entry.definition.ariaLabel}`);
|
|
511
|
+
configureSurfaceStyles(shell);
|
|
512
|
+
closeButton.style.background = "transparent";
|
|
513
|
+
closeButton.style.border = "0";
|
|
514
|
+
closeButton.style.color = "inherit";
|
|
515
|
+
closeButton.style.cursor = "pointer";
|
|
516
|
+
shell.append(container, closeButton);
|
|
517
|
+
const closeListener = () => requestPopupState(null, "pointer");
|
|
518
|
+
const keydownListener = (event) => {
|
|
519
|
+
if (event.key !== "Escape") return;
|
|
520
|
+
event.preventDefault();
|
|
521
|
+
event.stopPropagation();
|
|
522
|
+
requestPopupState(null, "keyboard");
|
|
523
|
+
};
|
|
524
|
+
closeButton.addEventListener("click", closeListener);
|
|
525
|
+
shell.addEventListener("keydown", keydownListener);
|
|
526
|
+
let overlay;
|
|
527
|
+
try {
|
|
528
|
+
overlay = options.createOverlay({
|
|
529
|
+
annotation: entry.definition,
|
|
530
|
+
container: shell,
|
|
531
|
+
kind: "popup"
|
|
532
|
+
});
|
|
533
|
+
overlay.setLngLat(copyCoordinate(entry.definition.coordinate));
|
|
534
|
+
overlay.addTo(options.map);
|
|
535
|
+
} catch {
|
|
536
|
+
closeButton.removeEventListener("click", closeListener);
|
|
537
|
+
shell.removeEventListener("keydown", keydownListener);
|
|
538
|
+
safelyRunAll([() => overlay?.remove(), () => shell.remove()]);
|
|
539
|
+
reportOverlayFailure(entry.definition, "popup");
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
activePopup = {
|
|
543
|
+
annotation: entry.definition,
|
|
544
|
+
closeButton,
|
|
545
|
+
container,
|
|
546
|
+
keydownListener,
|
|
547
|
+
kind: "popup",
|
|
548
|
+
overlay,
|
|
549
|
+
shell
|
|
550
|
+
};
|
|
551
|
+
configureOverlayContent(activePopup, entry.definition);
|
|
552
|
+
clearOverlayFailure(entry.definition.id, "popup");
|
|
553
|
+
entry.instance.element.setAttribute("aria-controls", shell.id);
|
|
554
|
+
entry.instance.element.setAttribute("aria-expanded", "true");
|
|
555
|
+
publishRenderTargets();
|
|
556
|
+
emit("popup:open", entry.definition, inputModality2);
|
|
557
|
+
closeButton.focus({ preventScroll: true });
|
|
558
|
+
return true;
|
|
559
|
+
};
|
|
560
|
+
const syncPopupFromInteractionState = (inputModality2, returnFocusWhenClosing) => {
|
|
561
|
+
const popupId = getAnnotationPopupId(interactionState);
|
|
562
|
+
if (popupId === null) {
|
|
563
|
+
closePopupVisual(returnFocusWhenClosing, inputModality2);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const entry = registry.get(popupId);
|
|
567
|
+
if (!entry?.definition.popup) {
|
|
568
|
+
closePopupVisual(false, inputModality2);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
openPopupVisual(popupId, inputModality2);
|
|
572
|
+
};
|
|
573
|
+
const notifyStateRequest = (next, previous, reason) => {
|
|
574
|
+
options.onInteractionStateChange?.(next, previous, reason);
|
|
575
|
+
};
|
|
576
|
+
function requestPopupState(popupId, inputModality2) {
|
|
577
|
+
const currentPopupId = getAnnotationPopupId(interactionState);
|
|
578
|
+
if (popupId === null ? interactionState.popup === null && activePopup === null : currentPopupId === popupId && activePopup?.annotation.id === popupId) {
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
if (popupId !== null && !registry.get(popupId)?.definition.popup) return false;
|
|
582
|
+
const previous = interactionState;
|
|
583
|
+
const next = createInteractionState(
|
|
584
|
+
popupId === null ? null : Object.freeze({ id: popupId, kind: "annotation" })
|
|
585
|
+
);
|
|
586
|
+
missingControlledPopupNotified = null;
|
|
587
|
+
if (controlled) {
|
|
588
|
+
pendingStateRequest = { inputModality: inputModality2, popupId };
|
|
589
|
+
notifyStateRequest(next, previous, popupId === null ? "popup:close" : "popup:open");
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
interactionState = next;
|
|
593
|
+
syncPopupFromInteractionState(inputModality2, popupId === null);
|
|
594
|
+
notifyStateRequest(next, previous, popupId === null ? "popup:close" : "popup:open");
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
const reconcileOverlayAvailability = () => {
|
|
598
|
+
if (activeTooltip) {
|
|
599
|
+
const tooltipEntry = registry.get(activeTooltip.annotation.id);
|
|
600
|
+
if (!tooltipEntry?.definition.tooltip) closeTooltipVisual();
|
|
601
|
+
else if (!Object.is(activeTooltip.annotation, tooltipEntry.definition)) {
|
|
602
|
+
updateActiveOverlay(activeTooltip, tooltipEntry.definition);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
const popupId = getAnnotationPopupId(interactionState);
|
|
606
|
+
if (popupId === null) {
|
|
607
|
+
closePopupVisual(false, "programmatic");
|
|
608
|
+
missingControlledPopupNotified = null;
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
const popupEntry = registry.get(popupId);
|
|
612
|
+
if (popupEntry?.definition.popup) {
|
|
613
|
+
missingControlledPopupNotified = null;
|
|
614
|
+
syncPopupFromInteractionState("programmatic", false);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
closePopupVisual(false, "programmatic");
|
|
618
|
+
if (controlled) {
|
|
619
|
+
if (missingControlledPopupNotified === popupId) return;
|
|
620
|
+
missingControlledPopupNotified = popupId;
|
|
621
|
+
notifyStateRequest(createInteractionState(null), interactionState, "target:remove");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const previous = interactionState;
|
|
625
|
+
interactionState = createInteractionState(null);
|
|
626
|
+
notifyStateRequest(interactionState, previous, "target:remove");
|
|
627
|
+
};
|
|
628
|
+
const closeAnnotationOverlays = (annotationId, inputModality2) => {
|
|
629
|
+
closeTooltipVisual(annotationId);
|
|
630
|
+
if (getAnnotationPopupId(interactionState) === annotationId) {
|
|
631
|
+
requestPopupState(null, inputModality2);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
closePopupVisual(true, inputModality2, annotationId);
|
|
635
|
+
};
|
|
636
|
+
const clearContentDiagnostic = (annotationId, surface, code) => {
|
|
637
|
+
if (!activeDiagnostics.delete(diagnosticKey(annotationId, surface, code))) return;
|
|
638
|
+
publishDiagnostics();
|
|
639
|
+
};
|
|
640
|
+
const reportContentDiagnostic = (annotation, surface, code) => {
|
|
641
|
+
const key = diagnosticKey(annotation.id, surface, code);
|
|
642
|
+
if (activeDiagnostics.has(key)) return;
|
|
643
|
+
const diagnostic2 = Object.freeze({
|
|
644
|
+
code,
|
|
645
|
+
level: "error",
|
|
646
|
+
message: code === "INVALID_FIELD" ? "Annotation field content did not resolve to a scalar value." : "Annotation view content has no custom renderer for its active surface.",
|
|
647
|
+
target: Object.freeze({ id: annotation.id, kind: "annotation" })
|
|
648
|
+
});
|
|
649
|
+
activeDiagnostics.set(key, diagnostic2);
|
|
650
|
+
options.onDiagnostic?.(diagnostic2);
|
|
651
|
+
publishDiagnostics();
|
|
652
|
+
};
|
|
653
|
+
const clearOverlayFailure = (annotationId, surface) => {
|
|
654
|
+
if (!activeDiagnostics.delete(overlayDiagnosticKey(annotationId, surface))) return;
|
|
655
|
+
publishDiagnostics();
|
|
656
|
+
};
|
|
657
|
+
const reportOverlayFailure = (annotation, surface) => {
|
|
658
|
+
const key = overlayDiagnosticKey(annotation.id, surface);
|
|
659
|
+
if (activeDiagnostics.has(key)) return;
|
|
660
|
+
const diagnostic2 = Object.freeze({
|
|
661
|
+
code: "OVERLAY_FAILURE",
|
|
662
|
+
level: "error",
|
|
663
|
+
message: `Tileflow could not mount or update the annotation ${surface} overlay.`,
|
|
664
|
+
target: Object.freeze({ id: annotation.id, kind: "annotation" })
|
|
665
|
+
});
|
|
666
|
+
activeDiagnostics.set(key, diagnostic2);
|
|
667
|
+
options.onDiagnostic?.(diagnostic2);
|
|
668
|
+
publishDiagnostics();
|
|
669
|
+
};
|
|
670
|
+
const resolveAnnotationContent = (annotation, content, surface) => {
|
|
671
|
+
if (content.kind === "view") {
|
|
672
|
+
clearContentDiagnostic(annotation.id, surface, "INVALID_FIELD");
|
|
673
|
+
if (customRenderers[surface]) {
|
|
674
|
+
clearContentDiagnostic(annotation.id, surface, "MISSING_VIEW");
|
|
675
|
+
} else {
|
|
676
|
+
reportContentDiagnostic(annotation, surface, "MISSING_VIEW");
|
|
677
|
+
}
|
|
678
|
+
return "";
|
|
679
|
+
}
|
|
680
|
+
clearContentDiagnostic(annotation.id, surface, "MISSING_VIEW");
|
|
681
|
+
const resolution = resolveContent(content, annotation.data);
|
|
682
|
+
if (resolution.invalidField) {
|
|
683
|
+
reportContentDiagnostic(annotation, surface, "INVALID_FIELD");
|
|
684
|
+
} else {
|
|
685
|
+
clearContentDiagnostic(annotation.id, surface, "INVALID_FIELD");
|
|
686
|
+
}
|
|
687
|
+
return resolution.text;
|
|
688
|
+
};
|
|
689
|
+
const clearAnnotationDiagnostics = (annotationId) => {
|
|
690
|
+
const prefix = `${annotationId}\0`;
|
|
691
|
+
let changed = false;
|
|
692
|
+
for (const key of activeDiagnostics.keys()) {
|
|
693
|
+
if (!key.startsWith(prefix)) continue;
|
|
694
|
+
activeDiagnostics.delete(key);
|
|
695
|
+
changed = true;
|
|
696
|
+
}
|
|
697
|
+
if (changed) publishDiagnostics();
|
|
698
|
+
};
|
|
699
|
+
function clearSurfaceDiagnostics(annotationId, surface) {
|
|
700
|
+
const removedInvalidField = activeDiagnostics.delete(
|
|
701
|
+
diagnosticKey(annotationId, surface, "INVALID_FIELD")
|
|
702
|
+
);
|
|
703
|
+
const removedMissingView = activeDiagnostics.delete(
|
|
704
|
+
diagnosticKey(annotationId, surface, "MISSING_VIEW")
|
|
705
|
+
);
|
|
706
|
+
if (removedInvalidField || removedMissingView) publishDiagnostics();
|
|
707
|
+
}
|
|
708
|
+
const configureAnnotationElement = (instance, annotation) => {
|
|
709
|
+
instance.annotation = annotation;
|
|
710
|
+
instance.element.setAttribute("aria-label", annotation.ariaLabel);
|
|
711
|
+
instance.element.setAttribute("data-tileflow-interaction", "marker");
|
|
712
|
+
instance.element.setAttribute("data-tileflow-target-kind", "annotation");
|
|
713
|
+
instance.element.title = annotation.ariaLabel;
|
|
714
|
+
instance.element.style.cursor = annotation.popup ? "pointer" : "default";
|
|
715
|
+
if (annotation.popup) {
|
|
716
|
+
instance.element.setAttribute("aria-haspopup", "dialog");
|
|
717
|
+
instance.element.setAttribute("role", "button");
|
|
718
|
+
instance.element.tabIndex = 0;
|
|
719
|
+
if (activePopup?.annotation.id !== annotation.id) {
|
|
720
|
+
instance.element.setAttribute("aria-expanded", "false");
|
|
721
|
+
}
|
|
722
|
+
} else {
|
|
723
|
+
instance.element.removeAttribute("aria-controls");
|
|
724
|
+
instance.element.removeAttribute("aria-expanded");
|
|
725
|
+
instance.element.removeAttribute("aria-haspopup");
|
|
726
|
+
instance.element.setAttribute("role", "img");
|
|
727
|
+
instance.element.tabIndex = annotation.tooltip ? 0 : -1;
|
|
728
|
+
}
|
|
729
|
+
if (annotation.marker?.color) {
|
|
730
|
+
instance.element.style.setProperty(
|
|
731
|
+
"--tileflow-interaction-marker-color",
|
|
732
|
+
annotation.marker.color
|
|
733
|
+
);
|
|
734
|
+
} else {
|
|
735
|
+
instance.element.style.removeProperty("--tileflow-interaction-marker-color");
|
|
736
|
+
}
|
|
737
|
+
const markerText = annotation.marker?.content ? resolveAnnotationContent(annotation, annotation.marker.content, "marker") : "\u2022";
|
|
738
|
+
if (!annotation.marker?.content) {
|
|
739
|
+
clearContentDiagnostic(annotation.id, "marker", "INVALID_FIELD");
|
|
740
|
+
clearContentDiagnostic(annotation.id, "marker", "MISSING_VIEW");
|
|
741
|
+
}
|
|
742
|
+
if (!customRenderers.marker) {
|
|
743
|
+
instance.container.textContent = markerText;
|
|
744
|
+
configureDefaultMarkerStyles(instance.container);
|
|
745
|
+
} else {
|
|
746
|
+
clearDefaultMarkerStyles(instance.container);
|
|
747
|
+
}
|
|
748
|
+
configureViewAttribute(instance.container, annotation.marker?.content);
|
|
749
|
+
};
|
|
750
|
+
const applyAnnotation = (instance, annotation, previousAnnotation) => {
|
|
751
|
+
configureAnnotationElement(instance, annotation);
|
|
752
|
+
instance.marker.setLngLat(copyCoordinate(annotation.coordinate));
|
|
753
|
+
options.updateMarker?.(instance.marker, {
|
|
754
|
+
annotation,
|
|
755
|
+
element: instance.element,
|
|
756
|
+
previousAnnotation
|
|
757
|
+
});
|
|
758
|
+
if (activeTooltip?.annotation.id === annotation.id && annotation.tooltip) {
|
|
759
|
+
try {
|
|
760
|
+
updateActiveOverlay(activeTooltip, annotation);
|
|
761
|
+
clearOverlayFailure(annotation.id, "tooltip");
|
|
762
|
+
} catch (error) {
|
|
763
|
+
reportOverlayFailure(annotation, "tooltip");
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (activePopup?.annotation.id === annotation.id && annotation.popup) {
|
|
768
|
+
try {
|
|
769
|
+
updateActiveOverlay(activePopup, annotation);
|
|
770
|
+
clearOverlayFailure(annotation.id, "popup");
|
|
771
|
+
} catch (error) {
|
|
772
|
+
reportOverlayFailure(annotation, "popup");
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
const createAnnotationInstance = (annotation) => {
|
|
778
|
+
const element = document.createElement("div");
|
|
779
|
+
const container = document.createElement("div");
|
|
780
|
+
element.className = "tileflow-interaction-marker";
|
|
781
|
+
container.className = "tileflow-interaction-marker-content";
|
|
782
|
+
element.append(container);
|
|
783
|
+
configureAnnotationElement({ annotation, container, element }, annotation);
|
|
784
|
+
const marker = options.createMarker({ annotation, element });
|
|
785
|
+
const instance = {
|
|
786
|
+
annotation,
|
|
787
|
+
container,
|
|
788
|
+
element,
|
|
789
|
+
focused: false,
|
|
790
|
+
hovered: false,
|
|
791
|
+
marker,
|
|
792
|
+
removed: false,
|
|
793
|
+
removeListeners: []
|
|
794
|
+
};
|
|
795
|
+
const removeListeners = [];
|
|
796
|
+
const listen = (type, listener) => {
|
|
797
|
+
element.addEventListener(type, listener);
|
|
798
|
+
removeListeners.push(() => element.removeEventListener(type, listener));
|
|
799
|
+
};
|
|
800
|
+
listen("pointerenter", () => {
|
|
801
|
+
instance.hovered = true;
|
|
802
|
+
emit("target:enter", instance.annotation, "pointer");
|
|
803
|
+
openTooltipVisual(instance.annotation.id);
|
|
804
|
+
});
|
|
805
|
+
listen("pointerleave", () => {
|
|
806
|
+
instance.hovered = false;
|
|
807
|
+
emit("target:leave", instance.annotation, "pointer");
|
|
808
|
+
if (!instance.focused) closeTooltipVisual(instance.annotation.id);
|
|
809
|
+
});
|
|
810
|
+
listen("focus", () => {
|
|
811
|
+
instance.focused = true;
|
|
812
|
+
emit("target:focus", instance.annotation, "keyboard");
|
|
813
|
+
openTooltipVisual(instance.annotation.id);
|
|
814
|
+
});
|
|
815
|
+
listen("blur", () => {
|
|
816
|
+
instance.focused = false;
|
|
817
|
+
emit("target:blur", instance.annotation, "keyboard");
|
|
818
|
+
if (!instance.hovered) closeTooltipVisual(instance.annotation.id);
|
|
819
|
+
});
|
|
820
|
+
listen("click", () => {
|
|
821
|
+
if (!instance.annotation.popup) return;
|
|
822
|
+
emit("target:activate", instance.annotation, "pointer");
|
|
823
|
+
requestPopupState(instance.annotation.id, "pointer");
|
|
824
|
+
});
|
|
825
|
+
listen("keydown", (event) => {
|
|
826
|
+
if (event.key === "Escape") {
|
|
827
|
+
const didCloseTooltip = closeTooltipVisual(instance.annotation.id);
|
|
828
|
+
const didRequestPopupClose = activePopup?.annotation.id === instance.annotation.id ? requestPopupState(null, "keyboard") : false;
|
|
829
|
+
if (didCloseTooltip || didRequestPopupClose) {
|
|
830
|
+
event.preventDefault();
|
|
831
|
+
event.stopPropagation();
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!instance.annotation.popup || event.key !== "Enter" && event.key !== " " && event.code !== "Space") {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
event.preventDefault();
|
|
839
|
+
event.stopPropagation();
|
|
840
|
+
emit("target:activate", instance.annotation, "keyboard");
|
|
841
|
+
requestPopupState(instance.annotation.id, "keyboard");
|
|
842
|
+
});
|
|
843
|
+
instance.removeListeners = removeListeners;
|
|
844
|
+
try {
|
|
845
|
+
marker.setLngLat(copyCoordinate(annotation.coordinate));
|
|
846
|
+
marker.addTo(options.map);
|
|
847
|
+
} catch (error) {
|
|
848
|
+
for (const removeListener of removeListeners) removeListener();
|
|
849
|
+
safelyRunAll([() => marker.remove(), () => element.remove()]);
|
|
850
|
+
throw error;
|
|
851
|
+
}
|
|
852
|
+
return instance;
|
|
853
|
+
};
|
|
854
|
+
const removeAnnotationInstance = (instance) => {
|
|
855
|
+
if (instance.removed) return;
|
|
856
|
+
closeTooltipVisual(instance.annotation.id);
|
|
857
|
+
closePopupVisual(false, "programmatic", instance.annotation.id);
|
|
858
|
+
const errors = safelyRunAll([
|
|
859
|
+
...instance.removeListeners,
|
|
860
|
+
() => instance.marker.remove(),
|
|
861
|
+
() => instance.element.remove()
|
|
862
|
+
]);
|
|
863
|
+
if (errors.length === 0) instance.removed = true;
|
|
864
|
+
clearAnnotationDiagnostics(instance.annotation.id);
|
|
865
|
+
throwCollectedErrors(
|
|
866
|
+
errors,
|
|
867
|
+
`Unable to remove Tileflow annotation "${instance.annotation.id}".`
|
|
868
|
+
);
|
|
869
|
+
};
|
|
870
|
+
const registry = createTileflowAnnotationRegistry({
|
|
871
|
+
context: void 0,
|
|
872
|
+
create: (_context, annotation) => createAnnotationInstance(annotation),
|
|
873
|
+
getKey: (annotation) => annotation.id,
|
|
874
|
+
remove: (instance) => removeAnnotationInstance(instance),
|
|
875
|
+
reorder: options.reorderMarkers ? (_context, entries, previousEntries) => {
|
|
876
|
+
options.reorderMarkers?.(
|
|
877
|
+
entries.map((entry) => entry.instance.marker),
|
|
878
|
+
entries.map((entry) => entry.definition),
|
|
879
|
+
previousEntries.map((entry) => entry.instance.marker),
|
|
880
|
+
previousEntries.map((entry) => entry.definition)
|
|
881
|
+
);
|
|
882
|
+
} : void 0,
|
|
883
|
+
update: (instance, _context, annotation, previousAnnotation) => {
|
|
884
|
+
applyAnnotation(instance, annotation, previousAnnotation);
|
|
885
|
+
return () => applyAnnotation(instance, previousAnnotation, annotation);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
function configureOverlayContent(overlay, annotation) {
|
|
889
|
+
if (overlay.kind === "tooltip") {
|
|
890
|
+
configureViewAttribute(overlay.container, annotation.tooltip?.content);
|
|
891
|
+
if (annotation.tooltip) {
|
|
892
|
+
const text = resolveAnnotationContent(annotation, annotation.tooltip.content, "tooltip");
|
|
893
|
+
if (!customRenderers.tooltip) overlay.container.textContent = text;
|
|
894
|
+
}
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
overlay.shell.setAttribute("aria-label", annotation.ariaLabel);
|
|
898
|
+
overlay.closeButton?.setAttribute("aria-label", `Close ${annotation.ariaLabel}`);
|
|
899
|
+
configureViewAttribute(overlay.container, annotation.popup?.content);
|
|
900
|
+
if (annotation.popup) {
|
|
901
|
+
const text = resolveAnnotationContent(annotation, annotation.popup.content, "popup");
|
|
902
|
+
if (!customRenderers.popup) overlay.container.textContent = text;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function buildRenderTargets() {
|
|
906
|
+
const targets = [];
|
|
907
|
+
if (customRenderers.marker) {
|
|
908
|
+
for (const entry of registry.entries()) {
|
|
909
|
+
const annotationId = entry.definition.id;
|
|
910
|
+
targets.push(
|
|
911
|
+
Object.freeze({
|
|
912
|
+
annotation: entry.definition,
|
|
913
|
+
close: () => closeAnnotationOverlays(annotationId, "programmatic"),
|
|
914
|
+
container: entry.instance.container,
|
|
915
|
+
key: renderTargetKey("marker", annotationId),
|
|
916
|
+
kind: "marker"
|
|
917
|
+
})
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (customRenderers.tooltip && activeTooltip) {
|
|
922
|
+
const annotationId = activeTooltip.annotation.id;
|
|
923
|
+
targets.push(
|
|
924
|
+
Object.freeze({
|
|
925
|
+
annotation: activeTooltip.annotation,
|
|
926
|
+
close: () => closeTooltipVisual(annotationId),
|
|
927
|
+
container: activeTooltip.container,
|
|
928
|
+
key: renderTargetKey("tooltip", annotationId),
|
|
929
|
+
kind: "tooltip"
|
|
930
|
+
})
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (customRenderers.popup && activePopup) {
|
|
934
|
+
const annotationId = activePopup.annotation.id;
|
|
935
|
+
targets.push(
|
|
936
|
+
Object.freeze({
|
|
937
|
+
annotation: activePopup.annotation,
|
|
938
|
+
close: () => requestPopupState(null, "programmatic"),
|
|
939
|
+
container: activePopup.container,
|
|
940
|
+
key: renderTargetKey("popup", annotationId),
|
|
941
|
+
kind: "popup"
|
|
942
|
+
})
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
return Object.freeze(targets);
|
|
946
|
+
}
|
|
947
|
+
return {
|
|
948
|
+
closePopup() {
|
|
949
|
+
assertRuntimeActive(disposeStarted);
|
|
950
|
+
return requestPopupState(null, "programmatic");
|
|
951
|
+
},
|
|
952
|
+
dispose() {
|
|
953
|
+
if (disposeComplete) return;
|
|
954
|
+
if (!disposeStarted) {
|
|
955
|
+
disposeStarted = true;
|
|
956
|
+
reconciling = true;
|
|
957
|
+
const errors = safelyRunAll([
|
|
958
|
+
() => closeTooltipVisual(),
|
|
959
|
+
() => closePopupVisual(false, "programmatic")
|
|
960
|
+
]);
|
|
961
|
+
reconciling = false;
|
|
962
|
+
renderTargets = Object.freeze([]);
|
|
963
|
+
activeDiagnostics.clear();
|
|
964
|
+
diagnosticSnapshot = Object.freeze([]);
|
|
965
|
+
notifyListeners(
|
|
966
|
+
diagnosticListeners,
|
|
967
|
+
diagnosticSnapshot,
|
|
968
|
+
"Tileflow diagnostic listeners failed.",
|
|
969
|
+
errors
|
|
970
|
+
);
|
|
971
|
+
notifyListeners(
|
|
972
|
+
renderTargetListeners,
|
|
973
|
+
renderTargets,
|
|
974
|
+
"Tileflow render target listeners failed.",
|
|
975
|
+
errors
|
|
976
|
+
);
|
|
977
|
+
renderTargetListeners.clear();
|
|
978
|
+
diagnosticListeners.clear();
|
|
979
|
+
eventListeners.clear();
|
|
980
|
+
throwCollectedErrors(errors, "Unable to begin Tileflow interaction runtime disposal.");
|
|
981
|
+
}
|
|
982
|
+
registry.dispose();
|
|
983
|
+
disposeComplete = true;
|
|
984
|
+
},
|
|
985
|
+
getInteractionState() {
|
|
986
|
+
return interactionState;
|
|
987
|
+
},
|
|
988
|
+
getDiagnostics() {
|
|
989
|
+
return diagnosticSnapshot;
|
|
990
|
+
},
|
|
991
|
+
getRenderTargets() {
|
|
992
|
+
return renderTargets;
|
|
993
|
+
},
|
|
994
|
+
openPopup(annotationId) {
|
|
995
|
+
assertRuntimeActive(disposeStarted);
|
|
996
|
+
return requestPopupState(annotationId, "programmatic");
|
|
997
|
+
},
|
|
998
|
+
reconcile(annotations) {
|
|
999
|
+
assertRuntimeActive(disposeStarted);
|
|
1000
|
+
reconciling = true;
|
|
1001
|
+
try {
|
|
1002
|
+
registry.reconcile(annotations);
|
|
1003
|
+
} finally {
|
|
1004
|
+
reconciling = false;
|
|
1005
|
+
reconcileOverlayAvailability();
|
|
1006
|
+
publishDiagnostics();
|
|
1007
|
+
publishRenderTargets();
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
setCustomRenderers(renderers) {
|
|
1011
|
+
assertRuntimeActive(disposeStarted);
|
|
1012
|
+
if (renderers.marker === customRenderers.marker && renderers.tooltip === customRenderers.tooltip && renderers.popup === customRenderers.popup) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
reconciling = true;
|
|
1016
|
+
try {
|
|
1017
|
+
const previous = customRenderers;
|
|
1018
|
+
customRenderers = Object.freeze({ ...renderers });
|
|
1019
|
+
if (!previous.marker && customRenderers.marker) {
|
|
1020
|
+
for (const entry of registry.entries()) entry.instance.container.textContent = "";
|
|
1021
|
+
}
|
|
1022
|
+
if (!previous.tooltip && customRenderers.tooltip && activeTooltip) {
|
|
1023
|
+
activeTooltip.container.textContent = "";
|
|
1024
|
+
}
|
|
1025
|
+
if (!previous.popup && customRenderers.popup && activePopup) {
|
|
1026
|
+
activePopup.container.textContent = "";
|
|
1027
|
+
}
|
|
1028
|
+
for (const entry of registry.entries()) {
|
|
1029
|
+
configureAnnotationElement(entry.instance, entry.definition);
|
|
1030
|
+
}
|
|
1031
|
+
if (activeTooltip) configureOverlayContent(activeTooltip, activeTooltip.annotation);
|
|
1032
|
+
if (activePopup) configureOverlayContent(activePopup, activePopup.annotation);
|
|
1033
|
+
} finally {
|
|
1034
|
+
reconciling = false;
|
|
1035
|
+
publishDiagnostics();
|
|
1036
|
+
publishRenderTargets();
|
|
1037
|
+
}
|
|
1038
|
+
},
|
|
1039
|
+
setInteractionState(state) {
|
|
1040
|
+
assertRuntimeActive(disposeStarted);
|
|
1041
|
+
const popupId = getAnnotationPopupId(state);
|
|
1042
|
+
const inputModality2 = pendingStateRequest?.popupId === popupId ? pendingStateRequest.inputModality : "programmatic";
|
|
1043
|
+
pendingStateRequest = null;
|
|
1044
|
+
interactionState = state;
|
|
1045
|
+
missingControlledPopupNotified = null;
|
|
1046
|
+
syncPopupFromInteractionState(inputModality2, popupId === null);
|
|
1047
|
+
},
|
|
1048
|
+
subscribeEvents(listener) {
|
|
1049
|
+
assertRuntimeActive(disposeStarted);
|
|
1050
|
+
eventListeners.add(listener);
|
|
1051
|
+
return createUnsubscribe(eventListeners, listener);
|
|
1052
|
+
},
|
|
1053
|
+
subscribeDiagnostics(listener) {
|
|
1054
|
+
assertRuntimeActive(disposeStarted);
|
|
1055
|
+
diagnosticListeners.add(listener);
|
|
1056
|
+
return createUnsubscribe(diagnosticListeners, listener);
|
|
1057
|
+
},
|
|
1058
|
+
subscribeRenderTargets(listener) {
|
|
1059
|
+
assertRuntimeActive(disposeStarted);
|
|
1060
|
+
renderTargetListeners.add(listener);
|
|
1061
|
+
return createUnsubscribe(renderTargetListeners, listener);
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
function createInteractionState(popup) {
|
|
1066
|
+
return Object.freeze({ popup });
|
|
1067
|
+
}
|
|
1068
|
+
function getAnnotationPopupId(state) {
|
|
1069
|
+
return state.popup?.kind === "annotation" ? state.popup.id : null;
|
|
1070
|
+
}
|
|
1071
|
+
function copyCoordinate(coordinate) {
|
|
1072
|
+
return [coordinate[0], coordinate[1]];
|
|
1073
|
+
}
|
|
1074
|
+
function renderTargetKey(kind, annotationId) {
|
|
1075
|
+
return `${kind}:${annotationId}`;
|
|
1076
|
+
}
|
|
1077
|
+
function domId(prefix, kind, sequence) {
|
|
1078
|
+
return `${prefix}-${kind}-${sequence}`;
|
|
1079
|
+
}
|
|
1080
|
+
function assertIdPrefix(idPrefix) {
|
|
1081
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/u.test(idPrefix)) {
|
|
1082
|
+
throw new TypeError("Tileflow DOM idPrefix must be a portable identifier.");
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function assertRuntimeActive(disposed) {
|
|
1086
|
+
if (disposed) throw new Error("Tileflow MapLibre DOM runtime is disposed.");
|
|
1087
|
+
}
|
|
1088
|
+
function configureViewAttribute(element, content) {
|
|
1089
|
+
if (content?.kind === "view") {
|
|
1090
|
+
element.setAttribute("data-tileflow-view", content.name);
|
|
1091
|
+
} else {
|
|
1092
|
+
element.removeAttribute("data-tileflow-view");
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
function configureSurfaceStyles(element) {
|
|
1096
|
+
element.style.background = "var(--tileflow-interaction-surface, #ffffff)";
|
|
1097
|
+
element.style.borderColor = "var(--tileflow-interaction-border-color, #d1d5db)";
|
|
1098
|
+
element.style.borderRadius = "var(--tileflow-interaction-border-radius, 0.375rem)";
|
|
1099
|
+
element.style.borderStyle = "solid";
|
|
1100
|
+
element.style.borderWidth = "1px";
|
|
1101
|
+
element.style.boxShadow = "var(--tileflow-interaction-shadow, 0 2px 8px rgb(0 0 0 / 0.18))";
|
|
1102
|
+
element.style.color = "var(--tileflow-interaction-foreground, #111827)";
|
|
1103
|
+
element.style.maxWidth = "var(--tileflow-interaction-max-width, 20rem)";
|
|
1104
|
+
element.style.padding = "0.5rem";
|
|
1105
|
+
}
|
|
1106
|
+
function configureDefaultMarkerStyles(element) {
|
|
1107
|
+
element.style.alignItems = "center";
|
|
1108
|
+
element.style.background = "var(--tileflow-interaction-marker-color, #2563eb)";
|
|
1109
|
+
element.style.borderColor = "var(--tileflow-interaction-border-color, #ffffff)";
|
|
1110
|
+
element.style.borderRadius = "var(--tileflow-interaction-border-radius, 9999px)";
|
|
1111
|
+
element.style.borderStyle = "solid";
|
|
1112
|
+
element.style.borderWidth = "1px";
|
|
1113
|
+
element.style.boxShadow = "var(--tileflow-interaction-shadow, 0 1px 4px rgb(0 0 0 / 0.25))";
|
|
1114
|
+
element.style.color = "var(--tileflow-interaction-foreground, #ffffff)";
|
|
1115
|
+
element.style.display = "inline-flex";
|
|
1116
|
+
element.style.justifyContent = "center";
|
|
1117
|
+
element.style.minHeight = "1.5rem";
|
|
1118
|
+
element.style.minWidth = "1.5rem";
|
|
1119
|
+
element.style.padding = "0.25rem";
|
|
1120
|
+
}
|
|
1121
|
+
function clearDefaultMarkerStyles(element) {
|
|
1122
|
+
element.style.alignItems = "";
|
|
1123
|
+
element.style.background = "";
|
|
1124
|
+
element.style.borderColor = "";
|
|
1125
|
+
element.style.borderRadius = "";
|
|
1126
|
+
element.style.borderStyle = "";
|
|
1127
|
+
element.style.borderWidth = "";
|
|
1128
|
+
element.style.boxShadow = "";
|
|
1129
|
+
element.style.color = "";
|
|
1130
|
+
element.style.display = "";
|
|
1131
|
+
element.style.justifyContent = "";
|
|
1132
|
+
element.style.minHeight = "";
|
|
1133
|
+
element.style.minWidth = "";
|
|
1134
|
+
element.style.padding = "";
|
|
1135
|
+
}
|
|
1136
|
+
function resolveContent(content, data) {
|
|
1137
|
+
if (content.kind === "text") return { invalidField: false, text: content.text };
|
|
1138
|
+
if (content.kind === "view") return { invalidField: false, text: "" };
|
|
1139
|
+
const fallback = content.fallback ?? "";
|
|
1140
|
+
const invalid = () => ({ invalidField: content.fallback === void 0, text: fallback });
|
|
1141
|
+
let current = data;
|
|
1142
|
+
try {
|
|
1143
|
+
for (const segment of content.field.split(".")) {
|
|
1144
|
+
if (current === null || typeof current !== "object" || isUnsafeFieldSegment(segment) || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
1145
|
+
return invalid();
|
|
1146
|
+
}
|
|
1147
|
+
current = current[segment];
|
|
1148
|
+
}
|
|
1149
|
+
} catch {
|
|
1150
|
+
return invalid();
|
|
1151
|
+
}
|
|
1152
|
+
return current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string" ? { invalidField: false, text: String(current) } : invalid();
|
|
1153
|
+
}
|
|
1154
|
+
function diagnosticKey(annotationId, surface, code) {
|
|
1155
|
+
return `${annotationId}\0${surface}\0${code}`;
|
|
1156
|
+
}
|
|
1157
|
+
function overlayDiagnosticKey(annotationId, surface) {
|
|
1158
|
+
return `${annotationId}\0${surface}\0OVERLAY_FAILURE`;
|
|
1159
|
+
}
|
|
1160
|
+
function isUnsafeFieldSegment(segment) {
|
|
1161
|
+
return segment === "__proto__" || segment === "constructor" || segment === "prototype";
|
|
1162
|
+
}
|
|
1163
|
+
function createUnsubscribe(values, value) {
|
|
1164
|
+
let subscribed = true;
|
|
1165
|
+
return () => {
|
|
1166
|
+
if (!subscribed) return;
|
|
1167
|
+
subscribed = false;
|
|
1168
|
+
values.delete(value);
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
function notifyListeners(listeners, value, message, errors = []) {
|
|
1172
|
+
for (const listener of [...listeners]) {
|
|
1173
|
+
try {
|
|
1174
|
+
listener(value);
|
|
1175
|
+
} catch (error) {
|
|
1176
|
+
errors.push(error);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
throwCollectedErrors(errors, message);
|
|
1180
|
+
}
|
|
1181
|
+
function safelyRunAll(operations) {
|
|
1182
|
+
const errors = [];
|
|
1183
|
+
for (const operation of operations) {
|
|
1184
|
+
try {
|
|
1185
|
+
operation();
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
errors.push(error);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
return errors;
|
|
1191
|
+
}
|
|
1192
|
+
function throwCollectedErrors(errors, message) {
|
|
1193
|
+
if (errors.length === 0) return;
|
|
1194
|
+
if (errors.length === 1) throw errors[0];
|
|
1195
|
+
throw new AggregateError(errors, message);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/reducer.ts
|
|
1199
|
+
var initialTileflowInteractionState = Object.freeze({
|
|
1200
|
+
popup: null
|
|
1201
|
+
});
|
|
1202
|
+
function tileflowInteractionTargetRefsEqual(left, right) {
|
|
1203
|
+
if (left === right) return true;
|
|
1204
|
+
if (left === null || right === null || left.kind !== right.kind) return false;
|
|
1205
|
+
switch (left.kind) {
|
|
1206
|
+
case "annotation":
|
|
1207
|
+
return left.id === right.id;
|
|
1208
|
+
case "semantic-feature": {
|
|
1209
|
+
const candidate = right;
|
|
1210
|
+
return left.domain === candidate.domain && left.featureId === candidate.featureId;
|
|
1211
|
+
}
|
|
1212
|
+
case "style-feature": {
|
|
1213
|
+
const candidate = right;
|
|
1214
|
+
return left.layerId === candidate.layerId && left.featureId === candidate.featureId;
|
|
1215
|
+
}
|
|
1216
|
+
case "map": {
|
|
1217
|
+
const candidate = right;
|
|
1218
|
+
return left.coordinate[0] === candidate.coordinate[0] && left.coordinate[1] === candidate.coordinate[1];
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// src/maplibre-coordinator.ts
|
|
1224
|
+
function createTileflowMapLibreInteractionCoordinator(options = {}) {
|
|
1225
|
+
const controlled = options.interactionState !== void 0;
|
|
1226
|
+
let state = options.interactionState ?? options.defaultInteractionState ?? Object.freeze({ popup: null });
|
|
1227
|
+
let disposed = false;
|
|
1228
|
+
let committing = false;
|
|
1229
|
+
const participants = /* @__PURE__ */ new Map();
|
|
1230
|
+
const commit = (next) => {
|
|
1231
|
+
if (committing) throw new Error("Tileflow interaction state commit is reentrant.");
|
|
1232
|
+
const previous = state;
|
|
1233
|
+
const attempted = [];
|
|
1234
|
+
committing = true;
|
|
1235
|
+
try {
|
|
1236
|
+
for (const kind of participantCommitOrder(next)) {
|
|
1237
|
+
const participant = participants.get(kind);
|
|
1238
|
+
if (!participant) continue;
|
|
1239
|
+
attempted.push(participant);
|
|
1240
|
+
participant.setInteractionState(next);
|
|
1241
|
+
}
|
|
1242
|
+
state = next;
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
const rollbackErrors = [];
|
|
1245
|
+
for (const participant of attempted.reverse()) {
|
|
1246
|
+
try {
|
|
1247
|
+
participant.setInteractionState(previous);
|
|
1248
|
+
} catch (rollbackError) {
|
|
1249
|
+
rollbackErrors.push(rollbackError);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
if (rollbackErrors.length > 0) {
|
|
1253
|
+
throw new AggregateError(
|
|
1254
|
+
[error, ...rollbackErrors],
|
|
1255
|
+
"Tileflow interaction state commit and rollback failed."
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
throw error;
|
|
1259
|
+
} finally {
|
|
1260
|
+
committing = false;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
return {
|
|
1264
|
+
attach(kind, participant) {
|
|
1265
|
+
assertActive(disposed);
|
|
1266
|
+
if (participants.has(kind)) {
|
|
1267
|
+
throw new Error(`Tileflow interaction participant "${kind}" is already attached.`);
|
|
1268
|
+
}
|
|
1269
|
+
participants.set(kind, participant);
|
|
1270
|
+
try {
|
|
1271
|
+
participant.setInteractionState(state);
|
|
1272
|
+
} catch (error) {
|
|
1273
|
+
participants.delete(kind);
|
|
1274
|
+
throw error;
|
|
1275
|
+
}
|
|
1276
|
+
let attached = true;
|
|
1277
|
+
return () => {
|
|
1278
|
+
if (!attached) return;
|
|
1279
|
+
attached = false;
|
|
1280
|
+
if (participants.get(kind) === participant) participants.delete(kind);
|
|
1281
|
+
};
|
|
1282
|
+
},
|
|
1283
|
+
dispose() {
|
|
1284
|
+
if (disposed) return;
|
|
1285
|
+
disposed = true;
|
|
1286
|
+
participants.clear();
|
|
1287
|
+
},
|
|
1288
|
+
getInteractionState: () => state,
|
|
1289
|
+
requestInteractionState(next, _participantPrevious, reason) {
|
|
1290
|
+
assertActive(disposed);
|
|
1291
|
+
if (interactionStatesEqual(state, next)) return;
|
|
1292
|
+
const previous = state;
|
|
1293
|
+
if (!controlled) commit(next);
|
|
1294
|
+
options.onInteractionStateChange?.(next, previous, reason);
|
|
1295
|
+
},
|
|
1296
|
+
setInteractionState(next) {
|
|
1297
|
+
assertActive(disposed);
|
|
1298
|
+
if (interactionStatesEqual(state, next)) return;
|
|
1299
|
+
commit(next);
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function participantCommitOrder(state) {
|
|
1304
|
+
return state.popup?.kind === "annotation" ? ["semantic", "annotation"] : ["annotation", "semantic"];
|
|
1305
|
+
}
|
|
1306
|
+
function interactionStatesEqual(left, right) {
|
|
1307
|
+
return tileflowInteractionTargetRefsEqual(left.popup, right.popup);
|
|
1308
|
+
}
|
|
1309
|
+
function assertActive(disposed) {
|
|
1310
|
+
if (disposed) throw new Error("Tileflow interaction coordinator is disposed.");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// src/contracts.ts
|
|
1314
|
+
import { z } from "zod/mini";
|
|
1315
|
+
var tileflowPoiCategories = [
|
|
1316
|
+
"arts-entertainment",
|
|
1317
|
+
"education",
|
|
1318
|
+
"food-drink",
|
|
1319
|
+
"landmark",
|
|
1320
|
+
"lodging",
|
|
1321
|
+
"medical",
|
|
1322
|
+
"park-nature",
|
|
1323
|
+
"public-services",
|
|
1324
|
+
"religion",
|
|
1325
|
+
"retail",
|
|
1326
|
+
"sport-leisure",
|
|
1327
|
+
"transport",
|
|
1328
|
+
"visitor-amenity"
|
|
1329
|
+
];
|
|
1330
|
+
var tileflowInteractionLimits = {
|
|
1331
|
+
maxAnnotations: 1e3,
|
|
1332
|
+
maxAriaLabelLength: 256,
|
|
1333
|
+
maxBindings: 100,
|
|
1334
|
+
maxCategories: 32,
|
|
1335
|
+
maxColorLength: 64,
|
|
1336
|
+
maxContentTextLength: 4096,
|
|
1337
|
+
maxDocumentBytes: 256e3,
|
|
1338
|
+
maxDocumentDepth: 64,
|
|
1339
|
+
maxDocumentNodes: 2e4,
|
|
1340
|
+
maxDocumentProperties: 5e4,
|
|
1341
|
+
maxFeatureIdLength: 128,
|
|
1342
|
+
maxFieldSelectorLength: 128,
|
|
1343
|
+
maxIdLength: 64,
|
|
1344
|
+
maxTargetNameLength: 128,
|
|
1345
|
+
maxViewNameLength: 64
|
|
1346
|
+
};
|
|
1347
|
+
function auditTileflowInteractionJsonDocument(input) {
|
|
1348
|
+
try {
|
|
1349
|
+
const stack = [{ depth: 0, value: input }];
|
|
1350
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1351
|
+
if (isObject(input)) seen.add(input);
|
|
1352
|
+
let bytes = 0;
|
|
1353
|
+
let nodes = 1;
|
|
1354
|
+
let properties = 0;
|
|
1355
|
+
const consumeBytes = (addition) => {
|
|
1356
|
+
if (!Number.isSafeInteger(addition) || addition < 0 || addition > tileflowInteractionLimits.maxDocumentBytes - bytes) {
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
bytes += addition;
|
|
1360
|
+
return true;
|
|
1361
|
+
};
|
|
1362
|
+
const enqueue = (value, depth) => {
|
|
1363
|
+
if (depth > tileflowInteractionLimits.maxDocumentDepth) return "depth";
|
|
1364
|
+
if (isObject(value)) {
|
|
1365
|
+
if (seen.has(value)) return "invalid";
|
|
1366
|
+
seen.add(value);
|
|
1367
|
+
}
|
|
1368
|
+
stack.push({ depth, value });
|
|
1369
|
+
return void 0;
|
|
1370
|
+
};
|
|
1371
|
+
while (stack.length > 0) {
|
|
1372
|
+
const entry = stack.pop();
|
|
1373
|
+
if (!entry) return { ok: false, reason: "invalid" };
|
|
1374
|
+
const { depth, value } = entry;
|
|
1375
|
+
if (value === null) {
|
|
1376
|
+
if (!consumeBytes(4)) return { ok: false, reason: "bytes" };
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
if (typeof value === "boolean") {
|
|
1380
|
+
if (!consumeBytes(value ? 4 : 5)) return { ok: false, reason: "bytes" };
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (typeof value === "number") {
|
|
1384
|
+
if (!Number.isFinite(value)) return { ok: false, reason: "invalid" };
|
|
1385
|
+
const length = value === 0 ? 1 : String(value).length;
|
|
1386
|
+
if (!consumeBytes(length)) return { ok: false, reason: "bytes" };
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
if (typeof value === "string") {
|
|
1390
|
+
const length = jsonStringByteLength(
|
|
1391
|
+
value,
|
|
1392
|
+
tileflowInteractionLimits.maxDocumentBytes - bytes
|
|
1393
|
+
);
|
|
1394
|
+
if (length === void 0) return { ok: false, reason: "bytes" };
|
|
1395
|
+
bytes += length;
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (!isObject(value)) return { ok: false, reason: "invalid" };
|
|
1399
|
+
const array = Array.isArray(value);
|
|
1400
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1401
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) {
|
|
1402
|
+
return { ok: false, reason: "invalid" };
|
|
1403
|
+
}
|
|
1404
|
+
if (array) {
|
|
1405
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
1406
|
+
if (!lengthDescriptor || !("value" in lengthDescriptor) || typeof lengthDescriptor.value !== "number" || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) {
|
|
1407
|
+
return { ok: false, reason: "invalid" };
|
|
1408
|
+
}
|
|
1409
|
+
const length = lengthDescriptor.value;
|
|
1410
|
+
if (length > tileflowInteractionLimits.maxDocumentProperties - properties) {
|
|
1411
|
+
return { ok: false, reason: "properties" };
|
|
1412
|
+
}
|
|
1413
|
+
properties += length;
|
|
1414
|
+
if (length > 0 && depth >= tileflowInteractionLimits.maxDocumentDepth) {
|
|
1415
|
+
return { ok: false, reason: "depth" };
|
|
1416
|
+
}
|
|
1417
|
+
const syntaxBytes = length === 0 ? 2 : length + 1;
|
|
1418
|
+
if (!consumeBytes(syntaxBytes)) return { ok: false, reason: "bytes" };
|
|
1419
|
+
const keys2 = Reflect.ownKeys(value);
|
|
1420
|
+
const children2 = [];
|
|
1421
|
+
let sawLength = false;
|
|
1422
|
+
for (const key of keys2) {
|
|
1423
|
+
if (typeof key !== "string") return { ok: false, reason: "invalid" };
|
|
1424
|
+
if (key === "length") {
|
|
1425
|
+
sawLength = true;
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
if (!isCanonicalArrayIndex(key, length)) return { ok: false, reason: "invalid" };
|
|
1429
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1430
|
+
if (!isEnumerableDataDescriptor(descriptor)) return { ok: false, reason: "invalid" };
|
|
1431
|
+
children2.push(descriptor.value);
|
|
1432
|
+
}
|
|
1433
|
+
if (!sawLength || children2.length > length) return { ok: false, reason: "invalid" };
|
|
1434
|
+
if (children2.length > tileflowInteractionLimits.maxDocumentNodes - nodes) {
|
|
1435
|
+
return { ok: false, reason: "nodes" };
|
|
1436
|
+
}
|
|
1437
|
+
nodes += children2.length;
|
|
1438
|
+
const holes = length - children2.length;
|
|
1439
|
+
if (!consumeBytes(holes * 4)) return { ok: false, reason: "bytes" };
|
|
1440
|
+
for (const child of children2) {
|
|
1441
|
+
const failure = enqueue(child, depth + 1);
|
|
1442
|
+
if (failure) return { ok: false, reason: failure };
|
|
1443
|
+
}
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
const keys = Reflect.ownKeys(value);
|
|
1447
|
+
if (keys.length > tileflowInteractionLimits.maxDocumentProperties - properties) {
|
|
1448
|
+
return { ok: false, reason: "properties" };
|
|
1449
|
+
}
|
|
1450
|
+
if (keys.length > tileflowInteractionLimits.maxDocumentNodes - nodes) {
|
|
1451
|
+
return { ok: false, reason: "nodes" };
|
|
1452
|
+
}
|
|
1453
|
+
if (keys.length > 0 && depth >= tileflowInteractionLimits.maxDocumentDepth) {
|
|
1454
|
+
return { ok: false, reason: "depth" };
|
|
1455
|
+
}
|
|
1456
|
+
properties += keys.length;
|
|
1457
|
+
nodes += keys.length;
|
|
1458
|
+
if (!consumeBytes(keys.length === 0 ? 2 : keys.length * 2 + 1)) {
|
|
1459
|
+
return { ok: false, reason: "bytes" };
|
|
1460
|
+
}
|
|
1461
|
+
const children = [];
|
|
1462
|
+
for (const key of keys) {
|
|
1463
|
+
if (typeof key !== "string" || key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1464
|
+
return { ok: false, reason: "invalid" };
|
|
1465
|
+
}
|
|
1466
|
+
const keyBytes = jsonStringByteLength(
|
|
1467
|
+
key,
|
|
1468
|
+
tileflowInteractionLimits.maxDocumentBytes - bytes
|
|
1469
|
+
);
|
|
1470
|
+
if (keyBytes === void 0) return { ok: false, reason: "bytes" };
|
|
1471
|
+
bytes += keyBytes;
|
|
1472
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1473
|
+
if (!isEnumerableDataDescriptor(descriptor)) return { ok: false, reason: "invalid" };
|
|
1474
|
+
children.push(descriptor.value);
|
|
1475
|
+
}
|
|
1476
|
+
for (const child of children) {
|
|
1477
|
+
const failure = enqueue(child, depth + 1);
|
|
1478
|
+
if (failure) return { ok: false, reason: failure };
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return { bytes, ok: true };
|
|
1482
|
+
} catch {
|
|
1483
|
+
return { ok: false, reason: "invalid" };
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
function isObject(value) {
|
|
1487
|
+
return value !== null && typeof value === "object";
|
|
1488
|
+
}
|
|
1489
|
+
function isEnumerableDataDescriptor(descriptor) {
|
|
1490
|
+
return Boolean(descriptor?.enumerable && "value" in descriptor);
|
|
1491
|
+
}
|
|
1492
|
+
function isCanonicalArrayIndex(key, length) {
|
|
1493
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(key)) return false;
|
|
1494
|
+
const index = Number(key);
|
|
1495
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length && String(index) === key;
|
|
1496
|
+
}
|
|
1497
|
+
function jsonStringByteLength(value, maximum) {
|
|
1498
|
+
let bytes = 2;
|
|
1499
|
+
if (bytes > maximum) return void 0;
|
|
1500
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1501
|
+
const code = value.charCodeAt(index);
|
|
1502
|
+
let addition;
|
|
1503
|
+
if (code === 34 || code === 92) addition = 2;
|
|
1504
|
+
else if (code <= 31) {
|
|
1505
|
+
addition = code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
|
|
1506
|
+
} else if (code <= 127) addition = 1;
|
|
1507
|
+
else if (code <= 2047) addition = 2;
|
|
1508
|
+
else if (code >= 55296 && code <= 56319) {
|
|
1509
|
+
const next = value.charCodeAt(index + 1);
|
|
1510
|
+
if (next >= 56320 && next <= 57343) {
|
|
1511
|
+
addition = 4;
|
|
1512
|
+
index += 1;
|
|
1513
|
+
} else addition = 6;
|
|
1514
|
+
} else if (code >= 56320 && code <= 57343) addition = 6;
|
|
1515
|
+
else addition = 3;
|
|
1516
|
+
if (addition > maximum - bytes) return void 0;
|
|
1517
|
+
bytes += addition;
|
|
1518
|
+
}
|
|
1519
|
+
return bytes;
|
|
1520
|
+
}
|
|
1521
|
+
var portableIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u;
|
|
1522
|
+
var semanticDomainPattern = /^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)*$/u;
|
|
1523
|
+
var fieldSelectorPattern = /^[A-Za-z_][A-Za-z0-9_-]*(?:\.[A-Za-z_][A-Za-z0-9_-]*)*$/u;
|
|
1524
|
+
var unsafeFieldSegments = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
1525
|
+
var safeCssColorPattern = /^(?:#(?:[\da-f]{3}|[\da-f]{4}|[\da-f]{6}|[\da-f]{8})|[a-z]+|(?:color|hsla?|hwb|lab|lch|oklab|oklch|rgba?)\([A-Za-z0-9.,%+\- /]+\))$/iu;
|
|
1526
|
+
var portableIdSchema = z.string().check(
|
|
1527
|
+
z.minLength(1),
|
|
1528
|
+
z.maxLength(tileflowInteractionLimits.maxIdLength),
|
|
1529
|
+
z.regex(portableIdPattern, "Expected a portable identifier")
|
|
1530
|
+
);
|
|
1531
|
+
var targetNameSchema = z.string().check(
|
|
1532
|
+
z.trim(),
|
|
1533
|
+
z.minLength(1),
|
|
1534
|
+
z.maxLength(tileflowInteractionLimits.maxTargetNameLength),
|
|
1535
|
+
z.refine((value) => !hasControlCharacters(value), {
|
|
1536
|
+
message: "Target names may not contain control characters"
|
|
1537
|
+
})
|
|
1538
|
+
);
|
|
1539
|
+
function hasControlCharacters(value) {
|
|
1540
|
+
return [...value].some((character) => {
|
|
1541
|
+
const code = character.charCodeAt(0);
|
|
1542
|
+
return code <= 31 || code === 127;
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
var coordinateSchema = z.tuple([z.number(), z.number()]).check(
|
|
1546
|
+
z.refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
|
|
1547
|
+
message: "Expected [longitude, latitude] within world bounds"
|
|
1548
|
+
})
|
|
1549
|
+
);
|
|
1550
|
+
var featureIdSchema = z.union([
|
|
1551
|
+
z.int(),
|
|
1552
|
+
z.string().check(z.minLength(1), z.maxLength(tileflowInteractionLimits.maxFeatureIdLength))
|
|
1553
|
+
]);
|
|
1554
|
+
var tileflowInteractionJsonValueSchema = z.custom(
|
|
1555
|
+
(value) => auditTileflowInteractionJsonDocument(value).ok,
|
|
1556
|
+
"Expected a bounded, finite JSON value"
|
|
1557
|
+
);
|
|
1558
|
+
var textContentSchema = z.strictObject({
|
|
1559
|
+
kind: z.literal("text"),
|
|
1560
|
+
text: z.string().check(z.maxLength(tileflowInteractionLimits.maxContentTextLength))
|
|
1561
|
+
});
|
|
1562
|
+
var fieldContentSchema = z.strictObject({
|
|
1563
|
+
fallback: z.optional(
|
|
1564
|
+
z.string().check(z.maxLength(tileflowInteractionLimits.maxContentTextLength))
|
|
1565
|
+
),
|
|
1566
|
+
field: z.string().check(
|
|
1567
|
+
z.minLength(1),
|
|
1568
|
+
z.maxLength(tileflowInteractionLimits.maxFieldSelectorLength),
|
|
1569
|
+
z.regex(fieldSelectorPattern, "Expected a dotted declarative field selector"),
|
|
1570
|
+
z.refine((field) => field.split(".").every((segment) => !unsafeFieldSegments.has(segment)), {
|
|
1571
|
+
message: "Field selectors may not contain unsafe object keys"
|
|
1572
|
+
})
|
|
1573
|
+
),
|
|
1574
|
+
kind: z.literal("field")
|
|
1575
|
+
});
|
|
1576
|
+
var viewContentSchema = z.strictObject({
|
|
1577
|
+
kind: z.literal("view"),
|
|
1578
|
+
name: z.string().check(
|
|
1579
|
+
z.minLength(1),
|
|
1580
|
+
z.maxLength(tileflowInteractionLimits.maxViewNameLength),
|
|
1581
|
+
z.regex(portableIdPattern, "Expected a portable view name")
|
|
1582
|
+
)
|
|
1583
|
+
});
|
|
1584
|
+
var tileflowInteractionContentSchema = z.discriminatedUnion("kind", [
|
|
1585
|
+
textContentSchema,
|
|
1586
|
+
fieldContentSchema,
|
|
1587
|
+
viewContentSchema
|
|
1588
|
+
]);
|
|
1589
|
+
var annotationMarkerSchema = z.strictObject({
|
|
1590
|
+
color: z.optional(
|
|
1591
|
+
z.string().check(
|
|
1592
|
+
z.trim(),
|
|
1593
|
+
z.minLength(1),
|
|
1594
|
+
z.maxLength(tileflowInteractionLimits.maxColorLength),
|
|
1595
|
+
z.regex(safeCssColorPattern, "Expected a bounded CSS color without URL or variable input")
|
|
1596
|
+
)
|
|
1597
|
+
),
|
|
1598
|
+
content: z.optional(tileflowInteractionContentSchema)
|
|
1599
|
+
});
|
|
1600
|
+
var annotationSurfaceSchema = z.strictObject({ content: tileflowInteractionContentSchema });
|
|
1601
|
+
var tileflowAnnotationSchema = z.strictObject({
|
|
1602
|
+
ariaLabel: z.string().check(z.trim(), z.minLength(1), z.maxLength(tileflowInteractionLimits.maxAriaLabelLength)),
|
|
1603
|
+
coordinate: coordinateSchema,
|
|
1604
|
+
data: z.optional(tileflowInteractionJsonValueSchema),
|
|
1605
|
+
id: portableIdSchema,
|
|
1606
|
+
kind: z.literal("marker"),
|
|
1607
|
+
marker: z.optional(annotationMarkerSchema),
|
|
1608
|
+
popup: z.optional(annotationSurfaceSchema),
|
|
1609
|
+
tooltip: z.optional(annotationSurfaceSchema)
|
|
1610
|
+
});
|
|
1611
|
+
var tileflowAnnotationsSchema = z.array(tileflowAnnotationSchema).check(z.maxLength(tileflowInteractionLimits.maxAnnotations));
|
|
1612
|
+
var annotationTargetSelectorSchema = z.strictObject({
|
|
1613
|
+
id: portableIdSchema,
|
|
1614
|
+
kind: z.literal("annotation")
|
|
1615
|
+
});
|
|
1616
|
+
var semanticFeatureTargetSelectorSchema = z.strictObject({
|
|
1617
|
+
categories: z.optional(
|
|
1618
|
+
z.array(z.enum(tileflowPoiCategories)).check(z.minLength(1), z.maxLength(tileflowInteractionLimits.maxCategories))
|
|
1619
|
+
),
|
|
1620
|
+
domain: z.string().check(
|
|
1621
|
+
z.minLength(1),
|
|
1622
|
+
z.maxLength(tileflowInteractionLimits.maxTargetNameLength),
|
|
1623
|
+
z.regex(semanticDomainPattern, "Expected a dotted semantic domain")
|
|
1624
|
+
),
|
|
1625
|
+
kind: z.literal("semantic-feature")
|
|
1626
|
+
});
|
|
1627
|
+
var styleLayerTargetSelectorSchema = z.strictObject({
|
|
1628
|
+
kind: z.literal("style-layer"),
|
|
1629
|
+
layerId: targetNameSchema
|
|
1630
|
+
});
|
|
1631
|
+
var mapTargetSelectorSchema = z.strictObject({ kind: z.literal("map") });
|
|
1632
|
+
var tileflowInteractionTargetSchema = z.discriminatedUnion("kind", [
|
|
1633
|
+
annotationTargetSelectorSchema,
|
|
1634
|
+
semanticFeatureTargetSelectorSchema,
|
|
1635
|
+
styleLayerTargetSelectorSchema,
|
|
1636
|
+
mapTargetSelectorSchema
|
|
1637
|
+
]);
|
|
1638
|
+
var tileflowInteractionBindingSchema = z.strictObject({
|
|
1639
|
+
id: portableIdSchema,
|
|
1640
|
+
popup: z.optional(annotationSurfaceSchema),
|
|
1641
|
+
target: tileflowInteractionTargetSchema,
|
|
1642
|
+
tooltip: z.optional(annotationSurfaceSchema)
|
|
1643
|
+
});
|
|
1644
|
+
var tileflowInteractionBindingsSchema = z.array(tileflowInteractionBindingSchema).check(z.maxLength(tileflowInteractionLimits.maxBindings));
|
|
1645
|
+
var annotationTargetSchema = z.strictObject({
|
|
1646
|
+
id: portableIdSchema,
|
|
1647
|
+
kind: z.literal("annotation")
|
|
1648
|
+
});
|
|
1649
|
+
var semanticFeatureTargetSchema = z.strictObject({
|
|
1650
|
+
domain: z.string().check(
|
|
1651
|
+
z.minLength(1),
|
|
1652
|
+
z.maxLength(tileflowInteractionLimits.maxTargetNameLength),
|
|
1653
|
+
z.regex(semanticDomainPattern, "Expected a dotted semantic domain")
|
|
1654
|
+
),
|
|
1655
|
+
featureId: featureIdSchema,
|
|
1656
|
+
kind: z.literal("semantic-feature")
|
|
1657
|
+
});
|
|
1658
|
+
var styleFeatureTargetSchema = z.strictObject({
|
|
1659
|
+
featureId: featureIdSchema,
|
|
1660
|
+
kind: z.literal("style-feature"),
|
|
1661
|
+
layerId: targetNameSchema
|
|
1662
|
+
});
|
|
1663
|
+
var mapTargetSchema = z.strictObject({ coordinate: coordinateSchema, kind: z.literal("map") });
|
|
1664
|
+
var tileflowInteractionTargetRefSchema = z.discriminatedUnion("kind", [
|
|
1665
|
+
annotationTargetSchema,
|
|
1666
|
+
semanticFeatureTargetSchema,
|
|
1667
|
+
styleFeatureTargetSchema,
|
|
1668
|
+
mapTargetSchema
|
|
1669
|
+
]);
|
|
1670
|
+
var tileflowInteractionStateSchema = z.strictObject({
|
|
1671
|
+
popup: z.nullable(tileflowInteractionTargetRefSchema)
|
|
1672
|
+
});
|
|
1673
|
+
var tileflowInteractionActionSchema = z.discriminatedUnion("type", [
|
|
1674
|
+
z.strictObject({ target: tileflowInteractionTargetRefSchema, type: z.literal("open-popup") }),
|
|
1675
|
+
z.strictObject({ type: z.literal("close-popup") })
|
|
1676
|
+
]);
|
|
1677
|
+
|
|
1678
|
+
// src/maplibre-poi.ts
|
|
1679
|
+
var interactionManifestMetadataKey = "tileflow:interaction-manifest";
|
|
1680
|
+
var interactionManifestVersion = 2;
|
|
1681
|
+
var missingSourceLayerNamespace = "tileflow-no-source-layer";
|
|
1682
|
+
function createTileflowMapLibrePoiController(options) {
|
|
1683
|
+
let bindings = semanticPoiBindings(options.bindings ?? [], options.onDiagnostic);
|
|
1684
|
+
let manifest = null;
|
|
1685
|
+
let hovered = null;
|
|
1686
|
+
let pendingMove = null;
|
|
1687
|
+
let pendingFrame = null;
|
|
1688
|
+
let disposed = false;
|
|
1689
|
+
let disposeComplete = false;
|
|
1690
|
+
let framePendingCancellation = null;
|
|
1691
|
+
let hoverClearPending = false;
|
|
1692
|
+
const subscriptionDisposers = [];
|
|
1693
|
+
const emit = (type, match, modality) => {
|
|
1694
|
+
options.onInteractionEvent?.({
|
|
1695
|
+
bindingId: match.binding.id,
|
|
1696
|
+
coordinate: match.target.coordinate,
|
|
1697
|
+
inputModality: modality,
|
|
1698
|
+
target: match.target,
|
|
1699
|
+
type
|
|
1700
|
+
});
|
|
1701
|
+
};
|
|
1702
|
+
const setHovered = (next, modality) => {
|
|
1703
|
+
if (matchesEqual(hovered, next)) {
|
|
1704
|
+
if (next && !Object.is(hovered, next)) {
|
|
1705
|
+
hovered = next;
|
|
1706
|
+
options.onHoverChange?.(hovered);
|
|
1707
|
+
}
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (hovered) emit("target:leave", hovered, modality);
|
|
1711
|
+
hovered = next;
|
|
1712
|
+
if (hovered) emit("target:enter", hovered, modality);
|
|
1713
|
+
options.onHoverChange?.(hovered);
|
|
1714
|
+
};
|
|
1715
|
+
const hitTest = (event) => {
|
|
1716
|
+
if (!manifest || bindings.length === 0) return null;
|
|
1717
|
+
const layerIds = relevantLayerIds(manifest, bindings);
|
|
1718
|
+
if (layerIds.length === 0) return null;
|
|
1719
|
+
let features;
|
|
1720
|
+
try {
|
|
1721
|
+
features = options.map.queryRenderedFeatures(event.point, { layers: layerIds });
|
|
1722
|
+
} catch {
|
|
1723
|
+
diagnostic(
|
|
1724
|
+
options.onDiagnostic,
|
|
1725
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
1726
|
+
"Tileflow could not query the semantic POI layers declared by this style."
|
|
1727
|
+
);
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
const candidates = [];
|
|
1731
|
+
let manifestMismatch = false;
|
|
1732
|
+
for (const [renderedIndex, feature] of features.entries()) {
|
|
1733
|
+
const layer = manifest.layers.find((candidate) => candidate.layerId === feature.layer.id);
|
|
1734
|
+
if (!layer) continue;
|
|
1735
|
+
if (feature.source !== void 0 && feature.source !== layer.source || sourceLayerNamespace(feature.sourceLayer) !== sourceLayerNamespace(layer.sourceLayer)) {
|
|
1736
|
+
manifestMismatch = true;
|
|
1737
|
+
diagnostic(
|
|
1738
|
+
options.onDiagnostic,
|
|
1739
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
1740
|
+
"A rendered POI feature does not match its semantic interaction metadata."
|
|
1741
|
+
);
|
|
1742
|
+
continue;
|
|
1743
|
+
}
|
|
1744
|
+
const binding = bindings.find((candidate) => {
|
|
1745
|
+
const target = candidate.target;
|
|
1746
|
+
return target.kind === "semantic-feature" && target.domain === "poi" && (!target.categories || target.categories.includes(layer.category));
|
|
1747
|
+
});
|
|
1748
|
+
if (!binding) continue;
|
|
1749
|
+
const featureId = normalizeFeatureId(feature.id);
|
|
1750
|
+
candidates.push({
|
|
1751
|
+
layer,
|
|
1752
|
+
renderedIndex,
|
|
1753
|
+
match: {
|
|
1754
|
+
binding,
|
|
1755
|
+
target: {
|
|
1756
|
+
bindingId: binding.id,
|
|
1757
|
+
coordinate: [event.lngLat.lng, event.lngLat.lat],
|
|
1758
|
+
domain: "poi",
|
|
1759
|
+
feature: {
|
|
1760
|
+
category: layer.category,
|
|
1761
|
+
...featureId === void 0 ? {} : { id: featureId },
|
|
1762
|
+
properties: normalizePoiProperties(feature.properties, manifest.fields)
|
|
1763
|
+
},
|
|
1764
|
+
kind: "semantic-feature"
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
if (!manifestMismatch) {
|
|
1770
|
+
options.onDiagnosticResolved?.("SEMANTIC_MANIFEST_MISMATCH");
|
|
1771
|
+
}
|
|
1772
|
+
return choosePoiCandidate(candidates, manifest)?.match ?? null;
|
|
1773
|
+
};
|
|
1774
|
+
const handleMove = (value) => {
|
|
1775
|
+
const event = parsePointerEvent(value);
|
|
1776
|
+
if (!event || disposed) return;
|
|
1777
|
+
pendingMove = event;
|
|
1778
|
+
if (pendingFrame !== null) return;
|
|
1779
|
+
pendingFrame = options.requestFrame(() => {
|
|
1780
|
+
pendingFrame = null;
|
|
1781
|
+
const current = pendingMove;
|
|
1782
|
+
pendingMove = null;
|
|
1783
|
+
if (current && !disposed) setHovered(hitTest(current), inputModality(current));
|
|
1784
|
+
});
|
|
1785
|
+
};
|
|
1786
|
+
const handleLeave = () => {
|
|
1787
|
+
pendingMove = null;
|
|
1788
|
+
if (pendingFrame !== null) {
|
|
1789
|
+
options.cancelFrame(pendingFrame);
|
|
1790
|
+
pendingFrame = null;
|
|
1791
|
+
}
|
|
1792
|
+
setHovered(null, "pointer");
|
|
1793
|
+
};
|
|
1794
|
+
const handleClick = (value) => {
|
|
1795
|
+
const event = parsePointerEvent(value);
|
|
1796
|
+
if (!event || disposed) return;
|
|
1797
|
+
const modality = inputModality(event);
|
|
1798
|
+
const match = hitTest(event);
|
|
1799
|
+
if (!match) return;
|
|
1800
|
+
emit("target:activate", match, modality);
|
|
1801
|
+
if (match.binding.popup && match.target.feature.id === void 0) {
|
|
1802
|
+
diagnostic(
|
|
1803
|
+
options.onDiagnostic,
|
|
1804
|
+
"UNSTABLE_FEATURE_IDENTITY",
|
|
1805
|
+
"This semantic POI has no stable feature ID, so Tileflow cannot open durable popup state."
|
|
1806
|
+
);
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
options.onActivate?.(match, modality);
|
|
1810
|
+
};
|
|
1811
|
+
const refreshManifest = () => {
|
|
1812
|
+
if (disposed) return;
|
|
1813
|
+
setHovered(null, "programmatic");
|
|
1814
|
+
if (bindings.length === 0) {
|
|
1815
|
+
manifest = null;
|
|
1816
|
+
options.onManifestChange?.(true);
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
const style = options.map.getStyle();
|
|
1820
|
+
if (style === void 0) {
|
|
1821
|
+
manifest = null;
|
|
1822
|
+
options.onManifestChange?.(false);
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
manifest = parsePoiManifest(style, options.onDiagnostic);
|
|
1826
|
+
options.onManifestChange?.(manifest !== null);
|
|
1827
|
+
};
|
|
1828
|
+
let manifestRefreshScheduled = false;
|
|
1829
|
+
const scheduleManifestRefresh = () => {
|
|
1830
|
+
if (disposed || manifestRefreshScheduled) return;
|
|
1831
|
+
manifestRefreshScheduled = true;
|
|
1832
|
+
queueMicrotask(() => {
|
|
1833
|
+
if (!manifestRefreshScheduled) return;
|
|
1834
|
+
manifestRefreshScheduled = false;
|
|
1835
|
+
refreshManifest();
|
|
1836
|
+
});
|
|
1837
|
+
};
|
|
1838
|
+
const handleStyleLoad = () => {
|
|
1839
|
+
manifestRefreshScheduled = false;
|
|
1840
|
+
refreshManifest();
|
|
1841
|
+
};
|
|
1842
|
+
try {
|
|
1843
|
+
for (const [event, listener] of [
|
|
1844
|
+
["mousemove", handleMove],
|
|
1845
|
+
["mouseout", handleLeave],
|
|
1846
|
+
["click", handleClick],
|
|
1847
|
+
["styledata", scheduleManifestRefresh],
|
|
1848
|
+
["style.load", handleStyleLoad]
|
|
1849
|
+
]) {
|
|
1850
|
+
subscriptionDisposers.push({
|
|
1851
|
+
active: true,
|
|
1852
|
+
dispose: subscribe(options.map, event, listener)
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
refreshManifest();
|
|
1856
|
+
} catch (error) {
|
|
1857
|
+
disposed = true;
|
|
1858
|
+
const rollbackErrors = disposeSubscriptions(subscriptionDisposers);
|
|
1859
|
+
throwWithCleanupErrors(
|
|
1860
|
+
error,
|
|
1861
|
+
rollbackErrors,
|
|
1862
|
+
"Tileflow semantic POI controller construction failed during cleanup."
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
dispose() {
|
|
1867
|
+
if (disposeComplete) return;
|
|
1868
|
+
disposed = true;
|
|
1869
|
+
if (framePendingCancellation === null) framePendingCancellation = pendingFrame;
|
|
1870
|
+
pendingFrame = null;
|
|
1871
|
+
pendingMove = null;
|
|
1872
|
+
const errors = [];
|
|
1873
|
+
if (framePendingCancellation !== null) {
|
|
1874
|
+
try {
|
|
1875
|
+
options.cancelFrame(framePendingCancellation);
|
|
1876
|
+
framePendingCancellation = null;
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
errors.push(error);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
errors.push(...disposeSubscriptions(subscriptionDisposers));
|
|
1882
|
+
if (hovered) {
|
|
1883
|
+
hovered = null;
|
|
1884
|
+
hoverClearPending = true;
|
|
1885
|
+
}
|
|
1886
|
+
if (hoverClearPending) {
|
|
1887
|
+
try {
|
|
1888
|
+
options.onHoverChange?.(null);
|
|
1889
|
+
hoverClearPending = false;
|
|
1890
|
+
} catch (error) {
|
|
1891
|
+
errors.push(error);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
if (errors.length > 0) {
|
|
1895
|
+
throwCollectedErrors2(errors, "Unable to dispose the Tileflow semantic POI controller.");
|
|
1896
|
+
}
|
|
1897
|
+
disposeComplete = true;
|
|
1898
|
+
},
|
|
1899
|
+
getHovered: () => hovered,
|
|
1900
|
+
reconcile(nextBindings) {
|
|
1901
|
+
if (disposed) throw new Error("Tileflow semantic POI controller is disposed.");
|
|
1902
|
+
const previouslyRequiredManifest = bindings.length > 0;
|
|
1903
|
+
bindings = semanticPoiBindings(nextBindings, options.onDiagnostic);
|
|
1904
|
+
if (bindings.length === 0 || !previouslyRequiredManifest || manifest === null) {
|
|
1905
|
+
refreshManifest();
|
|
1906
|
+
}
|
|
1907
|
+
if (hovered) {
|
|
1908
|
+
const replacement = bindings.find(
|
|
1909
|
+
(binding) => binding.id === hovered?.binding.id && bindingMatchesTarget(binding, hovered.target)
|
|
1910
|
+
);
|
|
1911
|
+
if (!replacement) setHovered(null, "programmatic");
|
|
1912
|
+
else if (!Object.is(replacement, hovered.binding)) {
|
|
1913
|
+
hovered = {
|
|
1914
|
+
binding: replacement,
|
|
1915
|
+
target: { ...hovered.target, bindingId: replacement.id }
|
|
1916
|
+
};
|
|
1917
|
+
options.onHoverChange?.(hovered);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
},
|
|
1921
|
+
refreshManifest
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
function semanticPoiBindings(bindings, onDiagnostic) {
|
|
1925
|
+
const result = [];
|
|
1926
|
+
let hasUnsupportedTarget = false;
|
|
1927
|
+
for (const binding of bindings) {
|
|
1928
|
+
if (binding.target.kind === "semantic-feature" && binding.target.domain === "poi") {
|
|
1929
|
+
result.push(binding);
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
hasUnsupportedTarget = true;
|
|
1933
|
+
}
|
|
1934
|
+
if (hasUnsupportedTarget) {
|
|
1935
|
+
diagnostic(
|
|
1936
|
+
onDiagnostic,
|
|
1937
|
+
"UNSUPPORTED_TARGET",
|
|
1938
|
+
"One or more Tileflow interaction targets are not available in the current semantic runtime."
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
function parsePoiManifest(styleInput, onDiagnostic) {
|
|
1944
|
+
const style = asRecord(styleInput);
|
|
1945
|
+
const metadata = asRecord(style?.metadata);
|
|
1946
|
+
const artifact = asRecord(metadata?.[interactionManifestMetadataKey]);
|
|
1947
|
+
const domains = asRecord(artifact?.domains);
|
|
1948
|
+
const poi = asRecord(domains?.poi);
|
|
1949
|
+
if (!artifact || !poi) {
|
|
1950
|
+
diagnostic(
|
|
1951
|
+
onDiagnostic,
|
|
1952
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
1953
|
+
"This style does not include Tileflow semantic POI interaction metadata.",
|
|
1954
|
+
"warning"
|
|
1955
|
+
);
|
|
1956
|
+
return null;
|
|
1957
|
+
}
|
|
1958
|
+
if (artifact.version !== interactionManifestVersion) {
|
|
1959
|
+
diagnostic(
|
|
1960
|
+
onDiagnostic,
|
|
1961
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
1962
|
+
"This style uses an unsupported Tileflow interaction metadata version."
|
|
1963
|
+
);
|
|
1964
|
+
return null;
|
|
1965
|
+
}
|
|
1966
|
+
const fields = asRecord(poi.fields);
|
|
1967
|
+
const deduplication = asRecord(poi.deduplication);
|
|
1968
|
+
const hitTesting = asRecord(poi.hitTesting);
|
|
1969
|
+
const identityFields = Array.isArray(deduplication?.identity) ? deduplication.identity : [];
|
|
1970
|
+
const representationPriority = Array.isArray(deduplication?.representationPriority) ? deduplication.representationPriority : [];
|
|
1971
|
+
const layerInputs = Array.isArray(poi.layers) ? poi.layers : [];
|
|
1972
|
+
const styleLayers = Array.isArray(style?.layers) ? style.layers : [];
|
|
1973
|
+
const styleLayersById = new Map(
|
|
1974
|
+
styleLayers.flatMap((layer) => {
|
|
1975
|
+
const record = asRecord(layer);
|
|
1976
|
+
const id = record?.id;
|
|
1977
|
+
return typeof id === "string" && record ? [[id, record]] : [];
|
|
1978
|
+
})
|
|
1979
|
+
);
|
|
1980
|
+
if (!fields || !["category", "filterRank", "icon", "name", "sizeRank", "type"].every(
|
|
1981
|
+
(field) => isSafeManifestName(fields[field])
|
|
1982
|
+
) || poi.identity !== "maplibre-feature-id-if-present" || identityFields.length !== 3 || identityFields[0] !== "source" || identityFields[1] !== "source-layer" || identityFields[2] !== "feature-id" || !isRepresentationPriority(representationPriority) || hitTesting?.frequency !== "animation-frame" || hitTesting.order !== "rendered-topmost" || layerInputs.length === 0 || layerInputs.length > 256) {
|
|
1983
|
+
diagnostic(onDiagnostic, "SEMANTIC_MANIFEST_MISMATCH", "Invalid semantic POI metadata.");
|
|
1984
|
+
return null;
|
|
1985
|
+
}
|
|
1986
|
+
const layers = [];
|
|
1987
|
+
const ids = /* @__PURE__ */ new Set();
|
|
1988
|
+
let identityNamespace;
|
|
1989
|
+
for (const input of layerInputs) {
|
|
1990
|
+
const layer = asRecord(input);
|
|
1991
|
+
const sourceLayer = layer?.sourceLayer;
|
|
1992
|
+
if (!layer || !isTileflowPoiCategory(layer.category) || !isSafeManifestName(layer.layerId) || !isSafeManifestName(layer.source) || sourceLayer !== void 0 && !isSafeManifestName(sourceLayer) || layer.anchor !== "pointer-coordinate" || !isPoiRepresentation(layer.representation) || !Number.isSafeInteger(layer.priority) || layer.priority < 0 || layer.priority > 1e5 || ids.has(layer.layerId)) {
|
|
1993
|
+
diagnostic(onDiagnostic, "SEMANTIC_MANIFEST_MISMATCH", "Invalid semantic POI layer lookup.");
|
|
1994
|
+
return null;
|
|
1995
|
+
}
|
|
1996
|
+
const styleLayer = styleLayersById.get(layer.layerId);
|
|
1997
|
+
if (!styleLayer || styleLayer.source !== layer.source || styleLayer["source-layer"] !== sourceLayer) {
|
|
1998
|
+
diagnostic(
|
|
1999
|
+
onDiagnostic,
|
|
2000
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
2001
|
+
"Semantic POI metadata does not match the finalized style layer."
|
|
2002
|
+
);
|
|
2003
|
+
return null;
|
|
2004
|
+
}
|
|
2005
|
+
const namespace = [
|
|
2006
|
+
layer.source,
|
|
2007
|
+
sourceLayerNamespace(sourceLayer)
|
|
2008
|
+
];
|
|
2009
|
+
if (identityNamespace && (identityNamespace[0] !== namespace[0] || identityNamespace[1] !== namespace[1])) {
|
|
2010
|
+
diagnostic(
|
|
2011
|
+
onDiagnostic,
|
|
2012
|
+
"SEMANTIC_MANIFEST_MISMATCH",
|
|
2013
|
+
"Semantic POI metadata spans multiple feature-ID namespaces."
|
|
2014
|
+
);
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
identityNamespace = namespace;
|
|
2018
|
+
ids.add(layer.layerId);
|
|
2019
|
+
layers.push({
|
|
2020
|
+
category: layer.category,
|
|
2021
|
+
layerId: layer.layerId,
|
|
2022
|
+
priority: layer.priority,
|
|
2023
|
+
representation: layer.representation,
|
|
2024
|
+
source: layer.source,
|
|
2025
|
+
...sourceLayer === void 0 ? {} : { sourceLayer }
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
return {
|
|
2029
|
+
deduplication: {
|
|
2030
|
+
representationPriority
|
|
2031
|
+
},
|
|
2032
|
+
fields: {
|
|
2033
|
+
category: fields.category,
|
|
2034
|
+
filterRank: fields.filterRank,
|
|
2035
|
+
icon: fields.icon,
|
|
2036
|
+
name: fields.name,
|
|
2037
|
+
sizeRank: fields.sizeRank,
|
|
2038
|
+
type: fields.type
|
|
2039
|
+
},
|
|
2040
|
+
layers: layers.sort((left, right) => right.priority - left.priority)
|
|
2041
|
+
};
|
|
2042
|
+
}
|
|
2043
|
+
function choosePoiCandidate(candidates, manifest) {
|
|
2044
|
+
const deduplicated = [];
|
|
2045
|
+
const indexByIdentity = /* @__PURE__ */ new Map();
|
|
2046
|
+
for (const candidate of candidates) {
|
|
2047
|
+
const id = candidate.match.target.feature.id;
|
|
2048
|
+
if (id === void 0) {
|
|
2049
|
+
deduplicated.push(candidate);
|
|
2050
|
+
continue;
|
|
2051
|
+
}
|
|
2052
|
+
const identity = `${candidate.layer.source}\0${sourceLayerNamespace(candidate.layer.sourceLayer)}\0${typeof id}\0${String(id)}`;
|
|
2053
|
+
const existingIndex = indexByIdentity.get(identity);
|
|
2054
|
+
if (existingIndex === void 0) {
|
|
2055
|
+
indexByIdentity.set(identity, deduplicated.length);
|
|
2056
|
+
deduplicated.push(candidate);
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
const existing = deduplicated[existingIndex];
|
|
2060
|
+
if (preferPoiRepresentation(candidate, existing, manifest)) {
|
|
2061
|
+
deduplicated[existingIndex] = {
|
|
2062
|
+
...candidate,
|
|
2063
|
+
renderedIndex: Math.min(existing.renderedIndex, candidate.renderedIndex)
|
|
2064
|
+
};
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return deduplicated.sort(
|
|
2068
|
+
(left, right) => left.renderedIndex - right.renderedIndex || right.layer.priority - left.layer.priority
|
|
2069
|
+
)[0];
|
|
2070
|
+
}
|
|
2071
|
+
function preferPoiRepresentation(candidate, existing, manifest) {
|
|
2072
|
+
const priority = manifest.deduplication.representationPriority;
|
|
2073
|
+
const candidateIndex = priority.indexOf(candidate.layer.representation);
|
|
2074
|
+
const existingIndex = priority.indexOf(existing.layer.representation);
|
|
2075
|
+
return candidateIndex < existingIndex || candidateIndex === existingIndex && candidate.layer.priority > existing.layer.priority;
|
|
2076
|
+
}
|
|
2077
|
+
function isRepresentationPriority(input) {
|
|
2078
|
+
return input.length === 4 && input.every(isPoiRepresentation) && new Set(input).size === input.length;
|
|
2079
|
+
}
|
|
2080
|
+
function isPoiRepresentation(value) {
|
|
2081
|
+
return value === "combined" || value === "icon" || value === "label" || value === "marker";
|
|
2082
|
+
}
|
|
2083
|
+
function isTileflowPoiCategory(value) {
|
|
2084
|
+
return typeof value === "string" && tileflowPoiCategories.includes(value);
|
|
2085
|
+
}
|
|
2086
|
+
function sourceLayerNamespace(sourceLayer) {
|
|
2087
|
+
return sourceLayer ?? missingSourceLayerNamespace;
|
|
2088
|
+
}
|
|
2089
|
+
function relevantLayerIds(manifest, bindings) {
|
|
2090
|
+
return manifest.layers.filter(
|
|
2091
|
+
(layer) => bindings.some(
|
|
2092
|
+
(binding) => binding.target.kind === "semantic-feature" && (!binding.target.categories || binding.target.categories.includes(layer.category))
|
|
2093
|
+
)
|
|
2094
|
+
).map((layer) => layer.layerId);
|
|
2095
|
+
}
|
|
2096
|
+
function normalizePoiProperties(properties, fields) {
|
|
2097
|
+
const source = properties ?? {};
|
|
2098
|
+
const normalized = {};
|
|
2099
|
+
const read = (field) => {
|
|
2100
|
+
try {
|
|
2101
|
+
return source[fields[field]];
|
|
2102
|
+
} catch {
|
|
2103
|
+
return void 0;
|
|
2104
|
+
}
|
|
2105
|
+
};
|
|
2106
|
+
const category = read("category");
|
|
2107
|
+
const filterRank = read("filterRank");
|
|
2108
|
+
const icon = read("icon");
|
|
2109
|
+
const name = read("name");
|
|
2110
|
+
const sizeRank = read("sizeRank");
|
|
2111
|
+
const type = read("type");
|
|
2112
|
+
if (isTileflowPoiCategory(category)) normalized.category = category;
|
|
2113
|
+
if (isIntegerInRange(filterRank, 0, 5)) {
|
|
2114
|
+
normalized.filter_rank = filterRank;
|
|
2115
|
+
}
|
|
2116
|
+
if (isSnakeCaseValue(icon)) normalized.icon = icon;
|
|
2117
|
+
if (isBoundedText(name)) normalized.name = name;
|
|
2118
|
+
if (isIntegerInRange(sizeRank, 0, 16)) {
|
|
2119
|
+
normalized.size_rank = sizeRank;
|
|
2120
|
+
}
|
|
2121
|
+
if (isSnakeCaseValue(type)) normalized.type = type;
|
|
2122
|
+
return Object.freeze(normalized);
|
|
2123
|
+
}
|
|
2124
|
+
function isIntegerInRange(value, minimum, maximum) {
|
|
2125
|
+
return Number.isSafeInteger(value) && value >= minimum && value <= maximum;
|
|
2126
|
+
}
|
|
2127
|
+
function isBoundedText(value) {
|
|
2128
|
+
return typeof value === "string" && value.length <= tileflowInteractionLimits.maxContentTextLength;
|
|
2129
|
+
}
|
|
2130
|
+
function isSnakeCaseValue(value) {
|
|
2131
|
+
return isBoundedText(value) && /^[a-z0-9]+(?:_[a-z0-9]+)*$/u.test(value);
|
|
2132
|
+
}
|
|
2133
|
+
function normalizeFeatureId(value) {
|
|
2134
|
+
if (typeof value === "number") return Number.isSafeInteger(value) ? value : void 0;
|
|
2135
|
+
if (typeof value === "string" && value.length > 0 && value.length <= tileflowInteractionLimits.maxFeatureIdLength) {
|
|
2136
|
+
return value;
|
|
2137
|
+
}
|
|
2138
|
+
return void 0;
|
|
2139
|
+
}
|
|
2140
|
+
function matchesEqual(left, right) {
|
|
2141
|
+
if (left === right) return true;
|
|
2142
|
+
if (!left || !right || left.binding.id !== right.binding.id) return false;
|
|
2143
|
+
const leftFeature = left.target.feature;
|
|
2144
|
+
const rightFeature = right.target.feature;
|
|
2145
|
+
if (leftFeature.id !== void 0 || rightFeature.id !== void 0) {
|
|
2146
|
+
return leftFeature.id === rightFeature.id && left.target.domain === right.target.domain;
|
|
2147
|
+
}
|
|
2148
|
+
return leftFeature.category === rightFeature.category && JSON.stringify(leftFeature.properties) === JSON.stringify(rightFeature.properties);
|
|
2149
|
+
}
|
|
2150
|
+
function bindingMatchesTarget(binding, target) {
|
|
2151
|
+
const selector = binding.target;
|
|
2152
|
+
if (selector.kind !== "semantic-feature" || selector.domain !== target.domain || selector.domain !== "poi") {
|
|
2153
|
+
return false;
|
|
2154
|
+
}
|
|
2155
|
+
return !selector.categories || target.feature.category !== void 0 && selector.categories.includes(target.feature.category);
|
|
2156
|
+
}
|
|
2157
|
+
function parsePointerEvent(value) {
|
|
2158
|
+
const event = asRecord(value);
|
|
2159
|
+
const lngLat = asRecord(event?.lngLat);
|
|
2160
|
+
if (!event || !lngLat || event.point === void 0 || typeof lngLat.lng !== "number" || !Number.isFinite(lngLat.lng) || typeof lngLat.lat !== "number" || !Number.isFinite(lngLat.lat)) {
|
|
2161
|
+
return null;
|
|
2162
|
+
}
|
|
2163
|
+
return value;
|
|
2164
|
+
}
|
|
2165
|
+
function inputModality(event) {
|
|
2166
|
+
return event.originalEvent?.pointerType === "touch" || event.originalEvent?.touches !== void 0 ? "touch" : "pointer";
|
|
2167
|
+
}
|
|
2168
|
+
function subscribe(map, event, listener) {
|
|
2169
|
+
const subscription = map.on(event, listener);
|
|
2170
|
+
if (typeof subscription?.unsubscribe === "function") return () => subscription.unsubscribe?.();
|
|
2171
|
+
return () => map.off?.(event, listener);
|
|
2172
|
+
}
|
|
2173
|
+
function disposeSubscriptions(subscriptions) {
|
|
2174
|
+
const errors = [];
|
|
2175
|
+
for (const subscription of [...subscriptions].reverse()) {
|
|
2176
|
+
if (!subscription.active) continue;
|
|
2177
|
+
try {
|
|
2178
|
+
subscription.dispose();
|
|
2179
|
+
subscription.active = false;
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
errors.push(error);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
return errors;
|
|
2185
|
+
}
|
|
2186
|
+
function throwWithCleanupErrors(error, cleanupErrors, message) {
|
|
2187
|
+
if (cleanupErrors.length === 0) throw error;
|
|
2188
|
+
throw new AggregateError([error, ...cleanupErrors], message, { cause: error });
|
|
2189
|
+
}
|
|
2190
|
+
function throwCollectedErrors2(errors, message) {
|
|
2191
|
+
if (errors.length === 0) return;
|
|
2192
|
+
if (errors.length === 1) throw errors[0];
|
|
2193
|
+
throw new AggregateError(errors, message);
|
|
2194
|
+
}
|
|
2195
|
+
function diagnostic(listener, code, message, level = "error") {
|
|
2196
|
+
listener?.({ code, level, message });
|
|
2197
|
+
}
|
|
2198
|
+
function isSafeManifestName(value) {
|
|
2199
|
+
return typeof value === "string" && value.length > 0 && value.length <= 256 && ![...value].some((character) => {
|
|
2200
|
+
const code = character.charCodeAt(0);
|
|
2201
|
+
return code <= 31 || code === 127;
|
|
2202
|
+
});
|
|
2203
|
+
}
|
|
2204
|
+
function asRecord(value) {
|
|
2205
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
// src/maplibre-semantic-dom.ts
|
|
2209
|
+
var semanticRuntimeSequence = 0;
|
|
2210
|
+
function createTileflowMapLibreSemanticDomRuntime(options) {
|
|
2211
|
+
const controlled = options.interactionState !== void 0;
|
|
2212
|
+
const idPrefix = options.idPrefix ?? `tileflow-semantic-${++semanticRuntimeSequence}`;
|
|
2213
|
+
assertIdPrefix2(idPrefix);
|
|
2214
|
+
let bindings = Object.freeze([]);
|
|
2215
|
+
let interactionState = options.interactionState ?? options.defaultInteractionState ?? Object.freeze({ popup: null });
|
|
2216
|
+
let customRenderers = Object.freeze({
|
|
2217
|
+
popup: options.customPopup ?? false,
|
|
2218
|
+
tooltip: options.customTooltip ?? false
|
|
2219
|
+
});
|
|
2220
|
+
let pendingStateRequest = null;
|
|
2221
|
+
let hoveredMatch = null;
|
|
2222
|
+
let activeTooltip = null;
|
|
2223
|
+
let activePopup = null;
|
|
2224
|
+
let renderTargets = Object.freeze([]);
|
|
2225
|
+
let disposed = false;
|
|
2226
|
+
let disposeComplete = false;
|
|
2227
|
+
let overlaySequence = 0;
|
|
2228
|
+
const pendingCleanupTasks = /* @__PURE__ */ new Set();
|
|
2229
|
+
const eventListeners = /* @__PURE__ */ new Set();
|
|
2230
|
+
const renderTargetListeners = /* @__PURE__ */ new Set();
|
|
2231
|
+
const diagnosticListeners = /* @__PURE__ */ new Set();
|
|
2232
|
+
const diagnosticsByKey = /* @__PURE__ */ new Map();
|
|
2233
|
+
let diagnostics = Object.freeze([]);
|
|
2234
|
+
const publishRenderTargets = () => {
|
|
2235
|
+
if (disposed) return;
|
|
2236
|
+
renderTargets = buildRenderTargets();
|
|
2237
|
+
notify(renderTargetListeners, renderTargets);
|
|
2238
|
+
};
|
|
2239
|
+
const publishDiagnostics = () => {
|
|
2240
|
+
if (disposed) return;
|
|
2241
|
+
diagnostics = Object.freeze([...diagnosticsByKey.values()]);
|
|
2242
|
+
notify(diagnosticListeners, diagnostics);
|
|
2243
|
+
};
|
|
2244
|
+
const reportDiagnostic = (key, diagnostic2) => {
|
|
2245
|
+
if (diagnosticsByKey.has(key)) return;
|
|
2246
|
+
const frozen = Object.freeze(diagnostic2);
|
|
2247
|
+
diagnosticsByKey.set(key, frozen);
|
|
2248
|
+
publishDiagnostics();
|
|
2249
|
+
options.onDiagnostic?.(frozen);
|
|
2250
|
+
};
|
|
2251
|
+
const clearDiagnostic = (key) => {
|
|
2252
|
+
if (!diagnosticsByKey.delete(key)) return;
|
|
2253
|
+
publishDiagnostics();
|
|
2254
|
+
};
|
|
2255
|
+
const retireResources = (operations) => {
|
|
2256
|
+
const task = {
|
|
2257
|
+
operations: operations.map((run) => ({ complete: false, run }))
|
|
2258
|
+
};
|
|
2259
|
+
pendingCleanupTasks.add(task);
|
|
2260
|
+
const errors = runCleanupTask(task);
|
|
2261
|
+
if (cleanupTaskComplete(task)) pendingCleanupTasks.delete(task);
|
|
2262
|
+
return errors;
|
|
2263
|
+
};
|
|
2264
|
+
const flushPendingCleanupTasks = () => {
|
|
2265
|
+
const errors = [];
|
|
2266
|
+
for (const task of pendingCleanupTasks) {
|
|
2267
|
+
errors.push(...runCleanupTask(task));
|
|
2268
|
+
if (cleanupTaskComplete(task)) pendingCleanupTasks.delete(task);
|
|
2269
|
+
}
|
|
2270
|
+
return errors;
|
|
2271
|
+
};
|
|
2272
|
+
const emit = (event) => notify(eventListeners, event);
|
|
2273
|
+
const emitPopupEvent = (type, match, inputModality2) => {
|
|
2274
|
+
emit(
|
|
2275
|
+
Object.freeze({
|
|
2276
|
+
bindingId: match.binding.id,
|
|
2277
|
+
coordinate: match.target.coordinate,
|
|
2278
|
+
inputModality: inputModality2,
|
|
2279
|
+
target: match.target,
|
|
2280
|
+
type
|
|
2281
|
+
})
|
|
2282
|
+
);
|
|
2283
|
+
};
|
|
2284
|
+
const closeTooltipVisual = () => {
|
|
2285
|
+
const current = activeTooltip;
|
|
2286
|
+
if (!current) return false;
|
|
2287
|
+
activeTooltip = null;
|
|
2288
|
+
const errors = retireResources([() => current.overlay.remove(), () => current.shell.remove()]);
|
|
2289
|
+
errors.push(
|
|
2290
|
+
...safelyRunAll2([
|
|
2291
|
+
() => clearSurfaceDiagnostics(current.match, "tooltip"),
|
|
2292
|
+
() => publishRenderTargets()
|
|
2293
|
+
])
|
|
2294
|
+
);
|
|
2295
|
+
throwCollectedErrors3(errors, "Unable to close the Tileflow semantic tooltip.");
|
|
2296
|
+
return true;
|
|
2297
|
+
};
|
|
2298
|
+
const closePopupVisual = (inputModality2) => {
|
|
2299
|
+
const current = activePopup;
|
|
2300
|
+
if (!current) return false;
|
|
2301
|
+
activePopup = null;
|
|
2302
|
+
const errors = retireResources([
|
|
2303
|
+
...current.closeButton && current.closeListener ? [() => current.closeButton?.removeEventListener("click", current.closeListener)] : [],
|
|
2304
|
+
...current.keydownListener ? [() => current.shell.removeEventListener("keydown", current.keydownListener)] : [],
|
|
2305
|
+
() => current.overlay.remove(),
|
|
2306
|
+
() => current.shell.remove()
|
|
2307
|
+
]);
|
|
2308
|
+
errors.push(
|
|
2309
|
+
...safelyRunAll2([
|
|
2310
|
+
() => clearSurfaceDiagnostics(current.match, "popup"),
|
|
2311
|
+
() => publishRenderTargets(),
|
|
2312
|
+
() => emitPopupEvent("popup:close", current.match, inputModality2)
|
|
2313
|
+
])
|
|
2314
|
+
);
|
|
2315
|
+
throwCollectedErrors3(errors, "Unable to close the Tileflow semantic popup.");
|
|
2316
|
+
return true;
|
|
2317
|
+
};
|
|
2318
|
+
const configureOverlayContent = (current) => {
|
|
2319
|
+
const surface = current.match.binding[current.kind];
|
|
2320
|
+
if (!surface) return;
|
|
2321
|
+
configureViewAttribute2(current.container, surface.content);
|
|
2322
|
+
const text = resolveSemanticContent(current.match, current.kind, surface.content);
|
|
2323
|
+
if (!customRenderers[current.kind]) current.container.textContent = text;
|
|
2324
|
+
};
|
|
2325
|
+
const resolveSemanticContent = (match, kind, content) => {
|
|
2326
|
+
const missingViewKey = surfaceDiagnosticKey(match, kind, "MISSING_VIEW");
|
|
2327
|
+
const invalidFieldKey = surfaceDiagnosticKey(match, kind, "INVALID_FIELD");
|
|
2328
|
+
if (content.kind === "view") {
|
|
2329
|
+
clearDiagnostic(invalidFieldKey);
|
|
2330
|
+
if (customRenderers[kind]) clearDiagnostic(missingViewKey);
|
|
2331
|
+
else {
|
|
2332
|
+
const target = semanticTargetReference(match.target);
|
|
2333
|
+
reportDiagnostic(missingViewKey, {
|
|
2334
|
+
code: "MISSING_VIEW",
|
|
2335
|
+
level: "error",
|
|
2336
|
+
message: "Semantic view content has no custom renderer for its active surface.",
|
|
2337
|
+
...target ? { target } : {}
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
return "";
|
|
2341
|
+
}
|
|
2342
|
+
clearDiagnostic(missingViewKey);
|
|
2343
|
+
const resolution = resolveContent2(content, match.target.feature.properties);
|
|
2344
|
+
if (resolution.invalidField) {
|
|
2345
|
+
const target = semanticTargetReference(match.target);
|
|
2346
|
+
reportDiagnostic(invalidFieldKey, {
|
|
2347
|
+
code: "INVALID_FIELD",
|
|
2348
|
+
level: "error",
|
|
2349
|
+
message: "Semantic feature field content did not resolve to a scalar value.",
|
|
2350
|
+
...target ? { target } : {}
|
|
2351
|
+
});
|
|
2352
|
+
} else clearDiagnostic(invalidFieldKey);
|
|
2353
|
+
return resolution.text;
|
|
2354
|
+
};
|
|
2355
|
+
function clearSurfaceDiagnostics(match, kind) {
|
|
2356
|
+
clearDiagnostic(surfaceDiagnosticKey(match, kind, "MISSING_VIEW"));
|
|
2357
|
+
clearDiagnostic(surfaceDiagnosticKey(match, kind, "INVALID_FIELD"));
|
|
2358
|
+
}
|
|
2359
|
+
const openTooltipVisual = (match) => {
|
|
2360
|
+
if (!match.binding.tooltip || sameMatch(activePopup?.match, match)) {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
if (activeTooltip && sameMatch(activeTooltip.match, match)) {
|
|
2364
|
+
try {
|
|
2365
|
+
updateOverlayVisual(activeTooltip, match);
|
|
2366
|
+
clearDiagnostic(overlayDiagnosticKey2("tooltip"));
|
|
2367
|
+
} catch {
|
|
2368
|
+
reportOverlayFailure("tooltip", match.target);
|
|
2369
|
+
}
|
|
2370
|
+
return false;
|
|
2371
|
+
}
|
|
2372
|
+
closeTooltipVisual();
|
|
2373
|
+
const shell = options.document.createElement("div");
|
|
2374
|
+
const container = options.document.createElement("div");
|
|
2375
|
+
shell.className = "tileflow-interaction-tooltip";
|
|
2376
|
+
container.className = "tileflow-interaction-tooltip-content";
|
|
2377
|
+
shell.id = `${idPrefix}-tooltip-${++overlaySequence}`;
|
|
2378
|
+
shell.setAttribute("data-tileflow-interaction", "tooltip");
|
|
2379
|
+
shell.setAttribute("data-tileflow-target-kind", "semantic-feature");
|
|
2380
|
+
shell.setAttribute("role", "tooltip");
|
|
2381
|
+
shell.style.pointerEvents = "none";
|
|
2382
|
+
configureSurfaceStyles2(shell);
|
|
2383
|
+
shell.append(container);
|
|
2384
|
+
let overlay;
|
|
2385
|
+
try {
|
|
2386
|
+
overlay = options.createOverlay({
|
|
2387
|
+
binding: match.binding,
|
|
2388
|
+
container: shell,
|
|
2389
|
+
kind: "tooltip",
|
|
2390
|
+
target: match.target
|
|
2391
|
+
});
|
|
2392
|
+
overlay.setLngLat(copyCoordinate2(match.target.coordinate));
|
|
2393
|
+
overlay.addTo(options.map);
|
|
2394
|
+
} catch {
|
|
2395
|
+
retireResources([() => overlay?.remove(), () => shell.remove()]);
|
|
2396
|
+
reportOverlayFailure("tooltip", match.target);
|
|
2397
|
+
return false;
|
|
2398
|
+
}
|
|
2399
|
+
activeTooltip = { container, kind: "tooltip", match, overlay, shell };
|
|
2400
|
+
configureOverlayContent(activeTooltip);
|
|
2401
|
+
clearDiagnostic(overlayDiagnosticKey2("tooltip"));
|
|
2402
|
+
publishRenderTargets();
|
|
2403
|
+
return true;
|
|
2404
|
+
};
|
|
2405
|
+
const openPopupVisual = (match, inputModality2) => {
|
|
2406
|
+
if (!match.binding.popup) return false;
|
|
2407
|
+
if (activePopup && sameMatch(activePopup.match, match)) {
|
|
2408
|
+
try {
|
|
2409
|
+
updateOverlayVisual(activePopup, match);
|
|
2410
|
+
clearDiagnostic(overlayDiagnosticKey2("popup"));
|
|
2411
|
+
} catch {
|
|
2412
|
+
reportOverlayFailure("popup", match.target);
|
|
2413
|
+
}
|
|
2414
|
+
return false;
|
|
2415
|
+
}
|
|
2416
|
+
closePopupVisual(inputModality2);
|
|
2417
|
+
closeTooltipVisual();
|
|
2418
|
+
const shell = options.document.createElement("div");
|
|
2419
|
+
const container = options.document.createElement("div");
|
|
2420
|
+
const closeButton = options.document.createElement("button");
|
|
2421
|
+
const label = semanticAccessibleLabel(match.target);
|
|
2422
|
+
shell.className = "tileflow-interaction-popup";
|
|
2423
|
+
container.className = "tileflow-interaction-popup-content";
|
|
2424
|
+
closeButton.className = "tileflow-interaction-popup-close";
|
|
2425
|
+
closeButton.type = "button";
|
|
2426
|
+
closeButton.textContent = "Close";
|
|
2427
|
+
shell.id = `${idPrefix}-popup-${++overlaySequence}`;
|
|
2428
|
+
shell.setAttribute("data-tileflow-interaction", "popup");
|
|
2429
|
+
shell.setAttribute("data-tileflow-target-kind", "semantic-feature");
|
|
2430
|
+
shell.setAttribute("aria-label", label);
|
|
2431
|
+
shell.setAttribute("aria-modal", "false");
|
|
2432
|
+
shell.setAttribute("role", "dialog");
|
|
2433
|
+
closeButton.setAttribute("aria-label", `Close ${label}`);
|
|
2434
|
+
configureSurfaceStyles2(shell);
|
|
2435
|
+
closeButton.style.background = "transparent";
|
|
2436
|
+
closeButton.style.border = "0";
|
|
2437
|
+
closeButton.style.color = "inherit";
|
|
2438
|
+
closeButton.style.cursor = "pointer";
|
|
2439
|
+
shell.append(container, closeButton);
|
|
2440
|
+
const closeListener = () => requestPopupState(null, "pointer");
|
|
2441
|
+
const keydownListener = (event) => {
|
|
2442
|
+
if (event.key !== "Escape") return;
|
|
2443
|
+
event.preventDefault();
|
|
2444
|
+
event.stopPropagation();
|
|
2445
|
+
requestPopupState(null, "keyboard");
|
|
2446
|
+
};
|
|
2447
|
+
closeButton.addEventListener("click", closeListener);
|
|
2448
|
+
shell.addEventListener("keydown", keydownListener);
|
|
2449
|
+
let overlay;
|
|
2450
|
+
try {
|
|
2451
|
+
overlay = options.createOverlay({
|
|
2452
|
+
binding: match.binding,
|
|
2453
|
+
container: shell,
|
|
2454
|
+
kind: "popup",
|
|
2455
|
+
target: match.target
|
|
2456
|
+
});
|
|
2457
|
+
overlay.setLngLat(copyCoordinate2(match.target.coordinate));
|
|
2458
|
+
overlay.addTo(options.map);
|
|
2459
|
+
} catch {
|
|
2460
|
+
retireResources([
|
|
2461
|
+
() => closeButton.removeEventListener("click", closeListener),
|
|
2462
|
+
() => shell.removeEventListener("keydown", keydownListener),
|
|
2463
|
+
() => overlay?.remove(),
|
|
2464
|
+
() => shell.remove()
|
|
2465
|
+
]);
|
|
2466
|
+
reportOverlayFailure("popup", match.target);
|
|
2467
|
+
return false;
|
|
2468
|
+
}
|
|
2469
|
+
activePopup = {
|
|
2470
|
+
closeListener,
|
|
2471
|
+
closeButton,
|
|
2472
|
+
container,
|
|
2473
|
+
keydownListener,
|
|
2474
|
+
kind: "popup",
|
|
2475
|
+
match,
|
|
2476
|
+
overlay,
|
|
2477
|
+
shell
|
|
2478
|
+
};
|
|
2479
|
+
configureOverlayContent(activePopup);
|
|
2480
|
+
clearDiagnostic(overlayDiagnosticKey2("popup"));
|
|
2481
|
+
publishRenderTargets();
|
|
2482
|
+
emitPopupEvent("popup:open", match, inputModality2);
|
|
2483
|
+
closeButton.focus({ preventScroll: true });
|
|
2484
|
+
return true;
|
|
2485
|
+
};
|
|
2486
|
+
const updateOverlayVisual = (current, match) => {
|
|
2487
|
+
current.match = match;
|
|
2488
|
+
current.overlay.setLngLat(copyCoordinate2(match.target.coordinate));
|
|
2489
|
+
if (current.kind === "popup") {
|
|
2490
|
+
const label = semanticAccessibleLabel(match.target);
|
|
2491
|
+
current.shell.setAttribute("aria-label", label);
|
|
2492
|
+
current.closeButton?.setAttribute("aria-label", `Close ${label}`);
|
|
2493
|
+
}
|
|
2494
|
+
configureOverlayContent(current);
|
|
2495
|
+
publishRenderTargets();
|
|
2496
|
+
};
|
|
2497
|
+
const reportOverlayFailure = (kind, target) => {
|
|
2498
|
+
const reference = semanticTargetReference(target);
|
|
2499
|
+
reportDiagnostic(overlayDiagnosticKey2(kind), {
|
|
2500
|
+
code: "OVERLAY_FAILURE",
|
|
2501
|
+
level: "error",
|
|
2502
|
+
message: `Tileflow could not mount or update the semantic ${kind} overlay.`,
|
|
2503
|
+
...reference ? { target: reference } : {}
|
|
2504
|
+
});
|
|
2505
|
+
};
|
|
2506
|
+
const resolveAvailableMatch = (reference) => {
|
|
2507
|
+
for (const match of [pendingStateRequest?.match, hoveredMatch, activePopup?.match]) {
|
|
2508
|
+
if (!match) continue;
|
|
2509
|
+
if (!tileflowInteractionTargetRefsEqual(semanticTargetReference(match.target), reference)) {
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
const rebound = rebindSemanticMatch(match, bindings, "popup");
|
|
2513
|
+
if (rebound) return rebound;
|
|
2514
|
+
}
|
|
2515
|
+
return null;
|
|
2516
|
+
};
|
|
2517
|
+
const invalidateResolvedMatches = () => {
|
|
2518
|
+
pendingStateRequest = null;
|
|
2519
|
+
hoveredMatch = null;
|
|
2520
|
+
const errors = safelyRunAll2([
|
|
2521
|
+
() => closeTooltipVisual(),
|
|
2522
|
+
() => closePopupVisual("programmatic")
|
|
2523
|
+
]);
|
|
2524
|
+
throwCollectedErrors3(errors, "Unable to invalidate stale Tileflow semantic targets.");
|
|
2525
|
+
};
|
|
2526
|
+
const syncPopupFromInteractionState = (inputModality2) => {
|
|
2527
|
+
const popup = interactionState.popup;
|
|
2528
|
+
if (popup?.kind !== "semantic-feature" || popup.domain !== "poi") {
|
|
2529
|
+
closePopupVisual(inputModality2);
|
|
2530
|
+
clearDiagnostic("state:stale");
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
const match = resolveAvailableMatch(popup);
|
|
2534
|
+
if (!match?.binding.popup) {
|
|
2535
|
+
closePopupVisual(inputModality2);
|
|
2536
|
+
reportDiagnostic("state:stale", {
|
|
2537
|
+
code: "STALE_TARGET",
|
|
2538
|
+
level: "warning",
|
|
2539
|
+
message: "The semantic popup target is not available in the current rendered map.",
|
|
2540
|
+
target: popup
|
|
2541
|
+
});
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
clearDiagnostic("state:stale");
|
|
2545
|
+
openPopupVisual(match, inputModality2);
|
|
2546
|
+
};
|
|
2547
|
+
function requestPopupState(match, inputModality2) {
|
|
2548
|
+
const target = match?.target ?? null;
|
|
2549
|
+
const targetRef = target ? semanticTargetReference(target) : null;
|
|
2550
|
+
if (target && !targetRef) return false;
|
|
2551
|
+
const next = Object.freeze({ popup: targetRef });
|
|
2552
|
+
if (tileflowInteractionTargetRefsEqual(interactionState.popup, next.popup)) {
|
|
2553
|
+
if (target && !sameSemanticTarget(activePopup?.match.target, target)) {
|
|
2554
|
+
syncPopupFromInteractionState(inputModality2);
|
|
2555
|
+
return activePopup !== null;
|
|
2556
|
+
}
|
|
2557
|
+
return false;
|
|
2558
|
+
}
|
|
2559
|
+
const previous = interactionState;
|
|
2560
|
+
pendingStateRequest = { inputModality: inputModality2, match };
|
|
2561
|
+
if (!controlled) {
|
|
2562
|
+
interactionState = next;
|
|
2563
|
+
syncPopupFromInteractionState(inputModality2);
|
|
2564
|
+
pendingStateRequest = null;
|
|
2565
|
+
}
|
|
2566
|
+
options.onInteractionStateChange?.(
|
|
2567
|
+
next,
|
|
2568
|
+
previous,
|
|
2569
|
+
next.popup === null ? "popup:close" : "popup:open"
|
|
2570
|
+
);
|
|
2571
|
+
return true;
|
|
2572
|
+
}
|
|
2573
|
+
function buildRenderTargets() {
|
|
2574
|
+
const targets = [];
|
|
2575
|
+
for (const current of [activeTooltip, activePopup]) {
|
|
2576
|
+
if (!current || !customRenderers[current.kind]) continue;
|
|
2577
|
+
const surface = current.match.binding[current.kind];
|
|
2578
|
+
if (!surface) continue;
|
|
2579
|
+
const reference = semanticReferenceKey(current.match.target) ?? "unstable";
|
|
2580
|
+
targets.push(
|
|
2581
|
+
Object.freeze({
|
|
2582
|
+
binding: current.match.binding,
|
|
2583
|
+
close: () => current.kind === "popup" ? requestPopupState(null, "programmatic") : closeTooltipVisual(),
|
|
2584
|
+
container: current.container,
|
|
2585
|
+
content: surface.content,
|
|
2586
|
+
key: `${current.kind}:${current.match.binding.id}:${reference}`,
|
|
2587
|
+
kind: current.kind,
|
|
2588
|
+
target: current.match.target,
|
|
2589
|
+
...surface.content.kind === "view" ? { viewName: surface.content.name } : {}
|
|
2590
|
+
})
|
|
2591
|
+
);
|
|
2592
|
+
}
|
|
2593
|
+
return Object.freeze(targets);
|
|
2594
|
+
}
|
|
2595
|
+
const poiController = createTileflowMapLibrePoiController({
|
|
2596
|
+
bindings,
|
|
2597
|
+
cancelFrame: options.cancelFrame,
|
|
2598
|
+
map: options.poiMap,
|
|
2599
|
+
onActivate(match, inputModality2) {
|
|
2600
|
+
clearDiagnostic("poi:UNSTABLE_FEATURE_IDENTITY");
|
|
2601
|
+
if (match.binding.popup) requestPopupState(match, inputModality2);
|
|
2602
|
+
},
|
|
2603
|
+
onDiagnostic(diagnostic2) {
|
|
2604
|
+
reportDiagnostic(`poi:${diagnostic2.code}`, diagnostic2);
|
|
2605
|
+
},
|
|
2606
|
+
onDiagnosticResolved(code) {
|
|
2607
|
+
clearDiagnostic(`poi:${code}`);
|
|
2608
|
+
},
|
|
2609
|
+
onHoverChange(match) {
|
|
2610
|
+
hoveredMatch = match;
|
|
2611
|
+
if (!match) {
|
|
2612
|
+
closeTooltipVisual();
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
if (tileflowInteractionTargetRefsEqual(
|
|
2616
|
+
interactionState.popup,
|
|
2617
|
+
semanticTargetReference(match.target)
|
|
2618
|
+
)) {
|
|
2619
|
+
syncPopupFromInteractionState("programmatic");
|
|
2620
|
+
}
|
|
2621
|
+
if (match.binding.tooltip) openTooltipVisual(match);
|
|
2622
|
+
else closeTooltipVisual();
|
|
2623
|
+
},
|
|
2624
|
+
onInteractionEvent: emit,
|
|
2625
|
+
onManifestChange(available) {
|
|
2626
|
+
if (available) clearDiagnostic("poi:SEMANTIC_MANIFEST_MISMATCH");
|
|
2627
|
+
invalidateResolvedMatches();
|
|
2628
|
+
syncPopupFromInteractionState("programmatic");
|
|
2629
|
+
},
|
|
2630
|
+
requestFrame: options.requestFrame
|
|
2631
|
+
});
|
|
2632
|
+
return {
|
|
2633
|
+
closePopup() {
|
|
2634
|
+
assertActive2(disposed);
|
|
2635
|
+
return requestPopupState(null, "programmatic");
|
|
2636
|
+
},
|
|
2637
|
+
dispose() {
|
|
2638
|
+
if (disposeComplete) return;
|
|
2639
|
+
const errors = [];
|
|
2640
|
+
if (!disposed) {
|
|
2641
|
+
disposed = true;
|
|
2642
|
+
errors.push(
|
|
2643
|
+
...safelyRunAll2([() => closeTooltipVisual(), () => closePopupVisual("programmatic")])
|
|
2644
|
+
);
|
|
2645
|
+
renderTargets = Object.freeze([]);
|
|
2646
|
+
diagnostics = Object.freeze([]);
|
|
2647
|
+
eventListeners.clear();
|
|
2648
|
+
renderTargetListeners.clear();
|
|
2649
|
+
diagnosticListeners.clear();
|
|
2650
|
+
diagnosticsByKey.clear();
|
|
2651
|
+
pendingStateRequest = null;
|
|
2652
|
+
hoveredMatch = null;
|
|
2653
|
+
}
|
|
2654
|
+
errors.push(...flushPendingCleanupTasks());
|
|
2655
|
+
errors.push(...safelyRunAll2([() => poiController.dispose()]));
|
|
2656
|
+
if (errors.length > 0) {
|
|
2657
|
+
throwCollectedErrors3(errors, "Unable to dispose the Tileflow semantic DOM runtime.");
|
|
2658
|
+
}
|
|
2659
|
+
disposeComplete = true;
|
|
2660
|
+
},
|
|
2661
|
+
getDiagnostics: () => diagnostics,
|
|
2662
|
+
getInteractionState: () => interactionState,
|
|
2663
|
+
getRenderTargets: () => renderTargets,
|
|
2664
|
+
reconcile(nextBindings) {
|
|
2665
|
+
assertActive2(disposed);
|
|
2666
|
+
bindings = Object.freeze([...nextBindings]);
|
|
2667
|
+
if (bindings.every(
|
|
2668
|
+
(binding) => binding.target.kind === "semantic-feature" && binding.target.domain === "poi"
|
|
2669
|
+
)) {
|
|
2670
|
+
clearDiagnostic("poi:UNSUPPORTED_TARGET");
|
|
2671
|
+
}
|
|
2672
|
+
if (!bindings.some(
|
|
2673
|
+
(binding) => binding.target.kind === "semantic-feature" && binding.target.domain === "poi"
|
|
2674
|
+
)) {
|
|
2675
|
+
clearDiagnostic("poi:SEMANTIC_MANIFEST_MISMATCH");
|
|
2676
|
+
}
|
|
2677
|
+
clearDiagnostic("poi:UNSTABLE_FEATURE_IDENTITY");
|
|
2678
|
+
poiController.reconcile(bindings);
|
|
2679
|
+
if (pendingStateRequest?.match) {
|
|
2680
|
+
const replacement = rebindSemanticMatch(pendingStateRequest.match, bindings);
|
|
2681
|
+
pendingStateRequest = replacement ? { ...pendingStateRequest, match: replacement } : null;
|
|
2682
|
+
}
|
|
2683
|
+
if (hoveredMatch) {
|
|
2684
|
+
hoveredMatch = rebindSemanticMatch(hoveredMatch, bindings);
|
|
2685
|
+
}
|
|
2686
|
+
if (activeTooltip) {
|
|
2687
|
+
const replacement = rebindSemanticMatch(activeTooltip.match, bindings, "tooltip");
|
|
2688
|
+
if (!replacement) closeTooltipVisual();
|
|
2689
|
+
else if (!Object.is(replacement.binding, activeTooltip.match.binding)) {
|
|
2690
|
+
updateOverlayVisual(activeTooltip, replacement);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
if (activePopup) {
|
|
2694
|
+
const replacement = rebindSemanticMatch(activePopup.match, bindings, "popup");
|
|
2695
|
+
if (!replacement) {
|
|
2696
|
+
if (controlled) closePopupVisual("programmatic");
|
|
2697
|
+
requestPopupState(null, "programmatic");
|
|
2698
|
+
} else if (!Object.is(replacement.binding, activePopup.match.binding)) {
|
|
2699
|
+
updateOverlayVisual(activePopup, replacement);
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
syncPopupFromInteractionState("programmatic");
|
|
2703
|
+
},
|
|
2704
|
+
setCustomRenderers(renderers) {
|
|
2705
|
+
assertActive2(disposed);
|
|
2706
|
+
if (renderers.popup === customRenderers.popup && renderers.tooltip === customRenderers.tooltip) {
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
customRenderers = Object.freeze({ ...renderers });
|
|
2710
|
+
if (activeTooltip) {
|
|
2711
|
+
if (customRenderers.tooltip) activeTooltip.container.textContent = "";
|
|
2712
|
+
configureOverlayContent(activeTooltip);
|
|
2713
|
+
}
|
|
2714
|
+
if (activePopup) {
|
|
2715
|
+
if (customRenderers.popup) activePopup.container.textContent = "";
|
|
2716
|
+
configureOverlayContent(activePopup);
|
|
2717
|
+
}
|
|
2718
|
+
publishRenderTargets();
|
|
2719
|
+
},
|
|
2720
|
+
setInteractionState(state) {
|
|
2721
|
+
assertActive2(disposed);
|
|
2722
|
+
const requestedRef = pendingStateRequest?.match ? semanticTargetReference(pendingStateRequest.match.target) : null;
|
|
2723
|
+
const inputModality2 = tileflowInteractionTargetRefsEqual(requestedRef, state.popup) ? pendingStateRequest?.inputModality ?? "programmatic" : "programmatic";
|
|
2724
|
+
interactionState = state;
|
|
2725
|
+
syncPopupFromInteractionState(inputModality2);
|
|
2726
|
+
pendingStateRequest = null;
|
|
2727
|
+
},
|
|
2728
|
+
subscribeDiagnostics(listener) {
|
|
2729
|
+
assertActive2(disposed);
|
|
2730
|
+
diagnosticListeners.add(listener);
|
|
2731
|
+
return unsubscribe(diagnosticListeners, listener);
|
|
2732
|
+
},
|
|
2733
|
+
subscribeEvents(listener) {
|
|
2734
|
+
assertActive2(disposed);
|
|
2735
|
+
eventListeners.add(listener);
|
|
2736
|
+
return unsubscribe(eventListeners, listener);
|
|
2737
|
+
},
|
|
2738
|
+
subscribeRenderTargets(listener) {
|
|
2739
|
+
assertActive2(disposed);
|
|
2740
|
+
renderTargetListeners.add(listener);
|
|
2741
|
+
return unsubscribe(renderTargetListeners, listener);
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
function semanticTargetReference(target) {
|
|
2746
|
+
return target.feature.id === void 0 ? null : Object.freeze({
|
|
2747
|
+
domain: target.domain,
|
|
2748
|
+
featureId: target.feature.id,
|
|
2749
|
+
kind: "semantic-feature"
|
|
2750
|
+
});
|
|
2751
|
+
}
|
|
2752
|
+
function semanticReferenceKey(target) {
|
|
2753
|
+
return target.feature.id === void 0 ? null : targetReferenceKey(target.domain, target.feature.id);
|
|
2754
|
+
}
|
|
2755
|
+
function targetReferenceKey(domain, featureId) {
|
|
2756
|
+
return `${domain}\0${typeof featureId}\0${String(featureId)}`;
|
|
2757
|
+
}
|
|
2758
|
+
function surfaceDiagnosticKey(match, kind, code) {
|
|
2759
|
+
return `${match.binding.id}\0${semanticReferenceKey(match.target) ?? "unstable"}\0${kind}\0${code}`;
|
|
2760
|
+
}
|
|
2761
|
+
function overlayDiagnosticKey2(kind) {
|
|
2762
|
+
return `overlay\0${kind}`;
|
|
2763
|
+
}
|
|
2764
|
+
function sameMatch(left, right) {
|
|
2765
|
+
if (!left || left.binding.id !== right.binding.id) return false;
|
|
2766
|
+
const leftKey = semanticReferenceKey(left.target);
|
|
2767
|
+
const rightKey = semanticReferenceKey(right.target);
|
|
2768
|
+
return leftKey !== null && leftKey === rightKey;
|
|
2769
|
+
}
|
|
2770
|
+
function sameSemanticTarget(left, right) {
|
|
2771
|
+
if (!left) return false;
|
|
2772
|
+
const leftKey = semanticReferenceKey(left);
|
|
2773
|
+
return leftKey !== null && leftKey === semanticReferenceKey(right);
|
|
2774
|
+
}
|
|
2775
|
+
function rebindSemanticMatch(match, bindings, surface) {
|
|
2776
|
+
const replacement = bindings.find((binding) => {
|
|
2777
|
+
const target = binding.target;
|
|
2778
|
+
return binding.id === match.binding.id && target.kind === "semantic-feature" && target.domain === match.target.domain && target.domain === "poi" && (!target.categories || match.target.feature.category !== void 0 && target.categories.includes(match.target.feature.category)) && (surface === void 0 || binding[surface] !== void 0);
|
|
2779
|
+
});
|
|
2780
|
+
if (!replacement) return null;
|
|
2781
|
+
if (Object.is(replacement, match.binding)) return match;
|
|
2782
|
+
return {
|
|
2783
|
+
binding: replacement,
|
|
2784
|
+
target: { ...match.target, bindingId: replacement.id }
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
function semanticAccessibleLabel(target) {
|
|
2788
|
+
const name = target.feature.properties.name;
|
|
2789
|
+
if (typeof name === "string" && name.trim()) return name.slice(0, 256);
|
|
2790
|
+
return target.feature.category ? `Point of interest: ${target.feature.category}` : "Point of interest";
|
|
2791
|
+
}
|
|
2792
|
+
function configureViewAttribute2(element, content) {
|
|
2793
|
+
if (content.kind === "view") element.setAttribute("data-tileflow-view", content.name);
|
|
2794
|
+
else element.removeAttribute("data-tileflow-view");
|
|
2795
|
+
}
|
|
2796
|
+
function configureSurfaceStyles2(element) {
|
|
2797
|
+
element.style.background = "var(--tileflow-interaction-surface, #ffffff)";
|
|
2798
|
+
element.style.borderColor = "var(--tileflow-interaction-border-color, #d1d5db)";
|
|
2799
|
+
element.style.borderRadius = "var(--tileflow-interaction-border-radius, 0.375rem)";
|
|
2800
|
+
element.style.borderStyle = "solid";
|
|
2801
|
+
element.style.borderWidth = "1px";
|
|
2802
|
+
element.style.boxShadow = "var(--tileflow-interaction-shadow, 0 2px 8px rgb(0 0 0 / 0.18))";
|
|
2803
|
+
element.style.color = "var(--tileflow-interaction-foreground, #111827)";
|
|
2804
|
+
element.style.maxWidth = "var(--tileflow-interaction-max-width, 20rem)";
|
|
2805
|
+
element.style.padding = "0.5rem";
|
|
2806
|
+
}
|
|
2807
|
+
function resolveContent2(content, data) {
|
|
2808
|
+
if (content.kind === "text") return { invalidField: false, text: content.text };
|
|
2809
|
+
if (content.kind === "view") return { invalidField: false, text: "" };
|
|
2810
|
+
const fallback = content.fallback ?? "";
|
|
2811
|
+
const invalid = () => ({ invalidField: content.fallback === void 0, text: fallback });
|
|
2812
|
+
let current = data;
|
|
2813
|
+
for (const segment of content.field.split(".")) {
|
|
2814
|
+
if (current === null || typeof current !== "object" || segment === "__proto__" || segment === "constructor" || segment === "prototype" || !Object.prototype.hasOwnProperty.call(current, segment)) {
|
|
2815
|
+
return invalid();
|
|
2816
|
+
}
|
|
2817
|
+
current = current[segment];
|
|
2818
|
+
}
|
|
2819
|
+
return current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string" ? { invalidField: false, text: String(current) } : invalid();
|
|
2820
|
+
}
|
|
2821
|
+
function copyCoordinate2(coordinate) {
|
|
2822
|
+
return [coordinate[0], coordinate[1]];
|
|
2823
|
+
}
|
|
2824
|
+
function assertIdPrefix2(idPrefix) {
|
|
2825
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]{0,127}$/u.test(idPrefix)) {
|
|
2826
|
+
throw new TypeError("Tileflow semantic DOM idPrefix must be a portable identifier.");
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
function assertActive2(disposed) {
|
|
2830
|
+
if (disposed) throw new Error("Tileflow semantic DOM runtime is disposed.");
|
|
2831
|
+
}
|
|
2832
|
+
function runCleanupTask(task) {
|
|
2833
|
+
const errors = [];
|
|
2834
|
+
for (const operation of task.operations) {
|
|
2835
|
+
if (operation.complete) continue;
|
|
2836
|
+
try {
|
|
2837
|
+
operation.run();
|
|
2838
|
+
operation.complete = true;
|
|
2839
|
+
} catch (error) {
|
|
2840
|
+
errors.push(error);
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
return errors;
|
|
2844
|
+
}
|
|
2845
|
+
function cleanupTaskComplete(task) {
|
|
2846
|
+
return task.operations.every(({ complete }) => complete);
|
|
2847
|
+
}
|
|
2848
|
+
function safelyRunAll2(operations) {
|
|
2849
|
+
const errors = [];
|
|
2850
|
+
for (const operation of operations) {
|
|
2851
|
+
try {
|
|
2852
|
+
operation();
|
|
2853
|
+
} catch (error) {
|
|
2854
|
+
errors.push(error);
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
return errors;
|
|
2858
|
+
}
|
|
2859
|
+
function throwCollectedErrors3(errors, message) {
|
|
2860
|
+
if (errors.length === 0) return;
|
|
2861
|
+
if (errors.length === 1) throw errors[0];
|
|
2862
|
+
throw new AggregateError(errors, message);
|
|
2863
|
+
}
|
|
2864
|
+
function notify(listeners, value) {
|
|
2865
|
+
const errors = [];
|
|
2866
|
+
for (const listener of [...listeners]) {
|
|
2867
|
+
try {
|
|
2868
|
+
listener(value);
|
|
2869
|
+
} catch (error) {
|
|
2870
|
+
errors.push(error);
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
if (errors.length === 1) throw errors[0];
|
|
2874
|
+
if (errors.length > 1) throw new AggregateError(errors, "Tileflow semantic listeners failed.");
|
|
2875
|
+
}
|
|
2876
|
+
function unsubscribe(values, value) {
|
|
2877
|
+
let active = true;
|
|
2878
|
+
return () => {
|
|
2879
|
+
if (!active) return;
|
|
2880
|
+
active = false;
|
|
2881
|
+
values.delete(value);
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
export {
|
|
2885
|
+
createTileflowAnnotationRegistry,
|
|
2886
|
+
createTileflowMapLibreDomRuntime,
|
|
2887
|
+
createTileflowMapLibreInteractionCoordinator,
|
|
2888
|
+
createTileflowMapLibrePoiController,
|
|
2889
|
+
createTileflowMapLibreSemanticDomRuntime,
|
|
2890
|
+
createTileflowOverlayStateController,
|
|
2891
|
+
normalizeTileflowLegacyMarkers
|
|
2892
|
+
};
|