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

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