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.

548 lines 22.1 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. */ import { angularDiameter, AngularDiameterMethod, astronomicalUnit, Celestial, cKmPerSec, DEG2RAD, Earth, MS_PER_DAY, RAD2DEG, TAU, Vector3D, } from '../main.js'; /** * Sun metrics and operations. */ export class Sun { static J0_ = 0.0009; static J1970_ = 2440587.5; static J2000_ = 2451545; static e = DEG2RAD * 23.4397; /** * Array representing the times for different phases of the sun. Each element * in the array contains: * - The angle in degrees representing the time offset from solar noon. * - The name of the start time for the phase. * - The name of the end time for the phase. */ static times_ = [ [6, 'goldenHourDawnEnd', 'goldenHourDuskStart'], // GOLDEN_HOUR_2 [-0.3, 'sunriseEnd', 'sunsetStart'], // SUNRISE_END [-0.833, 'sunriseStart', 'sunsetEnd'], // SUNRISE [-1, 'goldenHourDawnStart', 'goldenHourDuskEnd'], // GOLDEN_HOUR_1 [-4, 'blueHourDawnEnd', 'blueHourDuskStart'], // BLUE_HOUR [-6, 'civilDawn', 'civilDusk'], // DAWN [-8, 'blueHourDawnStart', 'blueHourDuskEnd'], // BLUE_HOUR [-12, 'nauticalDawn', 'nauticalDusk'], // NAUTIC_DAWN [-15, 'amateurDawn', 'amateurDusk'], [-18, 'astronomicalDawn', 'astronomicalDusk'], // ASTRO_DAWN ]; /** * Gravitational parameter of the Sun. (km³/s²) */ static mu = 1.32712428e11; /** * The angle of the penumbra of the Sun, in radians. */ static penumbraAngle = (0.26900424 * DEG2RAD); /** * The radius of the Sun in kilometers. */ static radius = 695500.0; /** * The mean solar flux of the Sun. (W/m²) */ static solarFlux = 1367.0; /** * The solar pressure exerted by the Sun. (N/m²) It is calculated as the solar * flux divided by the speed of light. */ static solarPressure = Sun.solarFlux / (cKmPerSec * 1000); /** * The angle of the umbra, in radians. */ static umbraAngle = (0.26411888 * DEG2RAD); constructor() { // disable constructor } /** * Calculates the azimuth and elevation of the Sun for a given date, latitude, * and longitude. * @param date - The date for which to calculate the azimuth and elevation. * @param lat - The latitude in degrees. * @param lon - The longitude in degrees. * @param c - The right ascension and declination of the target. Defaults to * the Sun's right ascension and declination * @returns An object containing the azimuth and elevation of the Sun in * radians. */ static azEl(date, lat, lon, c) { const lw = (-lon * DEG2RAD); const phi = (lat * DEG2RAD); const d = Sun.date2jSince2000(date); c ??= Sun.raDec(date); const H = Sun.siderealTime(d, lw) - c.ra; return { az: Celestial.azimuth(H, phi, c.dec), el: Celestial.elevation(H, phi, c.dec), }; } /** * get number of days for a dateValue since 2000 * See: https://en.wikipedia.org/wiki/Epoch_(astronomy) * @param date date as timestamp to get days * @returns count of days */ static date2jSince2000(date) { return date.getTime() / MS_PER_DAY + Sun.J1970_ - Sun.J2000_; } /** * Calculates the angular diameter of the Sun given the observer's position * and the Sun's position. * @param obsPos The observer's position in kilometers. * @param sunPos The Sun's position in kilometers. * @returns The angular diameter of the Sun in radians. */ static diameter(obsPos, sunPos) { return angularDiameter(this.radius * 2, obsPos.subtract(sunPos).magnitude(), AngularDiameterMethod.Sphere); } /** * Calculate eclipse angles given a satellite ECI position and Sun ECI * position. * @param satPos the satellite position * @param sunPos the sun position * @returns [central body angle, central body apparent radius, sun apparent] */ static eclipseAngles(satPos, sunPos) { const satSun = sunPos.subtract(satPos); const r = satPos.magnitude(); return [ // central body angle satSun.angle(satPos.negate()), // central body apparent radius Math.asin(Earth.radiusEquator / r), // sun apparent radius Math.asin(this.radius / satSun.magnitude()), ]; } /** * Ecliptic latitude measures the distance north or south of the ecliptic, * attaining +90° at the north ecliptic pole (NEP) and -90° at the south * ecliptic pole (SEP). The ecliptic itself is 0° latitude. * @param B - ? * @returns ecliptic latitude */ static eclipticLatitude(B) { const C = TAU / 360; const L = B - 0.00569 - 0.00478 * Math.sin(C * B); return TAU * (L + 0.0003 * Math.sin(C * 2 * L)); } /** * Ecliptic longitude, also known as celestial longitude, measures the angular * distance of an object along the ecliptic from the primary direction. It is * measured positive eastwards in the fundamental plane (the ecliptic) from 0° * to 360°. The primary direction (0° ecliptic longitude) points from the * Earth towards the Sun at the vernal equinox of the Northern Hemisphere. Due * to axial precession, the ecliptic longitude of most "fixed stars" increases * by about 50.3 arcseconds per year, or 83.8 arcminutes per century. * @param M - solar mean anomaly * @returns ecliptic longitude */ static eclipticLongitude(M) { const C = DEG2RAD * (1.9148 * Math.sin(M) + 0.02 * Math.sin(2 * M) + 0.0003 * Math.sin(3 * M)); const P = DEG2RAD * 102.9372; // perihelion of Earth return (M + C + P + Math.PI); // Sun's mean longitude } /** * returns set time for the given sun altitude * @param h - height at 0 * @param lw - rad * -lng * @param phi - rad * lat; * @param dec - declination * @param n - Julian cycle * @param M - solar mean anomal * @param L - ecliptic longitude * @returns set time */ static getSetJulian(h, lw, phi, dec, n, M, L) { const w = Sun.hourAngle(h, phi, dec); const a = Sun.approxTransit_(w, lw, n); return Sun.solarTransitJulian(a, M, L); } /** * Calculates the time of the sun based on the given azimuth. * @param dateValue - The date value or Date object. * @param lat - The latitude in degrees. * @param lon - The longitude in degrees. * @param az - The azimuth in radians or degrees. * @param isDegrees - Indicates if the azimuth is in degrees. Default is false. * @returns The calculated time of the sun. * @throws Error if the azimuth, latitude, or longitude is missing. */ static getSunTimeByAz(dateValue, lat, lon, az, isDegrees = false) { if (isNaN(az)) { throw new Error('azimuth missing'); } if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lon)) { throw new Error('longitude missing'); } if (isDegrees) { az = (az * DEG2RAD); } const date = dateValue instanceof Date ? dateValue : new Date(dateValue); const lw = (DEG2RAD * -lon); const phi = (DEG2RAD * lat); let dateVal = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0, 0).getTime(); let addval = MS_PER_DAY; // / 2); dateVal += addval; while (addval > 200) { const newDate = new Date(dateVal); const d = Sun.date2jSince2000(newDate); const c = Sun.raDec(newDate); const H = Sun.siderealTime(d, lw) - c.ra; const newAz = Celestial.azimuth(H, phi, c.dec); addval /= 2; if (newAz < az) { dateVal += addval; } else { dateVal -= addval; } } return new Date(Math.floor(dateVal)); } /** * Calculates sun times for a given date and latitude/longitude * * Default altitude is 0 meters. If `isUtc` is `true`, the times are returned * as UTC, otherwise in local time. * @param dateVal - The date value or Date object. * @param lat - The latitude in degrees. * @param lon - The longitude in degrees. * @param alt - The altitude in meters. Default is 0. * @param isUtc - Indicates if the times should be returned as UTC. Default is * false. * @returns An object containing the times of the sun. */ static getTimes(dateVal, lat, lon, alt = 0, isUtc = false) { if (isNaN(lat)) { throw new Error('latitude missing'); } if (isNaN(lon)) { throw new Error('longitude missing'); } // Ensure date is a Date object const date = dateVal instanceof Date ? dateVal : new Date(dateVal); if (isUtc) { date.setUTCHours(12, 0, 0, 0); } else { date.setHours(12, 0, 0, 0); } let time; let h0 = 0; let Jset = 0; let Jrise = 0; const { Jnoon, dh, lw, phi, dec, n, M, L } = Sun.calculateJnoon_(lon, lat, alt, date); // Determine when the sun is at its highest and lowest (darkest) points. const result = { solarNoon: Sun.julian2date(Jnoon), nadir: Sun.julian2date(Jnoon + 0.5), // https://github.com/mourner/suncalc/pull/125 }; // Add all other unique times using Jnoon as a reference for (let i = 0, len = Sun.times_.length; i < len; i += 1) { time = Sun.times_[i]; const angle = time[0]; h0 = ((angle + dh) * DEG2RAD); Jset = Sun.getSetJ_(h0, lw, phi, dec, n, M, L); Jrise = Jnoon - (Jset - Jnoon); result[time[1]] = Sun.julian2date(Jrise); result[time[2]] = Sun.julian2date(Jset); } return result; } /** * hour angle * @param h - heigh at 0 * @param phi - rad * lat; * @param dec - declination * @returns hour angle */ static hourAngle(h, phi, dec) { return Math.acos((Math.sin(h) - Math.sin(phi) * Math.sin(dec)) / (Math.cos(phi) * Math.cos(dec))); } /** * convert Julian calendar to date object * @param julian day number in Julian calendar to convert * @returns result date as timestamp */ static julian2date(julian) { return new Date((julian - Sun.J1970_) * MS_PER_DAY); } /** * Julian cycle * * The Julian cycle is a period of 7980 years after which the positions of the * Sun, Moon, and planets repeat. It is used in astronomical calculations to * determine the position of celestial bodies. * * The Julian Period starts at noon on January 1, 4713 B.C.E. (Julian * calendar) and lasts for 7980 years. This was determined because it is a * time period long enough to include all of recorded history and includes * some time in the future that would incorporate the three important * calendrical cycles, the Golden Number Cycle, the Solar Cycle, and the Roman * Indiction. * * The Golden Number Cycle is a cycle of 19 years, while the Solar Cycle is a * cycle of 28 years and the Roman Indiction repeats every 15 years. Thus the * Julian Period is calculated to be 7980 years long or 2,914,695 days because * 19*28*15 = 7980. * @param date - Date object for calculating julian cycle * @param lon - Degrees longitude * @returns julian cycle */ static julianCycle(date, lon) { const lw = (-lon * DEG2RAD); const d = Sun.date2jSince2000(date); return Math.round(d - Sun.J0_ - lw / ((2 * TAU) / 2)); } /** * Calculate the lighting ratio given a satellite ECI position [satPos] _(km)_ * and Sun ECI position [sunPos] _(km)_. * * Returns `1.0` if the satellite is fully illuminated and `0.0` when fully * eclipsed. * @param satPos - The position of the satellite. * @param sunPos - The position of the sun. * @returns The lighting ratio. */ static lightingRatio(satPos, sunPos) { const [sunSatAngle, aCent, aSun] = Sun.eclipseAngles(satPos, sunPos); if (sunSatAngle - aCent + aSun <= 1e-10) { return 0.0; } else if (sunSatAngle - aCent - aSun < -1e-10) { const ssa2 = sunSatAngle * sunSatAngle; const ssaInv = 1.0 / (2.0 * sunSatAngle); const ac2 = aCent * aCent; const as2 = aSun * aSun; const acAsDiff = ac2 - as2; const a1 = (ssa2 - acAsDiff) * ssaInv; const a2 = (ssa2 + acAsDiff) * ssaInv; const asr1 = a1 / aSun; const asr2 = as2 - a1 * a1; const acr1 = a2 / aCent; const acr2 = ac2 - a2 * a2; const p1 = as2 * Math.acos(asr1) - a1 * Math.sqrt(asr2); const p2 = ac2 * Math.acos(acr1) - a2 * Math.sqrt(acr2); return 1.0 - (p1 + p2) / (Math.PI * as2); } return 1.0; } /** * Calculates the lighting factor based on the position of the satellite and the sun. * @deprecated This method was previously used. It is now deprecated and will be removed * in a future release. * @param satPos The position of the satellite. * @param sunPos The position of the sun. * @returns The lighting factor. */ static sunlightLegacy(satPos, sunPos) { let lighting = 1.0; const semiDiamEarth = Math.asin(Earth.radiusMean / Math.sqrt((-satPos.x) ** 2 + (-satPos.y) ** 2 + (-satPos.z) ** 2)) * RAD2DEG; const semiDiamSun = Math.asin(Sun.radius / Math.sqrt((-satPos.x + sunPos.x) ** 2 + (-satPos.y + sunPos.y) ** 2 + (-satPos.z + sunPos.z) ** 2)) * RAD2DEG; // Angle between earth and sun const theta = Math.acos(satPos.negate().dot(sunPos.negate()) / (Math.sqrt((-satPos.x) ** 2 + (-satPos.y) ** 2 + (-satPos.z) ** 2) * Math.sqrt((-satPos.x + sunPos.x) ** 2 + (-satPos.y + sunPos.y) ** 2 + (-satPos.z + sunPos.z) ** 2))) * RAD2DEG; if (semiDiamEarth > semiDiamSun && theta < semiDiamEarth - semiDiamSun) { lighting = 0; } if (Math.abs(semiDiamEarth - semiDiamSun) < theta && theta < semiDiamEarth + semiDiamSun) { lighting = 0.5; } if (semiDiamSun > semiDiamEarth) { lighting = 0.5; } if (theta < semiDiamSun - semiDiamEarth) { lighting = 0.5; } return lighting; } /** * Calculates the position vector of the Sun at a given epoch in the * Earth-centered inertial (ECI) coordinate system. * @param epoch - The epoch in UTC. * @returns The position vector of the Sun in Kilometers. */ static position(epoch) { const jc = epoch.toJulianCenturies(); const dtr = DEG2RAD; const lamSun = 280.46 + 36000.77 * jc; const mSun = 357.5291092 + 35999.05034 * jc; const lamEc = lamSun + 1.914666471 * Math.sin(mSun * dtr) + 0.019994643 * Math.sin(2.0 * mSun * dtr); const obliq = 23.439291 - 0.0130042 * jc; const rMag = 1.000140612 - 0.016708617 * Math.cos(mSun * dtr) - 0.000139589 * Math.cos(2.0 * mSun * dtr); const r = new Vector3D(rMag * Math.cos(lamEc * dtr), rMag * Math.cos(obliq * dtr) * Math.sin(lamEc * dtr), rMag * Math.sin(obliq * dtr) * Math.sin(lamEc * dtr)); const rMOD = r.scale(astronomicalUnit); const p = Earth.precession(epoch); return rMOD .rotZ(p.zed) .rotY(-p.theta) .rotZ(p.zeta); } /** * Calculate the Sun's apparent ECI position _(km)_ from Earth for a given UTC * [epoch]. * @param epoch - The epoch in UTC. * @returns The Sun's apparent ECI position in kilometers. */ static positionApparent(epoch) { const distance = Sun.position(epoch).magnitude(); const dSec = distance / cKmPerSec; return Sun.position(epoch.roll(-dSec)); } /** * Calculates the right ascension and declination of the Sun for a given date. * @param date - The date for which to calculate the right ascension and declination. * @returns An object containing the declination and right ascension of the Sun. */ static raDec(date) { const d = Sun.date2jSince2000(date); const M = Sun.solarMeanAnomaly_(d); const L = Sun.eclipticLongitude(M); return { dec: Celestial.declination(L, 0), ra: Celestial.rightAscension(L, 0), dist: 0, }; } /** * Return `true` if the ECI satellite position [posSat] is in eclipse at the * given UTC [epoch]. * @param epoch - The epoch in UTC. * @param posSat - The ECI position of the satellite in kilometers. * @returns `true` if the satellite is in eclipse. */ static shadow(epoch, posSat) { const posSun = Sun.positionApparent(epoch); let shadow = false; if (posSun.dot(posSat) < 0) { const angle = posSun.angle(posSat); const r = posSat.magnitude(); const satHoriz = r * Math.cos(angle); const satVert = r * Math.sin(angle); const penVert = Earth.radiusEquator + Math.tan(this.penumbraAngle) * satHoriz; if (satVert <= penVert) { shadow = true; } } return shadow; } /** * side real time * @param d - julian day * @param lw - longitude of the observer * @returns sidereal time */ static siderealTime(d, lw) { return DEG2RAD * (280.16 + 360.9856235 * d) - lw; } /** * solar transit in Julian * @param ds approxTransit * @param M solar mean anomal * @param L ecliptic longitude * @returns solar transit in Julian */ static solarTransitJulian(ds, M, L) { return Sun.J2000_ + ds + 0.0053 * Math.sin(M) - 0.0069 * Math.sin(2 * L); } /** * The approximate transit time * @param Ht hourAngle * @param lw rad * -lng * @param n Julian cycle * @returns approx transit */ static approxTransit_(Ht, lw, n) { return Sun.J0_ + (Ht + lw) / TAU + n; } static calculateJnoon_(lon, lat, alt, date) { const lw = (DEG2RAD * -lon); const phi = (DEG2RAD * lat); const dh = Sun.observerAngle_(alt); const d = Sun.date2jSince2000(date); const n = Sun.julianCycle_(d, lw); const ds = Sun.approxTransit_(0, lw, n); const M = Sun.solarMeanAnomaly_(ds); const L = Sun.eclipticLongitude(M); const dec = Celestial.declination(L, 0); const Jnoon = Sun.solarTransitJulian(ds, M, L); return { Jnoon, dh, lw, phi, dec, n, M, L }; } /** * returns set time for the given sun altitude * @param alt altitude at 0 * @param lw lng * @param phi lat * @param dec declination * @param n Julian cycle * @param M solar mean anomal * @param L ecliptic longitude * @returns sunset time in days since 2000 */ static getSetJ_(alt, lw, phi, dec, n, M, L) { const w = Sun.hourAngle(alt, phi, dec); const a = Sun.approxTransit_(w, lw, n); return Sun.solarTransitJulian(a, M, L); } static julianCycle_(d, lw) { const lonOffset = lw / TAU; return Math.round(d - Sun.J0_ - lonOffset); } /** * calculates the obderver angle * @param alt the observer altitude (in meters) relative to the horizon * @returns height for further calculations */ static observerAngle_(alt) { return ((-2.076 * Math.sqrt(alt)) / 60); } /** * get solar mean anomaly * @param d julian day * @returns solar mean anomaly */ static solarMeanAnomaly_(d) { return DEG2RAD * (357.5291 + 0.98560028 * d); } } //# sourceMappingURL=Sun.js.map