@dxos/react-ui-geo 0.7.5-labs.35b4b42
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 +8 -0
- package/README.md +3 -0
- package/data/airports.ts +52524 -0
- package/data/cities.ts +1211 -0
- package/data/countries-110m.ts +10587 -0
- package/data/countries-dots-3.ts +42989 -0
- package/data/countries-dots-4.ts +300941 -0
- package/data/raw/airports.json +39386 -0
- package/data/raw/countries-10m.json +1 -0
- package/data/raw/countries-110m.json +1 -0
- package/data/raw/countries-50m.json +1 -0
- package/data/raw/countries.json +1 -0
- package/dist/lib/browser/chunk-ENCWOTYX.mjs +9 -0
- package/dist/lib/browser/chunk-ENCWOTYX.mjs.map +7 -0
- package/dist/lib/browser/countries-110m-WI4PCLDF.mjs +37859 -0
- package/dist/lib/browser/countries-110m-WI4PCLDF.mjs.map +7 -0
- package/dist/lib/browser/data.mjs +7 -0
- package/dist/lib/browser/data.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1020 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/node/chunk-LAICG6L2.cjs +40 -0
- package/dist/lib/node/chunk-LAICG6L2.cjs.map +7 -0
- package/dist/lib/node/countries-110m-KQ5WAB2O.cjs +37877 -0
- package/dist/lib/node/countries-110m-KQ5WAB2O.cjs.map +7 -0
- package/dist/lib/node/data.cjs +28 -0
- package/dist/lib/node/data.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1045 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node-esm/chunk-PIIEDZEU.mjs +11 -0
- package/dist/lib/node-esm/chunk-PIIEDZEU.mjs.map +7 -0
- package/dist/lib/node-esm/countries-110m-DQ4XRC4B.mjs +37861 -0
- package/dist/lib/node-esm/countries-110m-DQ4XRC4B.mjs.map +7 -0
- package/dist/lib/node-esm/data.mjs +8 -0
- package/dist/lib/node-esm/data.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1021 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/types/data/airports.d.ts +18 -0
- package/dist/types/data/airports.d.ts.map +1 -0
- package/dist/types/data/cities.d.ts +17 -0
- package/dist/types/data/cities.d.ts.map +1 -0
- package/dist/types/data/countries-110m.d.ts +36 -0
- package/dist/types/data/countries-110m.d.ts.map +1 -0
- package/dist/types/data/countries-dots-3.d.ts +9 -0
- package/dist/types/data/countries-dots-3.d.ts.map +1 -0
- package/dist/types/data/countries-dots-4.d.ts +9 -0
- package/dist/types/data/countries-dots-4.d.ts.map +1 -0
- package/dist/types/src/components/Globe/Globe.d.ts +37 -0
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -0
- package/dist/types/src/components/Globe/Globe.stories.d.ts +15 -0
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -0
- package/dist/types/src/components/Globe/index.d.ts +2 -0
- package/dist/types/src/components/Globe/index.d.ts.map +1 -0
- package/dist/types/src/components/Map/Map.d.ts +34 -0
- package/dist/types/src/components/Map/Map.d.ts.map +1 -0
- package/dist/types/src/components/Map/Map.stories.d.ts +7 -0
- package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -0
- package/dist/types/src/components/Map/index.d.ts +2 -0
- package/dist/types/src/components/Map/index.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/Controls.d.ts +11 -0
- package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -0
- package/dist/types/src/components/Toolbar/index.d.ts +2 -0
- package/dist/types/src/components/Toolbar/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +5 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/components/types.d.ts +14 -0
- package/dist/types/src/components/types.d.ts.map +1 -0
- package/dist/types/src/data.d.ts +3 -0
- package/dist/types/src/data.d.ts.map +1 -0
- package/dist/types/src/hooks/context.d.ts +26 -0
- package/dist/types/src/hooks/context.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +7 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useDrag.d.ts +16 -0
- package/dist/types/src/hooks/useDrag.d.ts.map +1 -0
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +3 -0
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useMapZoomHandler.d.ts +3 -0
- package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -0
- package/dist/types/src/hooks/useSpinner.d.ts +11 -0
- package/dist/types/src/hooks/useSpinner.d.ts.map +1 -0
- package/dist/types/src/hooks/useTour.d.ts +13 -0
- package/dist/types/src/hooks/useTour.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +7 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/util/debug.d.ts +2 -0
- package/dist/types/src/util/debug.d.ts.map +1 -0
- package/dist/types/src/util/index.d.ts +5 -0
- package/dist/types/src/util/index.d.ts.map +1 -0
- package/dist/types/src/util/inertia.d.ts +16 -0
- package/dist/types/src/util/inertia.d.ts.map +1 -0
- package/dist/types/src/util/path.d.ts +15 -0
- package/dist/types/src/util/path.d.ts.map +1 -0
- package/dist/types/src/util/render.d.ts +26 -0
- package/dist/types/src/util/render.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +83 -0
- package/src/components/Globe/Globe.stories.tsx +332 -0
- package/src/components/Globe/Globe.tsx +270 -0
- package/src/components/Globe/index.ts +5 -0
- package/src/components/Map/Map.stories.tsx +39 -0
- package/src/components/Map/Map.tsx +203 -0
- package/src/components/Map/index.ts +5 -0
- package/src/components/Toolbar/Controls.tsx +71 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +9 -0
- package/src/components/types.ts +18 -0
- package/src/data.ts +9 -0
- package/src/hooks/context.tsx +59 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useDrag.ts +55 -0
- package/src/hooks/useGlobeZoomHandler.ts +29 -0
- package/src/hooks/useMapZoomHandler.ts +29 -0
- package/src/hooks/useSpinner.ts +69 -0
- package/src/hooks/useTour.ts +114 -0
- package/src/index.ts +9 -0
- package/src/types.ts +11 -0
- package/src/util/debug.ts +16 -0
- package/src/util/index.ts +8 -0
- package/src/util/inertia.ts +197 -0
- package/src/util/path.ts +56 -0
- package/src/util/render.ts +149 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import { useEffect } from 'react';
|
|
7
|
+
|
|
8
|
+
import { type GlobeController } from '../components';
|
|
9
|
+
import { geoInertiaDrag } from '../util';
|
|
10
|
+
|
|
11
|
+
export type GlobeDragEvent = {
|
|
12
|
+
type: 'start' | 'move' | 'end';
|
|
13
|
+
controller: GlobeController;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type DragOptions = {
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
duration?: number;
|
|
19
|
+
xAxis?: boolean; // TODO(burdon): Generalize.
|
|
20
|
+
onUpdate?: (event: GlobeDragEvent) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Allows user to drag globe.
|
|
25
|
+
*/
|
|
26
|
+
export const useDrag = (controller?: GlobeController | null, options: DragOptions = {}) => {
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const canvas = controller?.canvas;
|
|
29
|
+
if (!canvas || options.disabled) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
geoInertiaDrag(
|
|
34
|
+
d3.select(canvas),
|
|
35
|
+
() => {
|
|
36
|
+
controller.setRotation(controller.projection.rotate());
|
|
37
|
+
options.onUpdate?.({ type: 'move', controller });
|
|
38
|
+
},
|
|
39
|
+
controller.projection,
|
|
40
|
+
{
|
|
41
|
+
xAxis: options.xAxis,
|
|
42
|
+
time: 3_000,
|
|
43
|
+
start: () => options.onUpdate?.({ type: 'start', controller }),
|
|
44
|
+
finish: () => options.onUpdate?.({ type: 'end', controller }),
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// TODO(burdon): Cancel drag timer.
|
|
49
|
+
return () => {
|
|
50
|
+
cancelDrag(d3.select(canvas));
|
|
51
|
+
};
|
|
52
|
+
}, [controller, options]);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const cancelDrag = (node) => node.on('.drag', null);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type GlobeController, type ControlProps } from '../components';
|
|
8
|
+
|
|
9
|
+
export const useGlobeZoomHandler = (controller: GlobeController | null | undefined): ControlProps['onAction'] => {
|
|
10
|
+
return useCallback<ControlProps['onAction']>(
|
|
11
|
+
(event) => {
|
|
12
|
+
if (!controller) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
switch (event) {
|
|
17
|
+
case 'zoom-in': {
|
|
18
|
+
controller.setScale((scale) => scale * 1.1);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'zoom-out': {
|
|
22
|
+
controller.setScale((scale) => scale * 0.9);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
[controller],
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type MapController, type ControlProps } from '../components';
|
|
8
|
+
|
|
9
|
+
export const useMapZoomHandler = (controller: MapController | null | undefined): ControlProps['onAction'] => {
|
|
10
|
+
return useCallback<ControlProps['onAction']>(
|
|
11
|
+
(event) => {
|
|
12
|
+
if (!controller) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
switch (event) {
|
|
17
|
+
case 'zoom-in': {
|
|
18
|
+
controller.setZoom((scale) => scale + 1);
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
case 'zoom-out': {
|
|
22
|
+
controller.setZoom((scale) => scale - 1);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
[controller],
|
|
28
|
+
);
|
|
29
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import { type Timer } from 'd3';
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
|
|
9
|
+
import { type Vector } from './context';
|
|
10
|
+
import { type GlobeController } from '../components';
|
|
11
|
+
|
|
12
|
+
export type SpinnerOptions = {
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
delta?: Vector;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rotates globe.
|
|
19
|
+
*/
|
|
20
|
+
export const useSpinner = (controller?: GlobeController | null, options: SpinnerOptions = {}) => {
|
|
21
|
+
const [running, setRunning] = useState(false);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let timer: Timer | undefined;
|
|
24
|
+
|
|
25
|
+
const start = () => {
|
|
26
|
+
const delta: Vector = options.delta ?? [0.001, 0, 0];
|
|
27
|
+
|
|
28
|
+
let t = 0;
|
|
29
|
+
let lastRotation = controller.projection.rotate();
|
|
30
|
+
timer = d3.timer((elapsed) => {
|
|
31
|
+
const dt = elapsed - t;
|
|
32
|
+
t = elapsed;
|
|
33
|
+
|
|
34
|
+
const rotation: Vector = [
|
|
35
|
+
lastRotation[0] + delta[0] * dt,
|
|
36
|
+
lastRotation[1] + delta[1] * dt,
|
|
37
|
+
lastRotation[2] + delta[2] * dt,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
lastRotation = rotation;
|
|
41
|
+
controller.setRotation(rotation);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const stop = () => {
|
|
46
|
+
if (timer) {
|
|
47
|
+
timer.stop();
|
|
48
|
+
timer = undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (controller && running) {
|
|
53
|
+
start();
|
|
54
|
+
} else {
|
|
55
|
+
stop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return () => stop();
|
|
59
|
+
}, [controller, running]);
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
() => {
|
|
63
|
+
if (!options.disabled) {
|
|
64
|
+
setRunning(true);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
() => setRunning(false),
|
|
68
|
+
];
|
|
69
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import { useEffect, useState } from 'react';
|
|
7
|
+
import versor from 'versor';
|
|
8
|
+
|
|
9
|
+
import type { GlobeController } from '../components';
|
|
10
|
+
import { type Features, geoToPosition, type LatLng, positionToRotation, type StyleSet } from '../util';
|
|
11
|
+
|
|
12
|
+
const TRANSITION_NAME = 'globe-tour';
|
|
13
|
+
|
|
14
|
+
const defaultDuration = 1_500;
|
|
15
|
+
|
|
16
|
+
export type TourOptions = {
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
styles?: StyleSet;
|
|
19
|
+
duration?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Iterates between points.
|
|
24
|
+
* Inspired by: https://observablehq.com/@mbostock/top-100-cities
|
|
25
|
+
*/
|
|
26
|
+
export const useTour = (controller?: GlobeController | null, features?: Features, options: TourOptions = {}) => {
|
|
27
|
+
const selection = d3.selection();
|
|
28
|
+
const [running, setRunning] = useState(false);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (!running) {
|
|
31
|
+
selection.interrupt(TRANSITION_NAME);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let t: ReturnType<typeof setTimeout>;
|
|
36
|
+
if (controller && running) {
|
|
37
|
+
t = setTimeout(async () => {
|
|
38
|
+
const { canvas, projection, setRotation } = controller;
|
|
39
|
+
const context = canvas.getContext('2d', { alpha: false });
|
|
40
|
+
const path = d3.geoPath(projection, context).pointRadius(2);
|
|
41
|
+
|
|
42
|
+
const tilt = 0;
|
|
43
|
+
let last: LatLng;
|
|
44
|
+
try {
|
|
45
|
+
for (const next of features.points) {
|
|
46
|
+
if (!running) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Points.
|
|
51
|
+
const p1 = last ? geoToPosition(last) : undefined;
|
|
52
|
+
const p2 = geoToPosition(next);
|
|
53
|
+
const ip = d3.geoInterpolate(p1 || p2, p2);
|
|
54
|
+
const distance = d3.geoDistance(p1 || p2, p2);
|
|
55
|
+
|
|
56
|
+
// Rotation.
|
|
57
|
+
const r1 = p1 ? positionToRotation(p1, tilt) : controller.projection.rotate();
|
|
58
|
+
const r2 = positionToRotation(p2, tilt);
|
|
59
|
+
const iv = versor.interpolate(r1, r2);
|
|
60
|
+
|
|
61
|
+
const transition = selection
|
|
62
|
+
.transition(TRANSITION_NAME)
|
|
63
|
+
.duration(Math.max(options.duration ?? defaultDuration, distance * 2_000))
|
|
64
|
+
.tween('render', () => (t) => {
|
|
65
|
+
const t1 = Math.max(0, Math.min(1, t * 2 - 1));
|
|
66
|
+
const t2 = Math.min(1, t * 2);
|
|
67
|
+
|
|
68
|
+
context.save();
|
|
69
|
+
{
|
|
70
|
+
context.beginPath();
|
|
71
|
+
context.strokeStyle = options?.styles?.arc?.strokeStyle ?? 'yellow';
|
|
72
|
+
context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.scale ?? 1);
|
|
73
|
+
context.setLineDash(options?.styles?.arc?.lineDash ?? []);
|
|
74
|
+
path({ type: 'LineString', coordinates: [ip(t1), ip(t2)] });
|
|
75
|
+
context.stroke();
|
|
76
|
+
|
|
77
|
+
context.beginPath();
|
|
78
|
+
context.fillStyle = options?.styles?.cursor?.fillStyle ?? 'orange';
|
|
79
|
+
path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.scale ?? 1));
|
|
80
|
+
path({ type: 'Point', coordinates: ip(t2) });
|
|
81
|
+
context.fill();
|
|
82
|
+
}
|
|
83
|
+
context.restore();
|
|
84
|
+
|
|
85
|
+
// TODO(burdon): This has to come after rendering above. Add to features to correct order?
|
|
86
|
+
projection.rotate(iv(t));
|
|
87
|
+
setRotation(projection.rotate());
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Throws if interrupted.
|
|
91
|
+
await transition.end();
|
|
92
|
+
last = next;
|
|
93
|
+
}
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setRunning(false);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return () => {
|
|
100
|
+
clearTimeout(t);
|
|
101
|
+
selection.interrupt(TRANSITION_NAME);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}, [controller, running]);
|
|
105
|
+
|
|
106
|
+
return [
|
|
107
|
+
() => {
|
|
108
|
+
if (!options.disabled) {
|
|
109
|
+
setRunning(true);
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
() => setRunning(false),
|
|
113
|
+
];
|
|
114
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
const debug = false;
|
|
6
|
+
|
|
7
|
+
export const timer = <T = void>(cb: () => T): T => {
|
|
8
|
+
const start = Date.now();
|
|
9
|
+
const data = cb();
|
|
10
|
+
const t = Date.now() - start / 1_000;
|
|
11
|
+
if (debug) {
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.log({ t, data });
|
|
14
|
+
}
|
|
15
|
+
return data;
|
|
16
|
+
};
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2017 Philippe Rivière
|
|
3
|
+
// Copyright 2024 DXOS.org
|
|
4
|
+
// https://github.com/Fil/d3-inertia
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
import * as d3 from 'd3';
|
|
8
|
+
import versor from 'versor';
|
|
9
|
+
|
|
10
|
+
export const restrictAxis =
|
|
11
|
+
(axis: boolean[]) =>
|
|
12
|
+
(original: number[], current: number[]): number[] =>
|
|
13
|
+
current.map((d, i) => (axis[i] ? d : original[i]));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Applies a drag handler to the specified target element.
|
|
17
|
+
*/
|
|
18
|
+
// TODO(burdon): Define type.
|
|
19
|
+
export const geoInertiaDrag = (target, render, projection, options) => {
|
|
20
|
+
if (!options) {
|
|
21
|
+
options = {};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Target can be an element, a selector, a function, or a selection
|
|
25
|
+
// but in case of a selection we make sure to reselect it with d3-selection.
|
|
26
|
+
if (target.node) {
|
|
27
|
+
target = target.node();
|
|
28
|
+
}
|
|
29
|
+
target = d3.select(target);
|
|
30
|
+
|
|
31
|
+
// Complete params: (projection, render, startDrag, dragging, endDrag).
|
|
32
|
+
const inertia = geoInertiaDragHelper({
|
|
33
|
+
projection,
|
|
34
|
+
render: (rotation) => {
|
|
35
|
+
projection.rotate(rotation);
|
|
36
|
+
render && render();
|
|
37
|
+
},
|
|
38
|
+
axis: restrictAxis(options.xAxis ? [true, false, false] : [true, true, true]),
|
|
39
|
+
start: options.start,
|
|
40
|
+
move: options.move,
|
|
41
|
+
end: options.end,
|
|
42
|
+
stop: options.stop,
|
|
43
|
+
finish: options.finish,
|
|
44
|
+
time: options.time,
|
|
45
|
+
hold: options.hold,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
target.call(d3.drag().on('start', inertia.start).on('drag', inertia.move).on('end', inertia.end));
|
|
49
|
+
return inertia;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A versor is a compact way to describe a rotation in 3D space.
|
|
54
|
+
* It consists of four components [𝑤,x,y,z], where:
|
|
55
|
+
* 𝑤 is a scalar representing the angle of rotation.
|
|
56
|
+
* x, y, z are the vector components, representing the axis of rotation.
|
|
57
|
+
*/
|
|
58
|
+
const geoInertiaDragHelper = (opt) => {
|
|
59
|
+
const projection = opt.projection;
|
|
60
|
+
|
|
61
|
+
let v0; // Mouse position in Cartesian coordinates at start of drag gesture.
|
|
62
|
+
let r0; // Projection rotation as Euler angles at start.
|
|
63
|
+
let q0; // Projection rotation as versor at start.
|
|
64
|
+
let v10; // Mouse position in Cartesian coordinates just before end of drag gesture.
|
|
65
|
+
let v11; // Mouse position in Cartesian coordinates at end.
|
|
66
|
+
let q10; // Projection rotation as versor at end.
|
|
67
|
+
|
|
68
|
+
const inertia = inertiaHelper({
|
|
69
|
+
axis: opt.axis,
|
|
70
|
+
|
|
71
|
+
start: () => {
|
|
72
|
+
v0 = versor.cartesian(projection.invert(inertia.position));
|
|
73
|
+
r0 = projection.rotate();
|
|
74
|
+
q0 = versor(r0);
|
|
75
|
+
opt.start && opt.start();
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
move: () => {
|
|
79
|
+
const inv = projection.rotate(r0).invert(inertia.position);
|
|
80
|
+
if (isNaN(inv[0])) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const v1 = versor.cartesian(inv);
|
|
84
|
+
const q1 = versor.multiply(q0, versor.delta(v0, v1));
|
|
85
|
+
const r1 = versor.rotation(q1);
|
|
86
|
+
const r2 = opt.axis(r0, r1);
|
|
87
|
+
opt.render(r2);
|
|
88
|
+
opt.move && opt.move();
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
end: () => {
|
|
92
|
+
// Velocity.
|
|
93
|
+
v10 = versor.cartesian(projection.invert(inertia.position.map((d, i) => d - inertia.velocity[i] / 1_000)));
|
|
94
|
+
q10 = versor(projection.rotate());
|
|
95
|
+
v11 = versor.cartesian(projection.invert(inertia.position));
|
|
96
|
+
opt.end && opt.end();
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
stop: opt.stop,
|
|
100
|
+
|
|
101
|
+
finish: opt.finish,
|
|
102
|
+
|
|
103
|
+
render: (t) => {
|
|
104
|
+
const r1 = versor.rotation(versor.multiply(q10, versor.delta(v10, v11, t * 1_000)));
|
|
105
|
+
const r2 = opt.axis(r0, r1);
|
|
106
|
+
opt.render && opt.render(r2);
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
time: opt.time,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return inertia;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function inertiaHelper(opt) {
|
|
116
|
+
const A = opt.time || 5_000; // Reference time in ms.
|
|
117
|
+
const limit = 1.0001;
|
|
118
|
+
const B = -Math.log(1 - 1 / limit);
|
|
119
|
+
const inertia = {
|
|
120
|
+
position: [0, 0],
|
|
121
|
+
velocity: [0, 0], // Velocity in pixels/s.
|
|
122
|
+
timer: d3.timer(() => {}),
|
|
123
|
+
time: 0,
|
|
124
|
+
t: 0,
|
|
125
|
+
|
|
126
|
+
start: function (ev) {
|
|
127
|
+
const position = [ev.x, ev.y];
|
|
128
|
+
inertia.position = position;
|
|
129
|
+
inertia.velocity = [0, 0];
|
|
130
|
+
inertia.timer.stop();
|
|
131
|
+
this.classList.remove('inertia');
|
|
132
|
+
this.classList.add('dragging');
|
|
133
|
+
opt.start && opt.start.call(this, position);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
move: function (ev) {
|
|
137
|
+
const position = [ev.x, ev.y];
|
|
138
|
+
|
|
139
|
+
const time = performance.now();
|
|
140
|
+
const deltaTime = time - inertia.time;
|
|
141
|
+
const decay = 1 - Math.exp(-deltaTime / 1_000);
|
|
142
|
+
inertia.velocity = inertia.velocity.map((d, i) => {
|
|
143
|
+
const deltaPos = position[i] - inertia.position[i];
|
|
144
|
+
const deltaTime = time - inertia.time;
|
|
145
|
+
return (1_000 * (1 - decay) * deltaPos) / deltaTime + d * decay;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Clamp velocity axis.
|
|
149
|
+
inertia.velocity = opt.axis([0, 0], inertia.velocity);
|
|
150
|
+
|
|
151
|
+
inertia.time = time;
|
|
152
|
+
inertia.position = position;
|
|
153
|
+
opt.move && opt.move.call(this, position);
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
end: function (ev) {
|
|
157
|
+
this.classList.remove('dragging', 'inertia');
|
|
158
|
+
|
|
159
|
+
const v = inertia.velocity;
|
|
160
|
+
if (v[0] * v[0] + v[1] * v[1] < 100) {
|
|
161
|
+
inertia.timer.stop();
|
|
162
|
+
return opt.stop && opt.stop();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const time = performance.now();
|
|
166
|
+
const deltaTime = time - inertia.time;
|
|
167
|
+
|
|
168
|
+
if (opt.hold === undefined) {
|
|
169
|
+
opt.hold = 100;
|
|
170
|
+
} // Default flick->drag threshold time (0 disables inertia).
|
|
171
|
+
|
|
172
|
+
if (deltaTime >= opt.hold) {
|
|
173
|
+
inertia.timer.stop();
|
|
174
|
+
return opt.stop && opt.stop();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
this.classList.add('inertia');
|
|
178
|
+
opt.end && opt.end();
|
|
179
|
+
|
|
180
|
+
const self = this;
|
|
181
|
+
inertia.timer.restart((e) => {
|
|
182
|
+
inertia.t = limit * (1 - Math.exp((-B * e) / A));
|
|
183
|
+
opt.render && opt.render(inertia.t);
|
|
184
|
+
if (inertia.t > 1) {
|
|
185
|
+
inertia.timer.stop();
|
|
186
|
+
self.classList.remove('inertia');
|
|
187
|
+
inertia.velocity = [0, 0];
|
|
188
|
+
inertia.t = 1;
|
|
189
|
+
opt.finish && opt.finish();
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
inertia.timer.stop();
|
|
196
|
+
return inertia;
|
|
197
|
+
}
|
package/src/util/path.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import * as d3 from 'd3';
|
|
6
|
+
import { type GeoGeometryObjects } from 'd3';
|
|
7
|
+
import { type Point, type Polygon, type Position } from 'geojson';
|
|
8
|
+
|
|
9
|
+
import type { Vector } from '../hooks';
|
|
10
|
+
|
|
11
|
+
export type LatLng = { lat: number; lng: number };
|
|
12
|
+
|
|
13
|
+
// TODO(burdon): Clean-up.
|
|
14
|
+
|
|
15
|
+
export const positionToRotation = ([lng, lat]: [number, number], tilt = 0): Vector => [-lng, tilt - lat, 0];
|
|
16
|
+
|
|
17
|
+
export const geoToPosition = ({ lat, lng }: LatLng): [number, number] => [lng, lat];
|
|
18
|
+
|
|
19
|
+
export const geoPoint = (point: LatLng): Point => ({ type: 'Point', coordinates: geoToPosition(point) });
|
|
20
|
+
|
|
21
|
+
// https://github.com/d3/d3-geo#geoCircle
|
|
22
|
+
export const geoCircle = ({ lat, lng }: LatLng, radius: number): Polygon =>
|
|
23
|
+
d3.geoCircle().radius(radius).center([lng, lat])();
|
|
24
|
+
|
|
25
|
+
export const geoLine = (p1: LatLng, p2: LatLng): GeoGeometryObjects => ({
|
|
26
|
+
type: 'LineString',
|
|
27
|
+
coordinates: [
|
|
28
|
+
[p1.lng, p1.lat],
|
|
29
|
+
[p2.lng, p2.lat],
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const closestPoint = (points: Position[], target: Position): Position | null => {
|
|
34
|
+
if (points.length === 0) {
|
|
35
|
+
return target;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let closestPoint = points[0];
|
|
39
|
+
let minDistance = getDistance(points[0], target);
|
|
40
|
+
|
|
41
|
+
for (const point of points) {
|
|
42
|
+
const distance = getDistance(point, target);
|
|
43
|
+
if (distance < minDistance) {
|
|
44
|
+
minDistance = distance;
|
|
45
|
+
closestPoint = point;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return closestPoint;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const getDistance = (point1: Position, point2: Position): number => {
|
|
53
|
+
const dx = point1[0] - point2[0];
|
|
54
|
+
const dy = point1[1] - point2[1];
|
|
55
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
56
|
+
};
|