canvas-globe 0.1.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/CHANGELOG.md +137 -0
- package/COPYRIGHT +8 -0
- package/LICENSE +674 -0
- package/LICENSING.md +83 -0
- package/README.md +638 -0
- package/THIRD_PARTY_NOTICES.md +14 -0
- package/codemeta.json +34 -0
- package/dist/canvas-globe.umd.js +4338 -0
- package/dist/package.json +1 -0
- package/package.json +110 -0
- package/src/csv.js +151 -0
- package/src/data/timezones.js +5 -0
- package/src/data/world.js +6 -0
- package/src/element.js +144 -0
- package/src/export.js +19 -0
- package/src/geo-globe.js +2856 -0
- package/src/geo.js +286 -0
- package/src/index.js +19 -0
- package/src/license.js +36 -0
- package/src/media.js +160 -0
- package/src/presets.js +21 -0
- package/src/react.js +60 -0
- package/src/recorder.js +70 -0
- package/src/scenes.js +80 -0
- package/src/texture.js +129 -0
- package/src/themes.js +241 -0
- package/src/viewer.js +130 -0
- package/types/element.d.ts +41 -0
- package/types/index.d.ts +696 -0
- package/types/react.d.ts +15 -0
package/src/scenes.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scenes are whole compositions, not just palettes: a preset plus the layers,
|
|
3
|
+
* motion and overlays a given job needs. Presets answer "how should it look",
|
|
4
|
+
* scenes answer "what am I making".
|
|
5
|
+
*/
|
|
6
|
+
export const scenes = {
|
|
7
|
+
/** Live signups on a pricing or landing page. */
|
|
8
|
+
signups: {
|
|
9
|
+
preset: "hologram",
|
|
10
|
+
mode: "globe",
|
|
11
|
+
autoRotate: true,
|
|
12
|
+
rotateSpeed: 0.06,
|
|
13
|
+
cluster: true,
|
|
14
|
+
tooltip: true,
|
|
15
|
+
showViewer: true,
|
|
16
|
+
counter: { label: "signups this week", position: "top-left" },
|
|
17
|
+
},
|
|
18
|
+
/** A regional launch announcement, ready for country media. */
|
|
19
|
+
launch: {
|
|
20
|
+
preset: "atlas",
|
|
21
|
+
mode: "map",
|
|
22
|
+
landStyle: "fill",
|
|
23
|
+
graticule: false,
|
|
24
|
+
stars: false,
|
|
25
|
+
autoRotate: false,
|
|
26
|
+
focus: { isolate: true, outlineWidth: 2, dim: 1 },
|
|
27
|
+
},
|
|
28
|
+
/** "Trusted in N countries" with customer logos. */
|
|
29
|
+
logos: {
|
|
30
|
+
preset: "mono",
|
|
31
|
+
mode: "map",
|
|
32
|
+
markerStyle: "auto",
|
|
33
|
+
labels: false,
|
|
34
|
+
autoRotate: false,
|
|
35
|
+
graticule: false,
|
|
36
|
+
cluster: true,
|
|
37
|
+
clusterRadius: 54,
|
|
38
|
+
},
|
|
39
|
+
/** Where the team is, for a careers page. */
|
|
40
|
+
team: {
|
|
41
|
+
preset: "atlas",
|
|
42
|
+
mode: "map",
|
|
43
|
+
labels: "markers",
|
|
44
|
+
autoRotate: false,
|
|
45
|
+
graticule: false,
|
|
46
|
+
tooltip: true,
|
|
47
|
+
},
|
|
48
|
+
/** Campaign or revenue coverage by country. */
|
|
49
|
+
coverage: {
|
|
50
|
+
preset: "mono",
|
|
51
|
+
mode: "map",
|
|
52
|
+
countryColors: null,
|
|
53
|
+
labels: false,
|
|
54
|
+
autoRotate: false,
|
|
55
|
+
legend: { title: "Coverage", position: "bottom-left" },
|
|
56
|
+
},
|
|
57
|
+
/** Scroll-linked year in review. */
|
|
58
|
+
review: {
|
|
59
|
+
preset: "midnight",
|
|
60
|
+
mode: "globe",
|
|
61
|
+
autoRotate: false,
|
|
62
|
+
arcs: [],
|
|
63
|
+
counter: { label: "customers", position: "bottom-left" },
|
|
64
|
+
},
|
|
65
|
+
/** Traffic between regions. */
|
|
66
|
+
routes: {
|
|
67
|
+
preset: "blueprint",
|
|
68
|
+
mode: "globe",
|
|
69
|
+
autoRotate: true,
|
|
70
|
+
arcLift: 0.32,
|
|
71
|
+
orbits: 2,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Option keys a scene owns, so switching scenes cannot leak state. */
|
|
76
|
+
export const sceneKeys = [
|
|
77
|
+
"preset", "mode", "landStyle", "graticule", "stars", "autoRotate", "rotateSpeed", "cluster",
|
|
78
|
+
"clusterRadius", "tooltip", "labels", "legend", "counter", "focus", "countryColors", "orbits",
|
|
79
|
+
"arcLift", "showViewer", "markerStyle",
|
|
80
|
+
];
|
package/src/texture.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps an equirectangular image onto the orthographic sphere, pixel by pixel.
|
|
3
|
+
*
|
|
4
|
+
* The per-pixel inverse projection is the whole cost, so it is rendered at a
|
|
5
|
+
* reduced resolution and upscaled: roughly 2 ms for a 430 px globe, which
|
|
6
|
+
* fits comfortably inside a 30 fps budget.
|
|
7
|
+
*/
|
|
8
|
+
import { D2R, R2D, clamp } from "./geo.js";
|
|
9
|
+
|
|
10
|
+
const makeSurface = (w, h) => {
|
|
11
|
+
if (typeof OffscreenCanvas !== "undefined") return new OffscreenCanvas(w, h);
|
|
12
|
+
if (typeof document === "undefined") return null;
|
|
13
|
+
const c = document.createElement("canvas");
|
|
14
|
+
c.width = w;
|
|
15
|
+
c.height = h;
|
|
16
|
+
return c;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class SphereTexture {
|
|
20
|
+
/** `source` is a URL, HTMLImageElement, ImageBitmap or canvas. */
|
|
21
|
+
constructor(source, { maxWidth = 2048, onLoad } = {}) {
|
|
22
|
+
this.ready = false;
|
|
23
|
+
this.error = null;
|
|
24
|
+
this._onLoad = onLoad;
|
|
25
|
+
this._maxWidth = maxWidth;
|
|
26
|
+
if (typeof source === "string") this._loadUrl(source);
|
|
27
|
+
else if (source) this._ingest(source);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_loadUrl(url) {
|
|
31
|
+
if (typeof Image === "undefined") return;
|
|
32
|
+
const img = new Image();
|
|
33
|
+
// Required so the pixels stay readable when the image is cross-origin.
|
|
34
|
+
img.crossOrigin = "anonymous";
|
|
35
|
+
img.onload = () => this._ingest(img);
|
|
36
|
+
img.onerror = () => {
|
|
37
|
+
this.error = new Error(`canvas-globe: could not load texture "${url}"`);
|
|
38
|
+
this._onLoad?.(this);
|
|
39
|
+
};
|
|
40
|
+
img.src = url;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_ingest(image) {
|
|
44
|
+
const sw = image.naturalWidth || image.width;
|
|
45
|
+
const sh = image.naturalHeight || image.height;
|
|
46
|
+
if (!sw || !sh) return;
|
|
47
|
+
const w = Math.min(this._maxWidth, sw);
|
|
48
|
+
const h = Math.round((w / sw) * sh);
|
|
49
|
+
const surface = makeSurface(w, h);
|
|
50
|
+
if (!surface) return;
|
|
51
|
+
const ctx = surface.getContext("2d", { willReadFrequently: true });
|
|
52
|
+
ctx.drawImage(image, 0, 0, w, h);
|
|
53
|
+
try {
|
|
54
|
+
const data = ctx.getImageData(0, 0, w, h);
|
|
55
|
+
this.pixels = data.data;
|
|
56
|
+
this.tw = w;
|
|
57
|
+
this.th = h;
|
|
58
|
+
this.ready = true;
|
|
59
|
+
} catch {
|
|
60
|
+
this.error = new Error("canvas-globe: texture is cross-origin and could not be read");
|
|
61
|
+
}
|
|
62
|
+
this._onLoad?.(this);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Paints the lit hemisphere into `ctx`, centred at cx,cy with radius r. */
|
|
66
|
+
draw(ctx, cx, cy, r, lon0, lat0, { step = 2, shade = true } = {}) {
|
|
67
|
+
if (!this.ready || r <= 0) return false;
|
|
68
|
+
const size = Math.max(2, Math.ceil((r * 2) / step));
|
|
69
|
+
if (!this._surface || this._size !== size) {
|
|
70
|
+
this._surface = makeSurface(size, size);
|
|
71
|
+
if (!this._surface) return false;
|
|
72
|
+
this._ctx = this._surface.getContext("2d");
|
|
73
|
+
this._image = this._ctx.createImageData(size, size);
|
|
74
|
+
this._size = size;
|
|
75
|
+
}
|
|
76
|
+
const out = this._image.data;
|
|
77
|
+
const { pixels, tw, th } = this;
|
|
78
|
+
const f0 = lat0 * D2R;
|
|
79
|
+
const s0 = Math.sin(f0), c0 = Math.cos(f0);
|
|
80
|
+
const inv = 2 / size;
|
|
81
|
+
|
|
82
|
+
for (let py = 0; py < size; py++) {
|
|
83
|
+
const yu = 1 - (py + 0.5) * inv;
|
|
84
|
+
const yu2 = yu * yu;
|
|
85
|
+
let o = py * size * 4;
|
|
86
|
+
for (let px = 0; px < size; px++, o += 4) {
|
|
87
|
+
const x = (px + 0.5) * inv - 1;
|
|
88
|
+
const q = x * x + yu2;
|
|
89
|
+
if (q > 1) {
|
|
90
|
+
out[o + 3] = 0;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const z = Math.sqrt(1 - q);
|
|
94
|
+
const ey = c0 * z - s0 * yu;
|
|
95
|
+
const ez = s0 * z + c0 * yu;
|
|
96
|
+
const lat = Math.asin(clamp(ez, -1, 1)) * R2D;
|
|
97
|
+
const lon = lon0 + Math.atan2(x, ey) * R2D;
|
|
98
|
+
let u = Math.floor((((lon + 180) % 360) + 360) % 360 * (tw / 360));
|
|
99
|
+
if (u >= tw) u -= tw;
|
|
100
|
+
const v = clamp(Math.floor(((90 - lat) / 180) * th), 0, th - 1);
|
|
101
|
+
const s = (v * tw + u) * 4;
|
|
102
|
+
const k = shade ? 0.55 + 0.45 * z : 1;
|
|
103
|
+
out[o] = pixels[s] * k;
|
|
104
|
+
out[o + 1] = pixels[s + 1] * k;
|
|
105
|
+
out[o + 2] = pixels[s + 2] * k;
|
|
106
|
+
out[o + 3] = 255;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this._ctx.putImageData(this._image, 0, 0);
|
|
110
|
+
ctx.drawImage(this._surface, cx - r, cy - r, r * 2, r * 2);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Paints the whole texture into a flat-map viewport. */
|
|
115
|
+
drawFlat(ctx, fwd, w, h) {
|
|
116
|
+
if (!this.ready || !this._surface2) {
|
|
117
|
+
if (!this.ready) return false;
|
|
118
|
+
this._surface2 = makeSurface(this.tw, this.th);
|
|
119
|
+
if (!this._surface2) return false;
|
|
120
|
+
const c = this._surface2.getContext("2d");
|
|
121
|
+
const img = c.createImageData(this.tw, this.th);
|
|
122
|
+
img.data.set(this.pixels);
|
|
123
|
+
c.putImageData(img, 0, 0);
|
|
124
|
+
}
|
|
125
|
+
const a = fwd(-180, 90), b = fwd(180, -90);
|
|
126
|
+
ctx.drawImage(this._surface2, a[0], a[1], b[0] - a[0], b[1] - a[1]);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
}
|
package/src/themes.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/** Built-in palettes. Pass your own object to `theme` to override any key. */
|
|
2
|
+
export const themes = {
|
|
3
|
+
atlas: {
|
|
4
|
+
ocean: ["#c2e7fb", "#7cc0e8"],
|
|
5
|
+
land: "#cfe7b6",
|
|
6
|
+
border: "rgba(206,116,130,.55)",
|
|
7
|
+
graticule: "rgba(255,255,255,.2)",
|
|
8
|
+
atmosphere: "rgba(125,185,255,.45)",
|
|
9
|
+
rim: "rgba(255,255,255,.5)",
|
|
10
|
+
marker: "#6d28d9",
|
|
11
|
+
markerGlow: "rgba(109,40,217,.45)",
|
|
12
|
+
live: "#22c55e",
|
|
13
|
+
bubble: "#ffffff",
|
|
14
|
+
label: "#0f172a",
|
|
15
|
+
stars: "rgba(255,255,255,.75)",
|
|
16
|
+
dot: "#5b8c3a",
|
|
17
|
+
glow: "rgba(206,116,130,.9)",
|
|
18
|
+
orbit: "rgba(109,40,217,.35)",
|
|
19
|
+
arc: "#6d28d9",
|
|
20
|
+
arcHead: "#f97316",
|
|
21
|
+
night: "rgba(9,17,38,.42)",
|
|
22
|
+
cluster: "#4c1d95",
|
|
23
|
+
clusterLabel: "#ffffff",
|
|
24
|
+
focus: "#f59e0b",
|
|
25
|
+
countryHover: "rgba(255,255,255,.35)",
|
|
26
|
+
shade: true,
|
|
27
|
+
},
|
|
28
|
+
midnight: {
|
|
29
|
+
ocean: ["#16224a", "#070b18"],
|
|
30
|
+
land: "#39496b",
|
|
31
|
+
border: "rgba(148,163,184,.4)",
|
|
32
|
+
graticule: "rgba(148,163,184,.14)",
|
|
33
|
+
atmosphere: "rgba(129,140,248,.34)",
|
|
34
|
+
rim: "rgba(168,85,247,.6)",
|
|
35
|
+
marker: "#e879f9",
|
|
36
|
+
markerGlow: "rgba(232,121,249,.5)",
|
|
37
|
+
live: "#4ade80",
|
|
38
|
+
bubble: "#0f172a",
|
|
39
|
+
label: "#e2e8f0",
|
|
40
|
+
stars: "rgba(255,255,255,.8)",
|
|
41
|
+
dot: "#818cf8",
|
|
42
|
+
glow: "rgba(129,140,248,.9)",
|
|
43
|
+
orbit: "rgba(168,85,247,.4)",
|
|
44
|
+
arc: "#a78bfa",
|
|
45
|
+
arcHead: "#f0abfc",
|
|
46
|
+
night: "rgba(2,4,12,.5)",
|
|
47
|
+
cluster: "#7c3aed",
|
|
48
|
+
clusterLabel: "#f8fafc",
|
|
49
|
+
focus: "#fbbf24",
|
|
50
|
+
countryHover: "rgba(255,255,255,.16)",
|
|
51
|
+
shade: true,
|
|
52
|
+
},
|
|
53
|
+
mono: {
|
|
54
|
+
ocean: ["#f8fafc", "#e2e8f0"],
|
|
55
|
+
land: "#cbd5e1",
|
|
56
|
+
border: "rgba(100,116,139,.5)",
|
|
57
|
+
graticule: "rgba(100,116,139,.16)",
|
|
58
|
+
atmosphere: "rgba(148,163,184,.3)",
|
|
59
|
+
rim: "rgba(71,85,105,.45)",
|
|
60
|
+
marker: "#0f172a",
|
|
61
|
+
markerGlow: "rgba(15,23,42,.3)",
|
|
62
|
+
live: "#16a34a",
|
|
63
|
+
bubble: "#ffffff",
|
|
64
|
+
label: "#0f172a",
|
|
65
|
+
stars: "rgba(148,163,184,.6)",
|
|
66
|
+
dot: "#64748b",
|
|
67
|
+
glow: "rgba(71,85,105,.8)",
|
|
68
|
+
orbit: "rgba(71,85,105,.3)",
|
|
69
|
+
arc: "#334155",
|
|
70
|
+
arcHead: "#0f172a",
|
|
71
|
+
night: "rgba(15,23,42,.28)",
|
|
72
|
+
cluster: "#334155",
|
|
73
|
+
clusterLabel: "#ffffff",
|
|
74
|
+
focus: "#2563eb",
|
|
75
|
+
countryHover: "rgba(15,23,42,.16)",
|
|
76
|
+
shade: false,
|
|
77
|
+
},
|
|
78
|
+
/** Cyan dot-matrix earth on deep navy. */
|
|
79
|
+
hologram: {
|
|
80
|
+
ocean: ["rgba(10,26,53,.92)", "rgba(2,7,18,.98)"],
|
|
81
|
+
land: "rgba(56,189,248,.07)",
|
|
82
|
+
border: "rgba(56,189,248,.3)",
|
|
83
|
+
graticule: "rgba(56,189,248,.26)",
|
|
84
|
+
atmosphere: "rgba(56,189,248,.34)",
|
|
85
|
+
rim: "rgba(125,211,252,.85)",
|
|
86
|
+
marker: "#22d3ee",
|
|
87
|
+
markerGlow: "rgba(34,211,238,.5)",
|
|
88
|
+
live: "#5eead4",
|
|
89
|
+
bubble: "#04182b",
|
|
90
|
+
label: "#e0f2fe",
|
|
91
|
+
stars: "rgba(186,230,253,.7)",
|
|
92
|
+
dot: "#38bdf8",
|
|
93
|
+
glow: "rgba(56,189,248,.95)",
|
|
94
|
+
orbit: "rgba(125,211,252,.45)",
|
|
95
|
+
arc: "#22d3ee",
|
|
96
|
+
arcHead: "#a5f3fc",
|
|
97
|
+
night: "rgba(1,4,12,.45)",
|
|
98
|
+
cluster: "#0e7490",
|
|
99
|
+
clusterLabel: "#ecfeff",
|
|
100
|
+
focus: "#fbbf24",
|
|
101
|
+
countryHover: "rgba(56,189,248,.22)",
|
|
102
|
+
shade: false,
|
|
103
|
+
},
|
|
104
|
+
/** Magenta continents with a cyan rim: the "cyber network" look. */
|
|
105
|
+
neon: {
|
|
106
|
+
ocean: ["#180a2e", "#05010f"],
|
|
107
|
+
land: "rgba(236,72,153,.16)",
|
|
108
|
+
border: "#f472b6",
|
|
109
|
+
graticule: "rgba(168,85,247,.2)",
|
|
110
|
+
atmosphere: "rgba(168,85,247,.5)",
|
|
111
|
+
rim: "rgba(34,211,238,.9)",
|
|
112
|
+
marker: "#22d3ee",
|
|
113
|
+
markerGlow: "rgba(34,211,238,.55)",
|
|
114
|
+
live: "#f0abfc",
|
|
115
|
+
bubble: "#1a0b2e",
|
|
116
|
+
label: "#fae8ff",
|
|
117
|
+
stars: "rgba(240,171,252,.75)",
|
|
118
|
+
dot: "#f472b6",
|
|
119
|
+
glow: "rgba(236,72,153,1)",
|
|
120
|
+
orbit: "rgba(34,211,238,.4)",
|
|
121
|
+
arc: "#22d3ee",
|
|
122
|
+
arcHead: "#f0abfc",
|
|
123
|
+
night: "rgba(3,1,10,.5)",
|
|
124
|
+
cluster: "#a21caf",
|
|
125
|
+
clusterLabel: "#fdf4ff",
|
|
126
|
+
focus: "#fde047",
|
|
127
|
+
countryHover: "rgba(244,114,182,.25)",
|
|
128
|
+
shade: false,
|
|
129
|
+
},
|
|
130
|
+
/** Technical line-art on deep blue, for HUD-style dashboards. */
|
|
131
|
+
blueprint: {
|
|
132
|
+
ocean: ["#0c2b52", "#04101f"],
|
|
133
|
+
land: "rgba(125,211,252,.05)",
|
|
134
|
+
border: "#7dd3fc",
|
|
135
|
+
graticule: "rgba(125,211,252,.2)",
|
|
136
|
+
atmosphere: "rgba(59,130,246,.35)",
|
|
137
|
+
rim: "rgba(125,211,252,.55)",
|
|
138
|
+
marker: "#38bdf8",
|
|
139
|
+
markerGlow: "rgba(56,189,248,.45)",
|
|
140
|
+
live: "#4ade80",
|
|
141
|
+
bubble: "#0b2545",
|
|
142
|
+
label: "#e0f2fe",
|
|
143
|
+
stars: "rgba(148,197,255,.55)",
|
|
144
|
+
dot: "#7dd3fc",
|
|
145
|
+
glow: "rgba(125,211,252,.8)",
|
|
146
|
+
orbit: "rgba(125,211,252,.5)",
|
|
147
|
+
arc: "#38bdf8",
|
|
148
|
+
arcHead: "#e0f2fe",
|
|
149
|
+
night: "rgba(2,8,20,.45)",
|
|
150
|
+
cluster: "#1d4ed8",
|
|
151
|
+
clusterLabel: "#eff6ff",
|
|
152
|
+
focus: "#facc15",
|
|
153
|
+
countryHover: "rgba(125,211,252,.2)",
|
|
154
|
+
shade: false,
|
|
155
|
+
},
|
|
156
|
+
/** Green and violet, like a polar aurora. */
|
|
157
|
+
aurora: {
|
|
158
|
+
ocean: ["#04141c", "#01060a"],
|
|
159
|
+
land: "rgba(52,211,153,.08)",
|
|
160
|
+
border: "rgba(52,211,153,.35)",
|
|
161
|
+
graticule: "rgba(167,139,250,.16)",
|
|
162
|
+
atmosphere: "rgba(52,211,153,.32)",
|
|
163
|
+
rim: "rgba(167,139,250,.75)",
|
|
164
|
+
marker: "#a78bfa",
|
|
165
|
+
markerGlow: "rgba(167,139,250,.5)",
|
|
166
|
+
live: "#34d399",
|
|
167
|
+
bubble: "#03121a",
|
|
168
|
+
label: "#d1fae5",
|
|
169
|
+
stars: "rgba(209,250,229,.7)",
|
|
170
|
+
dot: "#34d399",
|
|
171
|
+
glow: "rgba(52,211,153,.9)",
|
|
172
|
+
orbit: "rgba(167,139,250,.45)",
|
|
173
|
+
arc: "#34d399",
|
|
174
|
+
arcHead: "#c4b5fd",
|
|
175
|
+
night: "rgba(1,5,9,.45)",
|
|
176
|
+
cluster: "#047857",
|
|
177
|
+
clusterLabel: "#ecfdf5",
|
|
178
|
+
focus: "#fbbf24",
|
|
179
|
+
countryHover: "rgba(52,211,153,.22)",
|
|
180
|
+
shade: false,
|
|
181
|
+
},
|
|
182
|
+
/** High-contrast black and white. */
|
|
183
|
+
noir: {
|
|
184
|
+
ocean: ["#101010", "#000000"],
|
|
185
|
+
land: "rgba(255,255,255,.05)",
|
|
186
|
+
border: "#fafafa",
|
|
187
|
+
graticule: "rgba(255,255,255,.1)",
|
|
188
|
+
atmosphere: "rgba(255,255,255,.16)",
|
|
189
|
+
rim: "rgba(255,255,255,.7)",
|
|
190
|
+
marker: "#ffffff",
|
|
191
|
+
markerGlow: "rgba(255,255,255,.35)",
|
|
192
|
+
live: "#ffffff",
|
|
193
|
+
bubble: "#000000",
|
|
194
|
+
label: "#ffffff",
|
|
195
|
+
stars: "rgba(255,255,255,.55)",
|
|
196
|
+
dot: "#fafafa",
|
|
197
|
+
glow: "rgba(255,255,255,.9)",
|
|
198
|
+
orbit: "rgba(255,255,255,.3)",
|
|
199
|
+
arc: "#ffffff",
|
|
200
|
+
arcHead: "#ffffff",
|
|
201
|
+
night: "rgba(0,0,0,.5)",
|
|
202
|
+
cluster: "#262626",
|
|
203
|
+
clusterLabel: "#ffffff",
|
|
204
|
+
focus: "#ffffff",
|
|
205
|
+
countryHover: "rgba(255,255,255,.2)",
|
|
206
|
+
shade: false,
|
|
207
|
+
},
|
|
208
|
+
/** Printed-atlas colours, made for per-country fills. */
|
|
209
|
+
political: {
|
|
210
|
+
ocean: ["#8fd0f0", "#3f8fc7"],
|
|
211
|
+
land: "#ece3c8",
|
|
212
|
+
border: "rgba(70,60,45,.5)",
|
|
213
|
+
graticule: "rgba(255,255,255,.32)",
|
|
214
|
+
atmosphere: "rgba(143,208,240,.5)",
|
|
215
|
+
rim: "rgba(255,255,255,.6)",
|
|
216
|
+
marker: "#b91c1c",
|
|
217
|
+
markerGlow: "rgba(185,28,28,.4)",
|
|
218
|
+
live: "#15803d",
|
|
219
|
+
bubble: "#ffffff",
|
|
220
|
+
label: "#1c1917",
|
|
221
|
+
stars: "rgba(255,255,255,.7)",
|
|
222
|
+
dot: "#78716c",
|
|
223
|
+
glow: "rgba(70,60,45,.7)",
|
|
224
|
+
orbit: "rgba(255,255,255,.4)",
|
|
225
|
+
arc: "#b91c1c",
|
|
226
|
+
arcHead: "#f59e0b",
|
|
227
|
+
night: "rgba(12,20,40,.38)",
|
|
228
|
+
cluster: "#7f1d1d",
|
|
229
|
+
clusterLabel: "#ffffff",
|
|
230
|
+
focus: "#1d4ed8",
|
|
231
|
+
countryHover: "rgba(255,255,255,.45)",
|
|
232
|
+
shade: true,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/** Distinct fills used by `countryColors: "auto"`. */
|
|
237
|
+
export const countryPalette = [
|
|
238
|
+
"#e8c39e", "#a8c8a0", "#e5b8b8", "#c5b8dd", "#f0d9a0",
|
|
239
|
+
"#9fc6d4", "#d9c2a3", "#b9d4b0", "#efc9c0", "#c9bfa8",
|
|
240
|
+
"#a9bcd8", "#e0cfe6",
|
|
241
|
+
];
|
package/src/viewer.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locates the person looking at the globe, with no permission prompt, no
|
|
3
|
+
* network call and no API key.
|
|
4
|
+
*
|
|
5
|
+
* The browser's IANA time zone is always available and maps to a published
|
|
6
|
+
* coordinate, which puts the viewer within their time-zone region. That is
|
|
7
|
+
* region-accurate, not street-accurate: call `locateViewerPrecise()` to offer
|
|
8
|
+
* a GPS upgrade behind the usual permission prompt.
|
|
9
|
+
*/
|
|
10
|
+
import { zoneTable, zoneAliases } from "./data/timezones.js";
|
|
11
|
+
|
|
12
|
+
let byZone = null;
|
|
13
|
+
let byCountry = null;
|
|
14
|
+
let byCity = null;
|
|
15
|
+
let aliasOf = null;
|
|
16
|
+
|
|
17
|
+
const parseZones = () => {
|
|
18
|
+
if (byZone) return;
|
|
19
|
+
byZone = new Map();
|
|
20
|
+
byCountry = new Map();
|
|
21
|
+
byCity = new Map();
|
|
22
|
+
aliasOf = new Map();
|
|
23
|
+
for (const row of zoneTable.split(";")) {
|
|
24
|
+
const [name, lon, lat, country] = row.split("|");
|
|
25
|
+
const entry = { lon: Number(lon) / 100, lat: Number(lat) / 100, country, timeZone: name };
|
|
26
|
+
byZone.set(name, entry);
|
|
27
|
+
if (!byCountry.has(country)) byCountry.set(country, entry);
|
|
28
|
+
// The last path segment is the zone's representative city.
|
|
29
|
+
const city = name.slice(name.lastIndexOf("/") + 1).replace(/_/g, " ").toLowerCase();
|
|
30
|
+
if (!byCity.has(city)) byCity.set(city, entry);
|
|
31
|
+
}
|
|
32
|
+
for (const link of zoneAliases.split(";")) {
|
|
33
|
+
const [alias, target] = link.split(">");
|
|
34
|
+
if (alias) aliasOf.set(alias, target);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Published coordinate for an IANA zone name, resolving legacy aliases. */
|
|
39
|
+
export function timeZoneLocation(name) {
|
|
40
|
+
if (!name) return null;
|
|
41
|
+
parseZones();
|
|
42
|
+
return byZone.get(name) || byZone.get(aliasOf.get(name)) || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Representative coordinate for an ISO 3166-1 alpha-2 country code. */
|
|
46
|
+
export function countryLocation(code) {
|
|
47
|
+
if (!code) return null;
|
|
48
|
+
parseZones();
|
|
49
|
+
return byCountry.get(String(code).toUpperCase()) || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Coordinate for a city name, drawn from the time-zone table's representative
|
|
54
|
+
* cities. Covers roughly 300 major cities with no extra payload; pass your own
|
|
55
|
+
* gazetteer to `fromRows` when you need more.
|
|
56
|
+
*/
|
|
57
|
+
export function placeLocation(name) {
|
|
58
|
+
if (!name) return null;
|
|
59
|
+
parseZones();
|
|
60
|
+
return byCity.get(String(name).trim().replace(/_/g, " ").toLowerCase()) || null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const currentZone = () => {
|
|
64
|
+
try {
|
|
65
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const localeCountry = () => {
|
|
72
|
+
const tags = (typeof navigator !== "undefined" && (navigator.languages || [navigator.language])) || [];
|
|
73
|
+
for (const tag of tags) {
|
|
74
|
+
if (!tag) continue;
|
|
75
|
+
const region = String(tag).split("-")[1];
|
|
76
|
+
if (region && /^[A-Za-z]{2}$/.test(region)) return region.toUpperCase();
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort viewer location, synchronously. Returns null only when the
|
|
83
|
+
* environment exposes neither a time zone nor a locale region.
|
|
84
|
+
*
|
|
85
|
+
* `accuracyMeters` is null here because a time zone says nothing about where
|
|
86
|
+
* inside it you are: the caller is expected to derive a radius from the
|
|
87
|
+
* region itself.
|
|
88
|
+
*/
|
|
89
|
+
export function locateViewer() {
|
|
90
|
+
const timeZone = currentZone();
|
|
91
|
+
const zone = timeZoneLocation(timeZone);
|
|
92
|
+
if (zone) return { ...zone, timeZone, source: "timezone", accuracy: "region", accuracyMeters: null };
|
|
93
|
+
const country = countryLocation(localeCountry());
|
|
94
|
+
if (country) return { ...country, timeZone, source: "locale", accuracy: "country", accuracyMeters: null };
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves to the precise coordinate if the viewer grants permission, and to
|
|
100
|
+
* the time-zone estimate otherwise. Never rejects.
|
|
101
|
+
*/
|
|
102
|
+
export function locateViewerPrecise({ timeout = 10000, maximumAge = 60000, enableHighAccuracy = true } = {}) {
|
|
103
|
+
const fallback = locateViewer();
|
|
104
|
+
const geo = typeof navigator !== "undefined" && navigator.geolocation;
|
|
105
|
+
if (!geo) return Promise.resolve(fallback);
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
let settled = false;
|
|
108
|
+
const done = (value) => {
|
|
109
|
+
if (settled) return;
|
|
110
|
+
settled = true;
|
|
111
|
+
resolve(value);
|
|
112
|
+
};
|
|
113
|
+
setTimeout(() => done(fallback), timeout + 500);
|
|
114
|
+
geo.getCurrentPosition(
|
|
115
|
+
(pos) =>
|
|
116
|
+
done({
|
|
117
|
+
lat: pos.coords.latitude,
|
|
118
|
+
lon: pos.coords.longitude,
|
|
119
|
+
timeZone: currentZone(),
|
|
120
|
+
country: fallback?.country ?? null,
|
|
121
|
+
source: "geolocation",
|
|
122
|
+
accuracy: "precise",
|
|
123
|
+
// Metres, as reported by the device. Wi-Fi/IP fixes can be tens of km.
|
|
124
|
+
accuracyMeters: pos.coords.accuracy ?? null,
|
|
125
|
+
}),
|
|
126
|
+
() => done(fallback),
|
|
127
|
+
{ timeout, maximumAge, enableHighAccuracy },
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Arc, CountryShape, GeoGlobe, GeoGlobeOptions, Marker, ClusterMarker, FlyToOptions } from "./index.js";
|
|
2
|
+
|
|
3
|
+
export interface GeoGlobeEventMap {
|
|
4
|
+
"geo-hover": CustomEvent<{ marker: Marker | ClusterMarker | null; pos: { x: number; y: number } | null }>;
|
|
5
|
+
"geo-click": CustomEvent<{ marker: Marker | ClusterMarker; pos: { x: number; y: number } }>;
|
|
6
|
+
"geo-country-hover": CustomEvent<{ country: CountryShape | null; pos: { x: number; y: number } | null }>;
|
|
7
|
+
"geo-country-click": CustomEvent<{ country: CountryShape; pos: { x: number; y: number } }>;
|
|
8
|
+
"geo-render": CustomEvent<{ globe: GeoGlobe }>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
declare class GeoGlobeElementInstance extends HTMLElement {
|
|
13
|
+
/** Underlying instance; available once the element is connected. */
|
|
14
|
+
globe: GeoGlobe | null;
|
|
15
|
+
markers: Marker[];
|
|
16
|
+
arcs: Arc[];
|
|
17
|
+
set options(value: Partial<GeoGlobeOptions>);
|
|
18
|
+
flyTo(lon: number, lat: number, opts?: FlyToOptions): this;
|
|
19
|
+
fitTo(bounds: [number, number, number, number], opts?: FlyToOptions & { padding?: number }): this;
|
|
20
|
+
snapshot(type?: string, quality?: number): string | undefined;
|
|
21
|
+
addEventListener<K extends keyof GeoGlobeEventMap>(
|
|
22
|
+
type: K,
|
|
23
|
+
listener: (this: GeoGlobeElementInstance, ev: GeoGlobeEventMap[K]) => unknown,
|
|
24
|
+
options?: boolean | AddEventListenerOptions
|
|
25
|
+
): void;
|
|
26
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type GeoGlobeElement = GeoGlobeElementInstance;
|
|
30
|
+
|
|
31
|
+
/** The element class, or null when there is no DOM. */
|
|
32
|
+
export declare const GeoGlobeElement: (new () => GeoGlobeElementInstance) | null;
|
|
33
|
+
|
|
34
|
+
/** Registers `<geo-globe>`. Returns null when there is no DOM. */
|
|
35
|
+
export declare function defineGeoGlobe(tag?: string): (new () => GeoGlobeElementInstance) | null;
|
|
36
|
+
|
|
37
|
+
declare global {
|
|
38
|
+
interface HTMLElementTagNameMap {
|
|
39
|
+
"geo-globe": GeoGlobeElementInstance;
|
|
40
|
+
}
|
|
41
|
+
}
|