@dxos/react-ui-geo 0.8.4-main.f9ba587 → 0.8.4-main.fffef41
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.
- package/data/airports.ts +1 -1
- package/data/cities.ts +1 -1
- package/data/countries-110m.ts +1 -1
- package/data/countries-dots-3.ts +1 -1
- package/data/countries-dots-4.ts +1 -1
- package/dist/lib/browser/chunk-GMWLKTLN.mjs +9 -0
- package/dist/lib/browser/{countries-110m-37VAAFCK.mjs → countries-110m-ZM3ZIEFS.mjs} +1 -1
- package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs.map +7 -0
- package/dist/lib/browser/data.mjs +1 -1
- package/dist/lib/browser/index.mjs +215 -178
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/{chunk-OPJPAAEK.mjs → chunk-JODBF4CC.mjs} +2 -2
- package/dist/lib/node-esm/{countries-110m-36TTKK5B.mjs → countries-110m-3SFASWVD.mjs} +1 -1
- package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs.map +7 -0
- package/dist/lib/node-esm/data.mjs +1 -1
- package/dist/lib/node-esm/index.mjs +215 -178
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +25 -9
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.d.ts +27 -17
- package/dist/types/src/components/Map/Map.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.stories.d.ts +14 -8
- package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
- package/dist/types/src/components/index.d.ts +0 -1
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/hooks/context.d.ts +7 -7
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useSpinner.d.ts +1 -1
- package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
- package/dist/types/src/hooks/useTour.d.ts +4 -3
- package/dist/types/src/hooks/useTour.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +12 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +2 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/path.d.ts +5 -8
- package/dist/types/src/util/path.d.ts.map +1 -1
- package/dist/types/src/util/render.d.ts +4 -4
- package/dist/types/src/util/render.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +22 -19
- package/src/components/Globe/Globe.stories.tsx +82 -34
- package/src/components/Globe/Globe.tsx +79 -62
- package/src/components/Map/Map.stories.tsx +25 -14
- package/src/components/Map/Map.tsx +180 -95
- package/src/components/Toolbar/Controls.tsx +14 -20
- package/src/components/index.ts +0 -2
- package/src/hooks/context.tsx +22 -16
- package/src/hooks/useGlobeZoomHandler.ts +9 -3
- package/src/hooks/useMapZoomHandler.ts +1 -1
- package/src/hooks/useSpinner.ts +2 -1
- package/src/hooks/useTour.ts +10 -8
- package/src/index.ts +2 -1
- package/src/translations.ts +20 -0
- package/src/types.ts +3 -1
- package/src/util/inertia.ts +1 -1
- package/src/util/path.ts +5 -6
- package/src/util/render.ts +5 -3
- package/dist/lib/browser/chunk-CYCBMCOP.mjs +0 -9
- package/dist/lib/browser/countries-110m-37VAAFCK.mjs.map +0 -7
- package/dist/lib/node-esm/countries-110m-36TTKK5B.mjs.map +0 -7
- package/dist/types/src/components/types.d.ts +0 -15
- package/dist/types/src/components/types.d.ts.map +0 -1
- package/src/components/types.ts +0 -19
- /package/dist/lib/browser/{chunk-CYCBMCOP.mjs.map → chunk-GMWLKTLN.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-OPJPAAEK.mjs.map → chunk-JODBF4CC.mjs.map} +0 -0
|
@@ -2,109 +2,193 @@
|
|
|
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
|
|
9
|
-
import
|
|
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, useState } from 'react';
|
|
10
10
|
import { createRoot } from 'react-dom/client';
|
|
11
|
-
import type
|
|
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 {
|
|
16
|
-
import { ThemeProvider, Tooltip, type ThemedClassName } from '@dxos/react-ui';
|
|
13
|
+
import { ThemeProvider, type ThemedClassName, Tooltip } from '@dxos/react-ui';
|
|
17
14
|
import { defaultTx, mx } from '@dxos/react-ui-theme';
|
|
18
15
|
|
|
19
|
-
import {
|
|
20
|
-
import { type
|
|
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
|
-
|
|
27
|
-
center: { lat: 51, lng: 0 } as L.LatLngExpression,
|
|
24
|
+
center: { lat: 51, lng: 0 } as L.LatLngLiteral,
|
|
28
25
|
zoom: 4,
|
|
29
|
-
};
|
|
26
|
+
} as const;
|
|
30
27
|
|
|
31
28
|
//
|
|
32
|
-
//
|
|
29
|
+
// Controller
|
|
33
30
|
//
|
|
34
31
|
|
|
35
|
-
type
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const MapRoot = ({ classNames, center = defaults.center, zoom = defaults.zoom, ...props }: MapRootProps) => {
|
|
39
|
-
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
|
-
/>
|
|
50
|
-
);
|
|
32
|
+
type MapController = {
|
|
33
|
+
setCenter: (center: LatLngLiteral, zoom?: number) => void;
|
|
34
|
+
setZoom: (cb: (zoom: number) => number) => void;
|
|
51
35
|
};
|
|
52
36
|
|
|
53
37
|
//
|
|
54
|
-
//
|
|
38
|
+
// Context
|
|
55
39
|
//
|
|
56
40
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
setZoom: (cb: (zoom: number) => number) => void;
|
|
41
|
+
type MapContextValue = {
|
|
42
|
+
attention?: boolean;
|
|
43
|
+
onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
|
|
61
44
|
};
|
|
62
45
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
46
|
+
const [MapContextProvier, useMapContext] = createContext<MapContextValue>('Map');
|
|
47
|
+
|
|
48
|
+
//
|
|
49
|
+
// Root
|
|
50
|
+
//
|
|
66
51
|
|
|
67
|
-
|
|
52
|
+
type MapRootProps = ThemedClassName<MapContainerProps & Pick<MapContextValue, 'onChange'>>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* https://react-leaflet.js.org/docs/api-map
|
|
56
|
+
*/
|
|
57
|
+
const MapRoot = forwardRef<MapController, MapRootProps>(
|
|
58
|
+
(
|
|
59
|
+
{ classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, onChange, ...props },
|
|
68
60
|
forwardedRef,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
setZoom: (cb) => {
|
|
74
|
-
map.setZoom(cb(map.getZoom()));
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
[map],
|
|
78
|
-
);
|
|
61
|
+
) => {
|
|
62
|
+
const [attention, setAttention] = useState(false);
|
|
63
|
+
const mapRef = useRef<L.Map>(null);
|
|
64
|
+
const map = mapRef.current;
|
|
79
65
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
66
|
+
useImperativeHandle(
|
|
67
|
+
forwardedRef,
|
|
68
|
+
() => ({
|
|
69
|
+
setCenter: (center: LatLngLiteral, zoom?: number) => {
|
|
70
|
+
mapRef.current?.setView(center, zoom);
|
|
71
|
+
},
|
|
72
|
+
setZoom: (cb: (zoom: number) => number) => {
|
|
73
|
+
mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
[],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Enable/disable scroll wheel zoom.
|
|
80
|
+
// TODO(burdon): Use attention:
|
|
81
|
+
// const {hasAttention} = useAttention(props.id);
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!map) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (attention) {
|
|
88
|
+
map.scrollWheelZoom.enable();
|
|
89
|
+
} else {
|
|
90
|
+
map.scrollWheelZoom.disable();
|
|
91
|
+
}
|
|
92
|
+
}, [map, attention]);
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<MapContextProvier attention={attention} onChange={onChange}>
|
|
96
|
+
<MapContainer
|
|
97
|
+
{...props}
|
|
98
|
+
ref={mapRef}
|
|
99
|
+
className={mx('group relative grid bs-full is-full !bg-baseSurface dx-focus-ring-inset', classNames)}
|
|
100
|
+
attributionControl={false}
|
|
101
|
+
zoomControl={false}
|
|
102
|
+
scrollWheelZoom={scrollWheelZoom}
|
|
103
|
+
doubleClickZoom={doubleClickZoom}
|
|
104
|
+
touchZoom={touchZoom}
|
|
105
|
+
center={center ?? defaults.center}
|
|
106
|
+
zoom={zoom ?? defaults.zoom}
|
|
107
|
+
// whenReady={() => {}}
|
|
108
|
+
/>
|
|
109
|
+
</MapContextProvier>
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
MapRoot.displayName = 'Map.Root';
|
|
115
|
+
|
|
116
|
+
//
|
|
117
|
+
// Tiles
|
|
118
|
+
// https://react-leaflet.js.org/docs/api-components/#tilelayer
|
|
119
|
+
//
|
|
120
|
+
|
|
121
|
+
type MapTilesProps = {};
|
|
122
|
+
|
|
123
|
+
const MapTiles = (_props: MapTilesProps) => {
|
|
124
|
+
const ref = useRef<L.TileLayer>(null);
|
|
125
|
+
const { onChange } = useMapContext(MapTiles.displayName);
|
|
126
|
+
|
|
127
|
+
useMapEvents({
|
|
128
|
+
zoomstart: (ev) => {
|
|
129
|
+
onChange?.({
|
|
130
|
+
center: ev.target.getCenter(),
|
|
131
|
+
zoom: ev.target.getZoom(),
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
});
|
|
86
135
|
|
|
87
|
-
//
|
|
136
|
+
// NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
|
|
137
|
+
// Tailwind requires setting the property for static analysis.
|
|
138
|
+
const { attention } = useMapContext(MapTiles.displayName);
|
|
88
139
|
useEffect(() => {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
} else if (zoom !== undefined) {
|
|
92
|
-
map.setZoom(zoom);
|
|
140
|
+
if (ref.current) {
|
|
141
|
+
ref.current.getContainer().dataset.attention = attention ? '1' : '0';
|
|
93
142
|
}
|
|
94
|
-
}, [
|
|
143
|
+
}, [attention]);
|
|
95
144
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
145
|
+
// TODO(burdon): Option to add class 'invert'.
|
|
146
|
+
return (
|
|
147
|
+
<>
|
|
148
|
+
<TileLayer
|
|
149
|
+
ref={ref}
|
|
150
|
+
data-attention={attention}
|
|
151
|
+
detectRetina={true}
|
|
152
|
+
className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
|
|
153
|
+
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
|
154
|
+
keepBuffer={4}
|
|
155
|
+
// opacity={attention ? 1 : 0.7}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{/* Temperature map. */}
|
|
159
|
+
{/* <WMSTileLayer
|
|
160
|
+
url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
|
|
161
|
+
layers='MODIS_Terra_Land_Surface_Temp_Day'
|
|
162
|
+
format='image/png'
|
|
163
|
+
transparent={true}
|
|
164
|
+
version='1.3.0'
|
|
165
|
+
attribution='NASA GIBS'
|
|
166
|
+
/> */}
|
|
167
|
+
|
|
168
|
+
{/* US Weather. */}
|
|
169
|
+
{/* <WMSTileLayer
|
|
170
|
+
url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
|
|
171
|
+
layers='nexrad-n0r' // layers='nexrad-n0r'
|
|
172
|
+
format='image/png'
|
|
173
|
+
transparent={true}
|
|
174
|
+
/> */}
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
MapTiles.displayName = 'Map.Tiles';
|
|
180
|
+
|
|
181
|
+
//
|
|
182
|
+
// Markers
|
|
183
|
+
//
|
|
184
|
+
|
|
185
|
+
type MapMarkersProps = {
|
|
186
|
+
markers?: GeoMarker[];
|
|
187
|
+
selected?: string[];
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
|
|
191
|
+
const map = useMap();
|
|
108
192
|
|
|
109
193
|
// Set the viewport around the markers, or show the whole world map if `markers` is empty.
|
|
110
194
|
useEffect(() => {
|
|
@@ -117,14 +201,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
117
201
|
}, [markers]);
|
|
118
202
|
|
|
119
203
|
return (
|
|
120
|
-
|
|
121
|
-
{/* Map tiles. */}
|
|
122
|
-
<TileLayer
|
|
123
|
-
className='dark:filter dark:grayscale dark:invert'
|
|
124
|
-
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
|
125
|
-
/>
|
|
126
|
-
|
|
127
|
-
{/* Markers. */}
|
|
204
|
+
<>
|
|
128
205
|
{markers?.map(({ id, title, location: { lat, lng } }) => {
|
|
129
206
|
return (
|
|
130
207
|
<Marker
|
|
@@ -132,6 +209,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
132
209
|
position={{ lat, lng }}
|
|
133
210
|
icon={
|
|
134
211
|
// TODO(burdon): Create custom icon from bundled assets.
|
|
212
|
+
// TODO(burdon): Selection state.
|
|
135
213
|
new L.Icon({
|
|
136
214
|
iconUrl: 'https://dxos.network/marker-icon.png',
|
|
137
215
|
iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
|
|
@@ -147,9 +225,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
147
225
|
</Marker>
|
|
148
226
|
);
|
|
149
227
|
})}
|
|
150
|
-
|
|
228
|
+
</>
|
|
151
229
|
);
|
|
152
|
-
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
MapMarkers.displayName = 'Map.Markers';
|
|
153
233
|
|
|
154
234
|
//
|
|
155
235
|
// Controls
|
|
@@ -192,23 +272,28 @@ const CustomControl = ({
|
|
|
192
272
|
|
|
193
273
|
type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
|
|
194
274
|
|
|
275
|
+
const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
|
|
276
|
+
<CustomControl position={position} {...props}>
|
|
277
|
+
<ZoomControls onAction={onAction} />
|
|
278
|
+
</CustomControl>
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
|
|
282
|
+
<CustomControl position={position} {...props}>
|
|
283
|
+
<ActionControls onAction={onAction} />
|
|
284
|
+
</CustomControl>
|
|
285
|
+
);
|
|
286
|
+
|
|
195
287
|
//
|
|
196
288
|
// Map
|
|
197
289
|
//
|
|
198
290
|
|
|
199
291
|
export const Map = {
|
|
200
292
|
Root: MapRoot,
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
</CustomControl>
|
|
206
|
-
),
|
|
207
|
-
Action: ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
|
|
208
|
-
<CustomControl position={position} {...props}>
|
|
209
|
-
<ActionControls onAction={onAction} />
|
|
210
|
-
</CustomControl>
|
|
211
|
-
),
|
|
293
|
+
Tiles: MapTiles,
|
|
294
|
+
Markers: MapMarkers,
|
|
295
|
+
Zoom: MapZoom,
|
|
296
|
+
Action: MapAction,
|
|
212
297
|
};
|
|
213
298
|
|
|
214
|
-
export { type
|
|
299
|
+
export { type MapController, type MapRootProps, type MapTilesProps, type MapMarkersProps, type MapControlProps };
|
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
66
|
-
classNames='px-0 aspect-square'
|
|
60
|
+
label={t('toggle icon button')}
|
|
67
61
|
onClick={() => onAction?.('toggle')}
|
|
68
62
|
/>
|
|
69
63
|
</Toolbar.Root>
|
package/src/components/index.ts
CHANGED
package/src/hooks/context.tsx
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type Dispatch, type PropsWithChildren, type SetStateAction, createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
7
|
import { raise } from '@dxos/debug';
|
|
8
8
|
import { useControlledState } from '@dxos/react-ui';
|
|
9
9
|
|
|
10
|
-
import { type
|
|
10
|
+
import { type LatLngLiteral } from '../types';
|
|
11
11
|
|
|
12
12
|
// TODO(burdon): Factor out common geometry types.
|
|
13
13
|
export type Size = { width: number; height: number };
|
|
@@ -16,38 +16,44 @@ export type Vector = [number, number, number];
|
|
|
16
16
|
|
|
17
17
|
export type GlobeContextType = {
|
|
18
18
|
size: Size;
|
|
19
|
-
center
|
|
20
|
-
|
|
19
|
+
center?: LatLngLiteral;
|
|
20
|
+
zoom: number;
|
|
21
21
|
translation: Point;
|
|
22
22
|
rotation: Vector;
|
|
23
|
-
setCenter: Dispatch<SetStateAction<
|
|
24
|
-
|
|
23
|
+
setCenter: Dispatch<SetStateAction<LatLngLiteral>>;
|
|
24
|
+
setZoom: Dispatch<SetStateAction<number>>;
|
|
25
25
|
setTranslation: Dispatch<SetStateAction<Point>>;
|
|
26
26
|
setRotation: Dispatch<SetStateAction<Vector>>;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const defaults = {
|
|
30
|
+
center: { lat: 51, lng: 0 } as LatLngLiteral,
|
|
31
|
+
zoom: 4,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Replace with radix.
|
|
29
35
|
const GlobeContext = createContext<GlobeContextType>(undefined);
|
|
30
36
|
|
|
31
37
|
export type GlobeContextProviderProps = PropsWithChildren<
|
|
32
|
-
Partial<Pick<GlobeContextType, 'size' | 'center' | '
|
|
38
|
+
Partial<Pick<GlobeContextType, 'size' | 'center' | 'zoom' | 'translation' | 'rotation'>>
|
|
33
39
|
>;
|
|
34
40
|
|
|
35
41
|
export const GlobeContextProvider = ({
|
|
36
42
|
children,
|
|
37
43
|
size,
|
|
38
|
-
center:
|
|
39
|
-
|
|
40
|
-
translation:
|
|
41
|
-
rotation:
|
|
44
|
+
center: centerParam = defaults.center,
|
|
45
|
+
zoom: zoomParam = defaults.zoom,
|
|
46
|
+
translation: translationParam,
|
|
47
|
+
rotation: rotationParam,
|
|
42
48
|
}: GlobeContextProviderProps) => {
|
|
43
|
-
const [center, setCenter] = useControlledState(
|
|
44
|
-
const [
|
|
45
|
-
const [translation, setTranslation] = useControlledState<Point>(
|
|
46
|
-
const [rotation, setRotation] = useControlledState<Vector>(
|
|
49
|
+
const [center, setCenter] = useControlledState(centerParam);
|
|
50
|
+
const [zoom, setZoom] = useControlledState(zoomParam);
|
|
51
|
+
const [translation, setTranslation] = useControlledState<Point>(translationParam);
|
|
52
|
+
const [rotation, setRotation] = useControlledState<Vector>(rotationParam);
|
|
47
53
|
|
|
48
54
|
return (
|
|
49
55
|
<GlobeContext.Provider
|
|
50
|
-
value={{ size, center,
|
|
56
|
+
value={{ size, center, zoom, translation, rotation, setCenter, setZoom, setTranslation, setRotation }}
|
|
51
57
|
>
|
|
52
58
|
{children}
|
|
53
59
|
</GlobeContext.Provider>
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { useCallback } from 'react';
|
|
6
6
|
|
|
7
|
-
import { type
|
|
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.
|
|
20
|
+
controller.setZoom((zoom) => {
|
|
21
|
+
return zoom * (1 + ZOOM_FACTOR);
|
|
22
|
+
});
|
|
19
23
|
break;
|
|
20
24
|
}
|
|
21
25
|
case 'zoom-out': {
|
|
22
|
-
controller.
|
|
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
|
|
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']>(
|
package/src/hooks/useSpinner.ts
CHANGED
|
@@ -6,9 +6,10 @@ 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';
|
|
11
10
|
|
|
11
|
+
import { type Vector } from './context';
|
|
12
|
+
|
|
12
13
|
export type SpinnerOptions = {
|
|
13
14
|
disabled?: boolean;
|
|
14
15
|
delta?: Vector;
|
package/src/hooks/useTour.ts
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { type
|
|
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 {
|
|
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?:
|
|
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:
|
|
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?.
|
|
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?.
|
|
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
|
|
109
|
+
} catch {
|
|
108
110
|
// Ignore.
|
|
109
111
|
} finally {
|
|
110
112
|
setRunning(false);
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Resource } from '@dxos/react-ui';
|
|
6
|
+
|
|
7
|
+
export const translationKey = 'react-ui-geo';
|
|
8
|
+
|
|
9
|
+
export const translations = [
|
|
10
|
+
{
|
|
11
|
+
'en-US': {
|
|
12
|
+
[translationKey]: {
|
|
13
|
+
'zoom in icon button': 'Zoom in',
|
|
14
|
+
'zoom out icon button': 'Zoom out',
|
|
15
|
+
'start icon button': 'Start',
|
|
16
|
+
'toggle icon button': 'Toggle',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
] as const satisfies Resource[];
|
package/src/types.ts
CHANGED