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

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