@dxos/react-ui-geo 0.10.0 → 0.11.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/lib/chunk-countries-10m.mjs +10 -0
- package/dist/lib/chunk-countries-10m.mjs.map +1 -0
- package/dist/lib/chunk-countries-110m.mjs +10 -0
- package/dist/lib/chunk-countries-110m.mjs.map +1 -0
- package/dist/lib/chunk-countries-50m.mjs +10 -0
- package/dist/lib/chunk-countries-50m.mjs.map +1 -0
- package/dist/lib/data.mjs +12 -0
- package/dist/lib/data.mjs.map +1 -0
- package/dist/lib/index.mjs +1415 -0
- package/dist/lib/index.mjs.map +1 -0
- package/dist/lib/translations.mjs +12 -0
- package/dist/lib/translations.mjs.map +1 -0
- package/dist/types/src/components/Globe/Globe.d.ts +1 -1
- package/dist/types/src/components/Map/Map.d.ts +1 -1
- package/dist/types/src/components/Toolbar/Controls.d.ts +0 -2
- package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/control-positions.d.ts +3 -0
- package/dist/types/src/components/Toolbar/control-positions.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -1
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +14 -17
- package/src/components/Toolbar/Controls.tsx +0 -8
- package/src/components/Toolbar/control-positions.ts +16 -0
- package/src/components/Toolbar/index.ts +2 -0
- package/dist/lib/browser/chunk-SC2FBYFU.mjs +0 -17
- package/dist/lib/browser/chunk-SC2FBYFU.mjs.map +0 -7
- package/dist/lib/browser/countries-10m-CWWDOKH7.mjs +0 -6
- package/dist/lib/browser/countries-10m-CWWDOKH7.mjs.map +0 -7
- package/dist/lib/browser/countries-110m-72QBAA5E.mjs +0 -6
- package/dist/lib/browser/countries-110m-72QBAA5E.mjs.map +0 -7
- package/dist/lib/browser/countries-50m-H7SL7KVF.mjs +0 -6
- package/dist/lib/browser/countries-50m-H7SL7KVF.mjs.map +0 -7
- package/dist/lib/browser/data.mjs +0 -7
- package/dist/lib/browser/data.mjs.map +0 -7
- package/dist/lib/browser/index.mjs +0 -1666
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/browser/translations.mjs +0 -19
- package/dist/lib/browser/translations.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-VZENBYLJ.mjs +0 -19
- package/dist/lib/node-esm/chunk-VZENBYLJ.mjs.map +0 -7
- package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs +0 -8
- package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs.map +0 -7
- package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs +0 -8
- package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs.map +0 -7
- package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs +0 -8
- package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs.map +0 -7
- package/dist/lib/node-esm/data.mjs +0 -8
- package/dist/lib/node-esm/data.mjs.map +0 -7
- package/dist/lib/node-esm/index.mjs +0 -1667
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
- package/dist/lib/node-esm/translations.mjs +0 -21
- package/dist/lib/node-esm/translations.mjs.map +0 -7
- /package/src/hooks/{context.tsx → context.ts} +0 -0
|
@@ -0,0 +1,1415 @@
|
|
|
1
|
+
import { loadTopology } from "./data.mjs";
|
|
2
|
+
import { drag, easeLinear, easeSinOut, geoBounds, geoCentroid, geoCircle as geoCircle$1, geoDistance, geoGraticule, geoInterpolate, geoMercator, geoOrthographic, geoPath, geoTransverseMercator, interpolateNumber, select, selection, timer as timer$1, transition } from "d3";
|
|
3
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useResizeDetector } from "react-resize-detector";
|
|
5
|
+
import { IconButton, ThemeProvider, Toolbar, Tooltip, composable, composableProps, defaultTx, useComposedRefs, useControlledState, useDynamicRef, useThemeContext, useTranslation } from "@dxos/react-ui";
|
|
6
|
+
import { mx } from "@dxos/ui-theme";
|
|
7
|
+
import { raise } from "@dxos/debug";
|
|
8
|
+
import versor from "versor";
|
|
9
|
+
import { feature, mesh } from "topojson-client";
|
|
10
|
+
import { presimplify, quantile, simplify } from "topojson-simplify";
|
|
11
|
+
import { log } from "@dxos/log";
|
|
12
|
+
import { translationKey } from "#translations";
|
|
13
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
import "leaflet/dist/leaflet.css";
|
|
15
|
+
import { createContext as createContext$1 } from "@radix-ui/react-context";
|
|
16
|
+
import L, { Control, DomEvent, DomUtil, latLngBounds, point } from "leaflet";
|
|
17
|
+
import { createRoot } from "react-dom/client";
|
|
18
|
+
import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
|
|
19
|
+
//#region src/hooks/context.ts
|
|
20
|
+
/** @internal */
|
|
21
|
+
var GlobeContext = createContext(void 0);
|
|
22
|
+
var useGlobeContext = () => {
|
|
23
|
+
return useContext(GlobeContext) ?? raise(/* @__PURE__ */ new Error("Missing GlobeContext"));
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/util/animation.ts
|
|
27
|
+
/**
|
|
28
|
+
* Duration scaled by great-circle distance between two geo positions.
|
|
29
|
+
* Ensures long jumps animate longer than short ones while clamping to a
|
|
30
|
+
* minimum base. `scale` controls how steeply duration grows with distance
|
|
31
|
+
* (ms per radian of arc).
|
|
32
|
+
*/
|
|
33
|
+
var flyDuration = (p1, p2, base, scale) => Math.max(base, geoDistance(p1, p2) * scale);
|
|
34
|
+
/**
|
|
35
|
+
* Per-frame tween that interpolates the projection's rotation between two
|
|
36
|
+
* Euler triples along the shortest great-circle arc using versors. Mutates
|
|
37
|
+
* the projection and pushes the normalised rotation through `setRotation`.
|
|
38
|
+
*/
|
|
39
|
+
var createRotationTween = (projection, setRotation, r1, r2) => {
|
|
40
|
+
const iv = versor.interpolate(r1, r2);
|
|
41
|
+
return (t) => {
|
|
42
|
+
projection.rotate(iv(t));
|
|
43
|
+
setRotation(projection.rotate());
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/util/debug.ts
|
|
48
|
+
var timer = (cb) => {
|
|
49
|
+
const start = Date.now();
|
|
50
|
+
const data = cb();
|
|
51
|
+
Date.now() - start / 1e3;
|
|
52
|
+
return data;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/util/inertia.ts
|
|
56
|
+
var restrictAxis = (axis) => (original, current) => current.map((d, i) => axis[i] ? d : original[i]);
|
|
57
|
+
/**
|
|
58
|
+
* Applies a drag handler to the specified target element.
|
|
59
|
+
*/
|
|
60
|
+
var geoInertiaDrag = (target, render, projection, options) => {
|
|
61
|
+
if (!options) options = {};
|
|
62
|
+
if (target.node) target = target.node();
|
|
63
|
+
target = select(target);
|
|
64
|
+
const linear = (options.mode ?? "linear") === "linear";
|
|
65
|
+
const sharedHandlers = {
|
|
66
|
+
projection,
|
|
67
|
+
render: (rotation) => {
|
|
68
|
+
projection.rotate(rotation);
|
|
69
|
+
render && render();
|
|
70
|
+
},
|
|
71
|
+
axis: restrictAxis(options.lockTilt ? [
|
|
72
|
+
true,
|
|
73
|
+
false,
|
|
74
|
+
false
|
|
75
|
+
] : [
|
|
76
|
+
true,
|
|
77
|
+
true,
|
|
78
|
+
true
|
|
79
|
+
]),
|
|
80
|
+
start: options.start,
|
|
81
|
+
move: options.move,
|
|
82
|
+
end: options.end,
|
|
83
|
+
stop: options.stop,
|
|
84
|
+
finish: options.finish,
|
|
85
|
+
time: options.time,
|
|
86
|
+
hold: options.hold
|
|
87
|
+
};
|
|
88
|
+
const inertia = linear ? geoInertiaDragLinearHelper({
|
|
89
|
+
...sharedHandlers,
|
|
90
|
+
sensitivity: options.sensitivity,
|
|
91
|
+
getZoom: options.getZoom
|
|
92
|
+
}) : geoInertiaDragHelper(sharedHandlers);
|
|
93
|
+
target.call(drag().on("start", inertia.start).on("drag", inertia.move).on("end", inertia.end));
|
|
94
|
+
return inertia;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* A versor is a compact way to describe a rotation in 3D space.
|
|
98
|
+
* It consists of four components [𝑤,x,y,z], where:
|
|
99
|
+
* 𝑤 is a scalar representing the angle of rotation.
|
|
100
|
+
* x, y, z are the vector components, representing the axis of rotation.
|
|
101
|
+
*/
|
|
102
|
+
var geoInertiaDragHelper = (opt) => {
|
|
103
|
+
const projection = opt.projection;
|
|
104
|
+
let v0;
|
|
105
|
+
let r0;
|
|
106
|
+
let q0;
|
|
107
|
+
let v10;
|
|
108
|
+
let v11;
|
|
109
|
+
let q10;
|
|
110
|
+
const inertia = inertiaHelper({
|
|
111
|
+
axis: opt.axis,
|
|
112
|
+
start: () => {
|
|
113
|
+
v0 = versor.cartesian(projection.invert(inertia.position));
|
|
114
|
+
r0 = projection.rotate();
|
|
115
|
+
q0 = versor(r0);
|
|
116
|
+
opt.start && opt.start();
|
|
117
|
+
},
|
|
118
|
+
move: () => {
|
|
119
|
+
const inv = projection.rotate(r0).invert(inertia.position);
|
|
120
|
+
if (isNaN(inv[0])) return;
|
|
121
|
+
const v1 = versor.cartesian(inv);
|
|
122
|
+
const q1 = versor.multiply(q0, versor.delta(v0, v1));
|
|
123
|
+
const r1 = versor.rotation(q1);
|
|
124
|
+
const r2 = opt.axis(r0, r1);
|
|
125
|
+
opt.render(r2);
|
|
126
|
+
opt.move && opt.move();
|
|
127
|
+
},
|
|
128
|
+
end: () => {
|
|
129
|
+
v10 = versor.cartesian(projection.invert(inertia.position.map((d, i) => d - inertia.velocity[i] / 1e3)));
|
|
130
|
+
q10 = versor(projection.rotate());
|
|
131
|
+
v11 = versor.cartesian(projection.invert(inertia.position));
|
|
132
|
+
opt.end && opt.end();
|
|
133
|
+
},
|
|
134
|
+
stop: opt.stop,
|
|
135
|
+
finish: opt.finish,
|
|
136
|
+
render: (t) => {
|
|
137
|
+
const r1 = versor.rotation(versor.multiply(q10, versor.delta(v10, v11, t * 1e3)));
|
|
138
|
+
const r2 = opt.axis(r0, r1);
|
|
139
|
+
opt.render && opt.render(r2);
|
|
140
|
+
},
|
|
141
|
+
time: opt.time
|
|
142
|
+
});
|
|
143
|
+
return inertia;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Linear pixel-to-Euler drag: Δx → lambda, Δy → phi, gamma fixed at 0.
|
|
147
|
+
* Inertia spin-down reuses the same shared decay curve as the versor path.
|
|
148
|
+
*/
|
|
149
|
+
var DEFAULT_LINEAR_SENSITIVITY = .25;
|
|
150
|
+
var geoInertiaDragLinearHelper = (opt) => {
|
|
151
|
+
const projection = opt.projection;
|
|
152
|
+
const sensitivity = opt.sensitivity ?? DEFAULT_LINEAR_SENSITIVITY;
|
|
153
|
+
const gain = () => sensitivity / Math.max(opt.getZoom?.() ?? 1, .1);
|
|
154
|
+
let r0;
|
|
155
|
+
let p0;
|
|
156
|
+
let kStart;
|
|
157
|
+
let rEnd;
|
|
158
|
+
let vEnd;
|
|
159
|
+
const inertia = inertiaHelper({
|
|
160
|
+
axis: opt.axis,
|
|
161
|
+
start: () => {
|
|
162
|
+
r0 = projection.rotate();
|
|
163
|
+
p0 = [inertia.position[0], inertia.position[1]];
|
|
164
|
+
kStart = gain();
|
|
165
|
+
opt.start && opt.start();
|
|
166
|
+
},
|
|
167
|
+
move: () => {
|
|
168
|
+
const dx = inertia.position[0] - p0[0];
|
|
169
|
+
const dy = inertia.position[1] - p0[1];
|
|
170
|
+
const r1 = [
|
|
171
|
+
r0[0] + dx * kStart,
|
|
172
|
+
r0[1] - dy * kStart,
|
|
173
|
+
0
|
|
174
|
+
];
|
|
175
|
+
const r2 = opt.axis(r0, r1);
|
|
176
|
+
opt.render(r2);
|
|
177
|
+
opt.move && opt.move();
|
|
178
|
+
},
|
|
179
|
+
end: () => {
|
|
180
|
+
rEnd = projection.rotate();
|
|
181
|
+
vEnd = [inertia.velocity[0], inertia.velocity[1]];
|
|
182
|
+
opt.end && opt.end();
|
|
183
|
+
},
|
|
184
|
+
stop: opt.stop,
|
|
185
|
+
finish: opt.finish,
|
|
186
|
+
render: (t) => {
|
|
187
|
+
const r1 = [
|
|
188
|
+
rEnd[0] + vEnd[0] * kStart * t,
|
|
189
|
+
rEnd[1] - vEnd[1] * kStart * t,
|
|
190
|
+
0
|
|
191
|
+
];
|
|
192
|
+
const r2 = opt.axis(rEnd, r1);
|
|
193
|
+
opt.render && opt.render(r2);
|
|
194
|
+
},
|
|
195
|
+
time: opt.time
|
|
196
|
+
});
|
|
197
|
+
return inertia;
|
|
198
|
+
};
|
|
199
|
+
function inertiaHelper(opt) {
|
|
200
|
+
const A = opt.time || 5e3;
|
|
201
|
+
const limit = 1.0001;
|
|
202
|
+
const B = -Math.log(1 - 1 / limit);
|
|
203
|
+
const inertia = {
|
|
204
|
+
position: [0, 0],
|
|
205
|
+
velocity: [0, 0],
|
|
206
|
+
timer: timer$1(() => {}),
|
|
207
|
+
time: 0,
|
|
208
|
+
t: 0,
|
|
209
|
+
start: function(ev) {
|
|
210
|
+
const position = [ev.x, ev.y];
|
|
211
|
+
inertia.position = position;
|
|
212
|
+
inertia.velocity = [0, 0];
|
|
213
|
+
inertia.timer.stop();
|
|
214
|
+
this.classList.remove("inertia");
|
|
215
|
+
this.classList.add("dragging");
|
|
216
|
+
opt.start && opt.start.call(this, position);
|
|
217
|
+
},
|
|
218
|
+
move: function(ev) {
|
|
219
|
+
const position = [ev.x, ev.y];
|
|
220
|
+
const time = performance.now();
|
|
221
|
+
const deltaTime = time - inertia.time;
|
|
222
|
+
const decay = 1 - Math.exp(-deltaTime / 1e3);
|
|
223
|
+
inertia.velocity = inertia.velocity.map((d, i) => {
|
|
224
|
+
const deltaPos = position[i] - inertia.position[i];
|
|
225
|
+
const deltaTime = time - inertia.time;
|
|
226
|
+
return 1e3 * (1 - decay) * deltaPos / deltaTime + d * decay;
|
|
227
|
+
});
|
|
228
|
+
inertia.velocity = opt.axis([0, 0], inertia.velocity);
|
|
229
|
+
inertia.time = time;
|
|
230
|
+
inertia.position = position;
|
|
231
|
+
opt.move && opt.move.call(this, position);
|
|
232
|
+
},
|
|
233
|
+
end: function(ev) {
|
|
234
|
+
this.classList.remove("dragging", "inertia");
|
|
235
|
+
const v = inertia.velocity;
|
|
236
|
+
if (v[0] * v[0] + v[1] * v[1] < 100) {
|
|
237
|
+
inertia.timer.stop();
|
|
238
|
+
return opt.stop && opt.stop();
|
|
239
|
+
}
|
|
240
|
+
const deltaTime = performance.now() - inertia.time;
|
|
241
|
+
if (opt.hold === void 0) opt.hold = 100;
|
|
242
|
+
if (deltaTime >= opt.hold) {
|
|
243
|
+
inertia.timer.stop();
|
|
244
|
+
return opt.stop && opt.stop();
|
|
245
|
+
}
|
|
246
|
+
this.classList.add("inertia");
|
|
247
|
+
opt.end && opt.end();
|
|
248
|
+
const self = this;
|
|
249
|
+
inertia.timer.restart((e) => {
|
|
250
|
+
inertia.t = limit * (1 - Math.exp(-B * e / A));
|
|
251
|
+
opt.render && opt.render(inertia.t);
|
|
252
|
+
if (inertia.t > 1) {
|
|
253
|
+
inertia.timer.stop();
|
|
254
|
+
self.classList.remove("inertia");
|
|
255
|
+
inertia.velocity = [0, 0];
|
|
256
|
+
inertia.t = 1;
|
|
257
|
+
opt.finish && opt.finish();
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
inertia.timer.stop();
|
|
263
|
+
return inertia;
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/util/path.ts
|
|
267
|
+
var positionToRotation = ([lng, lat], tilt = 0) => [
|
|
268
|
+
-lng,
|
|
269
|
+
tilt - lat,
|
|
270
|
+
0
|
|
271
|
+
];
|
|
272
|
+
var geoToPosition = ({ lat, lng }) => [lng, lat];
|
|
273
|
+
var geoPoint = (point) => ({
|
|
274
|
+
type: "Point",
|
|
275
|
+
coordinates: geoToPosition(point)
|
|
276
|
+
});
|
|
277
|
+
var geoCircle = ({ lat, lng }, radius) => geoCircle$1().radius(radius).center([lng, lat])();
|
|
278
|
+
var geoLine = (p1, p2) => ({
|
|
279
|
+
type: "LineString",
|
|
280
|
+
coordinates: [[p1.lng, p1.lat], [p2.lng, p2.lat]]
|
|
281
|
+
});
|
|
282
|
+
var closestPoint = (points, target) => {
|
|
283
|
+
if (points.length === 0) return target;
|
|
284
|
+
let closestPoint = points[0];
|
|
285
|
+
let minDistance = getDistance(points[0], target);
|
|
286
|
+
for (const point of points) {
|
|
287
|
+
const distance = getDistance(point, target);
|
|
288
|
+
if (distance < minDistance) {
|
|
289
|
+
minDistance = distance;
|
|
290
|
+
closestPoint = point;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return closestPoint;
|
|
294
|
+
};
|
|
295
|
+
var getDistance = (point1, point2) => {
|
|
296
|
+
const dx = point1[0] - point2[0];
|
|
297
|
+
const dy = point1[1] - point2[1];
|
|
298
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
299
|
+
};
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/util/render.ts
|
|
302
|
+
var RAD_TO_DEG = 180 / Math.PI;
|
|
303
|
+
/**
|
|
304
|
+
* Compute a spherical bounding circle for a GeoJSON feature. We sample the
|
|
305
|
+
* geoBounds corners (cheap, sufficient for typical country shapes); for
|
|
306
|
+
* features crossing the antimeridian d3.geoBounds returns west > east, which
|
|
307
|
+
* geoDistance handles correctly when called on the actual centroid.
|
|
308
|
+
*/
|
|
309
|
+
var computeBounds = (geometry) => {
|
|
310
|
+
const feat = {
|
|
311
|
+
type: "Feature",
|
|
312
|
+
geometry,
|
|
313
|
+
properties: {}
|
|
314
|
+
};
|
|
315
|
+
const centroid = geoCentroid(feat);
|
|
316
|
+
const [[w, s], [e, n]] = geoBounds(feat);
|
|
317
|
+
const corners = [
|
|
318
|
+
[w, s],
|
|
319
|
+
[w, n],
|
|
320
|
+
[e, s],
|
|
321
|
+
[e, n]
|
|
322
|
+
];
|
|
323
|
+
let radius = 0;
|
|
324
|
+
for (const corner of corners) {
|
|
325
|
+
const d = geoDistance(centroid, corner) * RAD_TO_DEG;
|
|
326
|
+
if (d > radius) radius = d;
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
geometry,
|
|
330
|
+
centroid,
|
|
331
|
+
radius
|
|
332
|
+
};
|
|
333
|
+
};
|
|
334
|
+
/**
|
|
335
|
+
* Create rendering layers.
|
|
336
|
+
*/
|
|
337
|
+
var createLayers = (topology, features, styles) => {
|
|
338
|
+
const layers = [];
|
|
339
|
+
if (styles.water) layers.push({
|
|
340
|
+
styles: styles.water,
|
|
341
|
+
path: { type: "Sphere" }
|
|
342
|
+
});
|
|
343
|
+
if (styles.graticule) layers.push({
|
|
344
|
+
styles: styles.graticule,
|
|
345
|
+
path: geoGraticule().step([6, 6])()
|
|
346
|
+
});
|
|
347
|
+
if (topology) {
|
|
348
|
+
if (styles.land) {
|
|
349
|
+
if (topology.objects.countries) {
|
|
350
|
+
const memberGeoms = feature(topology, topology.objects.countries).features.map((f) => f.geometry);
|
|
351
|
+
const bounds = memberGeoms.map(computeBounds);
|
|
352
|
+
layers.push({
|
|
353
|
+
styles: styles.land,
|
|
354
|
+
path: {
|
|
355
|
+
type: "GeometryCollection",
|
|
356
|
+
geometries: memberGeoms
|
|
357
|
+
},
|
|
358
|
+
cullable: bounds
|
|
359
|
+
});
|
|
360
|
+
} else if (topology.objects.land) layers.push({
|
|
361
|
+
styles: styles.land,
|
|
362
|
+
path: feature(topology, topology.objects.land)
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (topology.objects.countries && styles.border) layers.push({
|
|
366
|
+
styles: styles.border,
|
|
367
|
+
path: mesh(topology, topology.objects.countries, (a, b) => a !== b)
|
|
368
|
+
});
|
|
369
|
+
if (topology.objects.dots && styles.dots) layers.push({
|
|
370
|
+
styles: styles.dots,
|
|
371
|
+
path: topology.objects.dots
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (features) {
|
|
375
|
+
const { points, lines } = features;
|
|
376
|
+
if (lines && styles.line) layers.push({
|
|
377
|
+
styles: styles.line,
|
|
378
|
+
path: {
|
|
379
|
+
type: "GeometryCollection",
|
|
380
|
+
geometries: lines.map(({ source, target }) => geoLine(source, target))
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
if (points && styles.point) layers.push({
|
|
384
|
+
styles: styles.point,
|
|
385
|
+
path: {
|
|
386
|
+
type: "GeometryCollection",
|
|
387
|
+
geometries: points.map((point) => geoPoint(point))
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
return layers;
|
|
392
|
+
};
|
|
393
|
+
/**
|
|
394
|
+
* Render layers created above.
|
|
395
|
+
*
|
|
396
|
+
* When `viewCenter` is supplied (orthographic globe), layers with a
|
|
397
|
+
* `cullable` index are filtered to just the features whose bounding circle
|
|
398
|
+
* intersects the visible hemisphere — keeping the d3-geo walk proportional
|
|
399
|
+
* to what's actually on-screen.
|
|
400
|
+
*/
|
|
401
|
+
var renderLayers = (generator, layers = [], scale, styles, viewCenter) => {
|
|
402
|
+
const context = generator.context();
|
|
403
|
+
const { canvas: { width, height } } = context;
|
|
404
|
+
context.reset();
|
|
405
|
+
if (styles.background) {
|
|
406
|
+
context.fillStyle = styles.background.fillStyle;
|
|
407
|
+
context.fillRect(0, 0, width, height);
|
|
408
|
+
} else context.clearRect(0, 0, width, height);
|
|
409
|
+
layers.forEach((layer) => {
|
|
410
|
+
const { path, styles, cullable } = layer;
|
|
411
|
+
context.save();
|
|
412
|
+
let fill = false;
|
|
413
|
+
let stroke = false;
|
|
414
|
+
if (styles) Object.entries(styles).forEach(([key, value]) => {
|
|
415
|
+
if (key === "pointRadius") generator.pointRadius(value * scale);
|
|
416
|
+
else {
|
|
417
|
+
context[key] = value;
|
|
418
|
+
fill ||= key === "fillStyle";
|
|
419
|
+
stroke ||= key === "strokeStyle";
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
let renderPath = path;
|
|
423
|
+
if (cullable && viewCenter) {
|
|
424
|
+
const geometries = [];
|
|
425
|
+
for (let index = 0; index < cullable.length; index++) {
|
|
426
|
+
const bounds = cullable[index];
|
|
427
|
+
if (geoDistance(viewCenter, bounds.centroid) * RAD_TO_DEG < 90 + bounds.radius) geometries.push(bounds.geometry);
|
|
428
|
+
}
|
|
429
|
+
renderPath = {
|
|
430
|
+
type: "GeometryCollection",
|
|
431
|
+
geometries
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
context.beginPath();
|
|
435
|
+
generator(renderPath);
|
|
436
|
+
fill && context.fill();
|
|
437
|
+
stroke && context.stroke();
|
|
438
|
+
context.restore();
|
|
439
|
+
});
|
|
440
|
+
return context;
|
|
441
|
+
};
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/util/styles.ts
|
|
444
|
+
/**
|
|
445
|
+
* Default style set for the Globe, theme-aware. Originated in plugin-map's
|
|
446
|
+
* GlobeControl; lifted here so other plugins (plugin-trip, etc.) get the same
|
|
447
|
+
* baseline without copying the palette.
|
|
448
|
+
*/
|
|
449
|
+
var POINT_COLOR = "rgb(220, 38, 38)";
|
|
450
|
+
var LINE_COLOR = "rgba(220, 38, 38, 0.5)";
|
|
451
|
+
var globeStyles = (themeMode) => themeMode === "dark" ? {
|
|
452
|
+
water: { fillStyle: "#191919" },
|
|
453
|
+
land: {
|
|
454
|
+
fillStyle: "#444",
|
|
455
|
+
strokeStyle: "#222"
|
|
456
|
+
},
|
|
457
|
+
border: { strokeStyle: "#111" },
|
|
458
|
+
graticule: { strokeStyle: "#111" },
|
|
459
|
+
line: {
|
|
460
|
+
lineWidth: 1.5,
|
|
461
|
+
lineDash: [4, 16],
|
|
462
|
+
strokeStyle: LINE_COLOR
|
|
463
|
+
},
|
|
464
|
+
point: {
|
|
465
|
+
radius: .2,
|
|
466
|
+
fillStyle: POINT_COLOR
|
|
467
|
+
}
|
|
468
|
+
} : {
|
|
469
|
+
water: { fillStyle: "#C0DAE4" },
|
|
470
|
+
land: {
|
|
471
|
+
fillStyle: "#C2D8B4",
|
|
472
|
+
strokeStyle: "#A6C291"
|
|
473
|
+
},
|
|
474
|
+
line: {
|
|
475
|
+
lineWidth: 1.5,
|
|
476
|
+
lineDash: [4, 16],
|
|
477
|
+
strokeStyle: LINE_COLOR
|
|
478
|
+
},
|
|
479
|
+
point: {
|
|
480
|
+
radius: .2,
|
|
481
|
+
fillStyle: POINT_COLOR
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
//#endregion
|
|
485
|
+
//#region src/hooks/useDrag.ts
|
|
486
|
+
/**
|
|
487
|
+
* Allows user to drag globe.
|
|
488
|
+
*/
|
|
489
|
+
var useDrag = (controller, options = {}) => {
|
|
490
|
+
useEffect(() => {
|
|
491
|
+
const canvas = controller?.canvas;
|
|
492
|
+
if (!canvas || options.disabled) return;
|
|
493
|
+
const inertia = geoInertiaDrag(select(canvas), () => {
|
|
494
|
+
controller.setRotation(controller.projection.rotate());
|
|
495
|
+
options.onUpdate?.({
|
|
496
|
+
type: "move",
|
|
497
|
+
controller
|
|
498
|
+
});
|
|
499
|
+
}, controller.projection, {
|
|
500
|
+
lockTilt: options.lockTilt,
|
|
501
|
+
mode: options.mode,
|
|
502
|
+
sensitivity: options.sensitivity,
|
|
503
|
+
getZoom: () => controller.zoom,
|
|
504
|
+
time: 3e3,
|
|
505
|
+
start: () => options.onUpdate?.({
|
|
506
|
+
type: "start",
|
|
507
|
+
controller
|
|
508
|
+
}),
|
|
509
|
+
finish: () => options.onUpdate?.({
|
|
510
|
+
type: "end",
|
|
511
|
+
controller
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
return () => {
|
|
515
|
+
cancelDrag(select(canvas));
|
|
516
|
+
inertia?.timer?.stop();
|
|
517
|
+
};
|
|
518
|
+
}, [controller, JSON.stringify(options)]);
|
|
519
|
+
};
|
|
520
|
+
var cancelDrag = (node) => node.on(".drag", null);
|
|
521
|
+
//#endregion
|
|
522
|
+
//#region src/hooks/useGlobeZoomHandler.ts
|
|
523
|
+
var ZOOM_FACTOR = .1;
|
|
524
|
+
var useGlobeZoomHandler = (controller) => {
|
|
525
|
+
return useCallback((event) => {
|
|
526
|
+
if (!controller) return;
|
|
527
|
+
switch (event) {
|
|
528
|
+
case "zoom-in":
|
|
529
|
+
controller.setZoom((zoom) => {
|
|
530
|
+
return zoom * 1.1;
|
|
531
|
+
});
|
|
532
|
+
break;
|
|
533
|
+
case "zoom-out":
|
|
534
|
+
controller.setZoom((zoom) => {
|
|
535
|
+
return zoom * (1 - ZOOM_FACTOR);
|
|
536
|
+
});
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}, [controller]);
|
|
540
|
+
};
|
|
541
|
+
//#endregion
|
|
542
|
+
//#region src/hooks/useMapZoomHandler.ts
|
|
543
|
+
var useMapZoomHandler = (controller) => {
|
|
544
|
+
return useCallback((event) => {
|
|
545
|
+
if (!controller) return;
|
|
546
|
+
switch (event) {
|
|
547
|
+
case "zoom-in":
|
|
548
|
+
controller.setZoom((scale) => scale + 1);
|
|
549
|
+
break;
|
|
550
|
+
case "zoom-out":
|
|
551
|
+
controller.setZoom((scale) => scale - 1);
|
|
552
|
+
break;
|
|
553
|
+
}
|
|
554
|
+
}, [controller]);
|
|
555
|
+
};
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/hooks/useSimplifiedTopology.ts
|
|
558
|
+
var DEFAULT_TIERS$1 = [
|
|
559
|
+
{
|
|
560
|
+
minZoom: 0,
|
|
561
|
+
percentile: .95
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
minZoom: 2,
|
|
565
|
+
percentile: .85
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
minZoom: 4,
|
|
569
|
+
percentile: .6
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
minZoom: 7,
|
|
573
|
+
percentile: .3
|
|
574
|
+
},
|
|
575
|
+
{
|
|
576
|
+
minZoom: 12,
|
|
577
|
+
percentile: 0
|
|
578
|
+
}
|
|
579
|
+
];
|
|
580
|
+
var pickTier$1 = (zoom, tiers) => {
|
|
581
|
+
let match = tiers[0];
|
|
582
|
+
for (const tier of tiers) if (zoom >= tier.minZoom) match = tier;
|
|
583
|
+
return match;
|
|
584
|
+
};
|
|
585
|
+
/**
|
|
586
|
+
* Returns a simplified copy of `topology` whose detail tracks the current
|
|
587
|
+
* `zoom`. The source topology is annotated with point weights once via
|
|
588
|
+
* `presimplify`; subsequent zoom changes reuse that work and only re-run
|
|
589
|
+
* the cheap `simplify` pass against a tier-bucketed `minWeight`.
|
|
590
|
+
*
|
|
591
|
+
* Pass the highest-resolution source you have (typically `10m`) and let the
|
|
592
|
+
* hook decimate at low zoom — that's the cheap end of the curve.
|
|
593
|
+
*/
|
|
594
|
+
var useSimplifiedTopology = (topology, zoom, options = {}) => {
|
|
595
|
+
const { tiers = DEFAULT_TIERS$1 } = options;
|
|
596
|
+
const presimplified = useMemo(() => topology ? presimplify(topology) : void 0, [topology]);
|
|
597
|
+
const tier = pickTier$1(zoom, tiers);
|
|
598
|
+
return useMemo(() => {
|
|
599
|
+
if (!presimplified) return;
|
|
600
|
+
if (tier.percentile <= 0) return presimplified;
|
|
601
|
+
const minWeight = quantile(presimplified, tier.percentile);
|
|
602
|
+
return simplify(presimplified, minWeight);
|
|
603
|
+
}, [presimplified, tier]);
|
|
604
|
+
};
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/hooks/useSpinner.ts
|
|
607
|
+
/**
|
|
608
|
+
* Rotates globe.
|
|
609
|
+
*/
|
|
610
|
+
var useSpinner = (controller, options = {}) => {
|
|
611
|
+
const [running, setRunning] = useState(false);
|
|
612
|
+
useEffect(() => {
|
|
613
|
+
let timer;
|
|
614
|
+
const start = () => {
|
|
615
|
+
const delta = options.delta ?? [
|
|
616
|
+
.001,
|
|
617
|
+
0,
|
|
618
|
+
0
|
|
619
|
+
];
|
|
620
|
+
let t = 0;
|
|
621
|
+
let lastRotation = controller.projection.rotate();
|
|
622
|
+
timer = timer$1((elapsed) => {
|
|
623
|
+
const dt = elapsed - t;
|
|
624
|
+
t = elapsed;
|
|
625
|
+
const rotation = [
|
|
626
|
+
lastRotation[0] + delta[0] * dt,
|
|
627
|
+
lastRotation[1] + delta[1] * dt,
|
|
628
|
+
lastRotation[2] + delta[2] * dt
|
|
629
|
+
];
|
|
630
|
+
lastRotation = rotation;
|
|
631
|
+
controller.setRotation(rotation);
|
|
632
|
+
});
|
|
633
|
+
};
|
|
634
|
+
const stop = () => {
|
|
635
|
+
if (timer) {
|
|
636
|
+
timer.stop();
|
|
637
|
+
timer = void 0;
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
if (controller && running) start();
|
|
641
|
+
else stop();
|
|
642
|
+
return () => stop();
|
|
643
|
+
}, [controller, running]);
|
|
644
|
+
return [() => {
|
|
645
|
+
if (!options.disabled) setRunning(true);
|
|
646
|
+
}, () => setRunning(false)];
|
|
647
|
+
};
|
|
648
|
+
//#endregion
|
|
649
|
+
//#region src/hooks/useTopology.ts
|
|
650
|
+
var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-geo/src/hooks/useTopology.ts";
|
|
651
|
+
/** Default zoom buckets: 110m below zoom 3, 50m at zoom 3 and above. */
|
|
652
|
+
var DEFAULT_TIERS = [{
|
|
653
|
+
minZoom: 0,
|
|
654
|
+
level: "110m"
|
|
655
|
+
}];
|
|
656
|
+
var pickTier = (zoom, tiers) => {
|
|
657
|
+
let match = tiers[0];
|
|
658
|
+
for (const tier of tiers) if (zoom >= tier.minZoom) match = tier;
|
|
659
|
+
return match;
|
|
660
|
+
};
|
|
661
|
+
var topologyCache = /* @__PURE__ */ new Map();
|
|
662
|
+
/**
|
|
663
|
+
* Loads TopoJSON country data.
|
|
664
|
+
*
|
|
665
|
+
* - With no arguments, loads the default `110m` resolution.
|
|
666
|
+
* - With a `zoom`, loads the resolution matching the current zoom tier (progressive level-of-detail).
|
|
667
|
+
* Each resolution is a separate dynamic `import()`, so unused detail levels are never fetched, and
|
|
668
|
+
* while a heavier tier loads the previously-displayed topology stays on screen (no blank canvas).
|
|
669
|
+
*/
|
|
670
|
+
var useTopology = (zoom, options = {}) => {
|
|
671
|
+
const { tiers = DEFAULT_TIERS } = options;
|
|
672
|
+
const level = zoom === void 0 ? "110m" : pickTier(zoom, tiers).level;
|
|
673
|
+
const [topology, setTopology] = useState(() => topologyCache.get(level));
|
|
674
|
+
useEffect(() => {
|
|
675
|
+
const cached = topologyCache.get(level);
|
|
676
|
+
if (cached) {
|
|
677
|
+
setTopology(cached);
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
let disposed = false;
|
|
681
|
+
loadTopology(level).then((loaded) => {
|
|
682
|
+
topologyCache.set(level, loaded);
|
|
683
|
+
if (!disposed) setTopology(loaded);
|
|
684
|
+
}).catch((err) => {
|
|
685
|
+
if (!disposed) log.warn("failed to load topology", {
|
|
686
|
+
level,
|
|
687
|
+
err
|
|
688
|
+
}, {
|
|
689
|
+
"~LogMeta": "~LogMeta",
|
|
690
|
+
F: __dxlog_file,
|
|
691
|
+
L: 85,
|
|
692
|
+
S: void 0
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
return () => {
|
|
696
|
+
disposed = true;
|
|
697
|
+
};
|
|
698
|
+
}, [level]);
|
|
699
|
+
return topology;
|
|
700
|
+
};
|
|
701
|
+
//#endregion
|
|
702
|
+
//#region src/hooks/useTour.ts
|
|
703
|
+
var defaultDuration = 1500;
|
|
704
|
+
/**
|
|
705
|
+
* Iterates between points by chaining `controller.flyTo` calls, rendering
|
|
706
|
+
* an arc + cursor on the canvas per frame via the flyTo `onTick` hook.
|
|
707
|
+
* Inspired by: https://observablehq.com/@mbostock/top-100-cities
|
|
708
|
+
*/
|
|
709
|
+
var useTour = (controller, points, options = {}) => {
|
|
710
|
+
const [running, setRunning] = useState(options.running ?? false);
|
|
711
|
+
useEffect(() => {
|
|
712
|
+
if (!controller || !running) return;
|
|
713
|
+
let cancelled = false;
|
|
714
|
+
const t = setTimeout(async () => {
|
|
715
|
+
const { canvas, projection } = controller;
|
|
716
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
717
|
+
const path = geoPath(projection, context).pointRadius(2);
|
|
718
|
+
try {
|
|
719
|
+
const tourPoints = [...points];
|
|
720
|
+
if (options.loop) tourPoints.push(tourPoints[0]);
|
|
721
|
+
let last;
|
|
722
|
+
for (const next of tourPoints) {
|
|
723
|
+
if (cancelled) break;
|
|
724
|
+
const p1 = last ? geoToPosition(last) : void 0;
|
|
725
|
+
const p2 = geoToPosition(next);
|
|
726
|
+
const ip = geoInterpolate(p1 ?? p2, p2);
|
|
727
|
+
const onTick = (t) => {
|
|
728
|
+
const t1 = Math.max(0, Math.min(1, t * 2 - 1));
|
|
729
|
+
const t2 = Math.min(1, t * 2);
|
|
730
|
+
context.save();
|
|
731
|
+
try {
|
|
732
|
+
context.beginPath();
|
|
733
|
+
context.strokeStyle = options.styles?.arc?.strokeStyle ?? "yellow";
|
|
734
|
+
context.lineWidth = (options.styles?.arc?.lineWidth ?? 1.5) * (controller.zoom ?? 1);
|
|
735
|
+
context.setLineDash(options.styles?.arc?.lineDash ?? []);
|
|
736
|
+
path({
|
|
737
|
+
type: "LineString",
|
|
738
|
+
coordinates: [ip(t1), ip(t2)]
|
|
739
|
+
});
|
|
740
|
+
context.stroke();
|
|
741
|
+
context.beginPath();
|
|
742
|
+
context.fillStyle = options.styles?.cursor?.fillStyle ?? "orange";
|
|
743
|
+
path.pointRadius((options.styles?.cursor?.pointRadius ?? 2) * (controller.zoom ?? 1));
|
|
744
|
+
path({
|
|
745
|
+
type: "Point",
|
|
746
|
+
coordinates: ip(t2)
|
|
747
|
+
});
|
|
748
|
+
context.fill();
|
|
749
|
+
} finally {
|
|
750
|
+
context.restore();
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
await controller.flyTo(next, {
|
|
754
|
+
duration: options.duration ?? defaultDuration,
|
|
755
|
+
tilt: options.tilt ?? 0,
|
|
756
|
+
onTick
|
|
757
|
+
});
|
|
758
|
+
last = next;
|
|
759
|
+
}
|
|
760
|
+
} catch {} finally {
|
|
761
|
+
if (!cancelled) setRunning(false);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
return () => {
|
|
765
|
+
cancelled = true;
|
|
766
|
+
clearTimeout(t);
|
|
767
|
+
controller.cancelFlyTo();
|
|
768
|
+
};
|
|
769
|
+
}, [
|
|
770
|
+
controller,
|
|
771
|
+
running,
|
|
772
|
+
JSON.stringify(options)
|
|
773
|
+
]);
|
|
774
|
+
return [running, setRunning];
|
|
775
|
+
};
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/hooks/useWheel.ts
|
|
778
|
+
var DEFAULT_SENSITIVITY = .25;
|
|
779
|
+
var DEFAULT_ZOOM_SENSITIVITY = .01;
|
|
780
|
+
/**
|
|
781
|
+
* Map mouse-wheel / trackpad gestures to globe motion:
|
|
782
|
+
* - Scroll (or two-finger pan on a trackpad) rotates the globe:
|
|
783
|
+
* deltaY → phi (tilt around screen-X), deltaX → lambda (polar spin).
|
|
784
|
+
* - Pinch-to-zoom on a trackpad — and ctrl+scroll on a mouse — are
|
|
785
|
+
* delivered as `wheel` events with `ctrlKey: true`. Those are routed
|
|
786
|
+
* to `setZoom` instead so the gesture matches the user's intent.
|
|
787
|
+
*/
|
|
788
|
+
var useWheel = (controller, options = {}) => {
|
|
789
|
+
useEffect(() => {
|
|
790
|
+
const canvas = controller?.canvas;
|
|
791
|
+
if (!canvas || options.disabled) return;
|
|
792
|
+
const sensitivity = options.sensitivity ?? DEFAULT_SENSITIVITY;
|
|
793
|
+
const zoomSensitivity = options.zoomSensitivity ?? DEFAULT_ZOOM_SENSITIVITY;
|
|
794
|
+
const handleWheel = (event) => {
|
|
795
|
+
event.preventDefault();
|
|
796
|
+
if (event.ctrlKey) {
|
|
797
|
+
const factor = Math.exp(-event.deltaY * zoomSensitivity);
|
|
798
|
+
controller.setZoom(controller.zoom * factor);
|
|
799
|
+
} else {
|
|
800
|
+
const [lambda, phi, gamma] = controller.projection.rotate();
|
|
801
|
+
const k = sensitivity / Math.max(controller.zoom, .1);
|
|
802
|
+
const next = [
|
|
803
|
+
lambda - event.deltaX * k,
|
|
804
|
+
phi + event.deltaY * k,
|
|
805
|
+
gamma
|
|
806
|
+
];
|
|
807
|
+
controller.projection.rotate(next);
|
|
808
|
+
controller.setRotation(controller.projection.rotate());
|
|
809
|
+
}
|
|
810
|
+
options.onUpdate?.(controller);
|
|
811
|
+
};
|
|
812
|
+
canvas.addEventListener("wheel", handleWheel, { passive: false });
|
|
813
|
+
return () => {
|
|
814
|
+
canvas.removeEventListener("wheel", handleWheel);
|
|
815
|
+
};
|
|
816
|
+
}, [
|
|
817
|
+
controller,
|
|
818
|
+
options.disabled,
|
|
819
|
+
options.sensitivity,
|
|
820
|
+
options.zoomSensitivity,
|
|
821
|
+
options.onUpdate
|
|
822
|
+
]);
|
|
823
|
+
};
|
|
824
|
+
//#endregion
|
|
825
|
+
//#region src/components/Toolbar/control-positions.ts
|
|
826
|
+
var controlPositions = {
|
|
827
|
+
topleft: "top-2 left-2",
|
|
828
|
+
topright: "top-2 right-2",
|
|
829
|
+
bottomleft: "bottom-2 left-2",
|
|
830
|
+
bottomright: "bottom-2 right-2"
|
|
831
|
+
};
|
|
832
|
+
//#endregion
|
|
833
|
+
//#region src/components/Toolbar/Controls.tsx
|
|
834
|
+
var ZoomControls = ({ classNames, onAction }) => {
|
|
835
|
+
const { t } = useTranslation(translationKey);
|
|
836
|
+
return /* @__PURE__ */ jsxs(Toolbar.Root, {
|
|
837
|
+
classNames: ["gap-2", classNames],
|
|
838
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
839
|
+
icon: "ph--plus--regular",
|
|
840
|
+
iconOnly: true,
|
|
841
|
+
label: t("zoom-in-icon.button"),
|
|
842
|
+
onClick: () => onAction?.("zoom-in")
|
|
843
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
844
|
+
icon: "ph--minus--regular",
|
|
845
|
+
iconOnly: true,
|
|
846
|
+
label: t("zoom-out-icon.button"),
|
|
847
|
+
onClick: () => onAction?.("zoom-out")
|
|
848
|
+
})]
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
var ActionControls = ({ classNames, onAction }) => {
|
|
852
|
+
const { t } = useTranslation(translationKey);
|
|
853
|
+
return /* @__PURE__ */ jsxs(Toolbar.Root, {
|
|
854
|
+
classNames: ["gap-2", classNames],
|
|
855
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
856
|
+
icon: "ph--path--regular",
|
|
857
|
+
iconOnly: true,
|
|
858
|
+
label: t("start-icon.button"),
|
|
859
|
+
onClick: () => onAction?.("start")
|
|
860
|
+
}), /* @__PURE__ */ jsx(IconButton, {
|
|
861
|
+
icon: "ph--globe-hemisphere-west--regular",
|
|
862
|
+
iconOnly: true,
|
|
863
|
+
label: t("toggle-icon.button"),
|
|
864
|
+
onClick: () => onAction?.("toggle")
|
|
865
|
+
})]
|
|
866
|
+
});
|
|
867
|
+
};
|
|
868
|
+
//#endregion
|
|
869
|
+
//#region src/components/Globe/Globe.tsx
|
|
870
|
+
/**
|
|
871
|
+
* https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
|
|
872
|
+
*/
|
|
873
|
+
var defaultStyles = {
|
|
874
|
+
light: {
|
|
875
|
+
background: { fillStyle: "#EEE" },
|
|
876
|
+
water: { fillStyle: "#555" },
|
|
877
|
+
land: { fillStyle: "#999" },
|
|
878
|
+
line: { strokeStyle: "darkred" },
|
|
879
|
+
point: {
|
|
880
|
+
fillStyle: "#111111",
|
|
881
|
+
strokeStyle: "#111111",
|
|
882
|
+
strokeWidth: 1,
|
|
883
|
+
pointRadius: .5
|
|
884
|
+
}
|
|
885
|
+
},
|
|
886
|
+
dark: {
|
|
887
|
+
background: { fillStyle: "#111111" },
|
|
888
|
+
water: { fillStyle: "#123E6A" },
|
|
889
|
+
land: { fillStyle: "#032153" },
|
|
890
|
+
line: { strokeStyle: "#111111" },
|
|
891
|
+
point: {
|
|
892
|
+
fillStyle: "#111111",
|
|
893
|
+
strokeStyle: "#111111",
|
|
894
|
+
strokeWidth: 1,
|
|
895
|
+
pointRadius: .5
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
var projectionMap = {
|
|
900
|
+
"orthographic": geoOrthographic,
|
|
901
|
+
"mercator": geoMercator,
|
|
902
|
+
"transverse-mercator": geoTransverseMercator
|
|
903
|
+
};
|
|
904
|
+
var getProjection = (type = "orthographic") => {
|
|
905
|
+
if (typeof type === "string") return (projectionMap[type] ?? geoOrthographic)();
|
|
906
|
+
return type ?? geoOrthographic();
|
|
907
|
+
};
|
|
908
|
+
var DEFAULT_ZOOM = 1.5;
|
|
909
|
+
/**
|
|
910
|
+
* Headless context/state provider for the globe. Renders no DOM; wrap a `Globe.Viewport` to mount
|
|
911
|
+
* the measured container. The current `GlobeController` (built by `Globe.Canvas`) is exposed via
|
|
912
|
+
* this component's `ref`.
|
|
913
|
+
*/
|
|
914
|
+
var GlobeRoot = forwardRef(({ children, center: centerProp, zoom: zoomProp = DEFAULT_ZOOM, translation: translationProp, rotation: rotationProp }, forwardedRef) => {
|
|
915
|
+
const [size, setSize] = useState({
|
|
916
|
+
width: 0,
|
|
917
|
+
height: 0
|
|
918
|
+
});
|
|
919
|
+
const [center, setCenter] = useControlledState(centerProp);
|
|
920
|
+
const [zoom, setZoom] = useControlledState(zoomProp);
|
|
921
|
+
const [translation, setTranslation] = useControlledState(translationProp);
|
|
922
|
+
const [rotation, setRotation] = useControlledState(rotationProp);
|
|
923
|
+
const [controller, setController] = useState(null);
|
|
924
|
+
const registerController = useCallback((next) => setController(next), []);
|
|
925
|
+
useImperativeHandle(forwardedRef, () => controller, [controller]);
|
|
926
|
+
return /* @__PURE__ */ jsx(GlobeContext.Provider, {
|
|
927
|
+
value: {
|
|
928
|
+
size,
|
|
929
|
+
center,
|
|
930
|
+
zoom,
|
|
931
|
+
translation,
|
|
932
|
+
rotation,
|
|
933
|
+
setSize,
|
|
934
|
+
setCenter,
|
|
935
|
+
setZoom,
|
|
936
|
+
setTranslation,
|
|
937
|
+
setRotation,
|
|
938
|
+
registerController
|
|
939
|
+
},
|
|
940
|
+
children
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
GlobeRoot.displayName = "Globe.Root";
|
|
944
|
+
/**
|
|
945
|
+
* Measured container for the globe. Renders the `relative dx-container` div, observes its size, and
|
|
946
|
+
* publishes measurements to the context so `Globe.Canvas` can size the canvas.
|
|
947
|
+
*/
|
|
948
|
+
var GlobeViewport = composable(({ children, ...props }, forwardedRef) => {
|
|
949
|
+
const { setSize } = useGlobeContext();
|
|
950
|
+
const localRef = useRef(null);
|
|
951
|
+
const composedRef = useComposedRefs(localRef, forwardedRef);
|
|
952
|
+
const { width, height } = useResizeDetector({ targetRef: localRef });
|
|
953
|
+
useEffect(() => {
|
|
954
|
+
setSize({
|
|
955
|
+
width: width ?? 0,
|
|
956
|
+
height: height ?? 0
|
|
957
|
+
});
|
|
958
|
+
}, [
|
|
959
|
+
width,
|
|
960
|
+
height,
|
|
961
|
+
setSize
|
|
962
|
+
]);
|
|
963
|
+
return /* @__PURE__ */ jsx("div", {
|
|
964
|
+
...composableProps(props, { classNames: "relative dx-container" }),
|
|
965
|
+
ref: composedRef,
|
|
966
|
+
children
|
|
967
|
+
});
|
|
968
|
+
});
|
|
969
|
+
GlobeViewport.displayName = "Globe.Viewport";
|
|
970
|
+
/**
|
|
971
|
+
* Basic globe renderer. Builds the imperative `GlobeController` and registers it with `Globe.Root`
|
|
972
|
+
* (via `registerController` from context) so consumers reading the controller from Root's ref get
|
|
973
|
+
* the live instance.
|
|
974
|
+
* https://github.com/topojson/world-atlas
|
|
975
|
+
*/
|
|
976
|
+
var GlobeCanvas = ({ projection: projectionProp, topology, features, styles: stylesProp }) => {
|
|
977
|
+
const { themeMode } = useThemeContext();
|
|
978
|
+
const styles = useMemo(() => stylesProp ?? defaultStyles[themeMode], [stylesProp, themeMode]);
|
|
979
|
+
const { size, center, zoom, translation, rotation, setZoom, setTranslation, setRotation, registerController } = useGlobeContext();
|
|
980
|
+
const zoomRef = useDynamicRef(zoom);
|
|
981
|
+
const [canvas, setCanvas] = useState(null);
|
|
982
|
+
const canvasRef = (canvas) => setCanvas(canvas);
|
|
983
|
+
const projection = useMemo(() => getProjection(projectionProp), [projectionProp]);
|
|
984
|
+
const layers = useMemo(() => {
|
|
985
|
+
return timer(() => createLayers(topology, features, styles));
|
|
986
|
+
}, [
|
|
987
|
+
topology,
|
|
988
|
+
features,
|
|
989
|
+
styles
|
|
990
|
+
]);
|
|
991
|
+
useEffect(() => {
|
|
992
|
+
if (center) setRotation(positionToRotation(geoToPosition(center)));
|
|
993
|
+
}, [center]);
|
|
994
|
+
const flyToSelection = useMemo(() => selection(), []);
|
|
995
|
+
const flyToTransitionName = `globe-fly-to-${useId()}`;
|
|
996
|
+
useEffect(() => () => {
|
|
997
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
998
|
+
}, [flyToSelection, flyToTransitionName]);
|
|
999
|
+
const zooming = useRef(false);
|
|
1000
|
+
const controller = useMemo(() => {
|
|
1001
|
+
return {
|
|
1002
|
+
canvas,
|
|
1003
|
+
projection,
|
|
1004
|
+
get zoom() {
|
|
1005
|
+
return zoomRef.current;
|
|
1006
|
+
},
|
|
1007
|
+
translation,
|
|
1008
|
+
rotation,
|
|
1009
|
+
setZoom: (state) => {
|
|
1010
|
+
if (typeof state === "function") {
|
|
1011
|
+
const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
|
|
1012
|
+
transition().ease(zooming.current ? easeLinear : easeSinOut).duration(200).tween("scale", () => (t) => setZoom(is(t))).on("end", () => {
|
|
1013
|
+
zooming.current = false;
|
|
1014
|
+
});
|
|
1015
|
+
} else setZoom(state);
|
|
1016
|
+
},
|
|
1017
|
+
setTranslation,
|
|
1018
|
+
setRotation,
|
|
1019
|
+
flyTo: (target, options = {}) => {
|
|
1020
|
+
const { duration = 1200, tilt = 0, onTick } = options;
|
|
1021
|
+
const p2 = geoToPosition(target);
|
|
1022
|
+
const r1 = projection.rotate();
|
|
1023
|
+
const r2 = positionToRotation(p2, tilt);
|
|
1024
|
+
const p1 = [-r1[0], -r1[1]];
|
|
1025
|
+
const rotationTween = createRotationTween(projection, setRotation, r1, r2);
|
|
1026
|
+
const iz = target.zoom !== void 0 ? interpolateNumber(zoomRef.current, target.zoom) : void 0;
|
|
1027
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
1028
|
+
const tx = flyToSelection.transition(flyToTransitionName).duration(flyDuration(p1, p2, duration, 1500));
|
|
1029
|
+
if (onTick) tx.tween("flyToOnTick", () => onTick);
|
|
1030
|
+
tx.tween("flyToRotation", () => rotationTween);
|
|
1031
|
+
if (iz) tx.tween("flyToZoom", () => (t) => setZoom(iz(t)));
|
|
1032
|
+
return tx.end();
|
|
1033
|
+
},
|
|
1034
|
+
cancelFlyTo: () => {
|
|
1035
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
}, [
|
|
1039
|
+
canvas,
|
|
1040
|
+
projection,
|
|
1041
|
+
flyToSelection,
|
|
1042
|
+
flyToTransitionName
|
|
1043
|
+
]);
|
|
1044
|
+
useEffect(() => {
|
|
1045
|
+
registerController(controller);
|
|
1046
|
+
return () => registerController(null);
|
|
1047
|
+
}, [registerController, controller]);
|
|
1048
|
+
const generator = useMemo(() => canvas && projection && geoPath(projection, canvas.getContext("2d")), [canvas, projection]);
|
|
1049
|
+
useEffect(() => {
|
|
1050
|
+
if (canvas && projection) timer(() => {
|
|
1051
|
+
projection.scale(Math.min(size.width, size.height) / 2 * zoom).translate([size.width / 2 + (translation?.x ?? 0), size.height / 2 + (translation?.y ?? 0)]).rotate(rotation ?? [
|
|
1052
|
+
0,
|
|
1053
|
+
0,
|
|
1054
|
+
0
|
|
1055
|
+
]);
|
|
1056
|
+
const isOrthographic = !projectionProp || projectionProp === "orthographic";
|
|
1057
|
+
const [lambda, phi] = rotation ?? [
|
|
1058
|
+
0,
|
|
1059
|
+
0,
|
|
1060
|
+
0
|
|
1061
|
+
];
|
|
1062
|
+
const viewCenter = isOrthographic ? [-lambda, -phi] : void 0;
|
|
1063
|
+
renderLayers(generator, layers, zoom, styles, viewCenter);
|
|
1064
|
+
});
|
|
1065
|
+
}, [
|
|
1066
|
+
generator,
|
|
1067
|
+
size,
|
|
1068
|
+
zoom,
|
|
1069
|
+
translation,
|
|
1070
|
+
rotation,
|
|
1071
|
+
layers,
|
|
1072
|
+
projectionProp
|
|
1073
|
+
]);
|
|
1074
|
+
if (!size.width || !size.height) return null;
|
|
1075
|
+
return /* @__PURE__ */ jsx("canvas", {
|
|
1076
|
+
ref: canvasRef,
|
|
1077
|
+
className: "bg-base-surface",
|
|
1078
|
+
width: size.width,
|
|
1079
|
+
height: size.height
|
|
1080
|
+
});
|
|
1081
|
+
};
|
|
1082
|
+
GlobeCanvas.displayName = "Globe.Canvas";
|
|
1083
|
+
var GlobeDebug = ({ position = "topleft" }) => {
|
|
1084
|
+
const { size, zoom, translation, rotation } = useGlobeContext();
|
|
1085
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1086
|
+
className: mx("z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm", controlPositions[position]),
|
|
1087
|
+
children: /* @__PURE__ */ jsx("pre", {
|
|
1088
|
+
className: "font-mono text-xs text-green-700",
|
|
1089
|
+
children: JSON.stringify({
|
|
1090
|
+
size,
|
|
1091
|
+
zoom,
|
|
1092
|
+
translation,
|
|
1093
|
+
rotation
|
|
1094
|
+
}, null, 2)
|
|
1095
|
+
})
|
|
1096
|
+
});
|
|
1097
|
+
};
|
|
1098
|
+
var GlobePanel = ({ position, classNames, children }) => {
|
|
1099
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1100
|
+
className: mx("z-10 absolute overflow-hidden", controlPositions[position], classNames),
|
|
1101
|
+
children
|
|
1102
|
+
});
|
|
1103
|
+
};
|
|
1104
|
+
var CustomControl$1 = ({ position, children }) => {
|
|
1105
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1106
|
+
className: mx("z-10 absolute overflow-hidden", controlPositions[position]),
|
|
1107
|
+
children
|
|
1108
|
+
});
|
|
1109
|
+
};
|
|
1110
|
+
var GlobeZoom = ({ onAction, position = "bottomleft", ...props }) => /* @__PURE__ */ jsx(CustomControl$1, {
|
|
1111
|
+
position,
|
|
1112
|
+
...props,
|
|
1113
|
+
children: /* @__PURE__ */ jsx(ZoomControls, { onAction })
|
|
1114
|
+
});
|
|
1115
|
+
var GlobeAction = ({ onAction, position = "bottomright", ...props }) => /* @__PURE__ */ jsx(CustomControl$1, {
|
|
1116
|
+
position,
|
|
1117
|
+
...props,
|
|
1118
|
+
children: /* @__PURE__ */ jsx(ActionControls, { onAction })
|
|
1119
|
+
});
|
|
1120
|
+
var Globe = {
|
|
1121
|
+
Root: GlobeRoot,
|
|
1122
|
+
Viewport: GlobeViewport,
|
|
1123
|
+
Canvas: GlobeCanvas,
|
|
1124
|
+
Zoom: GlobeZoom,
|
|
1125
|
+
Action: GlobeAction,
|
|
1126
|
+
Debug: GlobeDebug,
|
|
1127
|
+
Panel: GlobePanel
|
|
1128
|
+
};
|
|
1129
|
+
//#endregion
|
|
1130
|
+
//#region src/components/Map/Map.tsx
|
|
1131
|
+
var defaults = {
|
|
1132
|
+
center: {
|
|
1133
|
+
lat: 51,
|
|
1134
|
+
lng: 0
|
|
1135
|
+
},
|
|
1136
|
+
zoom: 4
|
|
1137
|
+
};
|
|
1138
|
+
var [MapContextProvider, useMapContext] = createContext$1("Map");
|
|
1139
|
+
/**
|
|
1140
|
+
* Context provider for the map. Must wrap Map.Viewport. The ref exposes a {@link MapController}.
|
|
1141
|
+
*/
|
|
1142
|
+
var MapRoot = forwardRef(({ children, onChange }, forwardedRef) => {
|
|
1143
|
+
const mapRef = useRef(null);
|
|
1144
|
+
const registerMap = useCallback((map) => {
|
|
1145
|
+
mapRef.current = map;
|
|
1146
|
+
}, []);
|
|
1147
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
1148
|
+
getCenter: () => {
|
|
1149
|
+
const center = mapRef.current?.getCenter();
|
|
1150
|
+
return center ? {
|
|
1151
|
+
lat: center.lat,
|
|
1152
|
+
lng: center.lng
|
|
1153
|
+
} : void 0;
|
|
1154
|
+
},
|
|
1155
|
+
getZoom: () => mapRef.current?.getZoom(),
|
|
1156
|
+
setCenter: (center, zoom) => {
|
|
1157
|
+
mapRef.current?.setView(center, zoom);
|
|
1158
|
+
},
|
|
1159
|
+
setZoom: (cb) => {
|
|
1160
|
+
mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
|
|
1161
|
+
}
|
|
1162
|
+
}), []);
|
|
1163
|
+
return /* @__PURE__ */ jsx(MapContextProvider, {
|
|
1164
|
+
attention: false,
|
|
1165
|
+
onChange,
|
|
1166
|
+
registerMap,
|
|
1167
|
+
children
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
MapRoot.displayName = "Map.Root";
|
|
1171
|
+
/**
|
|
1172
|
+
* https://react-leaflet.js.org/docs/api-map
|
|
1173
|
+
*/
|
|
1174
|
+
var MAP_VIEWPORT_NAME = "Map.Viewport";
|
|
1175
|
+
/**
|
|
1176
|
+
* Recalculates the leaflet map size when its container resizes (e.g. a companion
|
|
1177
|
+
* panel opening/closing). Without this, leaflet keeps its stale size and renders
|
|
1178
|
+
* blank/gray tiles in the newly-exposed area until the next pan/zoom. Coalesced
|
|
1179
|
+
* via rAF to avoid ResizeObserver feedback loops.
|
|
1180
|
+
*/
|
|
1181
|
+
var MapResize = () => {
|
|
1182
|
+
const map = useMap();
|
|
1183
|
+
useEffect(() => {
|
|
1184
|
+
const container = map.getContainer();
|
|
1185
|
+
let frame = 0;
|
|
1186
|
+
const observer = new ResizeObserver(() => {
|
|
1187
|
+
cancelAnimationFrame(frame);
|
|
1188
|
+
frame = requestAnimationFrame(() => map.invalidateSize());
|
|
1189
|
+
});
|
|
1190
|
+
observer.observe(container);
|
|
1191
|
+
return () => {
|
|
1192
|
+
cancelAnimationFrame(frame);
|
|
1193
|
+
observer.disconnect();
|
|
1194
|
+
};
|
|
1195
|
+
}, [map]);
|
|
1196
|
+
return null;
|
|
1197
|
+
};
|
|
1198
|
+
/**
|
|
1199
|
+
* Enables pinch-to-zoom on trackpads / ctrl+wheel. Browsers deliver a trackpad pinch as a `wheel`
|
|
1200
|
+
* event with `ctrlKey` set; Leaflet only zooms those via `scrollWheelZoom`, which is intentionally
|
|
1201
|
+
* off here (so plain scrolling doesn't hijack the page). This handler zooms on the pinch gesture
|
|
1202
|
+
* only, leaving normal wheel scrolling untouched. (Touchscreen pinch is handled by Leaflet's
|
|
1203
|
+
* `touchZoom`.)
|
|
1204
|
+
*/
|
|
1205
|
+
var PINCH_ZOOM_SENSITIVITY = .03;
|
|
1206
|
+
var MapPinchZoom = () => {
|
|
1207
|
+
const map = useMap();
|
|
1208
|
+
useEffect(() => {
|
|
1209
|
+
const container = map.getContainer();
|
|
1210
|
+
let frame = 0;
|
|
1211
|
+
let point;
|
|
1212
|
+
let target;
|
|
1213
|
+
const onWheel = (event) => {
|
|
1214
|
+
if (!event.ctrlKey) return;
|
|
1215
|
+
event.preventDefault();
|
|
1216
|
+
const rect = container.getBoundingClientRect();
|
|
1217
|
+
point = L.point(event.clientX - rect.left, event.clientY - rect.top);
|
|
1218
|
+
target = (target ?? map.getZoom()) - event.deltaY * PINCH_ZOOM_SENSITIVITY;
|
|
1219
|
+
if (!frame) frame = requestAnimationFrame(() => {
|
|
1220
|
+
frame = 0;
|
|
1221
|
+
if (target !== void 0 && point) {
|
|
1222
|
+
map.setZoomAround(point, target, { animate: false });
|
|
1223
|
+
target = void 0;
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
};
|
|
1227
|
+
container.addEventListener("wheel", onWheel, { passive: false });
|
|
1228
|
+
return () => {
|
|
1229
|
+
container.removeEventListener("wheel", onWheel);
|
|
1230
|
+
cancelAnimationFrame(frame);
|
|
1231
|
+
};
|
|
1232
|
+
}, [map]);
|
|
1233
|
+
return null;
|
|
1234
|
+
};
|
|
1235
|
+
/**
|
|
1236
|
+
* Map.Viewport is the focusable Leaflet frame. It can be the target of a parent `<Panel.Content asChild>`
|
|
1237
|
+
* (Slot), so it reconciles an injected `className` via `composableProps`. Leaflet owns the underlying
|
|
1238
|
+
* container element, so the forwarded DOM ref can't be attached and is intentionally unused.
|
|
1239
|
+
*/
|
|
1240
|
+
var MapViewport = composable((props, _forwardedRef) => {
|
|
1241
|
+
const { scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, whenReady, children, ...rest } = props;
|
|
1242
|
+
const { attention, registerMap } = useMapContext(MAP_VIEWPORT_NAME);
|
|
1243
|
+
const [map, setMap] = useState(null);
|
|
1244
|
+
const setMapRef = useCallback((next) => {
|
|
1245
|
+
setMap(next);
|
|
1246
|
+
registerMap(next);
|
|
1247
|
+
}, [registerMap]);
|
|
1248
|
+
useEffect(() => {
|
|
1249
|
+
if (!map) return;
|
|
1250
|
+
if (attention) map.scrollWheelZoom.enable();
|
|
1251
|
+
else map.scrollWheelZoom.disable();
|
|
1252
|
+
}, [map, attention]);
|
|
1253
|
+
return /* @__PURE__ */ jsxs(MapContainer, {
|
|
1254
|
+
...composableProps(rest, { classNames: "dx-container group relative grid dx-focus-ring-inset bg-base-surface!" }),
|
|
1255
|
+
attributionControl: false,
|
|
1256
|
+
zoomControl: false,
|
|
1257
|
+
scrollWheelZoom,
|
|
1258
|
+
doubleClickZoom,
|
|
1259
|
+
touchZoom,
|
|
1260
|
+
zoomSnap: 0,
|
|
1261
|
+
center: center ?? defaults.center,
|
|
1262
|
+
zoom: zoom ?? defaults.zoom,
|
|
1263
|
+
whenReady,
|
|
1264
|
+
ref: setMapRef,
|
|
1265
|
+
children: [
|
|
1266
|
+
/* @__PURE__ */ jsx(MapResize, {}),
|
|
1267
|
+
/* @__PURE__ */ jsx(MapPinchZoom, {}),
|
|
1268
|
+
children
|
|
1269
|
+
]
|
|
1270
|
+
});
|
|
1271
|
+
});
|
|
1272
|
+
MapViewport.displayName = "Map.Viewport";
|
|
1273
|
+
var MAP_TILES_NAME = "Map.Tiles";
|
|
1274
|
+
/** Default OpenStreetMap raster tile template. */
|
|
1275
|
+
var DEFAULT_TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
1276
|
+
var MapTiles = ({ url = DEFAULT_TILE_URL }) => {
|
|
1277
|
+
const ref = useRef(null);
|
|
1278
|
+
const { onChange } = useMapContext(MAP_TILES_NAME);
|
|
1279
|
+
useMapEvents({ moveend: (ev) => {
|
|
1280
|
+
onChange?.({
|
|
1281
|
+
center: ev.target.getCenter(),
|
|
1282
|
+
zoom: ev.target.getZoom()
|
|
1283
|
+
});
|
|
1284
|
+
} });
|
|
1285
|
+
const { attention } = useMapContext(MAP_TILES_NAME);
|
|
1286
|
+
useEffect(() => {
|
|
1287
|
+
if (ref.current) ref.current.getContainer().dataset.attention = attention ? "1" : "0";
|
|
1288
|
+
}, [attention]);
|
|
1289
|
+
return /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(TileLayer, {
|
|
1290
|
+
ref,
|
|
1291
|
+
"data-attention": attention,
|
|
1292
|
+
detectRetina: true,
|
|
1293
|
+
className: "dark:grayscale dark:invert data-[attention=\"0\"]:!opacity-80",
|
|
1294
|
+
url,
|
|
1295
|
+
keepBuffer: 4
|
|
1296
|
+
}) });
|
|
1297
|
+
};
|
|
1298
|
+
MapTiles.displayName = MAP_TILES_NAME;
|
|
1299
|
+
var MapMarkers = ({ selected, markers, lines, onSelect }) => {
|
|
1300
|
+
const map = useMap();
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
const points = [...markers?.map((marker) => marker.location) ?? [], ...lines?.flatMap((line) => [line.source, line.target]) ?? []];
|
|
1303
|
+
if (points.length > 0) {
|
|
1304
|
+
const bounds = latLngBounds(points);
|
|
1305
|
+
const size = map.getSize();
|
|
1306
|
+
const padding = Math.max(48, Math.min(size.x, size.y) / 6);
|
|
1307
|
+
map.fitBounds(bounds, {
|
|
1308
|
+
padding: point(padding, padding),
|
|
1309
|
+
animate: false
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
}, [
|
|
1313
|
+
markers,
|
|
1314
|
+
lines,
|
|
1315
|
+
map
|
|
1316
|
+
]);
|
|
1317
|
+
return /* @__PURE__ */ jsx(Fragment, { children: markers?.map(({ id, title, location: { lat, lng } }) => {
|
|
1318
|
+
return /* @__PURE__ */ jsx(Marker, {
|
|
1319
|
+
position: {
|
|
1320
|
+
lat,
|
|
1321
|
+
lng
|
|
1322
|
+
},
|
|
1323
|
+
eventHandlers: onSelect ? { click: () => onSelect(id) } : void 0,
|
|
1324
|
+
icon: new L.Icon({
|
|
1325
|
+
iconUrl: "https://dxos.network/marker-icon.png",
|
|
1326
|
+
iconRetinaUrl: "https://dxos.network/marker-icon-2x.png",
|
|
1327
|
+
shadowUrl: "https://dxos.network/marker-shadow.png",
|
|
1328
|
+
iconSize: [25, 41],
|
|
1329
|
+
iconAnchor: [12, 41],
|
|
1330
|
+
popupAnchor: [1, -34],
|
|
1331
|
+
shadowSize: [41, 41]
|
|
1332
|
+
}),
|
|
1333
|
+
children: title && /* @__PURE__ */ jsx(Popup, { children: title })
|
|
1334
|
+
}, id);
|
|
1335
|
+
}) });
|
|
1336
|
+
};
|
|
1337
|
+
MapMarkers.displayName = "Map.Markers";
|
|
1338
|
+
var MapLines = ({ lines }) => {
|
|
1339
|
+
if (!lines || lines.length === 0) return null;
|
|
1340
|
+
const polylines = [];
|
|
1341
|
+
for (const { source, target, color } of lines) {
|
|
1342
|
+
const last = polylines[polylines.length - 1];
|
|
1343
|
+
const lastPos = last?.positions[last.positions.length - 1];
|
|
1344
|
+
if (last && last.color === color && lastPos?.lat === source.lat && lastPos?.lng === source.lng) last.positions.push(target);
|
|
1345
|
+
else polylines.push({
|
|
1346
|
+
positions: [source, target],
|
|
1347
|
+
color
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
return /* @__PURE__ */ jsx(Fragment, { children: polylines.map(({ positions, color }, index) => /* @__PURE__ */ jsx(Polyline, {
|
|
1351
|
+
positions,
|
|
1352
|
+
pathOptions: {
|
|
1353
|
+
color,
|
|
1354
|
+
weight: 4,
|
|
1355
|
+
opacity: .8
|
|
1356
|
+
}
|
|
1357
|
+
}, index)) });
|
|
1358
|
+
};
|
|
1359
|
+
MapLines.displayName = "Map.Lines";
|
|
1360
|
+
var CustomControl = ({ position, children }) => {
|
|
1361
|
+
const map = useMap();
|
|
1362
|
+
const rootRef = useRef(void 0);
|
|
1363
|
+
useEffect(() => {
|
|
1364
|
+
const control = new Control({ position });
|
|
1365
|
+
control.onAdd = () => {
|
|
1366
|
+
const container = DomUtil.create("div", mx("m-0!", controlPositions[position]));
|
|
1367
|
+
DomEvent.disableClickPropagation(container);
|
|
1368
|
+
DomEvent.disableScrollPropagation(container);
|
|
1369
|
+
const root = createRoot(container);
|
|
1370
|
+
rootRef.current = root;
|
|
1371
|
+
root.render(/* @__PURE__ */ jsx(ThemeProvider, {
|
|
1372
|
+
tx: defaultTx,
|
|
1373
|
+
children: /* @__PURE__ */ jsx(Tooltip.Provider, { children })
|
|
1374
|
+
}));
|
|
1375
|
+
return container;
|
|
1376
|
+
};
|
|
1377
|
+
control.addTo(map);
|
|
1378
|
+
return () => {
|
|
1379
|
+
control.remove();
|
|
1380
|
+
const root = rootRef.current;
|
|
1381
|
+
rootRef.current = void 0;
|
|
1382
|
+
queueMicrotask(() => root?.unmount());
|
|
1383
|
+
};
|
|
1384
|
+
}, [map, position]);
|
|
1385
|
+
useEffect(() => {
|
|
1386
|
+
rootRef.current?.render(/* @__PURE__ */ jsx(ThemeProvider, {
|
|
1387
|
+
tx: defaultTx,
|
|
1388
|
+
children: /* @__PURE__ */ jsx(Tooltip.Provider, { children })
|
|
1389
|
+
}));
|
|
1390
|
+
}, [children]);
|
|
1391
|
+
return null;
|
|
1392
|
+
};
|
|
1393
|
+
var MapZoom = ({ onAction, position = "bottomleft", ...props }) => /* @__PURE__ */ jsx(CustomControl, {
|
|
1394
|
+
position,
|
|
1395
|
+
...props,
|
|
1396
|
+
children: /* @__PURE__ */ jsx(ZoomControls, { onAction })
|
|
1397
|
+
});
|
|
1398
|
+
var MapAction = ({ onAction, position = "bottomright", ...props }) => /* @__PURE__ */ jsx(CustomControl, {
|
|
1399
|
+
position,
|
|
1400
|
+
...props,
|
|
1401
|
+
children: /* @__PURE__ */ jsx(ActionControls, { onAction })
|
|
1402
|
+
});
|
|
1403
|
+
var Map$1 = {
|
|
1404
|
+
Root: MapRoot,
|
|
1405
|
+
Viewport: MapViewport,
|
|
1406
|
+
Tiles: MapTiles,
|
|
1407
|
+
Markers: MapMarkers,
|
|
1408
|
+
Lines: MapLines,
|
|
1409
|
+
Zoom: MapZoom,
|
|
1410
|
+
Action: MapAction
|
|
1411
|
+
};
|
|
1412
|
+
//#endregion
|
|
1413
|
+
export { ActionControls, DEFAULT_TILE_URL, Globe, GlobeContext, Map$1 as Map, ZoomControls, closestPoint, controlPositions, createLayers, createRotationTween, flyDuration, geoCircle, geoInertiaDrag, geoLine, geoPoint, geoToPosition, getDistance, globeStyles, positionToRotation, renderLayers, restrictAxis, timer, useDrag, useGlobeContext, useGlobeZoomHandler, useMapZoomHandler, useSimplifiedTopology, useSpinner, useTopology, useTour, useWheel };
|
|
1414
|
+
|
|
1415
|
+
//# sourceMappingURL=index.mjs.map
|