@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/README.md +153 -2
- package/dist/index.cjs +744 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +230 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +230 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +724 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -3
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
|