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