UNPKG

@tubular/astronomy

Version:

Astronomical calculations for planetary positions, moon phases, eclipses, rise, transit, and set times, and more.

417 lines 17.3 kB
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