@dxos/react-ui-geo 0.7.5-labs.ea4b4c2 → 0.7.5-labs.f400bbc

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.
Files changed (38) hide show
  1. package/dist/lib/browser/index.mjs +113 -48
  2. package/dist/lib/browser/index.mjs.map +3 -3
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +111 -46
  5. package/dist/lib/node/index.cjs.map +3 -3
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +113 -48
  8. package/dist/lib/node-esm/index.mjs.map +3 -3
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/components/Globe/Globe.d.ts +5 -5
  11. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  12. package/dist/types/src/components/Globe/Globe.stories.d.ts +9 -10
  13. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  14. package/dist/types/src/components/Map/Map.d.ts +7 -7
  15. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  16. package/dist/types/src/components/Map/Map.stories.d.ts +6 -2
  17. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  18. package/dist/types/src/components/Toolbar/Controls.d.ts +2 -3
  19. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  20. package/dist/types/src/components/types.d.ts +2 -1
  21. package/dist/types/src/components/types.d.ts.map +1 -1
  22. package/dist/types/src/hooks/context.d.ts +2 -2
  23. package/dist/types/src/hooks/context.d.ts.map +1 -1
  24. package/dist/types/src/hooks/useTour.d.ts +8 -3
  25. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  26. package/dist/types/src/util/debug.d.ts.map +1 -1
  27. package/dist/types/src/util/render.d.ts +2 -2
  28. package/dist/types/src/util/render.d.ts.map +1 -1
  29. package/package.json +12 -10
  30. package/src/components/Globe/Globe.stories.tsx +8 -11
  31. package/src/components/Globe/Globe.tsx +56 -32
  32. package/src/components/Map/Map.stories.tsx +28 -6
  33. package/src/components/Map/Map.tsx +93 -82
  34. package/src/components/types.ts +2 -1
  35. package/src/hooks/useDrag.ts +1 -1
  36. package/src/hooks/useTour.ts +31 -19
  37. package/src/util/debug.ts +1 -0
  38. package/src/util/render.ts +18 -4
@@ -11,29 +11,51 @@ import { withLayout, withTheme } from '@dxos/storybook-utils';
11
11
 
12
12
  import { Map, type MapController } from './Map';
13
13
  import { useMapZoomHandler } from '../../hooks';
14
+ import { type MapMarker } from '../../types';
14
15
 
15
- const Render = () => {
16
+ const Render = ({ markers }) => {
16
17
  const [controller, setController] = useState<MapController>();
17
18
  const handleZoomAction = useMapZoomHandler(controller);
18
19
 
19
20
  return (
20
21
  <Map.Root>
21
- <Map.Canvas ref={setController} />
22
+ <Map.Canvas ref={setController} markers={markers} />
22
23
  <Map.Zoom position='bottomleft' onAction={handleZoomAction} />
23
24
  <Map.Action position='bottomright' />
24
25
  </Map.Root>
25
26
  );
26
27
  };
27
28
 
28
- const meta: Meta = {
29
+ const meta: Meta<typeof Render> = {
29
30
  title: 'ui/react-ui-geo/Map',
30
- component: Map.Root,
31
- render: Render,
31
+ component: Render,
32
32
  decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
33
33
  };
34
34
 
35
35
  export default meta;
36
36
 
37
- type Story = StoryObj;
37
+ type Story = StoryObj<typeof Render>;
38
38
 
39
39
  export const Default: Story = {};
40
+
41
+ export const WithMarkers: Story = {
42
+ args: {
43
+ markers: [
44
+ { id: 'tokyo', title: 'Tokyo', location: { lat: 35.6762, lng: 139.6503 } },
45
+ { id: 'sydney', title: 'Sydney', location: { lat: -33.8688, lng: 151.2093 } },
46
+ { id: 'auckland', title: 'Auckland', location: { lat: -36.8509, lng: 174.7645 } },
47
+ { id: 'new-delhi', title: 'New Delhi', location: { lat: 28.6139, lng: 77.209 } },
48
+ { id: 'manila', title: 'Manila', location: { lat: 14.5995, lng: 120.9842 } },
49
+ { id: 'beijing', title: 'Beijing', location: { lat: 39.9042, lng: 116.4074 } },
50
+ { id: 'seoul', title: 'Seoul', location: { lat: 37.5665, lng: 126.978 } },
51
+ { id: 'bangkok', title: 'Bangkok', location: { lat: 13.7563, lng: 100.5018 } },
52
+ { id: 'singapore', title: 'Singapore', location: { lat: 1.3521, lng: 103.8198 } },
53
+ { id: 'kuala-lumpur', title: 'Kuala Lumpur', location: { lat: 3.139, lng: 101.6869 } },
54
+ { id: 'jakarta', title: 'Jakarta', location: { lat: -6.2088, lng: 106.8456 } },
55
+ { id: 'hanoi', title: 'Hanoi', location: { lat: 21.0285, lng: 105.8542 } },
56
+ { id: 'phnom-penh', title: 'Phnom Penh', location: { lat: 11.5564, lng: 104.9282 } },
57
+ { id: 'vientiane', title: 'Vientiane', location: { lat: 17.9757, lng: 102.6331 } },
58
+ { id: 'yangon', title: 'Yangon', location: { lat: 16.8661, lng: 96.1951 } },
59
+ ] as MapMarker[],
60
+ },
61
+ };
@@ -5,16 +5,15 @@
5
5
  // eslint-disable-next-line no-restricted-imports
6
6
  import 'leaflet/dist/leaflet.css';
7
7
 
8
- import type L from 'leaflet';
9
- import { type ControlPosition, Control, DomEvent, DomUtil, type LatLngExpression, latLngBounds } from 'leaflet';
10
- import React, { forwardRef, type PropsWithChildren, useEffect, useImperativeHandle } from 'react';
8
+ import L, { Control, DomEvent, DomUtil, latLngBounds, type ControlPosition, type LatLngExpression } from 'leaflet';
9
+ import React, { forwardRef, useEffect, useImperativeHandle, type PropsWithChildren } from 'react';
11
10
  import { createRoot } from 'react-dom/client';
11
+ import type { MapContainerProps } from 'react-leaflet';
12
12
  import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet';
13
- import { type MapContainerProps } from 'react-leaflet/lib/MapContainer';
14
13
  import { useResizeDetector } from 'react-resize-detector';
15
14
 
16
15
  import { debounce } from '@dxos/async';
17
- import { Tooltip, ThemeProvider, type ThemedClassName } from '@dxos/react-ui';
16
+ import { ThemeProvider, Tooltip, type ThemedClassName } from '@dxos/react-ui';
18
17
  import { defaultTx, mx } from '@dxos/react-ui-theme';
19
18
 
20
19
  import { ActionControls, controlPositions, ZoomControls, type ControlProps } from '../Toolbar';
@@ -61,84 +60,96 @@ type MapController = {
61
60
  setZoom: (cb: (zoom: number) => number) => void;
62
61
  };
63
62
 
64
- const MapCanvas = forwardRef<MapController, MapCanvasProps>(
65
- ({ markers = [], center, zoom, onChange }, forwardedRef) => {
66
- const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
67
- const map = useMap();
68
-
69
- useImperativeHandle(
70
- forwardedRef,
71
- () => ({
72
- setCenter: (center: LatLngExpression, zoom?: number) => {
73
- map.setView(center, zoom);
74
- },
75
- setZoom: (cb) => {
76
- map.setZoom(cb(map.getZoom()));
77
- },
78
- }),
79
- [map],
80
- );
81
-
82
- // Resize.
83
- useEffect(() => {
84
- if (width && height) {
85
- map.invalidateSize();
86
- }
87
- }, [width, height]);
88
-
89
- // Position.
90
- useEffect(() => {
91
- if (center) {
63
+ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center, zoom, onChange }, forwardedRef) => {
64
+ const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
65
+ const map = useMap();
66
+
67
+ useImperativeHandle(
68
+ forwardedRef,
69
+ () => ({
70
+ setCenter: (center: LatLngExpression, zoom?: number) => {
92
71
  map.setView(center, zoom);
93
- } else if (zoom !== undefined) {
94
- map.setZoom(zoom);
95
- }
96
- }, [center, zoom]);
97
-
98
- // Events.
99
- useEffect(() => {
100
- const handler = debounce(() => {
101
- onChange?.({ center: map.getCenter(), zoom: map.getZoom() });
102
- }, 100);
103
- map.on('move', handler);
104
- map.on('zoom', handler);
105
- return () => {
106
- map.off('move', handler);
107
- map.off('zoom', handler);
108
- };
109
- }, [map, onChange]);
110
-
111
- // Set the viewport around the markers, or show the whole world map if `markers` is empty.
112
- useEffect(() => {
113
- if (markers.length > 0) {
114
- const bounds = latLngBounds(markers.map((marker) => marker.location));
115
- map.fitBounds(bounds);
116
- } else {
117
- map.setView(defaults.center, defaults.zoom);
118
- }
119
- }, [markers]);
120
-
121
- return (
122
- <div ref={ref} className='flex w-full h-full overflow-hidden bg-baseSurface'>
123
- {/* Map tiles. */}
124
- <TileLayer
125
- className='dark:filter dark:grayscale dark:invert'
126
- url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
127
- />
128
-
129
- {/* Markers. */}
130
- {/* TODO(burdon): Marker icon doesn't load on mobile? */}
131
- {markers.map(({ id, title, location: { lat, lng } }) => {
132
- return (
133
- <Marker key={id} position={{ lat, lng }}>
134
- {title && <Popup>{title}</Popup>}
135
- </Marker>
136
- );
137
- })}
138
- </div>
139
- );
140
- },
141
- );
72
+ },
73
+ setZoom: (cb) => {
74
+ map.setZoom(cb(map.getZoom()));
75
+ },
76
+ }),
77
+ [map],
78
+ );
79
+
80
+ // Resize.
81
+ useEffect(() => {
82
+ if (width && height) {
83
+ map.invalidateSize();
84
+ }
85
+ }, [width, height]);
86
+
87
+ // Position.
88
+ useEffect(() => {
89
+ if (center) {
90
+ map.setView(center, zoom);
91
+ } else if (zoom !== undefined) {
92
+ map.setZoom(zoom);
93
+ }
94
+ }, [center, zoom]);
95
+
96
+ // Events.
97
+ useEffect(() => {
98
+ const handler = debounce(() => {
99
+ onChange?.({ center: map.getCenter(), zoom: map.getZoom() });
100
+ }, 100);
101
+ map.on('move', handler);
102
+ map.on('zoom', handler);
103
+ return () => {
104
+ map.off('move', handler);
105
+ map.off('zoom', handler);
106
+ };
107
+ }, [map, onChange]);
108
+
109
+ // Set the viewport around the markers, or show the whole world map if `markers` is empty.
110
+ useEffect(() => {
111
+ if (markers.length > 0) {
112
+ const bounds = latLngBounds(markers.map((marker) => marker.location));
113
+ map.fitBounds(bounds);
114
+ } else {
115
+ map.setView(defaults.center, defaults.zoom);
116
+ }
117
+ }, [markers]);
118
+
119
+ return (
120
+ <div ref={ref} className='flex w-full h-full overflow-hidden bg-baseSurface'>
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. */}
128
+ {markers?.map(({ id, title, location: { lat, lng } }) => {
129
+ return (
130
+ <Marker
131
+ key={id}
132
+ position={{ lat, lng }}
133
+ icon={
134
+ // TODO(burdon): Create custom icon from bundled assets.
135
+ new L.Icon({
136
+ iconUrl: 'https://dxos.network/marker-icon.png',
137
+ iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
138
+ shadowUrl: 'https://dxos.network/marker-shadow.png',
139
+ iconSize: [25, 41],
140
+ iconAnchor: [12, 41],
141
+ popupAnchor: [1, -34],
142
+ shadowSize: [41, 41],
143
+ })
144
+ }
145
+ >
146
+ {title && <Popup>{title}</Popup>}
147
+ </Marker>
148
+ );
149
+ })}
150
+ </div>
151
+ );
152
+ });
142
153
 
143
154
  //
144
155
  // Controls
@@ -8,10 +8,11 @@ import { type ThemedClassName } from '@dxos/react-ui';
8
8
 
9
9
  import { type MapMarker } from '../types';
10
10
 
11
- export type { LatLngLiteral };
11
+ export { type LatLngLiteral };
12
12
 
13
13
  export type MapCanvasProps = ThemedClassName<{
14
14
  markers?: MapMarker[];
15
+ selected?: string[];
15
16
  zoom?: number;
16
17
  center?: LatLngLiteral;
17
18
  onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
@@ -49,7 +49,7 @@ export const useDrag = (controller?: GlobeController | null, options: DragOption
49
49
  return () => {
50
50
  cancelDrag(d3.select(canvas));
51
51
  };
52
- }, [controller, options]);
52
+ }, [controller, JSON.stringify(options)]);
53
53
  };
54
54
 
55
55
  const cancelDrag = (node) => node.on('.drag', null);
@@ -3,29 +3,39 @@
3
3
  //
4
4
 
5
5
  import * as d3 from 'd3';
6
- import { useEffect, useState } from 'react';
6
+ import { type Dispatch, type SetStateAction, useEffect, useState } from 'react';
7
7
  import versor from 'versor';
8
8
 
9
+ import { log } from '@dxos/log';
10
+
9
11
  import type { GlobeController } from '../components';
10
- import { type Features, geoToPosition, type LatLng, positionToRotation, type StyleSet } from '../util';
12
+ import { geoToPosition, type LatLng, positionToRotation, type StyleSet } from '../util';
11
13
 
12
14
  const TRANSITION_NAME = 'globe-tour';
13
15
 
14
16
  const defaultDuration = 1_500;
15
17
 
16
18
  export type TourOptions = {
19
+ running?: boolean;
17
20
  disabled?: boolean;
18
- styles?: StyleSet;
19
21
  duration?: number;
22
+ loop?: boolean;
23
+ tilt?: number;
24
+ autoRotate?: boolean;
25
+ styles?: StyleSet;
20
26
  };
21
27
 
22
28
  /**
23
29
  * Iterates between points.
24
30
  * Inspired by: https://observablehq.com/@mbostock/top-100-cities
25
31
  */
26
- export const useTour = (controller?: GlobeController | null, features?: Features, options: TourOptions = {}) => {
32
+ export const useTour = (
33
+ controller?: GlobeController | null,
34
+ points?: LatLng[],
35
+ options: TourOptions = {},
36
+ ): [boolean, Dispatch<SetStateAction<boolean>>] => {
27
37
  const selection = d3.selection();
28
- const [running, setRunning] = useState(false);
38
+ const [running, setRunning] = useState(options.running ?? false);
29
39
  useEffect(() => {
30
40
  if (!running) {
31
41
  selection.interrupt(TRANSITION_NAME);
@@ -39,10 +49,15 @@ export const useTour = (controller?: GlobeController | null, features?: Features
39
49
  const context = canvas.getContext('2d', { alpha: false });
40
50
  const path = d3.geoPath(projection, context).pointRadius(2);
41
51
 
42
- const tilt = 0;
52
+ const tilt = options.tilt ?? 0;
43
53
  let last: LatLng;
44
54
  try {
45
- for (const next of features.points) {
55
+ const p = [...points];
56
+ if (options.loop) {
57
+ p.push(p[0]);
58
+ }
59
+
60
+ for (const next of p) {
46
61
  if (!running) {
47
62
  break;
48
63
  }
@@ -83,8 +98,10 @@ export const useTour = (controller?: GlobeController | null, features?: Features
83
98
  context.restore();
84
99
 
85
100
  // TODO(burdon): This has to come after rendering above. Add to features to correct order?
86
- projection.rotate(iv(t));
87
- setRotation(projection.rotate());
101
+ if (options.autoRotate) {
102
+ projection.rotate(iv(t));
103
+ setRotation(projection.rotate());
104
+ }
88
105
  });
89
106
 
90
107
  // Throws if interrupted.
@@ -92,6 +109,8 @@ export const useTour = (controller?: GlobeController | null, features?: Features
92
109
  last = next;
93
110
  }
94
111
  } catch (err) {
112
+ log.catch(err);
113
+ } finally {
95
114
  setRunning(false);
96
115
  }
97
116
  });
@@ -101,14 +120,7 @@ export const useTour = (controller?: GlobeController | null, features?: Features
101
120
  selection.interrupt(TRANSITION_NAME);
102
121
  };
103
122
  }
104
- }, [controller, running]);
105
-
106
- return [
107
- () => {
108
- if (!options.disabled) {
109
- setRunning(true);
110
- }
111
- },
112
- () => setRunning(false),
113
- ];
123
+ }, [controller, running, JSON.stringify(options)]);
124
+
125
+ return [running, setRunning];
114
126
  };
package/src/util/debug.ts CHANGED
@@ -12,5 +12,6 @@ export const timer = <T = void>(cb: () => T): T => {
12
12
  // eslint-disable-next-line no-console
13
13
  console.log({ t, data });
14
14
  }
15
+
15
16
  return data;
16
17
  };
@@ -11,7 +11,17 @@ import { type LatLng, geoLine, geoPoint } from './path';
11
11
 
12
12
  export type Styles = Record<string, any>;
13
13
 
14
- export type Style = 'water' | 'graticule' | 'land' | 'border' | 'dots' | 'point' | 'line' | 'cursor' | 'arc';
14
+ export type Style =
15
+ | 'background'
16
+ | 'water'
17
+ | 'graticule'
18
+ | 'land'
19
+ | 'border'
20
+ | 'dots'
21
+ | 'point'
22
+ | 'line'
23
+ | 'cursor'
24
+ | 'arc';
15
25
 
16
26
  export type StyleSet = Partial<Record<Style, Styles>>;
17
27
 
@@ -108,16 +118,20 @@ export const createLayers = (topology: Topology, features: Features, styles: Sty
108
118
  /**
109
119
  * Render layers created above.
110
120
  */
111
- export const renderLayers = (generator: GeoPath, layers: Layer[] = [], scale: number) => {
121
+ export const renderLayers = (generator: GeoPath, layers: Layer[] = [], scale: number, styles: StyleSet) => {
112
122
  const context: CanvasRenderingContext2D = generator.context();
113
123
  const {
114
124
  canvas: { width, height },
115
125
  } = context;
116
126
  context.reset();
117
127
 
118
- // TODO(burdon): Option.
119
128
  // Clear background.
120
- context.clearRect(0, 0, width, height);
129
+ if (styles.background) {
130
+ context.fillStyle = styles.background.fillStyle;
131
+ context.fillRect(0, 0, width, height);
132
+ } else {
133
+ context.clearRect(0, 0, width, height);
134
+ }
121
135
 
122
136
  // Render features.
123
137
  // https://github.com/d3/d3-geo#_path