@dxos/react-ui-geo 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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 (92) hide show
  1. package/LICENSE +102 -5
  2. package/data/airports.ts +1 -1
  3. package/data/cities.ts +1 -1
  4. package/data/countries-110m.ts +1 -1
  5. package/data/countries-dots-3.ts +1 -1
  6. package/data/countries-dots-4.ts +1 -1
  7. package/dist/lib/browser/{countries-110m-37VAAFCK.mjs → countries-110m-RE5RNRQG.mjs} +1 -1
  8. package/dist/lib/browser/countries-110m-RE5RNRQG.mjs.map +7 -0
  9. package/dist/lib/browser/data.mjs +4 -3
  10. package/dist/lib/browser/data.mjs.map +4 -4
  11. package/dist/lib/browser/index.mjs +388 -451
  12. package/dist/lib/browser/index.mjs.map +3 -3
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/translations.mjs +19 -0
  15. package/dist/lib/browser/translations.mjs.map +7 -0
  16. package/dist/lib/node-esm/{countries-110m-36TTKK5B.mjs → countries-110m-4EDBXSFJ.mjs} +1 -1
  17. package/dist/lib/node-esm/countries-110m-4EDBXSFJ.mjs.map +7 -0
  18. package/dist/lib/node-esm/data.mjs +5 -3
  19. package/dist/lib/node-esm/data.mjs.map +4 -4
  20. package/dist/lib/node-esm/index.mjs +388 -450
  21. package/dist/lib/node-esm/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/meta.json +1 -1
  23. package/dist/lib/node-esm/translations.mjs +21 -0
  24. package/dist/lib/node-esm/translations.mjs.map +7 -0
  25. package/dist/types/data/airports.d.ts +4 -4
  26. package/dist/types/data/airports.d.ts.map +1 -1
  27. package/dist/types/data/cities.d.ts.map +1 -1
  28. package/dist/types/data/countries-110m.d.ts.map +1 -1
  29. package/dist/types/data/countries-dots-3.d.ts.map +1 -1
  30. package/dist/types/data/countries-dots-4.d.ts.map +1 -1
  31. package/dist/types/src/components/Globe/Globe.d.ts +6 -4
  32. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  33. package/dist/types/src/components/Globe/Globe.stories.d.ts +27 -9
  34. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/Map/Map.d.ts +44 -18
  36. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  37. package/dist/types/src/components/Map/Map.stories.d.ts +14 -8
  38. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  40. package/dist/types/src/components/index.d.ts +0 -1
  41. package/dist/types/src/components/index.d.ts.map +1 -1
  42. package/dist/types/src/hooks/context.d.ts +6 -8
  43. package/dist/types/src/hooks/context.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +2 -2
  46. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  47. package/dist/types/src/hooks/useMapZoomHandler.d.ts +2 -2
  48. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useSpinner.d.ts +1 -1
  50. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useTour.d.ts +4 -3
  52. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  53. package/dist/types/src/index.d.ts +1 -2
  54. package/dist/types/src/index.d.ts.map +1 -1
  55. package/dist/types/src/translations.d.ts +12 -0
  56. package/dist/types/src/translations.d.ts.map +1 -0
  57. package/dist/types/src/types.d.ts +2 -1
  58. package/dist/types/src/types.d.ts.map +1 -1
  59. package/dist/types/src/util/debug.d.ts.map +1 -1
  60. package/dist/types/src/util/inertia.d.ts.map +1 -1
  61. package/dist/types/src/util/path.d.ts +5 -8
  62. package/dist/types/src/util/path.d.ts.map +1 -1
  63. package/dist/types/src/util/render.d.ts +4 -4
  64. package/dist/types/src/util/render.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +44 -35
  67. package/src/components/Globe/Globe.stories.tsx +82 -35
  68. package/src/components/Globe/Globe.tsx +133 -81
  69. package/src/components/Map/Map.stories.tsx +27 -15
  70. package/src/components/Map/Map.tsx +231 -99
  71. package/src/components/Toolbar/Controls.tsx +14 -20
  72. package/src/components/index.ts +0 -2
  73. package/src/hooks/context.tsx +11 -34
  74. package/src/hooks/useGlobeZoomHandler.ts +9 -3
  75. package/src/hooks/useMapZoomHandler.ts +1 -1
  76. package/src/hooks/useSpinner.ts +1 -1
  77. package/src/hooks/useTour.ts +10 -8
  78. package/src/index.ts +1 -2
  79. package/src/translations.ts +20 -0
  80. package/src/types.ts +3 -1
  81. package/src/util/inertia.ts +1 -1
  82. package/src/util/path.ts +5 -6
  83. package/src/util/render.ts +4 -3
  84. package/dist/lib/browser/chunk-CYCBMCOP.mjs +0 -9
  85. package/dist/lib/browser/chunk-CYCBMCOP.mjs.map +0 -7
  86. package/dist/lib/browser/countries-110m-37VAAFCK.mjs.map +0 -7
  87. package/dist/lib/node-esm/chunk-OPJPAAEK.mjs +0 -11
  88. package/dist/lib/node-esm/chunk-OPJPAAEK.mjs.map +0 -7
  89. package/dist/lib/node-esm/countries-110m-36TTKK5B.mjs.map +0 -7
  90. package/dist/types/src/components/types.d.ts +0 -15
  91. package/dist/types/src/components/types.d.ts.map +0 -1
  92. package/src/components/types.ts +0 -19
@@ -2,129 +2,245 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- // eslint-disable-next-line no-restricted-imports
6
5
  import 'leaflet/dist/leaflet.css';
7
6
 
8
- import L, { Control, DomEvent, DomUtil, latLngBounds, type ControlPosition, type LatLngExpression } from 'leaflet';
9
- import React, { forwardRef, useEffect, useImperativeHandle, type PropsWithChildren } from 'react';
7
+ import { createContext } from '@radix-ui/react-context';
8
+ import L, { Control, type ControlPosition, DomEvent, DomUtil, type LatLngLiteral, latLngBounds } from 'leaflet';
9
+ import React, { type PropsWithChildren, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
10
10
  import { createRoot } from 'react-dom/client';
11
- import type { MapContainerProps } from 'react-leaflet';
12
- import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet';
13
- import { useResizeDetector } from 'react-resize-detector';
11
+ import { MapContainer, type MapContainerProps, Marker, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet';
14
12
 
15
- import { debounce } from '@dxos/async';
16
- import { ThemeProvider, Tooltip, type ThemedClassName } from '@dxos/react-ui';
17
- import { defaultTx, mx } from '@dxos/react-ui-theme';
13
+ import { type ThemedClassName, ThemeProvider, Tooltip } from '@dxos/react-ui';
14
+ import { composable, composableProps, defaultTx, mx } from '@dxos/ui-theme';
18
15
 
19
- import { ActionControls, controlPositions, ZoomControls, type ControlProps } from '../Toolbar';
20
- import { type MapCanvasProps } from '../types';
16
+ import { type GeoMarker } from '../../types';
17
+ import { ActionControls, type ControlProps, ZoomControls, controlPositions } from '../Toolbar';
21
18
 
22
19
  // TODO(burdon): Explore plugins: https://www.npmjs.com/search?q=keywords%3Areact-leaflet-v4
23
20
  // TODO(burdon): react-leaflet v5 is not compatible with react 18.
21
+ // TODO(burdon): Guess initial location.
24
22
 
25
23
  const defaults = {
26
- // TODO(burdon): Guess location.
27
- center: { lat: 51, lng: 0 } as L.LatLngExpression,
24
+ center: { lat: 51, lng: 0 } as L.LatLngLiteral,
28
25
  zoom: 4,
26
+ } as const;
27
+
28
+ //
29
+ // Controller
30
+ //
31
+
32
+ type MapController = {
33
+ getCenter: () => LatLngLiteral | undefined;
34
+ getZoom: () => number | undefined;
35
+ setCenter: (center: LatLngLiteral, zoom?: number) => void;
36
+ setZoom: (cb: (zoom: number) => number) => void;
37
+ };
38
+
39
+ //
40
+ // Context
41
+ //
42
+
43
+ type MapContextValue = {
44
+ attention?: boolean;
45
+ onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
29
46
  };
30
47
 
48
+ const [MapContextProvider, useMapContext] = createContext<MapContextValue>('Map');
49
+
31
50
  //
32
51
  // Root
33
52
  //
34
53
 
35
- type MapRootProps = ThemedClassName<MapContainerProps>;
54
+ type MapRootProps = Pick<MapContextValue, 'onChange'>;
36
55
 
37
- // https://react-leaflet.js.org/docs/api-map
38
- const MapRoot = ({ classNames, center = defaults.center, zoom = defaults.zoom, ...props }: MapRootProps) => {
56
+ /**
57
+ * Context provider for the map. Must wrap Map.Content.
58
+ */
59
+ const MapRoot = composable<HTMLDivElement, MapRootProps>(({ children, onChange, ...props }, forwardedRef) => {
60
+ // TODO(burdon): Use attention: const [attention, setAttention] = useState(false);
61
+ const attention = false;
39
62
  return (
40
- <MapContainer
41
- className={mx('relative grid grow bg-baseSurface', classNames)}
42
- attributionControl={false}
43
- // TODO(burdon): Only if attention.
44
- scrollWheelZoom={true}
45
- zoomControl={false}
46
- center={center}
47
- zoom={zoom}
48
- {...props}
49
- />
63
+ <MapContextProvider attention={attention} onChange={onChange}>
64
+ <div
65
+ {...composableProps(props, {
66
+ role: 'none',
67
+ classNames: 'dx-container grid dx-focus-ring-inset',
68
+ })}
69
+ ref={forwardedRef}
70
+ >
71
+ {children}
72
+ </div>
73
+ </MapContextProvider>
50
74
  );
51
- };
75
+ });
76
+
77
+ MapRoot.displayName = 'Map.Root';
52
78
 
53
79
  //
54
- // Control
80
+ // Content
55
81
  //
56
82
 
57
- // TODO(burdon): Normalize with Globe.
58
- type MapController = {
59
- setCenter: (center: LatLngExpression, zoom?: number) => void;
60
- setZoom: (cb: (zoom: number) => number) => void;
61
- };
83
+ type MapContentProps = ThemedClassName<Omit<MapContainerProps, 'children'> & PropsWithChildren>;
62
84
 
63
- const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center, zoom, onChange }, forwardedRef) => {
64
- const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
65
- const map = useMap();
85
+ /**
86
+ * https://react-leaflet.js.org/docs/api-map
87
+ */
88
+ const MAP_CONTENT_NAME = 'Map.Content';
66
89
 
67
- useImperativeHandle(
90
+ const MapContent = forwardRef<MapController, MapContentProps>(
91
+ (
92
+ { classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props },
68
93
  forwardedRef,
69
- () => ({
70
- setCenter: (center: LatLngExpression, zoom?: number) => {
71
- map.setView(center, zoom);
72
- },
73
- setZoom: (cb) => {
74
- map.setZoom(cb(map.getZoom()));
75
- },
76
- }),
77
- [map],
78
- );
94
+ ) => {
95
+ const { attention } = useMapContext(MAP_CONTENT_NAME);
96
+ const mapRef = useRef<L.Map>(null);
97
+ const map = mapRef.current;
79
98
 
80
- // Resize.
81
- useEffect(() => {
82
- if (width && height) {
83
- map.invalidateSize();
84
- }
85
- }, [width, height]);
99
+ useImperativeHandle(
100
+ forwardedRef,
101
+ () => ({
102
+ getCenter: () => {
103
+ const center = mapRef.current?.getCenter();
104
+ return center ? { lat: center.lat, lng: center.lng } : undefined;
105
+ },
106
+ getZoom: () => mapRef.current?.getZoom(),
107
+ setCenter: (center: LatLngLiteral, zoom?: number) => {
108
+ mapRef.current?.setView(center, zoom);
109
+ },
110
+ setZoom: (cb: (zoom: number) => number) => {
111
+ mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
112
+ },
113
+ }),
114
+ [],
115
+ );
86
116
 
87
- // Position.
88
- useEffect(() => {
89
- if (center) {
90
- map.setView(center, zoom);
91
- } else if (zoom !== undefined) {
92
- map.setZoom(zoom);
93
- }
94
- }, [center, zoom]);
117
+ // Enable/disable scroll wheel zoom.
118
+ // TODO(burdon): Use attention:
119
+ // const {hasAttention} = useAttention(props.id);
120
+ useEffect(() => {
121
+ if (!map) {
122
+ return;
123
+ }
95
124
 
96
- // Events.
97
- useEffect(() => {
98
- const handler = debounce(() => {
99
- onChange?.({ center: map.getCenter(), zoom: map.getZoom() });
100
- }, 100);
101
- map.on('move', handler);
102
- map.on('zoom', handler);
103
- return () => {
104
- map.off('move', handler);
105
- map.off('zoom', handler);
106
- };
107
- }, [map, onChange]);
125
+ if (attention) {
126
+ map.scrollWheelZoom.enable();
127
+ } else {
128
+ map.scrollWheelZoom.disable();
129
+ }
130
+ }, [map, attention]);
131
+
132
+ return (
133
+ <MapContainer
134
+ {...props}
135
+ className={mx('group relative grid bg-base-surface!', classNames)}
136
+ attributionControl={false}
137
+ zoomControl={false}
138
+ scrollWheelZoom={scrollWheelZoom}
139
+ doubleClickZoom={doubleClickZoom}
140
+ touchZoom={touchZoom}
141
+ center={center ?? defaults.center}
142
+ zoom={zoom ?? defaults.zoom}
143
+ whenReady={() => {}}
144
+ ref={mapRef}
145
+ >
146
+ {children}
147
+ </MapContainer>
148
+ );
149
+ },
150
+ );
151
+
152
+ MapContent.displayName = 'Map.Content';
153
+
154
+ //
155
+ // Tiles
156
+ // https://react-leaflet.js.org/docs/api-components/#tilelayer
157
+ //
158
+
159
+ const MAP_TILES_NAME = 'Map.Tiles';
160
+
161
+ type MapTilesProps = {};
162
+
163
+ const MapTiles = (_props: MapTilesProps) => {
164
+ const ref = useRef<L.TileLayer>(null);
165
+ const { onChange } = useMapContext(MAP_TILES_NAME);
108
166
 
109
- // Set the viewport around the markers, or show the whole world map if `markers` is empty.
167
+ useMapEvents({
168
+ moveend: (ev) => {
169
+ onChange?.({
170
+ center: ev.target.getCenter(),
171
+ zoom: ev.target.getZoom(),
172
+ });
173
+ },
174
+ });
175
+
176
+ // NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
177
+ // Tailwind requires setting the property for static analysis.
178
+ const { attention } = useMapContext(MAP_TILES_NAME);
110
179
  useEffect(() => {
111
- if (markers.length > 0) {
112
- const bounds = latLngBounds(markers.map((marker) => marker.location));
113
- map.fitBounds(bounds);
114
- } else {
115
- map.setView(defaults.center, defaults.zoom);
180
+ if (ref.current) {
181
+ ref.current.getContainer().dataset.attention = attention ? '1' : '0';
116
182
  }
117
- }, [markers]);
183
+ }, [attention]);
118
184
 
185
+ // TODO(burdon): Option to add class 'invert'.
119
186
  return (
120
- <div ref={ref} className='flex w-full h-full overflow-hidden bg-baseSurface'>
121
- {/* Map tiles. */}
187
+ <>
122
188
  <TileLayer
123
- className='dark:filter dark:grayscale dark:invert'
189
+ ref={ref}
190
+ data-attention={attention}
191
+ detectRetina={true}
192
+ className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
124
193
  url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
194
+ keepBuffer={4}
195
+ // opacity={attention ? 1 : 0.7}
125
196
  />
126
197
 
127
- {/* Markers. */}
198
+ {/* Temperature map. */}
199
+ {/* <WMSTileLayer
200
+ url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
201
+ layers='MODIS_Terra_Land_Surface_Temp_Day'
202
+ format='image/png'
203
+ transparent={true}
204
+ version='1.3.0'
205
+ attribution='NASA GIBS'
206
+ /> */}
207
+
208
+ {/* US Weather. */}
209
+ {/* <WMSTileLayer
210
+ url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
211
+ layers='nexrad-n0r' // layers='nexrad-n0r'
212
+ format='image/png'
213
+ transparent={true}
214
+ /> */}
215
+ </>
216
+ );
217
+ };
218
+
219
+ MapTiles.displayName = MAP_TILES_NAME;
220
+
221
+ //
222
+ // Markers
223
+ //
224
+
225
+ type MapMarkersProps = {
226
+ markers?: GeoMarker[];
227
+ selected?: string[];
228
+ };
229
+
230
+ const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
231
+ const map = useMap();
232
+
233
+ // Fit the viewport around the markers. When there are no markers, leave the current view alone
234
+ // so caller-provided center/zoom (or the user's prior interaction) is preserved.
235
+ useEffect(() => {
236
+ if (markers && markers.length > 0) {
237
+ const bounds = latLngBounds(markers.map((marker) => marker.location));
238
+ map.fitBounds(bounds);
239
+ }
240
+ }, [markers, map]);
241
+
242
+ return (
243
+ <>
128
244
  {markers?.map(({ id, title, location: { lat, lng } }) => {
129
245
  return (
130
246
  <Marker
@@ -132,6 +248,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
132
248
  position={{ lat, lng }}
133
249
  icon={
134
250
  // TODO(burdon): Create custom icon from bundled assets.
251
+ // TODO(burdon): Selection state.
135
252
  new L.Icon({
136
253
  iconUrl: 'https://dxos.network/marker-icon.png',
137
254
  iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
@@ -147,9 +264,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
147
264
  </Marker>
148
265
  );
149
266
  })}
150
- </div>
267
+ </>
151
268
  );
152
- });
269
+ };
270
+
271
+ MapMarkers.displayName = 'Map.Markers';
153
272
 
154
273
  //
155
274
  // Controls
@@ -167,7 +286,7 @@ const CustomControl = ({
167
286
  useEffect(() => {
168
287
  const control = new Control({ position });
169
288
  control.onAdd = () => {
170
- const container = DomUtil.create('div', mx('!m-0', controlPositions[position]));
289
+ const container = DomUtil.create('div', mx('m-0!', controlPositions[position]));
171
290
  DomEvent.disableClickPropagation(container);
172
291
  DomEvent.disableScrollPropagation(container);
173
292
 
@@ -192,23 +311,36 @@ const CustomControl = ({
192
311
 
193
312
  type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
194
313
 
314
+ const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
315
+ <CustomControl position={position} {...props}>
316
+ <ZoomControls onAction={onAction} />
317
+ </CustomControl>
318
+ );
319
+
320
+ const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
321
+ <CustomControl position={position} {...props}>
322
+ <ActionControls onAction={onAction} />
323
+ </CustomControl>
324
+ );
325
+
195
326
  //
196
327
  // Map
197
328
  //
198
329
 
199
330
  export const Map = {
200
331
  Root: MapRoot,
201
- Canvas: MapCanvas,
202
- Zoom: ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
203
- <CustomControl position={position} {...props}>
204
- <ZoomControls onAction={onAction} />
205
- </CustomControl>
206
- ),
207
- Action: ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
208
- <CustomControl position={position} {...props}>
209
- <ActionControls onAction={onAction} />
210
- </CustomControl>
211
- ),
332
+ Content: MapContent,
333
+ Tiles: MapTiles,
334
+ Markers: MapMarkers,
335
+ Zoom: MapZoom,
336
+ Action: MapAction,
212
337
  };
213
338
 
214
- export { type MapCanvasProps, type MapController };
339
+ export {
340
+ type MapController,
341
+ type MapRootProps,
342
+ type MapContentProps,
343
+ type MapTilesProps,
344
+ type MapMarkersProps,
345
+ type MapControlProps,
346
+ };
@@ -5,7 +5,9 @@
5
5
  import { type ControlPosition } from 'leaflet';
6
6
  import React from 'react';
7
7
 
8
- import { IconButton, type ThemedClassName, Toolbar } from '@dxos/react-ui';
8
+ import { IconButton, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { translationKey } from '#translations';
9
11
 
10
12
  export type ControlAction = 'toggle' | 'start' | 'zoom-in' | 'zoom-out';
11
13
 
@@ -21,24 +23,20 @@ export const controlPositions: Record<ControlPosition, string> = {
21
23
  };
22
24
 
23
25
  export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
26
+ const { t } = useTranslation(translationKey);
27
+
24
28
  return (
25
- <Toolbar.Root classNames={['gap-1', classNames]}>
29
+ <Toolbar.Root classNames={['gap-2', classNames]}>
26
30
  <IconButton
27
- //
28
31
  icon='ph--plus--regular'
29
- label='zoom in'
30
32
  iconOnly
31
- size={5}
32
- classNames='px-0 aspect-square'
33
+ label={t('zoom-in-icon.button')}
33
34
  onClick={() => onAction?.('zoom-in')}
34
35
  />
35
36
  <IconButton
36
- //
37
37
  icon='ph--minus--regular'
38
- label='zoom out'
39
38
  iconOnly
40
- size={5}
41
- classNames='px-0 aspect-square'
39
+ label={t('zoom-out-icon.button')}
42
40
  onClick={() => onAction?.('zoom-out')}
43
41
  />
44
42
  </Toolbar.Root>
@@ -46,24 +44,20 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
46
44
  };
47
45
 
48
46
  export const ActionControls = ({ classNames, onAction }: ControlProps) => {
47
+ const { t } = useTranslation(translationKey);
48
+
49
49
  return (
50
- <Toolbar.Root classNames={['gap-1', classNames]}>
50
+ <Toolbar.Root classNames={['gap-2', classNames]}>
51
51
  <IconButton
52
- //
53
- icon='ph--play--regular'
54
- label='start'
52
+ icon='ph--path--regular'
55
53
  iconOnly
56
- size={5}
57
- classNames='px-0 aspect-square'
54
+ label={t('start-icon.button')}
58
55
  onClick={() => onAction?.('start')}
59
56
  />
60
57
  <IconButton
61
- //
62
58
  icon='ph--globe-hemisphere-west--regular'
63
- label='toggle'
64
59
  iconOnly
65
- size={5}
66
- classNames='px-0 aspect-square'
60
+ label={t('toggle-icon.button')}
67
61
  onClick={() => onAction?.('toggle')}
68
62
  />
69
63
  </Toolbar.Root>
@@ -2,8 +2,6 @@
2
2
  // Copyright 2019 DXOS.org
3
3
  //
4
4
 
5
- export * from './types';
6
-
7
5
  export * from './Globe';
8
6
  export * from './Map';
9
7
  export * from './Toolbar';
@@ -2,57 +2,34 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { createContext, type Dispatch, type PropsWithChildren, type SetStateAction, useContext } from 'react';
5
+ import { type Dispatch, type SetStateAction, createContext, useContext } from 'react';
6
6
 
7
7
  import { raise } from '@dxos/debug';
8
- import { useControlledState } from '@dxos/react-ui';
9
8
 
10
- import { type LatLng } from '../util';
9
+ import { type LatLngLiteral } from '../types';
11
10
 
12
11
  // TODO(burdon): Factor out common geometry types.
13
12
  export type Size = { width: number; height: number };
13
+
14
14
  export type Point = { x: number; y: number };
15
+
15
16
  export type Vector = [number, number, number];
16
17
 
17
18
  export type GlobeContextType = {
18
19
  size: Size;
19
- center: LatLng;
20
- scale: number;
20
+ center?: LatLngLiteral;
21
+ zoom: number;
21
22
  translation: Point;
22
23
  rotation: Vector;
23
- setCenter: Dispatch<SetStateAction<LatLng>>;
24
- setScale: Dispatch<SetStateAction<number>>;
24
+ setCenter: Dispatch<SetStateAction<LatLngLiteral>>;
25
+ setZoom: Dispatch<SetStateAction<number>>;
25
26
  setTranslation: Dispatch<SetStateAction<Point>>;
26
27
  setRotation: Dispatch<SetStateAction<Vector>>;
27
28
  };
28
29
 
29
- const GlobeContext = createContext<GlobeContextType>(undefined);
30
-
31
- export type GlobeContextProviderProps = PropsWithChildren<
32
- Partial<Pick<GlobeContextType, 'size' | 'center' | 'scale' | 'translation' | 'rotation'>>
33
- >;
34
-
35
- export const GlobeContextProvider = ({
36
- children,
37
- size,
38
- center: _center,
39
- scale: _scale,
40
- translation: _translation,
41
- rotation: _rotation,
42
- }: GlobeContextProviderProps) => {
43
- const [center, setCenter] = useControlledState(_center);
44
- const [scale, setScale] = useControlledState(_scale);
45
- const [translation, setTranslation] = useControlledState<Point>(_translation);
46
- const [rotation, setRotation] = useControlledState<Vector>(_rotation);
47
-
48
- return (
49
- <GlobeContext.Provider
50
- value={{ size, center, scale, translation, rotation, setCenter, setScale, setTranslation, setRotation }}
51
- >
52
- {children}
53
- </GlobeContext.Provider>
54
- );
55
- };
30
+ /** @internal */
31
+ // TODO(burdon): Replace with radix.
32
+ export const GlobeContext = createContext<GlobeContextType>(undefined);
56
33
 
57
34
  export const useGlobeContext = () => {
58
35
  return useContext(GlobeContext) ?? raise(new Error('Missing GlobeContext'));
@@ -4,7 +4,9 @@
4
4
 
5
5
  import { useCallback } from 'react';
6
6
 
7
- import { type GlobeController, type ControlProps } from '../components';
7
+ import { type ControlProps, type GlobeController } from '../components';
8
+
9
+ const ZOOM_FACTOR = 0.1;
8
10
 
9
11
  export const useGlobeZoomHandler = (controller: GlobeController | null | undefined): ControlProps['onAction'] => {
10
12
  return useCallback<ControlProps['onAction']>(
@@ -15,11 +17,15 @@ export const useGlobeZoomHandler = (controller: GlobeController | null | undefin
15
17
 
16
18
  switch (event) {
17
19
  case 'zoom-in': {
18
- controller.setScale((scale) => scale * 1.1);
20
+ controller.setZoom((zoom) => {
21
+ return zoom * (1 + ZOOM_FACTOR);
22
+ });
19
23
  break;
20
24
  }
21
25
  case 'zoom-out': {
22
- controller.setScale((scale) => scale * 0.9);
26
+ controller.setZoom((zoom) => {
27
+ return zoom * (1 - ZOOM_FACTOR);
28
+ });
23
29
  break;
24
30
  }
25
31
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useCallback } from 'react';
6
6
 
7
- import { type MapController, type ControlProps } from '../components';
7
+ import { type ControlProps, type MapController } from '../components';
8
8
 
9
9
  export const useMapZoomHandler = (controller: MapController | null | undefined): ControlProps['onAction'] => {
10
10
  return useCallback<ControlProps['onAction']>(
@@ -6,8 +6,8 @@ import { timer as d3Timer } from 'd3';
6
6
  import { type Timer } from 'd3';
7
7
  import { useEffect, useState } from 'react';
8
8
 
9
- import { type Vector } from './context';
10
9
  import { type GlobeController } from '../components';
10
+ import { type Vector } from './context';
11
11
 
12
12
  export type SpinnerOptions = {
13
13
  disabled?: boolean;
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { geoPath, geoInterpolate, geoDistance, selection as d3Selection } from 'd3';
6
- import { type SetStateAction, type Dispatch, useEffect, useState, useMemo } from 'react';
5
+ import { selection as d3Selection, geoDistance, geoInterpolate, geoPath } from 'd3';
6
+ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
7
7
  import versor from 'versor';
8
8
 
9
9
  import type { GlobeController } from '../components';
10
- import { geoToPosition, type LatLng, positionToRotation, type StyleSet } from '../util';
10
+ import { type LatLngLiteral } from '../types';
11
+ import { type StyleSet, geoToPosition, positionToRotation } from '../util';
11
12
 
12
13
  const TRANSITION_NAME = 'globe-tour';
13
14
 
@@ -29,10 +30,11 @@ export type TourOptions = {
29
30
  */
30
31
  export const useTour = (
31
32
  controller?: GlobeController | null,
32
- points?: LatLng[],
33
+ points?: LatLngLiteral[],
33
34
  options: TourOptions = {},
34
35
  ): [boolean, Dispatch<SetStateAction<boolean>>] => {
35
36
  const selection = useMemo(() => d3Selection(), []);
37
+ // TODO(burdon): Redo controlled state.
36
38
  const [running, setRunning] = useState(options.running ?? false);
37
39
  useEffect(() => {
38
40
  if (!running) {
@@ -48,7 +50,7 @@ export const useTour = (
48
50
  const path = geoPath(projection, context).pointRadius(2);
49
51
 
50
52
  const tilt = options.tilt ?? 0;
51
- let last: LatLng;
53
+ let last: LatLngLiteral;
52
54
  try {
53
55
  const p = [...points];
54
56
  if (options.loop) {
@@ -82,14 +84,14 @@ export const useTour = (
82
84
  {
83
85
  context.beginPath();
84
86
  context.strokeStyle = options?.styles?.arc?.strokeStyle ?? 'yellow';
85
- context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.scale ?? 1);
87
+ context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
86
88
  context.setLineDash(options?.styles?.arc?.lineDash ?? []);
87
89
  path({ type: 'LineString', coordinates: [ip(t1), ip(t2)] });
88
90
  context.stroke();
89
91
 
90
92
  context.beginPath();
91
93
  context.fillStyle = options?.styles?.cursor?.fillStyle ?? 'orange';
92
- path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.scale ?? 1));
94
+ path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
93
95
  path({ type: 'Point', coordinates: ip(t2) });
94
96
  context.fill();
95
97
  }
@@ -104,7 +106,7 @@ export const useTour = (
104
106
  await transition.end();
105
107
  last = next;
106
108
  }
107
- } catch (err) {
109
+ } catch {
108
110
  // Ignore.
109
111
  } finally {
110
112
  setRunning(false);