@dxos/react-ui-geo 0.8.3 → 0.8.4-main.1068cf700f
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-WI4PCLDF.mjs → countries-110m-ZM3ZIEFS.mjs} +2 -2
- 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 +404 -458
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/{chunk-PIIEDZEU.mjs → chunk-JODBF4CC.mjs} +3 -3
- package/dist/lib/node-esm/{countries-110m-DQ4XRC4B.mjs → countries-110m-3SFASWVD.mjs} +2 -2
- 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 +404 -458
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +25 -9
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.d.ts +28 -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 +7 -7
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useSpinner.d.ts +1 -1
- package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
- package/dist/types/src/hooks/useTour.d.ts +4 -3
- package/dist/types/src/hooks/useTour.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +12 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +2 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/dist/types/src/util/path.d.ts +5 -8
- package/dist/types/src/util/path.d.ts.map +1 -1
- package/dist/types/src/util/render.d.ts +4 -4
- package/dist/types/src/util/render.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +29 -23
- package/src/components/Globe/Globe.stories.tsx +85 -37
- package/src/components/Globe/Globe.tsx +80 -63
- package/src/components/Map/Map.stories.tsx +25 -14
- package/src/components/Map/Map.tsx +183 -96
- package/src/components/Toolbar/Controls.tsx +14 -20
- package/src/components/index.ts +0 -2
- package/src/hooks/context.tsx +22 -16
- package/src/hooks/useGlobeZoomHandler.ts +9 -3
- package/src/hooks/useMapZoomHandler.ts +1 -1
- package/src/hooks/useSpinner.ts +2 -1
- package/src/hooks/useTour.ts +10 -8
- package/src/index.ts +2 -1
- package/src/translations.ts +20 -0
- package/src/types.ts +3 -1
- package/src/util/inertia.ts +1 -1
- package/src/util/path.ts +5 -6
- package/src/util/render.ts +5 -3
- package/dist/lib/browser/chunk-ENCWOTYX.mjs +0 -9
- 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/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
- /package/dist/lib/browser/{chunk-ENCWOTYX.mjs.map → chunk-GMWLKTLN.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-PIIEDZEU.mjs.map → chunk-JODBF4CC.mjs.map} +0 -0
|
@@ -2,45 +2,56 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import '@
|
|
6
|
-
|
|
7
|
-
import { type StoryObj, type Meta } from '@storybook/react';
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
8
6
|
import React, { useState } from 'react';
|
|
9
7
|
|
|
10
|
-
import {
|
|
8
|
+
import { withTheme } from '@dxos/react-ui/testing';
|
|
11
9
|
|
|
12
|
-
import { Map, type MapController } from './Map';
|
|
13
10
|
import { useMapZoomHandler } from '../../hooks';
|
|
14
|
-
import { type
|
|
11
|
+
import { type GeoMarker } from '../../types';
|
|
12
|
+
|
|
13
|
+
import { Map, type MapController } from './Map';
|
|
15
14
|
|
|
16
|
-
const DefaultStory = ({ markers = [] }: { markers?:
|
|
15
|
+
const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
|
|
17
16
|
const [controller, setController] = useState<MapController>();
|
|
18
17
|
const handleZoomAction = useMapZoomHandler(controller);
|
|
19
18
|
|
|
20
19
|
return (
|
|
21
|
-
<Map.Root>
|
|
22
|
-
<Map.
|
|
20
|
+
<Map.Root ref={setController}>
|
|
21
|
+
<Map.Tiles />
|
|
22
|
+
<Map.Markers markers={markers} />
|
|
23
23
|
<Map.Zoom position='bottomleft' onAction={handleZoomAction} />
|
|
24
24
|
<Map.Action position='bottomright' />
|
|
25
25
|
</Map.Root>
|
|
26
26
|
);
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
const meta
|
|
29
|
+
const meta = {
|
|
30
30
|
title: 'ui/react-ui-geo/Map',
|
|
31
|
+
component: Map.Root as any,
|
|
31
32
|
render: DefaultStory,
|
|
32
|
-
decorators: [withTheme
|
|
33
|
-
|
|
33
|
+
decorators: [withTheme()],
|
|
34
|
+
parameters: {
|
|
35
|
+
layout: 'fullscreen',
|
|
36
|
+
},
|
|
37
|
+
} satisfies Meta<typeof DefaultStory>;
|
|
34
38
|
|
|
35
39
|
export default meta;
|
|
36
40
|
|
|
37
|
-
type Story = StoryObj<typeof
|
|
41
|
+
type Story = StoryObj<typeof meta>;
|
|
38
42
|
|
|
39
43
|
export const Default: Story = {};
|
|
40
44
|
|
|
41
45
|
export const WithMarkers: Story = {
|
|
42
46
|
args: {
|
|
43
47
|
markers: [
|
|
48
|
+
{ id: 'los angeles', title: 'Los Angeles', location: { lat: 34.0522, lng: -118.2437 } },
|
|
49
|
+
{ id: 'new york', title: 'New York', location: { lat: 40.7128, lng: -74.006 } },
|
|
50
|
+
{ id: 'warsaw', title: 'Warsaw', location: { lat: 52.2297, lng: 21.0122 } },
|
|
51
|
+
{ id: 'london', title: 'London', location: { lat: 51.5074, lng: -0.1278 } },
|
|
52
|
+
{ id: 'toronto', title: 'Toronto', location: { lat: 43.6532, lng: -79.3832 } },
|
|
53
|
+
{ id: 'seattle', title: 'Seattle', location: { lat: 47.6062, lng: -122.3321 } },
|
|
54
|
+
{ id: 'barcelona', title: 'Barcelona', location: { lat: 41.3851, lng: 2.1734 } },
|
|
44
55
|
{ id: 'tokyo', title: 'Tokyo', location: { lat: 35.6762, lng: 139.6503 } },
|
|
45
56
|
{ id: 'sydney', title: 'Sydney', location: { lat: -33.8688, lng: 151.2093 } },
|
|
46
57
|
{ id: 'auckland', title: 'Auckland', location: { lat: -36.8509, lng: 174.7645 } },
|
|
@@ -56,6 +67,6 @@ export const WithMarkers: Story = {
|
|
|
56
67
|
{ id: 'phnom-penh', title: 'Phnom Penh', location: { lat: 11.5564, lng: 104.9282 } },
|
|
57
68
|
{ id: 'vientiane', title: 'Vientiane', location: { lat: 17.9757, lng: 102.6331 } },
|
|
58
69
|
{ id: 'yangon', title: 'Yangon', location: { lat: 16.8661, lng: 96.1951 } },
|
|
59
|
-
] as
|
|
70
|
+
] as GeoMarker[],
|
|
60
71
|
},
|
|
61
72
|
};
|
|
@@ -2,109 +2,195 @@
|
|
|
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 {
|
|
17
|
-
import { defaultTx, mx } from '@dxos/react-ui-theme';
|
|
13
|
+
import { ThemeProvider, type ThemedClassName, Tooltip } from '@dxos/react-ui';
|
|
14
|
+
import { 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,
|
|
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
|
+
const MAP_TILES_NAME = 'Map.Tiles';
|
|
122
|
+
|
|
123
|
+
type MapTilesProps = {};
|
|
124
|
+
|
|
125
|
+
const MapTiles = (_props: MapTilesProps) => {
|
|
126
|
+
const ref = useRef<L.TileLayer>(null);
|
|
127
|
+
const { onChange } = useMapContext(MAP_TILES_NAME);
|
|
86
128
|
|
|
87
|
-
|
|
129
|
+
useMapEvents({
|
|
130
|
+
zoomstart: (ev) => {
|
|
131
|
+
onChange?.({
|
|
132
|
+
center: ev.target.getCenter(),
|
|
133
|
+
zoom: ev.target.getZoom(),
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
|
|
139
|
+
// Tailwind requires setting the property for static analysis.
|
|
140
|
+
const { attention } = useMapContext(MAP_TILES_NAME);
|
|
88
141
|
useEffect(() => {
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
} else if (zoom !== undefined) {
|
|
92
|
-
map.setZoom(zoom);
|
|
142
|
+
if (ref.current) {
|
|
143
|
+
ref.current.getContainer().dataset.attention = attention ? '1' : '0';
|
|
93
144
|
}
|
|
94
|
-
}, [
|
|
145
|
+
}, [attention]);
|
|
95
146
|
|
|
96
|
-
//
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
147
|
+
// TODO(burdon): Option to add class 'invert'.
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<TileLayer
|
|
151
|
+
ref={ref}
|
|
152
|
+
data-attention={attention}
|
|
153
|
+
detectRetina={true}
|
|
154
|
+
className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
|
|
155
|
+
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
|
156
|
+
keepBuffer={4}
|
|
157
|
+
// opacity={attention ? 1 : 0.7}
|
|
158
|
+
/>
|
|
159
|
+
|
|
160
|
+
{/* Temperature map. */}
|
|
161
|
+
{/* <WMSTileLayer
|
|
162
|
+
url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
|
|
163
|
+
layers='MODIS_Terra_Land_Surface_Temp_Day'
|
|
164
|
+
format='image/png'
|
|
165
|
+
transparent={true}
|
|
166
|
+
version='1.3.0'
|
|
167
|
+
attribution='NASA GIBS'
|
|
168
|
+
/> */}
|
|
169
|
+
|
|
170
|
+
{/* US Weather. */}
|
|
171
|
+
{/* <WMSTileLayer
|
|
172
|
+
url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
|
|
173
|
+
layers='nexrad-n0r' // layers='nexrad-n0r'
|
|
174
|
+
format='image/png'
|
|
175
|
+
transparent={true}
|
|
176
|
+
/> */}
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
MapTiles.displayName = MAP_TILES_NAME;
|
|
182
|
+
|
|
183
|
+
//
|
|
184
|
+
// Markers
|
|
185
|
+
//
|
|
186
|
+
|
|
187
|
+
type MapMarkersProps = {
|
|
188
|
+
markers?: GeoMarker[];
|
|
189
|
+
selected?: string[];
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
|
|
193
|
+
const map = useMap();
|
|
108
194
|
|
|
109
195
|
// Set the viewport around the markers, or show the whole world map if `markers` is empty.
|
|
110
196
|
useEffect(() => {
|
|
@@ -117,14 +203,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
117
203
|
}, [markers]);
|
|
118
204
|
|
|
119
205
|
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. */}
|
|
206
|
+
<>
|
|
128
207
|
{markers?.map(({ id, title, location: { lat, lng } }) => {
|
|
129
208
|
return (
|
|
130
209
|
<Marker
|
|
@@ -132,6 +211,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
132
211
|
position={{ lat, lng }}
|
|
133
212
|
icon={
|
|
134
213
|
// TODO(burdon): Create custom icon from bundled assets.
|
|
214
|
+
// TODO(burdon): Selection state.
|
|
135
215
|
new L.Icon({
|
|
136
216
|
iconUrl: 'https://dxos.network/marker-icon.png',
|
|
137
217
|
iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
|
|
@@ -147,9 +227,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
|
|
|
147
227
|
</Marker>
|
|
148
228
|
);
|
|
149
229
|
})}
|
|
150
|
-
|
|
230
|
+
</>
|
|
151
231
|
);
|
|
152
|
-
}
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
MapMarkers.displayName = 'Map.Markers';
|
|
153
235
|
|
|
154
236
|
//
|
|
155
237
|
// Controls
|
|
@@ -192,23 +274,28 @@ const CustomControl = ({
|
|
|
192
274
|
|
|
193
275
|
type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
|
|
194
276
|
|
|
277
|
+
const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
|
|
278
|
+
<CustomControl position={position} {...props}>
|
|
279
|
+
<ZoomControls onAction={onAction} />
|
|
280
|
+
</CustomControl>
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
|
|
284
|
+
<CustomControl position={position} {...props}>
|
|
285
|
+
<ActionControls onAction={onAction} />
|
|
286
|
+
</CustomControl>
|
|
287
|
+
);
|
|
288
|
+
|
|
195
289
|
//
|
|
196
290
|
// Map
|
|
197
291
|
//
|
|
198
292
|
|
|
199
293
|
export const Map = {
|
|
200
294
|
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
|
-
),
|
|
295
|
+
Tiles: MapTiles,
|
|
296
|
+
Markers: MapMarkers,
|
|
297
|
+
Zoom: MapZoom,
|
|
298
|
+
Action: MapAction,
|
|
212
299
|
};
|
|
213
300
|
|
|
214
|
-
export { type
|
|
301
|
+
export { type MapController, type MapRootProps, type MapTilesProps, type MapMarkersProps, type MapControlProps };
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
import { type ControlPosition } from 'leaflet';
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
-
import { IconButton, type ThemedClassName, Toolbar } from '@dxos/react-ui';
|
|
8
|
+
import { IconButton, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
|
|
9
|
+
|
|
10
|
+
import { translationKey } from '../../translations';
|
|
9
11
|
|
|
10
12
|
export type ControlAction = 'toggle' | 'start' | 'zoom-in' | 'zoom-out';
|
|
11
13
|
|
|
@@ -21,24 +23,20 @@ export const controlPositions: Record<ControlPosition, string> = {
|
|
|
21
23
|
};
|
|
22
24
|
|
|
23
25
|
export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
|
|
26
|
+
const { t } = useTranslation(translationKey);
|
|
27
|
+
|
|
24
28
|
return (
|
|
25
|
-
<Toolbar.Root classNames={['gap-
|
|
29
|
+
<Toolbar.Root classNames={['gap-2', classNames]}>
|
|
26
30
|
<IconButton
|
|
27
|
-
//
|
|
28
31
|
icon='ph--plus--regular'
|
|
29
|
-
label='zoom in'
|
|
30
32
|
iconOnly
|
|
31
|
-
|
|
32
|
-
classNames='px-0 aspect-square'
|
|
33
|
+
label={t('zoom in icon button')}
|
|
33
34
|
onClick={() => onAction?.('zoom-in')}
|
|
34
35
|
/>
|
|
35
36
|
<IconButton
|
|
36
|
-
//
|
|
37
37
|
icon='ph--minus--regular'
|
|
38
|
-
label='zoom out'
|
|
39
38
|
iconOnly
|
|
40
|
-
|
|
41
|
-
classNames='px-0 aspect-square'
|
|
39
|
+
label={t('zoom out icon button')}
|
|
42
40
|
onClick={() => onAction?.('zoom-out')}
|
|
43
41
|
/>
|
|
44
42
|
</Toolbar.Root>
|
|
@@ -46,24 +44,20 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
|
|
|
46
44
|
};
|
|
47
45
|
|
|
48
46
|
export const ActionControls = ({ classNames, onAction }: ControlProps) => {
|
|
47
|
+
const { t } = useTranslation(translationKey);
|
|
48
|
+
|
|
49
49
|
return (
|
|
50
|
-
<Toolbar.Root classNames={['gap-
|
|
50
|
+
<Toolbar.Root classNames={['gap-2', classNames]}>
|
|
51
51
|
<IconButton
|
|
52
|
-
|
|
53
|
-
icon='ph--play--regular'
|
|
54
|
-
label='start'
|
|
52
|
+
icon='ph--path--regular'
|
|
55
53
|
iconOnly
|
|
56
|
-
|
|
57
|
-
classNames='px-0 aspect-square'
|
|
54
|
+
label={t('start icon button')}
|
|
58
55
|
onClick={() => onAction?.('start')}
|
|
59
56
|
/>
|
|
60
57
|
<IconButton
|
|
61
|
-
//
|
|
62
58
|
icon='ph--globe-hemisphere-west--regular'
|
|
63
|
-
label='toggle'
|
|
64
59
|
iconOnly
|
|
65
|
-
|
|
66
|
-
classNames='px-0 aspect-square'
|
|
60
|
+
label={t('toggle icon button')}
|
|
67
61
|
onClick={() => onAction?.('toggle')}
|
|
68
62
|
/>
|
|
69
63
|
</Toolbar.Root>
|
package/src/components/index.ts
CHANGED
package/src/hooks/context.tsx
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { type Dispatch, type PropsWithChildren, type SetStateAction, createContext, useContext } from 'react';
|
|
6
6
|
|
|
7
7
|
import { raise } from '@dxos/debug';
|
|
8
8
|
import { useControlledState } from '@dxos/react-ui';
|
|
9
9
|
|
|
10
|
-
import { type
|
|
10
|
+
import { type LatLngLiteral } from '../types';
|
|
11
11
|
|
|
12
12
|
// TODO(burdon): Factor out common geometry types.
|
|
13
13
|
export type Size = { width: number; height: number };
|
|
@@ -16,38 +16,44 @@ export type Vector = [number, number, number];
|
|
|
16
16
|
|
|
17
17
|
export type GlobeContextType = {
|
|
18
18
|
size: Size;
|
|
19
|
-
center
|
|
20
|
-
|
|
19
|
+
center?: LatLngLiteral;
|
|
20
|
+
zoom: number;
|
|
21
21
|
translation: Point;
|
|
22
22
|
rotation: Vector;
|
|
23
|
-
setCenter: Dispatch<SetStateAction<
|
|
24
|
-
|
|
23
|
+
setCenter: Dispatch<SetStateAction<LatLngLiteral>>;
|
|
24
|
+
setZoom: Dispatch<SetStateAction<number>>;
|
|
25
25
|
setTranslation: Dispatch<SetStateAction<Point>>;
|
|
26
26
|
setRotation: Dispatch<SetStateAction<Vector>>;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const defaults = {
|
|
30
|
+
center: { lat: 51, lng: 0 } as LatLngLiteral,
|
|
31
|
+
zoom: 4,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
// TODO(burdon): Replace with radix.
|
|
29
35
|
const GlobeContext = createContext<GlobeContextType>(undefined);
|
|
30
36
|
|
|
31
37
|
export type GlobeContextProviderProps = PropsWithChildren<
|
|
32
|
-
Partial<Pick<GlobeContextType, 'size' | 'center' | '
|
|
38
|
+
Partial<Pick<GlobeContextType, 'size' | 'center' | 'zoom' | 'translation' | 'rotation'>>
|
|
33
39
|
>;
|
|
34
40
|
|
|
35
41
|
export const GlobeContextProvider = ({
|
|
36
42
|
children,
|
|
37
43
|
size,
|
|
38
|
-
center:
|
|
39
|
-
|
|
40
|
-
translation:
|
|
41
|
-
rotation:
|
|
44
|
+
center: centerProp = defaults.center,
|
|
45
|
+
zoom: zoomProp = defaults.zoom,
|
|
46
|
+
translation: translationProp,
|
|
47
|
+
rotation: rotationProp,
|
|
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(centerProp);
|
|
50
|
+
const [zoom, setZoom] = useControlledState(zoomProp);
|
|
51
|
+
const [translation, setTranslation] = useControlledState<Point>(translationProp);
|
|
52
|
+
const [rotation, setRotation] = useControlledState<Vector>(rotationProp);
|
|
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;
|