@johnmorrisdotca/chikyu-earth 0.4.0 → 0.5.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 +36 -0
- package/NOTICE.md +28 -1
- package/README.md +30 -0
- package/dist/element.d.ts +6 -1
- package/dist/element.js +55 -3
- package/dist/markers/cluster.d.ts +11 -0
- package/dist/markers/cluster.js +26 -0
- package/dist/markers/index.d.ts +5 -0
- package/dist/markers/index.js +6 -0
- package/dist/markers/labels.d.ts +24 -0
- package/dist/markers/labels.js +52 -0
- package/dist/markers/layer.d.ts +49 -0
- package/dist/markers/layer.js +434 -0
- package/dist/markers/project.d.ts +21 -0
- package/dist/markers/project.js +43 -0
- package/dist/markers/types.d.ts +38 -0
- package/dist/markers/types.js +1 -0
- package/dist/realistic/earth.d.ts +56 -0
- package/dist/realistic/earth.js +256 -0
- package/dist/realistic/element.d.ts +53 -0
- package/dist/realistic/element.js +280 -0
- package/dist/realistic/gate.d.ts +20 -0
- package/dist/realistic/gate.js +40 -0
- package/dist/realistic/index.d.ts +5 -0
- package/dist/realistic/index.js +6 -0
- package/dist/realistic/shaders.d.ts +5 -0
- package/dist/realistic/shaders.js +79 -0
- package/dist/realistic/sun.d.ts +7 -0
- package/dist/realistic/sun.js +28 -0
- package/dist/realistic/tiers.d.ts +20 -0
- package/dist/realistic/tiers.js +30 -0
- package/dist/textures/clouds-1024.webp +0 -0
- package/dist/textures/clouds-2048.webp +0 -0
- package/dist/textures/day-1024.webp +0 -0
- package/dist/textures/day-2048.webp +0 -0
- package/dist/textures/day-4096.webp +0 -0
- package/dist/textures/night-1024.webp +0 -0
- package/dist/textures/night-2048.webp +0 -0
- package/dist/textures/water-1024.webp +0 -0
- package/dist/textures/water-2048.webp +0 -0
- package/dist/view.d.ts +2 -0
- package/dist/view.js +5 -0
- package/package.json +45 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
The Earth from space, and markers for any place on either globe, both under a hard performance budget.
|
|
6
|
+
|
|
7
|
+
### Markers
|
|
8
|
+
|
|
9
|
+
- **`attachMarkers(element)`** (`@johnmorrisdotca/chikyu-earth/markers`) puts your places on `<chikyu-earth>` or `<chikyu-earth-realistic>`: add, update and remove markers at `[lon, lat]` with a label, colour, icon and size; groups (cities, stadiums) shown or hidden together; markers that turn, zoom and pan with the globe and fade on the far side; crowded places gathered into clusters; labels that make room for each other; `chikyu-marker-hover`, `-click` and `-focus` events carrying your own `data`; a button for each shown label (Tab and Enter work) and a hidden list of every place for a screen reader. Chikyu ships no places; the demo shows 22 cities and 10 stadiums (Wikidata coordinates, CC0). [docs/markers.md](docs/markers.md).
|
|
10
|
+
- **Budget**, each line tested: all dots in one 2D canvas pass, and a pool of 24 label buttons, never an element per place; 5,000 markers cost 0.36 ms a view change on the flat globe; a 5,000-marker drag on the Earth from space made 117 draws in 4 s (29 a second), the markers drawn with them one for one and not at all at rest or out of sight; the overlay canvas holds the same 4096-a-side, 6 MP limit.
|
|
11
|
+
- The flat element's picture now sits in a holder beside the markers' overlay (a redraw replaces only the picture), and `lookAt` moves to `view.ts`.
|
|
12
|
+
|
|
13
|
+
### The Earth from space
|
|
14
|
+
|
|
15
|
+
An optional realistic look, drawn with three.js through kazoku-ui's shared renderer.
|
|
16
|
+
|
|
17
|
+
- **`<chikyu-earth-realistic>`** (`@johnmorrisdotca/chikyu-earth/realistic/element`): day side from NASA's Blue Marble, the cities' lights on the night side, a cloud
|
|
18
|
+
layer that drifts with the date, a sun glint on the sea, an atmosphere at the rim, and the day and night line placed by the real Sun for a date and time (now by
|
|
19
|
+
default). The same shared controls (turn, tilt, zoom, pan) and the same `setView(view, final)` as the flat globe. `time`, `clouds`, `lights`, `center`,
|
|
20
|
+
`texture-base`, `max-texture` and `live-seconds` attributes; `chikyu-ready`, `chikyu-unavailable` and `chikyu-tier` events. [docs/realistic.md](docs/realistic.md).
|
|
21
|
+
- **The flat look stays**: `<chikyu-earth>` is unchanged (and gains a `centre` getter); the demo's Line, Thick and Realistic buttons choose between them, keep the
|
|
22
|
+
camera, and keep the choice, the graticule, the clouds, the lights and the time in the address (kazoku-ui's address state). Without WebGL the demo says so and
|
|
23
|
+
shows the line globe.
|
|
24
|
+
- **Optional, so the base install stays small**: `three` and `@johnmorrisdotca/kazoku-ui` are optional peer dependencies; the base entry and `<chikyu-earth>` need neither.
|
|
25
|
+
The realistic element, its Sun, its scene and its shaders, with kazoku-ui's renderer, are 22.6 KB minified (9.2 KB gzipped); three.js is the page's, and the demo's
|
|
26
|
+
subset of it (`demo/three-subset.ts`) is 188 KB gzipped. Sun position: `subsolarPoint(ms)` in `@johnmorrisdotca/chikyu-earth/realistic`, tested against the almanac
|
|
27
|
+
(declination within 0.05 degree, the equation of time in the longitude).
|
|
28
|
+
- **Pictures** (1.55 MB in `dist/textures/`, WebP, all NASA public domain, credited in NOTICE.md): day at 1024, 2048 and 4096; night, clouds and a water mask at 1024 and 2048.
|
|
29
|
+
The 1024 ones load first; a larger one loads when the zoom needs it (about 3 texels per pixel of the disc's radius), replaces the smaller, and is freed 4 s after the
|
|
30
|
+
zoom drops. No texture is wider than 4096 (2048 on a device with 2 GB or less); `pnpm textures` rebuilds them.
|
|
31
|
+
- **Performance, each proved by `e2e/realistic.gl.ts` (Chromium, software WebGL)**: 0 draw calls and 0 animation frames in 4 s at rest; a drag at 125 moves a second
|
|
32
|
+
draws 29.3 times a second, and 2,700 `setView` calls in 2 s make 59 draws; an Auto-turn at the Earth's rate draws 0 times in 6 s; one WebGL context; the canvas on a
|
|
33
|
+
5K screen at ratio 2 is 3264 x 1835 = 5.99 MP; the textures alive grow one a layer with the zoom and return to the start after it; 0 draws and 0 frames while out of
|
|
34
|
+
sight or in a hidden tab; after removal 0 buffers, 0 programs and none of our textures remain, and the element can come back; a lost context draws nothing and then
|
|
35
|
+
recovers, not black; no WebGL gives `chikyu-unavailable`. The soak (10 minutes, 976 moves, 2,000 markers on): JS heap 7.29 to 7.80 MB after garbage collection, GPU objects 20 to 20, textures, buffers and programs the same as at the start, the draw rate 5.4 a second on average, one WebGL context, no error.
|
|
36
|
+
- `pnpm test:soak` (`SOAK_MINUTES`, 10 by default; CI runs 1) turns, zooms, re-times and toggles for that long and compares the heap, the GPU objects and the draw rate with the start.
|
|
37
|
+
- `pnpm screenshots` also makes the realistic pictures; `scripts/make-textures.py` makes the textures from NASA's sources.
|
|
38
|
+
|
|
3
39
|
## 0.4.0
|
|
4
40
|
|
|
5
41
|
The outlines now come from chizu, the controls from kazoku-ui 0.9, and the release from the family's shared workflow.
|
package/NOTICE.md
CHANGED
|
@@ -16,6 +16,33 @@ Each outline's English name, Japanese name and country code come from **chizu**'
|
|
|
16
16
|
**kuni**'s (https://github.com/johnmorrisdotca/kuni, also by John Morris) and Unicode CLDR's
|
|
17
17
|
(https://www.unicode.org/license.txt) and Wikidata's (CC0). chizu's own NOTICE.md has the terms.
|
|
18
18
|
|
|
19
|
+
## The realistic Earth's pictures
|
|
20
|
+
|
|
21
|
+
The pictures in `textures/` (and `dist/textures/`) are made by `scripts/make-textures.py` from four NASA Earth Observatory images, resized and
|
|
22
|
+
re-encoded as WebP. NASA says its content "generally are not subject to copyright in the United States" and asks that "NASA should be acknowledged as the
|
|
23
|
+
source of the material" (https://www.nasa.gov/nasa-brand-center/images-and-media/, read 2026-11-02). These are NASA's own images, not third-party
|
|
24
|
+
material, so no other licence applies. This package is not endorsed by NASA and uses none of NASA's insignia or logotype.
|
|
25
|
+
|
|
26
|
+
| Picture | Source | Credit |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| `day-*.webp` | Blue Marble: Next Generation with topography and bathymetry, September 2004, 5400 x 2700 (https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73801/world.topo.bathy.200409.3x5400x2700.jpg, Visible Earth 73801) | NASA Earth Observatory (Reto Stöckli, Blue Marble Next Generation) |
|
|
29
|
+
| `water-*.webp` | made by `scripts/make-textures.py` from the day picture: where it is blue and dark | as above |
|
|
30
|
+
| `night-*.webp` | Earth at Night, Black Marble 2016 color map at 0.1 degree, 3600 x 1800 (https://eoimages.gsfc.nasa.gov/images/imagerecords/144000/144898/BlackMarble_2016_01deg.jpg, Visible Earth 144898) | NASA Earth Observatory, Suomi NPP VIIRS data (Miguel Roman, NASA's Goddard Space Flight Center) |
|
|
31
|
+
| `clouds-*.webp` | Blue Marble clouds, 2048 x 1024 (https://eoimages.gsfc.nasa.gov/images/imagerecords/57000/57747/cloud_combined_2048.jpg, Visible Earth 57747) | NASA Earth Observatory (Reto Stöckli) |
|
|
32
|
+
|
|
33
|
+
The credit names are NASA's usual lines for these series; the Visible Earth pages (now at science.nasa.gov/earth/earth-observatory) did not show a credit line in the
|
|
34
|
+
text read on 2026-11-02, so check them if you need the exact wording for print.
|
|
35
|
+
|
|
36
|
+
## Sun position
|
|
37
|
+
|
|
38
|
+
`src/realistic/sun.ts` uses the low-precision solar position formulas of NOAA's solar calculator (https://gml.noaa.gov/grad/solcalc/, after Jean Meeus,
|
|
39
|
+
*Astronomical Algorithms*). Formulas are not copyrightable; the code is this package's own and MIT.
|
|
40
|
+
|
|
41
|
+
## three.js
|
|
42
|
+
|
|
43
|
+
The realistic look draws with three.js (https://threejs.org, MIT, copyright the three.js authors), the page's own optional peer dependency; this package
|
|
44
|
+
does not bundle or ship it.
|
|
45
|
+
|
|
19
46
|
## Projection
|
|
20
47
|
|
|
21
48
|
The orthographic projection, the spherical clipping and the SVG path generation are **d3-geo**
|
|
@@ -25,7 +52,7 @@ d3-geo is the package's projection dependency.
|
|
|
25
52
|
## Shared view controls
|
|
26
53
|
|
|
27
54
|
The turn, tilt, zoom, pan and touch controls are **kazoku-ui** (https://github.com/johnmorrisdotca/kazoku-ui),
|
|
28
|
-
also by John Morris, under the MIT licence.
|
|
55
|
+
also by John Morris, under the MIT licence. Its shared 3D renderer draws the realistic Earth.
|
|
29
56
|
|
|
30
57
|
## Marks
|
|
31
58
|
|
package/README.md
CHANGED
|
@@ -46,6 +46,36 @@ Centre it yourself instead:
|
|
|
46
46
|
<chikyu-earth center="139.7,35.7" style="thick" graticule></chikyu-earth>
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
## The Earth from space
|
|
50
|
+
|
|
51
|
+
An optional second look, for a page with room for a real picture: day and night by the real Sun, cities' lights, a sun glint on the sea, a cloud layer and the
|
|
52
|
+
atmosphere at the rim, drawn with three.js through kazoku-ui's shared renderer. It is its own entry point, so the line globe above costs nothing extra.
|
|
53
|
+
|
|
54
|
+
<p align="center"><img alt="The Earth from space: Asia and the Pacific by day, with clouds and the sun glint" src="docs/images/realistic-desk.png" width="560"></p>
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<script type="module">
|
|
58
|
+
import "@johnmorrisdotca/chikyu-earth/realistic/element";
|
|
59
|
+
</script>
|
|
60
|
+
<chikyu-earth-realistic center="139.7,35.7" time="2026-06-21T03:00Z" texture-base="/textures/"></chikyu-earth-realistic>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Needs `three` and `@johnmorrisdotca/kazoku-ui` (both optional peers) and the pictures (1.6 MB, from NASA, in `dist/textures/`). It draws nothing while nothing changes,
|
|
64
|
+
at most 30 times a second while it does, in one WebGL context, with textures that grow as you zoom and are freed after. All of it, the budget and the tests:
|
|
65
|
+
[docs/realistic.md](docs/realistic.md).
|
|
66
|
+
|
|
67
|
+
## Markers
|
|
68
|
+
|
|
69
|
+
Mark any place at `[lon, lat]` with a label, colour or icon, in groups you can show or hide, on either look. Chikyu ships no places; yours move with the globe, fade on the far side, make room
|
|
70
|
+
for their labels, gather into clusters when crowded, and fire hover, click and keyboard events with your own data. One canvas pass for thousands, a pool of 24 buttons for the labels. [docs/markers.md](docs/markers.md).
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { attachMarkers } from "@johnmorrisdotca/chikyu-earth/markers";
|
|
74
|
+
|
|
75
|
+
const markers = attachMarkers(document.querySelector("chikyu-earth")!);
|
|
76
|
+
markers.add({ id: "tokyo", lon: 139.69, lat: 35.69, label: "Tokyo", group: "Cities" });
|
|
77
|
+
```
|
|
78
|
+
|
|
49
79
|
## Controls
|
|
50
80
|
|
|
51
81
|
The shared [kazoku-ui view controls](https://github.com/johnmorrisdotca/kazoku-ui) supply every hand and button:
|
package/dist/element.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { OverlayHost } from "./markers/layer.ts";
|
|
2
|
+
import type { GeoPoint, GlobeView } from "./types.ts";
|
|
2
3
|
export declare class ChikyuEarth extends HTMLElement {
|
|
3
4
|
#private;
|
|
4
5
|
static observedAttributes: string[];
|
|
6
|
+
/** Where the view is centred now: the `center` attribute, else the reader's place once the browser has answered. */
|
|
7
|
+
get centre(): GeoPoint;
|
|
8
|
+
/** A box laid over the globe for markers (see `@johnmorrisdotca/chikyu-earth/markers`): where it is, the pose of the last drawing, and a way to hear the next. */
|
|
9
|
+
get overlay(): OverlayHost;
|
|
5
10
|
connectedCallback(): void;
|
|
6
11
|
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
7
12
|
/**
|
package/dist/element.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// it opens centred on where you are (the `center` attribute wins over geolocation). Importing registers it once.
|
|
3
3
|
import { globe } from "./globe.js";
|
|
4
4
|
import { cleanStyle } from "./styles.js";
|
|
5
|
-
import { HOME_VIEW } from "./view.js";
|
|
5
|
+
import { globeBaseScale, HOME_VIEW, lookAt } from "./view.js";
|
|
6
6
|
const readCentre = (value) => {
|
|
7
7
|
if (!value)
|
|
8
8
|
return null;
|
|
@@ -18,6 +18,25 @@ export class ChikyuEarth extends HTMLElement {
|
|
|
18
18
|
#centre = { lon: 0, lat: 0 };
|
|
19
19
|
#view = { ...HOME_VIEW };
|
|
20
20
|
#drawn = { ...HOME_VIEW };
|
|
21
|
+
#listeners = new Set();
|
|
22
|
+
#overlay;
|
|
23
|
+
#pose = null;
|
|
24
|
+
/** Where the view is centred now: the `center` attribute, else the reader's place once the browser has answered. */
|
|
25
|
+
get centre() {
|
|
26
|
+
return { ...this.#centre };
|
|
27
|
+
}
|
|
28
|
+
/** A box laid over the globe for markers (see `@johnmorrisdotca/chikyu-earth/markers`): where it is, the pose of the last drawing, and a way to hear the next. */
|
|
29
|
+
get overlay() {
|
|
30
|
+
this.#ensureShell();
|
|
31
|
+
return {
|
|
32
|
+
overlayRoot: this.#overlay,
|
|
33
|
+
pose: this.#pose,
|
|
34
|
+
subscribe: (listener) => {
|
|
35
|
+
this.#listeners.add(listener);
|
|
36
|
+
return () => this.#listeners.delete(listener);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
21
40
|
connectedCallback() {
|
|
22
41
|
this.#centre = readCentre(this.getAttribute("center")) ?? { lon: 0, lat: 0 };
|
|
23
42
|
this.#render();
|
|
@@ -59,14 +78,47 @@ export class ChikyuEarth extends HTMLElement {
|
|
|
59
78
|
const y = this.#view.panY - this.#drawn.panY;
|
|
60
79
|
svg.style.transformOrigin = "50% 50%";
|
|
61
80
|
svg.style.transform = zoom === 1 && x === 0 && y === 0 ? "" : `translate(${x}px, ${y}px) scale(${zoom})`;
|
|
81
|
+
this.#publish(svg.style.transform);
|
|
82
|
+
}
|
|
83
|
+
/** The shell: the picture's holder and the overlay for markers, made once; a redraw replaces only the picture. */
|
|
84
|
+
#ensureShell() {
|
|
85
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
86
|
+
if (!this.#overlay || !root.contains(this.#overlay)) {
|
|
87
|
+
root.innerHTML = `<style>:host{display:block;position:relative}.holder svg{display:block;width:100%;height:auto}.overlay{position:absolute;inset:0;pointer-events:none;transform-origin:50% 50%}</style><div class="holder"></div><div class="overlay"></div>`;
|
|
88
|
+
this.#overlay = root.querySelector(".overlay");
|
|
89
|
+
}
|
|
90
|
+
return root;
|
|
91
|
+
}
|
|
92
|
+
#publish(transform) {
|
|
93
|
+
if (this.#listeners.size === 0) {
|
|
94
|
+
this.#pose = null;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const size = Number(this.getAttribute("size")) || 320;
|
|
98
|
+
const k = (this.clientWidth || size) / size;
|
|
99
|
+
const look = lookAt(this.#centre, this.#drawn);
|
|
100
|
+
const pose = {
|
|
101
|
+
lon: look.lon,
|
|
102
|
+
lat: look.lat,
|
|
103
|
+
radius: globeBaseScale(size) * this.#drawn.zoom * k,
|
|
104
|
+
cx: (size / 2 + this.#drawn.panX) * k,
|
|
105
|
+
cy: (size / 2 + this.#drawn.panY) * k,
|
|
106
|
+
width: size * k,
|
|
107
|
+
height: size * k,
|
|
108
|
+
transform,
|
|
109
|
+
};
|
|
110
|
+
this.#pose = pose;
|
|
111
|
+
for (const listener of this.#listeners)
|
|
112
|
+
listener(pose);
|
|
62
113
|
}
|
|
63
114
|
#render() {
|
|
64
115
|
const size = Number(this.getAttribute("size")) || 320;
|
|
65
116
|
const style = cleanStyle(this.getAttribute("style"));
|
|
66
117
|
const svg = globe({ size, style, centre: this.#centre, view: this.#view, graticule: this.hasAttribute("graticule") });
|
|
67
|
-
const root = this
|
|
68
|
-
root.innerHTML =
|
|
118
|
+
const root = this.#ensureShell();
|
|
119
|
+
root.querySelector(".holder").innerHTML = svg;
|
|
69
120
|
this.#drawn = { ...this.#view };
|
|
121
|
+
this.#publish("");
|
|
70
122
|
}
|
|
71
123
|
}
|
|
72
124
|
if (typeof customElements !== "undefined" && !customElements.get("chikyu-earth")) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface Cluster {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
/** Indices of the points in it. */
|
|
5
|
+
members: number[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Group the points `indices` (into `xs` and `ys`) by the grid cell they fall in. A cell with one point is that point alone; with more it is one cluster at their
|
|
9
|
+
* average. Order is stable: clusters come in the order their first member was seen.
|
|
10
|
+
*/
|
|
11
|
+
export declare function clusterPoints(indices: ArrayLike<number>, count: number, xs: ArrayLike<number>, ys: ArrayLike<number>, cell: number): Cluster[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Crowded markers become one: points closer than a cell on the screen share a cluster. A grid, so thousands of points cost one pass. Pure.
|
|
2
|
+
/**
|
|
3
|
+
* Group the points `indices` (into `xs` and `ys`) by the grid cell they fall in. A cell with one point is that point alone; with more it is one cluster at their
|
|
4
|
+
* average. Order is stable: clusters come in the order their first member was seen.
|
|
5
|
+
*/
|
|
6
|
+
export function clusterPoints(indices, count, xs, ys, cell) {
|
|
7
|
+
const cells = new Map();
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let k = 0; k < count; k += 1) {
|
|
10
|
+
const i = indices[k];
|
|
11
|
+
const key = Math.floor(xs[i] / cell) * 100_003 + Math.floor(ys[i] / cell);
|
|
12
|
+
const found = cells.get(key);
|
|
13
|
+
if (found) {
|
|
14
|
+
const n = found.members.length;
|
|
15
|
+
found.x = (found.x * n + xs[i]) / (n + 1);
|
|
16
|
+
found.y = (found.y * n + ys[i]) / (n + 1);
|
|
17
|
+
found.members.push(i);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const made = { x: xs[i], y: ys[i], members: [i] };
|
|
21
|
+
cells.set(key, made);
|
|
22
|
+
out.push(made);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { attachMarkers, type Markers, type MarkerOptions, type OverlayElement, type OverlayHost, type OverlayPose } from "./layer.ts";
|
|
2
|
+
export { project, projectAll, canvasRatio, type Pose } from "./project.ts";
|
|
3
|
+
export { clusterPoints, type Cluster } from "./cluster.ts";
|
|
4
|
+
export { placeLabels, type LabelItem, type Placement } from "./labels.ts";
|
|
5
|
+
export type { GroupInfo, MarkerEventDetail, MarkerIcon, MarkerInput } from "./types.ts";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Markers on the globe: `attachMarkers(element)` for `<chikyu-earth>` or `<chikyu-earth-realistic>`. The pure parts (projection, clusters, label placement)
|
|
2
|
+
// import on a server.
|
|
3
|
+
export { attachMarkers } from "./layer.js";
|
|
4
|
+
export { project, projectAll, canvasRatio } from "./project.js";
|
|
5
|
+
export { clusterPoints } from "./cluster.js";
|
|
6
|
+
export { placeLabels } from "./labels.js";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface LabelItem {
|
|
2
|
+
/** The dot's centre and radius. */
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
r: number;
|
|
6
|
+
/** The label's box size. */
|
|
7
|
+
w: number;
|
|
8
|
+
h: number;
|
|
9
|
+
/** Higher is placed first. */
|
|
10
|
+
priority: number;
|
|
11
|
+
}
|
|
12
|
+
export interface Placement {
|
|
13
|
+
/** Index into the items given. */
|
|
14
|
+
item: number;
|
|
15
|
+
/** The label box's top-left corner. */
|
|
16
|
+
left: number;
|
|
17
|
+
top: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Place up to `max` labels inside `width` x `height`. Each label is tried to the right of its dot, then to the left, above and below; the first spot that is inside
|
|
21
|
+
* and free of the labels already placed wins. Items are taken highest priority first (a tie keeps the order given). The cost is the number of items times at most
|
|
22
|
+
* four tries against a grid of what is placed.
|
|
23
|
+
*/
|
|
24
|
+
export declare function placeLabels(items: readonly LabelItem[], width: number, height: number, max: number, cell?: number): Placement[];
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Which labels to show: the most important first, each beside its dot, none over another label or a dot's label slot already taken. Pure.
|
|
2
|
+
const GAP = 3;
|
|
3
|
+
/**
|
|
4
|
+
* Place up to `max` labels inside `width` x `height`. Each label is tried to the right of its dot, then to the left, above and below; the first spot that is inside
|
|
5
|
+
* and free of the labels already placed wins. Items are taken highest priority first (a tie keeps the order given). The cost is the number of items times at most
|
|
6
|
+
* four tries against a grid of what is placed.
|
|
7
|
+
*/
|
|
8
|
+
export function placeLabels(items, width, height, max, cell = 64) {
|
|
9
|
+
const order = items.map((_, i) => i).sort((a, b) => items[b].priority - items[a].priority || a - b);
|
|
10
|
+
const grid = new Map();
|
|
11
|
+
const placed = [];
|
|
12
|
+
const keys = (box) => {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (let gx = Math.floor(box.l / cell); gx <= Math.floor(box.r / cell); gx += 1)
|
|
15
|
+
for (let gy = Math.floor(box.t / cell); gy <= Math.floor(box.b / cell); gy += 1)
|
|
16
|
+
out.push(gx * 100_003 + gy);
|
|
17
|
+
return out;
|
|
18
|
+
};
|
|
19
|
+
const free = (box) => {
|
|
20
|
+
for (const key of keys(box))
|
|
21
|
+
for (const other of grid.get(key) ?? [])
|
|
22
|
+
if (box.l < other.r && box.r > other.l && box.t < other.b && box.b > other.t)
|
|
23
|
+
return false;
|
|
24
|
+
return true;
|
|
25
|
+
};
|
|
26
|
+
for (const index of order) {
|
|
27
|
+
if (placed.length >= max)
|
|
28
|
+
break;
|
|
29
|
+
const it = items[index];
|
|
30
|
+
const tries = [
|
|
31
|
+
[it.x + it.r + GAP, it.y - it.h / 2],
|
|
32
|
+
[it.x - it.r - GAP - it.w, it.y - it.h / 2],
|
|
33
|
+
[it.x - it.w / 2, it.y - it.r - GAP - it.h],
|
|
34
|
+
[it.x - it.w / 2, it.y + it.r + GAP],
|
|
35
|
+
];
|
|
36
|
+
for (const [left, top] of tries) {
|
|
37
|
+
const box = { l: left, t: top, r: left + it.w, b: top + it.h };
|
|
38
|
+
if (box.l < 0 || box.t < 0 || box.r > width || box.b > height || !free(box))
|
|
39
|
+
continue;
|
|
40
|
+
for (const key of keys(box)) {
|
|
41
|
+
const list = grid.get(key);
|
|
42
|
+
if (list)
|
|
43
|
+
list.push(box);
|
|
44
|
+
else
|
|
45
|
+
grid.set(key, [box]);
|
|
46
|
+
}
|
|
47
|
+
placed.push({ item: index, left, top });
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return placed;
|
|
52
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type Pose } from "./project.ts";
|
|
2
|
+
import type { GroupInfo, MarkerInput } from "./types.ts";
|
|
3
|
+
/** The view as an element tells it: the Pose, the size of the box it is drawn in, and the CSS transform its picture carries between redraws. */
|
|
4
|
+
export interface OverlayPose extends Pose {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
transform: string;
|
|
8
|
+
}
|
|
9
|
+
/** What an element gives the markers: a box laid over its view, the pose of its last draw, and a way to hear the next ones. */
|
|
10
|
+
export interface OverlayHost {
|
|
11
|
+
readonly overlayRoot: HTMLElement;
|
|
12
|
+
readonly pose: OverlayPose | null;
|
|
13
|
+
subscribe(listener: (pose: OverlayPose) => void): () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface MarkerOptions {
|
|
16
|
+
/** Most labels shown at once (24). */
|
|
17
|
+
maxLabels?: number;
|
|
18
|
+
/** Gather markers closer than this many pixels into a cluster (28); 0 turns clustering off. */
|
|
19
|
+
clusterRadius?: number;
|
|
20
|
+
/** The colour of a marker with none of its own. */
|
|
21
|
+
color?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface Markers {
|
|
24
|
+
add(marker: MarkerInput): void;
|
|
25
|
+
addAll(markers: readonly MarkerInput[]): void;
|
|
26
|
+
update(id: string, patch: Partial<Omit<MarkerInput, "id">>): void;
|
|
27
|
+
remove(id: string): void;
|
|
28
|
+
clear(): void;
|
|
29
|
+
get(id: string): MarkerInput | undefined;
|
|
30
|
+
list(): MarkerInput[];
|
|
31
|
+
readonly size: number;
|
|
32
|
+
groups(): GroupInfo[];
|
|
33
|
+
showGroup(name: string): void;
|
|
34
|
+
hideGroup(name: string): void;
|
|
35
|
+
/** Show it if hidden and hide it if shown (or set it with `force`). Returns whether it is shown. */
|
|
36
|
+
toggleGroup(name: string, force?: boolean): boolean;
|
|
37
|
+
/** Markers and clusters drawn at the last render (for a page or a test). */
|
|
38
|
+
readonly stats: {
|
|
39
|
+
markers: number;
|
|
40
|
+
clusters: number;
|
|
41
|
+
labels: number;
|
|
42
|
+
renders: number;
|
|
43
|
+
};
|
|
44
|
+
detach(): void;
|
|
45
|
+
}
|
|
46
|
+
export type OverlayElement = HTMLElement & {
|
|
47
|
+
overlay: OverlayHost;
|
|
48
|
+
};
|
|
49
|
+
export declare function attachMarkers(element: OverlayElement, options?: MarkerOptions): Markers;
|