mirror of
https://github.com/cosinekitty/astronomy.git
synced 2026-01-18 04:18:08 -05:00
8019 lines
329 KiB
JavaScript
8019 lines
329 KiB
JavaScript
/**
|
|
@preserve
|
|
|
|
Astronomy library for JavaScript (browser and Node.js).
|
|
https://github.com/cosinekitty/astronomy
|
|
|
|
MIT License
|
|
|
|
Copyright (c) 2019-2021 Don Cross <cosinekitty@gmail.com>
|
|
|
|
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.
|
|
*/
|
|
/**
|
|
* @fileoverview Astronomy calculation library for browser scripting and Node.js.
|
|
* @author Don Cross <cosinekitty@gmail.com>
|
|
* @license MIT
|
|
*/
|
|
'use strict';
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.HelioState = exports.BaryState = exports.GeoVector = exports.HelioDistance = exports.HelioVector = exports.JupiterMoons = exports.JupiterMoonsInfo = exports.GeoEmbState = exports.GeoMoonState = exports.GeoMoon = exports.Ecliptic = exports.ObserverGravity = exports.VectorObserver = exports.ObserverState = exports.ObserverVector = exports.Equator = exports.SunPosition = exports.Observer = exports.Horizon = exports.EclipticCoordinates = exports.HorizontalCoordinates = exports.MakeRotation = exports.RotationMatrix = exports.EquatorialCoordinates = exports.Spherical = exports.StateVector = exports.Vector = exports.Libration = exports.LibrationInfo = exports.CalcMoonCount = exports.MakeTime = exports.AstroTime = exports.SetDeltaTFunction = exports.DeltaT_JplHorizons = exports.DeltaT_EspenakMeeus = exports.Body = exports.AngleBetween = exports.CALLISTO_RADIUS_KM = exports.GANYMEDE_RADIUS_KM = exports.EUROPA_RADIUS_KM = exports.IO_RADIUS_KM = exports.JUPITER_MEAN_RADIUS_KM = exports.JUPITER_POLAR_RADIUS_KM = exports.JUPITER_EQUATORIAL_RADIUS_KM = exports.RAD2HOUR = exports.RAD2DEG = exports.HOUR2RAD = exports.DEG2RAD = exports.KM_PER_AU = exports.C_AUDAY = void 0;
|
|
exports.Rotation_EQD_ECL = exports.Rotation_EQJ_HOR = exports.Rotation_HOR_EQJ = exports.Rotation_HOR_EQD = exports.Rotation_EQD_HOR = exports.Rotation_EQD_EQJ = exports.Rotation_EQJ_EQD = exports.Rotation_ECL_EQJ = exports.Rotation_EQJ_ECL = exports.RotateState = exports.RotateVector = exports.InverseRefraction = exports.Refraction = exports.VectorFromHorizon = exports.HorizonFromVector = exports.SphereFromVector = exports.EquatorFromVector = exports.VectorFromSphere = exports.Pivot = exports.IdentityMatrix = exports.CombineRotation = exports.InverseRotation = exports.NextPlanetApsis = exports.SearchPlanetApsis = exports.NextLunarApsis = exports.SearchLunarApsis = exports.Apsis = exports.SearchPeakMagnitude = exports.SearchMaxElongation = exports.Elongation = exports.ElongationEvent = exports.Seasons = exports.SeasonInfo = exports.SearchHourAngle = exports.HourAngleEvent = exports.SearchAltitude = exports.SearchRiseSet = exports.NextMoonQuarter = exports.SearchMoonQuarter = exports.MoonQuarter = exports.SearchMoonPhase = exports.MoonPhase = exports.SearchRelativeLongitude = exports.Illumination = exports.IlluminationInfo = exports.EclipticLongitude = exports.AngleFromSun = exports.PairLongitude = exports.SearchSunLongitude = exports.Search = void 0;
|
|
exports.NextTransit = exports.SearchTransit = exports.TransitInfo = exports.NextLocalSolarEclipse = exports.SearchLocalSolarEclipse = exports.LocalSolarEclipseInfo = exports.EclipseEvent = exports.NextGlobalSolarEclipse = exports.SearchGlobalSolarEclipse = exports.NextLunarEclipse = exports.GlobalSolarEclipseInfo = exports.SearchLunarEclipse = exports.LunarEclipseInfo = exports.Constellation = exports.ConstellationInfo = exports.Rotation_GAL_EQJ = exports.Rotation_EQJ_GAL = exports.Rotation_HOR_ECL = exports.Rotation_ECL_HOR = exports.Rotation_ECL_EQD = void 0;
|
|
/**
|
|
* @brief The speed of light in AU/day.
|
|
*/
|
|
exports.C_AUDAY = 173.1446326846693;
|
|
/**
|
|
* @brief The number of kilometers per astronomical unit.
|
|
*/
|
|
exports.KM_PER_AU = 1.4959787069098932e+8;
|
|
/**
|
|
* @brief The factor to convert degrees to radians = pi/180.
|
|
*/
|
|
exports.DEG2RAD = 0.017453292519943296;
|
|
/**
|
|
* @brief The factor to convert sidereal hours to radians = pi/12.
|
|
*/
|
|
exports.HOUR2RAD = 0.2617993877991494365;
|
|
/**
|
|
* @brief The factor to convert radians to degrees = 180/pi.
|
|
*/
|
|
exports.RAD2DEG = 57.295779513082321;
|
|
/**
|
|
* @brief The factor to convert radians to sidereal hours = 12/pi.
|
|
*/
|
|
exports.RAD2HOUR = 3.819718634205488;
|
|
// Jupiter radius data are nominal values obtained from:
|
|
// https://www.iau.org/static/resolutions/IAU2015_English.pdf
|
|
// https://nssdc.gsfc.nasa.gov/planetary/factsheet/jupiterfact.html
|
|
/**
|
|
* @brief The equatorial radius of Jupiter, expressed in kilometers.
|
|
*/
|
|
exports.JUPITER_EQUATORIAL_RADIUS_KM = 71492.0;
|
|
/**
|
|
* @brief The polar radius of Jupiter, expressed in kilometers.
|
|
*/
|
|
exports.JUPITER_POLAR_RADIUS_KM = 66854.0;
|
|
/**
|
|
* @brief The volumetric mean radius of Jupiter, expressed in kilometers.
|
|
*/
|
|
exports.JUPITER_MEAN_RADIUS_KM = 69911.0;
|
|
// The radii of Jupiter's four major moons are obtained from:
|
|
// https://ssd.jpl.nasa.gov/?sat_phys_par
|
|
/**
|
|
* @brief The mean radius of Jupiter's moon Io, expressed in kilometers.
|
|
*/
|
|
exports.IO_RADIUS_KM = 1821.6;
|
|
/**
|
|
* @brief The mean radius of Jupiter's moon Europa, expressed in kilometers.
|
|
*/
|
|
exports.EUROPA_RADIUS_KM = 1560.8;
|
|
/**
|
|
* @brief The mean radius of Jupiter's moon Ganymede, expressed in kilometers.
|
|
*/
|
|
exports.GANYMEDE_RADIUS_KM = 2631.2;
|
|
/**
|
|
* @brief The mean radius of Jupiter's moon Callisto, expressed in kilometers.
|
|
*/
|
|
exports.CALLISTO_RADIUS_KM = 2410.3;
|
|
const DAYS_PER_TROPICAL_YEAR = 365.24217;
|
|
const J2000 = new Date('2000-01-01T12:00:00Z');
|
|
const PI2 = 2 * Math.PI;
|
|
const ARC = 3600 * (180 / Math.PI); // arcseconds per radian
|
|
const ASEC2RAD = 4.848136811095359935899141e-6;
|
|
const ASEC180 = 180 * 60 * 60; // arcseconds per 180 degrees (or pi radians)
|
|
const ASEC360 = 2 * ASEC180; // arcseconds per 360 degrees (or 2*pi radians)
|
|
const ANGVEL = 7.2921150e-5;
|
|
const AU_PER_PARSEC = ASEC180 / Math.PI; // exact definition of how many AU = one parsec
|
|
const SUN_MAG_1AU = -0.17 - 5 * Math.log10(AU_PER_PARSEC); // formula from JPL Horizons
|
|
const MEAN_SYNODIC_MONTH = 29.530588; // average number of days for Moon to return to the same phase
|
|
const SECONDS_PER_DAY = 24 * 3600;
|
|
const MILLIS_PER_DAY = SECONDS_PER_DAY * 1000;
|
|
const SOLAR_DAYS_PER_SIDEREAL_DAY = 0.9972695717592592;
|
|
const SUN_RADIUS_KM = 695700.0;
|
|
const SUN_RADIUS_AU = SUN_RADIUS_KM / exports.KM_PER_AU;
|
|
const EARTH_FLATTENING = 0.996647180302104;
|
|
const EARTH_EQUATORIAL_RADIUS_KM = 6378.1366;
|
|
const EARTH_EQUATORIAL_RADIUS_AU = EARTH_EQUATORIAL_RADIUS_KM / exports.KM_PER_AU;
|
|
const EARTH_POLAR_RADIUS_KM = EARTH_EQUATORIAL_RADIUS_KM * EARTH_FLATTENING;
|
|
const EARTH_MEAN_RADIUS_KM = 6371.0; /* mean radius of the Earth's geoid, without atmosphere */
|
|
const EARTH_ATMOSPHERE_KM = 88.0; /* effective atmosphere thickness for lunar eclipses */
|
|
const EARTH_ECLIPSE_RADIUS_KM = EARTH_MEAN_RADIUS_KM + EARTH_ATMOSPHERE_KM;
|
|
const MOON_EQUATORIAL_RADIUS_KM = 1738.1;
|
|
const MOON_MEAN_RADIUS_KM = 1737.4;
|
|
const MOON_POLAR_RADIUS_KM = 1736.0;
|
|
const MOON_EQUATORIAL_RADIUS_AU = (MOON_EQUATORIAL_RADIUS_KM / exports.KM_PER_AU);
|
|
const REFRACTION_NEAR_HORIZON = 34 / 60; // degrees of refractive "lift" seen for objects near horizon
|
|
const EARTH_MOON_MASS_RATIO = 81.30056;
|
|
/*
|
|
Masses of the Sun and outer planets, used for:
|
|
(1) Calculating the Solar System Barycenter
|
|
(2) Integrating the movement of Pluto
|
|
|
|
https://web.archive.org/web/20120220062549/http://iau-comm4.jpl.nasa.gov/de405iom/de405iom.pdf
|
|
|
|
Page 10 in the above document describes the constants used in the DE405 ephemeris.
|
|
The following are G*M values (gravity constant * mass) in [au^3 / day^2].
|
|
This side-steps issues of not knowing the exact values of G and masses M[i];
|
|
the products GM[i] are known extremely accurately.
|
|
*/
|
|
const SUN_GM = 0.2959122082855911e-03;
|
|
const JUPITER_GM = 0.2825345909524226e-06;
|
|
const SATURN_GM = 0.8459715185680659e-07;
|
|
const URANUS_GM = 0.1292024916781969e-07;
|
|
const NEPTUNE_GM = 0.1524358900784276e-07;
|
|
let ob2000; // lazy-evaluated mean obliquity of the ecliptic at J2000, in radians
|
|
let cos_ob2000;
|
|
let sin_ob2000;
|
|
function VerifyBoolean(b) {
|
|
if (b !== true && b !== false) {
|
|
console.trace();
|
|
throw `Value is not boolean: ${b}`;
|
|
}
|
|
return b;
|
|
}
|
|
function VerifyNumber(x) {
|
|
if (!Number.isFinite(x)) {
|
|
console.trace();
|
|
throw `Value is not a finite number: ${x}`;
|
|
}
|
|
return x;
|
|
}
|
|
function Frac(x) {
|
|
return x - Math.floor(x);
|
|
}
|
|
/**
|
|
* @brief Calculates the angle in degrees between two vectors.
|
|
*
|
|
* Given a pair of vectors, this function returns the angle in degrees
|
|
* between the two vectors in 3D space.
|
|
* The angle is measured in the plane that contains both vectors.
|
|
*
|
|
* @param {Vector} a
|
|
* The first of a pair of vectors between which to measure an angle.
|
|
*
|
|
* @param {Vector} b
|
|
* The second of a pair of vectors between which to measure an angle.
|
|
*
|
|
* @returns {number}
|
|
* The angle between the two vectors expressed in degrees.
|
|
* The value is in the range [0, 180].
|
|
*/
|
|
function AngleBetween(a, b) {
|
|
const aa = (a.x * a.x + a.y * a.y + a.z * a.z);
|
|
if (Math.abs(aa) < 1.0e-8)
|
|
throw `AngleBetween: first vector is too short.`;
|
|
const bb = (b.x * b.x + b.y * b.y + b.z * b.z);
|
|
if (Math.abs(bb) < 1.0e-8)
|
|
throw `AngleBetween: second vector is too short.`;
|
|
const dot = (a.x * b.x + a.y * b.y + a.z * b.z) / Math.sqrt(aa * bb);
|
|
if (dot <= -1.0)
|
|
return 180;
|
|
if (dot >= +1.0)
|
|
return 0;
|
|
const angle = exports.RAD2DEG * Math.acos(dot);
|
|
return angle;
|
|
}
|
|
exports.AngleBetween = AngleBetween;
|
|
/**
|
|
* @brief String constants that represent the solar system bodies supported by Astronomy Engine.
|
|
*
|
|
* The following strings represent solar system bodies supported by various Astronomy Engine functions.
|
|
* Not every body is supported by every function; consult the documentation for each function
|
|
* to find which bodies it supports.
|
|
*
|
|
* "Sun", "Moon", "Mercury", "Venus", "Earth", "Mars", "Jupiter",
|
|
* "Saturn", "Uranus", "Neptune", "Pluto",
|
|
* "SSB" (Solar System Barycenter),
|
|
* "EMB" (Earth/Moon Barycenter)
|
|
*
|
|
* You can also use enumeration syntax for the bodies, like
|
|
* `Astronomy.Body.Moon`, `Astronomy.Body.Jupiter`, etc.
|
|
*
|
|
* @enum {string}
|
|
*/
|
|
var Body;
|
|
(function (Body) {
|
|
Body["Sun"] = "Sun";
|
|
Body["Moon"] = "Moon";
|
|
Body["Mercury"] = "Mercury";
|
|
Body["Venus"] = "Venus";
|
|
Body["Earth"] = "Earth";
|
|
Body["Mars"] = "Mars";
|
|
Body["Jupiter"] = "Jupiter";
|
|
Body["Saturn"] = "Saturn";
|
|
Body["Uranus"] = "Uranus";
|
|
Body["Neptune"] = "Neptune";
|
|
Body["Pluto"] = "Pluto";
|
|
Body["SSB"] = "SSB";
|
|
Body["EMB"] = "EMB"; // Earth/Moon Barycenter
|
|
})(Body = exports.Body || (exports.Body = {}));
|
|
var PrecessDirection;
|
|
(function (PrecessDirection) {
|
|
PrecessDirection[PrecessDirection["From2000"] = 0] = "From2000";
|
|
PrecessDirection[PrecessDirection["Into2000"] = 1] = "Into2000";
|
|
})(PrecessDirection || (PrecessDirection = {}));
|
|
const Planet = {
|
|
Mercury: { OrbitalPeriod: 87.969 },
|
|
Venus: { OrbitalPeriod: 224.701 },
|
|
Earth: { OrbitalPeriod: 365.256 },
|
|
Mars: { OrbitalPeriod: 686.980 },
|
|
Jupiter: { OrbitalPeriod: 4332.589 },
|
|
Saturn: { OrbitalPeriod: 10759.22 },
|
|
Uranus: { OrbitalPeriod: 30685.4 },
|
|
Neptune: { OrbitalPeriod: 60189.0 },
|
|
Pluto: { OrbitalPeriod: 90560.0 }
|
|
};
|
|
const vsop = {
|
|
Mercury: [
|
|
[
|
|
[
|
|
[4.40250710144, 0.00000000000, 0.00000000000],
|
|
[0.40989414977, 1.48302034195, 26087.90314157420],
|
|
[0.05046294200, 4.47785489551, 52175.80628314840],
|
|
[0.00855346844, 1.16520322459, 78263.70942472259],
|
|
[0.00165590362, 4.11969163423, 104351.61256629678],
|
|
[0.00034561897, 0.77930768443, 130439.51570787099],
|
|
[0.00007583476, 3.71348404924, 156527.41884944518]
|
|
],
|
|
[
|
|
[26087.90313685529, 0.00000000000, 0.00000000000],
|
|
[0.01131199811, 6.21874197797, 26087.90314157420],
|
|
[0.00292242298, 3.04449355541, 52175.80628314840],
|
|
[0.00075775081, 6.08568821653, 78263.70942472259],
|
|
[0.00019676525, 2.80965111777, 104351.61256629678]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.11737528961, 1.98357498767, 26087.90314157420],
|
|
[0.02388076996, 5.03738959686, 52175.80628314840],
|
|
[0.01222839532, 3.14159265359, 0.00000000000],
|
|
[0.00543251810, 1.79644363964, 78263.70942472259],
|
|
[0.00129778770, 4.83232503958, 104351.61256629678],
|
|
[0.00031866927, 1.58088495658, 130439.51570787099],
|
|
[0.00007963301, 4.60972126127, 156527.41884944518]
|
|
],
|
|
[
|
|
[0.00274646065, 3.95008450011, 26087.90314157420],
|
|
[0.00099737713, 3.14159265359, 0.00000000000]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.39528271651, 0.00000000000, 0.00000000000],
|
|
[0.07834131818, 6.19233722598, 26087.90314157420],
|
|
[0.00795525558, 2.95989690104, 52175.80628314840],
|
|
[0.00121281764, 6.01064153797, 78263.70942472259],
|
|
[0.00021921969, 2.77820093972, 104351.61256629678],
|
|
[0.00004354065, 5.82894543774, 130439.51570787099]
|
|
],
|
|
[
|
|
[0.00217347740, 4.65617158665, 26087.90314157420],
|
|
[0.00044141826, 1.42385544001, 52175.80628314840]
|
|
]
|
|
]
|
|
],
|
|
Venus: [
|
|
[
|
|
[
|
|
[3.17614666774, 0.00000000000, 0.00000000000],
|
|
[0.01353968419, 5.59313319619, 10213.28554621100],
|
|
[0.00089891645, 5.30650047764, 20426.57109242200],
|
|
[0.00005477194, 4.41630661466, 7860.41939243920],
|
|
[0.00003455741, 2.69964447820, 11790.62908865880],
|
|
[0.00002372061, 2.99377542079, 3930.20969621960],
|
|
[0.00001317168, 5.18668228402, 26.29831979980],
|
|
[0.00001664146, 4.25018630147, 1577.34354244780],
|
|
[0.00001438387, 4.15745084182, 9683.59458111640],
|
|
[0.00001200521, 6.15357116043, 30639.85663863300]
|
|
],
|
|
[
|
|
[10213.28554621638, 0.00000000000, 0.00000000000],
|
|
[0.00095617813, 2.46406511110, 10213.28554621100],
|
|
[0.00007787201, 0.62478482220, 20426.57109242200]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.05923638472, 0.26702775812, 10213.28554621100],
|
|
[0.00040107978, 1.14737178112, 20426.57109242200],
|
|
[0.00032814918, 3.14159265359, 0.00000000000]
|
|
],
|
|
[
|
|
[0.00287821243, 1.88964962838, 10213.28554621100]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.72334820891, 0.00000000000, 0.00000000000],
|
|
[0.00489824182, 4.02151831717, 10213.28554621100],
|
|
[0.00001658058, 4.90206728031, 20426.57109242200],
|
|
[0.00001378043, 1.12846591367, 11790.62908865880],
|
|
[0.00001632096, 2.84548795207, 7860.41939243920],
|
|
[0.00000498395, 2.58682193892, 9683.59458111640],
|
|
[0.00000221985, 2.01346696541, 19367.18916223280],
|
|
[0.00000237454, 2.55136053886, 15720.83878487840]
|
|
],
|
|
[
|
|
[0.00034551041, 0.89198706276, 10213.28554621100]
|
|
]
|
|
]
|
|
],
|
|
Earth: [
|
|
[
|
|
[
|
|
[1.75347045673, 0.00000000000, 0.00000000000],
|
|
[0.03341656453, 4.66925680415, 6283.07584999140],
|
|
[0.00034894275, 4.62610242189, 12566.15169998280],
|
|
[0.00003417572, 2.82886579754, 3.52311834900],
|
|
[0.00003497056, 2.74411783405, 5753.38488489680],
|
|
[0.00003135899, 3.62767041756, 77713.77146812050],
|
|
[0.00002676218, 4.41808345438, 7860.41939243920],
|
|
[0.00002342691, 6.13516214446, 3930.20969621960],
|
|
[0.00001273165, 2.03709657878, 529.69096509460],
|
|
[0.00001324294, 0.74246341673, 11506.76976979360],
|
|
[0.00000901854, 2.04505446477, 26.29831979980],
|
|
[0.00001199167, 1.10962946234, 1577.34354244780],
|
|
[0.00000857223, 3.50849152283, 398.14900340820],
|
|
[0.00000779786, 1.17882681962, 5223.69391980220],
|
|
[0.00000990250, 5.23268072088, 5884.92684658320],
|
|
[0.00000753141, 2.53339052847, 5507.55323866740],
|
|
[0.00000505267, 4.58292599973, 18849.22754997420],
|
|
[0.00000492392, 4.20505711826, 775.52261132400],
|
|
[0.00000356672, 2.91954114478, 0.06731030280],
|
|
[0.00000284125, 1.89869240932, 796.29800681640],
|
|
[0.00000242879, 0.34481445893, 5486.77784317500],
|
|
[0.00000317087, 5.84901948512, 11790.62908865880],
|
|
[0.00000271112, 0.31486255375, 10977.07880469900],
|
|
[0.00000206217, 4.80646631478, 2544.31441988340],
|
|
[0.00000205478, 1.86953770281, 5573.14280143310],
|
|
[0.00000202318, 2.45767790232, 6069.77675455340],
|
|
[0.00000126225, 1.08295459501, 20.77539549240],
|
|
[0.00000155516, 0.83306084617, 213.29909543800]
|
|
],
|
|
[
|
|
[6283.07584999140, 0.00000000000, 0.00000000000],
|
|
[0.00206058863, 2.67823455808, 6283.07584999140],
|
|
[0.00004303419, 2.63512233481, 12566.15169998280]
|
|
],
|
|
[
|
|
[0.00008721859, 1.07253635559, 6283.07584999140]
|
|
]
|
|
],
|
|
[
|
|
[],
|
|
[
|
|
[0.00227777722, 3.41376620530, 6283.07584999140],
|
|
[0.00003805678, 3.37063423795, 12566.15169998280]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[1.00013988784, 0.00000000000, 0.00000000000],
|
|
[0.01670699632, 3.09846350258, 6283.07584999140],
|
|
[0.00013956024, 3.05524609456, 12566.15169998280],
|
|
[0.00003083720, 5.19846674381, 77713.77146812050],
|
|
[0.00001628463, 1.17387558054, 5753.38488489680],
|
|
[0.00001575572, 2.84685214877, 7860.41939243920],
|
|
[0.00000924799, 5.45292236722, 11506.76976979360],
|
|
[0.00000542439, 4.56409151453, 3930.20969621960],
|
|
[0.00000472110, 3.66100022149, 5884.92684658320],
|
|
[0.00000085831, 1.27079125277, 161000.68573767410],
|
|
[0.00000057056, 2.01374292245, 83996.84731811189],
|
|
[0.00000055736, 5.24159799170, 71430.69561812909],
|
|
[0.00000174844, 3.01193636733, 18849.22754997420],
|
|
[0.00000243181, 4.27349530790, 11790.62908865880]
|
|
],
|
|
[
|
|
[0.00103018607, 1.10748968172, 6283.07584999140],
|
|
[0.00001721238, 1.06442300386, 12566.15169998280]
|
|
],
|
|
[
|
|
[0.00004359385, 5.78455133808, 6283.07584999140]
|
|
]
|
|
]
|
|
],
|
|
Mars: [
|
|
[
|
|
[
|
|
[6.20347711581, 0.00000000000, 0.00000000000],
|
|
[0.18656368093, 5.05037100270, 3340.61242669980],
|
|
[0.01108216816, 5.40099836344, 6681.22485339960],
|
|
[0.00091798406, 5.75478744667, 10021.83728009940],
|
|
[0.00027744987, 5.97049513147, 3.52311834900],
|
|
[0.00010610235, 2.93958560338, 2281.23049651060],
|
|
[0.00012315897, 0.84956094002, 2810.92146160520],
|
|
[0.00008926784, 4.15697846427, 0.01725365220],
|
|
[0.00008715691, 6.11005153139, 13362.44970679920],
|
|
[0.00006797556, 0.36462229657, 398.14900340820],
|
|
[0.00007774872, 3.33968761376, 5621.84292321040],
|
|
[0.00003575078, 1.66186505710, 2544.31441988340],
|
|
[0.00004161108, 0.22814971327, 2942.46342329160],
|
|
[0.00003075252, 0.85696614132, 191.44826611160],
|
|
[0.00002628117, 0.64806124465, 3337.08930835080],
|
|
[0.00002937546, 6.07893711402, 0.06731030280],
|
|
[0.00002389414, 5.03896442664, 796.29800681640],
|
|
[0.00002579844, 0.02996736156, 3344.13554504880],
|
|
[0.00001528141, 1.14979301996, 6151.53388830500],
|
|
[0.00001798806, 0.65634057445, 529.69096509460],
|
|
[0.00001264357, 3.62275122593, 5092.15195811580],
|
|
[0.00001286228, 3.06796065034, 2146.16541647520],
|
|
[0.00001546404, 2.91579701718, 1751.53953141600],
|
|
[0.00001024902, 3.69334099279, 8962.45534991020],
|
|
[0.00000891566, 0.18293837498, 16703.06213349900],
|
|
[0.00000858759, 2.40093811940, 2914.01423582380],
|
|
[0.00000832715, 2.46418619474, 3340.59517304760],
|
|
[0.00000832720, 4.49495782139, 3340.62968035200],
|
|
[0.00000712902, 3.66335473479, 1059.38193018920],
|
|
[0.00000748723, 3.82248614017, 155.42039943420],
|
|
[0.00000723861, 0.67497311481, 3738.76143010800],
|
|
[0.00000635548, 2.92182225127, 8432.76438481560],
|
|
[0.00000655162, 0.48864064125, 3127.31333126180],
|
|
[0.00000550474, 3.81001042328, 0.98032106820],
|
|
[0.00000552750, 4.47479317037, 1748.01641306700],
|
|
[0.00000425966, 0.55364317304, 6283.07584999140],
|
|
[0.00000415131, 0.49662285038, 213.29909543800],
|
|
[0.00000472167, 3.62547124025, 1194.44701022460],
|
|
[0.00000306551, 0.38052848348, 6684.74797174860],
|
|
[0.00000312141, 0.99853944405, 6677.70173505060],
|
|
[0.00000293198, 4.22131299634, 20.77539549240],
|
|
[0.00000302375, 4.48618007156, 3532.06069281140],
|
|
[0.00000274027, 0.54222167059, 3340.54511639700],
|
|
[0.00000281079, 5.88163521788, 1349.86740965880],
|
|
[0.00000231183, 1.28242156993, 3870.30339179440],
|
|
[0.00000283602, 5.76885434940, 3149.16416058820],
|
|
[0.00000236117, 5.75503217933, 3333.49887969900],
|
|
[0.00000274033, 0.13372524985, 3340.67973700260],
|
|
[0.00000299395, 2.78323740866, 6254.62666252360]
|
|
],
|
|
[
|
|
[3340.61242700512, 0.00000000000, 0.00000000000],
|
|
[0.01457554523, 3.60433733236, 3340.61242669980],
|
|
[0.00168414711, 3.92318567804, 6681.22485339960],
|
|
[0.00020622975, 4.26108844583, 10021.83728009940],
|
|
[0.00003452392, 4.73210393190, 3.52311834900],
|
|
[0.00002586332, 4.60670058555, 13362.44970679920],
|
|
[0.00000841535, 4.45864030426, 2281.23049651060]
|
|
],
|
|
[
|
|
[0.00058152577, 2.04961712429, 3340.61242669980],
|
|
[0.00013459579, 2.45738706163, 6681.22485339960]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.03197134986, 3.76832042431, 3340.61242669980],
|
|
[0.00298033234, 4.10616996305, 6681.22485339960],
|
|
[0.00289104742, 0.00000000000, 0.00000000000],
|
|
[0.00031365539, 4.44651053090, 10021.83728009940],
|
|
[0.00003484100, 4.78812549260, 13362.44970679920]
|
|
],
|
|
[
|
|
[0.00217310991, 6.04472194776, 3340.61242669980],
|
|
[0.00020976948, 3.14159265359, 0.00000000000],
|
|
[0.00012834709, 1.60810667915, 6681.22485339960]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[1.53033488271, 0.00000000000, 0.00000000000],
|
|
[0.14184953160, 3.47971283528, 3340.61242669980],
|
|
[0.00660776362, 3.81783443019, 6681.22485339960],
|
|
[0.00046179117, 4.15595316782, 10021.83728009940],
|
|
[0.00008109733, 5.55958416318, 2810.92146160520],
|
|
[0.00007485318, 1.77239078402, 5621.84292321040],
|
|
[0.00005523191, 1.36436303770, 2281.23049651060],
|
|
[0.00003825160, 4.49407183687, 13362.44970679920],
|
|
[0.00002306537, 0.09081579001, 2544.31441988340],
|
|
[0.00001999396, 5.36059617709, 3337.08930835080],
|
|
[0.00002484394, 4.92545639920, 2942.46342329160],
|
|
[0.00001960195, 4.74249437639, 3344.13554504880],
|
|
[0.00001167119, 2.11260868341, 5092.15195811580],
|
|
[0.00001102816, 5.00908403998, 398.14900340820],
|
|
[0.00000899066, 4.40791133207, 529.69096509460],
|
|
[0.00000992252, 5.83861961952, 6151.53388830500],
|
|
[0.00000807354, 2.10217065501, 1059.38193018920],
|
|
[0.00000797915, 3.44839203899, 796.29800681640],
|
|
[0.00000740975, 1.49906336885, 2146.16541647520]
|
|
],
|
|
[
|
|
[0.01107433345, 2.03250524857, 3340.61242669980],
|
|
[0.00103175887, 2.37071847807, 6681.22485339960],
|
|
[0.00012877200, 0.00000000000, 0.00000000000],
|
|
[0.00010815880, 2.70888095665, 10021.83728009940]
|
|
],
|
|
[
|
|
[0.00044242249, 0.47930604954, 3340.61242669980],
|
|
[0.00008138042, 0.86998389204, 6681.22485339960]
|
|
]
|
|
]
|
|
],
|
|
Jupiter: [
|
|
[
|
|
[
|
|
[0.59954691494, 0.00000000000, 0.00000000000],
|
|
[0.09695898719, 5.06191793158, 529.69096509460],
|
|
[0.00573610142, 1.44406205629, 7.11354700080],
|
|
[0.00306389205, 5.41734730184, 1059.38193018920],
|
|
[0.00097178296, 4.14264726552, 632.78373931320],
|
|
[0.00072903078, 3.64042916389, 522.57741809380],
|
|
[0.00064263975, 3.41145165351, 103.09277421860],
|
|
[0.00039806064, 2.29376740788, 419.48464387520],
|
|
[0.00038857767, 1.27231755835, 316.39186965660],
|
|
[0.00027964629, 1.78454591820, 536.80451209540],
|
|
[0.00013589730, 5.77481040790, 1589.07289528380],
|
|
[0.00008246349, 3.58227925840, 206.18554843720],
|
|
[0.00008768704, 3.63000308199, 949.17560896980],
|
|
[0.00007368042, 5.08101194270, 735.87651353180],
|
|
[0.00006263150, 0.02497628807, 213.29909543800],
|
|
[0.00006114062, 4.51319998626, 1162.47470440780],
|
|
[0.00004905396, 1.32084470588, 110.20632121940],
|
|
[0.00005305285, 1.30671216791, 14.22709400160],
|
|
[0.00005305441, 4.18625634012, 1052.26838318840],
|
|
[0.00004647248, 4.69958103684, 3.93215326310],
|
|
[0.00003045023, 4.31676431084, 426.59819087600],
|
|
[0.00002609999, 1.56667394063, 846.08283475120],
|
|
[0.00002028191, 1.06376530715, 3.18139373770],
|
|
[0.00001764763, 2.14148655117, 1066.49547719000],
|
|
[0.00001722972, 3.88036268267, 1265.56747862640],
|
|
[0.00001920945, 0.97168196472, 639.89728631400],
|
|
[0.00001633223, 3.58201833555, 515.46387109300],
|
|
[0.00001431999, 4.29685556046, 625.67019231240],
|
|
[0.00000973272, 4.09764549134, 95.97922721780]
|
|
],
|
|
[
|
|
[529.69096508814, 0.00000000000, 0.00000000000],
|
|
[0.00489503243, 4.22082939470, 529.69096509460],
|
|
[0.00228917222, 6.02646855621, 7.11354700080],
|
|
[0.00030099479, 4.54540782858, 1059.38193018920],
|
|
[0.00020720920, 5.45943156902, 522.57741809380],
|
|
[0.00012103653, 0.16994816098, 536.80451209540],
|
|
[0.00006067987, 4.42422292017, 103.09277421860],
|
|
[0.00005433968, 3.98480737746, 419.48464387520],
|
|
[0.00004237744, 5.89008707199, 14.22709400160]
|
|
],
|
|
[
|
|
[0.00047233601, 4.32148536482, 7.11354700080],
|
|
[0.00030649436, 2.92977788700, 529.69096509460],
|
|
[0.00014837605, 3.14159265359, 0.00000000000]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.02268615702, 3.55852606721, 529.69096509460],
|
|
[0.00109971634, 3.90809347197, 1059.38193018920],
|
|
[0.00110090358, 0.00000000000, 0.00000000000],
|
|
[0.00008101428, 3.60509572885, 522.57741809380],
|
|
[0.00006043996, 4.25883108339, 1589.07289528380],
|
|
[0.00006437782, 0.30627119215, 536.80451209540]
|
|
],
|
|
[
|
|
[0.00078203446, 1.52377859742, 529.69096509460]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[5.20887429326, 0.00000000000, 0.00000000000],
|
|
[0.25209327119, 3.49108639871, 529.69096509460],
|
|
[0.00610599976, 3.84115365948, 1059.38193018920],
|
|
[0.00282029458, 2.57419881293, 632.78373931320],
|
|
[0.00187647346, 2.07590383214, 522.57741809380],
|
|
[0.00086792905, 0.71001145545, 419.48464387520],
|
|
[0.00072062974, 0.21465724607, 536.80451209540],
|
|
[0.00065517248, 5.97995884790, 316.39186965660],
|
|
[0.00029134542, 1.67759379655, 103.09277421860],
|
|
[0.00030135335, 2.16132003734, 949.17560896980],
|
|
[0.00023453271, 3.54023522184, 735.87651353180],
|
|
[0.00022283743, 4.19362594399, 1589.07289528380],
|
|
[0.00023947298, 0.27458037480, 7.11354700080],
|
|
[0.00013032614, 2.96042965363, 1162.47470440780],
|
|
[0.00009703360, 1.90669633585, 206.18554843720],
|
|
[0.00012749023, 2.71550286592, 1052.26838318840],
|
|
[0.00007057931, 2.18184839926, 1265.56747862640],
|
|
[0.00006137703, 6.26418240033, 846.08283475120],
|
|
[0.00002616976, 2.00994012876, 1581.95934828300]
|
|
],
|
|
[
|
|
[0.01271801520, 2.64937512894, 529.69096509460],
|
|
[0.00061661816, 3.00076460387, 1059.38193018920],
|
|
[0.00053443713, 3.89717383175, 522.57741809380],
|
|
[0.00031185171, 4.88276958012, 536.80451209540],
|
|
[0.00041390269, 0.00000000000, 0.00000000000]
|
|
]
|
|
]
|
|
],
|
|
Saturn: [
|
|
[
|
|
[
|
|
[0.87401354025, 0.00000000000, 0.00000000000],
|
|
[0.11107659762, 3.96205090159, 213.29909543800],
|
|
[0.01414150957, 4.58581516874, 7.11354700080],
|
|
[0.00398379389, 0.52112032699, 206.18554843720],
|
|
[0.00350769243, 3.30329907896, 426.59819087600],
|
|
[0.00206816305, 0.24658372002, 103.09277421860],
|
|
[0.00079271300, 3.84007056878, 220.41264243880],
|
|
[0.00023990355, 4.66976924553, 110.20632121940],
|
|
[0.00016573588, 0.43719228296, 419.48464387520],
|
|
[0.00014906995, 5.76903183869, 316.39186965660],
|
|
[0.00015820290, 0.93809155235, 632.78373931320],
|
|
[0.00014609559, 1.56518472000, 3.93215326310],
|
|
[0.00013160301, 4.44891291899, 14.22709400160],
|
|
[0.00015053543, 2.71669915667, 639.89728631400],
|
|
[0.00013005299, 5.98119023644, 11.04570026390],
|
|
[0.00010725067, 3.12939523827, 202.25339517410],
|
|
[0.00005863206, 0.23656938524, 529.69096509460],
|
|
[0.00005227757, 4.20783365759, 3.18139373770],
|
|
[0.00006126317, 1.76328667907, 277.03499374140],
|
|
[0.00005019687, 3.17787728405, 433.71173787680],
|
|
[0.00004592550, 0.61977744975, 199.07200143640],
|
|
[0.00004005867, 2.24479718502, 63.73589830340],
|
|
[0.00002953796, 0.98280366998, 95.97922721780],
|
|
[0.00003873670, 3.22283226966, 138.51749687070],
|
|
[0.00002461186, 2.03163875071, 735.87651353180],
|
|
[0.00003269484, 0.77492638211, 949.17560896980],
|
|
[0.00001758145, 3.26580109940, 522.57741809380],
|
|
[0.00001640172, 5.50504453050, 846.08283475120],
|
|
[0.00001391327, 4.02333150505, 323.50541665740],
|
|
[0.00001580648, 4.37265307169, 309.27832265580],
|
|
[0.00001123498, 2.83726798446, 415.55249061210],
|
|
[0.00001017275, 3.71700135395, 227.52618943960],
|
|
[0.00000848642, 3.19150170830, 209.36694217490]
|
|
],
|
|
[
|
|
[213.29909521690, 0.00000000000, 0.00000000000],
|
|
[0.01297370862, 1.82834923978, 213.29909543800],
|
|
[0.00564345393, 2.88499717272, 7.11354700080],
|
|
[0.00093734369, 1.06311793502, 426.59819087600],
|
|
[0.00107674962, 2.27769131009, 206.18554843720],
|
|
[0.00040244455, 2.04108104671, 220.41264243880],
|
|
[0.00019941774, 1.27954390470, 103.09277421860],
|
|
[0.00010511678, 2.74880342130, 14.22709400160],
|
|
[0.00006416106, 0.38238295041, 639.89728631400],
|
|
[0.00004848994, 2.43037610229, 419.48464387520],
|
|
[0.00004056892, 2.92133209468, 110.20632121940],
|
|
[0.00003768635, 3.64965330780, 3.93215326310]
|
|
],
|
|
[
|
|
[0.00116441330, 1.17988132879, 7.11354700080],
|
|
[0.00091841837, 0.07325195840, 213.29909543800],
|
|
[0.00036661728, 0.00000000000, 0.00000000000],
|
|
[0.00015274496, 4.06493179167, 206.18554843720]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.04330678039, 3.60284428399, 213.29909543800],
|
|
[0.00240348302, 2.85238489373, 426.59819087600],
|
|
[0.00084745939, 0.00000000000, 0.00000000000],
|
|
[0.00030863357, 3.48441504555, 220.41264243880],
|
|
[0.00034116062, 0.57297307557, 206.18554843720],
|
|
[0.00014734070, 2.11846596715, 639.89728631400],
|
|
[0.00009916667, 5.79003188904, 419.48464387520],
|
|
[0.00006993564, 4.73604689720, 7.11354700080],
|
|
[0.00004807588, 5.43305312061, 316.39186965660]
|
|
],
|
|
[
|
|
[0.00198927992, 4.93901017903, 213.29909543800],
|
|
[0.00036947916, 3.14159265359, 0.00000000000],
|
|
[0.00017966989, 0.51979431110, 426.59819087600]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[9.55758135486, 0.00000000000, 0.00000000000],
|
|
[0.52921382865, 2.39226219573, 213.29909543800],
|
|
[0.01873679867, 5.23549604660, 206.18554843720],
|
|
[0.01464663929, 1.64763042902, 426.59819087600],
|
|
[0.00821891141, 5.93520042303, 316.39186965660],
|
|
[0.00547506923, 5.01532618980, 103.09277421860],
|
|
[0.00371684650, 2.27114821115, 220.41264243880],
|
|
[0.00361778765, 3.13904301847, 7.11354700080],
|
|
[0.00140617506, 5.70406606781, 632.78373931320],
|
|
[0.00108974848, 3.29313390175, 110.20632121940],
|
|
[0.00069006962, 5.94099540992, 419.48464387520],
|
|
[0.00061053367, 0.94037691801, 639.89728631400],
|
|
[0.00048913294, 1.55733638681, 202.25339517410],
|
|
[0.00034143772, 0.19519102597, 277.03499374140],
|
|
[0.00032401773, 5.47084567016, 949.17560896980],
|
|
[0.00020936596, 0.46349251129, 735.87651353180],
|
|
[0.00009796004, 5.20477537945, 1265.56747862640],
|
|
[0.00011993338, 5.98050967385, 846.08283475120],
|
|
[0.00020839300, 1.52102476129, 433.71173787680],
|
|
[0.00015298404, 3.05943814940, 529.69096509460],
|
|
[0.00006465823, 0.17732249942, 1052.26838318840],
|
|
[0.00011380257, 1.73105427040, 522.57741809380],
|
|
[0.00003419618, 4.94550542171, 1581.95934828300]
|
|
],
|
|
[
|
|
[0.06182981340, 0.25843511480, 213.29909543800],
|
|
[0.00506577242, 0.71114625261, 206.18554843720],
|
|
[0.00341394029, 5.79635741658, 426.59819087600],
|
|
[0.00188491195, 0.47215589652, 220.41264243880],
|
|
[0.00186261486, 3.14159265359, 0.00000000000],
|
|
[0.00143891146, 1.40744822888, 7.11354700080]
|
|
],
|
|
[
|
|
[0.00436902572, 4.78671677509, 213.29909543800]
|
|
]
|
|
]
|
|
],
|
|
Uranus: [
|
|
[
|
|
[
|
|
[5.48129294297, 0.00000000000, 0.00000000000],
|
|
[0.09260408234, 0.89106421507, 74.78159856730],
|
|
[0.01504247898, 3.62719260920, 1.48447270830],
|
|
[0.00365981674, 1.89962179044, 73.29712585900],
|
|
[0.00272328168, 3.35823706307, 149.56319713460],
|
|
[0.00070328461, 5.39254450063, 63.73589830340],
|
|
[0.00068892678, 6.09292483287, 76.26607127560],
|
|
[0.00061998615, 2.26952066061, 2.96894541660],
|
|
[0.00061950719, 2.85098872691, 11.04570026390],
|
|
[0.00026468770, 3.14152083966, 71.81265315070],
|
|
[0.00025710476, 6.11379840493, 454.90936652730],
|
|
[0.00021078850, 4.36059339067, 148.07872442630],
|
|
[0.00017818647, 1.74436930289, 36.64856292950],
|
|
[0.00014613507, 4.73732166022, 3.93215326310],
|
|
[0.00011162509, 5.82681796350, 224.34479570190],
|
|
[0.00010997910, 0.48865004018, 138.51749687070],
|
|
[0.00009527478, 2.95516862826, 35.16409022120],
|
|
[0.00007545601, 5.23626582400, 109.94568878850],
|
|
[0.00004220241, 3.23328220918, 70.84944530420],
|
|
[0.00004051900, 2.27755017300, 151.04766984290],
|
|
[0.00003354596, 1.06549007380, 4.45341812490],
|
|
[0.00002926718, 4.62903718891, 9.56122755560],
|
|
[0.00003490340, 5.48306144511, 146.59425171800],
|
|
[0.00003144069, 4.75199570434, 77.75054398390],
|
|
[0.00002922333, 5.35235361027, 85.82729883120],
|
|
[0.00002272788, 4.36600400036, 70.32818044240],
|
|
[0.00002051219, 1.51773566586, 0.11187458460],
|
|
[0.00002148602, 0.60745949945, 38.13303563780],
|
|
[0.00001991643, 4.92437588682, 277.03499374140],
|
|
[0.00001376226, 2.04283539351, 65.22037101170],
|
|
[0.00001666902, 3.62744066769, 380.12776796000],
|
|
[0.00001284107, 3.11347961505, 202.25339517410],
|
|
[0.00001150429, 0.93343589092, 3.18139373770],
|
|
[0.00001533221, 2.58594681212, 52.69019803950],
|
|
[0.00001281604, 0.54271272721, 222.86032299360],
|
|
[0.00001372139, 4.19641530878, 111.43016149680],
|
|
[0.00001221029, 0.19900650030, 108.46121608020],
|
|
[0.00000946181, 1.19253165736, 127.47179660680],
|
|
[0.00001150989, 4.17898916639, 33.67961751290]
|
|
],
|
|
[
|
|
[74.78159860910, 0.00000000000, 0.00000000000],
|
|
[0.00154332863, 5.24158770553, 74.78159856730],
|
|
[0.00024456474, 1.71260334156, 1.48447270830],
|
|
[0.00009258442, 0.42829732350, 11.04570026390],
|
|
[0.00008265977, 1.50218091379, 63.73589830340],
|
|
[0.00009150160, 1.41213765216, 149.56319713460]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.01346277648, 2.61877810547, 74.78159856730],
|
|
[0.00062341400, 5.08111189648, 149.56319713460],
|
|
[0.00061601196, 3.14159265359, 0.00000000000],
|
|
[0.00009963722, 1.61603805646, 76.26607127560],
|
|
[0.00009926160, 0.57630380333, 73.29712585900]
|
|
],
|
|
[
|
|
[0.00034101978, 0.01321929936, 74.78159856730]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[19.21264847206, 0.00000000000, 0.00000000000],
|
|
[0.88784984413, 5.60377527014, 74.78159856730],
|
|
[0.03440836062, 0.32836099706, 73.29712585900],
|
|
[0.02055653860, 1.78295159330, 149.56319713460],
|
|
[0.00649322410, 4.52247285911, 76.26607127560],
|
|
[0.00602247865, 3.86003823674, 63.73589830340],
|
|
[0.00496404167, 1.40139935333, 454.90936652730],
|
|
[0.00338525369, 1.58002770318, 138.51749687070],
|
|
[0.00243509114, 1.57086606044, 71.81265315070],
|
|
[0.00190522303, 1.99809394714, 1.48447270830],
|
|
[0.00161858838, 2.79137786799, 148.07872442630],
|
|
[0.00143706183, 1.38368544947, 11.04570026390],
|
|
[0.00093192405, 0.17437220467, 36.64856292950],
|
|
[0.00071424548, 4.24509236074, 224.34479570190],
|
|
[0.00089806014, 3.66105364565, 109.94568878850],
|
|
[0.00039009723, 1.66971401684, 70.84944530420],
|
|
[0.00046677296, 1.39976401694, 35.16409022120],
|
|
[0.00039025624, 3.36234773834, 277.03499374140],
|
|
[0.00036755274, 3.88649278513, 146.59425171800],
|
|
[0.00030348723, 0.70100838798, 151.04766984290],
|
|
[0.00029156413, 3.18056336700, 77.75054398390],
|
|
[0.00022637073, 0.72518687029, 529.69096509460],
|
|
[0.00011959076, 1.75043392140, 984.60033162190],
|
|
[0.00025620756, 5.25656086672, 380.12776796000]
|
|
],
|
|
[
|
|
[0.01479896629, 3.67205697578, 74.78159856730]
|
|
]
|
|
]
|
|
],
|
|
Neptune: [
|
|
[
|
|
[
|
|
[5.31188633046, 0.00000000000, 0.00000000000],
|
|
[0.01798475530, 2.90101273890, 38.13303563780],
|
|
[0.01019727652, 0.48580922867, 1.48447270830],
|
|
[0.00124531845, 4.83008090676, 36.64856292950],
|
|
[0.00042064466, 5.41054993053, 2.96894541660],
|
|
[0.00037714584, 6.09221808686, 35.16409022120],
|
|
[0.00033784738, 1.24488874087, 76.26607127560],
|
|
[0.00016482741, 0.00007727998, 491.55792945680],
|
|
[0.00009198584, 4.93747051954, 39.61750834610],
|
|
[0.00008994250, 0.27462171806, 175.16605980020]
|
|
],
|
|
[
|
|
[38.13303563957, 0.00000000000, 0.00000000000],
|
|
[0.00016604172, 4.86323329249, 1.48447270830],
|
|
[0.00015744045, 2.27887427527, 38.13303563780]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[0.03088622933, 1.44104372644, 38.13303563780],
|
|
[0.00027780087, 5.91271884599, 76.26607127560],
|
|
[0.00027623609, 0.00000000000, 0.00000000000],
|
|
[0.00015355489, 2.52123799551, 36.64856292950],
|
|
[0.00015448133, 3.50877079215, 39.61750834610]
|
|
]
|
|
],
|
|
[
|
|
[
|
|
[30.07013205828, 0.00000000000, 0.00000000000],
|
|
[0.27062259632, 1.32999459377, 38.13303563780],
|
|
[0.01691764014, 3.25186135653, 36.64856292950],
|
|
[0.00807830553, 5.18592878704, 1.48447270830],
|
|
[0.00537760510, 4.52113935896, 35.16409022120],
|
|
[0.00495725141, 1.57105641650, 491.55792945680],
|
|
[0.00274571975, 1.84552258866, 175.16605980020],
|
|
[0.00012012320, 1.92059384991, 1021.24889455140],
|
|
[0.00121801746, 5.79754470298, 76.26607127560],
|
|
[0.00100896068, 0.37702724930, 73.29712585900],
|
|
[0.00135134092, 3.37220609835, 39.61750834610],
|
|
[0.00007571796, 1.07149207335, 388.46515523820]
|
|
]
|
|
]
|
|
]
|
|
};
|
|
function DeltaT_EspenakMeeus(ut) {
|
|
var u, u2, u3, u4, u5, u6, u7;
|
|
/*
|
|
Fred Espenak writes about Delta-T generically here:
|
|
https://eclipse.gsfc.nasa.gov/SEhelp/deltaT.html
|
|
https://eclipse.gsfc.nasa.gov/SEhelp/deltat2004.html
|
|
|
|
He provides polynomial approximations for distant years here:
|
|
https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html
|
|
|
|
They start with a year value 'y' such that y=2000 corresponds
|
|
to the UTC Date 15-January-2000. Convert difference in days
|
|
to mean tropical years.
|
|
*/
|
|
const y = 2000 + ((ut - 14) / DAYS_PER_TROPICAL_YEAR);
|
|
if (y < -500) {
|
|
u = (y - 1820) / 100;
|
|
return -20 + (32 * u * u);
|
|
}
|
|
if (y < 500) {
|
|
u = y / 100;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
u5 = u2 * u3;
|
|
u6 = u3 * u3;
|
|
return 10583.6 - 1014.41 * u + 33.78311 * u2 - 5.952053 * u3 - 0.1798452 * u4 + 0.022174192 * u5 + 0.0090316521 * u6;
|
|
}
|
|
if (y < 1600) {
|
|
u = (y - 1000) / 100;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
u5 = u2 * u3;
|
|
u6 = u3 * u3;
|
|
return 1574.2 - 556.01 * u + 71.23472 * u2 + 0.319781 * u3 - 0.8503463 * u4 - 0.005050998 * u5 + 0.0083572073 * u6;
|
|
}
|
|
if (y < 1700) {
|
|
u = y - 1600;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
return 120 - 0.9808 * u - 0.01532 * u2 + u3 / 7129.0;
|
|
}
|
|
if (y < 1800) {
|
|
u = y - 1700;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
return 8.83 + 0.1603 * u - 0.0059285 * u2 + 0.00013336 * u3 - u4 / 1174000;
|
|
}
|
|
if (y < 1860) {
|
|
u = y - 1800;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
u5 = u2 * u3;
|
|
u6 = u3 * u3;
|
|
u7 = u3 * u4;
|
|
return 13.72 - 0.332447 * u + 0.0068612 * u2 + 0.0041116 * u3 - 0.00037436 * u4 + 0.0000121272 * u5 - 0.0000001699 * u6 + 0.000000000875 * u7;
|
|
}
|
|
if (y < 1900) {
|
|
u = y - 1860;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
u5 = u2 * u3;
|
|
return 7.62 + 0.5737 * u - 0.251754 * u2 + 0.01680668 * u3 - 0.0004473624 * u4 + u5 / 233174;
|
|
}
|
|
if (y < 1920) {
|
|
u = y - 1900;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
return -2.79 + 1.494119 * u - 0.0598939 * u2 + 0.0061966 * u3 - 0.000197 * u4;
|
|
}
|
|
if (y < 1941) {
|
|
u = y - 1920;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
return 21.20 + 0.84493 * u - 0.076100 * u2 + 0.0020936 * u3;
|
|
}
|
|
if (y < 1961) {
|
|
u = y - 1950;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
return 29.07 + 0.407 * u - u2 / 233 + u3 / 2547;
|
|
}
|
|
if (y < 1986) {
|
|
u = y - 1975;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
return 45.45 + 1.067 * u - u2 / 260 - u3 / 718;
|
|
}
|
|
if (y < 2005) {
|
|
u = y - 2000;
|
|
u2 = u * u;
|
|
u3 = u * u2;
|
|
u4 = u2 * u2;
|
|
u5 = u2 * u3;
|
|
return 63.86 + 0.3345 * u - 0.060374 * u2 + 0.0017275 * u3 + 0.000651814 * u4 + 0.00002373599 * u5;
|
|
}
|
|
if (y < 2050) {
|
|
u = y - 2000;
|
|
return 62.92 + 0.32217 * u + 0.005589 * u * u;
|
|
}
|
|
if (y < 2150) {
|
|
u = (y - 1820) / 100;
|
|
return -20 + 32 * u * u - 0.5628 * (2150 - y);
|
|
}
|
|
/* all years after 2150 */
|
|
u = (y - 1820) / 100;
|
|
return -20 + (32 * u * u);
|
|
}
|
|
exports.DeltaT_EspenakMeeus = DeltaT_EspenakMeeus;
|
|
function DeltaT_JplHorizons(ut) {
|
|
return DeltaT_EspenakMeeus(Math.min(ut, 17.0 * DAYS_PER_TROPICAL_YEAR));
|
|
}
|
|
exports.DeltaT_JplHorizons = DeltaT_JplHorizons;
|
|
let DeltaT = DeltaT_EspenakMeeus;
|
|
function SetDeltaTFunction(func) {
|
|
DeltaT = func;
|
|
}
|
|
exports.SetDeltaTFunction = SetDeltaTFunction;
|
|
/**
|
|
* @ignore
|
|
*
|
|
* @brief Calculates Terrestrial Time (TT) from Universal Time (UT).
|
|
*
|
|
* @param {number} ut
|
|
* The Universal Time expressed as a floating point number of days since the 2000.0 epoch.
|
|
*
|
|
* @returns {number}
|
|
* A Terrestrial Time expressed as a floating point number of days since the 2000.0 epoch.
|
|
*/
|
|
function TerrestrialTime(ut) {
|
|
return ut + DeltaT(ut) / 86400;
|
|
}
|
|
/**
|
|
* @brief The date and time of an astronomical observation.
|
|
*
|
|
* Objects of type `AstroTime` are used throughout the internals
|
|
* of the Astronomy library, and are included in certain return objects.
|
|
* Use the constructor or the {@link MakeTime} function to create an `AstroTime` object.
|
|
*
|
|
* @property {Date} date
|
|
* The JavaScript Date object for the given date and time.
|
|
* This Date corresponds to the numeric day value stored in the `ut` property.
|
|
*
|
|
* @property {number} ut
|
|
* Universal Time (UT1/UTC) in fractional days since the J2000 epoch.
|
|
* Universal Time represents time measured with respect to the Earth's rotation,
|
|
* tracking mean solar days.
|
|
* The Astronomy library approximates UT1 and UTC as being the same thing.
|
|
* This gives sufficient accuracy for the precision requirements of this project.
|
|
*
|
|
* @property {number} tt
|
|
* Terrestrial Time in fractional days since the J2000 epoch.
|
|
* TT represents a continuously flowing ephemeris timescale independent of
|
|
* any variations of the Earth's rotation, and is adjusted from UT
|
|
* using a best-fit piecewise polynomial model devised by
|
|
* [Espenak and Meeus](https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html).
|
|
*/
|
|
class AstroTime {
|
|
/**
|
|
* @param {FlexibleDateTime} date
|
|
* A JavaScript Date object, a numeric UTC value expressed in J2000 days, or another AstroTime object.
|
|
*/
|
|
constructor(date) {
|
|
if (date instanceof AstroTime) {
|
|
// Construct a clone of the AstroTime passed in.
|
|
this.date = date.date;
|
|
this.ut = date.ut;
|
|
this.tt = date.tt;
|
|
return;
|
|
}
|
|
const MillisPerDay = 1000 * 3600 * 24;
|
|
if ((date instanceof Date) && Number.isFinite(date.getTime())) {
|
|
this.date = date;
|
|
this.ut = (date.getTime() - J2000.getTime()) / MillisPerDay;
|
|
this.tt = TerrestrialTime(this.ut);
|
|
return;
|
|
}
|
|
if (Number.isFinite(date)) {
|
|
this.date = new Date(J2000.getTime() + date * MillisPerDay);
|
|
this.ut = date;
|
|
this.tt = TerrestrialTime(this.ut);
|
|
return;
|
|
}
|
|
throw 'Argument must be a Date object, an AstroTime object, or a numeric UTC Julian date.';
|
|
}
|
|
/**
|
|
* @brief Creates an `AstroTime` value from a Terrestrial Time (TT) day value.
|
|
*
|
|
* This function can be used in rare cases where a time must be based
|
|
* on Terrestrial Time (TT) rather than Universal Time (UT).
|
|
* Most developers will want to invoke `new AstroTime(ut)` with a universal time
|
|
* instead of this function, because usually time is based on civil time adjusted
|
|
* by leap seconds to match the Earth's rotation, rather than the uniformly
|
|
* flowing TT used to calculate solar system dynamics. In rare cases
|
|
* where the caller already knows TT, this function is provided to create
|
|
* an `AstroTime` value that can be passed to Astronomy Engine functions.
|
|
*
|
|
* @param {number} tt
|
|
* The number of days since the J2000 epoch as expressed in Terrestrial Time.
|
|
*
|
|
* @returns {AstroTime}
|
|
* An `AstroTime` object for the specified terrestrial time.
|
|
*/
|
|
static FromTerrestrialTime(tt) {
|
|
let time = new AstroTime(tt);
|
|
for (;;) {
|
|
const err = tt - time.tt;
|
|
if (Math.abs(err) < 1.0e-12)
|
|
return time;
|
|
time = time.AddDays(err);
|
|
}
|
|
}
|
|
/**
|
|
* Formats an `AstroTime` object as an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601)
|
|
* date/time string in UTC, to millisecond resolution.
|
|
* Example: `2018-08-17T17:22:04.050Z`
|
|
* @returns {string}
|
|
*/
|
|
toString() {
|
|
return this.date.toISOString();
|
|
}
|
|
/**
|
|
* Returns a new `AstroTime` object adjusted by the floating point number of days.
|
|
* Does NOT modify the original `AstroTime` object.
|
|
*
|
|
* @param {number} days
|
|
* The floating point number of days by which to adjust the given date and time.
|
|
* Positive values adjust the date toward the future, and
|
|
* negative values adjust the date toward the past.
|
|
*
|
|
* @returns {AstroTime}
|
|
*/
|
|
AddDays(days) {
|
|
// This is slightly wrong, but the error is tiny.
|
|
// We really should be adding to TT, not to UT.
|
|
// But using TT would require creating an inverse function for DeltaT,
|
|
// which would be quite a bit of extra calculation.
|
|
// I estimate the error is in practice on the order of 10^(-7)
|
|
// times the value of 'days'.
|
|
// This is based on a typical drift of 1 second per year between UT and TT.
|
|
return new AstroTime(this.ut + days);
|
|
}
|
|
}
|
|
exports.AstroTime = AstroTime;
|
|
function InterpolateTime(time1, time2, fraction) {
|
|
return new AstroTime(time1.ut + fraction * (time2.ut - time1.ut));
|
|
}
|
|
/**
|
|
* @brief A `Date`, `number`, or `AstroTime` value that specifies the date and time of an astronomical event.
|
|
*
|
|
* `FlexibleDateTime` is a placeholder type that represents three different types
|
|
* that may be passed to many Astronomy Engine functions: a JavaScript `Date` object,
|
|
* a number representing the real-valued number of UT days since the J2000 epoch,
|
|
* or an {@link AstroTime} object.
|
|
*
|
|
* This flexibility is for convenience of outside callers.
|
|
* Internally, Astronomy Engine always converts a `FlexibleTime` parameter
|
|
* to an `AstroTime` object by calling {@link MakeTime}.
|
|
*
|
|
* @typedef {Date | number | AstroTime} FlexibleDateTime
|
|
*/
|
|
/**
|
|
* @brief Converts multiple date/time formats to `AstroTime` format.
|
|
*
|
|
* Given a Date object or a number days since noon (12:00) on January 1, 2000 (UTC),
|
|
* this function creates an {@link AstroTime} object.
|
|
*
|
|
* Given an {@link AstroTime} object, returns the same object unmodified.
|
|
* Use of this function is not required for any of the other exposed functions in this library,
|
|
* because they all guarantee converting date/time parameters to `AstroTime`
|
|
* as needed. However, it may be convenient for callers who need to understand
|
|
* the difference between UTC and TT (Terrestrial Time). In some use cases,
|
|
* converting once to `AstroTime` format and passing the result into multiple
|
|
* function calls may be more efficient than passing in native JavaScript Date objects.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* A Date object, a number of UTC days since the J2000 epoch (noon on January 1, 2000),
|
|
* or an AstroTime object. See remarks above.
|
|
*
|
|
* @returns {AstroTime}
|
|
*/
|
|
function MakeTime(date) {
|
|
if (date instanceof AstroTime) {
|
|
return date;
|
|
}
|
|
return new AstroTime(date);
|
|
}
|
|
exports.MakeTime = MakeTime;
|
|
const iaudata = [
|
|
[[0, 0, 0, 0, 1], [-172064161, -174666, 33386, 92052331, 9086, 15377]],
|
|
[[0, 0, 2, -2, 2], [-13170906, -1675, -13696, 5730336, -3015, -4587]],
|
|
[[0, 0, 2, 0, 2], [-2276413, -234, 2796, 978459, -485, 1374]],
|
|
[[0, 0, 0, 0, 2], [2074554, 207, -698, -897492, 470, -291]],
|
|
[[0, 1, 0, 0, 0], [1475877, -3633, 11817, 73871, -184, -1924]],
|
|
[[0, 1, 2, -2, 2], [-516821, 1226, -524, 224386, -677, -174]],
|
|
[[1, 0, 0, 0, 0], [711159, 73, -872, -6750, 0, 358]],
|
|
[[0, 0, 2, 0, 1], [-387298, -367, 380, 200728, 18, 318]],
|
|
[[1, 0, 2, 0, 2], [-301461, -36, 816, 129025, -63, 367]],
|
|
[[0, -1, 2, -2, 2], [215829, -494, 111, -95929, 299, 132]],
|
|
[[0, 0, 2, -2, 1], [128227, 137, 181, -68982, -9, 39]],
|
|
[[-1, 0, 2, 0, 2], [123457, 11, 19, -53311, 32, -4]],
|
|
[[-1, 0, 0, 2, 0], [156994, 10, -168, -1235, 0, 82]],
|
|
[[1, 0, 0, 0, 1], [63110, 63, 27, -33228, 0, -9]],
|
|
[[-1, 0, 0, 0, 1], [-57976, -63, -189, 31429, 0, -75]],
|
|
[[-1, 0, 2, 2, 2], [-59641, -11, 149, 25543, -11, 66]],
|
|
[[1, 0, 2, 0, 1], [-51613, -42, 129, 26366, 0, 78]],
|
|
[[-2, 0, 2, 0, 1], [45893, 50, 31, -24236, -10, 20]],
|
|
[[0, 0, 0, 2, 0], [63384, 11, -150, -1220, 0, 29]],
|
|
[[0, 0, 2, 2, 2], [-38571, -1, 158, 16452, -11, 68]],
|
|
[[0, -2, 2, -2, 2], [32481, 0, 0, -13870, 0, 0]],
|
|
[[-2, 0, 0, 2, 0], [-47722, 0, -18, 477, 0, -25]],
|
|
[[2, 0, 2, 0, 2], [-31046, -1, 131, 13238, -11, 59]],
|
|
[[1, 0, 2, -2, 2], [28593, 0, -1, -12338, 10, -3]],
|
|
[[-1, 0, 2, 0, 1], [20441, 21, 10, -10758, 0, -3]],
|
|
[[2, 0, 0, 0, 0], [29243, 0, -74, -609, 0, 13]],
|
|
[[0, 0, 2, 0, 0], [25887, 0, -66, -550, 0, 11]],
|
|
[[0, 1, 0, 0, 1], [-14053, -25, 79, 8551, -2, -45]],
|
|
[[-1, 0, 0, 2, 1], [15164, 10, 11, -8001, 0, -1]],
|
|
[[0, 2, 2, -2, 2], [-15794, 72, -16, 6850, -42, -5]],
|
|
[[0, 0, -2, 2, 0], [21783, 0, 13, -167, 0, 13]],
|
|
[[1, 0, 0, -2, 1], [-12873, -10, -37, 6953, 0, -14]],
|
|
[[0, -1, 0, 0, 1], [-12654, 11, 63, 6415, 0, 26]],
|
|
[[-1, 0, 2, 2, 1], [-10204, 0, 25, 5222, 0, 15]],
|
|
[[0, 2, 0, 0, 0], [16707, -85, -10, 168, -1, 10]],
|
|
[[1, 0, 2, 2, 2], [-7691, 0, 44, 3268, 0, 19]],
|
|
[[-2, 0, 2, 0, 0], [-11024, 0, -14, 104, 0, 2]],
|
|
[[0, 1, 2, 0, 2], [7566, -21, -11, -3250, 0, -5]],
|
|
[[0, 0, 2, 2, 1], [-6637, -11, 25, 3353, 0, 14]],
|
|
[[0, -1, 2, 0, 2], [-7141, 21, 8, 3070, 0, 4]],
|
|
[[0, 0, 0, 2, 1], [-6302, -11, 2, 3272, 0, 4]],
|
|
[[1, 0, 2, -2, 1], [5800, 10, 2, -3045, 0, -1]],
|
|
[[2, 0, 2, -2, 2], [6443, 0, -7, -2768, 0, -4]],
|
|
[[-2, 0, 0, 2, 1], [-5774, -11, -15, 3041, 0, -5]],
|
|
[[2, 0, 2, 0, 1], [-5350, 0, 21, 2695, 0, 12]],
|
|
[[0, -1, 2, -2, 1], [-4752, -11, -3, 2719, 0, -3]],
|
|
[[0, 0, 0, -2, 1], [-4940, -11, -21, 2720, 0, -9]],
|
|
[[-1, -1, 0, 2, 0], [7350, 0, -8, -51, 0, 4]],
|
|
[[2, 0, 0, -2, 1], [4065, 0, 6, -2206, 0, 1]],
|
|
[[1, 0, 0, 2, 0], [6579, 0, -24, -199, 0, 2]],
|
|
[[0, 1, 2, -2, 1], [3579, 0, 5, -1900, 0, 1]],
|
|
[[1, -1, 0, 0, 0], [4725, 0, -6, -41, 0, 3]],
|
|
[[-2, 0, 2, 0, 2], [-3075, 0, -2, 1313, 0, -1]],
|
|
[[3, 0, 2, 0, 2], [-2904, 0, 15, 1233, 0, 7]],
|
|
[[0, -1, 0, 2, 0], [4348, 0, -10, -81, 0, 2]],
|
|
[[1, -1, 2, 0, 2], [-2878, 0, 8, 1232, 0, 4]],
|
|
[[0, 0, 0, 1, 0], [-4230, 0, 5, -20, 0, -2]],
|
|
[[-1, -1, 2, 2, 2], [-2819, 0, 7, 1207, 0, 3]],
|
|
[[-1, 0, 2, 0, 0], [-4056, 0, 5, 40, 0, -2]],
|
|
[[0, -1, 2, 2, 2], [-2647, 0, 11, 1129, 0, 5]],
|
|
[[-2, 0, 0, 0, 1], [-2294, 0, -10, 1266, 0, -4]],
|
|
[[1, 1, 2, 0, 2], [2481, 0, -7, -1062, 0, -3]],
|
|
[[2, 0, 0, 0, 1], [2179, 0, -2, -1129, 0, -2]],
|
|
[[-1, 1, 0, 1, 0], [3276, 0, 1, -9, 0, 0]],
|
|
[[1, 1, 0, 0, 0], [-3389, 0, 5, 35, 0, -2]],
|
|
[[1, 0, 2, 0, 0], [3339, 0, -13, -107, 0, 1]],
|
|
[[-1, 0, 2, -2, 1], [-1987, 0, -6, 1073, 0, -2]],
|
|
[[1, 0, 0, 0, 2], [-1981, 0, 0, 854, 0, 0]],
|
|
[[-1, 0, 0, 1, 0], [4026, 0, -353, -553, 0, -139]],
|
|
[[0, 0, 2, 1, 2], [1660, 0, -5, -710, 0, -2]],
|
|
[[-1, 0, 2, 4, 2], [-1521, 0, 9, 647, 0, 4]],
|
|
[[-1, 1, 0, 1, 1], [1314, 0, 0, -700, 0, 0]],
|
|
[[0, -2, 2, -2, 1], [-1283, 0, 0, 672, 0, 0]],
|
|
[[1, 0, 2, 2, 1], [-1331, 0, 8, 663, 0, 4]],
|
|
[[-2, 0, 2, 2, 2], [1383, 0, -2, -594, 0, -2]],
|
|
[[-1, 0, 0, 0, 2], [1405, 0, 4, -610, 0, 2]],
|
|
[[1, 1, 2, -2, 2], [1290, 0, 0, -556, 0, 0]]
|
|
];
|
|
function iau2000b(time) {
|
|
var i, t, el, elp, f, d, om, arg, dp, de, sarg, carg;
|
|
var nals, cls;
|
|
function mod(x) {
|
|
return (x % ASEC360) * ASEC2RAD;
|
|
}
|
|
t = time.tt / 36525;
|
|
el = mod(485868.249036 + t * 1717915923.2178);
|
|
elp = mod(1287104.79305 + t * 129596581.0481);
|
|
f = mod(335779.526232 + t * 1739527262.8478);
|
|
d = mod(1072260.70369 + t * 1602961601.2090);
|
|
om = mod(450160.398036 - t * 6962890.5431);
|
|
dp = 0;
|
|
de = 0;
|
|
for (i = 76; i >= 0; --i) {
|
|
nals = iaudata[i][0];
|
|
cls = iaudata[i][1];
|
|
arg = (nals[0] * el + nals[1] * elp + nals[2] * f + nals[3] * d + nals[4] * om) % PI2;
|
|
sarg = Math.sin(arg);
|
|
carg = Math.cos(arg);
|
|
dp += (cls[0] + cls[1] * t) * sarg + cls[2] * carg;
|
|
de += (cls[3] + cls[4] * t) * carg + cls[5] * sarg;
|
|
}
|
|
return {
|
|
dpsi: -0.000135 + (dp * 1.0e-7),
|
|
deps: +0.000388 + (de * 1.0e-7)
|
|
};
|
|
}
|
|
function mean_obliq(time) {
|
|
var t = time.tt / 36525;
|
|
var asec = (((((-0.0000000434 * t
|
|
- 0.000000576) * t
|
|
+ 0.00200340) * t
|
|
- 0.0001831) * t
|
|
- 46.836769) * t + 84381.406);
|
|
return asec / 3600.0;
|
|
}
|
|
var cache_e_tilt;
|
|
function e_tilt(time) {
|
|
if (!cache_e_tilt || Math.abs(cache_e_tilt.tt - time.tt) > 1.0e-6) {
|
|
const nut = iau2000b(time);
|
|
const mean_ob = mean_obliq(time);
|
|
const true_ob = mean_ob + (nut.deps / 3600);
|
|
cache_e_tilt = {
|
|
tt: time.tt,
|
|
dpsi: nut.dpsi,
|
|
deps: nut.deps,
|
|
ee: nut.dpsi * Math.cos(mean_ob * exports.DEG2RAD) / 15,
|
|
mobl: mean_ob,
|
|
tobl: true_ob
|
|
};
|
|
}
|
|
return cache_e_tilt;
|
|
}
|
|
function ecl2equ_vec(time, pos) {
|
|
var obl = mean_obliq(time) * exports.DEG2RAD;
|
|
var cos_obl = Math.cos(obl);
|
|
var sin_obl = Math.sin(obl);
|
|
return [
|
|
pos[0],
|
|
pos[1] * cos_obl - pos[2] * sin_obl,
|
|
pos[1] * sin_obl + pos[2] * cos_obl
|
|
];
|
|
}
|
|
exports.CalcMoonCount = 0;
|
|
function CalcMoon(time) {
|
|
++exports.CalcMoonCount;
|
|
const T = time.tt / 36525;
|
|
function DeclareArray1(xmin, xmax) {
|
|
const array = [];
|
|
let i;
|
|
for (i = 0; i <= xmax - xmin; ++i) {
|
|
array.push(0);
|
|
}
|
|
return { min: xmin, array: array };
|
|
}
|
|
function DeclareArray2(xmin, xmax, ymin, ymax) {
|
|
const array = [];
|
|
for (let i = 0; i <= xmax - xmin; ++i) {
|
|
array.push(DeclareArray1(ymin, ymax));
|
|
}
|
|
return { min: xmin, array: array };
|
|
}
|
|
function ArrayGet2(a, x, y) {
|
|
const m = a.array[x - a.min];
|
|
return m.array[y - m.min];
|
|
}
|
|
function ArraySet2(a, x, y, v) {
|
|
const m = a.array[x - a.min];
|
|
m.array[y - m.min] = v;
|
|
}
|
|
let S, MAX, ARG, FAC, I, J, T2, DGAM, DLAM, N, GAM1C, SINPI, L0, L, LS, F, D, DL0, DL, DLS, DF, DD, DS;
|
|
let coArray = DeclareArray2(-6, 6, 1, 4);
|
|
let siArray = DeclareArray2(-6, 6, 1, 4);
|
|
function CO(x, y) {
|
|
return ArrayGet2(coArray, x, y);
|
|
}
|
|
function SI(x, y) {
|
|
return ArrayGet2(siArray, x, y);
|
|
}
|
|
function SetCO(x, y, v) {
|
|
return ArraySet2(coArray, x, y, v);
|
|
}
|
|
function SetSI(x, y, v) {
|
|
return ArraySet2(siArray, x, y, v);
|
|
}
|
|
function AddThe(c1, s1, c2, s2, func) {
|
|
func(c1 * c2 - s1 * s2, s1 * c2 + c1 * s2);
|
|
}
|
|
function Sine(phi) {
|
|
return Math.sin(PI2 * phi);
|
|
}
|
|
T2 = T * T;
|
|
DLAM = 0;
|
|
DS = 0;
|
|
GAM1C = 0;
|
|
SINPI = 3422.7000;
|
|
var S1 = Sine(0.19833 + 0.05611 * T);
|
|
var S2 = Sine(0.27869 + 0.04508 * T);
|
|
var S3 = Sine(0.16827 - 0.36903 * T);
|
|
var S4 = Sine(0.34734 - 5.37261 * T);
|
|
var S5 = Sine(0.10498 - 5.37899 * T);
|
|
var S6 = Sine(0.42681 - 0.41855 * T);
|
|
var S7 = Sine(0.14943 - 5.37511 * T);
|
|
DL0 = 0.84 * S1 + 0.31 * S2 + 14.27 * S3 + 7.26 * S4 + 0.28 * S5 + 0.24 * S6;
|
|
DL = 2.94 * S1 + 0.31 * S2 + 14.27 * S3 + 9.34 * S4 + 1.12 * S5 + 0.83 * S6;
|
|
DLS = -6.40 * S1 - 1.89 * S6;
|
|
DF = 0.21 * S1 + 0.31 * S2 + 14.27 * S3 - 88.70 * S4 - 15.30 * S5 + 0.24 * S6 - 1.86 * S7;
|
|
DD = DL0 - DLS;
|
|
DGAM = (-3332E-9 * Sine(0.59734 - 5.37261 * T)
|
|
- 539E-9 * Sine(0.35498 - 5.37899 * T)
|
|
- 64E-9 * Sine(0.39943 - 5.37511 * T));
|
|
L0 = PI2 * Frac(0.60643382 + 1336.85522467 * T - 0.00000313 * T2) + DL0 / ARC;
|
|
L = PI2 * Frac(0.37489701 + 1325.55240982 * T + 0.00002565 * T2) + DL / ARC;
|
|
LS = PI2 * Frac(0.99312619 + 99.99735956 * T - 0.00000044 * T2) + DLS / ARC;
|
|
F = PI2 * Frac(0.25909118 + 1342.22782980 * T - 0.00000892 * T2) + DF / ARC;
|
|
D = PI2 * Frac(0.82736186 + 1236.85308708 * T - 0.00000397 * T2) + DD / ARC;
|
|
for (I = 1; I <= 4; ++I) {
|
|
switch (I) {
|
|
case 1:
|
|
ARG = L;
|
|
MAX = 4;
|
|
FAC = 1.000002208;
|
|
break;
|
|
case 2:
|
|
ARG = LS;
|
|
MAX = 3;
|
|
FAC = 0.997504612 - 0.002495388 * T;
|
|
break;
|
|
case 3:
|
|
ARG = F;
|
|
MAX = 4;
|
|
FAC = 1.000002708 + 139.978 * DGAM;
|
|
break;
|
|
case 4:
|
|
ARG = D;
|
|
MAX = 6;
|
|
FAC = 1.0;
|
|
break;
|
|
default: throw `Internal error: I = ${I}`; // persuade TypeScript that ARG, ... are all initialized before use.
|
|
}
|
|
SetCO(0, I, 1);
|
|
SetCO(1, I, Math.cos(ARG) * FAC);
|
|
SetSI(0, I, 0);
|
|
SetSI(1, I, Math.sin(ARG) * FAC);
|
|
for (J = 2; J <= MAX; ++J) {
|
|
AddThe(CO(J - 1, I), SI(J - 1, I), CO(1, I), SI(1, I), (c, s) => (SetCO(J, I, c), SetSI(J, I, s)));
|
|
}
|
|
for (J = 1; J <= MAX; ++J) {
|
|
SetCO(-J, I, CO(J, I));
|
|
SetSI(-J, I, -SI(J, I));
|
|
}
|
|
}
|
|
function Term(p, q, r, s) {
|
|
var result = { x: 1, y: 0 };
|
|
var I = [0, p, q, r, s]; // I[0] is not used; it is a placeholder
|
|
for (var k = 1; k <= 4; ++k)
|
|
if (I[k] !== 0)
|
|
AddThe(result.x, result.y, CO(I[k], k), SI(I[k], k), (c, s) => (result.x = c, result.y = s));
|
|
return result;
|
|
}
|
|
function AddSol(coeffl, coeffs, coeffg, coeffp, p, q, r, s) {
|
|
var result = Term(p, q, r, s);
|
|
DLAM += coeffl * result.y;
|
|
DS += coeffs * result.y;
|
|
GAM1C += coeffg * result.x;
|
|
SINPI += coeffp * result.x;
|
|
}
|
|
AddSol(13.9020, 14.0600, -0.0010, 0.2607, 0, 0, 0, 4);
|
|
AddSol(0.4030, -4.0100, 0.3940, 0.0023, 0, 0, 0, 3);
|
|
AddSol(2369.9120, 2373.3600, 0.6010, 28.2333, 0, 0, 0, 2);
|
|
AddSol(-125.1540, -112.7900, -0.7250, -0.9781, 0, 0, 0, 1);
|
|
AddSol(1.9790, 6.9800, -0.4450, 0.0433, 1, 0, 0, 4);
|
|
AddSol(191.9530, 192.7200, 0.0290, 3.0861, 1, 0, 0, 2);
|
|
AddSol(-8.4660, -13.5100, 0.4550, -0.1093, 1, 0, 0, 1);
|
|
AddSol(22639.5000, 22609.0700, 0.0790, 186.5398, 1, 0, 0, 0);
|
|
AddSol(18.6090, 3.5900, -0.0940, 0.0118, 1, 0, 0, -1);
|
|
AddSol(-4586.4650, -4578.1300, -0.0770, 34.3117, 1, 0, 0, -2);
|
|
AddSol(3.2150, 5.4400, 0.1920, -0.0386, 1, 0, 0, -3);
|
|
AddSol(-38.4280, -38.6400, 0.0010, 0.6008, 1, 0, 0, -4);
|
|
AddSol(-0.3930, -1.4300, -0.0920, 0.0086, 1, 0, 0, -6);
|
|
AddSol(-0.2890, -1.5900, 0.1230, -0.0053, 0, 1, 0, 4);
|
|
AddSol(-24.4200, -25.1000, 0.0400, -0.3000, 0, 1, 0, 2);
|
|
AddSol(18.0230, 17.9300, 0.0070, 0.1494, 0, 1, 0, 1);
|
|
AddSol(-668.1460, -126.9800, -1.3020, -0.3997, 0, 1, 0, 0);
|
|
AddSol(0.5600, 0.3200, -0.0010, -0.0037, 0, 1, 0, -1);
|
|
AddSol(-165.1450, -165.0600, 0.0540, 1.9178, 0, 1, 0, -2);
|
|
AddSol(-1.8770, -6.4600, -0.4160, 0.0339, 0, 1, 0, -4);
|
|
AddSol(0.2130, 1.0200, -0.0740, 0.0054, 2, 0, 0, 4);
|
|
AddSol(14.3870, 14.7800, -0.0170, 0.2833, 2, 0, 0, 2);
|
|
AddSol(-0.5860, -1.2000, 0.0540, -0.0100, 2, 0, 0, 1);
|
|
AddSol(769.0160, 767.9600, 0.1070, 10.1657, 2, 0, 0, 0);
|
|
AddSol(1.7500, 2.0100, -0.0180, 0.0155, 2, 0, 0, -1);
|
|
AddSol(-211.6560, -152.5300, 5.6790, -0.3039, 2, 0, 0, -2);
|
|
AddSol(1.2250, 0.9100, -0.0300, -0.0088, 2, 0, 0, -3);
|
|
AddSol(-30.7730, -34.0700, -0.3080, 0.3722, 2, 0, 0, -4);
|
|
AddSol(-0.5700, -1.4000, -0.0740, 0.0109, 2, 0, 0, -6);
|
|
AddSol(-2.9210, -11.7500, 0.7870, -0.0484, 1, 1, 0, 2);
|
|
AddSol(1.2670, 1.5200, -0.0220, 0.0164, 1, 1, 0, 1);
|
|
AddSol(-109.6730, -115.1800, 0.4610, -0.9490, 1, 1, 0, 0);
|
|
AddSol(-205.9620, -182.3600, 2.0560, 1.4437, 1, 1, 0, -2);
|
|
AddSol(0.2330, 0.3600, 0.0120, -0.0025, 1, 1, 0, -3);
|
|
AddSol(-4.3910, -9.6600, -0.4710, 0.0673, 1, 1, 0, -4);
|
|
AddSol(0.2830, 1.5300, -0.1110, 0.0060, 1, -1, 0, 4);
|
|
AddSol(14.5770, 31.7000, -1.5400, 0.2302, 1, -1, 0, 2);
|
|
AddSol(147.6870, 138.7600, 0.6790, 1.1528, 1, -1, 0, 0);
|
|
AddSol(-1.0890, 0.5500, 0.0210, 0.0000, 1, -1, 0, -1);
|
|
AddSol(28.4750, 23.5900, -0.4430, -0.2257, 1, -1, 0, -2);
|
|
AddSol(-0.2760, -0.3800, -0.0060, -0.0036, 1, -1, 0, -3);
|
|
AddSol(0.6360, 2.2700, 0.1460, -0.0102, 1, -1, 0, -4);
|
|
AddSol(-0.1890, -1.6800, 0.1310, -0.0028, 0, 2, 0, 2);
|
|
AddSol(-7.4860, -0.6600, -0.0370, -0.0086, 0, 2, 0, 0);
|
|
AddSol(-8.0960, -16.3500, -0.7400, 0.0918, 0, 2, 0, -2);
|
|
AddSol(-5.7410, -0.0400, 0.0000, -0.0009, 0, 0, 2, 2);
|
|
AddSol(0.2550, 0.0000, 0.0000, 0.0000, 0, 0, 2, 1);
|
|
AddSol(-411.6080, -0.2000, 0.0000, -0.0124, 0, 0, 2, 0);
|
|
AddSol(0.5840, 0.8400, 0.0000, 0.0071, 0, 0, 2, -1);
|
|
AddSol(-55.1730, -52.1400, 0.0000, -0.1052, 0, 0, 2, -2);
|
|
AddSol(0.2540, 0.2500, 0.0000, -0.0017, 0, 0, 2, -3);
|
|
AddSol(0.0250, -1.6700, 0.0000, 0.0031, 0, 0, 2, -4);
|
|
AddSol(1.0600, 2.9600, -0.1660, 0.0243, 3, 0, 0, 2);
|
|
AddSol(36.1240, 50.6400, -1.3000, 0.6215, 3, 0, 0, 0);
|
|
AddSol(-13.1930, -16.4000, 0.2580, -0.1187, 3, 0, 0, -2);
|
|
AddSol(-1.1870, -0.7400, 0.0420, 0.0074, 3, 0, 0, -4);
|
|
AddSol(-0.2930, -0.3100, -0.0020, 0.0046, 3, 0, 0, -6);
|
|
AddSol(-0.2900, -1.4500, 0.1160, -0.0051, 2, 1, 0, 2);
|
|
AddSol(-7.6490, -10.5600, 0.2590, -0.1038, 2, 1, 0, 0);
|
|
AddSol(-8.6270, -7.5900, 0.0780, -0.0192, 2, 1, 0, -2);
|
|
AddSol(-2.7400, -2.5400, 0.0220, 0.0324, 2, 1, 0, -4);
|
|
AddSol(1.1810, 3.3200, -0.2120, 0.0213, 2, -1, 0, 2);
|
|
AddSol(9.7030, 11.6700, -0.1510, 0.1268, 2, -1, 0, 0);
|
|
AddSol(-0.3520, -0.3700, 0.0010, -0.0028, 2, -1, 0, -1);
|
|
AddSol(-2.4940, -1.1700, -0.0030, -0.0017, 2, -1, 0, -2);
|
|
AddSol(0.3600, 0.2000, -0.0120, -0.0043, 2, -1, 0, -4);
|
|
AddSol(-1.1670, -1.2500, 0.0080, -0.0106, 1, 2, 0, 0);
|
|
AddSol(-7.4120, -6.1200, 0.1170, 0.0484, 1, 2, 0, -2);
|
|
AddSol(-0.3110, -0.6500, -0.0320, 0.0044, 1, 2, 0, -4);
|
|
AddSol(0.7570, 1.8200, -0.1050, 0.0112, 1, -2, 0, 2);
|
|
AddSol(2.5800, 2.3200, 0.0270, 0.0196, 1, -2, 0, 0);
|
|
AddSol(2.5330, 2.4000, -0.0140, -0.0212, 1, -2, 0, -2);
|
|
AddSol(-0.3440, -0.5700, -0.0250, 0.0036, 0, 3, 0, -2);
|
|
AddSol(-0.9920, -0.0200, 0.0000, 0.0000, 1, 0, 2, 2);
|
|
AddSol(-45.0990, -0.0200, 0.0000, -0.0010, 1, 0, 2, 0);
|
|
AddSol(-0.1790, -9.5200, 0.0000, -0.0833, 1, 0, 2, -2);
|
|
AddSol(-0.3010, -0.3300, 0.0000, 0.0014, 1, 0, 2, -4);
|
|
AddSol(-6.3820, -3.3700, 0.0000, -0.0481, 1, 0, -2, 2);
|
|
AddSol(39.5280, 85.1300, 0.0000, -0.7136, 1, 0, -2, 0);
|
|
AddSol(9.3660, 0.7100, 0.0000, -0.0112, 1, 0, -2, -2);
|
|
AddSol(0.2020, 0.0200, 0.0000, 0.0000, 1, 0, -2, -4);
|
|
AddSol(0.4150, 0.1000, 0.0000, 0.0013, 0, 1, 2, 0);
|
|
AddSol(-2.1520, -2.2600, 0.0000, -0.0066, 0, 1, 2, -2);
|
|
AddSol(-1.4400, -1.3000, 0.0000, 0.0014, 0, 1, -2, 2);
|
|
AddSol(0.3840, -0.0400, 0.0000, 0.0000, 0, 1, -2, -2);
|
|
AddSol(1.9380, 3.6000, -0.1450, 0.0401, 4, 0, 0, 0);
|
|
AddSol(-0.9520, -1.5800, 0.0520, -0.0130, 4, 0, 0, -2);
|
|
AddSol(-0.5510, -0.9400, 0.0320, -0.0097, 3, 1, 0, 0);
|
|
AddSol(-0.4820, -0.5700, 0.0050, -0.0045, 3, 1, 0, -2);
|
|
AddSol(0.6810, 0.9600, -0.0260, 0.0115, 3, -1, 0, 0);
|
|
AddSol(-0.2970, -0.2700, 0.0020, -0.0009, 2, 2, 0, -2);
|
|
AddSol(0.2540, 0.2100, -0.0030, 0.0000, 2, -2, 0, -2);
|
|
AddSol(-0.2500, -0.2200, 0.0040, 0.0014, 1, 3, 0, -2);
|
|
AddSol(-3.9960, 0.0000, 0.0000, 0.0004, 2, 0, 2, 0);
|
|
AddSol(0.5570, -0.7500, 0.0000, -0.0090, 2, 0, 2, -2);
|
|
AddSol(-0.4590, -0.3800, 0.0000, -0.0053, 2, 0, -2, 2);
|
|
AddSol(-1.2980, 0.7400, 0.0000, 0.0004, 2, 0, -2, 0);
|
|
AddSol(0.5380, 1.1400, 0.0000, -0.0141, 2, 0, -2, -2);
|
|
AddSol(0.2630, 0.0200, 0.0000, 0.0000, 1, 1, 2, 0);
|
|
AddSol(0.4260, 0.0700, 0.0000, -0.0006, 1, 1, -2, -2);
|
|
AddSol(-0.3040, 0.0300, 0.0000, 0.0003, 1, -1, 2, 0);
|
|
AddSol(-0.3720, -0.1900, 0.0000, -0.0027, 1, -1, -2, 2);
|
|
AddSol(0.4180, 0.0000, 0.0000, 0.0000, 0, 0, 4, 0);
|
|
AddSol(-0.3300, -0.0400, 0.0000, 0.0000, 3, 0, 2, 0);
|
|
function ADDN(coeffn, p, q, r, s) {
|
|
return coeffn * Term(p, q, r, s).y;
|
|
}
|
|
N = 0;
|
|
N += ADDN(-526.069, 0, 0, 1, -2);
|
|
N += ADDN(-3.352, 0, 0, 1, -4);
|
|
N += ADDN(+44.297, +1, 0, 1, -2);
|
|
N += ADDN(-6.000, +1, 0, 1, -4);
|
|
N += ADDN(+20.599, -1, 0, 1, 0);
|
|
N += ADDN(-30.598, -1, 0, 1, -2);
|
|
N += ADDN(-24.649, -2, 0, 1, 0);
|
|
N += ADDN(-2.000, -2, 0, 1, -2);
|
|
N += ADDN(-22.571, 0, +1, 1, -2);
|
|
N += ADDN(+10.985, 0, -1, 1, -2);
|
|
DLAM += (+0.82 * Sine(0.7736 - 62.5512 * T) + 0.31 * Sine(0.0466 - 125.1025 * T)
|
|
+ 0.35 * Sine(0.5785 - 25.1042 * T) + 0.66 * Sine(0.4591 + 1335.8075 * T)
|
|
+ 0.64 * Sine(0.3130 - 91.5680 * T) + 1.14 * Sine(0.1480 + 1331.2898 * T)
|
|
+ 0.21 * Sine(0.5918 + 1056.5859 * T) + 0.44 * Sine(0.5784 + 1322.8595 * T)
|
|
+ 0.24 * Sine(0.2275 - 5.7374 * T) + 0.28 * Sine(0.2965 + 2.6929 * T)
|
|
+ 0.33 * Sine(0.3132 + 6.3368 * T));
|
|
S = F + DS / ARC;
|
|
let lat_seconds = (1.000002708 + 139.978 * DGAM) * (18518.511 + 1.189 + GAM1C) * Math.sin(S) - 6.24 * Math.sin(3 * S) + N;
|
|
return {
|
|
geo_eclip_lon: PI2 * Frac((L0 + DLAM / ARC) / PI2),
|
|
geo_eclip_lat: (Math.PI / (180 * 3600)) * lat_seconds,
|
|
distance_au: (ARC * EARTH_EQUATORIAL_RADIUS_AU) / (0.999953253 * SINPI)
|
|
};
|
|
}
|
|
/**
|
|
* @brief Lunar libration angles, returned by {@link Libration}.
|
|
*
|
|
* @property {number} elat
|
|
* Sub-Earth libration ecliptic latitude angle, in degrees.
|
|
* @property {number} elon
|
|
* Sub-Earth libration ecliptic longitude angle, in degrees.
|
|
* @property {number} mlat
|
|
* Moon's geocentric ecliptic latitude.
|
|
* @property {number} mlon
|
|
* Moon's geocentric ecliptic longitude.
|
|
* @property {number} dist_km
|
|
* Distance between the centers of the Earth and Moon in kilometers.
|
|
* @property {number} diam_deg
|
|
* The apparent angular diameter of the Moon, in degrees, as seen from the center of the Earth.
|
|
*/
|
|
class LibrationInfo {
|
|
constructor(elat, elon, mlat, mlon, dist_km, diam_deg) {
|
|
this.elat = elat;
|
|
this.elon = elon;
|
|
this.mlat = mlat;
|
|
this.mlon = mlon;
|
|
this.dist_km = dist_km;
|
|
this.diam_deg = diam_deg;
|
|
}
|
|
}
|
|
exports.LibrationInfo = LibrationInfo;
|
|
/**
|
|
* @brief Calculates the Moon's libration angles at a given moment in time.
|
|
*
|
|
* Libration is an observed back-and-forth wobble of the portion of the
|
|
* Moon visible from the Earth. It is caused by the imperfect tidal locking
|
|
* of the Moon's fixed rotation rate, compared to its variable angular speed
|
|
* of orbit around the Earth.
|
|
*
|
|
* This function calculates a pair of perpendicular libration angles,
|
|
* one representing rotation of the Moon in eclitpic longitude `elon`, the other
|
|
* in ecliptic latitude `elat`, both relative to the Moon's mean Earth-facing position.
|
|
*
|
|
* This function also returns the geocentric position of the Moon
|
|
* expressed in ecliptic longitude `mlon`, ecliptic latitude `mlat`, the
|
|
* distance `dist_km` between the centers of the Earth and Moon expressed in kilometers,
|
|
* and the apparent angular diameter of the Moon `diam_deg`.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* A Date object, a number of UTC days since the J2000 epoch (noon on January 1, 2000),
|
|
* or an AstroTime object.
|
|
*
|
|
* @returns {LibrationInfo}
|
|
*/
|
|
function Libration(date) {
|
|
const time = MakeTime(date);
|
|
const t = time.tt / 36525.0;
|
|
const t2 = t * t;
|
|
const t3 = t2 * t;
|
|
const t4 = t2 * t2;
|
|
const moon = CalcMoon(time);
|
|
const mlon = moon.geo_eclip_lon;
|
|
const mlat = moon.geo_eclip_lat;
|
|
const dist_km = moon.distance_au * exports.KM_PER_AU;
|
|
// Inclination angle
|
|
const I = exports.DEG2RAD * 1.54242;
|
|
// Moon's argument of latitude in radians.
|
|
const f = exports.DEG2RAD * NormalizeLongitude(93.2720950 + 483202.0175233 * t - 0.0036539 * t2 - t3 / 3526000 + t4 / 863310000);
|
|
// Moon's ascending node's mean longitude in radians.
|
|
const omega = exports.DEG2RAD * NormalizeLongitude(125.0445479 - 1934.1362891 * t + 0.0020754 * t2 + t3 / 467441 - t4 / 60616000);
|
|
// Sun's mean anomaly.
|
|
const m = exports.DEG2RAD * NormalizeLongitude(357.5291092 + 35999.0502909 * t - 0.0001536 * t2 + t3 / 24490000);
|
|
// Moon's mean anomaly.
|
|
const mdash = exports.DEG2RAD * NormalizeLongitude(134.9633964 + 477198.8675055 * t + 0.0087414 * t2 + t3 / 69699 - t4 / 14712000);
|
|
// Moon's mean elongation.
|
|
const d = exports.DEG2RAD * NormalizeLongitude(297.8501921 + 445267.1114034 * t - 0.0018819 * t2 + t3 / 545868 - t4 / 113065000);
|
|
// Eccentricity of the Earth's orbit.
|
|
const e = 1.0 - 0.002516 * t - 0.0000074 * t2;
|
|
// Optical librations
|
|
const w = mlon - omega;
|
|
const a = Math.atan2(Math.sin(w) * Math.cos(mlat) * Math.cos(I) - Math.sin(mlat) * Math.sin(I), Math.cos(w) * Math.cos(mlat));
|
|
const ldash = LongitudeOffset(exports.RAD2DEG * (a - f));
|
|
const bdash = Math.asin(-Math.sin(w) * Math.cos(mlat) * Math.sin(I) - Math.sin(mlat) * Math.cos(I));
|
|
// Physical librations
|
|
const k1 = exports.DEG2RAD * (119.75 + 131.849 * t);
|
|
const k2 = exports.DEG2RAD * (72.56 + 20.186 * t);
|
|
const rho = (-0.02752 * Math.cos(mdash) +
|
|
-0.02245 * Math.sin(f) +
|
|
+0.00684 * Math.cos(mdash - 2 * f) +
|
|
-0.00293 * Math.cos(2 * f) +
|
|
-0.00085 * Math.cos(2 * f - 2 * d) +
|
|
-0.00054 * Math.cos(mdash - 2 * d) +
|
|
-0.00020 * Math.sin(mdash + f) +
|
|
-0.00020 * Math.cos(mdash + 2 * f) +
|
|
-0.00020 * Math.cos(mdash - f) +
|
|
+0.00014 * Math.cos(mdash + 2 * f - 2 * d));
|
|
const sigma = (-0.02816 * Math.sin(mdash) +
|
|
+0.02244 * Math.cos(f) +
|
|
-0.00682 * Math.sin(mdash - 2 * f) +
|
|
-0.00279 * Math.sin(2 * f) +
|
|
-0.00083 * Math.sin(2 * f - 2 * d) +
|
|
+0.00069 * Math.sin(mdash - 2 * d) +
|
|
+0.00040 * Math.cos(mdash + f) +
|
|
-0.00025 * Math.sin(2 * mdash) +
|
|
-0.00023 * Math.sin(mdash + 2 * f) +
|
|
+0.00020 * Math.cos(mdash - f) +
|
|
+0.00019 * Math.sin(mdash - f) +
|
|
+0.00013 * Math.sin(mdash + 2 * f - 2 * d) +
|
|
-0.00010 * Math.cos(mdash - 3 * f));
|
|
const tau = (+0.02520 * e * Math.sin(m) +
|
|
+0.00473 * Math.sin(2 * mdash - 2 * f) +
|
|
-0.00467 * Math.sin(mdash) +
|
|
+0.00396 * Math.sin(k1) +
|
|
+0.00276 * Math.sin(2 * mdash - 2 * d) +
|
|
+0.00196 * Math.sin(omega) +
|
|
-0.00183 * Math.cos(mdash - f) +
|
|
+0.00115 * Math.sin(mdash - 2 * d) +
|
|
-0.00096 * Math.sin(mdash - d) +
|
|
+0.00046 * Math.sin(2 * f - 2 * d) +
|
|
-0.00039 * Math.sin(mdash - f) +
|
|
-0.00032 * Math.sin(mdash - m - d) +
|
|
+0.00027 * Math.sin(2 * mdash - m - 2 * d) +
|
|
+0.00023 * Math.sin(k2) +
|
|
-0.00014 * Math.sin(2 * d) +
|
|
+0.00014 * Math.cos(2 * mdash - 2 * f) +
|
|
-0.00012 * Math.sin(mdash - 2 * f) +
|
|
-0.00012 * Math.sin(2 * mdash) +
|
|
+0.00011 * Math.sin(2 * mdash - 2 * m - 2 * d));
|
|
const ldash2 = -tau + (rho * Math.cos(a) + sigma * Math.sin(a)) * Math.tan(bdash);
|
|
const bdash2 = sigma * Math.cos(a) - rho * Math.sin(a);
|
|
const diam_deg = 2.0 * exports.RAD2DEG * Math.atan(MOON_MEAN_RADIUS_KM / Math.sqrt(dist_km * dist_km - MOON_MEAN_RADIUS_KM * MOON_MEAN_RADIUS_KM));
|
|
return new LibrationInfo(exports.RAD2DEG * bdash + bdash2, ldash + ldash2, mlat, mlon, dist_km, diam_deg);
|
|
}
|
|
exports.Libration = Libration;
|
|
function rotate(rot, vec) {
|
|
return [
|
|
rot.rot[0][0] * vec[0] + rot.rot[1][0] * vec[1] + rot.rot[2][0] * vec[2],
|
|
rot.rot[0][1] * vec[0] + rot.rot[1][1] * vec[1] + rot.rot[2][1] * vec[2],
|
|
rot.rot[0][2] * vec[0] + rot.rot[1][2] * vec[1] + rot.rot[2][2] * vec[2]
|
|
];
|
|
}
|
|
function precession(pos, time, dir) {
|
|
const r = precession_rot(time, dir);
|
|
return rotate(r, pos);
|
|
}
|
|
function precession_posvel(state, time, dir) {
|
|
const r = precession_rot(time, dir);
|
|
return RotateState(r, state);
|
|
}
|
|
function precession_rot(time, dir) {
|
|
const t = time.tt / 36525;
|
|
let eps0 = 84381.406;
|
|
let psia = (((((-0.0000000951 * t
|
|
+ 0.000132851) * t
|
|
- 0.00114045) * t
|
|
- 1.0790069) * t
|
|
+ 5038.481507) * t);
|
|
let omegaa = (((((+0.0000003337 * t
|
|
- 0.000000467) * t
|
|
- 0.00772503) * t
|
|
+ 0.0512623) * t
|
|
- 0.025754) * t + eps0);
|
|
let chia = (((((-0.0000000560 * t
|
|
+ 0.000170663) * t
|
|
- 0.00121197) * t
|
|
- 2.3814292) * t
|
|
+ 10.556403) * t);
|
|
eps0 *= ASEC2RAD;
|
|
psia *= ASEC2RAD;
|
|
omegaa *= ASEC2RAD;
|
|
chia *= ASEC2RAD;
|
|
const sa = Math.sin(eps0);
|
|
const ca = Math.cos(eps0);
|
|
const sb = Math.sin(-psia);
|
|
const cb = Math.cos(-psia);
|
|
const sc = Math.sin(-omegaa);
|
|
const cc = Math.cos(-omegaa);
|
|
const sd = Math.sin(chia);
|
|
const cd = Math.cos(chia);
|
|
const xx = cd * cb - sb * sd * cc;
|
|
const yx = cd * sb * ca + sd * cc * cb * ca - sa * sd * sc;
|
|
const zx = cd * sb * sa + sd * cc * cb * sa + ca * sd * sc;
|
|
const xy = -sd * cb - sb * cd * cc;
|
|
const yy = -sd * sb * ca + cd * cc * cb * ca - sa * cd * sc;
|
|
const zy = -sd * sb * sa + cd * cc * cb * sa + ca * cd * sc;
|
|
const xz = sb * sc;
|
|
const yz = -sc * cb * ca - sa * cc;
|
|
const zz = -sc * cb * sa + cc * ca;
|
|
if (dir === PrecessDirection.Into2000) {
|
|
// Perform rotation from epoch to J2000.0.
|
|
return new RotationMatrix([
|
|
[xx, yx, zx],
|
|
[xy, yy, zy],
|
|
[xz, yz, zz]
|
|
]);
|
|
}
|
|
if (dir === PrecessDirection.From2000) {
|
|
// Perform rotation from J2000.0 to epoch.
|
|
return new RotationMatrix([
|
|
[xx, xy, xz],
|
|
[yx, yy, yz],
|
|
[zx, zy, zz]
|
|
]);
|
|
}
|
|
throw 'Invalid precess direction';
|
|
}
|
|
function era(time) {
|
|
const thet1 = 0.7790572732640 + 0.00273781191135448 * time.ut;
|
|
const thet3 = time.ut % 1;
|
|
let theta = 360 * ((thet1 + thet3) % 1);
|
|
if (theta < 0) {
|
|
theta += 360;
|
|
}
|
|
return theta;
|
|
}
|
|
function sidereal_time(time) {
|
|
const t = time.tt / 36525;
|
|
let eqeq = 15 * e_tilt(time).ee; // Replace with eqeq=0 to get GMST instead of GAST (if we ever need it)
|
|
const theta = era(time);
|
|
const st = (eqeq + 0.014506 +
|
|
((((-0.0000000368 * t
|
|
- 0.000029956) * t
|
|
- 0.00000044) * t
|
|
+ 1.3915817) * t
|
|
+ 4612.156534) * t);
|
|
let gst = ((st / 3600 + theta) % 360) / 15;
|
|
if (gst < 0) {
|
|
gst += 24;
|
|
}
|
|
return gst; // return sidereal hours in the half-open range [0, 24).
|
|
}
|
|
function inverse_terra(ovec, st) {
|
|
// Convert from AU to kilometers
|
|
const x = ovec[0] * exports.KM_PER_AU;
|
|
const y = ovec[1] * exports.KM_PER_AU;
|
|
const z = ovec[2] * exports.KM_PER_AU;
|
|
const p = Math.sqrt(x * x + y * y);
|
|
let lon_deg, lat_deg, height_km;
|
|
if (p < 1.0e-6) {
|
|
// Special case: within 1 millimeter of a pole!
|
|
// Use arbitrary longitude, and latitude determined by polarity of z.
|
|
lon_deg = 0;
|
|
lat_deg = (z > 0.0) ? +90 : -90;
|
|
// Elevation is calculated directly from z.
|
|
height_km = Math.abs(z) - EARTH_POLAR_RADIUS_KM;
|
|
}
|
|
else {
|
|
const stlocl = Math.atan2(y, x);
|
|
// Calculate exact longitude.
|
|
lon_deg = (exports.RAD2DEG * stlocl) - (15.0 * st);
|
|
// Normalize longitude to the range (-180, +180].
|
|
while (lon_deg <= -180)
|
|
lon_deg += 360;
|
|
while (lon_deg > +180)
|
|
lon_deg -= 360;
|
|
// Numerically solve for exact latitude, using Newton's Method.
|
|
const F = EARTH_FLATTENING * EARTH_FLATTENING;
|
|
// Start with initial latitude estimate, based on a spherical Earth.
|
|
let lat = Math.atan2(z, p);
|
|
let cos, sin, denom;
|
|
for (;;) {
|
|
// Calculate the error function W(lat).
|
|
// We try to find the root of W, meaning where the error is 0.
|
|
cos = Math.cos(lat);
|
|
sin = Math.sin(lat);
|
|
const factor = (F - 1) * EARTH_EQUATORIAL_RADIUS_KM;
|
|
const cos2 = cos * cos;
|
|
const sin2 = sin * sin;
|
|
const radicand = cos2 + F * sin2;
|
|
denom = Math.sqrt(radicand);
|
|
const W = (factor * sin * cos) / denom - z * cos + p * sin;
|
|
if (Math.abs(W) < 1.0e-12)
|
|
break; // The error is now negligible
|
|
// Error is still too large. Find the next estimate.
|
|
// Calculate D = the derivative of W with respect to lat.
|
|
const D = factor * ((cos2 - sin2) / denom - sin2 * cos2 * (F - 1) / (factor * radicand)) + z * sin + p * cos;
|
|
lat -= W / D;
|
|
}
|
|
// We now have a solution for the latitude in radians.
|
|
lat_deg = exports.RAD2DEG * lat;
|
|
// Solve for exact height in meters.
|
|
// There are two formulas I can use. Use whichever has the less risky denominator.
|
|
const adjust = EARTH_EQUATORIAL_RADIUS_KM / denom;
|
|
if (Math.abs(sin) > Math.abs(cos))
|
|
height_km = z / sin - F * adjust;
|
|
else
|
|
height_km = p / cos - adjust;
|
|
}
|
|
return new Observer(lat_deg, lon_deg, 1000 * height_km);
|
|
}
|
|
function terra(observer, st) {
|
|
const df = 1 - 0.003352819697896; // flattening of the Earth
|
|
const df2 = df * df;
|
|
const phi = observer.latitude * exports.DEG2RAD;
|
|
const sinphi = Math.sin(phi);
|
|
const cosphi = Math.cos(phi);
|
|
const c = 1 / Math.sqrt(cosphi * cosphi + df2 * sinphi * sinphi);
|
|
const s = df2 * c;
|
|
const ht_km = observer.height / 1000;
|
|
const ach = EARTH_EQUATORIAL_RADIUS_KM * c + ht_km;
|
|
const ash = EARTH_EQUATORIAL_RADIUS_KM * s + ht_km;
|
|
const stlocl = (15 * st + observer.longitude) * exports.DEG2RAD;
|
|
const sinst = Math.sin(stlocl);
|
|
const cosst = Math.cos(stlocl);
|
|
return {
|
|
pos: [ach * cosphi * cosst / exports.KM_PER_AU, ach * cosphi * sinst / exports.KM_PER_AU, ash * sinphi / exports.KM_PER_AU],
|
|
vel: [-ANGVEL * ach * cosphi * sinst * 86400 / exports.KM_PER_AU, ANGVEL * ach * cosphi * cosst * 86400 / exports.KM_PER_AU, 0]
|
|
};
|
|
}
|
|
function nutation(pos, time, dir) {
|
|
const r = nutation_rot(time, dir);
|
|
return rotate(r, pos);
|
|
}
|
|
function nutation_posvel(state, time, dir) {
|
|
const r = nutation_rot(time, dir);
|
|
return RotateState(r, state);
|
|
}
|
|
function nutation_rot(time, dir) {
|
|
const tilt = e_tilt(time);
|
|
const oblm = tilt.mobl * exports.DEG2RAD;
|
|
const oblt = tilt.tobl * exports.DEG2RAD;
|
|
const psi = tilt.dpsi * ASEC2RAD;
|
|
const cobm = Math.cos(oblm);
|
|
const sobm = Math.sin(oblm);
|
|
const cobt = Math.cos(oblt);
|
|
const sobt = Math.sin(oblt);
|
|
const cpsi = Math.cos(psi);
|
|
const spsi = Math.sin(psi);
|
|
const xx = cpsi;
|
|
const yx = -spsi * cobm;
|
|
const zx = -spsi * sobm;
|
|
const xy = spsi * cobt;
|
|
const yy = cpsi * cobm * cobt + sobm * sobt;
|
|
const zy = cpsi * sobm * cobt - cobm * sobt;
|
|
const xz = spsi * sobt;
|
|
const yz = cpsi * cobm * sobt - sobm * cobt;
|
|
const zz = cpsi * sobm * sobt + cobm * cobt;
|
|
if (dir === PrecessDirection.From2000) {
|
|
// convert J2000 to of-date
|
|
return new RotationMatrix([
|
|
[xx, xy, xz],
|
|
[yx, yy, yz],
|
|
[zx, zy, zz]
|
|
]);
|
|
}
|
|
if (dir === PrecessDirection.Into2000) {
|
|
// convert of-date to J2000
|
|
return new RotationMatrix([
|
|
[xx, yx, zx],
|
|
[xy, yy, zy],
|
|
[xz, yz, zz]
|
|
]);
|
|
}
|
|
throw 'Invalid precess direction';
|
|
}
|
|
function gyration(pos, time, dir) {
|
|
// Combine nutation and precession into a single operation I call "gyration".
|
|
// The order they are composed depends on the direction,
|
|
// because both directions are mutual inverse functions.
|
|
return (dir === PrecessDirection.Into2000) ?
|
|
precession(nutation(pos, time, dir), time, dir) :
|
|
nutation(precession(pos, time, dir), time, dir);
|
|
}
|
|
function gyration_posvel(state, time, dir) {
|
|
// Combine nutation and precession into a single operation I call "gyration".
|
|
// The order they are composed depends on the direction,
|
|
// because both directions are mutual inverse functions.
|
|
return (dir === PrecessDirection.Into2000) ?
|
|
precession_posvel(nutation_posvel(state, time, dir), time, dir) :
|
|
nutation_posvel(precession_posvel(state, time, dir), time, dir);
|
|
}
|
|
function geo_pos(time, observer) {
|
|
const gast = sidereal_time(time);
|
|
const pos = terra(observer, gast).pos;
|
|
return gyration(pos, time, PrecessDirection.Into2000);
|
|
}
|
|
/**
|
|
* @brief A 3D Cartesian vector with a time attached to it.
|
|
*
|
|
* Holds the Cartesian coordinates of a vector in 3D space,
|
|
* along with the time at which the vector is valid.
|
|
*
|
|
* @property {number} x The x-coordinate expressed in astronomical units (AU).
|
|
* @property {number} y The y-coordinate expressed in astronomical units (AU).
|
|
* @property {number} z The z-coordinate expressed in astronomical units (AU).
|
|
* @property {AstroTime} t The time at which the vector is valid.
|
|
*/
|
|
class Vector {
|
|
constructor(x, y, z, t) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.t = t;
|
|
}
|
|
/**
|
|
* Returns the length of the vector in astronomical units (AU).
|
|
* @returns {number}
|
|
*/
|
|
Length() {
|
|
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
|
|
}
|
|
}
|
|
exports.Vector = Vector;
|
|
/**
|
|
* @brief A combination of a position vector, a velocity vector, and a time.
|
|
*
|
|
* Holds the state vector of a body at a given time, including its position,
|
|
* velocity, and the time they are valid.
|
|
*
|
|
* @property {number} x The position x-coordinate expressed in astronomical units (AU).
|
|
* @property {number} y The position y-coordinate expressed in astronomical units (AU).
|
|
* @property {number} z The position z-coordinate expressed in astronomical units (AU).
|
|
* @property {number} vx The velocity x-coordinate expressed in AU/day.
|
|
* @property {number} vy The velocity y-coordinate expressed in AU/day.
|
|
* @property {number} vz The velocity z-coordinate expressed in AU/day.
|
|
* @property {AstroTime} t The time at which the vector is valid.
|
|
*/
|
|
class StateVector {
|
|
constructor(x, y, z, vx, vy, vz, t) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
this.vx = vx;
|
|
this.vy = vy;
|
|
this.vz = vz;
|
|
this.t = t;
|
|
}
|
|
}
|
|
exports.StateVector = StateVector;
|
|
/**
|
|
* @brief Holds spherical coordinates: latitude, longitude, distance.
|
|
*
|
|
* Spherical coordinates represent the location of
|
|
* a point using two angles and a distance.
|
|
*
|
|
* @property {number} lat The latitude angle: -90..+90 degrees.
|
|
* @property {number} lon The longitude angle: 0..360 degrees.
|
|
* @property {number} dist Distance in AU.
|
|
*/
|
|
class Spherical {
|
|
constructor(lat, lon, dist) {
|
|
this.lat = VerifyNumber(lat);
|
|
this.lon = VerifyNumber(lon);
|
|
this.dist = VerifyNumber(dist);
|
|
}
|
|
}
|
|
exports.Spherical = Spherical;
|
|
/**
|
|
* @brief Holds right ascension, declination, and distance of a celestial object.
|
|
*
|
|
* @property {number} ra
|
|
* Right ascension in sidereal hours: [0, 24).
|
|
*
|
|
* @property {number} dec
|
|
* Declination in degrees: [-90, +90].
|
|
*
|
|
* @property {number} dist
|
|
* Distance to the celestial object expressed in
|
|
* <a href="https://en.wikipedia.org/wiki/Astronomical_unit">astronomical units</a> (AU).
|
|
*
|
|
* @property {Vector} vec
|
|
* The equatorial coordinates in cartesian form, using AU distance units.
|
|
* x = direction of the March equinox,
|
|
* y = direction of the June solstice,
|
|
* z = north.
|
|
*/
|
|
class EquatorialCoordinates {
|
|
constructor(ra, dec, dist, vec) {
|
|
this.ra = VerifyNumber(ra);
|
|
this.dec = VerifyNumber(dec);
|
|
this.dist = VerifyNumber(dist);
|
|
this.vec = vec;
|
|
}
|
|
}
|
|
exports.EquatorialCoordinates = EquatorialCoordinates;
|
|
function IsValidRotationArray(rot) {
|
|
if (!(rot instanceof Array) || (rot.length !== 3))
|
|
return false;
|
|
for (let i = 0; i < 3; ++i) {
|
|
if (!(rot[i] instanceof Array) || (rot[i].length !== 3))
|
|
return false;
|
|
for (let j = 0; j < 3; ++j)
|
|
if (!Number.isFinite(rot[i][j]))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* @brief Contains a rotation matrix that can be used to transform one coordinate system to another.
|
|
*
|
|
* @property {number[][]} rot
|
|
* A normalized 3x3 rotation matrix. For example, the identity matrix is represented
|
|
* as `[[1, 0, 0], [0, 1, 0], [0, 0, 1]]`.
|
|
*/
|
|
class RotationMatrix {
|
|
constructor(rot) {
|
|
this.rot = rot;
|
|
}
|
|
}
|
|
exports.RotationMatrix = RotationMatrix;
|
|
/**
|
|
* @brief Creates a rotation matrix that can be used to transform one coordinate system to another.
|
|
*
|
|
* This function verifies that the `rot` parameter is of the correct format:
|
|
* a number[3][3] array. It throws an exception if `rot` is not of that shape.
|
|
* Otherwise it creates a new {@link RotationMatrix} object based on `rot`.
|
|
*
|
|
* @param {number[][]} rot
|
|
* An array [3][3] of numbers. Defines a rotation matrix used to premultiply
|
|
* a 3D vector to reorient it into another coordinate system.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
*/
|
|
function MakeRotation(rot) {
|
|
if (!IsValidRotationArray(rot))
|
|
throw 'Argument must be a [3][3] array of numbers';
|
|
return new RotationMatrix(rot);
|
|
}
|
|
exports.MakeRotation = MakeRotation;
|
|
/**
|
|
* @brief Represents the location of an object seen by an observer on the Earth.
|
|
*
|
|
* Holds azimuth (compass direction) and altitude (angle above/below the horizon)
|
|
* of a celestial object as seen by an observer at a particular location on the Earth's surface.
|
|
* Also holds right ascension and declination of the same object.
|
|
* All of these coordinates are optionally adjusted for atmospheric refraction;
|
|
* therefore the right ascension and declination values may not exactly match
|
|
* those found inside a corresponding {@link EquatorialCoordinates} object.
|
|
*
|
|
* @property {number} azimuth
|
|
* A horizontal compass direction angle in degrees measured starting at north
|
|
* and increasing positively toward the east.
|
|
* The value is in the range [0, 360).
|
|
* North = 0, east = 90, south = 180, west = 270.
|
|
*
|
|
* @property {number} altitude
|
|
* A vertical angle in degrees above (positive) or below (negative) the horizon.
|
|
* The value is in the range [-90, +90].
|
|
* The altitude angle is optionally adjusted upward due to atmospheric refraction.
|
|
*
|
|
* @property {number} ra
|
|
* The right ascension of the celestial body in sidereal hours.
|
|
* The value is in the reange [0, 24).
|
|
* If `altitude` was adjusted for atmospheric reaction, `ra`
|
|
* is likewise adjusted.
|
|
*
|
|
* @property {number} dec
|
|
* The declination of of the celestial body in degrees.
|
|
* The value in the range [-90, +90].
|
|
* If `altitude` was adjusted for atmospheric reaction, `dec`
|
|
* is likewise adjusted.
|
|
*/
|
|
class HorizontalCoordinates {
|
|
constructor(azimuth, altitude, ra, dec) {
|
|
this.azimuth = VerifyNumber(azimuth);
|
|
this.altitude = VerifyNumber(altitude);
|
|
this.ra = VerifyNumber(ra);
|
|
this.dec = VerifyNumber(dec);
|
|
}
|
|
}
|
|
exports.HorizontalCoordinates = HorizontalCoordinates;
|
|
/**
|
|
* @brief Ecliptic coordinates of a celestial body.
|
|
*
|
|
* The origin and date of the coordinate system may vary depending on the caller's usage.
|
|
* In general, ecliptic coordinates are measured with respect to the mean plane of the Earth's
|
|
* orbit around the Sun.
|
|
* Includes Cartesian coordinates `(ex, ey, ez)` measured in
|
|
* <a href="https://en.wikipedia.org/wiki/Astronomical_unit">astronomical units</a> (AU)
|
|
* and spherical coordinates `(elon, elat)` measured in degrees.
|
|
*
|
|
* @property {Vector} vec
|
|
* Ecliptic cartesian vector with components measured in astronomical units (AU).
|
|
* The x-axis is within the ecliptic plane and is oriented in the direction of the
|
|
* <a href="https://en.wikipedia.org/wiki/Equinox_(celestial_coordinates)">equinox</a>.
|
|
* The y-axis is within the ecliptic plane and is oriented 90 degrees
|
|
* counterclockwise from the equinox, as seen from above the Sun's north pole.
|
|
* The z-axis is oriented perpendicular to the ecliptic plane,
|
|
* along the direction of the Sun's north pole.
|
|
*
|
|
* @property {number} elat
|
|
* The ecliptic latitude of the body in degrees.
|
|
* This is the angle north or south of the ecliptic plane.
|
|
* The value is in the range [-90, +90].
|
|
* Positive values are north and negative values are south.
|
|
*
|
|
* @property {number} elon
|
|
* The ecliptic longitude of the body in degrees.
|
|
* This is the angle measured counterclockwise around the ecliptic plane,
|
|
* as seen from above the Sun's north pole.
|
|
* This is the same direction that the Earth orbits around the Sun.
|
|
* The angle is measured starting at 0 from the equinox and increases
|
|
* up to 360 degrees.
|
|
*/
|
|
class EclipticCoordinates {
|
|
constructor(vec, elat, elon) {
|
|
this.vec = vec;
|
|
this.elat = VerifyNumber(elat);
|
|
this.elon = VerifyNumber(elon);
|
|
}
|
|
}
|
|
exports.EclipticCoordinates = EclipticCoordinates;
|
|
function VectorFromArray(av, time) {
|
|
return new Vector(av[0], av[1], av[2], time);
|
|
}
|
|
function vector2radec(pos, time) {
|
|
const vec = VectorFromArray(pos, time);
|
|
const xyproj = vec.x * vec.x + vec.y * vec.y;
|
|
const dist = Math.sqrt(xyproj + vec.z * vec.z);
|
|
if (xyproj === 0) {
|
|
if (vec.z === 0)
|
|
throw 'Indeterminate sky coordinates';
|
|
return new EquatorialCoordinates(0, (vec.z < 0) ? -90 : +90, dist, vec);
|
|
}
|
|
let ra = exports.RAD2HOUR * Math.atan2(vec.y, vec.x);
|
|
if (ra < 0)
|
|
ra += 24;
|
|
const dec = exports.RAD2DEG * Math.atan2(pos[2], Math.sqrt(xyproj));
|
|
return new EquatorialCoordinates(ra, dec, dist, vec);
|
|
}
|
|
function spin(angle, pos) {
|
|
const angr = angle * exports.DEG2RAD;
|
|
const c = Math.cos(angr);
|
|
const s = Math.sin(angr);
|
|
return [c * pos[0] + s * pos[1], c * pos[1] - s * pos[0], pos[2]];
|
|
}
|
|
/**
|
|
* @brief Converts equatorial coordinates to horizontal coordinates.
|
|
*
|
|
* Given a date and time, a geographic location of an observer on the Earth, and
|
|
* equatorial coordinates (right ascension and declination) of a celestial body,
|
|
* returns horizontal coordinates (azimuth and altitude angles) for that body
|
|
* as seen by that observer. Allows optional correction for atmospheric refraction.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to find horizontal coordinates.
|
|
*
|
|
* @param {Observer} observer
|
|
* The location of the observer for which to find horizontal coordinates.
|
|
*
|
|
* @param {number} ra
|
|
* Right ascension in sidereal hours of the celestial object,
|
|
* referred to the mean equinox of date for the J2000 epoch.
|
|
*
|
|
* @param {number} dec
|
|
* Declination in degrees of the celestial object,
|
|
* referred to the mean equator of date for the J2000 epoch.
|
|
* Positive values are north of the celestial equator and negative values are south.
|
|
*
|
|
* @param {string} refraction
|
|
* If omitted or has a false-like value (false, null, undefined, etc.)
|
|
* the calculations are performed without any correction for atmospheric
|
|
* refraction. If the value is the string `"normal"`,
|
|
* uses the recommended refraction correction based on Meeus "Astronomical Algorithms"
|
|
* with a linear taper more than 1 degree below the horizon. The linear
|
|
* taper causes the refraction to linearly approach 0 as the altitude of the
|
|
* body approaches the nadir (-90 degrees).
|
|
* If the value is the string `"jplhor"`, uses a JPL Horizons
|
|
* compatible formula. This is the same algorithm as `"normal"`,
|
|
* only without linear tapering; this can result in physically impossible
|
|
* altitudes of less than -90 degrees, which may cause problems for some applications.
|
|
* (The `"jplhor"` option was created for unit testing against data
|
|
* generated by JPL Horizons, and is otherwise not recommended for use.)
|
|
*
|
|
* @returns {HorizontalCoordinates}
|
|
*/
|
|
function Horizon(date, observer, ra, dec, refraction) {
|
|
// based on NOVAS equ2hor()
|
|
let time = MakeTime(date);
|
|
VerifyObserver(observer);
|
|
VerifyNumber(ra);
|
|
VerifyNumber(dec);
|
|
const sinlat = Math.sin(observer.latitude * exports.DEG2RAD);
|
|
const coslat = Math.cos(observer.latitude * exports.DEG2RAD);
|
|
const sinlon = Math.sin(observer.longitude * exports.DEG2RAD);
|
|
const coslon = Math.cos(observer.longitude * exports.DEG2RAD);
|
|
const sindc = Math.sin(dec * exports.DEG2RAD);
|
|
const cosdc = Math.cos(dec * exports.DEG2RAD);
|
|
const sinra = Math.sin(ra * exports.HOUR2RAD);
|
|
const cosra = Math.cos(ra * exports.HOUR2RAD);
|
|
// Calculate three mutually perpendicular unit vectors
|
|
// in equatorial coordinates: uze, une, uwe.
|
|
//
|
|
// uze = The direction of the observer's local zenith (straight up).
|
|
// une = The direction toward due north on the observer's horizon.
|
|
// uwe = The direction toward due west on the observer's horizon.
|
|
//
|
|
// HOWEVER, these are uncorrected for the Earth's rotation due to the time of day.
|
|
//
|
|
// The components of these 3 vectors are as follows:
|
|
// x = direction from center of Earth toward 0 degrees longitude (the prime meridian) on equator.
|
|
// y = direction from center of Earth toward 90 degrees west longitude on equator.
|
|
// z = direction from center of Earth toward the north pole.
|
|
let uze = [coslat * coslon, coslat * sinlon, sinlat];
|
|
let une = [-sinlat * coslon, -sinlat * sinlon, coslat];
|
|
let uwe = [sinlon, -coslon, 0];
|
|
// Correct the vectors uze, une, uwe for the Earth's rotation by calculating
|
|
// sideral time. Call spin() for each uncorrected vector to rotate about
|
|
// the Earth's axis to yield corrected unit vectors uz, un, uw.
|
|
// Multiply sidereal hours by -15 to convert to degrees and flip eastward
|
|
// rotation of the Earth to westward apparent movement of objects with time.
|
|
const spin_angle = -15 * sidereal_time(time);
|
|
let uz = spin(spin_angle, uze);
|
|
let un = spin(spin_angle, une);
|
|
let uw = spin(spin_angle, uwe);
|
|
// Convert angular equatorial coordinates (RA, DEC) to
|
|
// cartesian equatorial coordinates in 'p', using the
|
|
// same orientation system as uze, une, uwe.
|
|
let p = [cosdc * cosra, cosdc * sinra, sindc];
|
|
// Use dot products of p with the zenith, north, and west
|
|
// vectors to obtain the cartesian coordinates of the body in
|
|
// the observer's horizontal orientation system.
|
|
// pz = zenith component [-1, +1]
|
|
// pn = north component [-1, +1]
|
|
// pw = west component [-1, +1]
|
|
const pz = p[0] * uz[0] + p[1] * uz[1] + p[2] * uz[2];
|
|
const pn = p[0] * un[0] + p[1] * un[1] + p[2] * un[2];
|
|
const pw = p[0] * uw[0] + p[1] * uw[1] + p[2] * uw[2];
|
|
// proj is the "shadow" of the body vector along the observer's flat ground.
|
|
let proj = Math.sqrt(pn * pn + pw * pw);
|
|
// Calculate az = azimuth (compass direction clockwise from East.)
|
|
let az;
|
|
if (proj > 0) {
|
|
// If the body is not exactly straight up/down, it has an azimuth.
|
|
// Invert the angle to produce degrees eastward from north.
|
|
az = -exports.RAD2DEG * Math.atan2(pw, pn);
|
|
if (az < 0)
|
|
az += 360;
|
|
}
|
|
else {
|
|
// The body is straight up/down, so it does not have an azimuth.
|
|
// Report an arbitrary but reasonable value.
|
|
az = 0;
|
|
}
|
|
// zd = the angle of the body away from the observer's zenith, in degrees.
|
|
let zd = exports.RAD2DEG * Math.atan2(proj, pz);
|
|
let out_ra = ra;
|
|
let out_dec = dec;
|
|
if (refraction) {
|
|
let zd0 = zd;
|
|
let refr = Refraction(refraction, 90 - zd);
|
|
zd -= refr;
|
|
if (refr > 0.0 && zd > 3.0e-4) {
|
|
const sinzd = Math.sin(zd * exports.DEG2RAD);
|
|
const coszd = Math.cos(zd * exports.DEG2RAD);
|
|
const sinzd0 = Math.sin(zd0 * exports.DEG2RAD);
|
|
const coszd0 = Math.cos(zd0 * exports.DEG2RAD);
|
|
var pr = [];
|
|
for (let j = 0; j < 3; ++j) {
|
|
pr.push(((p[j] - coszd0 * uz[j]) / sinzd0) * sinzd + uz[j] * coszd);
|
|
}
|
|
proj = Math.sqrt(pr[0] * pr[0] + pr[1] * pr[1]);
|
|
if (proj > 0) {
|
|
out_ra = exports.RAD2HOUR * Math.atan2(pr[1], pr[0]);
|
|
if (out_ra < 0) {
|
|
out_ra += 24;
|
|
}
|
|
}
|
|
else {
|
|
out_ra = 0;
|
|
}
|
|
out_dec = exports.RAD2DEG * Math.atan2(pr[2], proj);
|
|
}
|
|
}
|
|
return new HorizontalCoordinates(az, 90 - zd, out_ra, out_dec);
|
|
}
|
|
exports.Horizon = Horizon;
|
|
function VerifyObserver(observer) {
|
|
if (!(observer instanceof Observer)) {
|
|
throw `Not an instance of the Observer class: ${observer}`;
|
|
}
|
|
VerifyNumber(observer.latitude);
|
|
VerifyNumber(observer.longitude);
|
|
VerifyNumber(observer.height);
|
|
if (observer.latitude < -90 || observer.latitude > +90) {
|
|
throw `Latitude ${observer.latitude} is out of range. Must be -90..+90.`;
|
|
}
|
|
return observer;
|
|
}
|
|
/**
|
|
* @brief Represents the geographic location of an observer on the surface of the Earth.
|
|
*
|
|
* @property {number} latitude
|
|
* The observer's geographic latitude in degrees north of the Earth's equator.
|
|
* The value is negative for observers south of the equator.
|
|
* Must be in the range -90 to +90.
|
|
*
|
|
* @property {number} longitude
|
|
* The observer's geographic longitude in degrees east of the prime meridian
|
|
* passing through Greenwich, England.
|
|
* The value is negative for observers west of the prime meridian.
|
|
* The value should be kept in the range -180 to +180 to minimize floating point errors.
|
|
*
|
|
* @property {number} height
|
|
* The observer's elevation above mean sea level, expressed in meters.
|
|
*/
|
|
class Observer {
|
|
constructor(latitude, longitude, height) {
|
|
this.latitude = latitude;
|
|
this.longitude = longitude;
|
|
this.height = height;
|
|
VerifyObserver(this);
|
|
}
|
|
}
|
|
exports.Observer = Observer;
|
|
/**
|
|
* @brief Returns apparent geocentric true ecliptic coordinates of date for the Sun.
|
|
*
|
|
* This function is used for calculating the times of equinoxes and solstices.
|
|
*
|
|
* <i>Geocentric</i> means coordinates as the Sun would appear to a hypothetical observer
|
|
* at the center of the Earth.
|
|
* <i>Ecliptic coordinates of date</i> are measured along the plane of the Earth's mean
|
|
* orbit around the Sun, using the
|
|
* <a href="https://en.wikipedia.org/wiki/Equinox_(celestial_coordinates)">equinox</a>
|
|
* of the Earth as adjusted for precession and nutation of the Earth's
|
|
* axis of rotation on the given date.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time at which to calculate the Sun's apparent location as seen from
|
|
* the center of the Earth.
|
|
*
|
|
* @returns {EclipticCoordinates}
|
|
*/
|
|
function SunPosition(date) {
|
|
// Correct for light travel time from the Sun.
|
|
// This is really the same as correcting for aberration.
|
|
// Otherwise season calculations (equinox, solstice) will all be early by about 8 minutes!
|
|
const time = MakeTime(date).AddDays(-1 / exports.C_AUDAY);
|
|
// Get heliocentric cartesian coordinates of Earth in J2000.
|
|
const earth2000 = CalcVsop(vsop.Earth, time);
|
|
// Convert to geocentric location of the Sun.
|
|
const sun2000 = [-earth2000.x, -earth2000.y, -earth2000.z];
|
|
// Convert to equator-of-date equatorial cartesian coordinates.
|
|
const [gx, gy, gz] = gyration(sun2000, time, PrecessDirection.From2000);
|
|
// Convert to ecliptic coordinates of date.
|
|
const true_obliq = exports.DEG2RAD * e_tilt(time).tobl;
|
|
const cos_ob = Math.cos(true_obliq);
|
|
const sin_ob = Math.sin(true_obliq);
|
|
const vec = new Vector(gx, gy, gz, time);
|
|
const sun_ecliptic = RotateEquatorialToEcliptic(vec, cos_ob, sin_ob);
|
|
return sun_ecliptic;
|
|
}
|
|
exports.SunPosition = SunPosition;
|
|
/**
|
|
* @brief Calculates equatorial coordinates of a Solar System body at a given time.
|
|
*
|
|
* Returns topocentric equatorial coordinates (right ascension and declination)
|
|
* in one of two different systems: J2000 or true-equator-of-date.
|
|
* Allows optional correction for aberration.
|
|
* Always corrects for light travel time (represents the object as seen by the observer
|
|
* with light traveling to the Earth at finite speed, not where the object is right now).
|
|
* <i>Topocentric</i> refers to a position as seen by an observer on the surface of the Earth.
|
|
* This function corrects for
|
|
* <a href="https://en.wikipedia.org/wiki/Parallax">parallax</a>
|
|
* of the object between a geocentric observer and a topocentric observer.
|
|
* This is most significant for the Moon, because it is so close to the Earth.
|
|
* However, it can have a small effect on the apparent positions of other bodies.
|
|
*
|
|
* @param {Body} body
|
|
* The body for which to find equatorial coordinates.
|
|
* Not allowed to be `"Earth"`.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* Specifies the date and time at which the body is to be observed.
|
|
*
|
|
* @param {Observer} observer
|
|
* The location on the Earth of the observer.
|
|
*
|
|
* @param {bool} ofdate
|
|
* Pass `true` to return equatorial coordinates of date,
|
|
* i.e. corrected for precession and nutation at the given date.
|
|
* This is needed to get correct horizontal coordinates when you call
|
|
* {@link Horizon}.
|
|
* Pass `false` to return equatorial coordinates in the J2000 system.
|
|
*
|
|
* @param {bool} aberration
|
|
* Pass `true` to correct for
|
|
* <a href="https://en.wikipedia.org/wiki/Aberration_of_light">aberration</a>,
|
|
* or `false` to leave uncorrected.
|
|
*
|
|
* @returns {EquatorialCoordinates}
|
|
* The topocentric coordinates of the body as adjusted for the given observer.
|
|
*/
|
|
function Equator(body, date, observer, ofdate, aberration) {
|
|
VerifyObserver(observer);
|
|
VerifyBoolean(ofdate);
|
|
VerifyBoolean(aberration);
|
|
const time = MakeTime(date);
|
|
const gc_observer = geo_pos(time, observer);
|
|
const gc = GeoVector(body, time, aberration);
|
|
const j2000 = [
|
|
gc.x - gc_observer[0],
|
|
gc.y - gc_observer[1],
|
|
gc.z - gc_observer[2]
|
|
];
|
|
if (!ofdate)
|
|
return vector2radec(j2000, time);
|
|
const datevect = gyration(j2000, time, PrecessDirection.From2000);
|
|
return vector2radec(datevect, time);
|
|
}
|
|
exports.Equator = Equator;
|
|
/**
|
|
* @brief Calculates geocentric equatorial coordinates of an observer on the surface of the Earth.
|
|
*
|
|
* This function calculates a vector from the center of the Earth to
|
|
* a point on or near the surface of the Earth, expressed in equatorial
|
|
* coordinates. It takes into account the rotation of the Earth at the given
|
|
* time, along with the given latitude, longitude, and elevation of the observer.
|
|
*
|
|
* The caller may pass `ofdate` as `true` to return coordinates relative to the Earth's
|
|
* equator at the specified time, or `false` to use the J2000 equator.
|
|
*
|
|
* The returned vector has components expressed in astronomical units (AU).
|
|
* To convert to kilometers, multiply the `x`, `y`, and `z` values by
|
|
* the constant value {@link KM_PER_AU}.
|
|
*
|
|
* The inverse of this function is also available: {@link VectorObserver}.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the observer's position vector.
|
|
*
|
|
* @param {Observer} observer
|
|
* The geographic location of a point on or near the surface of the Earth.
|
|
*
|
|
* @param {boolean} ofdate
|
|
* Selects the date of the Earth's equator in which to express the equatorial coordinates.
|
|
* The caller may pass `false` to use the orientation of the Earth's equator
|
|
* at noon UTC on January 1, 2000, in which case this function corrects for precession
|
|
* and nutation of the Earth as it was at the moment specified by the `time` parameter.
|
|
* Or the caller may pass `true` to use the Earth's equator at `time`
|
|
* as the orientation.
|
|
*
|
|
* @returns {Vector}
|
|
* An equatorial vector from the center of the Earth to the specified location
|
|
* on (or near) the Earth's surface.
|
|
*/
|
|
function ObserverVector(date, observer, ofdate) {
|
|
const time = MakeTime(date);
|
|
const gast = sidereal_time(time);
|
|
let ovec = terra(observer, gast).pos;
|
|
if (!ofdate)
|
|
ovec = gyration(ovec, time, PrecessDirection.Into2000);
|
|
return VectorFromArray(ovec, time);
|
|
}
|
|
exports.ObserverVector = ObserverVector;
|
|
/**
|
|
* @brief Calculates geocentric equatorial position and velocity of an observer on the surface of the Earth.
|
|
*
|
|
* This function calculates position and velocity vectors of an observer
|
|
* on or near the surface of the Earth, expressed in equatorial
|
|
* coordinates. It takes into account the rotation of the Earth at the given
|
|
* time, along with the given latitude, longitude, and elevation of the observer.
|
|
*
|
|
* The caller may pass `ofdate` as `true` to return coordinates relative to the Earth's
|
|
* equator at the specified time, or `false` to use the J2000 equator.
|
|
*
|
|
* The returned position vector has components expressed in astronomical units (AU).
|
|
* To convert to kilometers, multiply the `x`, `y`, and `z` values by
|
|
* the constant value {@link KM_PER_AU}.
|
|
* The returned velocity vector has components expressed in AU/day.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the observer's position and velocity vectors.
|
|
*
|
|
* @param {Observer} observer
|
|
* The geographic location of a point on or near the surface of the Earth.
|
|
*
|
|
* @param {boolean} ofdate
|
|
* Selects the date of the Earth's equator in which to express the equatorial coordinates.
|
|
* The caller may pass `false` to use the orientation of the Earth's equator
|
|
* at noon UTC on January 1, 2000, in which case this function corrects for precession
|
|
* and nutation of the Earth as it was at the moment specified by the `time` parameter.
|
|
* Or the caller may pass `true` to use the Earth's equator at `time`
|
|
* as the orientation.
|
|
*
|
|
* @returns {StateVector}
|
|
*/
|
|
function ObserverState(date, observer, ofdate) {
|
|
const time = MakeTime(date);
|
|
const gast = sidereal_time(time);
|
|
const svec = terra(observer, gast);
|
|
const state = new StateVector(svec.pos[0], svec.pos[1], svec.pos[2], svec.vel[0], svec.vel[1], svec.vel[2], time);
|
|
if (!ofdate)
|
|
return gyration_posvel(state, time, PrecessDirection.Into2000);
|
|
return state;
|
|
}
|
|
exports.ObserverState = ObserverState;
|
|
/**
|
|
* @brief Calculates the geographic location corresponding to an equatorial vector.
|
|
*
|
|
* This is the inverse function of {@link ObserverVector}.
|
|
* Given a geocentric equatorial vector, it returns the geographic
|
|
* latitude, longitude, and elevation for that vector.
|
|
*
|
|
* @param {Vector} vector
|
|
* The geocentric equatorial position vector for which to find geographic coordinates.
|
|
* The components are expressed in Astronomical Units (AU).
|
|
* You can calculate AU by dividing kilometers by the constant {@link KM_PER_AU}.
|
|
* The time `vector.t` determines the Earth's rotation.
|
|
*
|
|
* @param {boolean} ofdate
|
|
* Selects the date of the Earth's equator in which `vector` is expressed.
|
|
* The caller may select `false` to use the orientation of the Earth's equator
|
|
* at noon UTC on January 1, 2000, in which case this function corrects for precession
|
|
* and nutation of the Earth as it was at the moment specified by `vector.t`.
|
|
* Or the caller may select `true` to use the Earth's equator at `vector.t`
|
|
* as the orientation.
|
|
*
|
|
* @returns {Observer}
|
|
* The geographic latitude, longitude, and elevation above sea level
|
|
* that corresponds to the given equatorial vector.
|
|
*/
|
|
function VectorObserver(vector, ofdate) {
|
|
const gast = sidereal_time(vector.t);
|
|
let ovec = [vector.x, vector.y, vector.z];
|
|
if (!ofdate) {
|
|
ovec = precession(ovec, vector.t, PrecessDirection.From2000);
|
|
ovec = nutation(ovec, vector.t, PrecessDirection.From2000);
|
|
}
|
|
return inverse_terra(ovec, gast);
|
|
}
|
|
exports.VectorObserver = VectorObserver;
|
|
/**
|
|
* @brief Calculates the gravitational acceleration experienced by an observer on the Earth.
|
|
*
|
|
* This function implements the WGS 84 Ellipsoidal Gravity Formula.
|
|
* The result is a combination of inward gravitational acceleration
|
|
* with outward centrifugal acceleration, as experienced by an observer
|
|
* in the Earth's rotating frame of reference.
|
|
* The resulting value increases toward the Earth's poles and decreases
|
|
* toward the equator, consistent with changes of the weight measured
|
|
* by a spring scale of a fixed mass moved to different latitudes and heights
|
|
* on the Earth.
|
|
*
|
|
* @param {number} latitude
|
|
* The latitude of the observer in degrees north or south of the equator.
|
|
* By formula symmetry, positive latitudes give the same answer as negative
|
|
* latitudes, so the sign does not matter.
|
|
*
|
|
* @param {number} height
|
|
* The height above the sea level geoid in meters.
|
|
* No range checking is done; however, accuracy is only valid in the
|
|
* range 0 to 100000 meters.
|
|
*
|
|
* @returns {number}
|
|
* The effective gravitational acceleration expressed in meters per second squared [m/s^2].
|
|
*/
|
|
function ObserverGravity(latitude, height) {
|
|
const s = Math.sin(latitude * exports.DEG2RAD);
|
|
const s2 = s * s;
|
|
const g0 = 9.7803253359 * (1.0 + 0.00193185265241 * s2) / Math.sqrt(1.0 - 0.00669437999013 * s2);
|
|
return g0 * (1.0 - (3.15704e-07 - 2.10269e-09 * s2) * height + 7.37452e-14 * height * height);
|
|
}
|
|
exports.ObserverGravity = ObserverGravity;
|
|
function RotateEquatorialToEcliptic(equ, cos_ob, sin_ob) {
|
|
// Rotate equatorial vector to obtain ecliptic vector.
|
|
const ex = equ.x;
|
|
const ey = equ.y * cos_ob + equ.z * sin_ob;
|
|
const ez = -equ.y * sin_ob + equ.z * cos_ob;
|
|
const xyproj = Math.sqrt(ex * ex + ey * ey);
|
|
let elon = 0;
|
|
if (xyproj > 0) {
|
|
elon = exports.RAD2DEG * Math.atan2(ey, ex);
|
|
if (elon < 0)
|
|
elon += 360;
|
|
}
|
|
let elat = exports.RAD2DEG * Math.atan2(ez, xyproj);
|
|
let ecl = new Vector(ex, ey, ez, equ.t);
|
|
return new EclipticCoordinates(ecl, elat, elon);
|
|
}
|
|
/**
|
|
* @brief Converts equatorial Cartesian coordinates to ecliptic Cartesian and angular coordinates.
|
|
*
|
|
* Given J2000 equatorial Cartesian coordinates,
|
|
* returns J2000 ecliptic latitude, longitude, and cartesian coordinates.
|
|
* You can call {@link GeoVector} and pass the resulting vector to this function.
|
|
*
|
|
* @param {Vector} equ
|
|
* A vector in the J2000 equatorial coordinate system.
|
|
*
|
|
* @returns {EclipticCoordinates}
|
|
*/
|
|
function Ecliptic(equ) {
|
|
// Based on NOVAS functions equ2ecl() and equ2ecl_vec().
|
|
if (ob2000 === undefined) {
|
|
// Lazy-evaluate and keep the mean obliquity of the ecliptic at J2000.
|
|
// This way we don't need to crunch the numbers more than once.
|
|
ob2000 = exports.DEG2RAD * e_tilt(MakeTime(J2000)).mobl;
|
|
cos_ob2000 = Math.cos(ob2000);
|
|
sin_ob2000 = Math.sin(ob2000);
|
|
}
|
|
return RotateEquatorialToEcliptic(equ, cos_ob2000, sin_ob2000);
|
|
}
|
|
exports.Ecliptic = Ecliptic;
|
|
/**
|
|
* @brief Calculates the geocentric Cartesian coordinates for the Moon in the J2000 equatorial system.
|
|
*
|
|
* Based on the Nautical Almanac Office's <i>Improved Lunar Ephemeris</i> of 1954,
|
|
* which in turn derives from E. W. Brown's lunar theories.
|
|
* Adapted from Turbo Pascal code from the book
|
|
* <a href="https://www.springer.com/us/book/9783540672210">Astronomy on the Personal Computer</a>
|
|
* by Montenbruck and Pfleger.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the Moon's geocentric position.
|
|
*
|
|
* @returns {Vector}
|
|
*/
|
|
function GeoMoon(date) {
|
|
var time = MakeTime(date);
|
|
var moon = CalcMoon(time);
|
|
// Convert geocentric ecliptic spherical coords to cartesian coords.
|
|
var dist_cos_lat = moon.distance_au * Math.cos(moon.geo_eclip_lat);
|
|
var gepos = [
|
|
dist_cos_lat * Math.cos(moon.geo_eclip_lon),
|
|
dist_cos_lat * Math.sin(moon.geo_eclip_lon),
|
|
moon.distance_au * Math.sin(moon.geo_eclip_lat)
|
|
];
|
|
// Convert ecliptic coordinates to equatorial coordinates, both in mean equinox of date.
|
|
var mpos1 = ecl2equ_vec(time, gepos);
|
|
// Convert from mean equinox of date to J2000...
|
|
var mpos2 = precession(mpos1, time, PrecessDirection.Into2000);
|
|
return new Vector(mpos2[0], mpos2[1], mpos2[2], time);
|
|
}
|
|
exports.GeoMoon = GeoMoon;
|
|
/**
|
|
* @brief Calculates the geocentric position and velocity of the Moon at a given time.
|
|
*
|
|
* Given a time of observation, calculates the Moon's position and velocity vectors.
|
|
* The position and velocity are of the Moon's center relative to the Earth's center.
|
|
* The position (x, y, z) components are expressed in AU (astronomical units).
|
|
* The velocity (vx, vy, vz) components are expressed in AU/day.
|
|
* If you need the Moon's position only, and not its velocity,
|
|
* it is much more efficient to use {@link GeoMoon} instead.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the Moon's geocentric state.
|
|
*
|
|
* @returns {StateVector}
|
|
*/
|
|
function GeoMoonState(date) {
|
|
const time = MakeTime(date);
|
|
// This is a hack, because trying to figure out how to derive a time
|
|
// derivative for CalcMoon() would be extremely painful!
|
|
// Calculate just before and just after the given time.
|
|
// Average to find position, subtract to find velocity.
|
|
const dt = 1.0e-5; // 0.864 seconds
|
|
const t1 = time.AddDays(-dt);
|
|
const t2 = time.AddDays(+dt);
|
|
const r1 = GeoMoon(t1);
|
|
const r2 = GeoMoon(t2);
|
|
return new StateVector((r1.x + r2.x) / 2, (r1.y + r2.y) / 2, (r1.z + r2.z) / 2, (r2.x - r1.x) / (2 * dt), (r2.y - r1.y) / (2 * dt), (r2.z - r1.z) / (2 * dt), time);
|
|
}
|
|
exports.GeoMoonState = GeoMoonState;
|
|
/**
|
|
* @brief Calculates the geocentric position and velocity of the Earth/Moon barycenter.
|
|
*
|
|
* Given a time of observation, calculates the geocentric position and velocity vectors
|
|
* of the Earth/Moon barycenter (EMB).
|
|
* The position (x, y, z) components are expressed in AU (astronomical units).
|
|
* The velocity (vx, vy, vz) components are expressed in AU/day.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the EMB's geocentric state.
|
|
*
|
|
* @returns {StateVector}
|
|
*/
|
|
function GeoEmbState(date) {
|
|
const time = MakeTime(date);
|
|
const s = GeoMoonState(time);
|
|
const d = 1.0 + EARTH_MOON_MASS_RATIO;
|
|
return new StateVector(s.x / d, s.y / d, s.z / d, s.vx / d, s.vy / d, s.vz / d, time);
|
|
}
|
|
exports.GeoEmbState = GeoEmbState;
|
|
function VsopFormula(formula, t, clamp_angle) {
|
|
let tpower = 1;
|
|
let coord = 0;
|
|
for (let series of formula) {
|
|
let sum = 0;
|
|
for (let [ampl, phas, freq] of series)
|
|
sum += ampl * Math.cos(phas + (t * freq));
|
|
let incr = tpower * sum;
|
|
if (clamp_angle)
|
|
incr %= PI2; // improve precision for longitudes: they can be hundreds of radians
|
|
coord += incr;
|
|
tpower *= t;
|
|
}
|
|
return coord;
|
|
}
|
|
function VsopDeriv(formula, t) {
|
|
let tpower = 1; // t^s
|
|
let dpower = 0; // t^(s-1)
|
|
let deriv = 0;
|
|
let s = 0;
|
|
for (let series of formula) {
|
|
let sin_sum = 0;
|
|
let cos_sum = 0;
|
|
for (let [ampl, phas, freq] of series) {
|
|
let angle = phas + (t * freq);
|
|
sin_sum += ampl * freq * Math.sin(angle);
|
|
if (s > 0)
|
|
cos_sum += ampl * Math.cos(angle);
|
|
}
|
|
deriv += (s * dpower * cos_sum) - (tpower * sin_sum);
|
|
dpower = tpower;
|
|
tpower *= t;
|
|
++s;
|
|
}
|
|
return deriv;
|
|
}
|
|
const DAYS_PER_MILLENNIUM = 365250;
|
|
const LON_INDEX = 0;
|
|
const LAT_INDEX = 1;
|
|
const RAD_INDEX = 2;
|
|
function VsopRotate(eclip) {
|
|
// Convert ecliptic cartesian coordinates to equatorial cartesian coordinates.
|
|
return new TerseVector(eclip[0] + 0.000000440360 * eclip[1] - 0.000000190919 * eclip[2], -0.000000479966 * eclip[0] + 0.917482137087 * eclip[1] - 0.397776982902 * eclip[2], 0.397776982902 * eclip[1] + 0.917482137087 * eclip[2]);
|
|
}
|
|
function VsopSphereToRect(lon, lat, radius) {
|
|
// Convert spherical coordinates to ecliptic cartesian coordinates.
|
|
const r_coslat = radius * Math.cos(lat);
|
|
const coslon = Math.cos(lon);
|
|
const sinlon = Math.sin(lon);
|
|
return [
|
|
r_coslat * coslon,
|
|
r_coslat * sinlon,
|
|
radius * Math.sin(lat)
|
|
];
|
|
}
|
|
function CalcVsop(model, time) {
|
|
const t = time.tt / DAYS_PER_MILLENNIUM; // millennia since 2000
|
|
const lon = VsopFormula(model[LON_INDEX], t, true);
|
|
const lat = VsopFormula(model[LAT_INDEX], t, false);
|
|
const rad = VsopFormula(model[RAD_INDEX], t, false);
|
|
const eclip = VsopSphereToRect(lon, lat, rad);
|
|
return VsopRotate(eclip).ToAstroVector(time);
|
|
}
|
|
function CalcVsopPosVel(model, tt) {
|
|
const t = tt / DAYS_PER_MILLENNIUM;
|
|
// Calculate the VSOP "B" trigonometric series to obtain ecliptic spherical coordinates.
|
|
const lon = VsopFormula(model[LON_INDEX], t, true);
|
|
const lat = VsopFormula(model[LAT_INDEX], t, false);
|
|
const rad = VsopFormula(model[RAD_INDEX], t, false);
|
|
const dlon_dt = VsopDeriv(model[LON_INDEX], t);
|
|
const dlat_dt = VsopDeriv(model[LAT_INDEX], t);
|
|
const drad_dt = VsopDeriv(model[RAD_INDEX], t);
|
|
// Use spherical coords and spherical derivatives to calculate
|
|
// the velocity vector in rectangular coordinates.
|
|
const coslon = Math.cos(lon);
|
|
const sinlon = Math.sin(lon);
|
|
const coslat = Math.cos(lat);
|
|
const sinlat = Math.sin(lat);
|
|
const vx = (+(drad_dt * coslat * coslon)
|
|
- (rad * sinlat * coslon * dlat_dt)
|
|
- (rad * coslat * sinlon * dlon_dt));
|
|
const vy = (+(drad_dt * coslat * sinlon)
|
|
- (rad * sinlat * sinlon * dlat_dt)
|
|
+ (rad * coslat * coslon * dlon_dt));
|
|
const vz = (+(drad_dt * sinlat)
|
|
+ (rad * coslat * dlat_dt));
|
|
const eclip_pos = VsopSphereToRect(lon, lat, rad);
|
|
// Convert speed units from [AU/millennium] to [AU/day].
|
|
const eclip_vel = [
|
|
vx / DAYS_PER_MILLENNIUM,
|
|
vy / DAYS_PER_MILLENNIUM,
|
|
vz / DAYS_PER_MILLENNIUM
|
|
];
|
|
// Rotate the vectors from ecliptic to equatorial coordinates.
|
|
const equ_pos = VsopRotate(eclip_pos);
|
|
const equ_vel = VsopRotate(eclip_vel);
|
|
return new body_state_t(tt, equ_pos, equ_vel);
|
|
}
|
|
function AdjustBarycenter(ssb, time, body, pmass) {
|
|
const shift = pmass / (pmass + SUN_GM);
|
|
const planet = CalcVsop(vsop[body], time);
|
|
ssb.x += shift * planet.x;
|
|
ssb.y += shift * planet.y;
|
|
ssb.z += shift * planet.z;
|
|
}
|
|
function CalcSolarSystemBarycenter(time) {
|
|
const ssb = new Vector(0.0, 0.0, 0.0, time);
|
|
AdjustBarycenter(ssb, time, Body.Jupiter, JUPITER_GM);
|
|
AdjustBarycenter(ssb, time, Body.Saturn, SATURN_GM);
|
|
AdjustBarycenter(ssb, time, Body.Uranus, URANUS_GM);
|
|
AdjustBarycenter(ssb, time, Body.Neptune, NEPTUNE_GM);
|
|
return ssb;
|
|
}
|
|
// Pluto integrator begins ----------------------------------------------------
|
|
const PLUTO_NUM_STATES = 51;
|
|
const PLUTO_TIME_STEP = 29200;
|
|
const PLUTO_DT = 146;
|
|
const PLUTO_NSTEPS = 201;
|
|
const PlutoStateTable = [
|
|
[-730000.0, [-26.1182072321076, -14.3761681778250, 3.3844025152995], [1.6339372163656e-03, -2.7861699588508e-03, -1.3585880229445e-03]],
|
|
[-700800.0, [41.9749052021271, -0.4485029529294, -12.7703515059887], [7.3458569351457e-04, 2.2785014891658e-03, 4.8619778602049e-04]],
|
|
[-671600.0, [14.7069307807442, 44.2691105400275, 9.3536984747716], [-2.1000147999800e-03, 2.2295915939915e-04, 7.0143443551414e-04]],
|
|
[-642400.0, [-29.4410039299574, -6.4301615305696, 6.8584810113048], [8.4495803960544e-04, -3.0783914758711e-03, -1.2106305981192e-03]],
|
|
[-613200.0, [39.4443969462341, -6.5579897605705, -13.9137602964634], [1.1480029005873e-03, 2.2400006880665e-03, 3.5168075922288e-04]],
|
|
[-584000.0, [20.2303809506997, 43.2669666571891, 7.3829660919234], [-1.9754081700585e-03, 5.3457141292226e-04, 7.5929169129793e-04]],
|
|
[-554800.0, [-30.6583253646200, 2.0938188745519, 9.8805311380706], [6.1010603013347e-05, -3.1326500935382e-03, -9.9346125151067e-04]],
|
|
[-525600.0, [35.7377032516732, -12.5877060247644, -14.6778472475634], [1.5802939375649e-03, 2.1347678412429e-03, 1.9074436384343e-04]],
|
|
[-496400.0, [25.4662951885462, 41.3674783384167, 5.2164768733816], [-1.8054401046468e-03, 8.3283083599510e-04, 8.0260156912107e-04]],
|
|
[-467200.0, [-29.8471749040710, 10.6364263130814, 12.2979041801063], [-6.3257063052907e-04, -2.9969577578221e-03, -7.4476074151596e-04]],
|
|
[-438000.0, [30.7746921076872, -18.2366370153037, -14.9455358798963], [2.0113162005465e-03, 1.9353827024189e-03, -2.0937793168297e-06]],
|
|
[-408800.0, [30.2431533240283, 38.6562678885026, 2.9385017502184], [-1.6052508674468e-03, 1.1183495337525e-03, 8.3333973416824e-04]],
|
|
[-379600.0, [-27.2889847725327, 18.6431621478740, 14.0236336233295], [-1.1856388898191e-03, -2.7170609282181e-03, -4.9015526126399e-04]],
|
|
[-350400.0, [24.5196051967735, -23.2457560647266, -14.6268623673677], [2.4322321483154e-03, 1.6062008146048e-03, -2.3369181613312e-04]],
|
|
[-321200.0, [34.5052748058754, 35.1253385869542, 0.5573614756374], [-1.3824391637782e-03, 1.3833397561817e-03, 8.4823598806262e-04]],
|
|
[-292000.0, [-23.2753639151193, 25.8185142987694, 15.0553815885983], [-1.6062295460975e-03, -2.3395961498533e-03, -2.4377362639479e-04]],
|
|
[-262800.0, [17.0503847980918, -27.1803762901257, -13.6089633216945], [2.8175521080578e-03, 1.1358749093955e-03, -4.9548725258825e-04]],
|
|
[-233600.0, [38.0936719102855, 30.8805883833368, -1.8436880674133], [-1.1317697153459e-03, 1.6128814698472e-03, 8.4177586176055e-04]],
|
|
[-204400.0, [-18.1978529308782, 31.9328699343091, 15.4382948262792], [-1.9117272501813e-03, -1.9146495909842e-03, -1.9657304369835e-05]],
|
|
[-175200.0, [8.5289240399968, -29.6184222000484, -11.8054009942579], [3.1034370787005e-03, 5.1393633292430e-04, -7.7293066202546e-04]],
|
|
[-146000.0, [40.9468572586403, 25.9049735920209, -4.2563362404988], [-8.3652705194051e-04, 1.8129497136404e-03, 8.1564228273060e-04]],
|
|
[-116800.0, [-12.3269588953252, 36.8818834462925, 15.2171582587111], [-2.1166103705038e-03, -1.4814420035990e-03, 1.7401209844705e-04]],
|
|
[-87600.0, [-0.6332583759085, -30.0187597947092, -9.1719328749499], [3.2016994581737e-03, -2.5279858672148e-04, -1.0411088271861e-03]],
|
|
[-58400.0, [42.9360484238833, 20.3446855844523, -6.5880270079125], [-5.0525450073192e-04, 1.9910074335507e-03, 7.7440196540269e-04]],
|
|
[-29200.0, [-5.9759105529742, 40.6118099584602, 14.4701317236731], [-2.2184202156107e-03, -1.0562361130164e-03, 3.3652250216211e-04]],
|
|
[0.0, [-9.8753695807739, -27.9789262247367, -5.7537118247043], [3.0287533248818e-03, -1.1276087003636e-03, -1.2651326732361e-03]],
|
|
[29200.0, [43.9588319861654, 14.2141479732919, -8.8083062271628], [-1.4717608981871e-04, 2.1404187242141e-03, 7.1486567806614e-04]],
|
|
[58400.0, [0.6781367635199, 43.0944616393616, 13.2432387807209], [-2.2358226110718e-03, -6.3233636090933e-04, 4.7664798895648e-04]],
|
|
[87600.0, [-18.2826020968342, -23.3050395866603, -1.7666205080281], [2.5567245263557e-03, -1.9902940754171e-03, -1.3943491701082e-03]],
|
|
[116800.0, [43.8733387445258, 7.7007056172154, -10.8142736664251], [2.3174803055677e-04, 2.2402163127924e-03, 6.2988756452032e-04]],
|
|
[146000.0, [7.3929490279056, 44.3826789515344, 11.6295002148543], [-2.1932815453830e-03, -2.1751799585364e-04, 5.9556516201114e-04]],
|
|
[175200.0, [-24.9816902292606, -16.2040128514258, 2.4664575442976], [1.8193989149580e-03, -2.6765419531201e-03, -1.3848283502247e-03]],
|
|
[204400.0, [42.5301870395114, 0.8459355080215, -12.5549075276829], [6.5059779150669e-04, 2.2725657282262e-03, 5.1133743202822e-04]],
|
|
[233600.0, [13.9995264868224, 44.4623630448942, 9.6694184864653], [-2.1079296569252e-03, 1.7533423831993e-04, 6.9128485798076e-04]],
|
|
[262800.0, [-29.1840248030306, -7.3712439957619, 6.4932759579280], [9.3581363109681e-04, -3.0610357109184e-03, -1.2364201089345e-03]],
|
|
[292000.0, [39.8319806717528, -6.0784057667647, -13.9098153586562], [1.1117769689167e-03, 2.2362097830152e-03, 3.6230548231153e-04]],
|
|
[321200.0, [20.2949551084758, 43.4171904202513, 7.4500919859316], [-1.9742157451535e-03, 5.3102050468554e-04, 7.5938408813008e-04]],
|
|
[350400.0, [-30.6699923021597, 2.3187435589549, 9.9734809138580], [4.5605107450676e-05, -3.1308219926928e-03, -9.9066533301924e-04]],
|
|
[379600.0, [35.6261221559830, -12.8976475092243, -14.7775865084436], [1.6015684949743e-03, 2.1171931182284e-03, 1.8002516202204e-04]],
|
|
[408800.0, [26.1331861485607, 41.2321391875990, 5.0064013262205], [-1.7857704419579e-03, 8.6046232702817e-04, 8.0614690298954e-04]],
|
|
[438000.0, [-29.5767402292299, 11.8635359435865, 12.6313230398719], [-7.2292830060955e-04, -2.9587820140709e-03, -7.0824296450300e-04]],
|
|
[467200.0, [29.9108057873914, -19.1590192939999, -15.0133638651945], [2.0871080437997e-03, 1.8848372554514e-03, -3.8528655083926e-05]],
|
|
[496400.0, [31.3759574518189, 38.0503727207625, 2.4331383437537], [-1.5546055556611e-03, 1.1699815465629e-03, 8.3565439266001e-04]],
|
|
[525600.0, [-26.3600713369277, 20.6625059049518, 14.4146962589584], [-1.3142373118349e-03, -2.6236647854842e-03, -4.2542017598193e-04]],
|
|
[554800.0, [22.5994414886480, -24.5088798983064, -14.4840457314680], [2.5454108304806e-03, 1.4917058755191e-03, -3.0243665086079e-04]],
|
|
[584000.0, [35.8778640130144, 33.8942263660709, -0.2245246362769], [-1.2941245730845e-03, 1.4560427668319e-03, 8.4762160640137e-04]],
|
|
[613200.0, [-21.5381497624167, 28.2040682697607, 15.3219737995345], [-1.7312117409010e-03, -2.1939631314577e-03, -1.6316913275180e-04]],
|
|
[642400.0, [13.9715213744146, -28.3399417647887, -13.0837928718857], [2.9334630526035e-03, 9.1860931752944e-04, -5.9939422488627e-04]],
|
|
[671600.0, [39.5269420441426, 28.9398973601104, -2.8727995275393], [-1.0068481658095e-03, 1.7021132888090e-03, 8.3578230511981e-04]],
|
|
[700800.0, [-15.5762007013935, 34.3994129612753, 15.4660337378540], [-2.0098814612884e-03, -1.7191109825989e-03, 7.0414782780416e-05]],
|
|
[730000.0, [4.2432528370899, -30.1182016908248, -10.7074412313491], [3.1725847067411e-03, 1.6098461202270e-04, -9.0672150593868e-04]]
|
|
];
|
|
class TerseVector {
|
|
constructor(x, y, z) {
|
|
this.x = x;
|
|
this.y = y;
|
|
this.z = z;
|
|
}
|
|
ToAstroVector(t) {
|
|
return new Vector(this.x, this.y, this.z, t);
|
|
}
|
|
quadrature() {
|
|
return this.x * this.x + this.y * this.y + this.z * this.z;
|
|
}
|
|
add(other) {
|
|
return new TerseVector(this.x + other.x, this.y + other.y, this.z + other.z);
|
|
}
|
|
sub(other) {
|
|
return new TerseVector(this.x - other.x, this.y - other.y, this.z - other.z);
|
|
}
|
|
incr(other) {
|
|
this.x += other.x;
|
|
this.y += other.y;
|
|
this.z += other.z;
|
|
}
|
|
decr(other) {
|
|
this.x -= other.x;
|
|
this.y -= other.y;
|
|
this.z -= other.z;
|
|
}
|
|
mul(scalar) {
|
|
return new TerseVector(scalar * this.x, scalar * this.y, scalar * this.z);
|
|
}
|
|
div(scalar) {
|
|
return new TerseVector(this.x / scalar, this.y / scalar, this.z / scalar);
|
|
}
|
|
mean(other) {
|
|
return new TerseVector((this.x + other.x) / 2, (this.y + other.y) / 2, (this.z + other.z) / 2);
|
|
}
|
|
}
|
|
class body_state_t {
|
|
constructor(tt, r, v) {
|
|
this.tt = tt;
|
|
this.r = r;
|
|
this.v = v;
|
|
}
|
|
}
|
|
function BodyStateFromTable(entry) {
|
|
let [tt, [rx, ry, rz], [vx, vy, vz]] = entry;
|
|
return new body_state_t(tt, new TerseVector(rx, ry, rz), new TerseVector(vx, vy, vz));
|
|
}
|
|
function AdjustBarycenterPosVel(ssb, tt, body, planet_gm) {
|
|
const shift = planet_gm / (planet_gm + SUN_GM);
|
|
const planet = CalcVsopPosVel(vsop[body], tt);
|
|
ssb.r.incr(planet.r.mul(shift));
|
|
ssb.v.incr(planet.v.mul(shift));
|
|
return planet;
|
|
}
|
|
function AccelerationIncrement(small_pos, gm, major_pos) {
|
|
const delta = major_pos.sub(small_pos);
|
|
const r2 = delta.quadrature();
|
|
return delta.mul(gm / (r2 * Math.sqrt(r2)));
|
|
}
|
|
class major_bodies_t {
|
|
constructor(tt) {
|
|
// Accumulate the Solar System Barycenter position.
|
|
let ssb = new body_state_t(tt, new TerseVector(0, 0, 0), new TerseVector(0, 0, 0));
|
|
this.Jupiter = AdjustBarycenterPosVel(ssb, tt, Body.Jupiter, JUPITER_GM);
|
|
this.Saturn = AdjustBarycenterPosVel(ssb, tt, Body.Saturn, SATURN_GM);
|
|
this.Uranus = AdjustBarycenterPosVel(ssb, tt, Body.Uranus, URANUS_GM);
|
|
this.Neptune = AdjustBarycenterPosVel(ssb, tt, Body.Neptune, NEPTUNE_GM);
|
|
// Convert planets' [pos, vel] vectors from heliocentric to barycentric.
|
|
this.Jupiter.r.decr(ssb.r);
|
|
this.Jupiter.v.decr(ssb.v);
|
|
this.Saturn.r.decr(ssb.r);
|
|
this.Saturn.v.decr(ssb.v);
|
|
this.Uranus.r.decr(ssb.r);
|
|
this.Uranus.v.decr(ssb.v);
|
|
this.Neptune.r.decr(ssb.r);
|
|
this.Neptune.v.decr(ssb.v);
|
|
// Convert heliocentric SSB to barycentric Sun.
|
|
this.Sun = new body_state_t(tt, ssb.r.mul(-1), ssb.v.mul(-1));
|
|
}
|
|
Acceleration(pos) {
|
|
// Use barycentric coordinates of the Sun and major planets to calculate
|
|
// the gravitational acceleration vector experienced at location 'pos'.
|
|
let acc = AccelerationIncrement(pos, SUN_GM, this.Sun.r);
|
|
acc.incr(AccelerationIncrement(pos, JUPITER_GM, this.Jupiter.r));
|
|
acc.incr(AccelerationIncrement(pos, SATURN_GM, this.Saturn.r));
|
|
acc.incr(AccelerationIncrement(pos, URANUS_GM, this.Uranus.r));
|
|
acc.incr(AccelerationIncrement(pos, NEPTUNE_GM, this.Neptune.r));
|
|
return acc;
|
|
}
|
|
}
|
|
/**
|
|
* @ignore
|
|
*
|
|
* @brief The state of a body at an incremental step in a gravity simulation.
|
|
*
|
|
* This is an internal data structure used to represent the
|
|
* position, velocity, and acceleration vectors of a body
|
|
* in a gravity simulation at a given moment in time.
|
|
*
|
|
* @property tt
|
|
* The J2000 terrestrial time of the state [days].
|
|
*
|
|
* @property r
|
|
* The position vector [au].
|
|
*
|
|
* @property v
|
|
* The velocity vector [au/day].
|
|
*
|
|
* @property a
|
|
* The acceleration vector [au/day^2].
|
|
*/
|
|
class body_grav_calc_t {
|
|
constructor(tt, r, v, a) {
|
|
this.tt = tt;
|
|
this.r = r;
|
|
this.v = v;
|
|
this.a = a;
|
|
}
|
|
}
|
|
class grav_sim_t {
|
|
constructor(bary, grav) {
|
|
this.bary = bary;
|
|
this.grav = grav;
|
|
}
|
|
}
|
|
function UpdatePosition(dt, r, v, a) {
|
|
return new TerseVector(r.x + dt * (v.x + dt * a.x / 2), r.y + dt * (v.y + dt * a.y / 2), r.z + dt * (v.z + dt * a.z / 2));
|
|
}
|
|
function UpdateVelocity(dt, v, a) {
|
|
return new TerseVector(v.x + dt * a.x, v.y + dt * a.y, v.z + dt * a.z);
|
|
}
|
|
function GravSim(tt2, calc1) {
|
|
const dt = tt2 - calc1.tt;
|
|
// Calculate where the major bodies (Sun, Jupiter...Neptune) will be at tt2.
|
|
const bary2 = new major_bodies_t(tt2);
|
|
// Estimate position of small body as if current acceleration applies across the whole time interval.
|
|
const approx_pos = UpdatePosition(dt, calc1.r, calc1.v, calc1.a);
|
|
// Calculate the average acceleration of the endpoints.
|
|
// This becomes our estimate of the mean effective acceleration over the whole interval.
|
|
const mean_acc = bary2.Acceleration(approx_pos).mean(calc1.a);
|
|
// Refine the estimates of [pos, vel, acc] at tt2 using the mean acceleration.
|
|
const pos = UpdatePosition(dt, calc1.r, calc1.v, mean_acc);
|
|
const vel = calc1.v.add(mean_acc.mul(dt));
|
|
const acc = bary2.Acceleration(pos);
|
|
const grav = new body_grav_calc_t(tt2, pos, vel, acc);
|
|
return new grav_sim_t(bary2, grav);
|
|
}
|
|
const pluto_cache = [];
|
|
function ClampIndex(frac, nsteps) {
|
|
const index = Math.floor(frac);
|
|
if (index < 0)
|
|
return 0;
|
|
if (index >= nsteps)
|
|
return nsteps - 1;
|
|
return index;
|
|
}
|
|
function GravFromState(entry) {
|
|
const state = BodyStateFromTable(entry);
|
|
const bary = new major_bodies_t(state.tt);
|
|
const r = state.r.add(bary.Sun.r);
|
|
const v = state.v.add(bary.Sun.v);
|
|
const a = bary.Acceleration(r);
|
|
const grav = new body_grav_calc_t(state.tt, r, v, a);
|
|
return new grav_sim_t(bary, grav);
|
|
}
|
|
function GetSegment(cache, tt) {
|
|
const t0 = PlutoStateTable[0][0];
|
|
if (tt < t0 || tt > PlutoStateTable[PLUTO_NUM_STATES - 1][0]) {
|
|
// Don't bother calculating a segment. Let the caller crawl backward/forward to this time.
|
|
return null;
|
|
}
|
|
const seg_index = ClampIndex((tt - t0) / PLUTO_TIME_STEP, PLUTO_NUM_STATES - 1);
|
|
if (!cache[seg_index]) {
|
|
const seg = cache[seg_index] = [];
|
|
// Each endpoint is exact.
|
|
seg[0] = GravFromState(PlutoStateTable[seg_index]).grav;
|
|
seg[PLUTO_NSTEPS - 1] = GravFromState(PlutoStateTable[seg_index + 1]).grav;
|
|
// Simulate forwards from the lower time bound.
|
|
let i;
|
|
let step_tt = seg[0].tt;
|
|
for (i = 1; i < PLUTO_NSTEPS - 1; ++i)
|
|
seg[i] = GravSim(step_tt += PLUTO_DT, seg[i - 1]).grav;
|
|
// Simulate backwards from the upper time bound.
|
|
step_tt = seg[PLUTO_NSTEPS - 1].tt;
|
|
var reverse = [];
|
|
reverse[PLUTO_NSTEPS - 1] = seg[PLUTO_NSTEPS - 1];
|
|
for (i = PLUTO_NSTEPS - 2; i > 0; --i)
|
|
reverse[i] = GravSim(step_tt -= PLUTO_DT, reverse[i + 1]).grav;
|
|
// Fade-mix the two series so that there are no discontinuities.
|
|
for (i = PLUTO_NSTEPS - 2; i > 0; --i) {
|
|
const ramp = i / (PLUTO_NSTEPS - 1);
|
|
seg[i].r = seg[i].r.mul(1 - ramp).add(reverse[i].r.mul(ramp));
|
|
seg[i].v = seg[i].v.mul(1 - ramp).add(reverse[i].v.mul(ramp));
|
|
seg[i].a = seg[i].a.mul(1 - ramp).add(reverse[i].a.mul(ramp));
|
|
}
|
|
}
|
|
return cache[seg_index];
|
|
}
|
|
function CalcPlutoOneWay(entry, target_tt, dt) {
|
|
let sim = GravFromState(entry);
|
|
const n = Math.ceil((target_tt - sim.grav.tt) / dt);
|
|
for (let i = 0; i < n; ++i)
|
|
sim = GravSim((i + 1 === n) ? target_tt : (sim.grav.tt + dt), sim.grav);
|
|
return sim;
|
|
}
|
|
function CalcPluto(time, helio) {
|
|
let r, v, bary;
|
|
const seg = GetSegment(pluto_cache, time.tt);
|
|
if (!seg) {
|
|
// The target time is outside the year range 0000..4000.
|
|
// Calculate it by crawling backward from 0000 or forward from 4000.
|
|
// FIXFIXFIX - This is super slow. Could optimize this with extra caching if needed.
|
|
let sim;
|
|
if (time.tt < PlutoStateTable[0][0])
|
|
sim = CalcPlutoOneWay(PlutoStateTable[0], time.tt, -PLUTO_DT);
|
|
else
|
|
sim = CalcPlutoOneWay(PlutoStateTable[PLUTO_NUM_STATES - 1], time.tt, +PLUTO_DT);
|
|
r = sim.grav.r;
|
|
v = sim.grav.v;
|
|
bary = sim.bary;
|
|
}
|
|
else {
|
|
const left = ClampIndex((time.tt - seg[0].tt) / PLUTO_DT, PLUTO_NSTEPS - 1);
|
|
const s1 = seg[left];
|
|
const s2 = seg[left + 1];
|
|
// Find mean acceleration vector over the interval.
|
|
const acc = s1.a.mean(s2.a);
|
|
// Use Newtonian mechanics to extrapolate away from t1 in the positive time direction.
|
|
const ra = UpdatePosition(time.tt - s1.tt, s1.r, s1.v, acc);
|
|
const va = UpdateVelocity(time.tt - s1.tt, s1.v, acc);
|
|
// Use Newtonian mechanics to extrapolate away from t2 in the negative time direction.
|
|
const rb = UpdatePosition(time.tt - s2.tt, s2.r, s2.v, acc);
|
|
const vb = UpdateVelocity(time.tt - s2.tt, s2.v, acc);
|
|
// Use fade in/out idea to blend the two position estimates.
|
|
const ramp = (time.tt - s1.tt) / PLUTO_DT;
|
|
r = ra.mul(1 - ramp).add(rb.mul(ramp));
|
|
v = va.mul(1 - ramp).add(vb.mul(ramp));
|
|
}
|
|
if (helio) {
|
|
// Convert barycentric vectors to heliocentric vectors.
|
|
if (!bary)
|
|
bary = new major_bodies_t(time.tt);
|
|
r = r.sub(bary.Sun.r);
|
|
v = v.sub(bary.Sun.v);
|
|
}
|
|
return new StateVector(r.x, r.y, r.z, v.x, v.y, v.z, time);
|
|
}
|
|
;
|
|
const Rotation_JUP_EQJ = new RotationMatrix([
|
|
[9.9943276533865444e-01, -3.3677107469764142e-02, 0.0000000000000000e+00],
|
|
[3.0395942890628476e-02, 9.0205791235280897e-01, 4.3054338854229507e-01],
|
|
[-1.4499455966335291e-02, -4.3029916940910073e-01, 9.0256988127375404e-01]
|
|
]);
|
|
const JupiterMoonModel = [
|
|
// [0] Io
|
|
{
|
|
mu: 2.8248942843381399e-07,
|
|
al: [1.4462132960212239e+00, 3.5515522861824000e+00],
|
|
a: [
|
|
[0.0028210960212903, 0.0000000000000000e+00, 0.0000000000000000e+00]
|
|
],
|
|
l: [
|
|
[-0.0001925258348666, 4.9369589722644998e+00, 1.3584836583050000e-02],
|
|
[-0.0000970803596076, 4.3188796477322002e+00, 1.3034138432430000e-02],
|
|
[-0.0000898817416500, 1.9080016428616999e+00, 3.0506486715799999e-03],
|
|
[-0.0000553101050262, 1.4936156681568999e+00, 1.2938928911549999e-02]
|
|
],
|
|
z: [
|
|
[0.0041510849668155, 4.0899396355450000e+00, -1.2906864146660001e-02],
|
|
[0.0006260521444113, 1.4461888986270000e+00, 3.5515522949801999e+00],
|
|
[0.0000352747346169, 2.1256287034577999e+00, 1.2727416566999999e-04]
|
|
],
|
|
zeta: [
|
|
[0.0003142172466014, 2.7964219722923001e+00, -2.3150960980000000e-03],
|
|
[0.0000904169207946, 1.0477061879627001e+00, -5.6920638196000003e-04]
|
|
]
|
|
},
|
|
// [1] Europa
|
|
{
|
|
mu: 2.8248327439289299e-07,
|
|
al: [-3.7352634374713622e-01, 1.7693227111234699e+00],
|
|
a: [
|
|
[0.0044871037804314, 0.0000000000000000e+00, 0.0000000000000000e+00],
|
|
[0.0000004324367498, 1.8196456062910000e+00, 1.7822295777568000e+00]
|
|
],
|
|
l: [
|
|
[0.0008576433172936, 4.3188693178264002e+00, 1.3034138308049999e-02],
|
|
[0.0004549582875086, 1.4936531751079001e+00, 1.2938928819619999e-02],
|
|
[0.0003248939825174, 1.8196494533458001e+00, 1.7822295777568000e+00],
|
|
[-0.0003074250079334, 4.9377037005910998e+00, 1.3584832867240000e-02],
|
|
[0.0001982386144784, 1.9079869054759999e+00, 3.0510121286900001e-03],
|
|
[0.0001834063551804, 2.1402853388529000e+00, 1.4500978933800000e-03],
|
|
[-0.0001434383188452, 5.6222140366630002e+00, 8.9111478887838003e-01],
|
|
[-0.0000771939140944, 4.3002724372349999e+00, 2.6733443704265998e+00]
|
|
],
|
|
z: [
|
|
[-0.0093589104136341, 4.0899396509038999e+00, -1.2906864146660001e-02],
|
|
[0.0002988994545555, 5.9097265185595003e+00, 1.7693227079461999e+00],
|
|
[0.0002139036390350, 2.1256289300016000e+00, 1.2727418406999999e-04],
|
|
[0.0001980963564781, 2.7435168292649998e+00, 6.7797343008999997e-04],
|
|
[0.0001210388158965, 5.5839943711203004e+00, 3.2056614899999997e-05],
|
|
[0.0000837042048393, 1.6094538368039000e+00, -9.0402165808846002e-01],
|
|
[0.0000823525166369, 1.4461887708689001e+00, 3.5515522949801999e+00]
|
|
],
|
|
zeta: [
|
|
[0.0040404917832303, 1.0477063169425000e+00, -5.6920640539999997e-04],
|
|
[0.0002200421034564, 3.3368857864364001e+00, -1.2491307306999999e-04],
|
|
[0.0001662544744719, 2.4134862374710999e+00, 0.0000000000000000e+00],
|
|
[0.0000590282470983, 5.9719930968366004e+00, -3.0561602250000000e-05]
|
|
]
|
|
},
|
|
// [2] Ganymede
|
|
{
|
|
mu: 2.8249818418472298e-07,
|
|
al: [2.8740893911433479e-01, 8.7820792358932798e-01],
|
|
a: [
|
|
[0.0071566594572575, 0.0000000000000000e+00, 0.0000000000000000e+00],
|
|
[0.0000013930299110, 1.1586745884981000e+00, 2.6733443704265998e+00]
|
|
],
|
|
l: [
|
|
[0.0002310797886226, 2.1402987195941998e+00, 1.4500978438400001e-03],
|
|
[-0.0001828635964118, 4.3188672736968003e+00, 1.3034138282630000e-02],
|
|
[0.0001512378778204, 4.9373102372298003e+00, 1.3584834812520000e-02],
|
|
[-0.0001163720969778, 4.3002659861490002e+00, 2.6733443704265998e+00],
|
|
[-0.0000955478069846, 1.4936612842567001e+00, 1.2938928798570001e-02],
|
|
[0.0000815246854464, 5.6222137132535002e+00, 8.9111478887838003e-01],
|
|
[-0.0000801219679602, 1.2995922951532000e+00, 1.0034433456728999e+00],
|
|
[-0.0000607017260182, 6.4978769669238001e-01, 5.0172167043264004e-01]
|
|
],
|
|
z: [
|
|
[0.0014289811307319, 2.1256295942738999e+00, 1.2727413029000001e-04],
|
|
[0.0007710931226760, 5.5836330003496002e+00, 3.2064341100000001e-05],
|
|
[0.0005925911780766, 4.0899396636447998e+00, -1.2906864146660001e-02],
|
|
[0.0002045597496146, 5.2713683670371996e+00, -1.2523544076106000e-01],
|
|
[0.0001785118648258, 2.8743156721063001e-01, 8.7820792442520001e-01],
|
|
[0.0001131999784893, 1.4462127277818000e+00, 3.5515522949801999e+00],
|
|
[-0.0000658778169210, 2.2702423990985001e+00, -1.7951364394536999e+00],
|
|
[0.0000497058888328, 5.9096792204858000e+00, 1.7693227129285001e+00]
|
|
],
|
|
zeta: [
|
|
[0.0015932721570848, 3.3368862796665000e+00, -1.2491307058000000e-04],
|
|
[0.0008533093128905, 2.4133881688166001e+00, 0.0000000000000000e+00],
|
|
[0.0003513347911037, 5.9720789850126996e+00, -3.0561017709999999e-05],
|
|
[-0.0001441929255483, 1.0477061764435001e+00, -5.6920632124000004e-04]
|
|
]
|
|
},
|
|
// [3] Callisto
|
|
{
|
|
mu: 2.8249214488990899e-07,
|
|
al: [-3.6203412913757038e-01, 3.7648623343382798e-01],
|
|
a: [
|
|
[0.0125879701715314, 0.0000000000000000e+00, 0.0000000000000000e+00],
|
|
[0.0000035952049470, 6.4965776007116005e-01, 5.0172168165034003e-01],
|
|
[0.0000027580210652, 1.8084235781510001e+00, 3.1750660413359002e+00]
|
|
],
|
|
l: [
|
|
[0.0005586040123824, 2.1404207189814999e+00, 1.4500979323100001e-03],
|
|
[-0.0003805813868176, 2.7358844897852999e+00, 2.9729650620000000e-05],
|
|
[0.0002205152863262, 6.4979652596399995e-01, 5.0172167243580001e-01],
|
|
[0.0001877895151158, 1.8084787604004999e+00, 3.1750660413359002e+00],
|
|
[0.0000766916975242, 6.2720114319754998e+00, 1.3928364636651001e+00],
|
|
[0.0000747056855106, 1.2995916202344000e+00, 1.0034433456728999e+00]
|
|
],
|
|
z: [
|
|
[0.0073755808467977, 5.5836071576083999e+00, 3.2065099140000001e-05],
|
|
[0.0002065924169942, 5.9209831565786004e+00, 3.7648624194703001e-01],
|
|
[0.0001589869764021, 2.8744006242622999e-01, 8.7820792442520001e-01],
|
|
[-0.0001561131605348, 2.1257397865089001e+00, 1.2727441285000001e-04],
|
|
[0.0001486043380971, 1.4462134301023000e+00, 3.5515522949801999e+00],
|
|
[0.0000635073108731, 5.9096803285953996e+00, 1.7693227129285001e+00],
|
|
[0.0000599351698525, 4.1125517584797997e+00, -2.7985797954588998e+00],
|
|
[0.0000540660842731, 5.5390350845569003e+00, 2.8683408228299999e-03],
|
|
[-0.0000489596900866, 4.6218149483337996e+00, -6.2695712529518999e-01]
|
|
],
|
|
zeta: [
|
|
[0.0038422977898495, 2.4133922085556998e+00, 0.0000000000000000e+00],
|
|
[0.0022453891791894, 5.9721736773277003e+00, -3.0561255249999997e-05],
|
|
[-0.0002604479450559, 3.3368746306408998e+00, -1.2491309972000001e-04],
|
|
[0.0000332112143230, 5.5604137742336999e+00, 2.9003768850700000e-03]
|
|
]
|
|
}
|
|
];
|
|
/**
|
|
* @brief Holds the positions and velocities of Jupiter's major 4 moons.
|
|
*
|
|
* The {@link JupiterMoons} function returns an object of this type
|
|
* to report position and velocity vectors for Jupiter's largest 4 moons
|
|
* Io, Europa, Ganymede, and Callisto. Each position vector is relative
|
|
* to the center of Jupiter. Both position and velocity are oriented in
|
|
* the EQJ system (that is, using Earth's equator at the J2000 epoch).
|
|
* The positions are expressed in astronomical units (AU),
|
|
* and the velocities in AU/day.
|
|
*
|
|
* @property {StateVector[]} moon
|
|
* An array of state vectors, one for each of the four major moons
|
|
* of Jupiter, in the following order: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.
|
|
*/
|
|
class JupiterMoonsInfo {
|
|
constructor(moon) {
|
|
this.moon = moon;
|
|
}
|
|
}
|
|
exports.JupiterMoonsInfo = JupiterMoonsInfo;
|
|
function JupiterMoon_elem2pv(time, mu, elem) {
|
|
// Translation of FORTRAN subroutine ELEM2PV from:
|
|
// https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/
|
|
const A = elem[0];
|
|
const AL = elem[1];
|
|
const K = elem[2];
|
|
const H = elem[3];
|
|
const Q = elem[4];
|
|
const P = elem[5];
|
|
const AN = Math.sqrt(mu / (A * A * A));
|
|
let CE, SE, DE;
|
|
let EE = AL + K * Math.sin(AL) - H * Math.cos(AL);
|
|
do {
|
|
CE = Math.cos(EE);
|
|
SE = Math.sin(EE);
|
|
DE = (AL - EE + K * SE - H * CE) / (1.0 - K * CE - H * SE);
|
|
EE += DE;
|
|
} while (Math.abs(DE) >= 1.0e-12);
|
|
CE = Math.cos(EE);
|
|
SE = Math.sin(EE);
|
|
const DLE = H * CE - K * SE;
|
|
const RSAM1 = -K * CE - H * SE;
|
|
const ASR = 1.0 / (1.0 + RSAM1);
|
|
const PHI = Math.sqrt(1.0 - K * K - H * H);
|
|
const PSI = 1.0 / (1.0 + PHI);
|
|
const X1 = A * (CE - K - PSI * H * DLE);
|
|
const Y1 = A * (SE - H + PSI * K * DLE);
|
|
const VX1 = AN * ASR * A * (-SE - PSI * H * RSAM1);
|
|
const VY1 = AN * ASR * A * (+CE + PSI * K * RSAM1);
|
|
const F2 = 2.0 * Math.sqrt(1.0 - Q * Q - P * P);
|
|
const P2 = 1.0 - 2.0 * P * P;
|
|
const Q2 = 1.0 - 2.0 * Q * Q;
|
|
const PQ = 2.0 * P * Q;
|
|
return new StateVector(X1 * P2 + Y1 * PQ, X1 * PQ + Y1 * Q2, (Q * Y1 - X1 * P) * F2, VX1 * P2 + VY1 * PQ, VX1 * PQ + VY1 * Q2, (Q * VY1 - VX1 * P) * F2, time);
|
|
}
|
|
function CalcJupiterMoon(time, m) {
|
|
// This is a translation of FORTRAN code by Duriez, Lainey, and Vienne:
|
|
// https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/
|
|
const t = time.tt + 18262.5; // number of days since 1950-01-01T00:00:00Z
|
|
// Calculate 6 orbital elements at the given time t
|
|
const elem = [0, m.al[0] + (t * m.al[1]), 0, 0, 0, 0];
|
|
for (let [amplitude, phase, frequency] of m.a)
|
|
elem[0] += amplitude * Math.cos(phase + (t * frequency));
|
|
for (let [amplitude, phase, frequency] of m.l)
|
|
elem[1] += amplitude * Math.sin(phase + (t * frequency));
|
|
elem[1] %= PI2;
|
|
if (elem[1] < 0)
|
|
elem[1] += PI2;
|
|
for (let [amplitude, phase, frequency] of m.z) {
|
|
const arg = phase + (t * frequency);
|
|
elem[2] += amplitude * Math.cos(arg);
|
|
elem[3] += amplitude * Math.sin(arg);
|
|
}
|
|
for (let [amplitude, phase, frequency] of m.zeta) {
|
|
const arg = phase + (t * frequency);
|
|
elem[4] += amplitude * Math.cos(arg);
|
|
elem[5] += amplitude * Math.sin(arg);
|
|
}
|
|
// Convert the oribital elements into position vectors in the Jupiter equatorial system (JUP).
|
|
const state = JupiterMoon_elem2pv(time, m.mu, elem);
|
|
// Re-orient position and velocity vectors from Jupiter-equatorial (JUP) to Earth-equatorial in J2000 (EQJ).
|
|
return RotateState(Rotation_JUP_EQJ, state);
|
|
}
|
|
/**
|
|
* @brief Calculates jovicentric positions and velocities of Jupiter's largest 4 moons.
|
|
*
|
|
* Calculates position and velocity vectors for Jupiter's moons
|
|
* Io, Europa, Ganymede, and Callisto, at the given date and time.
|
|
* The vectors are jovicentric (relative to the center of Jupiter).
|
|
* Their orientation is the Earth's equatorial system at the J2000 epoch (EQJ).
|
|
* The position components are expressed in astronomical units (AU), and the
|
|
* velocity components are in AU/day.
|
|
*
|
|
* To convert to heliocentric vectors, call {@link HelioVector}
|
|
* with `Astronomy.Body.Jupiter` to get Jupiter's heliocentric position, then
|
|
* add the jovicentric vectors. Likewise, you can call {@link GeoVector}
|
|
* to convert to geocentric vectors.
|
|
*
|
|
* @param {FlexibleDateTime} date The date and time for which to calculate Jupiter's moons.
|
|
* @return {JupiterMoonsInfo} Position and velocity vectors of Jupiter's largest 4 moons.
|
|
*/
|
|
function JupiterMoons(date) {
|
|
const time = new AstroTime(date);
|
|
let infolist = [];
|
|
for (let moon of JupiterMoonModel)
|
|
infolist.push(CalcJupiterMoon(time, moon));
|
|
return new JupiterMoonsInfo(infolist);
|
|
}
|
|
exports.JupiterMoons = JupiterMoons;
|
|
// Jupiter Moons ends --------------------------------------------------------
|
|
/**
|
|
* @brief Calculates a vector from the center of the Sun to the given body at the given time.
|
|
*
|
|
* Calculates heliocentric (i.e., with respect to the center of the Sun)
|
|
* Cartesian coordinates in the J2000 equatorial system of a celestial
|
|
* body at a specified time. The position is not corrected for light travel time or aberration.
|
|
*
|
|
* @param {Body} body
|
|
* One of the strings
|
|
* `"Sun"`, `"Moon"`, `"Mercury"`, `"Venus"`,
|
|
* `"Earth"`, `"Mars"`, `"Jupiter"`, `"Saturn"`,
|
|
* `"Uranus"`, `"Neptune"`, `"Pluto"`,
|
|
* `"SSB"`, or `"EMB"`.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which the body's position is to be calculated.
|
|
*
|
|
* @returns {Vector}
|
|
*/
|
|
function HelioVector(body, date) {
|
|
var time = MakeTime(date);
|
|
if (body in vsop)
|
|
return CalcVsop(vsop[body], time);
|
|
if (body === Body.Pluto) {
|
|
const p = CalcPluto(time, true);
|
|
return new Vector(p.x, p.y, p.z, time);
|
|
}
|
|
if (body === Body.Sun)
|
|
return new Vector(0, 0, 0, time);
|
|
if (body === Body.Moon) {
|
|
var e = CalcVsop(vsop.Earth, time);
|
|
var m = GeoMoon(time);
|
|
return new Vector(e.x + m.x, e.y + m.y, e.z + m.z, time);
|
|
}
|
|
if (body === Body.EMB) {
|
|
const e = CalcVsop(vsop.Earth, time);
|
|
const m = GeoMoon(time);
|
|
const denom = 1.0 + EARTH_MOON_MASS_RATIO;
|
|
return new Vector(e.x + (m.x / denom), e.y + (m.y / denom), e.z + (m.z / denom), time);
|
|
}
|
|
if (body === Body.SSB)
|
|
return CalcSolarSystemBarycenter(time);
|
|
throw `HelioVector: Unknown body "${body}"`;
|
|
}
|
|
exports.HelioVector = HelioVector;
|
|
;
|
|
/**
|
|
* @brief Calculates the distance between a body and the Sun at a given time.
|
|
*
|
|
* Given a date and time, this function calculates the distance between
|
|
* the center of `body` and the center of the Sun.
|
|
* For the planets Mercury through Neptune, this function is significantly
|
|
* more efficient than calling {@link HelioVector} followed by taking the length
|
|
* of the resulting vector.
|
|
*
|
|
* @param {Body} body
|
|
* A body for which to calculate a heliocentric distance:
|
|
* the Sun, Moon, or any of the planets.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the heliocentric distance.
|
|
*
|
|
* @returns {number}
|
|
* The heliocentric distance in AU.
|
|
*/
|
|
function HelioDistance(body, date) {
|
|
const time = MakeTime(date);
|
|
if (body in vsop)
|
|
return VsopFormula(vsop[body][RAD_INDEX], time.tt / DAYS_PER_MILLENNIUM, false);
|
|
return HelioVector(body, time).Length();
|
|
}
|
|
exports.HelioDistance = HelioDistance;
|
|
/**
|
|
* @brief Calculates a vector from the center of the Earth to the given body at the given time.
|
|
*
|
|
* Calculates geocentric (i.e., with respect to the center of the Earth)
|
|
* Cartesian coordinates in the J2000 equatorial system of a celestial
|
|
* body at a specified time. The position is always corrected for light travel time:
|
|
* this means the position of the body is "back-dated" based on how long it
|
|
* takes light to travel from the body to an observer on the Earth.
|
|
* Also, the position can optionally be corrected for aberration, an effect
|
|
* causing the apparent direction of the body to be shifted based on
|
|
* transverse movement of the Earth with respect to the rays of light
|
|
* coming from that body.
|
|
*
|
|
* @param {Body} body
|
|
* One of the strings
|
|
* `"Sun"`, `"Moon"`, `"Mercury"`, `"Venus"`,
|
|
* `"Earth"`, `"Mars"`, `"Jupiter"`, `"Saturn"`,
|
|
* `"Uranus"`, `"Neptune"`, or `"Pluto"`.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which the body's position is to be calculated.
|
|
*
|
|
* @param {bool} aberration
|
|
* Pass `true` to correct for
|
|
* <a href="https://en.wikipedia.org/wiki/Aberration_of_light">aberration</a>,
|
|
* or `false` to leave uncorrected.
|
|
*
|
|
* @returns {Vector}
|
|
*/
|
|
function GeoVector(body, date, aberration) {
|
|
VerifyBoolean(aberration);
|
|
const time = MakeTime(date);
|
|
if (body === Body.Moon)
|
|
return GeoMoon(time);
|
|
if (body === Body.Earth)
|
|
return new Vector(0, 0, 0, time);
|
|
let earth = null;
|
|
let h;
|
|
let geo;
|
|
let dt = 0;
|
|
let ltime = time;
|
|
// Correct for light-travel time, to get position of body as seen from Earth's center.
|
|
for (let iter = 0; iter < 10; ++iter) {
|
|
h = HelioVector(body, ltime);
|
|
if (aberration) {
|
|
/*
|
|
Include aberration, so make a good first-order approximation
|
|
by backdating the Earth's position also.
|
|
This is confusing, but it works for objects within the Solar System
|
|
because the distance the Earth moves in that small amount of light
|
|
travel time (a few minutes to a few hours) is well approximated
|
|
by a line segment that substends the angle seen from the remote
|
|
body viewing Earth. That angle is pretty close to the aberration
|
|
angle of the moving Earth viewing the remote body.
|
|
In other words, both of the following approximate the aberration angle:
|
|
(transverse distance Earth moves) / (distance to body)
|
|
(transverse speed of Earth) / (speed of light).
|
|
*/
|
|
earth = CalcVsop(vsop.Earth, ltime);
|
|
}
|
|
else {
|
|
if (!earth) {
|
|
// No aberration, so calculate Earth's position once, at the time of observation.
|
|
earth = CalcVsop(vsop.Earth, time);
|
|
}
|
|
}
|
|
geo = new Vector(h.x - earth.x, h.y - earth.y, h.z - earth.z, time);
|
|
let ltime2 = time.AddDays(-geo.Length() / exports.C_AUDAY);
|
|
dt = Math.abs(ltime2.tt - ltime.tt);
|
|
if (dt < 1.0e-9)
|
|
return geo;
|
|
ltime = ltime2;
|
|
}
|
|
throw `Light-travel time solver did not converge: dt=${dt}`;
|
|
}
|
|
exports.GeoVector = GeoVector;
|
|
function ExportState(terse, time) {
|
|
return new StateVector(terse.r.x, terse.r.y, terse.r.z, terse.v.x, terse.v.y, terse.v.z, time);
|
|
}
|
|
/**
|
|
* @brief Calculates barycentric position and velocity vectors for the given body.
|
|
*
|
|
* Given a body and a time, calculates the barycentric position and velocity
|
|
* vectors for the center of that body at that time.
|
|
* The vectors are expressed in equatorial J2000 coordinates (EQJ).
|
|
*
|
|
* @param {Body} body
|
|
* The celestial body whose barycentric state vector is to be calculated.
|
|
* Supported values are `Body.Sun`, `Body.Moon`, `Body.EMB`, `Body.SSB`, and all planets:
|
|
* `Body.Mercury`, `Body.Venus`, `Body.Earth`, `Body.Mars`, `Body.Jupiter`,
|
|
* `Body.Saturn`, `Body.Uranus`, `Body.Neptune`, `Body.Pluto`.
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate position and velocity.
|
|
* @returns {StateVector}
|
|
* An object that contains barycentric position and velocity vectors.
|
|
*/
|
|
function BaryState(body, date) {
|
|
const time = MakeTime(date);
|
|
if (body === Body.SSB) {
|
|
// Trivial case: the solar system barycenter itself.
|
|
return new StateVector(0, 0, 0, 0, 0, 0, time);
|
|
}
|
|
if (body === Body.Pluto) {
|
|
return CalcPluto(time, false);
|
|
}
|
|
// Find the barycentric positions and velocities for the 5 major bodies:
|
|
// Sun, Jupiter, Saturn, Uranus, Neptune.
|
|
const bary = new major_bodies_t(time.tt);
|
|
switch (body) {
|
|
case Body.Sun: return ExportState(bary.Sun, time);
|
|
case Body.Jupiter: return ExportState(bary.Jupiter, time);
|
|
case Body.Saturn: return ExportState(bary.Saturn, time);
|
|
case Body.Uranus: return ExportState(bary.Uranus, time);
|
|
case Body.Neptune: return ExportState(bary.Neptune, time);
|
|
case Body.Moon:
|
|
case Body.EMB:
|
|
const earth = CalcVsopPosVel(vsop[Body.Earth], time.tt);
|
|
const state = (body === Body.Moon) ? GeoMoonState(time) : GeoEmbState(time);
|
|
return new StateVector(state.x + bary.Sun.r.x + earth.r.x, state.y + bary.Sun.r.y + earth.r.y, state.z + bary.Sun.r.z + earth.r.z, state.vx + bary.Sun.v.x + earth.v.x, state.vy + bary.Sun.v.y + earth.v.y, state.vz + bary.Sun.v.z + earth.v.z, time);
|
|
}
|
|
// Handle the remaining VSOP bodies: Mercury, Venus, Earth, Mars.
|
|
if (body in vsop) {
|
|
const planet = CalcVsopPosVel(vsop[body], time.tt);
|
|
return new StateVector(bary.Sun.r.x + planet.r.x, bary.Sun.r.y + planet.r.y, bary.Sun.r.z + planet.r.z, bary.Sun.v.x + planet.v.x, bary.Sun.v.y + planet.v.y, bary.Sun.v.z + planet.v.z, time);
|
|
}
|
|
throw `BaryState: Unsupported body "${body}"`;
|
|
}
|
|
exports.BaryState = BaryState;
|
|
/**
|
|
* @brief Calculates heliocentric position and velocity vectors for the given body.
|
|
*
|
|
* Given a body and a time, calculates the position and velocity
|
|
* vectors for the center of that body at that time, relative to the center of the Sun.
|
|
* The vectors are expressed in equatorial J2000 coordinates (EQJ).
|
|
* If you need the position vector only, it is more efficient to call {@link HelioVector}.
|
|
* The Sun's center is a non-inertial frame of reference. In other words, the Sun
|
|
* experiences acceleration due to gravitational forces, mostly from the larger
|
|
* planets (Jupiter, Saturn, Uranus, and Neptune). If you want to calculate momentum,
|
|
* kinetic energy, or other quantities that require a non-accelerating frame
|
|
* of reference, consider using {@link BaryState} instead.
|
|
*
|
|
* @param {Body} body
|
|
* The celestial body whose heliocentric state vector is to be calculated.
|
|
* Supported values are `Body.Sun`, `Body.Moon`, `Body.EMB`, `Body.SSB`, and all planets:
|
|
* `Body.Mercury`, `Body.Venus`, `Body.Earth`, `Body.Mars`, `Body.Jupiter`,
|
|
* `Body.Saturn`, `Body.Uranus`, `Body.Neptune`, `Body.Pluto`.
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate position and velocity.
|
|
* @returns {StateVector}
|
|
* An object that contains heliocentric position and velocity vectors.
|
|
*/
|
|
function HelioState(body, date) {
|
|
const time = MakeTime(date);
|
|
switch (body) {
|
|
case Body.Sun:
|
|
// Trivial case: the Sun is the origin of the heliocentric frame.
|
|
return new StateVector(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, time);
|
|
case Body.SSB:
|
|
// Calculate the barycentric Sun. Then the negative of that is the heliocentric SSB.
|
|
const bary = new major_bodies_t(time.tt);
|
|
return new StateVector(-bary.Sun.r.x, -bary.Sun.r.y, -bary.Sun.r.z, -bary.Sun.v.x, -bary.Sun.v.y, -bary.Sun.v.z, time);
|
|
case Body.Mercury:
|
|
case Body.Venus:
|
|
case Body.Earth:
|
|
case Body.Mars:
|
|
case Body.Jupiter:
|
|
case Body.Saturn:
|
|
case Body.Uranus:
|
|
case Body.Neptune:
|
|
// Planets included in the VSOP87 model.
|
|
const planet = CalcVsopPosVel(vsop[body], time.tt);
|
|
return ExportState(planet, time);
|
|
case Body.Pluto:
|
|
return CalcPluto(time, true);
|
|
case Body.Moon:
|
|
case Body.EMB:
|
|
const earth = CalcVsopPosVel(vsop.Earth, time.tt);
|
|
const state = (body == Body.Moon) ? GeoMoonState(time) : GeoEmbState(time);
|
|
return new StateVector(state.x + earth.r.x, state.y + earth.r.y, state.z + earth.r.z, state.vx + earth.v.x, state.vy + earth.v.y, state.vz + earth.v.z, time);
|
|
default:
|
|
throw `HelioState: Unsupported body "${body}"`;
|
|
}
|
|
}
|
|
exports.HelioState = HelioState;
|
|
function QuadInterp(tm, dt, fa, fm, fb) {
|
|
let Q = (fb + fa) / 2 - fm;
|
|
let R = (fb - fa) / 2;
|
|
let S = fm;
|
|
let x;
|
|
if (Q == 0) {
|
|
// This is a line, not a parabola.
|
|
if (R == 0) {
|
|
// This is a HORIZONTAL line... can't make progress!
|
|
return null;
|
|
}
|
|
x = -S / R;
|
|
if (x < -1 || x > +1)
|
|
return null; // out of bounds
|
|
}
|
|
else {
|
|
// It really is a parabola. Find roots x1, x2.
|
|
let u = R * R - 4 * Q * S;
|
|
if (u <= 0)
|
|
return null;
|
|
let ru = Math.sqrt(u);
|
|
let x1 = (-R + ru) / (2 * Q);
|
|
let x2 = (-R - ru) / (2 * Q);
|
|
if (-1 <= x1 && x1 <= +1) {
|
|
if (-1 <= x2 && x2 <= +1)
|
|
return null;
|
|
x = x1;
|
|
}
|
|
else if (-1 <= x2 && x2 <= +1) {
|
|
x = x2;
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
let t = tm + x * dt;
|
|
let df_dt = (2 * Q * x + R) / dt;
|
|
return { x: x, t: t, df_dt: df_dt };
|
|
}
|
|
/**
|
|
* @brief Options for the {@link Search} function.
|
|
*
|
|
* @typedef {object} SearchOptions
|
|
*
|
|
* @property {number | undefined} dt_tolerance_seconds
|
|
* The number of seconds for a time window smaller than which the search
|
|
* is considered successful. Using too large a tolerance can result in
|
|
* an inaccurate time estimate. Using too small a tolerance can cause
|
|
* excessive computation, or can even cause the search to fail because of
|
|
* limited floating-point resolution. Defaults to 1 second.
|
|
*
|
|
* @property {number | undefined} init_f1
|
|
* As an optimization, if the caller of {@link Search}
|
|
* has already calculated the value of the function being searched (the parameter `func`)
|
|
* at the time coordinate `t1`, it can pass in that value as `init_f1`.
|
|
* For very expensive calculations, this can measurably improve performance.
|
|
*
|
|
* @property {number | undefined} init_f2
|
|
* The same as `init_f1`, except this is the optional initial value of `func(t2)`
|
|
* instead of `func(t1)`.
|
|
*
|
|
* @property {number | undefined} iter_limit
|
|
*/
|
|
/**
|
|
* @brief Finds the time when a function ascends through zero.
|
|
*
|
|
* Search for next time <i>t</i> (such that <i>t</i> is between `t1` and `t2`)
|
|
* that `func(t)` crosses from a negative value to a non-negative value.
|
|
* The given function must have "smooth" behavior over the entire inclusive range [`t1`, `t2`],
|
|
* meaning that it behaves like a continuous differentiable function.
|
|
* It is not required that `t1` < `t2`; `t1` > `t2`
|
|
* allows searching backward in time.
|
|
* Note: `t1` and `t2` must be chosen such that there is no possibility
|
|
* of more than one zero-crossing (ascending or descending), or it is possible
|
|
* that the "wrong" event will be found (i.e. not the first event after t1)
|
|
* or even that the function will return `null`, indicating that no event was found.
|
|
*
|
|
* @param {function(AstroTime): number} func
|
|
* The function to find an ascending zero crossing for.
|
|
* The function must accept a single parameter of type {@link AstroTime}
|
|
* and return a numeric value.
|
|
*
|
|
* @param {AstroTime} t1
|
|
* The lower time bound of a search window.
|
|
*
|
|
* @param {AstroTime} t2
|
|
* The upper time bound of a search window.
|
|
*
|
|
* @param {SearchOptions | undefined} options
|
|
* Options that can tune the behavior of the search.
|
|
* Most callers can omit this argument.
|
|
*
|
|
* @returns {AstroTime | null}
|
|
* If the search is successful, returns the date and time of the solution.
|
|
* If the search fails, returns `null`.
|
|
*/
|
|
function Search(f, t1, t2, options) {
|
|
const dt_tolerance_seconds = VerifyNumber((options && options.dt_tolerance_seconds) || 1);
|
|
const dt_days = Math.abs(dt_tolerance_seconds / SECONDS_PER_DAY);
|
|
let f1 = (options && options.init_f1) || f(t1);
|
|
let f2 = (options && options.init_f2) || f(t2);
|
|
let fmid = NaN;
|
|
let iter = 0;
|
|
let iter_limit = (options && options.iter_limit) || 20;
|
|
let calc_fmid = true;
|
|
while (true) {
|
|
if (++iter > iter_limit)
|
|
throw `Excessive iteration in Search()`;
|
|
let tmid = InterpolateTime(t1, t2, 0.5);
|
|
let dt = tmid.ut - t1.ut;
|
|
if (Math.abs(dt) < dt_days) {
|
|
// We are close enough to the event to stop the search.
|
|
return tmid;
|
|
}
|
|
if (calc_fmid)
|
|
fmid = f(tmid);
|
|
else
|
|
calc_fmid = true; // we already have the correct value of fmid from the previous loop
|
|
// Quadratic interpolation:
|
|
// Try to find a parabola that passes through the 3 points we have sampled:
|
|
// (t1,f1), (tmid,fmid), (t2,f2).
|
|
let q = QuadInterp(tmid.ut, t2.ut - tmid.ut, f1, fmid, f2);
|
|
// Did we find an approximate root-crossing?
|
|
if (q) {
|
|
// Evaluate the function at our candidate solution.
|
|
let tq = MakeTime(q.t);
|
|
let fq = f(tq);
|
|
if (q.df_dt !== 0) {
|
|
if (Math.abs(fq / q.df_dt) < dt_days) {
|
|
// The estimated time error is small enough that we can quit now.
|
|
return tq;
|
|
}
|
|
// Try guessing a tighter boundary with the interpolated root at the center.
|
|
let dt_guess = 1.2 * Math.abs(fq / q.df_dt);
|
|
if (dt_guess < dt / 10) {
|
|
let tleft = tq.AddDays(-dt_guess);
|
|
let tright = tq.AddDays(+dt_guess);
|
|
if ((tleft.ut - t1.ut) * (tleft.ut - t2.ut) < 0) {
|
|
if ((tright.ut - t1.ut) * (tright.ut - t2.ut) < 0) {
|
|
let fleft = f(tleft);
|
|
let fright = f(tright);
|
|
if (fleft < 0 && fright >= 0) {
|
|
f1 = fleft;
|
|
f2 = fright;
|
|
t1 = tleft;
|
|
t2 = tright;
|
|
fmid = fq;
|
|
calc_fmid = false;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (f1 < 0 && fmid >= 0) {
|
|
t2 = tmid;
|
|
f2 = fmid;
|
|
continue;
|
|
}
|
|
if (fmid < 0 && f2 >= 0) {
|
|
t1 = tmid;
|
|
f1 = fmid;
|
|
continue;
|
|
}
|
|
// Either there is no ascending zero-crossing in this range
|
|
// or the search window is too wide.
|
|
return null;
|
|
}
|
|
}
|
|
exports.Search = Search;
|
|
function LongitudeOffset(diff) {
|
|
let offset = diff;
|
|
while (offset <= -180)
|
|
offset += 360;
|
|
while (offset > 180)
|
|
offset -= 360;
|
|
return offset;
|
|
}
|
|
function NormalizeLongitude(lon) {
|
|
while (lon < 0)
|
|
lon += 360;
|
|
while (lon >= 360)
|
|
lon -= 360;
|
|
return lon;
|
|
}
|
|
/**
|
|
* @brief Searches for when the Sun reaches a given ecliptic longitude.
|
|
*
|
|
* Searches for the moment in time when the center of the Sun reaches a given apparent
|
|
* ecliptic longitude, as seen from the center of the Earth, within a given range of dates.
|
|
* This function can be used to determine equinoxes and solstices.
|
|
* However, it is usually more convenient and efficient to call {@link Seasons}
|
|
* to calculate equinoxes and solstices for a given calendar year.
|
|
* `SearchSunLongitude` is more general in that it allows searching for arbitrary longitude values.
|
|
*
|
|
* @param {number} targetLon
|
|
* The desired ecliptic longitude of date in degrees.
|
|
* This may be any value in the range [0, 360), although certain
|
|
* values have conventional meanings:
|
|
*
|
|
* When `targetLon` is 0, finds the March equinox,
|
|
* which is the moment spring begins in the northern hemisphere
|
|
* and the beginning of autumn in the southern hemisphere.
|
|
*
|
|
* When `targetLon` is 180, finds the September equinox,
|
|
* which is the moment autumn begins in the northern hemisphere and
|
|
* spring begins in the southern hemisphere.
|
|
*
|
|
* When `targetLon` is 90, finds the northern solstice, which is the
|
|
* moment summer begins in the northern hemisphere and winter
|
|
* begins in the southern hemisphere.
|
|
*
|
|
* When `targetLon` is 270, finds the southern solstice, which is the
|
|
* moment winter begins in the northern hemisphere and summer
|
|
* begins in the southern hemisphere.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* A date and time known to be earlier than the desired longitude event.
|
|
*
|
|
* @param {number} limitDays
|
|
* A floating point number of days, which when added to `dateStart`,
|
|
* yields a date and time known to be after the desired longitude event.
|
|
*
|
|
* @returns {AstroTime | null}
|
|
* The date and time when the Sun reaches the apparent ecliptic longitude `targetLon`
|
|
* within the range of times specified by `dateStart` and `limitDays`.
|
|
* If the Sun does not reach the target longitude within the specified time range, or the
|
|
* time range is excessively wide, the return value is `null`.
|
|
* To avoid a `null` return value, the caller must pick a time window around
|
|
* the event that is within a few days but not so small that the event might fall outside the window.
|
|
*/
|
|
function SearchSunLongitude(targetLon, dateStart, limitDays) {
|
|
function sun_offset(t) {
|
|
let pos = SunPosition(t);
|
|
return LongitudeOffset(pos.elon - targetLon);
|
|
}
|
|
VerifyNumber(targetLon);
|
|
VerifyNumber(limitDays);
|
|
let t1 = MakeTime(dateStart);
|
|
let t2 = t1.AddDays(limitDays);
|
|
return Search(sun_offset, t1, t2);
|
|
}
|
|
exports.SearchSunLongitude = SearchSunLongitude;
|
|
/**
|
|
* @brief Returns one body's ecliptic longitude with respect to another, as seen from the Earth.
|
|
*
|
|
* This function determines where one body appears around the ecliptic plane
|
|
* (the plane of the Earth's orbit around the Sun) as seen from the Earth,
|
|
* relative to the another body's apparent position.
|
|
* The function returns an angle in the half-open range [0, 360) degrees.
|
|
* The value is the ecliptic longitude of `body1` relative to the ecliptic
|
|
* longitude of `body2`.
|
|
*
|
|
* The angle is 0 when the two bodies are at the same ecliptic longitude
|
|
* as seen from the Earth. The angle increases in the prograde direction
|
|
* (the direction that the planets orbit the Sun and the Moon orbits the Earth).
|
|
*
|
|
* When the angle is 180 degrees, it means the two bodies appear on opposite sides
|
|
* of the sky for an Earthly observer.
|
|
*
|
|
* Neither `body1` nor `body2` is allowed to be `Body.Earth`.
|
|
* If this happens, the function throws an exception.
|
|
*
|
|
* @param {Body} body1
|
|
* The first body, whose longitude is to be found relative to the second body.
|
|
*
|
|
* @param {Body} body2
|
|
* The second body, relative to which the longitude of the first body is to be found.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time of the observation.
|
|
*
|
|
* @returns {number}
|
|
* An angle in the range [0, 360), expressed in degrees.
|
|
*/
|
|
function PairLongitude(body1, body2, date) {
|
|
if (body1 === Body.Earth || body2 === Body.Earth)
|
|
throw 'The Earth does not have a longitude as seen from itself.';
|
|
const time = MakeTime(date);
|
|
const vector1 = GeoVector(body1, time, false);
|
|
const eclip1 = Ecliptic(vector1);
|
|
const vector2 = GeoVector(body2, time, false);
|
|
const eclip2 = Ecliptic(vector2);
|
|
return NormalizeLongitude(eclip1.elon - eclip2.elon);
|
|
}
|
|
exports.PairLongitude = PairLongitude;
|
|
/**
|
|
* @brief Calculates the angular separation between the Sun and the given body.
|
|
*
|
|
* Returns the full angle seen from
|
|
* the Earth, between the given body and the Sun.
|
|
* Unlike {@link PairLongitude}, this function does not
|
|
* project the body's "shadow" onto the ecliptic;
|
|
* the angle is measured in 3D space around the plane that
|
|
* contains the centers of the Earth, the Sun, and `body`.
|
|
*
|
|
* @param {Body} body
|
|
* The name of a supported celestial body other than the Earth.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The time at which the angle from the Sun is to be found.
|
|
*
|
|
* @returns {number}
|
|
* An angle in degrees in the range [0, 180].
|
|
*/
|
|
function AngleFromSun(body, date) {
|
|
if (body == Body.Earth)
|
|
throw 'The Earth does not have an angle as seen from itself.';
|
|
const time = MakeTime(date);
|
|
const sv = GeoVector(Body.Sun, time, true);
|
|
const bv = GeoVector(body, time, true);
|
|
const angle = AngleBetween(sv, bv);
|
|
return angle;
|
|
}
|
|
exports.AngleFromSun = AngleFromSun;
|
|
/**
|
|
* @brief Calculates heliocentric ecliptic longitude based on the J2000 equinox.
|
|
*
|
|
* @param {Body} body
|
|
* The name of a celestial body other than the Sun.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the ecliptic longitude.
|
|
*
|
|
* @returns {number}
|
|
* The ecliptic longitude angle of the body in degrees measured counterclockwise around the mean
|
|
* plane of the Earth's orbit, as seen from above the Sun's north pole.
|
|
* Ecliptic longitude starts at 0 at the J2000
|
|
* <a href="https://en.wikipedia.org/wiki/Equinox_(celestial_coordinates)">equinox</a> and
|
|
* increases in the same direction the Earth orbits the Sun.
|
|
* The returned value is always in the range [0, 360).
|
|
*/
|
|
function EclipticLongitude(body, date) {
|
|
if (body === Body.Sun)
|
|
throw 'Cannot calculate heliocentric longitude of the Sun.';
|
|
const hv = HelioVector(body, date);
|
|
const eclip = Ecliptic(hv);
|
|
return eclip.elon;
|
|
}
|
|
exports.EclipticLongitude = EclipticLongitude;
|
|
function VisualMagnitude(body, phase, helio_dist, geo_dist) {
|
|
// For Mercury and Venus, see: https://iopscience.iop.org/article/10.1086/430212
|
|
let c0, c1 = 0, c2 = 0, c3 = 0;
|
|
switch (body) {
|
|
case Body.Mercury:
|
|
c0 = -0.60;
|
|
c1 = +4.98;
|
|
c2 = -4.88;
|
|
c3 = +3.02;
|
|
break;
|
|
case Body.Venus:
|
|
if (phase < 163.6) {
|
|
c0 = -4.47;
|
|
c1 = +1.03;
|
|
c2 = +0.57;
|
|
c3 = +0.13;
|
|
}
|
|
else {
|
|
c0 = 0.98;
|
|
c1 = -1.02;
|
|
}
|
|
break;
|
|
case Body.Mars:
|
|
c0 = -1.52;
|
|
c1 = +1.60;
|
|
break;
|
|
case Body.Jupiter:
|
|
c0 = -9.40;
|
|
c1 = +0.50;
|
|
break;
|
|
case Body.Uranus:
|
|
c0 = -7.19;
|
|
c1 = +0.25;
|
|
break;
|
|
case Body.Neptune:
|
|
c0 = -6.87;
|
|
break;
|
|
case Body.Pluto:
|
|
c0 = -1.00;
|
|
c1 = +4.00;
|
|
break;
|
|
default: throw `VisualMagnitude: unsupported body ${body}`;
|
|
}
|
|
const x = phase / 100;
|
|
let mag = c0 + x * (c1 + x * (c2 + x * c3));
|
|
mag += 5 * Math.log10(helio_dist * geo_dist);
|
|
return mag;
|
|
}
|
|
function SaturnMagnitude(phase, helio_dist, geo_dist, gc, time) {
|
|
// Based on formulas by Paul Schlyter found here:
|
|
// http://www.stjarnhimlen.se/comp/ppcomp.html#15
|
|
// We must handle Saturn's rings as a major component of its visual magnitude.
|
|
// Find geocentric ecliptic coordinates of Saturn.
|
|
const eclip = Ecliptic(gc);
|
|
const ir = exports.DEG2RAD * 28.06; // tilt of Saturn's rings to the ecliptic, in radians
|
|
const Nr = exports.DEG2RAD * (169.51 + (3.82e-5 * time.tt)); // ascending node of Saturn's rings, in radians
|
|
// Find tilt of Saturn's rings, as seen from Earth.
|
|
const lat = exports.DEG2RAD * eclip.elat;
|
|
const lon = exports.DEG2RAD * eclip.elon;
|
|
const tilt = Math.asin(Math.sin(lat) * Math.cos(ir) - Math.cos(lat) * Math.sin(ir) * Math.sin(lon - Nr));
|
|
const sin_tilt = Math.sin(Math.abs(tilt));
|
|
let mag = -9.0 + 0.044 * phase;
|
|
mag += sin_tilt * (-2.6 + 1.2 * sin_tilt);
|
|
mag += 5 * Math.log10(helio_dist * geo_dist);
|
|
return { mag: mag, ring_tilt: exports.RAD2DEG * tilt };
|
|
}
|
|
function MoonMagnitude(phase, helio_dist, geo_dist) {
|
|
// https://astronomy.stackexchange.com/questions/10246/is-there-a-simple-analytical-formula-for-the-lunar-phase-brightness-curve
|
|
let rad = phase * exports.DEG2RAD;
|
|
let rad2 = rad * rad;
|
|
let rad4 = rad2 * rad2;
|
|
let mag = -12.717 + 1.49 * Math.abs(rad) + 0.0431 * rad4;
|
|
const moon_mean_distance_au = 385000.6 / exports.KM_PER_AU;
|
|
let geo_au = geo_dist / moon_mean_distance_au;
|
|
mag += 5 * Math.log10(helio_dist * geo_au);
|
|
return mag;
|
|
}
|
|
/**
|
|
* @brief Information about the apparent brightness and sunlit phase of a celestial object.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time pertaining to the other calculated values in this object.
|
|
*
|
|
* @property {number} mag
|
|
* The <a href="https://en.wikipedia.org/wiki/Apparent_magnitude">apparent visual magnitude</a> of the celestial body.
|
|
*
|
|
* @property {number} phase_angle
|
|
* The angle in degrees as seen from the center of the celestial body between the Sun and the Earth.
|
|
* The value is always in the range 0 to 180.
|
|
* The phase angle provides a measure of what fraction of the body's face appears
|
|
* illuminated by the Sun as seen from the Earth.
|
|
* When the observed body is the Sun, the `phase` property is set to 0,
|
|
* although this has no physical meaning because the Sun emits, rather than reflects, light.
|
|
* When the phase is near 0 degrees, the body appears "full".
|
|
* When it is 90 degrees, the body appears "half full".
|
|
* And when it is 180 degrees, the body appears "new" and is very difficult to see
|
|
* because it is both dim and lost in the Sun's glare as seen from the Earth.
|
|
*
|
|
* @property {number} phase_fraction
|
|
* The fraction of the body's face that is illuminated by the Sun, as seen from the Earth.
|
|
* Calculated from `phase_angle` for convenience.
|
|
* This value ranges from 0 to 1.
|
|
*
|
|
* @property {number} helio_dist
|
|
* The distance between the center of the Sun and the center of the body in
|
|
* <a href="https://en.wikipedia.org/wiki/Astronomical_unit">astronomical units</a> (AU).
|
|
*
|
|
* @property {number} geo_dist
|
|
* The distance between the center of the Earth and the center of the body in AU.
|
|
*
|
|
* @property {Vector} gc
|
|
* Geocentric coordinates: the 3D vector from the center of the Earth to the center of the body.
|
|
* The components are in expressed in AU and are oriented with respect to the J2000 equatorial plane.
|
|
*
|
|
* @property {Vector} hc
|
|
* Heliocentric coordinates: The 3D vector from the center of the Sun to the center of the body.
|
|
* Like `gc`, `hc` is expressed in AU and oriented with respect
|
|
* to the J2000 equatorial plane.
|
|
*
|
|
* @property {number | undefined} ring_tilt
|
|
* For Saturn, this is the angular tilt of the planet's rings in degrees away
|
|
* from the line of sight from the Earth. When the value is near 0, the rings
|
|
* appear edge-on from the Earth and are therefore difficult to see.
|
|
* When `ring_tilt` approaches its maximum value (about 27 degrees),
|
|
* the rings appear widest and brightest from the Earth.
|
|
* Unlike the <a href="https://ssd.jpl.nasa.gov/horizons.cgi">JPL Horizons</a> online tool,
|
|
* this library includes the effect of the ring tilt angle in the calculated value
|
|
* for Saturn's visual magnitude.
|
|
* For all bodies other than Saturn, the value of `ring_tilt` is `undefined`.
|
|
*/
|
|
class IlluminationInfo {
|
|
constructor(time, mag, phase_angle, helio_dist, geo_dist, gc, hc, ring_tilt) {
|
|
this.time = time;
|
|
this.mag = mag;
|
|
this.phase_angle = phase_angle;
|
|
this.helio_dist = helio_dist;
|
|
this.geo_dist = geo_dist;
|
|
this.gc = gc;
|
|
this.hc = hc;
|
|
this.ring_tilt = ring_tilt;
|
|
this.phase_fraction = (1 + Math.cos(exports.DEG2RAD * phase_angle)) / 2;
|
|
}
|
|
}
|
|
exports.IlluminationInfo = IlluminationInfo;
|
|
/**
|
|
* @brief Calculates visual magnitude and related information about a body.
|
|
*
|
|
* Calculates the phase angle, visual magnitude,
|
|
* and other values relating to the body's illumination
|
|
* at the given date and time, as seen from the Earth.
|
|
*
|
|
* @param {Body} body
|
|
* The name of the celestial body being observed.
|
|
* Not allowed to be `"Earth"`.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the illumination data for the given body.
|
|
*
|
|
* @returns {IlluminationInfo}
|
|
*/
|
|
function Illumination(body, date) {
|
|
if (body === Body.Earth)
|
|
throw `The illumination of the Earth is not defined.`;
|
|
const time = MakeTime(date);
|
|
const earth = CalcVsop(vsop.Earth, time);
|
|
let phase; // phase angle in degrees between Earth and Sun as seen from body
|
|
let hc; // vector from Sun to body
|
|
let gc; // vector from Earth to body
|
|
let mag; // visual magnitude
|
|
if (body === Body.Sun) {
|
|
gc = new Vector(-earth.x, -earth.y, -earth.z, time);
|
|
hc = new Vector(0, 0, 0, time);
|
|
phase = 0; // a placeholder value; the Sun does not have an illumination phase because it emits, rather than reflects, light.
|
|
}
|
|
else {
|
|
if (body === Body.Moon) {
|
|
// For extra numeric precision, use geocentric moon formula directly.
|
|
gc = GeoMoon(time);
|
|
hc = new Vector(earth.x + gc.x, earth.y + gc.y, earth.z + gc.z, time);
|
|
}
|
|
else {
|
|
// For planets, heliocentric vector is most direct to calculate.
|
|
hc = HelioVector(body, date);
|
|
gc = new Vector(hc.x - earth.x, hc.y - earth.y, hc.z - earth.z, time);
|
|
}
|
|
phase = AngleBetween(gc, hc);
|
|
}
|
|
let geo_dist = gc.Length(); // distance from body to center of Earth
|
|
let helio_dist = hc.Length(); // distance from body to center of Sun
|
|
let ring_tilt; // only reported for Saturn
|
|
if (body === Body.Sun) {
|
|
mag = SUN_MAG_1AU + 5 * Math.log10(geo_dist);
|
|
}
|
|
else if (body === Body.Moon) {
|
|
mag = MoonMagnitude(phase, helio_dist, geo_dist);
|
|
}
|
|
else if (body === Body.Saturn) {
|
|
const saturn = SaturnMagnitude(phase, helio_dist, geo_dist, gc, time);
|
|
mag = saturn.mag;
|
|
ring_tilt = saturn.ring_tilt;
|
|
}
|
|
else {
|
|
mag = VisualMagnitude(body, phase, helio_dist, geo_dist);
|
|
}
|
|
return new IlluminationInfo(time, mag, phase, helio_dist, geo_dist, gc, hc, ring_tilt);
|
|
}
|
|
exports.Illumination = Illumination;
|
|
function SynodicPeriod(body) {
|
|
if (body === Body.Earth)
|
|
throw 'The Earth does not have a synodic period as seen from itself.';
|
|
if (body === Body.Moon)
|
|
return MEAN_SYNODIC_MONTH;
|
|
// Calculate the synodic period of the planet from its and the Earth's sidereal periods.
|
|
// The sidereal period of a planet is how long it takes to go around the Sun in days, on average.
|
|
// The synodic period of a planet is how long it takes between consecutive oppositions
|
|
// or conjunctions, on average.
|
|
let planet = Planet[body];
|
|
if (!planet)
|
|
throw `Not a valid planet name: ${body}`;
|
|
// See here for explanation of the formula:
|
|
// https://en.wikipedia.org/wiki/Elongation_(astronomy)#Elongation_period
|
|
const Te = Planet.Earth.OrbitalPeriod;
|
|
const Tp = planet.OrbitalPeriod;
|
|
const synodicPeriod = Math.abs(Te / (Te / Tp - 1));
|
|
return synodicPeriod;
|
|
}
|
|
/**
|
|
* @brief Searches for when the Earth and a given body reach a relative ecliptic longitude separation.
|
|
*
|
|
* Searches for the date and time the relative ecliptic longitudes of
|
|
* the specified body and the Earth, as seen from the Sun, reach a certain
|
|
* difference. This function is useful for finding conjunctions and oppositions
|
|
* of the planets. For the opposition of a superior planet (Mars, Jupiter, ..., Pluto),
|
|
* or the inferior conjunction of an inferior planet (Mercury, Venus),
|
|
* call with `targetRelLon` = 0. The 0 value indicates that both
|
|
* planets are on the same ecliptic longitude line, ignoring the other planet's
|
|
* distance above or below the plane of the Earth's orbit.
|
|
* For superior conjunctions, call with `targetRelLon` = 180.
|
|
* This means the Earth and the other planet are on opposite sides of the Sun.
|
|
*
|
|
* @param {Body} body
|
|
* The name of a planet other than the Earth.
|
|
*
|
|
* @param {number} targetRelLon
|
|
* The desired angular difference in degrees between the ecliptic longitudes
|
|
* of `body` and the Earth. Must be in the range (-180, +180].
|
|
*
|
|
* @param {FlexibleDateTime} startDate
|
|
* The date and time after which to find the next occurrence of the
|
|
* body and the Earth reaching the desired relative longitude.
|
|
*
|
|
* @returns {AstroTime}
|
|
* The time when the Earth and the body next reach the specified relative longitudes.
|
|
*/
|
|
function SearchRelativeLongitude(body, targetRelLon, startDate) {
|
|
VerifyNumber(targetRelLon);
|
|
const planet = Planet[body];
|
|
if (!planet)
|
|
throw `Cannot search relative longitude because body is not a planet: ${body}`;
|
|
if (body === Body.Earth)
|
|
throw 'Cannot search relative longitude for the Earth (it is always 0)';
|
|
// Determine whether the Earth "gains" (+1) on the planet or "loses" (-1)
|
|
// as both race around the Sun.
|
|
const direction = (planet.OrbitalPeriod > Planet.Earth.OrbitalPeriod) ? +1 : -1;
|
|
function offset(t) {
|
|
const plon = EclipticLongitude(body, t);
|
|
const elon = EclipticLongitude(Body.Earth, t);
|
|
const diff = direction * (elon - plon);
|
|
return LongitudeOffset(diff - targetRelLon);
|
|
}
|
|
let syn = SynodicPeriod(body);
|
|
let time = MakeTime(startDate);
|
|
// Iterate until we converge on the desired event.
|
|
// Calculate the error angle, which will be a negative number of degrees,
|
|
// meaning we are "behind" the target relative longitude.
|
|
let error_angle = offset(time);
|
|
if (error_angle > 0)
|
|
error_angle -= 360; // force searching forward in time
|
|
for (let iter = 0; iter < 100; ++iter) {
|
|
// Estimate how many days in the future (positive) or past (negative)
|
|
// we have to go to get closer to the target relative longitude.
|
|
let day_adjust = (-error_angle / 360) * syn;
|
|
time = time.AddDays(day_adjust);
|
|
if (Math.abs(day_adjust) * SECONDS_PER_DAY < 1)
|
|
return time;
|
|
let prev_angle = error_angle;
|
|
error_angle = offset(time);
|
|
if (Math.abs(prev_angle) < 30) {
|
|
// Improve convergence for Mercury/Mars (eccentric orbits)
|
|
// by adjusting the synodic period to more closely match the
|
|
// variable speed of both planets in this part of their respective orbits.
|
|
if (prev_angle !== error_angle) {
|
|
let ratio = prev_angle / (prev_angle - error_angle);
|
|
if (ratio > 0.5 && ratio < 2.0)
|
|
syn *= ratio;
|
|
}
|
|
}
|
|
}
|
|
throw `Relative longitude search failed to converge for ${body} near ${time.toString()} (error_angle = ${error_angle}).`;
|
|
}
|
|
exports.SearchRelativeLongitude = SearchRelativeLongitude;
|
|
/**
|
|
* @brief Determines the moon's phase expressed as an ecliptic longitude.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for which to calculate the moon's phase.
|
|
*
|
|
* @returns {number}
|
|
* A value in the range [0, 360) indicating the difference
|
|
* in ecliptic longitude between the center of the Sun and the
|
|
* center of the Moon, as seen from the center of the Earth.
|
|
* Certain longitude values have conventional meanings:
|
|
*
|
|
* * 0 = new moon
|
|
* * 90 = first quarter
|
|
* * 180 = full moon
|
|
* * 270 = third quarter
|
|
*/
|
|
function MoonPhase(date) {
|
|
return PairLongitude(Body.Moon, Body.Sun, date);
|
|
}
|
|
exports.MoonPhase = MoonPhase;
|
|
/**
|
|
* @brief Searches for the date and time that the Moon reaches a specified phase.
|
|
*
|
|
* Lunar phases are defined in terms of geocentric ecliptic longitudes
|
|
* with respect to the Sun. When the Moon and the Sun have the same ecliptic
|
|
* longitude, that is defined as a new moon. When the two ecliptic longitudes
|
|
* are 180 degrees apart, that is defined as a full moon.
|
|
* To enumerate quarter lunar phases, it is simpler to call
|
|
* {@link SearchMoonQuarter} once, followed by repeatedly calling
|
|
* {@link NextMoonQuarter}. `SearchMoonPhase` is only
|
|
* necessary for finding other lunar phases than the usual quarter phases.
|
|
*
|
|
* @param {number} targetLon
|
|
* The difference in geocentric ecliptic longitude between the Sun and Moon
|
|
* that specifies the lunar phase being sought. This can be any value
|
|
* in the range [0, 360). Here are some helpful examples:
|
|
* 0 = new moon,
|
|
* 90 = first quarter,
|
|
* 180 = full moon,
|
|
* 270 = third quarter.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* The beginning of the window of time in which to search.
|
|
*
|
|
* @param {number} limitDays
|
|
* The floating point number of days after `dateStart`
|
|
* that limits the window of time in which to search.
|
|
*
|
|
* @returns {AstroTime | null}
|
|
* If the specified lunar phase occurs after `dateStart`
|
|
* and before `limitDays` days after `dateStart`,
|
|
* this function returns the date and time of the first such occurrence.
|
|
* Otherwise, it returns `null`.
|
|
*/
|
|
function SearchMoonPhase(targetLon, dateStart, limitDays) {
|
|
function moon_offset(t) {
|
|
let mlon = MoonPhase(t);
|
|
return LongitudeOffset(mlon - targetLon);
|
|
}
|
|
VerifyNumber(targetLon);
|
|
VerifyNumber(limitDays);
|
|
// To avoid discontinuities in the moon_offset function causing problems,
|
|
// we need to approximate when that function will next return 0.
|
|
// We probe it with the start time and take advantage of the fact
|
|
// that every lunar phase repeats roughly every 29.5 days.
|
|
// There is a surprising uncertainty in the quarter timing,
|
|
// due to the eccentricity of the moon's orbit.
|
|
// I have seen more than 0.9 days away from the simple prediction.
|
|
// To be safe, we take the predicted time of the event and search
|
|
// +/-1.5 days around it (a 3.0-day wide window).
|
|
// But we must return null if the final result goes beyond limitDays after dateStart.
|
|
const uncertainty = 1.5;
|
|
let ta = MakeTime(dateStart);
|
|
let ya = moon_offset(ta);
|
|
if (ya > 0)
|
|
ya -= 360; // force searching forward in time, not backward
|
|
let est_dt = -(MEAN_SYNODIC_MONTH * ya) / 360;
|
|
let dt1 = est_dt - uncertainty;
|
|
if (dt1 > limitDays)
|
|
return null; // not possible for moon phase to occur within the specified window
|
|
let dt2 = Math.min(limitDays, est_dt + uncertainty);
|
|
let t1 = ta.AddDays(dt1);
|
|
let t2 = ta.AddDays(dt2);
|
|
return Search(moon_offset, t1, t2);
|
|
}
|
|
exports.SearchMoonPhase = SearchMoonPhase;
|
|
/**
|
|
* @brief A quarter lunar phase, along with when it occurs.
|
|
*
|
|
* @property {number} quarter
|
|
* An integer as follows:
|
|
* 0 = new moon,
|
|
* 1 = first quarter,
|
|
* 2 = full moon,
|
|
* 3 = third quarter.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time of the quarter lunar phase.
|
|
*/
|
|
class MoonQuarter {
|
|
constructor(quarter, time) {
|
|
this.quarter = quarter;
|
|
this.time = time;
|
|
}
|
|
}
|
|
exports.MoonQuarter = MoonQuarter;
|
|
/**
|
|
* @brief Finds the first quarter lunar phase after the specified date and time.
|
|
*
|
|
* The quarter lunar phases are: new moon, first quarter, full moon, and third quarter.
|
|
* To enumerate quarter lunar phases, call `SearchMoonQuarter` once,
|
|
* then pass its return value to {@link NextMoonQuarter} to find the next
|
|
* `MoonQuarter`. Keep calling `NextMoonQuarter` in a loop,
|
|
* passing the previous return value as the argument to the next call.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* The date and time after which to find the first quarter lunar phase.
|
|
*
|
|
* @returns {MoonQuarter}
|
|
*/
|
|
function SearchMoonQuarter(dateStart) {
|
|
// Determine what the next quarter phase will be.
|
|
let phaseStart = MoonPhase(dateStart);
|
|
let quarterStart = Math.floor(phaseStart / 90);
|
|
let quarter = (quarterStart + 1) % 4;
|
|
let time = SearchMoonPhase(90 * quarter, dateStart, 10);
|
|
if (!time)
|
|
throw 'Cannot find moon quarter';
|
|
return new MoonQuarter(quarter, time);
|
|
}
|
|
exports.SearchMoonQuarter = SearchMoonQuarter;
|
|
/**
|
|
* @brief Finds the next quarter lunar phase in a series.
|
|
*
|
|
* Given a {@link MoonQuarter} object, finds the next consecutive
|
|
* quarter lunar phase. See remarks in {@link SearchMoonQuarter}
|
|
* for explanation of usage.
|
|
*
|
|
* @param {MoonQuarter} mq
|
|
* The return value of a prior call to {@link MoonQuarter} or `NextMoonQuarter`.
|
|
*/
|
|
function NextMoonQuarter(mq) {
|
|
// Skip 6 days past the previous found moon quarter to find the next one.
|
|
// This is less than the minimum possible increment.
|
|
// So far I have seen the interval well contained by the range (6.5, 8.3) days.
|
|
let date = new Date(mq.time.date.getTime() + 6 * MILLIS_PER_DAY);
|
|
return SearchMoonQuarter(date);
|
|
}
|
|
exports.NextMoonQuarter = NextMoonQuarter;
|
|
function BodyRadiusAu(body) {
|
|
// For the purposes of calculating rise/set times,
|
|
// only the Sun and Moon appear large enough to an observer
|
|
// on the Earth for their radius to matter.
|
|
// All other bodies are treated as points.
|
|
switch (body) {
|
|
case Body.Sun: return SUN_RADIUS_AU;
|
|
case Body.Moon: return MOON_EQUATORIAL_RADIUS_AU;
|
|
default: return 0;
|
|
}
|
|
}
|
|
/**
|
|
* @brief Finds the next rise or set time for a body.
|
|
*
|
|
* Finds a rise or set time for the given body as
|
|
* seen by an observer at the specified location on the Earth.
|
|
* Rise time is defined as the moment when the top of the body
|
|
* is observed to first appear above the horizon in the east.
|
|
* Set time is defined as the moment the top of the body
|
|
* is observed to sink below the horizon in the west.
|
|
* The times are adjusted for typical atmospheric refraction conditions.
|
|
*
|
|
* @param {Body} body
|
|
* The name of the body to find the rise or set time for.
|
|
*
|
|
* @param {Observer} observer
|
|
* Specifies the geographic coordinates and elevation above sea level of the observer.
|
|
*
|
|
* @param {number} direction
|
|
* Either +1 to find rise time or -1 to find set time.
|
|
* Any other value will cause an exception to be thrown.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* The date and time after which the specified rise or set time is to be found.
|
|
*
|
|
* @param {number} limitDays
|
|
* The fractional number of days after `dateStart` that limits
|
|
* when the rise or set time is to be found.
|
|
*
|
|
* @returns {AstroTime | null}
|
|
* The date and time of the rise or set event, or null if no such event
|
|
* occurs within the specified time window.
|
|
*/
|
|
function SearchRiseSet(body, observer, direction, dateStart, limitDays) {
|
|
let body_radius_au = BodyRadiusAu(body);
|
|
function peak_altitude(t) {
|
|
// Return the angular altitude above or below the horizon
|
|
// of the highest part (the peak) of the given object.
|
|
// This is defined as the apparent altitude of the center of the body plus
|
|
// the body's angular radius.
|
|
// The 'direction' variable in the enclosing function controls
|
|
// whether the angle is measured positive above the horizon or
|
|
// positive below the horizon, depending on whether the caller
|
|
// wants rise times or set times, respectively.
|
|
const ofdate = Equator(body, t, observer, true, true);
|
|
const hor = Horizon(t, observer, ofdate.ra, ofdate.dec);
|
|
const alt = hor.altitude + exports.RAD2DEG * (body_radius_au / ofdate.dist) + REFRACTION_NEAR_HORIZON;
|
|
return direction * alt;
|
|
}
|
|
return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, peak_altitude);
|
|
}
|
|
exports.SearchRiseSet = SearchRiseSet;
|
|
/**
|
|
* @brief Finds the next time a body reaches a given altitude.
|
|
*
|
|
* Finds when the given body ascends or descends through a given
|
|
* altitude angle, as seen by an observer at the specified location on the Earth.
|
|
* By using the appropriate combination of `direction` and `altitude` parameters,
|
|
* this function can be used to find when civil, nautical, or astronomical twilight
|
|
* begins (dawn) or ends (dusk).
|
|
*
|
|
* Civil dawn begins before sunrise when the Sun ascends through 6 degrees below
|
|
* the horizon. To find civil dawn, pass +1 for `direction` and -6 for `altitude`.
|
|
*
|
|
* Civil dusk ends after sunset when the Sun descends through 6 degrees below the horizon.
|
|
* To find civil dusk, pass -1 for `direction` and -6 for `altitude`.
|
|
*
|
|
* Nautical twilight is similar to civil twilight, only the `altitude` value should be -12 degrees.
|
|
*
|
|
* Astronomical twilight uses -18 degrees as the `altitude` value.
|
|
*
|
|
* @param {Body} body
|
|
* The name of the body for which to find the altitude event.
|
|
* Can be the Sun, Moon, or any planet other than the Earth.
|
|
*
|
|
* @param {Observer} observer
|
|
* Specifies the geographic coordinates and elevation above sea level of the observer.
|
|
*
|
|
* @param {number} direction
|
|
* Either +1 to find when the body ascends through the altitude,
|
|
* or -1 for when the body descends through the altitude.
|
|
* Any other value will cause an exception to be thrown.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* The date and time after which the specified altitude event is to be found.
|
|
*
|
|
* @param {number} limitDays
|
|
* The fractional number of days after `dateStart` that limits
|
|
* when the altitude event is to be found. Must be a positive number.
|
|
*
|
|
* @param {number} altitude
|
|
* The desired altitude angle of the body's center above (positive)
|
|
* or below (negative) the observer's local horizon, expressed in degrees.
|
|
* Must be in the range [-90, +90].
|
|
*
|
|
* @returns {AstroTime | null}
|
|
* The date and time of the altitude event, or null if no such event
|
|
* occurs within the specified time window.
|
|
*/
|
|
function SearchAltitude(body, observer, direction, dateStart, limitDays, altitude) {
|
|
if (!Number.isFinite(altitude) || altitude < -90 || altitude > +90)
|
|
throw `Invalid altitude angle: ${altitude}`;
|
|
function altitude_error(t) {
|
|
const ofdate = Equator(body, t, observer, true, true);
|
|
const hor = Horizon(t, observer, ofdate.ra, ofdate.dec);
|
|
return direction * (hor.altitude - altitude);
|
|
}
|
|
return InternalSearchAltitude(body, observer, direction, dateStart, limitDays, altitude_error);
|
|
}
|
|
exports.SearchAltitude = SearchAltitude;
|
|
function InternalSearchAltitude(body, observer, direction, dateStart, limitDays, altitude_error) {
|
|
VerifyObserver(observer);
|
|
VerifyNumber(limitDays);
|
|
if (limitDays <= 0)
|
|
throw `Invalid value for limitDays: ${limitDays}`;
|
|
if (body === Body.Earth)
|
|
throw 'Cannot find altitude event for the Earth.';
|
|
// See if the body is currently above/below the horizon.
|
|
// If we are looking for next rise time and the body is below the horizon,
|
|
// we use the current time as the lower time bound and the next culmination
|
|
// as the upper bound.
|
|
// If the body is above the horizon, we search for the next bottom and use it
|
|
// as the lower bound and the next culmination after that bottom as the upper bound.
|
|
// The same logic applies for finding set times, only we swap the hour angles.
|
|
// The peak_altitude() function already considers the 'direction' parameter.
|
|
let ha_before, ha_after;
|
|
if (direction === +1) {
|
|
ha_before = 12; // minimum altitude (bottom) happens BEFORE the body rises.
|
|
ha_after = 0; // maximum altitude (culmination) happens AFTER the body rises.
|
|
}
|
|
else if (direction === -1) {
|
|
ha_before = 0; // culmination happens BEFORE the body sets.
|
|
ha_after = 12; // bottom happens AFTER the body sets.
|
|
}
|
|
else {
|
|
throw `Invalid direction parameter ${direction} -- must be +1 or -1`;
|
|
}
|
|
let time_start = MakeTime(dateStart);
|
|
let time_before;
|
|
let evt_before;
|
|
let evt_after;
|
|
let error_before = altitude_error(time_start);
|
|
let error_after;
|
|
if (error_before > 0) {
|
|
// We are past the sought event, so we have to wait for the next "before" event (culm/bottom).
|
|
evt_before = SearchHourAngle(body, observer, ha_before, time_start);
|
|
time_before = evt_before.time;
|
|
error_before = altitude_error(time_before);
|
|
}
|
|
else {
|
|
// We are before or at the sought event, so we find the next "after" event (bottom/culm),
|
|
// and use the current time as the "before" event.
|
|
time_before = time_start;
|
|
}
|
|
evt_after = SearchHourAngle(body, observer, ha_after, time_before);
|
|
error_after = altitude_error(evt_after.time);
|
|
while (true) {
|
|
if (error_before <= 0 && error_after > 0) {
|
|
// Search between evt_before and evt_after for the desired event.
|
|
let tx = Search(altitude_error, time_before, evt_after.time, { init_f1: error_before, init_f2: error_after });
|
|
if (tx)
|
|
return tx;
|
|
}
|
|
// If we didn't find the desired event, use time_after to find the next before-event.
|
|
evt_before = SearchHourAngle(body, observer, ha_before, evt_after.time);
|
|
evt_after = SearchHourAngle(body, observer, ha_after, evt_before.time);
|
|
if (evt_before.time.ut >= time_start.ut + limitDays)
|
|
return null;
|
|
time_before = evt_before.time;
|
|
error_before = altitude_error(evt_before.time);
|
|
error_after = altitude_error(evt_after.time);
|
|
}
|
|
}
|
|
/**
|
|
* @brief Horizontal position of a body upon reaching an hour angle.
|
|
*
|
|
* Returns information about an occurrence of a celestial body
|
|
* reaching a given hour angle as seen by an observer at a given
|
|
* location on the surface of the Earth.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time of the celestial body reaching the hour angle.
|
|
*
|
|
* @property {HorizontalCoordinates} hor
|
|
* Topocentric horizontal coordinates for the body
|
|
* at the time indicated by the `time` property.
|
|
*/
|
|
class HourAngleEvent {
|
|
constructor(time, hor) {
|
|
this.time = time;
|
|
this.hor = hor;
|
|
}
|
|
}
|
|
exports.HourAngleEvent = HourAngleEvent;
|
|
/**
|
|
* @brief Finds when a body will reach a given hour angle.
|
|
*
|
|
* Finds the next time the given body is seen to reach the specified
|
|
* <a href="https://en.wikipedia.org/wiki/Hour_angle">hour angle</a>
|
|
* by the given observer.
|
|
* Providing `hourAngle` = 0 finds the next maximum altitude event (culmination).
|
|
* Providing `hourAngle` = 12 finds the next minimum altitude event.
|
|
* Note that, especially close to the Earth's poles, a body as seen on a given day
|
|
* may always be above the horizon or always below the horizon, so the caller cannot
|
|
* assume that a culminating object is visible nor that an object is below the horizon
|
|
* at its minimum altitude.
|
|
*
|
|
* @param {Body} body
|
|
* The name of a celestial body other than the Earth.
|
|
*
|
|
* @param {Observer} observer
|
|
* Specifies the geographic coordinates and elevation above sea level of the observer.
|
|
*
|
|
* @param {number} hourAngle
|
|
* The hour angle expressed in
|
|
* <a href="https://en.wikipedia.org/wiki/Sidereal_time">sidereal</a>
|
|
* hours for which the caller seeks to find the body attain.
|
|
* The value must be in the range [0, 24).
|
|
* The hour angle represents the number of sidereal hours that have
|
|
* elapsed since the most recent time the body crossed the observer's local
|
|
* <a href="https://en.wikipedia.org/wiki/Meridian_(astronomy)">meridian</a>.
|
|
* This specifying `hourAngle` = 0 finds the moment in time
|
|
* the body reaches the highest angular altitude in a given sidereal day.
|
|
*
|
|
* @param {FlexibleDateTime} dateStart
|
|
* The date and time after which the desired hour angle crossing event
|
|
* is to be found.
|
|
*
|
|
* @returns {HourAngleEvent}
|
|
*/
|
|
function SearchHourAngle(body, observer, hourAngle, dateStart) {
|
|
VerifyObserver(observer);
|
|
let time = MakeTime(dateStart);
|
|
let iter = 0;
|
|
if (body === Body.Earth)
|
|
throw 'Cannot search for hour angle of the Earth.';
|
|
VerifyNumber(hourAngle);
|
|
if (hourAngle < 0.0 || hourAngle >= 24.0)
|
|
throw `Invalid hour angle ${hourAngle}`;
|
|
while (true) {
|
|
++iter;
|
|
// Calculate Greenwich Apparent Sidereal Time (GAST) at the given time.
|
|
let gast = sidereal_time(time);
|
|
let ofdate = Equator(body, time, observer, true, true);
|
|
// Calculate the adjustment needed in sidereal time to bring
|
|
// the hour angle to the desired value.
|
|
let delta_sidereal_hours = ((hourAngle + ofdate.ra - observer.longitude / 15) - gast) % 24;
|
|
if (iter === 1) {
|
|
// On the first iteration, always search forward in time.
|
|
if (delta_sidereal_hours < 0)
|
|
delta_sidereal_hours += 24;
|
|
}
|
|
else {
|
|
// On subsequent iterations, we make the smallest possible adjustment,
|
|
// either forward or backward in time.
|
|
if (delta_sidereal_hours < -12)
|
|
delta_sidereal_hours += 24;
|
|
else if (delta_sidereal_hours > +12)
|
|
delta_sidereal_hours -= 24;
|
|
}
|
|
// If the error is tolerable (less than 0.1 seconds), stop searching.
|
|
if (Math.abs(delta_sidereal_hours) * 3600 < 0.1) {
|
|
const hor = Horizon(time, observer, ofdate.ra, ofdate.dec, 'normal');
|
|
return new HourAngleEvent(time, hor);
|
|
}
|
|
// We need to loop another time to get more accuracy.
|
|
// Update the terrestrial time adjusting by sidereal time.
|
|
let delta_days = (delta_sidereal_hours / 24) * SOLAR_DAYS_PER_SIDEREAL_DAY;
|
|
time = time.AddDays(delta_days);
|
|
}
|
|
}
|
|
exports.SearchHourAngle = SearchHourAngle;
|
|
/**
|
|
* @brief When the seasons change for a given calendar year.
|
|
*
|
|
* Represents the dates and times of the two solstices
|
|
* and the two equinoxes in a given calendar year.
|
|
* These four events define the changing of the seasons on the Earth.
|
|
*
|
|
* @property {AstroTime} mar_equinox
|
|
* The date and time of the March equinox in the given calendar year.
|
|
* This is the moment in March that the plane of the Earth's equator passes
|
|
* through the center of the Sun; thus the Sun's declination
|
|
* changes from a negative number to a positive number.
|
|
* The March equinox defines
|
|
* the beginning of spring in the northern hemisphere and
|
|
* the beginning of autumn in the southern hemisphere.
|
|
*
|
|
* @property {AstroTime} jun_solstice
|
|
* The date and time of the June solstice in the given calendar year.
|
|
* This is the moment in June that the Sun reaches its most positive
|
|
* declination value.
|
|
* At this moment the Earth's north pole is most tilted most toward the Sun.
|
|
* The June solstice defines
|
|
* the beginning of summer in the northern hemisphere and
|
|
* the beginning of winter in the southern hemisphere.
|
|
*
|
|
* @property {AstroTime} sep_equinox
|
|
* The date and time of the September equinox in the given calendar year.
|
|
* This is the moment in September that the plane of the Earth's equator passes
|
|
* through the center of the Sun; thus the Sun's declination
|
|
* changes from a positive number to a negative number.
|
|
* The September equinox defines
|
|
* the beginning of autumn in the northern hemisphere and
|
|
* the beginning of spring in the southern hemisphere.
|
|
*
|
|
* @property {AstroTime} dec_solstice
|
|
* The date and time of the December solstice in the given calendar year.
|
|
* This is the moment in December that the Sun reaches its most negative
|
|
* declination value.
|
|
* At this moment the Earth's south pole is tilted most toward the Sun.
|
|
* The December solstice defines
|
|
* the beginning of winter in the northern hemisphere and
|
|
* the beginning of summer in the southern hemisphere.
|
|
*/
|
|
class SeasonInfo {
|
|
constructor(mar_equinox, jun_solstice, sep_equinox, dec_solstice) {
|
|
this.mar_equinox = mar_equinox;
|
|
this.jun_solstice = jun_solstice;
|
|
this.sep_equinox = sep_equinox;
|
|
this.dec_solstice = dec_solstice;
|
|
}
|
|
}
|
|
exports.SeasonInfo = SeasonInfo;
|
|
/**
|
|
* @brief Finds the equinoxes and solstices for a given calendar year.
|
|
*
|
|
* @param {number | AstroTime} year
|
|
* The integer value or `AstroTime` object that specifies
|
|
* the UTC calendar year for which to find equinoxes and solstices.
|
|
*
|
|
* @returns {SeasonInfo}
|
|
*/
|
|
function Seasons(year) {
|
|
function find(targetLon, month, day) {
|
|
let startDate = new Date(Date.UTC(year, month - 1, day));
|
|
let time = SearchSunLongitude(targetLon, startDate, 4);
|
|
if (!time)
|
|
throw `Cannot find season change near ${startDate.toISOString()}`;
|
|
return time;
|
|
}
|
|
if ((year instanceof Date) && Number.isFinite(year.getTime()))
|
|
year = year.getUTCFullYear();
|
|
if (!Number.isSafeInteger(year))
|
|
throw `Cannot calculate seasons because year argument ${year} is neither a Date nor a safe integer.`;
|
|
let mar_equinox = find(0, 3, 19);
|
|
let jun_solstice = find(90, 6, 19);
|
|
let sep_equinox = find(180, 9, 21);
|
|
let dec_solstice = find(270, 12, 20);
|
|
return new SeasonInfo(mar_equinox, jun_solstice, sep_equinox, dec_solstice);
|
|
}
|
|
exports.Seasons = Seasons;
|
|
/**
|
|
* @brief The viewing conditions of a body relative to the Sun.
|
|
*
|
|
* Represents the angular separation of a body from the Sun as seen from the Earth
|
|
* and the relative ecliptic longitudes between that body and the Earth as seen from the Sun.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time of the observation.
|
|
*
|
|
* @property {string} visibility
|
|
* Either `"morning"` or `"evening"`,
|
|
* indicating when the body is most easily seen.
|
|
*
|
|
* @property {number} elongation
|
|
* The angle in degrees, as seen from the center of the Earth,
|
|
* of the apparent separation between the body and the Sun.
|
|
* This angle is measured in 3D space and is not projected onto the ecliptic plane.
|
|
* When `elongation` is less than a few degrees, the body is very
|
|
* difficult to see from the Earth because it is lost in the Sun's glare.
|
|
* The elongation is always in the range [0, 180].
|
|
*
|
|
* @property {number} ecliptic_separation
|
|
* The absolute value of the difference between the body's ecliptic longitude
|
|
* and the Sun's ecliptic longitude, both as seen from the center of the Earth.
|
|
* This angle measures around the plane of the Earth's orbit (the ecliptic),
|
|
* and ignores how far above or below that plane the body is.
|
|
* The ecliptic separation is measured in degrees and is always in the range [0, 180].
|
|
*
|
|
* @see {@link Elongation}
|
|
*/
|
|
class ElongationEvent {
|
|
constructor(time, visibility, elongation, ecliptic_separation) {
|
|
this.time = time;
|
|
this.visibility = visibility;
|
|
this.elongation = elongation;
|
|
this.ecliptic_separation = ecliptic_separation;
|
|
}
|
|
}
|
|
exports.ElongationEvent = ElongationEvent;
|
|
/**
|
|
* @brief Calculates the viewing conditions of a body relative to the Sun.
|
|
*
|
|
* Calculates angular separation of a body from the Sun as seen from the Earth
|
|
* and the relative ecliptic longitudes between that body and the Earth as seen from the Sun.
|
|
* See the return type {@link ElongationEvent} for details.
|
|
*
|
|
* This function is helpful for determining how easy
|
|
* it is to view a planet away from the Sun's glare on a given date.
|
|
* It also determines whether the object is visible in the morning or evening;
|
|
* this is more important the smaller the elongation is.
|
|
* It is also used to determine how far a planet is from opposition, conjunction, or quadrature.
|
|
*
|
|
* @param {Body} body
|
|
* The name of the observed body. Not allowed to be `"Earth"`.
|
|
*
|
|
* @returns {ElongationEvent}
|
|
*/
|
|
function Elongation(body, date) {
|
|
let time = MakeTime(date);
|
|
let lon = PairLongitude(body, Body.Sun, time);
|
|
let vis;
|
|
if (lon > 180) {
|
|
vis = 'morning';
|
|
lon = 360 - lon;
|
|
}
|
|
else {
|
|
vis = 'evening';
|
|
}
|
|
let angle = AngleFromSun(body, time);
|
|
return new ElongationEvent(time, vis, angle, lon);
|
|
}
|
|
exports.Elongation = Elongation;
|
|
/**
|
|
* @brief Finds the next time Mercury or Venus reaches maximum elongation.
|
|
*
|
|
* Searches for the next maximum elongation event for Mercury or Venus
|
|
* that occurs after the given start date. Calling with other values
|
|
* of `body` will result in an exception.
|
|
* Maximum elongation occurs when the body has the greatest
|
|
* angular separation from the Sun, as seen from the Earth.
|
|
* Returns an `ElongationEvent` object containing the date and time of the next
|
|
* maximum elongation, the elongation in degrees, and whether
|
|
* the body is visible in the morning or evening.
|
|
*
|
|
* @param {Body} body Either `"Mercury"` or `"Venus"`.
|
|
* @param {FlexibleDateTime} startDate The date and time after which to search for the next maximum elongation event.
|
|
*
|
|
* @returns {ElongationEvent}
|
|
*/
|
|
function SearchMaxElongation(body, startDate) {
|
|
const dt = 0.01;
|
|
function neg_slope(t) {
|
|
// The slope de/dt goes from positive to negative at the maximum elongation event.
|
|
// But Search() is designed for functions that ascend through zero.
|
|
// So this function returns the negative slope.
|
|
const t1 = t.AddDays(-dt / 2);
|
|
const t2 = t.AddDays(+dt / 2);
|
|
let e1 = AngleFromSun(body, t1);
|
|
let e2 = AngleFromSun(body, t2);
|
|
let m = (e1 - e2) / dt;
|
|
return m;
|
|
}
|
|
let startTime = MakeTime(startDate);
|
|
const table = {
|
|
Mercury: { s1: 50.0, s2: 85.0 },
|
|
Venus: { s1: 40.0, s2: 50.0 }
|
|
};
|
|
const planet = table[body];
|
|
if (!planet)
|
|
throw 'SearchMaxElongation works for Mercury and Venus only.';
|
|
let iter = 0;
|
|
while (++iter <= 2) {
|
|
// Find current heliocentric relative longitude between the
|
|
// inferior planet and the Earth.
|
|
let plon = EclipticLongitude(body, startTime);
|
|
let elon = EclipticLongitude(Body.Earth, startTime);
|
|
let rlon = LongitudeOffset(plon - elon); // clamp to (-180, +180]
|
|
// The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees
|
|
// because there is a cusp there that causes a discontinuity in the derivative.
|
|
// So we need to guard against searching near such times.
|
|
let rlon_lo, rlon_hi, adjust_days;
|
|
if (rlon >= -planet.s1 && rlon < +planet.s1) {
|
|
// Seek to the window [+s1, +s2].
|
|
adjust_days = 0;
|
|
// Search forward for the time t1 when rel lon = +s1.
|
|
rlon_lo = +planet.s1;
|
|
// Search forward for the time t2 when rel lon = +s2.
|
|
rlon_hi = +planet.s2;
|
|
}
|
|
else if (rlon >= +planet.s2 || rlon < -planet.s2) {
|
|
// Seek to the next search window at [-s2, -s1].
|
|
adjust_days = 0;
|
|
// Search forward for the time t1 when rel lon = -s2.
|
|
rlon_lo = -planet.s2;
|
|
// Search forward for the time t2 when rel lon = -s1.
|
|
rlon_hi = -planet.s1;
|
|
}
|
|
else if (rlon >= 0) {
|
|
// rlon must be in the middle of the window [+s1, +s2].
|
|
// Search BACKWARD for the time t1 when rel lon = +s1.
|
|
adjust_days = -SynodicPeriod(body) / 4;
|
|
rlon_lo = +planet.s1;
|
|
rlon_hi = +planet.s2;
|
|
// Search forward from t1 to find t2 such that rel lon = +s2.
|
|
}
|
|
else {
|
|
// rlon must be in the middle of the window [-s2, -s1].
|
|
// Search BACKWARD for the time t1 when rel lon = -s2.
|
|
adjust_days = -SynodicPeriod(body) / 4;
|
|
rlon_lo = -planet.s2;
|
|
// Search forward from t1 to find t2 such that rel lon = -s1.
|
|
rlon_hi = -planet.s1;
|
|
}
|
|
let t_start = startTime.AddDays(adjust_days);
|
|
let t1 = SearchRelativeLongitude(body, rlon_lo, t_start);
|
|
let t2 = SearchRelativeLongitude(body, rlon_hi, t1);
|
|
// Now we have a time range [t1,t2] that brackets a maximum elongation event.
|
|
// Confirm the bracketing.
|
|
let m1 = neg_slope(t1);
|
|
if (m1 >= 0)
|
|
throw `SearchMaxElongation: internal error: m1 = ${m1}`;
|
|
let m2 = neg_slope(t2);
|
|
if (m2 <= 0)
|
|
throw `SearchMaxElongation: internal error: m2 = ${m2}`;
|
|
// Use the generic search algorithm to home in on where the slope crosses from negative to positive.
|
|
let tx = Search(neg_slope, t1, t2, { init_f1: m1, init_f2: m2, dt_tolerance_seconds: 10 });
|
|
if (!tx)
|
|
throw `SearchMaxElongation: failed search iter ${iter} (t1=${t1.toString()}, t2=${t2.toString()})`;
|
|
if (tx.tt >= startTime.tt)
|
|
return Elongation(body, tx);
|
|
// This event is in the past (earlier than startDate).
|
|
// We need to search forward from t2 to find the next possible window.
|
|
// We never need to search more than twice.
|
|
startTime = t2.AddDays(1);
|
|
}
|
|
throw `SearchMaxElongation: failed to find event after 2 tries.`;
|
|
}
|
|
exports.SearchMaxElongation = SearchMaxElongation;
|
|
/**
|
|
* @brief Searches for the date and time Venus will next appear brightest as seen from the Earth.
|
|
*
|
|
* @param {Body} body
|
|
* Currently only `"Venus"` is supported.
|
|
* Mercury's peak magnitude occurs at superior conjunction, when it is virtually impossible to see from Earth,
|
|
* so peak magnitude events have little practical value for that planet.
|
|
* The Moon reaches peak magnitude very close to full moon, which can be found using
|
|
* {@link SearchMoonQuarter} or {@link SearchMoonPhase}.
|
|
* The other planets reach peak magnitude very close to opposition,
|
|
* which can be found using {@link SearchRelativeLongitude}.
|
|
*
|
|
* @param {FlexibleDateTime} startDate
|
|
* The date and time after which to find the next peak magnitude event.
|
|
*
|
|
* @returns {IlluminationInfo}
|
|
*/
|
|
function SearchPeakMagnitude(body, startDate) {
|
|
if (body !== Body.Venus)
|
|
throw 'SearchPeakMagnitude currently works for Venus only.';
|
|
const dt = 0.01;
|
|
function slope(t) {
|
|
// The Search() function finds a transition from negative to positive values.
|
|
// The derivative of magnitude y with respect to time t (dy/dt)
|
|
// is negative as an object gets brighter, because the magnitude numbers
|
|
// get smaller. At peak magnitude dy/dt = 0, then as the object gets dimmer,
|
|
// dy/dt > 0.
|
|
const t1 = t.AddDays(-dt / 2);
|
|
const t2 = t.AddDays(+dt / 2);
|
|
const y1 = Illumination(body, t1).mag;
|
|
const y2 = Illumination(body, t2).mag;
|
|
const m = (y2 - y1) / dt;
|
|
return m;
|
|
}
|
|
let startTime = MakeTime(startDate);
|
|
// s1 and s2 are relative longitudes within which peak magnitude of Venus can occur.
|
|
const s1 = 10.0;
|
|
const s2 = 30.0;
|
|
let iter = 0;
|
|
while (++iter <= 2) {
|
|
// Find current heliocentric relative longitude between the
|
|
// inferior planet and the Earth.
|
|
let plon = EclipticLongitude(body, startTime);
|
|
let elon = EclipticLongitude(Body.Earth, startTime);
|
|
let rlon = LongitudeOffset(plon - elon); // clamp to (-180, +180]
|
|
// The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees
|
|
// because there is a cusp there that causes a discontinuity in the derivative.
|
|
// So we need to guard against searching near such times.
|
|
let rlon_lo, rlon_hi, adjust_days;
|
|
if (rlon >= -s1 && rlon < +s1) {
|
|
// Seek to the window [+s1, +s2].
|
|
adjust_days = 0;
|
|
// Search forward for the time t1 when rel lon = +s1.
|
|
rlon_lo = +s1;
|
|
// Search forward for the time t2 when rel lon = +s2.
|
|
rlon_hi = +s2;
|
|
}
|
|
else if (rlon >= +s2 || rlon < -s2) {
|
|
// Seek to the next search window at [-s2, -s1].
|
|
adjust_days = 0;
|
|
// Search forward for the time t1 when rel lon = -s2.
|
|
rlon_lo = -s2;
|
|
// Search forward for the time t2 when rel lon = -s1.
|
|
rlon_hi = -s1;
|
|
}
|
|
else if (rlon >= 0) {
|
|
// rlon must be in the middle of the window [+s1, +s2].
|
|
// Search BACKWARD for the time t1 when rel lon = +s1.
|
|
adjust_days = -SynodicPeriod(body) / 4;
|
|
rlon_lo = +s1;
|
|
// Search forward from t1 to find t2 such that rel lon = +s2.
|
|
rlon_hi = +s2;
|
|
}
|
|
else {
|
|
// rlon must be in the middle of the window [-s2, -s1].
|
|
// Search BACKWARD for the time t1 when rel lon = -s2.
|
|
adjust_days = -SynodicPeriod(body) / 4;
|
|
rlon_lo = -s2;
|
|
// Search forward from t1 to find t2 such that rel lon = -s1.
|
|
rlon_hi = -s1;
|
|
}
|
|
let t_start = startTime.AddDays(adjust_days);
|
|
let t1 = SearchRelativeLongitude(body, rlon_lo, t_start);
|
|
let t2 = SearchRelativeLongitude(body, rlon_hi, t1);
|
|
// Now we have a time range [t1,t2] that brackets a maximum magnitude event.
|
|
// Confirm the bracketing.
|
|
let m1 = slope(t1);
|
|
if (m1 >= 0)
|
|
throw `SearchPeakMagnitude: internal error: m1 = ${m1}`;
|
|
let m2 = slope(t2);
|
|
if (m2 <= 0)
|
|
throw `SearchPeakMagnitude: internal error: m2 = ${m2}`;
|
|
// Use the generic search algorithm to home in on where the slope crosses from negative to positive.
|
|
let tx = Search(slope, t1, t2, { init_f1: m1, init_f2: m2, dt_tolerance_seconds: 10 });
|
|
if (!tx)
|
|
throw `SearchPeakMagnitude: failed search iter ${iter} (t1=${t1.toString()}, t2=${t2.toString()})`;
|
|
if (tx.tt >= startTime.tt)
|
|
return Illumination(body, tx);
|
|
// This event is in the past (earlier than startDate).
|
|
// We need to search forward from t2 to find the next possible window.
|
|
// We never need to search more than twice.
|
|
startTime = t2.AddDays(1);
|
|
}
|
|
throw `SearchPeakMagnitude: failed to find event after 2 tries.`;
|
|
}
|
|
exports.SearchPeakMagnitude = SearchPeakMagnitude;
|
|
/**
|
|
* @brief A closest or farthest point in a body's orbit around its primary.
|
|
*
|
|
* For a planet orbiting the Sun, apsis is a perihelion or aphelion, respectively.
|
|
* For the Moon orbiting the Earth, apsis is a perigee or apogee, respectively.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time of the apsis.
|
|
*
|
|
* @property {number} kind
|
|
* For a closest approach (perigee or perihelion), `kind` is 0.
|
|
* For a farthest distance event (apogee or aphelion), `kind` is 1.
|
|
*
|
|
* @property {number} dist_au
|
|
* The distance between the centers of the two bodies in astronomical units (AU).
|
|
*
|
|
* @property {number} dist_km
|
|
* The distance between the centers of the two bodies in kilometers.
|
|
*
|
|
* @see {@link SearchLunarApsis}
|
|
* @see {@link NextLunarApsis}
|
|
*/
|
|
class Apsis {
|
|
constructor(time, kind, dist_au) {
|
|
this.time = time;
|
|
this.kind = kind;
|
|
this.dist_au = dist_au;
|
|
this.dist_km = dist_au * exports.KM_PER_AU;
|
|
}
|
|
}
|
|
exports.Apsis = Apsis;
|
|
/**
|
|
* @brief Finds the next perigee or apogee of the Moon.
|
|
*
|
|
* Finds the next perigee (closest approach) or apogee (farthest remove) of the Moon
|
|
* that occurs after the specified date and time.
|
|
*
|
|
* @param {FlexibleDateTime} startDate
|
|
* The date and time after which to find the next perigee or apogee.
|
|
*
|
|
* @returns {Apsis}
|
|
*/
|
|
function SearchLunarApsis(startDate) {
|
|
const dt = 0.001;
|
|
function distance_slope(t) {
|
|
let t1 = t.AddDays(-dt / 2);
|
|
let t2 = t.AddDays(+dt / 2);
|
|
let r1 = CalcMoon(t1).distance_au;
|
|
let r2 = CalcMoon(t2).distance_au;
|
|
let m = (r2 - r1) / dt;
|
|
return m;
|
|
}
|
|
function negative_distance_slope(t) {
|
|
return -distance_slope(t);
|
|
}
|
|
// Check the rate of change of the distance dr/dt at the start time.
|
|
// If it is positive, the Moon is currently getting farther away,
|
|
// so start looking for apogee.
|
|
// Conversely, if dr/dt < 0, start looking for perigee.
|
|
// Either way, the polarity of the slope will change, so the product will be negative.
|
|
// Handle the crazy corner case of exactly touching zero by checking for m1*m2 <= 0.
|
|
let t1 = MakeTime(startDate);
|
|
let m1 = distance_slope(t1);
|
|
const increment = 5; // number of days to skip in each iteration
|
|
for (var iter = 0; iter * increment < 2 * MEAN_SYNODIC_MONTH; ++iter) {
|
|
let t2 = t1.AddDays(increment);
|
|
let m2 = distance_slope(t2);
|
|
if (m1 * m2 <= 0) {
|
|
// The time range [t1, t2] contains an apsis.
|
|
// Figure out whether it is perigee or apogee.
|
|
if (m1 < 0 || m2 > 0) {
|
|
// We found a minimum distance event: perigee.
|
|
// Search the time range [t1, t2] for the time when the slope goes
|
|
// from negative to positive.
|
|
let tx = Search(distance_slope, t1, t2, { init_f1: m1, init_f2: m2 });
|
|
if (!tx)
|
|
throw 'SearchLunarApsis INTERNAL ERROR: perigee search failed!';
|
|
let dist = CalcMoon(tx).distance_au;
|
|
return new Apsis(tx, 0, dist);
|
|
}
|
|
if (m1 > 0 || m2 < 0) {
|
|
// We found a maximum distance event: apogee.
|
|
// Search the time range [t1, t2] for the time when the slope goes
|
|
// from positive to negative.
|
|
let tx = Search(negative_distance_slope, t1, t2, { init_f1: -m1, init_f2: -m2 });
|
|
if (!tx)
|
|
throw 'SearchLunarApsis INTERNAL ERROR: apogee search failed!';
|
|
let dist = CalcMoon(tx).distance_au;
|
|
return new Apsis(tx, 1, dist);
|
|
}
|
|
// This should never happen; it should not be possible for consecutive
|
|
// times t1 and t2 to both have zero slope.
|
|
throw 'SearchLunarApsis INTERNAL ERROR: cannot classify apsis event!';
|
|
}
|
|
t1 = t2;
|
|
m1 = m2;
|
|
}
|
|
// It should not be possible to fail to find an apsis within 2 synodic months.
|
|
throw 'SearchLunarApsis INTERNAL ERROR: could not find apsis within 2 synodic months of start date.';
|
|
}
|
|
exports.SearchLunarApsis = SearchLunarApsis;
|
|
/**
|
|
* @brief Finds the next lunar apsis (perigee or apogee) in a series.
|
|
*
|
|
* Given a lunar apsis returned by an initial call to {@link SearchLunarApsis},
|
|
* or a previous call to `NextLunarApsis`, finds the next lunar apsis.
|
|
* If the given apsis is a perigee, this function finds the next apogee, and vice versa.
|
|
*
|
|
* @param {Apsis} apsis
|
|
* A lunar perigee or apogee event.
|
|
*
|
|
* @returns {Apsis}
|
|
* The successor apogee for the given perigee, or the successor perigee for the given apogee.
|
|
*/
|
|
function NextLunarApsis(apsis) {
|
|
const skip = 11; // number of days to skip to start looking for next apsis event
|
|
let next = SearchLunarApsis(apsis.time.AddDays(skip));
|
|
if (next.kind + apsis.kind !== 1)
|
|
throw `NextLunarApsis INTERNAL ERROR: did not find alternating apogee/perigee: prev=${apsis.kind} @ ${apsis.time.toString()}, next=${next.kind} @ ${next.time.toString()}`;
|
|
return next;
|
|
}
|
|
exports.NextLunarApsis = NextLunarApsis;
|
|
function PlanetExtreme(body, kind, start_time, dayspan) {
|
|
const direction = (kind === 1) ? +1.0 : -1.0;
|
|
const npoints = 10;
|
|
for (;;) {
|
|
const interval = dayspan / (npoints - 1);
|
|
// iterate until uncertainty is less than one minute
|
|
if (interval < 1.0 / 1440.0) {
|
|
const apsis_time = start_time.AddDays(interval / 2.0);
|
|
const dist_au = HelioDistance(body, apsis_time);
|
|
return new Apsis(apsis_time, kind, dist_au);
|
|
}
|
|
let best_i = -1;
|
|
let best_dist = 0.0;
|
|
for (let i = 0; i < npoints; ++i) {
|
|
const time = start_time.AddDays(i * interval);
|
|
const dist = direction * HelioDistance(body, time);
|
|
if (i == 0 || dist > best_dist) {
|
|
best_i = i;
|
|
best_dist = dist;
|
|
}
|
|
}
|
|
/* Narrow in on the extreme point. */
|
|
start_time = start_time.AddDays((best_i - 1) * interval);
|
|
dayspan = 2.0 * interval;
|
|
}
|
|
}
|
|
function BruteSearchPlanetApsis(body, startTime) {
|
|
/*
|
|
Neptune is a special case for two reasons:
|
|
1. Its orbit is nearly circular (low orbital eccentricity).
|
|
2. It is so distant from the Sun that the orbital period is very long.
|
|
Put together, this causes wobbling of the Sun around the Solar System Barycenter (SSB)
|
|
to be so significant that there are 3 local minima in the distance-vs-time curve
|
|
near each apsis. Therefore, unlike for other planets, we can't use an optimized
|
|
algorithm for finding dr/dt = 0.
|
|
Instead, we use a dumb, brute-force algorithm of sampling and finding min/max
|
|
heliocentric distance.
|
|
|
|
There is a similar problem in the TOP2013 model for Pluto:
|
|
Its position vector has high-frequency oscillations that confuse the
|
|
slope-based determination of apsides.
|
|
*/
|
|
/*
|
|
Rewind approximately 30 degrees in the orbit,
|
|
then search forward for 270 degrees.
|
|
This is a very cautious way to prevent missing an apsis.
|
|
Typically we will find two apsides, and we pick whichever
|
|
apsis is ealier, but after startTime.
|
|
Sample points around this orbital arc and find when the distance
|
|
is greatest and smallest.
|
|
*/
|
|
const npoints = 100;
|
|
const t1 = startTime.AddDays(Planet[body].OrbitalPeriod * (-30 / 360));
|
|
const t2 = startTime.AddDays(Planet[body].OrbitalPeriod * (+270 / 360));
|
|
let t_min = t1;
|
|
let t_max = t1;
|
|
let min_dist = -1.0;
|
|
let max_dist = -1.0;
|
|
const interval = (t2.ut - t1.ut) / (npoints - 1);
|
|
for (let i = 0; i < npoints; ++i) {
|
|
const time = t1.AddDays(i * interval);
|
|
const dist = HelioDistance(body, time);
|
|
if (i === 0) {
|
|
max_dist = min_dist = dist;
|
|
}
|
|
else {
|
|
if (dist > max_dist) {
|
|
max_dist = dist;
|
|
t_max = time;
|
|
}
|
|
if (dist < min_dist) {
|
|
min_dist = dist;
|
|
t_min = time;
|
|
}
|
|
}
|
|
}
|
|
const perihelion = PlanetExtreme(body, 0, t_min.AddDays(-2 * interval), 4 * interval);
|
|
const aphelion = PlanetExtreme(body, 1, t_max.AddDays(-2 * interval), 4 * interval);
|
|
if (perihelion.time.tt >= startTime.tt) {
|
|
if (aphelion.time.tt >= startTime.tt && aphelion.time.tt < perihelion.time.tt)
|
|
return aphelion;
|
|
return perihelion;
|
|
}
|
|
if (aphelion.time.tt >= startTime.tt)
|
|
return aphelion;
|
|
throw 'Internal error: failed to find Neptune apsis.';
|
|
}
|
|
/**
|
|
* @brief Finds the next perihelion or aphelion of a planet.
|
|
*
|
|
* Finds the date and time of a planet's perihelion (closest approach to the Sun)
|
|
* or aphelion (farthest distance from the Sun) after a given time.
|
|
*
|
|
* Given a date and time to start the search in `startTime`, this function finds the
|
|
* next date and time that the center of the specified planet reaches the closest or farthest point
|
|
* in its orbit with respect to the center of the Sun, whichever comes first
|
|
* after `startTime`.
|
|
*
|
|
* The closest point is called *perihelion* and the farthest point is called *aphelion*.
|
|
* The word *apsis* refers to either event.
|
|
*
|
|
* To iterate through consecutive alternating perihelion and aphelion events,
|
|
* call `SearchPlanetApsis` once, then use the return value to call
|
|
* {@link NextPlanetApsis}. After that, keep feeding the previous return value
|
|
* from `NextPlanetApsis` into another call of `NextPlanetApsis`
|
|
* as many times as desired.
|
|
*
|
|
* @param {Body} body
|
|
* The planet for which to find the next perihelion/aphelion event.
|
|
* Not allowed to be `"Sun"` or `"Moon"`.
|
|
*
|
|
* @param {AstroTime} startTime
|
|
* The date and time at which to start searching for the next perihelion or aphelion.
|
|
*
|
|
* @returns {Apsis}
|
|
* The next perihelion or aphelion that occurs after `startTime`.
|
|
*/
|
|
function SearchPlanetApsis(body, startTime) {
|
|
if (body === Body.Neptune || body === Body.Pluto)
|
|
return BruteSearchPlanetApsis(body, startTime);
|
|
function positive_slope(t) {
|
|
const dt = 0.001;
|
|
let t1 = t.AddDays(-dt / 2);
|
|
let t2 = t.AddDays(+dt / 2);
|
|
let r1 = HelioDistance(body, t1);
|
|
let r2 = HelioDistance(body, t2);
|
|
let m = (r2 - r1) / dt;
|
|
return m;
|
|
}
|
|
function negative_slope(t) {
|
|
return -positive_slope(t);
|
|
}
|
|
const orbit_period_days = Planet[body].OrbitalPeriod;
|
|
const increment = orbit_period_days / 6.0;
|
|
let t1 = startTime;
|
|
let m1 = positive_slope(t1);
|
|
for (let iter = 0; iter * increment < 2.0 * orbit_period_days; ++iter) {
|
|
const t2 = t1.AddDays(increment);
|
|
const m2 = positive_slope(t2);
|
|
if (m1 * m2 <= 0.0) {
|
|
/* There is a change of slope polarity within the time range [t1, t2]. */
|
|
/* Therefore this time range contains an apsis. */
|
|
/* Figure out whether it is perihelion or aphelion. */
|
|
let slope_func;
|
|
let kind;
|
|
if (m1 < 0.0 || m2 > 0.0) {
|
|
/* We found a minimum-distance event: perihelion. */
|
|
/* Search the time range for the time when the slope goes from negative to positive. */
|
|
slope_func = positive_slope;
|
|
kind = 0; // perihelion
|
|
}
|
|
else if (m1 > 0.0 || m2 < 0.0) {
|
|
/* We found a maximum-distance event: aphelion. */
|
|
/* Search the time range for the time when the slope goes from positive to negative. */
|
|
slope_func = negative_slope;
|
|
kind = 1; // aphelion
|
|
}
|
|
else {
|
|
/* This should never happen. It should not be possible for both slopes to be zero. */
|
|
throw "Internal error with slopes in SearchPlanetApsis";
|
|
}
|
|
const search = Search(slope_func, t1, t2);
|
|
if (!search)
|
|
throw "Failed to find slope transition in planetary apsis search.";
|
|
const dist = HelioDistance(body, search);
|
|
return new Apsis(search, kind, dist);
|
|
}
|
|
/* We have not yet found a slope polarity change. Keep searching. */
|
|
t1 = t2;
|
|
m1 = m2;
|
|
}
|
|
throw "Internal error: should have found planetary apsis within 2 orbital periods.";
|
|
}
|
|
exports.SearchPlanetApsis = SearchPlanetApsis;
|
|
/**
|
|
* @brief Finds the next planetary perihelion or aphelion event in a series.
|
|
*
|
|
* This function requires an {@link Apsis} value obtained from a call
|
|
* to {@link SearchPlanetApsis} or `NextPlanetApsis`.
|
|
* Given an aphelion event, this function finds the next perihelion event, and vice versa.
|
|
* See {@link SearchPlanetApsis} for more details.
|
|
*
|
|
* @param {Body} body
|
|
* The planet for which to find the next perihelion/aphelion event.
|
|
* Not allowed to be `"Sun"` or `"Moon"`.
|
|
* Must match the body passed into the call that produced the `apsis` parameter.
|
|
*
|
|
* @param {Apsis} apsis
|
|
* An apsis event obtained from a call to {@link SearchPlanetApsis} or `NextPlanetApsis`.
|
|
*
|
|
* @returns {Apsis}
|
|
* Same as the return value for {@link SearchPlanetApsis}.
|
|
*/
|
|
function NextPlanetApsis(body, apsis) {
|
|
if (apsis.kind !== 0 && apsis.kind !== 1)
|
|
throw `Invalid apsis kind: ${apsis.kind}`;
|
|
/* skip 1/4 of an orbit before starting search again */
|
|
const skip = 0.25 * Planet[body].OrbitalPeriod;
|
|
const time = apsis.time.AddDays(skip);
|
|
const next = SearchPlanetApsis(body, time);
|
|
/* Verify that we found the opposite apsis from the previous one. */
|
|
if (next.kind + apsis.kind !== 1)
|
|
throw `Internal error: previous apsis was ${apsis.kind}, but found ${next.kind} for next apsis.`;
|
|
return next;
|
|
}
|
|
exports.NextPlanetApsis = NextPlanetApsis;
|
|
/**
|
|
* @brief Calculates the inverse of a rotation matrix.
|
|
*
|
|
* Given a rotation matrix that performs some coordinate transform,
|
|
* this function returns the matrix that reverses that trasnform.
|
|
*
|
|
* @param {RotationMatrix} rotation
|
|
* The rotation matrix to be inverted.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* The inverse rotation matrix.
|
|
*/
|
|
function InverseRotation(rotation) {
|
|
return new RotationMatrix([
|
|
[rotation.rot[0][0], rotation.rot[1][0], rotation.rot[2][0]],
|
|
[rotation.rot[0][1], rotation.rot[1][1], rotation.rot[2][1]],
|
|
[rotation.rot[0][2], rotation.rot[1][2], rotation.rot[2][2]]
|
|
]);
|
|
}
|
|
exports.InverseRotation = InverseRotation;
|
|
/**
|
|
* @brief Creates a rotation based on applying one rotation followed by another.
|
|
*
|
|
* Given two rotation matrices, returns a combined rotation matrix that is
|
|
* equivalent to rotating based on the first matrix, followed by the second.
|
|
*
|
|
* @param {RotationMatrix} a
|
|
* The first rotation to apply.
|
|
*
|
|
* @param {RotationMatrix} b
|
|
* The second rotation to apply.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* The combined rotation matrix.
|
|
*/
|
|
function CombineRotation(a, b) {
|
|
/*
|
|
Use matrix multiplication: c = b*a.
|
|
We put 'b' on the left and 'a' on the right because,
|
|
just like when you use a matrix M to rotate a vector V,
|
|
you put the M on the left in the product M*V.
|
|
We can think of this as 'b' rotating all the 3 column vectors in 'a'.
|
|
*/
|
|
return new RotationMatrix([
|
|
[
|
|
b.rot[0][0] * a.rot[0][0] + b.rot[1][0] * a.rot[0][1] + b.rot[2][0] * a.rot[0][2],
|
|
b.rot[0][1] * a.rot[0][0] + b.rot[1][1] * a.rot[0][1] + b.rot[2][1] * a.rot[0][2],
|
|
b.rot[0][2] * a.rot[0][0] + b.rot[1][2] * a.rot[0][1] + b.rot[2][2] * a.rot[0][2]
|
|
],
|
|
[
|
|
b.rot[0][0] * a.rot[1][0] + b.rot[1][0] * a.rot[1][1] + b.rot[2][0] * a.rot[1][2],
|
|
b.rot[0][1] * a.rot[1][0] + b.rot[1][1] * a.rot[1][1] + b.rot[2][1] * a.rot[1][2],
|
|
b.rot[0][2] * a.rot[1][0] + b.rot[1][2] * a.rot[1][1] + b.rot[2][2] * a.rot[1][2]
|
|
],
|
|
[
|
|
b.rot[0][0] * a.rot[2][0] + b.rot[1][0] * a.rot[2][1] + b.rot[2][0] * a.rot[2][2],
|
|
b.rot[0][1] * a.rot[2][0] + b.rot[1][1] * a.rot[2][1] + b.rot[2][1] * a.rot[2][2],
|
|
b.rot[0][2] * a.rot[2][0] + b.rot[1][2] * a.rot[2][1] + b.rot[2][2] * a.rot[2][2]
|
|
]
|
|
]);
|
|
}
|
|
exports.CombineRotation = CombineRotation;
|
|
/**
|
|
* @brief Creates an identity rotation matrix.
|
|
*
|
|
* Returns a rotation matrix that has no effect on orientation.
|
|
* This matrix can be the starting point for other operations,
|
|
* such as using a series of calls to {@link Pivot} to
|
|
* create a custom rotation matrix.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* The identity matrix.
|
|
*/
|
|
function IdentityMatrix() {
|
|
return new RotationMatrix([
|
|
[1, 0, 0],
|
|
[0, 1, 0],
|
|
[0, 0, 1]
|
|
]);
|
|
}
|
|
exports.IdentityMatrix = IdentityMatrix;
|
|
/**
|
|
* @brief Re-orients a rotation matrix by pivoting it by an angle around one of its axes.
|
|
*
|
|
* Given a rotation matrix, a selected coordinate axis, and an angle in degrees,
|
|
* this function pivots the rotation matrix by that angle around that coordinate axis.
|
|
*
|
|
* For example, if you have rotation matrix that converts ecliptic coordinates (ECL)
|
|
* to horizontal coordinates (HOR), but you really want to convert ECL to the orientation
|
|
* of a telescope camera pointed at a given body, you can use `Astronomy_Pivot` twice:
|
|
* (1) pivot around the zenith axis by the body's azimuth, then (2) pivot around the
|
|
* western axis by the body's altitude angle. The resulting rotation matrix will then
|
|
* reorient ECL coordinates to the orientation of your telescope camera.
|
|
*
|
|
* @param {RotationMatrix} rotation
|
|
* The input rotation matrix.
|
|
*
|
|
* @param {number} axis
|
|
* An integer that selects which coordinate axis to rotate around:
|
|
* 0 = x, 1 = y, 2 = z. Any other value will cause an exception.
|
|
*
|
|
* @param {number} angle
|
|
* An angle in degrees indicating the amount of rotation around the specified axis.
|
|
* Positive angles indicate rotation counterclockwise as seen from the positive
|
|
* direction along that axis, looking towards the origin point of the orientation system.
|
|
* Any finite number of degrees is allowed, but best precision will result from
|
|
* keeping `angle` in the range [-360, +360].
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A pivoted matrix object.
|
|
*/
|
|
function Pivot(rotation, axis, angle) {
|
|
// Check for an invalid coordinate axis.
|
|
if (axis !== 0 && axis !== 1 && axis !== 2)
|
|
throw `Invalid axis ${axis}. Must be [0, 1, 2].`;
|
|
const radians = VerifyNumber(angle) * exports.DEG2RAD;
|
|
const c = Math.cos(radians);
|
|
const s = Math.sin(radians);
|
|
/*
|
|
We need to maintain the "right-hand" rule, no matter which
|
|
axis was selected. That means we pick (i, j, k) axis order
|
|
such that the following vector cross product is satisfied:
|
|
i x j = k
|
|
*/
|
|
const i = (axis + 1) % 3;
|
|
const j = (axis + 2) % 3;
|
|
const k = axis;
|
|
let rot = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
|
|
rot[i][i] = c * rotation.rot[i][i] - s * rotation.rot[i][j];
|
|
rot[i][j] = s * rotation.rot[i][i] + c * rotation.rot[i][j];
|
|
rot[i][k] = rotation.rot[i][k];
|
|
rot[j][i] = c * rotation.rot[j][i] - s * rotation.rot[j][j];
|
|
rot[j][j] = s * rotation.rot[j][i] + c * rotation.rot[j][j];
|
|
rot[j][k] = rotation.rot[j][k];
|
|
rot[k][i] = c * rotation.rot[k][i] - s * rotation.rot[k][j];
|
|
rot[k][j] = s * rotation.rot[k][i] + c * rotation.rot[k][j];
|
|
rot[k][k] = rotation.rot[k][k];
|
|
return new RotationMatrix(rot);
|
|
}
|
|
exports.Pivot = Pivot;
|
|
/**
|
|
* @brief Converts spherical coordinates to Cartesian coordinates.
|
|
*
|
|
* Given spherical coordinates and a time at which they are valid,
|
|
* returns a vector of Cartesian coordinates. The returned value
|
|
* includes the time, as required by `AstroTime`.
|
|
*
|
|
* @param {Spherical} sphere
|
|
* Spherical coordinates to be converted.
|
|
*
|
|
* @param {AstroTime} time
|
|
* The time that should be included in the returned vector.
|
|
*
|
|
* @returns {Vector}
|
|
* The vector form of the supplied spherical coordinates.
|
|
*/
|
|
function VectorFromSphere(sphere, time) {
|
|
const radlat = sphere.lat * exports.DEG2RAD;
|
|
const radlon = sphere.lon * exports.DEG2RAD;
|
|
const rcoslat = sphere.dist * Math.cos(radlat);
|
|
return new Vector(rcoslat * Math.cos(radlon), rcoslat * Math.sin(radlon), sphere.dist * Math.sin(radlat), time);
|
|
}
|
|
exports.VectorFromSphere = VectorFromSphere;
|
|
/**
|
|
* @brief Given an equatorial vector, calculates equatorial angular coordinates.
|
|
*
|
|
* @param {Vector} vec
|
|
* A vector in an equatorial coordinate system.
|
|
*
|
|
* @returns {EquatorialCoordinates}
|
|
* Angular coordinates expressed in the same equatorial system as `vec`.
|
|
*/
|
|
function EquatorFromVector(vec) {
|
|
const sphere = SphereFromVector(vec);
|
|
return new EquatorialCoordinates(sphere.lon / 15, sphere.lat, sphere.dist, vec);
|
|
}
|
|
exports.EquatorFromVector = EquatorFromVector;
|
|
/**
|
|
* @brief Converts Cartesian coordinates to spherical coordinates.
|
|
*
|
|
* Given a Cartesian vector, returns latitude, longitude, and distance.
|
|
*
|
|
* @param {Vector} vector
|
|
* Cartesian vector to be converted to spherical coordinates.
|
|
*
|
|
* @returns {Spherical}
|
|
* Spherical coordinates that are equivalent to the given vector.
|
|
*/
|
|
function SphereFromVector(vector) {
|
|
const xyproj = vector.x * vector.x + vector.y * vector.y;
|
|
const dist = Math.sqrt(xyproj + vector.z * vector.z);
|
|
let lat, lon;
|
|
if (xyproj === 0.0) {
|
|
if (vector.z === 0.0)
|
|
throw 'Zero-length vector not allowed.';
|
|
lon = 0.0;
|
|
lat = (vector.z < 0.0) ? -90.0 : +90.0;
|
|
}
|
|
else {
|
|
lon = exports.RAD2DEG * Math.atan2(vector.y, vector.x);
|
|
if (lon < 0.0)
|
|
lon += 360.0;
|
|
lat = exports.RAD2DEG * Math.atan2(vector.z, Math.sqrt(xyproj));
|
|
}
|
|
return new Spherical(lat, lon, dist);
|
|
}
|
|
exports.SphereFromVector = SphereFromVector;
|
|
function ToggleAzimuthDirection(az) {
|
|
az = 360.0 - az;
|
|
if (az >= 360.0)
|
|
az -= 360.0;
|
|
else if (az < 0.0)
|
|
az += 360.0;
|
|
return az;
|
|
}
|
|
/**
|
|
* @brief Converts Cartesian coordinates to horizontal coordinates.
|
|
*
|
|
* Given a horizontal Cartesian vector, returns horizontal azimuth and altitude.
|
|
*
|
|
* *IMPORTANT:* This function differs from {@link SphereFromVector} in two ways:
|
|
* - `SphereFromVector` returns a `lon` value that represents azimuth defined counterclockwise
|
|
* from north (e.g., west = +90), but this function represents a clockwise rotation
|
|
* (e.g., east = +90). The difference is because `SphereFromVector` is intended
|
|
* to preserve the vector "right-hand rule", while this function defines azimuth in a more
|
|
* traditional way as used in navigation and cartography.
|
|
* - This function optionally corrects for atmospheric refraction, while `SphereFromVector` does not.
|
|
*
|
|
* The returned object contains the azimuth in `lon`.
|
|
* It is measured in degrees clockwise from north: east = +90 degrees, west = +270 degrees.
|
|
*
|
|
* The altitude is stored in `lat`.
|
|
*
|
|
* The distance to the observed object is stored in `dist`,
|
|
* and is expressed in astronomical units (AU).
|
|
*
|
|
* @param {Vector} vector
|
|
* Cartesian vector to be converted to horizontal coordinates.
|
|
*
|
|
* @param {string} refraction
|
|
* `"normal"`: correct altitude for atmospheric refraction (recommended).
|
|
* `"jplhor"`: for JPL Horizons compatibility testing only; not recommended for normal use.
|
|
* `null`: no atmospheric refraction correction is performed.
|
|
*
|
|
* @returns {Spherical}
|
|
*/
|
|
function HorizonFromVector(vector, refraction) {
|
|
const sphere = SphereFromVector(vector);
|
|
sphere.lon = ToggleAzimuthDirection(sphere.lon);
|
|
sphere.lat += Refraction(refraction, sphere.lat);
|
|
return sphere;
|
|
}
|
|
exports.HorizonFromVector = HorizonFromVector;
|
|
/**
|
|
* @brief Given apparent angular horizontal coordinates in `sphere`, calculate horizontal vector.
|
|
*
|
|
* @param {Spherical} sphere
|
|
* A structure that contains apparent horizontal coordinates:
|
|
* `lat` holds the refracted azimuth angle,
|
|
* `lon` holds the azimuth in degrees clockwise from north,
|
|
* and `dist` holds the distance from the observer to the object in AU.
|
|
*
|
|
* @param {AstroTime} time
|
|
* The date and time of the observation. This is needed because the returned
|
|
* vector object requires a valid time value when passed to certain other functions.
|
|
*
|
|
* @param {string} refraction
|
|
* `"normal"`: correct altitude for atmospheric refraction (recommended).
|
|
* `"jplhor"`: for JPL Horizons compatibility testing only; not recommended for normal use.
|
|
* `null`: no atmospheric refraction correction is performed.
|
|
*
|
|
* @returns {Vector}
|
|
* A vector in the horizontal system: `x` = north, `y` = west, and `z` = zenith (up).
|
|
*/
|
|
function VectorFromHorizon(sphere, time, refraction) {
|
|
/* Convert azimuth from clockwise-from-north to counterclockwise-from-north. */
|
|
const lon = ToggleAzimuthDirection(sphere.lon);
|
|
/* Reverse any applied refraction. */
|
|
const lat = sphere.lat + InverseRefraction(refraction, sphere.lat);
|
|
const xsphere = new Spherical(lat, lon, sphere.dist);
|
|
return VectorFromSphere(xsphere, time);
|
|
}
|
|
exports.VectorFromHorizon = VectorFromHorizon;
|
|
/**
|
|
* @brief Calculates the amount of "lift" to an altitude angle caused by atmospheric refraction.
|
|
*
|
|
* Given an altitude angle and a refraction option, calculates
|
|
* the amount of "lift" caused by atmospheric refraction.
|
|
* This is the number of degrees higher in the sky an object appears
|
|
* due to the lensing of the Earth's atmosphere.
|
|
*
|
|
* @param {string} refraction
|
|
* `"normal"`: correct altitude for atmospheric refraction (recommended).
|
|
* `"jplhor"`: for JPL Horizons compatibility testing only; not recommended for normal use.
|
|
* `null`: no atmospheric refraction correction is performed.
|
|
*
|
|
* @param {number} altitude
|
|
* An altitude angle in a horizontal coordinate system. Must be a value between -90 and +90.
|
|
*
|
|
* @returns {number}
|
|
* The angular adjustment in degrees to be added to the altitude angle to correct for atmospheric lensing.
|
|
*/
|
|
function Refraction(refraction, altitude) {
|
|
let refr;
|
|
VerifyNumber(altitude);
|
|
if (altitude < -90.0 || altitude > +90.0)
|
|
return 0.0; /* no attempt to correct an invalid altitude */
|
|
if (refraction === 'normal' || refraction === 'jplhor') {
|
|
// http://extras.springer.com/1999/978-1-4471-0555-8/chap4/horizons/horizons.pdf
|
|
// JPL Horizons says it uses refraction algorithm from
|
|
// Meeus "Astronomical Algorithms", 1991, p. 101-102.
|
|
// I found the following Go implementation:
|
|
// https://github.com/soniakeys/meeus/blob/master/v3/refraction/refract.go
|
|
// This is a translation from the function "Saemundsson" there.
|
|
// I found experimentally that JPL Horizons clamps the angle to 1 degree below the horizon.
|
|
// This is important because the 'refr' formula below goes crazy near hd = -5.11.
|
|
let hd = altitude;
|
|
if (hd < -1.0)
|
|
hd = -1.0;
|
|
refr = (1.02 / Math.tan((hd + 10.3 / (hd + 5.11)) * exports.DEG2RAD)) / 60.0;
|
|
if (refraction === 'normal' && altitude < -1.0) {
|
|
// In "normal" mode we gradually reduce refraction toward the nadir
|
|
// so that we never get an altitude angle less than -90 degrees.
|
|
// When horizon angle is -1 degrees, the factor is exactly 1.
|
|
// As altitude approaches -90 (the nadir), the fraction approaches 0 linearly.
|
|
refr *= (altitude + 90.0) / 89.0;
|
|
}
|
|
}
|
|
else if (!refraction) {
|
|
// The caller does not want refraction correction.
|
|
refr = 0.0;
|
|
}
|
|
else {
|
|
throw `Invalid refraction option: ${refraction}`;
|
|
}
|
|
return refr;
|
|
}
|
|
exports.Refraction = Refraction;
|
|
/**
|
|
* @brief Calculates the inverse of an atmospheric refraction angle.
|
|
*
|
|
* Given an observed altitude angle that includes atmospheric refraction,
|
|
* calculate the negative angular correction to obtain the unrefracted
|
|
* altitude. This is useful for cases where observed horizontal
|
|
* coordinates are to be converted to another orientation system,
|
|
* but refraction first must be removed from the observed position.
|
|
*
|
|
* @param {string} refraction
|
|
* `"normal"`: correct altitude for atmospheric refraction (recommended).
|
|
* `"jplhor"`: for JPL Horizons compatibility testing only; not recommended for normal use.
|
|
* `null`: no atmospheric refraction correction is performed.
|
|
*
|
|
* @param {number} bent_altitude
|
|
* The apparent altitude that includes atmospheric refraction.
|
|
*
|
|
* @returns {number}
|
|
* The angular adjustment in degrees to be added to the
|
|
* altitude angle to correct for atmospheric lensing.
|
|
* This will be less than or equal to zero.
|
|
*/
|
|
function InverseRefraction(refraction, bent_altitude) {
|
|
if (bent_altitude < -90.0 || bent_altitude > +90.0)
|
|
return 0.0; /* no attempt to correct an invalid altitude */
|
|
/* Find the pre-adjusted altitude whose refraction correction leads to 'altitude'. */
|
|
let altitude = bent_altitude - Refraction(refraction, bent_altitude);
|
|
for (;;) {
|
|
/* See how close we got. */
|
|
let diff = (altitude + Refraction(refraction, altitude)) - bent_altitude;
|
|
if (Math.abs(diff) < 1.0e-14)
|
|
return altitude - bent_altitude;
|
|
altitude -= diff;
|
|
}
|
|
}
|
|
exports.InverseRefraction = InverseRefraction;
|
|
/**
|
|
* @brief Applies a rotation to a vector, yielding a rotated vector.
|
|
*
|
|
* This function transforms a vector in one orientation to a vector
|
|
* in another orientation.
|
|
*
|
|
* @param {RotationMatrix} rotation
|
|
* A rotation matrix that specifies how the orientation of the vector is to be changed.
|
|
*
|
|
* @param {Vector} vector
|
|
* The vector whose orientation is to be changed.
|
|
*
|
|
* @returns {Vector}
|
|
* A vector in the orientation specified by `rotation`.
|
|
*/
|
|
function RotateVector(rotation, vector) {
|
|
return new Vector(rotation.rot[0][0] * vector.x + rotation.rot[1][0] * vector.y + rotation.rot[2][0] * vector.z, rotation.rot[0][1] * vector.x + rotation.rot[1][1] * vector.y + rotation.rot[2][1] * vector.z, rotation.rot[0][2] * vector.x + rotation.rot[1][2] * vector.y + rotation.rot[2][2] * vector.z, vector.t);
|
|
}
|
|
exports.RotateVector = RotateVector;
|
|
/**
|
|
* @brief Applies a rotation to a state vector, yielding a rotated vector.
|
|
*
|
|
* This function transforms a state vector in one orientation to a vector
|
|
* in another orientation.
|
|
*
|
|
* @param {RotationMatrix} rotation
|
|
* A rotation matrix that specifies how the orientation of the state vector is to be changed.
|
|
*
|
|
* @param {StateVector} state
|
|
* The state vector whose orientation is to be changed.
|
|
* Both the position and velocity components are transformed.
|
|
*
|
|
* @return {StateVector}
|
|
* A state vector in the orientation specified by `rotation`.
|
|
*/
|
|
function RotateState(rotation, state) {
|
|
return new StateVector(rotation.rot[0][0] * state.x + rotation.rot[1][0] * state.y + rotation.rot[2][0] * state.z, rotation.rot[0][1] * state.x + rotation.rot[1][1] * state.y + rotation.rot[2][1] * state.z, rotation.rot[0][2] * state.x + rotation.rot[1][2] * state.y + rotation.rot[2][2] * state.z, rotation.rot[0][0] * state.vx + rotation.rot[1][0] * state.vy + rotation.rot[2][0] * state.vz, rotation.rot[0][1] * state.vx + rotation.rot[1][1] * state.vy + rotation.rot[2][1] * state.vz, rotation.rot[0][2] * state.vx + rotation.rot[1][2] * state.vy + rotation.rot[2][2] * state.vz, state.t);
|
|
}
|
|
exports.RotateState = RotateState;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial J2000 (EQJ) to ecliptic J2000 (ECL).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQJ = equatorial system, using equator at J2000 epoch.
|
|
* Target: ECL = ecliptic system, using equator at J2000 epoch.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQJ to ECL.
|
|
*/
|
|
function Rotation_EQJ_ECL() {
|
|
/* ob = mean obliquity of the J2000 ecliptic = 0.40909260059599012 radians. */
|
|
const c = 0.9174821430670688; /* cos(ob) */
|
|
const s = 0.3977769691083922; /* sin(ob) */
|
|
return new RotationMatrix([
|
|
[1, 0, 0],
|
|
[0, +c, -s],
|
|
[0, +s, +c]
|
|
]);
|
|
}
|
|
exports.Rotation_EQJ_ECL = Rotation_EQJ_ECL;
|
|
/**
|
|
* @brief Calculates a rotation matrix from ecliptic J2000 (ECL) to equatorial J2000 (EQJ).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: ECL = ecliptic system, using equator at J2000 epoch.
|
|
* Target: EQJ = equatorial system, using equator at J2000 epoch.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts ECL to EQJ.
|
|
*/
|
|
function Rotation_ECL_EQJ() {
|
|
/* ob = mean obliquity of the J2000 ecliptic = 0.40909260059599012 radians. */
|
|
const c = 0.9174821430670688; /* cos(ob) */
|
|
const s = 0.3977769691083922; /* sin(ob) */
|
|
return new RotationMatrix([
|
|
[1, 0, 0],
|
|
[0, +c, +s],
|
|
[0, -s, +c]
|
|
]);
|
|
}
|
|
exports.Rotation_ECL_EQJ = Rotation_ECL_EQJ;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial J2000 (EQJ) to equatorial of-date (EQD).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQJ = equatorial system, using equator at J2000 epoch.
|
|
* Target: EQD = equatorial system, using equator of the specified date/time.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time at which the Earth's equator defines the target orientation.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQJ to EQD at `time`.
|
|
*/
|
|
function Rotation_EQJ_EQD(time) {
|
|
time = MakeTime(time);
|
|
const prec = precession_rot(time, PrecessDirection.From2000);
|
|
const nut = nutation_rot(time, PrecessDirection.From2000);
|
|
return CombineRotation(prec, nut);
|
|
}
|
|
exports.Rotation_EQJ_EQD = Rotation_EQJ_EQD;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial of-date (EQD) to equatorial J2000 (EQJ).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQD = equatorial system, using equator of the specified date/time.
|
|
* Target: EQJ = equatorial system, using equator at J2000 epoch.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time at which the Earth's equator defines the source orientation.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQD at `time` to EQJ.
|
|
*/
|
|
function Rotation_EQD_EQJ(time) {
|
|
time = MakeTime(time);
|
|
const nut = nutation_rot(time, PrecessDirection.Into2000);
|
|
const prec = precession_rot(time, PrecessDirection.Into2000);
|
|
return CombineRotation(nut, prec);
|
|
}
|
|
exports.Rotation_EQD_EQJ = Rotation_EQD_EQJ;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial of-date (EQD) to horizontal (HOR).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQD = equatorial system, using equator of the specified date/time.
|
|
* Target: HOR = horizontal system.
|
|
*
|
|
* Use `HorizonFromVector` to convert the return value
|
|
* to a traditional altitude/azimuth pair.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time at which the Earth's equator applies.
|
|
*
|
|
* @param {Observer} observer
|
|
* A location near the Earth's mean sea level that defines the observer's horizon.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQD to HOR at `time` and for `observer`.
|
|
* The components of the horizontal vector are:
|
|
* x = north, y = west, z = zenith (straight up from the observer).
|
|
* These components are chosen so that the "right-hand rule" works for the vector
|
|
* and so that north represents the direction where azimuth = 0.
|
|
*/
|
|
function Rotation_EQD_HOR(time, observer) {
|
|
time = MakeTime(time);
|
|
const sinlat = Math.sin(observer.latitude * exports.DEG2RAD);
|
|
const coslat = Math.cos(observer.latitude * exports.DEG2RAD);
|
|
const sinlon = Math.sin(observer.longitude * exports.DEG2RAD);
|
|
const coslon = Math.cos(observer.longitude * exports.DEG2RAD);
|
|
const uze = [coslat * coslon, coslat * sinlon, sinlat];
|
|
const une = [-sinlat * coslon, -sinlat * sinlon, coslat];
|
|
const uwe = [sinlon, -coslon, 0];
|
|
const spin_angle = -15 * sidereal_time(time);
|
|
const uz = spin(spin_angle, uze);
|
|
const un = spin(spin_angle, une);
|
|
const uw = spin(spin_angle, uwe);
|
|
return new RotationMatrix([
|
|
[un[0], uw[0], uz[0]],
|
|
[un[1], uw[1], uz[1]],
|
|
[un[2], uw[2], uz[2]],
|
|
]);
|
|
}
|
|
exports.Rotation_EQD_HOR = Rotation_EQD_HOR;
|
|
/**
|
|
* @brief Calculates a rotation matrix from horizontal (HOR) to equatorial of-date (EQD).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: HOR = horizontal system (x=North, y=West, z=Zenith).
|
|
* Target: EQD = equatorial system, using equator of the specified date/time.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time at which the Earth's equator applies.
|
|
*
|
|
* @param {Observer} observer
|
|
* A location near the Earth's mean sea level that defines the observer's horizon.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts HOR to EQD at `time` and for `observer`.
|
|
*/
|
|
function Rotation_HOR_EQD(time, observer) {
|
|
const rot = Rotation_EQD_HOR(time, observer);
|
|
return InverseRotation(rot);
|
|
}
|
|
exports.Rotation_HOR_EQD = Rotation_HOR_EQD;
|
|
/**
|
|
* @brief Calculates a rotation matrix from horizontal (HOR) to J2000 equatorial (EQJ).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: HOR = horizontal system (x=North, y=West, z=Zenith).
|
|
* Target: EQJ = equatorial system, using equator at the J2000 epoch.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the observation.
|
|
*
|
|
* @param {Observer} observer
|
|
* A location near the Earth's mean sea level that defines the observer's horizon.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts HOR to EQD at `time` and for `observer`.
|
|
*/
|
|
function Rotation_HOR_EQJ(time, observer) {
|
|
time = MakeTime(time);
|
|
const hor_eqd = Rotation_HOR_EQD(time, observer);
|
|
const eqd_eqj = Rotation_EQD_EQJ(time);
|
|
return CombineRotation(hor_eqd, eqd_eqj);
|
|
}
|
|
exports.Rotation_HOR_EQJ = Rotation_HOR_EQJ;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial J2000 (EQJ) to horizontal (HOR).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQJ = equatorial system, using the equator at the J2000 epoch.
|
|
* Target: HOR = horizontal system.
|
|
*
|
|
* Use {@link HorizonFromVector} to convert the return value
|
|
* to a traditional altitude/azimuth pair.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the desired horizontal orientation.
|
|
*
|
|
* @param {Observer} observer
|
|
* A location near the Earth's mean sea level that defines the observer's horizon.
|
|
*
|
|
* @return
|
|
* A rotation matrix that converts EQJ to HOR at `time` and for `observer`.
|
|
* The components of the horizontal vector are:
|
|
* x = north, y = west, z = zenith (straight up from the observer).
|
|
* These components are chosen so that the "right-hand rule" works for the vector
|
|
* and so that north represents the direction where azimuth = 0.
|
|
*/
|
|
function Rotation_EQJ_HOR(time, observer) {
|
|
const rot = Rotation_HOR_EQJ(time, observer);
|
|
return InverseRotation(rot);
|
|
}
|
|
exports.Rotation_EQJ_HOR = Rotation_EQJ_HOR;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial of-date (EQD) to ecliptic J2000 (ECL).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQD = equatorial system, using equator of date.
|
|
* Target: ECL = ecliptic system, using equator at J2000 epoch.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the source equator.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQD to ECL.
|
|
*/
|
|
function Rotation_EQD_ECL(time) {
|
|
const eqd_eqj = Rotation_EQD_EQJ(time);
|
|
const eqj_ecl = Rotation_EQJ_ECL();
|
|
return CombineRotation(eqd_eqj, eqj_ecl);
|
|
}
|
|
exports.Rotation_EQD_ECL = Rotation_EQD_ECL;
|
|
/**
|
|
* @brief Calculates a rotation matrix from ecliptic J2000 (ECL) to equatorial of-date (EQD).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: ECL = ecliptic system, using equator at J2000 epoch.
|
|
* Target: EQD = equatorial system, using equator of date.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the desired equator.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts ECL to EQD.
|
|
*/
|
|
function Rotation_ECL_EQD(time) {
|
|
const rot = Rotation_EQD_ECL(time);
|
|
return InverseRotation(rot);
|
|
}
|
|
exports.Rotation_ECL_EQD = Rotation_ECL_EQD;
|
|
/**
|
|
* @brief Calculates a rotation matrix from ecliptic J2000 (ECL) to horizontal (HOR).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: ECL = ecliptic system, using equator at J2000 epoch.
|
|
* Target: HOR = horizontal system.
|
|
*
|
|
* Use {@link HorizonFromVector} to convert the return value
|
|
* to a traditional altitude/azimuth pair.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the desired horizontal orientation.
|
|
*
|
|
* @param {Observer} observer
|
|
* A location near the Earth's mean sea level that defines the observer's horizon.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts ECL to HOR at `time` and for `observer`.
|
|
* The components of the horizontal vector are:
|
|
* x = north, y = west, z = zenith (straight up from the observer).
|
|
* These components are chosen so that the "right-hand rule" works for the vector
|
|
* and so that north represents the direction where azimuth = 0.
|
|
*/
|
|
function Rotation_ECL_HOR(time, observer) {
|
|
time = MakeTime(time);
|
|
const ecl_eqd = Rotation_ECL_EQD(time);
|
|
const eqd_hor = Rotation_EQD_HOR(time, observer);
|
|
return CombineRotation(ecl_eqd, eqd_hor);
|
|
}
|
|
exports.Rotation_ECL_HOR = Rotation_ECL_HOR;
|
|
/**
|
|
* @brief Calculates a rotation matrix from horizontal (HOR) to ecliptic J2000 (ECL).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: HOR = horizontal system.
|
|
* Target: ECL = ecliptic system, using equator at J2000 epoch.
|
|
*
|
|
* @param {FlexibleDateTime} time
|
|
* The date and time of the horizontal observation.
|
|
*
|
|
* @param {Observer} observer
|
|
* The location of the horizontal observer.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts HOR to ECL.
|
|
*/
|
|
function Rotation_HOR_ECL(time, observer) {
|
|
const rot = Rotation_ECL_HOR(time, observer);
|
|
return InverseRotation(rot);
|
|
}
|
|
exports.Rotation_HOR_ECL = Rotation_HOR_ECL;
|
|
/**
|
|
* @brief Calculates a rotation matrix from equatorial J2000 (EQJ) to galactic (GAL).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: EQJ = equatorial system, using the equator at the J2000 epoch.
|
|
* Target: GAL = galactic system (IAU 1958 definition).
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts EQJ to GAL.
|
|
*/
|
|
function Rotation_EQJ_GAL() {
|
|
// This rotation matrix was calculated by the following script
|
|
// in this same source code repository:
|
|
// demo/python/galeqj_matrix.py
|
|
return new RotationMatrix([
|
|
[-0.0548624779711344, +0.4941095946388765, -0.8676668813529025],
|
|
[-0.8734572784246782, -0.4447938112296831, -0.1980677870294097],
|
|
[-0.4838000529948520, +0.7470034631630423, +0.4559861124470794]
|
|
]);
|
|
}
|
|
exports.Rotation_EQJ_GAL = Rotation_EQJ_GAL;
|
|
/**
|
|
* @brief Calculates a rotation matrix from galactic (GAL) to equatorial J2000 (EQJ).
|
|
*
|
|
* This is one of the family of functions that returns a rotation matrix
|
|
* for converting from one orientation to another.
|
|
* Source: GAL = galactic system (IAU 1958 definition).
|
|
* Target: EQJ = equatorial system, using the equator at the J2000 epoch.
|
|
*
|
|
* @returns {RotationMatrix}
|
|
* A rotation matrix that converts GAL to EQJ.
|
|
*/
|
|
function Rotation_GAL_EQJ() {
|
|
// This rotation matrix was calculated by the following script
|
|
// in this same source code repository:
|
|
// demo/python/galeqj_matrix.py
|
|
return new RotationMatrix([
|
|
[-0.0548624779711344, -0.8734572784246782, -0.4838000529948520],
|
|
[+0.4941095946388765, -0.4447938112296831, +0.7470034631630423],
|
|
[-0.8676668813529025, -0.1980677870294097, +0.4559861124470794]
|
|
]);
|
|
}
|
|
exports.Rotation_GAL_EQJ = Rotation_GAL_EQJ;
|
|
const ConstelNames = [
|
|
['And', 'Andromeda'] // 0
|
|
,
|
|
['Ant', 'Antila'] // 1
|
|
,
|
|
['Aps', 'Apus'] // 2
|
|
,
|
|
['Aql', 'Aquila'] // 3
|
|
,
|
|
['Aqr', 'Aquarius'] // 4
|
|
,
|
|
['Ara', 'Ara'] // 5
|
|
,
|
|
['Ari', 'Aries'] // 6
|
|
,
|
|
['Aur', 'Auriga'] // 7
|
|
,
|
|
['Boo', 'Bootes'] // 8
|
|
,
|
|
['Cae', 'Caelum'] // 9
|
|
,
|
|
['Cam', 'Camelopardis'] // 10
|
|
,
|
|
['Cap', 'Capricornus'] // 11
|
|
,
|
|
['Car', 'Carina'] // 12
|
|
,
|
|
['Cas', 'Cassiopeia'] // 13
|
|
,
|
|
['Cen', 'Centaurus'] // 14
|
|
,
|
|
['Cep', 'Cepheus'] // 15
|
|
,
|
|
['Cet', 'Cetus'] // 16
|
|
,
|
|
['Cha', 'Chamaeleon'] // 17
|
|
,
|
|
['Cir', 'Circinus'] // 18
|
|
,
|
|
['CMa', 'Canis Major'] // 19
|
|
,
|
|
['CMi', 'Canis Minor'] // 20
|
|
,
|
|
['Cnc', 'Cancer'] // 21
|
|
,
|
|
['Col', 'Columba'] // 22
|
|
,
|
|
['Com', 'Coma Berenices'] // 23
|
|
,
|
|
['CrA', 'Corona Australis'] // 24
|
|
,
|
|
['CrB', 'Corona Borealis'] // 25
|
|
,
|
|
['Crt', 'Crater'] // 26
|
|
,
|
|
['Cru', 'Crux'] // 27
|
|
,
|
|
['Crv', 'Corvus'] // 28
|
|
,
|
|
['CVn', 'Canes Venatici'] // 29
|
|
,
|
|
['Cyg', 'Cygnus'] // 30
|
|
,
|
|
['Del', 'Delphinus'] // 31
|
|
,
|
|
['Dor', 'Dorado'] // 32
|
|
,
|
|
['Dra', 'Draco'] // 33
|
|
,
|
|
['Equ', 'Equuleus'] // 34
|
|
,
|
|
['Eri', 'Eridanus'] // 35
|
|
,
|
|
['For', 'Fornax'] // 36
|
|
,
|
|
['Gem', 'Gemini'] // 37
|
|
,
|
|
['Gru', 'Grus'] // 38
|
|
,
|
|
['Her', 'Hercules'] // 39
|
|
,
|
|
['Hor', 'Horologium'] // 40
|
|
,
|
|
['Hya', 'Hydra'] // 41
|
|
,
|
|
['Hyi', 'Hydrus'] // 42
|
|
,
|
|
['Ind', 'Indus'] // 43
|
|
,
|
|
['Lac', 'Lacerta'] // 44
|
|
,
|
|
['Leo', 'Leo'] // 45
|
|
,
|
|
['Lep', 'Lepus'] // 46
|
|
,
|
|
['Lib', 'Libra'] // 47
|
|
,
|
|
['LMi', 'Leo Minor'] // 48
|
|
,
|
|
['Lup', 'Lupus'] // 49
|
|
,
|
|
['Lyn', 'Lynx'] // 50
|
|
,
|
|
['Lyr', 'Lyra'] // 51
|
|
,
|
|
['Men', 'Mensa'] // 52
|
|
,
|
|
['Mic', 'Microscopium'] // 53
|
|
,
|
|
['Mon', 'Monoceros'] // 54
|
|
,
|
|
['Mus', 'Musca'] // 55
|
|
,
|
|
['Nor', 'Norma'] // 56
|
|
,
|
|
['Oct', 'Octans'] // 57
|
|
,
|
|
['Oph', 'Ophiuchus'] // 58
|
|
,
|
|
['Ori', 'Orion'] // 59
|
|
,
|
|
['Pav', 'Pavo'] // 60
|
|
,
|
|
['Peg', 'Pegasus'] // 61
|
|
,
|
|
['Per', 'Perseus'] // 62
|
|
,
|
|
['Phe', 'Phoenix'] // 63
|
|
,
|
|
['Pic', 'Pictor'] // 64
|
|
,
|
|
['PsA', 'Pisces Austrinus'] // 65
|
|
,
|
|
['Psc', 'Pisces'] // 66
|
|
,
|
|
['Pup', 'Puppis'] // 67
|
|
,
|
|
['Pyx', 'Pyxis'] // 68
|
|
,
|
|
['Ret', 'Reticulum'] // 69
|
|
,
|
|
['Scl', 'Sculptor'] // 70
|
|
,
|
|
['Sco', 'Scorpius'] // 71
|
|
,
|
|
['Sct', 'Scutum'] // 72
|
|
,
|
|
['Ser', 'Serpens'] // 73
|
|
,
|
|
['Sex', 'Sextans'] // 74
|
|
,
|
|
['Sge', 'Sagitta'] // 75
|
|
,
|
|
['Sgr', 'Sagittarius'] // 76
|
|
,
|
|
['Tau', 'Taurus'] // 77
|
|
,
|
|
['Tel', 'Telescopium'] // 78
|
|
,
|
|
['TrA', 'Triangulum Australe'] // 79
|
|
,
|
|
['Tri', 'Triangulum'] // 80
|
|
,
|
|
['Tuc', 'Tucana'] // 81
|
|
,
|
|
['UMa', 'Ursa Major'] // 82
|
|
,
|
|
['UMi', 'Ursa Minor'] // 83
|
|
,
|
|
['Vel', 'Vela'] // 84
|
|
,
|
|
['Vir', 'Virgo'] // 85
|
|
,
|
|
['Vol', 'Volans'] // 86
|
|
,
|
|
['Vul', 'Vulpecula'] // 87
|
|
];
|
|
const ConstelBounds = [
|
|
[83, 0, 8640, 2112] // UMi
|
|
,
|
|
[83, 2880, 5220, 2076] // UMi
|
|
,
|
|
[83, 7560, 8280, 2068] // UMi
|
|
,
|
|
[83, 6480, 7560, 2064] // UMi
|
|
,
|
|
[15, 0, 2880, 2040] // Cep
|
|
,
|
|
[10, 3300, 3840, 1968] // Cam
|
|
,
|
|
[15, 0, 1800, 1920] // Cep
|
|
,
|
|
[10, 3840, 5220, 1920] // Cam
|
|
,
|
|
[83, 6300, 6480, 1920] // UMi
|
|
,
|
|
[33, 7260, 7560, 1920] // Dra
|
|
,
|
|
[15, 0, 1263, 1848] // Cep
|
|
,
|
|
[10, 4140, 4890, 1848] // Cam
|
|
,
|
|
[83, 5952, 6300, 1800] // UMi
|
|
,
|
|
[15, 7260, 7440, 1800] // Cep
|
|
,
|
|
[10, 2868, 3300, 1764] // Cam
|
|
,
|
|
[33, 3300, 4080, 1764] // Dra
|
|
,
|
|
[83, 4680, 5952, 1680] // UMi
|
|
,
|
|
[13, 1116, 1230, 1632] // Cas
|
|
,
|
|
[33, 7350, 7440, 1608] // Dra
|
|
,
|
|
[33, 4080, 4320, 1596] // Dra
|
|
,
|
|
[15, 0, 120, 1584] // Cep
|
|
,
|
|
[83, 5040, 5640, 1584] // UMi
|
|
,
|
|
[15, 8490, 8640, 1584] // Cep
|
|
,
|
|
[33, 4320, 4860, 1536] // Dra
|
|
,
|
|
[33, 4860, 5190, 1512] // Dra
|
|
,
|
|
[15, 8340, 8490, 1512] // Cep
|
|
,
|
|
[10, 2196, 2520, 1488] // Cam
|
|
,
|
|
[33, 7200, 7350, 1476] // Dra
|
|
,
|
|
[15, 7393.2, 7416, 1462] // Cep
|
|
,
|
|
[10, 2520, 2868, 1440] // Cam
|
|
,
|
|
[82, 2868, 3030, 1440] // UMa
|
|
,
|
|
[33, 7116, 7200, 1428] // Dra
|
|
,
|
|
[15, 7200, 7393.2, 1428] // Cep
|
|
,
|
|
[15, 8232, 8340, 1418] // Cep
|
|
,
|
|
[13, 0, 876, 1404] // Cas
|
|
,
|
|
[33, 6990, 7116, 1392] // Dra
|
|
,
|
|
[13, 612, 687, 1380] // Cas
|
|
,
|
|
[13, 876, 1116, 1368] // Cas
|
|
,
|
|
[10, 1116, 1140, 1368] // Cam
|
|
,
|
|
[15, 8034, 8232, 1350] // Cep
|
|
,
|
|
[10, 1800, 2196, 1344] // Cam
|
|
,
|
|
[82, 5052, 5190, 1332] // UMa
|
|
,
|
|
[33, 5190, 6990, 1332] // Dra
|
|
,
|
|
[10, 1140, 1200, 1320] // Cam
|
|
,
|
|
[15, 7968, 8034, 1320] // Cep
|
|
,
|
|
[15, 7416, 7908, 1316] // Cep
|
|
,
|
|
[13, 0, 612, 1296] // Cas
|
|
,
|
|
[50, 2196, 2340, 1296] // Lyn
|
|
,
|
|
[82, 4350, 4860, 1272] // UMa
|
|
,
|
|
[33, 5490, 5670, 1272] // Dra
|
|
,
|
|
[15, 7908, 7968, 1266] // Cep
|
|
,
|
|
[10, 1200, 1800, 1260] // Cam
|
|
,
|
|
[13, 8232, 8400, 1260] // Cas
|
|
,
|
|
[33, 5670, 6120, 1236] // Dra
|
|
,
|
|
[62, 735, 906, 1212] // Per
|
|
,
|
|
[33, 6120, 6564, 1212] // Dra
|
|
,
|
|
[13, 0, 492, 1200] // Cas
|
|
,
|
|
[62, 492, 600, 1200] // Per
|
|
,
|
|
[50, 2340, 2448, 1200] // Lyn
|
|
,
|
|
[13, 8400, 8640, 1200] // Cas
|
|
,
|
|
[82, 4860, 5052, 1164] // UMa
|
|
,
|
|
[13, 0, 402, 1152] // Cas
|
|
,
|
|
[13, 8490, 8640, 1152] // Cas
|
|
,
|
|
[39, 6543, 6564, 1140] // Her
|
|
,
|
|
[33, 6564, 6870, 1140] // Dra
|
|
,
|
|
[30, 6870, 6900, 1140] // Cyg
|
|
,
|
|
[62, 600, 735, 1128] // Per
|
|
,
|
|
[82, 3030, 3300, 1128] // UMa
|
|
,
|
|
[13, 60, 312, 1104] // Cas
|
|
,
|
|
[82, 4320, 4350, 1080] // UMa
|
|
,
|
|
[50, 2448, 2652, 1068] // Lyn
|
|
,
|
|
[30, 7887, 7908, 1056] // Cyg
|
|
,
|
|
[30, 7875, 7887, 1050] // Cyg
|
|
,
|
|
[30, 6900, 6984, 1044] // Cyg
|
|
,
|
|
[82, 3300, 3660, 1008] // UMa
|
|
,
|
|
[82, 3660, 3882, 960] // UMa
|
|
,
|
|
[8, 5556, 5670, 960] // Boo
|
|
,
|
|
[39, 5670, 5880, 960] // Her
|
|
,
|
|
[50, 3330, 3450, 954] // Lyn
|
|
,
|
|
[0, 0, 906, 882] // And
|
|
,
|
|
[62, 906, 924, 882] // Per
|
|
,
|
|
[51, 6969, 6984, 876] // Lyr
|
|
,
|
|
[62, 1620, 1689, 864] // Per
|
|
,
|
|
[30, 7824, 7875, 864] // Cyg
|
|
,
|
|
[44, 7875, 7920, 864] // Lac
|
|
,
|
|
[7, 2352, 2652, 852] // Aur
|
|
,
|
|
[50, 2652, 2790, 852] // Lyn
|
|
,
|
|
[0, 0, 720, 840] // And
|
|
,
|
|
[44, 7920, 8214, 840] // Lac
|
|
,
|
|
[44, 8214, 8232, 828] // Lac
|
|
,
|
|
[0, 8232, 8460, 828] // And
|
|
,
|
|
[62, 924, 978, 816] // Per
|
|
,
|
|
[82, 3882, 3960, 816] // UMa
|
|
,
|
|
[29, 4320, 4440, 816] // CVn
|
|
,
|
|
[50, 2790, 3330, 804] // Lyn
|
|
,
|
|
[48, 3330, 3558, 804] // LMi
|
|
,
|
|
[0, 258, 507, 792] // And
|
|
,
|
|
[8, 5466, 5556, 792] // Boo
|
|
,
|
|
[0, 8460, 8550, 770] // And
|
|
,
|
|
[29, 4440, 4770, 768] // CVn
|
|
,
|
|
[0, 8550, 8640, 752] // And
|
|
,
|
|
[29, 5025, 5052, 738] // CVn
|
|
,
|
|
[80, 870, 978, 736] // Tri
|
|
,
|
|
[62, 978, 1620, 736] // Per
|
|
,
|
|
[7, 1620, 1710, 720] // Aur
|
|
,
|
|
[51, 6543, 6969, 720] // Lyr
|
|
,
|
|
[82, 3960, 4320, 696] // UMa
|
|
,
|
|
[30, 7080, 7530, 696] // Cyg
|
|
,
|
|
[7, 1710, 2118, 684] // Aur
|
|
,
|
|
[48, 3558, 3780, 684] // LMi
|
|
,
|
|
[29, 4770, 5025, 684] // CVn
|
|
,
|
|
[0, 0, 24, 672] // And
|
|
,
|
|
[80, 507, 600, 672] // Tri
|
|
,
|
|
[7, 2118, 2352, 672] // Aur
|
|
,
|
|
[37, 2838, 2880, 672] // Gem
|
|
,
|
|
[30, 7530, 7824, 672] // Cyg
|
|
,
|
|
[30, 6933, 7080, 660] // Cyg
|
|
,
|
|
[80, 690, 870, 654] // Tri
|
|
,
|
|
[25, 5820, 5880, 648] // CrB
|
|
,
|
|
[8, 5430, 5466, 624] // Boo
|
|
,
|
|
[25, 5466, 5820, 624] // CrB
|
|
,
|
|
[51, 6612, 6792, 624] // Lyr
|
|
,
|
|
[48, 3870, 3960, 612] // LMi
|
|
,
|
|
[51, 6792, 6933, 612] // Lyr
|
|
,
|
|
[80, 600, 690, 600] // Tri
|
|
,
|
|
[66, 258, 306, 570] // Psc
|
|
,
|
|
[48, 3780, 3870, 564] // LMi
|
|
,
|
|
[87, 7650, 7710, 564] // Vul
|
|
,
|
|
[77, 2052, 2118, 548] // Tau
|
|
,
|
|
[0, 24, 51, 528] // And
|
|
,
|
|
[73, 5730, 5772, 528] // Ser
|
|
,
|
|
[37, 2118, 2238, 516] // Gem
|
|
,
|
|
[87, 7140, 7290, 510] // Vul
|
|
,
|
|
[87, 6792, 6930, 506] // Vul
|
|
,
|
|
[0, 51, 306, 504] // And
|
|
,
|
|
[87, 7290, 7404, 492] // Vul
|
|
,
|
|
[37, 2811, 2838, 480] // Gem
|
|
,
|
|
[87, 7404, 7650, 468] // Vul
|
|
,
|
|
[87, 6930, 7140, 460] // Vul
|
|
,
|
|
[6, 1182, 1212, 456] // Ari
|
|
,
|
|
[75, 6792, 6840, 444] // Sge
|
|
,
|
|
[59, 2052, 2076, 432] // Ori
|
|
,
|
|
[37, 2238, 2271, 420] // Gem
|
|
,
|
|
[75, 6840, 7140, 388] // Sge
|
|
,
|
|
[77, 1788, 1920, 384] // Tau
|
|
,
|
|
[39, 5730, 5790, 384] // Her
|
|
,
|
|
[75, 7140, 7290, 378] // Sge
|
|
,
|
|
[77, 1662, 1788, 372] // Tau
|
|
,
|
|
[77, 1920, 2016, 372] // Tau
|
|
,
|
|
[23, 4620, 4860, 360] // Com
|
|
,
|
|
[39, 6210, 6570, 344] // Her
|
|
,
|
|
[23, 4272, 4620, 336] // Com
|
|
,
|
|
[37, 2700, 2811, 324] // Gem
|
|
,
|
|
[39, 6030, 6210, 308] // Her
|
|
,
|
|
[61, 0, 51, 300] // Peg
|
|
,
|
|
[77, 2016, 2076, 300] // Tau
|
|
,
|
|
[37, 2520, 2700, 300] // Gem
|
|
,
|
|
[61, 7602, 7680, 300] // Peg
|
|
,
|
|
[37, 2271, 2496, 288] // Gem
|
|
,
|
|
[39, 6570, 6792, 288] // Her
|
|
,
|
|
[31, 7515, 7578, 284] // Del
|
|
,
|
|
[61, 7578, 7602, 284] // Peg
|
|
,
|
|
[45, 4146, 4272, 264] // Leo
|
|
,
|
|
[59, 2247, 2271, 240] // Ori
|
|
,
|
|
[37, 2496, 2520, 240] // Gem
|
|
,
|
|
[21, 2811, 2853, 240] // Cnc
|
|
,
|
|
[61, 8580, 8640, 240] // Peg
|
|
,
|
|
[6, 600, 1182, 238] // Ari
|
|
,
|
|
[31, 7251, 7308, 204] // Del
|
|
,
|
|
[8, 4860, 5430, 192] // Boo
|
|
,
|
|
[61, 8190, 8580, 180] // Peg
|
|
,
|
|
[21, 2853, 3330, 168] // Cnc
|
|
,
|
|
[45, 3330, 3870, 168] // Leo
|
|
,
|
|
[58, 6570, 6718.4, 150] // Oph
|
|
,
|
|
[3, 6718.4, 6792, 150] // Aql
|
|
,
|
|
[31, 7500, 7515, 144] // Del
|
|
,
|
|
[20, 2520, 2526, 132] // CMi
|
|
,
|
|
[73, 6570, 6633, 108] // Ser
|
|
,
|
|
[39, 5790, 6030, 96] // Her
|
|
,
|
|
[58, 6570, 6633, 72] // Oph
|
|
,
|
|
[61, 7728, 7800, 66] // Peg
|
|
,
|
|
[66, 0, 720, 48] // Psc
|
|
,
|
|
[73, 6690, 6792, 48] // Ser
|
|
,
|
|
[31, 7308, 7500, 48] // Del
|
|
,
|
|
[34, 7500, 7680, 48] // Equ
|
|
,
|
|
[61, 7680, 7728, 48] // Peg
|
|
,
|
|
[61, 7920, 8190, 48] // Peg
|
|
,
|
|
[61, 7800, 7920, 42] // Peg
|
|
,
|
|
[20, 2526, 2592, 36] // CMi
|
|
,
|
|
[77, 1290, 1662, 0] // Tau
|
|
,
|
|
[59, 1662, 1680, 0] // Ori
|
|
,
|
|
[20, 2592, 2910, 0] // CMi
|
|
,
|
|
[85, 5280, 5430, 0] // Vir
|
|
,
|
|
[58, 6420, 6570, 0] // Oph
|
|
,
|
|
[16, 954, 1182, -42] // Cet
|
|
,
|
|
[77, 1182, 1290, -42] // Tau
|
|
,
|
|
[73, 5430, 5856, -78] // Ser
|
|
,
|
|
[59, 1680, 1830, -96] // Ori
|
|
,
|
|
[59, 2100, 2247, -96] // Ori
|
|
,
|
|
[73, 6420, 6468, -96] // Ser
|
|
,
|
|
[73, 6570, 6690, -96] // Ser
|
|
,
|
|
[3, 6690, 6792, -96] // Aql
|
|
,
|
|
[66, 8190, 8580, -96] // Psc
|
|
,
|
|
[45, 3870, 4146, -144] // Leo
|
|
,
|
|
[85, 4146, 4260, -144] // Vir
|
|
,
|
|
[66, 0, 120, -168] // Psc
|
|
,
|
|
[66, 8580, 8640, -168] // Psc
|
|
,
|
|
[85, 5130, 5280, -192] // Vir
|
|
,
|
|
[58, 5730, 5856, -192] // Oph
|
|
,
|
|
[3, 7200, 7392, -216] // Aql
|
|
,
|
|
[4, 7680, 7872, -216] // Aqr
|
|
,
|
|
[58, 6180, 6468, -240] // Oph
|
|
,
|
|
[54, 2100, 2910, -264] // Mon
|
|
,
|
|
[35, 1770, 1830, -264] // Eri
|
|
,
|
|
[59, 1830, 2100, -264] // Ori
|
|
,
|
|
[41, 2910, 3012, -264] // Hya
|
|
,
|
|
[74, 3450, 3870, -264] // Sex
|
|
,
|
|
[85, 4260, 4620, -264] // Vir
|
|
,
|
|
[58, 6330, 6360, -280] // Oph
|
|
,
|
|
[3, 6792, 7200, -288.8] // Aql
|
|
,
|
|
[35, 1740, 1770, -348] // Eri
|
|
,
|
|
[4, 7392, 7680, -360] // Aqr
|
|
,
|
|
[73, 6180, 6570, -384] // Ser
|
|
,
|
|
[72, 6570, 6792, -384] // Sct
|
|
,
|
|
[41, 3012, 3090, -408] // Hya
|
|
,
|
|
[58, 5856, 5895, -438] // Oph
|
|
,
|
|
[41, 3090, 3270, -456] // Hya
|
|
,
|
|
[26, 3870, 3900, -456] // Crt
|
|
,
|
|
[71, 5856, 5895, -462] // Sco
|
|
,
|
|
[47, 5640, 5730, -480] // Lib
|
|
,
|
|
[28, 4530, 4620, -528] // Crv
|
|
,
|
|
[85, 4620, 5130, -528] // Vir
|
|
,
|
|
[41, 3270, 3510, -576] // Hya
|
|
,
|
|
[16, 600, 954, -585.2] // Cet
|
|
,
|
|
[35, 954, 1350, -585.2] // Eri
|
|
,
|
|
[26, 3900, 4260, -588] // Crt
|
|
,
|
|
[28, 4260, 4530, -588] // Crv
|
|
,
|
|
[47, 5130, 5370, -588] // Lib
|
|
,
|
|
[58, 5856, 6030, -590] // Oph
|
|
,
|
|
[16, 0, 600, -612] // Cet
|
|
,
|
|
[11, 7680, 7872, -612] // Cap
|
|
,
|
|
[4, 7872, 8580, -612] // Aqr
|
|
,
|
|
[16, 8580, 8640, -612] // Cet
|
|
,
|
|
[41, 3510, 3690, -636] // Hya
|
|
,
|
|
[35, 1692, 1740, -654] // Eri
|
|
,
|
|
[46, 1740, 2202, -654] // Lep
|
|
,
|
|
[11, 7200, 7680, -672] // Cap
|
|
,
|
|
[41, 3690, 3810, -700] // Hya
|
|
,
|
|
[41, 4530, 5370, -708] // Hya
|
|
,
|
|
[47, 5370, 5640, -708] // Lib
|
|
,
|
|
[71, 5640, 5760, -708] // Sco
|
|
,
|
|
[35, 1650, 1692, -720] // Eri
|
|
,
|
|
[58, 6030, 6336, -720] // Oph
|
|
,
|
|
[76, 6336, 6420, -720] // Sgr
|
|
,
|
|
[41, 3810, 3900, -748] // Hya
|
|
,
|
|
[19, 2202, 2652, -792] // CMa
|
|
,
|
|
[41, 4410, 4530, -792] // Hya
|
|
,
|
|
[41, 3900, 4410, -840] // Hya
|
|
,
|
|
[36, 1260, 1350, -864] // For
|
|
,
|
|
[68, 3012, 3372, -882] // Pyx
|
|
,
|
|
[35, 1536, 1650, -888] // Eri
|
|
,
|
|
[76, 6420, 6900, -888] // Sgr
|
|
,
|
|
[65, 7680, 8280, -888] // PsA
|
|
,
|
|
[70, 8280, 8400, -888] // Scl
|
|
,
|
|
[36, 1080, 1260, -950] // For
|
|
,
|
|
[1, 3372, 3960, -954] // Ant
|
|
,
|
|
[70, 0, 600, -960] // Scl
|
|
,
|
|
[36, 600, 1080, -960] // For
|
|
,
|
|
[35, 1392, 1536, -960] // Eri
|
|
,
|
|
[70, 8400, 8640, -960] // Scl
|
|
,
|
|
[14, 5100, 5370, -1008] // Cen
|
|
,
|
|
[49, 5640, 5760, -1008] // Lup
|
|
,
|
|
[71, 5760, 5911.5, -1008] // Sco
|
|
,
|
|
[9, 1740, 1800, -1032] // Cae
|
|
,
|
|
[22, 1800, 2370, -1032] // Col
|
|
,
|
|
[67, 2880, 3012, -1032] // Pup
|
|
,
|
|
[35, 1230, 1392, -1056] // Eri
|
|
,
|
|
[71, 5911.5, 6420, -1092] // Sco
|
|
,
|
|
[24, 6420, 6900, -1092] // CrA
|
|
,
|
|
[76, 6900, 7320, -1092] // Sgr
|
|
,
|
|
[53, 7320, 7680, -1092] // Mic
|
|
,
|
|
[35, 1080, 1230, -1104] // Eri
|
|
,
|
|
[9, 1620, 1740, -1116] // Cae
|
|
,
|
|
[49, 5520, 5640, -1152] // Lup
|
|
,
|
|
[63, 0, 840, -1156] // Phe
|
|
,
|
|
[35, 960, 1080, -1176] // Eri
|
|
,
|
|
[40, 1470, 1536, -1176] // Hor
|
|
,
|
|
[9, 1536, 1620, -1176] // Cae
|
|
,
|
|
[38, 7680, 7920, -1200] // Gru
|
|
,
|
|
[67, 2160, 2880, -1218] // Pup
|
|
,
|
|
[84, 2880, 2940, -1218] // Vel
|
|
,
|
|
[35, 870, 960, -1224] // Eri
|
|
,
|
|
[40, 1380, 1470, -1224] // Hor
|
|
,
|
|
[63, 0, 660, -1236] // Phe
|
|
,
|
|
[12, 2160, 2220, -1260] // Car
|
|
,
|
|
[84, 2940, 3042, -1272] // Vel
|
|
,
|
|
[40, 1260, 1380, -1276] // Hor
|
|
,
|
|
[32, 1380, 1440, -1276] // Dor
|
|
,
|
|
[63, 0, 570, -1284] // Phe
|
|
,
|
|
[35, 780, 870, -1296] // Eri
|
|
,
|
|
[64, 1620, 1800, -1296] // Pic
|
|
,
|
|
[49, 5418, 5520, -1296] // Lup
|
|
,
|
|
[84, 3042, 3180, -1308] // Vel
|
|
,
|
|
[12, 2220, 2340, -1320] // Car
|
|
,
|
|
[14, 4260, 4620, -1320] // Cen
|
|
,
|
|
[49, 5100, 5418, -1320] // Lup
|
|
,
|
|
[56, 5418, 5520, -1320] // Nor
|
|
,
|
|
[32, 1440, 1560, -1356] // Dor
|
|
,
|
|
[84, 3180, 3960, -1356] // Vel
|
|
,
|
|
[14, 3960, 4050, -1356] // Cen
|
|
,
|
|
[5, 6300, 6480, -1368] // Ara
|
|
,
|
|
[78, 6480, 7320, -1368] // Tel
|
|
,
|
|
[38, 7920, 8400, -1368] // Gru
|
|
,
|
|
[40, 1152, 1260, -1380] // Hor
|
|
,
|
|
[64, 1800, 1980, -1380] // Pic
|
|
,
|
|
[12, 2340, 2460, -1392] // Car
|
|
,
|
|
[63, 0, 480, -1404] // Phe
|
|
,
|
|
[35, 480, 780, -1404] // Eri
|
|
,
|
|
[63, 8400, 8640, -1404] // Phe
|
|
,
|
|
[32, 1560, 1650, -1416] // Dor
|
|
,
|
|
[56, 5520, 5911.5, -1440] // Nor
|
|
,
|
|
[43, 7320, 7680, -1440] // Ind
|
|
,
|
|
[64, 1980, 2160, -1464] // Pic
|
|
,
|
|
[18, 5460, 5520, -1464] // Cir
|
|
,
|
|
[5, 5911.5, 5970, -1464] // Ara
|
|
,
|
|
[18, 5370, 5460, -1526] // Cir
|
|
,
|
|
[5, 5970, 6030, -1526] // Ara
|
|
,
|
|
[64, 2160, 2460, -1536] // Pic
|
|
,
|
|
[12, 2460, 3252, -1536] // Car
|
|
,
|
|
[14, 4050, 4260, -1536] // Cen
|
|
,
|
|
[27, 4260, 4620, -1536] // Cru
|
|
,
|
|
[14, 4620, 5232, -1536] // Cen
|
|
,
|
|
[18, 4860, 4920, -1560] // Cir
|
|
,
|
|
[5, 6030, 6060, -1560] // Ara
|
|
,
|
|
[40, 780, 1152, -1620] // Hor
|
|
,
|
|
[69, 1152, 1650, -1620] // Ret
|
|
,
|
|
[18, 5310, 5370, -1620] // Cir
|
|
,
|
|
[5, 6060, 6300, -1620] // Ara
|
|
,
|
|
[60, 6300, 6480, -1620] // Pav
|
|
,
|
|
[81, 7920, 8400, -1620] // Tuc
|
|
,
|
|
[32, 1650, 2370, -1680] // Dor
|
|
,
|
|
[18, 4920, 5310, -1680] // Cir
|
|
,
|
|
[79, 5310, 6120, -1680] // TrA
|
|
,
|
|
[81, 0, 480, -1800] // Tuc
|
|
,
|
|
[42, 1260, 1650, -1800] // Hyi
|
|
,
|
|
[86, 2370, 3252, -1800] // Vol
|
|
,
|
|
[12, 3252, 4050, -1800] // Car
|
|
,
|
|
[55, 4050, 4920, -1800] // Mus
|
|
,
|
|
[60, 6480, 7680, -1800] // Pav
|
|
,
|
|
[43, 7680, 8400, -1800] // Ind
|
|
,
|
|
[81, 8400, 8640, -1800] // Tuc
|
|
,
|
|
[81, 270, 480, -1824] // Tuc
|
|
,
|
|
[42, 0, 1260, -1980] // Hyi
|
|
,
|
|
[17, 2760, 4920, -1980] // Cha
|
|
,
|
|
[2, 4920, 6480, -1980] // Aps
|
|
,
|
|
[52, 1260, 2760, -2040] // Men
|
|
,
|
|
[57, 0, 8640, -2160] // Oct
|
|
];
|
|
let ConstelRot;
|
|
let Epoch2000;
|
|
/**
|
|
* @brief Reports the constellation that a given celestial point lies within.
|
|
*
|
|
* @property {string} symbol
|
|
* 3-character mnemonic symbol for the constellation, e.g. "Ori".
|
|
*
|
|
* @property {string} name
|
|
* Full name of constellation, e.g. "Orion".
|
|
*
|
|
* @property {number} ra1875
|
|
* Right ascension expressed in B1875 coordinates.
|
|
*
|
|
* @property {number} dec1875
|
|
* Declination expressed in B1875 coordinates.
|
|
*/
|
|
class ConstellationInfo {
|
|
constructor(symbol, name, ra1875, dec1875) {
|
|
this.symbol = symbol;
|
|
this.name = name;
|
|
this.ra1875 = ra1875;
|
|
this.dec1875 = dec1875;
|
|
}
|
|
}
|
|
exports.ConstellationInfo = ConstellationInfo;
|
|
/**
|
|
* @brief Determines the constellation that contains the given point in the sky.
|
|
*
|
|
* Given J2000 equatorial (EQJ) coordinates of a point in the sky,
|
|
* determines the constellation that contains that point.
|
|
*
|
|
* @param {number} ra
|
|
* The right ascension (RA) of a point in the sky, using the J2000 equatorial system.
|
|
*
|
|
* @param {number} dec
|
|
* The declination (DEC) of a point in the sky, using the J2000 equatorial system.
|
|
*
|
|
* @returns {ConstellationInfo}
|
|
* An object that contains the 3-letter abbreviation and full name
|
|
* of the constellation that contains the given (ra,dec), along with
|
|
* the converted B1875 (ra,dec) for that point.
|
|
*/
|
|
function Constellation(ra, dec) {
|
|
VerifyNumber(ra);
|
|
VerifyNumber(dec);
|
|
if (dec < -90 || dec > +90)
|
|
throw 'Invalid declination angle. Must be -90..+90.';
|
|
// Clamp right ascension to [0, 24) sidereal hours.
|
|
ra %= 24.0;
|
|
if (ra < 0.0)
|
|
ra += 24.0;
|
|
// Lazy-initialize rotation matrix.
|
|
if (!ConstelRot) {
|
|
// Need to calculate the B1875 epoch. Based on this:
|
|
// https://en.wikipedia.org/wiki/Epoch_(astronomy)#Besselian_years
|
|
// B = 1900 + (JD - 2415020.31352) / 365.242198781
|
|
// I'm interested in using TT instead of JD, giving:
|
|
// B = 1900 + ((TT+2451545) - 2415020.31352) / 365.242198781
|
|
// B = 1900 + (TT + 36524.68648) / 365.242198781
|
|
// TT = 365.242198781*(B - 1900) - 36524.68648 = -45655.741449525
|
|
// But the AstroTime constructor wants UT, not TT.
|
|
// Near that date, I get a historical correction of ut-tt = 3.2 seconds.
|
|
// That gives UT = -45655.74141261017 for the B1875 epoch,
|
|
// or 1874-12-31T18:12:21.950Z.
|
|
ConstelRot = Rotation_EQJ_EQD(new AstroTime(-45655.74141261017));
|
|
Epoch2000 = new AstroTime(0);
|
|
}
|
|
// Convert coordinates from J2000 to B1875.
|
|
const sph2000 = new Spherical(dec, 15.0 * ra, 1.0);
|
|
const vec2000 = VectorFromSphere(sph2000, Epoch2000);
|
|
const vec1875 = RotateVector(ConstelRot, vec2000);
|
|
const equ1875 = EquatorFromVector(vec1875);
|
|
// Search for the constellation using the B1875 coordinates.
|
|
const fd = 10 / (4 * 60); // conversion factor from compact units to DEC degrees
|
|
const fr = fd / 15; // conversion factor from compact units to RA sidereal hours
|
|
for (let b of ConstelBounds) {
|
|
// Convert compact angular units to RA in hours, DEC in degrees.
|
|
const dec = b[3] * fd;
|
|
const ra_lo = b[1] * fr;
|
|
const ra_hi = b[2] * fr;
|
|
if (dec <= equ1875.dec && ra_lo <= equ1875.ra && equ1875.ra < ra_hi) {
|
|
const c = ConstelNames[b[0]];
|
|
return new ConstellationInfo(c[0], c[1], equ1875.ra, equ1875.dec);
|
|
}
|
|
}
|
|
// This should never happen!
|
|
throw 'Unable to find constellation for given coordinates.';
|
|
}
|
|
exports.Constellation = Constellation;
|
|
/**
|
|
* @brief Returns information about a lunar eclipse.
|
|
*
|
|
* Returned by {@link SearchLunarEclipse} or {@link NextLunarEclipse}
|
|
* to report information about a lunar eclipse event.
|
|
* When a lunar eclipse is found, it is classified as penumbral, partial, or total.
|
|
* Penumbral eclipses are difficult to observe, because the moon is only slightly dimmed
|
|
* by the Earth's penumbra; no part of the Moon touches the Earth's umbra.
|
|
* Partial eclipses occur when part, but not all, of the Moon touches the Earth's umbra.
|
|
* Total eclipses occur when the entire Moon passes into the Earth's umbra.
|
|
*
|
|
* The `kind` field thus holds one of the strings `"penumbral"`, `"partial"`,
|
|
* or `"total"`, depending on the kind of lunar eclipse found.
|
|
*
|
|
* Field `peak` holds the date and time of the peak of the eclipse, when it is at its peak.
|
|
*
|
|
* Fields `sd_penum`, `sd_partial`, and `sd_total` hold the semi-duration of each phase
|
|
* of the eclipse, which is half of the amount of time the eclipse spends in each
|
|
* phase (expressed in minutes), or 0 if the eclipse never reaches that phase.
|
|
* By converting from minutes to days, and subtracting/adding with `peak`, the caller
|
|
* may determine the date and time of the beginning/end of each eclipse phase.
|
|
*
|
|
* @property {string} kind
|
|
* The type of lunar eclipse found.
|
|
*
|
|
* @property {AstroTime} peak
|
|
* The time of the eclipse at its peak.
|
|
*
|
|
* @property {number} sd_penum
|
|
* The semi-duration of the penumbral phase in minutes.
|
|
*
|
|
* @property {number} sd_partial
|
|
* The semi-duration of the penumbral phase in minutes, or 0.0 if none.
|
|
*
|
|
* @property {number} sd_total
|
|
* The semi-duration of the penumbral phase in minutes, or 0.0 if none.
|
|
*
|
|
*/
|
|
class LunarEclipseInfo {
|
|
constructor(kind, peak, sd_penum, sd_partial, sd_total) {
|
|
this.kind = kind;
|
|
this.peak = peak;
|
|
this.sd_penum = sd_penum;
|
|
this.sd_partial = sd_partial;
|
|
this.sd_total = sd_total;
|
|
}
|
|
}
|
|
exports.LunarEclipseInfo = LunarEclipseInfo;
|
|
/**
|
|
* @ignore
|
|
*
|
|
* @brief Represents the relative alignment of the Earth and another body, and their respective shadows.
|
|
*
|
|
* This is an internal data structure used to assist calculation of
|
|
* lunar eclipses, solar eclipses, and transits of Mercury and Venus.
|
|
*
|
|
* Definitions:
|
|
*
|
|
* casting body = A body that casts a shadow of interest, possibly striking another body.
|
|
*
|
|
* receiving body = A body on which the shadow of another body might land.
|
|
*
|
|
* shadow axis = The line passing through the center of the Sun and the center of the casting body.
|
|
*
|
|
* shadow plane = The plane passing through the center of a receiving body,
|
|
* and perpendicular to the shadow axis.
|
|
*
|
|
* @property {AstroTime} time
|
|
* The time associated with the shadow calculation.
|
|
*
|
|
* @property {number} u
|
|
* The distance [au] between the center of the casting body and the shadow plane.
|
|
*
|
|
* @property {number} r
|
|
* The distance [km] between center of receiving body and the shadow axis.
|
|
*
|
|
* @property {number} k
|
|
* The umbra radius [km] at the shadow plane.
|
|
*
|
|
* @property {number} p
|
|
* The penumbra radius [km] at the shadow plane.
|
|
*
|
|
* @property {Vector} target
|
|
* The location in space where we are interested in determining how close a shadow falls.
|
|
* For example, when calculating lunar eclipses, `target` would be the center of the Moon
|
|
* expressed in geocentric coordinates. Then we can evaluate how far the center of the Earth's
|
|
* shadow cone approaches the center of the Moon.
|
|
* The vector components are expressed in [au].
|
|
*
|
|
* @property {Vector} dir
|
|
* The direction in space that the shadow points away from the center of a shadow-casting body.
|
|
* This vector lies on the shadow axis and points away from the Sun.
|
|
* In other words: the direction light from the Sun would be traveling,
|
|
* except that the center of a body (Earth, Moon, Mercury, or Venus) is blocking it.
|
|
* The distance units do not matter, because the vector will be normalized.
|
|
*/
|
|
class ShadowInfo {
|
|
constructor(time, u, r, k, p, target, dir) {
|
|
this.time = time;
|
|
this.u = u;
|
|
this.r = r;
|
|
this.k = k;
|
|
this.p = p;
|
|
this.target = target;
|
|
this.dir = dir;
|
|
}
|
|
}
|
|
function CalcShadow(body_radius_km, time, target, dir) {
|
|
const u = (dir.x * target.x + dir.y * target.y + dir.z * target.z) / (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z);
|
|
const dx = (u * dir.x) - target.x;
|
|
const dy = (u * dir.y) - target.y;
|
|
const dz = (u * dir.z) - target.z;
|
|
const r = exports.KM_PER_AU * Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
const k = +SUN_RADIUS_KM - (1.0 + u) * (SUN_RADIUS_KM - body_radius_km);
|
|
const p = -SUN_RADIUS_KM + (1.0 + u) * (SUN_RADIUS_KM + body_radius_km);
|
|
return new ShadowInfo(time, u, r, k, p, target, dir);
|
|
}
|
|
function EarthShadow(time) {
|
|
const e = CalcVsop(vsop.Earth, time);
|
|
const m = GeoMoon(time);
|
|
return CalcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, e);
|
|
}
|
|
function MoonShadow(time) {
|
|
// This is a variation on the logic in _EarthShadow().
|
|
// Instead of a heliocentric Earth and a geocentric Moon,
|
|
// we want a heliocentric Moon and a lunacentric Earth.
|
|
const h = CalcVsop(vsop.Earth, time); // heliocentric Earth
|
|
const m = GeoMoon(time); // geocentric Moon
|
|
// Calculate lunacentric Earth.
|
|
const e = new Vector(-m.x, -m.y, -m.z, m.t);
|
|
// Convert geocentric moon to heliocentric Moon.
|
|
m.x += h.x;
|
|
m.y += h.y;
|
|
m.z += h.z;
|
|
return CalcShadow(MOON_MEAN_RADIUS_KM, time, e, m);
|
|
}
|
|
function LocalMoonShadow(time, observer) {
|
|
// Calculate observer's geocentric position.
|
|
// For efficiency, do this first, to populate the earth rotation parameters in 'time'.
|
|
// That way they can be recycled instead of recalculated.
|
|
const pos = geo_pos(time, observer);
|
|
const h = CalcVsop(vsop.Earth, time); // heliocentric Earth
|
|
const m = GeoMoon(time); // geocentric Moon
|
|
// Calculate lunacentric location of an observer on the Earth's surface.
|
|
const o = new Vector(pos[0] - m.x, pos[1] - m.y, pos[2] - m.z, time);
|
|
// Convert geocentric moon to heliocentric Moon.
|
|
m.x += h.x;
|
|
m.y += h.y;
|
|
m.z += h.z;
|
|
return CalcShadow(MOON_MEAN_RADIUS_KM, time, o, m);
|
|
}
|
|
function PlanetShadow(body, planet_radius_km, time) {
|
|
// Calculate light-travel-corrected vector from Earth to planet.
|
|
const g = GeoVector(body, time, false);
|
|
// Calculate light-travel-corrected vector from Earth to Sun.
|
|
const e = GeoVector(Body.Sun, time, false);
|
|
// Deduce light-travel-corrected vector from Sun to planet.
|
|
const p = new Vector(g.x - e.x, g.y - e.y, g.z - e.z, time);
|
|
// Calcluate Earth's position from the planet's point of view.
|
|
e.x = -g.x;
|
|
e.y = -g.y;
|
|
e.z = -g.z;
|
|
return CalcShadow(planet_radius_km, time, e, p);
|
|
}
|
|
function ShadowDistanceSlope(shadowfunc, time) {
|
|
const dt = 1.0 / 86400.0;
|
|
const t1 = time.AddDays(-dt);
|
|
const t2 = time.AddDays(+dt);
|
|
const shadow1 = shadowfunc(t1);
|
|
const shadow2 = shadowfunc(t2);
|
|
return (shadow2.r - shadow1.r) / dt;
|
|
}
|
|
function PlanetShadowSlope(body, planet_radius_km, time) {
|
|
const dt = 1.0 / 86400.0;
|
|
const shadow1 = PlanetShadow(body, planet_radius_km, time.AddDays(-dt));
|
|
const shadow2 = PlanetShadow(body, planet_radius_km, time.AddDays(+dt));
|
|
return (shadow2.r - shadow1.r) / dt;
|
|
}
|
|
function PeakEarthShadow(search_center_time) {
|
|
const window = 0.03; /* initial search window, in days, before/after given time */
|
|
const t1 = search_center_time.AddDays(-window);
|
|
const t2 = search_center_time.AddDays(+window);
|
|
const tx = Search((time) => ShadowDistanceSlope(EarthShadow, time), t1, t2);
|
|
if (!tx)
|
|
throw 'Failed to find peak Earth shadow time.';
|
|
return EarthShadow(tx);
|
|
}
|
|
function PeakMoonShadow(search_center_time) {
|
|
const window = 0.03; /* initial search window, in days, before/after given time */
|
|
const t1 = search_center_time.AddDays(-window);
|
|
const t2 = search_center_time.AddDays(+window);
|
|
const tx = Search((time) => ShadowDistanceSlope(MoonShadow, time), t1, t2);
|
|
if (!tx)
|
|
throw 'Failed to find peak Moon shadow time.';
|
|
return MoonShadow(tx);
|
|
}
|
|
function PeakPlanetShadow(body, planet_radius_km, search_center_time) {
|
|
// Search for when the body's shadow is closest to the center of the Earth.
|
|
const window = 1.0; // days before/after inferior conjunction to search for minimum shadow distance.
|
|
const t1 = search_center_time.AddDays(-window);
|
|
const t2 = search_center_time.AddDays(+window);
|
|
const tx = Search((time) => PlanetShadowSlope(body, planet_radius_km, time), t1, t2);
|
|
if (!tx)
|
|
throw 'Failed to find peak planet shadow time.';
|
|
return PlanetShadow(body, planet_radius_km, tx);
|
|
}
|
|
function PeakLocalMoonShadow(search_center_time, observer) {
|
|
// Search for the time near search_center_time that the Moon's shadow comes
|
|
// closest to the given observer.
|
|
const window = 0.2;
|
|
const t1 = search_center_time.AddDays(-window);
|
|
const t2 = search_center_time.AddDays(+window);
|
|
function shadowfunc(time) {
|
|
return LocalMoonShadow(time, observer);
|
|
}
|
|
const time = Search((time) => ShadowDistanceSlope(shadowfunc, time), t1, t2);
|
|
if (!time)
|
|
throw `PeakLocalMoonShadow: search failure for search_center_time = ${search_center_time}`;
|
|
return LocalMoonShadow(time, observer);
|
|
}
|
|
function ShadowSemiDurationMinutes(center_time, radius_limit, window_minutes) {
|
|
// Search backwards and forwards from the center time until shadow axis distance crosses radius limit.
|
|
const window = window_minutes / (24.0 * 60.0);
|
|
const before = center_time.AddDays(-window);
|
|
const after = center_time.AddDays(+window);
|
|
const t1 = Search((time) => -(EarthShadow(time).r - radius_limit), before, center_time);
|
|
const t2 = Search((time) => +(EarthShadow(time).r - radius_limit), center_time, after);
|
|
if (!t1 || !t2)
|
|
throw 'Failed to find shadow semiduration';
|
|
return (t2.ut - t1.ut) * ((24.0 * 60.0) / 2.0); // convert days to minutes and average the semi-durations.
|
|
}
|
|
function MoonEclipticLatitudeDegrees(time) {
|
|
const moon = CalcMoon(time);
|
|
return exports.RAD2DEG * moon.geo_eclip_lat;
|
|
}
|
|
/**
|
|
* @brief Searches for a lunar eclipse.
|
|
*
|
|
* This function finds the first lunar eclipse that occurs after `startTime`.
|
|
* A lunar eclipse may be penumbral, partial, or total.
|
|
* See {@link LunarEclipseInfo} for more information.
|
|
* To find a series of lunar eclipses, call this function once,
|
|
* then keep calling {@link NextLunarEclipse} as many times as desired,
|
|
* passing in the `peak` value returned from the previous call.
|
|
*
|
|
* @param {FlexibleDateTime} date
|
|
* The date and time for starting the search for a lunar eclipse.
|
|
*
|
|
* @returns {LunarEclipseInfo}
|
|
*/
|
|
function SearchLunarEclipse(date) {
|
|
const PruneLatitude = 1.8; /* full Moon's ecliptic latitude above which eclipse is impossible */
|
|
let fmtime = MakeTime(date);
|
|
for (let fmcount = 0; fmcount < 12; ++fmcount) {
|
|
/* Search for the next full moon. Any eclipse will be near it. */
|
|
const fullmoon = SearchMoonPhase(180, fmtime, 40);
|
|
if (!fullmoon)
|
|
throw 'Cannot find full moon.';
|
|
/*
|
|
Pruning: if the full Moon's ecliptic latitude is too large,
|
|
a lunar eclipse is not possible. Avoid needless work searching for
|
|
the minimum moon distance.
|
|
*/
|
|
const eclip_lat = MoonEclipticLatitudeDegrees(fullmoon);
|
|
if (Math.abs(eclip_lat) < PruneLatitude) {
|
|
/* Search near the full moon for the time when the center of the Moon */
|
|
/* is closest to the line passing through the centers of the Sun and Earth. */
|
|
const shadow = PeakEarthShadow(fullmoon);
|
|
if (shadow.r < shadow.p + MOON_MEAN_RADIUS_KM) {
|
|
/* This is at least a penumbral eclipse. We will return a result. */
|
|
let kind = 'penumbral';
|
|
let sd_total = 0.0;
|
|
let sd_partial = 0.0;
|
|
let sd_penum = ShadowSemiDurationMinutes(shadow.time, shadow.p + MOON_MEAN_RADIUS_KM, 200.0);
|
|
if (shadow.r < shadow.k + MOON_MEAN_RADIUS_KM) {
|
|
/* This is at least a partial eclipse. */
|
|
kind = 'partial';
|
|
sd_partial = ShadowSemiDurationMinutes(shadow.time, shadow.k + MOON_MEAN_RADIUS_KM, sd_penum);
|
|
if (shadow.r + MOON_MEAN_RADIUS_KM < shadow.k) {
|
|
/* This is a total eclipse. */
|
|
kind = 'total';
|
|
sd_total = ShadowSemiDurationMinutes(shadow.time, shadow.k - MOON_MEAN_RADIUS_KM, sd_partial);
|
|
}
|
|
}
|
|
return new LunarEclipseInfo(kind, shadow.time, sd_penum, sd_partial, sd_total);
|
|
}
|
|
}
|
|
/* We didn't find an eclipse on this full moon, so search for the next one. */
|
|
fmtime = fullmoon.AddDays(10);
|
|
}
|
|
/* This should never happen because there are always at least 2 full moons per year. */
|
|
throw 'Failed to find lunar eclipse within 12 full moons.';
|
|
}
|
|
exports.SearchLunarEclipse = SearchLunarEclipse;
|
|
/**
|
|
* @brief Reports the time and geographic location of the peak of a solar eclipse.
|
|
*
|
|
* Returned by {@link SearchGlobalSolarEclipse} or {@link NextGlobalSolarEclipse}
|
|
* to report information about a solar eclipse event.
|
|
*
|
|
* Field `peak` holds the date and time of the peak of the eclipse, defined as
|
|
* the instant when the axis of the Moon's shadow cone passes closest to the Earth's center.
|
|
*
|
|
* The eclipse is classified as partial, annular, or total, depending on the
|
|
* maximum amount of the Sun's disc obscured, as seen at the peak location
|
|
* on the surface of the Earth.
|
|
*
|
|
* The `kind` field thus holds one of the strings `"partial"`, `"annular"`, or `"total"`.
|
|
* A total eclipse is when the peak observer sees the Sun completely blocked by the Moon.
|
|
* An annular eclipse is like a total eclipse, but the Moon is too far from the Earth's surface
|
|
* to completely block the Sun; instead, the Sun takes on a ring-shaped appearance.
|
|
* A partial eclipse is when the Moon blocks part of the Sun's disc, but nobody on the Earth
|
|
* observes either a total or annular eclipse.
|
|
*
|
|
* If `kind` is `"total"` or `"annular"`, the `latitude` and `longitude`
|
|
* fields give the geographic coordinates of the center of the Moon's shadow projected
|
|
* onto the daytime side of the Earth at the instant of the eclipse's peak.
|
|
* If `kind` has any other value, `latitude` and `longitude` are undefined and should
|
|
* not be used.
|
|
*
|
|
* @property {string} kind
|
|
* One of the following string values: `"partial"`, `"annular"`, `"total"`.
|
|
*
|
|
* @property {AstroTime} peak
|
|
* The date and time of the peak of the eclipse, defined as the instant
|
|
* when the axis of the Moon's shadow cone passes closest to the Earth's center.
|
|
*
|
|
* @property {number} distance
|
|
* The distance in kilometers between the axis of the Moon's shadow cone
|
|
* and the center of the Earth at the time indicated by `peak`.
|
|
*
|
|
* @property {number | undefined} latitude
|
|
* If `kind` holds `"total"`, the geographic latitude in degrees
|
|
* where the center of the Moon's shadow falls on the Earth at the
|
|
* time indicated by `peak`; otherwise, `latitude` holds `undefined`.
|
|
*
|
|
* @property {number | undefined} longitude
|
|
* If `kind` holds `"total"`, the geographic longitude in degrees
|
|
* where the center of the Moon's shadow falls on the Earth at the
|
|
* time indicated by `peak`; otherwise, `longitude` holds `undefined`.
|
|
*/
|
|
class GlobalSolarEclipseInfo {
|
|
constructor(kind, peak, distance, latitude, longitude) {
|
|
this.kind = kind;
|
|
this.peak = peak;
|
|
this.distance = distance;
|
|
this.latitude = latitude;
|
|
this.longitude = longitude;
|
|
}
|
|
}
|
|
exports.GlobalSolarEclipseInfo = GlobalSolarEclipseInfo;
|
|
function EclipseKindFromUmbra(k) {
|
|
// The umbra radius tells us what kind of eclipse the observer sees.
|
|
// If the umbra radius is positive, this is a total eclipse. Otherwise, it's annular.
|
|
// HACK: I added a tiny bias (14 meters) to match Espenak test data.
|
|
return (k > 0.014) ? 'total' : 'annular';
|
|
}
|
|
function GeoidIntersect(shadow) {
|
|
let kind = 'partial';
|
|
let peak = shadow.time;
|
|
let distance = shadow.r;
|
|
let latitude; // left undefined for partial eclipses
|
|
let longitude; // left undefined for partial eclipses
|
|
// We want to calculate the intersection of the shadow axis with the Earth's geoid.
|
|
// First we must convert EQJ (equator of J2000) coordinates to EQD (equator of date)
|
|
// coordinates that are perfectly aligned with the Earth's equator at this
|
|
// moment in time.
|
|
const rot = Rotation_EQJ_EQD(shadow.time);
|
|
const v = RotateVector(rot, shadow.dir); // shadow-axis vector in equator-of-date coordinates
|
|
const e = RotateVector(rot, shadow.target); // lunacentric Earth in equator-of-date coordinates
|
|
// Convert all distances from AU to km.
|
|
// But dilate the z-coordinates so that the Earth becomes a perfect sphere.
|
|
// Then find the intersection of the vector with the sphere.
|
|
// See p 184 in Montenbruck & Pfleger's "Astronomy on the Personal Computer", second edition.
|
|
v.x *= exports.KM_PER_AU;
|
|
v.y *= exports.KM_PER_AU;
|
|
v.z *= exports.KM_PER_AU / EARTH_FLATTENING;
|
|
e.x *= exports.KM_PER_AU;
|
|
e.y *= exports.KM_PER_AU;
|
|
e.z *= exports.KM_PER_AU / EARTH_FLATTENING;
|
|
// Solve the quadratic equation that finds whether and where
|
|
// the shadow axis intersects with the Earth in the dilated coordinate system.
|
|
const R = EARTH_EQUATORIAL_RADIUS_KM;
|
|
const A = v.x * v.x + v.y * v.y + v.z * v.z;
|
|
const B = -2.0 * (v.x * e.x + v.y * e.y + v.z * e.z);
|
|
const C = (e.x * e.x + e.y * e.y + e.z * e.z) - R * R;
|
|
const radic = B * B - 4 * A * C;
|
|
if (radic > 0.0) {
|
|
// Calculate the closer of the two intersection points.
|
|
// This will be on the day side of the Earth.
|
|
const u = (-B - Math.sqrt(radic)) / (2 * A);
|
|
// Convert lunacentric dilated coordinates to geocentric coordinates.
|
|
const px = u * v.x - e.x;
|
|
const py = u * v.y - e.y;
|
|
const pz = (u * v.z - e.z) * EARTH_FLATTENING;
|
|
// Convert cartesian coordinates into geodetic latitude/longitude.
|
|
const proj = Math.sqrt(px * px + py * py) * (EARTH_FLATTENING * EARTH_FLATTENING);
|
|
if (proj == 0.0)
|
|
latitude = (pz > 0.0) ? +90.0 : -90.0;
|
|
else
|
|
latitude = exports.RAD2DEG * Math.atan(pz / proj);
|
|
// Adjust longitude for Earth's rotation at the given UT.
|
|
const gast = sidereal_time(peak);
|
|
longitude = (exports.RAD2DEG * Math.atan2(py, px) - (15 * gast)) % 360.0;
|
|
if (longitude <= -180.0)
|
|
longitude += 360.0;
|
|
else if (longitude > +180.0)
|
|
longitude -= 360.0;
|
|
// We want to determine whether the observer sees a total eclipse or an annular eclipse.
|
|
// We need to perform a series of vector calculations...
|
|
// Calculate the inverse rotation matrix, so we can convert EQD to EQJ.
|
|
const inv = InverseRotation(rot);
|
|
// Put the EQD geocentric coordinates of the observer into the vector 'o'.
|
|
// Also convert back from kilometers to astronomical units.
|
|
let o = new Vector(px / exports.KM_PER_AU, py / exports.KM_PER_AU, pz / exports.KM_PER_AU, shadow.time);
|
|
// Rotate the observer's geocentric EQD back to the EQJ system.
|
|
o = RotateVector(inv, o);
|
|
// Convert geocentric vector to lunacentric vector.
|
|
o.x += shadow.target.x;
|
|
o.y += shadow.target.y;
|
|
o.z += shadow.target.z;
|
|
// Recalculate the shadow using a vector from the Moon's center toward the observer.
|
|
const surface = CalcShadow(MOON_POLAR_RADIUS_KM, shadow.time, o, shadow.dir);
|
|
// If we did everything right, the shadow distance should be very close to zero.
|
|
// That's because we already determined the observer 'o' is on the shadow axis!
|
|
if (surface.r > 1.0e-9 || surface.r < 0.0)
|
|
throw `Unexpected shadow distance from geoid intersection = ${surface.r}`;
|
|
kind = EclipseKindFromUmbra(surface.k);
|
|
}
|
|
return new GlobalSolarEclipseInfo(kind, peak, distance, latitude, longitude);
|
|
}
|
|
/**
|
|
* @brief Searches for the next lunar eclipse in a series.
|
|
*
|
|
* After using {@link SearchLunarEclipse} to find the first lunar eclipse
|
|
* in a series, you can call this function to find the next consecutive lunar eclipse.
|
|
* Pass in the `peak` value from the {@link LunarEclipseInfo} returned by the
|
|
* previous call to `SearchLunarEclipse` or `NextLunarEclipse`
|
|
* to find the next lunar eclipse.
|
|
*
|
|
* @param {AstroTime} prevEclipseTime
|
|
* A date and time near a full moon. Lunar eclipse search will start at the next full moon.
|
|
*
|
|
* @returns {LunarEclipseInfo}
|
|
*/
|
|
function NextLunarEclipse(prevEclipseTime) {
|
|
const startTime = prevEclipseTime.AddDays(10);
|
|
return SearchLunarEclipse(startTime);
|
|
}
|
|
exports.NextLunarEclipse = NextLunarEclipse;
|
|
/**
|
|
* @brief Searches for a solar eclipse visible anywhere on the Earth's surface.
|
|
*
|
|
* This function finds the first solar eclipse that occurs after `startTime`.
|
|
* A solar eclipse may be partial, annular, or total.
|
|
* See {@link GlobalSolarEclipseInfo} for more information.
|
|
* To find a series of solar eclipses, call this function once,
|
|
* then keep calling {@link NextGlobalSolarEclipse} as many times as desired,
|
|
* passing in the `peak` value returned from the previous call.
|
|
*
|
|
* @param {AstroTime} startTime
|
|
* The date and time for starting the search for a solar eclipse.
|
|
*
|
|
* @returns {GlobalSolarEclipseInfo}
|
|
*/
|
|
function SearchGlobalSolarEclipse(startTime) {
|
|
const PruneLatitude = 1.8; // Moon's ecliptic latitude beyond which eclipse is impossible
|
|
// Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth.
|
|
let nmtime = startTime;
|
|
let nmcount;
|
|
for (nmcount = 0; nmcount < 12; ++nmcount) {
|
|
// Search for the next new moon. Any eclipse will be near it.
|
|
const newmoon = SearchMoonPhase(0.0, nmtime, 40.0);
|
|
if (!newmoon)
|
|
throw 'Cannot find new moon';
|
|
// Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible.
|
|
const eclip_lat = MoonEclipticLatitudeDegrees(newmoon);
|
|
if (Math.abs(eclip_lat) < PruneLatitude) {
|
|
// Search near the new moon for the time when the center of the Earth
|
|
// is closest to the line passing through the centers of the Sun and Moon.
|
|
const shadow = PeakMoonShadow(newmoon);
|
|
if (shadow.r < shadow.p + EARTH_MEAN_RADIUS_KM) {
|
|
// This is at least a partial solar eclipse visible somewhere on Earth.
|
|
// Try to find an intersection between the shadow axis and the Earth's oblate geoid.
|
|
return GeoidIntersect(shadow);
|
|
}
|
|
}
|
|
// We didn't find an eclipse on this new moon, so search for the next one.
|
|
nmtime = newmoon.AddDays(10.0);
|
|
}
|
|
// Safety valve to prevent infinite loop.
|
|
// This should never happen, because at least 2 solar eclipses happen per year.
|
|
throw 'Failed to find solar eclipse within 12 full moons.';
|
|
}
|
|
exports.SearchGlobalSolarEclipse = SearchGlobalSolarEclipse;
|
|
/**
|
|
* @brief Searches for the next global solar eclipse in a series.
|
|
*
|
|
* After using {@link SearchGlobalSolarEclipse} to find the first solar eclipse
|
|
* in a series, you can call this function to find the next consecutive solar eclipse.
|
|
* Pass in the `peak` value from the {@link GlobalSolarEclipseInfo} returned by the
|
|
* previous call to `SearchGlobalSolarEclipse` or `NextGlobalSolarEclipse`
|
|
* to find the next solar eclipse.
|
|
*
|
|
* @param {AstroTime} prevEclipseTime
|
|
* A date and time near a new moon. Solar eclipse search will start at the next new moon.
|
|
*
|
|
* @returns {GlobalSolarEclipseInfo}
|
|
*/
|
|
function NextGlobalSolarEclipse(prevEclipseTime) {
|
|
const startTime = prevEclipseTime.AddDays(10.0);
|
|
return SearchGlobalSolarEclipse(startTime);
|
|
}
|
|
exports.NextGlobalSolarEclipse = NextGlobalSolarEclipse;
|
|
/**
|
|
* @brief Holds a time and the observed altitude of the Sun at that time.
|
|
*
|
|
* When reporting a solar eclipse observed at a specific location on the Earth
|
|
* (a "local" solar eclipse), a series of events occur. In addition
|
|
* to the time of each event, it is important to know the altitude of the Sun,
|
|
* because each event may be invisible to the observer if the Sun is below
|
|
* the horizon (i.e. it at night).
|
|
*
|
|
* If `altitude` is negative, the event is theoretical only; it would be
|
|
* visible if the Earth were transparent, but the observer cannot actually see it.
|
|
* If `altitude` is positive but less than a few degrees, visibility will be impaired by
|
|
* atmospheric interference (sunrise or sunset conditions).
|
|
*
|
|
* @property {AstroTime} time
|
|
* The date and time of the event.
|
|
*
|
|
* @property {number} altitude
|
|
* The angular altitude of the center of the Sun above/below the horizon, at `time`,
|
|
* corrected for atmospheric refraction and expressed in degrees.
|
|
*/
|
|
class EclipseEvent {
|
|
constructor(time, altitude) {
|
|
this.time = time;
|
|
this.altitude = altitude;
|
|
}
|
|
}
|
|
exports.EclipseEvent = EclipseEvent;
|
|
/**
|
|
* @brief Information about a solar eclipse as seen by an observer at a given time and geographic location.
|
|
*
|
|
* Returned by {@link SearchLocalSolarEclipse} or {@link NextLocalSolarEclipse}
|
|
* to report information about a solar eclipse as seen at a given geographic location.
|
|
*
|
|
* When a solar eclipse is found, it is classified by setting `kind`
|
|
* to `"partial"`, `"annular"`, or `"total"`.
|
|
* A partial solar eclipse is when the Moon does not line up directly enough with the Sun
|
|
* to completely block the Sun's light from reaching the observer.
|
|
* An annular eclipse occurs when the Moon's disc is completely visible against the Sun
|
|
* but the Moon is too far away to completely block the Sun's light; this leaves the
|
|
* Sun with a ring-like appearance.
|
|
* A total eclipse occurs when the Moon is close enough to the Earth and aligned with the
|
|
* Sun just right to completely block all sunlight from reaching the observer.
|
|
*
|
|
* There are 5 "event" fields, each of which contains a time and a solar altitude.
|
|
* Field `peak` holds the date and time of the center of the eclipse, when it is at its peak.
|
|
* The fields `partial_begin` and `partial_end` are always set, and indicate when
|
|
* the eclipse begins/ends. If the eclipse reaches totality or becomes annular,
|
|
* `total_begin` and `total_end` indicate when the total/annular phase begins/ends.
|
|
* When an event field is valid, the caller must also check its `altitude` field to
|
|
* see whether the Sun is above the horizon at the time indicated by the `time` field.
|
|
* See {@link EclipseEvent} for more information.
|
|
*
|
|
* @property {string} kind
|
|
* The type of solar eclipse found: `"partial"`, `"annular"`, or `"total"`.
|
|
*
|
|
* @property {EclipseEvent} partial_begin
|
|
* The time and Sun altitude at the beginning of the eclipse.
|
|
*
|
|
* @property {EclipseEvent | undefined} total_begin
|
|
* If this is an annular or a total eclipse, the time and Sun altitude when annular/total phase begins; otherwise undefined.
|
|
*
|
|
* @property {EclipseEvent} peak
|
|
* The time and Sun altitude when the eclipse reaches its peak.
|
|
*
|
|
* @property {EclipseEvent | undefined} total_end
|
|
* If this is an annular or a total eclipse, the time and Sun altitude when annular/total phase ends; otherwise undefined.
|
|
*
|
|
* @property {EclipseEvent} partial_end
|
|
* The time and Sun altitude at the end of the eclipse.
|
|
*/
|
|
class LocalSolarEclipseInfo {
|
|
constructor(kind, partial_begin, total_begin, peak, total_end, partial_end) {
|
|
this.kind = kind;
|
|
this.partial_begin = partial_begin;
|
|
this.total_begin = total_begin;
|
|
this.peak = peak;
|
|
this.total_end = total_end;
|
|
this.partial_end = partial_end;
|
|
}
|
|
}
|
|
exports.LocalSolarEclipseInfo = LocalSolarEclipseInfo;
|
|
function local_partial_distance(shadow) {
|
|
return shadow.p - shadow.r;
|
|
}
|
|
function local_total_distance(shadow) {
|
|
// Must take the absolute value of the umbra radius 'k'
|
|
// because it can be negative for an annular eclipse.
|
|
return Math.abs(shadow.k) - shadow.r;
|
|
}
|
|
function LocalEclipse(shadow, observer) {
|
|
const PARTIAL_WINDOW = 0.2;
|
|
const TOTAL_WINDOW = 0.01;
|
|
const peak = CalcEvent(observer, shadow.time);
|
|
let t1 = shadow.time.AddDays(-PARTIAL_WINDOW);
|
|
let t2 = shadow.time.AddDays(+PARTIAL_WINDOW);
|
|
const partial_begin = LocalEclipseTransition(observer, +1.0, local_partial_distance, t1, shadow.time);
|
|
const partial_end = LocalEclipseTransition(observer, -1.0, local_partial_distance, shadow.time, t2);
|
|
let total_begin;
|
|
let total_end;
|
|
let kind;
|
|
if (shadow.r < Math.abs(shadow.k)) { // take absolute value of 'k' to handle annular eclipses too.
|
|
t1 = shadow.time.AddDays(-TOTAL_WINDOW);
|
|
t2 = shadow.time.AddDays(+TOTAL_WINDOW);
|
|
total_begin = LocalEclipseTransition(observer, +1.0, local_total_distance, t1, shadow.time);
|
|
total_end = LocalEclipseTransition(observer, -1.0, local_total_distance, shadow.time, t2);
|
|
kind = EclipseKindFromUmbra(shadow.k);
|
|
}
|
|
else {
|
|
kind = 'partial';
|
|
}
|
|
return new LocalSolarEclipseInfo(kind, partial_begin, total_begin, peak, total_end, partial_end);
|
|
}
|
|
function LocalEclipseTransition(observer, direction, func, t1, t2) {
|
|
function evaluate(time) {
|
|
const shadow = LocalMoonShadow(time, observer);
|
|
return direction * func(shadow);
|
|
}
|
|
const search = Search(evaluate, t1, t2);
|
|
if (!search)
|
|
throw "Local eclipse transition search failed.";
|
|
return CalcEvent(observer, search);
|
|
}
|
|
function CalcEvent(observer, time) {
|
|
const altitude = SunAltitude(time, observer);
|
|
return new EclipseEvent(time, altitude);
|
|
}
|
|
function SunAltitude(time, observer) {
|
|
const equ = Equator(Body.Sun, time, observer, true, true);
|
|
const hor = Horizon(time, observer, equ.ra, equ.dec, 'normal');
|
|
return hor.altitude;
|
|
}
|
|
/**
|
|
* @brief Searches for a solar eclipse visible at a specific location on the Earth's surface.
|
|
*
|
|
* This function finds the first solar eclipse that occurs after `startTime`.
|
|
* A solar eclipse may be partial, annular, or total.
|
|
* See {@link LocalSolarEclipseInfo} for more information.
|
|
*
|
|
* To find a series of solar eclipses, call this function once,
|
|
* then keep calling {@link NextLocalSolarEclipse} as many times as desired,
|
|
* passing in the `peak` value returned from the previous call.
|
|
*
|
|
* IMPORTANT: An eclipse reported by this function might be partly or
|
|
* completely invisible to the observer due to the time of day.
|
|
* See {@link LocalSolarEclipseInfo} for more information about this topic.
|
|
*
|
|
* @param {AstroTime} startTime
|
|
* The date and time for starting the search for a solar eclipse.
|
|
*
|
|
* @param {Observer} observer
|
|
* The geographic location of the observer.
|
|
*
|
|
* @returns {LocalSolarEclipseInfo}
|
|
*/
|
|
function SearchLocalSolarEclipse(startTime, observer) {
|
|
VerifyObserver(observer);
|
|
const PruneLatitude = 1.8; /* Moon's ecliptic latitude beyond which eclipse is impossible */
|
|
/* Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth. */
|
|
let nmtime = startTime;
|
|
for (;;) {
|
|
/* Search for the next new moon. Any eclipse will be near it. */
|
|
const newmoon = SearchMoonPhase(0.0, nmtime, 40.0);
|
|
if (!newmoon)
|
|
throw 'Cannot find next new moon';
|
|
/* Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible. */
|
|
const eclip_lat = MoonEclipticLatitudeDegrees(newmoon);
|
|
if (Math.abs(eclip_lat) < PruneLatitude) {
|
|
/* Search near the new moon for the time when the observer */
|
|
/* is closest to the line passing through the centers of the Sun and Moon. */
|
|
const shadow = PeakLocalMoonShadow(newmoon, observer);
|
|
if (shadow.r < shadow.p) {
|
|
/* This is at least a partial solar eclipse for the observer. */
|
|
const eclipse = LocalEclipse(shadow, observer);
|
|
/* Ignore any eclipse that happens completely at night. */
|
|
/* More precisely, the center of the Sun must be above the horizon */
|
|
/* at the beginning or the end of the eclipse, or we skip the event. */
|
|
if (eclipse.partial_begin.altitude > 0.0 || eclipse.partial_end.altitude > 0.0)
|
|
return eclipse;
|
|
}
|
|
}
|
|
/* We didn't find an eclipse on this new moon, so search for the next one. */
|
|
nmtime = newmoon.AddDays(10.0);
|
|
}
|
|
}
|
|
exports.SearchLocalSolarEclipse = SearchLocalSolarEclipse;
|
|
/**
|
|
* @brief Searches for the next local solar eclipse in a series.
|
|
*
|
|
* After using {@link SearchLocalSolarEclipse} to find the first solar eclipse
|
|
* in a series, you can call this function to find the next consecutive solar eclipse.
|
|
* Pass in the `peak` value from the {@link LocalSolarEclipseInfo} returned by the
|
|
* previous call to `SearchLocalSolarEclipse` or `NextLocalSolarEclipse`
|
|
* to find the next solar eclipse.
|
|
* This function finds the first solar eclipse that occurs after `startTime`.
|
|
* A solar eclipse may be partial, annular, or total.
|
|
* See {@link LocalSolarEclipseInfo} for more information.
|
|
*
|
|
* @param {AstroTime} prevEclipseTime
|
|
* The date and time for starting the search for a solar eclipse.
|
|
*
|
|
* @param {Observer} observer
|
|
* The geographic location of the observer.
|
|
*
|
|
* @returns {LocalSolarEclipseInfo}
|
|
*/
|
|
function NextLocalSolarEclipse(prevEclipseTime, observer) {
|
|
const startTime = prevEclipseTime.AddDays(10.0);
|
|
return SearchLocalSolarEclipse(startTime, observer);
|
|
}
|
|
exports.NextLocalSolarEclipse = NextLocalSolarEclipse;
|
|
/**
|
|
* @brief Information about a transit of Mercury or Venus, as seen from the Earth.
|
|
*
|
|
* Returned by {@link SearchTransit} or {@link NextTransit} to report
|
|
* information about a transit of Mercury or Venus.
|
|
* A transit is when Mercury or Venus passes between the Sun and Earth so that
|
|
* the other planet is seen in silhouette against the Sun.
|
|
*
|
|
* The calculations are performed from the point of view of a geocentric observer.
|
|
*
|
|
* @property {AstroTime} start
|
|
* The date and time at the beginning of the transit.
|
|
* This is the moment the planet first becomes visible against the Sun in its background.
|
|
*
|
|
* @property {AstroTime} peak
|
|
* When the planet is most aligned with the Sun, as seen from the Earth.
|
|
*
|
|
* @property {AstroTime} finish
|
|
* The date and time at the end of the transit.
|
|
* This is the moment the planet is last seen against the Sun in its background.
|
|
*
|
|
* @property {number} separation
|
|
* The minimum angular separation, in arcminutes, between the centers of the Sun and the planet.
|
|
* This angle pertains to the time stored in `peak`.
|
|
*/
|
|
class TransitInfo {
|
|
constructor(start, peak, finish, separation) {
|
|
this.start = start;
|
|
this.peak = peak;
|
|
this.finish = finish;
|
|
this.separation = separation;
|
|
}
|
|
}
|
|
exports.TransitInfo = TransitInfo;
|
|
function PlanetShadowBoundary(time, body, planet_radius_km, direction) {
|
|
const shadow = PlanetShadow(body, planet_radius_km, time);
|
|
return direction * (shadow.r - shadow.p);
|
|
}
|
|
function PlanetTransitBoundary(body, planet_radius_km, t1, t2, direction) {
|
|
// Search for the time the planet's penumbra begins/ends making contact with the center of the Earth.
|
|
const tx = Search((time) => PlanetShadowBoundary(time, body, planet_radius_km, direction), t1, t2);
|
|
if (!tx)
|
|
throw 'Planet transit boundary search failed';
|
|
return tx;
|
|
}
|
|
/**
|
|
* @brief Searches for the first transit of Mercury or Venus after a given date.
|
|
*
|
|
* Finds the first transit of Mercury or Venus after a specified date.
|
|
* A transit is when an inferior planet passes between the Sun and the Earth
|
|
* so that the silhouette of the planet is visible against the Sun in the background.
|
|
* To continue the search, pass the `finish` time in the returned structure to
|
|
* {@link NextTransit}.
|
|
*
|
|
* @param {Body} body
|
|
* The planet whose transit is to be found. Must be `"Mercury"` or `"Venus"`.
|
|
*
|
|
* @param {AstroTime} startTime
|
|
* The date and time for starting the search for a transit.
|
|
*
|
|
* @returns {TransitInfo}
|
|
*/
|
|
function SearchTransit(body, startTime) {
|
|
const threshold_angle = 0.4; // maximum angular separation to attempt transit calculation
|
|
const dt_days = 1.0;
|
|
// Validate the planet and find its mean radius.
|
|
let planet_radius_km;
|
|
switch (body) {
|
|
case Body.Mercury:
|
|
planet_radius_km = 2439.7;
|
|
break;
|
|
case Body.Venus:
|
|
planet_radius_km = 6051.8;
|
|
break;
|
|
default:
|
|
throw `Invalid body: ${body}`;
|
|
}
|
|
let search_time = startTime;
|
|
for (;;) {
|
|
// Search for the next inferior conjunction of the given planet.
|
|
// This is the next time the Earth and the other planet have the same
|
|
// ecliptic longitude as seen from the Sun.
|
|
const conj = SearchRelativeLongitude(body, 0.0, search_time);
|
|
// Calculate the angular separation between the body and the Sun at this time.
|
|
const conj_separation = AngleFromSun(body, conj);
|
|
if (conj_separation < threshold_angle) {
|
|
// The planet's angular separation from the Sun is small enough
|
|
// to consider it a transit candidate.
|
|
// Search for the moment when the line passing through the Sun
|
|
// and planet are closest to the Earth's center.
|
|
const shadow = PeakPlanetShadow(body, planet_radius_km, conj);
|
|
if (shadow.r < shadow.p) { // does the planet's penumbra touch the Earth's center?
|
|
// Find the beginning and end of the penumbral contact.
|
|
const time_before = shadow.time.AddDays(-dt_days);
|
|
const start = PlanetTransitBoundary(body, planet_radius_km, time_before, shadow.time, -1.0);
|
|
const time_after = shadow.time.AddDays(+dt_days);
|
|
const finish = PlanetTransitBoundary(body, planet_radius_km, shadow.time, time_after, +1.0);
|
|
const min_separation = 60.0 * AngleFromSun(body, shadow.time);
|
|
return new TransitInfo(start, shadow.time, finish, min_separation);
|
|
}
|
|
}
|
|
// This inferior conjunction was not a transit. Try the next inferior conjunction.
|
|
search_time = conj.AddDays(10.0);
|
|
}
|
|
}
|
|
exports.SearchTransit = SearchTransit;
|
|
/**
|
|
* @brief Searches for the next transit of Mercury or Venus in a series.
|
|
*
|
|
* After calling {@link SearchTransit} to find a transit of Mercury or Venus,
|
|
* this function finds the next transit after that.
|
|
* Keep calling this function as many times as you want to keep finding more transits.
|
|
*
|
|
* @param {Body} body
|
|
* The planet whose transit is to be found. Must be `"Mercury"` or `"Venus"`.
|
|
*
|
|
* @param {AstroTime} prevTransitTime
|
|
* A date and time near the previous transit.
|
|
*
|
|
* @returns {TransitInfo}
|
|
*/
|
|
function NextTransit(body, prevTransitTime) {
|
|
const startTime = prevTransitTime.AddDays(100.0);
|
|
return SearchTransit(body, startTime);
|
|
}
|
|
exports.NextTransit = NextTransit;
|