The date field's description read "Each moon's and dwarf planet's card says how far its orbit strays from 1950 to 2100." Pluto is a dwarf planet on its card, but it moves on Standish's planet elements, and the ETL measures only moons and the SBDB bodies against Horizons: its card ends "Orbit: JPL approximate mean elements (Standish), fit for 3000 BC to AD 3000." and gives no stray figure. In bodies.json, Ceres (7.2), Eris (0.1), Haumea (0.4), Makemake (0.3) and every moon carry one; Pluto does not. The description now reads "AD 1 to AD 3000, where the planets' and Pluto's elements hold. Each moon's card, and Ceres's, Eris's, Haumea's and Makemake's, says how far its orbit strays from 1950 to 2100." The CLOCK_WINDOW comment and the scene's note comment make the same distinction. Test: hud-dock 'opens the date field on the clock's date...' asserts the new sentence. Control: putting the old sentence back fails that test only (1 failed, 836 passed). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
138 lines
5.8 KiB
TypeScript
138 lines
5.8 KiB
TypeScript
import { Injectable, signal } from '@angular/core';
|
|
|
|
import { dateToJulianDate } from '../astro/constants';
|
|
|
|
/**
|
|
* How fast the map's clock runs, in seconds of sky per second of wall clock.
|
|
*
|
|
* The map is built on propagated orbits and published rotation periods, both of which are
|
|
* functions of a date — so the only thing standing between it and a working orrery is the number
|
|
* on this list. At real time nothing appears to move: Earth turns 15 degrees an hour and takes a
|
|
* year to go round, and a reader watching for a minute sees a still picture.
|
|
*
|
|
* An hour a second is the rate at which rotation reads — Jupiter turns once every ten seconds of
|
|
* watching. A day a second is the rate at which the inner planets read. A month a second carries
|
|
* the outer ones, at which point the inner four are a blur, which is honest: that is what the
|
|
* solar system does.
|
|
*/
|
|
export const TIME_RATES = [
|
|
{ label: 'Real time', secondsPerSecond: 1 },
|
|
{ label: '1 h/s', secondsPerSecond: 3600 },
|
|
{ label: '1 d/s', secondsPerSecond: 86_400 },
|
|
{ label: '1 mo/s', secondsPerSecond: 2_629_800 },
|
|
] as const;
|
|
|
|
const MS_PER_DAY = 86_400_000;
|
|
const JULIAN_DATE_AT_EPOCH = 2440587.5;
|
|
|
|
/**
|
|
* The dates the clock can be set to, as `datetime-local` values read as UTC.
|
|
*
|
|
* The end is where Standish's Table 2, the mean elements that carry the planets, stops being
|
|
* fitted: it covers 3000 BC to AD 3000, and every planet was within 0.29 degrees of Horizons at
|
|
* each date measured out to 3000. The start is not the fit's but the date input's, which cannot
|
|
* go before 0001-01-01. Both are proleptic Gregorian, as a `Date` is, so before 1582 they part from
|
|
* the Julian-calendar dates history gives: two days behind them at AD 1, level from AD 200 to 300,
|
|
* ten days ahead by 1582. Pluto, on Standish's elements too, holds with them; the moons and the four
|
|
* dwarf planets from the SBDB hold for far less of it: each of their cards says how
|
|
* far its orbit strays from Horizons from 1950 to 2100 (Ceres 7.1 degrees there, 11.6 by 2200 and
|
|
* 39 by 1600).
|
|
*/
|
|
export const CLOCK_WINDOW = { min: '0001-01-01T00:00', max: '3000-01-01T00:00' } as const;
|
|
const WINDOW_MS = {
|
|
min: Date.parse(`${CLOCK_WINDOW.min}Z`),
|
|
max: Date.parse(`${CLOCK_WINDOW.max}Z`),
|
|
};
|
|
const WINDOW_JD = {
|
|
min: WINDOW_MS.min / MS_PER_DAY + JULIAN_DATE_AT_EPOCH,
|
|
max: WINDOW_MS.max / MS_PER_DAY + JULIAN_DATE_AT_EPOCH,
|
|
};
|
|
|
|
/**
|
|
* The date the map is drawn for.
|
|
*
|
|
* Read every frame rather than held in a signal: it changes continuously, and a signal that
|
|
* changed sixty times a second would ask the whole HUD to re-render for a number nothing is
|
|
* watching. The rate *is* a signal, since a reader sets it and the controls read it back.
|
|
*
|
|
* Changing the rate re-anchors instead of rewinding: the date carries on from where it had got
|
|
* to, so speeding up and slowing down never jumps the sky. A negative rate runs the same clock
|
|
* backwards: every orbit and every rotation is a function of the date, so going back is the same
|
|
* sum with the sign turned.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class TimeStore {
|
|
readonly rate = signal<number>(TIME_RATES[0].secondsPerSecond);
|
|
/**
|
|
* Whether the map is drawn for the present. Not the same as a rate of one: after an excursion at
|
|
* a month a second, real time carries on from months ahead, and the map is still away from now.
|
|
*/
|
|
readonly atNow = signal(true);
|
|
|
|
private anchorJd = dateToJulianDate();
|
|
private anchorWallMs = Date.now();
|
|
|
|
/**
|
|
* Julian date for this instant, at the rate the reader chose, held to {@link CLOCK_WINDOW}: a
|
|
* clock run past either end stops there, at real time turned back into the window, as if the
|
|
* reader had set that date. Unheld, a month a second carried it past AD 3000, where the planets'
|
|
* elements were never fitted, and before AD 1, where `toISOString` writes a six-digit year the
|
|
* date strip, the note and the date field cut in the wrong places ("-000001-12-01 00 UTC").
|
|
*/
|
|
julianDate(): number {
|
|
const jd = this.anchorJd + ((Date.now() - this.anchorWallMs) * this.rate()) / MS_PER_DAY;
|
|
if (jd >= WINDOW_JD.min && jd <= WINDOW_JD.max) {
|
|
return jd;
|
|
}
|
|
this.anchorJd = jd < WINDOW_JD.min ? WINDOW_JD.min : WINDOW_JD.max;
|
|
this.anchorWallMs = Date.now();
|
|
this.rate.set(jd < WINDOW_JD.min ? 1 : -1);
|
|
return this.anchorJd;
|
|
}
|
|
|
|
/**
|
|
* The same instant as a date, for anything that prints it.
|
|
*
|
|
* Rounded to the millisecond, which is all a `Date` holds: a Julian date near 2 461 000 has
|
|
* about a twentieth of a millisecond of resolution left in a double, and `new Date` truncates
|
|
* what is left rather than rounding it, so ten seconds came back as 9.999.
|
|
*/
|
|
date(): Date {
|
|
return new Date(Math.round((this.julianDate() - JULIAN_DATE_AT_EPOCH) * MS_PER_DAY));
|
|
}
|
|
|
|
setRate(secondsPerSecond: number): void {
|
|
this.anchorJd = this.julianDate();
|
|
this.anchorWallMs = Date.now();
|
|
this.rate.set(secondsPerSecond);
|
|
if (secondsPerSecond !== 1) {
|
|
this.atNow.set(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Jumps the clock to a date, from which it carries on at whatever rate it was running at.
|
|
* Refuses one outside {@link CLOCK_WINDOW}, rather than draw planets where elements that were
|
|
* never fitted there put them.
|
|
*/
|
|
setDate(date: Date): boolean {
|
|
const ms = date.getTime();
|
|
// Written so that NaN, an unparsable field, fails it too.
|
|
if (!(ms >= WINDOW_MS.min && ms <= WINDOW_MS.max)) {
|
|
return false;
|
|
}
|
|
this.anchorJd = dateToJulianDate(date);
|
|
this.anchorWallMs = Date.now();
|
|
this.atNow.set(false);
|
|
return true;
|
|
}
|
|
|
|
/** Back to now, at real time — the state the map opens in. */
|
|
reset(): void {
|
|
this.anchorJd = dateToJulianDate();
|
|
this.anchorWallMs = Date.now();
|
|
this.rate.set(TIME_RATES[0].secondsPerSecond);
|
|
this.atNow.set(true);
|
|
}
|
|
}
|