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