UNPKG

@tubular/astronomy

Version:

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

900 lines 42.1 kB
import { abs, acos, acos_deg, Angle, asin_deg, atan2_deg, atan_deg, cos, cos_deg, exp, floor, limitNeg1to1, log, log10, max, min, mod, mod2, pow, round, sin, sin_deg, SphericalPosition, SphericalPosition3D, sqrt, tan, tan_deg, to_radian, TWO_PI, Unit } from '@tubular/math'; import { tdtToUt, ttime, utToTdt } from '@tubular/time'; import { AdditionalOrbitingObjects } from './additional-orbiting-objects'; import { ABERRATION, ASTEROID_BASE, ASTEROID_MAX, ASTROMETRIC, COMET_BASE, COMET_MAX, DEFAULT_FLAGS, DELAYED_TIME, EARTH, EARTH_RADIUS_KM, EARTH_RADIUS_POLAR_KM, FIRST_PLANET, HIGH_PRECISION, INCLINATION_MEAN_LUNAR_EQUATOR, JD_J2000, JUPITER, KM_PER_AU, LAST_PLANET, LIGHT_DAYS_PER_AU, LOW_PRECISION, MARS, MERCURY, MOON, MOON_RADIUS_KM, NEPTUNE, NO_MATCH, NUTATION, PLUTO, QUICK_PLANET, QUICK_SUN, SATURN, SIGNED_HOUR_ANGLE, SUN, SUN_RADIUS_KM, TOPOCENTRIC, TRUE_DISTANCE, UNKNOWN_MAGNITUDE, URANUS, VENUS } from './astro-constants'; import { Ecliptic, NMode } from './ecliptic'; import { MeeusMoon } from './meeus-moon'; import { Pluto } from './pluto'; import { Vsop87Planets } from './vsop87-planets'; var millisFromJulianDay = ttime.millisFromJulianDay; import { padLeft } from '@tubular/util'; function toDuration(secs) { let result = ''; let pad = 1; secs = round(secs); const hours = floor(secs / 3600); secs -= hours * 3600; const mins = floor(secs / 60); secs -= mins * 60; if (hours) { result += hours + 'h'; pad = 2; } if (hours || mins) { result += padLeft(mins, pad, '0') + 'm'; pad = 2; } result += padLeft(secs, pad, '0') + 's'; return result; } function toUtc(jdu) { return new Date(millisFromJulianDay(jdu) + 500).toISOString().replace('T', ' ').replace(/\.\d{3}Z$/, 'Z'); } export function lecToString(lec) { let result = ''; if (lec.penumbralFirstContact != null) result += 'P1 : ' + toUtc(lec.penumbralFirstContact) + '\n'; result += 'U1 : ' + toUtc(lec.firstContact) + '\n'; if (lec.peakDuration) result += 'U2 : ' + toUtc(lec.peakStarts) + '\n'; result += `max: ${toUtc(lec.maxTime)}, magnitude: ${(lec.maxEclipse / 100).toFixed(2)}${(lec.annular ? ' (annular)' : '')}\n`; if (lec.peakDuration) result += 'U3 : ' + toUtc(lec.peakEnds) + ' (duration: ' + toDuration(lec.peakDuration) + ')\n'; result += 'U4 : ' + toUtc(lec.lastContact) + ' (duration: ' + toDuration(lec.duration) + ')'; if (lec.penumbralLastContact != null) result += '\nP4 : ' + toUtc(lec.penumbralLastContact) + ' (duration: ' + toDuration(lec.penumbralDuration) + ')'; return result; } export class CircumstancesOfEclipse { constructor(ec) { Object.assign(this, ec); } toString() { return lecToString(this); } } // Orbital elements for mean equinox of date (except Pluto, J2000.0). // // t^0, t^1, t^2, t^3 // L mean longitude // a semi-major axis (no time-dependent terms) // e eccentricity // i inclination // OMEGA longitude of the ascending node // pi longitude of the perihelion // const elems = [ [ [252.250906, 149474.0722491, 0.00030350, 0.000000018], [0.387098310, 0, 0, 0], [0.20563175, 0.000020407, -0.0000000283, -0.00000000018], [7.004986, 0.0018215, -0.00001810, 0.000000056], [48.330893, 1.1861883, 0.00017542, 0.000000215], [77.456119, 1.5564776, 0.00029544, 0.000000009] ], [ [181.979801, 58519.2130302, 0.00031014, 0.000000015], [0.723329820, 0, 0, 0], [0.00677192, -0.000047765, 0.0000000981, 0.00000000046], [3.394662, 0.0010037, -0.00000088, -0.000000007], [76.679920, 0.9011206, 0.00040618, -0.000000093], [131.563703, 1.4022288, -0.00107618, -0.000005678] ], [ [100.466457, 36000.7698278, 0.00030322, 0.000000020], [1.000001018, 0, 0, 0], [0.01670863, -0.000042037, -0.0000001267, 0.00000000014], [0, 0, 0, 0], [0, 0, 0, 0], [102.937348, 1.7195366, 0.00045688, -0.000000018] ], [ [355.433000, 19141.6964471, 0.00031052, 0.000000016], [1.523679342, 0, 0, 0], [0.09340065, 0.000090484, -0.0000000806, -0.00000000025], [1.849726, -0.0006011, 0.00001276, -0.000000007], [49.558093, 0.7720959, 0.00001557, 0.000002267], [336.060234, 1.8410449, 0.00013477, 0.000000536] ], [ [34.351519, 3036.3027748, 0.00022330, 0.000000037], [5.202603209, 0.0000001913, 0, 0], [0.04849793, 0.000163225, -0.0000004714, -0.00000000201], [1.303267, -0.0054965, 0.00000466, -0.000000002], [100.464407, 1.0209774, 0.00040315, 0.000000404], [14.331207, 1.6126352, 0.00103042, -0.000004464] ], [ [50.077444, 1223.5110686, 0.00051908, -0.000000030], [9.554909192, -0.0000021390, 0.000000004, 0], [0.05554814, -0.000346641, -0.0000006436, 0.00000000340], [2.488879, -0.0037362, -0.00001519, 0.000000087], [113.665503, 0.8770880, -0.00012176, -0.000002249], [93.057237, 1.9637613, 0.00083753, 0.000004928] ], [ [314.055005, 429.8640561, 0.00030390, 0.000000026], [19.218446062, -0.0000000372, 0.00000000098, 0], [0.04638122, -0.000027293, 0.0000000789, 0.00000000024], [0.773197, 0.0007744, 0.00003749, -0.000000092], [74.005957, 0.5211278, 0.00133947, 0.000018484], [173.005291, 1.4863790, 0.00021406, 0.000000434] ], [ [304.348665, 219.8833092, 0.00030882, 0.000000018], [30.110386869, -0.0000001663, 0.00000000069, 0], [0.00945575, 0.000006033, 0.0000000000, -0.00000000005], [1.769953, -0.0093082, -0.00000708, 0.000000027], [131.784057, 1.1022039, 0.00025952, -0.000000637], [48.120276, 1.4262957, 0.00038434, 0.000000020] ], [ [238.96, 144.96, 0, 0], [39.543, 0, 0, 0], [0.2490, 0, 0, 0], [17.140, 0, 0, 0], [110.307, 0, 0, 0], [224.075, 0, 0, 0], ] ]; export class SolarSystem { constructor() { this.ecliptic = new Ecliptic(); this.moon = new MeeusMoon(); this.planets = new Vsop87Planets(); this.pluto = new Pluto(); this.planetNames = ['Sun', 'Mercury', 'Venus', 'Earth', 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', 'Pluto', 'Moon']; this.planetSymbols = ['\u2609', '\u263F', '\u2640', '\u2641', '\u2642', '\u2643', '\u2644', '\u2645', '\u2646', '\u2647', '\u263D']; } static getPrecision(planet, flags) { if ((flags & HIGH_PRECISION) !== 0 || planet === PLUTO) return 0; else if (planet === MOON) return (flags & LOW_PRECISION) !== 0 ? 0.1 : 0.01; else return 0; } // Result in degrees // static getGreenwichMeanSiderealTime(time_JDU) { const t = time_JDU - JD_J2000; const T = t / 36525; return mod(280.46061837 + 360.98564736629 * t + 0.000387933 * T ** 2 - T ** 3 / 38710000, 360); } static isNominalPlanet(planet) { return (FIRST_PLANET <= planet && planet <= LAST_PLANET); } static isTruePlanet(planet) { return (MERCURY <= planet || planet <= NEPTUNE); } static isAsteroid(planet) { return (ASTEROID_BASE < planet && planet <= ASTEROID_MAX); } static isComet(planet) { return (COMET_BASE < planet && planet <= COMET_MAX); } static getAsteroidCount() { if (this.sharedAdditionals) return this.sharedAdditionals.getAsteroidCount(); else return 0; } static getCometCount() { if (this.sharedAdditionals) return this.sharedAdditionals.getCometCount(); else return 0; } static getAsteroidAndCometNames(forMenu = false, shortMenuNames = true) { if (this.sharedAdditionals) return this.sharedAdditionals.getObjectNames(forMenu, shortMenuNames); else return []; } static isAsteroidOrComet(planet) { return (SolarSystem.isAsteroid(planet) || SolarSystem.isComet(planet)); } static orbitsSun(planet) { return ((MERCURY <= planet && planet <= PLUTO) || SolarSystem.isAsteroidOrComet(planet)); } static getOrbitalElements(planet, time_JDE) { if (planet < MERCURY || planet > PLUTO) { if (this.sharedAdditionals && this.isAsteroidOrComet(planet)) return this.sharedAdditionals.getOrbitalElements(planet, time_JDE); return undefined; } const index = planet - MERCURY; const oe = {}; const T = (time_JDE - JD_J2000) / 36525; let t = 1; const elem = [0, 0, 0, 0, 0, 0]; for (let p = 0; p < 4; ++p) { for (let n = 0; n < 6; ++n) elem[n] += elems[index][n][p] * t; t *= T; } oe.L = mod(elem[0], 360); oe.a = elem[1]; oe.e = elem[2]; oe.i = elem[3]; oe.Ω = mod(elem[4], 360); oe.pi = mod(elem[5], 360); // All other planets besides Pluto have automatically computed precession. if (planet === PLUTO) { const ΔL = Ecliptic.precessEcliptical(new SphericalPosition(), time_JDE).longitude.degrees; oe.L = mod(oe.L + ΔL, 360); oe.Ω = mod(oe.Ω + ΔL, 360); oe.pi = mod(oe.pi + ΔL, 360); } oe.ω = mod(oe.pi - oe.Ω, 360); oe.M = mod(oe.L - oe.pi, 360); const M = to_radian(oe.M); let E0 = M, E1 = M; for (let i = 0; i < 100; ++i) { // Limiting number of iterations to a max of 100 E1 = M + oe.e * sin(E0); if (abs(mod2(E1 - E0, TWO_PI)) < 1.0E-6) break; E0 = E1; } oe.v = mod(2 * atan_deg(sqrt((1 + oe.e) / (1 - oe.e)) * tan(E1 / 2)), 360); oe.C = mod(oe.v - oe.M, 360); return oe; } static getHeliocentricPositionFromElements(oe) { const cos_i = cos_deg(oe.i); const sin_i = sin_deg(oe.i); const cos_o = cos_deg(oe.Ω); const sin_o = sin_deg(oe.Ω); const r = oe.a * (1 - oe.e * oe.e) / (1 + oe.e * cos_deg(oe.v)); const vpo = oe.v + oe.pi - oe.Ω; const cos_vpo = cos_deg(vpo); const sin_vpo = sin_deg(vpo); const x = r * (cos_o * cos_vpo - sin_o * sin_vpo * cos_i); const y = r * (sin_o * cos_vpo + cos_o * sin_vpo * cos_i); const z = r * sin_vpo * sin_i; return new SphericalPosition3D(Angle.atan2_nonneg(y, x), Angle.atan2(z, sqrt(x ** 2 + y ** 2)), r); } // Result in days per revolution. // static getMeanOrbitalPeriod(planet) { if (planet < MERCURY || planet > PLUTO) return 0; // Convert degrees per Julian century into days per revolution. return 100 * 365.25 * 360 / elems[planet - MERCURY][0][1]; } // Result in days per mean conjunction period. // static getMeanConjunctionPeriod(planet) { if (planet === EARTH || planet < FIRST_PLANET || planet > LAST_PLANET) return 0; let p0 = SolarSystem.getMeanOrbitalPeriod(planet); let p1 = SolarSystem.getMeanOrbitalPeriod(EARTH); if (p0 === 0) return 0; if (p1 < p0) { const temp = p0; p0 = p1; p1 = temp; } let catchUp = 1; let total = 0; for (let i = 0; i < 25; ++i) { total += catchUp * p0; catchUp = catchUp * p0 / p1; } return total; } static initAsteroidsAndComets(dataService) { if (this.sharedAdditionalsInitPending) { this.sharedAdditionalsInitPending = false; this.sharedAdditionalsPendingPromise = AdditionalOrbitingObjects.getAdditionalOrbitingObjects(dataService).then(ao => { this.sharedAdditionals = ao; this.sharedAdditionalsPendingPromise = null; return Promise.resolve(true); }).catch(result => { this.sharedAdditionalsPendingPromise = null; console.log('Failed to initialize asteroids and comets: ', result); return Promise.resolve(false); }); return this.sharedAdditionalsPendingPromise; } else if (this.sharedAdditionalsPendingPromise) return this.sharedAdditionalsPendingPromise; else return Promise.resolve(!!this.sharedAdditionals); } getPlanetName(planet) { if (SolarSystem.sharedAdditionals && SolarSystem.isAsteroidOrComet(planet)) return SolarSystem.sharedAdditionals.getObjectName(planet); if (planet >= 0 && planet < this.planetNames.length) return this.planetNames[planet]; return undefined; } getPlanetByName(planetName) { planetName = planetName.toLowerCase(); for (let i = 0; i < this.planetNames.length; ++i) if (this.planetNames[i].toLowerCase() === planetName) return i; if (SolarSystem.sharedAdditionals) return SolarSystem.sharedAdditionals.getObjectByName(planetName); return NO_MATCH; } getPlanetSymbol(planet) { if (planet >= 0 && planet < this.planetSymbols.length) return this.planetSymbols[planet]; return undefined; } getHeliocentricPosition(planet, time_JDE, flags = 0) { let result = null; const precisionFlags = flags & ~LOW_PRECISION & ~HIGH_PRECISION; if (MERCURY <= planet && planet <= NEPTUNE) { if (this.planets !== null && (flags & QUICK_PLANET) === 0) result = this.planets.getHeliocentricPosition(planet, time_JDE, SolarSystem.getPrecision(planet, flags)); else result = SolarSystem.getHeliocentricPositionFromElements(SolarSystem.getOrbitalElements(planet, time_JDE)); } else if (planet === SUN) return new SphericalPosition3D(); else if (planet === MOON) { const sunPos = this.getEclipticPosition(SUN, time_JDE, null, precisionFlags); result = this.getEclipticPosition(MOON, time_JDE, null, precisionFlags).translate(sunPos); } else if (planet === PLUTO) { if (this.pluto !== null && (flags & QUICK_PLANET) === 0) result = this.pluto.getHeliocentricPosition(time_JDE); else result = SolarSystem.getHeliocentricPositionFromElements(SolarSystem.getOrbitalElements(planet, time_JDE)); } else if (SolarSystem.isAsteroidOrComet(planet) && SolarSystem.sharedAdditionals) result = SolarSystem.sharedAdditionals.getHeliocentricPosition(planet, time_JDE); return result; } getEclipticPosition(planet, time_JDE, observer, flags = DEFAULT_FLAGS, earthTime) { if (earthTime == null) earthTime = time_JDE; if (flags === DEFAULT_FLAGS) { flags = ABERRATION | NUTATION; if (observer) flags |= TOPOCENTRIC; } let result; // Round-about, but effective: If we're asked to compute an ecliptic position with // topocentric correction, we compute an equatorial position with topocentric // correction and convert that result into an ecliptic position. The computation // of an equatorial position, however, first requires the computation of an ecliptic // position. A catch-22 is avoided by making sure the equatorial computation does // not pass the topocentric flag into this function. if ((flags & TOPOCENTRIC) !== 0 && observer != null) { let equPos = this.getEquatorialPosition(planet, time_JDE, observer, flags); equPos = new SphericalPosition3D(equPos.longitude, equPos.latitude, equPos.radius - EARTH_RADIUS_KM / KM_PER_AU); return this.ecliptic.equatorialToEcliptic3D(equPos, time_JDE, (flags & NUTATION) !== 0 ? NMode.NUTATED : NMode.J2000); } if (planet === EARTH) return new SphericalPosition3D(); else if (planet === MOON) result = this.moon.getEclipticPosition(time_JDE); else if (planet === SUN && (flags & QUICK_SUN) !== 0) { const T = (time_JDE - JD_J2000) / 36525; const T2 = T ** 2; const e = 0.016708634 - 0.000042037 * T - 0.0000001267 * T2; const L0 = 280.46646 + 36000.76983 * T + 0.0003032 * T2; const M = 357.52911 + 35999.05029 * T - 0.0001537 * T2; const C = (1.914602 - 0.004817 * T - 0.000014 * T2) * sin_deg(M) + (0.019993 - 0.000101 * T) * sin_deg(2 * M) + 0.000289 * sin_deg(3 * M); const L = mod(L0 + C, 360); const R = 1.000001018 * (1 - e ** 2) / (1 + e * cos_deg(M + C)); result = new SphericalPosition3D(L, 0, R, Unit.DEGREES, Unit.RADIANS); } else if (SolarSystem.isNominalPlanet(planet) || SolarSystem.isAsteroidOrComet(planet)) { const earthPos = this.getHeliocentricPosition(EARTH, earthTime, flags); const planetPos = this.getHeliocentricPosition(planet, time_JDE, flags); if (planetPos == null) return null; else result = planetPos.translate(earthPos); } else return null; if ((flags & ABERRATION) !== 0 || (flags & ASTROMETRIC) !== 0 || (flags & DELAYED_TIME) !== 0) { let adjPos = null; let distance = result.radius; let delayedTime = time_JDE; const flags2 = flags & ~ABERRATION & ~ASTROMETRIC & ~DELAYED_TIME & ~NUTATION; // This converges very quickly. Three iterations is easily enough, // just one for the Moon. for (let i = 0; i < (planet === MOON ? 1 : 3); ++i) { delayedTime = time_JDE - LIGHT_DAYS_PER_AU * distance; if ((flags & ASTROMETRIC) !== 0) adjPos = this.getEclipticPosition(planet, delayedTime, null, flags2, earthTime); else adjPos = this.getEclipticPosition(planet, delayedTime, null, flags2, delayedTime); distance = adjPos.radius; } if ((flags & TRUE_DISTANCE) !== 0) result = new SphericalPosition3D(adjPos.longitude, adjPos.latitude, result.radius); else if ((flags & DELAYED_TIME) !== 0) // The DELAYED_TIME flag indicates that light delay time should replace distance in the // result so that the caller of this method can know the moment in time when a body was // in the calculated position. result = new SphericalPosition3D(adjPos.longitude, adjPos.latitude, delayedTime); else result = adjPos; } if ((flags & NUTATION) !== 0) result = this.ecliptic.nutateEclipticPosition3D(result, time_JDE); return result; } getEquatorialPosition(planet, time_JDE, observer, flags = DEFAULT_FLAGS) { if (planet === EARTH) return new SphericalPosition3D(); if (flags === DEFAULT_FLAGS) { flags = ABERRATION | NUTATION; if (observer) flags |= TOPOCENTRIC; } let obliquityMode; if ((flags & NUTATION) !== 0) obliquityMode = NMode.NUTATED; else obliquityMode = NMode.MEAN_OBLIQUITY; const eclipticPos = this.getEclipticPosition(planet, time_JDE, null, flags & ~TOPOCENTRIC); let pos = this.ecliptic.eclipticToEquatorial3D(eclipticPos, time_JDE, obliquityMode); if ((flags & TOPOCENTRIC) !== 0 && observer != null) pos = observer.equatorialTopocentricAdjustment(pos, time_JDE, flags); return pos; } // Result in degrees // getGreenwichApparentSiderealTime(time_JDU) { const gmst = SolarSystem.getGreenwichMeanSiderealTime(time_JDU); const nutation = this.ecliptic.getNutation(utToTdt(time_JDU)); return mod(gmst + nutation.Δψ.degrees * nutation.ε.cos, 360); } // Note that getHorizontalPosition() is LOW_PRECISION by default -- which is still typically better // than one arcsecond for the planets, 2-3 arcseconds for the Moon. // // Always topocentric for Moon, even if TOPOCENTRIC flag isn't set -- error too great otherwise. // Topocentric adjustment, on the other hand, is usually quite small for other planets // getHorizontalPosition(planet, time_JDU, observer, flags = ABERRATION | LOW_PRECISION) { if (observer == null || (!SolarSystem.isNominalPlanet(planet) && !SolarSystem.isAsteroidOrComet(planet))) return null; else if (planet === EARTH) return new SphericalPosition3D(); // The SkyObserver method equatorialToHorizontal3D() expects to process coordinates // that do not include the effects of nutation. flags &= ~NUTATION; if (planet === MOON) flags |= TOPOCENTRIC; const pos = this.getEquatorialPosition(planet, utToTdt(time_JDU), observer, flags); return observer.equatorialToHorizontal(pos, time_JDU, flags); } getHourAngle(planet, time_JDU, observer, flags = DEFAULT_FLAGS) { if (flags === DEFAULT_FLAGS) { flags = ABERRATION; if (planet === MOON) flags |= TOPOCENTRIC; } flags &= ~NUTATION; const pos = this.getEquatorialPosition(planet, utToTdt(time_JDU), observer, flags); if ((flags & SIGNED_HOUR_ANGLE) !== 0) return observer.getLocalHourAngle(time_JDU, false).subtract(pos.rightAscension); else return observer.getLocalHourAngle(time_JDU, false).subtract_nonneg(pos.rightAscension); } getParallacticAngle(planet, time_JDU, observer, flags = DEFAULT_FLAGS) { var _a; if (planet < SUN || planet > MOON) return null; if (flags === DEFAULT_FLAGS) { flags = ABERRATION; if (observer != null) flags |= TOPOCENTRIC; } flags &= ~NUTATION; const pos = this.getEquatorialPosition(planet, utToTdt(time_JDU), observer, flags); const hourAngle = this.getHourAngle(planet, time_JDU, observer, flags); const numerator = hourAngle.sin; const denominator = (_a = (observer && observer.latitude.tan * pos.declination.cos - pos.declination.sin * hourAngle.cos)) !== null && _a !== void 0 ? _a : 0; if (denominator === 0) return null; return Angle.atan2(numerator, denominator); } // Result continuously-variable value in degrees. // Key values: 0 - new, 90 - first quarter, 180 - full, 270 - last quarter. // getLunarPhase(time_JDE) { const posMoon = this.getEclipticPosition(MOON, time_JDE, null, ABERRATION | LOW_PRECISION); const posSun = this.getEclipticPosition(SUN, time_JDE, null, ABERRATION | LOW_PRECISION); return mod(posMoon.longitude.degrees - posSun.longitude.degrees, 360); } // Note: this method is different from using getIlluminatedFraction(MOON, time_JDE) // because it depends solely on longitudinal separation. // getLunarIlluminatedFraction(time_JDE) { return (1 - cos_deg(this.getLunarPhase(time_JDE))) / 2; } getCosPhaseAngle(planet, time_JDE) { const r = this.getHeliocentricPosition(planet, time_JDE, LOW_PRECISION).radius; const D = this.getEclipticPosition(planet, time_JDE, null, ABERRATION | LOW_PRECISION).radius; const R = this.getHeliocentricPosition(EARTH, time_JDE, LOW_PRECISION).radius; const cpa = (r ** 2 + D ** 2 - R ** 2) / (2 * r * D); // Rounding error can cause this number to slightly exceed the valid // range [-1, 1], returning an invalid argument for the arc cos function. return limitNeg1to1(cpa); } getPhaseAngle(planet, time_JDE) { if (planet <= SUN || planet === EARTH || planet > MOON) return 0; return acos_deg(this.getCosPhaseAngle(planet, time_JDE)); } getIlluminatedFraction(planet, time_JDE) { if (planet <= SUN || planet === EARTH || planet > MOON) return 0; return (1 + this.getCosPhaseAngle(planet, time_JDE)) / 2; } // Angular separation of apparent ecliptic coordinates. // Result in non-negative degrees. // getSolarElongation(planet, time_JDE, observer, flags = DEFAULT_FLAGS) { if (planet === SUN || planet === EARTH) return 0; if (flags === DEFAULT_FLAGS) { flags = ABERRATION; if (observer != null) flags |= TOPOCENTRIC; } const sunPos = this.getEclipticPosition(SUN, time_JDE, observer, flags); const planetPos = this.getEclipticPosition(planet, time_JDE, observer, flags); return sunPos.distanceFrom(planetPos).degrees; } // Difference in apparent longitude. // Result in degrees, positive when planet is east of Sun, negative when west. // getSolarElongationInLongitude(planet, time_JDE) { const sunPos = this.getEclipticPosition(SUN, time_JDE); const planetPos = this.getEclipticPosition(planet, time_JDE); return planetPos.longitude.subtract(sunPos.longitude).degrees; } getSaturnRingInfo(time_JDE) { const T = (time_JDE - JD_J2000) / 36525; const i = 28.075216 - 0.012998 * T + 0.000004 * T ** 2; const sin_i = sin_deg(i); const cos_i = cos_deg(i); const Ω = 169.508470 + 1.394681 * T + 0.000412 * T ** 2; const ri = {}; const delayedTime = this.getEclipticPosition(SATURN, time_JDE, null, DELAYED_TIME | LOW_PRECISION).radius; const hpos = this.getHeliocentricPosition(SATURN, delayedTime, LOW_PRECISION); const N = 113.6655 + 0.8771 * T; const r = hpos.radius; const l = hpos.longitude.degrees; const l1 = l - 0.01759 / r; const b1 = hpos.latitude.degrees - 0.000764 * cos_deg(l - N) / r; const epos = this.getEclipticPosition(SATURN, delayedTime, null, LOW_PRECISION); const λ = epos.longitude.degrees; const β = epos.latitude.degrees; const sin_β = sin_deg(β); const cos_β = cos_deg(β); const Δ = epos.radius; ri.B = asin_deg(sin_i * cos_β * sin_deg(λ - Ω) - cos_i * sin_β); ri.a = 375.35 / Δ; ri.b = ri.a * sin_deg(abs(ri.B)); ri.B1 = asin_deg(sin_deg(i) * sin_deg(b1) * sin_deg(l1 - Ω) - cos_deg(i) * sin_deg(b1)); const sin_b1 = sin_deg(b1); const cos_b1 = cos_deg(b1); const U1 = atan2_deg(sin_i * sin_b1 + cos_i * cos_b1 * sin_deg(l1 - Ω), cos_b1 * cos_deg(l1 - Ω)); const U2 = atan2_deg(sin_i * sin_β + cos_i * cos_β * sin_deg(λ - Ω), cos_b1 * cos_deg(λ - Ω)); ri.dU = abs(U1 - U2); const lambda0 = Ω - 90; const beta0 = 90 - i; // Equatorial position of Saturn (with aberration). const eqpos = this.getEquatorialPosition(SATURN, time_JDE, null, ABERRATION | LOW_PRECISION); // Equatorial position of northern pole of the ring plain. const eqpos0 = this.ecliptic.eclipticToEquatorial(new SphericalPosition(lambda0, beta0, Unit.DEGREES, Unit.DEGREES)); const a = eqpos.rightAscension.radians; const d = eqpos.declination.radians; const a0 = eqpos0.rightAscension.radians; const d0 = eqpos0.declination.radians; ri.P = atan2_deg(cos(d0) * sin(a0 - a), sin(d0) * cos(d) - cos(d0) * sin(d) * cos(a0 - a)); return ri; } getMagnitude(planet, time_JDE) { const r = this.getHeliocentricPosition(planet, time_JDE, QUICK_SUN | LOW_PRECISION).radius; const Δ = this.getEclipticPosition(planet, time_JDE, null, QUICK_SUN | LOW_PRECISION).radius; const i = this.getPhaseAngle(planet, time_JDE); const i2 = i ** 2; const i3 = i2 * i; const _5log_rD = 5 * log10(r * Δ); switch (planet) { // Mercury and Venus from "Improving the Visual Magnitudes of the Planets in the Astronomical // Almanac. I. Mercury and Venus", by James L. Hilton, The Astronomical Journal, June 2005. case MERCURY: return -0.60 + _5log_rD + 0.0498 * i - 0.000488 * i2 + 0.00000302 * i3; case VENUS: if (i < 163.3) return -4.47 + _5log_rD + 0.0103 * i + 0.000057 * i2 + 0.00000013 * i3; else return 0.98 + _5log_rD - 0.0102 * i; case MARS: return -1.52 + _5log_rD + 0.016 * i; case JUPITER: return -9.40 + _5log_rD + 0.005 * i; case SATURN: // eslint-disable-next-line no-case-declarations const ri = this.getSaturnRingInfo(time_JDE); // eslint-disable-next-line no-case-declarations const sin_B = sin_deg(ri.B); return -8.88 + _5log_rD + 0.044 * ri.dU - 2.60 * sin_deg(abs(ri.B)) + 1.25 * sin_B ** 2; case URANUS: return -7.19 + _5log_rD; case NEPTUNE: return -6.87 + _5log_rD; case PLUTO: return -1.00 + _5log_rD; case MOON: return 0.23 + _5log_rD + 0.026 * i + 4.0E-9 * i3 * i; case SUN: return -26.74 + 5 * log10(Δ); default: if (SolarSystem.sharedAdditionals && SolarSystem.isAsteroidOrComet(planet)) { const mp = SolarSystem.sharedAdditionals.getMagnitudeParameters(planet); if (mp) { const f1 = exp(-3.33 * pow(tan_deg(i / 2), 0.63)); const f2 = exp(-1.87 * pow(tan_deg(i / 2), 1.22)); const H = mp[0], G = mp[1]; return H + _5log_rD - 2.5 * log((1 - G) * f1 + G * f2); } } } return UNKNOWN_MAGNITUDE; } // Result in arcseconds. getAngularDiameter(planet, time_JDE, observer = null, polarSize = false) { if (planet < SUN || planet === EARTH || planet > MOON) return 0; let Δ = (planet === MOON ? KM_PER_AU : 1); if (observer != null && planet === MOON) Δ *= this.getHorizontalPosition(planet, time_JDE, observer).radius; else Δ *= this.getEclipticPosition(planet, time_JDE, null, ABERRATION + QUICK_SUN).radius; let r = 0; switch (planet) { case SUN: r = 959.63 / Δ; break; case MOON: r = acos_deg(sqrt(Δ ** 2 - MOON_RADIUS_KM ** 2) / Δ) * 3600; break; case MERCURY: r = 3.36 / Δ; break; case VENUS: r = 8.34 / Δ; break; case MARS: r = 4.68 / Δ; break; case JUPITER: r = (polarSize ? 92.06 : 98.44) / Δ; break; case SATURN: r = (polarSize ? 73.82 : 82.73) / Δ; break; case URANUS: r = 35.02 / Δ; break; case NEPTUNE: r = 33.50 / Δ; break; case PLUTO: r = 2.07 / Δ; break; } return r * 2; } getLunarLibration(time_JDE, observer) { // Adapted from _Astronomical Algorithms, 2nd Ed._ by Jean Meeus, pp. 371-375. const pos = this.getEclipticPosition(MOON, time_JDE, observer, ABERRATION | (observer ? TOPOCENTRIC : 0)); // Δψ not needed, since pos is computed without nutation. const T = (time_JDE - JD_J2000) / 36525; const F = 93.2720950 + 483202.0175233 * T - 0.0036539 * T ** 2 - T ** 3 / 3526000 + T ** 4 / 863310000; const Ω = 125.04452 - 1934.136261 * T + 0.0020708 * T ** 2 + T ** 3 / 450000; const W = pos.longitude.degrees - Ω; const cosβ = pos.latitude.cos; const sinβ = pos.latitude.sin; const cosI = cos_deg(INCLINATION_MEAN_LUNAR_EQUATOR); const sinI = sin_deg(INCLINATION_MEAN_LUNAR_EQUATOR); const A = atan2_deg(sin_deg(W) * cosβ * cosI - sinβ * sinI, cos_deg(W) * cosβ); return { l: mod2(A - F, 360), b: asin_deg(-sin_deg(W) * cosβ * sinI - sinβ * cosI), d: this.getAngularDiameter(MOON, time_JDE, observer), D: pos.radius }; } // I treat the umbra and penumbra of the Earth as imaginary circular objects // directly opposite to the Sun and located at the same distance from the // Earth as the Moon. // // If you can imagine the typical diagram of how umbral and penumbral shadows are // cast, I'm simply solving some similar triangles that can be drawn into such a // diagram to figure out the size of Moon-distanced cross-sections of the two // shadows. // getLunarEclipseInfo(time_JDE, raw = false) { const ei = {}; ei.isSolar = false; ei.pos = this.getEclipticPosition(MOON, time_JDE, null, ABERRATION | NUTATION); const sunPos = this.getEclipticPosition(SUN, time_JDE, null, ABERRATION | NUTATION); let opp = SUN_RADIUS_KM - EARTH_RADIUS_KM; // For umbral shadow. const adj = sunPos.radius * KM_PER_AU; let tanθ = opp / adj; const adj2 = ei.pos.radius * KM_PER_AU; let opp2 = tanθ * adj2; const umbra = EARTH_RADIUS_KM - opp2; ei.radius = atan_deg(MOON_RADIUS_KM / adj2) * 3600; ei.umbraRadius = atan_deg(umbra / adj2) * 3600 * 1.01398; // 1.01398 for atmospheric effect opp = SUN_RADIUS_KM + EARTH_RADIUS_KM; // For penumbral shadow. tanθ = opp / adj; opp2 = tanθ * adj2; const penumbra = EARTH_RADIUS_KM + opp2; ei.penumbraRadius = atan_deg(penumbra / adj2) * 3600 * 1.0078; // 1.0078 for atmospheric effect ei.shadowPos = new SphericalPosition(sunPos.longitude.opposite_nonneg(), sunPos.latitude.negate()); ei.centerSeparation = ei.pos.distanceFrom(ei.shadowPos).getAngle(Unit.ARC_SECONDS); ei.penumbralSeparation = ei.centerSeparation - ei.radius - ei.penumbraRadius; ei.inPenumbra = (ei.penumbralSeparation <= 0); ei.umbralSeparation = ei.centerSeparation - ei.radius - ei.umbraRadius; ei.inUmbra = (ei.umbralSeparation <= 0); ei.total = (ei.centerSeparation + ei.radius <= ei.umbraRadius); const totality = -ei.umbralSeparation / ei.radius / 2; ei.totality = raw ? totality : ei.inUmbra ? min(totality, 1) : 0; const penumbralMagnitude = -ei.penumbralSeparation / ei.radius / 2; ei.penumbralMagnitude = raw ? penumbralMagnitude : ei.inPenumbra ? min(penumbralMagnitude, 1) : 0; ei.annular = false; ei.hybrid = false; return ei; } // Similar to above method, but based on looking at the shadow of the Moon on // the Earth from a selenocentric perspective. // // Detection of hybrid eclipses may require surveying a number of moments during // the duration of a solar eclipse, not just the peak of the eclipse. // getSolarEclipseInfo(time_JDE, locateShadow = false) { const ei = {}; const moonPos = this.getEclipticPosition(MOON, time_JDE, null, ABERRATION); ei.isSolar = true; ei.pos = new SphericalPosition3D().translate(moonPos); // Earth, in selenocentric coordinates const sunPos = this.getEclipticPosition(SUN, time_JDE, null, ABERRATION).translate(moonPos); const B = sunPos.radius * KM_PER_AU; const A = SUN_RADIUS_KM - MOON_RADIUS_KM; // For umbral shadow. let b = ei.pos.radius * KM_PER_AU; let a = A * b / B; let umbra = MOON_RADIUS_KM - a; if (umbra < 0) { ei.annular = true; umbra *= -1; } else ei.annular = false; ei.radius = atan_deg(EARTH_RADIUS_KM / b) * 3600; ei.umbraRadius = atan_deg(umbra / b) * 3600; const A1 = SUN_RADIUS_KM + MOON_RADIUS_KM; // For penumbral shadow. const a1 = A1 * b / B; const penumbra = MOON_RADIUS_KM + a1; ei.penumbraRadius = atan_deg(penumbra / b) * 3600; ei.shadowPos = new SphericalPosition(sunPos.longitude.opposite_nonneg(), sunPos.latitude.negate()); ei.centerSeparation = ei.pos.distanceFrom(ei.shadowPos).getAngle(Unit.ARC_SECONDS); ei.penumbralSeparation = ei.centerSeparation - ei.radius - ei.penumbraRadius; ei.inPenumbra = (ei.penumbralSeparation <= 0); ei.umbralSeparation = ei.centerSeparation - ei.radius - ei.umbraRadius; ei.inUmbra = (ei.umbralSeparation <= 0); ei.total = ei.inUmbra && !ei.annular; ei.totality = ei.inUmbra ? min(-ei.umbralSeparation / ei.radius / 2, 1) : 0; ei.annular = ei.annular && ei.inUmbra; ei.hybrid = false; // Taking into account how the curvature of the Earth brings an observer closer // to the Moon, there's a possibility of moving out of the anti-umbra into the // umbra, resulting in a hybrid eclipse. if (ei.annular) { const umbraFromCenter = max(ei.centerSeparation - ei.umbraRadius, 0); if (umbraFromCenter < ei.radius) { const earthCurveAdj = EARTH_RADIUS_KM * sin(acos(limitNeg1to1(umbraFromCenter / ei.radius))); b -= earthCurveAdj; a = A * b / B; umbra = MOON_RADIUS_KM - a; if (umbra >= 0) { ei.annular = false; ei.hybrid = true; ei.total = false; } } } if (locateShadow) { // Compute where a line going through the center of the Sun and the Moon // intersects the sphere of the Earth. const time_JDU = tdtToUt(time_JDE); const siderealTime = SolarSystem.getGreenwichMeanSiderealTime(time_JDU); const flattening = EARTH_RADIUS_KM / EARTH_RADIUS_POLAR_KM; const sunPt = this.getEquatorialPosition(SUN, time_JDE, null, ABERRATION).xyz; const xs = sunPt.x, ys = sunPt.y, zs = sunPt.z * flattening; const moonPt = this.getEquatorialPosition(MOON, time_JDE, null, ABERRATION).xyz; const xm = moonPt.x, ym = moonPt.y, zm = moonPt.z * flattening; const r = EARTH_RADIUS_KM / KM_PER_AU; const dx = xs - xm; const dy = ys - ym; const dz = zs - zm; a = dx ** 2 + dy ** 2 + dz ** 2; b = 2 * (xm * dx + ym * dy + zm * dz); const c = xm ** 2 + ym ** 2 + zm ** 2 - r ** 2; const radicand = max(b ** 2 - 4 * a * c, 0); const u = (-b + sqrt(radicand)) / 2 / a; const xh = xm + u * dx; const yh = ym + u * dy; const zh = (zm + u * dz) / flattening; const shadowCtr = SphericalPosition3D.convertRectangular(xh, yh, zh); ei.surfaceShadow = SolarSystem.createSkyObserver(shadowCtr.longitude.degrees - siderealTime, shadowCtr.latitude.degrees); } return ei; } getLunarEclipseTotality(time_JDE, raw = false, penumbraMagnitude) { const ei = this.getLunarEclipseInfo(time_JDE, raw); if (penumbraMagnitude) penumbraMagnitude[0] = ei.penumbralMagnitude; return raw ? ei.totality : min(max(ei.totality, 0), 1); } getLocalSolarEclipseTotality(time_JDE, observer, raw = false, annularity) { const separation = this.getSolarElongation(MOON, time_JDE, observer); if (separation > 1 && !raw) return 0; const moonRadius = this.getAngularDiameter(MOON, time_JDE, observer) / 7200; const sunRadius = this.getAngularDiameter(SUN, time_JDE) / 7200; const overlap = sunRadius + moonRadius - separation; const totality = overlap / sunRadius / 2; if (annularity) { if (moonRadius < sunRadius) annularity[0] = overlap / moonRadius / 2; else annularity[0] = 0; } return raw ? totality : min(max(totality, 0), 1); } getTimeForDegreesOfChange(bodyID, startTime_JDE, degrees, maxTime_JDE) { const startPos = this.getHeliocentricPosition(bodyID, startTime_JDE); let testPos; const tolerance = degrees / 100000; const sign = (maxTime_JDE < startTime_JDE ? -1 : 1); let minTime = startTime_JDE; let δ = sign; let result = startTime_JDE + δ; let change; let found = false; for (let i = 0; i < 200; ++i) { // Impose a maximum number of iterations before giving up. testPos = this.getHeliocentricPosition(bodyID, result); change = startPos.distanceFrom(testPos).degrees; if (abs(change - degrees) < tolerance || result === maxTime_JDE) { found = true; break; } else if (change < degrees) { minTime = result; δ *= 2; if (sign < 0) result = Math.max(result + δ, maxTime_JDE); else result = Math.min(result + δ, maxTime_JDE); } else { result = (result + minTime) / 2; δ /= 2; } } return (found ? result : Number.MAX_VALUE); } } SolarSystem.sharedAdditionalsInitPending = true; //# sourceMappingURL=solar-system.js.map