@dxos/react-ui-geo 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (121) 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 +1054 -578
  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 +1054 -578
  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 +19 -9
  45. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  46. package/dist/types/src/components/Globe/Globe.stories.d.ts +37 -11
  47. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  48. package/dist/types/src/components/Map/Map.d.ts +55 -16
  49. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  50. package/dist/types/src/components/Map/Map.stories.d.ts +19 -9
  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 +38 -3
  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 -1
  76. package/dist/types/src/index.d.ts.map +1 -1
  77. package/dist/types/src/translations.d.ts +12 -0
  78. package/dist/types/src/translations.d.ts.map +1 -0
  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 +43 -37
  92. package/src/components/Globe/Globe.stories.tsx +201 -82
  93. package/src/components/Globe/Globe.tsx +263 -119
  94. package/src/components/Map/Map.stories.tsx +66 -17
  95. package/src/components/Map/Map.tsx +333 -120
  96. package/src/components/Toolbar/Controls.tsx +12 -14
  97. package/src/data.ts +19 -2
  98. package/src/hooks/context.tsx +49 -28
  99. package/src/hooks/index.ts +3 -0
  100. package/src/hooks/useDrag.ts +33 -5
  101. package/src/hooks/useGlobeZoomHandler.ts +10 -3
  102. package/src/hooks/useSimplifiedTopology.ts +81 -0
  103. package/src/hooks/useSpinner.ts +1 -2
  104. package/src/hooks/useTopology.ts +95 -0
  105. package/src/hooks/useTour.ts +71 -81
  106. package/src/hooks/useWheel.ts +83 -0
  107. package/src/index.ts +0 -1
  108. package/src/translations.ts +20 -0
  109. package/src/util/animation.ts +35 -0
  110. package/src/util/index.ts +2 -0
  111. package/src/util/inertia.ts +87 -4
  112. package/src/util/render.ts +105 -17
  113. package/src/util/styles.ts +62 -0
  114. package/dist/lib/browser/chunk-GMWLKTLN.mjs +0 -9
  115. package/dist/lib/browser/chunk-GMWLKTLN.mjs.map +0 -7
  116. package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs +0 -37859
  117. package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs.map +0 -7
  118. package/dist/lib/node-esm/chunk-JODBF4CC.mjs +0 -11
  119. package/dist/lib/node-esm/chunk-JODBF4CC.mjs.map +0 -7
  120. package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs +0 -37861
  121. package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs.map +0 -7
@@ -1,45 +1,19 @@
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 { useSignals as _useSignals3 } from "@preact-signals/safe-react/tracking";
7
- import { easeLinear, easeSinOut, geoMercator, geoOrthographic, geoPath as geoPath2, geoTransverseMercator, interpolateNumber, transition } from "d3";
8
- import React3, { 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";
9
8
  import { useResizeDetector } from "react-resize-detector";
10
- import { useDynamicRef, useThemeContext } from "@dxos/react-ui";
11
- import { mx } from "@dxos/react-ui-theme";
9
+ import { useComposedRefs, useControlledState, useDynamicRef, useThemeContext } from "@dxos/react-ui";
10
+ import { composable, composableProps } from "@dxos/react-ui";
11
+ import { mx } from "@dxos/ui-theme";
12
12
 
13
13
  // src/hooks/context.tsx
14
- import { useSignals as _useSignals } from "@preact-signals/safe-react/tracking";
15
- import React, { createContext, useContext } from "react";
14
+ import { createContext, useContext } from "react";
16
15
  import { raise } from "@dxos/debug";
17
- import { useControlledState } from "@dxos/react-ui";
18
16
  var GlobeContext = /* @__PURE__ */ createContext(void 0);
19
- var GlobeContextProvider = ({ children, size, center: _center, zoom: _zoom, translation: _translation, rotation: _rotation }) => {
20
- var _effect = _useSignals();
21
- try {
22
- const [center, setCenter] = useControlledState(_center);
23
- const [zoom, setZoom] = useControlledState(_zoom);
24
- const [translation, setTranslation] = useControlledState(_translation);
25
- const [rotation, setRotation] = useControlledState(_rotation);
26
- return /* @__PURE__ */ React.createElement(GlobeContext.Provider, {
27
- value: {
28
- size,
29
- center,
30
- zoom,
31
- translation,
32
- rotation,
33
- setCenter,
34
- setZoom,
35
- setTranslation,
36
- setRotation
37
- }
38
- }, children);
39
- } finally {
40
- _effect.f();
41
- }
42
- };
43
17
  var useGlobeContext = () => {
44
18
  return useContext(GlobeContext) ?? raise(new Error("Missing GlobeContext"));
45
19
  };
@@ -48,6 +22,18 @@ var useGlobeContext = () => {
48
22
  import { select as select2 } from "d3";
49
23
  import { useEffect } from "react";
50
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
+
51
37
  // src/util/debug.ts
52
38
  var debug = false;
53
39
  var timer = (cb) => {
@@ -65,7 +51,7 @@ var timer = (cb) => {
65
51
 
66
52
  // src/util/inertia.ts
67
53
  import { drag, select, timer as timer2 } from "d3";
68
- import versor from "versor";
54
+ import versor2 from "versor";
69
55
  var restrictAxis = (axis) => (original, current) => current.map((d, i) => axis[i] ? d : original[i]);
70
56
  var geoInertiaDrag = (target, render, projection, options) => {
71
57
  if (!options) {
@@ -75,21 +61,23 @@ var geoInertiaDrag = (target, render, projection, options) => {
75
61
  target = target.node();
76
62
  }
77
63
  target = select(target);
78
- 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 = {
79
75
  projection,
80
76
  render: (rotation) => {
81
77
  projection.rotate(rotation);
82
78
  render && render();
83
79
  },
84
- axis: restrictAxis(options.xAxis ? [
85
- true,
86
- false,
87
- false
88
- ] : [
89
- true,
90
- true,
91
- true
92
- ]),
80
+ axis,
93
81
  start: options.start,
94
82
  move: options.move,
95
83
  end: options.end,
@@ -97,7 +85,12 @@ var geoInertiaDrag = (target, render, projection, options) => {
97
85
  finish: options.finish,
98
86
  time: options.time,
99
87
  hold: options.hold
100
- });
88
+ };
89
+ const inertia = linear ? geoInertiaDragLinearHelper({
90
+ ...sharedHandlers,
91
+ sensitivity: options.sensitivity,
92
+ getZoom: options.getZoom
93
+ }) : geoInertiaDragHelper(sharedHandlers);
101
94
  target.call(drag().on("start", inertia.start).on("drag", inertia.move).on("end", inertia.end));
102
95
  return inertia;
103
96
  };
@@ -112,9 +105,9 @@ var geoInertiaDragHelper = (opt) => {
112
105
  const inertia = inertiaHelper({
113
106
  axis: opt.axis,
114
107
  start: () => {
115
- v0 = versor.cartesian(projection.invert(inertia.position));
108
+ v0 = versor2.cartesian(projection.invert(inertia.position));
116
109
  r0 = projection.rotate();
117
- q0 = versor(r0);
110
+ q0 = versor2(r0);
118
111
  opt.start && opt.start();
119
112
  },
120
113
  move: () => {
@@ -122,23 +115,23 @@ var geoInertiaDragHelper = (opt) => {
122
115
  if (isNaN(inv[0])) {
123
116
  return;
124
117
  }
125
- const v1 = versor.cartesian(inv);
126
- const q1 = versor.multiply(q0, versor.delta(v0, v1));
127
- 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);
128
121
  const r2 = opt.axis(r0, r1);
129
122
  opt.render(r2);
130
123
  opt.move && opt.move();
131
124
  },
132
125
  end: () => {
133
- v10 = versor.cartesian(projection.invert(inertia.position.map((d, i) => d - inertia.velocity[i] / 1e3)));
134
- q10 = versor(projection.rotate());
135
- 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));
136
129
  opt.end && opt.end();
137
130
  },
138
131
  stop: opt.stop,
139
132
  finish: opt.finish,
140
133
  render: (t) => {
141
- 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)));
142
135
  const r2 = opt.axis(r0, r1);
143
136
  opt.render && opt.render(r2);
144
137
  },
@@ -146,6 +139,62 @@ var geoInertiaDragHelper = (opt) => {
146
139
  });
147
140
  return inertia;
148
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
+ };
149
198
  function inertiaHelper(opt) {
150
199
  const A = opt.time || 5e3;
151
200
  const limit = 1.0001;
@@ -249,9 +298,9 @@ var geoToPosition = ({ lat, lng }) => [
249
298
  lng,
250
299
  lat
251
300
  ];
252
- var geoPoint = (point) => ({
301
+ var geoPoint = (point2) => ({
253
302
  type: "Point",
254
- coordinates: geoToPosition(point)
303
+ coordinates: geoToPosition(point2)
255
304
  });
256
305
  var geoCircle = ({ lat, lng }, radius) => d3GeoCircle().radius(radius).center([
257
306
  lng,
@@ -276,11 +325,11 @@ var closestPoint = (points, target) => {
276
325
  }
277
326
  let closestPoint2 = points[0];
278
327
  let minDistance = getDistance(points[0], target);
279
- for (const point of points) {
280
- const distance = getDistance(point, target);
328
+ for (const point2 of points) {
329
+ const distance = getDistance(point2, target);
281
330
  if (distance < minDistance) {
282
331
  minDistance = distance;
283
- closestPoint2 = point;
332
+ closestPoint2 = point2;
284
333
  }
285
334
  }
286
335
  return closestPoint2;
@@ -292,8 +341,48 @@ var getDistance = (point1, point2) => {
292
341
  };
293
342
 
294
343
  // src/util/render.ts
295
- import { geoGraticule } from "d3";
344
+ import { geoBounds, geoCentroid, geoDistance as geoDistance2, geoGraticule } from "d3";
296
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
+ };
297
386
  var createLayers = (topology, features, styles) => {
298
387
  const layers = [];
299
388
  if (styles.water) {
@@ -314,11 +403,25 @@ var createLayers = (topology, features, styles) => {
314
403
  });
315
404
  }
316
405
  if (topology) {
317
- if (topology.objects.land && styles.land) {
318
- layers.push({
319
- styles: styles.land,
320
- path: feature(topology, topology.objects.land)
321
- });
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
+ }
322
425
  }
323
426
  if (topology.objects.countries && styles.border) {
324
427
  layers.push({
@@ -335,28 +438,28 @@ var createLayers = (topology, features, styles) => {
335
438
  }
336
439
  if (features) {
337
440
  const { points, lines } = features;
338
- if (points && styles.point) {
441
+ if (lines && styles.line) {
339
442
  layers.push({
340
- styles: styles.point,
443
+ styles: styles.line,
341
444
  path: {
342
445
  type: "GeometryCollection",
343
- geometries: points.map((point) => geoPoint(point))
446
+ geometries: lines.map(({ source, target }) => geoLine(source, target))
344
447
  }
345
448
  });
346
449
  }
347
- if (lines && styles.line) {
450
+ if (points && styles.point) {
348
451
  layers.push({
349
- styles: styles.line,
452
+ styles: styles.point,
350
453
  path: {
351
454
  type: "GeometryCollection",
352
- geometries: lines.map(({ source, target }) => geoLine(source, target))
455
+ geometries: points.map((point2) => geoPoint(point2))
353
456
  }
354
457
  });
355
458
  }
356
459
  }
357
460
  return layers;
358
461
  };
359
- var renderLayers = (generator, layers = [], scale, styles) => {
462
+ var renderLayers = (generator, layers = [], scale, styles, viewCenter) => {
360
463
  const context = generator.context();
361
464
  const { canvas: { width, height } } = context;
362
465
  context.reset();
@@ -366,7 +469,8 @@ var renderLayers = (generator, layers = [], scale, styles) => {
366
469
  } else {
367
470
  context.clearRect(0, 0, width, height);
368
471
  }
369
- layers.forEach(({ path, styles: styles2 }) => {
472
+ layers.forEach((layer) => {
473
+ const { path, styles: styles2, cullable } = layer;
370
474
  context.save();
371
475
  let fill = false;
372
476
  let stroke = false;
@@ -381,8 +485,23 @@ var renderLayers = (generator, layers = [], scale, styles) => {
381
485
  }
382
486
  });
383
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
+ }
384
503
  context.beginPath();
385
- generator(path);
504
+ generator(renderPath);
386
505
  fill && context.fill();
387
506
  stroke && context.stroke();
388
507
  context.restore();
@@ -390,6 +509,57 @@ var renderLayers = (generator, layers = [], scale, styles) => {
390
509
  return context;
391
510
  };
392
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
+
393
563
  // src/hooks/useDrag.ts
394
564
  var useDrag = (controller, options = {}) => {
395
565
  useEffect(() => {
@@ -397,14 +567,19 @@ var useDrag = (controller, options = {}) => {
397
567
  if (!canvas || options.disabled) {
398
568
  return;
399
569
  }
400
- geoInertiaDrag(select2(canvas), () => {
570
+ const inertia = geoInertiaDrag(select2(canvas), () => {
401
571
  controller.setRotation(controller.projection.rotate());
402
572
  options.onUpdate?.({
403
573
  type: "move",
404
574
  controller
405
575
  });
406
576
  }, controller.projection, {
407
- 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,
408
583
  time: 3e3,
409
584
  start: () => options.onUpdate?.({
410
585
  type: "start",
@@ -417,6 +592,7 @@ var useDrag = (controller, options = {}) => {
417
592
  });
418
593
  return () => {
419
594
  cancelDrag(select2(canvas));
595
+ inertia?.timer?.stop();
420
596
  };
421
597
  }, [
422
598
  controller,
@@ -427,6 +603,7 @@ var cancelDrag = (node) => node.on(".drag", null);
427
603
 
428
604
  // src/hooks/useGlobeZoomHandler.ts
429
605
  import { useCallback } from "react";
606
+ var ZOOM_FACTOR = 0.1;
430
607
  var useGlobeZoomHandler = (controller) => {
431
608
  return useCallback((event) => {
432
609
  if (!controller) {
@@ -434,11 +611,15 @@ var useGlobeZoomHandler = (controller) => {
434
611
  }
435
612
  switch (event) {
436
613
  case "zoom-in": {
437
- controller.setZoom((zoom) => zoom * 1.1);
614
+ controller.setZoom((zoom) => {
615
+ return zoom * (1 + ZOOM_FACTOR);
616
+ });
438
617
  break;
439
618
  }
440
619
  case "zoom-out": {
441
- controller.setZoom((zoom) => zoom * 0.9);
620
+ controller.setZoom((zoom) => {
621
+ return zoom * (1 - ZOOM_FACTOR);
622
+ });
442
623
  break;
443
624
  }
444
625
  }
@@ -469,6 +650,61 @@ var useMapZoomHandler = (controller) => {
469
650
  ]);
470
651
  };
471
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
+
472
708
  // src/hooks/useSpinner.ts
473
709
  import { timer as d3Timer } from "d3";
474
710
  import { useEffect as useEffect2, useState } from "react";
@@ -522,91 +758,139 @@ var useSpinner = (controller, options = {}) => {
522
758
  ];
523
759
  };
524
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
+
525
814
  // src/hooks/useTour.ts
526
- import { selection as d3Selection, geoDistance, geoInterpolate, geoPath } from "d3";
527
- import { useEffect as useEffect3, useMemo, useState as useState2 } from "react";
528
- import versor2 from "versor";
529
- var TRANSITION_NAME = "globe-tour";
815
+ import { geoInterpolate, geoPath } from "d3";
816
+ import { useEffect as useEffect4, useState as useState3 } from "react";
530
817
  var defaultDuration = 1500;
531
818
  var useTour = (controller, points, options = {}) => {
532
- const selection = useMemo(() => d3Selection(), []);
533
- const [running, setRunning] = useState2(options.running ?? false);
534
- useEffect3(() => {
535
- if (!running) {
536
- selection.interrupt(TRANSITION_NAME);
819
+ const [running, setRunning] = useState3(options.running ?? false);
820
+ useEffect4(() => {
821
+ if (!controller || !running) {
537
822
  return;
538
823
  }
539
- let t;
540
- if (controller && running) {
541
- t = setTimeout(async () => {
542
- const { canvas, projection, setRotation } = controller;
543
- const context = canvas.getContext("2d", {
544
- alpha: false
545
- });
546
- const path = geoPath(projection, context).pointRadius(2);
547
- 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
+ }
548
838
  let last;
549
- try {
550
- const p = [
551
- ...points
552
- ];
553
- if (options.loop) {
554
- p.push(p[0]);
839
+ for (const next of tourPoints) {
840
+ if (cancelled) {
841
+ break;
555
842
  }
556
- for (const next of p) {
557
- if (!running) {
558
- break;
559
- }
560
- const p1 = last ? geoToPosition(last) : void 0;
561
- const p2 = geoToPosition(next);
562
- const ip = geoInterpolate(p1 || p2, p2);
563
- const distance = geoDistance(p1 || p2, p2);
564
- const r1 = p1 ? positionToRotation(p1, tilt) : controller.projection.rotate();
565
- const r2 = positionToRotation(p2, tilt);
566
- const iv = versor2.interpolate(r1, r2);
567
- const transition2 = selection.transition(TRANSITION_NAME).duration(Math.max(options.duration ?? defaultDuration, distance * 2e3)).tween("render", () => (t2) => {
568
- const t1 = Math.max(0, Math.min(1, t2 * 2 - 1));
569
- const t22 = Math.min(1, t2 * 2);
570
- context.save();
571
- {
572
- context.beginPath();
573
- context.strokeStyle = options?.styles?.arc?.strokeStyle ?? "yellow";
574
- context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
575
- context.setLineDash(options?.styles?.arc?.lineDash ?? []);
576
- path({
577
- type: "LineString",
578
- coordinates: [
579
- ip(t1),
580
- ip(t22)
581
- ]
582
- });
583
- context.stroke();
584
- context.beginPath();
585
- context.fillStyle = options?.styles?.cursor?.fillStyle ?? "orange";
586
- path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
587
- path({
588
- type: "Point",
589
- coordinates: ip(t22)
590
- });
591
- context.fill();
592
- }
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 {
593
872
  context.restore();
594
- projection.rotate(iv(t2));
595
- setRotation(projection.rotate());
596
- });
597
- await transition2.end();
598
- last = next;
599
- }
600
- } catch {
601
- } 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) {
602
885
  setRunning(false);
603
886
  }
604
- });
605
- return () => {
606
- clearTimeout(t);
607
- selection.interrupt(TRANSITION_NAME);
608
- };
609
- }
887
+ }
888
+ });
889
+ return () => {
890
+ cancelled = true;
891
+ clearTimeout(t);
892
+ controller.cancelFlyTo();
893
+ };
610
894
  }, [
611
895
  controller,
612
896
  running,
@@ -618,10 +902,55 @@ var useTour = (controller, points, options = {}) => {
618
902
  ];
619
903
  };
620
904
 
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;
914
+ }
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
+ };
949
+
621
950
  // src/components/Toolbar/Controls.tsx
622
- import { useSignals as _useSignals2 } from "@preact-signals/safe-react/tracking";
623
- import React2 from "react";
624
- import { IconButton, Toolbar } from "@dxos/react-ui";
951
+ import React from "react";
952
+ import { IconButton, Toolbar, useTranslation } from "@dxos/react-ui";
953
+ import { translationKey } from "#translations";
625
954
  var controlPositions = {
626
955
  topleft: "top-2 left-2",
627
956
  topright: "top-2 right-2",
@@ -629,58 +958,42 @@ var controlPositions = {
629
958
  bottomright: "bottom-2 right-2"
630
959
  };
631
960
  var ZoomControls = ({ classNames, onAction }) => {
632
- var _effect = _useSignals2();
633
- try {
634
- return /* @__PURE__ */ React2.createElement(Toolbar.Root, {
635
- classNames: [
636
- "gap-2",
637
- classNames
638
- ]
639
- }, /* @__PURE__ */ React2.createElement(IconButton, {
640
- icon: "ph--plus--regular",
641
- label: "zoom in",
642
- iconOnly: true,
643
- size: 5,
644
- classNames: "px-0 aspect-square",
645
- onClick: () => onAction?.("zoom-in")
646
- }), /* @__PURE__ */ React2.createElement(IconButton, {
647
- icon: "ph--minus--regular",
648
- label: "zoom out",
649
- iconOnly: true,
650
- size: 5,
651
- classNames: "px-0 aspect-square",
652
- onClick: () => onAction?.("zoom-out")
653
- }));
654
- } finally {
655
- _effect.f();
656
- }
961
+ const { t } = useTranslation(translationKey);
962
+ return /* @__PURE__ */ React.createElement(Toolbar.Root, {
963
+ classNames: [
964
+ "gap-2",
965
+ classNames
966
+ ]
967
+ }, /* @__PURE__ */ React.createElement(IconButton, {
968
+ icon: "ph--plus--regular",
969
+ iconOnly: true,
970
+ label: t("zoom-in-icon.button"),
971
+ onClick: () => onAction?.("zoom-in")
972
+ }), /* @__PURE__ */ React.createElement(IconButton, {
973
+ icon: "ph--minus--regular",
974
+ iconOnly: true,
975
+ label: t("zoom-out-icon.button"),
976
+ onClick: () => onAction?.("zoom-out")
977
+ }));
657
978
  };
658
979
  var ActionControls = ({ classNames, onAction }) => {
659
- var _effect = _useSignals2();
660
- try {
661
- return /* @__PURE__ */ React2.createElement(Toolbar.Root, {
662
- classNames: [
663
- "gap-2",
664
- classNames
665
- ]
666
- }, /* @__PURE__ */ React2.createElement(IconButton, {
667
- icon: "ph--play--regular",
668
- label: "start",
669
- iconOnly: true,
670
- size: 5,
671
- classNames: "px-0 aspect-square",
672
- onClick: () => onAction?.("start")
673
- }), /* @__PURE__ */ React2.createElement(IconButton, {
674
- icon: "ph--globe-hemisphere-west--regular",
675
- label: "toggle",
676
- iconOnly: true,
677
- size: 5,
678
- classNames: "px-0 aspect-square",
679
- onClick: () => onAction?.("toggle")
680
- }));
681
- } finally {
682
- _effect.f();
683
- }
980
+ const { t } = useTranslation(translationKey);
981
+ return /* @__PURE__ */ React.createElement(Toolbar.Root, {
982
+ classNames: [
983
+ "gap-2",
984
+ classNames
985
+ ]
986
+ }, /* @__PURE__ */ React.createElement(IconButton, {
987
+ icon: "ph--path--regular",
988
+ iconOnly: true,
989
+ label: t("start-icon.button"),
990
+ onClick: () => onAction?.("start")
991
+ }), /* @__PURE__ */ React.createElement(IconButton, {
992
+ icon: "ph--globe-hemisphere-west--regular",
993
+ iconOnly: true,
994
+ label: t("toggle-icon.button"),
995
+ onClick: () => onAction?.("toggle")
996
+ }));
684
997
  };
685
998
 
686
999
  // src/components/Globe/Globe.tsx
@@ -738,188 +1051,245 @@ var getProjection = (type = "orthographic") => {
738
1051
  }
739
1052
  return type ?? geoOrthographic();
740
1053
  };
741
- var GlobeRoot = ({ classNames, children, ...props }) => {
742
- var _effect = _useSignals3();
743
- try {
744
- const { ref, width, height } = useResizeDetector();
745
- return /* @__PURE__ */ React3.createElement("div", {
746
- ref,
747
- className: mx("relative flex grow overflow-hidden", classNames)
748
- }, /* @__PURE__ */ React3.createElement(GlobeContextProvider, {
749
- size: {
750
- width,
751
- height
752
- },
753
- ...props
754
- }, children));
755
- } finally {
756
- _effect.f();
757
- }
758
- };
759
- var GlobeCanvas = /* @__PURE__ */ forwardRef(({ projection: _projection, topology, features, styles: _styles }, forwardRef3) => {
760
- var _effect = _useSignals3();
761
- try {
762
- const { themeMode } = useThemeContext();
763
- const styles = useMemo2(() => _styles ?? defaultStyles[themeMode], [
764
- _styles,
765
- themeMode
766
- ]);
767
- const [canvas, setCanvas] = useState3(null);
768
- const canvasRef = (canvas2) => setCanvas(canvas2);
769
- const projection = useMemo2(() => getProjection(_projection), [
770
- _projection
771
- ]);
772
- const layers = useMemo2(() => {
773
- return timer(() => createLayers(topology, features, styles));
774
- }, [
775
- topology,
776
- features,
777
- styles
778
- ]);
779
- const { size, center, zoom, translation, rotation, setCenter, setZoom, setTranslation, setRotation } = useGlobeContext();
780
- const zoomRef = useDynamicRef(zoom);
781
- useEffect4(() => {
782
- if (center) {
783
- setZoom(1);
784
- setRotation(positionToRotation(geoToPosition(center)));
785
- }
786
- }, [
787
- center
788
- ]);
789
- const zooming = useRef(false);
790
- useImperativeHandle(forwardRef3, () => {
791
- return {
792
- canvas,
793
- projection,
794
- center,
795
- get zoom() {
796
- return zoomRef.current;
797
- },
798
- translation,
799
- rotation,
800
- setCenter,
801
- setZoom: (s) => {
802
- if (typeof s === "function") {
803
- const is = interpolateNumber(zoomRef.current, s(zoomRef.current));
804
- transition().ease(zooming.current ? easeLinear : easeSinOut).duration(200).tween("scale", () => (t) => setZoom(is(t))).on("end", () => {
805
- zooming.current = false;
806
- });
807
- } else {
808
- setZoom(s);
809
- }
810
- },
811
- setTranslation,
812
- setRotation
813
- };
814
- }, [
815
- canvas
816
- ]);
817
- const generator = useMemo2(() => canvas && projection && geoPath2(projection, canvas.getContext("2d", {
818
- alpha: false
819
- })), [
820
- canvas,
821
- projection
822
- ]);
823
- useEffect4(() => {
824
- if (canvas && projection) {
825
- timer(() => {
826
- projection.scale(Math.min(size.width, size.height) / 2 * zoom).translate([
827
- size.width / 2 + (translation?.x ?? 0),
828
- size.height / 2 + (translation?.y ?? 0)
829
- ]).rotate(rotation ?? [
830
- 0,
831
- 0,
832
- 0
833
- ]);
834
- renderLayers(generator, layers, zoom, styles);
835
- });
836
- }
837
- }, [
838
- generator,
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
1059
+ });
1060
+ const [center, setCenter] = useControlledState(centerProp);
1061
+ const [zoom, setZoom] = useControlledState(zoomProp);
1062
+ const [translation, setTranslation] = useControlledState(translationProp);
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
+ ]);
1069
+ return /* @__PURE__ */ React2.createElement(GlobeContext.Provider, {
1070
+ value: {
839
1071
  size,
1072
+ center,
840
1073
  zoom,
841
1074
  translation,
842
1075
  rotation,
843
- layers
844
- ]);
845
- if (!size.width || !size.height) {
846
- return null;
1076
+ setSize,
1077
+ setCenter,
1078
+ setZoom,
1079
+ setTranslation,
1080
+ setRotation,
1081
+ registerController
847
1082
  }
848
- return /* @__PURE__ */ React3.createElement("canvas", {
849
- ref: canvasRef,
850
- width: size.width,
851
- height: size.height
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
852
1097
  });
853
- } finally {
854
- _effect.f();
855
- }
1098
+ }, [
1099
+ width,
1100
+ height,
1101
+ setSize
1102
+ ]);
1103
+ return /* @__PURE__ */ React2.createElement("div", {
1104
+ ...composableProps(props, {
1105
+ classNames: "relative dx-container"
1106
+ }),
1107
+ ref: composedRef
1108
+ }, children);
856
1109
  });
857
- var GlobeDebug = ({ position = "topleft" }) => {
858
- var _effect = _useSignals3();
859
- try {
860
- const { size, zoom, translation, rotation } = useGlobeContext();
861
- return /* @__PURE__ */ React3.createElement("div", {
862
- className: mx("z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded", controlPositions[position])
863
- }, /* @__PURE__ */ React3.createElement("pre", {
864
- className: "font-mono text-xs text-green-700"
865
- }, JSON.stringify({
866
- size,
867
- zoom,
1110
+ GlobeViewport.displayName = "Globe.Viewport";
1111
+ var GlobeCanvas = ({ projection: projectionProp, topology, features, styles: stylesProp }) => {
1112
+ const { themeMode } = useThemeContext();
1113
+ const styles = useMemo2(() => stylesProp ?? defaultStyles[themeMode], [
1114
+ stylesProp,
1115
+ themeMode
1116
+ ]);
1117
+ const { size, center, zoom, translation, rotation, setZoom, setTranslation, setRotation, registerController } = useGlobeContext();
1118
+ const zoomRef = useDynamicRef(zoom);
1119
+ const [canvas, setCanvas] = useState4(null);
1120
+ const canvasRef = (canvas2) => setCanvas(canvas2);
1121
+ const projection = useMemo2(() => getProjection(projectionProp), [
1122
+ projectionProp
1123
+ ]);
1124
+ const layers = useMemo2(() => {
1125
+ return timer(() => createLayers(topology, features, styles));
1126
+ }, [
1127
+ topology,
1128
+ features,
1129
+ styles
1130
+ ]);
1131
+ useEffect6(() => {
1132
+ if (center) {
1133
+ setRotation(positionToRotation(geoToPosition(center)));
1134
+ }
1135
+ }, [
1136
+ center
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
+ ]);
1146
+ const zooming = useRef(false);
1147
+ const controller = useMemo2(() => {
1148
+ return {
1149
+ canvas,
1150
+ projection,
1151
+ get zoom() {
1152
+ return zoomRef.current;
1153
+ },
868
1154
  translation,
869
- rotation
870
- }, null, 2)));
871
- } finally {
872
- _effect.f();
1155
+ rotation,
1156
+ setZoom: (state) => {
1157
+ if (typeof state === "function") {
1158
+ const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
1159
+ transition().ease(zooming.current ? easeLinear : easeSinOut).duration(200).tween("scale", () => (t) => setZoom(is(t))).on("end", () => {
1160
+ zooming.current = false;
1161
+ });
1162
+ } else {
1163
+ setZoom(state);
1164
+ }
1165
+ },
1166
+ setTranslation,
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
+ }
1193
+ };
1194
+ }, [
1195
+ canvas,
1196
+ projection,
1197
+ flyToSelection,
1198
+ flyToTransitionName
1199
+ ]);
1200
+ useEffect6(() => {
1201
+ registerController(controller);
1202
+ return () => registerController(null);
1203
+ }, [
1204
+ registerController,
1205
+ controller
1206
+ ]);
1207
+ const generator = useMemo2(() => canvas && projection && geoPath2(projection, canvas.getContext("2d")), [
1208
+ canvas,
1209
+ projection
1210
+ ]);
1211
+ useEffect6(() => {
1212
+ if (canvas && projection) {
1213
+ timer(() => {
1214
+ projection.scale(Math.min(size.width, size.height) / 2 * zoom).translate([
1215
+ size.width / 2 + (translation?.x ?? 0),
1216
+ size.height / 2 + (translation?.y ?? 0)
1217
+ ]).rotate(rotation ?? [
1218
+ 0,
1219
+ 0,
1220
+ 0
1221
+ ]);
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);
1233
+ });
1234
+ }
1235
+ }, [
1236
+ generator,
1237
+ size,
1238
+ zoom,
1239
+ translation,
1240
+ rotation,
1241
+ layers,
1242
+ projectionProp
1243
+ ]);
1244
+ if (!size.width || !size.height) {
1245
+ return null;
873
1246
  }
1247
+ return /* @__PURE__ */ React2.createElement("canvas", {
1248
+ ref: canvasRef,
1249
+ className: "bg-base-surface",
1250
+ width: size.width,
1251
+ height: size.height
1252
+ });
1253
+ };
1254
+ GlobeCanvas.displayName = "Globe.Canvas";
1255
+ var GlobeDebug = ({ position = "topleft" }) => {
1256
+ const { size, zoom, translation, rotation } = useGlobeContext();
1257
+ return /* @__PURE__ */ React2.createElement("div", {
1258
+ className: mx("z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm", controlPositions[position])
1259
+ }, /* @__PURE__ */ React2.createElement("pre", {
1260
+ className: "font-mono text-xs text-green-700"
1261
+ }, JSON.stringify({
1262
+ size,
1263
+ zoom,
1264
+ translation,
1265
+ rotation
1266
+ }, null, 2)));
874
1267
  };
875
1268
  var GlobePanel = ({ position, classNames, children }) => {
876
- var _effect = _useSignals3();
877
- try {
878
- return /* @__PURE__ */ React3.createElement("div", {
879
- className: mx("z-10 absolute overflow-hidden", controlPositions[position], classNames)
880
- }, children);
881
- } finally {
882
- _effect.f();
883
- }
1269
+ return /* @__PURE__ */ React2.createElement("div", {
1270
+ className: mx("z-10 absolute overflow-hidden", controlPositions[position], classNames)
1271
+ }, children);
884
1272
  };
885
1273
  var CustomControl = ({ position, children }) => {
886
- var _effect = _useSignals3();
887
- try {
888
- return /* @__PURE__ */ React3.createElement("div", {
889
- className: mx("z-10 absolute overflow-hidden", controlPositions[position])
890
- }, children);
891
- } finally {
892
- _effect.f();
893
- }
894
- };
895
- var GlobeZoom = ({ onAction, position = "bottomleft", ...props }) => {
896
- var _effect = _useSignals3();
897
- try {
898
- return /* @__PURE__ */ React3.createElement(CustomControl, {
899
- position,
900
- ...props
901
- }, /* @__PURE__ */ React3.createElement(ZoomControls, {
902
- onAction
903
- }));
904
- } finally {
905
- _effect.f();
906
- }
907
- };
908
- var GlobeAction = ({ onAction, position = "bottomright", ...props }) => {
909
- var _effect = _useSignals3();
910
- try {
911
- return /* @__PURE__ */ React3.createElement(CustomControl, {
912
- position,
913
- ...props
914
- }, /* @__PURE__ */ React3.createElement(ActionControls, {
915
- onAction
916
- }));
917
- } finally {
918
- _effect.f();
919
- }
1274
+ return /* @__PURE__ */ React2.createElement("div", {
1275
+ className: mx("z-10 absolute overflow-hidden", controlPositions[position])
1276
+ }, children);
920
1277
  };
1278
+ var GlobeZoom = ({ onAction, position = "bottomleft", ...props }) => /* @__PURE__ */ React2.createElement(CustomControl, {
1279
+ position,
1280
+ ...props
1281
+ }, /* @__PURE__ */ React2.createElement(ZoomControls, {
1282
+ onAction
1283
+ }));
1284
+ var GlobeAction = ({ onAction, position = "bottomright", ...props }) => /* @__PURE__ */ React2.createElement(CustomControl, {
1285
+ position,
1286
+ ...props
1287
+ }, /* @__PURE__ */ React2.createElement(ActionControls, {
1288
+ onAction
1289
+ }));
921
1290
  var Globe = {
922
1291
  Root: GlobeRoot,
1292
+ Viewport: GlobeViewport,
923
1293
  Canvas: GlobeCanvas,
924
1294
  Zoom: GlobeZoom,
925
1295
  Action: GlobeAction,
@@ -928,16 +1298,15 @@ var Globe = {
928
1298
  };
929
1299
 
930
1300
  // src/components/Map/Map.tsx
931
- import { useSignals as _useSignals4 } from "@preact-signals/safe-react/tracking";
932
1301
  import "leaflet/dist/leaflet.css";
933
1302
  import { createContext as createContext2 } from "@radix-ui/react-context";
934
- import L, { Control, DomEvent, DomUtil, latLngBounds } from "leaflet";
935
- import React4, { forwardRef as forwardRef2, useEffect as useEffect5, useImperativeHandle as useImperativeHandle2, useRef as useRef2, useState as useState4 } 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";
936
1305
  import { createRoot } from "react-dom/client";
937
- import { MapContainer, Marker, Popup, TileLayer, useMap } from "react-leaflet";
938
- import { debounce } from "@dxos/async";
1306
+ import { MapContainer, Marker, Polyline, Popup, TileLayer, useMap, useMapEvents } from "react-leaflet";
939
1307
  import { ThemeProvider, Tooltip } from "@dxos/react-ui";
940
- import { defaultTx, mx as mx2 } from "@dxos/react-ui-theme";
1308
+ import { composable as composable2, composableProps as composableProps2, defaultTx } from "@dxos/react-ui";
1309
+ import { mx as mx2 } from "@dxos/ui-theme";
941
1310
  var defaults = {
942
1311
  center: {
943
1312
  lat: 51,
@@ -945,237 +1314,341 @@ var defaults = {
945
1314
  },
946
1315
  zoom: 4
947
1316
  };
948
- var [MapContextProvier, useMapContext] = createContext2("Map");
949
- var MapRoot = /* @__PURE__ */ forwardRef2(({ classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center = defaults.center, zoom = defaults.zoom, onChange, ...props }, forwardedRef) => {
950
- var _effect = _useSignals4();
951
- try {
952
- const [attention, setAttention] = useState4(false);
953
- const mapRef = useRef2(null);
954
- const map = mapRef.current;
955
- useImperativeHandle2(forwardedRef, () => ({
956
- setCenter: (center2, zoom2) => {
957
- mapRef.current?.setView(center2, zoom2);
958
- },
959
- setZoom: (cb) => {
960
- mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
961
- }
962
- }), []);
963
- useEffect5(() => {
964
- if (!map) {
1317
+ var [MapContextProvider, useMapContext] = createContext2("Map");
1318
+ var MapRoot = /* @__PURE__ */ forwardRef2(({ children, onChange }, forwardedRef) => {
1319
+ const mapRef = useRef2(null);
1320
+ const registerMap = useCallback4((map) => {
1321
+ mapRef.current = map;
1322
+ }, []);
1323
+ useImperativeHandle2(forwardedRef, () => ({
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);
1334
+ },
1335
+ setZoom: (cb) => {
1336
+ mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
1337
+ }
1338
+ }), []);
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) {
965
1377
  return;
966
1378
  }
967
- const handler = debounce(() => {
968
- setAttention(true);
969
- onChange?.({
970
- center: map.getCenter(),
971
- zoom: map.getZoom()
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
+ }
972
1392
  });
973
- }, 100);
974
- map.on("move", handler);
975
- map.on("zoom", handler);
976
- map.on("focus", () => setAttention(true));
977
- map.on("blur", () => setAttention(false));
978
- return () => {
979
- map.off("move");
980
- map.off("zoom");
981
- map.off("focus");
982
- map.off("blur");
983
- };
984
- }, [
985
- map,
986
- onChange
987
- ]);
988
- useEffect5(() => {
989
- if (!map) {
990
- return;
991
- }
992
- if (attention) {
993
- map.scrollWheelZoom.enable();
994
- } else {
995
- map.scrollWheelZoom.disable();
996
1393
  }
997
- }, [
998
- map,
999
- attention
1000
- ]);
1001
- return /* @__PURE__ */ React4.createElement(MapContextProvier, {
1002
- attention
1003
- }, /* @__PURE__ */ React4.createElement(MapContainer, {
1004
- ...props,
1005
- ref: mapRef,
1006
- className: mx2("group relative grid bs-full is-full !bg-baseSurface dx-focus-ring-inset", classNames),
1007
- attributionControl: false,
1008
- zoomControl: false,
1009
- scrollWheelZoom,
1010
- doubleClickZoom,
1011
- touchZoom,
1012
- center,
1013
- zoom
1014
- }));
1015
- } finally {
1016
- _effect.f();
1017
- }
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(() => {
1418
+ if (!map) {
1419
+ return;
1420
+ }
1421
+ if (attention) {
1422
+ map.scrollWheelZoom.enable();
1423
+ } else {
1424
+ map.scrollWheelZoom.disable();
1425
+ }
1426
+ }, [
1427
+ map,
1428
+ attention
1429
+ ]);
1430
+ return /* @__PURE__ */ React3.createElement(MapContainer, {
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
+ }),
1435
+ attributionControl: false,
1436
+ zoomControl: false,
1437
+ scrollWheelZoom,
1438
+ doubleClickZoom,
1439
+ touchZoom,
1440
+ // Allow fractional zoom so trackpad pinch (small ctrl+wheel deltas) isn't rounded away.
1441
+ zoomSnap: 0,
1442
+ center: center ?? defaults.center,
1443
+ zoom: zoom ?? defaults.zoom,
1444
+ whenReady,
1445
+ ref: setMapRef
1446
+ }, /* @__PURE__ */ React3.createElement(MapResize, null), /* @__PURE__ */ React3.createElement(MapPinchZoom, null), children);
1018
1447
  });
1019
- MapRoot.displayName = "Map.Root";
1020
- var MapTiles = (_props) => {
1021
- var _effect = _useSignals4();
1022
- try {
1023
- const ref = useRef2(null);
1024
- const { attention } = useMapContext(MapTiles.displayName);
1025
- useEffect5(() => {
1026
- if (ref.current) {
1027
- ref.current.getContainer().dataset.attention = attention ? "1" : "0";
1028
- }
1029
- }, [
1030
- attention
1031
- ]);
1032
- return /* @__PURE__ */ React4.createElement(React4.Fragment, null, /* @__PURE__ */ React4.createElement(TileLayer, {
1033
- ref,
1034
- "data-attention": attention,
1035
- detectRetina: true,
1036
- className: 'dark:grayscale dark:invert data-[attention="0"]:!opacity-80',
1037
- url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
1038
- keepBuffer: 4
1039
- }));
1040
- } finally {
1041
- _effect.f();
1042
- }
1448
+ MapViewport.displayName = "Map.Viewport";
1449
+ var MAP_TILES_NAME = "Map.Tiles";
1450
+ var DEFAULT_TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
1451
+ var MapTiles = ({ url = DEFAULT_TILE_URL }) => {
1452
+ const ref = useRef2(null);
1453
+ const { onChange } = useMapContext(MAP_TILES_NAME);
1454
+ useMapEvents({
1455
+ moveend: (ev) => {
1456
+ onChange?.({
1457
+ center: ev.target.getCenter(),
1458
+ zoom: ev.target.getZoom()
1459
+ });
1460
+ }
1461
+ });
1462
+ const { attention } = useMapContext(MAP_TILES_NAME);
1463
+ useEffect7(() => {
1464
+ if (ref.current) {
1465
+ ref.current.getContainer().dataset.attention = attention ? "1" : "0";
1466
+ }
1467
+ }, [
1468
+ attention
1469
+ ]);
1470
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement(TileLayer, {
1471
+ ref,
1472
+ "data-attention": attention,
1473
+ detectRetina: true,
1474
+ className: 'dark:grayscale dark:invert data-[attention="0"]:!opacity-80',
1475
+ url,
1476
+ keepBuffer: 4
1477
+ }));
1043
1478
  };
1044
- MapTiles.displayName = "Map.Tiles";
1045
- var MapMarkers = ({ selected, markers }) => {
1046
- var _effect = _useSignals4();
1047
- try {
1048
- const map = useMap();
1049
- useEffect5(() => {
1050
- if (markers.length > 0) {
1051
- const bounds = latLngBounds(markers.map((marker) => marker.location));
1052
- map.fitBounds(bounds);
1053
- } else {
1054
- map.setView(defaults.center, defaults.zoom);
1055
- }
1056
- }, [
1057
- markers
1058
- ]);
1059
- return /* @__PURE__ */ React4.createElement(React4.Fragment, null, markers?.map(({ id, title, location: { lat, lng } }) => {
1060
- return /* @__PURE__ */ React4.createElement(Marker, {
1061
- key: id,
1062
- position: {
1063
- lat,
1064
- lng
1065
- },
1066
- icon: (
1067
- // TODO(burdon): Create custom icon from bundled assets.
1068
- // TODO(burdon): Selection state.
1069
- new L.Icon({
1070
- iconUrl: "https://dxos.network/marker-icon.png",
1071
- iconRetinaUrl: "https://dxos.network/marker-icon-2x.png",
1072
- shadowUrl: "https://dxos.network/marker-shadow.png",
1073
- iconSize: [
1074
- 25,
1075
- 41
1076
- ],
1077
- iconAnchor: [
1078
- 12,
1079
- 41
1080
- ],
1081
- popupAnchor: [
1082
- 1,
1083
- -34
1084
- ],
1085
- shadowSize: [
1086
- 41,
1087
- 41
1088
- ]
1089
- })
1090
- )
1091
- }, title && /* @__PURE__ */ React4.createElement(Popup, null, title));
1092
- }));
1093
- } finally {
1094
- _effect.f();
1095
- }
1479
+ MapTiles.displayName = MAP_TILES_NAME;
1480
+ var MapMarkers = ({ selected, markers, lines, onSelect }) => {
1481
+ const map = useMap();
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
+ });
1498
+ }
1499
+ }, [
1500
+ markers,
1501
+ lines,
1502
+ map
1503
+ ]);
1504
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, markers?.map(({ id, title, location: { lat, lng } }) => {
1505
+ return /* @__PURE__ */ React3.createElement(Marker, {
1506
+ key: id,
1507
+ position: {
1508
+ lat,
1509
+ lng
1510
+ },
1511
+ eventHandlers: onSelect ? {
1512
+ click: () => onSelect(id)
1513
+ } : void 0,
1514
+ icon: (
1515
+ // TODO(burdon): Create custom icon from bundled assets.
1516
+ // TODO(burdon): Selection state.
1517
+ new L.Icon({
1518
+ iconUrl: "https://dxos.network/marker-icon.png",
1519
+ iconRetinaUrl: "https://dxos.network/marker-icon-2x.png",
1520
+ shadowUrl: "https://dxos.network/marker-shadow.png",
1521
+ iconSize: [
1522
+ 25,
1523
+ 41
1524
+ ],
1525
+ iconAnchor: [
1526
+ 12,
1527
+ 41
1528
+ ],
1529
+ popupAnchor: [
1530
+ 1,
1531
+ -34
1532
+ ],
1533
+ shadowSize: [
1534
+ 41,
1535
+ 41
1536
+ ]
1537
+ })
1538
+ )
1539
+ }, title && /* @__PURE__ */ React3.createElement(Popup, null, title));
1540
+ }));
1096
1541
  };
1097
1542
  MapMarkers.displayName = "Map.Markers";
1098
- var CustomControl2 = ({ position, children }) => {
1099
- var _effect = _useSignals4();
1100
- try {
1101
- const map = useMap();
1102
- useEffect5(() => {
1103
- const control = new Control({
1104
- position
1105
- });
1106
- control.onAdd = () => {
1107
- const container = DomUtil.create("div", mx2("!m-0", controlPositions[position]));
1108
- DomEvent.disableClickPropagation(container);
1109
- DomEvent.disableScrollPropagation(container);
1110
- const root = createRoot(container);
1111
- root.render(/* @__PURE__ */ React4.createElement(ThemeProvider, {
1112
- tx: defaultTx
1113
- }, /* @__PURE__ */ React4.createElement(Tooltip.Provider, null, children)));
1114
- return container;
1115
- };
1116
- control.addTo(map);
1117
- return () => {
1118
- control.remove();
1119
- };
1120
- }, [
1121
- map,
1122
- position,
1123
- children
1124
- ]);
1543
+ var MapLines = ({ lines }) => {
1544
+ if (!lines || lines.length === 0) {
1125
1545
  return null;
1126
- } finally {
1127
- _effect.f();
1128
1546
  }
1129
- };
1130
- var MapZoom = ({ onAction, position = "bottomleft", ...props }) => {
1131
- var _effect = _useSignals4();
1132
- try {
1133
- return /* @__PURE__ */ React4.createElement(CustomControl2, {
1134
- position,
1135
- ...props
1136
- }, /* @__PURE__ */ React4.createElement(ZoomControls, {
1137
- onAction
1138
- }));
1139
- } finally {
1140
- _effect.f();
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
+ }
1141
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
+ })));
1142
1572
  };
1143
- var MapAction = ({ onAction, position = "bottomright", ...props }) => {
1144
- var _effect = _useSignals4();
1145
- try {
1146
- return /* @__PURE__ */ React4.createElement(CustomControl2, {
1147
- position,
1148
- ...props
1149
- }, /* @__PURE__ */ React4.createElement(ActionControls, {
1150
- onAction
1151
- }));
1152
- } finally {
1153
- _effect.f();
1154
- }
1573
+ MapLines.displayName = "Map.Lines";
1574
+ var CustomControl2 = ({ position, children }) => {
1575
+ const map = useMap();
1576
+ const rootRef = useRef2(void 0);
1577
+ useEffect7(() => {
1578
+ const control = new Control({
1579
+ position
1580
+ });
1581
+ control.onAdd = () => {
1582
+ const container = DomUtil.create("div", mx2("m-0!", controlPositions[position]));
1583
+ DomEvent.disableClickPropagation(container);
1584
+ DomEvent.disableScrollPropagation(container);
1585
+ const root = createRoot(container);
1586
+ rootRef.current = root;
1587
+ root.render(/* @__PURE__ */ React3.createElement(ThemeProvider, {
1588
+ tx: defaultTx
1589
+ }, /* @__PURE__ */ React3.createElement(Tooltip.Provider, null, children)));
1590
+ return container;
1591
+ };
1592
+ control.addTo(map);
1593
+ return () => {
1594
+ control.remove();
1595
+ const root = rootRef.current;
1596
+ rootRef.current = void 0;
1597
+ queueMicrotask(() => root?.unmount());
1598
+ };
1599
+ }, [
1600
+ map,
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
+ }, [
1608
+ children
1609
+ ]);
1610
+ return null;
1155
1611
  };
1156
- var Map = {
1612
+ var MapZoom = ({ onAction, position = "bottomleft", ...props }) => /* @__PURE__ */ React3.createElement(CustomControl2, {
1613
+ position,
1614
+ ...props
1615
+ }, /* @__PURE__ */ React3.createElement(ZoomControls, {
1616
+ onAction
1617
+ }));
1618
+ var MapAction = ({ onAction, position = "bottomright", ...props }) => /* @__PURE__ */ React3.createElement(CustomControl2, {
1619
+ position,
1620
+ ...props
1621
+ }, /* @__PURE__ */ React3.createElement(ActionControls, {
1622
+ onAction
1623
+ }));
1624
+ var Map2 = {
1157
1625
  Root: MapRoot,
1626
+ Viewport: MapViewport,
1158
1627
  Tiles: MapTiles,
1159
1628
  Markers: MapMarkers,
1629
+ Lines: MapLines,
1160
1630
  Zoom: MapZoom,
1161
1631
  Action: MapAction
1162
1632
  };
1163
1633
  export {
1164
1634
  ActionControls,
1635
+ DEFAULT_TILE_URL,
1165
1636
  Globe,
1166
- GlobeContextProvider,
1167
- Map,
1637
+ GlobeContext,
1638
+ Map2 as Map,
1168
1639
  ZoomControls,
1169
1640
  closestPoint,
1170
1641
  controlPositions,
1171
1642
  createLayers,
1643
+ createRotationTween,
1644
+ flyDuration,
1172
1645
  geoCircle,
1173
1646
  geoInertiaDrag,
1174
1647
  geoLine,
1175
1648
  geoPoint,
1176
1649
  geoToPosition,
1177
1650
  getDistance,
1178
- loadTopology,
1651
+ globeStyles,
1179
1652
  positionToRotation,
1180
1653
  renderLayers,
1181
1654
  restrictAxis,
@@ -1184,7 +1657,10 @@ export {
1184
1657
  useGlobeContext,
1185
1658
  useGlobeZoomHandler,
1186
1659
  useMapZoomHandler,
1660
+ useSimplifiedTopology,
1187
1661
  useSpinner,
1188
- useTour
1662
+ useTopology,
1663
+ useTour,
1664
+ useWheel
1189
1665
  };
1190
1666
  //# sourceMappingURL=index.mjs.map