@dxos/react-ui-geo 0.8.4-main.84f28bd → 0.8.4-main.ae835ea
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 +187 -163
- package/dist/lib/browser/index.mjs.map +3 -3
- 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 +187 -163
- package/dist/lib/node-esm/index.mjs.map +3 -3
- 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 +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- 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 +81 -33
- package/src/components/Globe/Globe.tsx +78 -61
- package/src/components/Map/Map.stories.tsx +25 -14
- package/src/components/Map/Map.tsx +180 -95
- package/src/components/Toolbar/Controls.tsx +2 -6
- 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 +9 -8
- package/src/index.ts +1 -1
- 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 };
|
|
@@ -22,9 +22,8 @@ export const controlPositions: Record<ControlPosition, string> = {
|
|
|
22
22
|
|
|
23
23
|
export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
|
|
24
24
|
return (
|
|
25
|
-
<Toolbar.Root classNames={['gap-
|
|
25
|
+
<Toolbar.Root classNames={['gap-2', classNames]}>
|
|
26
26
|
<IconButton
|
|
27
|
-
//
|
|
28
27
|
icon='ph--plus--regular'
|
|
29
28
|
label='zoom in'
|
|
30
29
|
iconOnly
|
|
@@ -33,7 +32,6 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
|
|
|
33
32
|
onClick={() => onAction?.('zoom-in')}
|
|
34
33
|
/>
|
|
35
34
|
<IconButton
|
|
36
|
-
//
|
|
37
35
|
icon='ph--minus--regular'
|
|
38
36
|
label='zoom out'
|
|
39
37
|
iconOnly
|
|
@@ -47,9 +45,8 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
|
|
|
47
45
|
|
|
48
46
|
export const ActionControls = ({ classNames, onAction }: ControlProps) => {
|
|
49
47
|
return (
|
|
50
|
-
<Toolbar.Root classNames={['gap-
|
|
48
|
+
<Toolbar.Root classNames={['gap-2', classNames]}>
|
|
51
49
|
<IconButton
|
|
52
|
-
//
|
|
53
50
|
icon='ph--play--regular'
|
|
54
51
|
label='start'
|
|
55
52
|
iconOnly
|
|
@@ -58,7 +55,6 @@ export const ActionControls = ({ classNames, onAction }: ControlProps) => {
|
|
|
58
55
|
onClick={() => onAction?.('start')}
|
|
59
56
|
/>
|
|
60
57
|
<IconButton
|
|
61
|
-
//
|
|
62
58
|
icon='ph--globe-hemisphere-west--regular'
|
|
63
59
|
label='toggle'
|
|
64
60
|
iconOnly
|
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,7 +30,7 @@ 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(), []);
|
|
@@ -48,7 +49,7 @@ export const useTour = (
|
|
|
48
49
|
const path = geoPath(projection, context).pointRadius(2);
|
|
49
50
|
|
|
50
51
|
const tilt = options.tilt ?? 0;
|
|
51
|
-
let last:
|
|
52
|
+
let last: LatLngLiteral;
|
|
52
53
|
try {
|
|
53
54
|
const p = [...points];
|
|
54
55
|
if (options.loop) {
|
|
@@ -82,14 +83,14 @@ export const useTour = (
|
|
|
82
83
|
{
|
|
83
84
|
context.beginPath();
|
|
84
85
|
context.strokeStyle = options?.styles?.arc?.strokeStyle ?? 'yellow';
|
|
85
|
-
context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.
|
|
86
|
+
context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
|
|
86
87
|
context.setLineDash(options?.styles?.arc?.lineDash ?? []);
|
|
87
88
|
path({ type: 'LineString', coordinates: [ip(t1), ip(t2)] });
|
|
88
89
|
context.stroke();
|
|
89
90
|
|
|
90
91
|
context.beginPath();
|
|
91
92
|
context.fillStyle = options?.styles?.cursor?.fillStyle ?? 'orange';
|
|
92
|
-
path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.
|
|
93
|
+
path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
|
|
93
94
|
path({ type: 'Point', coordinates: ip(t2) });
|
|
94
95
|
context.fill();
|
|
95
96
|
}
|
|
@@ -104,7 +105,7 @@ export const useTour = (
|
|
|
104
105
|
await transition.end();
|
|
105
106
|
last = next;
|
|
106
107
|
}
|
|
107
|
-
} catch
|
|
108
|
+
} catch {
|
|
108
109
|
// Ignore.
|
|
109
110
|
} finally {
|
|
110
111
|
setRunning(false);
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
package/src/util/inertia.ts
CHANGED
package/src/util/path.ts
CHANGED
|
@@ -4,22 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
import { type GeoGeometryObjects, geoCircle as d3GeoCircle } from 'd3';
|
|
6
6
|
import { type Point, type Polygon, type Position } from 'geojson';
|
|
7
|
+
import { type LatLngLiteral } from 'leaflet';
|
|
7
8
|
|
|
8
9
|
import type { Vector } from '../hooks';
|
|
9
10
|
|
|
10
|
-
export type LatLng = { lat: number; lng: number };
|
|
11
|
-
|
|
12
11
|
export const positionToRotation = ([lng, lat]: [number, number], tilt = 0): Vector => [-lng, tilt - lat, 0];
|
|
13
12
|
|
|
14
|
-
export const geoToPosition = ({ lat, lng }:
|
|
13
|
+
export const geoToPosition = ({ lat, lng }: LatLngLiteral): [number, number] => [lng, lat];
|
|
15
14
|
|
|
16
|
-
export const geoPoint = (point:
|
|
15
|
+
export const geoPoint = (point: LatLngLiteral): Point => ({ type: 'Point', coordinates: geoToPosition(point) });
|
|
17
16
|
|
|
18
17
|
// https://github.com/d3/d3-geo#geoCircle
|
|
19
|
-
export const geoCircle = ({ lat, lng }:
|
|
18
|
+
export const geoCircle = ({ lat, lng }: LatLngLiteral, radius: number): Polygon =>
|
|
20
19
|
d3GeoCircle().radius(radius).center([lng, lat])();
|
|
21
20
|
|
|
22
|
-
export const geoLine = (p1:
|
|
21
|
+
export const geoLine = (p1: LatLngLiteral, p2: LatLngLiteral): GeoGeometryObjects => ({
|
|
23
22
|
type: 'LineString',
|
|
24
23
|
coordinates: [
|
|
25
24
|
[p1.lng, p1.lat],
|
package/src/util/render.ts
CHANGED
|
@@ -6,7 +6,9 @@ import { type GeoPath, type GeoPermissibleObjects, geoGraticule } from 'd3';
|
|
|
6
6
|
import { feature, mesh } from 'topojson-client';
|
|
7
7
|
import { type Topology } from 'topojson-specification';
|
|
8
8
|
|
|
9
|
-
import { type
|
|
9
|
+
import { type LatLngLiteral } from '../types';
|
|
10
|
+
|
|
11
|
+
import { geoLine, geoPoint } from './path';
|
|
10
12
|
|
|
11
13
|
export type Styles = Record<string, any>;
|
|
12
14
|
|
|
@@ -25,8 +27,8 @@ export type Style =
|
|
|
25
27
|
export type StyleSet = Partial<Record<Style, Styles>>;
|
|
26
28
|
|
|
27
29
|
export type Features = {
|
|
28
|
-
points?:
|
|
29
|
-
lines?: { source:
|
|
30
|
+
points?: LatLngLiteral[];
|
|
31
|
+
lines?: { source: LatLngLiteral; target: LatLngLiteral }[];
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
export type Layer = {
|