@johnmorrisdotca/chikyu-earth 0.5.0 → 0.8.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 +70 -0
- package/NOTICE.md +22 -0
- package/README.md +186 -27
- package/dist/countries.d.ts +11 -0
- package/dist/countries.js +11 -0
- package/dist/element.d.ts +18 -0
- package/dist/element.js +15 -2
- package/dist/globe.d.ts +15 -1
- package/dist/globe.js +15 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -1
- package/dist/markers/cluster.d.ts +27 -0
- package/dist/markers/cluster.js +16 -0
- package/dist/markers/labels.d.ts +40 -0
- package/dist/markers/labels.js +18 -0
- package/dist/markers/layer.d.ts +80 -0
- package/dist/markers/layer.js +21 -0
- package/dist/markers/project.d.ts +63 -2
- package/dist/markers/project.js +52 -2
- package/dist/markers/types.d.ts +48 -3
- package/dist/realistic/earth.d.ts +30 -5
- package/dist/realistic/earth.js +41 -134
- package/dist/realistic/element.d.ts +47 -7
- package/dist/realistic/element.js +48 -44
- package/dist/realistic/gate.d.ts +45 -1
- package/dist/realistic/gate.js +33 -1
- package/dist/realistic/shaders.d.ts +9 -4
- package/dist/realistic/shaders.js +54 -9
- package/dist/realistic/shared.d.ts +23 -0
- package/dist/realistic/shared.js +51 -0
- package/dist/realistic/sun.d.ts +40 -3
- package/dist/realistic/sun.js +40 -3
- package/dist/realistic/textures.d.ts +43 -0
- package/dist/realistic/textures.js +141 -0
- package/dist/realistic/tiers.d.ts +80 -6
- package/dist/realistic/tiers.js +69 -5
- package/dist/rotate.d.ts +59 -4
- package/dist/rotate.js +59 -4
- package/dist/satellites/index.d.ts +3 -0
- package/dist/satellites/index.js +3 -0
- package/dist/satellites/limits.d.ts +22 -0
- package/dist/satellites/limits.js +23 -0
- package/dist/satellites/track.d.ts +50 -0
- package/dist/satellites/track.js +227 -0
- package/dist/satellites/types.d.ts +161 -0
- package/dist/satellites/types.js +3 -0
- package/dist/space/element.d.ts +179 -0
- package/dist/space/element.js +541 -0
- package/dist/space/ephemeris.d.ts +151 -0
- package/dist/space/ephemeris.js +162 -0
- package/dist/space/index.d.ts +3 -0
- package/dist/space/index.js +5 -0
- package/dist/space/satellite-layer.d.ts +53 -0
- package/dist/space/satellite-layer.js +230 -0
- package/dist/space/scale.d.ts +124 -0
- package/dist/space/scale.js +141 -0
- package/dist/space/scene.d.ts +146 -0
- package/dist/space/scene.js +320 -0
- package/dist/styles.d.ts +65 -2
- package/dist/styles.js +43 -2
- package/dist/textures/moon-1024.webp +0 -0
- package/dist/textures/moon-2048.webp +0 -0
- package/dist/types.d.ts +55 -5
- package/dist/view.d.ts +63 -4
- package/dist/view.js +52 -4
- package/package.json +71 -8
package/dist/rotate.d.ts
CHANGED
|
@@ -1,14 +1,69 @@
|
|
|
1
1
|
import type { GeoPoint, Rotation } from "./types.ts";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* One sidereal day — the Earth's turn against the stars, 23 h 56 min 4.0905 s — in milliseconds.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
8
|
+
*
|
|
9
|
+
* console.log(SIDEREAL_DAY_MS / 3_600_000, "hours");
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
3
12
|
export declare const SIDEREAL_DAY_MS = 86164090.5;
|
|
4
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* How far the Earth turns in one millisecond, in degrees.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { DEGREES_PER_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
19
|
+
*
|
|
20
|
+
* console.log(DEGREES_PER_MS * 1000, "degrees a second");
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
5
23
|
export declare const DEGREES_PER_MS: number;
|
|
6
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* How far the Earth has turned after `ms` milliseconds, in degrees.
|
|
26
|
+
*
|
|
27
|
+
* @param ms - A time span in milliseconds.
|
|
28
|
+
* @returns The turn in degrees; a whole {@link SIDEREAL_DAY_MS} is 360.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { SIDEREAL_DAY_MS, spinAt } from "@johnmorrisdotca/chikyu-earth";
|
|
33
|
+
*
|
|
34
|
+
* console.log(spinAt(SIDEREAL_DAY_MS / 4));
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
7
37
|
export declare const spinAt: (ms: number) => number;
|
|
8
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The rotation that puts `centre` in the middle of the disc: d3-geo's rotate is the negative of the point.
|
|
40
|
+
*
|
|
41
|
+
* @param centre - The place to centre on.
|
|
42
|
+
* @returns The `[lambda, phi]` pair for `globe()`'s `rotation` option.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { centreRotation } from "@johnmorrisdotca/chikyu-earth";
|
|
47
|
+
*
|
|
48
|
+
* console.log(centreRotation({ lon: 139.7, lat: 35.7 }));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
9
51
|
export declare const centreRotation: (centre: GeoPoint) => Rotation;
|
|
10
52
|
/**
|
|
11
53
|
* The rotation at `now` for a globe that was centred on `centre` at `start`. The tilt stays; the spin advances at the
|
|
12
54
|
* Earth's own rate, so the disc turns as the real Earth does and the centred place drifts with it.
|
|
55
|
+
*
|
|
56
|
+
* @param centre - The place the globe was centred on at `start`.
|
|
57
|
+
* @param now - The time to draw for, in milliseconds.
|
|
58
|
+
* @param start - The time the globe was centred, in the same milliseconds as `now`.
|
|
59
|
+
* @returns The `[lambda, phi]` rotation at `now`.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* import { rotationAt, SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
64
|
+
*
|
|
65
|
+
* const tokyo = { lon: 139.7, lat: 35.7 };
|
|
66
|
+
* console.log(rotationAt(tokyo, SIDEREAL_DAY_MS / 2, 0));
|
|
67
|
+
* ```
|
|
13
68
|
*/
|
|
14
69
|
export declare function rotationAt(centre: GeoPoint, now: number, start: number): Rotation;
|
package/dist/rotate.js
CHANGED
|
@@ -1,14 +1,69 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* One sidereal day — the Earth's turn against the stars, 23 h 56 min 4.0905 s — in milliseconds.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
7
|
+
*
|
|
8
|
+
* console.log(SIDEREAL_DAY_MS / 3_600_000, "hours");
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
2
11
|
export const SIDEREAL_DAY_MS = 86_164_090.5;
|
|
3
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* How far the Earth turns in one millisecond, in degrees.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { DEGREES_PER_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
18
|
+
*
|
|
19
|
+
* console.log(DEGREES_PER_MS * 1000, "degrees a second");
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
4
22
|
export const DEGREES_PER_MS = 360 / SIDEREAL_DAY_MS;
|
|
5
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* How far the Earth has turned after `ms` milliseconds, in degrees.
|
|
25
|
+
*
|
|
26
|
+
* @param ms - A time span in milliseconds.
|
|
27
|
+
* @returns The turn in degrees; a whole {@link SIDEREAL_DAY_MS} is 360.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* import { SIDEREAL_DAY_MS, spinAt } from "@johnmorrisdotca/chikyu-earth";
|
|
32
|
+
*
|
|
33
|
+
* console.log(spinAt(SIDEREAL_DAY_MS / 4));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
6
36
|
export const spinAt = (ms) => ms * DEGREES_PER_MS;
|
|
7
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* The rotation that puts `centre` in the middle of the disc: d3-geo's rotate is the negative of the point.
|
|
39
|
+
*
|
|
40
|
+
* @param centre - The place to centre on.
|
|
41
|
+
* @returns The `[lambda, phi]` pair for `globe()`'s `rotation` option.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { centreRotation } from "@johnmorrisdotca/chikyu-earth";
|
|
46
|
+
*
|
|
47
|
+
* console.log(centreRotation({ lon: 139.7, lat: 35.7 }));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
8
50
|
export const centreRotation = (centre) => [-centre.lon, -centre.lat];
|
|
9
51
|
/**
|
|
10
52
|
* The rotation at `now` for a globe that was centred on `centre` at `start`. The tilt stays; the spin advances at the
|
|
11
53
|
* Earth's own rate, so the disc turns as the real Earth does and the centred place drifts with it.
|
|
54
|
+
*
|
|
55
|
+
* @param centre - The place the globe was centred on at `start`.
|
|
56
|
+
* @param now - The time to draw for, in milliseconds.
|
|
57
|
+
* @param start - The time the globe was centred, in the same milliseconds as `now`.
|
|
58
|
+
* @returns The `[lambda, phi]` rotation at `now`.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```ts
|
|
62
|
+
* import { rotationAt, SIDEREAL_DAY_MS } from "@johnmorrisdotca/chikyu-earth";
|
|
63
|
+
*
|
|
64
|
+
* const tokyo = { lon: 139.7, lat: 35.7 };
|
|
65
|
+
* console.log(rotationAt(tokyo, SIDEREAL_DAY_MS / 2, 0));
|
|
66
|
+
* ```
|
|
12
67
|
*/
|
|
13
68
|
export function rotationAt(centre, now, start) {
|
|
14
69
|
const [lambda, phi] = centreRotation(centre);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// The satellites' pure parts: orbital elements in, positions out (SGP4 by satellite.js, an optional peer). They import on a server. The space view takes a group with `setSatellites`.
|
|
2
|
+
export { createSatellites } from "./track.js";
|
|
3
|
+
export { DEFAULT_MAX_AGE_DAYS, MAX_SATELLITE_SPEED } from "./limits.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How old, in days, an element set may be at the moment shown before the satellite is left out: SGP4's error grows by a few kilometres a day, so a fortnight is as far as a picture should trust.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { DEFAULT_MAX_AGE_DAYS } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
7
|
+
*
|
|
8
|
+
* console.log(DEFAULT_MAX_AGE_DAYS);
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare const DEFAULT_MAX_AGE_DAYS = 14;
|
|
12
|
+
/**
|
|
13
|
+
* The fastest clock, in game seconds a second, at which the space view still places a group of satellites: above it the two moments SGP4 is run at would be too far apart to interpolate, so the group is hidden rather than drawn wrongly. A satellite that was chosen with `selectSatellite` or ridden along is still placed exactly, one SGP4 call a frame.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { MAX_SATELLITE_SPEED } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
18
|
+
*
|
|
19
|
+
* console.log(MAX_SATELLITE_SPEED);
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare const MAX_SATELLITE_SPEED = 60;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The satellites' two limits, apart from the SGP4 code so the space view can read them without needing satellite.js.
|
|
2
|
+
/**
|
|
3
|
+
* How old, in days, an element set may be at the moment shown before the satellite is left out: SGP4's error grows by a few kilometres a day, so a fortnight is as far as a picture should trust.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { DEFAULT_MAX_AGE_DAYS } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
8
|
+
*
|
|
9
|
+
* console.log(DEFAULT_MAX_AGE_DAYS);
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_MAX_AGE_DAYS = 14;
|
|
13
|
+
/**
|
|
14
|
+
* The fastest clock, in game seconds a second, at which the space view still places a group of satellites: above it the two moments SGP4 is run at would be too far apart to interpolate, so the group is hidden rather than drawn wrongly. A satellite that was chosen with `selectSatellite` or ridden along is still placed exactly, one SGP4 call a frame.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { MAX_SATELLITE_SPEED } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
19
|
+
*
|
|
20
|
+
* console.log(MAX_SATELLITE_SPEED);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const MAX_SATELLITE_SPEED = 60;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ElementSet, Satellites } from "./types.ts";
|
|
2
|
+
/**
|
|
3
|
+
* What `createSatellites` can be told.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import type { SatellitesOptions } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
8
|
+
*
|
|
9
|
+
* const options: SatellitesOptions = { maxAgeDays: 7 };
|
|
10
|
+
* console.log(options.maxAgeDays);
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export interface SatellitesOptions {
|
|
14
|
+
/** The oldest an element set may be at the moment shown, in days (14 by default). */
|
|
15
|
+
maxAgeDays?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Make a group of satellites from their element sets. Each starts SGP4 once here; one that does not (bad numbers, decayed at its own epoch) is listed in `refused` and left out.
|
|
19
|
+
* It needs satellite.js (MIT, an optional peer dependency) and imports in a browser or on a server.
|
|
20
|
+
*
|
|
21
|
+
* @param sets - Element sets in CelesTrak's JSON format (`ElementSet`), fetched by your page.
|
|
22
|
+
* @param options - `maxAgeDays`: the oldest an element set may be at the moment shown.
|
|
23
|
+
* @returns The group: `at(ms, out)` for positions, `exact`, `orbit`, `info`, and counters.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createSatellites } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
28
|
+
*
|
|
29
|
+
* const group = createSatellites([
|
|
30
|
+
* {
|
|
31
|
+
* OBJECT_NAME: "ISS (ZARYA)",
|
|
32
|
+
* OBJECT_ID: "1998-067A",
|
|
33
|
+
* EPOCH: "2026-11-02T10:25:21.580320",
|
|
34
|
+
* MEAN_MOTION: 15.49150046,
|
|
35
|
+
* ECCENTRICITY: 0.0007139,
|
|
36
|
+
* INCLINATION: 51.6332,
|
|
37
|
+
* RA_OF_ASC_NODE: 335.9757,
|
|
38
|
+
* ARG_OF_PERICENTER: 339.3739,
|
|
39
|
+
* MEAN_ANOMALY: 20.6961,
|
|
40
|
+
* NORAD_CAT_ID: 25544,
|
|
41
|
+
* BSTAR: 0.00012,
|
|
42
|
+
* MEAN_MOTION_DOT: 0.0001,
|
|
43
|
+
* MEAN_MOTION_DDOT: 0,
|
|
44
|
+
* },
|
|
45
|
+
* ]);
|
|
46
|
+
* const noon = Date.UTC(2026, 10, 2, 12);
|
|
47
|
+
* console.log(group.count, group.info(0, noon)?.altitudeKm.toFixed(0));
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function createSatellites(sets: readonly ElementSet[], options?: SatellitesOptions): Satellites;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// SGP4 for a group of satellites, kept cheap: the whole group is run at two moments a step apart and every position in between is an interpolation, so the cost is one batch a step
|
|
2
|
+
// (about once a second), never one a frame. satellite.js (MIT) does the SGP4 itself; this file never touches the screen and imports on a server.
|
|
3
|
+
import { json2satrec, sgp4 } from "satellite.js";
|
|
4
|
+
import { DEFAULT_MAX_AGE_DAYS } from "./limits.js";
|
|
5
|
+
/** The Earth's mean radius in kilometres: the unit of every position here (the same as the space view's). */
|
|
6
|
+
const EARTH_RADIUS_KM = 6371;
|
|
7
|
+
const DAY_MS = 86_400_000;
|
|
8
|
+
const MIN_STEP_MS = 1000;
|
|
9
|
+
const MAX_STEP_MS = 60_000;
|
|
10
|
+
const epochOf = (text) => Date.parse(/[zZ]|[+-]\d\d:?\d\d$/.test(text) ? text : `${text}Z`);
|
|
11
|
+
/**
|
|
12
|
+
* Make a group of satellites from their element sets. Each starts SGP4 once here; one that does not (bad numbers, decayed at its own epoch) is listed in `refused` and left out.
|
|
13
|
+
* It needs satellite.js (MIT, an optional peer dependency) and imports in a browser or on a server.
|
|
14
|
+
*
|
|
15
|
+
* @param sets - Element sets in CelesTrak's JSON format (`ElementSet`), fetched by your page.
|
|
16
|
+
* @param options - `maxAgeDays`: the oldest an element set may be at the moment shown.
|
|
17
|
+
* @returns The group: `at(ms, out)` for positions, `exact`, `orbit`, `info`, and counters.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { createSatellites } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
22
|
+
*
|
|
23
|
+
* const group = createSatellites([
|
|
24
|
+
* {
|
|
25
|
+
* OBJECT_NAME: "ISS (ZARYA)",
|
|
26
|
+
* OBJECT_ID: "1998-067A",
|
|
27
|
+
* EPOCH: "2026-11-02T10:25:21.580320",
|
|
28
|
+
* MEAN_MOTION: 15.49150046,
|
|
29
|
+
* ECCENTRICITY: 0.0007139,
|
|
30
|
+
* INCLINATION: 51.6332,
|
|
31
|
+
* RA_OF_ASC_NODE: 335.9757,
|
|
32
|
+
* ARG_OF_PERICENTER: 339.3739,
|
|
33
|
+
* MEAN_ANOMALY: 20.6961,
|
|
34
|
+
* NORAD_CAT_ID: 25544,
|
|
35
|
+
* BSTAR: 0.00012,
|
|
36
|
+
* MEAN_MOTION_DOT: 0.0001,
|
|
37
|
+
* MEAN_MOTION_DDOT: 0,
|
|
38
|
+
* },
|
|
39
|
+
* ]);
|
|
40
|
+
* const noon = Date.UTC(2026, 10, 2, 12);
|
|
41
|
+
* console.log(group.count, group.info(0, noon)?.altitudeKm.toFixed(0));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function createSatellites(sets, options = {}) {
|
|
45
|
+
const maxAgeDays = options.maxAgeDays ?? DEFAULT_MAX_AGE_DAYS;
|
|
46
|
+
const maxAgeMs = maxAgeDays * DAY_MS;
|
|
47
|
+
const recs = [];
|
|
48
|
+
const usable = [];
|
|
49
|
+
const epochs = [];
|
|
50
|
+
const refused = [];
|
|
51
|
+
for (const set of sets) {
|
|
52
|
+
const epoch = epochOf(set.EPOCH);
|
|
53
|
+
const refuse = (reason) => void refused.push({ name: set.OBJECT_NAME, noradId: set.NORAD_CAT_ID, reason });
|
|
54
|
+
if (!Number.isFinite(epoch)) {
|
|
55
|
+
refuse("the epoch is not a date");
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const rec = json2satrec(set);
|
|
60
|
+
const start = sgp4(rec, 0);
|
|
61
|
+
if (!start || !start.position || !start.velocity || rec.error !== 0) {
|
|
62
|
+
refuse("SGP4 could not start from these elements");
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
recs.push(rec);
|
|
66
|
+
usable.push(set);
|
|
67
|
+
epochs.push(epoch);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
refuse(error instanceof Error ? error.message : "SGP4 could not start from these elements");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const n = recs.length;
|
|
74
|
+
let a = new Float32Array(n * 3);
|
|
75
|
+
let b = new Float32Array(n * 3);
|
|
76
|
+
let okA = new Uint8Array(n);
|
|
77
|
+
let okB = new Uint8Array(n);
|
|
78
|
+
const shown = new Uint8Array(n);
|
|
79
|
+
let aMs = 0;
|
|
80
|
+
let bMs = 0;
|
|
81
|
+
let ready = false;
|
|
82
|
+
let stepMs = MIN_STEP_MS;
|
|
83
|
+
let propagations = 0;
|
|
84
|
+
let placed = 0;
|
|
85
|
+
const timing = { lastBatchMs: 0, maxBatchMs: 0 };
|
|
86
|
+
/** SGP4 for the whole group at one moment, into `buffer` in the space view's axes (TEME x, z, -y: x to the vernal equinox, y north). */
|
|
87
|
+
const sample = (buffer, ok, ms) => {
|
|
88
|
+
const started = performance.now();
|
|
89
|
+
for (let i = 0; i < n; i += 1) {
|
|
90
|
+
const epoch = epochs[i];
|
|
91
|
+
if (Math.abs(ms - epoch) > maxAgeMs) {
|
|
92
|
+
ok[i] = 0;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
propagations += 1;
|
|
96
|
+
const result = sgp4(recs[i], (ms - epoch) / 60_000);
|
|
97
|
+
const p = result && result.position;
|
|
98
|
+
if (!p || !Number.isFinite(p.x)) {
|
|
99
|
+
ok[i] = 0;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
ok[i] = 1;
|
|
103
|
+
buffer[i * 3] = p.x / EARTH_RADIUS_KM;
|
|
104
|
+
buffer[i * 3 + 1] = p.z / EARTH_RADIUS_KM;
|
|
105
|
+
buffer[i * 3 + 2] = -p.y / EARTH_RADIUS_KM;
|
|
106
|
+
}
|
|
107
|
+
timing.lastBatchMs = performance.now() - started;
|
|
108
|
+
if (timing.lastBatchMs > timing.maxBatchMs)
|
|
109
|
+
timing.maxBatchMs = timing.lastBatchMs;
|
|
110
|
+
};
|
|
111
|
+
const exactAt = (index, ms, out) => {
|
|
112
|
+
const epoch = epochs[index];
|
|
113
|
+
if (epoch === undefined || Math.abs(ms - epoch) > maxAgeMs)
|
|
114
|
+
return false;
|
|
115
|
+
propagations += 1;
|
|
116
|
+
const result = sgp4(recs[index], (ms - epoch) / 60_000);
|
|
117
|
+
if (!result || !result.position || !result.velocity || !Number.isFinite(result.position.x))
|
|
118
|
+
return false;
|
|
119
|
+
out[0] = result.position.x / EARTH_RADIUS_KM;
|
|
120
|
+
out[1] = result.position.z / EARTH_RADIUS_KM;
|
|
121
|
+
out[2] = -result.position.y / EARTH_RADIUS_KM;
|
|
122
|
+
out[3] = result.velocity.x;
|
|
123
|
+
out[4] = result.velocity.z;
|
|
124
|
+
out[5] = -result.velocity.y;
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
return {
|
|
128
|
+
count: n,
|
|
129
|
+
sets: usable,
|
|
130
|
+
refused,
|
|
131
|
+
maxAgeDays,
|
|
132
|
+
get propagations() {
|
|
133
|
+
return propagations;
|
|
134
|
+
},
|
|
135
|
+
timing,
|
|
136
|
+
get placed() {
|
|
137
|
+
return placed;
|
|
138
|
+
},
|
|
139
|
+
setStep(ms) {
|
|
140
|
+
const next = Math.min(MAX_STEP_MS, Math.max(MIN_STEP_MS, Math.round(ms)));
|
|
141
|
+
if (next !== stepMs) {
|
|
142
|
+
stepMs = next;
|
|
143
|
+
ready = false;
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
at(ms, out) {
|
|
147
|
+
if (n === 0)
|
|
148
|
+
return;
|
|
149
|
+
if (!ready || ms < aMs || ms > bMs) {
|
|
150
|
+
if (ready && ms > bMs && ms - bMs < stepMs) {
|
|
151
|
+
[a, b] = [b, a];
|
|
152
|
+
[okA, okB] = [okB, okA];
|
|
153
|
+
aMs = bMs;
|
|
154
|
+
bMs = aMs + stepMs;
|
|
155
|
+
sample(b, okB, bMs);
|
|
156
|
+
}
|
|
157
|
+
else if (ready && ms < aMs && aMs - ms < stepMs) {
|
|
158
|
+
[a, b] = [b, a];
|
|
159
|
+
[okA, okB] = [okB, okA];
|
|
160
|
+
bMs = aMs;
|
|
161
|
+
aMs = bMs - stepMs;
|
|
162
|
+
sample(a, okA, aMs);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
aMs = ms;
|
|
166
|
+
bMs = ms + stepMs;
|
|
167
|
+
sample(a, okA, aMs);
|
|
168
|
+
sample(b, okB, bMs);
|
|
169
|
+
ready = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const t = (ms - aMs) / (bMs - aMs);
|
|
173
|
+
let count = 0;
|
|
174
|
+
for (let i = 0; i < n; i += 1) {
|
|
175
|
+
const k = i * 3;
|
|
176
|
+
if (okA[i] === 1 && okB[i] === 1) {
|
|
177
|
+
out[k] = a[k] + (b[k] - a[k]) * t;
|
|
178
|
+
out[k + 1] = a[k + 1] + (b[k + 1] - a[k + 1]) * t;
|
|
179
|
+
out[k + 2] = a[k + 2] + (b[k + 2] - a[k + 2]) * t;
|
|
180
|
+
shown[i] = 1;
|
|
181
|
+
count += 1;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
out[k] = 0;
|
|
185
|
+
out[k + 1] = 0;
|
|
186
|
+
out[k + 2] = 0;
|
|
187
|
+
shown[i] = 0;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
placed = count;
|
|
191
|
+
},
|
|
192
|
+
visible: (index) => shown[index] === 1,
|
|
193
|
+
exact: exactAt,
|
|
194
|
+
orbit(index, ms, points) {
|
|
195
|
+
const path = new Float32Array((points + 1) * 3);
|
|
196
|
+
const period = 1440 / (usable[index]?.MEAN_MOTION || 15);
|
|
197
|
+
const one = new Float64Array(6);
|
|
198
|
+
for (let k = 0; k < points; k += 1) {
|
|
199
|
+
if (exactAt(index, ms + (k / points) * period * 60_000, one))
|
|
200
|
+
path.set(one.subarray(0, 3), k * 3);
|
|
201
|
+
else if (k > 0)
|
|
202
|
+
path.copyWithin(k * 3, (k - 1) * 3, k * 3);
|
|
203
|
+
}
|
|
204
|
+
path.copyWithin(points * 3, 0, 3);
|
|
205
|
+
return path;
|
|
206
|
+
},
|
|
207
|
+
info(index, ms) {
|
|
208
|
+
const set = usable[index];
|
|
209
|
+
const one = new Float64Array(6);
|
|
210
|
+
if (!set || !exactAt(index, ms, one))
|
|
211
|
+
return undefined;
|
|
212
|
+
const info = {
|
|
213
|
+
index,
|
|
214
|
+
name: set.OBJECT_NAME,
|
|
215
|
+
noradId: set.NORAD_CAT_ID,
|
|
216
|
+
objectId: set.OBJECT_ID,
|
|
217
|
+
epochMs: epochs[index],
|
|
218
|
+
ageDays: (ms - epochs[index]) / DAY_MS,
|
|
219
|
+
altitudeKm: Math.hypot(one[0], one[1], one[2]) * EARTH_RADIUS_KM - EARTH_RADIUS_KM,
|
|
220
|
+
speedKmS: Math.hypot(one[3], one[4], one[5]),
|
|
221
|
+
periodMinutes: 1440 / set.MEAN_MOTION,
|
|
222
|
+
inclinationDeg: set.INCLINATION,
|
|
223
|
+
};
|
|
224
|
+
return info;
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One satellite's orbital elements in the "GP" format that CelesTrak and Space-Track both serve as JSON (CCSDS OMM, one object per satellite). Only these fields are read.
|
|
3
|
+
* The package fetches nothing: your page gets the elements (see the satellites guide for the rules of the sources) and passes them in.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* import type { ElementSet } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
8
|
+
*
|
|
9
|
+
* const iss: ElementSet = {
|
|
10
|
+
* OBJECT_NAME: "ISS (ZARYA)",
|
|
11
|
+
* OBJECT_ID: "1998-067A",
|
|
12
|
+
* EPOCH: "2026-11-02T10:25:21.580320",
|
|
13
|
+
* MEAN_MOTION: 15.49150046,
|
|
14
|
+
* ECCENTRICITY: 0.0007139,
|
|
15
|
+
* INCLINATION: 51.6332,
|
|
16
|
+
* RA_OF_ASC_NODE: 335.9757,
|
|
17
|
+
* ARG_OF_PERICENTER: 339.3739,
|
|
18
|
+
* MEAN_ANOMALY: 20.6961,
|
|
19
|
+
* NORAD_CAT_ID: 25544,
|
|
20
|
+
* BSTAR: 0.00012,
|
|
21
|
+
* MEAN_MOTION_DOT: 0.0001,
|
|
22
|
+
* MEAN_MOTION_DDOT: 0,
|
|
23
|
+
* };
|
|
24
|
+
* console.log(iss.OBJECT_NAME);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export interface ElementSet {
|
|
28
|
+
OBJECT_NAME: string;
|
|
29
|
+
OBJECT_ID?: string;
|
|
30
|
+
/** The moment the elements describe, ISO 8601 in UTC (a trailing `Z` is optional). */
|
|
31
|
+
EPOCH: string;
|
|
32
|
+
/** Revolutions a day. */
|
|
33
|
+
MEAN_MOTION: number;
|
|
34
|
+
ECCENTRICITY: number;
|
|
35
|
+
/** Degrees. */
|
|
36
|
+
INCLINATION: number;
|
|
37
|
+
/** Degrees. */
|
|
38
|
+
RA_OF_ASC_NODE: number;
|
|
39
|
+
/** Degrees. */
|
|
40
|
+
ARG_OF_PERICENTER: number;
|
|
41
|
+
/** Degrees. */
|
|
42
|
+
MEAN_ANOMALY: number;
|
|
43
|
+
NORAD_CAT_ID: number;
|
|
44
|
+
BSTAR: number;
|
|
45
|
+
MEAN_MOTION_DOT: number;
|
|
46
|
+
MEAN_MOTION_DDOT: number;
|
|
47
|
+
EPHEMERIS_TYPE?: number;
|
|
48
|
+
CLASSIFICATION_TYPE?: string;
|
|
49
|
+
ELEMENT_SET_NO?: number;
|
|
50
|
+
REV_AT_EPOCH?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* What a page can tell a reader about one satellite at a moment: its name, its number, how old its elements are, how high and how fast it is.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* import type { SatelliteInfo } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
58
|
+
*
|
|
59
|
+
* const describe = (info: SatelliteInfo): string => `${info.name}: ${info.altitudeKm.toFixed(0)} km up, ${info.speedKmS.toFixed(2)} km/s`;
|
|
60
|
+
* console.log(typeof describe);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export interface SatelliteInfo {
|
|
64
|
+
/** Where it is in its group. */
|
|
65
|
+
index: number;
|
|
66
|
+
name: string;
|
|
67
|
+
/** The NORAD catalogue number. */
|
|
68
|
+
noradId: number;
|
|
69
|
+
/** The international designator (for example `1998-067A`), when the elements carry one. */
|
|
70
|
+
objectId: string | undefined;
|
|
71
|
+
/** The moment of its elements, in milliseconds since the Unix epoch. */
|
|
72
|
+
epochMs: number;
|
|
73
|
+
/** Days between the elements' moment and the moment asked about (negative before it). */
|
|
74
|
+
ageDays: number;
|
|
75
|
+
/** Height above the mean Earth radius (6371 km) at that moment. */
|
|
76
|
+
altitudeKm: number;
|
|
77
|
+
/** Speed against the stars, kilometres a second. */
|
|
78
|
+
speedKmS: number;
|
|
79
|
+
/** One revolution, minutes. */
|
|
80
|
+
periodMinutes: number;
|
|
81
|
+
/** Degrees. */
|
|
82
|
+
inclinationDeg: number;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* A satellite chosen from a group: which group and which place in it.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* import type { SatelliteRef } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
90
|
+
*
|
|
91
|
+
* const first: SatelliteRef = { group: "stations", index: 0 };
|
|
92
|
+
* console.log(first.group);
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export interface SatelliteRef {
|
|
96
|
+
group: string;
|
|
97
|
+
index: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A satellite the group could not use, and why: its elements did not start SGP4 (bad numbers), or it had decayed at its own epoch.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* import type { RefusedElementSet } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
105
|
+
*
|
|
106
|
+
* const show = (one: RefusedElementSet): string => `${one.name}: ${one.reason}`;
|
|
107
|
+
* console.log(typeof show);
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
export interface RefusedElementSet {
|
|
111
|
+
name: string;
|
|
112
|
+
noradId: number;
|
|
113
|
+
reason: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A group of satellites, ready to give positions for any moment: what `createSatellites` returns, and what `<chikyu-space>` takes in `setSatellites`.
|
|
117
|
+
* Positions are in Earth radii (6371 km) from the Earth's centre, in the space view's axes (x to the vernal equinox, y north); a satellite whose elements
|
|
118
|
+
* are older than `maxAgeDays` at the moment asked about is not placed (its place stays at the centre and `visible(index)` is false).
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* import type { Satellites } from "@johnmorrisdotca/chikyu-earth/satellites";
|
|
123
|
+
*
|
|
124
|
+
* const first = (group: Satellites): string => group.sets[0]?.OBJECT_NAME ?? "none";
|
|
125
|
+
* console.log(typeof first);
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
export interface Satellites {
|
|
129
|
+
/** Satellites that started SGP4. */
|
|
130
|
+
readonly count: number;
|
|
131
|
+
/** Their elements, in the order of their places. */
|
|
132
|
+
readonly sets: readonly ElementSet[];
|
|
133
|
+
/** The ones left out, with the reason. */
|
|
134
|
+
readonly refused: readonly RefusedElementSet[];
|
|
135
|
+
/** The oldest an element set may be at the moment shown, in days. */
|
|
136
|
+
readonly maxAgeDays: number;
|
|
137
|
+
/** SGP4 calls made so far, for a page or a test to count. */
|
|
138
|
+
readonly propagations: number;
|
|
139
|
+
/** Milliseconds the last batch of SGP4 took, and the longest so far. */
|
|
140
|
+
readonly timing: {
|
|
141
|
+
lastBatchMs: number;
|
|
142
|
+
maxBatchMs: number;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Fill `out` (3 numbers a satellite) with the positions at `ms`: SGP4 is run for the whole group at two moments `step` apart and the positions between are interpolated, so a
|
|
146
|
+
* steady clock costs one batch a step and every call in between is arithmetic.
|
|
147
|
+
*/
|
|
148
|
+
at(ms: number, out: Float32Array): void;
|
|
149
|
+
/** True if satellite `index` was placed by the last `at` (its elements are not too old at that moment). */
|
|
150
|
+
visible(index: number): boolean;
|
|
151
|
+
/** How many of them the last `at` placed. */
|
|
152
|
+
readonly placed: number;
|
|
153
|
+
/** The game time between the two moments SGP4 is run at (1000 to 60000 ms); the space view sets it from the clock's speed. */
|
|
154
|
+
setStep(ms: number): void;
|
|
155
|
+
/** One satellite at exactly `ms` (no interpolation): position in Earth radii and velocity in km/s into `out` (6 numbers). False if its elements are too old or SGP4 gave up. */
|
|
156
|
+
exact(index: number, ms: number, out: Float64Array): boolean;
|
|
157
|
+
/** The path of satellite `index` for one revolution from `ms`, `points` + 1 points in order along it (the last on the first), into a new array of Earth radii. */
|
|
158
|
+
orbit(index: number, ms: number, points: number): Float32Array;
|
|
159
|
+
/** Facts about one satellite at `ms`; undefined if it cannot be placed then. */
|
|
160
|
+
info(index: number, ms: number): SatelliteInfo | undefined;
|
|
161
|
+
}
|