@tileflow/react 0.1.0-alpha.3 → 0.1.0-alpha.31

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