@deftui/addons 0.1.2 → 0.1.4

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.
Files changed (31) hide show
  1. package/dist/form.d.mts +2 -13
  2. package/dist/form.mjs +2 -13
  3. package/dist/map.d.mts +30 -0
  4. package/dist/map.mjs +18 -0
  5. package/dist/maplibregl/map.d.mts +388 -0
  6. package/dist/maplibregl/map.mjs +1641 -0
  7. package/dist/node_modules/.bun/@types_geojson@7946.0.16/node_modules/@types/geojson/index.d.mts +174 -0
  8. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/Icon.mjs +37 -0
  9. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/context.mjs +12 -0
  10. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/createLucideIcon.mjs +23 -0
  11. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/loader-circle.mjs +20 -0
  12. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/locate.mjs +51 -0
  13. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/maximize.mjs +33 -0
  14. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/minus.mjs +19 -0
  15. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/plus.mjs +22 -0
  16. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/x.mjs +22 -0
  17. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/build/buildLucideIconForReact.mjs +22 -0
  18. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/build/buildLucideIconNode.mjs +57 -0
  19. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/build/defaultAttributes.mjs +19 -0
  20. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/hasA11yProp.mjs +12 -0
  21. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/mergeClasses.mjs +11 -0
  22. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/toCamelCase.mjs +22 -0
  23. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/toKebabCase.mjs +9 -0
  24. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/toLucideIconData.mjs +18 -0
  25. package/dist/node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/shared/src/utils/toPascalCase.mjs +13 -0
  26. package/dist/react-hook-form/field.d.mts +11 -6
  27. package/dist/react-hook-form/field.mjs +3 -4
  28. package/dist/react-hook-form/form.d.mts +1 -1
  29. package/dist/react-hook-form/form.mjs +1 -1
  30. package/dist/styles.css +21 -0
  31. package/package.json +18 -9
@@ -0,0 +1,1641 @@
1
+ "use client";
2
+ import LoaderCircle from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/loader-circle.mjs";
3
+ import Locate from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/locate.mjs";
4
+ import Maximize from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/maximize.mjs";
5
+ import Minus from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/minus.mjs";
6
+ import Plus from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/plus.mjs";
7
+ import X from "../node_modules/.bun/lucide-react@1.66.0_c841d34b494ec30a/node_modules/lucide-react/dist/esm/icons/x.mjs";
8
+ import "maplibre-gl/dist/maplibre-gl.css";
9
+ import { cn } from "@deftui/react";
10
+ import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
11
+ import { createPortal } from "react-dom";
12
+ import { jsx, jsxs } from "react/jsx-runtime";
13
+ // src/maplibregl/map.tsx
14
+ let maplibrePromise = null;
15
+ function loadMapLibre() {
16
+ maplibrePromise ??= import("maplibre-gl").then((lib) => {
17
+ if (!lib.getWorkerUrl()) lib.setWorkerUrl(`https://unpkg.com/maplibre-gl@${lib.getVersion()}/dist/maplibre-gl-worker.mjs`);
18
+ return lib;
19
+ }).catch((error) => {
20
+ maplibrePromise = null;
21
+ throw error;
22
+ });
23
+ return maplibrePromise;
24
+ }
25
+ if (typeof window !== "undefined") loadMapLibre().catch(() => {});
26
+ const defaultStyles = {
27
+ dark: "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json",
28
+ light: "https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
29
+ };
30
+ const blankMapStyle = {
31
+ version: 8,
32
+ sources: {},
33
+ layers: [{
34
+ id: "background",
35
+ type: "background",
36
+ paint: { "background-color": "rgba(0, 0, 0, 0)" }
37
+ }]
38
+ };
39
+ const blankGlobeStyle = {
40
+ ...blankMapStyle,
41
+ projection: { type: "globe" },
42
+ layers: [{
43
+ id: "background",
44
+ type: "background",
45
+ paint: { "background-color": "rgba(128, 128, 128, 0.12)" }
46
+ }]
47
+ };
48
+ function useStableValue(value) {
49
+ const key = useMemo(() => JSON.stringify(value) ?? "", [value]);
50
+ return useMemo(() => value, [key]);
51
+ }
52
+ function mergeHoverPaint(paint, hoverPaint) {
53
+ if (!hoverPaint) return paint;
54
+ const merged = { ...paint };
55
+ for (const [key, hoverValue] of Object.entries(hoverPaint)) {
56
+ if (hoverValue === void 0) continue;
57
+ const baseValue = merged[key];
58
+ merged[key] = baseValue === void 0 ? hoverValue : [
59
+ "case",
60
+ [
61
+ "boolean",
62
+ ["feature-state", "hover"],
63
+ false
64
+ ],
65
+ hoverValue,
66
+ baseValue
67
+ ];
68
+ }
69
+ return merged;
70
+ }
71
+ function getDocumentTheme() {
72
+ if (typeof document === "undefined") return null;
73
+ const root = document.documentElement;
74
+ if (root.classList.contains("dark")) return "dark";
75
+ if (root.classList.contains("light")) return "light";
76
+ const dataTheme = root.dataset.theme;
77
+ if (dataTheme === "dark" || dataTheme === "light") return dataTheme;
78
+ return null;
79
+ }
80
+ function getSystemTheme() {
81
+ if (typeof window === "undefined") return "light";
82
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
83
+ }
84
+ function useResolvedTheme(themeProp) {
85
+ const [detectedTheme, setDetectedTheme] = useState(() => getDocumentTheme() ?? getSystemTheme());
86
+ useEffect(() => {
87
+ if (themeProp) return;
88
+ const observer = new MutationObserver(() => {
89
+ const docTheme = getDocumentTheme();
90
+ if (docTheme) setDetectedTheme(docTheme);
91
+ });
92
+ observer.observe(document.documentElement, {
93
+ attributes: true,
94
+ attributeFilter: ["class", "data-theme"]
95
+ });
96
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
97
+ const handleSystemChange = (e) => {
98
+ if (!getDocumentTheme()) setDetectedTheme(e.matches ? "dark" : "light");
99
+ };
100
+ mediaQuery.addEventListener("change", handleSystemChange);
101
+ return () => {
102
+ observer.disconnect();
103
+ mediaQuery.removeEventListener("change", handleSystemChange);
104
+ };
105
+ }, [themeProp]);
106
+ return themeProp ?? detectedTheme;
107
+ }
108
+ const MapContext = createContext(null);
109
+ function useMap() {
110
+ const context = useContext(MapContext);
111
+ if (!context) throw new Error("useMap must be used within a Map component");
112
+ return context;
113
+ }
114
+ function useMapLib() {
115
+ const { mapLib } = useMap();
116
+ if (!mapLib) throw new Error("useMapLib must be used within a Map component");
117
+ return mapLib;
118
+ }
119
+ function DefaultLoader() {
120
+ return /* @__PURE__ */ jsx("div", {
121
+ className: "bg-background/50 absolute inset-0 z-10 flex items-center justify-center backdrop-blur-xs",
122
+ children: /* @__PURE__ */ jsxs("div", {
123
+ className: "flex gap-1",
124
+ children: [
125
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/60 size-1.5 animate-pulse rounded-full" }),
126
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:150ms]" }),
127
+ /* @__PURE__ */ jsx("span", { className: "bg-muted-foreground/60 size-1.5 animate-pulse rounded-full [animation-delay:300ms]" })
128
+ ]
129
+ })
130
+ });
131
+ }
132
+ function getViewport(map) {
133
+ const center = map.getCenter();
134
+ return {
135
+ center: [center.lng, center.lat],
136
+ zoom: map.getZoom(),
137
+ bearing: map.getBearing(),
138
+ pitch: map.getPitch()
139
+ };
140
+ }
141
+ const Map = forwardRef(function Map({ children, className, theme: themeProp, styles, blank = false, projection, viewport, onViewportChange, loading = false, ...props }, ref) {
142
+ const containerRef = useRef(null);
143
+ const [mapLib, setMapLib] = useState(null);
144
+ const [mapInstance, setMapInstance] = useState(null);
145
+ const [isLoaded, setIsLoaded] = useState(false);
146
+ const [isStyleLoaded, setIsStyleLoaded] = useState(false);
147
+ const [pendingStyle, setPendingStyle] = useState(null);
148
+ const currentStyleRef = useRef(null);
149
+ const styleSwapInFlightRef = useRef(false);
150
+ const internalUpdateRef = useRef(false);
151
+ const resolvedTheme = useResolvedTheme(themeProp);
152
+ const isControlled = viewport !== void 0 && onViewportChange !== void 0;
153
+ const onViewportChangeRef = useRef(onViewportChange);
154
+ onViewportChangeRef.current = onViewportChange;
155
+ const stableStyles = useStableValue(styles);
156
+ const isGlobe = projection?.type === "globe";
157
+ const mapStyles = useMemo(() => {
158
+ if (stableStyles) return {
159
+ dark: stableStyles.dark ?? defaultStyles.dark,
160
+ light: stableStyles.light ?? defaultStyles.light
161
+ };
162
+ if (blank) {
163
+ const style = isGlobe ? blankGlobeStyle : blankMapStyle;
164
+ return {
165
+ dark: style,
166
+ light: style
167
+ };
168
+ }
169
+ return defaultStyles;
170
+ }, [
171
+ stableStyles,
172
+ blank,
173
+ isGlobe
174
+ ]);
175
+ const initOptionsRef = useRef({
176
+ props,
177
+ viewport,
178
+ resolvedTheme,
179
+ mapStyles
180
+ });
181
+ initOptionsRef.current = {
182
+ props,
183
+ viewport,
184
+ resolvedTheme,
185
+ mapStyles
186
+ };
187
+ useImperativeHandle(ref, () => mapInstance, [mapInstance]);
188
+ useEffect(() => {
189
+ let cancelled = false;
190
+ let map;
191
+ loadMapLibre().then((lib) => {
192
+ if (cancelled || !containerRef.current) return;
193
+ const { props, viewport, resolvedTheme, mapStyles } = initOptionsRef.current;
194
+ const initialStyle = resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light;
195
+ currentStyleRef.current = initialStyle;
196
+ const instance = new lib.Map({
197
+ container: containerRef.current,
198
+ style: initialStyle,
199
+ renderWorldCopies: false,
200
+ attributionControl: { compact: true },
201
+ ...props,
202
+ ...viewport
203
+ });
204
+ if (!instance.painter) throw new lib.GPUInitializationError({}, null);
205
+ map = instance;
206
+ instance.on("load", () => setIsLoaded(true));
207
+ instance.on("style.load", () => {
208
+ styleSwapInFlightRef.current = false;
209
+ setIsStyleLoaded(true);
210
+ });
211
+ instance.on("move", () => {
212
+ if (internalUpdateRef.current) return;
213
+ onViewportChangeRef.current?.(getViewport(instance));
214
+ });
215
+ setMapLib(lib);
216
+ setMapInstance(instance);
217
+ }).catch((error) => {
218
+ if (cancelled) return;
219
+ setMapInstance(() => {
220
+ throw error;
221
+ });
222
+ });
223
+ return () => {
224
+ cancelled = true;
225
+ if (!map) return;
226
+ map.remove();
227
+ setIsLoaded(false);
228
+ setIsStyleLoaded(false);
229
+ setMapInstance(null);
230
+ };
231
+ }, []);
232
+ useEffect(() => {
233
+ if (!mapInstance || !isControlled || !viewport) return;
234
+ if (mapInstance.isMoving()) return;
235
+ const current = getViewport(mapInstance);
236
+ const next = {
237
+ center: viewport.center ?? current.center,
238
+ zoom: viewport.zoom ?? current.zoom,
239
+ bearing: viewport.bearing ?? current.bearing,
240
+ pitch: viewport.pitch ?? current.pitch
241
+ };
242
+ if (next.center[0] === current.center[0] && next.center[1] === current.center[1] && next.zoom === current.zoom && next.bearing === current.bearing && next.pitch === current.pitch) return;
243
+ internalUpdateRef.current = true;
244
+ mapInstance.jumpTo(next);
245
+ internalUpdateRef.current = false;
246
+ }, [
247
+ mapInstance,
248
+ isControlled,
249
+ viewport
250
+ ]);
251
+ useEffect(() => {
252
+ if (!mapInstance || !resolvedTheme) return;
253
+ const newStyle = resolvedTheme === "dark" ? mapStyles.dark : mapStyles.light;
254
+ if (currentStyleRef.current === newStyle) return;
255
+ currentStyleRef.current = newStyle;
256
+ setIsStyleLoaded(false);
257
+ setPendingStyle(newStyle);
258
+ }, [
259
+ mapInstance,
260
+ resolvedTheme,
261
+ mapStyles
262
+ ]);
263
+ useEffect(() => {
264
+ if (!mapInstance || !pendingStyle) return;
265
+ setPendingStyle(null);
266
+ styleSwapInFlightRef.current = true;
267
+ mapInstance.setStyle(pendingStyle, { diff: false });
268
+ }, [mapInstance, pendingStyle]);
269
+ useEffect(() => {
270
+ if (!mapInstance || !isStyleLoaded || !projection) return;
271
+ if (styleSwapInFlightRef.current) return;
272
+ mapInstance.setProjection(projection);
273
+ }, [
274
+ mapInstance,
275
+ isStyleLoaded,
276
+ projection
277
+ ]);
278
+ const contextValue = useMemo(() => ({
279
+ map: mapInstance,
280
+ mapLib,
281
+ isLoaded: isLoaded && isStyleLoaded,
282
+ resolvedTheme
283
+ }), [
284
+ mapInstance,
285
+ mapLib,
286
+ isLoaded,
287
+ isStyleLoaded,
288
+ resolvedTheme
289
+ ]);
290
+ return /* @__PURE__ */ jsx(MapContext.Provider, {
291
+ value: contextValue,
292
+ children: /* @__PURE__ */ jsxs("div", {
293
+ ref: containerRef,
294
+ className: cn("relative h-full w-full", className),
295
+ children: [(!isLoaded || loading) && /* @__PURE__ */ jsx(DefaultLoader, {}), mapInstance && children]
296
+ })
297
+ });
298
+ });
299
+ const MarkerContext = createContext(null);
300
+ function useMarkerContext() {
301
+ const context = useContext(MarkerContext);
302
+ if (!context) throw new Error("Marker components must be used within MapMarker");
303
+ return context;
304
+ }
305
+ function MapMarker({ longitude, latitude, children, onClick, onMouseEnter, onMouseLeave, onDragStart, onDrag, onDragEnd, draggable = false, ...markerOptions }) {
306
+ const { map } = useMap();
307
+ const { Marker } = useMapLib();
308
+ const callbacksRef = useRef({
309
+ onClick,
310
+ onMouseEnter,
311
+ onMouseLeave,
312
+ onDragStart,
313
+ onDrag,
314
+ onDragEnd
315
+ });
316
+ callbacksRef.current = {
317
+ onClick,
318
+ onMouseEnter,
319
+ onMouseLeave,
320
+ onDragStart,
321
+ onDrag,
322
+ onDragEnd
323
+ };
324
+ const marker = useMemo(() => {
325
+ const markerInstance = new Marker({
326
+ ...markerOptions,
327
+ element: document.createElement("div"),
328
+ draggable
329
+ }).setLngLat([longitude, latitude]);
330
+ const handleClick = (e) => callbacksRef.current.onClick?.(e);
331
+ const handleMouseEnter = (e) => callbacksRef.current.onMouseEnter?.(e);
332
+ const handleMouseLeave = (e) => callbacksRef.current.onMouseLeave?.(e);
333
+ markerInstance.getElement()?.addEventListener("click", handleClick);
334
+ markerInstance.getElement()?.addEventListener("mouseenter", handleMouseEnter);
335
+ markerInstance.getElement()?.addEventListener("mouseleave", handleMouseLeave);
336
+ const handleDragStart = () => {
337
+ const lngLat = markerInstance.getLngLat();
338
+ callbacksRef.current.onDragStart?.({
339
+ lng: lngLat.lng,
340
+ lat: lngLat.lat
341
+ });
342
+ };
343
+ const handleDrag = () => {
344
+ const lngLat = markerInstance.getLngLat();
345
+ callbacksRef.current.onDrag?.({
346
+ lng: lngLat.lng,
347
+ lat: lngLat.lat
348
+ });
349
+ };
350
+ const handleDragEnd = () => {
351
+ const lngLat = markerInstance.getLngLat();
352
+ callbacksRef.current.onDragEnd?.({
353
+ lng: lngLat.lng,
354
+ lat: lngLat.lat
355
+ });
356
+ };
357
+ markerInstance.on("dragstart", handleDragStart);
358
+ markerInstance.on("drag", handleDrag);
359
+ markerInstance.on("dragend", handleDragEnd);
360
+ return markerInstance;
361
+ }, []);
362
+ useEffect(() => {
363
+ if (!map) return;
364
+ marker.addTo(map);
365
+ return () => {
366
+ marker.remove();
367
+ };
368
+ }, [map]);
369
+ const { offset, rotation, rotationAlignment, pitchAlignment } = markerOptions;
370
+ useEffect(() => {
371
+ const current = marker.getLngLat();
372
+ if (current.lng !== longitude || current.lat !== latitude) marker.setLngLat([longitude, latitude]);
373
+ if (marker.isDraggable() !== draggable) marker.setDraggable(draggable);
374
+ const currentOffset = marker.getOffset();
375
+ const newOffset = offset ?? [0, 0];
376
+ const [newOffsetX, newOffsetY] = Array.isArray(newOffset) ? newOffset : [newOffset.x, newOffset.y];
377
+ if (currentOffset.x !== newOffsetX || currentOffset.y !== newOffsetY) marker.setOffset(newOffset);
378
+ if (marker.getRotation() !== (rotation ?? 0)) marker.setRotation(rotation ?? 0);
379
+ if (marker.getRotationAlignment() !== (rotationAlignment ?? "auto")) marker.setRotationAlignment(rotationAlignment ?? "auto");
380
+ if (marker.getPitchAlignment() !== (pitchAlignment ?? "auto")) marker.setPitchAlignment(pitchAlignment ?? "auto");
381
+ }, [
382
+ marker,
383
+ longitude,
384
+ latitude,
385
+ draggable,
386
+ offset,
387
+ rotation,
388
+ rotationAlignment,
389
+ pitchAlignment
390
+ ]);
391
+ return /* @__PURE__ */ jsx(MarkerContext.Provider, {
392
+ value: {
393
+ marker,
394
+ map
395
+ },
396
+ children
397
+ });
398
+ }
399
+ function MarkerContent({ children, className }) {
400
+ const { marker } = useMarkerContext();
401
+ return createPortal(/* @__PURE__ */ jsx("div", {
402
+ className: cn("relative cursor-pointer", className),
403
+ children: children || /* @__PURE__ */ jsx(DefaultMarkerIcon, {})
404
+ }), marker.getElement());
405
+ }
406
+ function DefaultMarkerIcon() {
407
+ return /* @__PURE__ */ jsx("div", { className: "relative h-4 w-4 rounded-full border-2 border-white bg-blue-500 shadow-lg" });
408
+ }
409
+ function PopupCloseButton({ onClick }) {
410
+ return /* @__PURE__ */ jsx("button", {
411
+ type: "button",
412
+ onClick,
413
+ "aria-label": "Close popup",
414
+ className: "focus-visible:ring-ring hover:bg-muted text-foreground absolute top-1 right-1 z-10 inline-flex size-5 cursor-pointer items-center justify-center rounded-sm transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-inset",
415
+ children: /* @__PURE__ */ jsx(X, { className: "size-3.5" })
416
+ });
417
+ }
418
+ function MarkerPopup({ children, className, closeButton = false, ...popupOptions }) {
419
+ const { marker, map } = useMarkerContext();
420
+ const { Popup } = useMapLib();
421
+ const container = useMemo(() => document.createElement("div"), []);
422
+ const { offset, maxWidth } = popupOptions;
423
+ const popup = useMemo(() => {
424
+ return new Popup({
425
+ offset: 16,
426
+ ...popupOptions,
427
+ closeButton: false
428
+ }).setMaxWidth("none").setDOMContent(container);
429
+ }, []);
430
+ useEffect(() => {
431
+ if (!map) return;
432
+ popup.setDOMContent(container);
433
+ marker.setPopup(popup);
434
+ return () => {
435
+ marker.setPopup(null);
436
+ };
437
+ }, [map]);
438
+ useEffect(() => {
439
+ popup.setOffset(offset ?? 16);
440
+ if (maxWidth) popup.setMaxWidth(maxWidth);
441
+ }, [
442
+ popup,
443
+ offset,
444
+ maxWidth
445
+ ]);
446
+ const handleClose = () => popup.remove();
447
+ return createPortal(/* @__PURE__ */ jsxs("div", {
448
+ className: cn("bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md", "animate-in fade-in-0 zoom-in-95 duration-200 ease-out", className),
449
+ children: [closeButton && /* @__PURE__ */ jsx(PopupCloseButton, { onClick: handleClose }), children]
450
+ }), container);
451
+ }
452
+ function MarkerTooltip({ children, className, ...popupOptions }) {
453
+ const { marker, map } = useMarkerContext();
454
+ const { Popup } = useMapLib();
455
+ const container = useMemo(() => document.createElement("div"), []);
456
+ const { offset, maxWidth } = popupOptions;
457
+ const tooltip = useMemo(() => {
458
+ return new Popup({
459
+ offset: 16,
460
+ ...popupOptions,
461
+ closeOnClick: true,
462
+ closeButton: false
463
+ }).setMaxWidth("none");
464
+ }, []);
465
+ useEffect(() => {
466
+ if (!map) return;
467
+ tooltip.setDOMContent(container);
468
+ const handleMouseEnter = () => {
469
+ tooltip.setLngLat(marker.getLngLat()).addTo(map);
470
+ };
471
+ const handleMouseLeave = () => tooltip.remove();
472
+ marker.getElement()?.addEventListener("mouseenter", handleMouseEnter);
473
+ marker.getElement()?.addEventListener("mouseleave", handleMouseLeave);
474
+ return () => {
475
+ marker.getElement()?.removeEventListener("mouseenter", handleMouseEnter);
476
+ marker.getElement()?.removeEventListener("mouseleave", handleMouseLeave);
477
+ tooltip.remove();
478
+ };
479
+ }, [map]);
480
+ useEffect(() => {
481
+ tooltip.setOffset(offset ?? 16);
482
+ if (maxWidth) tooltip.setMaxWidth(maxWidth);
483
+ }, [
484
+ tooltip,
485
+ offset,
486
+ maxWidth
487
+ ]);
488
+ return createPortal(/* @__PURE__ */ jsx("div", {
489
+ className: cn("bg-foreground text-background pointer-events-none rounded-md px-2 py-1 text-xs text-balance shadow-md", "animate-in fade-in-0 zoom-in-95 duration-200 ease-out", className),
490
+ children
491
+ }), container);
492
+ }
493
+ function MarkerLabel({ children, className, position = "top" }) {
494
+ return /* @__PURE__ */ jsx("div", {
495
+ className: cn("absolute left-1/2 -translate-x-1/2 whitespace-nowrap", "text-foreground text-[10px] font-medium", {
496
+ top: "bottom-full mb-1",
497
+ bottom: "top-full mt-1"
498
+ }[position], className),
499
+ children
500
+ });
501
+ }
502
+ const positionClasses = {
503
+ "top-left": "top-2 left-2",
504
+ "top-right": "top-2 right-2",
505
+ "bottom-left": "bottom-2 left-2",
506
+ "bottom-right": "bottom-10 right-2"
507
+ };
508
+ function ControlGroup({ children }) {
509
+ return /* @__PURE__ */ jsx("div", {
510
+ className: "border-border bg-background [&>button:not(:last-child)]:border-border flex flex-col overflow-hidden rounded-md border shadow-sm [&>button:not(:last-child)]:border-b",
511
+ children
512
+ });
513
+ }
514
+ function ControlButton({ onClick, label, children, disabled = false }) {
515
+ return /* @__PURE__ */ jsx("button", {
516
+ onClick,
517
+ "aria-label": label,
518
+ type: "button",
519
+ className: cn("flex size-8 items-center justify-center transition-colors", "first:rounded-t-md last:rounded-b-md", "hover:bg-accent dark:hover:bg-accent/40", "focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset", "disabled:pointer-events-none disabled:opacity-50"),
520
+ disabled,
521
+ children
522
+ });
523
+ }
524
+ function MapControls({ position = "bottom-right", showZoom = true, showCompass = false, showLocate = false, showFullscreen = false, className, onLocate }) {
525
+ const { map } = useMap();
526
+ const [waitingForLocation, setWaitingForLocation] = useState(false);
527
+ const handleZoomIn = useCallback(() => {
528
+ map?.zoomTo(map.getZoom() + 1, { duration: 300 });
529
+ }, [map]);
530
+ const handleZoomOut = useCallback(() => {
531
+ map?.zoomTo(map.getZoom() - 1, { duration: 300 });
532
+ }, [map]);
533
+ const handleResetBearing = useCallback(() => {
534
+ map?.resetNorthPitch({ duration: 300 });
535
+ }, [map]);
536
+ const handleLocate = useCallback(() => {
537
+ if (!("geolocation" in navigator)) return;
538
+ setWaitingForLocation(true);
539
+ navigator.geolocation.getCurrentPosition((pos) => {
540
+ const coords = {
541
+ longitude: pos.coords.longitude,
542
+ latitude: pos.coords.latitude
543
+ };
544
+ map?.flyTo({
545
+ center: [coords.longitude, coords.latitude],
546
+ zoom: 14,
547
+ duration: 1500
548
+ });
549
+ onLocate?.(coords);
550
+ setWaitingForLocation(false);
551
+ }, (error) => {
552
+ console.error("Error getting location:", error);
553
+ setWaitingForLocation(false);
554
+ }, { timeout: 1e4 });
555
+ }, [map, onLocate]);
556
+ const handleFullscreen = useCallback(() => {
557
+ const container = map?.getContainer();
558
+ if (!container) return;
559
+ if (document.fullscreenElement) document.exitFullscreen();
560
+ else container.requestFullscreen();
561
+ }, [map]);
562
+ return /* @__PURE__ */ jsxs("div", {
563
+ className: cn("absolute z-10 flex flex-col gap-1.5", positionClasses[position], className),
564
+ children: [
565
+ showZoom && /* @__PURE__ */ jsxs(ControlGroup, { children: [/* @__PURE__ */ jsx(ControlButton, {
566
+ onClick: handleZoomIn,
567
+ label: "Zoom in",
568
+ children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
569
+ }), /* @__PURE__ */ jsx(ControlButton, {
570
+ onClick: handleZoomOut,
571
+ label: "Zoom out",
572
+ children: /* @__PURE__ */ jsx(Minus, { className: "size-4" })
573
+ })] }),
574
+ showCompass && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(CompassButton, { onClick: handleResetBearing }) }),
575
+ showLocate && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(ControlButton, {
576
+ onClick: handleLocate,
577
+ label: "Find my location",
578
+ disabled: waitingForLocation,
579
+ children: waitingForLocation ? /* @__PURE__ */ jsx(LoaderCircle, { className: "size-4 animate-spin" }) : /* @__PURE__ */ jsx(Locate, { className: "size-4" })
580
+ }) }),
581
+ showFullscreen && /* @__PURE__ */ jsx(ControlGroup, { children: /* @__PURE__ */ jsx(ControlButton, {
582
+ onClick: handleFullscreen,
583
+ label: "Toggle fullscreen",
584
+ children: /* @__PURE__ */ jsx(Maximize, { className: "size-4" })
585
+ }) })
586
+ ]
587
+ });
588
+ }
589
+ function CompassButton({ onClick }) {
590
+ const { map } = useMap();
591
+ const compassRef = useRef(null);
592
+ useEffect(() => {
593
+ if (!map || !compassRef.current) return;
594
+ const compass = compassRef.current;
595
+ const updateRotation = () => {
596
+ const bearing = map.getBearing();
597
+ const pitch = map.getPitch();
598
+ compass.style.transform = `rotateX(${pitch}deg) rotateZ(${-bearing}deg)`;
599
+ };
600
+ map.on("rotate", updateRotation);
601
+ map.on("pitch", updateRotation);
602
+ updateRotation();
603
+ return () => {
604
+ map.off("rotate", updateRotation);
605
+ map.off("pitch", updateRotation);
606
+ };
607
+ }, [map]);
608
+ return /* @__PURE__ */ jsx(ControlButton, {
609
+ onClick,
610
+ label: "Reset bearing to north",
611
+ children: /* @__PURE__ */ jsxs("svg", {
612
+ ref: compassRef,
613
+ viewBox: "0 0 24 24",
614
+ className: "size-5",
615
+ style: { transformStyle: "preserve-3d" },
616
+ children: [
617
+ /* @__PURE__ */ jsx("path", {
618
+ d: "M12 2L16 12H12V2Z",
619
+ className: "fill-red-500"
620
+ }),
621
+ /* @__PURE__ */ jsx("path", {
622
+ d: "M12 2L8 12H12V2Z",
623
+ className: "fill-red-300"
624
+ }),
625
+ /* @__PURE__ */ jsx("path", {
626
+ d: "M12 22L16 12H12V22Z",
627
+ className: "fill-muted-foreground/60"
628
+ }),
629
+ /* @__PURE__ */ jsx("path", {
630
+ d: "M12 22L8 12H12V22Z",
631
+ className: "fill-muted-foreground/30"
632
+ })
633
+ ]
634
+ })
635
+ });
636
+ }
637
+ function MapPopup({ longitude, latitude, onClose, children, className, closeButton = false, ...popupOptions }) {
638
+ const { map } = useMap();
639
+ const { Popup } = useMapLib();
640
+ const onCloseRef = useRef(onClose);
641
+ onCloseRef.current = onClose;
642
+ const container = useMemo(() => document.createElement("div"), []);
643
+ const { offset, maxWidth } = popupOptions;
644
+ const popup = useMemo(() => {
645
+ return new Popup({
646
+ offset: 16,
647
+ ...popupOptions,
648
+ closeButton: false
649
+ }).setMaxWidth("none").setLngLat([longitude, latitude]);
650
+ }, []);
651
+ useEffect(() => {
652
+ if (!map) return;
653
+ const onCloseProp = () => onCloseRef.current?.();
654
+ popup.on("close", onCloseProp);
655
+ popup.setDOMContent(container);
656
+ popup.addTo(map);
657
+ return () => {
658
+ popup.off("close", onCloseProp);
659
+ if (popup.isOpen()) popup.remove();
660
+ };
661
+ }, [map]);
662
+ useEffect(() => {
663
+ const current = popup.getLngLat();
664
+ if (!current || current.lng !== longitude || current.lat !== latitude) popup.setLngLat([longitude, latitude]);
665
+ popup.setOffset(offset ?? 16);
666
+ if (maxWidth) popup.setMaxWidth(maxWidth);
667
+ }, [
668
+ popup,
669
+ longitude,
670
+ latitude,
671
+ offset,
672
+ maxWidth
673
+ ]);
674
+ const handleClose = () => {
675
+ popup.remove();
676
+ };
677
+ return createPortal(/* @__PURE__ */ jsxs("div", {
678
+ className: cn("bg-popover text-popover-foreground relative max-w-62 rounded-md border p-3 shadow-md", "animate-in fade-in-0 zoom-in-95 duration-200 ease-out", className),
679
+ children: [closeButton && /* @__PURE__ */ jsx(PopupCloseButton, { onClick: handleClose }), children]
680
+ }), container);
681
+ }
682
+ const EMPTY_ROUTE_MEASURE = {
683
+ cumulative: [],
684
+ total: 0
685
+ };
686
+ const EMPTY_COORDINATES = [];
687
+ /**
688
+ * `beforeId`, but only when that layer is actually in the style. MapLibre's
689
+ * `addLayer` fires an error and returns *without adding the layer* when
690
+ * `before` is missing, which would silently drop the route — easy to hit with
691
+ * a `beforeId` that exists in one basemap but not another.
692
+ */
693
+ function resolveBeforeId(map, beforeId) {
694
+ return beforeId && map.getLayer(beforeId) ? beforeId : void 0;
695
+ }
696
+ function clampFraction(value) {
697
+ if (!Number.isFinite(value)) return 0;
698
+ return Math.min(1, Math.max(0, value));
699
+ }
700
+ /**
701
+ * Cumulative length along the route. Segment lengths use an equirectangular
702
+ * approximation (longitude scaled by cos(latitude)) — accurate enough for
703
+ * splitting a route at a fraction, and much cheaper than haversine.
704
+ */
705
+ function measureRoute(coordinates) {
706
+ if (coordinates.length < 2) return EMPTY_ROUTE_MEASURE;
707
+ const cumulative = [0];
708
+ let total = 0;
709
+ for (let i = 1; i < coordinates.length; i += 1) {
710
+ const [lng1, lat1] = coordinates[i - 1];
711
+ const [lng2, lat2] = coordinates[i];
712
+ const midLat = (lat1 + lat2) / 2 * (Math.PI / 180);
713
+ total += Math.hypot((lng2 - lng1) * Math.cos(midLat), lat2 - lat1);
714
+ cumulative.push(total);
715
+ }
716
+ return {
717
+ cumulative,
718
+ total
719
+ };
720
+ }
721
+ /** Index of the segment that contains `distance`, clamped to the route. */
722
+ function findSegmentIndex(cumulative, distance) {
723
+ let low = 0;
724
+ let high = cumulative.length - 1;
725
+ while (low < high) {
726
+ const mid = Math.floor((low + high) / 2);
727
+ if (cumulative[mid] < distance) low = mid + 1;
728
+ else high = mid;
729
+ }
730
+ return Math.min(low === 0 ? 0 : low - 1, cumulative.length - 2);
731
+ }
732
+ /** The [longitude, latitude] sitting `fraction` of the way along the route. */
733
+ function pointAtFraction(coordinates, measure, fraction) {
734
+ if (coordinates.length === 0) return null;
735
+ if (coordinates.length === 1 || measure.total === 0) return coordinates[0];
736
+ const target = measure.total * clampFraction(fraction);
737
+ const index = findSegmentIndex(measure.cumulative, target);
738
+ const [lng1, lat1] = coordinates[index];
739
+ const [lng2, lat2] = coordinates[index + 1];
740
+ const segment = measure.cumulative[index + 1] - measure.cumulative[index];
741
+ const ratio = segment === 0 ? 0 : (target - measure.cumulative[index]) / segment;
742
+ return [lng1 + (lng2 - lng1) * ratio, lat1 + (lat2 - lat1) * ratio];
743
+ }
744
+ /**
745
+ * The portion of the route up to `fraction`. The final point is interpolated,
746
+ * so the slice ends exactly at the fraction rather than at the nearest vertex.
747
+ */
748
+ function sliceAtFraction(coordinates, measure, fraction) {
749
+ if (coordinates.length < 2) return [];
750
+ const t = clampFraction(fraction);
751
+ if (t <= 0 || measure.total === 0) return [];
752
+ if (t >= 1) return coordinates;
753
+ const target = measure.total * t;
754
+ const index = findSegmentIndex(measure.cumulative, target);
755
+ const point = pointAtFraction(coordinates, measure, t);
756
+ const traveled = coordinates.slice(0, index + 1);
757
+ if (point) traveled.push(point);
758
+ return traveled;
759
+ }
760
+ const RouteContext = createContext(null);
761
+ function useMapRoute() {
762
+ const context = useContext(RouteContext);
763
+ if (!context) throw new Error("Route components must be used within MapRoute");
764
+ return context;
765
+ }
766
+ function MapRoute({ id: propId, coordinates: coordinatesProp, color = "#4285F4", width = 3, opacity = .8, dashArray, progress, active = false, activeColor, activeWidth, activeOpacity, activeDashArray, beforeId, onClick, onMouseEnter, onMouseLeave, interactive = true, children }) {
767
+ const { map, isLoaded } = useMap();
768
+ const autoId = useId();
769
+ const id = propId ?? autoId;
770
+ const sourceId = `route-source-${id}`;
771
+ const layerId = `route-layer-${id}`;
772
+ const [ready, setReady] = useState(false);
773
+ const coordinates = coordinatesProp.length > 0 ? coordinatesProp : EMPTY_COORDINATES;
774
+ const resolvedColor = active ? activeColor ?? color : color;
775
+ const resolvedWidth = active ? activeWidth ?? width : width;
776
+ const resolvedOpacity = active ? activeOpacity ?? opacity : opacity;
777
+ const resolvedDashArray = active ? activeDashArray ?? dashArray : dashArray;
778
+ const measure = useMemo(() => measureRoute(coordinates), [coordinates]);
779
+ const traveled = useMemo(() => progress === void 0 ? [] : sliceAtFraction(coordinates, measure, progress), [
780
+ coordinates,
781
+ measure,
782
+ progress
783
+ ]);
784
+ const pointAt = useCallback((at) => {
785
+ if (coordinates.length === 0) return null;
786
+ if (at === "start") return coordinates[0];
787
+ if (at === "end") return coordinates[coordinates.length - 1];
788
+ if (at === "progress") {
789
+ if (progress === void 0) return null;
790
+ return pointAtFraction(coordinates, measure, progress);
791
+ }
792
+ return pointAtFraction(coordinates, measure, at);
793
+ }, [
794
+ coordinates,
795
+ measure,
796
+ progress
797
+ ]);
798
+ const childLayersRef = useRef([]);
799
+ const registerLayer = useCallback((childLayerId) => {
800
+ childLayersRef.current = [...childLayersRef.current, childLayerId];
801
+ return () => {
802
+ childLayersRef.current = childLayersRef.current.filter((entry) => entry !== childLayerId);
803
+ };
804
+ }, []);
805
+ useEffect(() => {
806
+ if (!isLoaded || !map) return;
807
+ map.addSource(sourceId, {
808
+ type: "geojson",
809
+ data: {
810
+ type: "Feature",
811
+ properties: {},
812
+ geometry: {
813
+ type: "LineString",
814
+ coordinates: []
815
+ }
816
+ }
817
+ });
818
+ map.addLayer({
819
+ id: layerId,
820
+ type: "line",
821
+ source: sourceId,
822
+ layout: {
823
+ "line-join": "round",
824
+ "line-cap": "round"
825
+ },
826
+ paint: {
827
+ "line-color": resolvedColor,
828
+ "line-width": resolvedWidth,
829
+ "line-opacity": resolvedOpacity,
830
+ ...resolvedDashArray && { "line-dasharray": resolvedDashArray }
831
+ }
832
+ }, resolveBeforeId(map, beforeId));
833
+ setReady(true);
834
+ return () => {
835
+ setReady(false);
836
+ try {
837
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
838
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
839
+ } catch {}
840
+ };
841
+ }, [isLoaded, map]);
842
+ useEffect(() => {
843
+ if (!isLoaded || !map) return;
844
+ const source = map.getSource(sourceId);
845
+ if (source) source.setData({
846
+ type: "Feature",
847
+ properties: {},
848
+ geometry: {
849
+ type: "LineString",
850
+ coordinates: coordinates.length < 2 ? [] : coordinates
851
+ }
852
+ });
853
+ }, [
854
+ isLoaded,
855
+ map,
856
+ coordinates,
857
+ sourceId
858
+ ]);
859
+ useEffect(() => {
860
+ if (!isLoaded || !map || !map.getLayer(layerId)) return;
861
+ map.setPaintProperty(layerId, "line-color", resolvedColor);
862
+ map.setPaintProperty(layerId, "line-width", resolvedWidth);
863
+ map.setPaintProperty(layerId, "line-opacity", resolvedOpacity);
864
+ map.setPaintProperty(layerId, "line-dasharray", resolvedDashArray);
865
+ }, [
866
+ isLoaded,
867
+ map,
868
+ layerId,
869
+ resolvedColor,
870
+ resolvedWidth,
871
+ resolvedOpacity,
872
+ resolvedDashArray
873
+ ]);
874
+ useEffect(() => {
875
+ if (!ready || !map || !active) return;
876
+ let lastLayerSet = "";
877
+ const raise = () => {
878
+ const order = map.getLayersOrder();
879
+ const layerSet = [...order].sort().join("|");
880
+ if (layerSet === lastLayerSet) return;
881
+ lastLayerSet = layerSet;
882
+ const owned = [layerId, ...childLayersRef.current].filter((entry) => map.getLayer(entry));
883
+ if (owned.length === 0) return;
884
+ const before = resolveBeforeId(map, beforeId);
885
+ const limit = before ? order.indexOf(before) : order.length;
886
+ const top = order.slice(Math.max(0, limit - owned.length), limit);
887
+ if (owned.every((entry, index) => top[index] === entry)) return;
888
+ for (const entry of owned) map.moveLayer(entry, before);
889
+ };
890
+ raise();
891
+ map.on("styledata", raise);
892
+ return () => {
893
+ map.off("styledata", raise);
894
+ };
895
+ }, [
896
+ ready,
897
+ map,
898
+ active,
899
+ layerId,
900
+ beforeId
901
+ ]);
902
+ useEffect(() => {
903
+ if (!isLoaded || !map || !interactive) return;
904
+ const handleClick = () => {
905
+ onClick?.();
906
+ };
907
+ const handleMouseEnter = () => {
908
+ map.getCanvas().style.cursor = "pointer";
909
+ onMouseEnter?.();
910
+ };
911
+ const handleMouseLeave = () => {
912
+ map.getCanvas().style.cursor = "";
913
+ onMouseLeave?.();
914
+ };
915
+ map.on("click", layerId, handleClick);
916
+ map.on("mouseenter", layerId, handleMouseEnter);
917
+ map.on("mouseleave", layerId, handleMouseLeave);
918
+ return () => {
919
+ map.off("click", layerId, handleClick);
920
+ map.off("mouseenter", layerId, handleMouseEnter);
921
+ map.off("mouseleave", layerId, handleMouseLeave);
922
+ };
923
+ }, [
924
+ isLoaded,
925
+ map,
926
+ layerId,
927
+ onClick,
928
+ onMouseEnter,
929
+ onMouseLeave,
930
+ interactive
931
+ ]);
932
+ const contextValue = useMemo(() => ({
933
+ id,
934
+ ready,
935
+ coordinates,
936
+ traveled,
937
+ progress,
938
+ color: resolvedColor,
939
+ width: resolvedWidth,
940
+ opacity: resolvedOpacity,
941
+ dashArray: resolvedDashArray,
942
+ beforeId,
943
+ pointAt,
944
+ registerLayer
945
+ }), [
946
+ id,
947
+ ready,
948
+ coordinates,
949
+ traveled,
950
+ progress,
951
+ resolvedColor,
952
+ resolvedWidth,
953
+ resolvedOpacity,
954
+ resolvedDashArray,
955
+ beforeId,
956
+ pointAt,
957
+ registerLayer
958
+ ]);
959
+ return /* @__PURE__ */ jsx(RouteContext.Provider, {
960
+ value: contextValue,
961
+ children
962
+ });
963
+ }
964
+ /**
965
+ * Draws the traveled portion of the parent `MapRoute` on top of the base line.
966
+ * Renders nothing until the route has a `progress` value.
967
+ */
968
+ function RouteProgress({ color, width, opacity, dashArray }) {
969
+ const { map, isLoaded } = useMap();
970
+ const route = useMapRoute();
971
+ const { ready, traveled, registerLayer, beforeId } = route;
972
+ const sourceId = `route-progress-source-${route.id}`;
973
+ const layerId = `route-progress-layer-${route.id}`;
974
+ const resolvedColor = color ?? route.color;
975
+ const resolvedWidth = width ?? route.width;
976
+ const resolvedOpacity = opacity ?? route.opacity;
977
+ useEffect(() => {
978
+ if (!ready || !map) return;
979
+ map.addSource(sourceId, {
980
+ type: "geojson",
981
+ data: {
982
+ type: "Feature",
983
+ properties: {},
984
+ geometry: {
985
+ type: "LineString",
986
+ coordinates: []
987
+ }
988
+ }
989
+ });
990
+ map.addLayer({
991
+ id: layerId,
992
+ type: "line",
993
+ source: sourceId,
994
+ layout: {
995
+ "line-join": "round",
996
+ "line-cap": "round"
997
+ },
998
+ paint: {
999
+ "line-color": resolvedColor,
1000
+ "line-width": resolvedWidth,
1001
+ "line-opacity": resolvedOpacity,
1002
+ ...dashArray && { "line-dasharray": dashArray }
1003
+ }
1004
+ }, resolveBeforeId(map, beforeId));
1005
+ const unregister = registerLayer(layerId);
1006
+ return () => {
1007
+ unregister();
1008
+ try {
1009
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
1010
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
1011
+ } catch {}
1012
+ };
1013
+ }, [ready, map]);
1014
+ useEffect(() => {
1015
+ if (!ready || !map) return;
1016
+ const source = map.getSource(sourceId);
1017
+ if (source) source.setData({
1018
+ type: "Feature",
1019
+ properties: {},
1020
+ geometry: {
1021
+ type: "LineString",
1022
+ coordinates: traveled.length < 2 ? [] : traveled
1023
+ }
1024
+ });
1025
+ }, [
1026
+ ready,
1027
+ map,
1028
+ traveled,
1029
+ sourceId
1030
+ ]);
1031
+ useEffect(() => {
1032
+ if (!isLoaded || !map || !map.getLayer(layerId)) return;
1033
+ map.setPaintProperty(layerId, "line-color", resolvedColor);
1034
+ map.setPaintProperty(layerId, "line-width", resolvedWidth);
1035
+ map.setPaintProperty(layerId, "line-opacity", resolvedOpacity);
1036
+ map.setPaintProperty(layerId, "line-dasharray", dashArray);
1037
+ }, [
1038
+ isLoaded,
1039
+ map,
1040
+ layerId,
1041
+ resolvedColor,
1042
+ resolvedWidth,
1043
+ resolvedOpacity,
1044
+ dashArray
1045
+ ]);
1046
+ return null;
1047
+ }
1048
+ /**
1049
+ * A `MapMarker` anchored to a position along the parent `MapRoute`. Takes the
1050
+ * same children as `MapMarker` (`MarkerContent`, `MarkerPopup`, ...).
1051
+ */
1052
+ function RouteMarker({ at, children, ...markerProps }) {
1053
+ const { pointAt } = useMapRoute();
1054
+ const position = pointAt(at);
1055
+ if (!position) return null;
1056
+ return /* @__PURE__ */ jsx(MapMarker, {
1057
+ longitude: position[0],
1058
+ latitude: position[1],
1059
+ ...markerProps,
1060
+ children
1061
+ });
1062
+ }
1063
+ const GEOJSON_DEFAULT_COLORS = {
1064
+ light: {
1065
+ fill: "#d4d4d4",
1066
+ line: "#ffffff"
1067
+ },
1068
+ dark: {
1069
+ fill: "#404040",
1070
+ line: "#171717"
1071
+ }
1072
+ };
1073
+ /**
1074
+ * Renders arbitrary GeoJSON as fill + outline layers on the map. Composes like
1075
+ * `MapRoute` / `MapArc` — drop it inside `<Map>` (typically with `blank`) for
1076
+ * choropleths and region/data maps. For full control over expressions and
1077
+ * multiple layers, manage layers directly via `useMap()` instead.
1078
+ */
1079
+ function MapGeoJSON({ data, id: propId, promoteId, fillPaint, linePaint, fillHoverPaint, onClick, onHover, interactive = false, beforeId }) {
1080
+ const { map, isLoaded, resolvedTheme } = useMap();
1081
+ const autoId = useId();
1082
+ const id = propId ?? autoId;
1083
+ const sourceId = `geojson-source-${id}`;
1084
+ const fillLayerId = `geojson-fill-${id}`;
1085
+ const lineLayerId = `geojson-line-${id}`;
1086
+ const defaults = GEOJSON_DEFAULT_COLORS[resolvedTheme];
1087
+ const showFill = fillPaint !== false;
1088
+ const showLine = linePaint !== false;
1089
+ const mergedFillPaint = useMemo(() => mergeHoverPaint({
1090
+ "fill-color": defaults.fill,
1091
+ ...fillPaint
1092
+ }, fillHoverPaint), [
1093
+ defaults.fill,
1094
+ fillPaint,
1095
+ fillHoverPaint
1096
+ ]);
1097
+ const mergedLinePaint = useMemo(() => ({
1098
+ "line-color": defaults.line,
1099
+ "line-width": .5,
1100
+ ...linePaint
1101
+ }), [defaults.line, linePaint]);
1102
+ const latestRef = useRef({
1103
+ onClick,
1104
+ onHover
1105
+ });
1106
+ latestRef.current = {
1107
+ onClick,
1108
+ onHover
1109
+ };
1110
+ useEffect(() => {
1111
+ if (!isLoaded || !map) return;
1112
+ map.addSource(sourceId, {
1113
+ type: "geojson",
1114
+ data,
1115
+ ...promoteId ? { promoteId } : {}
1116
+ });
1117
+ return () => {
1118
+ try {
1119
+ if (map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);
1120
+ if (map.getLayer(fillLayerId)) map.removeLayer(fillLayerId);
1121
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
1122
+ } catch {}
1123
+ };
1124
+ }, [isLoaded, map]);
1125
+ useEffect(() => {
1126
+ if (!isLoaded || !map) return;
1127
+ map.getSource(sourceId)?.setData(data);
1128
+ }, [
1129
+ isLoaded,
1130
+ map,
1131
+ data,
1132
+ sourceId
1133
+ ]);
1134
+ useEffect(() => {
1135
+ if (!isLoaded || !map) return;
1136
+ if (!map.getSource(sourceId)) return;
1137
+ if (showFill && !map.getLayer(fillLayerId)) map.addLayer({
1138
+ id: fillLayerId,
1139
+ type: "fill",
1140
+ source: sourceId,
1141
+ paint: mergedFillPaint
1142
+ }, beforeId);
1143
+ else if (!showFill && map.getLayer(fillLayerId)) map.removeLayer(fillLayerId);
1144
+ if (showLine && !map.getLayer(lineLayerId)) map.addLayer({
1145
+ id: lineLayerId,
1146
+ type: "line",
1147
+ source: sourceId,
1148
+ paint: mergedLinePaint
1149
+ }, beforeId);
1150
+ else if (!showLine && map.getLayer(lineLayerId)) map.removeLayer(lineLayerId);
1151
+ if (showFill && map.getLayer(fillLayerId)) for (const [key, value] of Object.entries(mergedFillPaint)) map.setPaintProperty(fillLayerId, key, value);
1152
+ if (showLine && map.getLayer(lineLayerId)) for (const [key, value] of Object.entries(mergedLinePaint)) map.setPaintProperty(lineLayerId, key, value);
1153
+ }, [
1154
+ isLoaded,
1155
+ map,
1156
+ sourceId,
1157
+ fillLayerId,
1158
+ lineLayerId,
1159
+ showFill,
1160
+ showLine,
1161
+ mergedFillPaint,
1162
+ mergedLinePaint,
1163
+ beforeId
1164
+ ]);
1165
+ useEffect(() => {
1166
+ if (!isLoaded || !map || !interactive || !showFill) return;
1167
+ let hoveredId = null;
1168
+ const setHover = (next) => {
1169
+ if (next === hoveredId) return;
1170
+ const sourceExists = !!map.getSource(sourceId);
1171
+ if (hoveredId != null && sourceExists) map.setFeatureState({
1172
+ source: sourceId,
1173
+ id: hoveredId
1174
+ }, { hover: false });
1175
+ hoveredId = next;
1176
+ if (next != null && sourceExists) map.setFeatureState({
1177
+ source: sourceId,
1178
+ id: next
1179
+ }, { hover: true });
1180
+ };
1181
+ const handleMouseMove = (e) => {
1182
+ const feature = e.features?.[0];
1183
+ if (!feature) return;
1184
+ map.getCanvas().style.cursor = "pointer";
1185
+ const featureId = feature.id;
1186
+ if (featureId === hoveredId) return;
1187
+ setHover(featureId ?? null);
1188
+ latestRef.current.onHover?.({
1189
+ feature,
1190
+ longitude: e.lngLat.lng,
1191
+ latitude: e.lngLat.lat,
1192
+ originalEvent: e
1193
+ });
1194
+ };
1195
+ const handleMouseLeave = () => {
1196
+ setHover(null);
1197
+ map.getCanvas().style.cursor = "";
1198
+ latestRef.current.onHover?.(null);
1199
+ };
1200
+ const handleClick = (e) => {
1201
+ const feature = e.features?.[0];
1202
+ if (!feature) return;
1203
+ latestRef.current.onClick?.({
1204
+ feature,
1205
+ longitude: e.lngLat.lng,
1206
+ latitude: e.lngLat.lat,
1207
+ originalEvent: e
1208
+ });
1209
+ };
1210
+ map.on("mousemove", fillLayerId, handleMouseMove);
1211
+ map.on("mouseleave", fillLayerId, handleMouseLeave);
1212
+ map.on("click", fillLayerId, handleClick);
1213
+ return () => {
1214
+ map.off("mousemove", fillLayerId, handleMouseMove);
1215
+ map.off("mouseleave", fillLayerId, handleMouseLeave);
1216
+ map.off("click", fillLayerId, handleClick);
1217
+ setHover(null);
1218
+ map.getCanvas().style.cursor = "";
1219
+ };
1220
+ }, [
1221
+ isLoaded,
1222
+ map,
1223
+ fillLayerId,
1224
+ sourceId,
1225
+ interactive,
1226
+ showFill
1227
+ ]);
1228
+ return null;
1229
+ }
1230
+ const DEFAULT_ARC_CURVATURE = .2;
1231
+ const DEFAULT_ARC_SAMPLES = 64;
1232
+ const ARC_HIT_MIN_WIDTH = 12;
1233
+ const ARC_HIT_PADDING = 6;
1234
+ const DEFAULT_ARC_PAINT = {
1235
+ "line-color": "#4285F4",
1236
+ "line-width": 2,
1237
+ "line-opacity": .85
1238
+ };
1239
+ const DEFAULT_ARC_LAYOUT = {
1240
+ "line-join": "round",
1241
+ "line-cap": "round"
1242
+ };
1243
+ function buildArcCoordinates(from, to, curvature, samples) {
1244
+ const [x0, y0] = from;
1245
+ const [xTo, y2] = to;
1246
+ const rawDx = xTo - x0;
1247
+ const x2 = rawDx > 180 ? xTo - 360 : rawDx < -180 ? xTo + 360 : xTo;
1248
+ const dx = x2 - x0;
1249
+ const dy = y2 - y0;
1250
+ const distance = Math.hypot(dx, dy);
1251
+ if (distance === 0 || curvature === 0) return [from, [x2, y2]];
1252
+ const mx = (x0 + x2) / 2;
1253
+ const my = (y0 + y2) / 2;
1254
+ const nx = -dy / distance;
1255
+ const ny = dx / distance;
1256
+ const offset = distance * curvature;
1257
+ const cx = mx + nx * offset;
1258
+ const cy = my + ny * offset;
1259
+ const points = [];
1260
+ const segments = Math.max(2, Math.floor(samples));
1261
+ for (let i = 0; i <= segments; i += 1) {
1262
+ const t = i / segments;
1263
+ const inv = 1 - t;
1264
+ const x = inv * inv * x0 + 2 * inv * t * cx + t * t * x2;
1265
+ const y = inv * inv * y0 + 2 * inv * t * cy + t * t * y2;
1266
+ points.push([x, y]);
1267
+ }
1268
+ return points;
1269
+ }
1270
+ function MapArc({ data, id: propId, curvature = DEFAULT_ARC_CURVATURE, samples = DEFAULT_ARC_SAMPLES, paint, layout, hoverPaint, onClick, onHover, interactive = true, beforeId }) {
1271
+ const { map, isLoaded } = useMap();
1272
+ const autoId = useId();
1273
+ const id = propId ?? autoId;
1274
+ const sourceId = `arc-source-${id}`;
1275
+ const layerId = `arc-layer-${id}`;
1276
+ const hitLayerId = `arc-hit-layer-${id}`;
1277
+ const mergedPaint = useMemo(() => mergeHoverPaint({
1278
+ ...DEFAULT_ARC_PAINT,
1279
+ ...paint
1280
+ }, hoverPaint), [paint, hoverPaint]);
1281
+ const mergedLayout = useMemo(() => ({
1282
+ ...DEFAULT_ARC_LAYOUT,
1283
+ ...layout
1284
+ }), [layout]);
1285
+ const hitWidth = useMemo(() => {
1286
+ const w = paint?.["line-width"] ?? DEFAULT_ARC_PAINT["line-width"];
1287
+ return Math.max((typeof w === "number" ? w : ARC_HIT_MIN_WIDTH) + ARC_HIT_PADDING, ARC_HIT_MIN_WIDTH);
1288
+ }, [paint]);
1289
+ const geoJSON = useMemo(() => ({
1290
+ type: "FeatureCollection",
1291
+ features: data.map((arc) => {
1292
+ const { from, to, ...properties } = arc;
1293
+ return {
1294
+ type: "Feature",
1295
+ properties,
1296
+ geometry: {
1297
+ type: "LineString",
1298
+ coordinates: buildArcCoordinates(from, to, curvature, samples)
1299
+ }
1300
+ };
1301
+ })
1302
+ }), [
1303
+ data,
1304
+ curvature,
1305
+ samples
1306
+ ]);
1307
+ const latestRef = useRef({
1308
+ data,
1309
+ onClick,
1310
+ onHover
1311
+ });
1312
+ latestRef.current = {
1313
+ data,
1314
+ onClick,
1315
+ onHover
1316
+ };
1317
+ useEffect(() => {
1318
+ if (!isLoaded || !map) return;
1319
+ map.addSource(sourceId, {
1320
+ type: "geojson",
1321
+ data: geoJSON,
1322
+ promoteId: "id"
1323
+ });
1324
+ map.addLayer({
1325
+ id: hitLayerId,
1326
+ type: "line",
1327
+ source: sourceId,
1328
+ layout: DEFAULT_ARC_LAYOUT,
1329
+ paint: {
1330
+ "line-color": "rgba(0, 0, 0, 0)",
1331
+ "line-width": hitWidth,
1332
+ "line-opacity": 1
1333
+ }
1334
+ }, beforeId);
1335
+ map.addLayer({
1336
+ id: layerId,
1337
+ type: "line",
1338
+ source: sourceId,
1339
+ layout: mergedLayout,
1340
+ paint: mergedPaint
1341
+ }, beforeId);
1342
+ return () => {
1343
+ try {
1344
+ if (map.getLayer(layerId)) map.removeLayer(layerId);
1345
+ if (map.getLayer(hitLayerId)) map.removeLayer(hitLayerId);
1346
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
1347
+ } catch {}
1348
+ };
1349
+ }, [isLoaded, map]);
1350
+ useEffect(() => {
1351
+ if (!isLoaded || !map) return;
1352
+ map.getSource(sourceId)?.setData(geoJSON);
1353
+ }, [
1354
+ isLoaded,
1355
+ map,
1356
+ geoJSON,
1357
+ sourceId
1358
+ ]);
1359
+ useEffect(() => {
1360
+ if (!isLoaded || !map || !map.getLayer(layerId)) return;
1361
+ for (const [key, value] of Object.entries(mergedPaint)) map.setPaintProperty(layerId, key, value);
1362
+ for (const [key, value] of Object.entries(mergedLayout)) map.setLayoutProperty(layerId, key, value);
1363
+ if (map.getLayer(hitLayerId)) map.setPaintProperty(hitLayerId, "line-width", hitWidth);
1364
+ }, [
1365
+ isLoaded,
1366
+ map,
1367
+ layerId,
1368
+ hitLayerId,
1369
+ mergedPaint,
1370
+ mergedLayout,
1371
+ hitWidth
1372
+ ]);
1373
+ useEffect(() => {
1374
+ if (!isLoaded || !map || !interactive) return;
1375
+ let hoveredId = null;
1376
+ const setHover = (next) => {
1377
+ if (next === hoveredId) return;
1378
+ const sourceExists = !!map.getSource(sourceId);
1379
+ if (hoveredId != null && sourceExists) map.setFeatureState({
1380
+ source: sourceId,
1381
+ id: hoveredId
1382
+ }, { hover: false });
1383
+ hoveredId = next;
1384
+ if (next != null && sourceExists) map.setFeatureState({
1385
+ source: sourceId,
1386
+ id: next
1387
+ }, { hover: true });
1388
+ };
1389
+ const findArc = (featureId) => featureId == null ? void 0 : latestRef.current.data.find((arc) => String(arc.id) === String(featureId));
1390
+ const handleMouseMove = (e) => {
1391
+ const featureId = e.features?.[0]?.id;
1392
+ if (featureId == null || featureId === hoveredId) return;
1393
+ setHover(featureId);
1394
+ map.getCanvas().style.cursor = "pointer";
1395
+ const arc = findArc(featureId);
1396
+ if (arc) latestRef.current.onHover?.({
1397
+ arc,
1398
+ longitude: e.lngLat.lng,
1399
+ latitude: e.lngLat.lat,
1400
+ originalEvent: e
1401
+ });
1402
+ };
1403
+ const handleMouseLeave = () => {
1404
+ setHover(null);
1405
+ map.getCanvas().style.cursor = "";
1406
+ latestRef.current.onHover?.(null);
1407
+ };
1408
+ const handleClick = (e) => {
1409
+ const arc = findArc(e.features?.[0]?.id);
1410
+ if (!arc) return;
1411
+ latestRef.current.onClick?.({
1412
+ arc,
1413
+ longitude: e.lngLat.lng,
1414
+ latitude: e.lngLat.lat,
1415
+ originalEvent: e
1416
+ });
1417
+ };
1418
+ map.on("mousemove", hitLayerId, handleMouseMove);
1419
+ map.on("mouseleave", hitLayerId, handleMouseLeave);
1420
+ map.on("click", hitLayerId, handleClick);
1421
+ return () => {
1422
+ map.off("mousemove", hitLayerId, handleMouseMove);
1423
+ map.off("mouseleave", hitLayerId, handleMouseLeave);
1424
+ map.off("click", hitLayerId, handleClick);
1425
+ setHover(null);
1426
+ map.getCanvas().style.cursor = "";
1427
+ };
1428
+ }, [
1429
+ isLoaded,
1430
+ map,
1431
+ hitLayerId,
1432
+ sourceId,
1433
+ interactive
1434
+ ]);
1435
+ return null;
1436
+ }
1437
+ const DEFAULT_CLUSTER_COLORS = [
1438
+ "#3b82f6",
1439
+ "#1d4ed8",
1440
+ "#1e3a8a"
1441
+ ];
1442
+ const DEFAULT_CLUSTER_THRESHOLDS = [100, 750];
1443
+ function MapClusterLayer({ data, clusterMaxZoom = 14, clusterRadius = 50, clusterColors = DEFAULT_CLUSTER_COLORS, clusterThresholds = DEFAULT_CLUSTER_THRESHOLDS, pointColor = "#3b82f6", onPointClick, onClusterClick }) {
1444
+ const { map, isLoaded } = useMap();
1445
+ const id = useId();
1446
+ const sourceId = `cluster-source-${id}`;
1447
+ const clusterLayerId = `clusters-${id}`;
1448
+ const clusterCountLayerId = `cluster-count-${id}`;
1449
+ const unclusteredLayerId = `unclustered-point-${id}`;
1450
+ const stylePropsRef = useRef({
1451
+ clusterColors,
1452
+ clusterThresholds,
1453
+ pointColor
1454
+ });
1455
+ useEffect(() => {
1456
+ if (!isLoaded || !map) return;
1457
+ map.addSource(sourceId, {
1458
+ type: "geojson",
1459
+ data,
1460
+ cluster: true,
1461
+ clusterMaxZoom,
1462
+ clusterRadius
1463
+ });
1464
+ map.addLayer({
1465
+ id: clusterLayerId,
1466
+ type: "circle",
1467
+ source: sourceId,
1468
+ filter: ["has", "point_count"],
1469
+ paint: {
1470
+ "circle-color": [
1471
+ "step",
1472
+ ["get", "point_count"],
1473
+ clusterColors[0],
1474
+ clusterThresholds[0],
1475
+ clusterColors[1],
1476
+ clusterThresholds[1],
1477
+ clusterColors[2]
1478
+ ],
1479
+ "circle-radius": [
1480
+ "step",
1481
+ ["get", "point_count"],
1482
+ 20,
1483
+ clusterThresholds[0],
1484
+ 30,
1485
+ clusterThresholds[1],
1486
+ 40
1487
+ ],
1488
+ "circle-stroke-width": .75,
1489
+ "circle-stroke-color": "#fff",
1490
+ "circle-opacity": .85
1491
+ }
1492
+ });
1493
+ map.addLayer({
1494
+ id: clusterCountLayerId,
1495
+ type: "symbol",
1496
+ source: sourceId,
1497
+ filter: ["has", "point_count"],
1498
+ layout: {
1499
+ "text-field": "{point_count_abbreviated}",
1500
+ "text-font": ["Open Sans Semibold"],
1501
+ "text-size": 12
1502
+ },
1503
+ paint: { "text-color": "#fff" }
1504
+ });
1505
+ map.addLayer({
1506
+ id: unclusteredLayerId,
1507
+ type: "circle",
1508
+ source: sourceId,
1509
+ filter: ["!", ["has", "point_count"]],
1510
+ paint: {
1511
+ "circle-color": pointColor,
1512
+ "circle-radius": 5,
1513
+ "circle-stroke-width": 2,
1514
+ "circle-stroke-color": "#fff"
1515
+ }
1516
+ });
1517
+ return () => {
1518
+ try {
1519
+ if (map.getLayer(clusterCountLayerId)) map.removeLayer(clusterCountLayerId);
1520
+ if (map.getLayer(unclusteredLayerId)) map.removeLayer(unclusteredLayerId);
1521
+ if (map.getLayer(clusterLayerId)) map.removeLayer(clusterLayerId);
1522
+ if (map.getSource(sourceId)) map.removeSource(sourceId);
1523
+ } catch {}
1524
+ };
1525
+ }, [
1526
+ isLoaded,
1527
+ map,
1528
+ sourceId
1529
+ ]);
1530
+ useEffect(() => {
1531
+ if (!isLoaded || !map || typeof data === "string") return;
1532
+ const source = map.getSource(sourceId);
1533
+ if (source) source.setData(data);
1534
+ }, [
1535
+ isLoaded,
1536
+ map,
1537
+ data,
1538
+ sourceId
1539
+ ]);
1540
+ useEffect(() => {
1541
+ if (!isLoaded || !map) return;
1542
+ const prev = stylePropsRef.current;
1543
+ const colorsChanged = prev.clusterColors !== clusterColors || prev.clusterThresholds !== clusterThresholds;
1544
+ if (map.getLayer(clusterLayerId) && colorsChanged) {
1545
+ map.setPaintProperty(clusterLayerId, "circle-color", [
1546
+ "step",
1547
+ ["get", "point_count"],
1548
+ clusterColors[0],
1549
+ clusterThresholds[0],
1550
+ clusterColors[1],
1551
+ clusterThresholds[1],
1552
+ clusterColors[2]
1553
+ ]);
1554
+ map.setPaintProperty(clusterLayerId, "circle-radius", [
1555
+ "step",
1556
+ ["get", "point_count"],
1557
+ 20,
1558
+ clusterThresholds[0],
1559
+ 30,
1560
+ clusterThresholds[1],
1561
+ 40
1562
+ ]);
1563
+ }
1564
+ if (map.getLayer(unclusteredLayerId) && prev.pointColor !== pointColor) map.setPaintProperty(unclusteredLayerId, "circle-color", pointColor);
1565
+ stylePropsRef.current = {
1566
+ clusterColors,
1567
+ clusterThresholds,
1568
+ pointColor
1569
+ };
1570
+ }, [
1571
+ isLoaded,
1572
+ map,
1573
+ clusterLayerId,
1574
+ unclusteredLayerId,
1575
+ clusterColors,
1576
+ clusterThresholds,
1577
+ pointColor
1578
+ ]);
1579
+ useEffect(() => {
1580
+ if (!isLoaded || !map) return;
1581
+ const handleClusterClick = async (e) => {
1582
+ const features = map.queryRenderedFeatures(e.point, { layers: [clusterLayerId] });
1583
+ if (!features.length) return;
1584
+ const feature = features[0];
1585
+ const clusterId = feature.properties?.cluster_id;
1586
+ const pointCount = feature.properties?.point_count;
1587
+ const coordinates = feature.geometry.coordinates;
1588
+ if (onClusterClick) onClusterClick(clusterId, coordinates, pointCount);
1589
+ else {
1590
+ const zoom = await map.getSource(sourceId).getClusterExpansionZoom(clusterId);
1591
+ map.easeTo({
1592
+ center: coordinates,
1593
+ zoom
1594
+ });
1595
+ }
1596
+ };
1597
+ const handlePointClick = (e) => {
1598
+ if (!onPointClick || !e.features?.length) return;
1599
+ const feature = e.features[0];
1600
+ const coordinates = feature.geometry.coordinates.slice();
1601
+ while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
1602
+ onPointClick(feature, coordinates);
1603
+ };
1604
+ const handleMouseEnterCluster = () => {
1605
+ map.getCanvas().style.cursor = "pointer";
1606
+ };
1607
+ const handleMouseLeaveCluster = () => {
1608
+ map.getCanvas().style.cursor = "";
1609
+ };
1610
+ const handleMouseEnterPoint = () => {
1611
+ if (onPointClick) map.getCanvas().style.cursor = "pointer";
1612
+ };
1613
+ const handleMouseLeavePoint = () => {
1614
+ map.getCanvas().style.cursor = "";
1615
+ };
1616
+ map.on("click", clusterLayerId, handleClusterClick);
1617
+ map.on("click", unclusteredLayerId, handlePointClick);
1618
+ map.on("mouseenter", clusterLayerId, handleMouseEnterCluster);
1619
+ map.on("mouseleave", clusterLayerId, handleMouseLeaveCluster);
1620
+ map.on("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
1621
+ map.on("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
1622
+ return () => {
1623
+ map.off("click", clusterLayerId, handleClusterClick);
1624
+ map.off("click", unclusteredLayerId, handlePointClick);
1625
+ map.off("mouseenter", clusterLayerId, handleMouseEnterCluster);
1626
+ map.off("mouseleave", clusterLayerId, handleMouseLeaveCluster);
1627
+ map.off("mouseenter", unclusteredLayerId, handleMouseEnterPoint);
1628
+ map.off("mouseleave", unclusteredLayerId, handleMouseLeavePoint);
1629
+ };
1630
+ }, [
1631
+ isLoaded,
1632
+ map,
1633
+ clusterLayerId,
1634
+ unclusteredLayerId,
1635
+ sourceId,
1636
+ onClusterClick,
1637
+ onPointClick
1638
+ ]);
1639
+ return null;
1640
+ }
1641
+ export { Map, MapArc, MapClusterLayer, MapControls, MapGeoJSON, MapMarker, MapPopup, MapRoute, MarkerContent, MarkerLabel, MarkerPopup, MarkerTooltip, RouteMarker, RouteProgress, useMap };