@tileflow/svelte 0.1.0-alpha.18 → 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,158 +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 tileBaseUrl: string | undefined = undefined;
62
- 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;
63
103
 
64
104
  let container: HTMLDivElement;
65
- let captureState: 'error' | 'idle' | 'loading' = 'loading';
105
+ let mapCaptureState: 'error' | 'idle' | 'loading' = 'loading';
66
106
  let imageSize: {height: number; width: number} | null = null;
67
107
  let imageResizeObserver: ResizeObserver | null = null;
68
108
  let mapInstance: MapLibreMap | null = null;
109
+ let activeRuntimeResource: unknown = undefined;
110
+ let mapFairUseNotice: TileflowFairUseNoticeController | null = null;
69
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();
70
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;
71
147
  let manifestMap: TileflowRuntimeManifestMap | null = null;
72
148
  let manifestLoadId = 0;
149
+ let manifestResolutionKey = '';
150
+ let manifestResolutionState: 'error' | 'loading' | 'not-needed' | 'ready' = 'loading';
151
+ let mapLoadId = 0;
73
152
  let mounted = false;
74
153
  let refreshRunId = 0;
75
154
  let readinessRunId = 0;
76
155
 
77
156
  const dispatch = createEventDispatcher<{load: MapLibreMap}>();
78
- const markerController = createTileflowMarkerController<
79
- MapLibreMap,
80
- TileflowMapMarker,
81
- maplibregl.Marker
82
- >({
83
- attach(markerInstance, targetMap, definition) {
84
- markerInstance.setLngLat(definition.coordinates).addTo(targetMap);
85
- markerInstance.getElement().title = definition.label ?? definition.id;
86
- },
87
- create: (definition) =>
88
- new maplibregl.Marker({
89
- color: definition.color ?? '#C6A15B',
90
- }),
91
- remove: (markerInstance) => markerInstance.remove(),
92
- });
93
-
94
- $: 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});
95
174
  $: resolvedCaptureId = normalizeTileflowCaptureId(captureId);
96
175
  $: isImageMode = resolvedMode === 'image';
97
- $: imageCenter = center ?? defaultCenter;
98
- $: imageZoom = zoom ?? defaultZoom;
99
- $: resolvedCenter = center ?? mapOptions?.center ?? defaultCenter;
100
- $: 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;
101
213
  $: resolvedInteractive = interactive ?? mapOptions?.interactive ?? true;
102
- $: runtimeStyle = isImageMode
103
- ? null
104
- : resolveTileflowRuntimeStyle({
105
- config,
106
- manifestMap,
107
- map,
108
- preferLocalDev,
109
- style: mapStyle,
110
- styleBaseUrl,
111
- styleUrl,
112
- themes,
113
- tileBaseUrl,
114
- });
214
+ $: runtimeStyle =
215
+ isImageMode || themeResolution.error
216
+ ? null
217
+ : resolveTileflowRuntimeStyle({
218
+ colorScheme: systemColorScheme,
219
+ manifestMap,
220
+ source,
221
+ theme: themeResolution.name,
222
+ });
115
223
  $: resolvedAnalytics = mergeTileflowAnalytics(analytics, runtimeStyle?.analytics);
116
224
  $: runtimeImageUrl =
117
225
  imageUrl ??
118
- (isImageMode
226
+ (isImageMode && !themeResolution.error
119
227
  ? resolveTileflowStaticImageUrl({
120
228
  center: imageCenter,
229
+ colorScheme: systemColorScheme,
121
230
  imageSize,
122
231
  manifestMap,
232
+ theme: themeResolution.name,
123
233
  zoom: imageZoom,
124
234
  })
125
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';
126
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);
127
301
 
128
302
  $: if (mounted) {
129
- config;
130
303
  imageUrl;
131
304
  interactive;
132
- manifestUrl;
133
- map;
134
305
  mapOptions;
135
- mapStyle;
136
306
  mode;
137
- preferLocalDev;
138
- styleBaseUrl;
139
- styleUrl;
140
- themes;
141
- tileBaseUrl;
307
+ source;
142
308
  void refresh();
143
309
  }
144
310
 
311
+ $: if (mounted) {
312
+ theme;
313
+ syncSystemColorSchemeSubscription();
314
+ }
315
+
316
+ $: if (mounted) {
317
+ runtimeStyle;
318
+ void switchTheme();
319
+ }
320
+
145
321
  $: if (mounted) {
146
322
  center;
323
+ manifestMap;
147
324
  zoom;
325
+ mapOptions?.bearing;
148
326
  mapOptions?.center;
327
+ mapOptions?.pitch;
149
328
  mapOptions?.zoom;
150
329
  syncView();
151
330
  }
152
331
 
153
332
  $: if (mounted) {
333
+ annotations;
154
334
  markers;
155
- 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();
156
355
  }
157
356
 
158
357
  $: if (mounted && isImageMode) {
@@ -162,13 +361,19 @@
162
361
 
163
362
  onMount(() => {
164
363
  mounted = true;
364
+ syncSystemColorSchemeSubscription();
165
365
 
166
366
  return () => {
167
367
  mounted = false;
168
368
  refreshRunId += 1;
169
369
  manifestLoadId += 1;
370
+ unsubscribeSystemColorScheme?.();
170
371
  imageResizeObserver?.disconnect();
171
- destroyMap();
372
+ try {
373
+ destroyMap();
374
+ } finally {
375
+ interactionCoordinator.dispose();
376
+ }
172
377
  };
173
378
  });
174
379
 
@@ -179,41 +384,41 @@
179
384
  if (!mounted || runId !== refreshRunId) {
180
385
  return;
181
386
  }
182
-
183
387
  updateImageResizeObserver();
184
- recreateMap();
388
+ await recreateMap(runId);
185
389
  }
186
390
 
187
391
  async function refreshManifest() {
188
- const shouldLoad = shouldLoadTileflowManifest({
189
- config,
190
- imageMode: isImageMode,
191
- imageUrl,
192
- map,
193
- style: mapStyle,
194
- styleBaseUrl,
195
- styleUrl,
196
- });
392
+ const shouldLoad = shouldLoadManifest;
393
+ const requestKey = manifestRequestKey;
197
394
 
198
395
  if (!shouldLoad) {
199
396
  manifestLoadId += 1;
200
- manifestMap = null;
397
+ loadedManifestMap = null;
398
+ manifestResolutionKey = requestKey;
399
+ manifestResolutionState = 'not-needed';
201
400
  return;
202
401
  }
203
402
 
204
403
  const loadId = ++manifestLoadId;
205
- manifestMap = null;
404
+ loadedManifestMap = null;
405
+ manifestResolutionKey = requestKey;
406
+ manifestResolutionState = 'loading';
206
407
 
207
408
  try {
208
409
  const manifest = await loadTileflowManifest(manifestUrl);
209
410
 
210
411
  if (mounted && loadId === manifestLoadId) {
211
- 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';
212
416
  }
213
417
  } catch (error) {
214
418
  if (mounted && loadId === manifestLoadId) {
215
419
  console.error('Failed to load Tileflow manifest', error);
216
- manifestMap = null;
420
+ loadedManifestMap = null;
421
+ manifestResolutionState = 'error';
217
422
  }
218
423
  }
219
424
  }
@@ -240,46 +445,150 @@
240
445
  }
241
446
 
242
447
  function destroyMap() {
448
+ mapLoadId += 1;
243
449
  readinessRunId += 1;
244
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;
245
462
  const currentMap = mapInstance;
463
+ const currentThemeController = themeController;
464
+ const worldRequestBridge = mapWorldRequestBridge;
465
+ interactionRuntimesDisposing = true;
466
+ mapFairUseNotice = null;
246
467
  mapLifecycleAttachment = null;
247
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;
248
490
  mapResizeObserver?.disconnect();
249
491
  mapResizeObserver = null;
250
492
 
251
- try {
252
- lifecycleAttachment?.dispose();
253
- } 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;
254
514
  try {
255
- markerController.dispose();
256
- } finally {
257
- currentMap?.remove();
515
+ cleanup();
516
+ } catch (error) {
517
+ errors.push(error);
258
518
  }
259
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
+ }
260
527
  }
261
528
 
262
- function recreateMap() {
529
+ async function recreateMap(runId: number) {
263
530
  destroyMap();
264
- captureState = 'loading';
531
+ mapCaptureState = 'loading';
532
+ themeTransitionState = 'idle';
265
533
 
266
- if (!container || !runtimeStyle || isImageMode) {
534
+ if (isImageMode) {
535
+ await markImageLoading();
267
536
  return;
268
537
  }
269
538
 
539
+ activeRuntimeResource = runtimeStyle;
540
+ if (!container || !runtimeStyle) {
541
+ return;
542
+ }
543
+
544
+ const loadId = mapLoadId;
545
+ const targetContainer = container;
270
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
+
271
572
  const session = createTileflowSessionController({source: 'svelte'});
272
573
  const sessionStarter = createTileflowSessionStarter({
273
574
  getSessionId: () => session.sessionId,
274
575
  sessionId: session.sessionId,
275
576
  source: 'svelte',
276
577
  });
578
+ mapFairUseNotice = attachTileflowFairUseNotice(targetContainer);
579
+ registerTileflowContourProtocol({addProtocol: maplibregl.addProtocol});
580
+ mapWorldRequestBridge = registerTileflowWorldRequestBridge({
581
+ addProtocol: maplibregl.addProtocol,
582
+ onNotice: mapFairUseNotice.update,
583
+ });
277
584
  const maplibreMap = new maplibregl.Map({
278
585
  ...mapOptions,
279
586
  attributionControl: mapOptions?.attributionControl ?? {compact: true},
587
+ bearing: resolvedBearing,
280
588
  center: resolvedCenter as LngLatLike,
281
- container,
589
+ container: targetContainer,
282
590
  interactive: resolvedInteractive,
591
+ pitch: resolvedPitch,
283
592
  style: runtime.style as StyleSpecification | string,
284
593
  transformRequest: createTileflowTransformRequest({
285
594
  always: true,
@@ -288,11 +597,156 @@
288
597
  sessionController: session,
289
598
  sessionId: session.sessionId,
290
599
  transformRequest: mapOptions?.transformRequest ?? undefined,
600
+ worldRequestBridge: mapWorldRequestBridge,
291
601
  }),
292
602
  zoom: resolvedZoom,
293
603
  });
294
604
 
295
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();
296
750
 
297
751
  if (resolvedInteractive) {
298
752
  maplibreMap.addControl(new maplibregl.NavigationControl(), 'top-right');
@@ -301,7 +755,7 @@
301
755
  mapResizeObserver = new ResizeObserver(() => {
302
756
  maplibreMap.resize();
303
757
  });
304
- mapResizeObserver.observe(container);
758
+ mapResizeObserver.observe(targetContainer);
305
759
 
306
760
  mapLifecycleAttachment = attachTileflowMapLifecycle<MapLibreMap, number>({
307
761
  getSession: () => {
@@ -309,7 +763,8 @@
309
763
  return {
310
764
  analytics: analyticsForLoad,
311
765
  styleId:
312
- analyticsForLoad?.styleId ?? (typeof runtime.style === 'string' ? runtime.style : map),
766
+ analyticsForLoad?.styleId ??
767
+ (typeof runtime.style === 'string' ? runtime.style : mapName),
313
768
  };
314
769
  },
315
770
  map: maplibreMap,
@@ -320,31 +775,129 @@
320
775
  },
321
776
  sessionStarter,
322
777
  setState: (state) => {
323
- captureState = state;
778
+ mapCaptureState = state;
324
779
  },
325
780
  subscribe: (targetMap, event, listener) => {
326
781
  const subscription = targetMap.on(event, listener);
327
782
  return () => subscription.unsubscribe();
328
783
  },
329
784
  });
785
+ if (themeController && runtimeStyle && runtimeStyle !== runtime) {
786
+ activeRuntimeResource = runtimeStyle;
787
+ void themeController.setTheme(runtimeStyle);
788
+ }
789
+ }
330
790
 
331
- 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
+ });
332
814
  }
333
815
 
334
816
  function syncView() {
335
817
  mapInstance?.jumpTo({
818
+ bearing: resolvedBearing,
336
819
  center: resolvedCenter as LngLatLike,
820
+ pitch: resolvedPitch,
337
821
  zoom: resolvedZoom,
338
822
  });
339
823
  }
340
824
 
341
- function syncMarkers() {
342
- if (!mapInstance) {
343
- 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
+ ) {
344
869
  return;
345
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
+ }
346
879
 
347
- 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
+ }
348
901
  }
349
902
 
350
903
  async function handleImageLoad(event: Event) {
@@ -357,22 +910,24 @@
357
910
  if (typeof image.decode === 'function') await image.decode();
358
911
  } catch {
359
912
  if (!image.complete || image.naturalWidth === 0) {
360
- if (runId === readinessRunId) captureState = 'error';
913
+ if (runId === readinessRunId) mapCaptureState = 'error';
361
914
  return;
362
915
  }
363
916
  }
364
- if (runId === readinessRunId) captureState = 'idle';
917
+ if (runId === readinessRunId) mapCaptureState = 'idle';
365
918
  }
366
919
 
367
920
  async function markImageLoading() {
368
921
  const runId = ++readinessRunId;
369
- captureState = 'loading';
922
+ activeRuntimeResource = runtimeImageUrl;
923
+ mapCaptureState = hasInteractionConfiguration ? 'error' : 'loading';
924
+ if (hasInteractionConfiguration) return;
370
925
  await tick();
371
926
  if (!mounted || runId !== readinessRunId) return;
372
927
  const image = container?.querySelector('img');
373
928
  if (!image?.complete) return;
374
929
  if (image.naturalWidth === 0) {
375
- captureState = 'error';
930
+ mapCaptureState = 'error';
376
931
  return;
377
932
  }
378
933
  await markImageReady(image);
@@ -380,19 +935,375 @@
380
935
 
381
936
  function handleImageError() {
382
937
  readinessRunId += 1;
383
- 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
+ };
384
1247
  }
385
1248
 
386
1249
  function formatHeight(value: number | string): string {
387
1250
  return typeof value === 'number' ? `${value}px` : value;
388
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
+ }
389
1299
  </script>
390
1300
 
391
1301
  <div
392
1302
  bind:this={container}
393
1303
  class={className}
394
1304
  data-tileflow-capture-id={resolvedCaptureId}
395
- data-tileflow-map={map}
1305
+ data-tileflow-map={mapName}
1306
+ data-tileflow-theme={resolvedThemeName}
396
1307
  data-tileflow-state={captureState}
397
1308
  style={frameStyle}
398
1309
  >
@@ -407,4 +1318,30 @@
407
1318
  style="display: block; height: 100%; object-fit: cover; width: 100%;"
408
1319
  />
409
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}
410
1347
  </div>