UNPKG

ootk-core

Version:

Orbital Object Toolkit. A modern typed replacement for satellite.js including SGP4 propagation, TLE parsing, Sun and Moon calculations, and more.

557 lines (511 loc) 17 kB
/** * @author Theodore Kruczek. * @license MIT * @copyright (c) 2022-2025 Theodore Kruczek Permission is * hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the * Software without restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do * so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * @copyright (c) 2011-2015, Vladimir Agafonkin * @copyright (c) 2022 Robert Gester https://github.com/hypnos3/suncalc3 * @see suncalc.LICENSE.md * Some of the math in this file was originally created by Vladimir Agafonkin. * Robert Gester's update was referenced for documentation. There were a couple * of bugs in both versions so there will be some differences if you are * migrating from either to this library. * * suncalc is a JavaScript library for calculating sun/moon position and light * phases. https://github.com/mourner/suncalc * It was reworked and enhanced by Robert Gester. * * The original suncalc is released under the terms of the BSD 2-Clause License. * @see http://aa.quae.nl/en/reken/hemelpositie.html * moon calculations are based on formulas from this website */ import { AngularDiameterMethod, Celestial, Degrees, Kilometers, RaDec, Radians } from '../main.js'; import { Vector3D } from '../operations/Vector3D.js'; import { EpochUTC } from '../time/EpochUTC.js'; import { DEG2RAD, MS_PER_DAY } from '../utils/constants.js'; import { angularDiameter } from '../utils/functions.js'; import { Earth } from './Earth.js'; import { Sun } from './Sun.js'; type MoonIlluminationData = { fraction: number; phase: { from: number; to: number; id: string; emoji: string; code: string; name: string; weight: number; css: string; }; phaseValue: number; angle: number; next: { value: number; date: string; type: string; newMoon: { value: number; date: string; }; fullMoon: { value: number; date: string; }; firstQuarter: { value: number; date: string; }; thirdQuarter: { value: number; date: string; }; }; }; // / Moon metrics and operations. export class Moon { private constructor() { // disable constructor } // / Moon gravitational parameter _(km³/s²)_. static readonly mu = 4902.799; // / Moon equatorial radius _(km)_. static readonly radiusEquator = 1738.0; // / Calculate the Moon's ECI position _(km)_ for a given UTC [epoch]. static eci(epoch: EpochUTC = EpochUTC.fromDateTime(new Date())): Vector3D<Kilometers> { const jc = epoch.toJulianCenturies(); const dtr = DEG2RAD; const lamEcl = 218.32 + 481267.8813 * jc + 6.29 * Math.sin((134.9 + 477198.85 * jc) * dtr) - 1.27 * Math.sin((259.2 - 413335.38 * jc) * dtr) + 0.66 * Math.sin((235.7 + 890534.23 * jc) * dtr) + 0.21 * Math.sin((269.9 + 954397.7 * jc) * dtr) - 0.19 * Math.sin((357.5 + 35999.05 * jc) * dtr) - 0.11 * Math.sin((186.6 + 966404.05 * jc) * dtr); const phiEcl = 5.13 * Math.sin((93.3 + 483202.03 * jc) * dtr) + 0.28 * Math.sin((228.2 + 960400.87 * jc) * dtr) - 0.28 * Math.sin((318.3 + 6003.18 * jc) * dtr) - 0.17 * Math.sin((217.6 - 407332.2 * jc) * dtr); const pllx = 0.9508 + 0.0518 * Math.cos((134.9 + 477198.85 * jc) * dtr) + 0.0095 * Math.cos((259.2 - 413335.38 * jc) * dtr) + 0.0078 * Math.cos((235.7 + 890534.23 * jc) * dtr) + 0.0028 * Math.cos((269.9 + 954397.7 * jc) * dtr); const obq = 23.439291 - 0.0130042 * jc; const rMag = 1 / Math.sin(pllx * dtr); const r = new Vector3D( rMag * Math.cos(phiEcl * dtr) * Math.cos(lamEcl * dtr), rMag * (Math.cos(obq * dtr) * Math.cos(phiEcl * dtr) * Math.sin(lamEcl * dtr) - Math.sin(obq * dtr) * Math.sin(phiEcl * dtr)), rMag * (Math.sin(obq * dtr) * Math.cos(phiEcl * dtr) * Math.sin(lamEcl * dtr) + Math.cos(obq * dtr) * Math.sin(phiEcl * dtr)), ); const rMOD = r.scale(Earth.radiusEquator); const p = Earth.precession(epoch); return rMOD .rotZ(p.zed) .rotY(-p.theta as Radians) .rotZ(p.zeta); } /** * Calculates the illumination of the Moon at a given epoch. * @param epoch - The epoch in UTC. * @param origin - The origin vector. Defaults to the origin vector if not provided. * @returns The illumination of the Moon, ranging from 0 to 1. */ static illumination(epoch: EpochUTC, origin?: Vector3D<Kilometers>): number { const orig = origin ?? (Vector3D.origin as Vector3D<Kilometers>); const sunPos = Sun.position(epoch).subtract(orig); const moonPos = this.eci(epoch).subtract(orig); const phaseAngle = sunPos.angle(moonPos); return 0.5 * (1 - Math.cos(phaseAngle)); } /** * Calculates the diameter of the Moon. * @param obsPos - The position of the observer. * @param moonPos - The position of the Moon. * @returns The diameter of the Moon. */ static diameter(obsPos: Vector3D, moonPos: Vector3D): number { return angularDiameter(this.radiusEquator * 2, obsPos.subtract(moonPos).magnitude(), AngularDiameterMethod.Sphere); } /** * calculations for illumination parameters of the moon, based on * http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and Chapter 48 of "Astronomical Algorithms" 2nd * edition by Jean Meeus (Willmann-Bell, Richmond) 1998. * @param date Date object or timestamp for calculating moon-illumination * @returns result object of moon-illumination */ // eslint-disable-next-line max-statements static getMoonIllumination(date: number | Date): MoonIlluminationData { const dateValue = date instanceof Date ? date.getTime() : date; const lunarDaysMs = 2551442778; // The duration in days of a lunar cycle is 29.53058770576 days. const firstNewMoon2000 = 947178840000; // first newMoon in the year 2000 2000-01-06 18:14 const dateObj = new Date(dateValue); const d = Sun.date2jSince2000(dateObj); const s = Sun.raDec(dateObj); const m = Moon.moonCoords(d); const sdist = 149598000; // distance from Earth to Sun in km const phi = Math.acos( Math.sin(s.dec) * Math.sin(m.dec) + Math.cos(s.dec) * Math.cos(m.dec) * Math.cos(s.ra - m.ra), ); const inc = Math.atan2(sdist * Math.sin(phi), m.dist - sdist * Math.cos(phi)); const angle = Math.atan2( Math.cos(s.dec) * Math.sin(s.ra - m.ra), Math.sin(s.dec) * Math.cos(m.dec) - Math.cos(s.dec) * Math.sin(m.dec) * Math.cos(s.ra - m.ra), ); const phaseValue = 0.5 + (0.5 * inc * (angle < 0 ? -1 : 1)) / Math.PI; /* * calculates the difference in ms between the sirst fullMoon 2000 and given * Date */ const diffBase = dateValue - firstNewMoon2000; // Calculate modulus to drop completed cycles let cycleModMs = diffBase % lunarDaysMs; // If negative number (date before new moon 2000) add lunarDaysMs if (cycleModMs < 0) { cycleModMs += lunarDaysMs; } const nextNewMoon = lunarDaysMs - cycleModMs + dateValue; let nextFullMoon = lunarDaysMs / 2 - cycleModMs + dateValue; if (nextFullMoon < dateValue) { nextFullMoon += lunarDaysMs; } const quater = lunarDaysMs / 4; let nextFirstQuarter = quater - cycleModMs + dateValue; if (nextFirstQuarter < dateValue) { nextFirstQuarter += lunarDaysMs; } let nextThirdQuarter = lunarDaysMs - quater - cycleModMs + dateValue; if (nextThirdQuarter < dateValue) { nextThirdQuarter += lunarDaysMs; } /* * Calculate the fraction of the moon cycle const currentfrac = cycleModMs / * lunarDaysMs; */ const next = Math.min(nextNewMoon, nextFirstQuarter, nextFullMoon, nextThirdQuarter); // eslint-disable-next-line init-declarations let phase: (typeof Moon.moonCycles_)[0] | null = null; for (const moonCycle of Moon.moonCycles_) { if (phaseValue >= moonCycle.from && phaseValue <= moonCycle.to) { phase = moonCycle; break; } } if (!phase) { throw new Error('Moon phase not found'); } let type = ''; if (next === nextNewMoon) { type = 'newMoon'; } else if (next === nextFirstQuarter) { type = 'firstQuarter'; } else if (next === nextFullMoon) { type = 'fullMoon'; } else { type = 'thirdQuarter'; } return { fraction: (1 + Math.cos(inc)) / 2, phase, phaseValue, angle, next: { value: next, date: new Date(next).toISOString(), type, newMoon: { value: nextNewMoon, date: new Date(nextNewMoon).toISOString(), }, fullMoon: { value: nextFullMoon, date: new Date(nextFullMoon).toISOString(), }, firstQuarter: { value: nextFirstQuarter, date: new Date(nextFirstQuarter).toISOString(), }, thirdQuarter: { value: nextThirdQuarter, date: new Date(nextThirdQuarter).toISOString(), }, }, }; } static rae( date: Date, lat: Degrees, lon: Degrees, ): { az: Radians; el: Radians; rng: Kilometers; parallacticAngle: Radians; } { const lw = <Radians>(DEG2RAD * -lon); const phi = <Radians>(DEG2RAD * lat); const d = Sun.date2jSince2000(date); const c = Moon.moonCoords(d); const H = Sun.siderealTime(d, lw) - c.ra; let h = Celestial.elevation(H, phi, c.dec); /* * formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus * (Willmann-Bell, Richmond) 1998. */ const pa = Math.atan2(Math.sin(H), Math.tan(phi) * Math.cos(c.dec) - Math.sin(c.dec) * Math.cos(H)); h = <Radians>(h + Celestial.atmosphericRefraction(h)); // altitude correction for refraction return { az: Celestial.azimuth(H, phi, c.dec), el: h, rng: c.dist, parallacticAngle: pa as Radians, }; } /** * calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article * @param date Date object or timestamp for calculating moon rise/set * @param lat Latitude of observer in degrees * @param lon Longitude of observer in degrees * @param isUtc If true, date will be interpreted as UTC * @returns result object of moon rise/set */ static getMoonTimes(date: Date, lat: Degrees, lon: Degrees, isUtc = false) { // Clone the date so we don't change the original const date_ = new Date(date); if (isUtc) { date_.setUTCHours(0, 0, 0, 0); } else { date_.setHours(0, 0, 0, 0); } const { rise, set, ye } = Moon.calculateRiseSetTimes_(date_, lat, lon); const result = { rise: null as Date | null, set: null as Date | null, ye: null as number | null, alwaysUp: null as boolean | null, alwaysDown: null as boolean | null, highest: null as Date | null, }; if (rise) { result.rise = new Date(Moon.hoursLater_(date_, rise)); } if (set) { result.set = new Date(Moon.hoursLater_(date_, set)); } if (!rise && !set) { if (ye > 0) { result.alwaysUp = true; result.alwaysDown = false; } else { result.alwaysUp = false; result.alwaysDown = true; } } else if (rise && set) { result.alwaysUp = false; result.alwaysDown = false; result.highest = new Date(Moon.hoursLater_(date_, Math.min(rise, set) + Math.abs(set - rise) / 2)); } else { result.alwaysUp = false; result.alwaysDown = false; } return result; } private static hoursLater_(date: Date, h: number) { return new Date(date.getTime() + (h * MS_PER_DAY) / 24); } /** * Calculates the geocentric ecliptic coordinates of the moon. * @param d - The number of days since year 2000. * @returns An object containing the right ascension, declination, and * distance to the moon. */ static moonCoords(d: number): RaDec { const L = DEG2RAD * (218.316 + 13.176396 * d); // ecliptic longitude const M = DEG2RAD * (134.963 + 13.064993 * d); // mean anomaly const F = DEG2RAD * (93.272 + 13.22935 * d); // mean distance const l = L + DEG2RAD * 6.289 * Math.sin(M); // longitude const b = DEG2RAD * 5.128 * Math.sin(F); // latitude const dt = 385001 - 20905 * Math.cos(M); // distance to the moon in km return { ra: Celestial.rightAscension(l, b), dec: Celestial.declination(l, b), dist: dt as Kilometers, }; } private static calculateRiseSetTimes_(t: Date, lat: Degrees, lon: Degrees) { const hc = 0.133 * DEG2RAD; let h0 = Moon.rae(t, lat, lon).el - hc; let h1 = 0; let h2 = 0; let rise = 0; let set = 0; let a = 0; let b = 0; let xe = 0; let ye = 0; let d = 0; let roots = 0; let x1 = 0; let x2 = 0; let dx = 0; /* * go in 2-hour chunks, each time seeing if a 3-point quadratic curve * crosses zero (which means rise or set) */ for (let i = 1; i <= 24; i += 2) { h1 = Moon.rae(Moon.hoursLater_(t, i), lat, lon).el - hc; h2 = Moon.rae(Moon.hoursLater_(t, i + 1), lat, lon).el - hc; a = (h0 + h2) / 2 - h1; b = (h2 - h0) / 2; xe = -b / (2 * a); ye = (a * xe + b) * xe + h1; d = b * b - 4 * a * h1; roots = 0; if (d >= 0) { dx = Math.sqrt(d) / (Math.abs(a) * 2); x1 = xe - dx; x2 = xe + dx; if (Math.abs(x1) <= 1) { roots++; } if (Math.abs(x2) <= 1) { roots++; } if (x1 < -1) { x1 = x2; } } if (roots === 1) { if (h0 < 0) { rise = i + x1; } else { set = i + x1; } } else if (roots === 2) { rise = i + (ye < 0 ? x2 : x1); set = i + (ye < 0 ? x1 : x2); } if (rise && set) { break; } h0 = h2; } return { rise, set, ye }; } private static readonly moonCycles_ = [ { from: 0, to: 0.033863193308711, id: 'newMoon', emoji: '🌚', code: ':new_moon_with_face:', name: 'New Moon', weight: 1, css: 'wi-moon-new', }, { from: 0.033863193308711, to: 0.216136806691289, id: 'waxingCrescentMoon', emoji: '🌒', code: ':waxing_crescent_moon:', name: 'Waxing Crescent', weight: 6.3825, css: 'wi-moon-wax-cres', }, { from: 0.216136806691289, to: 0.283863193308711, id: 'firstQuarterMoon', emoji: '🌓', code: ':first_quarter_moon:', name: 'First Quarter', weight: 1, css: 'wi-moon-first-quart', }, { from: 0.283863193308711, to: 0.466136806691289, id: 'waxingGibbousMoon', emoji: '🌔', code: ':waxing_gibbous_moon:', name: 'Waxing Gibbous', weight: 6.3825, css: 'wi-moon-wax-gibb', }, { from: 0.466136806691289, to: 0.533863193308711, id: 'fullMoon', emoji: '🌝', code: ':full_moon_with_face:', name: 'Full Moon', weight: 1, css: 'wi-moon-full', }, { from: 0.533863193308711, to: 0.716136806691289, id: 'waningGibbousMoon', emoji: '🌖', code: ':waning_gibbous_moon:', name: 'Waning Gibbous', weight: 6.3825, css: 'wi-moon-wan-gibb', }, { from: 0.716136806691289, to: 0.783863193308711, id: 'thirdQuarterMoon', emoji: '🌗', code: ':last_quarter_moon:', name: 'third Quarter', weight: 1, css: 'wi-moon-third-quart', }, { from: 0.783863193308711, to: 0.966136806691289, id: 'waningCrescentMoon', emoji: '🌘', code: ':waning_crescent_moon:', name: 'Waning Crescent', weight: 6.3825, css: 'wi-moon-wan-cres', }, { from: 0.966136806691289, to: 1, id: 'newMoon', emoji: '🌚', code: ':new_moon_with_face:', name: 'New Moon', weight: 1, css: 'wi-moon-new', }, ]; }