@dxos/react-ui-geo 0.8.4-main.fffef41 → 0.8.4-staging.60fe92afc8
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/countries-10m.ts +12 -0
- package/data/countries-110m.ts +4 -10579
- package/data/countries-50m.ts +12 -0
- package/dist/lib/browser/chunk-SC2FBYFU.mjs +17 -0
- package/dist/lib/browser/chunk-SC2FBYFU.mjs.map +7 -0
- package/dist/lib/browser/countries-10m-CWWDOKH7.mjs +6 -0
- package/dist/lib/browser/countries-10m-CWWDOKH7.mjs.map +7 -0
- package/dist/lib/browser/countries-110m-72QBAA5E.mjs +6 -0
- package/dist/lib/browser/countries-110m-72QBAA5E.mjs.map +7 -0
- package/dist/lib/browser/countries-50m-H7SL7KVF.mjs +6 -0
- package/dist/lib/browser/countries-50m-H7SL7KVF.mjs.map +7 -0
- package/dist/lib/browser/data.mjs +1 -1
- package/dist/lib/browser/index.mjs +1046 -579
- package/dist/lib/browser/index.mjs.map +4 -4
- 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/chunk-VZENBYLJ.mjs +19 -0
- package/dist/lib/node-esm/chunk-VZENBYLJ.mjs.map +7 -0
- package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs +8 -0
- package/dist/lib/node-esm/countries-10m-DJZV66KG.mjs.map +7 -0
- package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs +8 -0
- package/dist/lib/node-esm/countries-110m-H3WY6K4Q.mjs.map +7 -0
- package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs +8 -0
- package/dist/lib/node-esm/countries-50m-ZY7Z3IWD.mjs.map +7 -0
- package/dist/lib/node-esm/data.mjs +1 -1
- package/dist/lib/node-esm/index.mjs +1046 -579
- package/dist/lib/node-esm/index.mjs.map +4 -4
- 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-10m.d.ts +8 -0
- package/dist/types/data/countries-10m.d.ts.map +1 -0
- package/dist/types/data/countries-110m.d.ts +2 -30
- package/dist/types/data/countries-110m.d.ts.map +1 -1
- package/dist/types/data/countries-50m.d.ts +8 -0
- package/dist/types/data/countries-50m.d.ts.map +1 -0
- 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 +19 -9
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +17 -7
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.d.ts +51 -9
- package/dist/types/src/components/Map/Map.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.stories.d.ts +9 -5
- 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/data.d.ts +9 -1
- package/dist/types/src/data.d.ts.map +1 -1
- package/dist/types/src/hooks/context.d.ts +38 -3
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -1
- package/dist/types/src/hooks/useDrag.d.ts +22 -2
- package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +3 -2
- 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/useSimplifiedTopology.d.ts +32 -0
- package/dist/types/src/hooks/useSimplifiedTopology.d.ts.map +1 -0
- 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/useTopology.d.ts +26 -0
- package/dist/types/src/hooks/useTopology.d.ts.map +1 -0
- package/dist/types/src/hooks/useTour.d.ts +3 -2
- package/dist/types/src/hooks/useTour.d.ts.map +1 -1
- package/dist/types/src/hooks/useWheel.d.ts +24 -0
- package/dist/types/src/hooks/useWheel.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +0 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +6 -6
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/util/animation.d.ts +16 -0
- package/dist/types/src/util/animation.d.ts.map +1 -0
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/index.d.ts +2 -0
- package/dist/types/src/util/index.d.ts.map +1 -1
- package/dist/types/src/util/inertia.d.ts.map +1 -1
- package/dist/types/src/util/path.d.ts.map +1 -1
- package/dist/types/src/util/render.d.ts +25 -1
- package/dist/types/src/util/render.d.ts.map +1 -1
- package/dist/types/src/util/styles.d.ts +4 -0
- package/dist/types/src/util/styles.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -35
- package/src/components/Globe/Globe.stories.tsx +141 -65
- package/src/components/Globe/Globe.tsx +262 -119
- package/src/components/Map/Map.stories.tsx +59 -12
- package/src/components/Map/Map.tsx +325 -82
- package/src/components/Toolbar/Controls.tsx +5 -5
- package/src/data.ts +19 -2
- package/src/hooks/context.tsx +46 -31
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useDrag.ts +33 -5
- package/src/hooks/useGlobeZoomHandler.ts +2 -1
- package/src/hooks/useSimplifiedTopology.ts +81 -0
- package/src/hooks/useSpinner.ts +1 -2
- package/src/hooks/useTopology.ts +95 -0
- package/src/hooks/useTour.ts +70 -81
- package/src/hooks/useWheel.ts +83 -0
- package/src/index.ts +0 -2
- package/src/translations.ts +5 -5
- package/src/util/animation.ts +35 -0
- package/src/util/index.ts +2 -0
- package/src/util/inertia.ts +87 -4
- package/src/util/render.ts +105 -17
- package/src/util/styles.ts +62 -0
- package/dist/lib/browser/chunk-GMWLKTLN.mjs +0 -9
- package/dist/lib/browser/chunk-GMWLKTLN.mjs.map +0 -7
- package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs +0 -37859
- package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JODBF4CC.mjs +0 -11
- package/dist/lib/node-esm/chunk-JODBF4CC.mjs.map +0 -7
- package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs +0 -37861
- package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs.map +0 -7
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
type GeoProjection,
|
|
7
|
+
selection as d3Selection,
|
|
7
8
|
easeLinear,
|
|
8
9
|
easeSinOut,
|
|
9
10
|
geoMercator,
|
|
@@ -17,7 +18,9 @@ import { type ControlPosition } from 'leaflet';
|
|
|
17
18
|
import React, {
|
|
18
19
|
type PropsWithChildren,
|
|
19
20
|
forwardRef,
|
|
21
|
+
useCallback,
|
|
20
22
|
useEffect,
|
|
23
|
+
useId,
|
|
21
24
|
useImperativeHandle,
|
|
22
25
|
useMemo,
|
|
23
26
|
useRef,
|
|
@@ -26,19 +29,32 @@ import React, {
|
|
|
26
29
|
import { useResizeDetector } from 'react-resize-detector';
|
|
27
30
|
import { type Topology } from 'topojson-specification';
|
|
28
31
|
|
|
29
|
-
import {
|
|
30
|
-
|
|
32
|
+
import {
|
|
33
|
+
type ThemeMode,
|
|
34
|
+
type ThemedClassName,
|
|
35
|
+
useComposedRefs,
|
|
36
|
+
useControlledState,
|
|
37
|
+
useDynamicRef,
|
|
38
|
+
useThemeContext,
|
|
39
|
+
} from '@dxos/react-ui';
|
|
40
|
+
import { composable, composableProps } from '@dxos/react-ui';
|
|
41
|
+
import { mx } from '@dxos/ui-theme';
|
|
31
42
|
|
|
32
43
|
import {
|
|
33
|
-
|
|
34
|
-
type GlobeContextProviderProps,
|
|
44
|
+
GlobeContext,
|
|
35
45
|
type GlobeContextType,
|
|
46
|
+
type GlobeController,
|
|
47
|
+
type Point,
|
|
48
|
+
type Size,
|
|
49
|
+
type Vector,
|
|
36
50
|
useGlobeContext,
|
|
37
51
|
} from '../../hooks';
|
|
38
52
|
import {
|
|
39
53
|
type Features,
|
|
40
54
|
type StyleSet,
|
|
41
55
|
createLayers,
|
|
56
|
+
createRotationTween,
|
|
57
|
+
flyDuration,
|
|
42
58
|
geoToPosition,
|
|
43
59
|
positionToRotation,
|
|
44
60
|
renderLayers,
|
|
@@ -54,19 +70,15 @@ const defaultStyles: Record<ThemeMode, StyleSet> = {
|
|
|
54
70
|
background: {
|
|
55
71
|
fillStyle: '#EEE',
|
|
56
72
|
},
|
|
57
|
-
|
|
58
73
|
water: {
|
|
59
74
|
fillStyle: '#555',
|
|
60
75
|
},
|
|
61
|
-
|
|
62
76
|
land: {
|
|
63
77
|
fillStyle: '#999',
|
|
64
78
|
},
|
|
65
|
-
|
|
66
79
|
line: {
|
|
67
80
|
strokeStyle: 'darkred',
|
|
68
81
|
},
|
|
69
|
-
|
|
70
82
|
point: {
|
|
71
83
|
fillStyle: '#111111',
|
|
72
84
|
strokeStyle: '#111111',
|
|
@@ -78,19 +90,15 @@ const defaultStyles: Record<ThemeMode, StyleSet> = {
|
|
|
78
90
|
background: {
|
|
79
91
|
fillStyle: '#111111',
|
|
80
92
|
},
|
|
81
|
-
|
|
82
93
|
water: {
|
|
83
94
|
fillStyle: '#123E6A',
|
|
84
95
|
},
|
|
85
|
-
|
|
86
96
|
land: {
|
|
87
97
|
fillStyle: '#032153',
|
|
88
98
|
},
|
|
89
|
-
|
|
90
99
|
line: {
|
|
91
100
|
strokeStyle: '#111111',
|
|
92
101
|
},
|
|
93
|
-
|
|
94
102
|
point: {
|
|
95
103
|
fillStyle: '#111111',
|
|
96
104
|
strokeStyle: '#111111',
|
|
@@ -100,11 +108,6 @@ const defaultStyles: Record<ThemeMode, StyleSet> = {
|
|
|
100
108
|
},
|
|
101
109
|
};
|
|
102
110
|
|
|
103
|
-
export type GlobeController = {
|
|
104
|
-
canvas: HTMLCanvasElement;
|
|
105
|
-
projection: GeoProjection;
|
|
106
|
-
} & Pick<GlobeContextType, 'zoom' | 'translation' | 'rotation' | 'setZoom' | 'setTranslation' | 'setRotation'>;
|
|
107
|
-
|
|
108
111
|
export type ProjectionType = 'orthographic' | 'mercator' | 'transverse-mercator';
|
|
109
112
|
|
|
110
113
|
const projectionMap: Record<ProjectionType, () => GeoProjection> = {
|
|
@@ -126,19 +129,96 @@ const getProjection = (type: GlobeCanvasProps['projection'] = 'orthographic'): G
|
|
|
126
129
|
// Root
|
|
127
130
|
//
|
|
128
131
|
|
|
129
|
-
|
|
132
|
+
const DEFAULT_ZOOM = 1.5;
|
|
130
133
|
|
|
131
|
-
|
|
132
|
-
|
|
134
|
+
type GlobeRootProps = Partial<Pick<GlobeContextType, 'center' | 'zoom' | 'translation' | 'rotation'>> &
|
|
135
|
+
PropsWithChildren;
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Headless context/state provider for the globe. Renders no DOM; wrap a `Globe.Viewport` to mount
|
|
139
|
+
* the measured container. The current `GlobeController` (built by `Globe.Canvas`) is exposed via
|
|
140
|
+
* this component's `ref`.
|
|
141
|
+
*/
|
|
142
|
+
const GlobeRoot = forwardRef<GlobeController | null, GlobeRootProps>(
|
|
143
|
+
(
|
|
144
|
+
{
|
|
145
|
+
children,
|
|
146
|
+
center: centerProp,
|
|
147
|
+
zoom: zoomProp = DEFAULT_ZOOM,
|
|
148
|
+
translation: translationProp,
|
|
149
|
+
rotation: rotationProp,
|
|
150
|
+
},
|
|
151
|
+
forwardedRef,
|
|
152
|
+
) => {
|
|
153
|
+
const [size, setSize] = useState<Size>({ width: 0, height: 0 });
|
|
154
|
+
const [center, setCenter] = useControlledState(centerProp);
|
|
155
|
+
const [zoom, setZoom] = useControlledState(zoomProp);
|
|
156
|
+
const [translation, setTranslation] = useControlledState<Point>(translationProp);
|
|
157
|
+
const [rotation, setRotation] = useControlledState<Vector>(rotationProp);
|
|
158
|
+
|
|
159
|
+
// The controller is built by Globe.Canvas and registered here; Globe.Root re-exposes it via its
|
|
160
|
+
// ref. Held in state (not a ref) so that when Globe.Canvas registers a new controller, Root
|
|
161
|
+
// re-renders and the imperative handle below updates — re-running consumer effects keyed on the
|
|
162
|
+
// controller (e.g. useDrag/useWheel) once the canvas mounts.
|
|
163
|
+
const [controller, setController] = useState<GlobeController | null>(null);
|
|
164
|
+
const registerController = useCallback((next: GlobeController | null) => setController(next), []);
|
|
165
|
+
|
|
166
|
+
// Expose the live controller (or null until Globe.Canvas mounts) via Root's ref.
|
|
167
|
+
useImperativeHandle(forwardedRef, () => controller, [controller]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<GlobeContext.Provider
|
|
171
|
+
value={{
|
|
172
|
+
size,
|
|
173
|
+
center,
|
|
174
|
+
zoom,
|
|
175
|
+
translation,
|
|
176
|
+
rotation,
|
|
177
|
+
setSize,
|
|
178
|
+
setCenter,
|
|
179
|
+
setZoom,
|
|
180
|
+
setTranslation,
|
|
181
|
+
setRotation,
|
|
182
|
+
registerController,
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
137
185
|
{children}
|
|
138
|
-
</
|
|
186
|
+
</GlobeContext.Provider>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
GlobeRoot.displayName = 'Globe.Root';
|
|
192
|
+
|
|
193
|
+
//
|
|
194
|
+
// Viewport
|
|
195
|
+
//
|
|
196
|
+
|
|
197
|
+
/** Consumer-facing props for `Globe.Viewport` (classNames + children). */
|
|
198
|
+
type GlobeViewportProps = ThemedClassName<PropsWithChildren>;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Measured container for the globe. Renders the `relative dx-container` div, observes its size, and
|
|
202
|
+
* publishes measurements to the context so `Globe.Canvas` can size the canvas.
|
|
203
|
+
*/
|
|
204
|
+
const GlobeViewport = composable<HTMLDivElement>(({ children, ...props }, forwardedRef) => {
|
|
205
|
+
const { setSize } = useGlobeContext();
|
|
206
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
207
|
+
const composedRef = useComposedRefs<HTMLDivElement>(localRef, forwardedRef);
|
|
208
|
+
const { width, height } = useResizeDetector<HTMLDivElement>({ targetRef: localRef });
|
|
209
|
+
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
setSize({ width: width ?? 0, height: height ?? 0 });
|
|
212
|
+
}, [width, height, setSize]);
|
|
213
|
+
|
|
214
|
+
return (
|
|
215
|
+
<div {...composableProps(props, { classNames: 'relative dx-container' })} ref={composedRef}>
|
|
216
|
+
{children}
|
|
139
217
|
</div>
|
|
140
218
|
);
|
|
141
|
-
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
GlobeViewport.displayName = 'Globe.Viewport';
|
|
142
222
|
|
|
143
223
|
//
|
|
144
224
|
// Canvas
|
|
@@ -152,103 +232,165 @@ type GlobeCanvasProps = {
|
|
|
152
232
|
};
|
|
153
233
|
|
|
154
234
|
/**
|
|
155
|
-
* Basic globe renderer.
|
|
235
|
+
* Basic globe renderer. Builds the imperative `GlobeController` and registers it with `Globe.Root`
|
|
236
|
+
* (via `registerController` from context) so consumers reading the controller from Root's ref get
|
|
237
|
+
* the live instance.
|
|
156
238
|
* https://github.com/topojson/world-atlas
|
|
157
239
|
*/
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
},
|
|
200
|
-
translation,
|
|
201
|
-
rotation,
|
|
202
|
-
setCenter,
|
|
203
|
-
setZoom: (s) => {
|
|
204
|
-
if (typeof s === 'function') {
|
|
205
|
-
const is = interpolateNumber(zoomRef.current, s(zoomRef.current));
|
|
206
|
-
// Stop easing if already zooming.
|
|
207
|
-
transition()
|
|
208
|
-
.ease(zooming.current ? easeLinear : easeSinOut)
|
|
209
|
-
.duration(200)
|
|
210
|
-
.tween('scale', () => (t) => setZoom(is(t)))
|
|
211
|
-
.on('end', () => {
|
|
212
|
-
zooming.current = false;
|
|
213
|
-
});
|
|
214
|
-
} else {
|
|
215
|
-
setZoom(s);
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
setTranslation,
|
|
219
|
-
setRotation,
|
|
220
|
-
};
|
|
221
|
-
}, [canvas]);
|
|
222
|
-
|
|
223
|
-
// https://d3js.org/d3-geo/path#geoPath
|
|
224
|
-
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
|
|
225
|
-
const generator = useMemo(
|
|
226
|
-
() => canvas && projection && geoPath(projection, canvas.getContext('2d', { alpha: false })),
|
|
227
|
-
[canvas, projection],
|
|
228
|
-
);
|
|
240
|
+
const GlobeCanvas = ({ projection: projectionProp, topology, features, styles: stylesProp }: GlobeCanvasProps) => {
|
|
241
|
+
const { themeMode } = useThemeContext();
|
|
242
|
+
const styles = useMemo(() => stylesProp ?? defaultStyles[themeMode], [stylesProp, themeMode]);
|
|
243
|
+
const { size, center, zoom, translation, rotation, setZoom, setTranslation, setRotation, registerController } =
|
|
244
|
+
useGlobeContext();
|
|
245
|
+
|
|
246
|
+
const zoomRef = useDynamicRef(zoom);
|
|
247
|
+
|
|
248
|
+
// Canvas.
|
|
249
|
+
const [canvas, setCanvas] = useState<HTMLCanvasElement>(null);
|
|
250
|
+
const canvasRef = (canvas: HTMLCanvasElement) => setCanvas(canvas);
|
|
251
|
+
|
|
252
|
+
// Projection.
|
|
253
|
+
const projection = useMemo(() => getProjection(projectionProp), [projectionProp]);
|
|
254
|
+
|
|
255
|
+
// Layers.
|
|
256
|
+
// TODO(burdon): Generate on-the-fly based on what is visible.
|
|
257
|
+
const layers = useMemo(() => {
|
|
258
|
+
return timer(() => createLayers(topology as Topology, features, styles));
|
|
259
|
+
}, [topology, features, styles]);
|
|
260
|
+
|
|
261
|
+
// Update rotation when the center changes. Preserve current zoom — callers can set zoom
|
|
262
|
+
// independently via the `zoom` prop or `setZoom` on the controller.
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (center) {
|
|
265
|
+
setRotation(positionToRotation(geoToPosition(center)));
|
|
266
|
+
}
|
|
267
|
+
}, [center]);
|
|
268
|
+
|
|
269
|
+
// Per-instance flyTo plumbing. d3 named transitions are scoped per DOM
|
|
270
|
+
// element and `d3Selection()` returns the documentElement root, so a
|
|
271
|
+
// shared name would let one globe's flyTo interrupt another's. The
|
|
272
|
+
// useId-scoped name keeps each Globe.Canvas's transition isolated.
|
|
273
|
+
const flyToSelection = useMemo(() => d3Selection(), []);
|
|
274
|
+
const flyToTransitionName = `globe-fly-to-${useId()}`;
|
|
275
|
+
useEffect(
|
|
276
|
+
() => () => {
|
|
277
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
278
|
+
},
|
|
279
|
+
[flyToSelection, flyToTransitionName],
|
|
280
|
+
);
|
|
229
281
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
282
|
+
// External controller.
|
|
283
|
+
const zooming = useRef(false);
|
|
284
|
+
const controller = useMemo<GlobeController>(() => {
|
|
285
|
+
return {
|
|
286
|
+
canvas,
|
|
287
|
+
projection,
|
|
288
|
+
get zoom() {
|
|
289
|
+
return zoomRef.current;
|
|
290
|
+
},
|
|
291
|
+
translation,
|
|
292
|
+
rotation,
|
|
293
|
+
setZoom: (state) => {
|
|
294
|
+
if (typeof state === 'function') {
|
|
295
|
+
const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
|
|
296
|
+
// Stop easing if already zooming.
|
|
297
|
+
transition()
|
|
298
|
+
.ease(zooming.current ? easeLinear : easeSinOut)
|
|
299
|
+
.duration(200)
|
|
300
|
+
.tween('scale', () => (t) => setZoom(is(t)))
|
|
301
|
+
.on('end', () => {
|
|
302
|
+
zooming.current = false;
|
|
303
|
+
});
|
|
304
|
+
} else {
|
|
305
|
+
setZoom(state);
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
setTranslation,
|
|
309
|
+
setRotation,
|
|
310
|
+
flyTo: (target, options = {}) => {
|
|
311
|
+
const { duration = 1_200, tilt = 0, onTick } = options;
|
|
312
|
+
const p2 = geoToPosition(target);
|
|
313
|
+
const r1 = projection.rotate() as Vector;
|
|
314
|
+
const r2 = positionToRotation(p2, tilt);
|
|
315
|
+
|
|
316
|
+
// Approximate current centre from the inverse of the rotation.
|
|
317
|
+
const p1: [number, number] = [-r1[0], -r1[1]];
|
|
318
|
+
const rotationTween = createRotationTween(projection, setRotation, r1, r2);
|
|
319
|
+
const iz = target.zoom !== undefined ? interpolateNumber(zoomRef.current, target.zoom) : undefined;
|
|
320
|
+
|
|
321
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
322
|
+
const tx = flyToSelection.transition(flyToTransitionName).duration(flyDuration(p1, p2, duration, 1_500));
|
|
323
|
+
if (onTick) {
|
|
324
|
+
tx.tween('flyToOnTick', () => onTick);
|
|
325
|
+
}
|
|
326
|
+
tx.tween('flyToRotation', () => rotationTween);
|
|
327
|
+
if (iz) {
|
|
328
|
+
tx.tween('flyToZoom', () => (t: number) => setZoom(iz(t)));
|
|
329
|
+
}
|
|
330
|
+
return tx.end();
|
|
331
|
+
},
|
|
332
|
+
cancelFlyTo: () => {
|
|
333
|
+
flyToSelection.interrupt(flyToTransitionName);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
// Keep the controller IDENTITY stable: only rebuild on these mount-stable deps. Deliberately
|
|
337
|
+
// exclude `center`/`translation`/`rotation` — they are read here as closure snapshots, and
|
|
338
|
+
// callers pass them as inline literals (e.g. `rotation={[0, 0, 0]}`), so a fresh reference every
|
|
339
|
+
// render would rebuild the controller, re-fire `registerController`, re-render Root, hand a new
|
|
340
|
+
// controller back through the consumer's `ref={setController}`, and loop forever. `projection`
|
|
341
|
+
// must stay in deps: switching between stories that pass different projection types creates a new
|
|
342
|
+
// instance via useMemo, and any consumer reading controller.projection (e.g. useDrag) would
|
|
343
|
+
// otherwise mutate a dead instance while the canvas renders the new one. The `useControlledState`
|
|
344
|
+
// setters and `zoomRef` are stable, so they need not be listed.
|
|
345
|
+
}, [canvas, projection, flyToSelection, flyToTransitionName]);
|
|
346
|
+
|
|
347
|
+
// Register the controller with Globe.Root and clear it on unmount.
|
|
348
|
+
useEffect(() => {
|
|
349
|
+
registerController(controller);
|
|
350
|
+
return () => registerController(null);
|
|
351
|
+
}, [registerController, controller]);
|
|
352
|
+
|
|
353
|
+
// https://d3js.org/d3-geo/path#geoPath
|
|
354
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
|
|
355
|
+
// Keep the context alpha-enabled: when a style set omits `background`, `renderLayers`
|
|
356
|
+
// clears to transparent so the canvas's themed CSS background (below) shows through the
|
|
357
|
+
// area outside the globe — correct in both light and dark mode.
|
|
358
|
+
const generator = useMemo(
|
|
359
|
+
() => canvas && projection && geoPath(projection, canvas.getContext('2d')),
|
|
360
|
+
[canvas, projection],
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Render on change.
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (canvas && projection) {
|
|
366
|
+
timer(() => {
|
|
367
|
+
// https://d3js.org/d3-geo/projection
|
|
368
|
+
projection
|
|
369
|
+
.scale((Math.min(size.width, size.height) / 2) * zoom)
|
|
370
|
+
.translate([size.width / 2 + (translation?.x ?? 0), size.height / 2 + (translation?.y ?? 0)])
|
|
371
|
+
.rotate(rotation ?? [0, 0, 0]);
|
|
372
|
+
|
|
373
|
+
// Provide a view-center for per-frame culling — only meaningful for
|
|
374
|
+
// projections that present a single visible hemisphere (e.g.
|
|
375
|
+
// orthographic). For Mercator/transverse-mercator the whole sphere
|
|
376
|
+
// is always visible, so we skip culling.
|
|
377
|
+
const isOrthographic = !projectionProp || projectionProp === 'orthographic';
|
|
378
|
+
const [lambda, phi] = (rotation ?? [0, 0, 0]) as Vector;
|
|
379
|
+
const viewCenter: [number, number] | undefined = isOrthographic ? [-lambda, -phi] : undefined;
|
|
380
|
+
|
|
381
|
+
renderLayers(generator, layers, zoom, styles, viewCenter);
|
|
382
|
+
});
|
|
247
383
|
}
|
|
384
|
+
}, [generator, size, zoom, translation, rotation, layers, projectionProp]);
|
|
248
385
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
386
|
+
if (!size.width || !size.height) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return <canvas ref={canvasRef} className='bg-base-surface' width={size.width} height={size.height} />;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
GlobeCanvas.displayName = 'Globe.Canvas';
|
|
252
394
|
|
|
253
395
|
//
|
|
254
396
|
// Debug
|
|
@@ -259,7 +401,7 @@ const GlobeDebug = ({ position = 'topleft' }: { position?: ControlPosition }) =>
|
|
|
259
401
|
return (
|
|
260
402
|
<div
|
|
261
403
|
className={mx(
|
|
262
|
-
'z-10 absolute
|
|
404
|
+
'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm',
|
|
263
405
|
controlPositions[position],
|
|
264
406
|
)}
|
|
265
407
|
>
|
|
@@ -310,6 +452,7 @@ const GlobeAction = ({ onAction, position = 'bottomright', ...props }: GlobeCont
|
|
|
310
452
|
|
|
311
453
|
export const Globe = {
|
|
312
454
|
Root: GlobeRoot,
|
|
455
|
+
Viewport: GlobeViewport,
|
|
313
456
|
Canvas: GlobeCanvas,
|
|
314
457
|
Zoom: GlobeZoom,
|
|
315
458
|
Action: GlobeAction,
|
|
@@ -317,4 +460,4 @@ export const Globe = {
|
|
|
317
460
|
Panel: GlobePanel,
|
|
318
461
|
};
|
|
319
462
|
|
|
320
|
-
export type { GlobeRootProps, GlobeCanvasProps };
|
|
463
|
+
export type { GlobeRootProps, GlobeViewportProps, GlobeCanvasProps };
|
|
@@ -3,26 +3,54 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
-
import React, { useState } from 'react';
|
|
6
|
+
import React, { useMemo, useState } from 'react';
|
|
7
7
|
|
|
8
|
-
import {
|
|
8
|
+
import { Input, Panel, Toolbar } from '@dxos/react-ui';
|
|
9
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
9
10
|
|
|
10
11
|
import { useMapZoomHandler } from '../../hooks';
|
|
11
12
|
import { type GeoMarker } from '../../types';
|
|
13
|
+
import { Map, MapMarkersProps, MapTilesProps, type MapController } from './Map';
|
|
12
14
|
|
|
13
|
-
|
|
15
|
+
type DefaultStoryProps = Pick<MapTilesProps, 'url'> & Pick<MapMarkersProps, 'markers'>;
|
|
14
16
|
|
|
15
|
-
const DefaultStory = ({ markers = [] }:
|
|
17
|
+
const DefaultStory = ({ url: urlProp, markers = [] }: DefaultStoryProps) => {
|
|
16
18
|
const [controller, setController] = useState<MapController>();
|
|
19
|
+
const [key, setKey] = useState('');
|
|
20
|
+
// Substitute the `${key}` placeholder in a keyed tile URL (e.g. MapTiler); undefined → default OSM tiles.
|
|
21
|
+
const url = useMemo(() => urlProp?.replace('${key}', key), [urlProp, key]);
|
|
22
|
+
|
|
17
23
|
const handleZoomAction = useMapZoomHandler(controller);
|
|
18
24
|
|
|
19
25
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
<Panel.Root>
|
|
27
|
+
{urlProp && (
|
|
28
|
+
<Panel.Toolbar asChild>
|
|
29
|
+
<Toolbar.Root>
|
|
30
|
+
<Input.Root>
|
|
31
|
+
<Input.TextInput
|
|
32
|
+
spellCheck={false}
|
|
33
|
+
placeholder='API KEY'
|
|
34
|
+
value={key}
|
|
35
|
+
onChange={(ev) => setKey(ev.target.value)}
|
|
36
|
+
/>
|
|
37
|
+
</Input.Root>
|
|
38
|
+
</Toolbar.Root>
|
|
39
|
+
</Panel.Toolbar>
|
|
40
|
+
)}
|
|
41
|
+
{/* Map.Root is headless (context only), so it sits outside Panel.Content; Panel.Content asChild
|
|
42
|
+
then targets the Leaflet frame (Map.Viewport) directly — no extra wrapper element. */}
|
|
43
|
+
<Map.Root ref={setController}>
|
|
44
|
+
<Panel.Content asChild>
|
|
45
|
+
<Map.Viewport>
|
|
46
|
+
<Map.Tiles url={url} />
|
|
47
|
+
<Map.Markers markers={markers} />
|
|
48
|
+
<Map.Zoom position='bottomleft' onAction={handleZoomAction} />
|
|
49
|
+
<Map.Action position='bottomright' />
|
|
50
|
+
</Map.Viewport>
|
|
51
|
+
</Panel.Content>
|
|
52
|
+
</Map.Root>
|
|
53
|
+
</Panel.Root>
|
|
26
54
|
);
|
|
27
55
|
};
|
|
28
56
|
|
|
@@ -30,7 +58,7 @@ const meta = {
|
|
|
30
58
|
title: 'ui/react-ui-geo/Map',
|
|
31
59
|
component: Map.Root as any,
|
|
32
60
|
render: DefaultStory,
|
|
33
|
-
decorators: [withTheme],
|
|
61
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
34
62
|
parameters: {
|
|
35
63
|
layout: 'fullscreen',
|
|
36
64
|
},
|
|
@@ -42,7 +70,17 @@ type Story = StoryObj<typeof meta>;
|
|
|
42
70
|
|
|
43
71
|
export const Default: Story = {};
|
|
44
72
|
|
|
45
|
-
export const
|
|
73
|
+
export const Bounds: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
markers: [
|
|
76
|
+
{ id: 'london', title: 'London', location: { lat: 51.5074, lng: -0.1278 } },
|
|
77
|
+
{ id: 'barcelona', title: 'Barcelona', location: { lat: 41.3851, lng: 2.1734 } },
|
|
78
|
+
{ id: 'warsaw', title: 'Warsaw', location: { lat: 52.2297, lng: 21.0122 } },
|
|
79
|
+
] as GeoMarker[],
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Markers: Story = {
|
|
46
84
|
args: {
|
|
47
85
|
markers: [
|
|
48
86
|
{ id: 'los angeles', title: 'Los Angeles', location: { lat: 34.0522, lng: -118.2437 } },
|
|
@@ -70,3 +108,12 @@ export const WithMarkers: Story = {
|
|
|
70
108
|
] as GeoMarker[],
|
|
71
109
|
},
|
|
72
110
|
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* https://docs.maptiler.com/leaflet
|
|
114
|
+
*/
|
|
115
|
+
export const CustomTiles: Story = {
|
|
116
|
+
args: {
|
|
117
|
+
url: 'https://api.maptiler.com/maps/streets-v4/{z}/{x}/{y}.png?&key=${key}',
|
|
118
|
+
},
|
|
119
|
+
};
|