@dxos/react-ui-geo 0.8.3 → 0.8.4-main.1c7ec43d41
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/{countries-110m-WI4PCLDF.mjs → countries-110m-RE5RNRQG.mjs} +2 -2
- 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 +396 -466
- 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-DQ4XRC4B.mjs → countries-110m-4EDBXSFJ.mjs} +2 -2
- 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 +396 -465
- 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 +42 -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 +43 -34
- package/src/components/Globe/Globe.stories.tsx +85 -38
- package/src/components/Globe/Globe.tsx +124 -81
- package/src/components/Map/Map.stories.tsx +27 -15
- package/src/components/Map/Map.tsx +220 -94
- 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-ENCWOTYX.mjs +0 -9
- package/dist/lib/browser/chunk-ENCWOTYX.mjs.map +0 -7
- package/dist/lib/browser/countries-110m-WI4PCLDF.mjs.map +0 -7
- package/dist/lib/node/chunk-LAICG6L2.cjs +0 -40
- package/dist/lib/node/chunk-LAICG6L2.cjs.map +0 -7
- package/dist/lib/node/countries-110m-KQ5WAB2O.cjs +0 -37877
- package/dist/lib/node/countries-110m-KQ5WAB2O.cjs.map +0 -7
- package/dist/lib/node/data.cjs +0 -28
- package/dist/lib/node/data.cjs.map +0 -7
- package/dist/lib/node/index.cjs +0 -1187
- package/dist/lib/node/index.cjs.map +0 -7
- package/dist/lib/node/meta.json +0 -1
- package/dist/lib/node-esm/chunk-PIIEDZEU.mjs +0 -11
- package/dist/lib/node-esm/chunk-PIIEDZEU.mjs.map +0 -7
- package/dist/lib/node-esm/countries-110m-DQ4XRC4B.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,109 +2,226 @@
|
|
|
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
|
+
setCenter: (center: LatLngLiteral, zoom?: number) => void;
|
|
34
|
+
setZoom: (cb: (zoom: number) => number) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
//
|
|
38
|
+
// Context
|
|
39
|
+
//
|
|
40
|
+
|
|
41
|
+
type MapContextValue = {
|
|
42
|
+
attention?: boolean;
|
|
43
|
+
onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
|
|
29
44
|
};
|
|
30
45
|
|
|
46
|
+
const [MapContextProvider, useMapContext] = createContext<MapContextValue>('Map');
|
|
47
|
+
|
|
31
48
|
//
|
|
32
49
|
// Root
|
|
33
50
|
//
|
|
34
51
|
|
|
35
|
-
type MapRootProps =
|
|
52
|
+
type MapRootProps = Pick<MapContextValue, 'onChange'>;
|
|
36
53
|
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Context provider for the map. Must wrap Map.Content.
|
|
56
|
+
*/
|
|
57
|
+
const MapRoot = composable<HTMLDivElement, MapRootProps>(({ children, onChange, ...props }, forwardedRef) => {
|
|
58
|
+
// TODO(burdon): Use attention: const [attention, setAttention] = useState(false);
|
|
59
|
+
const attention = false;
|
|
39
60
|
return (
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
<MapContextProvider attention={attention} onChange={onChange}>
|
|
62
|
+
<div
|
|
63
|
+
{...composableProps(props, {
|
|
64
|
+
role: 'none',
|
|
65
|
+
classNames: 'dx-container grid dx-focus-ring-inset',
|
|
66
|
+
})}
|
|
67
|
+
ref={forwardedRef}
|
|
68
|
+
>
|
|
69
|
+
{children}
|
|
70
|
+
</div>
|
|
71
|
+
</MapContextProvider>
|
|
50
72
|
);
|
|
51
|
-
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
MapRoot.displayName = 'Map.Root';
|
|
52
76
|
|
|
53
77
|
//
|
|
54
|
-
//
|
|
78
|
+
// Content
|
|
55
79
|
//
|
|
56
80
|
|
|
57
|
-
|
|
58
|
-
type MapController = {
|
|
59
|
-
setCenter: (center: LatLngExpression, zoom?: number) => void;
|
|
60
|
-
setZoom: (cb: (zoom: number) => number) => void;
|
|
61
|
-
};
|
|
81
|
+
type MapContentProps = ThemedClassName<Omit<MapContainerProps, 'children'> & PropsWithChildren>;
|
|
62
82
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
/**
|
|
84
|
+
* https://react-leaflet.js.org/docs/api-map
|
|
85
|
+
*/
|
|
86
|
+
const MAP_CONTENT_NAME = 'Map.Content';
|
|
66
87
|
|
|
67
|
-
|
|
88
|
+
const MapContent = forwardRef<MapController, MapContentProps>(
|
|
89
|
+
(
|
|
90
|
+
{ classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props },
|
|
68
91
|
forwardedRef,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
setZoom: (cb) => {
|
|
74
|
-
map.setZoom(cb(map.getZoom()));
|
|
75
|
-
},
|
|
76
|
-
}),
|
|
77
|
-
[map],
|
|
78
|
-
);
|
|
92
|
+
) => {
|
|
93
|
+
const { attention } = useMapContext(MAP_CONTENT_NAME);
|
|
94
|
+
const mapRef = useRef<L.Map>(null);
|
|
95
|
+
const map = mapRef.current;
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
97
|
+
useImperativeHandle(
|
|
98
|
+
forwardedRef,
|
|
99
|
+
() => ({
|
|
100
|
+
setCenter: (center: LatLngLiteral, zoom?: number) => {
|
|
101
|
+
mapRef.current?.setView(center, zoom);
|
|
102
|
+
},
|
|
103
|
+
setZoom: (cb: (zoom: number) => number) => {
|
|
104
|
+
mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
[],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// Enable/disable scroll wheel zoom.
|
|
111
|
+
// TODO(burdon): Use attention:
|
|
112
|
+
// const {hasAttention} = useAttention(props.id);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!map) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (attention) {
|
|
119
|
+
map.scrollWheelZoom.enable();
|
|
120
|
+
} else {
|
|
121
|
+
map.scrollWheelZoom.disable();
|
|
122
|
+
}
|
|
123
|
+
}, [map, attention]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<MapContainer
|
|
127
|
+
{...props}
|
|
128
|
+
className={mx('group relative grid bg-base-surface!', classNames)}
|
|
129
|
+
attributionControl={false}
|
|
130
|
+
zoomControl={false}
|
|
131
|
+
scrollWheelZoom={scrollWheelZoom}
|
|
132
|
+
doubleClickZoom={doubleClickZoom}
|
|
133
|
+
touchZoom={touchZoom}
|
|
134
|
+
center={center ?? defaults.center}
|
|
135
|
+
zoom={zoom ?? defaults.zoom}
|
|
136
|
+
whenReady={() => {}}
|
|
137
|
+
ref={mapRef}
|
|
138
|
+
>
|
|
139
|
+
{children}
|
|
140
|
+
</MapContainer>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
MapContent.displayName = 'Map.Content';
|
|
146
|
+
|
|
147
|
+
//
|
|
148
|
+
// Tiles
|
|
149
|
+
// https://react-leaflet.js.org/docs/api-components/#tilelayer
|
|
150
|
+
//
|
|
151
|
+
|
|
152
|
+
const MAP_TILES_NAME = 'Map.Tiles';
|
|
153
|
+
|
|
154
|
+
type MapTilesProps = {};
|
|
86
155
|
|
|
87
|
-
|
|
156
|
+
const MapTiles = (_props: MapTilesProps) => {
|
|
157
|
+
const ref = useRef<L.TileLayer>(null);
|
|
158
|
+
const { onChange } = useMapContext(MAP_TILES_NAME);
|
|
159
|
+
|
|
160
|
+
useMapEvents({
|
|
161
|
+
zoomstart: (ev) => {
|
|
162
|
+
onChange?.({
|
|
163
|
+
center: ev.target.getCenter(),
|
|
164
|
+
zoom: ev.target.getZoom(),
|
|
165
|
+
});
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
|
|
170
|
+
// Tailwind requires setting the property for static analysis.
|
|
171
|
+
const { attention } = useMapContext(MAP_TILES_NAME);
|
|
88
172
|
useEffect(() => {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
} else if (zoom !== undefined) {
|
|
92
|
-
map.setZoom(zoom);
|
|
173
|
+
if (ref.current) {
|
|
174
|
+
ref.current.getContainer().dataset.attention = attention ? '1' : '0';
|
|
93
175
|
}
|
|
94
|
-
}, [
|
|
176
|
+
}, [attention]);
|
|
95
177
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
178
|
+
// TODO(burdon): Option to add class 'invert'.
|
|
179
|
+
return (
|
|
180
|
+
<>
|
|
181
|
+
<TileLayer
|
|
182
|
+
ref={ref}
|
|
183
|
+
data-attention={attention}
|
|
184
|
+
detectRetina={true}
|
|
185
|
+
className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
|
|
186
|
+
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
|
187
|
+
keepBuffer={4}
|
|
188
|
+
// opacity={attention ? 1 : 0.7}
|
|
189
|
+
/>
|
|
190
|
+
|
|
191
|
+
{/* Temperature map. */}
|
|
192
|
+
{/* <WMSTileLayer
|
|
193
|
+
url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
|
|
194
|
+
layers='MODIS_Terra_Land_Surface_Temp_Day'
|
|
195
|
+
format='image/png'
|
|
196
|
+
transparent={true}
|
|
197
|
+
version='1.3.0'
|
|
198
|
+
attribution='NASA GIBS'
|
|
199
|
+
/> */}
|
|
200
|
+
|
|
201
|
+
{/* US Weather. */}
|
|
202
|
+
{/* <WMSTileLayer
|
|
203
|
+
url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
|
|
204
|
+
layers='nexrad-n0r' // layers='nexrad-n0r'
|
|
205
|
+
format='image/png'
|
|
206
|
+
transparent={true}
|
|
207
|
+
/> */}
|
|
208
|
+
</>
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
MapTiles.displayName = MAP_TILES_NAME;
|
|
213
|
+
|
|
214
|
+
//
|
|
215
|
+
// Markers
|
|
216
|
+
//
|
|
217
|
+
|
|
218
|
+
type MapMarkersProps = {
|
|
219
|
+
markers?: GeoMarker[];
|
|
220
|
+
selected?: string[];
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
|
|
224
|
+
const map = useMap();
|
|
108
225
|
|
|
109
226
|
// Set the viewport around the markers, or show the whole world map if `markers` is empty.
|
|
110
227
|
useEffect(() => {
|
|
@@ -117,14 +234,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
117
234
|
}, [markers]);
|
|
118
235
|
|
|
119
236
|
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. */}
|
|
237
|
+
<>
|
|
128
238
|
{markers?.map(({ id, title, location: { lat, lng } }) => {
|
|
129
239
|
return (
|
|
130
240
|
<Marker
|
|
@@ -132,6 +242,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
132
242
|
position={{ lat, lng }}
|
|
133
243
|
icon={
|
|
134
244
|
// TODO(burdon): Create custom icon from bundled assets.
|
|
245
|
+
// TODO(burdon): Selection state.
|
|
135
246
|
new L.Icon({
|
|
136
247
|
iconUrl: 'https://dxos.network/marker-icon.png',
|
|
137
248
|
iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
|
|
@@ -147,9 +258,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
147
258
|
</Marker>
|
|
148
259
|
);
|
|
149
260
|
})}
|
|
150
|
-
|
|
261
|
+
</>
|
|
151
262
|
);
|
|
152
|
-
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
MapMarkers.displayName = 'Map.Markers';
|
|
153
266
|
|
|
154
267
|
//
|
|
155
268
|
// Controls
|
|
@@ -167,7 +280,7 @@ const CustomControl = ({
|
|
|
167
280
|
useEffect(() => {
|
|
168
281
|
const control = new Control({ position });
|
|
169
282
|
control.onAdd = () => {
|
|
170
|
-
const container = DomUtil.create('div', mx('
|
|
283
|
+
const container = DomUtil.create('div', mx('m-0!', controlPositions[position]));
|
|
171
284
|
DomEvent.disableClickPropagation(container);
|
|
172
285
|
DomEvent.disableScrollPropagation(container);
|
|
173
286
|
|
|
@@ -192,23 +305,36 @@ const CustomControl = ({
|
|
|
192
305
|
|
|
193
306
|
type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
|
|
194
307
|
|
|
308
|
+
const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
|
|
309
|
+
<CustomControl position={position} {...props}>
|
|
310
|
+
<ZoomControls onAction={onAction} />
|
|
311
|
+
</CustomControl>
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
|
|
315
|
+
<CustomControl position={position} {...props}>
|
|
316
|
+
<ActionControls onAction={onAction} />
|
|
317
|
+
</CustomControl>
|
|
318
|
+
);
|
|
319
|
+
|
|
195
320
|
//
|
|
196
321
|
// Map
|
|
197
322
|
//
|
|
198
323
|
|
|
199
324
|
export const Map = {
|
|
200
325
|
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
|
-
),
|
|
326
|
+
Content: MapContent,
|
|
327
|
+
Tiles: MapTiles,
|
|
328
|
+
Markers: MapMarkers,
|
|
329
|
+
Zoom: MapZoom,
|
|
330
|
+
Action: MapAction,
|
|
212
331
|
};
|
|
213
332
|
|
|
214
|
-
export {
|
|
333
|
+
export {
|
|
334
|
+
type MapController,
|
|
335
|
+
type MapRootProps,
|
|
336
|
+
type MapContentProps,
|
|
337
|
+
type MapTilesProps,
|
|
338
|
+
type MapMarkersProps,
|
|
339
|
+
type MapControlProps,
|
|
340
|
+
};
|
|
@@ -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);
|