@tubular/astronomy
Version:
Astronomical calculations for planetary positions, moon phases, eclipses, rise, transit, and set times, and more.
417 lines • 17.3 kB
JavaScript
import { DateTime, Timezone, parseISODate } from '@tubular/time';
import { abs, Angle, atan, cos, cos_deg, cosh, HALF_PI, interpolate, interpolateModular, log, max, min, mod, PI, pow, sign, signZP, sin, sin_deg, sinh, SphericalPosition, SphericalPosition3D, sqrt, tan, to_radian, TWO_PI } from '@tubular/math';
import { compareCaseSecondary, compareStrings, isNumber, padLeft, replace } from '@tubular/util';
import { ASTEROID_BASE, COMET_BASE, K_DEG, K_RAD, NO_MATCH } from './astro-constants';
import { Ecliptic } from './ecliptic';
const NEAR_PARABOLIC_E_LOW = 0.98;
const NEAR_PARABOLIC_E_HIGH = 1.1;
export class ObjectInfo {
constructor() {
this.cfMin = Number.MAX_VALUE;
this.cfMax = -Number.MAX_VALUE;
}
toString() {
const tEpoch = new DateTime(DateTime.millisFromJulianDay(this.epoch), Timezone.UT_ZONE);
const epoch = tEpoch.toYMDhmString();
const tTp = new DateTime(DateTime.millisFromJulianDay(this.Tp), Timezone.UT_ZONE);
const Tp = tTp.toYMDhmString();
return `${this.name}: epoch=${epoch}, a=${this.a}, q=${this.q}, e=${this.e}, i=${this.i}, w=${this.ω}, ` +
`L=${this.L}, Tp=${Tp}, n=${this.n}` +
(this.hasMag ? `, H=${this.H}, G=${this.G}` : '');
}
}
export class AdditionalOrbitingObjects {
static getAdditionalOrbitingObjects(astroDataService) {
if (this.properlyInitialized)
return Promise.resolve(new AdditionalOrbitingObjects());
else if (this.properlyInitialized === false)
return Promise.reject(new Error('Failed to initialize AdditionalOrbitingObjects'));
else {
return Promise.all([astroDataService.getAsteroidData(), astroDataService.getCometData()]).then((data) => {
this.readElements(data[0], true);
this.readElements(data[1], false);
this.properlyInitialized = true;
return this.getAdditionalOrbitingObjects(astroDataService);
}).catch((reason) => {
this.properlyInitialized = false;
return Promise.reject(new Error('Failed to initialize AdditionalOrbitingObjects: ' + reason));
});
}
}
static readElements(data, asAsteroids) {
data.forEach((body) => {
const name = body.body.name;
let shortName = name;
const matches = /([^(]+) \([^()]+\)/.exec(name);
if (matches)
shortName = matches[1];
const menuNameBase = (asAsteroids ? 'Asteroid: ' : 'Comet: ');
let id;
const elements = [];
if (asAsteroids)
id = ++this.lastAsteroidId;
else
id = ++this.lastCometId;
body.elements.forEach((element) => {
var _a;
const oi = new ObjectInfo();
const ymd = parseISODate(element.epoch);
oi.name = name;
oi.menuName = menuNameBase + name;
oi.shortMenuName = menuNameBase + shortName;
oi.id = id;
oi.epoch = DateTime.julianDay_SGC(ymd.y, ymd.m, ymd.d, 0, 0, 0);
oi.hasMag = asAsteroids;
oi.asteroid = asAsteroids;
oi.a = element.q / (1 - element.e);
oi.q = element.q;
oi.e = element.e;
oi.i = element.i;
oi.ω = (_a = element.w) !== null && _a !== void 0 ? _a : element.ω;
oi.L = element.L;
oi.Tp = element.Tp;
oi.n = K_DEG / oi.a / sqrt(oi.a);
if (asAsteroids) {
oi.H = body.body.H;
oi.G = body.body.G;
}
elements.push(oi);
});
this.objects[id] = elements;
this.objectIds.push(id);
});
}
// noinspection JSMethodCanBeStatic
getObjectCount() {
return AdditionalOrbitingObjects.objectIds.length;
}
getObjectNames(forMenu = false, shortMenuNames = true) {
let names = [];
AdditionalOrbitingObjects.objectIds.forEach((id) => {
const oia = AdditionalOrbitingObjects.objects[id];
if (oia.length > 0)
names.push(oia[0].name + (forMenu ? '\t' +
(shortMenuNames ? oia[0].shortMenuName : oia[0].menuName) : '')); // In menu form, sort asteroids as one group, comets as another.
});
function adjustName(s) {
s = s.toLowerCase();
let prefix = '';
let pos = s.indexOf('\t');
if (pos >= 0) {
prefix = s.substring(pos + 1);
s = s.substring(0, pos);
prefix = replace(prefix, s, '').trim();
}
pos = s.indexOf('/');
if (pos > 0) {
let possibleNumPart = s.substring(0, pos);
const ch = possibleNumPart.charAt(0);
if (('0' <= ch && ch <= '9') && possibleNumPart.length < 6)
possibleNumPart = padLeft(possibleNumPart, 6, '0');
s = s.substring(pos + 1) + '/' + possibleNumPart;
}
return prefix + s;
}
names.sort((a, b) => {
let result = compareStrings(adjustName(a), adjustName(b));
if (result === 0)
result = compareCaseSecondary(a, b);
return result;
});
// Strip off the name that was added to menuName to aid sorting
if (forMenu) {
names = names.map(name => {
return name.substring(name.indexOf('\t') + 1);
});
}
return names;
}
// noinspection JSMethodCanBeStatic
getAsteroidCount() {
return AdditionalOrbitingObjects.lastAsteroidId - ASTEROID_BASE;
}
// noinspection JSMethodCanBeStatic
getCometCount() {
return AdditionalOrbitingObjects.lastCometId - COMET_BASE;
}
getObjectName(bodyID) {
const oi = this.getObjectInfo(bodyID);
if (oi)
return oi.name;
else
return undefined;
}
getObjectByName(name) {
name = name.toLowerCase();
const matchId = AdditionalOrbitingObjects.objectIds.find(id => {
const oia = AdditionalOrbitingObjects.objects[id];
if (oia.length > 0)
return oia[0].name.toLowerCase() === name || oia[0].menuName.toLowerCase() === name;
else
return false;
});
if (matchId)
return matchId;
else
return NO_MATCH;
}
// noinspection JSMethodCanBeStatic
getObjectInfo(bodyID, time_JDE) {
if (!AdditionalOrbitingObjects.properlyInitialized)
return undefined;
const oia = AdditionalOrbitingObjects.objects[bodyID];
if (!oia || oia.length === 0)
return undefined;
else if (time_JDE === undefined)
return oia[0];
if (time_JDE <= oia[0].epoch)
return oia[0];
else if (time_JDE >= oia[oia.length - 1].epoch)
return oia[oia.length - 1];
for (let i = 0; i < oia.length - 1; ++i) {
const a = oia[i];
const b = oia[i + 1];
const ta = a.epoch;
const tb = b.epoch;
if (tb === time_JDE)
return b;
else if (ta < time_JDE && time_JDE < tb) {
const oi = Object.assign(Object.create(Object.getPrototypeOf(a)), a);
oi.epoch = time_JDE;
oi.prev = a;
oi.next = b;
oi.convergenceFails = (a.convergenceFails || b.convergenceFails);
oi.cfMin = min(a.cfMin, b.cfMin);
oi.cfMax = max(a.cfMax, b.cfMax);
oi.q = interpolate(ta, time_JDE, tb, a.q, b.q);
oi.e = interpolate(ta, time_JDE, tb, a.e, b.e);
oi.i = interpolateModular(ta, time_JDE, tb, a.i, b.i, 360, true);
oi.w = interpolateModular(ta, time_JDE, tb, a.ω, b.ω, 360);
oi.L = interpolateModular(ta, time_JDE, tb, a.L, b.L, 360);
oi.a = oi.q / (1 - oi.e);
oi.n = K_DEG / oi.a / sqrt(oi.a);
// Tp (time of perihelion) takes a little extra effort to interpolate because the
// value occasionally jumps from the perihelion of one orbit to the perihelion of
// the next orbit. We need to normalize these values so that we're referring to the
// same orbital period when we interpolate.
let bTp = b.Tp;
const daysForFullOrbit = 360 / oi.n;
while (bTp >= a.Tp + daysForFullOrbit / 2)
bTp -= daysForFullOrbit;
while (bTp < a.Tp - daysForFullOrbit / 2)
bTp += daysForFullOrbit;
oi.Tp = interpolate(ta, time_JDE, tb, a.Tp, bTp);
return oi;
}
}
return undefined;
}
getMagnitudeParameters(bodyID) {
const oi = this.getObjectInfo(bodyID);
if (oi == null || !oi.hasMag)
return undefined;
else
return [oi.H, oi.G];
}
getOrbitalElements(bodyID, time_JDE) {
const oi = this.getObjectInfo(bodyID, time_JDE);
if (!oi)
return undefined;
const oe = {};
// Handle precession of orbit
const ΔL = Ecliptic.precessEcliptical(new SphericalPosition(), time_JDE).longitude.degrees;
oe.a = oi.a;
oe.e = oi.e;
oe.i = oi.i;
oe.Ω = mod(oi.L + ΔL, 360);
oe.pi = mod(oi.ω + oi.L + ΔL, 360);
oe.partial = true;
return oe;
}
getHeliocentricPosition(objectInfoOrBodyId, time_JDE, doNotConverge = false) {
let oi;
if (isNumber(objectInfoOrBodyId)) {
oi = this.getObjectInfo(objectInfoOrBodyId, time_JDE);
if (oi == null)
return null;
}
else
oi = objectInfoOrBodyId;
const t = time_JDE - oi.Tp;
const e = oi.e;
const a = oi.a;
const q = oi.q;
const meanA = mod(oi.n * t, 360);
let ea;
let ef;
let v;
let r;
if (oi.convergenceFails && oi.cfMin <= time_JDE && time_JDE <= oi.cfMax)
doNotConverge = true;
if (e === 1 || (doNotConverge && abs(e - 1) < 0.0001)) { // parabolic orbit
// Adapted from _Astronomical Algorithms, 2nd Ed._ by Jean Meeus, pp. 241-243.
const W = 0.03649116245 * t / q / sqrt(q);
const G = W / 2;
const Y = pow(G + sqrt(G ** 2 + 1), 1 / 3);
const s = Y - 1 / Y;
r = q * (1 + s ** 2);
v = 2 * atan(s);
}
else if (e < NEAR_PARABOLIC_E_LOW || (doNotConverge && e < 1)) { // elliptical orbit
ea = AdditionalOrbitingObjects.kepler(e, to_radian(meanA));
if (abs(ea) === PI)
v = PI;
else {
ef = sqrt((1 + e) / (1 - e));
v = 2 * atan(ef * tan(ea / 2));
}
r = a * (1 - e ** 2) / (1 + e * cos(v));
}
else if (e > NEAR_PARABOLIC_E_HIGH || doNotConverge) { // hyperbolic orbit
// Adapted from code by Robert D. Miller.
ea = AdditionalOrbitingObjects.keplerH(e, to_radian(meanA));
const sinhEA = sinh(ea);
const coshEA = cosh(ea);
ef = sqrt((e + 1) / (e - 1));
v = 2 * atan(ef * tan(0.5 * ea));
const rsinv = abs(a) * sqrt(e ** 2 - 1) * sinhEA;
const rcosv = abs(a) * (e - coshEA);
r = rsinv ** 2 + rcosv ** 2;
}
else { // Near parabolic orbit, eccentricity [0.98, 1.1].
// Adapted from _Astronomical Algorithms, 2nd Ed._ by Jean Meeus, pp. 245-246.
if (t === 0) {
r = q;
v = 0;
}
else {
const q1 = K_RAD * sqrt((1 + e) / q) / 2 / q;
const q2 = q1 * t;
let s = 2 / 3 / abs(q2);
s = 2 / tan(2 * atan(pow(tan(atan(s) / 2), 1 / 3))) * sign(t);
const maxErr = 1E-10;
const d1 = 10000;
const g = (1 - e) / (1 + e);
let L = 0;
let s0, s1;
do {
let z = 1;
const y = s ** 2;
let g1 = -y * s;
let q3 = q2 + 2 * g * s * y / 3;
let z1, f;
s0 = s;
do {
++z;
g1 = -g1 * g * y;
z1 = (z - (z + 1) * g) / (2 * z + 1);
f = z1 * g1;
q3 += f;
if (z > 50 || abs(f) > d1) {
AdditionalOrbitingObjects.failedToConverge(1, oi, time_JDE);
return this.getHeliocentricPosition(oi, time_JDE, true);
}
} while (abs(f) > maxErr);
if (++L > 50) {
AdditionalOrbitingObjects.failedToConverge(2, oi, time_JDE);
return this.getHeliocentricPosition(oi, time_JDE, true);
}
z = 0;
do {
if (++z > 50) {
AdditionalOrbitingObjects.failedToConverge(3, oi, time_JDE);
return this.getHeliocentricPosition(oi, time_JDE, true);
}
s1 = s;
s = (2 * s ** 3 / 3 + q3) / (s ** 2 + 1);
} while (abs(s - s1) > maxErr);
} while (abs(s - s0) > maxErr);
v = 2 * atan(s);
r = q * (1 + e) / (1 + e * cos(v));
}
}
// Adapted from _Astronomical Algorithms, 2nd Ed._ by Jean Meeus, p. 233.
const i = oi.i;
const L = oi.L;
const u = to_radian(oi.ω) + v;
const cosi = cos_deg(i);
const sini = sin_deg(i);
const cosL = cos_deg(L);
const sinL = sin_deg(L);
const cosu = cos(u);
const sinu = sin(u);
const x = r * (cosL * cosu - sinL * sinu * cosi);
const y = r * (sinL * cosu + cosL * sinu * cosi);
const z = r * sini * sinu;
let pos = new SphericalPosition3D(Angle.atan2_nonneg(y, x), Angle.atan2(z, sqrt(x ** 2 + y ** 2)), r);
pos = Ecliptic.precessEcliptical3D(pos, time_JDE);
return pos;
}
static failedToConverge(code, oi, time_JDE) {
oi.convergenceFails = true;
oi.cfMin = min(time_JDE, oi.cfMin);
oi.cfMax = max(time_JDE, oi.cfMax);
if (oi.prev) {
oi.prev.convergenceFails = true;
oi.prev.cfMin = min(time_JDE, oi.prev.cfMin);
oi.prev.cfMax = max(time_JDE, oi.prev.cfMax);
}
if (oi.next) {
oi.next.convergenceFails = true;
oi.next.cfMin = min(time_JDE, oi.next.cfMin);
oi.next.cfMax = max(time_JDE, oi.next.cfMax);
}
// if (debug)
// System.err.println("Failed to converge(" + code + ") for " + oi.name + " at JD " + time_JDE + " (" +
// TimeDateUtil.getISOFormatDateTime(time_JDE) + ")");
}
static kepler(ecc, meanAnomaly) {
// Binary search solution for Kepler's equation by Roger Sinnott,
// Adapted from _Astronomical Algorithms, 2nd Ed._ by Jean Meeus
// p. 206.
let f;
let e0, d, m1;
meanAnomaly = mod(meanAnomaly, TWO_PI);
if (meanAnomaly > PI) {
meanAnomaly = TWO_PI - meanAnomaly;
f = -1;
}
else
f = 1;
e0 = HALF_PI;
d = PI / 4;
for (let i = 0; i < 60; ++i) {
m1 = e0 - ecc * sin(e0);
e0 = e0 + d * sign(meanAnomaly - m1);
d /= 2;
}
return e0 * f;
}
static keplerH(ecc, meanAnomaly) {
// Solver for hyperbolic form of Kepler's equation using the
// Laguerre-Conway iteration scheme.
const maxError = 1.0E-12;
let h, dh, f, f1, f2, sine, cose;
const meanA = abs(meanAnomaly);
h = log(2 * meanA / ecc + 1.85);
do {
sine = sinh(h);
cose = cosh(h);
f = ecc * sine - h - meanA;
f1 = ecc * cose - 1;
f2 = ecc * sine;
dh = -5 * f / (f1 + signZP(f1) * sqrt(abs(16 * f1 ** 2 - 20 * f * f2)));
h = h + dh;
} while (abs(dh) >= maxError);
if (meanAnomaly < 0)
return -h;
else
return h;
}
}
AdditionalOrbitingObjects.properlyInitialized = undefined;
AdditionalOrbitingObjects.lastAsteroidId = ASTEROID_BASE;
AdditionalOrbitingObjects.lastCometId = COMET_BASE;
AdditionalOrbitingObjects.objects = {};
AdditionalOrbitingObjects.objectIds = [];
//# sourceMappingURL=additional-orbiting-objects.js.map