@dxos/solid-ui-geo 0.0.0
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/countries-110m.ts +10587 -0
- package/data/countries-dots-3.ts +42989 -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/package.json +75 -0
- package/src/components/Globe/Globe.solid-stories.tsx +397 -0
- package/src/components/Globe/Globe.tsx +339 -0
- package/src/components/Globe/index.ts +5 -0
- package/src/components/Map/Map.solid-stories.tsx +69 -0
- package/src/components/Map/Map.tsx +333 -0
- package/src/components/Map/index.ts +5 -0
- package/src/components/Toolbar/Controls.tsx +66 -0
- package/src/components/Toolbar/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/data.ts +9 -0
- package/src/hooks/context.tsx +79 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/useDrag.ts +54 -0
- package/src/hooks/useGlobeZoomHandler.ts +30 -0
- package/src/hooks/useMapZoomHandler.ts +24 -0
- package/src/hooks/useSpinner.ts +71 -0
- package/src/hooks/useTour.ts +124 -0
- package/src/index.ts +10 -0
- package/src/translations.ts +18 -0
- package/src/types.ts +13 -0
- package/src/util/debug.ts +17 -0
- package/src/util/index.ts +8 -0
- package/src/util/inertia.ts +196 -0
- package/src/util/path.ts +52 -0
- package/src/util/render.ts +164 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { selection as d3Selection, geoDistance, geoInterpolate, geoPath } from 'd3';
|
|
6
|
+
import { type Accessor, type Setter, createEffect, createSignal, onCleanup } from 'solid-js';
|
|
7
|
+
import versor from 'versor';
|
|
8
|
+
|
|
9
|
+
import type { GlobeController } from '../components';
|
|
10
|
+
import { type LatLngLiteral } from '../types';
|
|
11
|
+
import { type StyleSet, geoToPosition, positionToRotation } from '../util';
|
|
12
|
+
|
|
13
|
+
const TRANSITION_NAME = 'globe-tour';
|
|
14
|
+
|
|
15
|
+
const defaultDuration = 1_500;
|
|
16
|
+
|
|
17
|
+
export type TourOptions = {
|
|
18
|
+
running?: boolean;
|
|
19
|
+
disabled?: boolean;
|
|
20
|
+
duration?: number;
|
|
21
|
+
loop?: boolean;
|
|
22
|
+
tilt?: number;
|
|
23
|
+
autoRotate?: boolean;
|
|
24
|
+
styles?: StyleSet;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Iterates between points.
|
|
29
|
+
* Inspired by: https://observablehq.com/@mbostock/top-100-cities
|
|
30
|
+
*/
|
|
31
|
+
export const useTour = (
|
|
32
|
+
controller?: GlobeController | null,
|
|
33
|
+
points?: LatLngLiteral[],
|
|
34
|
+
options: TourOptions = {},
|
|
35
|
+
): { running: Accessor<boolean>; setRunning: Setter<boolean> } => {
|
|
36
|
+
const selection = d3Selection();
|
|
37
|
+
const [running, setRunning] = createSignal(options.running ?? false);
|
|
38
|
+
|
|
39
|
+
createEffect(() => {
|
|
40
|
+
if (!running()) {
|
|
41
|
+
selection.interrupt(TRANSITION_NAME);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let t: ReturnType<typeof setTimeout>;
|
|
46
|
+
if (controller && running()) {
|
|
47
|
+
t = setTimeout(async () => {
|
|
48
|
+
const { canvas, projection, setRotation } = controller;
|
|
49
|
+
const context = canvas.getContext('2d', { alpha: false });
|
|
50
|
+
const path = geoPath(projection, context).pointRadius(2);
|
|
51
|
+
|
|
52
|
+
const tilt = options.tilt ?? 0;
|
|
53
|
+
let last: LatLngLiteral;
|
|
54
|
+
try {
|
|
55
|
+
const p = [...(points ?? [])];
|
|
56
|
+
if (options.loop) {
|
|
57
|
+
p.push(p[0]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const next of p) {
|
|
61
|
+
if (!running()) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Points.
|
|
66
|
+
const p1 = last ? geoToPosition(last) : undefined;
|
|
67
|
+
const p2 = geoToPosition(next);
|
|
68
|
+
const ip = geoInterpolate(p1 || p2, p2);
|
|
69
|
+
const distance = geoDistance(p1 || p2, p2);
|
|
70
|
+
|
|
71
|
+
// Rotation.
|
|
72
|
+
const r1 = p1 ? positionToRotation(p1, tilt) : controller.projection.rotate();
|
|
73
|
+
const r2 = positionToRotation(p2, tilt);
|
|
74
|
+
const iv = versor.interpolate(r1, r2);
|
|
75
|
+
|
|
76
|
+
const transition = selection
|
|
77
|
+
.transition(TRANSITION_NAME)
|
|
78
|
+
.duration(Math.max(options.duration ?? defaultDuration, distance * 2_000))
|
|
79
|
+
.tween('render', () => (t) => {
|
|
80
|
+
const t1 = Math.max(0, Math.min(1, t * 2 - 1));
|
|
81
|
+
const t2 = Math.min(1, t * 2);
|
|
82
|
+
|
|
83
|
+
context.save();
|
|
84
|
+
{
|
|
85
|
+
context.beginPath();
|
|
86
|
+
context.strokeStyle = options?.styles?.arc?.strokeStyle ?? 'yellow';
|
|
87
|
+
context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
|
|
88
|
+
context.setLineDash(options?.styles?.arc?.lineDash ?? []);
|
|
89
|
+
path({ type: 'LineString', coordinates: [ip(t1), ip(t2)] });
|
|
90
|
+
context.stroke();
|
|
91
|
+
|
|
92
|
+
context.beginPath();
|
|
93
|
+
context.fillStyle = options?.styles?.cursor?.fillStyle ?? 'orange';
|
|
94
|
+
path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
|
|
95
|
+
path({ type: 'Point', coordinates: ip(t2) });
|
|
96
|
+
context.fill();
|
|
97
|
+
}
|
|
98
|
+
context.restore();
|
|
99
|
+
|
|
100
|
+
// TODO(burdon): This has to come after rendering above. Add to features to correct order?
|
|
101
|
+
projection.rotate(iv(t));
|
|
102
|
+
setRotation(projection.rotate());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Throws if interrupted.
|
|
106
|
+
await transition.end();
|
|
107
|
+
last = next;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Ignore.
|
|
111
|
+
} finally {
|
|
112
|
+
setRunning(false);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
onCleanup(() => {
|
|
117
|
+
clearTimeout(t);
|
|
118
|
+
selection.interrupt(TRANSITION_NAME);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return { running, setRunning };
|
|
124
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
export const translationKey = '@dxos/react-ui-geo';
|
|
6
|
+
|
|
7
|
+
export const translations = [
|
|
8
|
+
{
|
|
9
|
+
'en-US': {
|
|
10
|
+
[translationKey]: {
|
|
11
|
+
'zoom in icon button': 'Zoom in',
|
|
12
|
+
'zoom out icon button': 'Zoom out',
|
|
13
|
+
'start icon button': 'Start',
|
|
14
|
+
'toggle icon button': 'Toggle',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
] as const;
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 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
|
+
|
|
16
|
+
return data;
|
|
17
|
+
};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2017 Philippe Rivière
|
|
3
|
+
// Copyright 2025 DXOS.org
|
|
4
|
+
// https://github.com/Fil/d3-inertia
|
|
5
|
+
//
|
|
6
|
+
|
|
7
|
+
import { drag, select, timer } 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 = 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(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: 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
|
+
const time = performance.now();
|
|
139
|
+
const deltaTime = time - inertia.time;
|
|
140
|
+
const decay = 1 - Math.exp(-deltaTime / 1_000);
|
|
141
|
+
inertia.velocity = inertia.velocity.map((d, i) => {
|
|
142
|
+
const deltaPos = position[i] - inertia.position[i];
|
|
143
|
+
const deltaTime = time - inertia.time;
|
|
144
|
+
return (1_000 * (1 - decay) * deltaPos) / deltaTime + d * decay;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Clamp velocity axis.
|
|
148
|
+
inertia.velocity = opt.axis([0, 0], inertia.velocity);
|
|
149
|
+
|
|
150
|
+
inertia.time = time;
|
|
151
|
+
inertia.position = position;
|
|
152
|
+
opt.move && opt.move.call(this, position);
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
end: function (ev) {
|
|
156
|
+
this.classList.remove('dragging', 'inertia');
|
|
157
|
+
|
|
158
|
+
const v = inertia.velocity;
|
|
159
|
+
if (v[0] * v[0] + v[1] * v[1] < 100) {
|
|
160
|
+
inertia.timer.stop();
|
|
161
|
+
return opt.stop && opt.stop();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const time = performance.now();
|
|
165
|
+
const deltaTime = time - inertia.time;
|
|
166
|
+
|
|
167
|
+
if (opt.hold === undefined) {
|
|
168
|
+
opt.hold = 100;
|
|
169
|
+
} // Default flick->drag threshold time (0 disables inertia).
|
|
170
|
+
|
|
171
|
+
if (deltaTime >= opt.hold) {
|
|
172
|
+
inertia.timer.stop();
|
|
173
|
+
return opt.stop && opt.stop();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.classList.add('inertia');
|
|
177
|
+
opt.end && opt.end();
|
|
178
|
+
|
|
179
|
+
const self = this;
|
|
180
|
+
inertia.timer.restart((e) => {
|
|
181
|
+
inertia.t = limit * (1 - Math.exp((-B * e) / A));
|
|
182
|
+
opt.render && opt.render(inertia.t);
|
|
183
|
+
if (inertia.t > 1) {
|
|
184
|
+
inertia.timer.stop();
|
|
185
|
+
self.classList.remove('inertia');
|
|
186
|
+
inertia.velocity = [0, 0];
|
|
187
|
+
inertia.t = 1;
|
|
188
|
+
opt.finish && opt.finish();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
inertia.timer.stop();
|
|
195
|
+
return inertia;
|
|
196
|
+
}
|
package/src/util/path.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type GeoGeometryObjects, geoCircle as d3GeoCircle } from 'd3';
|
|
6
|
+
import { type Point, type Polygon, type Position } from 'geojson';
|
|
7
|
+
import { type LatLngLiteral } from 'leaflet';
|
|
8
|
+
|
|
9
|
+
import type { Vector } from '../hooks';
|
|
10
|
+
|
|
11
|
+
export const positionToRotation = ([lng, lat]: [number, number], tilt = 0): Vector => [-lng, tilt - lat, 0];
|
|
12
|
+
|
|
13
|
+
export const geoToPosition = ({ lat, lng }: LatLngLiteral): [number, number] => [lng, lat];
|
|
14
|
+
|
|
15
|
+
export const geoPoint = (point: LatLngLiteral): Point => ({ type: 'Point', coordinates: geoToPosition(point) });
|
|
16
|
+
|
|
17
|
+
// https://github.com/d3/d3-geo#geoCircle
|
|
18
|
+
export const geoCircle = ({ lat, lng }: LatLngLiteral, radius: number): Polygon =>
|
|
19
|
+
d3GeoCircle().radius(radius).center([lng, lat])();
|
|
20
|
+
|
|
21
|
+
export const geoLine = (p1: LatLngLiteral, p2: LatLngLiteral): GeoGeometryObjects => ({
|
|
22
|
+
type: 'LineString',
|
|
23
|
+
coordinates: [
|
|
24
|
+
[p1.lng, p1.lat],
|
|
25
|
+
[p2.lng, p2.lat],
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const closestPoint = (points: Position[], target: Position): Position | null => {
|
|
30
|
+
if (points.length === 0) {
|
|
31
|
+
return target;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let closestPoint = points[0];
|
|
35
|
+
let minDistance = getDistance(points[0], target);
|
|
36
|
+
|
|
37
|
+
for (const point of points) {
|
|
38
|
+
const distance = getDistance(point, target);
|
|
39
|
+
if (distance < minDistance) {
|
|
40
|
+
minDistance = distance;
|
|
41
|
+
closestPoint = point;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return closestPoint;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getDistance = (point1: Position, point2: Position): number => {
|
|
49
|
+
const dx = point1[0] - point2[0];
|
|
50
|
+
const dy = point1[1] - point2[1];
|
|
51
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
52
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type GeoPath, type GeoPermissibleObjects, geoGraticule } from 'd3';
|
|
6
|
+
import { feature, mesh } from 'topojson-client';
|
|
7
|
+
import { type Topology } from 'topojson-specification';
|
|
8
|
+
|
|
9
|
+
import { type LatLngLiteral } from '../types';
|
|
10
|
+
|
|
11
|
+
import { geoLine, geoPoint } from './path';
|
|
12
|
+
|
|
13
|
+
export type Styles = Record<string, any>;
|
|
14
|
+
|
|
15
|
+
export type Style =
|
|
16
|
+
| 'background'
|
|
17
|
+
| 'water'
|
|
18
|
+
| 'graticule'
|
|
19
|
+
| 'land'
|
|
20
|
+
| 'border'
|
|
21
|
+
| 'dots'
|
|
22
|
+
| 'point'
|
|
23
|
+
| 'line'
|
|
24
|
+
| 'cursor'
|
|
25
|
+
| 'arc';
|
|
26
|
+
|
|
27
|
+
export type StyleSet = Partial<Record<Style, Styles>>;
|
|
28
|
+
|
|
29
|
+
export type Features = {
|
|
30
|
+
points?: LatLngLiteral[];
|
|
31
|
+
lines?: { source: LatLngLiteral; target: LatLngLiteral }[];
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Layer = {
|
|
35
|
+
styles: Styles;
|
|
36
|
+
path: GeoPermissibleObjects;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Create rendering layers.
|
|
41
|
+
*/
|
|
42
|
+
export const createLayers = (topology: Topology, features: Features, styles: StyleSet): Layer[] => {
|
|
43
|
+
const layers: Layer[] = [];
|
|
44
|
+
|
|
45
|
+
if (styles.water) {
|
|
46
|
+
layers.push({
|
|
47
|
+
styles: styles.water,
|
|
48
|
+
path: {
|
|
49
|
+
type: 'Sphere',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (styles.graticule) {
|
|
55
|
+
layers.push({
|
|
56
|
+
styles: styles.graticule,
|
|
57
|
+
path: geoGraticule().step([6, 6])(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
//
|
|
62
|
+
// Topology.
|
|
63
|
+
//
|
|
64
|
+
|
|
65
|
+
if (topology) {
|
|
66
|
+
if (topology.objects.land && styles.land) {
|
|
67
|
+
layers.push({
|
|
68
|
+
styles: styles.land,
|
|
69
|
+
path: feature(topology, topology.objects.land),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (topology.objects.countries && styles.border) {
|
|
74
|
+
layers.push({
|
|
75
|
+
styles: styles.border,
|
|
76
|
+
path: mesh(topology, topology.objects.countries, (a: any, b: any) => a !== b),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (topology.objects.dots && styles.dots) {
|
|
81
|
+
layers.push({
|
|
82
|
+
styles: styles.dots,
|
|
83
|
+
path: topology.objects.dots as any, // TODO(burdon): Type.
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
//
|
|
89
|
+
// Features.
|
|
90
|
+
//
|
|
91
|
+
|
|
92
|
+
if (features) {
|
|
93
|
+
const { points, lines } = features;
|
|
94
|
+
|
|
95
|
+
if (points && styles.point) {
|
|
96
|
+
layers.push({
|
|
97
|
+
styles: styles.point,
|
|
98
|
+
path: {
|
|
99
|
+
type: 'GeometryCollection',
|
|
100
|
+
geometries: points.map((point) => geoPoint(point)),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (lines && styles.line) {
|
|
106
|
+
layers.push({
|
|
107
|
+
styles: styles.line,
|
|
108
|
+
path: {
|
|
109
|
+
type: 'GeometryCollection',
|
|
110
|
+
geometries: lines.map(({ source, target }) => geoLine(source, target)),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return layers;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Render layers created above.
|
|
121
|
+
*/
|
|
122
|
+
export const renderLayers = (generator: GeoPath, layers: Layer[] = [], scale: number, styles: StyleSet) => {
|
|
123
|
+
const context: CanvasRenderingContext2D = generator.context();
|
|
124
|
+
const {
|
|
125
|
+
canvas: { width, height },
|
|
126
|
+
} = context;
|
|
127
|
+
context.reset();
|
|
128
|
+
|
|
129
|
+
// Clear background.
|
|
130
|
+
if (styles.background) {
|
|
131
|
+
context.fillStyle = styles.background.fillStyle;
|
|
132
|
+
context.fillRect(0, 0, width, height);
|
|
133
|
+
} else {
|
|
134
|
+
context.clearRect(0, 0, width, height);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Render features.
|
|
138
|
+
// https://github.com/d3/d3-geo#_path
|
|
139
|
+
layers.forEach(({ path, styles }) => {
|
|
140
|
+
context.save();
|
|
141
|
+
let fill = false;
|
|
142
|
+
let stroke = false;
|
|
143
|
+
if (styles) {
|
|
144
|
+
Object.entries(styles).forEach(([key, value]) => {
|
|
145
|
+
if (key === 'pointRadius') {
|
|
146
|
+
generator.pointRadius(value * scale);
|
|
147
|
+
} else {
|
|
148
|
+
context[key] = value;
|
|
149
|
+
fill ||= key === 'fillStyle';
|
|
150
|
+
stroke ||= key === 'strokeStyle';
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
context.beginPath();
|
|
156
|
+
|
|
157
|
+
generator(path);
|
|
158
|
+
fill && context.fill();
|
|
159
|
+
stroke && context.stroke();
|
|
160
|
+
context.restore();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return context;
|
|
164
|
+
};
|