@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.
Files changed (57) hide show
  1. package/dist/lib/chunk-countries-10m.mjs +10 -0
  2. package/dist/lib/chunk-countries-10m.mjs.map +1 -0
  3. package/dist/lib/chunk-countries-110m.mjs +10 -0
  4. package/dist/lib/chunk-countries-110m.mjs.map +1 -0
  5. package/dist/lib/chunk-countries-50m.mjs +10 -0
  6. package/dist/lib/chunk-countries-50m.mjs.map +1 -0
  7. package/dist/lib/data.mjs +12 -0
  8. package/dist/lib/data.mjs.map +1 -0
  9. package/dist/lib/index.mjs +1415 -0
  10. package/dist/lib/index.mjs.map +1 -0
  11. package/dist/lib/translations.mjs +12 -0
  12. package/dist/lib/translations.mjs.map +1 -0
  13. package/dist/types/src/components/Globe/Globe.d.ts +1 -1
  14. package/dist/types/src/components/Map/Map.d.ts +1 -1
  15. package/dist/types/src/components/Toolbar/Controls.d.ts +0 -2
  16. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  17. package/dist/types/src/components/Toolbar/control-positions.d.ts +3 -0
  18. package/dist/types/src/components/Toolbar/control-positions.d.ts.map +1 -0
  19. package/dist/types/src/components/Toolbar/index.d.ts +1 -0
  20. package/dist/types/src/components/Toolbar/index.d.ts.map +1 -1
  21. package/dist/types/src/hooks/context.d.ts.map +1 -1
  22. package/dist/types/tsconfig.tsbuildinfo +1 -1
  23. package/package.json +14 -17
  24. package/src/components/Toolbar/Controls.tsx +0 -8
  25. package/src/components/Toolbar/control-positions.ts +16 -0
  26. package/src/components/Toolbar/index.ts +2 -0
  27. package/dist/lib/browser/chunk-SC2FBYFU.mjs +0 -17
  28. package/dist/lib/browser/chunk-SC2FBYFU.mjs.map +0 -7
  29. package/dist/lib/browser/countries-10m-CWWDOKH7.mjs +0 -6
  30. package/dist/lib/browser/countries-10m-CWWDOKH7.mjs.map +0 -7
  31. package/dist/lib/browser/countries-110m-72QBAA5E.mjs +0 -6
  32. package/dist/lib/browser/countries-110m-72QBAA5E.mjs.map +0 -7
  33. package/dist/lib/browser/countries-50m-H7SL7KVF.mjs +0 -6
  34. package/dist/lib/browser/countries-50m-H7SL7KVF.mjs.map +0 -7
  35. package/dist/lib/browser/data.mjs +0 -7
  36. package/dist/lib/browser/data.mjs.map +0 -7
  37. package/dist/lib/browser/index.mjs +0 -1666
  38. package/dist/lib/browser/index.mjs.map +0 -7
  39. package/dist/lib/browser/meta.json +0 -1
  40. package/dist/lib/browser/translations.mjs +0 -19
  41. package/dist/lib/browser/translations.mjs.map +0 -7
  42. package/dist/lib/node-esm/chunk-VZENBYLJ.mjs +0 -19
  43. package/dist/lib/node-esm/chunk-VZENBYLJ.mjs.map +0 -7
  44. package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs +0 -8
  45. package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs.map +0 -7
  46. package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs +0 -8
  47. package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs.map +0 -7
  48. package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs +0 -8
  49. package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs.map +0 -7
  50. package/dist/lib/node-esm/data.mjs +0 -8
  51. package/dist/lib/node-esm/data.mjs.map +0 -7
  52. package/dist/lib/node-esm/index.mjs +0 -1667
  53. package/dist/lib/node-esm/index.mjs.map +0 -7
  54. package/dist/lib/node-esm/meta.json +0 -1
  55. package/dist/lib/node-esm/translations.mjs +0 -21
  56. package/dist/lib/node-esm/translations.mjs.map +0 -7
  57. /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