@uiness/map 0.0.0-stage → 0.1.0

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.mjs ADDED
@@ -0,0 +1,724 @@
1
+ 'use client';
2
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/engine.ts
5
+ const scripts = {};
6
+ /**
7
+ * Loads a script once per `src`, resolving when it calls back the global named `callback`.
8
+ * Both Google and Apple announce they are ready that way.
9
+ */
10
+ function loadScript(src, callback, attributes = {}) {
11
+ scripts[src] ??= new Promise((resolve, reject) => {
12
+ const script = document.createElement("script");
13
+ window[callback] = () => resolve();
14
+ script.src = src;
15
+ script.async = true;
16
+ for (const [name, value] of Object.entries(attributes)) script.setAttribute(name, value);
17
+ script.onerror = () => {
18
+ delete scripts[src];
19
+ script.remove();
20
+ reject(/* @__PURE__ */ new Error(`Could not load ${src}`));
21
+ };
22
+ document.head.append(script);
23
+ });
24
+ return scripts[src];
25
+ }
26
+ //#endregion
27
+ //#region src/projection.ts
28
+ const TILE_SIZE = 256;
29
+ /** The latitude where Web Mercator turns the world into a square. */
30
+ const MAX_LAT = 85.0511287798;
31
+ const clamp = (n, min, max) => Math.min(max, Math.max(min, n));
32
+ const worldSize = (zoom) => 256 * 2 ** zoom;
33
+ /** Longitude into [-180, 180), latitude into what Web Mercator can draw. */
34
+ function normalize([lng, lat]) {
35
+ return [((lng + 180) % 360 + 360) % 360 - 180, clamp(lat, -85.0511287798, MAX_LAT)];
36
+ }
37
+ /** A coordinate to pixels of the whole world at `zoom`, from its top left corner. */
38
+ function lngLatToWorld([lng, lat], zoom) {
39
+ const size = worldSize(zoom);
40
+ const sin = Math.sin(clamp(lat, -85.0511287798, MAX_LAT) * Math.PI / 180);
41
+ return [(lng + 180) / 360 * size, (.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * size];
42
+ }
43
+ /** World pixels at `zoom` back to a coordinate. */
44
+ function worldToLngLat([x, y], zoom) {
45
+ const size = worldSize(zoom);
46
+ const n = Math.PI - 2 * Math.PI * y / size;
47
+ return [x / size * 360 - 180, 180 / Math.PI * Math.atan(Math.sinh(n))];
48
+ }
49
+ /** A coordinate to a point of a map of `size` showing `view`, on the copy of the world nearest the center. */
50
+ function project(lngLat, view, size) {
51
+ const [cx, cy] = lngLatToWorld(view.center, view.zoom);
52
+ const [x, y] = lngLatToWorld(lngLat, view.zoom);
53
+ const world = worldSize(view.zoom);
54
+ return [x - cx - world * Math.round((x - cx) / world) + size[0] / 2, y - cy + size[1] / 2];
55
+ }
56
+ /** A point of the map back to a coordinate. */
57
+ function unproject(point, view, size) {
58
+ const [cx, cy] = lngLatToWorld(view.center, view.zoom);
59
+ return normalize(worldToLngLat([cx + point[0] - size[0] / 2, cy + point[1] - size[1] / 2], view.zoom));
60
+ }
61
+ /** The view moved by `delta` pixels, the way a drag moves it. */
62
+ function pan(view, delta, size) {
63
+ return {
64
+ center: unproject([size[0] / 2 - delta[0], size[1] / 2 - delta[1]], view, size),
65
+ zoom: view.zoom
66
+ };
67
+ }
68
+ /** The view at `zoom` that keeps the coordinate under `point` where it is. */
69
+ function zoomAround(view, zoom, point, size) {
70
+ const [x, y] = lngLatToWorld(unproject(point, view, size), zoom);
71
+ return {
72
+ center: normalize(worldToLngLat([x - point[0] + size[0] / 2, y - point[1] + size[1] / 2], zoom)),
73
+ zoom
74
+ };
75
+ }
76
+ /** The tiles of zoom level `z` that cover a map of `size` showing `view`, scaled to its fractional zoom. */
77
+ function visibleTiles(view, size, z) {
78
+ const scale = 2 ** (view.zoom - z);
79
+ const [cx, cy] = lngLatToWorld(view.center, z);
80
+ const count = 2 ** z;
81
+ const left = (x) => Math.round((x * 256 - cx) * scale + size[0] / 2);
82
+ const top = (y) => Math.round((y * 256 - cy) * scale + size[1] / 2);
83
+ const span = (px, c) => [Math.floor((c - px / 2 / scale) / 256), Math.floor((c + px / 2 / scale) / 256)];
84
+ const [x0, x1] = span(size[0], cx);
85
+ const [y0, y1] = span(size[1], cy);
86
+ const tiles = [];
87
+ for (let y = Math.max(0, y0); y <= Math.min(count - 1, y1); y++) for (let x = x0; x <= x1; x++) tiles.push({
88
+ x: (x % count + count) % count,
89
+ y,
90
+ z,
91
+ left: left(x),
92
+ top: top(y),
93
+ width: left(x + 1) - left(x),
94
+ height: top(y + 1) - top(y),
95
+ key: `${z}/${x}/${y}`
96
+ });
97
+ return tiles;
98
+ }
99
+ //#endregion
100
+ //#region src/apple.ts
101
+ /**
102
+ * Draws the map with Apple Maps (MapKit JS), loading it on first use.
103
+ * Create it once, outside the component, so the map is not mounted again on every render.
104
+ */
105
+ function appleMaps({ token, options }) {
106
+ return async (container, view, onMove) => {
107
+ await loadScript("https://cdn.apple-mapkit.com/mk/5.x.x/mapkit.core.js", "__uinessMapKit", {
108
+ crossorigin: "anonymous",
109
+ "data-callback": "__uinessMapKit",
110
+ "data-libraries": "map",
111
+ "data-initial-token": token
112
+ });
113
+ const mapkit = window.mapkit;
114
+ const map = new mapkit.Map(container, options);
115
+ const span = (zoom) => 360 * container.clientWidth / (256 * 2 ** zoom);
116
+ const current = () => {
117
+ const { center, span: s } = map.region;
118
+ return {
119
+ center: [center.longitude, center.latitude],
120
+ zoom: Math.log2(360 * container.clientWidth / (256 * s.longitudeDelta))
121
+ };
122
+ };
123
+ const setView = (next) => {
124
+ const [lng, lat] = next.center;
125
+ const size = [container.clientWidth, container.clientHeight];
126
+ const lngDelta = span(next.zoom);
127
+ const latDelta = unproject([0, 0], next, size)[1] - unproject([0, size[1]], next, size)[1];
128
+ map.setRegionAnimated(new mapkit.CoordinateRegion(new mapkit.Coordinate(lat, lng), new mapkit.CoordinateSpan(latDelta, lngDelta)), false);
129
+ };
130
+ setView(view);
131
+ let frame = 0;
132
+ const follow = () => {
133
+ onMove(current());
134
+ frame = requestAnimationFrame(follow);
135
+ };
136
+ const start = () => {
137
+ cancelAnimationFrame(frame);
138
+ follow();
139
+ };
140
+ const end = () => {
141
+ cancelAnimationFrame(frame);
142
+ onMove(current());
143
+ };
144
+ map.addEventListener("region-change-start", start);
145
+ map.addEventListener("region-change-end", end);
146
+ const offset = () => {
147
+ const rect = container.getBoundingClientRect();
148
+ return [rect.left + window.scrollX, rect.top + window.scrollY];
149
+ };
150
+ return {
151
+ setView,
152
+ project: ([lng, lat]) => {
153
+ const p = map.convertCoordinateToPointOnPage(new mapkit.Coordinate(lat, lng));
154
+ const [x, y] = offset();
155
+ return [p.x - x, p.y - y];
156
+ },
157
+ unproject: ([px, py]) => {
158
+ const [x, y] = offset();
159
+ const c = map.convertPointOnPageToCoordinate(new DOMPoint(px + x, py + y));
160
+ return [c.longitude, c.latitude];
161
+ },
162
+ destroy: () => {
163
+ cancelAnimationFrame(frame);
164
+ map.removeEventListener("region-change-start", start);
165
+ map.removeEventListener("region-change-end", end);
166
+ map.destroy();
167
+ }
168
+ };
169
+ };
170
+ }
171
+ //#endregion
172
+ //#region src/google.ts
173
+ /**
174
+ * Draws the map with Google Maps (Maps JavaScript API), loading it on first use.
175
+ * Create it once, outside the component, so the map is not mounted again on every render.
176
+ */
177
+ function googleMaps({ apiKey, options }) {
178
+ return async (container, view, onMove) => {
179
+ await loadScript(`https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&loading=async&callback=__uinessGoogleMaps`, "__uinessGoogleMaps");
180
+ const maps = window.google.maps;
181
+ const toLatLng = ([lng, lat]) => ({
182
+ lat,
183
+ lng
184
+ });
185
+ const map = new maps.Map(container, {
186
+ center: toLatLng(view.center),
187
+ zoom: view.zoom,
188
+ gestureHandling: "cooperative",
189
+ ...options
190
+ });
191
+ const current = () => {
192
+ const center = map.getCenter();
193
+ return {
194
+ center: [center.lng(), center.lat()],
195
+ zoom: map.getZoom()
196
+ };
197
+ };
198
+ const overlay = new maps.OverlayView();
199
+ overlay.onAdd = () => {};
200
+ overlay.onRemove = () => {};
201
+ const ready = new Promise((resolve) => {
202
+ overlay.draw = () => {
203
+ resolve();
204
+ onMove(current());
205
+ };
206
+ });
207
+ overlay.setMap(map);
208
+ const listener = map.addListener("bounds_changed", () => onMove(current()));
209
+ await ready;
210
+ return {
211
+ setView: ({ center, zoom }) => {
212
+ map.setCenter(toLatLng(center));
213
+ map.setZoom(zoom);
214
+ },
215
+ project: (lngLat) => {
216
+ const p = overlay.getProjection().fromLatLngToContainerPixel(toLatLng(lngLat));
217
+ return [p.x, p.y];
218
+ },
219
+ unproject: ([x, y]) => {
220
+ const ll = overlay.getProjection().fromContainerPixelToLatLng(new maps.Point(x, y));
221
+ return [ll.lng(), ll.lat()];
222
+ },
223
+ destroy: () => {
224
+ listener.remove();
225
+ overlay.setMap(null);
226
+ maps.event.clearInstanceListeners(map);
227
+ container.replaceChildren();
228
+ }
229
+ };
230
+ };
231
+ }
232
+ //#endregion
233
+ //#region src/providers.tsx
234
+ const link = (href, text) => /* @__PURE__ */ jsx("a", {
235
+ href,
236
+ target: "_blank",
237
+ rel: "noreferrer",
238
+ children: text
239
+ });
240
+ const osmCredit = /* @__PURE__ */ jsxs(Fragment, { children: [
241
+ "© ",
242
+ link("https://www.openstreetmap.org/copyright", "OpenStreetMap"),
243
+ " contributors"
244
+ ] });
245
+ const providers = {
246
+ osm: {
247
+ url: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
248
+ attribution: osmCredit,
249
+ maxZoom: 19
250
+ },
251
+ cartoLight: {
252
+ url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
253
+ attribution: /* @__PURE__ */ jsxs(Fragment, { children: [
254
+ osmCredit,
255
+ " © ",
256
+ link("https://carto.com/attributions", "CARTO")
257
+ ] }),
258
+ maxZoom: 20,
259
+ subdomains: [
260
+ "a",
261
+ "b",
262
+ "c",
263
+ "d"
264
+ ]
265
+ },
266
+ cartoDark: {
267
+ url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
268
+ attribution: /* @__PURE__ */ jsxs(Fragment, { children: [
269
+ osmCredit,
270
+ " © ",
271
+ link("https://carto.com/attributions", "CARTO")
272
+ ] }),
273
+ maxZoom: 20,
274
+ subdomains: [
275
+ "a",
276
+ "b",
277
+ "c",
278
+ "d"
279
+ ]
280
+ },
281
+ openTopoMap: {
282
+ url: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
283
+ attribution: /* @__PURE__ */ jsxs(Fragment, { children: [
284
+ osmCredit,
285
+ ", SRTM | © ",
286
+ link("https://opentopomap.org", "OpenTopoMap"),
287
+ " (CC-BY-SA)"
288
+ ] }),
289
+ maxZoom: 17,
290
+ subdomains: [
291
+ "a",
292
+ "b",
293
+ "c"
294
+ ]
295
+ }
296
+ };
297
+ /** The address of one tile. */
298
+ function tileUrl(provider, z, x, y) {
299
+ const subdomains = provider.subdomains ?? [];
300
+ return provider.url.replace("{s}", subdomains[(x + y) % subdomains.length] ?? "").replace("{z}", String(z)).replace("{x}", String(x)).replace("{y}", String(y));
301
+ }
302
+ //#endregion
303
+ //#region src/map.tsx
304
+ const MapContext = createContext(null);
305
+ /** The map around a component: its view, the projection and `setView`, to overlay anything. */
306
+ function useMap() {
307
+ const map = useContext(MapContext);
308
+ if (!map) throw new Error("useMap must be used inside a <Map>");
309
+ return map;
310
+ }
311
+ /** Whether a wheel event over the map should zoom it rather than scroll the page. */
312
+ function zoomsOnWheel(mode, event) {
313
+ if (mode === "always") return true;
314
+ if (mode === "never") return false;
315
+ return event.ctrlKey || event.metaKey;
316
+ }
317
+ const KEY_PAN = 80;
318
+ const NAN = [NaN, NaN];
319
+ const sameView = (a, b) => a.zoom === b.zoom && a.center[0] === b.center[0] && a.center[1] === b.center[1];
320
+ /**
321
+ * A slippy map of XYZ raster tiles in Web Mercator, or of another platform through `engine`.
322
+ * Give it a size: it fills its box and has no height of its own.
323
+ */
324
+ function Map({ center, zoom, defaultCenter = [0, 20], defaultZoom = 2, onViewChange, minZoom = 0, maxZoom, wheelZoom = "modifier", provider = providers.osm, engine, children, style, onKeyDown, onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onDoubleClick, ...props }) {
325
+ const ref = useRef(null);
326
+ const engineRef = useRef(null);
327
+ const [inner, setInner] = useState({
328
+ center: defaultCenter,
329
+ zoom: defaultZoom
330
+ });
331
+ const [size, setSize] = useState([0, 0]);
332
+ const [platform, setPlatform] = useState(null);
333
+ const view = {
334
+ center: center ?? inner.center,
335
+ zoom: zoom ?? inner.zoom
336
+ };
337
+ const max = maxZoom ?? (engine ? 22 : provider.maxZoom ?? 19);
338
+ const latest = useRef({
339
+ view,
340
+ size,
341
+ setView: (_) => {}
342
+ });
343
+ const fromPlatform = useRef(null);
344
+ const setView = (next) => {
345
+ const merged = {
346
+ ...latest.current.view,
347
+ ...next
348
+ };
349
+ const v = {
350
+ center: normalize(merged.center),
351
+ zoom: Math.min(max, Math.max(minZoom, merged.zoom))
352
+ };
353
+ latest.current.view = v;
354
+ setInner(v);
355
+ onViewChange?.(v);
356
+ };
357
+ latest.current = {
358
+ view,
359
+ size,
360
+ setView
361
+ };
362
+ useEffect(() => {
363
+ const el = ref.current;
364
+ if (!el) return;
365
+ const measure = () => setSize([el.clientWidth, el.clientHeight]);
366
+ measure();
367
+ const observer = new ResizeObserver(measure);
368
+ observer.observe(el);
369
+ return () => observer.disconnect();
370
+ }, []);
371
+ useEffect(() => {
372
+ const el = ref.current;
373
+ if (!el || engine) return;
374
+ const wheel = (e) => {
375
+ if (!zoomsOnWheel(wheelZoom, e)) return;
376
+ e.preventDefault();
377
+ const { view, size, setView } = latest.current;
378
+ const rect = el.getBoundingClientRect();
379
+ const delta = e.deltaY * (e.deltaMode === 1 ? 40 : 1);
380
+ setView(zoomAround(view, view.zoom - delta / (e.ctrlKey ? 50 : 100), [e.clientX - rect.left, e.clientY - rect.top], size));
381
+ };
382
+ el.addEventListener("wheel", wheel, { passive: false });
383
+ return () => el.removeEventListener("wheel", wheel);
384
+ }, [engine, wheelZoom]);
385
+ useEffect(() => {
386
+ const el = engineRef.current;
387
+ if (!engine || !el) return;
388
+ let alive = true;
389
+ let mounted;
390
+ Promise.resolve(engine(el, latest.current.view, (v) => {
391
+ latest.current.setView(v);
392
+ fromPlatform.current = latest.current.view;
393
+ })).then((m) => {
394
+ if (!alive) return m.destroy();
395
+ mounted = m;
396
+ setPlatform(m);
397
+ }).catch((error) => console.error(error));
398
+ return () => {
399
+ alive = false;
400
+ mounted?.destroy();
401
+ setPlatform(null);
402
+ };
403
+ }, [engine]);
404
+ const [lng, lat] = view.center;
405
+ useEffect(() => {
406
+ const v = {
407
+ center: [lng, lat],
408
+ zoom: view.zoom
409
+ };
410
+ if (platform && !(fromPlatform.current && sameView(fromPlatform.current, v))) platform.setView(v);
411
+ }, [
412
+ platform,
413
+ lng,
414
+ lat,
415
+ view.zoom
416
+ ]);
417
+ const pointers = useRef({});
418
+ const local = (e) => {
419
+ const rect = ref.current?.getBoundingClientRect();
420
+ return [e.clientX - (rect?.left ?? 0), e.clientY - (rect?.top ?? 0)];
421
+ };
422
+ const onOverlay = (target) => target.closest?.("[data-map-overlay]");
423
+ const gestures = engine ? {} : {
424
+ onPointerDown: (e) => {
425
+ onPointerDown?.(e);
426
+ if (onOverlay(e.target) || e.button !== 0) return;
427
+ e.currentTarget.setPointerCapture(e.pointerId);
428
+ pointers.current[e.pointerId] = local(e);
429
+ },
430
+ onPointerMove: (e) => {
431
+ onPointerMove?.(e);
432
+ const before = pointers.current;
433
+ if (!before[e.pointerId]) return;
434
+ const { view, size } = latest.current;
435
+ const after = {
436
+ ...before,
437
+ [e.pointerId]: local(e)
438
+ };
439
+ pointers.current = after;
440
+ const [a, b] = Object.values(before);
441
+ const [c, d] = Object.values(after);
442
+ if (!a || !c) return;
443
+ if (!b || !d) {
444
+ setView(pan(view, [c[0] - a[0], c[1] - a[1]], size));
445
+ return;
446
+ }
447
+ const mid = (p, q) => [(p[0] + q[0]) / 2, (p[1] + q[1]) / 2];
448
+ const dist = (p, q) => Math.hypot(p[0] - q[0], p[1] - q[1]) || 1;
449
+ const [m0, m1] = [mid(a, b), mid(c, d)];
450
+ const moved = pan(view, [m1[0] - m0[0], m1[1] - m0[1]], size);
451
+ setView(zoomAround(moved, view.zoom + Math.log2(dist(c, d) / dist(a, b)), m1, size));
452
+ },
453
+ onPointerUp: (e) => {
454
+ onPointerUp?.(e);
455
+ delete pointers.current[e.pointerId];
456
+ },
457
+ onPointerCancel: (e) => {
458
+ onPointerCancel?.(e);
459
+ delete pointers.current[e.pointerId];
460
+ },
461
+ onDoubleClick: (e) => {
462
+ onDoubleClick?.(e);
463
+ if (onOverlay(e.target)) return;
464
+ const { view, size } = latest.current;
465
+ setView(zoomAround(view, Math.round(view.zoom) + (e.shiftKey ? -1 : 1), local(e), size));
466
+ }
467
+ };
468
+ const keyDown = (e) => {
469
+ onKeyDown?.(e);
470
+ if (e.target !== e.currentTarget || e.altKey || e.ctrlKey || e.metaKey) return;
471
+ const { view, size } = latest.current;
472
+ const by = {
473
+ ArrowLeft: [KEY_PAN, 0],
474
+ ArrowRight: [-80, 0],
475
+ ArrowUp: [0, KEY_PAN],
476
+ ArrowDown: [0, -80]
477
+ }[e.key];
478
+ if (by) setView(pan(view, by, size));
479
+ else if (e.key === "+" || e.key === "=") setView({ zoom: Math.round(view.zoom) + 1 });
480
+ else if (e.key === "-" || e.key === "_") setView({ zoom: Math.round(view.zoom) - 1 });
481
+ else return;
482
+ e.preventDefault();
483
+ };
484
+ const z = Math.round(Math.min(view.zoom, provider.maxZoom ?? 19));
485
+ const value = {
486
+ ...view,
487
+ size,
488
+ project: engine ? (ll) => platform?.project(ll) ?? NAN : (ll) => project(ll, view, size),
489
+ unproject: engine ? (p) => platform?.unproject(p) ?? NAN : (p) => unproject(p, view, size),
490
+ setView
491
+ };
492
+ return /* @__PURE__ */ jsxs("section", {
493
+ ref,
494
+ "aria-roledescription": "map",
495
+ "aria-label": "Map",
496
+ tabIndex: 0,
497
+ ...props,
498
+ ...gestures,
499
+ onKeyDown: keyDown,
500
+ style: {
501
+ position: "relative",
502
+ overflow: "hidden",
503
+ touchAction: engine ? void 0 : "none",
504
+ userSelect: "none",
505
+ ...style
506
+ },
507
+ children: [
508
+ engine ? /* @__PURE__ */ jsx("div", {
509
+ ref: engineRef,
510
+ style: {
511
+ position: "absolute",
512
+ inset: 0
513
+ }
514
+ }) : size[0] > 0 && visibleTiles(view, size, Math.max(0, z)).map((t) => /* @__PURE__ */ jsx("img", {
515
+ src: tileUrl(provider, t.z, t.x, t.y),
516
+ alt: "",
517
+ "data-map-tile": "",
518
+ draggable: false,
519
+ style: {
520
+ position: "absolute",
521
+ left: t.left,
522
+ top: t.top,
523
+ width: t.width,
524
+ height: t.height,
525
+ maxWidth: "none",
526
+ pointerEvents: "none"
527
+ }
528
+ }, t.key)),
529
+ /* @__PURE__ */ jsx(MapContext.Provider, {
530
+ value,
531
+ children: /* @__PURE__ */ jsx("div", {
532
+ style: {
533
+ position: "absolute",
534
+ inset: 0,
535
+ pointerEvents: "none"
536
+ },
537
+ children
538
+ })
539
+ }),
540
+ !engine && /* @__PURE__ */ jsx("div", {
541
+ "data-map-attribution": "",
542
+ "data-map-overlay": "",
543
+ style: {
544
+ position: "absolute",
545
+ right: 0,
546
+ bottom: 0,
547
+ padding: "1px 5px",
548
+ font: "11px/1.4 system-ui, sans-serif",
549
+ color: "#333",
550
+ background: "rgba(255, 255, 255, 0.8)"
551
+ },
552
+ children: provider.attribution
553
+ })
554
+ ]
555
+ });
556
+ }
557
+ //#endregion
558
+ //#region src/overlays.tsx
559
+ /** Placed at a coordinate, anchored by `transform`, hidden while the projection is unknown. */
560
+ function useAnchor(lngLat, transform) {
561
+ const [x, y] = useMap().project(lngLat);
562
+ return {
563
+ position: "absolute",
564
+ left: 0,
565
+ top: 0,
566
+ transform: `translate(${x}px, ${y}px) ${transform}`,
567
+ visibility: Number.isNaN(x) ? "hidden" : void 0
568
+ };
569
+ }
570
+ const pin = /* @__PURE__ */ jsxs("svg", {
571
+ width: "27",
572
+ height: "41",
573
+ viewBox: "0 0 27 41",
574
+ "aria-hidden": "true",
575
+ style: { display: "block" },
576
+ children: [/* @__PURE__ */ jsx("path", {
577
+ d: "M13.5 1C6.6 1 1 6.6 1 13.5 1 23 13.5 40 13.5 40S26 23 26 13.5C26 6.6 20.4 1 13.5 1Z",
578
+ fill: "#e5484d",
579
+ stroke: "#fff",
580
+ strokeWidth: "2"
581
+ }), /* @__PURE__ */ jsx("circle", {
582
+ cx: "13.5",
583
+ cy: "13.5",
584
+ r: "4.5",
585
+ fill: "#fff"
586
+ })]
587
+ });
588
+ /** A pin at a coordinate. With `onClick` it is a button, else it lets the map be dragged through it. */
589
+ function Marker({ lngLat, children = pin, onClick, label, className, style }) {
590
+ const anchor = {
591
+ ...useAnchor(lngLat, "translate(-50%, -100%)"),
592
+ ...style
593
+ };
594
+ if (!onClick) return /* @__PURE__ */ jsx("div", {
595
+ className,
596
+ style: anchor,
597
+ children
598
+ });
599
+ return /* @__PURE__ */ jsx("button", {
600
+ type: "button",
601
+ "data-map-overlay": "",
602
+ "aria-label": label,
603
+ onClick,
604
+ className,
605
+ style: {
606
+ padding: 0,
607
+ border: 0,
608
+ background: "none",
609
+ cursor: "pointer",
610
+ pointerEvents: "auto",
611
+ ...anchor
612
+ },
613
+ children
614
+ });
615
+ }
616
+ const card = {
617
+ padding: "8px 12px",
618
+ borderRadius: 8,
619
+ background: "#fff",
620
+ color: "#111",
621
+ font: "14px/1.4 system-ui, sans-serif",
622
+ boxShadow: "0 2px 12px rgba(0, 0, 0, 0.25)"
623
+ };
624
+ /**
625
+ * A box above a coordinate. Render it while it should be open. A `className` replaces its
626
+ * default white card, so the look can come from your styles.
627
+ */
628
+ function Popup({ lngLat, offset = 44, onClose, closeLabel = "Close", closeIcon = "×", className, style, children, onKeyDown, ...props }) {
629
+ const anchor = useAnchor(lngLat, `translate(-50%, calc(-100% - ${offset}px))`);
630
+ return /* @__PURE__ */ jsxs("div", {
631
+ role: "dialog",
632
+ "data-map-overlay": "",
633
+ className,
634
+ onKeyDown: (e) => {
635
+ onKeyDown?.(e);
636
+ if (e.key === "Escape" && onClose) {
637
+ e.stopPropagation();
638
+ onClose();
639
+ }
640
+ },
641
+ ...props,
642
+ style: {
643
+ ...className ? null : card,
644
+ ...anchor,
645
+ pointerEvents: "auto",
646
+ userSelect: "text",
647
+ ...style
648
+ },
649
+ children: [onClose && /* @__PURE__ */ jsx("button", {
650
+ type: "button",
651
+ "aria-label": closeLabel,
652
+ onClick: onClose,
653
+ "data-map-popup-close": "",
654
+ style: className ? void 0 : {
655
+ float: "right",
656
+ margin: "-4px -6px 0 8px",
657
+ padding: 0,
658
+ border: 0,
659
+ background: "none",
660
+ font: "inherit",
661
+ fontSize: 18,
662
+ lineHeight: 1,
663
+ cursor: "pointer",
664
+ color: "inherit"
665
+ },
666
+ children: /* @__PURE__ */ jsx("span", {
667
+ "aria-hidden": "true",
668
+ style: { display: "contents" },
669
+ children: closeIcon
670
+ })
671
+ }), children]
672
+ });
673
+ }
674
+ const zoomButton = {
675
+ display: "block",
676
+ width: 32,
677
+ height: 32,
678
+ border: 0,
679
+ background: "#fff",
680
+ color: "#111",
681
+ font: "20px/1 system-ui, sans-serif",
682
+ cursor: "pointer"
683
+ };
684
+ /** Plus and minus buttons in the top right corner. A `className` replaces their default look. */
685
+ function ZoomControl({ zoomInLabel = "Zoom in", zoomOutLabel = "Zoom out", className, style }) {
686
+ const map = useMap();
687
+ const look = className ? void 0 : zoomButton;
688
+ return /* @__PURE__ */ jsxs("div", {
689
+ "data-map-overlay": "",
690
+ className,
691
+ style: {
692
+ position: "absolute",
693
+ top: 10,
694
+ right: 10,
695
+ pointerEvents: "auto",
696
+ ...className ? null : {
697
+ borderRadius: 6,
698
+ overflow: "hidden",
699
+ boxShadow: "0 1px 6px rgba(0, 0, 0, 0.3)"
700
+ },
701
+ ...style
702
+ },
703
+ children: [/* @__PURE__ */ jsx("button", {
704
+ type: "button",
705
+ "aria-label": zoomInLabel,
706
+ style: look,
707
+ onClick: () => map.setView({ zoom: Math.round(map.zoom) + 1 }),
708
+ children: "+"
709
+ }), /* @__PURE__ */ jsx("button", {
710
+ type: "button",
711
+ "aria-label": zoomOutLabel,
712
+ style: look && {
713
+ ...look,
714
+ borderTop: "1px solid #ddd"
715
+ },
716
+ onClick: () => map.setView({ zoom: Math.round(map.zoom) - 1 }),
717
+ children: "−"
718
+ })]
719
+ });
720
+ }
721
+ //#endregion
722
+ export { Map, Marker, Popup, TILE_SIZE, ZoomControl, appleMaps, googleMaps, lngLatToWorld, loadScript, normalize, pan, project, providers, tileUrl, unproject, useMap, visibleTiles, worldToLngLat, zoomAround, zoomsOnWheel };
723
+
724
+ //# sourceMappingURL=index.mjs.map