@tileflow/svelte 0.1.0-alpha.19 → 0.1.0-alpha.21

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.
@@ -1,155 +1,352 @@
1
1
  <script lang="ts">
2
- import maplibregl, {
3
- type LngLatLike,
4
- type Map as MapLibreMap,
5
- type MapOptions as MapLibreMapOptions,
6
- type StyleSpecification,
2
+ import type {
3
+ LngLatLike,
4
+ Map as MapLibreMap,
5
+ Marker as MapLibreMarker,
6
+ PointLike,
7
+ Popup as MapLibrePopup,
8
+ StyleSpecification,
7
9
  } from 'maplibre-gl';
8
10
  import {createEventDispatcher, onMount, tick} from 'svelte';
11
+ import {normalizeTileflowCaptureId} from '@tileflow/core/capture';
9
12
  import {
10
13
  createTileflowSessionController,
11
14
  defaultTileflowManifestUrl,
15
+ defaultTileflowRuntimeView,
12
16
  loadTileflowManifest,
13
17
  mergeTileflowAnalytics,
14
- normalizeTileflowCaptureId,
18
+ normalizeTileflowRuntimeCenter,
15
19
  normalizeTileflowStaticImageSize,
16
20
  resolveTileflowManifestMap,
17
21
  resolveTileflowMapMode,
18
22
  resolveTileflowRuntimeStyle,
23
+ resolveTileflowRuntimeTheme,
24
+ resolveTileflowRuntimeView,
19
25
  resolveTileflowStaticImageUrl,
20
26
  shouldLoadTileflowManifest,
21
- type MapLibreStyle,
22
- type TileflowAnalytics,
23
- type TileflowConfig,
24
- type TileflowMapMarker,
25
- type TileflowProjectThemes,
26
27
  type TileflowRuntimeManifestMap,
27
- } from '@tileflow/core';
28
+ } from '@tileflow/core/runtime';
28
29
  import {
30
+ attachTileflowFairUseNotice,
29
31
  attachTileflowMapLifecycle,
30
- createTileflowMarkerController,
31
32
  createTileflowSessionStarter,
33
+ createTileflowThemeController,
32
34
  createTileflowTransformRequest,
35
+ getTileflowSystemColorScheme,
36
+ loadTileflowStyleFonts,
37
+ registerTileflowContourProtocol,
38
+ registerTileflowPmtilesProtocol,
39
+ registerTileflowWorldRequestBridge,
40
+ subscribeTileflowSystemColorScheme,
41
+ type TileflowFairUseNoticeController,
33
42
  type TileflowMapLifecycleAttachment,
43
+ type TileflowThemeController,
44
+ type TileflowWorldRequestBridge,
34
45
  } from '@tileflow/core/browser';
46
+ import {
47
+ initialTileflowInteractionState,
48
+ tileflowInteractionStateSchema,
49
+ validateTileflowAnnotations,
50
+ validateTileflowInteractionBindings,
51
+ type TileflowAnnotation,
52
+ type TileflowAnnotationViewContext,
53
+ type TileflowInteractionBinding,
54
+ type TileflowInteractionContent,
55
+ type TileflowInteractionDiagnostic,
56
+ type TileflowInteractionDiagnosticCode,
57
+ type TileflowInteractionState,
58
+ type TileflowInteractionViewContext,
59
+ } from '@tileflow/interactions';
60
+ import {
61
+ createTileflowMapLibreDomRuntime,
62
+ createTileflowMapLibreInteractionCoordinator,
63
+ createTileflowMapLibreSemanticDomRuntime,
64
+ type TileflowMapLibreDomRenderTarget,
65
+ type TileflowMapLibreDomRuntime,
66
+ type TileflowMapLibreInteractionCoordinator,
67
+ type TileflowMapLibrePoiMap,
68
+ type TileflowMapLibreSemanticDomRenderTarget,
69
+ type TileflowMapLibreSemanticDomRuntime,
70
+ } from '@tileflow/interactions/maplibre';
71
+ import type {TileflowMapProps} from './index.js';
72
+ import {loadTileflowMapLibre} from './maplibre.js';
73
+ import {assertTileflowMapStyleInputs} from './style-source.js';
74
+
75
+ export let alt: NonNullable<TileflowMapProps['alt']> = '';
76
+ export let analytics: TileflowMapProps['analytics'] = undefined;
77
+ export let annotations: TileflowMapProps['annotations'] = undefined;
35
78
 
36
- export let alt = '';
37
- export let analytics: TileflowAnalytics | undefined = undefined;
38
- type TileflowMapOptions = Omit<MapLibreMapOptions, 'container' | 'style'>;
39
-
40
- const defaultCenter: [number, number] = [0, 20];
41
- const defaultZoom = 2;
42
-
43
- export let center: [number, number] | undefined = undefined;
44
- export let captureId: string | undefined = undefined;
45
- export let className = '';
46
- export let config: TileflowConfig | undefined = undefined;
47
- export let height: number | string = 420;
48
- export let imageLoading: HTMLImageElement['loading'] = 'eager';
49
- export let imageUrl: string | undefined = undefined;
50
- export let interactive: boolean | undefined = undefined;
51
- export let manifestUrl = defaultTileflowManifestUrl;
52
- export let map: string | undefined = undefined;
53
- export let mapOptions: TileflowMapOptions | undefined = undefined;
54
- export let mapStyle: MapLibreStyle | undefined = undefined;
55
- export let markers: TileflowMapMarker[] = [];
56
- export let mode: 'interactive' | 'image' = 'interactive';
57
- export let preferLocalDev = true;
58
- export let styleBaseUrl: string | undefined = undefined;
59
- export let styleUrl: string | undefined = undefined;
60
- export let themes: TileflowProjectThemes | undefined = undefined;
61
- export let zoom: number | undefined = undefined;
79
+ export let center: TileflowMapProps['center'] = undefined;
80
+ export let captureId: TileflowMapProps['captureId'] = undefined;
81
+ export let className: NonNullable<TileflowMapProps['className']> = '';
82
+ export let defaultInteractionState: TileflowMapProps['defaultInteractionState'] = undefined;
83
+ export let height: NonNullable<TileflowMapProps['height']> = 420;
84
+ export let imageLoading: NonNullable<TileflowMapProps['imageLoading']> = 'eager';
85
+ export let imageUrl: TileflowMapProps['imageUrl'] = undefined;
86
+ export let interactive: TileflowMapProps['interactive'] = undefined;
87
+ export let interactions: TileflowMapProps['interactions'] = undefined;
88
+ export let interactionState: TileflowMapProps['interactionState'] = undefined;
89
+ export let mapOptions: TileflowMapProps['mapOptions'] = undefined;
90
+ export let marker: TileflowMapProps['marker'] = undefined;
91
+ export let mode: NonNullable<TileflowMapProps['mode']> = 'interactive';
92
+ export let onInteractionDiagnostic: TileflowMapProps['onInteractionDiagnostic'] = undefined;
93
+ export let onInteractionEvent: TileflowMapProps['onInteractionEvent'] = undefined;
94
+ export let onInteractionStateChange: TileflowMapProps['onInteractionStateChange'] = undefined;
95
+ export let onThemeChange: TileflowMapProps['onThemeChange'] = undefined;
96
+ export let popup: TileflowMapProps['popup'] = undefined;
97
+ export let source: TileflowMapProps['source'];
98
+ export let theme: TileflowMapProps['theme'] = undefined;
99
+ export let tooltip: TileflowMapProps['tooltip'] = undefined;
100
+ export let zoom: TileflowMapProps['zoom'] = undefined;
62
101
 
63
102
  let container: HTMLDivElement;
64
- let captureState: 'error' | 'idle' | 'loading' = 'loading';
103
+ let mapCaptureState: 'error' | 'idle' | 'loading' = 'loading';
65
104
  let imageSize: {height: number; width: number} | null = null;
66
105
  let imageResizeObserver: ResizeObserver | null = null;
67
106
  let mapInstance: MapLibreMap | null = null;
107
+ let activeRuntimeResource: unknown = undefined;
108
+ let mapFairUseNotice: TileflowFairUseNoticeController | null = null;
68
109
  let mapLifecycleAttachment: TileflowMapLifecycleAttachment | null = null;
110
+ let annotationRuntime: TileflowMapLibreDomRuntime<TileflowAnnotation> | null = null;
111
+ let annotationRuntimeDiagnosticsUnsubscribe: (() => void) | null = null;
112
+ let annotationRuntimeEventsUnsubscribe: (() => void) | null = null;
113
+ let annotationRuntimeTargetsUnsubscribe: (() => void) | null = null;
114
+ let annotationRenderTargets: readonly TileflowMapLibreDomRenderTarget<TileflowAnnotation>[] = [];
115
+ let annotationRuntimeCoordinatorDetach: (() => void) | null = null;
116
+ let annotationRuntimeDiagnostics: readonly TileflowInteractionDiagnostic[] = [];
117
+ let semanticRuntime: TileflowMapLibreSemanticDomRuntime | null = null;
118
+ let semanticRuntimeCoordinatorDetach: (() => void) | null = null;
119
+ let semanticRuntimeDiagnosticsUnsubscribe: (() => void) | null = null;
120
+ let semanticRuntimeEventsUnsubscribe: (() => void) | null = null;
121
+ let semanticRuntimeTargetsUnsubscribe: (() => void) | null = null;
122
+ let semanticRenderTargets: readonly TileflowMapLibreSemanticDomRenderTarget[] = [];
123
+ let semanticRuntimeDiagnostics: readonly TileflowInteractionDiagnostic[] = [];
124
+ let initializeSemanticRuntime: (() => void) | null = null;
125
+ let bridgeInteractionDiagnostics: Partial<
126
+ Record<BridgeInteractionDiagnosticSource, TileflowInteractionDiagnostic>
127
+ > = {};
128
+ let activeInteractionDiagnosticKeys = new Set<string>();
129
+ let interactionCaptureState: 'idle' | 'loading' = 'idle';
130
+ let themeTransitionState: 'error' | 'idle' | 'loading' = 'idle';
131
+ let effectiveInteractionCaptureState: 'error' | 'idle' | 'loading' = 'idle';
132
+ let captureState: 'error' | 'idle' | 'loading' = 'loading';
133
+ let hasInteractionErrors = false;
134
+ let interactionReadinessRunId = 0;
135
+ let interactionReadinessTargetKeys: readonly string[] = [];
136
+ let hadInteractionErrors = false;
137
+ let interactionRuntimesDisposing = false;
69
138
  let mapResizeObserver: ResizeObserver | null = null;
139
+ let mapWorldRequestBridge: TileflowWorldRequestBridge | null = null;
140
+ let themeController: TileflowThemeController | null = null;
141
+ let systemColorScheme: 'dark' | 'light' = 'light';
142
+ let unsubscribeSystemColorScheme: (() => void) | null = null;
143
+ let loadedManifestMap: TileflowRuntimeManifestMap | null = null;
70
144
  let manifestMap: TileflowRuntimeManifestMap | null = null;
71
145
  let manifestLoadId = 0;
146
+ let manifestResolutionKey = '';
147
+ let manifestResolutionState: 'error' | 'loading' | 'not-needed' | 'ready' = 'loading';
148
+ let mapLoadId = 0;
72
149
  let mounted = false;
73
150
  let refreshRunId = 0;
74
151
  let readinessRunId = 0;
75
152
 
76
153
  const dispatch = createEventDispatcher<{load: MapLibreMap}>();
77
- const markerController = createTileflowMarkerController<
78
- MapLibreMap,
79
- TileflowMapMarker,
80
- maplibregl.Marker
81
- >({
82
- attach(markerInstance, targetMap, definition) {
83
- markerInstance.setLngLat(definition.coordinates).addTo(targetMap);
84
- markerInstance.getElement().title = definition.label ?? definition.id;
85
- },
86
- create: (definition) =>
87
- new maplibregl.Marker({
88
- color: definition.color ?? '#C6A15B',
89
- }),
90
- remove: (markerInstance) => markerInstance.remove(),
91
- });
92
-
93
- $: resolvedMode = resolveTileflowMapMode({imageUrl, mode, preferLocalDev});
154
+ const controlledInteractionOwnership = interactionState !== undefined;
155
+ const initialInteractionState =
156
+ validateInteractionStateInput(interactionState, defaultInteractionState).length === 0
157
+ ? (interactionState ?? defaultInteractionState ?? initialTileflowInteractionState)
158
+ : initialTileflowInteractionState;
159
+ const interactionCoordinator: TileflowMapLibreInteractionCoordinator =
160
+ createTileflowMapLibreInteractionCoordinator({
161
+ onInteractionStateChange(nextState) {
162
+ if (interactionRuntimesDisposing) return;
163
+ onInteractionStateChange?.(nextState);
164
+ },
165
+ ...(controlledInteractionOwnership
166
+ ? {interactionState: initialInteractionState}
167
+ : {defaultInteractionState: initialInteractionState}),
168
+ });
169
+ $: resolvedMode = resolveTileflowMapMode({mode});
170
+ $: assertTileflowMapStyleInputs({source, theme});
94
171
  $: resolvedCaptureId = normalizeTileflowCaptureId(captureId);
95
172
  $: isImageMode = resolvedMode === 'image';
96
- $: imageCenter = center ?? defaultCenter;
97
- $: imageZoom = zoom ?? defaultZoom;
98
- $: resolvedCenter = center ?? mapOptions?.center ?? defaultCenter;
99
- $: resolvedZoom = zoom ?? mapOptions?.zoom ?? defaultZoom;
173
+ $: mapName = source?.kind === 'tileflow' ? source.map : undefined;
174
+ $: manifestUrl =
175
+ source?.kind === 'tileflow'
176
+ ? (source.manifestUrl ?? defaultTileflowManifestUrl)
177
+ : defaultTileflowManifestUrl;
178
+ $: shouldLoadManifest = shouldLoadTileflowManifest({
179
+ source,
180
+ });
181
+ $: manifestRequestKey = shouldLoadManifest
182
+ ? JSON.stringify([manifestUrl, mapName])
183
+ : 'not-needed';
184
+ $: currentManifestResolutionState =
185
+ manifestResolutionKey === manifestRequestKey
186
+ ? manifestResolutionState
187
+ : shouldLoadManifest
188
+ ? 'loading'
189
+ : 'not-needed';
190
+ $: manifestMap =
191
+ manifestResolutionKey === manifestRequestKey && manifestResolutionState === 'ready'
192
+ ? loadedManifestMap
193
+ : null;
194
+ $: themeResolution = resolveThemeSelection({
195
+ colorScheme: systemColorScheme,
196
+ manifestMap,
197
+ source,
198
+ theme,
199
+ });
200
+ $: resolvedThemeName = themeResolution.name;
201
+ $: manifestView = resolveTileflowRuntimeView({manifestMap});
202
+ $: manifestCenter = normalizeTileflowRuntimeCenter(manifestView.center);
203
+ $: imageCenter = normalizeTileflowRuntimeCenter(center ?? mapOptions?.center, manifestCenter);
204
+ $: imageZoom = zoom ?? mapOptions?.zoom ?? manifestView.zoom ?? defaultTileflowRuntimeView.zoom;
205
+ $: resolvedCenter = center ?? mapOptions?.center ?? manifestCenter;
206
+ $: resolvedZoom =
207
+ zoom ?? mapOptions?.zoom ?? manifestView.zoom ?? defaultTileflowRuntimeView.zoom;
208
+ $: resolvedBearing = mapOptions?.bearing ?? manifestView.bearing;
209
+ $: resolvedPitch = mapOptions?.pitch ?? manifestView.pitch;
100
210
  $: resolvedInteractive = interactive ?? mapOptions?.interactive ?? true;
101
- $: runtimeStyle = isImageMode
102
- ? null
103
- : resolveTileflowRuntimeStyle({
104
- config,
105
- manifestMap,
106
- map,
107
- preferLocalDev,
108
- style: mapStyle,
109
- styleBaseUrl,
110
- styleUrl,
111
- themes,
112
- });
211
+ $: runtimeStyle =
212
+ isImageMode || themeResolution.error
213
+ ? null
214
+ : resolveTileflowRuntimeStyle({
215
+ colorScheme: systemColorScheme,
216
+ manifestMap,
217
+ source,
218
+ theme: themeResolution.name,
219
+ });
113
220
  $: resolvedAnalytics = mergeTileflowAnalytics(analytics, runtimeStyle?.analytics);
114
221
  $: runtimeImageUrl =
115
222
  imageUrl ??
116
- (isImageMode
223
+ (isImageMode && !themeResolution.error
117
224
  ? resolveTileflowStaticImageUrl({
118
225
  center: imageCenter,
226
+ colorScheme: systemColorScheme,
119
227
  imageSize,
120
228
  manifestMap,
229
+ theme: themeResolution.name,
121
230
  zoom: imageZoom,
122
231
  })
123
232
  : undefined);
233
+ $: runtimeResolutionState = resolveRuntimeResolutionState({
234
+ currentManifestResolutionState,
235
+ imageSize,
236
+ imageUrl,
237
+ isImageMode,
238
+ runtimeImageUrl,
239
+ runtimeStyle,
240
+ shouldLoadManifest,
241
+ themeResolutionError: themeResolution.error,
242
+ });
243
+ $: currentMapCaptureState =
244
+ activeRuntimeResource === (isImageMode ? runtimeImageUrl : runtimeStyle)
245
+ ? mapCaptureState
246
+ : 'loading';
124
247
  $: frameStyle = `height: ${formatHeight(height)}; min-height: 240px; overflow: hidden; position: relative; width: 100%;`;
248
+ $: annotationResolution = resolveAnnotationInput(annotations);
249
+ $: interactionBindingResolution = resolveInteractionBindingInput(interactions);
250
+ $: interactionStateDiagnostics = validateInteractionStateInput(
251
+ interactionState,
252
+ defaultInteractionState,
253
+ );
254
+ $: hasInteractionConfiguration =
255
+ annotations !== undefined ||
256
+ interactions !== undefined ||
257
+ interactionState !== undefined ||
258
+ defaultInteractionState !== undefined ||
259
+ onInteractionDiagnostic !== undefined ||
260
+ onInteractionEvent !== undefined ||
261
+ onInteractionStateChange !== undefined ||
262
+ marker !== undefined ||
263
+ tooltip !== undefined ||
264
+ popup !== undefined;
265
+ $: declarativeInteractionDiagnostics = [
266
+ ...annotationResolution.diagnostics,
267
+ ...interactionBindingResolution.diagnostics,
268
+ ...interactionStateDiagnostics,
269
+ ...(isImageMode && hasInteractionConfiguration
270
+ ? [
271
+ createInteractionDiagnostic(
272
+ 'UNSUPPORTED_MODE',
273
+ 'Annotations, semantic interactions, and interaction state are unavailable in image mode.',
274
+ ),
275
+ ]
276
+ : []),
277
+ ];
278
+ $: interactionDiagnostics = [
279
+ ...declarativeInteractionDiagnostics,
280
+ ...annotationRuntimeDiagnostics,
281
+ ...semanticRuntimeDiagnostics,
282
+ ...Object.values(bridgeInteractionDiagnostics),
283
+ ];
284
+ $: hasInteractionErrors = interactionDiagnostics.some(({level}) => level === 'error');
285
+ $: effectiveInteractionCaptureState = hasInteractionErrors ? 'error' : interactionCaptureState;
286
+ $: captureState = combineCaptureStates(
287
+ combineCaptureStates(
288
+ combineCaptureStates(currentMapCaptureState, effectiveInteractionCaptureState),
289
+ runtimeResolutionState,
290
+ ),
291
+ themeTransitionState,
292
+ );
293
+ $: if (mounted && hasInteractionErrors !== hadInteractionErrors) {
294
+ handleInteractionErrorState(hasInteractionErrors);
295
+ }
296
+ $: if (mounted) publishNewInteractionDiagnostics(interactionDiagnostics);
125
297
 
126
298
  $: if (mounted) {
127
- config;
128
299
  imageUrl;
129
300
  interactive;
130
- manifestUrl;
131
- map;
132
301
  mapOptions;
133
- mapStyle;
134
302
  mode;
135
- preferLocalDev;
136
- styleBaseUrl;
137
- styleUrl;
138
- themes;
303
+ source;
139
304
  void refresh();
140
305
  }
141
306
 
307
+ $: if (mounted) {
308
+ theme;
309
+ syncSystemColorSchemeSubscription();
310
+ }
311
+
312
+ $: if (mounted) {
313
+ runtimeStyle;
314
+ void switchTheme();
315
+ }
316
+
142
317
  $: if (mounted) {
143
318
  center;
319
+ manifestMap;
144
320
  zoom;
321
+ mapOptions?.bearing;
145
322
  mapOptions?.center;
323
+ mapOptions?.pitch;
146
324
  mapOptions?.zoom;
147
325
  syncView();
148
326
  }
149
327
 
150
328
  $: if (mounted) {
151
- markers;
152
- syncMarkers();
329
+ annotations;
330
+ syncAnnotations();
331
+ }
332
+
333
+ $: if (mounted) {
334
+ interactions;
335
+ syncSemanticInteractions();
336
+ }
337
+
338
+ $: if (mounted) {
339
+ defaultInteractionState;
340
+ interactionState;
341
+ interactionStateDiagnostics;
342
+ syncInteractionState();
343
+ }
344
+
345
+ $: if (mounted) {
346
+ marker;
347
+ popup;
348
+ tooltip;
349
+ syncCustomRenderers();
153
350
  }
154
351
 
155
352
  $: if (mounted && isImageMode) {
@@ -159,13 +356,19 @@
159
356
 
160
357
  onMount(() => {
161
358
  mounted = true;
359
+ syncSystemColorSchemeSubscription();
162
360
 
163
361
  return () => {
164
362
  mounted = false;
165
363
  refreshRunId += 1;
166
364
  manifestLoadId += 1;
365
+ unsubscribeSystemColorScheme?.();
167
366
  imageResizeObserver?.disconnect();
168
- destroyMap();
367
+ try {
368
+ destroyMap();
369
+ } finally {
370
+ interactionCoordinator.dispose();
371
+ }
169
372
  };
170
373
  });
171
374
 
@@ -176,41 +379,41 @@
176
379
  if (!mounted || runId !== refreshRunId) {
177
380
  return;
178
381
  }
179
-
180
382
  updateImageResizeObserver();
181
- recreateMap();
383
+ await recreateMap(runId);
182
384
  }
183
385
 
184
386
  async function refreshManifest() {
185
- const shouldLoad = shouldLoadTileflowManifest({
186
- config,
187
- imageMode: isImageMode,
188
- imageUrl,
189
- map,
190
- style: mapStyle,
191
- styleBaseUrl,
192
- styleUrl,
193
- });
387
+ const shouldLoad = shouldLoadManifest;
388
+ const requestKey = manifestRequestKey;
194
389
 
195
390
  if (!shouldLoad) {
196
391
  manifestLoadId += 1;
197
- manifestMap = null;
392
+ loadedManifestMap = null;
393
+ manifestResolutionKey = requestKey;
394
+ manifestResolutionState = 'not-needed';
198
395
  return;
199
396
  }
200
397
 
201
398
  const loadId = ++manifestLoadId;
202
- manifestMap = null;
399
+ loadedManifestMap = null;
400
+ manifestResolutionKey = requestKey;
401
+ manifestResolutionState = 'loading';
203
402
 
204
403
  try {
205
404
  const manifest = await loadTileflowManifest(manifestUrl);
206
405
 
207
406
  if (mounted && loadId === manifestLoadId) {
208
- manifestMap = manifest && map ? resolveTileflowManifestMap(manifest, map) : null;
407
+ const resolvedMap =
408
+ manifest && mapName ? resolveTileflowManifestMap(manifest, mapName) : null;
409
+ loadedManifestMap = resolvedMap;
410
+ manifestResolutionState = resolvedMap ? 'ready' : 'error';
209
411
  }
210
412
  } catch (error) {
211
413
  if (mounted && loadId === manifestLoadId) {
212
414
  console.error('Failed to load Tileflow manifest', error);
213
- manifestMap = null;
415
+ loadedManifestMap = null;
416
+ manifestResolutionState = 'error';
214
417
  }
215
418
  }
216
419
  }
@@ -237,46 +440,151 @@
237
440
  }
238
441
 
239
442
  function destroyMap() {
443
+ mapLoadId += 1;
240
444
  readinessRunId += 1;
241
445
  const lifecycleAttachment = mapLifecycleAttachment;
446
+ const fairUseNotice = mapFairUseNotice;
447
+ const annotationInteractionsForMap = annotationRuntime;
448
+ const semanticInteractionsForMap = semanticRuntime;
449
+ const detachAnnotationCoordinator = annotationRuntimeCoordinatorDetach;
450
+ const detachSemanticCoordinator = semanticRuntimeCoordinatorDetach;
451
+ const unsubscribeAnnotationDiagnostics = annotationRuntimeDiagnosticsUnsubscribe;
452
+ const unsubscribeAnnotationEvents = annotationRuntimeEventsUnsubscribe;
453
+ const unsubscribeAnnotationTargets = annotationRuntimeTargetsUnsubscribe;
454
+ const unsubscribeSemanticDiagnostics = semanticRuntimeDiagnosticsUnsubscribe;
455
+ const unsubscribeSemanticEvents = semanticRuntimeEventsUnsubscribe;
456
+ const unsubscribeSemanticTargets = semanticRuntimeTargetsUnsubscribe;
242
457
  const currentMap = mapInstance;
458
+ const currentThemeController = themeController;
459
+ const worldRequestBridge = mapWorldRequestBridge;
460
+ interactionRuntimesDisposing = true;
461
+ mapFairUseNotice = null;
243
462
  mapLifecycleAttachment = null;
244
463
  mapInstance = null;
464
+ annotationRuntime = null;
465
+ annotationRuntimeCoordinatorDetach = null;
466
+ annotationRuntimeDiagnosticsUnsubscribe = null;
467
+ annotationRuntimeEventsUnsubscribe = null;
468
+ annotationRuntimeTargetsUnsubscribe = null;
469
+ semanticRuntime = null;
470
+ semanticRuntimeCoordinatorDetach = null;
471
+ semanticRuntimeDiagnosticsUnsubscribe = null;
472
+ semanticRuntimeEventsUnsubscribe = null;
473
+ semanticRuntimeTargetsUnsubscribe = null;
474
+ initializeSemanticRuntime = null;
475
+ annotationRenderTargets = [];
476
+ semanticRenderTargets = [];
477
+ interactionReadinessTargetKeys = [];
478
+ interactionReadinessRunId += 1;
479
+ interactionCaptureState = 'idle';
480
+ annotationRuntimeDiagnostics = [];
481
+ semanticRuntimeDiagnostics = [];
482
+ bridgeInteractionDiagnostics = {};
483
+ mapWorldRequestBridge = null;
484
+ themeController = null;
245
485
  mapResizeObserver?.disconnect();
246
486
  mapResizeObserver = null;
247
487
 
248
- try {
249
- lifecycleAttachment?.dispose();
250
- } finally {
488
+ const errors: unknown[] = [];
489
+ const cleanupSteps = [
490
+ unsubscribeAnnotationTargets,
491
+ unsubscribeAnnotationDiagnostics,
492
+ unsubscribeAnnotationEvents,
493
+ unsubscribeSemanticTargets,
494
+ unsubscribeSemanticDiagnostics,
495
+ unsubscribeSemanticEvents,
496
+ detachAnnotationCoordinator,
497
+ detachSemanticCoordinator,
498
+ () => semanticInteractionsForMap?.dispose(),
499
+ () => annotationInteractionsForMap?.dispose(),
500
+ () => lifecycleAttachment?.dispose(),
501
+ () => currentThemeController?.dispose(),
502
+ () => worldRequestBridge?.dispose(),
503
+ () => fairUseNotice?.dispose(),
504
+ () => currentMap?.remove(),
505
+ ];
506
+
507
+ for (const cleanup of cleanupSteps) {
508
+ if (!cleanup) continue;
251
509
  try {
252
- markerController.dispose();
253
- } finally {
254
- currentMap?.remove();
510
+ cleanup();
511
+ } catch (error) {
512
+ errors.push(error);
255
513
  }
256
514
  }
515
+ interactionRuntimesDisposing = false;
516
+ if (errors.length > 0) {
517
+ console.error(
518
+ 'Failed to fully dispose the Tileflow map runtime',
519
+ errors.length === 1 ? errors[0] : new AggregateError(errors),
520
+ );
521
+ }
257
522
  }
258
523
 
259
- function recreateMap() {
524
+ async function recreateMap(runId: number) {
260
525
  destroyMap();
261
- captureState = 'loading';
526
+ mapCaptureState = 'loading';
527
+ themeTransitionState = 'idle';
262
528
 
263
- if (!container || !runtimeStyle || isImageMode) {
529
+ if (isImageMode) {
530
+ await markImageLoading();
264
531
  return;
265
532
  }
266
533
 
534
+ activeRuntimeResource = runtimeStyle;
535
+ if (!container || !runtimeStyle) {
536
+ return;
537
+ }
538
+
539
+ const loadId = mapLoadId;
540
+ const targetContainer = container;
267
541
  const runtime = runtimeStyle;
542
+ let maplibregl: Awaited<ReturnType<typeof loadTileflowMapLibre>>;
543
+
544
+ try {
545
+ [maplibregl] = await Promise.all([
546
+ loadTileflowMapLibre(),
547
+ loadTileflowStyleFonts(runtime.style, {fontFaces: runtime.fontFaces}),
548
+ ]);
549
+ } catch (error) {
550
+ if (mounted && runId === refreshRunId && loadId === mapLoadId) {
551
+ console.error('Failed to load the Tileflow map runtime', error);
552
+ mapCaptureState = 'error';
553
+ }
554
+ return;
555
+ }
556
+
557
+ if (
558
+ !mounted ||
559
+ runId !== refreshRunId ||
560
+ loadId !== mapLoadId ||
561
+ container !== targetContainer ||
562
+ isImageMode
563
+ ) {
564
+ return;
565
+ }
566
+
268
567
  const session = createTileflowSessionController({source: 'svelte'});
269
568
  const sessionStarter = createTileflowSessionStarter({
270
569
  getSessionId: () => session.sessionId,
271
570
  sessionId: session.sessionId,
272
571
  source: 'svelte',
273
572
  });
573
+ mapFairUseNotice = attachTileflowFairUseNotice(targetContainer);
574
+ registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
575
+ registerTileflowPmtilesProtocol({addProtocol: maplibregl.addProtocol});
576
+ mapWorldRequestBridge = registerTileflowWorldRequestBridge({
577
+ addProtocol: maplibregl.addProtocol,
578
+ onNotice: mapFairUseNotice.update,
579
+ });
274
580
  const maplibreMap = new maplibregl.Map({
275
581
  ...mapOptions,
276
582
  attributionControl: mapOptions?.attributionControl ?? {compact: true},
583
+ bearing: resolvedBearing,
277
584
  center: resolvedCenter as LngLatLike,
278
- container,
585
+ container: targetContainer,
279
586
  interactive: resolvedInteractive,
587
+ pitch: resolvedPitch,
280
588
  style: runtime.style as StyleSpecification | string,
281
589
  transformRequest: createTileflowTransformRequest({
282
590
  always: true,
@@ -285,11 +593,156 @@
285
593
  sessionController: session,
286
594
  sessionId: session.sessionId,
287
595
  transformRequest: mapOptions?.transformRequest ?? undefined,
596
+ worldRequestBridge: mapWorldRequestBridge,
288
597
  }),
289
598
  zoom: resolvedZoom,
290
599
  });
291
600
 
292
601
  mapInstance = maplibreMap;
602
+ themeController =
603
+ runtime.theme === undefined
604
+ ? null
605
+ : createTileflowThemeController({
606
+ initial: runtime,
607
+ map: maplibreMap,
608
+ onTransition(transition) {
609
+ onThemeChange?.(transition);
610
+ if (transition.phase === 'preloading' || transition.phase === 'applying') {
611
+ themeTransitionState = 'loading';
612
+ } else if (transition.phase === 'error') {
613
+ themeTransitionState = 'error';
614
+ } else {
615
+ themeTransitionState = 'idle';
616
+ }
617
+ },
618
+ });
619
+
620
+ setBridgeInteractionDiagnostic('annotation-runtime', null);
621
+ try {
622
+ const annotationInteractionsForMap = createTileflowMapLibreDomRuntime<
623
+ MapLibreMap,
624
+ MapLibreMarker,
625
+ MapLibrePopup,
626
+ TileflowAnnotation
627
+ >({
628
+ createMarker({annotation, element}) {
629
+ element.title = annotation.ariaLabel;
630
+ return new maplibregl.Marker({element});
631
+ },
632
+ createOverlay({container: overlayContainer, kind}) {
633
+ return new maplibregl.Popup({
634
+ closeButton: false,
635
+ closeOnClick: false,
636
+ closeOnMove: false,
637
+ focusAfterOpen: false,
638
+ offset: kind === 'popup' ? 24 : 18,
639
+ }).setDOMContent(overlayContainer);
640
+ },
641
+ customMarker: marker !== undefined,
642
+ customPopup: popup !== undefined,
643
+ customTooltip: tooltip !== undefined,
644
+ document: targetContainer.ownerDocument,
645
+ interactionState: interactionCoordinator.getInteractionState(),
646
+ map: maplibreMap,
647
+ onInteractionStateChange: interactionCoordinator.requestInteractionState,
648
+ updateMarker(_markerInstance, {annotation, element}) {
649
+ element.title = annotation.ariaLabel;
650
+ },
651
+ });
652
+ annotationRuntime = annotationInteractionsForMap;
653
+ annotationRuntimeCoordinatorDetach = interactionCoordinator.attach(
654
+ 'annotation',
655
+ annotationInteractionsForMap,
656
+ );
657
+ annotationRuntimeDiagnosticsUnsubscribe = annotationInteractionsForMap.subscribeDiagnostics(
658
+ (diagnostics) => {
659
+ annotationRuntimeDiagnostics = diagnostics;
660
+ },
661
+ );
662
+ annotationRuntimeEventsUnsubscribe = annotationInteractionsForMap.subscribeEvents((event) => {
663
+ if (!interactionRuntimesDisposing) onInteractionEvent?.(event);
664
+ });
665
+ annotationRuntimeTargetsUnsubscribe = annotationInteractionsForMap.subscribeRenderTargets(
666
+ (targets) => {
667
+ syncAnnotationRenderTargets(targets);
668
+ },
669
+ );
670
+ annotationRuntimeDiagnostics = annotationInteractionsForMap.getDiagnostics();
671
+ syncAnnotationRenderTargets(annotationInteractionsForMap.getRenderTargets());
672
+ syncAnnotations();
673
+ } catch (error) {
674
+ console.error('Failed to initialize Tileflow annotations', error);
675
+ setBridgeInteractionDiagnostic(
676
+ 'annotation-runtime',
677
+ 'Unable to initialize the Tileflow annotation runtime.',
678
+ );
679
+ }
680
+
681
+ initializeSemanticRuntime = () => {
682
+ if (semanticRuntime || interactionRuntimesDisposing || mapInstance !== maplibreMap) return;
683
+ const browserWindow = targetContainer.ownerDocument.defaultView;
684
+ if (!browserWindow) {
685
+ setBridgeInteractionDiagnostic(
686
+ 'semantic-runtime',
687
+ 'Semantic interactions require a browser document.',
688
+ );
689
+ return;
690
+ }
691
+
692
+ setBridgeInteractionDiagnostic('semantic-runtime', null);
693
+ try {
694
+ const semanticInteractionsForMap = createTileflowMapLibreSemanticDomRuntime<
695
+ MapLibreMap,
696
+ MapLibrePopup
697
+ >({
698
+ cancelFrame: (frame) => browserWindow.cancelAnimationFrame(frame),
699
+ createOverlay({container: overlayContainer, kind}) {
700
+ return new maplibregl.Popup({
701
+ closeButton: false,
702
+ closeOnClick: false,
703
+ closeOnMove: false,
704
+ focusAfterOpen: false,
705
+ offset: kind === 'popup' ? 24 : 18,
706
+ }).setDOMContent(overlayContainer);
707
+ },
708
+ customPopup: popup !== undefined,
709
+ customTooltip: tooltip !== undefined,
710
+ document: targetContainer.ownerDocument,
711
+ interactionState: interactionCoordinator.getInteractionState(),
712
+ map: maplibreMap,
713
+ onInteractionStateChange: interactionCoordinator.requestInteractionState,
714
+ poiMap: createTileflowMapLibrePoiMap(maplibreMap),
715
+ requestFrame: (callback) => browserWindow.requestAnimationFrame(() => callback()),
716
+ });
717
+ semanticRuntime = semanticInteractionsForMap;
718
+ semanticRuntimeCoordinatorDetach = interactionCoordinator.attach(
719
+ 'semantic',
720
+ semanticInteractionsForMap,
721
+ );
722
+ semanticRuntimeDiagnosticsUnsubscribe = semanticInteractionsForMap.subscribeDiagnostics(
723
+ (diagnostics) => {
724
+ semanticRuntimeDiagnostics = diagnostics;
725
+ },
726
+ );
727
+ semanticRuntimeEventsUnsubscribe = semanticInteractionsForMap.subscribeEvents((event) => {
728
+ if (!interactionRuntimesDisposing) onInteractionEvent?.(event);
729
+ });
730
+ semanticRuntimeTargetsUnsubscribe = semanticInteractionsForMap.subscribeRenderTargets(
731
+ (targets) => {
732
+ syncSemanticRenderTargets(targets);
733
+ },
734
+ );
735
+ semanticRuntimeDiagnostics = semanticInteractionsForMap.getDiagnostics();
736
+ syncSemanticRenderTargets(semanticInteractionsForMap.getRenderTargets());
737
+ } catch (error) {
738
+ console.error('Failed to initialize Tileflow semantic interactions', error);
739
+ setBridgeInteractionDiagnostic(
740
+ 'semantic-runtime',
741
+ 'Unable to initialize the Tileflow semantic interaction runtime.',
742
+ );
743
+ }
744
+ };
745
+ syncSemanticInteractions();
293
746
 
294
747
  if (resolvedInteractive) {
295
748
  maplibreMap.addControl(new maplibregl.NavigationControl(), 'top-right');
@@ -298,7 +751,7 @@
298
751
  mapResizeObserver = new ResizeObserver(() => {
299
752
  maplibreMap.resize();
300
753
  });
301
- mapResizeObserver.observe(container);
754
+ mapResizeObserver.observe(targetContainer);
302
755
 
303
756
  mapLifecycleAttachment = attachTileflowMapLifecycle<MapLibreMap, number>({
304
757
  getSession: () => {
@@ -306,7 +759,8 @@
306
759
  return {
307
760
  analytics: analyticsForLoad,
308
761
  styleId:
309
- analyticsForLoad?.styleId ?? (typeof runtime.style === 'string' ? runtime.style : map),
762
+ analyticsForLoad?.styleId ??
763
+ (typeof runtime.style === 'string' ? runtime.style : mapName),
310
764
  };
311
765
  },
312
766
  map: maplibreMap,
@@ -317,31 +771,128 @@
317
771
  },
318
772
  sessionStarter,
319
773
  setState: (state) => {
320
- captureState = state;
774
+ mapCaptureState = state;
321
775
  },
322
776
  subscribe: (targetMap, event, listener) => {
323
777
  const subscription = targetMap.on(event, listener);
324
778
  return () => subscription.unsubscribe();
325
779
  },
326
780
  });
781
+ if (themeController && runtimeStyle && runtimeStyle !== runtime) {
782
+ activeRuntimeResource = runtimeStyle;
783
+ void themeController.setTheme(runtimeStyle);
784
+ }
785
+ }
327
786
 
328
- syncMarkers();
787
+ async function switchTheme() {
788
+ if (!themeController) return;
789
+ if (themeResolution.error) {
790
+ await themeController.setTheme(themeController.getCurrent());
791
+ return;
792
+ }
793
+ const next = runtimeStyle;
794
+ if (!next) return;
795
+ activeRuntimeResource = next;
796
+ const result = await themeController.setTheme(next);
797
+ if (result.status === 'failed') {
798
+ console.error('Failed to change the Tileflow map theme', result.error);
799
+ }
800
+ }
801
+
802
+ function syncSystemColorSchemeSubscription() {
803
+ unsubscribeSystemColorScheme?.();
804
+ unsubscribeSystemColorScheme = null;
805
+ if (theme !== 'system') return;
806
+ systemColorScheme = getTileflowSystemColorScheme();
807
+ unsubscribeSystemColorScheme = subscribeTileflowSystemColorScheme((scheme) => {
808
+ systemColorScheme = scheme;
809
+ });
329
810
  }
330
811
 
331
812
  function syncView() {
332
813
  mapInstance?.jumpTo({
814
+ bearing: resolvedBearing,
333
815
  center: resolvedCenter as LngLatLike,
816
+ pitch: resolvedPitch,
334
817
  zoom: resolvedZoom,
335
818
  });
336
819
  }
337
820
 
338
- function syncMarkers() {
339
- if (!mapInstance) {
340
- markerController.clear();
821
+ function syncAnnotations() {
822
+ if (!annotationRuntime) return;
823
+
824
+ try {
825
+ annotationRuntime.reconcile(
826
+ annotationResolution.ok && !isImageMode ? annotationResolution.annotations : [],
827
+ );
828
+ void settleInteractionReadiness();
829
+ setBridgeInteractionDiagnostic('annotation-runtime', null);
830
+ } catch (error) {
831
+ console.error('Failed to reconcile Tileflow annotations', error);
832
+ setBridgeInteractionDiagnostic(
833
+ 'annotation-runtime',
834
+ 'Unable to reconcile the Tileflow annotation runtime.',
835
+ );
836
+ }
837
+ }
838
+
839
+ function syncSemanticInteractions() {
840
+ const bindings =
841
+ interactionBindingResolution.ok && !isImageMode ? interactionBindingResolution.bindings : [];
842
+ if (bindings.length > 0 && !semanticRuntime) initializeSemanticRuntime?.();
843
+ if (!semanticRuntime) return;
844
+
845
+ try {
846
+ semanticRuntime.reconcile(bindings);
847
+ void settleInteractionReadiness();
848
+ setBridgeInteractionDiagnostic('semantic-runtime', null);
849
+ } catch (error) {
850
+ console.error('Failed to reconcile Tileflow semantic interactions', error);
851
+ setBridgeInteractionDiagnostic(
852
+ 'semantic-runtime',
853
+ 'Unable to reconcile the Tileflow semantic interaction runtime.',
854
+ );
855
+ }
856
+ }
857
+
858
+ function syncInteractionState() {
859
+ if (
860
+ !controlledInteractionOwnership ||
861
+ interactionState === undefined ||
862
+ interactionStateDiagnostics.length > 0
863
+ ) {
341
864
  return;
342
865
  }
866
+ try {
867
+ interactionCoordinator.setInteractionState(interactionState);
868
+ setBridgeInteractionDiagnostic('state', null);
869
+ } catch (error) {
870
+ console.error('Failed to synchronize Tileflow interaction state', error);
871
+ setBridgeInteractionDiagnostic('state', 'Unable to synchronize Tileflow interaction state.');
872
+ }
873
+ }
343
874
 
344
- markerController.replace(mapInstance, markers);
875
+ function syncCustomRenderers() {
876
+ if (!annotationRuntime && !semanticRuntime) return;
877
+ try {
878
+ annotationRuntime?.setCustomRenderers({
879
+ marker: marker !== undefined,
880
+ popup: popup !== undefined,
881
+ tooltip: tooltip !== undefined,
882
+ });
883
+ semanticRuntime?.setCustomRenderers({
884
+ popup: popup !== undefined,
885
+ tooltip: tooltip !== undefined,
886
+ });
887
+ void settleInteractionReadiness();
888
+ setBridgeInteractionDiagnostic('renderers', null);
889
+ } catch (error) {
890
+ console.error('Failed to synchronize Tileflow interaction snippets', error);
891
+ setBridgeInteractionDiagnostic(
892
+ 'renderers',
893
+ 'Unable to synchronize Tileflow interaction snippets.',
894
+ );
895
+ }
345
896
  }
346
897
 
347
898
  async function handleImageLoad(event: Event) {
@@ -354,22 +905,24 @@
354
905
  if (typeof image.decode === 'function') await image.decode();
355
906
  } catch {
356
907
  if (!image.complete || image.naturalWidth === 0) {
357
- if (runId === readinessRunId) captureState = 'error';
908
+ if (runId === readinessRunId) mapCaptureState = 'error';
358
909
  return;
359
910
  }
360
911
  }
361
- if (runId === readinessRunId) captureState = 'idle';
912
+ if (runId === readinessRunId) mapCaptureState = 'idle';
362
913
  }
363
914
 
364
915
  async function markImageLoading() {
365
916
  const runId = ++readinessRunId;
366
- captureState = 'loading';
917
+ activeRuntimeResource = runtimeImageUrl;
918
+ mapCaptureState = hasInteractionConfiguration ? 'error' : 'loading';
919
+ if (hasInteractionConfiguration) return;
367
920
  await tick();
368
921
  if (!mounted || runId !== readinessRunId) return;
369
922
  const image = container?.querySelector('img');
370
923
  if (!image?.complete) return;
371
924
  if (image.naturalWidth === 0) {
372
- captureState = 'error';
925
+ mapCaptureState = 'error';
373
926
  return;
374
927
  }
375
928
  await markImageReady(image);
@@ -377,19 +930,353 @@
377
930
 
378
931
  function handleImageError() {
379
932
  readinessRunId += 1;
380
- captureState = 'error';
933
+ mapCaptureState = 'error';
934
+ }
935
+
936
+ type AnnotationResolution = Readonly<{
937
+ annotations: readonly TileflowAnnotation[];
938
+ diagnostics: readonly TileflowInteractionDiagnostic[];
939
+ ok: boolean;
940
+ }>;
941
+
942
+ type InteractionBindingResolution = Readonly<{
943
+ bindings: readonly TileflowInteractionBinding[];
944
+ diagnostics: readonly TileflowInteractionDiagnostic[];
945
+ ok: boolean;
946
+ }>;
947
+
948
+ function resolveAnnotationInput(
949
+ nextAnnotations: readonly TileflowAnnotation[] | undefined,
950
+ ): AnnotationResolution {
951
+ const candidates = nextAnnotations ?? [];
952
+ const validation = validateTileflowAnnotations(candidates);
953
+ if (!validation.ok) {
954
+ return {
955
+ annotations: [],
956
+ diagnostics: validation.diagnostics,
957
+ ok: false,
958
+ };
959
+ }
960
+
961
+ return {annotations: candidates, diagnostics: [], ok: true};
962
+ }
963
+
964
+ function resolveInteractionBindingInput(
965
+ nextInteractions: readonly TileflowInteractionBinding[] | undefined,
966
+ ): InteractionBindingResolution {
967
+ const candidates = nextInteractions ?? [];
968
+ const validation = validateTileflowInteractionBindings(candidates);
969
+ if (!validation.ok) {
970
+ return {bindings: [], diagnostics: validation.diagnostics, ok: false};
971
+ }
972
+ return {bindings: candidates, diagnostics: [], ok: true};
973
+ }
974
+
975
+ function validateInteractionStateInput(
976
+ controlledState: TileflowInteractionState | undefined,
977
+ defaultState: TileflowInteractionState | undefined,
978
+ ): readonly TileflowInteractionDiagnostic[] {
979
+ if (controlledState !== undefined && defaultState !== undefined) {
980
+ return [
981
+ createInteractionDiagnostic(
982
+ 'INVALID_DOCUMENT',
983
+ 'interactionState and defaultInteractionState are mutually exclusive.',
984
+ ),
985
+ ];
986
+ }
987
+
988
+ if ((controlledState !== undefined) !== controlledInteractionOwnership) {
989
+ return [
990
+ createInteractionDiagnostic(
991
+ 'INVALID_DOCUMENT',
992
+ 'Tileflow interaction state ownership cannot switch between controlled and uncontrolled.',
993
+ ),
994
+ ];
995
+ }
996
+
997
+ const candidate = controlledState ?? defaultState;
998
+ if (candidate === undefined) return [];
999
+ const parsed = tileflowInteractionStateSchema.safeParse(candidate);
1000
+ if (parsed.success) return [];
1001
+ const root = controlledState === undefined ? '/defaultInteractionState' : '/interactionState';
1002
+ return parsed.error.issues.map((issue) =>
1003
+ createInteractionDiagnostic(
1004
+ 'INVALID_DOCUMENT',
1005
+ issue.message,
1006
+ `${root}${jsonPointer(issue.path)}`,
1007
+ ),
1008
+ );
1009
+ }
1010
+
1011
+ function createInteractionDiagnostic(
1012
+ code: TileflowInteractionDiagnosticCode,
1013
+ message: string,
1014
+ path?: string,
1015
+ ): TileflowInteractionDiagnostic {
1016
+ return {
1017
+ code,
1018
+ level: 'error',
1019
+ message,
1020
+ ...(path === undefined ? {} : {path}),
1021
+ };
1022
+ }
1023
+
1024
+ type BridgeInteractionDiagnosticSource =
1025
+ | 'annotation-runtime'
1026
+ | 'portal'
1027
+ | 'renderers'
1028
+ | 'semantic-runtime'
1029
+ | 'state';
1030
+
1031
+ function setBridgeInteractionDiagnostic(
1032
+ source: BridgeInteractionDiagnosticSource,
1033
+ message: string | null,
1034
+ ) {
1035
+ const current = bridgeInteractionDiagnostics[source];
1036
+ if (message === null ? current === undefined : current?.message === message) return;
1037
+ const next = {...bridgeInteractionDiagnostics};
1038
+ if (message === null) delete next[source];
1039
+ else next[source] = createInteractionDiagnostic('OVERLAY_FAILURE', message);
1040
+ bridgeInteractionDiagnostics = next;
1041
+ }
1042
+
1043
+ function publishNewInteractionDiagnostics(diagnostics: readonly TileflowInteractionDiagnostic[]) {
1044
+ const nextKeys = new Set(diagnostics.map(interactionDiagnosticKey));
1045
+ for (const diagnostic of diagnostics) {
1046
+ if (!activeInteractionDiagnosticKeys.has(interactionDiagnosticKey(diagnostic))) {
1047
+ onInteractionDiagnostic?.(diagnostic);
1048
+ }
1049
+ }
1050
+ activeInteractionDiagnosticKeys = nextKeys;
1051
+ }
1052
+
1053
+ function syncAnnotationRenderTargets(
1054
+ targets: readonly TileflowMapLibreDomRenderTarget<TileflowAnnotation>[],
1055
+ ) {
1056
+ annotationRenderTargets = targets;
1057
+ syncInteractionRenderTargetReadiness();
1058
+ }
1059
+
1060
+ function syncSemanticRenderTargets(targets: readonly TileflowMapLibreSemanticDomRenderTarget[]) {
1061
+ semanticRenderTargets = targets;
1062
+ syncInteractionRenderTargetReadiness();
1063
+ }
1064
+
1065
+ function syncInteractionRenderTargetReadiness() {
1066
+ const nextKeys = [
1067
+ ...annotationRenderTargets.map(({key}) => `annotation:${key}`),
1068
+ ...semanticRenderTargets.map(({key}) => `semantic:${key}`),
1069
+ ];
1070
+ if (
1071
+ nextKeys.length === interactionReadinessTargetKeys.length &&
1072
+ nextKeys.every((key, index) => key === interactionReadinessTargetKeys[index])
1073
+ ) {
1074
+ return;
1075
+ }
1076
+ interactionReadinessTargetKeys = nextKeys;
1077
+ void settleInteractionReadiness();
1078
+ }
1079
+
1080
+ async function settleInteractionReadiness() {
1081
+ const runId = ++interactionReadinessRunId;
1082
+ if (!mounted || hasInteractionErrors) return;
1083
+ interactionCaptureState = 'loading';
1084
+ await tick();
1085
+ if (!mounted || runId !== interactionReadinessRunId || hasInteractionErrors) return;
1086
+ await nextAnimationFrame();
1087
+ if (!mounted || runId !== interactionReadinessRunId || hasInteractionErrors) return;
1088
+ await nextAnimationFrame();
1089
+ if (!mounted || runId !== interactionReadinessRunId || hasInteractionErrors) return;
1090
+ interactionCaptureState = 'idle';
1091
+ }
1092
+
1093
+ function handleInteractionErrorState(hasErrors: boolean) {
1094
+ hadInteractionErrors = hasErrors;
1095
+ interactionReadinessRunId += 1;
1096
+ if (!hasErrors) void settleInteractionReadiness();
1097
+ }
1098
+
1099
+ function nextAnimationFrame(): Promise<void> {
1100
+ const view = container?.ownerDocument.defaultView;
1101
+ if (!view) return Promise.resolve();
1102
+ return new Promise((resolve) => view.requestAnimationFrame(() => resolve()));
1103
+ }
1104
+
1105
+ function combineCaptureStates(
1106
+ mapState: 'error' | 'idle' | 'loading',
1107
+ interactionStateForCapture: 'error' | 'idle' | 'loading',
1108
+ ): 'error' | 'idle' | 'loading' {
1109
+ if (mapState === 'error' || interactionStateForCapture === 'error') return 'error';
1110
+ return mapState === 'idle' && interactionStateForCapture === 'idle' ? 'idle' : 'loading';
1111
+ }
1112
+
1113
+ function interactionDiagnosticKey(diagnostic: TileflowInteractionDiagnostic): string {
1114
+ return JSON.stringify([
1115
+ diagnostic.code,
1116
+ diagnostic.message,
1117
+ diagnostic.path,
1118
+ diagnostic.target,
1119
+ ]);
1120
+ }
1121
+
1122
+ function jsonPointer(path: readonly PropertyKey[]): string {
1123
+ return path
1124
+ .map((segment) => String(segment).replaceAll('~', '~0').replaceAll('/', '~1'))
1125
+ .map((segment) => `/${segment}`)
1126
+ .join('');
1127
+ }
1128
+
1129
+ function portal(node: HTMLElement, target: HTMLElement) {
1130
+ let currentTarget = target;
1131
+ const move = (nextTarget: HTMLElement) => {
1132
+ currentTarget = nextTarget;
1133
+ currentTarget.append(node);
1134
+ setBridgeInteractionDiagnostic('portal', null);
1135
+ };
1136
+
1137
+ try {
1138
+ move(target);
1139
+ } catch (error) {
1140
+ console.error('Failed to mount a Tileflow interaction snippet', error);
1141
+ setBridgeInteractionDiagnostic('portal', 'Unable to mount a Tileflow interaction snippet.');
1142
+ }
1143
+
1144
+ return {
1145
+ destroy() {
1146
+ node.remove();
1147
+ },
1148
+ update(nextTarget: HTMLElement) {
1149
+ if (nextTarget === currentTarget) return;
1150
+ try {
1151
+ move(nextTarget);
1152
+ } catch (error) {
1153
+ console.error('Failed to move a Tileflow interaction snippet', error);
1154
+ setBridgeInteractionDiagnostic(
1155
+ 'portal',
1156
+ 'Unable to mount a Tileflow interaction snippet.',
1157
+ );
1158
+ }
1159
+ },
1160
+ };
1161
+ }
1162
+
1163
+ function createAnnotationViewContext(
1164
+ renderTarget: TileflowMapLibreDomRenderTarget<TileflowAnnotation>,
1165
+ ): TileflowAnnotationViewContext {
1166
+ const content = getRenderTargetContent(renderTarget);
1167
+ return {
1168
+ annotation: renderTarget.annotation,
1169
+ close: renderTarget.close,
1170
+ content,
1171
+ target: {
1172
+ annotation: renderTarget.annotation,
1173
+ coordinate: renderTarget.annotation.coordinate,
1174
+ kind: 'annotation',
1175
+ },
1176
+ viewName: content?.kind === 'view' ? content.name : undefined,
1177
+ };
1178
+ }
1179
+
1180
+ function getRenderTargetContent(
1181
+ renderTarget: TileflowMapLibreDomRenderTarget<TileflowAnnotation>,
1182
+ ): TileflowInteractionContent | undefined {
1183
+ switch (renderTarget.kind) {
1184
+ case 'marker':
1185
+ return renderTarget.annotation.marker?.content;
1186
+ case 'popup':
1187
+ return renderTarget.annotation.popup?.content;
1188
+ case 'tooltip':
1189
+ return renderTarget.annotation.tooltip?.content;
1190
+ }
1191
+ }
1192
+
1193
+ function createSemanticViewContext(
1194
+ renderTarget: TileflowMapLibreSemanticDomRenderTarget,
1195
+ ): TileflowInteractionViewContext {
1196
+ return {
1197
+ close: renderTarget.close,
1198
+ content: renderTarget.content,
1199
+ target: renderTarget.target,
1200
+ viewName: renderTarget.viewName,
1201
+ };
1202
+ }
1203
+
1204
+ function createTileflowMapLibrePoiMap(map: MapLibreMap): TileflowMapLibrePoiMap {
1205
+ return {
1206
+ getStyle: () => map.getStyle(),
1207
+ on: (event, listener) => map.on(event, listener),
1208
+ queryRenderedFeatures(point, options) {
1209
+ return map
1210
+ .queryRenderedFeatures(point as PointLike, {layers: [...options.layers]})
1211
+ .map((feature) => ({
1212
+ ...(feature.id === undefined ? {} : {id: feature.id}),
1213
+ layer: {id: feature.layer.id},
1214
+ ...(feature.properties === null ? {} : {properties: feature.properties}),
1215
+ ...(feature.source === undefined ? {} : {source: feature.source}),
1216
+ ...(feature.sourceLayer === undefined ? {} : {sourceLayer: feature.sourceLayer}),
1217
+ }));
1218
+ },
1219
+ };
381
1220
  }
382
1221
 
383
1222
  function formatHeight(value: number | string): string {
384
1223
  return typeof value === 'number' ? `${value}px` : value;
385
1224
  }
1225
+
1226
+ function resolveThemeSelection(input: {
1227
+ colorScheme: 'dark' | 'light';
1228
+ manifestMap: TileflowRuntimeManifestMap | null;
1229
+ source: TileflowMapProps['source'];
1230
+ theme: TileflowMapProps['theme'];
1231
+ }): Readonly<{error: boolean; name: string | undefined}> {
1232
+ if (!input.manifestMap || input.source.kind !== 'tileflow') {
1233
+ return {
1234
+ error: false,
1235
+ name:
1236
+ input.source.kind === 'tileflow' && input.theme && input.theme !== 'system'
1237
+ ? input.theme
1238
+ : undefined,
1239
+ };
1240
+ }
1241
+
1242
+ try {
1243
+ return {
1244
+ error: false,
1245
+ name: resolveTileflowRuntimeTheme(input.manifestMap, input.theme, input.colorScheme).name,
1246
+ };
1247
+ } catch {
1248
+ return {error: true, name: undefined};
1249
+ }
1250
+ }
1251
+
1252
+ function resolveRuntimeResolutionState(input: {
1253
+ currentManifestResolutionState: 'error' | 'loading' | 'not-needed' | 'ready';
1254
+ imageSize: {height: number; width: number} | null;
1255
+ imageUrl: string | undefined;
1256
+ isImageMode: boolean;
1257
+ runtimeImageUrl: string | undefined;
1258
+ runtimeStyle: ReturnType<typeof resolveTileflowRuntimeStyle>;
1259
+ shouldLoadManifest: boolean;
1260
+ themeResolutionError: boolean;
1261
+ }): 'error' | 'idle' | 'loading' {
1262
+ if (input.shouldLoadManifest && input.currentManifestResolutionState !== 'ready') {
1263
+ return input.currentManifestResolutionState === 'error' ? 'error' : 'loading';
1264
+ }
1265
+ if (input.themeResolutionError) return 'error';
1266
+ if (input.isImageMode) {
1267
+ if (input.runtimeImageUrl) return 'idle';
1268
+ return input.imageUrl === undefined && input.imageSize === null ? 'loading' : 'error';
1269
+ }
1270
+ return input.runtimeStyle ? 'idle' : 'error';
1271
+ }
386
1272
  </script>
387
1273
 
388
1274
  <div
389
1275
  bind:this={container}
390
1276
  class={className}
391
1277
  data-tileflow-capture-id={resolvedCaptureId}
392
- data-tileflow-map={map}
1278
+ data-tileflow-map={mapName}
1279
+ data-tileflow-theme={resolvedThemeName}
393
1280
  data-tileflow-state={captureState}
394
1281
  style={frameStyle}
395
1282
  >
@@ -404,4 +1291,30 @@
404
1291
  style="display: block; height: 100%; object-fit: cover; width: 100%;"
405
1292
  />
406
1293
  {/if}
1294
+ {#each annotationRenderTargets as renderTarget (renderTarget.key)}
1295
+ {#if renderTarget.kind === 'marker' && marker}
1296
+ <div style="display: contents;" use:portal={renderTarget.container}>
1297
+ {@render marker(createAnnotationViewContext(renderTarget))}
1298
+ </div>
1299
+ {:else if renderTarget.kind === 'tooltip' && tooltip}
1300
+ <div style="display: contents;" use:portal={renderTarget.container}>
1301
+ {@render tooltip(createAnnotationViewContext(renderTarget))}
1302
+ </div>
1303
+ {:else if renderTarget.kind === 'popup' && popup}
1304
+ <div style="display: contents;" use:portal={renderTarget.container}>
1305
+ {@render popup(createAnnotationViewContext(renderTarget))}
1306
+ </div>
1307
+ {/if}
1308
+ {/each}
1309
+ {#each semanticRenderTargets as renderTarget (renderTarget.key)}
1310
+ {#if renderTarget.kind === 'tooltip' && tooltip}
1311
+ <div style="display: contents;" use:portal={renderTarget.container}>
1312
+ {@render tooltip(createSemanticViewContext(renderTarget))}
1313
+ </div>
1314
+ {:else if renderTarget.kind === 'popup' && popup}
1315
+ <div style="display: contents;" use:portal={renderTarget.container}>
1316
+ {@render popup(createSemanticViewContext(renderTarget))}
1317
+ </div>
1318
+ {/if}
1319
+ {/each}
407
1320
  </div>