@tileflow/react 0.1.0-alpha.2 → 0.1.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,156 +1,543 @@
1
+ 'use client';
2
+
1
3
  // src/map.tsx
2
4
  import {
5
+ useCallback,
3
6
  useEffect,
7
+ useLayoutEffect,
4
8
  useMemo,
5
9
  useRef,
6
10
  useState
7
11
  } from "react";
8
- import maplibregl from "maplibre-gl";
12
+ import { createPortal } from "react-dom";
9
13
  import {
10
- createStyle
11
- } from "@tileflow/core";
12
- import { jsx } from "react/jsx-runtime";
13
- var defaultCenter = [0, 20];
14
- var defaultMarkers = [];
15
- var defaultManifestUrl = "/tileflow/manifest.json";
16
- var maxStaticImageDimension = 1280;
17
- var maxStaticImagePixels = 1280 * 1280;
18
- var manifestCache = new globalThis.Map();
19
- function Map({
20
- map: mapName,
21
- mode = "interactive",
22
- config,
23
- style,
24
- styleUrl,
25
- imageUrl,
26
- styleBaseUrl,
27
- tileBaseUrl,
28
- themes,
29
- manifestUrl = defaultManifestUrl,
30
- preferLocalDev = true,
31
- alt = "",
32
- center = defaultCenter,
33
- zoom = 2,
34
- className,
35
- height = 420,
36
- imageLoading = "eager",
37
- interactive = true,
38
- markers = defaultMarkers,
39
- analytics,
40
- onLoad
41
- }) {
14
+ attachTileflowFairUseNotice,
15
+ attachTileflowMapLifecycle,
16
+ createTileflowSessionStarter,
17
+ createTileflowThemeController,
18
+ createTileflowTransformRequest,
19
+ getTileflowSystemColorScheme,
20
+ loadTileflowStyleFonts,
21
+ registerTileflowContourProtocol,
22
+ registerTileflowPmtilesProtocol,
23
+ registerTileflowWorldRequestBridge,
24
+ subscribeTileflowSystemColorScheme
25
+ } from "@tileflow/core/browser";
26
+ import { normalizeTileflowCaptureId } from "@tileflow/core/capture";
27
+ import {
28
+ createTileflowSessionController,
29
+ defaultTileflowManifestUrl,
30
+ defaultTileflowRuntimeView,
31
+ loadTileflowManifest,
32
+ mergeTileflowAnalytics,
33
+ normalizeTileflowRuntimeCenter,
34
+ normalizeTileflowStaticImageSize,
35
+ resolveTileflowManifestMap,
36
+ resolveTileflowMapMode,
37
+ resolveTileflowRuntimeStyle,
38
+ resolveTileflowRuntimeTheme,
39
+ resolveTileflowRuntimeView,
40
+ resolveTileflowStaticImageUrl,
41
+ shouldLoadTileflowManifest
42
+ } from "@tileflow/core/runtime";
43
+ import {
44
+ createTileflowMapLibreDomRuntime,
45
+ createTileflowMapLibreInteractionCoordinator,
46
+ createTileflowMapLibreSemanticDomRuntime
47
+ } from "@tileflow/interactions/maplibre";
48
+
49
+ // src/annotations.ts
50
+ import {
51
+ validateTileflowAnnotations
52
+ } from "@tileflow/interactions";
53
+ function prepareTileflowReactAnnotations(annotations) {
54
+ const candidates = annotations ?? [];
55
+ const titles = new Map(candidates.map((annotation) => [annotation.id, annotation.ariaLabel]));
56
+ const validation = validateTileflowAnnotations(candidates);
57
+ if (!validation.ok) {
58
+ return {
59
+ annotations: [],
60
+ diagnostics: validation.diagnostics,
61
+ ok: false,
62
+ titles
63
+ };
64
+ }
65
+ return {
66
+ // Keep caller-owned identities after validation so generic data and portal reconciliation stay
67
+ // stable instead of replacing application objects with a schema clone.
68
+ annotations: candidates,
69
+ diagnostics: [],
70
+ ok: true,
71
+ titles
72
+ };
73
+ }
74
+ function imageModeAnnotationDiagnostic(annotationCount) {
75
+ return annotationCount > 0 ? {
76
+ code: "UNSUPPORTED_MODE",
77
+ level: "error",
78
+ message: 'Tileflow annotations require mode="interactive"; image mode cannot render interactive overlays.',
79
+ path: ""
80
+ } : void 0;
81
+ }
82
+ function imageModeMapInteractionDiagnostic(annotationCount, bindingCount, hasAdditionalInteractionConfiguration = false) {
83
+ if (bindingCount === 0 && !hasAdditionalInteractionConfiguration) {
84
+ return imageModeAnnotationDiagnostic(annotationCount);
85
+ }
86
+ return {
87
+ code: "UNSUPPORTED_MODE",
88
+ level: "error",
89
+ message: annotationCount > 0 && bindingCount > 0 ? 'Tileflow annotations and interactions require mode="interactive"; image mode cannot render interactive overlays.' : annotationCount > 0 ? 'Tileflow annotations require mode="interactive"; image mode cannot render interactive overlays.' : bindingCount > 0 ? 'Tileflow interactions require mode="interactive"; image mode cannot render interactive overlays.' : 'Tileflow interaction state, callbacks, and renderers require mode="interactive"; image mode cannot render interactive overlays.',
90
+ path: ""
91
+ };
92
+ }
93
+
94
+ // src/interaction-bindings.ts
95
+ import {
96
+ validateTileflowInteractionBindings
97
+ } from "@tileflow/interactions";
98
+ function prepareTileflowReactInteractionBindings(interactions) {
99
+ const validation = validateTileflowInteractionBindings(interactions ?? []);
100
+ return validation.ok ? {
101
+ bindings: validation.bindings,
102
+ diagnostics: [],
103
+ ok: true
104
+ } : {
105
+ diagnostics: validation.diagnostics,
106
+ ok: false
107
+ };
108
+ }
109
+
110
+ // src/interaction-state-input.ts
111
+ import {
112
+ initialTileflowInteractionState,
113
+ tileflowInteractionStateSchema
114
+ } from "@tileflow/interactions";
115
+ function prepareTileflowReactInteractionState(interactionState, defaultInteractionState, controlledOwnership = interactionState !== void 0) {
116
+ const controlled = interactionState !== void 0;
117
+ if (controlled && defaultInteractionState !== void 0) {
118
+ return {
119
+ controlled,
120
+ diagnostics: [
121
+ {
122
+ code: "INVALID_DOCUMENT",
123
+ level: "error",
124
+ message: "Tileflow Map accepts interactionState or defaultInteractionState, not both.",
125
+ path: ""
126
+ }
127
+ ],
128
+ ok: false,
129
+ state: initialTileflowInteractionState
130
+ };
131
+ }
132
+ if (controlled !== controlledOwnership) {
133
+ return {
134
+ controlled,
135
+ diagnostics: [
136
+ {
137
+ code: "INVALID_DOCUMENT",
138
+ level: "error",
139
+ message: "Tileflow interaction state ownership cannot switch between controlled and uncontrolled.",
140
+ path: ""
141
+ }
142
+ ],
143
+ ok: false,
144
+ state: initialTileflowInteractionState
145
+ };
146
+ }
147
+ const statePath = controlled ? "/interactionState" : "/defaultInteractionState";
148
+ const parsed = tileflowInteractionStateSchema.safeParse(
149
+ interactionState ?? defaultInteractionState ?? initialTileflowInteractionState
150
+ );
151
+ if (parsed.success) {
152
+ return { controlled, diagnostics: [], ok: true, state: parsed.data };
153
+ }
154
+ return {
155
+ controlled,
156
+ diagnostics: parsed.error.issues.map((issue) => ({
157
+ code: "INVALID_DOCUMENT",
158
+ level: "error",
159
+ message: issue.message,
160
+ path: `${statePath}${issue.path.map((segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1")).map((segment) => `/${segment}`).join("")}`
161
+ })),
162
+ ok: false,
163
+ state: initialTileflowInteractionState
164
+ };
165
+ }
166
+
167
+ // src/map-style-inputs.ts
168
+ import {
169
+ validateTileflowRuntimeSource,
170
+ validateTileflowThemeSelection
171
+ } from "@tileflow/core/runtime";
172
+ function validateTileflowMapStyleInputs(input) {
173
+ const sourceValidation = validateTileflowRuntimeSource(input.source);
174
+ if (!sourceValidation.ok) return sourceValidation;
175
+ if (input.theme !== void 0 && !validateTileflowThemeSelection(input.theme)) {
176
+ return { error: 'theme must be a concrete portable theme name or "system"', ok: false };
177
+ }
178
+ if (input.theme !== void 0 && input.source.kind !== "tileflow") {
179
+ return { error: "theme is only valid with a tileflow source", ok: false };
180
+ }
181
+ return { ok: true };
182
+ }
183
+ function assertTileflowMapStyleInputs(input) {
184
+ const validation = validateTileflowMapStyleInputs(input);
185
+ if (!validation.ok) {
186
+ throw new TypeError(`Invalid Tileflow <Map> source: ${validation.error}`);
187
+ }
188
+ }
189
+
190
+ // src/maplibre.ts
191
+ var mapLibrePromise;
192
+ function loadTileflowMapLibre() {
193
+ mapLibrePromise ??= import("maplibre-gl").then(resolveMapLibreModule).catch((error) => {
194
+ mapLibrePromise = void 0;
195
+ throw error;
196
+ });
197
+ return mapLibrePromise;
198
+ }
199
+ function resolveMapLibreModule(module) {
200
+ const defaultExport = module.default;
201
+ return defaultExport ?? module;
202
+ }
203
+
204
+ // src/map.tsx
205
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
206
+ var defaultInteractions = [];
207
+ function Map2(props) {
208
+ assertTileflowMapStyleInputs(props);
209
+ const {
210
+ annotations,
211
+ captureId,
212
+ defaultInteractionState,
213
+ mode = "interactive",
214
+ source,
215
+ imageUrl,
216
+ alt = "",
217
+ center,
218
+ zoom,
219
+ className,
220
+ height = 420,
221
+ imageLoading = "eager",
222
+ interactive,
223
+ mapOptions,
224
+ analytics,
225
+ theme,
226
+ interactionState,
227
+ interactions = defaultInteractions,
228
+ onInteractionDiagnostic,
229
+ onInteractionEvent,
230
+ onInteractionStateChange,
231
+ onLoad,
232
+ renderMarker,
233
+ renderPopup,
234
+ renderTooltip
235
+ } = props;
236
+ const generalRenderPopup = renderPopup;
237
+ const generalRenderTooltip = renderTooltip;
238
+ const sourceKind = source.kind;
239
+ const sourceMap = source.kind === "tileflow" ? source.map : void 0;
240
+ const sourceManifestUrl = source.kind === "tileflow" ? source.manifestUrl : void 0;
241
+ const sourceStyle = source.kind === "maplibre" ? source.style : void 0;
242
+ const runtimeSource = useMemo(
243
+ () => sourceKind === "tileflow" ? { kind: "tileflow", manifestUrl: sourceManifestUrl, map: sourceMap } : { kind: "maplibre", style: sourceStyle },
244
+ [sourceKind, sourceManifestUrl, sourceMap, sourceStyle]
245
+ );
246
+ const mapName = runtimeSource.kind === "tileflow" ? runtimeSource.map : void 0;
247
+ const manifestUrl = runtimeSource.kind === "tileflow" ? runtimeSource.manifestUrl ?? defaultTileflowManifestUrl : defaultTileflowManifestUrl;
248
+ const preparedAnnotations = useMemo(
249
+ () => prepareTileflowReactAnnotations(annotations),
250
+ [annotations]
251
+ );
252
+ const preparedInteractions = useMemo(
253
+ () => prepareTileflowReactInteractionBindings(interactions),
254
+ [interactions]
255
+ );
256
+ const controlledInteractionOwnershipRef = useRef(interactionState !== void 0);
257
+ const preparedInteractionState = useMemo(
258
+ () => prepareTileflowReactInteractionState(
259
+ interactionState,
260
+ defaultInteractionState,
261
+ controlledInteractionOwnershipRef.current
262
+ ),
263
+ [defaultInteractionState, interactionState]
264
+ );
42
265
  const containerRef = useRef(null);
266
+ const imageRef = useRef(null);
43
267
  const mapRef = useRef(null);
44
- const sessionId = useMemo(() => createSessionId(), []);
45
- const [manifestMap, setManifestMap] = useState(null);
268
+ const themeControllerRef = useRef(void 0);
269
+ const runtimeStyleRef = useRef(null);
270
+ const interactionRuntimeRef = useRef(
271
+ void 0
272
+ );
273
+ const semanticRuntimeRef = useRef(void 0);
274
+ const interactionCoordinatorRef = useRef(
275
+ void 0
276
+ );
277
+ const annotationsRef = useRef(preparedAnnotations.annotations);
278
+ const annotationTitlesRef = useRef(preparedAnnotations.titles);
279
+ const preparedInteractionsRef = useRef(preparedInteractions);
280
+ const coordinatedInteractionStateRef = useRef(preparedInteractionState.state);
281
+ const onInteractionDiagnosticRef = useRef(onInteractionDiagnostic);
282
+ const onInteractionEventRef = useRef(onInteractionEvent);
283
+ const onInteractionStateChangeRef = useRef(onInteractionStateChange);
284
+ const onThemeChangeRef = useRef(props.onThemeChange);
285
+ const renderMarkerRef = useRef(renderMarker);
286
+ const renderPopupRef = useRef(renderPopup);
287
+ const renderTooltipRef = useRef(renderTooltip);
288
+ const reportedDeclarativeDiagnosticKeysRef = useRef(/* @__PURE__ */ new Set());
289
+ const [manifestResolution, setManifestResolution] = useState({
290
+ key: "",
291
+ map: null,
292
+ state: "loading"
293
+ });
294
+ const [systemColorScheme, setSystemColorScheme] = useState("light");
46
295
  const [imageSize, setImageSize] = useState(null);
47
- const isImageMode = mode === "image";
48
- const useExplicitStyleBaseUrl = useMemo(
49
- () => Boolean(!isImageMode && mapName && !style && !styleUrl && !config && styleBaseUrl),
50
- [config, isImageMode, mapName, style, styleBaseUrl, styleUrl]
296
+ const [captureState, setCaptureState] = useState("loading");
297
+ const [themeTransitionState, setThemeTransitionState] = useState(
298
+ "idle"
299
+ );
300
+ const [activeRuntimeResource, setActiveRuntimeResource] = useState(void 0);
301
+ const [annotationRenderTargets, setAnnotationRenderTargets] = useState([]);
302
+ const [semanticRenderTargets, setSemanticRenderTargets] = useState([]);
303
+ const [annotationBridgeDiagnostic, setAnnotationBridgeDiagnostic] = useState();
304
+ const [semanticBridgeDiagnostic, setSemanticBridgeDiagnostic] = useState();
305
+ const [stateBridgeDiagnostic, setStateBridgeDiagnostic] = useState();
306
+ const [rendererBridgeDiagnostic, setRendererBridgeDiagnostic] = useState();
307
+ const [annotationDiagnostics, setAnnotationDiagnostics] = useState([]);
308
+ const [semanticDiagnostics, setSemanticDiagnostics] = useState([]);
309
+ const stableMapOptions = useStableMapOptions(mapOptions);
310
+ const resolvedCaptureId = normalizeTileflowCaptureId(captureId);
311
+ const resolvedMode = resolveTileflowMapMode({ mode });
312
+ const isImageMode = resolvedMode === "image";
313
+ const shouldLoadManifest = shouldLoadTileflowManifest({
314
+ source: runtimeSource
315
+ });
316
+ const manifestRequestKey = shouldLoadManifest ? JSON.stringify([manifestUrl, mapName]) : "not-needed";
317
+ const currentManifestResolution = manifestResolution.key === manifestRequestKey ? manifestResolution : {
318
+ key: manifestRequestKey,
319
+ map: null,
320
+ state: shouldLoadManifest ? "loading" : "not-needed"
321
+ };
322
+ const manifestMap = currentManifestResolution.state === "ready" ? currentManifestResolution.map : null;
323
+ const themeResolution = useMemo(() => {
324
+ if (!manifestMap || runtimeSource.kind !== "tileflow") {
325
+ return {
326
+ error: false,
327
+ name: runtimeSource.kind === "tileflow" && theme && theme !== "system" ? theme : void 0
328
+ };
329
+ }
330
+ try {
331
+ return {
332
+ error: false,
333
+ name: resolveTileflowRuntimeTheme(manifestMap, theme, systemColorScheme).name
334
+ };
335
+ } catch {
336
+ return { error: true, name: void 0 };
337
+ }
338
+ }, [manifestMap, runtimeSource, systemColorScheme, theme]);
339
+ const resolvedThemeName = themeResolution.name;
340
+ const manifestView = resolveTileflowRuntimeView({ manifestMap });
341
+ const manifestCenter = useStableMapOptionValue(
342
+ normalizeTileflowRuntimeCenter(manifestView.center)
343
+ );
344
+ const resolvedCenter = useStableMapOptionValue(
345
+ center ?? stableMapOptions?.center ?? manifestCenter
51
346
  );
52
- const useLocalDevFallback = useMemo(
53
- () => Boolean(
54
- !isImageMode && mapName && !style && !styleUrl && !config && !styleBaseUrl && preferLocalDev && isLocalDevHost()
347
+ const resolvedZoom = zoom ?? stableMapOptions?.zoom ?? manifestView.zoom ?? defaultTileflowRuntimeView.zoom;
348
+ const resolvedBearing = stableMapOptions?.bearing ?? manifestView.bearing;
349
+ const resolvedPitch = stableMapOptions?.pitch ?? manifestView.pitch;
350
+ const resolvedInteractive = interactive ?? stableMapOptions?.interactive ?? true;
351
+ const staticImageCenter = useStableMapOptionValue(
352
+ normalizeTileflowRuntimeCenter(center ?? stableMapOptions?.center, manifestCenter)
353
+ );
354
+ const staticImageZoom = zoom ?? stableMapOptions?.zoom ?? manifestView.zoom ?? defaultTileflowRuntimeView.zoom;
355
+ const centerRef = useRef(resolvedCenter);
356
+ const zoomRef = useRef(resolvedZoom);
357
+ const bearingRef = useRef(resolvedBearing);
358
+ const pitchRef = useRef(resolvedPitch);
359
+ const onLoadRef = useRef(onLoad);
360
+ const mapNameRef = useRef(mapName);
361
+ const resolvedAnalyticsRef = useRef(void 0);
362
+ const readinessRunRef = useRef(0);
363
+ const hasAdditionalInteractionConfiguration = annotations !== void 0 || defaultInteractionState !== void 0 || interactionState !== void 0 || onInteractionDiagnostic !== void 0 || onInteractionEvent !== void 0 || onInteractionStateChange !== void 0 || renderMarker !== void 0 || renderPopup !== void 0 || renderTooltip !== void 0 || props.interactions !== void 0;
364
+ const modeDiagnostic = useMemo(
365
+ () => isImageMode ? imageModeMapInteractionDiagnostic(
366
+ preparedAnnotations.annotations.length,
367
+ interactions.length,
368
+ hasAdditionalInteractionConfiguration
369
+ ) : void 0,
370
+ [
371
+ hasAdditionalInteractionConfiguration,
372
+ interactions.length,
373
+ isImageMode,
374
+ preparedAnnotations.annotations.length
375
+ ]
376
+ );
377
+ const declarativeInteractionDiagnostics = useMemo(
378
+ () => [
379
+ modeDiagnostic,
380
+ ...preparedAnnotations.diagnostics,
381
+ ...preparedInteractions.diagnostics,
382
+ ...preparedInteractionState.diagnostics
383
+ ].filter(
384
+ (diagnostic) => diagnostic !== void 0
55
385
  ),
56
- [config, isImageMode, mapName, preferLocalDev, style, styleBaseUrl, styleUrl]
386
+ [
387
+ modeDiagnostic,
388
+ preparedAnnotations.diagnostics,
389
+ preparedInteractions.diagnostics,
390
+ preparedInteractionState.diagnostics
391
+ ]
392
+ );
393
+ const interactionDiagnostics = [
394
+ ...declarativeInteractionDiagnostics,
395
+ ...annotationBridgeDiagnostic ? [annotationBridgeDiagnostic] : [],
396
+ ...semanticBridgeDiagnostic ? [semanticBridgeDiagnostic] : [],
397
+ ...stateBridgeDiagnostic ? [stateBridgeDiagnostic] : [],
398
+ ...rendererBridgeDiagnostic ? [rendererBridgeDiagnostic] : [],
399
+ ...annotationDiagnostics,
400
+ ...semanticDiagnostics
401
+ ];
402
+ const interactionDiagnostic = interactionDiagnostics.find((diagnostic) => diagnostic.level === "error") ?? interactionDiagnostics[0];
403
+ const hasInteractionError = interactionDiagnostics.some(
404
+ (diagnostic) => diagnostic.level === "error"
405
+ );
406
+ const portalKeys = useMemo(
407
+ () => [
408
+ ...annotationRenderTargets.map((target) => `annotation:${target.key}`),
409
+ ...semanticRenderTargets.map((target) => `semantic:${target.key}`)
410
+ ],
411
+ [annotationRenderTargets, semanticRenderTargets]
57
412
  );
413
+ const portalReadiness = useTileflowPortalReadiness(portalKeys);
414
+ const interactionReadiness = hasInteractionError ? "error" : portalReadiness.idle ? "idle" : "loading";
415
+ if (preparedAnnotations.ok) {
416
+ annotationsRef.current = preparedAnnotations.annotations;
417
+ annotationTitlesRef.current = preparedAnnotations.titles;
418
+ }
419
+ preparedInteractionsRef.current = preparedInteractions;
420
+ onInteractionDiagnosticRef.current = onInteractionDiagnostic;
421
+ onInteractionEventRef.current = onInteractionEvent;
422
+ onInteractionStateChangeRef.current = onInteractionStateChange;
423
+ renderMarkerRef.current = renderMarker;
424
+ renderPopupRef.current = renderPopup;
425
+ renderTooltipRef.current = renderTooltip;
426
+ onThemeChangeRef.current = props.onThemeChange;
427
+ useEffect(() => {
428
+ if (theme !== "system") return;
429
+ setSystemColorScheme(getTileflowSystemColorScheme());
430
+ return subscribeTileflowSystemColorScheme(setSystemColorScheme);
431
+ }, [theme]);
432
+ useEffect(() => {
433
+ const previousKeys = reportedDeclarativeDiagnosticKeysRef.current;
434
+ const nextKeys = /* @__PURE__ */ new Set();
435
+ for (const diagnostic of declarativeInteractionDiagnostics) {
436
+ const key = tileflowInteractionDiagnosticKey(diagnostic);
437
+ nextKeys.add(key);
438
+ if (!previousKeys.has(key)) onInteractionDiagnosticRef.current?.(diagnostic);
439
+ }
440
+ reportedDeclarativeDiagnosticKeysRef.current = nextKeys;
441
+ }, [declarativeInteractionDiagnostics]);
58
442
  useEffect(() => {
59
- const shouldLoadManifest = Boolean(
60
- mapName && !config && (isImageMode && !imageUrl || !isImageMode && !style && !styleUrl && !useExplicitStyleBaseUrl)
61
- );
62
443
  if (!shouldLoadManifest) {
63
- setManifestMap(null);
444
+ setManifestResolution({ key: manifestRequestKey, map: null, state: "not-needed" });
64
445
  return;
65
446
  }
66
447
  let cancelled = false;
67
- loadManifest(manifestUrl).then((manifest) => {
448
+ setManifestResolution({ key: manifestRequestKey, map: null, state: "loading" });
449
+ loadTileflowManifest(manifestUrl).then((manifest) => {
68
450
  if (cancelled) {
69
451
  return;
70
452
  }
71
- setManifestMap(manifest ? resolveManifestMap(manifest, mapName) : null);
453
+ const resolvedMap = manifest && mapName ? resolveTileflowManifestMap(manifest, mapName) : null;
454
+ setManifestResolution({
455
+ key: manifestRequestKey,
456
+ map: resolvedMap,
457
+ state: resolvedMap ? "ready" : "error"
458
+ });
72
459
  }).catch((error) => {
73
460
  if (!cancelled) {
74
461
  console.error("Failed to load Tileflow manifest", error);
75
- setManifestMap(null);
462
+ setManifestResolution({ key: manifestRequestKey, map: null, state: "error" });
76
463
  }
77
464
  });
78
465
  return () => {
79
466
  cancelled = true;
80
467
  };
81
- }, [
82
- config,
83
- imageUrl,
84
- isImageMode,
85
- manifestUrl,
86
- mapName,
87
- style,
88
- styleUrl,
89
- useExplicitStyleBaseUrl
90
- ]);
468
+ }, [manifestRequestKey, manifestUrl, mapName, shouldLoadManifest]);
91
469
  const runtimeStyle = useMemo(() => {
92
- if (isImageMode) {
470
+ if (isImageMode || themeResolution.error) {
93
471
  return null;
94
472
  }
95
- if (style) {
96
- return { style };
97
- }
98
- if (styleUrl) {
99
- return {
100
- analytics: inferAnalyticsFromStyleUrl(styleUrl),
101
- style: styleUrl
102
- };
103
- }
104
- if (mapName && useExplicitStyleBaseUrl) {
105
- return { style: resolveStyleUrl(mapName, styleBaseUrl) };
106
- }
107
- if (mapName) {
108
- if (manifestMap?.styleUrl) {
109
- return {
110
- analytics: {
111
- apiUrl: manifestMap.apiUrl,
112
- mapId: manifestMap.mapId,
113
- styleId: manifestMap.styleId
114
- },
115
- style: manifestMap.styleUrl
116
- };
117
- }
118
- if (useLocalDevFallback) {
119
- return { style: resolveStyleUrl(mapName) };
120
- }
121
- return null;
122
- }
123
- return { style: createStyle(config, { themes, tileBaseUrl }) };
124
- }, [
125
- config,
126
- isImageMode,
127
- manifestMap,
128
- mapName,
129
- style,
130
- styleBaseUrl,
131
- styleUrl,
132
- themes,
133
- tileBaseUrl,
134
- useExplicitStyleBaseUrl,
135
- useLocalDevFallback
136
- ]);
473
+ return resolveTileflowRuntimeStyle({
474
+ colorScheme: systemColorScheme,
475
+ manifestMap,
476
+ source: runtimeSource,
477
+ theme: themeResolution.name
478
+ });
479
+ }, [isImageMode, manifestMap, runtimeSource, systemColorScheme, themeResolution]);
480
+ runtimeStyleRef.current = runtimeStyle;
481
+ const runtimeMapIdentity = runtimeSource.kind === "tileflow" ? `${manifestUrl}\0${runtimeSource.map}` : runtimeSource.style;
482
+ const runtimeStyleReady = runtimeStyle !== null || themeResolution.error && mapRef.current !== null;
137
483
  const resolvedAnalytics = useMemo(
138
- () => mergeAnalytics(analytics, runtimeStyle?.analytics),
484
+ () => mergeTileflowAnalytics(analytics, runtimeStyle?.analytics),
139
485
  [analytics, runtimeStyle]
140
486
  );
487
+ useEffect(() => {
488
+ centerRef.current = resolvedCenter;
489
+ zoomRef.current = resolvedZoom;
490
+ bearingRef.current = resolvedBearing;
491
+ pitchRef.current = resolvedPitch;
492
+ mapRef.current?.jumpTo({
493
+ bearing: resolvedBearing,
494
+ center: resolvedCenter,
495
+ pitch: resolvedPitch,
496
+ zoom: resolvedZoom
497
+ });
498
+ }, [resolvedBearing, resolvedCenter, resolvedPitch, resolvedZoom]);
499
+ useEffect(() => {
500
+ onLoadRef.current = onLoad;
501
+ }, [onLoad]);
502
+ useEffect(() => {
503
+ mapNameRef.current = mapName;
504
+ }, [mapName]);
505
+ useEffect(() => {
506
+ resolvedAnalyticsRef.current = resolvedAnalytics;
507
+ }, [resolvedAnalytics]);
141
508
  const runtimeImageUrl = useMemo(
142
- () => imageUrl ?? (isImageMode ? resolveStaticImageUrl({
143
- center,
509
+ () => imageUrl ?? (isImageMode && !themeResolution.error ? resolveTileflowStaticImageUrl({
510
+ center: staticImageCenter,
511
+ colorScheme: systemColorScheme,
144
512
  imageSize,
145
513
  manifestMap,
146
- zoom
514
+ theme: themeResolution.name,
515
+ zoom: staticImageZoom
147
516
  }) : void 0),
148
- [center, imageSize, imageUrl, isImageMode, manifestMap, zoom]
149
- );
150
- const transformRequest = useMemo(
151
- () => createTransformRequest(resolvedAnalytics, sessionId),
152
- [resolvedAnalytics, sessionId]
517
+ [
518
+ imageSize,
519
+ imageUrl,
520
+ isImageMode,
521
+ manifestMap,
522
+ staticImageCenter,
523
+ staticImageZoom,
524
+ systemColorScheme,
525
+ themeResolution
526
+ ]
153
527
  );
528
+ const runtimeResolutionState = (() => {
529
+ if (shouldLoadManifest && currentManifestResolution.state !== "ready") {
530
+ return currentManifestResolution.state === "error" ? "error" : "loading";
531
+ }
532
+ if (themeResolution.error) return "error";
533
+ if (isImageMode) {
534
+ if (runtimeImageUrl) return "idle";
535
+ return imageUrl === void 0 && imageSize === null ? "loading" : "error";
536
+ }
537
+ return runtimeStyle ? "idle" : "error";
538
+ })();
539
+ const currentCaptureState = activeRuntimeResource === (isImageMode ? runtimeImageUrl : runtimeStyle) ? captureState : "loading";
540
+ const effectiveCaptureState = currentCaptureState === "error" || interactionReadiness === "error" || runtimeResolutionState === "error" || themeTransitionState === "error" ? "error" : currentCaptureState === "idle" && interactionReadiness === "idle" && runtimeResolutionState === "idle" && themeTransitionState === "idle" ? "idle" : "loading";
154
541
  useEffect(() => {
155
542
  if (!isImageMode || imageUrl || !containerRef.current) {
156
543
  setImageSize(null);
@@ -158,7 +545,7 @@ function Map({
158
545
  }
159
546
  const element = containerRef.current;
160
547
  const updateImageSize = () => {
161
- const nextSize = normalizeStaticImageSize({
548
+ const nextSize = normalizeTileflowStaticImageSize({
162
549
  height: element.clientHeight,
163
550
  width: element.clientWidth
164
551
  });
@@ -174,60 +561,423 @@ function Map({
174
561
  };
175
562
  }, [imageUrl, isImageMode]);
176
563
  useEffect(() => {
177
- if (!containerRef.current || !runtimeStyle) {
178
- return;
564
+ const runtime = interactionRuntimeRef.current;
565
+ if (!runtime || !preparedAnnotations.ok) return;
566
+ try {
567
+ setAnnotationBridgeDiagnostic(void 0);
568
+ runtime.reconcile(preparedAnnotations.annotations);
569
+ } catch (error) {
570
+ console.error("Failed to reconcile Tileflow annotations", error);
571
+ const diagnostic = createOverlayFailureDiagnostic(
572
+ "Tileflow annotations could not be reconciled."
573
+ );
574
+ setAnnotationBridgeDiagnostic(diagnostic);
575
+ onInteractionDiagnosticRef.current?.(diagnostic);
179
576
  }
180
- const map = new maplibregl.Map({
181
- container: containerRef.current,
182
- style: runtimeStyle.style,
183
- center,
184
- zoom,
185
- interactive,
186
- attributionControl: { compact: true },
187
- transformRequest
188
- });
189
- mapRef.current = map;
190
- const controls = interactive ? new maplibregl.NavigationControl() : null;
191
- if (controls) {
192
- map.addControl(controls, "top-right");
577
+ }, [preparedAnnotations]);
578
+ useEffect(() => {
579
+ const runtime = semanticRuntimeRef.current;
580
+ if (!runtime || !preparedInteractions.ok) return;
581
+ try {
582
+ setSemanticBridgeDiagnostic(void 0);
583
+ runtime.reconcile(preparedInteractions.bindings);
584
+ } catch (error) {
585
+ console.error("Failed to reconcile Tileflow semantic interactions", error);
586
+ const diagnostic = createOverlayFailureDiagnostic(
587
+ "Tileflow semantic interactions could not be reconciled."
588
+ );
589
+ setSemanticBridgeDiagnostic(diagnostic);
590
+ onInteractionDiagnosticRef.current?.(diagnostic);
193
591
  }
194
- const markerInstances = markers.map((marker) => {
195
- const element = document.createElement("div");
196
- element.className = "tileflow-marker";
197
- element.style.background = marker.color ?? "#C6A15B";
198
- element.title = marker.label ?? marker.id;
199
- return new maplibregl.Marker({ element }).setLngLat(marker.coordinates).addTo(map);
200
- });
201
- const resizeObserver = new ResizeObserver(() => {
202
- map.resize();
203
- });
204
- resizeObserver.observe(containerRef.current);
205
- map.on("load", () => {
206
- onLoad?.(map);
207
- startSession(resolvedAnalytics, {
208
- mapId: resolvedAnalytics?.mapId,
209
- sessionId,
210
- styleId: resolvedAnalytics?.styleId ?? (typeof runtimeStyle.style === "string" ? runtimeStyle.style : mapName)
592
+ }, [preparedInteractions]);
593
+ useEffect(() => {
594
+ if (!preparedInteractionState.controlled || !preparedInteractionState.ok) return;
595
+ try {
596
+ setStateBridgeDiagnostic(void 0);
597
+ interactionCoordinatorRef.current?.setInteractionState(preparedInteractionState.state);
598
+ coordinatedInteractionStateRef.current = preparedInteractionState.state;
599
+ } catch (error) {
600
+ console.error("Failed to update Tileflow interaction state", error);
601
+ const diagnostic = createOverlayFailureDiagnostic(
602
+ "Tileflow interaction state could not be updated."
603
+ );
604
+ setStateBridgeDiagnostic(diagnostic);
605
+ onInteractionDiagnosticRef.current?.(diagnostic);
606
+ }
607
+ }, [preparedInteractionState]);
608
+ useEffect(() => {
609
+ const annotationRuntime = interactionRuntimeRef.current;
610
+ const semanticRuntime = semanticRuntimeRef.current;
611
+ if (!annotationRuntime && !semanticRuntime) return;
612
+ try {
613
+ setRendererBridgeDiagnostic(void 0);
614
+ annotationRuntime?.setCustomRenderers({
615
+ marker: renderMarker !== void 0,
616
+ popup: renderPopup !== void 0,
617
+ tooltip: renderTooltip !== void 0
211
618
  });
619
+ semanticRuntime?.setCustomRenderers({
620
+ popup: renderPopup !== void 0,
621
+ tooltip: renderTooltip !== void 0
622
+ });
623
+ } catch (error) {
624
+ console.error("Failed to update Tileflow interaction renderers", error);
625
+ const diagnostic = createOverlayFailureDiagnostic(
626
+ "Tileflow interaction renderers could not be updated."
627
+ );
628
+ setRendererBridgeDiagnostic(diagnostic);
629
+ onInteractionDiagnosticRef.current?.(diagnostic);
630
+ }
631
+ }, [renderMarker, renderPopup, renderTooltip]);
632
+ useEffect(() => {
633
+ setThemeTransitionState("idle");
634
+ const initialRuntimeStyle = runtimeStyleRef.current;
635
+ if (!containerRef.current || !initialRuntimeStyle) {
636
+ return;
637
+ }
638
+ readinessRunRef.current += 1;
639
+ setActiveRuntimeResource(initialRuntimeStyle);
640
+ setCaptureState("loading");
641
+ const container = containerRef.current;
642
+ let cancelled = false;
643
+ let disposeMap;
644
+ void Promise.all([
645
+ loadTileflowMapLibre(),
646
+ loadTileflowStyleFonts(initialRuntimeStyle.style, {
647
+ fontFaces: initialRuntimeStyle.fontFaces
648
+ })
649
+ ]).then(([maplibregl]) => {
650
+ if (cancelled || containerRef.current !== container) {
651
+ return;
652
+ }
653
+ const setupCleanups = [];
654
+ let setupDisposed = false;
655
+ const setupResources = {};
656
+ const registerCleanup = (cleanup) => setupCleanups.push(cleanup);
657
+ disposeMap = () => {
658
+ if (setupDisposed) return;
659
+ setupDisposed = true;
660
+ try {
661
+ runTileflowReactCleanups([...setupCleanups].reverse());
662
+ } finally {
663
+ if (mapRef.current === setupResources.map) mapRef.current = null;
664
+ if (interactionRuntimeRef.current === setupResources.annotationRuntime) {
665
+ interactionRuntimeRef.current = void 0;
666
+ }
667
+ if (semanticRuntimeRef.current === setupResources.semanticRuntime) {
668
+ semanticRuntimeRef.current = void 0;
669
+ }
670
+ if (interactionCoordinatorRef.current === setupResources.coordinator) {
671
+ interactionCoordinatorRef.current = void 0;
672
+ }
673
+ setAnnotationDiagnostics([]);
674
+ setAnnotationRenderTargets([]);
675
+ setAnnotationBridgeDiagnostic(void 0);
676
+ setSemanticDiagnostics([]);
677
+ setSemanticRenderTargets([]);
678
+ setSemanticBridgeDiagnostic(void 0);
679
+ setStateBridgeDiagnostic(void 0);
680
+ setRendererBridgeDiagnostic(void 0);
681
+ }
682
+ };
683
+ const session = createTileflowSessionController({ source: "react" });
684
+ const sessionStarter = createTileflowSessionStarter({
685
+ getSessionId: () => session.sessionId,
686
+ sessionId: session.sessionId,
687
+ source: "react"
688
+ });
689
+ const fairUseNotice = attachTileflowFairUseNotice(container);
690
+ registerCleanup(() => fairUseNotice.dispose());
691
+ registerTileflowContourProtocol({ addProtocol: maplibregl.addProtocol });
692
+ registerTileflowPmtilesProtocol({ addProtocol: maplibregl.addProtocol });
693
+ const worldRequestBridge = registerTileflowWorldRequestBridge({
694
+ addProtocol: maplibregl.addProtocol,
695
+ onNotice: fairUseNotice.update
696
+ });
697
+ registerCleanup(() => worldRequestBridge.dispose());
698
+ const transformRequest = createTileflowTransformRequest({
699
+ always: true,
700
+ asyncAnalyticsTiming: "resolution",
701
+ getAnalytics: () => resolvedAnalyticsRef.current,
702
+ sessionController: session,
703
+ sessionId: session.sessionId,
704
+ transformRequest: stableMapOptions?.transformRequest ?? void 0,
705
+ worldRequestBridge
706
+ });
707
+ const map = new maplibregl.Map({
708
+ ...stableMapOptions,
709
+ container,
710
+ style: initialRuntimeStyle.style,
711
+ bearing: bearingRef.current,
712
+ center: centerRef.current,
713
+ pitch: pitchRef.current,
714
+ zoom: zoomRef.current,
715
+ interactive: resolvedInteractive,
716
+ attributionControl: stableMapOptions?.attributionControl ?? {
717
+ compact: true
718
+ },
719
+ transformRequest
720
+ });
721
+ mapRef.current = map;
722
+ setupResources.map = map;
723
+ registerCleanup(() => map.remove());
724
+ const themeController = initialRuntimeStyle.theme === void 0 ? void 0 : createTileflowThemeController({
725
+ initial: initialRuntimeStyle,
726
+ map,
727
+ onTransition(transition) {
728
+ onThemeChangeRef.current?.(transition);
729
+ if (transition.phase === "preloading" || transition.phase === "applying") {
730
+ setThemeTransitionState("loading");
731
+ } else if (transition.phase === "error") {
732
+ setThemeTransitionState("error");
733
+ } else {
734
+ setThemeTransitionState("idle");
735
+ }
736
+ }
737
+ });
738
+ if (themeController) {
739
+ themeControllerRef.current = themeController;
740
+ registerCleanup(() => {
741
+ themeController.dispose();
742
+ if (themeControllerRef.current === themeController) {
743
+ themeControllerRef.current = void 0;
744
+ }
745
+ });
746
+ }
747
+ const browserWindow = container.ownerDocument.defaultView;
748
+ if (!browserWindow) {
749
+ throw new Error("Tileflow semantic interactions require a browser document.");
750
+ }
751
+ const interactionCoordinator = createTileflowMapLibreInteractionCoordinator({
752
+ ...controlledInteractionOwnershipRef.current ? { interactionState: coordinatedInteractionStateRef.current } : { defaultInteractionState: coordinatedInteractionStateRef.current },
753
+ onInteractionStateChange(state) {
754
+ if (!controlledInteractionOwnershipRef.current) {
755
+ coordinatedInteractionStateRef.current = state;
756
+ }
757
+ onInteractionStateChangeRef.current?.(state);
758
+ }
759
+ });
760
+ interactionCoordinatorRef.current = interactionCoordinator;
761
+ setupResources.coordinator = interactionCoordinator;
762
+ registerCleanup(() => interactionCoordinator.dispose());
763
+ const interactionRuntime = createTileflowMapLibreDomRuntime({
764
+ createMarker({ annotation, element }) {
765
+ const marker = new maplibregl.Marker({ element });
766
+ element.title = annotationTitlesRef.current.get(annotation.id) ?? annotation.ariaLabel;
767
+ return wrapMapLibrePositioned(marker);
768
+ },
769
+ createOverlay({ container: overlayContainer, kind }) {
770
+ const popup = new maplibregl.Popup({
771
+ closeButton: false,
772
+ closeOnClick: false,
773
+ closeOnMove: false,
774
+ focusAfterOpen: false,
775
+ offset: kind === "popup" ? 24 : 18
776
+ }).setDOMContent(overlayContainer);
777
+ return wrapMapLibrePositioned(popup);
778
+ },
779
+ customMarker: renderMarkerRef.current !== void 0,
780
+ customPopup: renderPopupRef.current !== void 0,
781
+ customTooltip: renderTooltipRef.current !== void 0,
782
+ document: container.ownerDocument,
783
+ interactionState: interactionCoordinator.getInteractionState(),
784
+ map,
785
+ onInteractionStateChange: interactionCoordinator.requestInteractionState,
786
+ onDiagnostic(diagnostic) {
787
+ onInteractionDiagnosticRef.current?.(diagnostic);
788
+ },
789
+ updateMarker(_marker, { annotation, element }) {
790
+ element.title = annotationTitlesRef.current.get(annotation.id) ?? annotation.ariaLabel;
791
+ }
792
+ });
793
+ interactionRuntimeRef.current = interactionRuntime;
794
+ setupResources.annotationRuntime = interactionRuntime;
795
+ registerCleanup(() => interactionRuntime.dispose());
796
+ const semanticRuntime = createTileflowMapLibreSemanticDomRuntime({
797
+ cancelFrame: (frame) => browserWindow.cancelAnimationFrame(frame),
798
+ createOverlay({ container: overlayContainer, kind }) {
799
+ const popup = new maplibregl.Popup({
800
+ closeButton: false,
801
+ closeOnClick: false,
802
+ closeOnMove: false,
803
+ focusAfterOpen: false,
804
+ offset: kind === "popup" ? 24 : 18
805
+ }).setDOMContent(overlayContainer);
806
+ return wrapMapLibrePositioned(popup);
807
+ },
808
+ customPopup: renderPopupRef.current !== void 0,
809
+ customTooltip: renderTooltipRef.current !== void 0,
810
+ document: container.ownerDocument,
811
+ interactionState: interactionCoordinator.getInteractionState(),
812
+ map,
813
+ onDiagnostic(diagnostic) {
814
+ onInteractionDiagnosticRef.current?.(diagnostic);
815
+ },
816
+ onInteractionStateChange: interactionCoordinator.requestInteractionState,
817
+ poiMap: createTileflowMapLibrePoiMap(map),
818
+ requestFrame: (callback) => browserWindow.requestAnimationFrame(() => callback())
819
+ });
820
+ semanticRuntimeRef.current = semanticRuntime;
821
+ setupResources.semanticRuntime = semanticRuntime;
822
+ registerCleanup(() => semanticRuntime.dispose());
823
+ const detachAnnotationRuntime = interactionCoordinator.attach(
824
+ "annotation",
825
+ interactionRuntime
826
+ );
827
+ registerCleanup(detachAnnotationRuntime);
828
+ const detachSemanticRuntime = interactionCoordinator.attach("semantic", semanticRuntime);
829
+ registerCleanup(detachSemanticRuntime);
830
+ const unsubscribeInteractionEvents = interactionRuntime.subscribeEvents((event) => {
831
+ onInteractionEventRef.current?.(event);
832
+ });
833
+ registerCleanup(unsubscribeInteractionEvents);
834
+ const unsubscribeDiagnostics = interactionRuntime.subscribeDiagnostics((diagnostics) => {
835
+ setAnnotationDiagnostics(diagnostics);
836
+ });
837
+ registerCleanup(unsubscribeDiagnostics);
838
+ const unsubscribeRenderTargets = interactionRuntime.subscribeRenderTargets((targets) => {
839
+ setAnnotationRenderTargets(targets);
840
+ });
841
+ registerCleanup(unsubscribeRenderTargets);
842
+ const unsubscribeSemanticEvents = semanticRuntime.subscribeEvents((event) => {
843
+ if (event.target.kind === "annotation") return;
844
+ onInteractionEventRef.current?.({ ...event, target: event.target });
845
+ });
846
+ registerCleanup(unsubscribeSemanticEvents);
847
+ const unsubscribeSemanticDiagnostics = semanticRuntime.subscribeDiagnostics(
848
+ (diagnostics) => {
849
+ setSemanticDiagnostics(diagnostics);
850
+ }
851
+ );
852
+ registerCleanup(unsubscribeSemanticDiagnostics);
853
+ const unsubscribeSemanticRenderTargets = semanticRuntime.subscribeRenderTargets(
854
+ (targets) => {
855
+ setSemanticRenderTargets(targets);
856
+ }
857
+ );
858
+ registerCleanup(unsubscribeSemanticRenderTargets);
859
+ setAnnotationDiagnostics(interactionRuntime.getDiagnostics());
860
+ setAnnotationRenderTargets(interactionRuntime.getRenderTargets());
861
+ setSemanticDiagnostics(semanticRuntime.getDiagnostics());
862
+ setSemanticRenderTargets(semanticRuntime.getRenderTargets());
863
+ try {
864
+ setAnnotationBridgeDiagnostic(void 0);
865
+ interactionRuntime.reconcile(annotationsRef.current);
866
+ } catch (error) {
867
+ console.error("Failed to reconcile Tileflow annotations", error);
868
+ const diagnostic = createOverlayFailureDiagnostic(
869
+ "Tileflow annotations could not be reconciled."
870
+ );
871
+ setAnnotationBridgeDiagnostic(diagnostic);
872
+ onInteractionDiagnosticRef.current?.(diagnostic);
873
+ }
874
+ const initialInteractions = preparedInteractionsRef.current;
875
+ if (initialInteractions.ok) {
876
+ try {
877
+ setSemanticBridgeDiagnostic(void 0);
878
+ semanticRuntime.reconcile(initialInteractions.bindings);
879
+ } catch (error) {
880
+ console.error("Failed to reconcile Tileflow semantic interactions", error);
881
+ const diagnostic = createOverlayFailureDiagnostic(
882
+ "Tileflow semantic interactions could not be reconciled."
883
+ );
884
+ setSemanticBridgeDiagnostic(diagnostic);
885
+ onInteractionDiagnosticRef.current?.(diagnostic);
886
+ }
887
+ }
888
+ const controls = resolvedInteractive ? new maplibregl.NavigationControl() : null;
889
+ if (controls) {
890
+ map.addControl(controls, "top-right");
891
+ }
892
+ const resizeObserver = new ResizeObserver(() => {
893
+ map.resize();
894
+ });
895
+ registerCleanup(() => resizeObserver.disconnect());
896
+ resizeObserver.observe(container);
897
+ const lifecycle = attachTileflowMapLifecycle({
898
+ getSession: () => {
899
+ const analyticsForLoad = resolvedAnalyticsRef.current;
900
+ return {
901
+ analytics: analyticsForLoad,
902
+ styleId: analyticsForLoad?.styleId ?? (typeof initialRuntimeStyle.style === "string" ? initialRuntimeStyle.style : mapNameRef.current)
903
+ };
904
+ },
905
+ map,
906
+ onLoad: (loadedMap) => {
907
+ onLoadRef.current?.(loadedMap);
908
+ },
909
+ scheduler: {
910
+ cancelFrame: (frame) => cancelAnimationFrame(frame),
911
+ requestFrame: (callback) => requestAnimationFrame(callback)
912
+ },
913
+ sessionStarter,
914
+ setState: setCaptureState,
915
+ subscribe: (subscribedMap, event, listener) => {
916
+ const subscription = subscribedMap.on(event, listener);
917
+ return () => subscription.unsubscribe();
918
+ }
919
+ });
920
+ registerCleanup(() => lifecycle.dispose());
921
+ const latestRuntimeStyle = runtimeStyleRef.current;
922
+ if (themeController && latestRuntimeStyle && latestRuntimeStyle !== initialRuntimeStyle) {
923
+ setActiveRuntimeResource(latestRuntimeStyle);
924
+ void themeController.setTheme(latestRuntimeStyle);
925
+ }
926
+ }).catch((error) => {
927
+ try {
928
+ disposeMap?.();
929
+ } catch (cleanupError) {
930
+ console.error("Failed to clean up the Tileflow map runtime", cleanupError);
931
+ }
932
+ if (!cancelled) {
933
+ console.error("Failed to load the Tileflow map runtime", error);
934
+ setCaptureState("error");
935
+ }
212
936
  });
213
937
  return () => {
214
- resizeObserver.disconnect();
215
- markerInstances.forEach((marker) => marker.remove());
216
- map.remove();
217
- mapRef.current = null;
938
+ cancelled = true;
939
+ readinessRunRef.current += 1;
940
+ disposeMap?.();
218
941
  };
219
- }, [
220
- center,
221
- interactive,
222
- mapName,
223
- markers,
224
- onLoad,
225
- resolvedAnalytics,
226
- runtimeStyle,
227
- sessionId,
228
- transformRequest,
229
- zoom
230
- ]);
942
+ }, [resolvedInteractive, runtimeMapIdentity, runtimeStyleReady, stableMapOptions]);
943
+ useEffect(() => {
944
+ const controller = themeControllerRef.current;
945
+ if (!controller) return;
946
+ if (themeResolution.error) {
947
+ void controller.setTheme(controller.getCurrent());
948
+ return;
949
+ }
950
+ if (!runtimeStyle || controller.getCurrent() === runtimeStyle) return;
951
+ setActiveRuntimeResource(runtimeStyle);
952
+ void controller.setTheme(runtimeStyle).then((result) => {
953
+ if (result.status === "failed") {
954
+ console.error("Failed to change the Tileflow map theme", result.error);
955
+ }
956
+ });
957
+ }, [runtimeStyle, themeResolution.error]);
958
+ useLayoutEffect(() => {
959
+ if (!isImageMode) return;
960
+ readinessRunRef.current += 1;
961
+ setActiveRuntimeResource(runtimeImageUrl);
962
+ setCaptureState("loading");
963
+ const image = imageRef.current;
964
+ if (image?.complete) {
965
+ if (image.naturalWidth === 0) setCaptureState("error");
966
+ else void markImageReady(image);
967
+ }
968
+ }, [isImageMode, runtimeImageUrl]);
969
+ async function markImageReady(image) {
970
+ const run = ++readinessRunRef.current;
971
+ try {
972
+ if (typeof image.decode === "function") await image.decode();
973
+ } catch {
974
+ if (!image.complete || image.naturalWidth === 0) {
975
+ if (readinessRunRef.current === run) setCaptureState("error");
976
+ return;
977
+ }
978
+ }
979
+ if (readinessRunRef.current === run) setCaptureState("idle");
980
+ }
231
981
  const frameStyle = {
232
982
  height,
233
983
  minHeight: 240,
@@ -235,228 +985,318 @@ function Map({
235
985
  position: "relative",
236
986
  width: "100%"
237
987
  };
988
+ const diagnosticView = interactionDiagnostic ? /* @__PURE__ */ jsx(
989
+ "div",
990
+ {
991
+ "data-tileflow-interaction-diagnostic": interactionDiagnostic.code,
992
+ role: "alert",
993
+ style: interactionDiagnosticStyle,
994
+ children: interactionDiagnostic.message
995
+ }
996
+ ) : null;
238
997
  if (isImageMode) {
239
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle, children: runtimeImageUrl ? /* @__PURE__ */ jsx(
240
- "img",
998
+ return /* @__PURE__ */ jsxs(
999
+ "div",
241
1000
  {
242
- alt,
243
- decoding: "async",
244
- loading: imageLoading,
245
- src: runtimeImageUrl,
246
- style: {
247
- display: "block",
248
- height: "100%",
249
- objectFit: "cover",
250
- width: "100%"
251
- }
1001
+ className,
1002
+ "data-tileflow-capture-id": resolvedCaptureId,
1003
+ "data-tileflow-map": mapName,
1004
+ "data-tileflow-theme": resolvedThemeName,
1005
+ "data-tileflow-state": effectiveCaptureState,
1006
+ ref: containerRef,
1007
+ style: frameStyle,
1008
+ children: [
1009
+ runtimeImageUrl ? /* @__PURE__ */ jsx(
1010
+ "img",
1011
+ {
1012
+ alt,
1013
+ decoding: "async",
1014
+ loading: imageLoading,
1015
+ onError: () => {
1016
+ readinessRunRef.current += 1;
1017
+ setCaptureState("error");
1018
+ },
1019
+ onLoad: (event) => void markImageReady(event.currentTarget),
1020
+ ref: imageRef,
1021
+ src: runtimeImageUrl,
1022
+ style: {
1023
+ display: "block",
1024
+ height: "100%",
1025
+ objectFit: "cover",
1026
+ width: "100%"
1027
+ }
1028
+ }
1029
+ ) : null,
1030
+ diagnosticView
1031
+ ]
252
1032
  }
253
- ) : null });
1033
+ );
254
1034
  }
255
- return /* @__PURE__ */ jsx("div", { className, ref: containerRef, style: frameStyle });
256
- }
257
- function resolveStyleUrl(mapName, styleBaseUrl) {
258
- const baseUrl = normalizeUrl(styleBaseUrl ?? getDefaultStyleBaseUrl());
259
- return `${baseUrl}/styles/${mapName}.json`;
1035
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1036
+ /* @__PURE__ */ jsxs(
1037
+ "div",
1038
+ {
1039
+ className,
1040
+ "data-tileflow-capture-id": resolvedCaptureId,
1041
+ "data-tileflow-map": mapName,
1042
+ "data-tileflow-theme": resolvedThemeName,
1043
+ "data-tileflow-state": effectiveCaptureState,
1044
+ style: frameStyle,
1045
+ children: [
1046
+ /* @__PURE__ */ jsx("div", { ref: containerRef, style: mapContainerStyle }),
1047
+ diagnosticView
1048
+ ]
1049
+ }
1050
+ ),
1051
+ annotationRenderTargets.map((target) => {
1052
+ const portalKey = `annotation:${target.key}`;
1053
+ return createPortal(
1054
+ /* @__PURE__ */ jsx(
1055
+ TileflowAnnotationPortal,
1056
+ {
1057
+ onCommit: portalReadiness.markCommitted,
1058
+ portalKey,
1059
+ readinessToken: portalReadiness.token,
1060
+ renderMarker,
1061
+ renderPopup: generalRenderPopup,
1062
+ renderTarget: target,
1063
+ renderTooltip: generalRenderTooltip
1064
+ }
1065
+ ),
1066
+ target.container,
1067
+ portalKey
1068
+ );
1069
+ }),
1070
+ semanticRenderTargets.map((target) => {
1071
+ const portalKey = `semantic:${target.key}`;
1072
+ return createPortal(
1073
+ /* @__PURE__ */ jsx(
1074
+ TileflowSemanticPortal,
1075
+ {
1076
+ onCommit: portalReadiness.markCommitted,
1077
+ portalKey,
1078
+ readinessToken: portalReadiness.token,
1079
+ renderPopup: generalRenderPopup,
1080
+ renderTarget: target,
1081
+ renderTooltip: generalRenderTooltip
1082
+ }
1083
+ ),
1084
+ target.container,
1085
+ portalKey
1086
+ );
1087
+ })
1088
+ ] });
260
1089
  }
261
- function getDefaultStyleBaseUrl() {
262
- const globalValue = globalThis.__TILEFLOW_STYLE_BASE_URL__;
263
- return globalValue ?? "http://localhost:3333";
1090
+ var mapContainerStyle = {
1091
+ inset: 0,
1092
+ position: "absolute"
1093
+ };
1094
+ var interactionDiagnosticStyle = {
1095
+ background: "#7f1d1d",
1096
+ border: "1px solid #fecaca",
1097
+ borderRadius: 4,
1098
+ color: "#ffffff",
1099
+ fontFamily: "system-ui, sans-serif",
1100
+ fontSize: 13,
1101
+ left: 12,
1102
+ maxWidth: "calc(100% - 24px)",
1103
+ padding: "8px 10px",
1104
+ position: "absolute",
1105
+ top: 12,
1106
+ zIndex: 10
1107
+ };
1108
+ function TileflowAnnotationPortal({
1109
+ onCommit,
1110
+ portalKey,
1111
+ readinessToken,
1112
+ renderMarker,
1113
+ renderPopup,
1114
+ renderTarget,
1115
+ renderTooltip
1116
+ }) {
1117
+ useLayoutEffect(() => {
1118
+ onCommit(readinessToken, portalKey);
1119
+ }, [onCommit, portalKey, readinessToken]);
1120
+ const { annotation } = renderTarget;
1121
+ const content = renderTarget.kind === "marker" ? annotation.marker?.content : renderTarget.kind === "tooltip" ? annotation.tooltip?.content : annotation.popup?.content;
1122
+ const context = {
1123
+ annotation,
1124
+ close: renderTarget.close,
1125
+ ...content ? { content } : {},
1126
+ target: {
1127
+ annotation,
1128
+ coordinate: annotation.coordinate,
1129
+ kind: "annotation"
1130
+ },
1131
+ ...content?.kind === "view" ? { viewName: content.name } : {}
1132
+ };
1133
+ if (renderTarget.kind === "marker") return renderMarker?.(context) ?? null;
1134
+ const renderer = renderTarget.kind === "tooltip" ? renderTooltip : renderPopup;
1135
+ return renderer?.(context) ?? null;
264
1136
  }
265
- function normalizeUrl(value) {
266
- return value.replace(/\/+$/, "");
1137
+ function TileflowSemanticPortal({
1138
+ onCommit,
1139
+ portalKey,
1140
+ readinessToken,
1141
+ renderPopup,
1142
+ renderTarget,
1143
+ renderTooltip
1144
+ }) {
1145
+ useLayoutEffect(() => {
1146
+ onCommit(readinessToken, portalKey);
1147
+ }, [onCommit, portalKey, readinessToken]);
1148
+ const context = {
1149
+ close: renderTarget.close,
1150
+ content: renderTarget.content,
1151
+ target: renderTarget.target,
1152
+ ...renderTarget.viewName ? { viewName: renderTarget.viewName } : {}
1153
+ };
1154
+ const renderer = renderTarget.kind === "tooltip" ? renderTooltip : renderPopup;
1155
+ return renderer?.(context) ?? null;
267
1156
  }
268
- function createSessionId() {
269
- if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
270
- return `ses_${crypto.randomUUID()}`;
1157
+ function useTileflowPortalReadiness(portalKeys) {
1158
+ const signature = JSON.stringify([...portalKeys].sort());
1159
+ const versionRef = useRef({ signature, version: 0 });
1160
+ if (versionRef.current.signature !== signature) {
1161
+ versionRef.current = { signature, version: versionRef.current.version + 1 };
271
1162
  }
272
- return `ses_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`;
273
- }
274
- function createTransformRequest(analytics, sessionId) {
275
- if (!analytics || analytics.enabled === false) {
276
- return void 0;
1163
+ const token = `${versionRef.current.version}:${signature}`;
1164
+ const expectedKeysRef = useRef({ keys: [...portalKeys].sort(), token });
1165
+ if (expectedKeysRef.current.token !== token) {
1166
+ expectedKeysRef.current = { keys: [...portalKeys].sort(), token };
277
1167
  }
278
- return (url) => {
279
- const apiUrl = getAnalyticsApiUrl(analytics);
280
- if (!apiUrl || !isTileflowTileUrl(url, apiUrl)) {
281
- return void 0;
282
- }
283
- const nextUrl = new URL(url);
284
- nextUrl.searchParams.set("session", sessionId);
285
- if (analytics.mapId) {
286
- nextUrl.searchParams.set("map", analytics.mapId);
1168
+ const expectedKeys = expectedKeysRef.current.keys;
1169
+ const [committed, setCommitted] = useState(() => ({
1170
+ keys: /* @__PURE__ */ new Set(),
1171
+ token
1172
+ }));
1173
+ const [readyToken, setReadyToken] = useState(token);
1174
+ const readinessRunRef = useRef(0);
1175
+ const markCommitted = useCallback((committedToken, portalKey) => {
1176
+ setCommitted((current) => {
1177
+ if (current.token !== committedToken) {
1178
+ return { keys: /* @__PURE__ */ new Set([portalKey]), token: committedToken };
1179
+ }
1180
+ if (current.keys.has(portalKey)) return current;
1181
+ return { keys: /* @__PURE__ */ new Set([...current.keys, portalKey]), token: committedToken };
1182
+ });
1183
+ }, []);
1184
+ useLayoutEffect(() => {
1185
+ setCommitted((current) => current.token === token ? current : { keys: /* @__PURE__ */ new Set(), token });
1186
+ }, [token]);
1187
+ useLayoutEffect(() => {
1188
+ if (readyToken === token || committed.token !== token || !expectedKeys.every((key) => committed.keys.has(key))) {
1189
+ return;
287
1190
  }
288
- if (analytics.styleId) {
289
- nextUrl.searchParams.set("styleId", analytics.styleId);
1191
+ const browserWindow = globalThis.window;
1192
+ const run = ++readinessRunRef.current;
1193
+ let secondFrame;
1194
+ const firstFrame = browserWindow.requestAnimationFrame(() => {
1195
+ secondFrame = browserWindow.requestAnimationFrame(() => {
1196
+ if (readinessRunRef.current === run) setReadyToken(token);
1197
+ });
1198
+ });
1199
+ return () => {
1200
+ readinessRunRef.current += 1;
1201
+ browserWindow.cancelAnimationFrame(firstFrame);
1202
+ if (secondFrame !== void 0) browserWindow.cancelAnimationFrame(secondFrame);
1203
+ };
1204
+ }, [committed, expectedKeys, readyToken, token]);
1205
+ return { idle: readyToken === token, markCommitted, token };
1206
+ }
1207
+ function wrapMapLibrePositioned(positioned) {
1208
+ return {
1209
+ addTo(map) {
1210
+ positioned.addTo(map);
1211
+ },
1212
+ remove() {
1213
+ positioned.remove();
1214
+ },
1215
+ setLngLat(coordinate) {
1216
+ positioned.setLngLat([coordinate[0], coordinate[1]]);
290
1217
  }
291
- return { url: nextUrl.toString() };
292
1218
  };
293
1219
  }
294
- function startSession(analytics, input) {
295
- if (!analytics || analytics.enabled === false || !input.mapId || typeof navigator === "undefined") {
296
- return;
297
- }
298
- const endpoint = analytics.endpoint ?? `${getAnalyticsApiUrl(analytics)}/v1/sessions/start`;
299
- const payload = JSON.stringify({
300
- mapId: input.mapId,
301
- metadata: analytics.metadata,
302
- referrer: typeof document === "undefined" ? void 0 : document.referrer || "",
303
- sdkVersion: analytics.sdkVersion,
304
- sessionId: input.sessionId,
305
- source: analytics.source ?? "react",
306
- styleId: input.styleId,
307
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
308
- });
309
- const blob = new Blob([payload], { type: "application/json" });
310
- if (navigator.sendBeacon?.(endpoint, blob)) {
311
- return;
312
- }
313
- void fetch(endpoint, {
314
- body: payload,
315
- credentials: "omit",
316
- headers: {
317
- "Content-Type": "application/json"
1220
+ function createTileflowMapLibrePoiMap(map) {
1221
+ return {
1222
+ getStyle: () => map.getStyle(),
1223
+ on(event, listener) {
1224
+ return map.on(event, listener);
318
1225
  },
319
- keepalive: true,
320
- method: "POST"
321
- });
322
- }
323
- function getAnalyticsApiUrl(analytics) {
324
- if (analytics.apiUrl) {
325
- return normalizeUrl(analytics.apiUrl);
326
- }
327
- if (analytics.endpoint) {
328
- const url = new URL(analytics.endpoint);
329
- return url.origin;
330
- }
331
- return "https://api.tileflow.dev";
1226
+ queryRenderedFeatures(point, options) {
1227
+ return map.queryRenderedFeatures(point, {
1228
+ layers: [...options.layers]
1229
+ });
1230
+ }
1231
+ };
332
1232
  }
333
- function mergeAnalytics(userAnalytics, runtimeAnalytics) {
334
- if (userAnalytics?.enabled === false) {
335
- return userAnalytics;
336
- }
337
- if (!userAnalytics && !runtimeAnalytics?.mapId) {
338
- return void 0;
339
- }
1233
+ function createOverlayFailureDiagnostic(message) {
340
1234
  return {
341
- ...runtimeAnalytics,
342
- ...userAnalytics,
343
- apiUrl: userAnalytics?.apiUrl ?? runtimeAnalytics?.apiUrl,
344
- mapId: userAnalytics?.mapId ?? runtimeAnalytics?.mapId,
345
- styleId: userAnalytics?.styleId ?? runtimeAnalytics?.styleId
1235
+ code: "OVERLAY_FAILURE",
1236
+ level: "error",
1237
+ message,
1238
+ path: ""
346
1239
  };
347
1240
  }
348
- function isTileflowTileUrl(url, apiUrl) {
349
- try {
350
- const parsedUrl = new URL(url);
351
- const parsedApiUrl = new URL(apiUrl);
352
- return parsedUrl.origin === parsedApiUrl.origin && isTilePath(parsedUrl);
353
- } catch {
354
- return false;
355
- }
356
- }
357
- function isTilePath(url) {
358
- return url.pathname.startsWith("/tiles/") || url.pathname.startsWith("/v1/tiles/");
1241
+ function tileflowInteractionDiagnosticKey(diagnostic) {
1242
+ return [
1243
+ diagnostic.code,
1244
+ diagnostic.level,
1245
+ diagnostic.path ?? "",
1246
+ diagnostic.target ? JSON.stringify(diagnostic.target) : "",
1247
+ diagnostic.message
1248
+ ].join("\0");
359
1249
  }
360
- function isLocalDevHost() {
361
- if (typeof window === "undefined") {
362
- return false;
1250
+ function runTileflowReactCleanups(cleanups) {
1251
+ const errors = [];
1252
+ for (const cleanup of cleanups) {
1253
+ try {
1254
+ cleanup();
1255
+ } catch (error) {
1256
+ errors.push(error);
1257
+ }
363
1258
  }
364
- return ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname);
1259
+ if (errors.length === 1) throw errors[0];
1260
+ if (errors.length > 1) throw new AggregateError(errors, "Tileflow React cleanup failed.");
365
1261
  }
366
- function loadManifest(url) {
367
- if (!manifestCache.has(url)) {
368
- manifestCache.set(
369
- url,
370
- fetch(url, { cache: "no-store" }).then(async (response) => {
371
- if (response.status === 404) {
372
- return null;
373
- }
374
- if (!response.ok) {
375
- throw new Error(`Tileflow manifest failed: ${response.status}`);
376
- }
377
- return await response.json();
378
- })
379
- );
380
- }
381
- return manifestCache.get(url);
1262
+ function useStableMapOptions(options) {
1263
+ return useStableMapOptionValue(options);
382
1264
  }
383
- function resolveManifestMap(manifest, mapName) {
384
- const entry = manifest.maps?.[mapName] ?? manifest.styles?.[mapName];
385
- if (!entry) {
386
- return null;
387
- }
388
- if (typeof entry === "string") {
389
- const analytics2 = inferAnalyticsFromStyleUrl(entry);
390
- return {
391
- apiUrl: analytics2?.apiUrl ?? manifest.apiUrl,
392
- mapId: analytics2?.mapId,
393
- styleId: analytics2?.styleId,
394
- styleUrl: entry
395
- };
1265
+ function useStableMapOptionValue(value) {
1266
+ const valueRef = useRef(value);
1267
+ if (!areEquivalentMapOptionValues(valueRef.current, value)) {
1268
+ valueRef.current = value;
396
1269
  }
397
- const styleUrl = entry.styleUrl ?? entry.url;
398
- if (!styleUrl && !entry.mapId) {
399
- return null;
400
- }
401
- const analytics = styleUrl ? inferAnalyticsFromStyleUrl(styleUrl) : void 0;
402
- return {
403
- apiUrl: entry.apiUrl ?? manifest.apiUrl ?? analytics?.apiUrl,
404
- mapId: entry.mapId ?? analytics?.mapId,
405
- styleId: entry.styleId ?? analytics?.styleId,
406
- styleUrl
407
- };
1270
+ return valueRef.current;
408
1271
  }
409
- function resolveStaticImageUrl(input) {
410
- if (!input.manifestMap?.mapId || !input.imageSize) {
411
- return void 0;
1272
+ function areEquivalentMapOptionValues(left, right, seen = /* @__PURE__ */ new WeakMap()) {
1273
+ if (Object.is(left, right)) {
1274
+ return true;
412
1275
  }
413
- const apiUrl = normalizeUrl(input.manifestMap.apiUrl ?? "https://api.tileflow.dev");
414
- const url = new URL(`/maps/${input.manifestMap.mapId}/static.png`, apiUrl);
415
- url.searchParams.set("center", input.center.join(","));
416
- url.searchParams.set("zoom", String(input.zoom));
417
- url.searchParams.set("width", String(input.imageSize.width));
418
- url.searchParams.set("height", String(input.imageSize.height));
419
- return url.toString();
420
- }
421
- function normalizeStaticImageSize(input) {
422
- let width = clampDimension(input.width);
423
- let height = clampDimension(input.height);
424
- const pixelCount = width * height;
425
- if (pixelCount > maxStaticImagePixels) {
426
- const scale = Math.sqrt(maxStaticImagePixels / pixelCount);
427
- width = clampDimension(width * scale);
428
- height = clampDimension(height * scale);
1276
+ if (!left || !right || typeof left !== "object" || typeof right !== "object") {
1277
+ return false;
429
1278
  }
430
- return { height, width };
431
- }
432
- function clampDimension(value) {
433
- if (!Number.isFinite(value) || value <= 0) {
434
- return 640;
1279
+ const seenRight = seen.get(left);
1280
+ if (seenRight) {
1281
+ return seenRight === right;
435
1282
  }
436
- return Math.max(64, Math.min(maxStaticImageDimension, Math.round(value)));
437
- }
438
- function inferAnalyticsFromStyleUrl(styleUrl) {
439
- try {
440
- const url = new URL(styleUrl, getLocationOrigin());
441
- const match = url.pathname.match(/\/(?:v1\/)?maps\/([^/]+)\/style\.json$/);
442
- if (!match?.[1]) {
443
- return void 0;
444
- }
445
- return {
446
- apiUrl: url.origin,
447
- mapId: match[1],
448
- styleId: url.toString()
449
- };
450
- } catch {
451
- return void 0;
1283
+ seen.set(left, right);
1284
+ if (Array.isArray(left) || Array.isArray(right)) {
1285
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => areEquivalentMapOptionValues(value, right[index], seen));
452
1286
  }
453
- }
454
- function getLocationOrigin() {
455
- if (typeof window === "undefined") {
456
- return "http://localhost";
1287
+ if (!isPlainObject(left) || !isPlainObject(right)) {
1288
+ return false;
457
1289
  }
458
- return window.location.origin;
1290
+ const leftKeys = Object.keys(left);
1291
+ const rightKeys = Object.keys(right);
1292
+ return leftKeys.length === rightKeys.length && leftKeys.every(
1293
+ (key) => Object.hasOwn(right, key) && areEquivalentMapOptionValues(left[key], right[key], seen)
1294
+ );
1295
+ }
1296
+ function isPlainObject(value) {
1297
+ const prototype = Object.getPrototypeOf(value);
1298
+ return prototype === Object.prototype || prototype === null;
459
1299
  }
460
1300
  export {
461
- Map
1301
+ Map2 as Map
462
1302
  };