@dxos/react-ui-geo 0.8.4-staging.ac66bdf99f → 0.9.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 (120) hide show
  1. package/LICENSE +102 -5
  2. package/data/countries-10m.ts +12 -0
  3. package/data/countries-110m.ts +4 -10579
  4. package/data/countries-50m.ts +12 -0
  5. package/dist/lib/browser/chunk-SC2FBYFU.mjs +17 -0
  6. package/dist/lib/browser/chunk-SC2FBYFU.mjs.map +7 -0
  7. package/dist/lib/browser/countries-10m-CWWDOKH7.mjs +6 -0
  8. package/dist/lib/browser/countries-10m-CWWDOKH7.mjs.map +7 -0
  9. package/dist/lib/browser/countries-110m-72QBAA5E.mjs +6 -0
  10. package/dist/lib/browser/countries-110m-72QBAA5E.mjs.map +7 -0
  11. package/dist/lib/browser/countries-50m-H7SL7KVF.mjs +6 -0
  12. package/dist/lib/browser/countries-50m-H7SL7KVF.mjs.map +7 -0
  13. package/dist/lib/browser/data.mjs +1 -1
  14. package/dist/lib/browser/index.mjs +774 -223
  15. package/dist/lib/browser/index.mjs.map +4 -4
  16. package/dist/lib/browser/meta.json +1 -1
  17. package/dist/lib/browser/translations.mjs +19 -0
  18. package/dist/lib/browser/translations.mjs.map +7 -0
  19. package/dist/lib/node-esm/chunk-VZENBYLJ.mjs +19 -0
  20. package/dist/lib/node-esm/chunk-VZENBYLJ.mjs.map +7 -0
  21. package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs +8 -0
  22. package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs.map +7 -0
  23. package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs +8 -0
  24. package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs.map +7 -0
  25. package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs +8 -0
  26. package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs.map +7 -0
  27. package/dist/lib/node-esm/data.mjs +1 -1
  28. package/dist/lib/node-esm/index.mjs +774 -223
  29. package/dist/lib/node-esm/index.mjs.map +4 -4
  30. package/dist/lib/node-esm/meta.json +1 -1
  31. package/dist/lib/node-esm/translations.mjs +21 -0
  32. package/dist/lib/node-esm/translations.mjs.map +7 -0
  33. package/dist/types/data/airports.d.ts +4 -4
  34. package/dist/types/data/airports.d.ts.map +1 -1
  35. package/dist/types/data/cities.d.ts.map +1 -1
  36. package/dist/types/data/countries-10m.d.ts +8 -0
  37. package/dist/types/data/countries-10m.d.ts.map +1 -0
  38. package/dist/types/data/countries-110m.d.ts +2 -30
  39. package/dist/types/data/countries-110m.d.ts.map +1 -1
  40. package/dist/types/data/countries-50m.d.ts +8 -0
  41. package/dist/types/data/countries-50m.d.ts.map +1 -0
  42. package/dist/types/data/countries-dots-3.d.ts.map +1 -1
  43. package/dist/types/data/countries-dots-4.d.ts.map +1 -1
  44. package/dist/types/src/components/Globe/Globe.d.ts +18 -10
  45. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  46. package/dist/types/src/components/Globe/Globe.stories.d.ts +16 -8
  47. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  48. package/dist/types/src/components/Map/Map.d.ts +49 -13
  49. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  50. package/dist/types/src/components/Map/Map.stories.d.ts +9 -5
  51. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  53. package/dist/types/src/data.d.ts +9 -1
  54. package/dist/types/src/data.d.ts.map +1 -1
  55. package/dist/types/src/hooks/context.d.ts +37 -0
  56. package/dist/types/src/hooks/context.d.ts.map +1 -1
  57. package/dist/types/src/hooks/index.d.ts +3 -0
  58. package/dist/types/src/hooks/index.d.ts.map +1 -1
  59. package/dist/types/src/hooks/useDrag.d.ts +22 -2
  60. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  61. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +3 -2
  62. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  63. package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
  64. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  65. package/dist/types/src/hooks/useSimplifiedTopology.d.ts +32 -0
  66. package/dist/types/src/hooks/useSimplifiedTopology.d.ts.map +1 -0
  67. package/dist/types/src/hooks/useSpinner.d.ts +1 -1
  68. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  69. package/dist/types/src/hooks/useTopology.d.ts +26 -0
  70. package/dist/types/src/hooks/useTopology.d.ts.map +1 -0
  71. package/dist/types/src/hooks/useTour.d.ts +3 -2
  72. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  73. package/dist/types/src/hooks/useWheel.d.ts +24 -0
  74. package/dist/types/src/hooks/useWheel.d.ts.map +1 -0
  75. package/dist/types/src/index.d.ts +0 -2
  76. package/dist/types/src/index.d.ts.map +1 -1
  77. package/dist/types/src/translations.d.ts +4 -4
  78. package/dist/types/src/translations.d.ts.map +1 -1
  79. package/dist/types/src/util/animation.d.ts +16 -0
  80. package/dist/types/src/util/animation.d.ts.map +1 -0
  81. package/dist/types/src/util/debug.d.ts.map +1 -1
  82. package/dist/types/src/util/index.d.ts +2 -0
  83. package/dist/types/src/util/index.d.ts.map +1 -1
  84. package/dist/types/src/util/inertia.d.ts.map +1 -1
  85. package/dist/types/src/util/path.d.ts.map +1 -1
  86. package/dist/types/src/util/render.d.ts +25 -1
  87. package/dist/types/src/util/render.d.ts.map +1 -1
  88. package/dist/types/src/util/styles.d.ts +4 -0
  89. package/dist/types/src/util/styles.d.ts.map +1 -0
  90. package/dist/types/tsconfig.tsbuildinfo +1 -1
  91. package/package.json +26 -24
  92. package/src/components/Globe/Globe.stories.tsx +135 -58
  93. package/src/components/Globe/Globe.tsx +237 -120
  94. package/src/components/Map/Map.stories.tsx +58 -12
  95. package/src/components/Map/Map.tsx +293 -91
  96. package/src/components/Toolbar/Controls.tsx +1 -1
  97. package/src/data.ts +19 -2
  98. package/src/hooks/context.tsx +44 -0
  99. package/src/hooks/index.ts +3 -0
  100. package/src/hooks/useDrag.ts +33 -5
  101. package/src/hooks/useGlobeZoomHandler.ts +2 -1
  102. package/src/hooks/useSimplifiedTopology.ts +81 -0
  103. package/src/hooks/useSpinner.ts +1 -1
  104. package/src/hooks/useTopology.ts +95 -0
  105. package/src/hooks/useTour.ts +70 -81
  106. package/src/hooks/useWheel.ts +83 -0
  107. package/src/index.ts +0 -2
  108. package/src/util/animation.ts +35 -0
  109. package/src/util/index.ts +2 -0
  110. package/src/util/inertia.ts +87 -4
  111. package/src/util/render.ts +105 -16
  112. package/src/util/styles.ts +62 -0
  113. package/dist/lib/browser/chunk-GMWLKTLN.mjs +0 -9
  114. package/dist/lib/browser/chunk-GMWLKTLN.mjs.map +0 -7
  115. package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs +0 -37859
  116. package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs.map +0 -7
  117. package/dist/lib/node-esm/chunk-JODBF4CC.mjs +0 -11
  118. package/dist/lib/node-esm/chunk-JODBF4CC.mjs.map +0 -7
  119. package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs +0 -37861
  120. package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs.map +0 -7
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  loadTopology
3
- } from "./chunk-GMWLKTLN.mjs";
3
+ } from "./chunk-SC2FBYFU.mjs";
4
4
 
5
5
  // src/components/Globe/Globe.tsx
6
- import { easeLinear, easeSinOut, geoMercator, geoOrthographic, geoPath as geoPath2, geoTransverseMercator, interpolateNumber, transition } from "d3";
7
- import React2, { forwardRef, useEffect as useEffect4, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState3 } from "react";
6
+ import { selection as d3Selection, easeLinear, easeSinOut, geoMercator, geoOrthographic, geoPath as geoPath2, geoTransverseMercator, interpolateNumber, transition } from "d3";
7
+ import React2, { forwardRef, useCallback as useCallback3, useEffect as useEffect6, useId, useImperativeHandle, useMemo as useMemo2, useRef, useState as useState4 } from "react";
8
8
  import { useResizeDetector } from "react-resize-detector";
9
9
  import { useComposedRefs, useControlledState, useDynamicRef, useThemeContext } from "@dxos/react-ui";
10
- import { composable, composableProps, mx } from "@dxos/ui-theme";
10
+ import { composable, composableProps } from "@dxos/react-ui";
11
+ import { mx } from "@dxos/ui-theme";
11
12
 
12
13
  // src/hooks/context.tsx
13
14
  import { createContext, useContext } from "react";
@@ -21,6 +22,18 @@ var useGlobeContext = () => {
21
22
  import { select as select2 } from "d3";
22
23
  import { useEffect } from "react";
23
24
 
25
+ // src/util/animation.ts
26
+ import { geoDistance } from "d3";
27
+ import versor from "versor";
28
+ var flyDuration = (p1, p2, base, scale) => Math.max(base, geoDistance(p1, p2) * scale);
29
+ var createRotationTween = (projection, setRotation, r1, r2) => {
30
+ const iv = versor.interpolate(r1, r2);
31
+ return (t) => {
32
+ projection.rotate(iv(t));
33
+ setRotation(projection.rotate());
34
+ };
35
+ };
36
+
24
37
  // src/util/debug.ts
25
38
  var debug = false;
26
39
  var timer = (cb) => {
@@ -38,7 +51,7 @@ var timer = (cb) => {
38
51
 
39
52
  // src/util/inertia.ts
40
53
  import { drag, select, timer as timer2 } from "d3";
41
- import versor from "versor";
54
+ import versor2 from "versor";
42
55
  var restrictAxis = (axis) => (original, current) => current.map((d, i) => axis[i] ? d : original[i]);
43
56
  var geoInertiaDrag = (target, render, projection, options) => {
44
57
  if (!options) {
@@ -48,21 +61,23 @@ var geoInertiaDrag = (target, render, projection, options) => {
48
61
  target = target.node();
49
62
  }
50
63
  target = select(target);
51
- const inertia = geoInertiaDragHelper({
64
+ const linear = (options.mode ?? "linear") === "linear";
65
+ const axis = restrictAxis(options.lockTilt ? [
66
+ true,
67
+ false,
68
+ false
69
+ ] : [
70
+ true,
71
+ true,
72
+ true
73
+ ]);
74
+ const sharedHandlers = {
52
75
  projection,
53
76
  render: (rotation) => {
54
77
  projection.rotate(rotation);
55
78
  render && render();
56
79
  },
57
- axis: restrictAxis(options.xAxis ? [
58
- true,
59
- false,
60
- false
61
- ] : [
62
- true,
63
- true,
64
- true
65
- ]),
80
+ axis,
66
81
  start: options.start,
67
82
  move: options.move,
68
83
  end: options.end,
@@ -70,7 +85,12 @@ var geoInertiaDrag = (target, render, projection, options) => {
70
85
  finish: options.finish,
71
86
  time: options.time,
72
87
  hold: options.hold
73
- });
88
+ };
89
+ const inertia = linear ? geoInertiaDragLinearHelper({
90
+ ...sharedHandlers,
91
+ sensitivity: options.sensitivity,
92
+ getZoom: options.getZoom
93
+ }) : geoInertiaDragHelper(sharedHandlers);
74
94
  target.call(drag().on("start", inertia.start).on("drag", inertia.move).on("end", inertia.end));
75
95
  return inertia;
76
96
  };
@@ -85,9 +105,9 @@ var geoInertiaDragHelper = (opt) => {
85
105
  const inertia = inertiaHelper({
86
106
  axis: opt.axis,
87
107
  start: () => {
88
- v0 = versor.cartesian(projection.invert(inertia.position));
108
+ v0 = versor2.cartesian(projection.invert(inertia.position));
89
109
  r0 = projection.rotate();
90
- q0 = versor(r0);
110
+ q0 = versor2(r0);
91
111
  opt.start && opt.start();
92
112
  },
93
113
  move: () => {
@@ -95,23 +115,23 @@ var geoInertiaDragHelper = (opt) => {
95
115
  if (isNaN(inv[0])) {
96
116
  return;
97
117
  }
98
- const v1 = versor.cartesian(inv);
99
- const q1 = versor.multiply(q0, versor.delta(v0, v1));
100
- const r1 = versor.rotation(q1);
118
+ const v1 = versor2.cartesian(inv);
119
+ const q1 = versor2.multiply(q0, versor2.delta(v0, v1));
120
+ const r1 = versor2.rotation(q1);
101
121
  const r2 = opt.axis(r0, r1);
102
122
  opt.render(r2);
103
123
  opt.move && opt.move();
104
124
  },
105
125
  end: () => {
106
- v10 = versor.cartesian(projection.invert(inertia.position.map((d, i) => d - inertia.velocity[i] / 1e3)));
107
- q10 = versor(projection.rotate());
108
- v11 = versor.cartesian(projection.invert(inertia.position));
126
+ v10 = versor2.cartesian(projection.invert(inertia.position.map((d, i) => d - inertia.velocity[i] / 1e3)));
127
+ q10 = versor2(projection.rotate());
128
+ v11 = versor2.cartesian(projection.invert(inertia.position));
109
129
  opt.end && opt.end();
110
130
  },
111
131
  stop: opt.stop,
112
132
  finish: opt.finish,
113
133
  render: (t) => {
114
- const r1 = versor.rotation(versor.multiply(q10, versor.delta(v10, v11, t * 1e3)));
134
+ const r1 = versor2.rotation(versor2.multiply(q10, versor2.delta(v10, v11, t * 1e3)));
115
135
  const r2 = opt.axis(r0, r1);
116
136
  opt.render && opt.render(r2);
117
137
  },
@@ -119,6 +139,62 @@ var geoInertiaDragHelper = (opt) => {
119
139
  });
120
140
  return inertia;
121
141
  };
142
+ var DEFAULT_LINEAR_SENSITIVITY = 0.25;
143
+ var geoInertiaDragLinearHelper = (opt) => {
144
+ const projection = opt.projection;
145
+ const sensitivity = opt.sensitivity ?? DEFAULT_LINEAR_SENSITIVITY;
146
+ const gain = () => sensitivity / Math.max(opt.getZoom?.() ?? 1, 0.1);
147
+ let r0;
148
+ let p0;
149
+ let kStart;
150
+ let rEnd;
151
+ let vEnd;
152
+ const inertia = inertiaHelper({
153
+ axis: opt.axis,
154
+ start: () => {
155
+ r0 = projection.rotate();
156
+ p0 = [
157
+ inertia.position[0],
158
+ inertia.position[1]
159
+ ];
160
+ kStart = gain();
161
+ opt.start && opt.start();
162
+ },
163
+ move: () => {
164
+ const dx = inertia.position[0] - p0[0];
165
+ const dy = inertia.position[1] - p0[1];
166
+ const r1 = [
167
+ r0[0] + dx * kStart,
168
+ r0[1] - dy * kStart,
169
+ 0
170
+ ];
171
+ const r2 = opt.axis(r0, r1);
172
+ opt.render(r2);
173
+ opt.move && opt.move();
174
+ },
175
+ end: () => {
176
+ rEnd = projection.rotate();
177
+ vEnd = [
178
+ inertia.velocity[0],
179
+ inertia.velocity[1]
180
+ ];
181
+ opt.end && opt.end();
182
+ },
183
+ stop: opt.stop,
184
+ finish: opt.finish,
185
+ render: (t) => {
186
+ const r1 = [
187
+ rEnd[0] + vEnd[0] * kStart * t,
188
+ rEnd[1] - vEnd[1] * kStart * t,
189
+ 0
190
+ ];
191
+ const r2 = opt.axis(rEnd, r1);
192
+ opt.render && opt.render(r2);
193
+ },
194
+ time: opt.time
195
+ });
196
+ return inertia;
197
+ };
122
198
  function inertiaHelper(opt) {
123
199
  const A = opt.time || 5e3;
124
200
  const limit = 1.0001;
@@ -222,9 +298,9 @@ var geoToPosition = ({ lat, lng }) => [
222
298
  lng,
223
299
  lat
224
300
  ];
225
- var geoPoint = (point) => ({
301
+ var geoPoint = (point2) => ({
226
302
  type: "Point",
227
- coordinates: geoToPosition(point)
303
+ coordinates: geoToPosition(point2)
228
304
  });
229
305
  var geoCircle = ({ lat, lng }, radius) => d3GeoCircle().radius(radius).center([
230
306
  lng,
@@ -249,11 +325,11 @@ var closestPoint = (points, target) => {
249
325
  }
250
326
  let closestPoint2 = points[0];
251
327
  let minDistance = getDistance(points[0], target);
252
- for (const point of points) {
253
- const distance = getDistance(point, target);
328
+ for (const point2 of points) {
329
+ const distance = getDistance(point2, target);
254
330
  if (distance < minDistance) {
255
331
  minDistance = distance;
256
- closestPoint2 = point;
332
+ closestPoint2 = point2;
257
333
  }
258
334
  }
259
335
  return closestPoint2;
@@ -265,8 +341,48 @@ var getDistance = (point1, point2) => {
265
341
  };
266
342
 
267
343
  // src/util/render.ts
268
- import { geoGraticule } from "d3";
344
+ import { geoBounds, geoCentroid, geoDistance as geoDistance2, geoGraticule } from "d3";
269
345
  import { feature, mesh } from "topojson-client";
346
+ var RAD_TO_DEG = 180 / Math.PI;
347
+ var computeBounds = (geometry) => {
348
+ const feat = {
349
+ type: "Feature",
350
+ geometry,
351
+ properties: {}
352
+ };
353
+ const centroid = geoCentroid(feat);
354
+ const [[w, s], [e, n]] = geoBounds(feat);
355
+ const corners = [
356
+ [
357
+ w,
358
+ s
359
+ ],
360
+ [
361
+ w,
362
+ n
363
+ ],
364
+ [
365
+ e,
366
+ s
367
+ ],
368
+ [
369
+ e,
370
+ n
371
+ ]
372
+ ];
373
+ let radius = 0;
374
+ for (const corner of corners) {
375
+ const d = geoDistance2(centroid, corner) * RAD_TO_DEG;
376
+ if (d > radius) {
377
+ radius = d;
378
+ }
379
+ }
380
+ return {
381
+ geometry,
382
+ centroid,
383
+ radius
384
+ };
385
+ };
270
386
  var createLayers = (topology, features, styles) => {
271
387
  const layers = [];
272
388
  if (styles.water) {
@@ -287,11 +403,25 @@ var createLayers = (topology, features, styles) => {
287
403
  });
288
404
  }
289
405
  if (topology) {
290
- if (topology.objects.land && styles.land) {
291
- layers.push({
292
- styles: styles.land,
293
- path: feature(topology, topology.objects.land)
294
- });
406
+ if (styles.land) {
407
+ if (topology.objects.countries) {
408
+ const fc = feature(topology, topology.objects.countries);
409
+ const memberGeoms = fc.features.map((f) => f.geometry);
410
+ const bounds = memberGeoms.map(computeBounds);
411
+ layers.push({
412
+ styles: styles.land,
413
+ path: {
414
+ type: "GeometryCollection",
415
+ geometries: memberGeoms
416
+ },
417
+ cullable: bounds
418
+ });
419
+ } else if (topology.objects.land) {
420
+ layers.push({
421
+ styles: styles.land,
422
+ path: feature(topology, topology.objects.land)
423
+ });
424
+ }
295
425
  }
296
426
  if (topology.objects.countries && styles.border) {
297
427
  layers.push({
@@ -308,28 +438,28 @@ var createLayers = (topology, features, styles) => {
308
438
  }
309
439
  if (features) {
310
440
  const { points, lines } = features;
311
- if (points && styles.point) {
441
+ if (lines && styles.line) {
312
442
  layers.push({
313
- styles: styles.point,
443
+ styles: styles.line,
314
444
  path: {
315
445
  type: "GeometryCollection",
316
- geometries: points.map((point) => geoPoint(point))
446
+ geometries: lines.map(({ source, target }) => geoLine(source, target))
317
447
  }
318
448
  });
319
449
  }
320
- if (lines && styles.line) {
450
+ if (points && styles.point) {
321
451
  layers.push({
322
- styles: styles.line,
452
+ styles: styles.point,
323
453
  path: {
324
454
  type: "GeometryCollection",
325
- geometries: lines.map(({ source, target }) => geoLine(source, target))
455
+ geometries: points.map((point2) => geoPoint(point2))
326
456
  }
327
457
  });
328
458
  }
329
459
  }
330
460
  return layers;
331
461
  };
332
- var renderLayers = (generator, layers = [], scale, styles) => {
462
+ var renderLayers = (generator, layers = [], scale, styles, viewCenter) => {
333
463
  const context = generator.context();
334
464
  const { canvas: { width, height } } = context;
335
465
  context.reset();
@@ -339,7 +469,8 @@ var renderLayers = (generator, layers = [], scale, styles) => {
339
469
  } else {
340
470
  context.clearRect(0, 0, width, height);
341
471
  }
342
- layers.forEach(({ path, styles: styles2 }) => {
472
+ layers.forEach((layer) => {
473
+ const { path, styles: styles2, cullable } = layer;
343
474
  context.save();
344
475
  let fill = false;
345
476
  let stroke = false;
@@ -354,8 +485,23 @@ var renderLayers = (generator, layers = [], scale, styles) => {
354
485
  }
355
486
  });
356
487
  }
488
+ let renderPath = path;
489
+ if (cullable && viewCenter) {
490
+ const geometries = [];
491
+ for (let index = 0; index < cullable.length; index++) {
492
+ const bounds = cullable[index];
493
+ const angularDistance = geoDistance2(viewCenter, bounds.centroid) * RAD_TO_DEG;
494
+ if (angularDistance < 90 + bounds.radius) {
495
+ geometries.push(bounds.geometry);
496
+ }
497
+ }
498
+ renderPath = {
499
+ type: "GeometryCollection",
500
+ geometries
501
+ };
502
+ }
357
503
  context.beginPath();
358
- generator(path);
504
+ generator(renderPath);
359
505
  fill && context.fill();
360
506
  stroke && context.stroke();
361
507
  context.restore();
@@ -363,6 +509,57 @@ var renderLayers = (generator, layers = [], scale, styles) => {
363
509
  return context;
364
510
  };
365
511
 
512
+ // src/util/styles.ts
513
+ var POINT_COLOR = "rgb(220, 38, 38)";
514
+ var LINE_COLOR = "rgba(220, 38, 38, 0.5)";
515
+ var globeStyles = (themeMode) => themeMode === "dark" ? {
516
+ water: {
517
+ fillStyle: "#191919"
518
+ },
519
+ land: {
520
+ fillStyle: "#444",
521
+ strokeStyle: "#222"
522
+ },
523
+ border: {
524
+ strokeStyle: "#111"
525
+ },
526
+ graticule: {
527
+ strokeStyle: "#111"
528
+ },
529
+ line: {
530
+ lineWidth: 1.5,
531
+ lineDash: [
532
+ 4,
533
+ 16
534
+ ],
535
+ strokeStyle: LINE_COLOR
536
+ },
537
+ point: {
538
+ radius: 0.2,
539
+ fillStyle: POINT_COLOR
540
+ }
541
+ } : {
542
+ water: {
543
+ fillStyle: "#C0DAE4"
544
+ },
545
+ land: {
546
+ fillStyle: "#C2D8B4",
547
+ strokeStyle: "#A6C291"
548
+ },
549
+ line: {
550
+ lineWidth: 1.5,
551
+ lineDash: [
552
+ 4,
553
+ 16
554
+ ],
555
+ strokeStyle: LINE_COLOR
556
+ },
557
+ point: {
558
+ radius: 0.2,
559
+ fillStyle: POINT_COLOR
560
+ }
561
+ };
562
+
366
563
  // src/hooks/useDrag.ts
367
564
  var useDrag = (controller, options = {}) => {
368
565
  useEffect(() => {
@@ -370,14 +567,19 @@ var useDrag = (controller, options = {}) => {
370
567
  if (!canvas || options.disabled) {
371
568
  return;
372
569
  }
373
- geoInertiaDrag(select2(canvas), () => {
570
+ const inertia = geoInertiaDrag(select2(canvas), () => {
374
571
  controller.setRotation(controller.projection.rotate());
375
572
  options.onUpdate?.({
376
573
  type: "move",
377
574
  controller
378
575
  });
379
576
  }, controller.projection, {
380
- xAxis: options.xAxis,
577
+ lockTilt: options.lockTilt,
578
+ mode: options.mode,
579
+ sensitivity: options.sensitivity,
580
+ // Zoom-driven gain: matches useWheel — degrees-per-pixel shrinks as the
581
+ // globe gets larger on screen so the drag feel is consistent across zoom.
582
+ getZoom: () => controller.zoom,
381
583
  time: 3e3,
382
584
  start: () => options.onUpdate?.({
383
585
  type: "start",
@@ -390,6 +592,7 @@ var useDrag = (controller, options = {}) => {
390
592
  });
391
593
  return () => {
392
594
  cancelDrag(select2(canvas));
595
+ inertia?.timer?.stop();
393
596
  };
394
597
  }, [
395
598
  controller,
@@ -447,6 +650,61 @@ var useMapZoomHandler = (controller) => {
447
650
  ]);
448
651
  };
449
652
 
653
+ // src/hooks/useSimplifiedTopology.ts
654
+ import { useMemo } from "react";
655
+ import { presimplify, quantile, simplify } from "topojson-simplify";
656
+ var DEFAULT_TIERS = [
657
+ {
658
+ minZoom: 0,
659
+ percentile: 0.95
660
+ },
661
+ {
662
+ minZoom: 2,
663
+ percentile: 0.85
664
+ },
665
+ {
666
+ minZoom: 4,
667
+ percentile: 0.6
668
+ },
669
+ {
670
+ minZoom: 7,
671
+ percentile: 0.3
672
+ },
673
+ {
674
+ minZoom: 12,
675
+ percentile: 0
676
+ }
677
+ ];
678
+ var pickTier = (zoom, tiers) => {
679
+ let match = tiers[0];
680
+ for (const tier of tiers) {
681
+ if (zoom >= tier.minZoom) {
682
+ match = tier;
683
+ }
684
+ }
685
+ return match;
686
+ };
687
+ var useSimplifiedTopology = (topology, zoom, options = {}) => {
688
+ const { tiers = DEFAULT_TIERS } = options;
689
+ const presimplified = useMemo(() => topology ? presimplify(topology) : void 0, [
690
+ topology
691
+ ]);
692
+ const tier = pickTier(zoom, tiers);
693
+ return useMemo(() => {
694
+ if (!presimplified) {
695
+ return void 0;
696
+ }
697
+ if (tier.percentile <= 0) {
698
+ return presimplified;
699
+ }
700
+ const minWeight = quantile(presimplified, tier.percentile);
701
+ return simplify(presimplified, minWeight);
702
+ }, [
703
+ presimplified,
704
+ tier
705
+ ]);
706
+ };
707
+
450
708
  // src/hooks/useSpinner.ts
451
709
  import { timer as d3Timer } from "d3";
452
710
  import { useEffect as useEffect2, useState } from "react";
@@ -500,91 +758,139 @@ var useSpinner = (controller, options = {}) => {
500
758
  ];
501
759
  };
502
760
 
761
+ // src/hooks/useTopology.ts
762
+ import { useEffect as useEffect3, useState as useState2 } from "react";
763
+ import { log } from "@dxos/log";
764
+ var __dxlog_file = "/__w/dxos/dxos/packages/ui/react-ui-geo/src/hooks/useTopology.ts";
765
+ var DEFAULT_TIERS2 = [
766
+ {
767
+ minZoom: 0,
768
+ level: "110m"
769
+ }
770
+ ];
771
+ var pickTier2 = (zoom, tiers) => {
772
+ let match = tiers[0];
773
+ for (const tier of tiers) {
774
+ if (zoom >= tier.minZoom) {
775
+ match = tier;
776
+ }
777
+ }
778
+ return match;
779
+ };
780
+ var topologyCache = /* @__PURE__ */ new Map();
781
+ var useTopology = (zoom, options = {}) => {
782
+ const { tiers = DEFAULT_TIERS2 } = options;
783
+ const level = zoom === void 0 ? "110m" : pickTier2(zoom, tiers).level;
784
+ const [topology, setTopology] = useState2(() => topologyCache.get(level));
785
+ useEffect3(() => {
786
+ const cached = topologyCache.get(level);
787
+ if (cached) {
788
+ setTopology(cached);
789
+ return;
790
+ }
791
+ let disposed = false;
792
+ void loadTopology(level).then((loaded) => {
793
+ topologyCache.set(level, loaded);
794
+ if (!disposed) {
795
+ setTopology(loaded);
796
+ }
797
+ }).catch((err) => {
798
+ if (!disposed) {
799
+ log.warn("failed to load topology", {
800
+ level,
801
+ err
802
+ }, { "~LogMeta": "~LogMeta", F: __dxlog_file, L: 52, S: void 0 });
803
+ }
804
+ });
805
+ return () => {
806
+ disposed = true;
807
+ };
808
+ }, [
809
+ level
810
+ ]);
811
+ return topology;
812
+ };
813
+
503
814
  // src/hooks/useTour.ts
504
- import { selection as d3Selection, geoDistance, geoInterpolate, geoPath } from "d3";
505
- import { useEffect as useEffect3, useMemo, useState as useState2 } from "react";
506
- import versor2 from "versor";
507
- var TRANSITION_NAME = "globe-tour";
815
+ import { geoInterpolate, geoPath } from "d3";
816
+ import { useEffect as useEffect4, useState as useState3 } from "react";
508
817
  var defaultDuration = 1500;
509
818
  var useTour = (controller, points, options = {}) => {
510
- const selection = useMemo(() => d3Selection(), []);
511
- const [running, setRunning] = useState2(options.running ?? false);
512
- useEffect3(() => {
513
- if (!running) {
514
- selection.interrupt(TRANSITION_NAME);
819
+ const [running, setRunning] = useState3(options.running ?? false);
820
+ useEffect4(() => {
821
+ if (!controller || !running) {
515
822
  return;
516
823
  }
517
- let t;
518
- if (controller && running) {
519
- t = setTimeout(async () => {
520
- const { canvas, projection, setRotation } = controller;
521
- const context = canvas.getContext("2d", {
522
- alpha: false
523
- });
524
- const path = geoPath(projection, context).pointRadius(2);
525
- const tilt = options.tilt ?? 0;
824
+ let cancelled = false;
825
+ const t = setTimeout(async () => {
826
+ const { canvas, projection } = controller;
827
+ const context = canvas.getContext("2d", {
828
+ alpha: false
829
+ });
830
+ const path = geoPath(projection, context).pointRadius(2);
831
+ try {
832
+ const tourPoints = [
833
+ ...points
834
+ ];
835
+ if (options.loop) {
836
+ tourPoints.push(tourPoints[0]);
837
+ }
526
838
  let last;
527
- try {
528
- const p = [
529
- ...points
530
- ];
531
- if (options.loop) {
532
- p.push(p[0]);
839
+ for (const next of tourPoints) {
840
+ if (cancelled) {
841
+ break;
533
842
  }
534
- for (const next of p) {
535
- if (!running) {
536
- break;
537
- }
538
- const p1 = last ? geoToPosition(last) : void 0;
539
- const p2 = geoToPosition(next);
540
- const ip = geoInterpolate(p1 || p2, p2);
541
- const distance = geoDistance(p1 || p2, p2);
542
- const r1 = p1 ? positionToRotation(p1, tilt) : controller.projection.rotate();
543
- const r2 = positionToRotation(p2, tilt);
544
- const iv = versor2.interpolate(r1, r2);
545
- const transition2 = selection.transition(TRANSITION_NAME).duration(Math.max(options.duration ?? defaultDuration, distance * 2e3)).tween("render", () => (t2) => {
546
- const t1 = Math.max(0, Math.min(1, t2 * 2 - 1));
547
- const t22 = Math.min(1, t2 * 2);
548
- context.save();
549
- {
550
- context.beginPath();
551
- context.strokeStyle = options?.styles?.arc?.strokeStyle ?? "yellow";
552
- context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
553
- context.setLineDash(options?.styles?.arc?.lineDash ?? []);
554
- path({
555
- type: "LineString",
556
- coordinates: [
557
- ip(t1),
558
- ip(t22)
559
- ]
560
- });
561
- context.stroke();
562
- context.beginPath();
563
- context.fillStyle = options?.styles?.cursor?.fillStyle ?? "orange";
564
- path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
565
- path({
566
- type: "Point",
567
- coordinates: ip(t22)
568
- });
569
- context.fill();
570
- }
843
+ const p1 = last ? geoToPosition(last) : void 0;
844
+ const p2 = geoToPosition(next);
845
+ const ip = geoInterpolate(p1 ?? p2, p2);
846
+ const onTick = (t2) => {
847
+ const t1 = Math.max(0, Math.min(1, t2 * 2 - 1));
848
+ const t22 = Math.min(1, t2 * 2);
849
+ context.save();
850
+ try {
851
+ context.beginPath();
852
+ context.strokeStyle = options.styles?.arc?.strokeStyle ?? "yellow";
853
+ context.lineWidth = (options.styles?.arc?.lineWidth ?? 1.5) * (controller.zoom ?? 1);
854
+ context.setLineDash(options.styles?.arc?.lineDash ?? []);
855
+ path({
856
+ type: "LineString",
857
+ coordinates: [
858
+ ip(t1),
859
+ ip(t22)
860
+ ]
861
+ });
862
+ context.stroke();
863
+ context.beginPath();
864
+ context.fillStyle = options.styles?.cursor?.fillStyle ?? "orange";
865
+ path.pointRadius((options.styles?.cursor?.pointRadius ?? 2) * (controller.zoom ?? 1));
866
+ path({
867
+ type: "Point",
868
+ coordinates: ip(t22)
869
+ });
870
+ context.fill();
871
+ } finally {
571
872
  context.restore();
572
- projection.rotate(iv(t2));
573
- setRotation(projection.rotate());
574
- });
575
- await transition2.end();
576
- last = next;
577
- }
578
- } catch {
579
- } finally {
873
+ }
874
+ };
875
+ await controller.flyTo(next, {
876
+ duration: options.duration ?? defaultDuration,
877
+ tilt: options.tilt ?? 0,
878
+ onTick
879
+ });
880
+ last = next;
881
+ }
882
+ } catch {
883
+ } finally {
884
+ if (!cancelled) {
580
885
  setRunning(false);
581
886
  }
582
- });
583
- return () => {
584
- clearTimeout(t);
585
- selection.interrupt(TRANSITION_NAME);
586
- };
587
- }
887
+ }
888
+ });
889
+ return () => {
890
+ cancelled = true;
891
+ clearTimeout(t);
892
+ controller.cancelFlyTo();
893
+ };
588
894
  }, [
589
895
  controller,
590
896
  running,
@@ -596,26 +902,55 @@ var useTour = (controller, points, options = {}) => {
596
902
  ];
597
903
  };
598
904
 
599
- // src/components/Toolbar/Controls.tsx
600
- import React from "react";
601
- import { IconButton, Toolbar, useTranslation } from "@dxos/react-ui";
602
-
603
- // src/translations.ts
604
- var translationKey = "@dxos/react-ui-geo";
605
- var translations = [
606
- {
607
- "en-US": {
608
- [translationKey]: {
609
- "zoom-in-icon.button": "Zoom in",
610
- "zoom-out-icon.button": "Zoom out",
611
- "start-icon.button": "Start",
612
- "toggle-icon.button": "Toggle"
613
- }
905
+ // src/hooks/useWheel.ts
906
+ import { useEffect as useEffect5 } from "react";
907
+ var DEFAULT_SENSITIVITY = 0.25;
908
+ var DEFAULT_ZOOM_SENSITIVITY = 0.01;
909
+ var useWheel = (controller, options = {}) => {
910
+ useEffect5(() => {
911
+ const canvas = controller?.canvas;
912
+ if (!canvas || options.disabled) {
913
+ return;
614
914
  }
615
- }
616
- ];
915
+ const sensitivity = options.sensitivity ?? DEFAULT_SENSITIVITY;
916
+ const zoomSensitivity = options.zoomSensitivity ?? DEFAULT_ZOOM_SENSITIVITY;
917
+ const handleWheel = (event) => {
918
+ event.preventDefault();
919
+ if (event.ctrlKey) {
920
+ const factor = Math.exp(-event.deltaY * zoomSensitivity);
921
+ controller.setZoom(controller.zoom * factor);
922
+ } else {
923
+ const [lambda, phi, gamma] = controller.projection.rotate();
924
+ const k = sensitivity / Math.max(controller.zoom, 0.1);
925
+ const next = [
926
+ lambda - event.deltaX * k,
927
+ phi + event.deltaY * k,
928
+ gamma
929
+ ];
930
+ controller.projection.rotate(next);
931
+ controller.setRotation(controller.projection.rotate());
932
+ }
933
+ options.onUpdate?.(controller);
934
+ };
935
+ canvas.addEventListener("wheel", handleWheel, {
936
+ passive: false
937
+ });
938
+ return () => {
939
+ canvas.removeEventListener("wheel", handleWheel);
940
+ };
941
+ }, [
942
+ controller,
943
+ options.disabled,
944
+ options.sensitivity,
945
+ options.zoomSensitivity,
946
+ options.onUpdate
947
+ ]);
948
+ };
617
949
 
618
950
  // src/components/Toolbar/Controls.tsx
951
+ import React from "react";
952
+ import { IconButton, Toolbar, useTranslation } from "@dxos/react-ui";
953
+ import { translationKey } from "#translations";
619
954
  var controlPositions = {
620
955
  topleft: "top-2 left-2",
621
956
  topright: "top-2 right-2",
@@ -716,45 +1051,72 @@ var getProjection = (type = "orthographic") => {
716
1051
  }
717
1052
  return type ?? geoOrthographic();
718
1053
  };
719
- var GlobeRoot = composable(({ children, center: centerProp, zoom: zoomProp, translation: translationProp, rotation: rotationProp, ...props }, forwardedRef) => {
720
- const localRef = useRef(null);
721
- const composedRef = useComposedRefs(localRef, forwardedRef);
722
- const { width, height } = useResizeDetector({
723
- targetRef: localRef
1054
+ var DEFAULT_ZOOM = 1.5;
1055
+ var GlobeRoot = /* @__PURE__ */ forwardRef(({ children, center: centerProp, zoom: zoomProp = DEFAULT_ZOOM, translation: translationProp, rotation: rotationProp }, forwardedRef) => {
1056
+ const [size, setSize] = useState4({
1057
+ width: 0,
1058
+ height: 0
724
1059
  });
725
1060
  const [center, setCenter] = useControlledState(centerProp);
726
- const [zoom, setZoom] = useControlledState(zoomProp ?? 4);
1061
+ const [zoom, setZoom] = useControlledState(zoomProp);
727
1062
  const [translation, setTranslation] = useControlledState(translationProp);
728
1063
  const [rotation, setRotation] = useControlledState(rotationProp);
1064
+ const [controller, setController] = useState4(null);
1065
+ const registerController = useCallback3((next) => setController(next), []);
1066
+ useImperativeHandle(forwardedRef, () => controller, [
1067
+ controller
1068
+ ]);
729
1069
  return /* @__PURE__ */ React2.createElement(GlobeContext.Provider, {
730
1070
  value: {
731
- size: {
732
- width,
733
- height
734
- },
1071
+ size,
735
1072
  center,
736
1073
  zoom,
737
1074
  translation,
738
1075
  rotation,
1076
+ setSize,
739
1077
  setCenter,
740
1078
  setZoom,
741
1079
  setTranslation,
742
- setRotation
1080
+ setRotation,
1081
+ registerController
743
1082
  }
744
- }, /* @__PURE__ */ React2.createElement("div", {
1083
+ }, children);
1084
+ });
1085
+ GlobeRoot.displayName = "Globe.Root";
1086
+ var GlobeViewport = composable(({ children, ...props }, forwardedRef) => {
1087
+ const { setSize } = useGlobeContext();
1088
+ const localRef = useRef(null);
1089
+ const composedRef = useComposedRefs(localRef, forwardedRef);
1090
+ const { width, height } = useResizeDetector({
1091
+ targetRef: localRef
1092
+ });
1093
+ useEffect6(() => {
1094
+ setSize({
1095
+ width: width ?? 0,
1096
+ height: height ?? 0
1097
+ });
1098
+ }, [
1099
+ width,
1100
+ height,
1101
+ setSize
1102
+ ]);
1103
+ return /* @__PURE__ */ React2.createElement("div", {
745
1104
  ...composableProps(props, {
746
1105
  classNames: "relative dx-container"
747
1106
  }),
748
1107
  ref: composedRef
749
- }, children));
1108
+ }, children);
750
1109
  });
751
- var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: projectionProp, topology, features, styles: stylesProp }, forwardRef3) => {
1110
+ GlobeViewport.displayName = "Globe.Viewport";
1111
+ var GlobeCanvas = ({ projection: projectionProp, topology, features, styles: stylesProp }) => {
752
1112
  const { themeMode } = useThemeContext();
753
1113
  const styles = useMemo2(() => stylesProp ?? defaultStyles[themeMode], [
754
1114
  stylesProp,
755
1115
  themeMode
756
1116
  ]);
757
- const [canvas, setCanvas] = useState3(null);
1117
+ const { size, center, zoom, translation, rotation, setZoom, setTranslation, setRotation, registerController } = useGlobeContext();
1118
+ const zoomRef = useDynamicRef(zoom);
1119
+ const [canvas, setCanvas] = useState4(null);
758
1120
  const canvasRef = (canvas2) => setCanvas(canvas2);
759
1121
  const projection = useMemo2(() => getProjection(projectionProp), [
760
1122
  projectionProp
@@ -766,28 +1128,31 @@ var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: projectionProp, topo
766
1128
  features,
767
1129
  styles
768
1130
  ]);
769
- const { size, center, zoom, translation, rotation, setCenter, setZoom, setTranslation, setRotation } = useGlobeContext();
770
- const zoomRef = useDynamicRef(zoom);
771
- useEffect4(() => {
1131
+ useEffect6(() => {
772
1132
  if (center) {
773
- setZoom(1);
774
1133
  setRotation(positionToRotation(geoToPosition(center)));
775
1134
  }
776
1135
  }, [
777
1136
  center
778
1137
  ]);
1138
+ const flyToSelection = useMemo2(() => d3Selection(), []);
1139
+ const flyToTransitionName = `globe-fly-to-${useId()}`;
1140
+ useEffect6(() => () => {
1141
+ flyToSelection.interrupt(flyToTransitionName);
1142
+ }, [
1143
+ flyToSelection,
1144
+ flyToTransitionName
1145
+ ]);
779
1146
  const zooming = useRef(false);
780
- useImperativeHandle(forwardRef3, () => {
1147
+ const controller = useMemo2(() => {
781
1148
  return {
782
1149
  canvas,
783
1150
  projection,
784
- center,
785
1151
  get zoom() {
786
1152
  return zoomRef.current;
787
1153
  },
788
1154
  translation,
789
1155
  rotation,
790
- setCenter,
791
1156
  setZoom: (state) => {
792
1157
  if (typeof state === "function") {
793
1158
  const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
@@ -799,18 +1164,51 @@ var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: projectionProp, topo
799
1164
  }
800
1165
  },
801
1166
  setTranslation,
802
- setRotation
1167
+ setRotation,
1168
+ flyTo: (target, options = {}) => {
1169
+ const { duration = 1200, tilt = 0, onTick } = options;
1170
+ const p2 = geoToPosition(target);
1171
+ const r1 = projection.rotate();
1172
+ const r2 = positionToRotation(p2, tilt);
1173
+ const p1 = [
1174
+ -r1[0],
1175
+ -r1[1]
1176
+ ];
1177
+ const rotationTween = createRotationTween(projection, setRotation, r1, r2);
1178
+ const iz = target.zoom !== void 0 ? interpolateNumber(zoomRef.current, target.zoom) : void 0;
1179
+ flyToSelection.interrupt(flyToTransitionName);
1180
+ const tx = flyToSelection.transition(flyToTransitionName).duration(flyDuration(p1, p2, duration, 1500));
1181
+ if (onTick) {
1182
+ tx.tween("flyToOnTick", () => onTick);
1183
+ }
1184
+ tx.tween("flyToRotation", () => rotationTween);
1185
+ if (iz) {
1186
+ tx.tween("flyToZoom", () => (t) => setZoom(iz(t)));
1187
+ }
1188
+ return tx.end();
1189
+ },
1190
+ cancelFlyTo: () => {
1191
+ flyToSelection.interrupt(flyToTransitionName);
1192
+ }
803
1193
  };
804
1194
  }, [
805
- canvas
1195
+ canvas,
1196
+ projection,
1197
+ flyToSelection,
1198
+ flyToTransitionName
1199
+ ]);
1200
+ useEffect6(() => {
1201
+ registerController(controller);
1202
+ return () => registerController(null);
1203
+ }, [
1204
+ registerController,
1205
+ controller
806
1206
  ]);
807
- const generator = useMemo2(() => canvas && projection && geoPath2(projection, canvas.getContext("2d", {
808
- alpha: false
809
- })), [
1207
+ const generator = useMemo2(() => canvas && projection && geoPath2(projection, canvas.getContext("2d")), [
810
1208
  canvas,
811
1209
  projection
812
1210
  ]);
813
- useEffect4(() => {
1211
+ useEffect6(() => {
814
1212
  if (canvas && projection) {
815
1213
  timer(() => {
816
1214
  projection.scale(Math.min(size.width, size.height) / 2 * zoom).translate([
@@ -821,7 +1219,17 @@ var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: projectionProp, topo
821
1219
  0,
822
1220
  0
823
1221
  ]);
824
- renderLayers(generator, layers, zoom, styles);
1222
+ const isOrthographic = !projectionProp || projectionProp === "orthographic";
1223
+ const [lambda, phi] = rotation ?? [
1224
+ 0,
1225
+ 0,
1226
+ 0
1227
+ ];
1228
+ const viewCenter = isOrthographic ? [
1229
+ -lambda,
1230
+ -phi
1231
+ ] : void 0;
1232
+ renderLayers(generator, layers, zoom, styles, viewCenter);
825
1233
  });
826
1234
  }
827
1235
  }, [
@@ -830,17 +1238,20 @@ var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: projectionProp, topo
830
1238
  zoom,
831
1239
  translation,
832
1240
  rotation,
833
- layers
1241
+ layers,
1242
+ projectionProp
834
1243
  ]);
835
1244
  if (!size.width || !size.height) {
836
1245
  return null;
837
1246
  }
838
1247
  return /* @__PURE__ */ React2.createElement("canvas", {
839
1248
  ref: canvasRef,
1249
+ className: "bg-base-surface",
840
1250
  width: size.width,
841
1251
  height: size.height
842
1252
  });
843
- });
1253
+ };
1254
+ GlobeCanvas.displayName = "Globe.Canvas";
844
1255
  var GlobeDebug = ({ position = "topleft" }) => {
845
1256
  const { size, zoom, translation, rotation } = useGlobeContext();
846
1257
  return /* @__PURE__ */ React2.createElement("div", {
@@ -878,6 +1289,7 @@ var GlobeAction = ({ onAction, position = "bottomright", ...props }) => /* @__PU
878
1289
  }));
879
1290
  var Globe = {
880
1291
  Root: GlobeRoot,
1292
+ Viewport: GlobeViewport,
881
1293
  Canvas: GlobeCanvas,
882
1294
  Zoom: GlobeZoom,
883
1295
  Action: GlobeAction,
@@ -888,12 +1300,13 @@ var Globe = {
888
1300
  // src/components/Map/Map.tsx
889
1301
  import "leaflet/dist/leaflet.css";
890
1302
  import { createContext as createContext2 } from "@radix-ui/react-context";
891
- import L, { Control, DomEvent, DomUtil, latLngBounds } from "leaflet";
892
- import React3, { forwardRef as forwardRef2, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useRef as useRef2 } from "react";
1303
+ import L, { Control, DomEvent, DomUtil, point, latLngBounds } from "leaflet";
1304
+ import React3, { forwardRef as forwardRef2, useCallback as useCallback4, useEffect as useEffect7, useImperativeHandle as useImperativeHandle2, useRef as useRef2, useState as useState5 } from "react";
893
1305
  import { createRoot } from "react-dom/client";
894
- import { MapContainer, Marker, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
1306
+ import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
895
1307
  import { ThemeProvider, Tooltip } from "@dxos/react-ui";
896
- import { composable as composable2, composableProps as composableProps2, defaultTx, mx as mx2 } from "@dxos/ui-theme";
1308
+ import { composable as composable2, composableProps as composableProps2, defaultTx } from "@dxos/react-ui";
1309
+ import { mx as mx2 } from "@dxos/ui-theme";
897
1310
  var defaults = {
898
1311
  center: {
899
1312
  lat: 51,
@@ -902,34 +1315,106 @@ var defaults = {
902
1315
  zoom: 4
903
1316
  };
904
1317
  var [MapContextProvider, useMapContext] = createContext2("Map");
905
- var MapRoot = composable2(({ children, onChange, ...props }, forwardedRef) => {
906
- const attention = false;
907
- return /* @__PURE__ */ React3.createElement(MapContextProvider, {
908
- attention,
909
- onChange
910
- }, /* @__PURE__ */ React3.createElement("div", {
911
- ...composableProps2(props, {
912
- role: "none",
913
- classNames: "dx-container grid dx-focus-ring-inset"
914
- }),
915
- ref: forwardedRef
916
- }, children));
917
- });
918
- MapRoot.displayName = "Map.Root";
919
- var MAP_CONTENT_NAME = "Map.Content";
920
- var MapContent = /* @__PURE__ */ forwardRef2(({ classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props }, forwardedRef) => {
921
- const { attention } = useMapContext(MAP_CONTENT_NAME);
1318
+ var MapRoot = /* @__PURE__ */ forwardRef2(({ children, onChange }, forwardedRef) => {
922
1319
  const mapRef = useRef2(null);
923
- const map = mapRef.current;
1320
+ const registerMap = useCallback4((map) => {
1321
+ mapRef.current = map;
1322
+ }, []);
924
1323
  useImperativeHandle2(forwardedRef, () => ({
925
- setCenter: (center2, zoom2) => {
926
- mapRef.current?.setView(center2, zoom2);
1324
+ getCenter: () => {
1325
+ const center = mapRef.current?.getCenter();
1326
+ return center ? {
1327
+ lat: center.lat,
1328
+ lng: center.lng
1329
+ } : void 0;
1330
+ },
1331
+ getZoom: () => mapRef.current?.getZoom(),
1332
+ setCenter: (center, zoom) => {
1333
+ mapRef.current?.setView(center, zoom);
927
1334
  },
928
1335
  setZoom: (cb) => {
929
1336
  mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
930
1337
  }
931
1338
  }), []);
932
- useEffect5(() => {
1339
+ const attention = false;
1340
+ return /* @__PURE__ */ React3.createElement(MapContextProvider, {
1341
+ attention,
1342
+ onChange,
1343
+ registerMap
1344
+ }, children);
1345
+ });
1346
+ MapRoot.displayName = "Map.Root";
1347
+ var MAP_VIEWPORT_NAME = "Map.Viewport";
1348
+ var MapResize = () => {
1349
+ const map = useMap();
1350
+ useEffect7(() => {
1351
+ const container = map.getContainer();
1352
+ let frame = 0;
1353
+ const observer = new ResizeObserver(() => {
1354
+ cancelAnimationFrame(frame);
1355
+ frame = requestAnimationFrame(() => map.invalidateSize());
1356
+ });
1357
+ observer.observe(container);
1358
+ return () => {
1359
+ cancelAnimationFrame(frame);
1360
+ observer.disconnect();
1361
+ };
1362
+ }, [
1363
+ map
1364
+ ]);
1365
+ return null;
1366
+ };
1367
+ var PINCH_ZOOM_SENSITIVITY = 0.03;
1368
+ var MapPinchZoom = () => {
1369
+ const map = useMap();
1370
+ useEffect7(() => {
1371
+ const container = map.getContainer();
1372
+ let frame = 0;
1373
+ let point2;
1374
+ let target;
1375
+ const onWheel = (event) => {
1376
+ if (!event.ctrlKey) {
1377
+ return;
1378
+ }
1379
+ event.preventDefault();
1380
+ const rect = container.getBoundingClientRect();
1381
+ point2 = L.point(event.clientX - rect.left, event.clientY - rect.top);
1382
+ target = (target ?? map.getZoom()) - event.deltaY * PINCH_ZOOM_SENSITIVITY;
1383
+ if (!frame) {
1384
+ frame = requestAnimationFrame(() => {
1385
+ frame = 0;
1386
+ if (target !== void 0 && point2) {
1387
+ map.setZoomAround(point2, target, {
1388
+ animate: false
1389
+ });
1390
+ target = void 0;
1391
+ }
1392
+ });
1393
+ }
1394
+ };
1395
+ container.addEventListener("wheel", onWheel, {
1396
+ passive: false
1397
+ });
1398
+ return () => {
1399
+ container.removeEventListener("wheel", onWheel);
1400
+ cancelAnimationFrame(frame);
1401
+ };
1402
+ }, [
1403
+ map
1404
+ ]);
1405
+ return null;
1406
+ };
1407
+ var MapViewport = composable2((props, _forwardedRef) => {
1408
+ const { scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, whenReady, children, ...rest } = props;
1409
+ const { attention, registerMap } = useMapContext(MAP_VIEWPORT_NAME);
1410
+ const [map, setMap] = useState5(null);
1411
+ const setMapRef = useCallback4((next) => {
1412
+ setMap(next);
1413
+ registerMap(next);
1414
+ }, [
1415
+ registerMap
1416
+ ]);
1417
+ useEffect7(() => {
933
1418
  if (!map) {
934
1419
  return;
935
1420
  }
@@ -943,27 +1428,31 @@ var MapContent = /* @__PURE__ */ forwardRef2(({ classNames, scrollWheelZoom = tr
943
1428
  attention
944
1429
  ]);
945
1430
  return /* @__PURE__ */ React3.createElement(MapContainer, {
946
- ...props,
947
- className: mx2("group relative grid bg-base-surface!", classNames),
1431
+ ...composableProps2(rest, {
1432
+ // Frame classes (formerly on Map.Root): focusable grid container.
1433
+ classNames: "dx-container group relative grid dx-focus-ring-inset bg-base-surface!"
1434
+ }),
948
1435
  attributionControl: false,
949
1436
  zoomControl: false,
950
1437
  scrollWheelZoom,
951
1438
  doubleClickZoom,
952
1439
  touchZoom,
1440
+ // Allow fractional zoom so trackpad pinch (small ctrl+wheel deltas) isn't rounded away.
1441
+ zoomSnap: 0,
953
1442
  center: center ?? defaults.center,
954
1443
  zoom: zoom ?? defaults.zoom,
955
- whenReady: () => {
956
- },
957
- ref: mapRef
958
- }, children);
1444
+ whenReady,
1445
+ ref: setMapRef
1446
+ }, /* @__PURE__ */ React3.createElement(MapResize, null), /* @__PURE__ */ React3.createElement(MapPinchZoom, null), children);
959
1447
  });
960
- MapContent.displayName = "Map.Content";
1448
+ MapViewport.displayName = "Map.Viewport";
961
1449
  var MAP_TILES_NAME = "Map.Tiles";
962
- var MapTiles = (_props) => {
1450
+ var DEFAULT_TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
1451
+ var MapTiles = ({ url = DEFAULT_TILE_URL }) => {
963
1452
  const ref = useRef2(null);
964
1453
  const { onChange } = useMapContext(MAP_TILES_NAME);
965
1454
  useMapEvents({
966
- zoomstart: (ev) => {
1455
+ moveend: (ev) => {
967
1456
  onChange?.({
968
1457
  center: ev.target.getCenter(),
969
1458
  zoom: ev.target.getZoom()
@@ -971,7 +1460,7 @@ var MapTiles = (_props) => {
971
1460
  }
972
1461
  });
973
1462
  const { attention } = useMapContext(MAP_TILES_NAME);
974
- useEffect5(() => {
1463
+ useEffect7(() => {
975
1464
  if (ref.current) {
976
1465
  ref.current.getContainer().dataset.attention = attention ? "1" : "0";
977
1466
  }
@@ -983,22 +1472,34 @@ var MapTiles = (_props) => {
983
1472
  "data-attention": attention,
984
1473
  detectRetina: true,
985
1474
  className: 'dark:grayscale dark:invert data-[attention="0"]:!opacity-80',
986
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
1475
+ url,
987
1476
  keepBuffer: 4
988
1477
  }));
989
1478
  };
990
1479
  MapTiles.displayName = MAP_TILES_NAME;
991
- var MapMarkers = ({ selected, markers }) => {
1480
+ var MapMarkers = ({ selected, markers, lines, onSelect }) => {
992
1481
  const map = useMap();
993
- useEffect5(() => {
994
- if (markers.length > 0) {
995
- const bounds = latLngBounds(markers.map((marker) => marker.location));
996
- map.fitBounds(bounds);
997
- } else {
998
- map.setView(defaults.center, defaults.zoom);
1482
+ useEffect7(() => {
1483
+ const points = [
1484
+ ...markers?.map((marker) => marker.location) ?? [],
1485
+ ...lines?.flatMap((line) => [
1486
+ line.source,
1487
+ line.target
1488
+ ]) ?? []
1489
+ ];
1490
+ if (points.length > 0) {
1491
+ const bounds = latLngBounds(points);
1492
+ const size = map.getSize();
1493
+ const padding = Math.max(48, Math.min(size.x, size.y) / 6);
1494
+ map.fitBounds(bounds, {
1495
+ padding: point(padding, padding),
1496
+ animate: false
1497
+ });
999
1498
  }
1000
1499
  }, [
1001
- markers
1500
+ markers,
1501
+ lines,
1502
+ map
1002
1503
  ]);
1003
1504
  return /* @__PURE__ */ React3.createElement(React3.Fragment, null, markers?.map(({ id, title, location: { lat, lng } }) => {
1004
1505
  return /* @__PURE__ */ React3.createElement(Marker, {
@@ -1007,6 +1508,9 @@ var MapMarkers = ({ selected, markers }) => {
1007
1508
  lat,
1008
1509
  lng
1009
1510
  },
1511
+ eventHandlers: onSelect ? {
1512
+ click: () => onSelect(id)
1513
+ } : void 0,
1010
1514
  icon: (
1011
1515
  // TODO(burdon): Create custom icon from bundled assets.
1012
1516
  // TODO(burdon): Selection state.
@@ -1036,9 +1540,41 @@ var MapMarkers = ({ selected, markers }) => {
1036
1540
  }));
1037
1541
  };
1038
1542
  MapMarkers.displayName = "Map.Markers";
1543
+ var MapLines = ({ lines }) => {
1544
+ if (!lines || lines.length === 0) {
1545
+ return null;
1546
+ }
1547
+ const polylines = [];
1548
+ for (const { source, target, color } of lines) {
1549
+ const last = polylines[polylines.length - 1];
1550
+ const lastPos = last?.positions[last.positions.length - 1];
1551
+ if (last && last.color === color && lastPos?.lat === source.lat && lastPos?.lng === source.lng) {
1552
+ last.positions.push(target);
1553
+ } else {
1554
+ polylines.push({
1555
+ positions: [
1556
+ source,
1557
+ target
1558
+ ],
1559
+ color
1560
+ });
1561
+ }
1562
+ }
1563
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, polylines.map(({ positions, color }, index) => /* @__PURE__ */ React3.createElement(Polyline, {
1564
+ key: index,
1565
+ positions,
1566
+ pathOptions: {
1567
+ color,
1568
+ weight: 4,
1569
+ opacity: 0.8
1570
+ }
1571
+ })));
1572
+ };
1573
+ MapLines.displayName = "Map.Lines";
1039
1574
  var CustomControl2 = ({ position, children }) => {
1040
1575
  const map = useMap();
1041
- useEffect5(() => {
1576
+ const rootRef = useRef2(void 0);
1577
+ useEffect7(() => {
1042
1578
  const control = new Control({
1043
1579
  position
1044
1580
  });
@@ -1047,6 +1583,7 @@ var CustomControl2 = ({ position, children }) => {
1047
1583
  DomEvent.disableClickPropagation(container);
1048
1584
  DomEvent.disableScrollPropagation(container);
1049
1585
  const root = createRoot(container);
1586
+ rootRef.current = root;
1050
1587
  root.render(/* @__PURE__ */ React3.createElement(ThemeProvider, {
1051
1588
  tx: defaultTx
1052
1589
  }, /* @__PURE__ */ React3.createElement(Tooltip.Provider, null, children)));
@@ -1055,10 +1592,19 @@ var CustomControl2 = ({ position, children }) => {
1055
1592
  control.addTo(map);
1056
1593
  return () => {
1057
1594
  control.remove();
1595
+ const root = rootRef.current;
1596
+ rootRef.current = void 0;
1597
+ queueMicrotask(() => root?.unmount());
1058
1598
  };
1059
1599
  }, [
1060
1600
  map,
1061
- position,
1601
+ position
1602
+ ]);
1603
+ useEffect7(() => {
1604
+ rootRef.current?.render(/* @__PURE__ */ React3.createElement(ThemeProvider, {
1605
+ tx: defaultTx
1606
+ }, /* @__PURE__ */ React3.createElement(Tooltip.Provider, null, children)));
1607
+ }, [
1062
1608
  children
1063
1609
  ]);
1064
1610
  return null;
@@ -1075,41 +1621,46 @@ var MapAction = ({ onAction, position = "bottomright", ...props }) => /* @__PURE
1075
1621
  }, /* @__PURE__ */ React3.createElement(ActionControls, {
1076
1622
  onAction
1077
1623
  }));
1078
- var Map = {
1624
+ var Map2 = {
1079
1625
  Root: MapRoot,
1080
- Content: MapContent,
1626
+ Viewport: MapViewport,
1081
1627
  Tiles: MapTiles,
1082
1628
  Markers: MapMarkers,
1629
+ Lines: MapLines,
1083
1630
  Zoom: MapZoom,
1084
1631
  Action: MapAction
1085
1632
  };
1086
1633
  export {
1087
1634
  ActionControls,
1635
+ DEFAULT_TILE_URL,
1088
1636
  Globe,
1089
1637
  GlobeContext,
1090
- Map,
1638
+ Map2 as Map,
1091
1639
  ZoomControls,
1092
1640
  closestPoint,
1093
1641
  controlPositions,
1094
1642
  createLayers,
1643
+ createRotationTween,
1644
+ flyDuration,
1095
1645
  geoCircle,
1096
1646
  geoInertiaDrag,
1097
1647
  geoLine,
1098
1648
  geoPoint,
1099
1649
  geoToPosition,
1100
1650
  getDistance,
1101
- loadTopology,
1651
+ globeStyles,
1102
1652
  positionToRotation,
1103
1653
  renderLayers,
1104
1654
  restrictAxis,
1105
1655
  timer,
1106
- translationKey,
1107
- translations,
1108
1656
  useDrag,
1109
1657
  useGlobeContext,
1110
1658
  useGlobeZoomHandler,
1111
1659
  useMapZoomHandler,
1660
+ useSimplifiedTopology,
1112
1661
  useSpinner,
1113
- useTour
1662
+ useTopology,
1663
+ useTour,
1664
+ useWheel
1114
1665
  };
1115
1666
  //# sourceMappingURL=index.mjs.map