@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.
- package/LICENSE +102 -5
- 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/{countries-110m-37VAAFCK.mjs → countries-110m-RE5RNRQG.mjs} +1 -1
- package/dist/lib/browser/countries-110m-RE5RNRQG.mjs.map +7 -0
- package/dist/lib/browser/data.mjs +4 -3
- package/dist/lib/browser/data.mjs.map +4 -4
- package/dist/lib/browser/index.mjs +388 -451
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +19 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/{countries-110m-36TTKK5B.mjs → countries-110m-4EDBXSFJ.mjs} +1 -1
- package/dist/lib/node-esm/countries-110m-4EDBXSFJ.mjs.map +7 -0
- package/dist/lib/node-esm/data.mjs +5 -3
- package/dist/lib/node-esm/data.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +388 -450
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +21 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/data/airports.d.ts +4 -4
- package/dist/types/data/airports.d.ts.map +1 -1
- package/dist/types/data/cities.d.ts.map +1 -1
- package/dist/types/data/countries-110m.d.ts.map +1 -1
- package/dist/types/data/countries-dots-3.d.ts.map +1 -1
- package/dist/types/data/countries-dots-4.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +6 -4
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +27 -9
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.d.ts +44 -18
- 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 +6 -8
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +2 -2
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts +2 -2
- 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 +1 -2
- 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/debug.d.ts.map +1 -1
- package/dist/types/src/util/inertia.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 +44 -35
- package/src/components/Globe/Globe.stories.tsx +82 -35
- package/src/components/Globe/Globe.tsx +133 -81
- package/src/components/Map/Map.stories.tsx +27 -15
- package/src/components/Map/Map.tsx +231 -99
- package/src/components/Toolbar/Controls.tsx +14 -20
- package/src/components/index.ts +0 -2
- package/src/hooks/context.tsx +11 -34
- package/src/hooks/useGlobeZoomHandler.ts +9 -3
- package/src/hooks/useMapZoomHandler.ts +1 -1
- package/src/hooks/useSpinner.ts +1 -1
- package/src/hooks/useTour.ts +10 -8
- package/src/index.ts +1 -2
- 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 +4 -3
- package/dist/lib/browser/chunk-CYCBMCOP.mjs +0 -9
- package/dist/lib/browser/chunk-CYCBMCOP.mjs.map +0 -7
- package/dist/lib/browser/countries-110m-37VAAFCK.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-OPJPAAEK.mjs +0 -11
- package/dist/lib/node-esm/chunk-OPJPAAEK.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
|
@@ -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
|
|
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 } 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 {
|
|
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 {
|
|
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,
|
|
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 =
|
|
54
|
+
type MapRootProps = Pick<MapContextValue, 'onChange'>;
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
//
|
|
80
|
+
// Content
|
|
55
81
|
//
|
|
56
82
|
|
|
57
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
85
|
+
/**
|
|
86
|
+
* https://react-leaflet.js.org/docs/api-map
|
|
87
|
+
*/
|
|
88
|
+
const MAP_CONTENT_NAME = 'Map.Content';
|
|
66
89
|
|
|
67
|
-
|
|
90
|
+
const MapContent = forwardRef<MapController, MapContentProps>(
|
|
91
|
+
(
|
|
92
|
+
{ classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props },
|
|
68
93
|
forwardedRef,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
map
|
|
102
|
-
|
|
103
|
-
return (
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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 (
|
|
112
|
-
|
|
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
|
-
}, [
|
|
183
|
+
}, [attention]);
|
|
118
184
|
|
|
185
|
+
// TODO(burdon): Option to add class 'invert'.
|
|
119
186
|
return (
|
|
120
|
-
|
|
121
|
-
{/* Map tiles. */}
|
|
187
|
+
<>
|
|
122
188
|
<TileLayer
|
|
123
|
-
|
|
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
|
-
{/*
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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 {
|
|
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-
|
|
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,57 +2,34 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import
|
|
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
|
|
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
|
|
20
|
-
|
|
20
|
+
center?: LatLngLiteral;
|
|
21
|
+
zoom: number;
|
|
21
22
|
translation: Point;
|
|
22
23
|
rotation: Vector;
|
|
23
|
-
setCenter: Dispatch<SetStateAction<
|
|
24
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
export
|
|
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
|
|
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,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;
|
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);
|