Add the sun and moon to src/geo

A port of Cesium's Simon 1994 sun and moon positions, its TEME to
pseudo-fixed rotation and the JulianDate time handling, with epoch
milliseconds as the only input. Results are identical to Cesium's,
tested with exact equality from year -500 to 3000 and across every
leap second. Times outside the model throw a RangeError, where
Cesium's debug build throws.
This commit is contained in:
Sameh Khamis committed 2026-11-05 14:10:48 -08:00
1 parent 5652236a27
commit fd1c09dbbc
4 files changed
+1021 -2

No files matched your search

+5 -1
View File
@@ -2359,6 +2359,7 @@
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js",
"src/geo/celestial.js",
"src/layers/flights/icons.js",
"src/data/aircraftIconLayer.js",
"src/data/aircraftModelLayer.js",
@@ -5120,6 +5121,7 @@
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js",
"src/geo/celestial.js",
"src/agent/worldPacket.js",
"src/data/layerSnapshot.js",
"src/data/feedState.js",
@@ -5141,7 +5143,8 @@
"src/geo/index.js",
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js"
"src/geo/frames.js",
"src/geo/celestial.js"
],
"external": []
},
@@ -5162,6 +5165,7 @@
"src/geo/vector.js",
"src/geo/ellipsoid.js",
"src/geo/frames.js",
"src/geo/celestial.js",
"src/render/stub/motion.js",
"src/render/stub/camera.js",
"src/render/stub/geodesic.js",
+814
View File
@@ -0,0 +1,814 @@
/**
* @module geo/celestial
* @description The sun and the moon: a port of Cesium 1.138's
* `Simon1994PlanetaryPositions` (positions in Cesium's Earth inertial frame)
* and `Transforms.computeTemeToPseudoFixedMatrix` (inertial to Earth-fixed),
* with epoch milliseconds (UTC, as `Date.now()` gives) as the only time
* input. The time-scale handling (UTC to TAI through Cesium's leap-second
* table, TAI to TT to TDB) follows `JulianDate` step for step, so results
* are identical to Cesium's, bit for bit. Constants and operation order
* are Cesium's; do not "simplify" them. No engine.
*
* A rotation is returned as its axes `{x, y, z}` (the columns of Cesium's
* `Matrix3`), the form `geo/frames` uses. Times must be finite and inside
* what `Date` represents, and the model holds for a few thousand years
* either side of J2000; outside that its orbital elements leave their
* ranges and the functions throw a RangeError, where Cesium's debug build
* throws.
*/
import { frameVector } from './frames.js';
import { RADIANS_PER_DEGREE, TWO_PI, vec, zeroToTwoPi } from './vector.js';
// ---------------------------------------------------------------------------
// Constants (Core/Math.js, Core/TimeConstants.js)
const PI = Math.PI;
const RADIANS_PER_ARCSECOND = RADIANS_PER_DEGREE / 3600.0;
const SECONDS_PER_MILLISECOND = 0.001;
const SECONDS_PER_MINUTE = 60.0;
const SECONDS_PER_HOUR = 3600.0;
const SECONDS_PER_DAY = 86400.0;
const DAYS_PER_JULIAN_CENTURY = 36525.0;
// ---------------------------------------------------------------------------
// Julian dates (Core/JulianDate.js). A date is `{dayNumber, secondsOfDay}`,
// in TAI unless a comment says otherwise, as Cesium keeps them.
function setComponents(wholeDays, secondsOfDay, julianDate) {
const extraDays = (secondsOfDay / SECONDS_PER_DAY) | 0;
wholeDays += extraDays;
secondsOfDay -= SECONDS_PER_DAY * extraDays;
if (secondsOfDay < 0) {
wholeDays--;
secondsOfDay += SECONDS_PER_DAY;
}
julianDate.dayNumber = wholeDays;
julianDate.secondsOfDay = secondsOfDay;
return julianDate;
}
const taiDate = (dayNumber, secondsOfDay) =>
setComponents(dayNumber, secondsOfDay, {});
const addSeconds = (julianDate, seconds, result) =>
setComponents(
julianDate.dayNumber,
julianDate.secondsOfDay + seconds,
result,
);
const totalDays = (julianDate) =>
julianDate.dayNumber + julianDate.secondsOfDay / SECONDS_PER_DAY;
function secondsDifference(left, right) {
const dayDifference = (left.dayNumber - right.dayNumber) * SECONDS_PER_DAY;
return dayDifference + (left.secondsOfDay - right.secondsOfDay);
}
function compare(left, right) {
const julianDayNumberDifference = left.dayNumber - right.dayNumber;
if (julianDayNumberDifference !== 0) return julianDayNumberDifference;
return left.secondsOfDay - right.secondsOfDay;
}
/** `JulianDate.leapSeconds`: the TAI instant of each leap second, and TAI−UTC from it. */
const LEAP_SECONDS = [
[2441317, 43210.0, 10], // January 1, 1972 00:00:00 UTC
[2441499, 43211.0, 11], // July 1, 1972 00:00:00 UTC
[2441683, 43212.0, 12], // January 1, 1973 00:00:00 UTC
[2442048, 43213.0, 13], // January 1, 1974 00:00:00 UTC
[2442413, 43214.0, 14], // January 1, 1975 00:00:00 UTC
[2442778, 43215.0, 15], // January 1, 1976 00:00:00 UTC
[2443144, 43216.0, 16], // January 1, 1977 00:00:00 UTC
[2443509, 43217.0, 17], // January 1, 1978 00:00:00 UTC
[2443874, 43218.0, 18], // January 1, 1979 00:00:00 UTC
[2444239, 43219.0, 19], // January 1, 1980 00:00:00 UTC
[2444786, 43220.0, 20], // July 1, 1981 00:00:00 UTC
[2445151, 43221.0, 21], // July 1, 1982 00:00:00 UTC
[2445516, 43222.0, 22], // July 1, 1983 00:00:00 UTC
[2446247, 43223.0, 23], // July 1, 1985 00:00:00 UTC
[2447161, 43224.0, 24], // January 1, 1988 00:00:00 UTC
[2447892, 43225.0, 25], // January 1, 1990 00:00:00 UTC
[2448257, 43226.0, 26], // January 1, 1991 00:00:00 UTC
[2448804, 43227.0, 27], // July 1, 1992 00:00:00 UTC
[2449169, 43228.0, 28], // July 1, 1993 00:00:00 UTC
[2449534, 43229.0, 29], // July 1, 1994 00:00:00 UTC
[2450083, 43230.0, 30], // January 1, 1996 00:00:00 UTC
[2450630, 43231.0, 31], // July 1, 1997 00:00:00 UTC
[2451179, 43232.0, 32], // January 1, 1999 00:00:00 UTC
[2453736, 43233.0, 33], // January 1, 2006 00:00:00 UTC
[2454832, 43234.0, 34], // January 1, 2009 00:00:00 UTC
[2456109, 43235.0, 35], // July 1, 2012 00:00:00 UTC
[2457204, 43236.0, 36], // July 1, 2015 00:00:00 UTC
[2457754, 43237.0, 37], // January 1, 2017 00:00:00 UTC
].map(([dayNumber, secondsOfDay, offset]) =>
Object.freeze({ julianDate: taiDate(dayNumber, secondsOfDay), offset }),
);
/** Core/binarySearch.js over the leap-second table. */
function searchLeapSeconds(julianDate) {
let low = 0;
let high = LEAP_SECONDS.length - 1;
while (low <= high) {
const i = ~~((low + high) / 2);
const comparison = compare(LEAP_SECONDS[i].julianDate, julianDate);
if (comparison < 0) {
low = i + 1;
continue;
}
if (comparison > 0) {
high = i - 1;
continue;
}
return i;
}
return ~(high + 1);
}
/** `convertUtcToTai`: shifts a UTC date to TAI in place. */
function convertUtcToTai(julianDate) {
// Even though julianDate is in UTC, Cesium treats it as TAI to search.
let index = searchLeapSeconds(julianDate);
if (index < 0) index = ~index;
if (index >= LEAP_SECONDS.length) index = LEAP_SECONDS.length - 1;
let offset = LEAP_SECONDS[index].offset;
if (index > 0) {
const difference = secondsDifference(
LEAP_SECONDS[index].julianDate,
julianDate,
);
if (difference > offset) {
index--;
offset = LEAP_SECONDS[index].offset;
}
}
addSeconds(julianDate, offset, julianDate);
}
/** `JulianDate.computeTaiMinusUtc`. */
function computeTaiMinusUtc(julianDate) {
let index = searchLeapSeconds(julianDate);
if (index < 0) {
index = ~index;
--index;
if (index < 0) index = 0;
}
return LEAP_SECONDS[index].offset;
}
/**
* `JulianDate.fromDate(new Date(ms))`: the TAI Julian date of a UTC epoch
* millisecond (Seidelmann 1992, Explanatory Supplement p. 604).
*/
function julianDateFromEpochMs(ms) {
const date = new Date(ms);
if (!Number.isFinite(date.getTime()))
throw new RangeError(`not a representable time: ${ms}`);
const year = date.getUTCFullYear();
const month = date.getUTCMonth() + 1;
const day = date.getUTCDate();
let hour = date.getUTCHours();
const minute = date.getUTCMinutes();
const second = date.getUTCSeconds();
const millisecond = date.getUTCMilliseconds();
const a = ((month - 14) / 12) | 0;
const b = year + 4800 + a;
let dayNumber =
(((1461 * b) / 4) | 0) +
(((367 * (month - 2 - 12 * a)) / 12) | 0) -
(((3 * (((b + 100) / 100) | 0)) / 4) | 0) +
day -
32075;
// Julian dates are noon-based.
hour = hour - 12;
if (hour < 0) hour += 24;
let secondsOfDay =
second +
(hour * SECONDS_PER_HOUR +
minute * SECONDS_PER_MINUTE +
millisecond * SECONDS_PER_MILLISECOND);
if (secondsOfDay >= 43200.0) dayNumber -= 1;
// The JulianDate constructor: split a fractional day number (none here).
const wholeDays = dayNumber | 0;
secondsOfDay = secondsOfDay + (dayNumber - wholeDays) * SECONDS_PER_DAY;
const julianDate = setComponents(wholeDays, secondsOfDay, {});
convertUtcToTai(julianDate);
return julianDate;
}
// ---------------------------------------------------------------------------
// Matrices
/** Cesium's `Matrix3.multiplyByVector`, column-major. */
function multiplyByVector(matrix, v) {
return {
x: matrix[0] * v.x + matrix[3] * v.y + matrix[6] * v.z,
y: matrix[1] * v.x + matrix[4] * v.y + matrix[7] * v.z,
z: matrix[2] * v.x + matrix[5] * v.y + matrix[8] * v.z,
};
}
// ---------------------------------------------------------------------------
// Simon 1994 (Core/Simon1994PlanetaryPositions.js)
function computeTdbMinusTtSpice(daysSinceJ2000InTerrestrialTime) {
// SPICE N0051 (unitim.c) constants, with TT taken as the independent
// variable instead of TDB, as Cesium does.
const g = 6.239996 + 0.0172019696544 * daysSinceJ2000InTerrestrialTime;
return 1.657e-3 * Math.sin(g + 1.671e-2 * Math.sin(g));
}
const TdtMinusTai = 32.184;
const J2000d = 2451545;
function taiToTdb(date) {
// TAI to TT.
const result = addSeconds(date, TdtMinusTai, {});
// TT to TDB.
const days = totalDays(result) - J2000d;
return addSeconds(result, computeTdbMinusTtSpice(days), result);
}
const epoch = taiDate(2451545, 0); // Actually TDB (not TAI), as in Cesium.
const MetersPerKilometer = 1000.0;
const MetersPerAstronomicalUnit = 1.4959787e11; // IAU 1976 value
/** Days since J2000 TDB at a TAI date. */
function daysSinceEpochTdb(date) {
const tdb = taiToTdb(date);
return (
tdb.dayNumber -
epoch.dayNumber +
(tdb.secondsOfDay - epoch.secondsOfDay) / SECONDS_PER_DAY
);
}
function elementsToCartesian(
semimajorAxis,
eccentricity,
inclination,
longitudeOfPerigee,
longitudeOfNode,
meanLongitude,
) {
if (inclination < 0.0) {
inclination = -inclination;
longitudeOfNode += PI;
}
// Reached in the far past (Date's earliest times), as Cesium's debug
// check is.
if (inclination > PI)
throw new RangeError(
'inclination out of range: the time is too far from J2000',
);
const radiusOfPeriapsis = semimajorAxis * (1.0 - eccentricity);
const argumentOfPeriapsis = longitudeOfPerigee - longitudeOfNode;
const rightAscensionOfAscendingNode = longitudeOfNode;
const trueAnomaly = meanAnomalyToTrueAnomaly(
meanLongitude - longitudeOfPerigee,
eccentricity,
);
const perifocalToEquatorial = perifocalToCartesianMatrix(
argumentOfPeriapsis,
inclination,
rightAscensionOfAscendingNode,
);
const semilatus = radiusOfPeriapsis * (1.0 + eccentricity);
const costheta = Math.cos(trueAnomaly);
const sintheta = Math.sin(trueAnomaly);
const denom = 1.0 + eccentricity * costheta;
const radius = semilatus / denom;
return multiplyByVector(perifocalToEquatorial, {
x: radius * costheta,
y: radius * sintheta,
z: 0.0,
});
}
function meanAnomalyToTrueAnomaly(meanAnomaly, eccentricity) {
if (!(eccentricity >= 0.0 && eccentricity < 1.0))
throw new RangeError(
'eccentricity out of range: the time is too far from J2000',
);
const eccentricAnomaly = meanAnomalyToEccentricAnomaly(
meanAnomaly,
eccentricity,
);
return eccentricAnomalyToTrueAnomaly(eccentricAnomaly, eccentricity);
}
const maxIterationCount = 50;
const keplerEqConvergence = 0.00000001; // CesiumMath.EPSILON8
function meanAnomalyToEccentricAnomaly(meanAnomaly, eccentricity) {
const revs = Math.floor(meanAnomaly / TWO_PI);
// Angle in the current revolution.
meanAnomaly -= revs * TWO_PI;
// Starting value for the iteration.
let iterationValue =
meanAnomaly +
(eccentricity * Math.sin(meanAnomaly)) /
(1.0 - Math.sin(meanAnomaly + eccentricity) + Math.sin(meanAnomaly));
// Newton-Raphson on Kepler's equation.
let eccentricAnomaly = Number.MAX_VALUE;
let count = 0;
for (
;
count < maxIterationCount &&
Math.abs(eccentricAnomaly - iterationValue) > keplerEqConvergence;
++count
) {
eccentricAnomaly = iterationValue;
const NRfunction =
eccentricAnomaly -
eccentricity * Math.sin(eccentricAnomaly) -
meanAnomaly;
const dNRfunction = 1 - eccentricity * Math.cos(eccentricAnomaly);
iterationValue = eccentricAnomaly - NRfunction / dNRfunction;
}
// Not reached for an elliptic orbit; kept as Cesium's debug check.
if (count >= maxIterationCount)
throw new RangeError('Kepler equation did not converge');
eccentricAnomaly = iterationValue + revs * TWO_PI;
return eccentricAnomaly;
}
function eccentricAnomalyToTrueAnomaly(eccentricAnomaly, eccentricity) {
const revs = Math.floor(eccentricAnomaly / TWO_PI);
eccentricAnomaly -= revs * TWO_PI;
const trueAnomalyX = Math.cos(eccentricAnomaly) - eccentricity;
const trueAnomalyY =
Math.sin(eccentricAnomaly) * Math.sqrt(1 - eccentricity * eccentricity);
let trueAnomaly = Math.atan2(trueAnomalyY, trueAnomalyX);
// Correct quadrant.
trueAnomaly = zeroToTwoPi(trueAnomaly);
if (eccentricAnomaly < 0) trueAnomaly -= TWO_PI;
// Previous revolutions.
trueAnomaly += revs * TWO_PI;
return trueAnomaly;
}
/** Perifocal (PQW) to inertial axes, column-major. */
function perifocalToCartesianMatrix(
argumentOfPeriapsis,
inclination,
rightAscension,
) {
const cosap = Math.cos(argumentOfPeriapsis);
const sinap = Math.sin(argumentOfPeriapsis);
const cosi = Math.cos(inclination);
const sini = Math.sin(inclination);
const cosraan = Math.cos(rightAscension);
const sinraan = Math.sin(rightAscension);
const result = new Float64Array(9);
result[0] = cosraan * cosap - sinraan * sinap * cosi;
result[1] = sinraan * cosap + cosraan * sinap * cosi;
result[2] = sinap * sini;
result[3] = -cosraan * sinap - sinraan * cosap * cosi;
result[4] = -sinraan * sinap + cosraan * cosap * cosi;
result[5] = cosap * sini;
result[6] = sinraan * sini;
result[7] = -cosraan * sini;
result[8] = cosi;
return result;
}
// From section 5.8
const semiMajorAxis0 = 1.0000010178 * MetersPerAstronomicalUnit;
const meanLongitude0 = 100.46645683 * RADIANS_PER_DEGREE;
const meanLongitude1 = 1295977422.83429 * RADIANS_PER_ARCSECOND;
// From table 6
const p1u = 16002;
const p2u = 21863;
const p3u = 32004;
const p4u = 10931;
const p5u = 14529;
const p6u = 16368;
const p7u = 15318;
const p8u = 32794;
const Ca1 = 64 * 1e-7 * MetersPerAstronomicalUnit;
const Ca2 = -152 * 1e-7 * MetersPerAstronomicalUnit;
const Ca3 = 62 * 1e-7 * MetersPerAstronomicalUnit;
const Ca4 = -8 * 1e-7 * MetersPerAstronomicalUnit;
const Ca5 = 32 * 1e-7 * MetersPerAstronomicalUnit;
const Ca6 = -41 * 1e-7 * MetersPerAstronomicalUnit;
const Ca7 = 19 * 1e-7 * MetersPerAstronomicalUnit;
const Ca8 = -11 * 1e-7 * MetersPerAstronomicalUnit;
const Sa1 = -150 * 1e-7 * MetersPerAstronomicalUnit;
const Sa2 = -46 * 1e-7 * MetersPerAstronomicalUnit;
const Sa3 = 68 * 1e-7 * MetersPerAstronomicalUnit;
const Sa4 = 54 * 1e-7 * MetersPerAstronomicalUnit;
const Sa5 = 14 * 1e-7 * MetersPerAstronomicalUnit;
const Sa6 = 24 * 1e-7 * MetersPerAstronomicalUnit;
const Sa7 = -28 * 1e-7 * MetersPerAstronomicalUnit;
const Sa8 = 22 * 1e-7 * MetersPerAstronomicalUnit;
const q1u = 10;
const q2u = 16002;
const q3u = 21863;
const q4u = 10931;
const q5u = 1473;
const q6u = 32004;
const q7u = 4387;
const q8u = 73;
const Cl1 = -325 * 1e-7;
const Cl2 = -322 * 1e-7;
const Cl3 = -79 * 1e-7;
const Cl4 = 232 * 1e-7;
const Cl5 = -52 * 1e-7;
const Cl6 = 97 * 1e-7;
const Cl7 = 55 * 1e-7;
const Cl8 = -41 * 1e-7;
const Sl1 = -105 * 1e-7;
const Sl2 = -137 * 1e-7;
const Sl3 = 258 * 1e-7;
const Sl4 = 35 * 1e-7;
const Sl5 = -116 * 1e-7;
const Sl6 = -88 * 1e-7;
const Sl7 = -112 * 1e-7;
const Sl8 = -80 * 1e-7;
/** The Earth-Moon barycentre (section 6), at a TAI date. */
function computeSimonEarthMoonBarycenter(date) {
// t is thousands of years from J2000 TDB.
const x = daysSinceEpochTdb(date);
const t = x / (DAYS_PER_JULIAN_CENTURY * 10.0);
const u = 0.3595362 * t;
const semimajorAxis =
semiMajorAxis0 +
Ca1 * Math.cos(p1u * u) +
Sa1 * Math.sin(p1u * u) +
Ca2 * Math.cos(p2u * u) +
Sa2 * Math.sin(p2u * u) +
Ca3 * Math.cos(p3u * u) +
Sa3 * Math.sin(p3u * u) +
Ca4 * Math.cos(p4u * u) +
Sa4 * Math.sin(p4u * u) +
Ca5 * Math.cos(p5u * u) +
Sa5 * Math.sin(p5u * u) +
Ca6 * Math.cos(p6u * u) +
Sa6 * Math.sin(p6u * u) +
Ca7 * Math.cos(p7u * u) +
Sa7 * Math.sin(p7u * u) +
Ca8 * Math.cos(p8u * u) +
Sa8 * Math.sin(p8u * u);
const meanLongitude =
meanLongitude0 +
meanLongitude1 * t +
Cl1 * Math.cos(q1u * u) +
Sl1 * Math.sin(q1u * u) +
Cl2 * Math.cos(q2u * u) +
Sl2 * Math.sin(q2u * u) +
Cl3 * Math.cos(q3u * u) +
Sl3 * Math.sin(q3u * u) +
Cl4 * Math.cos(q4u * u) +
Sl4 * Math.sin(q4u * u) +
Cl5 * Math.cos(q5u * u) +
Sl5 * Math.sin(q5u * u) +
Cl6 * Math.cos(q6u * u) +
Sl6 * Math.sin(q6u * u) +
Cl7 * Math.cos(q7u * u) +
Sl7 * Math.sin(q7u * u) +
Cl8 * Math.cos(q8u * u) +
Sl8 * Math.sin(q8u * u);
// All constants in this part are from section 5.8.
const eccentricity = 0.0167086342 - 0.0004203654 * t;
const longitudeOfPerigee =
102.93734808 * RADIANS_PER_DEGREE + 11612.3529 * RADIANS_PER_ARCSECOND * t;
const inclination = 469.97289 * RADIANS_PER_ARCSECOND * t;
const longitudeOfNode =
174.87317577 * RADIANS_PER_DEGREE - 8679.27034 * RADIANS_PER_ARCSECOND * t;
return elementsToCartesian(
semimajorAxis,
eccentricity,
inclination,
longitudeOfPerigee,
longitudeOfNode,
meanLongitude,
);
}
/** The Moon relative to the Earth (section 4), at a TAI date. */
function computeSimonMoon(date) {
const x = daysSinceEpochTdb(date);
const t = x / DAYS_PER_JULIAN_CENTURY;
const t2 = t * t;
const t3 = t2 * t;
const t4 = t3 * t;
// Terms from section 3.4 (b.1)
let semimajorAxis = 383397.7725 + 0.004 * t;
let eccentricity = 0.055545526 - 0.000000016 * t;
const inclinationConstant = 5.15668983 * RADIANS_PER_DEGREE;
let inclinationSecPart =
-0.00008 * t + 0.02966 * t2 - 0.000042 * t3 - 0.00000013 * t4;
const longitudeOfPerigeeConstant = 83.35324312 * RADIANS_PER_DEGREE;
let longitudeOfPerigeeSecPart =
14643420.2669 * t - 38.2702 * t2 - 0.045047 * t3 + 0.00021301 * t4;
const longitudeOfNodeConstant = 125.04455501 * RADIANS_PER_DEGREE;
let longitudeOfNodeSecPart =
-6967919.3631 * t + 6.3602 * t2 + 0.007625 * t3 - 0.00003586 * t4;
const meanLongitudeConstant = 218.31664563 * RADIANS_PER_DEGREE;
let meanLongitudeSecPart =
1732559343.4847 * t - 6.391 * t2 + 0.006588 * t3 - 0.00003169 * t4;
// Delaunay arguments from section 3.5 b
const D =
297.85019547 * RADIANS_PER_DEGREE +
RADIANS_PER_ARCSECOND *
(1602961601.209 * t - 6.3706 * t2 + 0.006593 * t3 - 0.00003169 * t4);
const F =
93.27209062 * RADIANS_PER_DEGREE +
RADIANS_PER_ARCSECOND *
(1739527262.8478 * t - 12.7512 * t2 - 0.001037 * t3 + 0.00000417 * t4);
const l =
134.96340251 * RADIANS_PER_DEGREE +
RADIANS_PER_ARCSECOND *
(1717915923.2178 * t + 31.8792 * t2 + 0.051635 * t3 - 0.0002447 * t4);
const lprime =
357.52910918 * RADIANS_PER_DEGREE +
RADIANS_PER_ARCSECOND *
(129596581.0481 * t - 0.5532 * t2 + 0.000136 * t3 - 0.00001149 * t4);
const psi =
310.17137918 * RADIANS_PER_DEGREE -
RADIANS_PER_ARCSECOND *
(6967051.436 * t + 6.2068 * t2 + 0.007618 * t3 - 0.00003219 * t4);
// Add terms from Table 4
const twoD = 2.0 * D;
const fourD = 4.0 * D;
const sixD = 6.0 * D;
const twol = 2.0 * l;
const threel = 3.0 * l;
const fourl = 4.0 * l;
const twoF = 2.0 * F;
semimajorAxis +=
3400.4 * Math.cos(twoD) -
635.6 * Math.cos(twoD - l) -
235.6 * Math.cos(l) +
218.1 * Math.cos(twoD - lprime) +
181.0 * Math.cos(twoD + l);
eccentricity +=
0.014216 * Math.cos(twoD - l) +
0.008551 * Math.cos(twoD - twol) -
0.001383 * Math.cos(l) +
0.001356 * Math.cos(twoD + l) -
0.001147 * Math.cos(fourD - threel) -
0.000914 * Math.cos(fourD - twol) +
0.000869 * Math.cos(twoD - lprime - l) -
0.000627 * Math.cos(twoD) -
0.000394 * Math.cos(fourD - fourl) +
0.000282 * Math.cos(twoD - lprime - twol) -
0.000279 * Math.cos(D - l) -
0.000236 * Math.cos(twol) +
0.000231 * Math.cos(fourD) +
0.000229 * Math.cos(sixD - fourl) -
0.000201 * Math.cos(twol - twoF);
inclinationSecPart +=
486.26 * Math.cos(twoD - twoF) -
40.13 * Math.cos(twoD) +
37.51 * Math.cos(twoF) +
25.73 * Math.cos(twol - twoF) +
19.97 * Math.cos(twoD - lprime - twoF);
longitudeOfPerigeeSecPart +=
-55609 * Math.sin(twoD - l) -
34711 * Math.sin(twoD - twol) -
9792 * Math.sin(l) +
9385 * Math.sin(fourD - threel) +
7505 * Math.sin(fourD - twol) +
5318 * Math.sin(twoD + l) +
3484 * Math.sin(fourD - fourl) -
3417 * Math.sin(twoD - lprime - l) -
2530 * Math.sin(sixD - fourl) -
2376 * Math.sin(twoD) -
2075 * Math.sin(twoD - threel) -
1883 * Math.sin(twol) -
1736 * Math.sin(sixD - 5.0 * l) +
1626 * Math.sin(lprime) -
1370 * Math.sin(sixD - threel);
longitudeOfNodeSecPart +=
-5392 * Math.sin(twoD - twoF) -
540 * Math.sin(lprime) -
441 * Math.sin(twoD) +
423 * Math.sin(twoF) -
288 * Math.sin(twol - twoF);
meanLongitudeSecPart +=
-3332.9 * Math.sin(twoD) +
1197.4 * Math.sin(twoD - l) -
662.5 * Math.sin(lprime) +
396.3 * Math.sin(l) -
218.0 * Math.sin(twoD - lprime);
// Add terms from Table 5
const twoPsi = 2.0 * psi;
const threePsi = 3.0 * psi;
inclinationSecPart +=
46.997 * Math.cos(psi) * t -
0.614 * Math.cos(twoD - twoF + psi) * t +
0.614 * Math.cos(twoD - twoF - psi) * t -
0.0297 * Math.cos(twoPsi) * t2 -
0.0335 * Math.cos(psi) * t2 +
0.0012 * Math.cos(twoD - twoF + twoPsi) * t2 -
0.00016 * Math.cos(psi) * t3 +
0.00004 * Math.cos(threePsi) * t3 +
0.00004 * Math.cos(twoPsi) * t3;
const perigeeAndMean =
2.116 * Math.sin(psi) * t -
0.111 * Math.sin(twoD - twoF - psi) * t -
0.0015 * Math.sin(psi) * t2;
longitudeOfPerigeeSecPart += perigeeAndMean;
meanLongitudeSecPart += perigeeAndMean;
longitudeOfNodeSecPart +=
-520.77 * Math.sin(psi) * t +
13.66 * Math.sin(twoD - twoF + psi) * t +
1.12 * Math.sin(twoD - psi) * t -
1.06 * Math.sin(twoF - psi) * t +
0.66 * Math.sin(twoPsi) * t2 +
0.371 * Math.sin(psi) * t2 -
0.035 * Math.sin(twoD - twoF + twoPsi) * t2 -
0.015 * Math.sin(twoD - twoF + psi) * t2 +
0.0014 * Math.sin(psi) * t3 -
0.0011 * Math.sin(threePsi) * t3 -
0.0009 * Math.sin(twoPsi) * t3;
// Add constants and convert units
semimajorAxis *= MetersPerKilometer;
const inclination =
inclinationConstant + inclinationSecPart * RADIANS_PER_ARCSECOND;
const longitudeOfPerigee =
longitudeOfPerigeeConstant +
longitudeOfPerigeeSecPart * RADIANS_PER_ARCSECOND;
const meanLongitude =
meanLongitudeConstant + meanLongitudeSecPart * RADIANS_PER_ARCSECOND;
const longitudeOfNode =
longitudeOfNodeConstant + longitudeOfNodeSecPart * RADIANS_PER_ARCSECOND;
return elementsToCartesian(
semimajorAxis,
eccentricity,
inclination,
longitudeOfPerigee,
longitudeOfNode,
meanLongitude,
);
}
// The Earth relative to the Earth-Moon barycentre, from the Moon and the
// 1992 mu value (Moon/Earth mass ratio) in Table 2.
const moonEarthMassRatio = 0.012300034;
const factor = (moonEarthMassRatio / (moonEarthMassRatio + 1.0)) * -1;
function computeSimonEarth(date) {
const moon = computeSimonMoon(date);
return { x: moon.x * factor, y: moon.y * factor, z: moon.z * factor };
}
// Ecliptic to Cesium's Earth inertial axes (STK Components'
// GeographicTransformer), column-major.
const axesTransformation = Float64Array.of(
1.0000000000000002,
-5.154129427414611e-16,
-2.23970096136568e-16,
5.619723173785822e-16,
0.9174820620691819,
0.39777715593191376,
4.690511510146299e-19,
-0.39777715593191376,
0.9174820620691819,
);
/**
* The Sun in Cesium's Earth-centred inertial frame, in metres
* (`Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame`).
* @param {number} ms UTC epoch milliseconds.
* @returns {{x: number, y: number, z: number}}
*/
export function sunPositionInertial(ms) {
const date = julianDateFromEpochMs(ms);
const translation = computeSimonEarthMoonBarycenter(date);
const earth = computeSimonEarth(date);
return multiplyByVector(axesTransformation, {
x: -translation.x - earth.x,
y: -translation.y - earth.y,
z: -translation.z - earth.z,
});
}
/**
* The Moon in Cesium's Earth-centred inertial frame, in metres
* (`Simon1994PlanetaryPositions.computeMoonPositionInEarthInertialFrame`).
* @param {number} ms UTC epoch milliseconds.
* @returns {{x: number, y: number, z: number}}
*/
export function moonPositionInertial(ms) {
return multiplyByVector(
axesTransformation,
computeSimonMoon(julianDateFromEpochMs(ms)),
);
}
// ---------------------------------------------------------------------------
// TEME to pseudo-fixed (Core/Transforms.js)
const gmstConstant0 = 6 * 3600 + 41 * 60 + 50.54841;
const gmstConstant1 = 8640184.812866;
const gmstConstant2 = 0.093104;
const gmstConstant3 = -6.2e-6;
const rateCoef = 1.1772758384668e-19;
const wgs84WRPrecessing = 7.2921158553e-5;
const twoPiOverSecondsInDay = TWO_PI / 86400.0;
/**
* The rotation from True Equator Mean Equinox axes to Cesium's pseudo-fixed
* axes (`Transforms.computeTemeToPseudoFixedMatrix`), treating UT1 as UTC.
* Epoch milliseconds cannot name an instant inside a leap second
* (23:59:60), so none reaches it; the instants either side are Cesium's.
* @param {number} ms UTC epoch milliseconds.
* @returns {{x: object, y: object, z: object, origin: object}} A frame.
*/
export function temeToPseudoFixed(ms) {
const date = julianDateFromEpochMs(ms);
// GMST is computed from UT1; UTC stands in for it.
const dateInUtc = addSeconds(date, -computeTaiMinusUtc(date), {});
const utcDayNumber = dateInUtc.dayNumber;
const utcSecondsIntoDay = dateInUtc.secondsOfDay;
let t;
const diffDays = utcDayNumber - 2451545;
if (utcSecondsIntoDay >= 43200.0) {
t = (diffDays + 0.5) / DAYS_PER_JULIAN_CENTURY;
} else {
t = (diffDays - 0.5) / DAYS_PER_JULIAN_CENTURY;
}
const gmst0 =
gmstConstant0 +
t * (gmstConstant1 + t * (gmstConstant2 + t * gmstConstant3));
const angle = (gmst0 * twoPiOverSecondsInDay) % TWO_PI;
const ratio = wgs84WRPrecessing + rateCoef * (utcDayNumber - 2451545.5);
const secondsSinceMidnight =
(utcSecondsIntoDay + SECONDS_PER_DAY * 0.5) % SECONDS_PER_DAY;
const gha = angle + ratio * secondsSinceMidnight;
const cosGha = Math.cos(gha);
const sinGha = Math.sin(gha);
return {
x: vec(cosGha, -sinGha, 0.0),
y: vec(sinGha, cosGha, 0.0),
z: vec(0.0, 0.0, 1.0),
origin: vec(0.0, 0.0, 0.0),
};
}
/**
* The Sun in Earth-fixed (ECEF) metres: `sunPositionInertial` rotated by
* `temeToPseudoFixed`. This is the pseudo-fixed frame Core falls back to
* while EOP data is missing, not the EOP-corrected ICRF-to-fixed one
* (`Transforms.computeIcrfToFixedMatrix`); the two differ by precession,
* nutation and polar motion.
* @param {number} ms UTC epoch milliseconds.
* @returns {{x: number, y: number, z: number}}
*/
export function sunPositionFixed(ms) {
return frameVector(temeToPseudoFixed(ms), sunPositionInertial(ms));
}
/**
* The Moon in Earth-fixed (ECEF) metres, in the same pseudo-fixed frame as
* `sunPositionFixed`.
* @param {number} ms UTC epoch milliseconds.
* @returns {{x: number, y: number, z: number}}
*/
export function moonPositionFixed(ms) {
return frameVector(temeToPseudoFixed(ms), moonPositionInertial(ms));
}
+200
View File
@@ -0,0 +1,200 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import * as Cesium from 'cesium';
import {
moonPositionFixed,
moonPositionInertial,
sunPositionFixed,
sunPositionInertial,
temeToPseudoFixed,
} from './celestial.js';
/*
* The port runs Cesium's operations in the same order with the same Math
* functions, so every component must be identical (Object.is, so signed
* zeros count). Any operation-order slip shows up as a difference.
*/
/** A seeded generator, so a failure names a reproducible point. */
function random(seed) {
let s = seed >>> 0;
return () => {
s = (s * 1664525 + 1013904223) >>> 0;
return s / 2 ** 32;
};
}
const START = Date.UTC(1990, 0, 1);
const END = Date.UTC(2050, 0, 1);
const year = (y) => new Date(0).setUTCFullYear(y, 0, 1);
/** Seeded times in 1990-2050 and -500 to 3000, the epoch, and steps across every leap second. */
function sampleTimes() {
const next = random(0x5eed);
const times = [];
for (let i = 0; i < 4000; i++) {
times.push(Math.round(START + next() * (END - START)));
}
// Far from J2000, where Kepler's revolutions count, and before 1972, where
// UTC to TAI takes the table's first offset.
for (let i = 0; i < 400; i++)
times.push(Math.round(year(-500) + next() * (year(3000) - year(-500))));
times.push(
year(1900),
year(1960),
-1,
0,
Date.UTC(1971, 11, 31, 23, 59, 59, 999),
Date.UTC(1972, 0, 1),
Date.UTC(1972, 6, 1) - 1,
year(2100),
year(3000),
);
// Each leap second Cesium knows, 1.5 s either side.
const boundaries = [
[1972, 6],
[1973, 0],
[1974, 0],
[1975, 0],
[1976, 0],
[1977, 0],
[1978, 0],
[1979, 0],
[1980, 0],
[1981, 6],
[1982, 6],
[1983, 6],
[1985, 6],
[1988, 0],
[1990, 0],
[1991, 0],
[1992, 6],
[1993, 6],
[1994, 6],
[1996, 0],
[1997, 6],
[1999, 0],
[2006, 0],
[2009, 0],
[2012, 6],
[2015, 6],
[2017, 0],
];
for (const [year, month] of boundaries) {
const at = Date.UTC(year, month, 1);
for (let d = -1500; d <= 1500; d += 125) times.push(at + d);
}
// The span the task names, millisecond by millisecond at the edges.
const leap = Date.UTC(2017, 0, 1);
for (let d = -1000; d <= 1000; d += 1) {
if (Math.abs(d) <= 5 || Math.abs(d) >= 995 || d % 50 === 0) {
times.push(leap + d);
}
}
// Noon/midnight edges (secondsOfDay wrap), J2000, and before the epoch.
times.push(
Date.UTC(2000, 0, 1, 12),
Date.UTC(2000, 0, 1, 11, 58, 55, 816),
Date.UTC(2000, 0, 1, 11, 59, 59, 999),
Date.UTC(2024, 5, 30, 12, 0, 0, 0),
Date.UTC(2024, 5, 30, 11, 59, 59, 999),
Date.UTC(2024, 5, 30, 23, 59, 59, 999),
START,
END,
);
return times;
}
const TIMES = sampleTimes();
const julian = (ms) => Cesium.JulianDate.fromDate(new Date(ms));
const iso = (ms) => new Date(ms).toISOString();
function sameOver(label, compute, expected) {
for (const ms of TIMES) {
const ours = compute(ms);
const theirs = expected(ms);
for (const k of ['x', 'y', 'z'])
assert.ok(
Object.is(ours[k], theirs[k]),
`${label} ${k} at ${iso(ms)} (${ms}): ${ours[k]} vs ${theirs[k]}`,
);
}
}
function cesiumFixed(ms, compute) {
const time = julian(ms);
const inertial = compute(time, new Cesium.Cartesian3());
const matrix = Cesium.Transforms.computeTemeToPseudoFixedMatrix(time);
return Cesium.Matrix3.multiplyByVector(
matrix,
inertial,
new Cesium.Cartesian3(),
);
}
const { Simon1994PlanetaryPositions: Simon } = Cesium;
test('sunPositionInertial matches Simon1994PlanetaryPositions', () => {
sameOver('sun inertial', sunPositionInertial, (ms) =>
Simon.computeSunPositionInEarthInertialFrame(julian(ms)),
);
});
test('moonPositionInertial matches Simon1994PlanetaryPositions', () => {
sameOver('moon inertial', moonPositionInertial, (ms) =>
Simon.computeMoonPositionInEarthInertialFrame(julian(ms)),
);
});
test('temeToPseudoFixed matches Transforms.computeTemeToPseudoFixedMatrix', () => {
for (const ms of TIMES) {
const ours = temeToPseudoFixed(ms);
const theirs = Cesium.Transforms.computeTemeToPseudoFixedMatrix(julian(ms));
['x', 'y', 'z'].forEach((axis, c) =>
['x', 'y', 'z'].forEach((k, r) =>
assert.ok(
Object.is(ours[axis][k], theirs[c * 3 + r]),
`${axis}.${k} at ${iso(ms)} (${ms}): ${ours[axis][k]} vs ${theirs[c * 3 + r]}`,
),
),
);
}
});
test('sunPositionFixed matches the pseudo-fixed rotation of the sun', () => {
sameOver('sun fixed', sunPositionFixed, (ms) =>
cesiumFixed(ms, Simon.computeSunPositionInEarthInertialFrame),
);
});
test('moonPositionFixed matches the pseudo-fixed rotation of the moon', () => {
sameOver('moon fixed', moonPositionFixed, (ms) =>
cesiumFixed(ms, Simon.computeMoonPositionInEarthInertialFrame),
);
});
test('positions are plausible: 1 AU sun, ~384,000 km moon', () => {
const ms = Date.UTC(2026, 10, 5);
const sun = sunPositionInertial(ms);
const moon = moonPositionInertial(ms);
const au = Math.hypot(sun.x, sun.y, sun.z) / 1.4959787e11;
const moonKm = Math.hypot(moon.x, moon.y, moon.z) / 1000;
assert.ok(au > 0.98 && au < 1.02, `sun at ${au} AU`);
assert.ok(moonKm > 356000 && moonKm < 407000, `moon at ${moonKm} km`);
});
test('times outside the model throw', () => {
for (const fn of [
sunPositionInertial,
moonPositionInertial,
temeToPseudoFixed,
sunPositionFixed,
moonPositionFixed,
])
for (const ms of [NaN, Infinity, -Infinity, 8.64e15 + 1])
assert.throws(() => fn(ms), RangeError, `${fn.name}(${ms})`);
const farFuture = Date.parse('+050000-01-01T00:00:00Z');
assert.throws(() => sunPositionInertial(farFuture), /eccentricity/);
for (const fn of [sunPositionInertial, moonPositionInertial])
assert.throws(() => fn(-8.64e15), /inclination/);
});
+2 -1
View File
@@ -1,10 +1,11 @@
/**
* @module geo
* @description Renderer-neutral geodesy and vector maths: WGS84, ECEF and
* geodetic conversions, east-north-up frames, quaternions, and the sun and
* geodetic conversions, east-north-up frames, and the sun and
* moon. Plain numbers and `{x, y, z}` objects only, so code that needs maths
* but not a renderer does not import one.
*/
export * from './vector.js';
export * from './ellipsoid.js';
export * from './frames.js';
export * from './celestial.js';