The view had one rotation in it — the planet on the detail page, at 0.08 rad/s, a number with no source. Nothing in the system view turned at all. The data was already on disk: every cached Horizons page carries how its body spins, in one of five forms. The rate in radians per second is preferred where it appears, because it is signed — that is how Venus and Uranus are known to turn backwards — then a period in hours or days, then the `9h 55m 29.711 s` the giant planets use, and finally the word every major moon here carries instead of a number: Synchronous. A tidally locked moon's day is its orbit, so Kepler supplies it from the elements already parsed and the parent it goes round. Seventeen of the eighteen bodies come out within 1% of their published period — Earth 23.934 h, Jupiter 9.925 h, Venus -5832.5 h, Io 42.5 h, Callisto 400.5 h. Titan is the exception: its page states no period at all, so it is left still rather than turned at an invented rate. The axis is the orbit normal tilted by the obliquity about the orbit's ascending node, which is where an obliquity is measured from and the only line in the orbit the elements name. The phase at the epoch is published for none of these bodies, so the face turned toward the camera is not a claim; the rate and the direction are. At true rates nothing is visible moving — Earth turns 15 degrees an hour. A clock the reader can run faster is the next piece, and the audit asks for it anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
97 lines
4.5 KiB
TypeScript
97 lines
4.5 KiB
TypeScript
import { writeFileSync } from 'node:fs';
|
|
|
|
import { BodyRecord } from '../../src/app/shared/models/body.model';
|
|
import { gmForParent } from '../../src/app/shared/astro/constants';
|
|
import { orbitalPeriodDays } from '../../src/app/shared/astro/kepler';
|
|
import { SUN_STAR_ID } from '../../src/app/shared/models/star.model';
|
|
import { fetchHorizonsBody } from './lib/horizons';
|
|
import { dataPath, ensureDataDir } from './lib/paths';
|
|
|
|
const HOURS_PER_DAY = 24;
|
|
|
|
interface BodySpec {
|
|
id: string;
|
|
name: string;
|
|
kind: BodyRecord['kind'];
|
|
horizonsCommand: string;
|
|
center: string;
|
|
parentBodyId?: string;
|
|
}
|
|
|
|
// Sun-centered planets/dwarf, then their major moons (planetocentric elements).
|
|
const BODY_SPECS: BodySpec[] = [
|
|
{ id: 'mercury', name: 'Mercury', kind: 'planet', horizonsCommand: '199', center: '500@10' },
|
|
{ id: 'venus', name: 'Venus', kind: 'planet', horizonsCommand: '299', center: '500@10' },
|
|
{ id: 'earth', name: 'Earth', kind: 'planet', horizonsCommand: '399', center: '500@10' },
|
|
{ id: 'mars', name: 'Mars', kind: 'planet', horizonsCommand: '499', center: '500@10' },
|
|
{ id: 'jupiter', name: 'Jupiter', kind: 'planet', horizonsCommand: '599', center: '500@10' },
|
|
{ id: 'saturn', name: 'Saturn', kind: 'planet', horizonsCommand: '699', center: '500@10' },
|
|
{ id: 'uranus', name: 'Uranus', kind: 'planet', horizonsCommand: '799', center: '500@10' },
|
|
{ id: 'neptune', name: 'Neptune', kind: 'planet', horizonsCommand: '899', center: '500@10' },
|
|
{ id: 'pluto', name: 'Pluto', kind: 'dwarf', horizonsCommand: '999', center: '500@10' },
|
|
{ id: 'moon', name: 'Moon', kind: 'moon', horizonsCommand: '301', center: '500@399', parentBodyId: 'earth' },
|
|
{ id: 'phobos', name: 'Phobos', kind: 'moon', horizonsCommand: '401', center: '500@499', parentBodyId: 'mars' },
|
|
{ id: 'deimos', name: 'Deimos', kind: 'moon', horizonsCommand: '402', center: '500@499', parentBodyId: 'mars' },
|
|
{ id: 'io', name: 'Io', kind: 'moon', horizonsCommand: '501', center: '500@599', parentBodyId: 'jupiter' },
|
|
{ id: 'europa', name: 'Europa', kind: 'moon', horizonsCommand: '502', center: '500@599', parentBodyId: 'jupiter' },
|
|
{ id: 'ganymede', name: 'Ganymede', kind: 'moon', horizonsCommand: '503', center: '500@599', parentBodyId: 'jupiter' },
|
|
{ id: 'callisto', name: 'Callisto', kind: 'moon', horizonsCommand: '504', center: '500@599', parentBodyId: 'jupiter' },
|
|
{ id: 'titan', name: 'Titan', kind: 'moon', horizonsCommand: '606', center: '500@699', parentBodyId: 'saturn' },
|
|
{ id: 'triton', name: 'Triton', kind: 'moon', horizonsCommand: '801', center: '500@899', parentBodyId: 'neptune' }
|
|
];
|
|
|
|
/**
|
|
* Queries JPL Horizons for the osculating orbital elements (and mean radius, where
|
|
* reported) of the major planets, Pluto, and a curated set of major moons, and writes
|
|
* `bodies.json`.
|
|
*/
|
|
export async function fetchSolarSystem(): Promise<BodyRecord[]> {
|
|
console.log(`Fetching ${BODY_SPECS.length} solar-system bodies from JPL Horizons...`);
|
|
const bodies: BodyRecord[] = [];
|
|
|
|
for (const spec of BODY_SPECS) {
|
|
const result = await fetchHorizonsBody({
|
|
command: spec.horizonsCommand,
|
|
center: spec.center,
|
|
cacheKey: `horizons-${spec.id}.txt`
|
|
});
|
|
|
|
if (result.radiusKm === undefined) {
|
|
console.warn(` no physical radius found for ${spec.name}; defaulting to 0.`);
|
|
}
|
|
|
|
// A tidally locked moon's day is its orbit, which Horizons states as a word rather than a
|
|
// number: Kepler gives the period from the elements just parsed and the parent it goes round.
|
|
const rotationPeriodHours = result.tidallyLocked
|
|
? orbitalPeriodDays(result.orbit.semiMajorAxisAu, gmForParent(spec.parentBodyId)) * HOURS_PER_DAY
|
|
: result.rotationPeriodHours;
|
|
if (rotationPeriodHours === undefined) {
|
|
console.warn(` no rotation period found for ${spec.name}; it will not turn.`);
|
|
}
|
|
|
|
bodies.push({
|
|
id: spec.id,
|
|
systemStarId: SUN_STAR_ID,
|
|
name: spec.name,
|
|
kind: spec.kind,
|
|
radiusKm: result.radiusKm ?? 0,
|
|
orbit: result.orbit,
|
|
...(spec.parentBodyId ? { parentBodyId: spec.parentBodyId } : {}),
|
|
...(rotationPeriodHours !== undefined ? { rotationPeriodHours } : {}),
|
|
...(result.obliquityDeg !== undefined ? { obliquityDeg: result.obliquityDeg } : {})
|
|
});
|
|
}
|
|
|
|
ensureDataDir();
|
|
writeFileSync(dataPath('bodies.json'), JSON.stringify(bodies, null, 2));
|
|
console.log(` wrote ${bodies.length} bodies.`);
|
|
return bodies;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
fetchSolarSystem().catch((error) => {
|
|
console.error(error);
|
|
process.exitCode = 1;
|
|
});
|
|
}
|