From db9eb6b180d747bde4edd395cd51a2f4fa467f22 Mon Sep 17 00:00:00 2001 From: Don Cross Date: Mon, 10 Aug 2020 11:08:27 -0400 Subject: [PATCH] Maintain redundant copies of Astronomy Engine source in demo folders. Windows does not support relative links in Git by default. This broke the first-time experience for Windows users. From now on I will maintain copies of the astronomy.js and astronomy.py in the demo folders, so that the demos will work on Windows immediately after cloning the repo. --- demo/browser/astronomy.js | 7173 +++++++++++++++++++++++++++++ demo/browser/astronomy.min.js | 1 - demo/browser/barycenter.html | 2 +- demo/browser/moonphase.html | 2 +- demo/browser/positions.html | 2 +- demo/browser/riseset.html | 2 +- demo/nodejs/astronomy.js | 7173 +++++++++++++++++++++++++++++ demo/nodejs/culminate.js | 2 +- demo/nodejs/horizon.js | 2 +- demo/nodejs/lunar_eclipse.js | 2 +- demo/nodejs/moonphase.js | 2 +- demo/nodejs/positions.js | 2 +- demo/nodejs/riseset.js | 2 +- demo/nodejs/seasons.js | 2 +- demo/python/astronomy.py | 7980 +++++++++++++++++++++++++++++++++ generate/makedoc | 5 + generate/makedoc.bat | 11 + 17 files changed, 22353 insertions(+), 12 deletions(-) create mode 100644 demo/browser/astronomy.js delete mode 120000 demo/browser/astronomy.min.js create mode 100644 demo/nodejs/astronomy.js create mode 100644 demo/python/astronomy.py diff --git a/demo/browser/astronomy.js b/demo/browser/astronomy.js new file mode 100644 index 00000000..31331fae --- /dev/null +++ b/demo/browser/astronomy.js @@ -0,0 +1,7173 @@ +/** + @preserve + + Astronomy library for JavaScript (browser and Node.js). + https://github.com/cosinekitty/astronomy + + MIT License + + Copyright (c) 2019-2020 Don Cross + + 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 + * @license MIT + */ +'use strict'; + +/** + * @name Astronomy + * @namespace Astronomy + */ +(function(Astronomy){ +'use strict'; +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 KM_PER_AU = 1.4959787069098932e+8; +const C_AUDAY = 173.1446326846693; // speed of light in AU/day +const ASEC2RAD = 4.848136811095359935899141e-6; +const DEG2RAD = 0.017453292519943296; +const RAD2DEG = 57.295779513082321; +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 / KM_PER_AU; + +const EARTH_FLATTENING = 0.996647180302104; +const EARTH_EQUATORIAL_RADIUS_KM = 6378.1366; +const EARTH_EQUATORIAL_RADIUS_AU = EARTH_EQUATORIAL_RADIUS_KM / KM_PER_AU; +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 / 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; +const SUN_MASS = 333054.25318; /* Sun's mass relative to Earth. */ +const JUPITER_MASS = 317.84997; /* Jupiter's mass relative to Earth. */ +const SATURN_MASS = 95.16745; /* Saturn's mass relative to Earth. */ +const URANUS_MASS = 14.53617; /* Uranus's mass relative to Earth. */ +const NEPTUNE_MASS = 17.14886; /* Neptune's mass relative to Earth. */ +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 IsValidDate(d) { + return (d instanceof Date) && Number.isFinite(d.getTime()); +} + +function Frac(x) { + return x - Math.floor(x); +} + +/** + * Calculates the angle in degrees between two vectors. + * The angle is measured in the plane that contains both vectors. + * + * @param {Astronomy.Vector} a + * The first of a pair of vectors between which to measure an angle. + * + * @param {Astronomy.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 = RAD2DEG * Math.acos(dot); + return angle; +} + +/** + * @constant {string[]} Astronomy.Bodies + * An array of strings, each a name of a supported astronomical body. + * Not all bodies are valid for all functions, but any string not in this + * list is not supported at all. + */ +Astronomy.Bodies = [ + 'Sun', + 'Moon', + 'Mercury', + 'Venus', + 'Earth', + 'Mars', + 'Jupiter', + 'Saturn', + 'Uranus', + 'Neptune', + 'Pluto', + 'SSB', // Solar System Barycenter + 'EMB' // Earth/Moon Barycenter +]; + +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] + ] + ] +] +}; + +const top2013 = { + Pluto: [ + [ // f=0 + [ // f=0, s= 0 +[ 0, 3.9544617144029999e+01, 0.0000000000000000e+00], // f=0, s= 0, t= 0 +[ 1402, -1.8891373533434089e-01, -8.5258197635470073e-02], // f=0, s= 0, t= 1 +[ 1331, -4.1495877833812339e-02, -3.3387415274886263e-02], // f=0, s= 0, t= 2 +[ 522, -4.8502474919249819e-02, -7.3455412272547278e-03], // f=0, s= 0, t= 3 +[ 71, 2.8102132918948221e-02, -5.3468437660152152e-03], // f=0, s= 0, t= 4 +[ 1261, -9.1600251304608978e-03, -1.2309204554431390e-02], // f=0, s= 0, t= 5 +[ 452, -1.2202161344831991e-02, -5.2519856329982890e-03], // f=0, s= 0, t= 6 +[ 2875, -9.7845229475467185e-03, -7.2968560644718955e-04], // f=0, s= 0, t= 7 +[ 35, 4.8494518209585983e-03, -6.8918970374425084e-03], // f=0, s= 0, t= 8 +[ 141, 5.8271375488234932e-03, -3.1946778653436391e-03], // f=0, s= 0, t= 9 +[ 137, 1.5300059509576150e-03, -6.0327729791525954e-03], // f=0, s= 0, t= 10 +[ 4, -1.9494897408412360e-03, 4.5717130739994704e-03], // f=0, s= 0, t= 11 +[ 1190, -1.7524220672664240e-03, -4.2980683502911454e-03], // f=0, s= 0, t= 12 +[ 381, -3.1062775803702681e-03, -2.4728667551542258e-03], // f=0, s= 0, t= 13 +[ 8, -1.7188663433411050e-03, 2.9756270077158122e-03], // f=0, s= 0, t= 14 +[ 1543, -7.7472653128184826e-04, 2.6514626782777680e-03], // f=0, s= 0, t= 15 +[ 1115, -1.5405722125111840e-03, -2.0778390548994150e-03], // f=0, s= 0, t= 16 +[ 2804, -2.0048397209869230e-03, -4.1957951179189120e-04], // f=0, s= 0, t= 17 +[ 67, 9.6850762192148931e-04, -1.5811913714969829e-03], // f=0, s= 0, t= 18 +[ 212, 1.5466715821083480e-03, -9.6836654994834209e-04], // f=0, s= 0, t= 19 +[ 1119, -1.8820367463891121e-04, -1.4293834479379090e-03], // f=0, s= 0, t= 20 +[ 17405, -1.0738845199599739e-03, 9.5985010997943349e-04], // f=0, s= 0, t= 21 +[ 28337, 7.5821211083786067e-04, 1.1416213940389449e-03], // f=0, s= 0, t= 22 +[ 310, -6.9650370158153983e-04, -1.0024667762364200e-03], // f=0, s= 0, t= 23 +[ 1044, -3.2801771454650589e-04, -6.4947140155397116e-04], // f=0, s= 0, t= 24 +[ 63, 4.0424767075158291e-04, 5.7315886355325109e-04], // f=0, s= 0, t= 25 +[ 1614, -1.3238448498587051e-04, 6.7949492229369074e-04], // f=0, s= 0, t= 26 +[ 12, -6.5048480404874143e-04, -1.0430021074697129e-04], // f=0, s= 0, t= 27 +[ 283, 3.3929768009878552e-04, -5.1573678840263412e-04], // f=0, s= 0, t= 28 +[ 133, 4.0138197254613279e-04, 4.5284627712770291e-04], // f=0, s= 0, t= 29 +[ 1421, 5.0823717785117468e-04, 3.1010622577270548e-04], // f=0, s= 0, t= 30 +[ 1383, -5.6813906164891585e-04, -1.7225147327178090e-04], // f=0, s= 0, t= 31 +[ 4348, -5.1094469101100064e-04, 1.4416513132178369e-04], // f=0, s= 0, t= 32 +[ 2733, -4.8192307867155672e-04, -1.8631709444892481e-04], // f=0, s= 0, t= 33 +[ 664, 6.3695948832436563e-05, 5.0429756322537705e-04], // f=0, s= 0, t= 34 +[ 177, -3.0532744995821309e-04, 4.0226535349675440e-04], // f=0, s= 0, t= 35 +[ 204, 3.4677834246970749e-04, 3.2588064363496143e-04], // f=0, s= 0, t= 36 +[ 1048, 5.6219036569383140e-05, -4.5145044715373130e-04], // f=0, s= 0, t= 37 +[ 1406, 3.6764565360298558e-04, -2.4793326161876619e-04], // f=0, s= 0, t= 38 +[ 1398, -6.1399335668996843e-05, -4.0368084856225791e-04], // f=0, s= 0, t= 39 +[ 880, 3.8206123841964649e-04, 1.0727867737651879e-04], // f=0, s= 0, t= 40 +[ 239, -9.2482984334176681e-05, -3.6357760642322443e-04], // f=0, s= 0, t= 41 +[ 541, -3.5492935880988093e-04, -7.2696664262252120e-05], // f=0, s= 0, t= 42 +[ 17334, -3.1608420086033548e-04, 1.6282567766535830e-04], // f=0, s= 0, t= 43 +[ 503, 3.4204624270103872e-04, 6.9800261361389600e-06], // f=0, s= 0, t= 44 +[ 28266, 1.1026801272880990e-04, 3.1928911807394130e-04], // f=0, s= 0, t= 45 +[ 974, -1.5403436032839399e-04, -2.6908883645786191e-04], // f=0, s= 0, t= 46 +[ 345, -2.1738414747494940e-04, 1.5563797862269319e-04], // f=0, s= 0, t= 47 +[ 1924, 2.1398279518720060e-04, 1.4058024849360011e-04], // f=0, s= 0, t= 48 +[ 275, 1.7682640345886169e-04, 1.7533174872185701e-04], // f=0, s= 0, t= 49 +[ 106, 2.0541555697206479e-04, -1.2965626091678131e-04], // f=0, s= 0, t= 50 +[ 1335, 2.1493444414672229e-04, -1.0499061765897920e-04], // f=0, s= 0, t= 51 +[ 271, 2.1059070752496800e-04, -9.6647526271973576e-05] // f=0, s= 0, t= 52 + ], + [ // f=0, s= 1 +[ 1402, -2.4541007710854022e-02, 5.3822529675651168e-02], // f=0, s= 1, t= 0 +[ 0, 3.7890000000000000e-02, 0.0000000000000000e+00], // f=0, s= 1, t= 1 +[ 1331, -1.5981733929364941e-02, 1.9623571765255570e-02], // f=0, s= 1, t= 2 +[ 522, -2.1667846285401051e-03, 1.3851523320136261e-02], // f=0, s= 1, t= 3 +[ 71, 8.6723020043621556e-04, 4.7241967094509424e-03], // f=0, s= 1, t= 4 +[ 1261, -3.8008804453790821e-03, 2.7392780267271222e-03], // f=0, s= 1, t= 5 +[ 2875, -5.9015441259769620e-04, 3.3190792202711962e-03], // f=0, s= 1, t= 6 +[ 35, -2.0646844712970550e-03, -2.6221746776036530e-03], // f=0, s= 1, t= 7 +[ 4, -2.7341177070724500e-03, 8.4711655196688536e-04], // f=0, s= 1, t= 8 +[ 1190, -2.1427361371102339e-03, 8.3829844178717706e-04], // f=0, s= 1, t= 9 +[ 452, -6.5191457442425422e-04, 1.3962477909825410e-03], // f=0, s= 1, t= 10 +[ 8, -1.4174353266390050e-03, -4.8800609526062797e-04], // f=0, s= 1, t= 11 +[ 381, -7.7874017402358557e-04, 9.3105764052437356e-04], // f=0, s= 1, t= 12 +[ 137, -1.1495119655719700e-03, -2.9568842990989599e-04], // f=0, s= 1, t= 13 +[ 2804, -2.9320339800219448e-04, 1.0245691529492771e-03], // f=0, s= 1, t= 14 +[ 1119, -9.8344008047702861e-04, 1.2122440012927610e-04], // f=0, s= 1, t= 15 +[ 1543, 7.2662531437559094e-04, 2.2380632879329121e-04], // f=0, s= 1, t= 16 +[ 1115, -4.9059020449034973e-04, 5.4125269506251448e-04], // f=0, s= 1, t= 17 +[ 310, -5.0858314998190228e-04, 3.3071892812221339e-04] // f=0, s= 1, t= 18 + ], + [ // f=0, s= 2 +[ 1402, 6.1564734183986881e-03, 6.9727544398706350e-03], // f=0, s= 2, t= 0 +[ 1331, 3.4473718686991932e-03, 5.3547458162748890e-03], // f=0, s= 2, t= 1 +[ 0, -6.0199059744408881e-03, 0.0000000000000000e+00], // f=0, s= 2, t= 2 +[ 522, 1.8506017571807240e-03, 1.2255527089933200e-03], // f=0, s= 2, t= 3 +[ 4, -9.0791196955079877e-04, -1.0597464135497440e-03], // f=0, s= 2, t= 4 +[ 1261, -2.5360705302150178e-04, 1.1006253876392330e-03] // f=0, s= 2, t= 5 + ] + ], + [ // f=1 + [ // f=1, s= 0 +[ 0, 4.1654711248260003e+00, 0.0000000000000000e+00], // f=1, s= 0, t= 0 +[ 1402, 2.0610516934972331e-03, -4.5672163937433728e-03], // f=1, s= 0, t= 1 +[ 4, 3.4901370659179681e-03, 2.2214931280208532e-03], // f=1, s= 0, t= 2 +[ 1473, -2.5274121559055551e-04, 1.4438005411011950e-03], // f=1, s= 0, t= 3 +[ 8, 1.1811406701290150e-03, 5.6294240646172070e-04], // f=1, s= 0, t= 4 +[ 522, 1.6107717821433110e-04, -1.0623010008063969e-03], // f=1, s= 0, t= 5 +[ 1331, 2.7651217171242138e-04, -3.4287860661004818e-04], // f=1, s= 0, t= 6 +[ 593, 3.3064342491574279e-05, 3.2281574175741223e-04], // f=1, s= 0, t= 7 +[ 2875, 1.7391859631880900e-05, -2.4371046197993790e-04], // f=1, s= 0, t= 8 +[ 71, 4.3540142673103847e-05, -2.3324350638665871e-04], // f=1, s= 0, t= 9 +[ 12, -1.4733740390542089e-05, 1.6458293257830351e-04], // f=1, s= 0, t= 10 +[ 35, -7.7366158840780485e-05, -1.2373453663453069e-04], // f=1, s= 0, t= 11 +[ 137, 1.0874981420582371e-04, 2.9642106239502481e-05], // f=1, s= 0, t= 12 +[ 452, 3.2837879995135691e-05, -8.0083302669255936e-05], // f=1, s= 0, t= 13 +[ 106, -7.1058821662524569e-05, -2.9131217624735831e-05], // f=1, s= 0, t= 14 +[ 2945, 1.2419927037655510e-05, 6.9699905790296052e-05], // f=1, s= 0, t= 15 +[ 1115, 4.9015184839992363e-05, -3.7626776449287022e-05], // f=1, s= 0, t= 16 +[ 1261, 3.7725355301707661e-05, -2.7730601457362580e-05], // f=1, s= 0, t= 17 +[ 63, 3.4620953450636643e-05, -2.5727054077819990e-05], // f=1, s= 0, t= 18 +[ 17405, -2.4737834776937812e-05, -2.7668964560844859e-05], // f=1, s= 0, t= 19 +[ 141, -1.9152627579774910e-05, 3.0608840803230902e-05], // f=1, s= 0, t= 20 +[ 1543, 3.4403440209432860e-05, 8.2302490865683481e-06], // f=1, s= 0, t= 21 +[ 28337, -2.9450824049753070e-05, 1.9509947544013829e-05], // f=1, s= 0, t= 22 +[ 208, -3.3402595486149828e-05, 6.9721024372198099e-07], // f=1, s= 0, t= 23 +[ 16, -2.5708226560219951e-05, 5.4690438392817274e-06], // f=1, s= 0, t= 24 +[ 2804, 8.2434428446966487e-06, -2.2056820101697141e-05], // f=1, s= 0, t= 25 +[ 133, 1.7535496693018641e-05, -1.5317433153649361e-05], // f=1, s= 0, t= 26 +[ 177, 9.4209450622295435e-06, 1.8609791865429379e-05], // f=1, s= 0, t= 27 +[ 204, 1.3418024861975440e-05, -1.5519809537438971e-05], // f=1, s= 0, t= 28 +[ 67, 1.9542040889474939e-05, -2.0438794816549699e-07], // f=1, s= 0, t= 29 +[ 1186, -1.1497747150912189e-05, 1.4072246679624251e-05], // f=1, s= 0, t= 30 +[ 1421, -7.4843280005381670e-06, 1.2303197921178691e-05], // f=1, s= 0, t= 31 +[ 1383, 4.1628765439336240e-06, -1.3719681259667719e-05], // f=1, s= 0, t= 32 +[ 275, 7.1803653660818622e-06, -1.2018565522581090e-05], // f=1, s= 0, t= 33 +[ 212, 1.3642921909731060e-05, 2.1147480912091179e-06], // f=1, s= 0, t= 34 +[ 59, -2.8996742569855378e-06, -1.3093850574478501e-05], // f=1, s= 0, t= 35 +[ 4348, -3.6912767653086940e-06, -1.2768209379424250e-05], // f=1, s= 0, t= 36 +[ 17476, 8.8159150487736961e-06, 5.9364512059773267e-06], // f=1, s= 0, t= 37 +[ 27, 6.2980935945243974e-06, -8.4472797839010716e-06], // f=1, s= 0, t= 38 +[ 1406, 5.7734139509693619e-06, 8.7248712048903149e-06], // f=1, s= 0, t= 39 +[ 1398, 1.0115989678361140e-05, -1.3220473394572090e-06], // f=1, s= 0, t= 40 +[ 28407, 6.7642022362205018e-06, -7.5210649765782616e-06], // f=1, s= 0, t= 41 +[ 664, 7.9736063591689162e-06, -2.2024063975673750e-06], // f=1, s= 0, t= 42 +[ 381, 3.1761072779613261e-06, -7.3448935507901516e-06], // f=1, s= 0, t= 43 +[ 541, 2.5898481389558310e-06, -7.0323742443685127e-06], // f=1, s= 0, t= 44 +[ 503, -1.5979599372675240e-07, 7.4586091247535634e-06], // f=1, s= 0, t= 45 +[ 271, -3.1728511373578081e-06, -6.6603677156048786e-06], // f=1, s= 0, t= 46 +[ 129, -1.8397756711585990e-06, -7.0451043684758007e-06], // f=1, s= 0, t= 47 +[ 247, 3.1533300480278400e-07, -7.1604266714610849e-06], // f=1, s= 0, t= 48 +[ 200, -2.2707299296152400e-06, -6.6783936815674601e-06], // f=1, s= 0, t= 49 +[ 31, -4.2146281840702766e-06, -5.6388188974303981e-06], // f=1, s= 0, t= 50 +[ 341, -3.9640903174182786e-06, -5.5968952746387096e-06], // f=1, s= 0, t= 51 +[ 20, -2.1086487296606532e-06, -4.2993743513743323e-06], // f=1, s= 0, t= 52 +[ 974, -1.9337296464942458e-06, 4.1280092704873532e-06], // f=1, s= 0, t= 53 +[ 1492, 1.2386884071711840e-06, -4.0279260453190260e-06], // f=1, s= 0, t= 54 +[ 1454, -1.8674475925452479e-07, 4.1994903608977473e-06], // f=1, s= 0, t= 55 +[ 55, -4.1054351042334211e-06, -5.3592659852261218e-07], // f=1, s= 0, t= 56 +[ 1044, 3.5262751998119361e-06, 1.9367410649084749e-06], // f=1, s= 0, t= 57 +[ 39, -7.0347175355875052e-07, 3.9228440423731852e-06], // f=1, s= 0, t= 58 +[ 345, -2.7714162211172452e-06, -2.8012028299863999e-06], // f=1, s= 0, t= 59 +[ 283, -3.8855399506750791e-06, 4.2353837211089759e-07], // f=1, s= 0, t= 60 +[ 4418, 1.9380717555480920e-06, 3.3362906758656841e-06], // f=1, s= 0, t= 61 +[ 1708, 3.8363066095252423e-06, 4.0071146882428590e-07], // f=1, s= 0, t= 62 +[ 903, -2.4535489999476891e-06, 2.7713257447896069e-06], // f=1, s= 0, t= 63 +[ 412, -2.4194398659193562e-06, -2.6818837171571071e-06], // f=1, s= 0, t= 64 +[ 17334, -1.5878732753673419e-06, -3.0847459710695832e-06], // f=1, s= 0, t= 65 +[ 1410, 2.1528516528967772e-06, 2.5955495586096718e-06], // f=1, s= 0, t= 66 +[ 318, -1.6007757166478231e-06, 2.9567402302706461e-06], // f=1, s= 0, t= 67 +[ 28266, -3.1282886569147322e-06, 1.0733981547714400e-06], // f=1, s= 0, t= 68 +[ 1394, 3.2570224915190650e-06, -5.9986669416035846e-08], // f=1, s= 0, t= 69 +[ 72490, 7.8958524750142168e-07, 3.1029527324934821e-06], // f=1, s= 0, t= 70 +[ 9220, 2.8325155820290858e-06, -1.4397983817771050e-06], // f=1, s= 0, t= 71 +[ 1614, 3.0748937595146630e-06, 5.0326493238056022e-07], // f=1, s= 0, t= 72 +[ 408, -3.0722440820616450e-06, 4.9345659005564487e-07], // f=1, s= 0, t= 73 +[ 337, -2.9632105019568382e-06, 9.3548023528643972e-08], // f=1, s= 0, t= 74 +[ 79, 2.7845485358443480e-06, 7.4025133333557920e-07], // f=1, s= 0, t= 75 +[ 354, 1.9360680354843408e-06, 2.0552415375649160e-06], // f=1, s= 0, t= 76 +[ 612, 5.3703132984668158e-07, 2.6669817089723940e-06], // f=1, s= 0, t= 77 +[ 279, -2.5413275396432281e-07, 2.6502544396045040e-06], // f=1, s= 0, t= 78 +[ 75, -2.8830829576295098e-07, 2.6106707165907741e-06], // f=1, s= 0, t= 79 +[ 479, -2.4319941706034390e-06, 7.6942886776602959e-07], // f=1, s= 0, t= 80 +[ 416, 2.0096007406999570e-06, 1.5520759224768389e-06], // f=1, s= 0, t= 81 +[ 1096, -2.3257110689311771e-06, 9.7831153778294669e-07], // f=1, s= 0, t= 82 +[ 267, -2.5074175872274202e-06, -1.7701423128546709e-07], // f=1, s= 0, t= 83 +[ 3162, 2.0698330022272070e-06, -1.3093627746428469e-06], // f=1, s= 0, t= 84 +[ 832, -1.9285373422971391e-06, 1.4160461431375550e-06], // f=1, s= 0, t= 85 +[ 1190, 2.2341390259691221e-06, -7.2482790590962755e-07], // f=1, s= 0, t= 86 +[ 526, 1.7171371907181510e-06, 1.4529625626913769e-06], // f=1, s= 0, t= 87 +[ 2733, 1.0269773239622380e-06, -2.0007533912028411e-06], // f=1, s= 0, t= 88 +[ 574, -5.1232176836951890e-07, -2.1792410718247190e-06] // f=1, s= 0, t= 89 + ], + [ // f=1, s= 1 +[ 0, 2.5335660204370001e+01, 0.0000000000000000e+00], // f=1, s= 1, t= 0 +[ 4, -2.1897916824442529e-04, 1.7741955864290151e-03], // f=1, s= 1, t= 1 +[ 1402, -1.3029527067277161e-03, -5.8987171410210541e-04], // f=1, s= 1, t= 2 +[ 8, -5.7857466108113462e-05, 6.2766572590063064e-04], // f=1, s= 1, t= 3 +[ 522, -3.0338218770459020e-04, -4.6787236333551149e-05], // f=1, s= 1, t= 4 +[ 1331, -1.6234312756262061e-04, -1.3226650376835280e-04], // f=1, s= 1, t= 5 +[ 35, -1.6846578574212679e-04, 5.1474337733700072e-05], // f=1, s= 1, t= 6 +[ 1473, 1.3716672333829441e-04, 2.7605165443775319e-05], // f=1, s= 1, t= 7 +[ 12, -1.1426985570984979e-04, 1.7062621376287921e-05], // f=1, s= 1, t= 8 +[ 2875, -8.2654240464229338e-05, -1.4230432684507880e-05], // f=1, s= 1, t= 9 +[ 2945, 3.6084370654255798e-05, -3.8286603564628976e-06], // f=1, s= 1, t= 10 +[ 593, 3.1050555447062818e-05, -2.3120488185252789e-06], // f=1, s= 1, t= 11 +[ 16, -8.7245446550032187e-06, -2.2703299980076952e-05], // f=1, s= 1, t= 12 +[ 137, 5.5783261240786444e-06, -2.0642718216052691e-05], // f=1, s= 1, t= 13 +[ 1115, -1.3513503169043030e-05, -1.1380785261757990e-05], // f=1, s= 1, t= 14 +[ 71, 1.0574660674287450e-05, 1.3281184472910570e-05], // f=1, s= 1, t= 15 +[ 1261, -8.2709801392281214e-06, -1.1688074277507450e-05], // f=1, s= 1, t= 16 +[ 2804, -1.1505534938221521e-05, -5.1883886955282330e-06], // f=1, s= 1, t= 17 +[ 27, -9.0174735475683120e-06, -5.2285448475511598e-06], // f=1, s= 1, t= 18 +[ 452, -9.3099342968225354e-06, -3.8372164516008766e-06], // f=1, s= 1, t= 19 +[ 1543, 2.4828575108302229e-06, -9.6169483680059550e-06], // f=1, s= 1, t= 20 +[ 63, -6.5055787623425069e-06, -7.4385376578198243e-06], // f=1, s= 1, t= 21 +[ 106, -4.5512156458035857e-06, 8.6658259433823040e-06], // f=1, s= 1, t= 22 +[ 133, -6.3072609842624613e-06, -6.9148116074105151e-06], // f=1, s= 1, t= 23 +[ 28337, -4.4530735415054223e-06, -6.7124377273408592e-06], // f=1, s= 1, t= 24 +[ 141, 6.9466051844107804e-06, 4.0449257224296616e-06], // f=1, s= 1, t= 25 +[ 1398, 1.0522658062293921e-06, -7.0563446922276261e-06], // f=1, s= 1, t= 26 +[ 59, -5.7183266993529056e-06, 1.6273101086020201e-06], // f=1, s= 1, t= 27 +[ 1383, -5.6807273131313079e-06, -9.5798703870467336e-07], // f=1, s= 1, t= 28 +[ 20, 4.8130148431204634e-06, -2.5299767388566989e-06], // f=1, s= 1, t= 29 +[ 4348, -5.3269871823774314e-06, 6.0216192993201565e-07], // f=1, s= 1, t= 30 +[ 129, -4.2816786763917080e-06, 1.2524207693334740e-06] // f=1, s= 1, t= 31 + ], + [ // f=1, s= 2 +[ 0, -1.8272218839163919e-02, 0.0000000000000000e+00], // f=1, s= 2, t= 0 +[ 4, -4.2382205514535369e-04, 6.0951225139627217e-05], // f=1, s= 2, t= 1 +[ 8, -2.4214950161520300e-04, 1.3555498866999700e-04], // f=1, s= 2, t= 2 +[ 1402, -1.6731953542354990e-04, 1.4877590360992571e-04], // f=1, s= 2, t= 3 +[ 12, -4.0514580902466651e-05, -4.6859429953625132e-05], // f=1, s= 2, t= 4 +[ 1331, -4.4255479363996823e-05, 2.8523966290637259e-05], // f=1, s= 2, t= 5 +[ 522, -2.6626163791203940e-05, 4.0414506964273448e-05], // f=1, s= 2, t= 6 +[ 35, -2.3510812229573040e-06, 1.7218835328103482e-05], // f=1, s= 2, t= 7 +[ 2875, -7.9375576768093987e-06, 1.3976740463566499e-05], // f=1, s= 2, t= 8 +[ 16, 9.7305978044426803e-06, -1.0841117038814680e-05], // f=1, s= 2, t= 9 +[ 2945, -4.5741854272088092e-07, -9.4120776625816716e-06] // f=1, s= 2, t= 10 + ], + [ // f=1, s= 3 +[ 0, 1.9409931667071581e-03, 0.0000000000000000e+00], // f=1, s= 3, t= 0 +[ 8, -5.2528177259165953e-05, -6.1055439661395280e-05], // f=1, s= 3, t= 1 +[ 4, -6.0738316603187738e-05, 3.5387155556321078e-05], // f=1, s= 3, t= 2 +[ 1402, 1.5967276563962451e-05, 3.5538891371840628e-05], // f=1, s= 3, t= 3 +[ 12, 1.5332065032938759e-05, -2.1295985440213221e-05] // f=1, s= 3, t= 4 + ], + [ // f=1, s= 4 +[ 0, 8.6099959150566781e-05, 0.0000000000000000e+00], // f=1, s= 4, t= 0 +[ 4, -5.3544872037241911e-05, -3.2079251781364850e-05] // f=1, s= 4, t= 1 + ] + ], + [ // f=2 + [ // f=2, s= 0 +[ 0, -1.7873895940349999e-01, 0.0000000000000000e+00], // f=2, s= 0, t= 0 +[ 1473, 3.1629832749992988e-03, -2.0985870294942081e-03], // f=2, s= 0, t= 1 +[ 71, -6.8180357663860398e-04, 1.1113519163940930e-03], // f=2, s= 0, t= 2 +[ 1331, 1.6911781266743710e-04, 1.1885125258969610e-03], // f=2, s= 0, t= 3 +[ 593, 5.4774166542017916e-04, -6.3844158559171749e-04], // f=2, s= 0, t= 4 +[ 1402, -2.1438473609329679e-05, -5.2026929830140383e-04], // f=2, s= 0, t= 5 +[ 1261, -5.7368849879358177e-05, 4.6888445010058260e-04], // f=2, s= 0, t= 6 +[ 141, -9.6040157452198532e-05, 3.0415286563684890e-04], // f=2, s= 0, t= 7 +[ 452, 1.2124121714193030e-04, 2.7470351829330812e-04], // f=2, s= 0, t= 8 +[ 2945, 1.1035693239374779e-04, -1.4867545478165761e-04], // f=2, s= 0, t= 9 +[ 1190, -5.9767419503565077e-05, 1.5052450817429230e-04], // f=2, s= 0, t= 10 +[ 1543, -1.0081841648710751e-04, 1.1874253775582299e-04], // f=2, s= 0, t= 11 +[ 522, -3.7998691236472437e-05, -1.1710923038633279e-04], // f=2, s= 0, t= 12 +[ 381, 1.7939534333569889e-05, 1.2165766508553840e-04], // f=2, s= 0, t= 13 +[ 8, -5.1065826531599063e-05, -1.0380178618772450e-04], // f=2, s= 0, t= 14 +[ 4, -9.6101012457693182e-05, -3.7409967137145340e-05], // f=2, s= 0, t= 15 +[ 212, -1.0011374095546651e-05, 9.1861092027193070e-05], // f=2, s= 0, t= 16 +[ 208, 6.3118700287276614e-05, 6.6551813665925998e-05], // f=2, s= 0, t= 17 +[ 106, 4.6362839297978442e-05, 7.1895265631125496e-05], // f=2, s= 0, t= 18 +[ 2804, 2.5951137645879960e-05, 4.8810749391069221e-05], // f=2, s= 0, t= 19 +[ 1119, -3.1798344550251363e-05, 4.3442443246298743e-05], // f=2, s= 0, t= 20 +[ 35, -2.6365309213148771e-05, -3.7318748312868848e-05], // f=2, s= 0, t= 21 +[ 1186, 4.5373467873211630e-05, -3.9315678706168707e-06], // f=2, s= 0, t= 22 +[ 310, -5.7440916735318752e-06, 4.2608008327900133e-05], // f=2, s= 0, t= 23 +[ 67, -3.2628206231552743e-05, 1.4188485967101389e-05], // f=2, s= 0, t= 24 +[ 664, -1.3138986439777871e-05, 2.8745825031870391e-05], // f=2, s= 0, t= 25 +[ 17476, -4.5717070878285386e-06, -2.7229104695341480e-05], // f=2, s= 0, t= 26 +[ 283, 4.1649905603620409e-06, 2.5778764278582031e-05], // f=2, s= 0, t= 27 +[ 28407, -2.6086891392275169e-05, 6.5019643042548361e-07], // f=2, s= 0, t= 28 +[ 2875, -1.2604181155312930e-05, -2.2155626126791911e-05], // f=2, s= 0, t= 29 +[ 2733, 4.6350251894966598e-06, 2.1642396781861640e-05], // f=2, s= 0, t= 30 +[ 12, 1.3787761564064851e-05, -1.6882625493196851e-05], // f=2, s= 0, t= 31 +[ 63, 7.3243482301750183e-06, -2.0430415756143061e-05], // f=2, s= 0, t= 32 +[ 137, 1.7688914790024399e-05, -2.6466558968384412e-06], // f=2, s= 0, t= 33 +[ 1048, -1.3743478634746570e-05, 1.1178051438197709e-05], // f=2, s= 0, t= 34 +[ 1614, -1.4527685432017941e-05, -2.5021418914365550e-06], // f=2, s= 0, t= 35 +[ 1044, -5.5188522930647246e-06, 1.3429997819426629e-05], // f=2, s= 0, t= 36 +[ 239, -6.3224380303283023e-06, 1.2677314304305631e-05], // f=2, s= 0, t= 37 +[ 133, 2.9376054111895790e-06, -1.3077911011252820e-05], // f=2, s= 0, t= 38 +[ 1492, -9.7624713292315362e-06, 4.8639192065138997e-06], // f=2, s= 0, t= 39 +[ 1454, 8.1915641060518482e-06, -7.1484181790528786e-06], // f=2, s= 0, t= 40 +[ 4418, 2.9126133839920300e-06, -9.7148097661500440e-06], // f=2, s= 0, t= 41 +[ 177, -8.1108828396444682e-06, -5.8479936691072763e-06], // f=2, s= 0, t= 42 +[ 2663, -7.4109426380852051e-07, 8.1320485449777430e-06], // f=2, s= 0, t= 43 +[ 17334, 7.7062782105104396e-06, 2.1832991677727231e-06], // f=2, s= 0, t= 44 +[ 3016, -2.7097054305923721e-06, 7.2985152622098958e-06], // f=2, s= 0, t= 45 +[ 28266, 3.0978305562474201e-06, -6.9523478329398256e-06], // f=2, s= 0, t= 46 +[ 75, -7.5005963705916821e-06, 5.8552558907898478e-07], // f=2, s= 0, t= 47 +[ 354, 2.2767150385710288e-06, 7.1310390742790356e-06], // f=2, s= 0, t= 48 +[ 974, -2.8814074492595782e-06, 6.7739175425797296e-06], // f=2, s= 0, t= 49 +[ 59, -4.8255490447201409e-06, -5.3633858025303309e-06], // f=2, s= 0, t= 50 +[ 1115, 2.4165363833927252e-06, -5.9760260615249186e-06], // f=2, s= 0, t= 51 +[ 204, -1.5420460797033710e-07, -6.3645330720578637e-06], // f=2, s= 0, t= 52 +[ 612, 4.7791204064686441e-06, -4.1989180399775663e-06], // f=2, s= 0, t= 53 +[ 574, -3.2533619740383079e-06, 4.8942036839749464e-06], // f=2, s= 0, t= 54 +[ 978, -5.2816161970547299e-06, 2.4194318535884879e-06], // f=2, s= 0, t= 55 +[ 129, -4.1704589160563732e-06, -3.8074741128128591e-06], // f=2, s= 0, t= 56 +[ 200, -4.3860533690857434e-06, -2.8426817721421870e-06], // f=2, s= 0, t= 57 +[ 1685, -4.4937857977759297e-06, -2.6664132359831861e-06], // f=2, s= 0, t= 58 +[ 1327, -4.0394363815867892e-06, 3.0479150446235980e-06], // f=2, s= 0, t= 59 +[ 1257, -4.4570496522011644e-06, 1.8417244601604851e-06] // f=2, s= 0, t= 60 + ], + [ // f=2, s= 1 +[ 0, -6.1339663802007860e-04, 0.0000000000000000e+00], // f=2, s= 1, t= 0 +[ 1331, 5.6664110172313892e-04, -8.1133128701713292e-05], // f=2, s= 1, t= 1 +[ 1473, -1.9632805654171590e-04, -2.9843120844069362e-04], // f=2, s= 1, t= 2 +[ 71, -1.9721831884265259e-04, -1.2980246221969450e-04], // f=2, s= 1, t= 3 +[ 1402, -1.4909525632022481e-04, 5.5470899732068926e-06], // f=2, s= 1, t= 4 +[ 1261, 1.4379398981882231e-04, 1.8301247697520619e-05], // f=2, s= 1, t= 5 +[ 2945, -7.2143717725975376e-05, -6.1374574108146777e-05], // f=2, s= 1, t= 6 +[ 1190, 7.4611359353024315e-05, 3.0289539777815409e-05], // f=2, s= 1, t= 7 +[ 593, -5.9939495914062063e-05, -5.1836225465555612e-05], // f=2, s= 1, t= 8 +[ 8, 3.9984073707170713e-05, -3.0799116570219068e-05], // f=2, s= 1, t= 9 +[ 1543, 3.1094179263405468e-05, 2.6861784078602320e-05], // f=2, s= 1, t= 10 +[ 381, 3.7436713283265682e-05, -5.1576484771147526e-06], // f=2, s= 1, t= 11 +[ 1119, 2.9759191459225299e-05, 2.2175266912892650e-05], // f=2, s= 1, t= 12 +[ 452, 3.2286698454013622e-05, -1.4330028054859710e-05], // f=2, s= 1, t= 13 +[ 522, -3.3610639556594409e-05, 1.0732560764842151e-05], // f=2, s= 1, t= 14 +[ 2804, 2.6892727962532569e-05, -1.2202088159044841e-05], // f=2, s= 1, t= 15 +[ 4, 1.6145362682689730e-05, -2.1535180644982459e-05], // f=2, s= 1, t= 16 +[ 35, 1.1619842301292181e-05, 2.1259793782462840e-05], // f=2, s= 1, t= 17 +[ 310, 2.1159419772930708e-05, 3.2365387780938209e-06], // f=2, s= 1, t= 18 +[ 212, -1.7383881837993001e-05, -1.2570139978872199e-07], // f=2, s= 1, t= 19 +[ 2733, 1.5857546421435610e-05, -2.6232455173985150e-06], // f=2, s= 1, t= 20 +[ 1048, 9.8372722970562282e-06, 1.2182701032807050e-05], // f=2, s= 1, t= 21 +[ 12, 1.1952587407675961e-05, 7.5377841861106607e-06], // f=2, s= 1, t= 22 +[ 283, -1.0224581925761621e-05, 1.2303488561812810e-06], // f=2, s= 1, t= 23 +[ 239, 8.6293198115122691e-06, 4.6174857696641109e-06] // f=2, s= 1, t= 24 + ], + [ // f=2, s= 2 +[ 1331, 2.3820657956875651e-05, -1.4117217046583029e-04], // f=2, s= 2, t= 0 +[ 0, 5.7169784317623151e-05, 0.0000000000000000e+00], // f=2, s= 2, t= 1 +[ 1261, 2.8584658843704731e-05, -1.9153125455819128e-05], // f=2, s= 2, t= 2 +[ 71, -6.3863669075967202e-06, -3.0182516098252429e-05], // f=2, s= 2, t= 3 +[ 2945, -1.6831473476249729e-05, 1.7778057659448761e-05], // f=2, s= 2, t= 4 +[ 1402, -8.7282659443619542e-06, 2.1969730180690399e-05], // f=2, s= 2, t= 5 +[ 1190, 1.8655664239774939e-05, -1.4275349333837280e-05], // f=2, s= 2, t= 6 +[ 8, 2.0572423386833478e-05, 2.9970650695923720e-06] // f=2, s= 2, t= 7 + ] + ], + [ // f=3 + [ // f=3, s= 0 +[ 0, -1.7340471864230000e-01, 0.0000000000000000e+00], // f=3, s= 0, t= 0 +[ 1473, 2.1234813115076170e-03, 3.0130058621335031e-03], // f=3, s= 0, t= 1 +[ 71, -1.0505499200359431e-03, -7.0849649152681434e-04], // f=3, s= 0, t= 2 +[ 1331, 1.1926139685020671e-03, -1.2467514555563171e-04], // f=3, s= 0, t= 3 +[ 593, 6.3328818822552336e-04, 5.1838245948530805e-04], // f=3, s= 0, t= 4 +[ 1402, -4.0198902705438299e-04, 3.4879411820834029e-04], // f=3, s= 0, t= 5 +[ 1261, 4.6754614871725769e-04, 6.5799333440031267e-05], // f=3, s= 0, t= 6 +[ 141, -3.1574545029112658e-04, -1.1057917696717649e-04], // f=3, s= 0, t= 7 +[ 452, 2.7842572840314938e-04, -1.1076531434037220e-04], // f=3, s= 0, t= 8 +[ 2945, 1.4727652777389929e-04, 1.0316600256389621e-04], // f=3, s= 0, t= 9 +[ 1190, 1.4974726446281261e-04, 6.1544751468406387e-05], // f=3, s= 0, t= 10 +[ 1543, -1.1418008898386521e-04, -9.8159556344747838e-05], // f=3, s= 0, t= 11 +[ 522, -6.9432011255429296e-05, 1.0500648997119170e-04], // f=3, s= 0, t= 12 +[ 381, 1.2168346615540400e-04, -1.5724958971846889e-05], // f=3, s= 0, t= 13 +[ 4, 3.5694868279606838e-05, -1.1563400718698000e-04], // f=3, s= 0, t= 14 +[ 8, 1.0601495232676501e-04, -5.3792770194159239e-05], // f=3, s= 0, t= 15 +[ 212, -8.8206829660081605e-05, -5.6186044809908770e-06], // f=3, s= 0, t= 16 +[ 208, -6.2116275994912254e-05, 6.2681468828265711e-05], // f=3, s= 0, t= 17 +[ 106, -5.9881700950469839e-05, 1.8173312312951261e-05], // f=3, s= 0, t= 18 +[ 2804, 4.9734881336053070e-05, -2.4663551556821301e-05], // f=3, s= 0, t= 19 +[ 1119, 4.3142176920002318e-05, 3.2166118196149167e-05], // f=3, s= 0, t= 20 +[ 1186, 5.3160131153765831e-06, 4.6681999520445390e-05], // f=3, s= 0, t= 21 +[ 310, 4.2381917128956993e-05, 6.2114355060802278e-06], // f=3, s= 0, t= 22 +[ 67, 9.7801450369382322e-06, 3.8008739374555199e-05], // f=3, s= 0, t= 23 +[ 35, 3.4777523130477253e-05, 8.0807437621346330e-06], // f=3, s= 0, t= 24 +[ 664, -2.7772391338347379e-05, -1.2933056019872690e-05], // f=3, s= 0, t= 25 +[ 17476, 2.6116611382398249e-05, -5.2512707918252350e-06], // f=3, s= 0, t= 26 +[ 283, -2.5901894192097461e-05, 7.6613799535167943e-07], // f=3, s= 0, t= 27 +[ 28407, -1.3727149833568929e-06, -2.5571899427495732e-05], // f=3, s= 0, t= 28 +[ 12, 1.7840323650284740e-05, 1.4017491541647661e-05], // f=3, s= 0, t= 29 +[ 2875, -1.2752702589739361e-05, 1.8674367648851481e-05], // f=3, s= 0, t= 30 +[ 63, -2.1141833621778831e-05, -6.5305966080253417e-06], // f=3, s= 0, t= 31 +[ 2733, 2.1699001266656308e-05, -4.2325482084098446e-06], // f=3, s= 0, t= 32 +[ 1048, 1.1071845430997370e-05, 1.3823004095404599e-05], // f=3, s= 0, t= 33 +[ 137, -1.3154331920868999e-05, -9.3564722901572934e-06], // f=3, s= 0, t= 34 +[ 1044, 1.3602483536936790e-05, 6.6866532885834768e-06], // f=3, s= 0, t= 35 +[ 1614, 3.0521160394305390e-06, -1.4295846223173739e-05], // f=3, s= 0, t= 36 +[ 239, 1.2491201410693211e-05, 6.3469005652658392e-06], // f=3, s= 0, t= 37 +[ 133, -1.3409179117794041e-05, -2.6113349355778870e-06], // f=3, s= 0, t= 38 +[ 1492, -4.9926812258289137e-06, -9.3442347365693383e-06], // f=3, s= 0, t= 39 +[ 1454, 7.1182846121023137e-06, 7.7835699509488784e-06], // f=3, s= 0, t= 40 +[ 4418, 9.5085270250543767e-06, 2.5720788995216492e-06], // f=3, s= 0, t= 41 +[ 354, -7.7105865781446408e-06, 3.5195735852526461e-06], // f=3, s= 0, t= 42 +[ 2663, 8.0823599962064017e-06, 8.8289336639692517e-07], // f=3, s= 0, t= 43 +[ 75, -6.1089854320482681e-07, -8.0284473434343767e-06], // f=3, s= 0, t= 44 +[ 17334, 2.4666413292489791e-06, -7.6105265820617346e-06], // f=3, s= 0, t= 45 +[ 974, 6.9130746927758178e-06, 3.5048585089334658e-06], // f=3, s= 0, t= 46 +[ 28266, -6.8235793370187026e-06, -3.3487790585021339e-06], // f=3, s= 0, t= 47 +[ 3016, -7.0427593206860361e-06, -2.6580682576526168e-06], // f=3, s= 0, t= 48 +[ 59, -5.5456494164143571e-06, 4.9667283828556126e-06], // f=3, s= 0, t= 49 +[ 204, -7.0017722378178446e-06, -1.6817751033546979e-06], // f=3, s= 0, t= 50 +[ 1115, -6.4024937184914472e-06, 2.1192315231268140e-06], // f=3, s= 0, t= 51 +[ 978, 2.3623585675585379e-06, 5.3260537432133881e-06], // f=3, s= 0, t= 52 +[ 129, -3.9867297079366098e-06, 4.2341276287979132e-06], // f=3, s= 0, t= 53 +[ 574, -4.8314043929159330e-06, -2.9544972252560498e-06], // f=3, s= 0, t= 54 +[ 612, 4.0663883671886490e-06, 3.8452439907704313e-06], // f=3, s= 0, t= 55 +[ 200, -3.2782990554685469e-06, 4.3746397303989681e-06], // f=3, s= 0, t= 56 +[ 247, 4.4086088444075100e-06, -3.0262449322858620e-06], // f=3, s= 0, t= 57 +[ 1685, 2.7665561507113492e-06, -4.4616466628081997e-06], // f=3, s= 0, t= 58 +[ 1335, -2.3134382826680641e-06, 4.5013914052228881e-06], // f=3, s= 0, t= 59 +[ 1327, 3.1175433103076271e-06, 3.7924860549293981e-06], // f=3, s= 0, t= 60 +[ 903, 4.4741994730303496e-06, 1.7822080022821150e-06], // f=3, s= 0, t= 61 +[ 16, -1.4927342901310379e-06, 4.4884086825602869e-06], // f=3, s= 0, t= 62 +[ 169, 2.5354018835911621e-06, 3.5730000800789471e-06], // f=3, s= 0, t= 63 +[ 271, -2.1688629242033941e-06, 3.7820832945120280e-06], // f=3, s= 0, t= 64 +[ 79, 3.8979055894631164e-06, -1.8923407502835230e-06], // f=3, s= 0, t= 65 +[ 416, 3.9376438761867171e-06, -5.2538703904847715e-07], // f=3, s= 0, t= 66 +[ 17405, 1.0904344065136349e-06, 3.5409195078133210e-06], // f=3, s= 0, t= 67 +[ 28337, 3.4736718287656798e-06, -5.2407682497120616e-07], // f=3, s= 0, t= 68 +[ 1351, -3.4436468577006500e-06, -7.4140986208801818e-08], // f=3, s= 0, t= 69 +[ 1312, 3.3433376977429141e-06, -7.9547674738811048e-07], // f=3, s= 0, t= 70 +[ 832, 3.0647904819735712e-06, 1.2969722855848811e-06], // f=3, s= 0, t= 71 +[ 275, 1.5394331893610239e-06, -2.7751033895625021e-06], // f=3, s= 0, t= 72 +[ 2592, 2.8273974520595848e-06, 1.3333677990888271e-06], // f=3, s= 0, t= 73 +[ 17264, 1.6047868553221060e-06, -2.5731990436490451e-06] // f=3, s= 0, t= 74 + ], + [ // f=3, s= 1 +[ 1331, -6.0044174932694893e-05, -5.6841814115066798e-04], // f=3, s= 1, t= 0 +[ 1473, 2.8299001111328808e-04, -1.9899683117934169e-04], // f=3, s= 1, t= 1 +[ 71, 1.3109847892346901e-04, -1.9645547029607899e-04], // f=3, s= 1, t= 2 +[ 1402, 9.8753503746441094e-05, 1.1624141830784459e-04], // f=3, s= 1, t= 3 +[ 1261, 2.0857393493693379e-05, -1.4333407898431410e-04], // f=3, s= 1, t= 4 +[ 2945, 5.7600265965337831e-05, -7.1668848804472786e-05], // f=3, s= 1, t= 5 +[ 1190, 3.1158128562030883e-05, -7.4221508541133025e-05], // f=3, s= 1, t= 6 +[ 593, 4.9568824746840162e-05, -5.9443745710499677e-05], // f=3, s= 1, t= 7 +[ 0, -6.3916210233836622e-05, 0.0000000000000000e+00], // f=3, s= 1, t= 8 +[ 8, 3.2326459763508557e-05, 4.1097067153751401e-05], // f=3, s= 1, t= 9 +[ 4, 3.8505374914800768e-05, -2.4690750688454580e-05], // f=3, s= 1, t= 10 +[ 1543, -2.6127774810849349e-05, 2.9873942333388641e-05], // f=3, s= 1, t= 11 +[ 381, -4.4772545431527864e-06, -3.7396119251182150e-05], // f=3, s= 1, t= 12 +[ 1119, 2.2430310753365549e-05, -2.9557761386193919e-05], // f=3, s= 1, t= 13 +[ 522, 2.9886224978272480e-05, 2.0259791365064171e-05], // f=3, s= 1, t= 14 +[ 452, -1.3106498259692980e-05, -3.2637289766253343e-05], // f=3, s= 1, t= 15 +[ 2804, -1.1508149132666279e-05, -2.7316790371599908e-05], // f=3, s= 1, t= 16 +[ 310, 3.4738350608151140e-06, -2.1017437982180379e-05], // f=3, s= 1, t= 17 +[ 212, 2.1402716283134239e-06, -1.9178262093378069e-05], // f=3, s= 1, t= 18 +[ 2733, -2.3283762042050202e-06, -1.5882216399278110e-05], // f=3, s= 1, t= 19 +[ 1048, 1.2246829111767810e-05, -9.7512980427413643e-06], // f=3, s= 1, t= 20 +[ 12, -7.6135340345472092e-06, 1.2831304341856990e-05], // f=3, s= 1, t= 21 +[ 35, -5.7732549173295510e-06, 1.0016273100035810e-05], // f=3, s= 1, t= 22 +[ 283, -2.2363814820487809e-06, -9.6218108123675468e-06], // f=3, s= 1, t= 23 +[ 239, 4.6765179736328208e-06, -8.5111533123445213e-06], // f=3, s= 1, t= 24 +[ 106, -1.9836445213635969e-06, 7.8016311549488939e-06], // f=3, s= 1, t= 25 +[ 2875, 5.8334802150818700e-06, 5.1997278670085084e-06], // f=3, s= 1, t= 26 +[ 1044, 2.4978011849958161e-06, -6.6276491176842909e-06] // f=3, s= 1, t= 27 + ], + [ // f=3, s= 2 +[ 1331, -1.3993900805601181e-04, -2.9006879879181548e-05], // f=3, s= 2, t= 0 +[ 0, 6.7829783611580237e-05, 0.0000000000000000e+00], // f=3, s= 2, t= 1 +[ 1261, -1.8612445466711130e-05, -2.8908877766737650e-05], // f=3, s= 2, t= 2 +[ 71, 2.9853088727300880e-05, -7.1012915175069879e-06], // f=3, s= 2, t= 3 +[ 4, 2.6321188906063949e-05, 1.3464234343799510e-05], // f=3, s= 2, t= 4 +[ 1402, 2.3284325058403039e-05, -6.8144038337101704e-06], // f=3, s= 2, t= 5 +[ 2945, -1.7707706192590389e-05, -1.5833741011362669e-05], // f=3, s= 2, t= 6 +[ 1190, -1.4044910483808361e-05, -1.8816964596903641e-05], // f=3, s= 2, t= 7 +[ 8, -3.2757789019448882e-06, 2.1268129579065079e-05], // f=3, s= 2, t= 8 +[ 1473, -9.8933125150553904e-06, -1.3083286541079480e-05], // f=3, s= 2, t= 9 +[ 1119, -7.2140288335202669e-06, -1.1729524860644351e-05] // f=3, s= 2, t= 10 + ], + [ // f=3, s= 3 +[ 1331, -1.7737816039767501e-05, 2.8055420335966871e-05] // f=3, s= 3, t= 0 + ] + ], + [ // f=4 + [ // f=4, s= 0 +[ 0, -5.1702307822780000e-02, 0.0000000000000000e+00], // f=4, s= 0, t= 0 +[ 1402, -1.3296787157385281e-04, 1.3523784099823399e-04], // f=4, s= 0, t= 1 +[ 1543, 1.6300899640352639e-04, 5.5927755421839437e-05], // f=4, s= 0, t= 2 +[ 1473, -2.3179975765177140e-05, -9.8184622098344091e-05], // f=4, s= 0, t= 3 +[ 522, -1.9097390976957099e-05, 3.6607666661423369e-05], // f=4, s= 0, t= 4 +[ 664, 3.2543564829276270e-05, 1.0672339397019160e-06], // f=4, s= 0, t= 5 +[ 1331, -2.0841818514170151e-05, 1.2410223144740100e-05], // f=4, s= 0, t= 6 +[ 4, -8.0356073429830634e-06, 1.9857752253856971e-05], // f=4, s= 0, t= 7 +[ 593, -1.0459691293338759e-05, -1.7857031017673810e-05], // f=4, s= 0, t= 8 +[ 1614, 2.0002336741252269e-05, 1.4700260033157791e-06], // f=4, s= 0, t= 9 +[ 2875, -3.5880881863000781e-06, 8.0256130260543578e-06], // f=4, s= 0, t= 10 +[ 3016, 8.5254320868063325e-06, -1.8479247218425171e-07], // f=4, s= 0, t= 11 +[ 8, -5.4541260810660720e-06, 3.9805913226076404e-06], // f=4, s= 0, t= 12 +[ 452, -3.7355080445397231e-06, 4.0025013123176398e-06], // f=4, s= 0, t= 13 +[ 35, -2.1277230177098351e-06, -4.9607027782605714e-06], // f=4, s= 0, t= 14 +[ 137, -3.8233051639183096e-06, -3.1849844866680141e-06] // f=4, s= 0, t= 15 + ], + [ // f=4, s= 1 +[ 0, 1.9166844047183211e-04, 0.0000000000000000e+00], // f=4, s= 1, t= 0 +[ 1402, 3.9019701387765488e-05, 3.8671378812923788e-05], // f=4, s= 1, t= 1 +[ 1543, 1.5119207079965939e-05, -4.3332491992581899e-05], // f=4, s= 1, t= 2 +[ 1331, 5.8922122899993431e-06, 1.0029385340576861e-05], // f=4, s= 1, t= 3 +[ 522, 1.0280806202111021e-05, 5.2916455231996232e-06] // f=4, s= 1, t= 4 + ] + ], + [ // f=5 + [ // f=5, s= 0 +[ 0, 1.3977992515640000e-01, 0.0000000000000000e+00], // f=5, s= 0, t= 0 +[ 1402, 1.2883200572372361e-04, 1.3471156433694759e-04], // f=5, s= 0, t= 1 +[ 1543, -4.9979310098879121e-05, 1.6180896042292819e-04], // f=5, s= 0, t= 2 +[ 1473, -2.2060691195269469e-05, -9.3668360843677731e-05], // f=5, s= 0, t= 3 +[ 522, 3.5537595493916502e-05, 1.9797390613745271e-05], // f=5, s= 0, t= 4 +[ 664, -7.4709042862279015e-08, 3.1983280221550278e-05], // f=5, s= 0, t= 5 +[ 1331, 1.1695376261855580e-05, 2.0798562232570440e-05], // f=5, s= 0, t= 6 +[ 593, -9.9523808317349038e-06, -1.7074224315439731e-05], // f=5, s= 0, t= 7 +[ 1614, -9.4118113483089596e-07, 1.9684674172624949e-05], // f=5, s= 0, t= 8 +[ 4, -1.5932093431444279e-05, 9.4977011164352140e-06], // f=5, s= 0, t= 9 +[ 2875, 7.8417800319575014e-06, 3.7228551547920001e-06], // f=5, s= 0, t= 10 +[ 3016, 4.3444197029476981e-07, 8.3694483911743903e-06], // f=5, s= 0, t= 11 +[ 8, -5.9902893836260643e-06, -4.3498982251067908e-06], // f=5, s= 0, t= 12 +[ 137, -3.0046395968887028e-06, 4.7462972937701063e-06], // f=5, s= 0, t= 13 +[ 452, 3.9819715036126689e-06, 3.7540995120279268e-06], // f=5, s= 0, t= 14 +[ 35, -4.0365965056395762e-08, 5.3159364396026402e-06], // f=5, s= 0, t= 15 +[ 141, 1.2649492629014499e-07, 4.8536627302669076e-06], // f=5, s= 0, t= 16 +[ 1261, 1.1901325673451860e-06, 4.3994722705730970e-06], // f=5, s= 0, t= 17 +[ 2945, -2.7264749792923272e-06, -3.6454310686215340e-06], // f=5, s= 0, t= 18 +[ 1685, 6.9410338646847810e-07, 3.3566050467992739e-06], // f=5, s= 0, t= 19 +[ 734, 8.2952840171332113e-07, 3.2356209378486682e-06], // f=5, s= 0, t= 20 +[ 208, 2.4256846662067760e-06, -9.5181484992359002e-07], // f=5, s= 0, t= 21 +[ 279, -2.5271926303381422e-06, -3.7922031983877281e-07], // f=5, s= 0, t= 22 +[ 1115, 5.6515925473070844e-07, 2.1078436053637770e-06], // f=5, s= 0, t= 23 +[ 1257, -1.3558265037946960e-06, 1.4215822110290420e-06], // f=5, s= 0, t= 24 +[ 12, 1.3204389683748120e-07, -1.8835278008457151e-06], // f=5, s= 0, t= 25 +[ 71, -1.9042145379339269e-07, -1.8630215116559800e-06], // f=5, s= 0, t= 26 +[ 212, 1.6053042001407380e-06, 8.4671630560881122e-07], // f=5, s= 0, t= 27 +[ 17405, 1.3113174080732559e-06, -4.9311123653462177e-07], // f=5, s= 0, t= 28 +[ 17547, 1.0602949344425471e-06, 8.9833952832208403e-07], // f=5, s= 0, t= 29 +[ 2804, 9.2561678972169579e-07, 9.7236819416524451e-07], // f=5, s= 0, t= 30 +[ 28337, -9.4657252626238933e-08, -1.1160255453211769e-06], // f=5, s= 0, t= 31 +[ 28478, 9.2430781502463316e-07, -6.2265716348343233e-07], // f=5, s= 0, t= 32 +[ 75, -9.2130387409465058e-07, 5.9403283989994324e-07], // f=5, s= 0, t= 33 +[ 106, -2.0304770919600219e-07, -1.0388792453459521e-06], // f=5, s= 0, t= 34 +[ 381, 5.9915368756519242e-07, 8.3180916441107633e-07], // f=5, s= 0, t= 35 +[ 1190, 2.7201162062442490e-08, 1.0190741100380801e-06], // f=5, s= 0, t= 36 +[ 3087, 2.9873098731161120e-07, 9.6483981362737291e-07], // f=5, s= 0, t= 37 +[ 63, 5.8956418822020129e-07, -7.7745585971202028e-07], // f=5, s= 0, t= 38 +[ 1186, 2.4117256641652101e-07, -9.2801537744071498e-07], // f=5, s= 0, t= 39 +[ 67, -3.2941946279216570e-07, 7.9963658829793748e-07], // f=5, s= 0, t= 40 +[ 17476, -7.4361213537121858e-07, -1.2891468267067801e-07], // f=5, s= 0, t= 41 +[ 283, 2.8076344536095831e-08, 7.3853891759583075e-07], // f=5, s= 0, t= 42 +[ 1756, 3.1161152193772379e-07, 6.3104215205486464e-07], // f=5, s= 0, t= 43 +[ 247, -3.0658465478480348e-07, -6.2524043128321752e-07], // f=5, s= 0, t= 44 +[ 28407, -2.6107491970896922e-07, 5.4499817140536954e-07], // f=5, s= 0, t= 45 +[ 354, 4.9460726403829815e-07, 2.9447867230664199e-07], // f=5, s= 0, t= 46 +[ 1421, -3.1957781759109811e-07, -4.3033184225872391e-07], // f=5, s= 0, t= 47 +[ 1383, 4.0796989384481828e-07, 3.2647659230997358e-07], // f=5, s= 0, t= 48 +[ 133, 1.1815490710962760e-07, -5.0524398677283852e-07], // f=5, s= 0, t= 49 +[ 177, -4.6755777925983522e-07, 2.0641392675487111e-07], // f=5, s= 0, t= 50 +[ 805, 2.4510909183821792e-07, 4.3308098936875489e-07], // f=5, s= 0, t= 51 +[ 1563, 2.0041064699998999e-07, -4.4445832345851582e-07], // f=5, s= 0, t= 52 +[ 59, -1.2658294965005670e-07, -4.6671870382330612e-07], // f=5, s= 0, t= 53 +[ 1524, -7.8614887212231827e-08, 4.6760439016103602e-07], // f=5, s= 0, t= 54 +[ 4348, 4.6767825285878958e-07, 3.5290700804681861e-08], // f=5, s= 0, t= 55 +[ 16, 4.2127049178471667e-07, -1.8684252468076170e-07], // f=5, s= 0, t= 56 +[ 4489, 1.8055735116211371e-07, 4.2107641051871891e-07], // f=5, s= 0, t= 57 +[ 79, -4.0548063730912672e-07, -2.1065225409216059e-07], // f=5, s= 0, t= 58 +[ 1398, -1.2435454796683390e-07, 3.9919922981210330e-07], // f=5, s= 0, t= 59 +[ 145, -3.0487589061275071e-07, 2.8316787420643652e-07], // f=5, s= 0, t= 60 +[ 345, 3.4373900983948560e-07, -2.2193498580565619e-07], // f=5, s= 0, t= 61 +[ 275, -7.6720036516022037e-09, -4.0650745658300768e-07], // f=5, s= 0, t= 62 +[ 1406, -3.7816142635595027e-07, 9.1996049592574050e-08], // f=5, s= 0, t= 63 +[ 1044, 7.4671401050977241e-08, 3.7193907363226299e-07], // f=5, s= 0, t= 64 +[ 318, 7.1927751245935557e-08, 3.6928457890682560e-07], // f=5, s= 0, t= 65 +[ 416, 4.1798174498739052e-08, -3.6796074151811958e-07], // f=5, s= 0, t= 66 +[ 204, -1.5624747021437499e-08, -3.6071195814331651e-07], // f=5, s= 0, t= 67 +[ 1539, 3.3199296973509958e-07, -1.0438655407426570e-07], // f=5, s= 0, t= 68 +[ 1547, 2.1465474690281490e-07, 2.6396972342688539e-07], // f=5, s= 0, t= 69 +[ 1327, -1.5719732197123969e-07, 2.8693854391332578e-07], // f=5, s= 0, t= 70 +[ 541, 2.6198971229951878e-07, 1.7511268865594321e-07] // f=5, s= 0, t= 71 + ], + [ // f=5, s= 1 +[ 0, 7.7993297015907634e-05, 0.0000000000000000e+00], // f=5, s= 1, t= 0 +[ 1402, 3.9152687150310170e-05, -3.7172486736475712e-05], // f=5, s= 1, t= 1 +[ 1543, 4.3031136375758243e-05, 1.3517906141260201e-05], // f=5, s= 1, t= 2 +[ 1331, 1.0005173914454110e-05, -5.5510992766576859e-06], // f=5, s= 1, t= 3 +[ 522, 5.4799934835759310e-06, -9.9803123122233115e-06], // f=5, s= 1, t= 4 +[ 1473, -9.2379228749259814e-06, 2.0049455605464851e-06], // f=5, s= 1, t= 5 +[ 664, -3.2895419619690672e-06, -4.5974891215164072e-08], // f=5, s= 1, t= 6 +[ 8, 1.3908106265782320e-06, -2.9292856582721550e-06], // f=5, s= 1, t= 7 +[ 2875, 1.5562630279974180e-06, -2.5756858183805818e-06], // f=5, s= 1, t= 8 +[ 4, -2.4245388783587939e-06, -1.5068482062721909e-06], // f=5, s= 1, t= 9 +[ 3016, 2.7144173403334680e-06, 1.5168858882407361e-07], // f=5, s= 1, t= 10 +[ 2945, -1.9556025171954529e-06, 1.2707620314125440e-06], // f=5, s= 1, t= 11 +[ 35, 1.3344840689621260e-06, 1.4001396639490341e-06], // f=5, s= 1, t= 12 +[ 593, -1.5045227426363480e-06, 9.0293863845346919e-07], // f=5, s= 1, t= 13 +[ 1614, 1.4931074066475380e-06, 1.3901882598918031e-07], // f=5, s= 1, t= 14 +[ 1261, 1.3653087138211129e-06, -3.4669288959066708e-07], // f=5, s= 1, t= 15 +[ 12, 1.2315835838874919e-06, -5.9561839689487617e-08], // f=5, s= 1, t= 16 +[ 137, 9.0428610682743405e-07, 5.7865365381345505e-07], // f=5, s= 1, t= 17 +[ 2804, 5.6172637460901586e-07, -4.7331838000806132e-07], // f=5, s= 1, t= 18 +[ 212, -6.0813020695312143e-07, 1.1354550736721950e-07] // f=5, s= 1, t= 19 + ], + [ // f=5, s= 2 +[ 1402, -2.9412500484509639e-06, -8.0598049136548207e-06], // f=5, s= 2, t= 0 +[ 1543, -1.1369975198335059e-06, -6.6800431913703534e-06], // f=5, s= 2, t= 1 +[ 1331, -5.6564967655275266e-07, -2.8456118566588939e-06], // f=5, s= 2, t= 2 +[ 0, 2.4849999999999999e-06, 0.0000000000000000e+00], // f=5, s= 2, t= 3 +[ 522, -1.0188268325841671e-06, -1.4049711115470319e-06], // f=5, s= 2, t= 4 +[ 4, -1.2789041308060199e-06, -7.7265243668823065e-07], // f=5, s= 2, t= 5 +[ 8, 1.2656119872224440e-06, -4.3448350220923409e-07] // f=5, s= 2, t= 6 + ] + ] +] +}; + +const top_freq = { + Jupiter: 0.5296909622785881e+03, + Saturn: 0.2132990811942489e+03, + Uranus: 0.7478166163181234e+02, + Neptune: 0.3813297236217556e+02, + Pluto: 0.2533566020437000e+02 +}; + + +function calc_elliptical_coord(formula, dmu, f, t1) { + let el = 0.0; + let tpower = 1.0; + for (let s=0; s < formula.length; ++s) { + for (let [term_k, term_c, term_s] of formula[s]) { + if (f==1 && s==1 && term_k==0) + continue; + const arg = term_k * dmu * t1; + el += tpower * (term_c*Math.cos(arg) + term_s*Math.sin(arg)); + } + tpower *= t1; + } + return el; +} + + +function TopCalcElliptical(body, model, tt) +{ + /* Translated from: TOP2013.f */ + /* See: https://github.com/cosinekitty/ephemeris/tree/master/top2013 */ + /* Copied from: ftp://ftp.imcce.fr/pub/ephem/planets/top2013 */ + const t1 = tt / 365250.0; + const dmu = (top_freq.Jupiter - top_freq.Saturn) / 880.0; + + const ellip = { + a: calc_elliptical_coord(model[0], dmu, 0, t1), + lambda: calc_elliptical_coord(model[1], dmu, 1, t1), + k: calc_elliptical_coord(model[2], dmu, 2, t1), + h: calc_elliptical_coord(model[3], dmu, 3, t1), + q: calc_elliptical_coord(model[4], dmu, 4, t1), + p: calc_elliptical_coord(model[5], dmu, 5, t1) + }; + + let xl = ellip.lambda + top_freq[body] * t1; + xl %= PI2; + if (xl < 0.0) + xl += PI2; + ellip.lambda = xl; + return ellip; +} + + +function TopEcliptic(ellip, time) { + let xa = ellip.a; + let xl = ellip.lambda; + let xk = ellip.k; + let xh = ellip.h; + let xq = ellip.q; + let xp = ellip.p; + let xfi = Math.sqrt(1.0 - xk*xk - xh*xh); + let xki = Math.sqrt(1.0 - xq*xq - xp*xp); + let zr = xk; + let zi = xh; + let u = 1.0 / (1.0 + xfi); + let ex2 = zr*zr + zi*zi; + let ex = Math.sqrt(ex2); + let ex3 = ex * ex2; + let z1r = zr; + let z1i = -zi; + let gl = xl % PI2; + let gm = gl - Math.atan2(xh, xk); + let e = gl + (ex - 0.125*ex3)*Math.sin(gm) + 0.5*ex2*Math.sin(2.0*gm) + 0.375*ex3*Math.sin(3.0*gm); + + let dl, z2r, z2i, zteta_r, zteta_i, z3r, z3i, rsa; + do + { + z2r = 0.0; + z2i = e; + zteta_r = Math.cos(z2i); + zteta_i = Math.sin(z2i); + z3r = z1r*zteta_r - z1i*zteta_i; + z3i = z1r*zteta_i + z1i*zteta_r; + dl = gl - e + z3i; + rsa = 1.0 - z3r; + e += dl/rsa; + } while (Math.abs(dl) >= 1.0e-15); + + z1r = z3i * u * zr; + z1i = z3i * u * zi; + z2r = +z1i; + z2i = -z1r; + let zto_r = (-zr + zteta_r + z2r) / rsa; + let zto_i = (-zi + zteta_i + z2i) / rsa; + let xcw = zto_r; + let xsw = zto_i; + let xm = xp*xcw - xq*xsw; + let xr = xa*rsa; + + return new Vector( + xr*(xcw - 2.0*xp*xm), + xr*(xsw + 2.0*xq*xm), + -2.0*xr*xki*xm, + time + ); +} + + +let top_eq_rot; + + +function TopEquatorial(ecl) { + if (!top_eq_rot) { + const sdrad = DEG2RAD / 3600.0; + const eps = (23.0 + 26.0/60.0 + 21.41136/3600.0)*DEG2RAD; + const phi = -0.05188 * sdrad; + const ceps = Math.cos(eps); + const seps = Math.sin(eps); + const cphi = Math.cos(phi); + const sphi = Math.sin(phi); + + top_eq_rot = [ + [cphi, -sphi*ceps, sphi*seps], + [sphi, cphi*ceps, -cphi*seps], + [0.0, seps, ceps] + ]; + } + + return new Vector( + (top_eq_rot[0][0] * ecl.x) + (top_eq_rot[0][1] * ecl.y) + (top_eq_rot[0][2] * ecl.z), + (top_eq_rot[1][0] * ecl.x) + (top_eq_rot[1][1] * ecl.y) + (top_eq_rot[1][2] * ecl.z), + (top_eq_rot[2][0] * ecl.x) + (top_eq_rot[2][1] * ecl.y) + (top_eq_rot[2][2] * ecl.z), + ecl.t + ); +} + + +function TopPosition(body, time) { + const ellip = TopCalcElliptical(body, top2013[body], time.tt); + const ecl = TopEcliptic(ellip, time); + return TopEquatorial(ecl); +} + + +Astronomy.DeltaT_EspenakMeeus = function(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); +} + + +Astronomy.DeltaT_JplHorizons = function(ut) { + return Astronomy.DeltaT_EspenakMeeus(Math.min(ut, 17.0 * DAYS_PER_TROPICAL_YEAR)); +} + +var DeltaT = Astronomy.DeltaT_EspenakMeeus; + +Astronomy.SetDeltaTFunction = function(func) { + DeltaT = func; +} + +/** + * 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 this type are used throughout the internals + * of the Astronomy library, and are included in certain return objects. + * The constructor is not accessible outside the Astronomy library; + * outside users should call the {@link Astronomy.MakeTime} function + * to create an `AstroTime` object. + * + * @class + * @memberof Astronomy + * + * @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 historical and predictive models of those variations. + */ +class AstroTime { + /** + * @param {(Date|number)} date + * A JavaScript Date object or a numeric UTC value expressed in J2000 days. + */ + constructor(date) { + const MillisPerDay = 1000 * 3600 * 24; + + if (IsValidDate(date)) { + this.date = date; + this.ut = (date - J2000) / MillisPerDay; + this.tt = TerrestrialTime(this.ut); + return; + } + + if (Number.isFinite(date)) { + this.date = new Date(J2000 - (-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.'; + } + + /** + * 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 {Astronomy.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); + } +} + +function InterpolateTime(time1, time2, fraction) { + return new AstroTime(time1.ut + fraction*(time2.ut - time1.ut)); +} + +/** + * Given a Date object or a number days since noon (12:00) on January 1, 2000 (UTC), + * this function creates an {@link Astronomy.AstroTime} object. + * Given an {@link Astronomy.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 Astronomy.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 Astronomy.AstroTime format and passing the result into multiple + * function calls may be more efficient than passing in native JavaScript Date objects. + * + * @param {(Date | number | Astronomy.AstroTime)} date + * A Date object, a number of UTC days since the J2000 epoch (noon on January 1, 2000), + * or an Astronomy.AstroTime object. See remarks above. + * + * @returns {Astronomy.AstroTime} + */ +Astronomy.MakeTime = function(date) { + if (date instanceof AstroTime) { + return date; + } + return new AstroTime(date); +} + +const iaudata = [ + + { nals:[ 0, 0, 0, 0, 1 ], cls:[ -172064161, -174666, 33386, 92052331, 9086, 15377 ] }, + { nals:[ 0, 0, 2, -2, 2 ], cls:[ -13170906, -1675, -13696, 5730336, -3015, -4587 ] }, + { nals:[ 0, 0, 2, 0, 2 ], cls:[ -2276413, -234, 2796, 978459, -485, 1374 ] }, + { nals:[ 0, 0, 0, 0, 2 ], cls:[ 2074554, 207, -698, -897492, 470, -291 ] }, + { nals:[ 0, 1, 0, 0, 0 ], cls:[ 1475877, -3633, 11817, 73871, -184, -1924 ] }, + { nals:[ 0, 1, 2, -2, 2 ], cls:[ -516821, 1226, -524, 224386, -677, -174 ] }, + { nals:[ 1, 0, 0, 0, 0 ], cls:[ 711159, 73, -872, -6750, 0, 358 ] }, + { nals:[ 0, 0, 2, 0, 1 ], cls:[ -387298, -367, 380, 200728, 18, 318 ] }, + { nals:[ 1, 0, 2, 0, 2 ], cls:[ -301461, -36, 816, 129025, -63, 367 ] }, + { nals:[ 0, -1, 2, -2, 2 ], cls:[ 215829, -494, 111, -95929, 299, 132 ] }, + { nals:[ 0, 0, 2, -2, 1 ], cls:[ 128227, 137, 181, -68982, -9, 39 ] }, + { nals:[ -1, 0, 2, 0, 2 ], cls:[ 123457, 11, 19, -53311, 32, -4 ] }, + { nals:[ -1, 0, 0, 2, 0 ], cls:[ 156994, 10, -168, -1235, 0, 82 ] }, + { nals:[ 1, 0, 0, 0, 1 ], cls:[ 63110, 63, 27, -33228, 0, -9 ] }, + { nals:[ -1, 0, 0, 0, 1 ], cls:[ -57976, -63, -189, 31429, 0, -75 ] }, + { nals:[ -1, 0, 2, 2, 2 ], cls:[ -59641, -11, 149, 25543, -11, 66 ] }, + { nals:[ 1, 0, 2, 0, 1 ], cls:[ -51613, -42, 129, 26366, 0, 78 ] }, + { nals:[ -2, 0, 2, 0, 1 ], cls:[ 45893, 50, 31, -24236, -10, 20 ] }, + { nals:[ 0, 0, 0, 2, 0 ], cls:[ 63384, 11, -150, -1220, 0, 29 ] }, + { nals:[ 0, 0, 2, 2, 2 ], cls:[ -38571, -1, 158, 16452, -11, 68 ] }, + { nals:[ 0, -2, 2, -2, 2 ], cls:[ 32481, 0, 0, -13870, 0, 0 ] }, + { nals:[ -2, 0, 0, 2, 0 ], cls:[ -47722, 0, -18, 477, 0, -25 ] }, + { nals:[ 2, 0, 2, 0, 2 ], cls:[ -31046, -1, 131, 13238, -11, 59 ] }, + { nals:[ 1, 0, 2, -2, 2 ], cls:[ 28593, 0, -1, -12338, 10, -3 ] }, + { nals:[ -1, 0, 2, 0, 1 ], cls:[ 20441, 21, 10, -10758, 0, -3 ] }, + { nals:[ 2, 0, 0, 0, 0 ], cls:[ 29243, 0, -74, -609, 0, 13 ] }, + { nals:[ 0, 0, 2, 0, 0 ], cls:[ 25887, 0, -66, -550, 0, 11 ] }, + { nals:[ 0, 1, 0, 0, 1 ], cls:[ -14053, -25, 79, 8551, -2, -45 ] }, + { nals:[ -1, 0, 0, 2, 1 ], cls:[ 15164, 10, 11, -8001, 0, -1 ] }, + { nals:[ 0, 2, 2, -2, 2 ], cls:[ -15794, 72, -16, 6850, -42, -5 ] }, + { nals:[ 0, 0, -2, 2, 0 ], cls:[ 21783, 0, 13, -167, 0, 13 ] }, + { nals:[ 1, 0, 0, -2, 1 ], cls:[ -12873, -10, -37, 6953, 0, -14 ] }, + { nals:[ 0, -1, 0, 0, 1 ], cls:[ -12654, 11, 63, 6415, 0, 26 ] }, + { nals:[ -1, 0, 2, 2, 1 ], cls:[ -10204, 0, 25, 5222, 0, 15 ] }, + { nals:[ 0, 2, 0, 0, 0 ], cls:[ 16707, -85, -10, 168, -1, 10 ] }, + { nals:[ 1, 0, 2, 2, 2 ], cls:[ -7691, 0, 44, 3268, 0, 19 ] }, + { nals:[ -2, 0, 2, 0, 0 ], cls:[ -11024, 0, -14, 104, 0, 2 ] }, + { nals:[ 0, 1, 2, 0, 2 ], cls:[ 7566, -21, -11, -3250, 0, -5 ] }, + { nals:[ 0, 0, 2, 2, 1 ], cls:[ -6637, -11, 25, 3353, 0, 14 ] }, + { nals:[ 0, -1, 2, 0, 2 ], cls:[ -7141, 21, 8, 3070, 0, 4 ] }, + { nals:[ 0, 0, 0, 2, 1 ], cls:[ -6302, -11, 2, 3272, 0, 4 ] }, + { nals:[ 1, 0, 2, -2, 1 ], cls:[ 5800, 10, 2, -3045, 0, -1 ] }, + { nals:[ 2, 0, 2, -2, 2 ], cls:[ 6443, 0, -7, -2768, 0, -4 ] }, + { nals:[ -2, 0, 0, 2, 1 ], cls:[ -5774, -11, -15, 3041, 0, -5 ] }, + { nals:[ 2, 0, 2, 0, 1 ], cls:[ -5350, 0, 21, 2695, 0, 12 ] }, + { nals:[ 0, -1, 2, -2, 1 ], cls:[ -4752, -11, -3, 2719, 0, -3 ] }, + { nals:[ 0, 0, 0, -2, 1 ], cls:[ -4940, -11, -21, 2720, 0, -9 ] }, + { nals:[ -1, -1, 0, 2, 0 ], cls:[ 7350, 0, -8, -51, 0, 4 ] }, + { nals:[ 2, 0, 0, -2, 1 ], cls:[ 4065, 0, 6, -2206, 0, 1 ] }, + { nals:[ 1, 0, 0, 2, 0 ], cls:[ 6579, 0, -24, -199, 0, 2 ] }, + { nals:[ 0, 1, 2, -2, 1 ], cls:[ 3579, 0, 5, -1900, 0, 1 ] }, + { nals:[ 1, -1, 0, 0, 0 ], cls:[ 4725, 0, -6, -41, 0, 3 ] }, + { nals:[ -2, 0, 2, 0, 2 ], cls:[ -3075, 0, -2, 1313, 0, -1 ] }, + { nals:[ 3, 0, 2, 0, 2 ], cls:[ -2904, 0, 15, 1233, 0, 7 ] }, + { nals:[ 0, -1, 0, 2, 0 ], cls:[ 4348, 0, -10, -81, 0, 2 ] }, + { nals:[ 1, -1, 2, 0, 2 ], cls:[ -2878, 0, 8, 1232, 0, 4 ] }, + { nals:[ 0, 0, 0, 1, 0 ], cls:[ -4230, 0, 5, -20, 0, -2 ] }, + { nals:[ -1, -1, 2, 2, 2 ], cls:[ -2819, 0, 7, 1207, 0, 3 ] }, + { nals:[ -1, 0, 2, 0, 0 ], cls:[ -4056, 0, 5, 40, 0, -2 ] }, + { nals:[ 0, -1, 2, 2, 2 ], cls:[ -2647, 0, 11, 1129, 0, 5 ] }, + { nals:[ -2, 0, 0, 0, 1 ], cls:[ -2294, 0, -10, 1266, 0, -4 ] }, + { nals:[ 1, 1, 2, 0, 2 ], cls:[ 2481, 0, -7, -1062, 0, -3 ] }, + { nals:[ 2, 0, 0, 0, 1 ], cls:[ 2179, 0, -2, -1129, 0, -2 ] }, + { nals:[ -1, 1, 0, 1, 0 ], cls:[ 3276, 0, 1, -9, 0, 0 ] }, + { nals:[ 1, 1, 0, 0, 0 ], cls:[ -3389, 0, 5, 35, 0, -2 ] }, + { nals:[ 1, 0, 2, 0, 0 ], cls:[ 3339, 0, -13, -107, 0, 1 ] }, + { nals:[ -1, 0, 2, -2, 1 ], cls:[ -1987, 0, -6, 1073, 0, -2 ] }, + { nals:[ 1, 0, 0, 0, 2 ], cls:[ -1981, 0, 0, 854, 0, 0 ] }, + { nals:[ -1, 0, 0, 1, 0 ], cls:[ 4026, 0, -353, -553, 0, -139 ] }, + { nals:[ 0, 0, 2, 1, 2 ], cls:[ 1660, 0, -5, -710, 0, -2 ] }, + { nals:[ -1, 0, 2, 4, 2 ], cls:[ -1521, 0, 9, 647, 0, 4 ] }, + { nals:[ -1, 1, 0, 1, 1 ], cls:[ 1314, 0, 0, -700, 0, 0 ] }, + { nals:[ 0, -2, 2, -2, 1 ], cls:[ -1283, 0, 0, 672, 0, 0 ] }, + { nals:[ 1, 0, 2, 2, 1 ], cls:[ -1331, 0, 8, 663, 0, 4 ] }, + { nals:[ -2, 0, 2, 2, 2 ], cls:[ 1383, 0, -2, -594, 0, -2 ] }, + { nals:[ -1, 0, 0, 0, 2 ], cls:[ 1405, 0, 4, -610, 0, 2 ] }, + { nals:[ 1, 1, 2, -2, 2 ], cls:[ 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].nals; + cls = iaudata[i].cls; + 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 * ASEC2RAD) + (dp * 1.0e-7 * ASEC2RAD), + deps: (+0.000388 * ASEC2RAD) + (de * 1.0e-7 * ASEC2RAD) + }; + } + +function nutation_angles(time) { + var nut = iau2000b(time); + return { dpsi: nut.dpsi/ASEC2RAD, deps: nut.deps/ASEC2RAD }; +} + +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 = nutation_angles(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 * DEG2RAD) / 15, + mobl: mean_ob, + tobl: true_ob + }; + } + return cache_e_tilt; +} + +function ecl2equ_vec(time, pos) { + var obl = mean_obliq(time) * 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 + ]; +} + +Astronomy.CalcMoonCount = 0; + +function CalcMoon(time) { + ++Astronomy.CalcMoonCount; + + const T = time.tt / 36525; + + function DeclareArray1(xmin, xmax) { + var array = []; + var i; + for (i=0; i <= xmax-xmin; ++i) { + array.push(0); + } + return {min:xmin, array:array}; + } + + function DeclareArray2(xmin, xmax, ymin, ymax) { + var array = []; + var i; + for (i=0; i <= xmax-xmin; ++i) { + array.push(DeclareArray1(ymin, ymax)); + } + return {min:xmin, array:array}; + } + + function ArrayGet2(a, x, y) { + var m = a.array[x - a.min]; + return m.array[y - m.min]; + } + + function ArraySet2(a, x, y, v) { + var m = a.array[x - a.min]; + m.array[y - m.min] = v; + } + + var S, MAX, ARG, FAC, I, J, T2, DGAM, DLAM, N, GAM1C, SINPI, L0, L, LS, F, D, DL0, DL, DLS, DF, DD, DS; + var coArray = DeclareArray2(-6, 6, 1, 4); + var 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) { + return 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; + } + 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 = [ null, p, q, r, s ]; + 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; + + var 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) + }; +} + +function precession(tt1, pos1, tt2) { + const r = precession_rot(tt1, tt2); + return [ + r.rot[0][0]*pos1[0] + r.rot[1][0]*pos1[1] + r.rot[2][0]*pos1[2], + r.rot[0][1]*pos1[0] + r.rot[1][1]*pos1[1] + r.rot[2][1]*pos1[2], + r.rot[0][2]*pos1[0] + r.rot[1][2]*pos1[1] + r.rot[2][2]*pos1[2] + ]; +} + +function precession_rot(tt1, tt2) { + var xx, yx, zx, xy, yy, zy, xz, yz, zz; + var eps0 = 84381.406; + var t, psia, omegaa, chia, sa, ca, sb, cb, sc, cc, sd, cd; + + if ((tt1 !== 0) && (tt2 !== 0)) + throw 'One of (tt1, tt2) must be 0.'; + + t = (tt2 - tt1) / 36525; + if (tt2 === 0) + t = -t; + + psia = (((((- 0.0000000951 * t + + 0.000132851 ) * t + - 0.00114045 ) * t + - 1.0790069 ) * t + + 5038.481507 ) * t); + + omegaa = (((((+ 0.0000003337 * t + - 0.000000467 ) * t + - 0.00772503 ) * t + + 0.0512623 ) * t + - 0.025754 ) * t + eps0); + + chia = (((((- 0.0000000560 * t + + 0.000170663 ) * t + - 0.00121197 ) * t + - 2.3814292 ) * t + + 10.556403 ) * t); + + eps0 = eps0 * ASEC2RAD; + psia = psia * ASEC2RAD; + omegaa = omegaa * ASEC2RAD; + chia = chia * ASEC2RAD; + + sa = Math.sin(eps0); + ca = Math.cos(eps0); + sb = Math.sin(-psia); + cb = Math.cos(-psia); + sc = Math.sin(-omegaa); + cc = Math.cos(-omegaa); + sd = Math.sin(chia); + cd = Math.cos(chia); + + xx = cd * cb - sb * sd * cc; + yx = cd * sb * ca + sd * cc * cb * ca - sa * sd * sc; + zx = cd * sb * sa + sd * cc * cb * sa + ca * sd * sc; + xy = -sd * cb - sb * cd * cc; + yy = -sd * sb * ca + cd * cc * cb * ca - sa * cd * sc; + zy = -sd * sb * sa + cd * cc * cb * sa + ca * cd * sc; + xz = sb * sc; + yz = -sc * cb * ca - sa * cc; + zz = -sc * cb * sa + cc * ca; + + if (tt2 === 0) { + // Perform rotation from epoch to J2000.0. + return new RotationMatrix([ + [xx, yx, zx], + [xy, yy, zy], + [xz, yz, zz] + ]); + } + + // Perform rotation from J2000.0 to epoch. + return new RotationMatrix([ + [xx, xy, xz], + [yx, yy, yz], + [zx, zy, zz] + ]); +} + +function era(time) { // Earth Rotation Angle + 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) { // calculates Greenwich Apparent Sidereal Time (GAST) + 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; +} + +function terra(observer, st) { + const df = 1 - 0.003352819697896; // flattening of the Earth + const df2 = df * df; + const phi = observer.latitude * 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) * DEG2RAD; + const sinst = Math.sin(stlocl); + const cosst = Math.cos(stlocl); + return { + pos: [ach*cosphi*cosst/KM_PER_AU, ach*cosphi*sinst/KM_PER_AU, ash*sinphi/KM_PER_AU], + vel: [-ANGVEL*ach*cosphi*sinst*86400, ANGVEL*ach*cosphi*cosst*86400, 0] + }; +} + +function nutation(time, direction, pos) { + const r = nutation_rot(time, direction); + return [ + r.rot[0][0]*pos[0] + r.rot[1][0]*pos[1] + r.rot[2][0]*pos[2], + r.rot[0][1]*pos[0] + r.rot[1][1]*pos[1] + r.rot[2][1]*pos[2], + r.rot[0][2]*pos[0] + r.rot[1][2]*pos[1] + r.rot[2][2]*pos[2] + ]; +} + +function nutation_rot(time, direction) { + const tilt = e_tilt(time); + const oblm = tilt.mobl * DEG2RAD; + const oblt = tilt.tobl * 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 (direction === 0) { + // forward rotation + return new RotationMatrix([ + [xx, xy, xz], + [yx, yy, yz], + [zx, zy, zz] + ]); + } + + // inverse rotation + return new RotationMatrix([ + [xx, yx, zx], + [xy, yy, zy], + [xz, yz, zz] + ]); +} + +function geo_pos(time, observer) { + const gast = sidereal_time(time); + const pos1 = terra(observer, gast).pos; + const pos2 = nutation(time, -1, pos1); + const pos3 = precession(time.tt, pos2, 0); + return pos3; +} + +/** + * Holds the Cartesian coordinates of a vector in 3D space, + * along with the time at which the vector is valid. + * + * @class + * @memberof Astronomy + * + * @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 {Astronomy.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); + } +} + +/** + * Holds spherical coordinates: latitude, longitude, distance. + * + * @class + * @memberof Astronomy + * + * @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); + } +} + +/** + * Create spherical coordinates. + * + * @param {number} lat + * The angular distance above or below the reference plane, in degrees. + * + * @param {number} lon + * The angular distance around the reference plane, in degrees. + * + * @param {number} dist + * A radial distance in AU. + * + * @returns {Astronomy.Spherical} + */ +Astronomy.MakeSpherical = function(lat, lon, dist) { + return new Spherical(lat, lon, dist); +} + +/** + * Holds right ascension, declination, and distance of a celestial object. + * + * @class + * @memberof Astronomy + * + * @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 + * astronomical units (AU). + */ +class EquatorialCoordinates { + constructor(ra, dec, dist) { + this.ra = VerifyNumber(ra); + this.dec = VerifyNumber(dec); + this.dist = VerifyNumber(dist); + } +} + +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; +} + + +/** + * Contains a rotation matrix that can be used to transform one coordinate system to another. + * + * @class + * @memberof Astronomy + * + * @property {Array>} rot + * A normalized 3x3 rotation matrix. + */ +class RotationMatrix { + constructor(rot) { + this.rot = rot; + } +} + +/** + * Creates a rotation matrix that can be used to transform one coordinate system to another. + * + * @param {Array>} 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 {Astronomy.RotationMatrix} + */ +Astronomy.MakeRotation = function(rot) { + if (!IsValidRotationArray(rot)) + throw 'Argument must be a [3][3] array of numbers'; + + return new RotationMatrix(rot); +} + +/** + * 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 Astronomy.EquatorialCoordinates} object. + * + * @class + * @memberof Astronomy + * + * @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); + } +} + +/** + * Holds 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 + * astronomical units (AU) + * and spherical coordinates `(elon, elat)` measured in degrees. + * + * @class + * @memberof Astronomy + * + * @property {number} ex + * The Cartesian x-coordinate of the body in astronomical units (AU). + * The x-axis is within the ecliptic plane and is oriented in the direction of the + * equinox. + * + * @property {number} ey + * The Cartesian y-coordinate of the body in astronomical units (AU). + * 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. + * + * @property {number} ez + * The Cartesian z-coordinate of the body in astronomical units (AU). + * 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(ex, ey, ez, elat, elon) { + this.ex = VerifyNumber(ex); + this.ey = VerifyNumber(ey); + this.ez = VerifyNumber(ez); + this.elat = VerifyNumber(elat); + this.elon = VerifyNumber(elon); + } +} + +function vector2radec(pos) +{ + const xyproj = pos[0]*pos[0] + pos[1]*pos[1]; + const dist = Math.sqrt(xyproj + pos[2]*pos[2]); + if (xyproj === 0) + { + if (pos[2] === 0) + throw 'Indeterminate sky coordinates'; + + if (pos[2] < 0) + return { ra:0, dec:-90, dist:dist }; + + return { ra:0, dec:+90, dist:dist }; + } + + let ra = Math.atan2(pos[1], pos[0]) / (DEG2RAD * 15); + if (ra < 0) { + ra += 24; + } + let dec = Math.atan2(pos[2], Math.sqrt(xyproj)) / DEG2RAD; + return new EquatorialCoordinates(ra, dec, dist); +} + +function spin(angle, pos1) { + const angr = angle * DEG2RAD; + const cosang = Math.cos(angr); + const sinang = Math.sin(angr); + const xx = cosang; + const yx = sinang; + const zx = 0; + const xy = -sinang; + const yy = cosang; + const zy = 0; + const xz = 0; + const yz = 0; + const zz = 1; + let pos2 = [ + xx*pos1[0] + yx*pos1[1] + zx*pos1[2], + xy*pos1[0] + yy*pos1[1] + zy*pos1[2], + xz*pos1[0] + yz*pos1[1] + zz*pos1[2] + ]; + return pos2; +} + +/** + * 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 {(Date|number|Astronomy.AstroTime)} date + * The date and time for which to find horizontal coordinates. + * + * @param {Astronomy.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 {Astronomy.HorizontalCoordinates} + */ +Astronomy.Horizon = function(date, observer, ra, dec, refraction) { // based on NOVAS equ2hor() + let time = Astronomy.MakeTime(date); + VerifyObserver(observer); + VerifyNumber(ra); + VerifyNumber(dec); + + const sinlat = Math.sin(observer.latitude * DEG2RAD); + const coslat = Math.cos(observer.latitude * DEG2RAD); + const sinlon = Math.sin(observer.longitude * DEG2RAD); + const coslon = Math.cos(observer.longitude * DEG2RAD); + const sindc = Math.sin(dec * DEG2RAD); + const cosdc = Math.cos(dec * DEG2RAD); + const sinra = Math.sin(ra * 15 * DEG2RAD); + const cosra = Math.cos(ra * 15 * DEG2RAD); + let uze = [coslat*coslon, coslat*sinlon, sinlat]; + let une = [-sinlat*coslon, -sinlat*sinlon, coslat]; + let uwe = [sinlon, -coslon, 0]; + + 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); + + let p = [cosdc*cosra, cosdc*sinra, sindc]; + + 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]; + + let proj = Math.sqrt(pn*pn + pw*pw); + let az = 0; + if (proj > 0) { + az = -Math.atan2(pw, pn) * RAD2DEG; + if (az < 0) az += 360; + if (az >= 360) az -= 360; + } + let zd = Math.atan2(proj, pz) * RAD2DEG; + let out_ra = ra; + let out_dec = dec; + + if (refraction) { + let zd0 = zd; + let refr = Astronomy.Refraction(refraction, 90-zd); + zd -= refr; + if (refr > 0.0 && zd > 3.0e-4) { + const sinzd = Math.sin(zd * DEG2RAD); + const coszd = Math.cos(zd * DEG2RAD); + const sinzd0 = Math.sin(zd0 * DEG2RAD); + const coszd0 = Math.cos(zd0 * 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 = Math.atan2(pr[1], pr[0]) * RAD2DEG / 15; + if (out_ra < 0) { + out_ra += 24; + } + if (out_ra >= 24) { + out_ra -= 24; + } + } else { + out_ra = 0; + } + out_dec = Math.atan2(pr[2], proj) * RAD2DEG; + } + } + + return new HorizontalCoordinates(az, 90-zd, out_ra, out_dec); +} + + +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; +} + + +/** + * Represents the geographic location of an observer on the surface of the Earth. + * + * @class + * @memberof Astronomy + * + * @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_degrees, longitude_degrees, height_in_meters) { + this.latitude = latitude_degrees; + this.longitude = longitude_degrees; + this.height = height_in_meters; + VerifyObserver(this); + } +} + + +/** + * Creates an {@link Astronomy.Observer} object that represents a location + * on the surface of the Earth from which observations are made. + * + * @param {number} latitude_degrees + * 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. + * + * @param {number} longitude_degrees + * 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. + * + * @param {number} height_in_meters + * The observer's elevation above mean sea level, expressed in meters. + * If omitted, the elevation is assumed to be 0 meters. + */ +Astronomy.MakeObserver = function(latitude_degrees, longitude_degrees, height_in_meters) { + return new Observer(latitude_degrees, longitude_degrees, height_in_meters || 0); +} + +/** + * Returns apparent geocentric true ecliptic coordinates of date for the Sun. + * Geocentric means coordinates as the Sun would appear to a hypothetical observer + * at the center of the Earth. + * Ecliptic coordinates of date are measured along the plane of the Earth's mean + * orbit around the Sun, using the + * equinox + * of the Earth as adjusted for precession and nutation of the Earth's + * axis of rotation on the given date. + * + * @param {(Date | number | Astronomy.AstroTime)} date + * The date and time at which to calculate the Sun's apparent location as seen from + * the center of the Earth. + * + * @returns {Astronomy.EclipticCoordinates} + */ +Astronomy.SunPosition = function(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 = Astronomy.MakeTime(date).AddDays(-1 / 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 stemp = precession(0, sun2000, time.tt); + const sun_ofdate = nutation(time, 0, stemp); + + // Convert to ecliptic coordinates of date. + const true_obliq = DEG2RAD * e_tilt(time).tobl; + const cos_ob = Math.cos(true_obliq); + const sin_ob = Math.sin(true_obliq); + + const gx = sun_ofdate[0]; + const gy = sun_ofdate[1]; + const gz = sun_ofdate[2]; + + const sun_ecliptic = RotateEquatorialToEcliptic(gx, gy, gz, cos_ob, sin_ob); + return sun_ecliptic; +} + +/** + * 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). + * Topocentric refers to a position as seen by an observer on the surface of the Earth. + * This function corrects for + * parallax + * 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 {string} body + * The name of the body for which to find equatorial coordinates. + * Not allowed to be `"Earth"`. + * + * @param {(Date | number | Astronomy.Time)} date + * Specifies the date and time at which the body is to be observed. + * + * @param {Astronomy.Observer} observer + * The location on the Earth of the observer. + * Call {@link Astronomy.MakeObserver} to create an observer object. + * + * @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 Astronomy.Horizon}. + * Pass `false` to return equatorial coordinates in the J2000 system. + * + * @param {bool} aberration + * Pass `true` to correct for + * aberration, + * or `false` to leave uncorrected. + * + * @returns {Astronomy.EquatorialCoordinates} + * The topocentric coordinates of the body as adjusted for the given observer. + */ +Astronomy.Equator = function(body, date, observer, ofdate, aberration) { + VerifyObserver(observer); + VerifyBoolean(ofdate); + VerifyBoolean(aberration); + const time = Astronomy.MakeTime(date); + const gc_observer = geo_pos(time, observer); + const gc = Astronomy.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); + + const temp = precession(0, j2000, time.tt); + const datevect = nutation(time, 0, temp); + return vector2radec(datevect); +} + +function RotateEquatorialToEcliptic(gx, gy, gz, cos_ob, sin_ob) { + // Rotate equatorial vector to obtain ecliptic vector. + const ex = gx; + const ey = gy*cos_ob + gz*sin_ob; + const ez = -gy*sin_ob + gz*cos_ob; + + const xyproj = Math.sqrt(ex*ex + ey*ey); + let elon = 0; + if (xyproj > 0) { + elon = RAD2DEG * Math.atan2(ey, ex); + if (elon < 0) elon += 360; + } + let elat = RAD2DEG * Math.atan2(ez, xyproj); + return new EclipticCoordinates(ex, ey, ez, elat, elon); +} + +/** + * Given J2000 equatorial Cartesian coordinates, + * returns J2000 ecliptic latitude, longitude, and cartesian coordinates. + * You can call {@link Astronomy.GeoVector} and use its (x, y, z) return values + * to pass into this function. + * + * @param {number} gx + * The x-coordinate of a 3D vector in the J2000 equatorial coordinate system. + * + * @param {number} gy + * The y-coordinate of a 3D vector in the J2000 equatorial coordinate system. + * + * @param {number} gz + * The z-coordinate of a 3D vector in the J2000 equatorial coordinate system. + * + * @returns {Astronomy.EclipticCoordinates} + */ +Astronomy.Ecliptic = function(gx, gy, gz) { + // 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 = DEG2RAD * e_tilt(Astronomy.MakeTime(J2000)).mobl; + cos_ob2000 = Math.cos(ob2000); + sin_ob2000 = Math.sin(ob2000); + } + + VerifyNumber(gx); + VerifyNumber(gy); + VerifyNumber(gz); + + return RotateEquatorialToEcliptic(gx, gy, gz, cos_ob2000, sin_ob2000); +} + +/** + * Calculates the geocentric Cartesian coordinates for the Moon in the J2000 equatorial system. + * Based on the Nautical Almanac Office's Improved Lunar Ephemeris of 1954, + * which in turn derives from E. W. Brown's lunar theories. + * Adapted from Turbo Pascal code from the book + * Astronomy on the Personal Computer + * by Montenbruck and Pfleger. + * + * @param {(Date|number|Astronomy.AstroTime)} date + * The date and time for which to calculate the Moon's geocentric position. + * + * @returns {Astronomy.Vector} + */ +Astronomy.GeoMoon = function(date) { + var time = Astronomy.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(time.tt, mpos1, 0); + + return new Vector(mpos2[0], mpos2[1], mpos2[2], time); +} + +function VsopFormula(formula, t) { + let tpower = 1; + let coord = 0; + for (let series of formula) { + let sum = 0; + for (let term of series) { + sum += term[0] * Math.cos(term[1] + (t * term[2])); + } + coord += tpower * sum; + tpower *= t; + } + return coord; +} + +function CalcVsop(model, time) { + const t = time.tt / 365250; // millennia since 2000 + const spher = []; + for (let formula of model) { + spher.push(VsopFormula(formula, t)); + } + + // Convert spherical coordinates to ecliptic cartesian coordinates. + const r_coslat = spher[2] * Math.cos(spher[1]); + const eclip = [ + r_coslat * Math.cos(spher[0]), + r_coslat * Math.sin(spher[0]), + spher[2] * Math.sin(spher[1]) + ]; + + // Convert ecliptic cartesian coordinates to equatorial cartesian coordinates. + return new Vector( + 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], + time + ); +} + +function VsopHelioDistance(model, time) { + // The caller only wants to know the distance between the planet and the Sun. + // So we only need to calculate the radial component of the spherical coordinates. + return VsopFormula(model[2], time.tt / 365250); +} + +function AdjustBarycenter(ssb, time, body, pmass) { + const shift = pmass / (pmass + SUN_MASS); + 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, 'Jupiter', JUPITER_MASS); + AdjustBarycenter(ssb, time, 'Saturn', SATURN_MASS); + AdjustBarycenter(ssb, time, 'Uranus', URANUS_MASS); + AdjustBarycenter(ssb, time, 'Neptune', NEPTUNE_MASS); + return ssb; +} + +/** + * 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 {string} body + * One of the strings + * `"Sun"`, `"Moon"`, `"Mercury"`, `"Venus"`, + * `"Earth"`, `"Mars"`, `"Jupiter"`, `"Saturn"`, + * `"Uranus"`, `"Neptune"`, `"Pluto"`, + * `"SSB"`, or `"EMB"`. + * + * @param {(Date | number | Astronomy.AstroTime)} date + * The date and time for which the body's position is to be calculated. + * + * @returns {Astronomy.Vector} + */ +Astronomy.HelioVector = function(body, date) { + var time = Astronomy.MakeTime(date); + if (body in vsop) { + return CalcVsop(vsop[body], time); + } + if (body in top2013) { + return TopPosition(body, time); + } + if (body === 'Sun') { + return new Vector(0, 0, 0, time); + } + if (body === 'Moon') { + var e = CalcVsop(vsop.Earth, time); + var m = Astronomy.GeoMoon(time); + return new Vector(e.x+m.x, e.y+m.y, e.z+m.z, time); + } + if (body === 'EMB') { + const e = CalcVsop(vsop.Earth, time); + const m = Astronomy.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 === 'SSB') { + return CalcSolarSystemBarycenter(time); + } + throw `Astronomy.HelioVector: Unknown body "${body}"`; +}; + +/** + * 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 Astronomy.HelioVector} followed by taking the length + * of the resulting vector. + * + * @param {string} body + * A body for which to calculate a heliocentric distance: + * the Sun, Moon, or any of the planets. + * + * @param {(Date | number | Astronomy.AstroTime)} date + * The date and time for which to calculate the heliocentric distance. + * + * @returns {number} + * The heliocentric distance in AU. + */ +Astronomy.HelioDistance = function(body, date) { + const time = Astronomy.MakeTime(date); + if (body in vsop) { + return VsopHelioDistance(vsop[body], time); + } + return Astronomy.HelioVector(body, time).Length(); +} + +/** + * 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 {string} body + * One of the strings + * `"Sun"`, `"Moon"`, `"Mercury"`, `"Venus"`, + * `"Earth"`, `"Mars"`, `"Jupiter"`, `"Saturn"`, + * `"Uranus"`, `"Neptune"`, or `"Pluto"`. + * + * @param {(Date | number | Astronomy.AstroTime)} date + * The date and time for which the body's position is to be calculated. + * + * @param {bool} aberration + * Pass `true` to correct for + * aberration, + * or `false` to leave uncorrected. + * + * @returns {Astronomy.Vector} + */ +Astronomy.GeoVector = function(body, date, aberration) { + VerifyBoolean(aberration); + const time = Astronomy.MakeTime(date); + if (body === 'Moon') { + return Astronomy.GeoMoon(time); + } + if (body === 'Earth') { + return new Vector(0, 0, 0, time); + } + + let earth; + if (!aberration) { + // No aberration, so calculate Earth's position once, at the time of observation. + earth = CalcVsop(vsop.Earth, time); + } + + // Correct for light-travel time, to get position of body as seen from Earth's center. + let h, geo, dt; + let ltime = time; + for (let iter=0; iter < 10; ++iter) { + h = Astronomy.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); + } + + geo = new Vector(h.x-earth.x, h.y-earth.y, h.z-earth.z, time); + let ltime2 = time.AddDays(-geo.Length() / 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}`; +} + +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 }; +} + +/** + * A continuous function of time used in a call to the `Search` function. + * + * @callback ContinuousFunction + * @memberof Astronomy + * @param {Astronomy.AstroTime} t The time at which to evaluate the function. + * @returns {number} + */ + +/** + * Options for the {@link Astronomy.Search} function. + * @typedef {Object} SearchOptions + * @memberof Astronomy + * + * @property {(number|null)} 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|null)} init_f1 + * As an optimization, if the caller of {@link Astronomy.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|null)} init_f2 + * The same as `init_f1`, except this is the optional initial value of `func(t2)` + * instead of `func(t1)`. + */ + +/** + * Search for next time t (such that t 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 {Astronomy.ContinuousFunction} func + * The function to find an ascending zero crossing for. + * The function must accept a single parameter of type {@link Astronomy.AstroTime} + * and return a numeric value. + * + * @param {Astronomy.AstroTime} t1 + * The lower time bound of a search window. + * + * @param {Astronomy.AstroTime} t2 + * The upper time bound of a search window. + * + * @param {(null | Astronomy.SearchOptions)} options + * Options that can tune the behavior of the search. + * Most callers can omit this argument or pass in `null`. + * + * @returns {(null | Astronomy.AstroTime)} + * If the search is successful, returns the date and time of the solution. + * If the search fails, returns null. + */ +Astronomy.Search = function(func, t1, t2, options) { + const dt_tolerance_seconds = (options && options.dt_tolerance_seconds) || 1; + + function f(t) { + return func(t); + } + + 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; + + 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 = Astronomy.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; + } +} + +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; +} + +/** + * 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 Astronomy.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 {(Date | number | Astronomy.AstroTime)} 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 {Astronomy.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. + */ +Astronomy.SearchSunLongitude = function(targetLon, dateStart, limitDays) { + function sun_offset(t) { + let pos = Astronomy.SunPosition(t); + return LongitudeOffset(pos.elon - targetLon); + } + VerifyNumber(targetLon); + VerifyNumber(limitDays); + let t1 = Astronomy.MakeTime(dateStart); + let t2 = t1.AddDays(limitDays); + return Astronomy.Search(sun_offset, t1, t2); +} + +/** + * Calculates the ecliptic longitude difference + * between the given body and the Sun as seen from + * the Earth at a given moment in time. + * The returned value ranges [0, 360) degrees. + * By definition, the Earth and the Sun are both in the plane of the ecliptic. + * Ignores the height of the `body` above or below the ecliptic plane; + * the resulting angle is measured around the ecliptic plane for the "shadow" + * of the body onto that plane. + * + * @param {string} body + * The name of a supported celestial body other than the Earth. + * + * @param {(Date|number|Astronomy.AstroTime)} date + * The time at which the relative longitude is to be found. + * + * @returns {number} + * An angle in degrees in the range [0, 360). + * Values less than 180 indicate that the body is to the east + * of the Sun as seen from the Earth; that is, the body sets after + * the Sun does and is visible in the evening sky. + * Values greater than 180 indicate that the body is to the west of + * the Sun and is visible in the morning sky. + */ +Astronomy.LongitudeFromSun = function(body, date) { + if (body === 'Earth') + throw 'The Earth does not have a longitude as seen from itself.'; + + const t = Astronomy.MakeTime(date); + let gb = Astronomy.GeoVector(body, t, false); + const eb = Astronomy.Ecliptic(gb.x, gb.y, gb.z); + + let gs = Astronomy.GeoVector('Sun', t, false); + const es = Astronomy.Ecliptic(gs.x, gs.y, gs.z); + + return NormalizeLongitude(eb.elon - es.elon); +} + +/** + * Returns the full angle seen from + * the Earth, between the given body and the Sun. + * Unlike {@link Astronomy.LongitudeFromSun}, 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 {string} body + * The name of a supported celestial body other than the Earth. + * + * @param {(Date|number|Astronomy.AstroTime)} 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]. + */ +Astronomy.AngleFromSun = function(body, date) { + if (body == 'Earth') + throw 'The Earth does not have an angle as seen from itself.'; + + let sv = Astronomy.GeoVector('Sun', date, true); + let bv = Astronomy.GeoVector(body, date, true); + let angle = AngleBetween(sv, bv); + return angle; +} + +/** + * Calculates heliocentric ecliptic longitude based on the J2000 equinox. + * + * @param {string} body + * The name of a celestial body other than the Sun. + * + * @param {(Date | number | Astronomy.AstroTime)} 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 + * equinox and + * increases in the same direction the Earth orbits the Sun. + * The returned value is always in the range [0, 360). + */ +Astronomy.EclipticLongitude = function(body, date) { + if (body === 'Sun') + throw 'Cannot calculate heliocentric longitude of the Sun.'; + + let hv = Astronomy.HelioVector(body, date); + let eclip = Astronomy.Ecliptic(hv.x, hv.y, hv.z); + return eclip.elon; +} + +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 'Mercury': c0 = -0.60; c1 = +4.98; c2 = -4.88; c3 = +3.02; break; + case '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 'Mars': c0 = -1.52; c1 = +1.60; break; + case 'Jupiter': c0 = -9.40; c1 = +0.50; break; + case 'Uranus': c0 = -7.19; c1 = +0.25; break; + case 'Neptune': c0 = -6.87; break; + case '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 = Astronomy.Ecliptic(gc.x, gc.y, gc.z); + const ir = DEG2RAD * 28.06; // tilt of Saturn's rings to the ecliptic, in radians + const Nr = 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 = DEG2RAD * eclip.elat; + const lon = 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: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 * 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 / KM_PER_AU; + let geo_au = geo_dist / moon_mean_distance_au; + mag += 5*Math.log10(helio_dist * geo_au); + return mag; +} + +/** + * Contains information about the apparent brightness and sunlit phase of a celestial object. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.AstroTime} time + * The date and time pertaining to the other calculated values in this object. + * + * @property {number} mag + * The apparent visual magnitude 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 + * astronomical units (AU). + * + * @property {number} geo_dist + * The distance between the center of the Earth and the center of the body in AU. + * + * @property {Astronomy.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 {Astronomy.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 | null} 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 JPL Horizons 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 `null`. + */ +class IlluminationInfo { + constructor(time, mag, phase, helio_dist, geo_dist, gc, hc, ring_tilt) { + this.time = time; + this.mag = mag; + this.phase_angle = phase; + this.phase_fraction = (1 + Math.cos(DEG2RAD * phase)) / 2; + this.helio_dist = helio_dist; + this.geo_dist = geo_dist; + this.gc = gc; + this.hc = hc; + this.ring_tilt = ring_tilt; + } +} + +/** + * Calculates the phase angle, visual maginitude, + * and other values relating to the body's illumination + * at the given date and time, as seen from the Earth. + * + * @param {string} body + * The name of the celestial body being observed. + * Not allowed to be `"Earth"`. + * + * @param {Date | number | Astronomy.AstroTime} date + * The date and time for which to calculate the illumination data for the given body. + * + * @returns {Astronomy.IlluminationInfo} + */ +Astronomy.Illumination = function(body, date) { + if (body === 'Earth') + throw `The illumination of the Earth is not defined.`; + + const time = Astronomy.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 === '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 === 'Moon') { + // For extra numeric precision, use geocentric moon formula directly. + gc = Astronomy.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 = Astronomy.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 = null; // only reported for Saturn + + if (body === 'Sun') { + mag = SUN_MAG_1AU + 5*Math.log10(geo_dist); + } else if (body === 'Moon') { + mag = MoonMagnitude(phase, helio_dist, geo_dist); + } else if (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); +} + +function SynodicPeriod(body) { + if (body === 'Earth') + throw 'The Earth does not have a synodic period as seen from itself.'; + + if (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; +} + +/** + * 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 {string} 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 {(Date | number | Astronomy.AstroTime)} startDate + * The date and time after which to find the next occurrence of the + * body and the Earth reaching the desired relative longitude. + * + * @returns {Astronomy.AstroTime} + * The time when the Earth and the body next reach the specified relative longitudes. + */ +Astronomy.SearchRelativeLongitude = function(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 === '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 = Astronomy.EclipticLongitude(body, t); + const elon = Astronomy.EclipticLongitude('Earth', t); + const diff = direction * (elon - plon); + return LongitudeOffset(diff - targetRelLon); + } + + let syn = SynodicPeriod(body); + let time = Astronomy.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}).`; +} + +/** + * Determines the moon's phase expressed as an ecliptic longitude. + * + * @param {Date | number | Astronomy.AstroTime} 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 + */ +Astronomy.MoonPhase = function(date) { + return Astronomy.LongitudeFromSun('Moon', date); +} + +/** + * 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 Astronomy.SearchMoonQuarter} once, followed by repeatedly calling + * {@link Astronomy.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 {(Date|number|Astronomy.AstroTime)} 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 {(Astronomy.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`. + */ +Astronomy.SearchMoonPhase = function(targetLon, dateStart, limitDays) { + function moon_offset(t) { + let mlon = Astronomy.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 up to 0.826 days away from the simple prediction. + // To be safe, we take the predicted time of the event and search + // +/-0.9 days around it (a 1.8-day wide window). + // But we must return null if the final result goes beyond limitDays after dateStart. + const uncertainty = 0.9; + + let ta = Astronomy.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 Astronomy.Search(moon_offset, t1, t2); +} + +/** + * Represents a quarter lunar phase, along with when it occurs. + * + * @class + * @memberof Astronomy + * + * @property {number} quarter + * An integer as follows: + * 0 = new moon, + * 1 = first quarter, + * 2 = full moon, + * 3 = third quarter. + * + * @property {Astronomy.AstroTime} time + * The date and time of the quarter lunar phase. + */ +class MoonQuarter { + constructor(quarter, time) { + this.quarter = quarter; + this.time = time; + } +} + +/** + * 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 Astronomy.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 {(Date|number|Astronomy.AstroTime)} dateStart + * The date and time after which to find the first quarter lunar phase. + * + * @returns {Astronomy.MoonQuarter} + */ +Astronomy.SearchMoonQuarter = function(dateStart) { + // Determine what the next quarter phase will be. + let phaseStart = Astronomy.MoonPhase(dateStart); + let quarterStart = Math.floor(phaseStart / 90); + let quarter = (quarterStart + 1) % 4; + let time = Astronomy.SearchMoonPhase(90 * quarter, dateStart, 10); + return time && new MoonQuarter(quarter, time); +} + +/** + * Given a {@link Astronomy.MoonQuarter} object, finds the next consecutive + * quarter lunar phase. See remarks in {@link Astronomy.SearchMoonQuarter} + * for explanation of usage. + * + * @param {Astronomy.MoonQuarter} mq + * The return value of a prior call to {@link Astronomy.MoonQuarter} or `NextMoonQuarter`. + */ +Astronomy.NextMoonQuarter = function(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 Astronomy.SearchMoonQuarter(date); +} + +/** + * 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 {string} body + * The name of the body to find the rise or set time for. + * + * @param {Astronomy.Observer} observer + * Specifies the geographic coordinates and elevation above sea level of the observer. + * Call {@link Astronomy.MakeObserver} to create an observer object. + * + * @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 {(Date|number|Astronomy.AstroTime)} 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 {(Astronomy.AstroTime|null)} + * The date and time of the rise or set event, or null if no such event + * occurs within the specified time window. + */ +Astronomy.SearchRiseSet = function(body, observer, direction, dateStart, limitDays) { + VerifyObserver(observer); + VerifyNumber(limitDays); + + // We calculate the apparent angular radius of the Sun and Moon, + // but treat all other bodies as points. + let body_radius_au = { Sun:SUN_RADIUS_AU, Moon:MOON_EQUATORIAL_RADIUS_AU }[body] || 0; + + 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 = Astronomy.Equator(body, t, observer, true, true); + const hor = Astronomy.Horizon(t, observer, ofdate.ra, ofdate.dec); + const alt = hor.altitude + RAD2DEG*(body_radius_au / ofdate.dist) + REFRACTION_NEAR_HORIZON; + return direction * alt; + } + + if (body === 'Earth') + throw 'Cannot find rise or set time of 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 `Astronomy.SearchRiseSet: Invalid direction parameter ${direction} -- must be +1 or -1`; + } + + let time_start = Astronomy.MakeTime(dateStart); + let time_before; + let evt_before, evt_after; + let alt_before = peak_altitude(time_start); + let alt_after; + if (alt_before > 0) { + // We are past the sought event, so we have to wait for the next "before" event (culm/bottom). + evt_before = Astronomy.SearchHourAngle(body, observer, ha_before, time_start); + time_before = evt_before.time; + alt_before = peak_altitude(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 = Astronomy.SearchHourAngle(body, observer, ha_after, time_before); + alt_after = peak_altitude(evt_after.time); + + while (true) { + if (alt_before <= 0 && alt_after > 0) { + // Search between evt_before and evt_after for the desired event. + let tx = Astronomy.Search(peak_altitude, time_before, evt_after.time, {init_f1:alt_before, init_f2:alt_after}); + if (tx) + return tx; + } + + // If we didn't find the desired event, use time_after to find the next before-event. + evt_before = Astronomy.SearchHourAngle(body, observer, ha_before, evt_after.time); + evt_after = Astronomy.SearchHourAngle(body, observer, ha_after, evt_before.time); + if (evt_before.time.ut >= time_start.ut + limitDays) + return null; + + time_before = evt_before.time; + alt_before = peak_altitude(evt_before.time); + alt_after = peak_altitude(evt_after.time); + } +} + +/** + * 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. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.AstroTime} time + * The date and time of the celestial body reaching the hour angle. + * + * @property {Astronomy.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; + } +} + +/** + * Finds the next time the given body is seen to reach the specified + * hour angle + * 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 {string} body + * The name of a celestial body other than the Earth. + * + * @param {Astronomy.Observer} observer + * Specifies the geographic coordinates and elevation above sea level of the observer. + * Call {@link Astronomy.MakeObserver} to create an observer object. + * + * @param {number} hourAngle + * The hour angle expressed in + * sidereal + * 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 + * meridian. + * This specifying `hourAngle` = 0 finds the moment in time + * the body reaches the highest angular altitude in a given sidereal day. + * + * @param {(Date|number|Astronomy.AstroTime)} dateStart + * The date and time after which the desired hour angle crossing event + * is to be found. + * + * @returns {Astronomy.HourAngleEvent} + */ +Astronomy.SearchHourAngle = function(body, observer, hourAngle, dateStart) { + VerifyObserver(observer); + let time = Astronomy.MakeTime(dateStart); + let iter = 0; + + if (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 = Astronomy.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 = Astronomy.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); + } +} + +/** + * 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. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.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 {Astronomy.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 {Astronomy.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 {Astronomy.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; + } +} + +/** + * Finds the equinoxes and solstices for a given calendar year. + * + * @param {(number | Astronomy.AstroTime)} year + * The integer value or `AstroTime` object that specifies + * the UTC calendar year for which to find equinoxes and solstices. + * + * @returns {Astronomy.SeasonInfo} + */ +Astronomy.Seasons = function(year) { + function find(targetLon, month, day) { + let startDate = new Date(Date.UTC(year, month-1, day)); + let time = Astronomy.SearchSunLongitude(targetLon, startDate, 4); + if (!time) + throw `Cannot find season change near ${startDate.toISOString()}`; + return time; + } + + if (IsValidDate(year)) { + 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); +} + +/** + * 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. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.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 Astronomy.Elongation} + */ +class ElongationEvent { + constructor(time, visibility, elongation, ecliptic_separation) { + this.time = time; + this.visibility = visibility; + this.elongation = elongation; + this.ecliptic_separation = ecliptic_separation; + } +} + +/** + * 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 Astronomy.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 {string} body + * The name of the observed body. Not allowed to be `"Earth"`. + * + * @returns {Astronomy.ElongationEvent} + */ +Astronomy.Elongation = function(body, date) { + let time = Astronomy.MakeTime(date); + + let lon = Astronomy.LongitudeFromSun(body, time); + let vis; + if (lon > 180) { + vis = 'morning'; + lon = 360 - lon; + } else { + vis = 'evening'; + } + let angle = Astronomy.AngleFromSun(body, time); + return new ElongationEvent(time, vis, angle, lon); +} + +/** + * 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 {string} body Either `"Mercury"` or `"Venus"`. + * @param {Date} startDate The date and time after which to search for the next maximum elongation event. + * + * @returns {Astronomy.ElongationEvent} + */ +Astronomy.SearchMaxElongation = function(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 = Astronomy.AngleFromSun(body, t1); + let e2 = Astronomy.AngleFromSun(body, t2); + let m = (e1-e2)/dt; + return m; + } + + let startTime = Astronomy.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 = Astronomy.EclipticLongitude(body, startTime); + let elon = Astronomy.EclipticLongitude('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 t1, t2; + 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); + t1 = Astronomy.SearchRelativeLongitude(body, rlon_lo, t_start); + t2 = Astronomy.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 = Astronomy.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 Astronomy.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.`; +} + +/** + * Searches for the date and time Venus will next appear brightest as seen from the Earth. + * + * @param {string} 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 Astronomy.SearchMoonQuarter} or {@link Astronomy.SearchMoonPhase}. + * The other planets reach peak magnitude very close to opposition, + * which can be found using {@link Astronomy.SearchRelativeLongitude}. + * + * @param {(Date | number | Astronomy.AstroTime)} startDate + * The date and time after which to find the next peak magnitude event. + * + * @returns {Astronomy.IlluminationInfo} + */ +Astronomy.SearchPeakMagnitude = function(body, startDate) { + if (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 = Astronomy.Illumination(body, t1).mag; + const y2 = Astronomy.Illumination(body, t2).mag; + const m = (y2-y1) / dt; + return m; + } + + let startTime = Astronomy.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 = Astronomy.EclipticLongitude(body, startTime); + let elon = Astronomy.EclipticLongitude('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 t1, t2; + 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); + t1 = Astronomy.SearchRelativeLongitude(body, rlon_lo, t_start); + t2 = Astronomy.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 = Astronomy.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 Astronomy.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.`; +} + +/** + * Represents a closest or farthest point in a body's orbit around its primary. + * For a planet orbiting the Sun, this is a perihelion or aphelion, respectively. + * For the Moon orbiting the Earth, this is a perigee or apogee, respectively. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.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 Astronomy.SearchLunarApsis} + * @see {@link Astronomy.NextLunarApsis} + */ +class Apsis { + constructor(time, kind, dist_au) { + this.time = time; + this.kind = kind; + this.dist_au = dist_au; + this.dist_km = dist_au * KM_PER_AU; + } +} + +/** + * Finds the next perigee (closest approach) or apogee (farthest remove) of the Moon + * that occurs after the specified date and time. + * + * @param {(Date | number | Astronomy.AstroTime)} startDate + * The date and time after which to find the next perigee or apogee. + * + * @returns {Astronomy.Apsis} + */ +Astronomy.SearchLunarApsis = function(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 = Astronomy.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 = Astronomy.Search(distance_slope, t1, t2, {init_f1:m1, init_f2:m2}); + if (tx == null) + 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 = Astronomy.Search(negative_distance_slope, t1, t2, {init_f1:-m1, init_f2:-m2}); + if (tx == null) + 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.'; +} + +/** + * Given a lunar apsis returned by an initial call to {@link Astronomy.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 {Astronomy.Apsis} apsis + * A lunar perigee or apogee event. + * + * @returns {Astronomy.Apsis} + * The successor apogee for the given perigee, or the successor perigee for the given apogee. + */ +Astronomy.NextLunarApsis = function(apsis) { + const skip = 11; // number of days to skip to start looking for next apsis event + let next = Astronomy.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; +} + +function PlanetExtreme(body, kind, start_time, dayspan) { + const direction = (kind === 1) ? +1.0 : -1.0; + const npoints = 10; + + for(;;) { + const interval = dayspan / (npoints - 1); + + if (interval < 1.0 / 1440.0) /* iterate until uncertainty is less than one minute */ + { + const apsis_time = start_time.AddDays(interval / 2.0); + const dist_au = Astronomy.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 * Astronomy.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 = Astronomy.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.'; +} + +/** + * 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 Astronomy.NextPlanetApsis}. After that, keep feeding the previous return value + * from `NextPlanetApsis` into another call of `NextPlanetApsis` + * as many times as desired. + * + * @param {string} body + * The planet for which to find the next perihelion/aphelion event. + * Not allowed to be `"Sun"` or `"Moon"`. + * + * @param {Astronomy.AstroTime} startTime + * The date and time at which to start searching for the next perihelion or aphelion. + * + * @returns {Astronomy.Apsis} + * The next perihelion or aphelion that occurs after `startTime`. + */ +Astronomy.SearchPlanetApsis = function(body, startTime) { + if (body === 'Neptune' || 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 = Astronomy.HelioDistance(body, t1); + let r2 = Astronomy.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, 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 = Astronomy.Search(slope_func, t1, t2, 1.0); + if (search == null) + throw "Failed to find slope transition in planetary apsis search."; + + const dist = Astronomy.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."; +} + +/** + * Finds the next planetary perihelion or aphelion event in a series. + * + * This function requires an {@link Astronomy.Apsis} value obtained from a call + * to {@link Astronomy.SearchPlanetApsis} or `NextPlanetApsis`. + * Given an aphelion event, this function finds the next perihelion event, and vice versa. + * See {@link Astronomy.SearchPlanetApsis} for more details. + * + * @param {string} 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 {Astronomy.Apsis} apsis + * An apsis event obtained from a call to {@link Astronomy.SearchPlanetApsis} or `NextPlanetApsis`. + * + * @returns {Astronomy.Apsis} + * Same as the return value for {@link Astronomy.SearchPlanetApsis}. + */ +Astronomy.NextPlanetApsis = function(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 = Astronomy.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; +} + +/** + * 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 {Astronomy.RotationMatrix} rotation + * The rotation matrix to be inverted. + * + * @returns {Astronomy.RotationMatrix} + * The inverse rotation matrix. + */ +Astronomy.InverseRotation = function(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]] + ]); +} + +/** + * 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 {Astronomy.RotationMatrix} a + * The first rotation to apply. + * + * @param {Astronomy.RotationMatrix} b + * The second rotation to apply. + * + * @returns {Astronomy.RotationMatrix} + * The combined rotation matrix. + */ +Astronomy.CombineRotation = function(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] + ] + ]); +} + +/** + * 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 {Astronomy.Spherical} sphere + * Spherical coordinates to be converted. + * + * @param {Astronomy.AstroTime} time + * The time that should be included in the returned vector. + * + * @returns {Astronomy.Vector} + * The vector form of the supplied spherical coordinates. + */ +Astronomy.VectorFromSphere = function(sphere, time) { + const radlat = sphere.lat * DEG2RAD; + const radlon = sphere.lon * 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 + ); +} + +/** + * Given angular equatorial coordinates in `equ`, calculates equatorial vector. + * + * @param {Astronomy.EquatorialCoordinates} equ + * An object that contains angular equatorial coordinates to be converted to a vector. + * + * @param {Astronomy.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. + * + * @returns {Astronomy.Vector} + * A vector in the equatorial system. + */ +Astronomy.VectorFromEquator = function(equ, time) { + return Astronomy.VectorFromSphere(new Spherical(equ.dec, 15 * equ.ra, equ.dist), time); +} + +/** + * Given an equatorial vector, calculates equatorial angular coordinates. + * + * @param {Astronomy.Vector} vec + * A vector in an equatorial coordinate system. + * + * @returns {Astronomy.EquatorialCoordinates} + * Angular coordinates expressed in the same equatorial system as `vec`. + */ +Astronomy.EquatorFromVector = function(vec) { + const sphere = Astronomy.SphereFromVector(vec); + return new EquatorialCoordinates(sphere.lon / 15, sphere.lat, sphere.dist); +} + +/** + * Converts Cartesian coordinates to spherical coordinates. + * + * Given a Cartesian vector, returns latitude, longitude, and distance. + * + * @param {Astronomy.Vector} vector + * Cartesian vector to be converted to spherical coordinates. + * + * @returns {Astronomy.Spherical} + * Spherical coordinates that are equivalent to the given vector. + */ +Astronomy.SphereFromVector = function(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 = RAD2DEG * Math.atan2(vector.y, vector.x); + if (lon < 0.0) { + lon += 360.0; + } + lat = RAD2DEG * Math.atan2(vector.z, Math.sqrt(xyproj)); + } + return new Spherical(lat, lon, dist); +} + +function ToggleAzimuthDirection(az) { + az = 360.0 - az; + if (az >= 360.0) + az -= 360.0; + else if (az < 0.0) + az += 360.0; + return az; +} + +/** + * Converts Cartesian coordinates to horizontal coordinates. + * + * Given a horizontal Cartesian vector, returns horizontal azimuth and altitude. + * + * *IMPORTANT:* This function differs from {@link Astronomy.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 {Astronomy.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 {Astronomy.Spherical} + */ +Astronomy.HorizonFromVector = function(vector, refraction) { + const sphere = Astronomy.SphereFromVector(vector); + sphere.lon = ToggleAzimuthDirection(sphere.lon); + sphere.lat += Astronomy.Refraction(refraction, sphere.lat); + return sphere; +} + + +/** + * Given apparent angular horizontal coordinates in `sphere`, calculate horizontal vector. + * + * @param {Astronomy.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 {Astronomy.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 {Astronomy.Vector} + * A vector in the horizontal system: `x` = north, `y` = west, and `z` = zenith (up). + */ +Astronomy.VectorFromHorizon = function(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 + Astronomy.InverseRefraction(refraction, sphere.lat); + + const xsphere = new Spherical(lat, lon, sphere.dist); + return Astronomy.VectorFromSphere(xsphere, time); +} + + +/** + * 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. + */ +Astronomy.Refraction = function(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))*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 { + /* No refraction, or the refraction option is invalid. */ + refr = 0.0; + } + + return refr; +} + +/** + * 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. + */ +Astronomy.InverseRefraction = function(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 - Astronomy.Refraction(refraction, bent_altitude); + + for(;;) { + /* See how close we got. */ + let diff = (altitude + Astronomy.Refraction(refraction, altitude)) - bent_altitude; + if (Math.abs(diff) < 1.0e-14) + return altitude - bent_altitude; + + altitude -= diff; + } +} + +/** + * 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 {Astronomy.RotationMatrix} rotation + * A rotation matrix that specifies how the orientation of the vector is to be changed. + * + * @param {Astronomy.Vector} vector + * The vector whose orientation is to be changed. + * + * @returns {Astronomy.Vector} + * A vector in the orientation specified by `rotation`. + */ +Astronomy.RotateVector = function(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 + ); +} + + +/** + * 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 {Astronomy.RotationMatrix} + * A rotation matrix that converts EQJ to ECL. + */ +Astronomy.Rotation_EQJ_ECL = function() { + /* 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] + ]); +} + + +/** + * 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 {Astronomy.RotationMatrix} + * A rotation matrix that converts ECL to EQJ. + */ +Astronomy.Rotation_ECL_EQJ = function() { + /* 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] + ]); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time at which the Earth's equator defines the target orientation. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts EQJ to EQD at `time`. + */ +Astronomy.Rotation_EQJ_EQD = function(time) { + const prec = precession_rot(0.0, time.tt); + const nut = nutation_rot(time, 0); + return Astronomy.CombineRotation(prec, nut); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time at which the Earth's equator defines the source orientation. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts EQD at `time` to EQJ. + */ +Astronomy.Rotation_EQD_EQJ = function(time) { + const nut = nutation_rot(time, 1); + const prec = precession_rot(time.tt, 0.0); + return Astronomy.CombineRotation(nut, prec); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time at which the Earth's equator applies. + * + * @param {Astronomy.Observer} observer + * A location near the Earth's mean sea level that defines the observer's horizon. + * + * @returns {Astronomy.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. + */ +Astronomy.Rotation_EQD_HOR = function(time, observer) { + const sinlat = Math.sin(observer.latitude * DEG2RAD); + const coslat = Math.cos(observer.latitude * DEG2RAD); + const sinlon = Math.sin(observer.longitude * DEG2RAD); + const coslon = Math.cos(observer.longitude * 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]], + ]); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time at which the Earth's equator applies. + * + * @param {Astronomy.Observer} observer + * A location near the Earth's mean sea level that defines the observer's horizon. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts HOR to EQD at `time` and for `observer`. + */ +Astronomy.Rotation_HOR_EQD = function(time, observer) { + const rot = Astronomy.Rotation_EQD_HOR(time, observer); + return Astronomy.InverseRotation(rot); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time of the observation. + * + * @param {Astronomy.Observer} observer + * A location near the Earth's mean sea level that defines the observer's horizon. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts HOR to EQD at `time` and for `observer`. + */ +Astronomy.Rotation_HOR_EQJ = function(time, observer) { + const hor_eqd = Astronomy.Rotation_HOR_EQD(time, observer); + const eqd_eqj = Astronomy.Rotation_EQD_EQJ(time); + return Astronomy.CombineRotation(hor_eqd, eqd_eqj); +} + + +/** + * 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 Astronomy.HorizonFromVector} to convert the return value + * to a traditional altitude/azimuth pair. + * + * @param time + * The date and time of the desired horizontal orientation. + * + * @param 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. + */ +Astronomy.Rotation_EQJ_HOR = function(time, observer) { + const rot = Astronomy.Rotation_HOR_EQJ(time, observer); + return Astronomy.InverseRotation(rot); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time of the source equator. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts EQD to ECL. + */ +Astronomy.Rotation_EQD_ECL = function(time) { + const eqd_eqj = Astronomy.Rotation_EQD_EQJ(time); + const eqj_ecl = Astronomy.Rotation_EQJ_ECL(); + return Astronomy.CombineRotation(eqd_eqj, eqj_ecl); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time of the desired equator. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts ECL to EQD. + */ +Astronomy.Rotation_ECL_EQD = function(time) { + const rot = Astronomy.Rotation_EQD_ECL(time); + return Astronomy.InverseRotation(rot); +} + + +/** + * 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 Astronomy.HorizonFromVector} to convert the return value + * to a traditional altitude/azimuth pair. + * + * @param {Astronomy.AstroTime} time + * The date and time of the desired horizontal orientation. + * + * @param {Astronomy.Observer} observer + * A location near the Earth's mean sea level that defines the observer's horizon. + * + * @returns {Astronomy.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. + */ +Astronomy.Rotation_ECL_HOR = function(time, observer) { + const ecl_eqd = Astronomy.Rotation_ECL_EQD(time); + const eqd_hor = Astronomy.Rotation_EQD_HOR(time, observer); + return Astronomy.CombineRotation(ecl_eqd, eqd_hor); +} + + +/** + * 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 {Astronomy.AstroTime} time + * The date and time of the horizontal observation. + * + * @param {Astronomy.Observer} observer + * The location of the horizontal observer. + * + * @returns {Astronomy.RotationMatrix} + * A rotation matrix that converts HOR to ECL. + */ +Astronomy.Rotation_HOR_ECL = function(time, observer) { + const rot = Astronomy.Rotation_ECL_HOR(time, observer); + return Astronomy.InverseRotation(rot); +} + + +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; + +/** + * Reports the constellation that a given celestial point lies within. + * + * @class + * @memberof Astronomy + * + * @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; + } +} + + +/** + * 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 {Astronomy.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. + */ +Astronomy.Constellation = function(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 = Astronomy.Rotation_EQJ_EQD(new AstroTime(-45655.74141261017)); + Epoch2000 = new AstroTime(0); + } + + // Convert coordinates from J2000 to B1875. + const equ2000 = new EquatorialCoordinates(ra, dec, 1.0); + const vec2000 = Astronomy.VectorFromEquator(equ2000, Epoch2000); + const vec1875 = Astronomy.RotateVector(ConstelRot, vec2000); + const equ1875 = Astronomy.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.'; +} + +/** + * Returns information about a lunar eclipse. + * + * Returned by {@link Astronomy.SearchLunarEclipse} or {@link Astronomy.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. + * + * @class + * @memberof Astronomy + * + * @property {string} kind + * The type of lunar eclipse found. + * + * @property {Astronomy.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; + } +} + +class ShadowInfo { + constructor(time, u, r, k, p, target, dir) { + this.time = time; + this.u = u; // dot product of (heliocentric earth) and (geocentric moon): defines the shadow plane where the Moon is + this.r = r; // km distance between center of Moon and the line passing through the centers of the Sun and Earth. + this.k = k; // umbra radius in km, at the shadow plane + this.p = p; // penumbra radius in km, at the shadow plane + 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 = 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 = Astronomy.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 = Astronomy.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 = Astronomy.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 = Astronomy.GeoVector(body, time, false); + + // Calculate light-travel-corrected vector from Earth to Sun. + const e = Astronomy.GeoVector('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 = Astronomy.Search(time => ShadowDistanceSlope(EarthShadow, time), t1, t2, 1.0); + 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 = Astronomy.Search(time => ShadowDistanceSlope(MoonShadow, time), t1, t2, 1.0); + 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 = Astronomy.Search(time => PlanetShadowSlope(body, planet_radius_km, time), t1, t2, 1.0); + 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 = Astronomy.Search(time => ShadowDistanceSlope(shadowfunc, time), t1, t2, 1.0); + 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 = Astronomy.Search(time => -(EarthShadow(time).r - radius_limit), before, center_time, 1.0); + const t2 = Astronomy.Search(time => +(EarthShadow(time).r - radius_limit), center_time, after, 1.0); + if (t1 === null || t2 === null) + 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 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 Astronomy.LunarEclipseInfo} for more information. + * To find a series of lunar eclipses, call this function once, + * then keep calling {@link Astronomy.NextLunarEclipse} as many times as desired, + * passing in the `center` value returned from the previous call. + * + * @param {(Date|number|Astronomy.AstroTime)} date + * The date and time for starting the search for a lunar eclipse. + * + * @returns {Astronomy.LunarEclipseInfo} + */ +Astronomy.SearchLunarEclipse = function(date) { + const PruneLatitude = 1.8; /* full Moon's ecliptic latitude above which eclipse is impossible */ + let fmtime = Astronomy.MakeTime(date); + for (let fmcount = 0; fmcount < 12; ++fmcount) { + /* Search for the next full moon. Any eclipse will be near it. */ + const fullmoon = Astronomy.SearchMoonPhase(180, fmtime, 40); + if (fullmoon === null) + 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.'; +} + + +/** + Reports the time and geographic location of the peak of a solar eclipse. + + Returned by {@link Astronomy.SearchGlobalSolarEclipse} or {@link Astronomy.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. + + @class + @memberof Astronomy + + @property {string} kind + One of the following string values: `"partial"`, `"annular"`, `"total"`. + + @property {Astronomy.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 {(undefined|number)} 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 {(undefined|number)} 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; + } +} + + +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 = Astronomy.Rotation_EQJ_EQD(shadow.time); + const v = Astronomy.RotateVector(rot, shadow.dir); // shadow-axis vector in equator-of-date coordinates + const e = Astronomy.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 *= KM_PER_AU; + v.y *= KM_PER_AU; + v.z *= KM_PER_AU / EARTH_FLATTENING; + e.x *= KM_PER_AU; + e.y *= KM_PER_AU; + e.z *= 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 = RAD2DEG * Math.atan(pz / proj); + } + + // Adjust longitude for Earth's rotation at the given UT. + const gast = sidereal_time(peak); + longitude = (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 = Astronomy.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 / KM_PER_AU, py / KM_PER_AU, pz / KM_PER_AU, shadow.time); + + // Rotate the observer's geocentric EQD back to the EQJ system. + o = Astronomy.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 Astronomy.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 `center` value from the {@link Astronomy.LunarEclipseInfo} returned by the + * previous call to `Astronomy.SearchLunarEclipse` or `Astronomy.NextLunarEclipse` + * to find the next lunar eclipse. + * + * @param {Astronomy.AstroTime} prevEclipseTime + * A date and time near a full moon. Lunar eclipse search will start at the next full moon. + * + * @returns {Astronomy.LunarEclipseInfo} + */ +Astronomy.NextLunarEclipse = function(prevEclipseTime) { + const startTime = prevEclipseTime.AddDays(10); + return Astronomy.SearchLunarEclipse(startTime); +} + +/** + * @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 Astronomy.GlobalSolarEclipseInfo} for more information. + * To find a series of solar eclipses, call this function once, + * then keep calling {@link Astronomy.NextGlobalSolarEclipse} as many times as desired, + * passing in the `peak` value returned from the previous call. + * + * @param {Astronomy.AstroTime} startTime + * The date and time for starting the search for a solar eclipse. + * + * @returns {Astronomy.GlobalSolarEclipseInfo} + */ +Astronomy.SearchGlobalSolarEclipse = function(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 = Astronomy.SearchMoonPhase(0.0, nmtime, 40.0); + if (newmoon === null) { + 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.'; +} + + +/** + * @brief Searches for the next global solar eclipse in a series. + * + * After using {@link Astronomy.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 Astronomy.GlobalSolarEclipseInfo} returned by the + * previous call to `SearchGlobalSolarEclipse` or `NextGlobalSolarEclipse` + * to find the next solar eclipse. + * + * @param {Astronomy.AstroTime} prevEclipseTime + * A date and time near a new moon. Solar eclipse search will start at the next new moon. + * + * @returns {Astronomy.GlobalSolarEclipseInfo} + */ +Astronomy.NextGlobalSolarEclipse = function(prevEclipseTime) { + const startTime = prevEclipseTime.AddDays(10.0); + return Astronomy.SearchGlobalSolarEclipse(startTime); +} + + +/** + * @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). + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.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; + } +} + + +/** + * @brief Information about a solar eclipse as seen by an observer at a given time and geographic location. + * + * Returned by {@link Astronomy.SearchLocalSolarEclipse} or {@link Astronomy.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 #EclipseEvent for more information. + * + * @class + * @memberof Astronomy + * + * @property {string} kind + * The type of solar eclipse found: `"partial"`, `"annular"`, or `"total"`. + * + * @property {Astronomy.EclipseEvent} partial_begin + * The time and Sun altitude at the beginning of the eclipse. + * + * @property {Astronomy.EclipseEvent} total_begin + * If this is an annular or a total eclipse, the time and Sun altitude when annular/total phase begins; otherwise undefined. + * + * @property {Astronomy.EclipseEvent} peak + * The time and Sun altitude when the eclipse reaches its peak. + * + * @property {Astronomy.EclipseEvent} total_end + * If this is an annular or a total eclipse, the time and Sun altitude when annular/total phase ends; otherwise undefined. + * + * @property {Astronomy.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; + } +} + + +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, total_end, 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 = Astronomy.Search(evaluate, t1, t2, 1.0); + if (search == null) + 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 = Astronomy.Equator('Sun', time, observer, true, true); + const hor = Astronomy.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 Astronomy.LocalSolarEclipseInfo} for more information. + * + * To find a series of solar eclipses, call this function once, + * then keep calling {@link Astronomy.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 Astronomy.LocalSolarEclipseInfo} for more information about this topic. + * + * @param {Astronomy.AstroTime} startTime + * The date and time for starting the search for a solar eclipse. + * + * @param {Astronomy.Observer} observer + * The geographic location of the observer. + * + * @returns {Astronomy.LocalSolarEclipseInfo} + */ +Astronomy.SearchLocalSolarEclipse = function(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 = Astronomy.SearchMoonPhase(0.0, nmtime, 40.0); + + /* 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); + } +} + + +/** + * @brief Searches for the next local solar eclipse in a series. + * + * After using {@link Astronomy.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 Astronomy.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 Astronomy.LocalSolarEclipseInfo} for more information. + * + * @param {Astronomy.AstroTime} prevEclipseTime + * The date and time for starting the search for a solar eclipse. + * + * @param {Astronomy.Observer} observer + * The geographic location of the observer. + * + * @returns {Astronomy.LocalSolarEclipseInfo} + */ +Astronomy.NextLocalSolarEclipse = function(prevEclipseTime, observer) { + const startTime = prevEclipseTime.AddDays(10.0); + return Astronomy.SearchLocalSolarEclipse(startTime, observer); +} + + +/** + * @brief Information about a transit of Mercury or Venus, as seen from the Earth. + * + * Returned by {@link Astronomy.SearchTransit} or {@link Astronomy.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. + * + * @class + * @memberof Astronomy + * + * @property {Astronomy.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 {Astronomy.AstroTime} peak + * When the planet is most aligned with the Sun, as seen from the Earth. + * + * @property {Astronomy.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; + } +} + + +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 = Astronomy.Search(time => PlanetShadowBoundary(time, body, planet_radius_km, direction), t1, t2, 1.0); + if (tx == null) + 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 Astronomy.NextTransit}. + * + * @param {string} body + * The planet whose transit is to be found. Must be `"Mercury"` or `"Venus"`. + * + * @param {Astronomy.AstroTime} startTime + * The date and time for starting the search for a transit. + * + * @returns {Astronomy.TransitInfo} + */ +Astronomy.SearchTransit = function(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 'Mercury': + planet_radius_km = 2439.7; + break; + + case '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 = Astronomy.SearchRelativeLongitude(body, 0.0, search_time); + + // Calculate the angular separation between the body and the Sun at this time. + const conj_separation = Astronomy.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 * Astronomy.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); + } +} + + +/** + * @brief Searches for another transit of Mercury or Venus. + * + * After calling {@link Astronomy.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 {string} body + * The planet whose transit is to be found. Must be `"Mercury"` or `"Venus"`. + * + * @param {Astronomy.AstroTime} prevTransitTime + * A date and time near the previous transit. + * + * @returns {Astronomy.TransitInfo} + */ +Astronomy.NextTransit = function(body, prevTransitTime) { + const startTime = prevTransitTime.AddDays(100.0); + return Astronomy.SearchTransit(body, startTime); +} + + +})(typeof exports==='undefined' ? (this.Astronomy={}) : exports); diff --git a/demo/browser/astronomy.min.js b/demo/browser/astronomy.min.js deleted file mode 120000 index 4fcb1084..00000000 --- a/demo/browser/astronomy.min.js +++ /dev/null @@ -1 +0,0 @@ -../../source/js/astronomy.min.js \ No newline at end of file diff --git a/demo/browser/barycenter.html b/demo/browser/barycenter.html index 5aa83d5d..e7e97e56 100644 --- a/demo/browser/barycenter.html +++ b/demo/browser/barycenter.html @@ -3,7 +3,7 @@ Solar System Barycenter - + + + +