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
JavaScript
/**
* @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