acid-drop

- Hacking the planet from a LilyGo T-Deck using custom firmware
git clone git://git.acid.vegas/acid-drop.git
Log | Files | Refs | Archive | README | LICENSE

astronomy.c (345283B)

      1 /*
      2     Astronomy Engine for C/C++.
      3     https://github.com/cosinekitty/astronomy
      4 
      5     MIT License
      6 
      7     Copyright (c) 2019-2020 Don Cross <cosinekitty@gmail.com>
      8 
      9     Permission is hereby granted, free of charge, to any person obtaining a copy
     10     of this software and associated documentation files (the "Software"), to deal
     11     in the Software without restriction, including without limitation the rights
     12     to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     13     copies of the Software, and to permit persons to whom the Software is
     14     furnished to do so, subject to the following conditions:
     15 
     16     The above copyright notice and this permission notice shall be included in all
     17     copies or substantial portions of the Software.
     18 
     19     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     20     IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     21     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     22     AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     23     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     24     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     25     SOFTWARE.
     26 */
     27 
     28 #include <stdio.h>
     29 #include <stdlib.h>
     30 #include <string.h>
     31 #include <time.h>
     32 #include <math.h>
     33 #include "astronomy.h"
     34 
     35 #ifdef __cplusplus
     36 extern "C" {
     37 #endif
     38 
     39 /** @cond DOXYGEN_SKIP */
     40 #define PI      3.14159265358979323846
     41 
     42 typedef struct
     43 {
     44     double x;
     45     double y;
     46     double z;
     47 }
     48 terse_vector_t;
     49 
     50 static const terse_vector_t VecZero;
     51 
     52 static terse_vector_t VecAdd(terse_vector_t a, terse_vector_t b)
     53 {
     54     terse_vector_t c;
     55     c.x = a.x + b.x;
     56     c.y = a.y + b.y;
     57     c.z = a.z + b.z;
     58     return c;
     59 }
     60 
     61 static terse_vector_t VecSub(terse_vector_t a, terse_vector_t b)
     62 {
     63     terse_vector_t c;
     64     c.x = a.x - b.x;
     65     c.y = a.y - b.y;
     66     c.z = a.z - b.z;
     67     return c;
     68 }
     69 
     70 static void VecIncr(terse_vector_t *target, terse_vector_t source)
     71 {
     72     target->x += source.x;
     73     target->y += source.y;
     74     target->z += source.z;
     75 }
     76 
     77 static void VecDecr(terse_vector_t *target, terse_vector_t source)
     78 {
     79     target->x -= source.x;
     80     target->y -= source.y;
     81     target->z -= source.z;
     82 }
     83 
     84 static terse_vector_t VecMul(double s, terse_vector_t v)
     85 {
     86     terse_vector_t p;
     87     p.x = s * v.x;
     88     p.y = s * v.y;
     89     p.z = s * v.z;
     90     return p;
     91 }
     92 
     93 static void VecScale(terse_vector_t *target, double scalar)
     94 {
     95     target->x *= scalar;
     96     target->y *= scalar;
     97     target->z *= scalar;
     98 }
     99 
    100 static terse_vector_t VecRamp(terse_vector_t a, terse_vector_t b, double ramp)
    101 {
    102     terse_vector_t c;
    103     c.x = (1-ramp)*a.x + ramp*b.x;
    104     c.y = (1-ramp)*a.y + ramp*b.y;
    105     c.z = (1-ramp)*a.z + ramp*b.z;
    106     return c;
    107 }
    108 
    109 static terse_vector_t VecMean(terse_vector_t a, terse_vector_t b)
    110 {
    111     terse_vector_t c;
    112     c.x = (a.x + b.x) / 2;
    113     c.y = (a.y + b.y) / 2;
    114     c.z = (a.z + b.z) / 2;
    115     return c;
    116 }
    117 
    118 static astro_vector_t PublicVec(astro_time_t time, terse_vector_t terse)
    119 {
    120     astro_vector_t vector;
    121 
    122     vector.status = ASTRO_SUCCESS;
    123     vector.t = time;
    124     vector.x = terse.x;
    125     vector.y = terse.y;
    126     vector.z = terse.z;
    127 
    128     return vector;
    129 }
    130 
    131 typedef struct
    132 {
    133     double          tt;  /* Terrestrial Time in J2000 days */
    134     terse_vector_t  r;   /* position [au] */
    135     terse_vector_t  v;   /* velocity [au/day] */
    136 }
    137 body_state_t;
    138 /** @endcond */
    139 
    140 static const double DAYS_PER_TROPICAL_YEAR = 365.24217;
    141 static const double DEG2RAD = 0.017453292519943296;
    142 static const double RAD2DEG = 57.295779513082321;
    143 static const double ASEC360 = 1296000.0;
    144 static const double ASEC2RAD = 4.848136811095359935899141e-6;
    145 static const double PI2 = 2.0 * PI;
    146 static const double ARC = 3600.0 * 180.0 / PI;          /* arcseconds per radian */
    147 static const double C_AUDAY = 173.1446326846693;        /* speed of light in AU/day */
    148 static const double KM_PER_AU = 1.4959787069098932e+8;
    149 static const double SECONDS_PER_DAY = 24.0 * 3600.0;
    150 static const double SOLAR_DAYS_PER_SIDEREAL_DAY = 0.9972695717592592;
    151 static const double MEAN_SYNODIC_MONTH = 29.530588;     /* average number of days for Moon to return to the same phase */
    152 static const double EARTH_ORBITAL_PERIOD = 365.256;
    153 static const double NEPTUNE_ORBITAL_PERIOD = 60189.0;
    154 static const double REFRACTION_NEAR_HORIZON = 34.0 / 60.0;   /* degrees of refractive "lift" seen for objects near horizon */
    155 
    156 static const double SUN_RADIUS_KM  = 695700.0;
    157 #define             SUN_RADIUS_AU  (SUN_RADIUS_KM / KM_PER_AU)
    158 
    159 #define EARTH_FLATTENING            0.996647180302104
    160 #define EARTH_EQUATORIAL_RADIUS_KM  6378.1366
    161 #define EARTH_EQUATORIAL_RADIUS_AU  (EARTH_EQUATORIAL_RADIUS_KM / KM_PER_AU)
    162 #define EARTH_MEAN_RADIUS_KM        6371.0            /* mean radius of the Earth's geoid, without atmosphere */
    163 #define EARTH_ATMOSPHERE_KM           88.0            /* effective atmosphere thickness for lunar eclipses */
    164 #define EARTH_ECLIPSE_RADIUS_KM     (EARTH_MEAN_RADIUS_KM + EARTH_ATMOSPHERE_KM)
    165 /* Note: if we ever need Earth's polar radius, it is (EARTH_FLATTENING * EARTH_EQUATORIAL_RADIUS_KM) */
    166 
    167 #define MOON_EQUATORIAL_RADIUS_KM   1738.1
    168 #define MOON_MEAN_RADIUS_KM         1737.4
    169 #define MOON_POLAR_RADIUS_KM        1736.0
    170 #define MOON_EQUATORIAL_RADIUS_AU   (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU)
    171 
    172 static const double ASEC180 = 180.0 * 60.0 * 60.0;      /* arcseconds per 180 degrees (or pi radians) */
    173 static const double EARTH_MOON_MASS_RATIO = 81.30056;
    174 
    175 /*
    176     Masses of the Sun and outer planets, used for:
    177     (1) Calculating the Solar System Barycenter
    178     (2) Integrating the movement of Pluto
    179 
    180     https://web.archive.org/web/20120220062549/http://iau-comm4.jpl.nasa.gov/de405iom/de405iom.pdf
    181 
    182     Page 10 in the above document describes the constants used in the DE405 ephemeris.
    183     The following are G*M values (gravity constant * mass) in [au^3 / day^2].
    184     This side-steps issues of not knowing the exact values of G and masses M[i];
    185     the products GM[i] are known extremely accurately.
    186 */
    187 static const double SUN_GM     = 0.2959122082855911e-03;
    188 static const double JUPITER_GM = 0.2825345909524226e-06;
    189 static const double SATURN_GM  = 0.8459715185680659e-07;
    190 static const double URANUS_GM  = 0.1292024916781969e-07;
    191 static const double NEPTUNE_GM = 0.1524358900784276e-07;
    192 
    193 /** @cond DOXYGEN_SKIP */
    194 #define ARRAYSIZE(x)    (sizeof(x) / sizeof(x[0]))
    195 #define AU_PER_PARSEC   (ASEC180 / PI)             /* exact definition of how many AU = one parsec */
    196 #define Y2000_IN_MJD    (T0 - MJD_BASIS)
    197 /** @endcond */
    198 
    199 static astro_ecliptic_t RotateEquatorialToEcliptic(const double pos[3], double obliq_radians);
    200 static int QuadInterp(
    201     double tm, double dt, double fa, double fm, double fb,
    202     double *x, double *t, double *df_dt);
    203 
    204 static double LongitudeOffset(double diff)
    205 {
    206     double offset = diff;
    207 
    208     while (offset <= -180.0)
    209         offset += 360.0;
    210 
    211     while (offset > 180.0)
    212         offset -= 360.0;
    213 
    214     return offset;
    215 }
    216 
    217 static double NormalizeLongitude(double lon)
    218 {
    219     while (lon < 0.0)
    220         lon += 360.0;
    221 
    222     while (lon >= 360.0)
    223         lon -= 360.0;
    224 
    225     return lon;
    226 }
    227 
    228 /**
    229  * @brief Calculates the length of the given vector.
    230  *
    231  * Calculates the non-negative length of the given vector.
    232  * The length is expressed in the same units as the vector's components,
    233  * usually astronomical units (AU).
    234  *
    235  * @param vector The vector whose length is to be calculated.
    236  * @return The length of the vector.
    237  */
    238 double Astronomy_VectorLength(astro_vector_t vector)
    239 {
    240     return sqrt(vector.x*vector.x + vector.y*vector.y + vector.z*vector.z);
    241 }
    242 
    243 /**
    244  * @brief Finds the name of a celestial body.
    245  * @param body The celestial body whose name is to be found.
    246  * @return The English-language name of the celestial body, or "" if the body is not valid.
    247  */
    248 const char *Astronomy_BodyName(astro_body_t body)
    249 {
    250     switch (body)
    251     {
    252     case BODY_MERCURY:  return "Mercury";
    253     case BODY_VENUS:    return "Venus";
    254     case BODY_EARTH:    return "Earth";
    255     case BODY_MARS:     return "Mars";
    256     case BODY_JUPITER:  return "Jupiter";
    257     case BODY_SATURN:   return "Saturn";
    258     case BODY_URANUS:   return "Uranus";
    259     case BODY_NEPTUNE:  return "Neptune";
    260     case BODY_PLUTO:    return "Pluto";
    261     case BODY_SUN:      return "Sun";
    262     case BODY_MOON:     return "Moon";
    263     case BODY_EMB:      return "EMB";
    264     case BODY_SSB:      return "SSB";
    265     default:            return "";
    266     }
    267 }
    268 
    269 /**
    270  * @brief Returns the #astro_body_t value corresponding to the given English name.
    271  * @param name One of the following strings: Sun, Moon, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, EMB, SSB.
    272  * @return If `name` is one of the listed strings (case-sensitive), the returned value is the corresponding #astro_body_t value, otherwise it is `BODY_INVALID`.
    273  */
    274 astro_body_t Astronomy_BodyCode(const char *name)
    275 {
    276     if (name != NULL)
    277     {
    278         if (!strcmp(name, "Mercury"))   return BODY_MERCURY;
    279         if (!strcmp(name, "Venus"))     return BODY_VENUS;
    280         if (!strcmp(name, "Earth"))     return BODY_EARTH;
    281         if (!strcmp(name, "Mars"))      return BODY_MARS;
    282         if (!strcmp(name, "Jupiter"))   return BODY_JUPITER;
    283         if (!strcmp(name, "Saturn"))    return BODY_SATURN;
    284         if (!strcmp(name, "Uranus"))    return BODY_URANUS;
    285         if (!strcmp(name, "Neptune"))   return BODY_NEPTUNE;
    286         if (!strcmp(name, "Pluto"))     return BODY_PLUTO;
    287         if (!strcmp(name, "Sun"))       return BODY_SUN;
    288         if (!strcmp(name, "Moon"))      return BODY_MOON;
    289         if (!strcmp(name, "EMB"))       return BODY_EMB;
    290         if (!strcmp(name, "SSB"))       return BODY_SSB;
    291     }
    292     return BODY_INVALID;
    293 }
    294 
    295 /**
    296  * @brief Returns 1 for planets that are farther from the Sun than the Earth is, 0 otherwise.
    297  */
    298 static int IsSuperiorPlanet(astro_body_t body)
    299 {
    300     switch (body)
    301     {
    302     case BODY_MARS:
    303     case BODY_JUPITER:
    304     case BODY_SATURN:
    305     case BODY_URANUS:
    306     case BODY_NEPTUNE:
    307     case BODY_PLUTO:
    308         return 1;
    309 
    310     default:
    311         return 0;
    312     }
    313 }
    314 
    315 /**
    316  * @brief Returns the number of days it takes for a planet to orbit the Sun.
    317  */
    318 static double PlanetOrbitalPeriod(astro_body_t body)
    319 {
    320     switch (body)
    321     {
    322     case BODY_MERCURY:  return     87.969;
    323     case BODY_VENUS:    return    224.701;
    324     case BODY_EARTH:    return    EARTH_ORBITAL_PERIOD;
    325     case BODY_MARS:     return    686.980;
    326     case BODY_JUPITER:  return   4332.589;
    327     case BODY_SATURN:   return  10759.22;
    328     case BODY_URANUS:   return  30685.4;
    329     case BODY_NEPTUNE:  return  NEPTUNE_ORBITAL_PERIOD;
    330     case BODY_PLUTO:    return  90560.0;
    331     default:            return  0.0;        /* invalid body */
    332     }
    333 }
    334 
    335 static void FatalError(const char *message)
    336 {
    337     fprintf(stderr, "FATAL: %s\n", message);
    338     exit(1);
    339 }
    340 
    341 static astro_vector_t VecError(astro_status_t status, astro_time_t time)
    342 {
    343     astro_vector_t vec;
    344     vec.x = vec.y = vec.z = NAN;
    345     vec.t = time;
    346     vec.status = status;
    347     return vec;
    348 }
    349 
    350 static astro_spherical_t SphereError(astro_status_t status)
    351 {
    352     astro_spherical_t sphere;
    353     sphere.status = status;
    354     sphere.dist = sphere.lat = sphere.lon = NAN;
    355     return sphere;
    356 }
    357 
    358 static astro_equatorial_t EquError(astro_status_t status)
    359 {
    360     astro_equatorial_t equ;
    361     equ.ra = equ.dec = equ.dist = NAN;
    362     equ.status = status;
    363     return equ;
    364 }
    365 
    366 static astro_ecliptic_t EclError(astro_status_t status)
    367 {
    368     astro_ecliptic_t ecl;
    369     ecl.status = status;
    370     ecl.ex = ecl.ey = ecl.ez = ecl.elat = ecl.elon = NAN;
    371     return ecl;
    372 }
    373 
    374 static astro_angle_result_t AngleError(astro_status_t status)
    375 {
    376     astro_angle_result_t result;
    377     result.status = status;
    378     result.angle = NAN;
    379     return result;
    380 }
    381 
    382 static astro_func_result_t FuncError(astro_status_t status)
    383 {
    384     astro_func_result_t result;
    385     result.status = status;
    386     result.value = NAN;
    387     return result;
    388 }
    389 
    390 static astro_time_t TimeError(void)
    391 {
    392     astro_time_t time;
    393     time.tt = time.ut = time.eps = time.psi = NAN;
    394     return time;
    395 }
    396 
    397 static astro_rotation_t RotationErr(astro_status_t status)
    398 {
    399     astro_rotation_t rotation;
    400     int i, j;
    401 
    402     rotation.status = status;
    403     for (i=0; i<3; ++i)
    404         for (j=0; j<3; ++j)
    405             rotation.rot[i][j] = NAN;
    406 
    407     return rotation;
    408 }
    409 
    410 static astro_moon_quarter_t MoonQuarterError(astro_status_t status)
    411 {
    412     astro_moon_quarter_t result;
    413     result.status = status;
    414     result.quarter = -1;
    415     result.time = TimeError();
    416     return result;
    417 }
    418 
    419 static astro_elongation_t ElongError(astro_status_t status)
    420 {
    421     astro_elongation_t result;
    422 
    423     result.status = status;
    424     result.elongation = NAN;
    425     result.ecliptic_separation = NAN;
    426     result.time = TimeError();
    427     result.visibility = (astro_visibility_t)(-1);
    428 
    429     return result;
    430 }
    431 
    432 static astro_hour_angle_t HourAngleError(astro_status_t status)
    433 {
    434     astro_hour_angle_t result;
    435 
    436     result.status = status;
    437     result.time = TimeError();
    438     result.hor.altitude = result.hor.azimuth = result.hor.dec = result.hor.ra = NAN;
    439 
    440     return result;
    441 }
    442 
    443 static astro_illum_t IllumError(astro_status_t status)
    444 {
    445     astro_illum_t result;
    446 
    447     result.status = status;
    448     result.time = TimeError();
    449     result.mag = NAN;
    450     result.phase_angle = NAN;
    451     result.helio_dist = NAN;
    452     result.ring_tilt = NAN;
    453 
    454     return result;
    455 }
    456 
    457 static astro_apsis_t ApsisError(astro_status_t status)
    458 {
    459     astro_apsis_t result;
    460 
    461     result.status = status;
    462     result.time = TimeError();
    463     result.kind = APSIS_INVALID;
    464     result.dist_km = result.dist_au = NAN;
    465 
    466     return result;
    467 }
    468 
    469 static astro_search_result_t SearchError(astro_status_t status)
    470 {
    471     astro_search_result_t result;
    472     result.time = TimeError();
    473     result.status = status;
    474     return result;
    475 }
    476 
    477 static astro_constellation_t ConstelErr(astro_status_t status)
    478 {
    479     astro_constellation_t constel;
    480     constel.status = status;
    481     constel.symbol = constel.name = NULL;
    482     constel.ra_1875 = constel.dec_1875 = NAN;
    483     return constel;
    484 }
    485 
    486 static astro_transit_t TransitErr(astro_status_t status)
    487 {
    488     astro_transit_t transit;
    489     transit.status = status;
    490     transit.start = transit.peak = transit.finish = TimeError();
    491     transit.separation = NAN;
    492     return transit;
    493 }
    494 
    495 static astro_func_result_t SynodicPeriod(astro_body_t body)
    496 {
    497     double Tp;                         /* planet's orbital period in days */
    498     astro_func_result_t result;
    499 
    500     /* The Earth does not have a synodic period as seen from itself. */
    501     if (body == BODY_EARTH)
    502         return FuncError(ASTRO_EARTH_NOT_ALLOWED);
    503 
    504     if (body == BODY_MOON)
    505     {
    506         result.status = ASTRO_SUCCESS;
    507         result.value = MEAN_SYNODIC_MONTH;
    508         return result;
    509     }
    510 
    511     Tp = PlanetOrbitalPeriod(body);
    512     if (Tp <= 0.0)
    513         return FuncError(ASTRO_INVALID_BODY);
    514 
    515     result.status = ASTRO_SUCCESS;
    516     result.value = fabs(EARTH_ORBITAL_PERIOD / (EARTH_ORBITAL_PERIOD/Tp - 1.0));
    517     return result;
    518 }
    519 
    520 static astro_angle_result_t AngleBetween(astro_vector_t a, astro_vector_t b)
    521 {
    522     double r, dot;
    523     astro_angle_result_t result;
    524 
    525     r = Astronomy_VectorLength(a) * Astronomy_VectorLength(b);
    526     if (r < 1.0e-8)
    527         return AngleError(ASTRO_BAD_VECTOR);
    528 
    529     dot = (a.x*b.x + a.y*b.y + a.z*b.z) / r;
    530 
    531     if (dot <= -1.0)
    532         result.angle = 180.0;
    533     else if (dot >= +1.0)
    534         result.angle = 0.0;
    535     else
    536         result.angle = RAD2DEG * acos(dot);
    537 
    538     result.status = ASTRO_SUCCESS;
    539     return result;
    540 }
    541 
    542 /**
    543  * @brief The default Delta T function used by Astronomy Engine.
    544  *
    545  * Espenak and Meeus use a series of piecewise polynomials to
    546  * approximate DeltaT of the Earth in their "Five Millennium Canon of Solar Eclipses".
    547  * See: https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html
    548  * This is the default Delta T function used by Astronomy Engine.
    549  *
    550  * @param ut
    551  *      The floating point number of days since noon UTC on January 1, 2000.
    552  *
    553  * @returns
    554  *      The estimated difference TT-UT on the given date, expressed in seconds.
    555  */
    556 double Astronomy_DeltaT_EspenakMeeus(double ut)
    557 {
    558     double y, u, u2, u3, u4, u5, u6, u7;
    559 
    560     /*
    561         Fred Espenak writes about Delta-T generically here:
    562         https://eclipse.gsfc.nasa.gov/SEhelp/deltaT.html
    563         https://eclipse.gsfc.nasa.gov/SEhelp/deltat2004.html
    564 
    565         He provides polynomial approximations for distant years here:
    566         https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html
    567 
    568         They start with a year value 'y' such that y=2000 corresponds
    569         to the UTC Date 15-January-2000. Convert difference in days
    570         to mean tropical years.
    571     */
    572 
    573     y = 2000 + ((ut - 14) / DAYS_PER_TROPICAL_YEAR);
    574 
    575     if (y < -500)
    576     {
    577         u = (y - 1820) / 100;
    578         return -20 + (32 * u*u);
    579     }
    580     if (y < 500)
    581     {
    582         u = y / 100;
    583         u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3;
    584         return 10583.6 - 1014.41*u + 33.78311*u2 - 5.952053*u3 - 0.1798452*u4 + 0.022174192*u5 + 0.0090316521*u6;
    585     }
    586     if (y < 1600)
    587     {
    588         u = (y - 1000) / 100;
    589         u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3;
    590         return 1574.2 - 556.01*u + 71.23472*u2 + 0.319781*u3 - 0.8503463*u4 - 0.005050998*u5 + 0.0083572073*u6;
    591     }
    592     if (y < 1700)
    593     {
    594         u = y - 1600;
    595         u2 = u*u; u3 = u*u2;
    596         return 120 - 0.9808*u - 0.01532*u2 + u3/7129.0;
    597     }
    598     if (y < 1800)
    599     {
    600         u = y - 1700;
    601         u2 = u*u; u3 = u*u2; u4 = u2*u2;
    602         return 8.83 + 0.1603*u - 0.0059285*u2 + 0.00013336*u3 - u4/1174000;
    603     }
    604     if (y < 1860)
    605     {
    606         u = y - 1800;
    607         u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3; u7 = u3*u4;
    608         return 13.72 - 0.332447*u + 0.0068612*u2 + 0.0041116*u3 - 0.00037436*u4 + 0.0000121272*u5 - 0.0000001699*u6 + 0.000000000875*u7;
    609     }
    610     if (y < 1900)
    611     {
    612         u = y - 1860;
    613         u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3;
    614         return 7.62 + 0.5737*u - 0.251754*u2 + 0.01680668*u3 - 0.0004473624*u4 + u5/233174;
    615     }
    616     if (y < 1920)
    617     {
    618         u = y - 1900;
    619         u2 = u*u; u3 = u*u2; u4 = u2*u2;
    620         return -2.79 + 1.494119*u - 0.0598939*u2 + 0.0061966*u3 - 0.000197*u4;
    621     }
    622     if (y < 1941)
    623     {
    624         u = y - 1920;
    625         u2 = u*u; u3 = u*u2;
    626         return 21.20 + 0.84493*u - 0.076100*u2 + 0.0020936*u3;
    627     }
    628     if (y < 1961)
    629     {
    630         u = y - 1950;
    631         u2 = u*u; u3 = u*u2;
    632         return 29.07 + 0.407*u - u2/233 + u3/2547;
    633     }
    634     if (y < 1986)
    635     {
    636         u = y - 1975;
    637         u2 = u*u; u3 = u*u2;
    638         return 45.45 + 1.067*u - u2/260 - u3/718;
    639     }
    640     if (y < 2005)
    641     {
    642         u = y - 2000;
    643         u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3;
    644         return 63.86 + 0.3345*u - 0.060374*u2 + 0.0017275*u3 + 0.000651814*u4 + 0.00002373599*u5;
    645     }
    646     if (y < 2050)
    647     {
    648         u = y - 2000;
    649         return 62.92 + 0.32217*u + 0.005589*u*u;
    650     }
    651     if (y < 2150)
    652     {
    653         u = (y-1820)/100;
    654         return -20 + 32*u*u - 0.5628*(2150 - y);
    655     }
    656 
    657     /* all years after 2150 */
    658     u = (y - 1820) / 100;
    659     return -20 + (32 * u*u);
    660 }
    661 
    662 /**
    663  * @brief A Delta T function that approximates the one used by the JPL Horizons tool.
    664  *
    665  * In order to support unit tests based on data generated by the JPL Horizons online
    666  * tool, I had to reverse engineer their Delta T function by generating a table that
    667  * contained it. The main difference between their tool and the Espenak/Meeus function
    668  * is that they stop extrapolating the Earth's deceleration after the year 2017.
    669  *
    670  * @param ut
    671  *      The floating point number of days since noon UTC on January 1, 2000.
    672  *
    673  * @returns
    674  *      The estimated difference TT-UT on the given date, expressed in seconds.
    675  */
    676 double Astronomy_DeltaT_JplHorizons(double ut)
    677 {
    678     if (ut > 17.0 * DAYS_PER_TROPICAL_YEAR)
    679         ut = 17.0 * DAYS_PER_TROPICAL_YEAR;
    680 
    681     return Astronomy_DeltaT_EspenakMeeus(ut);
    682 }
    683 
    684 static astro_deltat_func DeltaTFunc = Astronomy_DeltaT_EspenakMeeus;
    685 
    686 /**
    687  * @brief Changes the function Astronomy Engine uses to calculate Delta T.
    688  *
    689  * Most programs should not call this function. It is for advanced use cases only.
    690  * By default, Astronomy Engine uses the function #Astronomy_DeltaT_EspenakMeeus
    691  * to estimate changes in the Earth's rotation rate over time.
    692  * However, for the sake of unit tests that compare calculations against
    693  * external data sources that use alternative models for Delta T,
    694  * it is sometimes useful to replace the Delta T model to match.
    695  * This function allows replacing the Delta T model with any other
    696  * desired model.
    697  *
    698  * @param func
    699  *      A pointer to a function to convert UT values to DeltaT values.
    700  */
    701 void Astronomy_SetDeltaTFunction(astro_deltat_func func)
    702 {
    703     DeltaTFunc = func;
    704 }
    705 
    706 static double TerrestrialTime(double ut)
    707 {
    708     return ut + DeltaTFunc(ut)/86400.0;
    709 }
    710 
    711 /**
    712  * @brief
    713  *      Converts a J2000 day value to an #astro_time_t value.
    714  *
    715  * This function can be useful for reproducing an #astro_time_t structure
    716  * from its `ut` field only.
    717  *
    718  * @param ut
    719  *      The floating point number of days since noon UTC on January 1, 2000.
    720  *
    721  * @returns
    722  *      An #astro_time_t value for the given `ut` value.
    723  */
    724 astro_time_t Astronomy_TimeFromDays(double ut)
    725 {
    726     astro_time_t  time;
    727     time.ut = ut;
    728     time.tt = TerrestrialTime(ut);
    729     time.psi = time.eps = NAN;
    730     return time;
    731 }
    732 
    733 /**
    734  * @brief Returns the computer's current date and time in the form of an #astro_time_t.
    735  *
    736  * Uses the computer's system clock to find the current UTC date and time with 1-second granularity.
    737  * Converts that date and time to an #astro_time_t value and returns the result.
    738  * Callers can pass this value to other Astronomy Engine functions to calculate
    739  * current observational conditions.
    740  */
    741 astro_time_t Astronomy_CurrentTime(void)
    742 {
    743     astro_time_t t;
    744 
    745     /* Get seconds since midnight January 1, 1970, divide to convert to days, */
    746     /* then subtract to get days since noon on January 1, 2000. */
    747 
    748     t.ut = (time(NULL) / SECONDS_PER_DAY) - 10957.5;
    749     t.tt = TerrestrialTime(t.ut);
    750     t.psi = t.eps = NAN;
    751     return t;
    752 }
    753 
    754 /**
    755  * @brief Creates an #astro_time_t value from a given calendar date and time.
    756  *
    757  * Given a UTC calendar date and time, calculates an #astro_time_t value that can
    758  * be passed to other Astronomy Engine functions for performing various calculations
    759  * relating to that date and time.
    760  *
    761  * It is the caller's responsibility to ensure that the parameter values are correct.
    762  * The parameters are not checked for validity,
    763  * and this function never returns any indication of an error.
    764  * Invalid values, for example passing in February 31, may cause unexpected return values.
    765  *
    766  * @param year      The UTC calendar year, e.g. 2019.
    767  * @param month     The UTC calendar month in the range 1..12.
    768  * @param day       The UTC calendar day in the range 1..31.
    769  * @param hour      The UTC hour of the day in the range 0..23.
    770  * @param minute    The UTC minute in the range 0..59.
    771  * @param second    The UTC floating-point second in the range [0, 60).
    772  *
    773  * @return  An #astro_time_t value that represents the given calendar date and time.
    774  */
    775 astro_time_t Astronomy_MakeTime(int year, int month, int day, int hour, int minute, double second)
    776 {
    777     astro_time_t time;
    778     long int jd12h;
    779     long int y2000;
    780 
    781     /* This formula is adapted from NOVAS C 3.1 function julian_date() */
    782     jd12h = (long) day - 32075L + 1461L * ((long) year + 4800L
    783         + ((long) month - 14L) / 12L) / 4L
    784         + 367L * ((long) month - 2L - ((long) month - 14L) / 12L * 12L)
    785         / 12L - 3L * (((long) year + 4900L + ((long) month - 14L) / 12L)
    786         / 100L) / 4L;
    787 
    788     y2000 = jd12h - 2451545L;
    789 
    790     time.ut = (double)y2000 - 0.5 + (hour / 24.0) + (minute / (24.0 * 60.0)) + (second / (24.0 * 3600.0));
    791     time.tt = TerrestrialTime(time.ut);
    792     time.psi = time.eps = NAN;
    793 
    794     return time;
    795 }
    796 
    797 /**
    798  * @brief   Calculates the sum or difference of an #astro_time_t with a specified floating point number of days.
    799  *
    800  * Sometimes we need to adjust a given #astro_time_t value by a certain amount of time.
    801  * This function adds the given real number of days in `days` to the date and time in `time`.
    802  *
    803  * More precisely, the result's Universal Time field `ut` is exactly adjusted by `days` and
    804  * the Terrestrial Time field `tt` is adjusted correctly for the resulting UTC date and time,
    805  * according to the historical and predictive Delta-T model provided by the
    806  * [United States Naval Observatory](http://maia.usno.navy.mil/ser7/).
    807  *
    808  * The value stored in `time` will not be modified; it is passed by value.
    809  *
    810  * @param time  A date and time for which to calculate an adjusted date and time.
    811  * @param days  A floating point number of days by which to adjust `time`. May be negative, 0, or positive.
    812  * @return  A date and time that is conceptually equal to `time + days`.
    813  */
    814 astro_time_t Astronomy_AddDays(astro_time_t time, double days)
    815 {
    816     /*
    817         This is slightly wrong, but the error is tiny.
    818         We really should be adding to TT, not to UT.
    819         But using TT would require creating an inverse function for DeltaT,
    820         which would be quite a bit of extra calculation.
    821         I estimate the error is in practice on the order of 10^(-7)
    822         times the value of 'days'.
    823         This is based on a typical drift of 1 second per year between UT and TT.
    824     */
    825 
    826     astro_time_t sum;
    827 
    828     sum.ut = time.ut + days;
    829     sum.tt = TerrestrialTime(sum.ut);
    830     sum.eps = sum.psi = NAN;
    831 
    832     return sum;
    833 }
    834 
    835 /**
    836  * @brief   Creates an #astro_time_t value from a given calendar date and time.
    837  *
    838  * This function is similar to #Astronomy_MakeTime, only it receives a
    839  * UTC calendar date and time in the form of an #astro_utc_t structure instead of
    840  * as separate numeric parameters.  Astronomy_TimeFromUtc is the inverse of
    841  * #Astronomy_UtcFromTime.
    842  *
    843  * @param utc   The UTC calendar date and time to be converted to #astro_time_t.
    844  * @return  A value that can be used for astronomical calculations for the given date and time.
    845  */
    846 astro_time_t Astronomy_TimeFromUtc(astro_utc_t utc)
    847 {
    848     return Astronomy_MakeTime(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second);
    849 }
    850 
    851 /**
    852  * @brief Determines the calendar year, month, day, and time from an #astro_time_t value.
    853  *
    854  * After calculating the date and time of an astronomical event in the form of
    855  * an #astro_time_t value, it is often useful to display the result in a human-readable
    856  * form. This function converts the linear time scales in the `ut` field of #astro_time_t
    857  * into a calendar date and time: year, month, day, hours, minutes, and seconds, expressed
    858  * in UTC.
    859  *
    860  * @param time  The astronomical time value to be converted to calendar date and time.
    861  * @return  A date and time broken out into conventional year, month, day, hour, minute, and second.
    862  */
    863 astro_utc_t Astronomy_UtcFromTime(astro_time_t time)
    864 {
    865     /* Adapted from the NOVAS C 3.1 function cal_date() */
    866     astro_utc_t utc;
    867     long jd, k, m, n;
    868     double djd, x;
    869 
    870     djd = time.ut + 2451545.5;
    871     jd = (long)djd;
    872 
    873     x = 24.0 * fmod(djd, 1.0);
    874     utc.hour = (int)x;
    875     x = 60.0 * fmod(x, 1.0);
    876     utc.minute = (int)x;
    877     utc.second = 60.0 * fmod(x, 1.0);
    878 
    879     k = jd + 68569L;
    880     n = 4L * k / 146097L;
    881     k = k - (146097L * n + 3L) / 4L;
    882     m = 4000L * (k + 1L) / 1461001L;
    883     k = k - 1461L * m / 4L + 31L;
    884 
    885     utc.month = (int) (80L * k / 2447L);
    886     utc.day = (int) (k - 2447L * (long)utc.month / 80L);
    887     k = (long) utc.month / 11L;
    888 
    889     utc.month = (int) ((long)utc.month + 2L - 12L * k);
    890     utc.year = (int) (100L * (n - 49L) + m + k);
    891 
    892     return utc;
    893 }
    894 
    895 
    896 /**
    897  * @brief Formats an #astro_time_t value as an ISO 8601 string.
    898  *
    899  * Given an #astro_time_t value `time`, formats it as an ISO 8601
    900  * string to the resolution specified by the `format` parameter.
    901  * The result is stored in the `text` buffer whose capacity in bytes
    902  * is specified by `size`.
    903  *
    904  * @param time
    905  *      The date and time whose civil time `time.ut` is to be formatted as an ISO 8601 string.
    906  *      If the civil time is outside the year range 0000 to 9999, the function fails
    907  *      and returns `ASTRO_BAD_TIME`. Years prior to 1583 are treated as if they are
    908  *      using the modern Gregorian calendar, even when the Julian calendar was actually in effect.
    909  *
    910  * @param format
    911  *      Specifies the resolution to which the date and time should be formatted,
    912  *      as explained at #astro_time_format_t.
    913  *      If the value of `format` is not recognized, the function fails and
    914  *      returns `ASTRO_INVALID_PARAMETER`.
    915  *
    916  * @param text
    917  *      A pointer to a text buffer to receive the output.
    918  *      If `text` is `NULL`, this function returns `ASTRO_INVALID_PARAMETER`.
    919  *      If the function fails for any reason, and `text` is not `NULL`,
    920  *      and `size` is greater than 0, the `text` buffer is set to an empty string.
    921  *
    922  * @param size
    923  *      The size in bytes of the buffer pointed to by `text`. The buffer must
    924  *      be large enough to accomodate the output format selected by the
    925  *      `format` parameter, as specified at #astro_time_format_t.
    926  *      If `size` is too small to hold the string as specified by `format`,
    927  *      the `text` buffer is set to `""` (if possible)
    928  *      and the function returns `ASTRO_BUFFER_TOO_SMALL`.
    929  *      A buffer that is `TIME_TEXT_BYTES` (25) bytes or larger is always large enough for this function.
    930  *
    931  * @return `ASTRO_SUCCESS` on success; otherwise an error as described in the parameter notes.
    932  */
    933 astro_status_t Astronomy_FormatTime(
    934     astro_time_t time,
    935     astro_time_format_t format,
    936     char *text,
    937     size_t size)
    938 {
    939     int nprinted;
    940     double rounding;
    941     size_t min_size;
    942     astro_utc_t utc;
    943 
    944     if (text == NULL)
    945         return ASTRO_INVALID_PARAMETER;
    946 
    947     if (size == 0)
    948         return ASTRO_BUFFER_TOO_SMALL;
    949 
    950     text[0] = '\0';     /* initialize to empty string, in case an error occurs */
    951 
    952     /* Validate 'size' parameter and perform date/time rounding. */
    953     switch (format)
    954     {
    955     case TIME_FORMAT_DAY:
    956         min_size = 11;                          /* "2020-12-31" */
    957         rounding = 0.0;                         /* no rounding */
    958         break;
    959 
    960     case TIME_FORMAT_MINUTE:
    961         min_size = 18;                          /* "2020-12-31T15:47Z" */
    962         rounding = 0.5 / (24.0 * 60.0);         /* round to nearest minute */
    963         break;
    964 
    965     case TIME_FORMAT_SECOND:
    966         min_size = 21;                          /* "2020-12-31T15:47:59Z" */
    967         rounding = 0.5 / (24.0 * 3600.0);       /* round to nearest second */
    968         break;
    969 
    970     case TIME_FORMAT_MILLI:
    971         min_size = 25;                          /* "2020-12-31T15:47:59.123Z" */
    972         rounding = 0.5 / (24.0 * 3600000.0);    /* round to nearest millisecond */
    973         break;
    974 
    975     default:
    976         return ASTRO_INVALID_PARAMETER;
    977     }
    978 
    979     /* Check for insufficient buffer size. */
    980     if (size < min_size)
    981         return ASTRO_BUFFER_TOO_SMALL;
    982 
    983     /* Perform rounding. */
    984     time.ut += rounding;
    985 
    986     /* Convert linear J2000 days to Gregorian UTC date/time. */
    987     utc = Astronomy_UtcFromTime(time);
    988 
    989     /* We require the year to be formatted as a 4-digit non-negative integer. */
    990     if (utc.year < 0 || utc.year > 9999)
    991         return ASTRO_BAD_TIME;
    992 
    993     /* Format the string. */
    994     switch (format)
    995     {
    996     case TIME_FORMAT_DAY:
    997         nprinted = snprintf(text, size, "%04d-%02d-%02d",
    998             utc.year, utc.month, utc.day);
    999         break;
   1000 
   1001     case TIME_FORMAT_MINUTE:
   1002         nprinted = snprintf(text, size, "%04d-%02d-%02dT%02d:%02dZ",
   1003             utc.year, utc.month, utc.day,
   1004             utc.hour, utc.minute);
   1005         break;
   1006 
   1007     case TIME_FORMAT_SECOND:
   1008         nprinted = snprintf(text, size, "%04d-%02d-%02dT%02d:%02d:%02.0lfZ",
   1009             utc.year, utc.month, utc.day,
   1010             utc.hour, utc.minute, floor(utc.second));
   1011         break;
   1012 
   1013     case TIME_FORMAT_MILLI:
   1014         nprinted = snprintf(text, size, "%04d-%02d-%02dT%02d:%02d:%06.3lfZ",
   1015             utc.year, utc.month, utc.day,
   1016             utc.hour, utc.minute, floor(1000.0 * utc.second) / 1000.0);
   1017         break;
   1018 
   1019     default:
   1020         /* We should have already failed for any unknown 'format' value. */
   1021         return ASTRO_INTERNAL_ERROR;
   1022     }
   1023 
   1024     if (nprinted < 0)
   1025         return ASTRO_INTERNAL_ERROR;    /* should not be possible for snprintf to return a negative number */
   1026 
   1027     if (1+(int)nprinted != min_size)
   1028         return ASTRO_INTERNAL_ERROR;    /* there must be a bug calculating min_size or formatting the string */
   1029 
   1030     return ASTRO_SUCCESS;
   1031 }
   1032 
   1033 
   1034 /**
   1035  * @brief   Creates an observer object that represents a location on or near the surface of the Earth.
   1036  *
   1037  * Some Astronomy Engine functions calculate values pertaining to an observer on the Earth.
   1038  * These functions require a value of type #astro_observer_t that represents the location
   1039  * of such an observer.
   1040  *
   1041  * @param latitude      The geographic latitude of the observer in degrees north (positive) or south (negative) of the equator.
   1042  * @param longitude     The geographic longitude of the observer in degrees east (positive) or west (negative) of the prime meridian at Greenwich, England.
   1043  * @param height        The height of the observer in meters above mean sea level.
   1044  * @return An observer object that can be passed to astronomy functions that require a geographic location.
   1045  */
   1046 astro_observer_t Astronomy_MakeObserver(double latitude, double longitude, double height)
   1047 {
   1048     astro_observer_t observer;
   1049 
   1050     observer.latitude = latitude;
   1051     observer.longitude = longitude;
   1052     observer.height = height;
   1053 
   1054     return observer;
   1055 }
   1056 
   1057 static void iau2000b(astro_time_t *time)
   1058 {
   1059     /* Adapted from the NOVAS C 3.1 function of the same name. */
   1060 
   1061     struct row_t
   1062     {
   1063         int nals[5];
   1064         double cls[6];
   1065     };
   1066 
   1067     static const struct row_t row[77] =
   1068     {
   1069 
   1070         { {  0,  0,  0,  0,  1 }, {   -172064161,      -174666,        33386,     92052331,         9086,        15377 } },
   1071         { {  0,  0,  2, -2,  2 }, {    -13170906,        -1675,       -13696,      5730336,        -3015,        -4587 } },
   1072         { {  0,  0,  2,  0,  2 }, {     -2276413,         -234,         2796,       978459,         -485,         1374 } },
   1073         { {  0,  0,  0,  0,  2 }, {      2074554,          207,         -698,      -897492,          470,         -291 } },
   1074         { {  0,  1,  0,  0,  0 }, {      1475877,        -3633,        11817,        73871,         -184,        -1924 } },
   1075         { {  0,  1,  2, -2,  2 }, {      -516821,         1226,         -524,       224386,         -677,         -174 } },
   1076         { {  1,  0,  0,  0,  0 }, {       711159,           73,         -872,        -6750,            0,          358 } },
   1077         { {  0,  0,  2,  0,  1 }, {      -387298,         -367,          380,       200728,           18,          318 } },
   1078         { {  1,  0,  2,  0,  2 }, {      -301461,          -36,          816,       129025,          -63,          367 } },
   1079         { {  0, -1,  2, -2,  2 }, {       215829,         -494,          111,       -95929,          299,          132 } },
   1080         { {  0,  0,  2, -2,  1 }, {       128227,          137,          181,       -68982,           -9,           39 } },
   1081         { { -1,  0,  2,  0,  2 }, {       123457,           11,           19,       -53311,           32,           -4 } },
   1082         { { -1,  0,  0,  2,  0 }, {       156994,           10,         -168,        -1235,            0,           82 } },
   1083         { {  1,  0,  0,  0,  1 }, {        63110,           63,           27,       -33228,            0,           -9 } },
   1084         { { -1,  0,  0,  0,  1 }, {       -57976,          -63,         -189,        31429,            0,          -75 } },
   1085         { { -1,  0,  2,  2,  2 }, {       -59641,          -11,          149,        25543,          -11,           66 } },
   1086         { {  1,  0,  2,  0,  1 }, {       -51613,          -42,          129,        26366,            0,           78 } },
   1087         { { -2,  0,  2,  0,  1 }, {        45893,           50,           31,       -24236,          -10,           20 } },
   1088         { {  0,  0,  0,  2,  0 }, {        63384,           11,         -150,        -1220,            0,           29 } },
   1089         { {  0,  0,  2,  2,  2 }, {       -38571,           -1,          158,        16452,          -11,           68 } },
   1090         { {  0, -2,  2, -2,  2 }, {        32481,            0,            0,       -13870,            0,            0 } },
   1091         { { -2,  0,  0,  2,  0 }, {       -47722,            0,          -18,          477,            0,          -25 } },
   1092         { {  2,  0,  2,  0,  2 }, {       -31046,           -1,          131,        13238,          -11,           59 } },
   1093         { {  1,  0,  2, -2,  2 }, {        28593,            0,           -1,       -12338,           10,           -3 } },
   1094         { { -1,  0,  2,  0,  1 }, {        20441,           21,           10,       -10758,            0,           -3 } },
   1095         { {  2,  0,  0,  0,  0 }, {        29243,            0,          -74,         -609,            0,           13 } },
   1096         { {  0,  0,  2,  0,  0 }, {        25887,            0,          -66,         -550,            0,           11 } },
   1097         { {  0,  1,  0,  0,  1 }, {       -14053,          -25,           79,         8551,           -2,          -45 } },
   1098         { { -1,  0,  0,  2,  1 }, {        15164,           10,           11,        -8001,            0,           -1 } },
   1099         { {  0,  2,  2, -2,  2 }, {       -15794,           72,          -16,         6850,          -42,           -5 } },
   1100         { {  0,  0, -2,  2,  0 }, {        21783,            0,           13,         -167,            0,           13 } },
   1101         { {  1,  0,  0, -2,  1 }, {       -12873,          -10,          -37,         6953,            0,          -14 } },
   1102         { {  0, -1,  0,  0,  1 }, {       -12654,           11,           63,         6415,            0,           26 } },
   1103         { { -1,  0,  2,  2,  1 }, {       -10204,            0,           25,         5222,            0,           15 } },
   1104         { {  0,  2,  0,  0,  0 }, {        16707,          -85,          -10,          168,           -1,           10 } },
   1105         { {  1,  0,  2,  2,  2 }, {        -7691,            0,           44,         3268,            0,           19 } },
   1106         { { -2,  0,  2,  0,  0 }, {       -11024,            0,          -14,          104,            0,            2 } },
   1107         { {  0,  1,  2,  0,  2 }, {         7566,          -21,          -11,        -3250,            0,           -5 } },
   1108         { {  0,  0,  2,  2,  1 }, {        -6637,          -11,           25,         3353,            0,           14 } },
   1109         { {  0, -1,  2,  0,  2 }, {        -7141,           21,            8,         3070,            0,            4 } },
   1110         { {  0,  0,  0,  2,  1 }, {        -6302,          -11,            2,         3272,            0,            4 } },
   1111         { {  1,  0,  2, -2,  1 }, {         5800,           10,            2,        -3045,            0,           -1 } },
   1112         { {  2,  0,  2, -2,  2 }, {         6443,            0,           -7,        -2768,            0,           -4 } },
   1113         { { -2,  0,  0,  2,  1 }, {        -5774,          -11,          -15,         3041,            0,           -5 } },
   1114         { {  2,  0,  2,  0,  1 }, {        -5350,            0,           21,         2695,            0,           12 } },
   1115         { {  0, -1,  2, -2,  1 }, {        -4752,          -11,           -3,         2719,            0,           -3 } },
   1116         { {  0,  0,  0, -2,  1 }, {        -4940,          -11,          -21,         2720,            0,           -9 } },
   1117         { { -1, -1,  0,  2,  0 }, {         7350,            0,           -8,          -51,            0,            4 } },
   1118         { {  2,  0,  0, -2,  1 }, {         4065,            0,            6,        -2206,            0,            1 } },
   1119         { {  1,  0,  0,  2,  0 }, {         6579,            0,          -24,         -199,            0,            2 } },
   1120         { {  0,  1,  2, -2,  1 }, {         3579,            0,            5,        -1900,            0,            1 } },
   1121         { {  1, -1,  0,  0,  0 }, {         4725,            0,           -6,          -41,            0,            3 } },
   1122         { { -2,  0,  2,  0,  2 }, {        -3075,            0,           -2,         1313,            0,           -1 } },
   1123         { {  3,  0,  2,  0,  2 }, {        -2904,            0,           15,         1233,            0,            7 } },
   1124         { {  0, -1,  0,  2,  0 }, {         4348,            0,          -10,          -81,            0,            2 } },
   1125         { {  1, -1,  2,  0,  2 }, {        -2878,            0,            8,         1232,            0,            4 } },
   1126         { {  0,  0,  0,  1,  0 }, {        -4230,            0,            5,          -20,            0,           -2 } },
   1127         { { -1, -1,  2,  2,  2 }, {        -2819,            0,            7,         1207,            0,            3 } },
   1128         { { -1,  0,  2,  0,  0 }, {        -4056,            0,            5,           40,            0,           -2 } },
   1129         { {  0, -1,  2,  2,  2 }, {        -2647,            0,           11,         1129,            0,            5 } },
   1130         { { -2,  0,  0,  0,  1 }, {        -2294,            0,          -10,         1266,            0,           -4 } },
   1131         { {  1,  1,  2,  0,  2 }, {         2481,            0,           -7,        -1062,            0,           -3 } },
   1132         { {  2,  0,  0,  0,  1 }, {         2179,            0,           -2,        -1129,            0,           -2 } },
   1133         { { -1,  1,  0,  1,  0 }, {         3276,            0,            1,           -9,            0,            0 } },
   1134         { {  1,  1,  0,  0,  0 }, {        -3389,            0,            5,           35,            0,           -2 } },
   1135         { {  1,  0,  2,  0,  0 }, {         3339,            0,          -13,         -107,            0,            1 } },
   1136         { { -1,  0,  2, -2,  1 }, {        -1987,            0,           -6,         1073,            0,           -2 } },
   1137         { {  1,  0,  0,  0,  2 }, {        -1981,            0,            0,          854,            0,            0 } },
   1138         { { -1,  0,  0,  1,  0 }, {         4026,            0,         -353,         -553,            0,         -139 } },
   1139         { {  0,  0,  2,  1,  2 }, {         1660,            0,           -5,         -710,            0,           -2 } },
   1140         { { -1,  0,  2,  4,  2 }, {        -1521,            0,            9,          647,            0,            4 } },
   1141         { { -1,  1,  0,  1,  1 }, {         1314,            0,            0,         -700,            0,            0 } },
   1142         { {  0, -2,  2, -2,  1 }, {        -1283,            0,            0,          672,            0,            0 } },
   1143         { {  1,  0,  2,  2,  1 }, {        -1331,            0,            8,          663,            0,            4 } },
   1144         { { -2,  0,  2,  2,  2 }, {         1383,            0,           -2,         -594,            0,           -2 } },
   1145         { { -1,  0,  0,  0,  2 }, {         1405,            0,            4,         -610,            0,            2 } },
   1146         { {  1,  1,  2, -2,  2 }, {         1290,            0,            0,         -556,            0,            0 } }
   1147 
   1148     };
   1149 
   1150     double t, el, elp, f, d, om, arg, dp, de, sarg, carg;
   1151     int i;
   1152 
   1153     if (isnan(time->psi))
   1154     {
   1155         t = time->tt / 36525;
   1156         el  = fmod(485868.249036 + t * 1717915923.2178, ASEC360) * ASEC2RAD;
   1157         elp = fmod(1287104.79305 + t * 129596581.0481,  ASEC360) * ASEC2RAD;
   1158         f   = fmod(335779.526232 + t * 1739527262.8478, ASEC360) * ASEC2RAD;
   1159         d   = fmod(1072260.70369 + t * 1602961601.2090, ASEC360) * ASEC2RAD;
   1160         om  = fmod(450160.398036 - t * 6962890.5431,    ASEC360) * ASEC2RAD;
   1161         dp = 0;
   1162         de = 0;
   1163         for (i=76; i >= 0; --i)
   1164         {
   1165             arg = fmod((row[i].nals[0]*el + row[i].nals[1]*elp + row[i].nals[2]*f + row[i].nals[3]*d + row[i].nals[4]*om), PI2);
   1166             sarg = sin(arg);
   1167             carg = cos(arg);
   1168             dp += (row[i].cls[0] + row[i].cls[1]*t) * sarg + row[i].cls[2]*carg;
   1169             de += (row[i].cls[3] + row[i].cls[4]*t) * carg + row[i].cls[5]*sarg;
   1170         }
   1171 
   1172         time->psi = -0.000135 + (dp * 1.0e-7);
   1173         time->eps = +0.000388 + (de * 1.0e-7);
   1174     }
   1175 }
   1176 
   1177 static double mean_obliq(double tt)
   1178 {
   1179     double t = tt / 36525.0;
   1180     double asec =
   1181         (((( -  0.0000000434   * t
   1182              -  0.000000576  ) * t
   1183              +  0.00200340   ) * t
   1184              -  0.0001831    ) * t
   1185              - 46.836769     ) * t + 84381.406;
   1186 
   1187     return asec / 3600.0;
   1188 }
   1189 
   1190 /** @cond DOXYGEN_SKIP */
   1191 typedef struct
   1192 {
   1193     double tt;
   1194     double dpsi;
   1195     double deps;
   1196     double ee;
   1197     double mobl;
   1198     double tobl;
   1199 }
   1200 earth_tilt_t;
   1201 /** @endcond */
   1202 
   1203 static earth_tilt_t e_tilt(astro_time_t *time)
   1204 {
   1205     earth_tilt_t et;
   1206 
   1207     iau2000b(time);
   1208     et.dpsi = time->psi;
   1209     et.deps = time->eps;
   1210     et.mobl = mean_obliq(time->tt);
   1211     et.tobl = et.mobl + (et.deps / 3600.0);
   1212     et.tt = time->tt;
   1213     et.ee = et.dpsi * cos(et.mobl * DEG2RAD) / 15.0;
   1214 
   1215     return et;
   1216 }
   1217 
   1218 static void ecl2equ_vec(astro_time_t time, const double ecl[3], double equ[3])
   1219 {
   1220     double obl = mean_obliq(time.tt) * DEG2RAD;
   1221     double cos_obl = cos(obl);
   1222     double sin_obl = sin(obl);
   1223 
   1224     equ[0] = ecl[0];
   1225     equ[1] = ecl[1]*cos_obl - ecl[2]*sin_obl;
   1226     equ[2] = ecl[1]*sin_obl + ecl[2]*cos_obl;
   1227 }
   1228 
   1229 
   1230 static astro_rotation_t precession_rot(double tt1, double tt2)
   1231 {
   1232     astro_rotation_t rotation;
   1233     double xx, yx, zx, xy, yy, zy, xz, yz, zz;
   1234     double t, psia, omegaa, chia, sa, ca, sb, cb, sc, cc, sd, cd;
   1235     double eps0 = 84381.406;
   1236 
   1237     if ((tt1 != 0.0) && (tt2 != 0.0))
   1238         FatalError("precession_rot: one of (tt1, tt2) must be zero.");
   1239 
   1240     t = (tt2 - tt1) / 36525;
   1241     if (tt2 == 0)
   1242         t = -t;
   1243 
   1244     psia   = (((((-    0.0000000951  * t
   1245                  +    0.000132851 ) * t
   1246                  -    0.00114045  ) * t
   1247                  -    1.0790069   ) * t
   1248                  + 5038.481507    ) * t);
   1249 
   1250     omegaa = (((((+    0.0000003337  * t
   1251                  -    0.000000467 ) * t
   1252                  -    0.00772503  ) * t
   1253                  +    0.0512623   ) * t
   1254                  -    0.025754    ) * t + eps0);
   1255 
   1256     chia   = (((((-    0.0000000560  * t
   1257                  +    0.000170663 ) * t
   1258                  -    0.00121197  ) * t
   1259                  -    2.3814292   ) * t
   1260                  +   10.556403    ) * t);
   1261 
   1262     eps0 = eps0 * ASEC2RAD;
   1263     psia = psia * ASEC2RAD;
   1264     omegaa = omegaa * ASEC2RAD;
   1265     chia = chia * ASEC2RAD;
   1266 
   1267     sa = sin(eps0);
   1268     ca = cos(eps0);
   1269     sb = sin(-psia);
   1270     cb = cos(-psia);
   1271     sc = sin(-omegaa);
   1272     cc = cos(-omegaa);
   1273     sd = sin(chia);
   1274     cd = cos(chia);
   1275 
   1276     xx =  cd * cb - sb * sd * cc;
   1277     yx =  cd * sb * ca + sd * cc * cb * ca - sa * sd * sc;
   1278     zx =  cd * sb * sa + sd * cc * cb * sa + ca * sd * sc;
   1279     xy = -sd * cb - sb * cd * cc;
   1280     yy = -sd * sb * ca + cd * cc * cb * ca - sa * cd * sc;
   1281     zy = -sd * sb * sa + cd * cc * cb * sa + ca * cd * sc;
   1282     xz =  sb * sc;
   1283     yz = -sc * cb * ca - sa * cc;
   1284     zz = -sc * cb * sa + cc * ca;
   1285 
   1286     if (tt2 == 0.0)
   1287     {
   1288         /* Perform rotation from other epoch to J2000.0. */
   1289         rotation.rot[0][0] = xx;
   1290         rotation.rot[0][1] = yx;
   1291         rotation.rot[0][2] = zx;
   1292         rotation.rot[1][0] = xy;
   1293         rotation.rot[1][1] = yy;
   1294         rotation.rot[1][2] = zy;
   1295         rotation.rot[2][0] = xz;
   1296         rotation.rot[2][1] = yz;
   1297         rotation.rot[2][2] = zz;
   1298     }
   1299     else
   1300     {
   1301         /* Perform rotation from J2000.0 to other epoch. */
   1302         rotation.rot[0][0] = xx;
   1303         rotation.rot[0][1] = xy;
   1304         rotation.rot[0][2] = xz;
   1305         rotation.rot[1][0] = yx;
   1306         rotation.rot[1][1] = yy;
   1307         rotation.rot[1][2] = yz;
   1308         rotation.rot[2][0] = zx;
   1309         rotation.rot[2][1] = zy;
   1310         rotation.rot[2][2] = zz;
   1311     }
   1312 
   1313     rotation.status = ASTRO_SUCCESS;
   1314     return rotation;
   1315 }
   1316 
   1317 
   1318 static void precession(double tt1, const double pos1[3], double tt2, double pos2[3])
   1319 {
   1320     astro_rotation_t r = precession_rot(tt1, tt2);
   1321     pos2[0] = r.rot[0][0]*pos1[0] + r.rot[1][0]*pos1[1] + r.rot[2][0]*pos1[2];
   1322     pos2[1] = r.rot[0][1]*pos1[0] + r.rot[1][1]*pos1[1] + r.rot[2][1]*pos1[2];
   1323     pos2[2] = r.rot[0][2]*pos1[0] + r.rot[1][2]*pos1[1] + r.rot[2][2]*pos1[2];
   1324 }
   1325 
   1326 
   1327 static astro_equatorial_t vector2radec(const double pos[3])
   1328 {
   1329     astro_equatorial_t equ;
   1330     double xyproj;
   1331 
   1332     xyproj = pos[0]*pos[0] + pos[1]*pos[1];
   1333     equ.dist = sqrt(xyproj + pos[2]*pos[2]);
   1334     equ.status = ASTRO_SUCCESS;
   1335     if (xyproj == 0.0)
   1336     {
   1337         if (pos[2] == 0.0)
   1338         {
   1339             /* Indeterminate coordinates; pos vector has zero length. */
   1340             equ = EquError(ASTRO_BAD_VECTOR);
   1341         }
   1342         else if (pos[2] < 0)
   1343         {
   1344             equ.ra = 0.0;
   1345             equ.dec = -90.0;
   1346         }
   1347         else
   1348         {
   1349             equ.ra = 0.0;
   1350             equ.dec = +90.0;
   1351         }
   1352     }
   1353     else
   1354     {
   1355         equ.ra = atan2(pos[1], pos[0]) / (DEG2RAD * 15.0);
   1356         if (equ.ra < 0)
   1357             equ.ra += 24.0;
   1358 
   1359         equ.dec = RAD2DEG * atan2(pos[2], sqrt(xyproj));
   1360     }
   1361 
   1362     return equ;
   1363 }
   1364 
   1365 
   1366 static astro_rotation_t nutation_rot(astro_time_t *time, int direction)
   1367 {
   1368     astro_rotation_t rotation;
   1369     earth_tilt_t tilt = e_tilt(time);
   1370     double oblm = tilt.mobl * DEG2RAD;
   1371     double oblt = tilt.tobl * DEG2RAD;
   1372     double psi = tilt.dpsi * ASEC2RAD;
   1373     double cobm = cos(oblm);
   1374     double sobm = sin(oblm);
   1375     double cobt = cos(oblt);
   1376     double sobt = sin(oblt);
   1377     double cpsi = cos(psi);
   1378     double spsi = sin(psi);
   1379 
   1380     double xx = cpsi;
   1381     double yx = -spsi * cobm;
   1382     double zx = -spsi * sobm;
   1383     double xy = spsi * cobt;
   1384     double yy = cpsi * cobm * cobt + sobm * sobt;
   1385     double zy = cpsi * sobm * cobt - cobm * sobt;
   1386     double xz = spsi * sobt;
   1387     double yz = cpsi * cobm * sobt - sobm * cobt;
   1388     double zz = cpsi * sobm * sobt + cobm * cobt;
   1389 
   1390     if (direction == 0)
   1391     {
   1392         /* forward rotation */
   1393         rotation.rot[0][0] = xx;
   1394         rotation.rot[0][1] = xy;
   1395         rotation.rot[0][2] = xz;
   1396         rotation.rot[1][0] = yx;
   1397         rotation.rot[1][1] = yy;
   1398         rotation.rot[1][2] = yz;
   1399         rotation.rot[2][0] = zx;
   1400         rotation.rot[2][1] = zy;
   1401         rotation.rot[2][2] = zz;
   1402     }
   1403     else
   1404     {
   1405         /* inverse rotation */
   1406         rotation.rot[0][0] = xx;
   1407         rotation.rot[0][1] = yx;
   1408         rotation.rot[0][2] = zx;
   1409         rotation.rot[1][0] = xy;
   1410         rotation.rot[1][1] = yy;
   1411         rotation.rot[1][2] = zy;
   1412         rotation.rot[2][0] = xz;
   1413         rotation.rot[2][1] = yz;
   1414         rotation.rot[2][2] = zz;
   1415     }
   1416 
   1417     rotation.status = ASTRO_SUCCESS;
   1418     return rotation;
   1419 }
   1420 
   1421 static void nutation(astro_time_t *time, int direction, const double inpos[3], double outpos[3])
   1422 {
   1423     astro_rotation_t r = nutation_rot(time, direction);
   1424     outpos[0] = r.rot[0][0]*inpos[0] + r.rot[1][0]*inpos[1] + r.rot[2][0]*inpos[2];
   1425     outpos[1] = r.rot[0][1]*inpos[0] + r.rot[1][1]*inpos[1] + r.rot[2][1]*inpos[2];
   1426     outpos[2] = r.rot[0][2]*inpos[0] + r.rot[1][2]*inpos[1] + r.rot[2][2]*inpos[2];
   1427 }
   1428 
   1429 static double era(double ut)        /* Earth Rotation Angle */
   1430 {
   1431     double thet1 = 0.7790572732640 + 0.00273781191135448 * ut;
   1432     double thet3 = fmod(ut, 1.0);
   1433     double theta = 360.0 * fmod(thet1 + thet3, 1.0);
   1434     if (theta < 0.0)
   1435         theta += 360.0;
   1436 
   1437     return theta;
   1438 }
   1439 
   1440 static double sidereal_time(astro_time_t *time)
   1441 {
   1442     double t = time->tt / 36525.0;
   1443     double eqeq = 15.0 * e_tilt(time).ee;    /* Replace with eqeq=0 to get GMST instead of GAST (if we ever need it) */
   1444     double theta = era(time->ut);
   1445     double st = (eqeq + 0.014506 +
   1446         (((( -    0.0000000368   * t
   1447             -    0.000029956  ) * t
   1448             -    0.00000044   ) * t
   1449             +    1.3915817    ) * t
   1450             + 4612.156534     ) * t);
   1451 
   1452     double gst = fmod(st/3600.0 + theta, 360.0) / 15.0;
   1453     if (gst < 0.0)
   1454         gst += 24.0;
   1455 
   1456     return gst;
   1457 }
   1458 
   1459 static void terra(astro_observer_t observer, double st, double pos[3])
   1460 {
   1461     double df2 = EARTH_FLATTENING * EARTH_FLATTENING;
   1462     double phi = observer.latitude * DEG2RAD;
   1463     double sinphi = sin(phi);
   1464     double cosphi = cos(phi);
   1465     double c = 1.0 / sqrt(cosphi*cosphi + df2*sinphi*sinphi);
   1466     double s = df2 * c;
   1467     double ht_km = observer.height / 1000.0;
   1468     double ach = EARTH_EQUATORIAL_RADIUS_KM*c + ht_km;
   1469     double ash = EARTH_EQUATORIAL_RADIUS_KM*s + ht_km;
   1470     double stlocl = (15.0*st + observer.longitude) * DEG2RAD;
   1471     double sinst = sin(stlocl);
   1472     double cosst = cos(stlocl);
   1473 
   1474     pos[0] = ach * cosphi * cosst / KM_PER_AU;
   1475     pos[1] = ach * cosphi * sinst / KM_PER_AU;
   1476     pos[2] = ash * sinphi / KM_PER_AU;
   1477 
   1478 #if 0
   1479     /* If we ever need to calculate the observer's velocity vector, here is how NOVAS C 3.1 does it... */
   1480     static const double ANGVEL = 7.2921150e-5;
   1481     vel[0] = -ANGVEL * ach * cosphi * sinst * 86400.0;
   1482     vel[1] = +ANGVEL * ach * cosphi * cosst * 86400.0;
   1483     vel[2] = 0.0;
   1484 #endif
   1485 }
   1486 
   1487 static void geo_pos(astro_time_t *time, astro_observer_t observer, double outpos[3])
   1488 {
   1489     double gast, pos1[3], pos2[3];
   1490 
   1491     gast = sidereal_time(time);
   1492     terra(observer, gast, pos1);
   1493     nutation(time, -1, pos1, pos2);
   1494     precession(time->tt, pos2, 0.0, outpos);
   1495 }
   1496 
   1497 static void spin(double angle, const double pos1[3], double vec2[3])
   1498 {
   1499     double angr = angle * DEG2RAD;
   1500     double cosang = cos(angr);
   1501     double sinang = sin(angr);
   1502     vec2[0] = +cosang*pos1[0] + sinang*pos1[1];
   1503     vec2[1] = -sinang*pos1[0] + cosang*pos1[1];
   1504     vec2[2] = pos1[2];
   1505 }
   1506 
   1507 /*------------------ CalcMoon ------------------*/
   1508 
   1509 /** @cond DOXYGEN_SKIP */
   1510 
   1511 #define DECLARE_PASCAL_ARRAY_1(elemtype,name,xmin,xmax) \
   1512     elemtype name[(xmax)-(xmin)+1]
   1513 
   1514 #define DECLARE_PASCAL_ARRAY_2(elemtype,name,xmin,xmax,ymin,ymax) \
   1515     elemtype name[(xmax)-(xmin)+1][(ymax)-(ymin)+1]
   1516 
   1517 #define ACCESS_PASCAL_ARRAY_1(name,xmin,x) \
   1518     ((name)[(x)-(xmin)])
   1519 
   1520 #define ACCESS_PASCAL_ARRAY_2(name,xmin,ymin,x,y) \
   1521     ((name)[(x)-(xmin)][(y)-(ymin)])
   1522 
   1523 typedef struct
   1524 {
   1525     double t;
   1526     double dgam;
   1527     double dlam, n, gam1c, sinpi;
   1528     double l0, l, ls, f, d, s;
   1529     double dl0, dl, dls, df, dd, ds;
   1530     DECLARE_PASCAL_ARRAY_2(double,co,-6,6,1,4);   /* ARRAY[-6..6,1..4] OF REAL */
   1531     DECLARE_PASCAL_ARRAY_2(double,si,-6,6,1,4);   /* ARRAY[-6..6,1..4] OF REAL */
   1532 }
   1533 MoonContext;
   1534 
   1535 #define T           (ctx->t)
   1536 #define DGAM        (ctx->dgam)
   1537 #define DLAM        (ctx->dlam)
   1538 #define N           (ctx->n)
   1539 #define GAM1C       (ctx->gam1c)
   1540 #define SINPI       (ctx->sinpi)
   1541 #define L0          (ctx->l0)
   1542 #define L           (ctx->l)
   1543 #define LS          (ctx->ls)
   1544 #define F           (ctx->f)
   1545 #define D           (ctx->d)
   1546 #define S           (ctx->s)
   1547 #define DL0         (ctx->dl0)
   1548 #define DL          (ctx->dl)
   1549 #define DLS         (ctx->dls)
   1550 #define DF          (ctx->df)
   1551 #define DD          (ctx->dd)
   1552 #define DS          (ctx->ds)
   1553 #define CO(x,y)     ACCESS_PASCAL_ARRAY_2(ctx->co,-6,1,x,y)
   1554 #define SI(x,y)     ACCESS_PASCAL_ARRAY_2(ctx->si,-6,1,x,y)
   1555 
   1556 static double Frac(double x)
   1557 {
   1558     return x - floor(x);
   1559 }
   1560 
   1561 static void AddThe(
   1562     double c1, double s1, double c2, double s2,
   1563     double *c, double *s)
   1564 {
   1565     *c = c1*c2 - s1*s2;
   1566     *s = s1*c2 + c1*s2;
   1567 }
   1568 
   1569 static double Sine(double phi)
   1570 {
   1571     /* sine, of phi in revolutions, not radians */
   1572     return sin(PI2 * phi);
   1573 }
   1574 
   1575 static void LongPeriodic(MoonContext *ctx)
   1576 {
   1577     double S1 = Sine(0.19833+0.05611*T);
   1578     double S2 = Sine(0.27869+0.04508*T);
   1579     double S3 = Sine(0.16827-0.36903*T);
   1580     double S4 = Sine(0.34734-5.37261*T);
   1581     double S5 = Sine(0.10498-5.37899*T);
   1582     double S6 = Sine(0.42681-0.41855*T);
   1583     double S7 = Sine(0.14943-5.37511*T);
   1584 
   1585     DL0 = 0.84*S1+0.31*S2+14.27*S3+ 7.26*S4+ 0.28*S5+0.24*S6;
   1586     DL  = 2.94*S1+0.31*S2+14.27*S3+ 9.34*S4+ 1.12*S5+0.83*S6;
   1587     DLS =-6.40*S1                                   -1.89*S6;
   1588     DF  = 0.21*S1+0.31*S2+14.27*S3-88.70*S4-15.30*S5+0.24*S6-1.86*S7;
   1589     DD  = DL0-DLS;
   1590     DGAM  = -3332E-9 * Sine(0.59734-5.37261*T)
   1591              -539E-9 * Sine(0.35498-5.37899*T)
   1592               -64E-9 * Sine(0.39943-5.37511*T);
   1593 }
   1594 
   1595 static void Init(MoonContext *ctx)
   1596 {
   1597     int I, J, MAX;
   1598     double T2, ARG, FAC;
   1599 
   1600     T2 = T*T;
   1601     DLAM = 0;
   1602     DS = 0;
   1603     GAM1C = 0;
   1604     SINPI = 3422.7000;
   1605     LongPeriodic(ctx);
   1606     L0 = PI2*Frac(0.60643382+1336.85522467*T-0.00000313*T2) + DL0/ARC;
   1607     L  = PI2*Frac(0.37489701+1325.55240982*T+0.00002565*T2) + DL /ARC;
   1608     LS = PI2*Frac(0.99312619+  99.99735956*T-0.00000044*T2) + DLS/ARC;
   1609     F  = PI2*Frac(0.25909118+1342.22782980*T-0.00000892*T2) + DF /ARC;
   1610     D  = PI2*Frac(0.82736186+1236.85308708*T-0.00000397*T2) + DD /ARC;
   1611     for (I=1; I<=4; ++I)
   1612     {
   1613         switch(I)
   1614         {
   1615             case 1:  ARG=L;  MAX=4; FAC=1.000002208;               break;
   1616             case 2:  ARG=LS; MAX=3; FAC=0.997504612-0.002495388*T; break;
   1617             case 3:  ARG=F;  MAX=4; FAC=1.000002708+139.978*DGAM;  break;
   1618             default: ARG=D;  MAX=6; FAC=1.0;                       break;
   1619         }
   1620         CO(0,I) = 1.0;
   1621         CO(1,I) = cos(ARG)*FAC;
   1622         SI(0,I) = 0.0;
   1623         SI(1,I) = sin(ARG)*FAC;
   1624         for (J=2; J<=MAX; ++J)
   1625             AddThe(CO(J-1,I), SI(J-1,I), CO(1,I), SI(1,I), &CO(J,I), &SI(J,I));
   1626 
   1627         for (J=1; J<=MAX; ++J)
   1628         {
   1629             CO(-J,I) =  CO(J,I);
   1630             SI(-J,I) = -SI(J,I);
   1631         }
   1632     }
   1633 }
   1634 
   1635 static void Term(MoonContext *ctx, int p, int q, int r, int s, double *x, double *y)
   1636 {
   1637     int k;
   1638     DECLARE_PASCAL_ARRAY_1(int, i, 1, 4);
   1639     #define I(n) ACCESS_PASCAL_ARRAY_1(i,1,n)
   1640 
   1641     I(1) = p;
   1642     I(2) = q;
   1643     I(3) = r;
   1644     I(4) = s;
   1645     *x = 1.0;
   1646     *y = 0.0;
   1647 
   1648     for (k=1; k<=4; ++k)
   1649         if (I(k) != 0.0)
   1650             AddThe(*x, *y, CO(I(k), k), SI(I(k), k), x, y);
   1651 
   1652     #undef I
   1653 }
   1654 
   1655 static void AddSol(
   1656     MoonContext *ctx,
   1657     double coeffl,
   1658     double coeffs,
   1659     double coeffg,
   1660     double coeffp,
   1661     int p,
   1662     int q,
   1663     int r,
   1664     int s)
   1665 {
   1666     double x, y;
   1667     Term(ctx, p, q, r, s, &x, &y);
   1668     DLAM += coeffl*y;
   1669     DS += coeffs*y;
   1670     GAM1C += coeffg*x;
   1671     SINPI += coeffp*x;
   1672 }
   1673 
   1674 #define ADDN(coeffn,p,q,r,s)    ( Term(ctx, (p),(q),(r),(s),&x,&y), (N += (coeffn)*y) )
   1675 
   1676 static void SolarN(MoonContext *ctx)
   1677 {
   1678     double x, y;
   1679 
   1680     N = 0.0;
   1681     ADDN(-526.069, 0, 0,1,-2);
   1682     ADDN(  -3.352, 0, 0,1,-4);
   1683     ADDN( +44.297,+1, 0,1,-2);
   1684     ADDN(  -6.000,+1, 0,1,-4);
   1685     ADDN( +20.599,-1, 0,1, 0);
   1686     ADDN( -30.598,-1, 0,1,-2);
   1687     ADDN( -24.649,-2, 0,1, 0);
   1688     ADDN(  -2.000,-2, 0,1,-2);
   1689     ADDN( -22.571, 0,+1,1,-2);
   1690     ADDN( +10.985, 0,-1,1,-2);
   1691 }
   1692 
   1693 static void Planetary(MoonContext *ctx)
   1694 {
   1695     DLAM +=
   1696         +0.82*Sine(0.7736  -62.5512*T)+0.31*Sine(0.0466 -125.1025*T)
   1697         +0.35*Sine(0.5785  -25.1042*T)+0.66*Sine(0.4591+1335.8075*T)
   1698         +0.64*Sine(0.3130  -91.5680*T)+1.14*Sine(0.1480+1331.2898*T)
   1699         +0.21*Sine(0.5918+1056.5859*T)+0.44*Sine(0.5784+1322.8595*T)
   1700         +0.24*Sine(0.2275   -5.7374*T)+0.28*Sine(0.2965   +2.6929*T)
   1701         +0.33*Sine(0.3132   +6.3368*T);
   1702 }
   1703 
   1704 int _CalcMoonCount;     /* Undocumented global for performance tuning. */
   1705 
   1706 static void CalcMoon(
   1707     double centuries_since_j2000,
   1708     double *geo_eclip_lon,      /* (LAMBDA) equinox of date */
   1709     double *geo_eclip_lat,      /* (BETA)   equinox of date */
   1710     double *distance_au)        /* (R) */
   1711 {
   1712     double lat_seconds;
   1713     MoonContext context;
   1714     MoonContext *ctx = &context;    /* goofy, but makes macros work inside this function */
   1715 
   1716     context.t = centuries_since_j2000;
   1717     Init(ctx);
   1718 
   1719     AddSol(ctx,    13.9020,    14.0600,    -0.0010,     0.2607, 0, 0, 0, 4);
   1720     AddSol(ctx,     0.4030,    -4.0100,     0.3940,     0.0023, 0, 0, 0, 3);
   1721     AddSol(ctx,  2369.9120,  2373.3600,     0.6010,    28.2333, 0, 0, 0, 2);
   1722     AddSol(ctx,  -125.1540,  -112.7900,    -0.7250,    -0.9781, 0, 0, 0, 1);
   1723     AddSol(ctx,     1.9790,     6.9800,    -0.4450,     0.0433, 1, 0, 0, 4);
   1724     AddSol(ctx,   191.9530,   192.7200,     0.0290,     3.0861, 1, 0, 0, 2);
   1725     AddSol(ctx,    -8.4660,   -13.5100,     0.4550,    -0.1093, 1, 0, 0, 1);
   1726     AddSol(ctx, 22639.5000, 22609.0700,     0.0790,   186.5398, 1, 0, 0, 0);
   1727     AddSol(ctx,    18.6090,     3.5900,    -0.0940,     0.0118, 1, 0, 0,-1);
   1728     AddSol(ctx, -4586.4650, -4578.1300,    -0.0770,    34.3117, 1, 0, 0,-2);
   1729     AddSol(ctx,     3.2150,     5.4400,     0.1920,    -0.0386, 1, 0, 0,-3);
   1730     AddSol(ctx,   -38.4280,   -38.6400,     0.0010,     0.6008, 1, 0, 0,-4);
   1731     AddSol(ctx,    -0.3930,    -1.4300,    -0.0920,     0.0086, 1, 0, 0,-6);
   1732     AddSol(ctx,    -0.2890,    -1.5900,     0.1230,    -0.0053, 0, 1, 0, 4);
   1733     AddSol(ctx,   -24.4200,   -25.1000,     0.0400,    -0.3000, 0, 1, 0, 2);
   1734     AddSol(ctx,    18.0230,    17.9300,     0.0070,     0.1494, 0, 1, 0, 1);
   1735     AddSol(ctx,  -668.1460,  -126.9800,    -1.3020,    -0.3997, 0, 1, 0, 0);
   1736     AddSol(ctx,     0.5600,     0.3200,    -0.0010,    -0.0037, 0, 1, 0,-1);
   1737     AddSol(ctx,  -165.1450,  -165.0600,     0.0540,     1.9178, 0, 1, 0,-2);
   1738     AddSol(ctx,    -1.8770,    -6.4600,    -0.4160,     0.0339, 0, 1, 0,-4);
   1739     AddSol(ctx,     0.2130,     1.0200,    -0.0740,     0.0054, 2, 0, 0, 4);
   1740     AddSol(ctx,    14.3870,    14.7800,    -0.0170,     0.2833, 2, 0, 0, 2);
   1741     AddSol(ctx,    -0.5860,    -1.2000,     0.0540,    -0.0100, 2, 0, 0, 1);
   1742     AddSol(ctx,   769.0160,   767.9600,     0.1070,    10.1657, 2, 0, 0, 0);
   1743     AddSol(ctx,     1.7500,     2.0100,    -0.0180,     0.0155, 2, 0, 0,-1);
   1744     AddSol(ctx,  -211.6560,  -152.5300,     5.6790,    -0.3039, 2, 0, 0,-2);
   1745     AddSol(ctx,     1.2250,     0.9100,    -0.0300,    -0.0088, 2, 0, 0,-3);
   1746     AddSol(ctx,   -30.7730,   -34.0700,    -0.3080,     0.3722, 2, 0, 0,-4);
   1747     AddSol(ctx,    -0.5700,    -1.4000,    -0.0740,     0.0109, 2, 0, 0,-6);
   1748     AddSol(ctx,    -2.9210,   -11.7500,     0.7870,    -0.0484, 1, 1, 0, 2);
   1749     AddSol(ctx,     1.2670,     1.5200,    -0.0220,     0.0164, 1, 1, 0, 1);
   1750     AddSol(ctx,  -109.6730,  -115.1800,     0.4610,    -0.9490, 1, 1, 0, 0);
   1751     AddSol(ctx,  -205.9620,  -182.3600,     2.0560,     1.4437, 1, 1, 0,-2);
   1752     AddSol(ctx,     0.2330,     0.3600,     0.0120,    -0.0025, 1, 1, 0,-3);
   1753     AddSol(ctx,    -4.3910,    -9.6600,    -0.4710,     0.0673, 1, 1, 0,-4);
   1754     AddSol(ctx,     0.2830,     1.5300,    -0.1110,     0.0060, 1,-1, 0, 4);
   1755     AddSol(ctx,    14.5770,    31.7000,    -1.5400,     0.2302, 1,-1, 0, 2);
   1756     AddSol(ctx,   147.6870,   138.7600,     0.6790,     1.1528, 1,-1, 0, 0);
   1757     AddSol(ctx,    -1.0890,     0.5500,     0.0210,     0.0000, 1,-1, 0,-1);
   1758     AddSol(ctx,    28.4750,    23.5900,    -0.4430,    -0.2257, 1,-1, 0,-2);
   1759     AddSol(ctx,    -0.2760,    -0.3800,    -0.0060,    -0.0036, 1,-1, 0,-3);
   1760     AddSol(ctx,     0.6360,     2.2700,     0.1460,    -0.0102, 1,-1, 0,-4);
   1761     AddSol(ctx,    -0.1890,    -1.6800,     0.1310,    -0.0028, 0, 2, 0, 2);
   1762     AddSol(ctx,    -7.4860,    -0.6600,    -0.0370,    -0.0086, 0, 2, 0, 0);
   1763     AddSol(ctx,    -8.0960,   -16.3500,    -0.7400,     0.0918, 0, 2, 0,-2);
   1764     AddSol(ctx,    -5.7410,    -0.0400,     0.0000,    -0.0009, 0, 0, 2, 2);
   1765     AddSol(ctx,     0.2550,     0.0000,     0.0000,     0.0000, 0, 0, 2, 1);
   1766     AddSol(ctx,  -411.6080,    -0.2000,     0.0000,    -0.0124, 0, 0, 2, 0);
   1767     AddSol(ctx,     0.5840,     0.8400,     0.0000,     0.0071, 0, 0, 2,-1);
   1768     AddSol(ctx,   -55.1730,   -52.1400,     0.0000,    -0.1052, 0, 0, 2,-2);
   1769     AddSol(ctx,     0.2540,     0.2500,     0.0000,    -0.0017, 0, 0, 2,-3);
   1770     AddSol(ctx,     0.0250,    -1.6700,     0.0000,     0.0031, 0, 0, 2,-4);
   1771     AddSol(ctx,     1.0600,     2.9600,    -0.1660,     0.0243, 3, 0, 0, 2);
   1772     AddSol(ctx,    36.1240,    50.6400,    -1.3000,     0.6215, 3, 0, 0, 0);
   1773     AddSol(ctx,   -13.1930,   -16.4000,     0.2580,    -0.1187, 3, 0, 0,-2);
   1774     AddSol(ctx,    -1.1870,    -0.7400,     0.0420,     0.0074, 3, 0, 0,-4);
   1775     AddSol(ctx,    -0.2930,    -0.3100,    -0.0020,     0.0046, 3, 0, 0,-6);
   1776     AddSol(ctx,    -0.2900,    -1.4500,     0.1160,    -0.0051, 2, 1, 0, 2);
   1777     AddSol(ctx,    -7.6490,   -10.5600,     0.2590,    -0.1038, 2, 1, 0, 0);
   1778     AddSol(ctx,    -8.6270,    -7.5900,     0.0780,    -0.0192, 2, 1, 0,-2);
   1779     AddSol(ctx,    -2.7400,    -2.5400,     0.0220,     0.0324, 2, 1, 0,-4);
   1780     AddSol(ctx,     1.1810,     3.3200,    -0.2120,     0.0213, 2,-1, 0, 2);
   1781     AddSol(ctx,     9.7030,    11.6700,    -0.1510,     0.1268, 2,-1, 0, 0);
   1782     AddSol(ctx,    -0.3520,    -0.3700,     0.0010,    -0.0028, 2,-1, 0,-1);
   1783     AddSol(ctx,    -2.4940,    -1.1700,    -0.0030,    -0.0017, 2,-1, 0,-2);
   1784     AddSol(ctx,     0.3600,     0.2000,    -0.0120,    -0.0043, 2,-1, 0,-4);
   1785     AddSol(ctx,    -1.1670,    -1.2500,     0.0080,    -0.0106, 1, 2, 0, 0);
   1786     AddSol(ctx,    -7.4120,    -6.1200,     0.1170,     0.0484, 1, 2, 0,-2);
   1787     AddSol(ctx,    -0.3110,    -0.6500,    -0.0320,     0.0044, 1, 2, 0,-4);
   1788     AddSol(ctx,     0.7570,     1.8200,    -0.1050,     0.0112, 1,-2, 0, 2);
   1789     AddSol(ctx,     2.5800,     2.3200,     0.0270,     0.0196, 1,-2, 0, 0);
   1790     AddSol(ctx,     2.5330,     2.4000,    -0.0140,    -0.0212, 1,-2, 0,-2);
   1791     AddSol(ctx,    -0.3440,    -0.5700,    -0.0250,     0.0036, 0, 3, 0,-2);
   1792     AddSol(ctx,    -0.9920,    -0.0200,     0.0000,     0.0000, 1, 0, 2, 2);
   1793     AddSol(ctx,   -45.0990,    -0.0200,     0.0000,    -0.0010, 1, 0, 2, 0);
   1794     AddSol(ctx,    -0.1790,    -9.5200,     0.0000,    -0.0833, 1, 0, 2,-2);
   1795     AddSol(ctx,    -0.3010,    -0.3300,     0.0000,     0.0014, 1, 0, 2,-4);
   1796     AddSol(ctx,    -6.3820,    -3.3700,     0.0000,    -0.0481, 1, 0,-2, 2);
   1797     AddSol(ctx,    39.5280,    85.1300,     0.0000,    -0.7136, 1, 0,-2, 0);
   1798     AddSol(ctx,     9.3660,     0.7100,     0.0000,    -0.0112, 1, 0,-2,-2);
   1799     AddSol(ctx,     0.2020,     0.0200,     0.0000,     0.0000, 1, 0,-2,-4);
   1800     AddSol(ctx,     0.4150,     0.1000,     0.0000,     0.0013, 0, 1, 2, 0);
   1801     AddSol(ctx,    -2.1520,    -2.2600,     0.0000,    -0.0066, 0, 1, 2,-2);
   1802     AddSol(ctx,    -1.4400,    -1.3000,     0.0000,     0.0014, 0, 1,-2, 2);
   1803     AddSol(ctx,     0.3840,    -0.0400,     0.0000,     0.0000, 0, 1,-2,-2);
   1804     AddSol(ctx,     1.9380,     3.6000,    -0.1450,     0.0401, 4, 0, 0, 0);
   1805     AddSol(ctx,    -0.9520,    -1.5800,     0.0520,    -0.0130, 4, 0, 0,-2);
   1806     AddSol(ctx,    -0.5510,    -0.9400,     0.0320,    -0.0097, 3, 1, 0, 0);
   1807     AddSol(ctx,    -0.4820,    -0.5700,     0.0050,    -0.0045, 3, 1, 0,-2);
   1808     AddSol(ctx,     0.6810,     0.9600,    -0.0260,     0.0115, 3,-1, 0, 0);
   1809     AddSol(ctx,    -0.2970,    -0.2700,     0.0020,    -0.0009, 2, 2, 0,-2);
   1810     AddSol(ctx,     0.2540,     0.2100,    -0.0030,     0.0000, 2,-2, 0,-2);
   1811     AddSol(ctx,    -0.2500,    -0.2200,     0.0040,     0.0014, 1, 3, 0,-2);
   1812     AddSol(ctx,    -3.9960,     0.0000,     0.0000,     0.0004, 2, 0, 2, 0);
   1813     AddSol(ctx,     0.5570,    -0.7500,     0.0000,    -0.0090, 2, 0, 2,-2);
   1814     AddSol(ctx,    -0.4590,    -0.3800,     0.0000,    -0.0053, 2, 0,-2, 2);
   1815     AddSol(ctx,    -1.2980,     0.7400,     0.0000,     0.0004, 2, 0,-2, 0);
   1816     AddSol(ctx,     0.5380,     1.1400,     0.0000,    -0.0141, 2, 0,-2,-2);
   1817     AddSol(ctx,     0.2630,     0.0200,     0.0000,     0.0000, 1, 1, 2, 0);
   1818     AddSol(ctx,     0.4260,     0.0700,     0.0000,    -0.0006, 1, 1,-2,-2);
   1819     AddSol(ctx,    -0.3040,     0.0300,     0.0000,     0.0003, 1,-1, 2, 0);
   1820     AddSol(ctx,    -0.3720,    -0.1900,     0.0000,    -0.0027, 1,-1,-2, 2);
   1821     AddSol(ctx,     0.4180,     0.0000,     0.0000,     0.0000, 0, 0, 4, 0);
   1822     AddSol(ctx,    -0.3300,    -0.0400,     0.0000,     0.0000, 3, 0, 2, 0);
   1823 
   1824     SolarN(ctx);
   1825     Planetary(ctx);
   1826     S = F + DS/ARC;
   1827 
   1828     lat_seconds = (1.000002708 + 139.978*DGAM)*(18518.511+1.189+GAM1C)*sin(S)-6.24*sin(3*S) + N;
   1829 
   1830     *geo_eclip_lon = PI2 * Frac((L0+DLAM/ARC) / PI2);
   1831     *geo_eclip_lat = lat_seconds * (DEG2RAD / 3600.0);
   1832     *distance_au = (ARC * EARTH_EQUATORIAL_RADIUS_AU) / (0.999953253 * SINPI);
   1833     ++_CalcMoonCount;
   1834 }
   1835 
   1836 #undef T
   1837 #undef DGAM
   1838 #undef DLAM
   1839 #undef N
   1840 #undef GAM1C
   1841 #undef SINPI
   1842 #undef L0
   1843 #undef L
   1844 #undef LS
   1845 #undef F
   1846 #undef D
   1847 #undef S
   1848 #undef DL0
   1849 #undef DL
   1850 #undef DLS
   1851 #undef DF
   1852 #undef DD
   1853 #undef DS
   1854 #undef CO
   1855 #undef SI
   1856 
   1857 /** @endcond */
   1858 
   1859 /**
   1860  * @brief Calculates the geocentric position of the Moon at a given time.
   1861  *
   1862  * Given a time of observation, calculates the Moon's position as a vector.
   1863  * The vector gives the location of the Moon's center relative to the Earth's center
   1864  * with x-, y-, and z-components measured in astronomical units.
   1865  *
   1866  * This algorithm is based on Nautical Almanac Office's *Improved Lunar Ephemeris* of 1954,
   1867  * which in turn derives from E. W. Brown's lunar theories from the early twentieth century.
   1868  * It is adapted from Turbo Pascal code from the book
   1869  * [Astronomy on the Personal Computer](https://www.springer.com/us/book/9783540672210)
   1870  * by Montenbruck and Pfleger.
   1871  *
   1872  * @param time  The date and time for which to calculate the Moon's position.
   1873  * @return The Moon's position as a vector in J2000 Cartesian equatorial coordinates.
   1874  */
   1875 astro_vector_t Astronomy_GeoMoon(astro_time_t time)
   1876 {
   1877     double geo_eclip_lon, geo_eclip_lat, distance_au;
   1878     double dist_cos_lat;
   1879     astro_vector_t vector;
   1880     double gepos[3];
   1881     double mpos1[3];
   1882     double mpos2[3];
   1883 
   1884     CalcMoon(time.tt / 36525.0, &geo_eclip_lon, &geo_eclip_lat, &distance_au);
   1885 
   1886     /* Convert geocentric ecliptic spherical coordinates to Cartesian coordinates. */
   1887     dist_cos_lat = distance_au * cos(geo_eclip_lat);
   1888     gepos[0] = dist_cos_lat * cos(geo_eclip_lon);
   1889     gepos[1] = dist_cos_lat * sin(geo_eclip_lon);
   1890     gepos[2] = distance_au * sin(geo_eclip_lat);
   1891 
   1892     /* Convert ecliptic coordinates to equatorial coordinates, both in mean equinox of date. */
   1893     ecl2equ_vec(time, gepos, mpos1);
   1894 
   1895     /* Convert from mean equinox of date to J2000. */
   1896     precession(time.tt, mpos1, 0, mpos2);
   1897 
   1898     vector.status = ASTRO_SUCCESS;
   1899     vector.x = mpos2[0];
   1900     vector.y = mpos2[1];
   1901     vector.z = mpos2[2];
   1902     vector.t = time;
   1903     return vector;
   1904 }
   1905 
   1906 /*------------------ VSOP ------------------*/
   1907 
   1908 /** @cond DOXYGEN_SKIP */
   1909 typedef struct
   1910 {
   1911     double amplitude;
   1912     double phase;
   1913     double frequency;
   1914 }
   1915 vsop_term_t;
   1916 
   1917 typedef struct
   1918 {
   1919     int nterms;
   1920     const vsop_term_t *term;
   1921 }
   1922 vsop_series_t;
   1923 
   1924 typedef struct
   1925 {
   1926     int nseries;
   1927     const vsop_series_t *series;
   1928 }
   1929 vsop_formula_t;
   1930 
   1931 typedef struct
   1932 {
   1933     const vsop_formula_t formula[3];
   1934 }
   1935 vsop_model_t;
   1936 /** @endcond */
   1937 
   1938 static const vsop_term_t vsop_lon_Mercury_0[] =
   1939 {
   1940     { 4.40250710144, 0.00000000000, 0.00000000000 },
   1941     { 0.40989414977, 1.48302034195, 26087.90314157420 },
   1942     { 0.05046294200, 4.47785489551, 52175.80628314840 },
   1943     { 0.00855346844, 1.16520322459, 78263.70942472259 },
   1944     { 0.00165590362, 4.11969163423, 104351.61256629678 },
   1945     { 0.00034561897, 0.77930768443, 130439.51570787099 },
   1946     { 0.00007583476, 3.71348404924, 156527.41884944518 }
   1947 };
   1948 
   1949 static const vsop_term_t vsop_lon_Mercury_1[] =
   1950 {
   1951     { 26087.90313685529, 0.00000000000, 0.00000000000 },
   1952     { 0.01131199811, 6.21874197797, 26087.90314157420 },
   1953     { 0.00292242298, 3.04449355541, 52175.80628314840 },
   1954     { 0.00075775081, 6.08568821653, 78263.70942472259 },
   1955     { 0.00019676525, 2.80965111777, 104351.61256629678 }
   1956 };
   1957 
   1958 static const vsop_series_t vsop_lon_Mercury[] =
   1959 {
   1960     { 7, vsop_lon_Mercury_0 },
   1961     { 5, vsop_lon_Mercury_1 }
   1962 };
   1963 
   1964 static const vsop_term_t vsop_lat_Mercury_0[] =
   1965 {
   1966     { 0.11737528961, 1.98357498767, 26087.90314157420 },
   1967     { 0.02388076996, 5.03738959686, 52175.80628314840 },
   1968     { 0.01222839532, 3.14159265359, 0.00000000000 },
   1969     { 0.00543251810, 1.79644363964, 78263.70942472259 },
   1970     { 0.00129778770, 4.83232503958, 104351.61256629678 },
   1971     { 0.00031866927, 1.58088495658, 130439.51570787099 },
   1972     { 0.00007963301, 4.60972126127, 156527.41884944518 }
   1973 };
   1974 
   1975 static const vsop_term_t vsop_lat_Mercury_1[] =
   1976 {
   1977     { 0.00274646065, 3.95008450011, 26087.90314157420 },
   1978     { 0.00099737713, 3.14159265359, 0.00000000000 }
   1979 };
   1980 
   1981 static const vsop_series_t vsop_lat_Mercury[] =
   1982 {
   1983     { 7, vsop_lat_Mercury_0 },
   1984     { 2, vsop_lat_Mercury_1 }
   1985 };
   1986 
   1987 static const vsop_term_t vsop_rad_Mercury_0[] =
   1988 {
   1989     { 0.39528271651, 0.00000000000, 0.00000000000 },
   1990     { 0.07834131818, 6.19233722598, 26087.90314157420 },
   1991     { 0.00795525558, 2.95989690104, 52175.80628314840 },
   1992     { 0.00121281764, 6.01064153797, 78263.70942472259 },
   1993     { 0.00021921969, 2.77820093972, 104351.61256629678 },
   1994     { 0.00004354065, 5.82894543774, 130439.51570787099 }
   1995 };
   1996 
   1997 static const vsop_term_t vsop_rad_Mercury_1[] =
   1998 {
   1999     { 0.00217347740, 4.65617158665, 26087.90314157420 },
   2000     { 0.00044141826, 1.42385544001, 52175.80628314840 }
   2001 };
   2002 
   2003 static const vsop_series_t vsop_rad_Mercury[] =
   2004 {
   2005     { 6, vsop_rad_Mercury_0 },
   2006     { 2, vsop_rad_Mercury_1 }
   2007 };
   2008 
   2009 ;
   2010 static const vsop_term_t vsop_lon_Venus_0[] =
   2011 {
   2012     { 3.17614666774, 0.00000000000, 0.00000000000 },
   2013     { 0.01353968419, 5.59313319619, 10213.28554621100 },
   2014     { 0.00089891645, 5.30650047764, 20426.57109242200 },
   2015     { 0.00005477194, 4.41630661466, 7860.41939243920 },
   2016     { 0.00003455741, 2.69964447820, 11790.62908865880 },
   2017     { 0.00002372061, 2.99377542079, 3930.20969621960 },
   2018     { 0.00001317168, 5.18668228402, 26.29831979980 },
   2019     { 0.00001664146, 4.25018630147, 1577.34354244780 },
   2020     { 0.00001438387, 4.15745084182, 9683.59458111640 },
   2021     { 0.00001200521, 6.15357116043, 30639.85663863300 }
   2022 };
   2023 
   2024 static const vsop_term_t vsop_lon_Venus_1[] =
   2025 {
   2026     { 10213.28554621638, 0.00000000000, 0.00000000000 },
   2027     { 0.00095617813, 2.46406511110, 10213.28554621100 },
   2028     { 0.00007787201, 0.62478482220, 20426.57109242200 }
   2029 };
   2030 
   2031 static const vsop_series_t vsop_lon_Venus[] =
   2032 {
   2033     { 10, vsop_lon_Venus_0 },
   2034     { 3, vsop_lon_Venus_1 }
   2035 };
   2036 
   2037 static const vsop_term_t vsop_lat_Venus_0[] =
   2038 {
   2039     { 0.05923638472, 0.26702775812, 10213.28554621100 },
   2040     { 0.00040107978, 1.14737178112, 20426.57109242200 },
   2041     { 0.00032814918, 3.14159265359, 0.00000000000 }
   2042 };
   2043 
   2044 static const vsop_term_t vsop_lat_Venus_1[] =
   2045 {
   2046     { 0.00287821243, 1.88964962838, 10213.28554621100 }
   2047 };
   2048 
   2049 static const vsop_series_t vsop_lat_Venus[] =
   2050 {
   2051     { 3, vsop_lat_Venus_0 },
   2052     { 1, vsop_lat_Venus_1 }
   2053 };
   2054 
   2055 static const vsop_term_t vsop_rad_Venus_0[] =
   2056 {
   2057     { 0.72334820891, 0.00000000000, 0.00000000000 },
   2058     { 0.00489824182, 4.02151831717, 10213.28554621100 },
   2059     { 0.00001658058, 4.90206728031, 20426.57109242200 },
   2060     { 0.00001378043, 1.12846591367, 11790.62908865880 },
   2061     { 0.00001632096, 2.84548795207, 7860.41939243920 },
   2062     { 0.00000498395, 2.58682193892, 9683.59458111640 },
   2063     { 0.00000221985, 2.01346696541, 19367.18916223280 },
   2064     { 0.00000237454, 2.55136053886, 15720.83878487840 }
   2065 };
   2066 
   2067 static const vsop_term_t vsop_rad_Venus_1[] =
   2068 {
   2069     { 0.00034551041, 0.89198706276, 10213.28554621100 }
   2070 };
   2071 
   2072 static const vsop_series_t vsop_rad_Venus[] =
   2073 {
   2074     { 8, vsop_rad_Venus_0 },
   2075     { 1, vsop_rad_Venus_1 }
   2076 };
   2077 
   2078 ;
   2079 static const vsop_term_t vsop_lon_Earth_0[] =
   2080 {
   2081     { 1.75347045673, 0.00000000000, 0.00000000000 },
   2082     { 0.03341656453, 4.66925680415, 6283.07584999140 },
   2083     { 0.00034894275, 4.62610242189, 12566.15169998280 },
   2084     { 0.00003417572, 2.82886579754, 3.52311834900 },
   2085     { 0.00003497056, 2.74411783405, 5753.38488489680 },
   2086     { 0.00003135899, 3.62767041756, 77713.77146812050 },
   2087     { 0.00002676218, 4.41808345438, 7860.41939243920 },
   2088     { 0.00002342691, 6.13516214446, 3930.20969621960 },
   2089     { 0.00001273165, 2.03709657878, 529.69096509460 },
   2090     { 0.00001324294, 0.74246341673, 11506.76976979360 },
   2091     { 0.00000901854, 2.04505446477, 26.29831979980 },
   2092     { 0.00001199167, 1.10962946234, 1577.34354244780 },
   2093     { 0.00000857223, 3.50849152283, 398.14900340820 },
   2094     { 0.00000779786, 1.17882681962, 5223.69391980220 },
   2095     { 0.00000990250, 5.23268072088, 5884.92684658320 },
   2096     { 0.00000753141, 2.53339052847, 5507.55323866740 },
   2097     { 0.00000505267, 4.58292599973, 18849.22754997420 },
   2098     { 0.00000492392, 4.20505711826, 775.52261132400 },
   2099     { 0.00000356672, 2.91954114478, 0.06731030280 },
   2100     { 0.00000284125, 1.89869240932, 796.29800681640 },
   2101     { 0.00000242879, 0.34481445893, 5486.77784317500 },
   2102     { 0.00000317087, 5.84901948512, 11790.62908865880 },
   2103     { 0.00000271112, 0.31486255375, 10977.07880469900 },
   2104     { 0.00000206217, 4.80646631478, 2544.31441988340 },
   2105     { 0.00000205478, 1.86953770281, 5573.14280143310 },
   2106     { 0.00000202318, 2.45767790232, 6069.77675455340 },
   2107     { 0.00000126225, 1.08295459501, 20.77539549240 },
   2108     { 0.00000155516, 0.83306084617, 213.29909543800 }
   2109 };
   2110 
   2111 static const vsop_term_t vsop_lon_Earth_1[] =
   2112 {
   2113     { 6283.07584999140, 0.00000000000, 0.00000000000 },
   2114     { 0.00206058863, 2.67823455808, 6283.07584999140 },
   2115     { 0.00004303419, 2.63512233481, 12566.15169998280 }
   2116 };
   2117 
   2118 static const vsop_term_t vsop_lon_Earth_2[] =
   2119 {
   2120     { 0.00008721859, 1.07253635559, 6283.07584999140 }
   2121 };
   2122 
   2123 static const vsop_series_t vsop_lon_Earth[] =
   2124 {
   2125     { 28, vsop_lon_Earth_0 },
   2126     { 3, vsop_lon_Earth_1 },
   2127     { 1, vsop_lon_Earth_2 }
   2128 };
   2129 
   2130 static const vsop_term_t vsop_lat_Earth_1[] =
   2131 {
   2132     { 0.00227777722, 3.41376620530, 6283.07584999140 },
   2133     { 0.00003805678, 3.37063423795, 12566.15169998280 }
   2134 };
   2135 
   2136 static const vsop_series_t vsop_lat_Earth[] =
   2137 {
   2138     { 0, NULL },
   2139     { 2, vsop_lat_Earth_1 }
   2140 };
   2141 
   2142 static const vsop_term_t vsop_rad_Earth_0[] =
   2143 {
   2144     { 1.00013988784, 0.00000000000, 0.00000000000 },
   2145     { 0.01670699632, 3.09846350258, 6283.07584999140 },
   2146     { 0.00013956024, 3.05524609456, 12566.15169998280 },
   2147     { 0.00003083720, 5.19846674381, 77713.77146812050 },
   2148     { 0.00001628463, 1.17387558054, 5753.38488489680 },
   2149     { 0.00001575572, 2.84685214877, 7860.41939243920 },
   2150     { 0.00000924799, 5.45292236722, 11506.76976979360 },
   2151     { 0.00000542439, 4.56409151453, 3930.20969621960 },
   2152     { 0.00000472110, 3.66100022149, 5884.92684658320 },
   2153     { 0.00000085831, 1.27079125277, 161000.68573767410 },
   2154     { 0.00000057056, 2.01374292245, 83996.84731811189 },
   2155     { 0.00000055736, 5.24159799170, 71430.69561812909 },
   2156     { 0.00000174844, 3.01193636733, 18849.22754997420 },
   2157     { 0.00000243181, 4.27349530790, 11790.62908865880 }
   2158 };
   2159 
   2160 static const vsop_term_t vsop_rad_Earth_1[] =
   2161 {
   2162     { 0.00103018607, 1.10748968172, 6283.07584999140 },
   2163     { 0.00001721238, 1.06442300386, 12566.15169998280 }
   2164 };
   2165 
   2166 static const vsop_term_t vsop_rad_Earth_2[] =
   2167 {
   2168     { 0.00004359385, 5.78455133808, 6283.07584999140 }
   2169 };
   2170 
   2171 static const vsop_series_t vsop_rad_Earth[] =
   2172 {
   2173     { 14, vsop_rad_Earth_0 },
   2174     { 2, vsop_rad_Earth_1 },
   2175     { 1, vsop_rad_Earth_2 }
   2176 };
   2177 
   2178 ;
   2179 static const vsop_term_t vsop_lon_Mars_0[] =
   2180 {
   2181     { 6.20347711581, 0.00000000000, 0.00000000000 },
   2182     { 0.18656368093, 5.05037100270, 3340.61242669980 },
   2183     { 0.01108216816, 5.40099836344, 6681.22485339960 },
   2184     { 0.00091798406, 5.75478744667, 10021.83728009940 },
   2185     { 0.00027744987, 5.97049513147, 3.52311834900 },
   2186     { 0.00010610235, 2.93958560338, 2281.23049651060 },
   2187     { 0.00012315897, 0.84956094002, 2810.92146160520 },
   2188     { 0.00008926784, 4.15697846427, 0.01725365220 },
   2189     { 0.00008715691, 6.11005153139, 13362.44970679920 },
   2190     { 0.00006797556, 0.36462229657, 398.14900340820 },
   2191     { 0.00007774872, 3.33968761376, 5621.84292321040 },
   2192     { 0.00003575078, 1.66186505710, 2544.31441988340 },
   2193     { 0.00004161108, 0.22814971327, 2942.46342329160 },
   2194     { 0.00003075252, 0.85696614132, 191.44826611160 },
   2195     { 0.00002628117, 0.64806124465, 3337.08930835080 },
   2196     { 0.00002937546, 6.07893711402, 0.06731030280 },
   2197     { 0.00002389414, 5.03896442664, 796.29800681640 },
   2198     { 0.00002579844, 0.02996736156, 3344.13554504880 },
   2199     { 0.00001528141, 1.14979301996, 6151.53388830500 },
   2200     { 0.00001798806, 0.65634057445, 529.69096509460 },
   2201     { 0.00001264357, 3.62275122593, 5092.15195811580 },
   2202     { 0.00001286228, 3.06796065034, 2146.16541647520 },
   2203     { 0.00001546404, 2.91579701718, 1751.53953141600 },
   2204     { 0.00001024902, 3.69334099279, 8962.45534991020 },
   2205     { 0.00000891566, 0.18293837498, 16703.06213349900 },
   2206     { 0.00000858759, 2.40093811940, 2914.01423582380 },
   2207     { 0.00000832715, 2.46418619474, 3340.59517304760 },
   2208     { 0.00000832720, 4.49495782139, 3340.62968035200 },
   2209     { 0.00000712902, 3.66335473479, 1059.38193018920 },
   2210     { 0.00000748723, 3.82248614017, 155.42039943420 },
   2211     { 0.00000723861, 0.67497311481, 3738.76143010800 },
   2212     { 0.00000635548, 2.92182225127, 8432.76438481560 },
   2213     { 0.00000655162, 0.48864064125, 3127.31333126180 },
   2214     { 0.00000550474, 3.81001042328, 0.98032106820 },
   2215     { 0.00000552750, 4.47479317037, 1748.01641306700 },
   2216     { 0.00000425966, 0.55364317304, 6283.07584999140 },
   2217     { 0.00000415131, 0.49662285038, 213.29909543800 },
   2218     { 0.00000472167, 3.62547124025, 1194.44701022460 },
   2219     { 0.00000306551, 0.38052848348, 6684.74797174860 },
   2220     { 0.00000312141, 0.99853944405, 6677.70173505060 },
   2221     { 0.00000293198, 4.22131299634, 20.77539549240 },
   2222     { 0.00000302375, 4.48618007156, 3532.06069281140 },
   2223     { 0.00000274027, 0.54222167059, 3340.54511639700 },
   2224     { 0.00000281079, 5.88163521788, 1349.86740965880 },
   2225     { 0.00000231183, 1.28242156993, 3870.30339179440 },
   2226     { 0.00000283602, 5.76885434940, 3149.16416058820 },
   2227     { 0.00000236117, 5.75503217933, 3333.49887969900 },
   2228     { 0.00000274033, 0.13372524985, 3340.67973700260 },
   2229     { 0.00000299395, 2.78323740866, 6254.62666252360 }
   2230 };
   2231 
   2232 static const vsop_term_t vsop_lon_Mars_1[] =
   2233 {
   2234     { 3340.61242700512, 0.00000000000, 0.00000000000 },
   2235     { 0.01457554523, 3.60433733236, 3340.61242669980 },
   2236     { 0.00168414711, 3.92318567804, 6681.22485339960 },
   2237     { 0.00020622975, 4.26108844583, 10021.83728009940 },
   2238     { 0.00003452392, 4.73210393190, 3.52311834900 },
   2239     { 0.00002586332, 4.60670058555, 13362.44970679920 },
   2240     { 0.00000841535, 4.45864030426, 2281.23049651060 }
   2241 };
   2242 
   2243 static const vsop_term_t vsop_lon_Mars_2[] =
   2244 {
   2245     { 0.00058152577, 2.04961712429, 3340.61242669980 },
   2246     { 0.00013459579, 2.45738706163, 6681.22485339960 }
   2247 };
   2248 
   2249 static const vsop_series_t vsop_lon_Mars[] =
   2250 {
   2251     { 49, vsop_lon_Mars_0 },
   2252     { 7, vsop_lon_Mars_1 },
   2253     { 2, vsop_lon_Mars_2 }
   2254 };
   2255 
   2256 static const vsop_term_t vsop_lat_Mars_0[] =
   2257 {
   2258     { 0.03197134986, 3.76832042431, 3340.61242669980 },
   2259     { 0.00298033234, 4.10616996305, 6681.22485339960 },
   2260     { 0.00289104742, 0.00000000000, 0.00000000000 },
   2261     { 0.00031365539, 4.44651053090, 10021.83728009940 },
   2262     { 0.00003484100, 4.78812549260, 13362.44970679920 }
   2263 };
   2264 
   2265 static const vsop_term_t vsop_lat_Mars_1[] =
   2266 {
   2267     { 0.00217310991, 6.04472194776, 3340.61242669980 },
   2268     { 0.00020976948, 3.14159265359, 0.00000000000 },
   2269     { 0.00012834709, 1.60810667915, 6681.22485339960 }
   2270 };
   2271 
   2272 static const vsop_series_t vsop_lat_Mars[] =
   2273 {
   2274     { 5, vsop_lat_Mars_0 },
   2275     { 3, vsop_lat_Mars_1 }
   2276 };
   2277 
   2278 static const vsop_term_t vsop_rad_Mars_0[] =
   2279 {
   2280     { 1.53033488271, 0.00000000000, 0.00000000000 },
   2281     { 0.14184953160, 3.47971283528, 3340.61242669980 },
   2282     { 0.00660776362, 3.81783443019, 6681.22485339960 },
   2283     { 0.00046179117, 4.15595316782, 10021.83728009940 },
   2284     { 0.00008109733, 5.55958416318, 2810.92146160520 },
   2285     { 0.00007485318, 1.77239078402, 5621.84292321040 },
   2286     { 0.00005523191, 1.36436303770, 2281.23049651060 },
   2287     { 0.00003825160, 4.49407183687, 13362.44970679920 },
   2288     { 0.00002306537, 0.09081579001, 2544.31441988340 },
   2289     { 0.00001999396, 5.36059617709, 3337.08930835080 },
   2290     { 0.00002484394, 4.92545639920, 2942.46342329160 },
   2291     { 0.00001960195, 4.74249437639, 3344.13554504880 },
   2292     { 0.00001167119, 2.11260868341, 5092.15195811580 },
   2293     { 0.00001102816, 5.00908403998, 398.14900340820 },
   2294     { 0.00000899066, 4.40791133207, 529.69096509460 },
   2295     { 0.00000992252, 5.83861961952, 6151.53388830500 },
   2296     { 0.00000807354, 2.10217065501, 1059.38193018920 },
   2297     { 0.00000797915, 3.44839203899, 796.29800681640 },
   2298     { 0.00000740975, 1.49906336885, 2146.16541647520 }
   2299 };
   2300 
   2301 static const vsop_term_t vsop_rad_Mars_1[] =
   2302 {
   2303     { 0.01107433345, 2.03250524857, 3340.61242669980 },
   2304     { 0.00103175887, 2.37071847807, 6681.22485339960 },
   2305     { 0.00012877200, 0.00000000000, 0.00000000000 },
   2306     { 0.00010815880, 2.70888095665, 10021.83728009940 }
   2307 };
   2308 
   2309 static const vsop_term_t vsop_rad_Mars_2[] =
   2310 {
   2311     { 0.00044242249, 0.47930604954, 3340.61242669980 },
   2312     { 0.00008138042, 0.86998389204, 6681.22485339960 }
   2313 };
   2314 
   2315 static const vsop_series_t vsop_rad_Mars[] =
   2316 {
   2317     { 19, vsop_rad_Mars_0 },
   2318     { 4, vsop_rad_Mars_1 },
   2319     { 2, vsop_rad_Mars_2 }
   2320 };
   2321 
   2322 ;
   2323 static const vsop_term_t vsop_lon_Jupiter_0[] =
   2324 {
   2325     { 0.59954691494, 0.00000000000, 0.00000000000 },
   2326     { 0.09695898719, 5.06191793158, 529.69096509460 },
   2327     { 0.00573610142, 1.44406205629, 7.11354700080 },
   2328     { 0.00306389205, 5.41734730184, 1059.38193018920 },
   2329     { 0.00097178296, 4.14264726552, 632.78373931320 },
   2330     { 0.00072903078, 3.64042916389, 522.57741809380 },
   2331     { 0.00064263975, 3.41145165351, 103.09277421860 },
   2332     { 0.00039806064, 2.29376740788, 419.48464387520 },
   2333     { 0.00038857767, 1.27231755835, 316.39186965660 },
   2334     { 0.00027964629, 1.78454591820, 536.80451209540 },
   2335     { 0.00013589730, 5.77481040790, 1589.07289528380 },
   2336     { 0.00008246349, 3.58227925840, 206.18554843720 },
   2337     { 0.00008768704, 3.63000308199, 949.17560896980 },
   2338     { 0.00007368042, 5.08101194270, 735.87651353180 },
   2339     { 0.00006263150, 0.02497628807, 213.29909543800 },
   2340     { 0.00006114062, 4.51319998626, 1162.47470440780 },
   2341     { 0.00004905396, 1.32084470588, 110.20632121940 },
   2342     { 0.00005305285, 1.30671216791, 14.22709400160 },
   2343     { 0.00005305441, 4.18625634012, 1052.26838318840 },
   2344     { 0.00004647248, 4.69958103684, 3.93215326310 },
   2345     { 0.00003045023, 4.31676431084, 426.59819087600 },
   2346     { 0.00002609999, 1.56667394063, 846.08283475120 },
   2347     { 0.00002028191, 1.06376530715, 3.18139373770 },
   2348     { 0.00001764763, 2.14148655117, 1066.49547719000 },
   2349     { 0.00001722972, 3.88036268267, 1265.56747862640 },
   2350     { 0.00001920945, 0.97168196472, 639.89728631400 },
   2351     { 0.00001633223, 3.58201833555, 515.46387109300 },
   2352     { 0.00001431999, 4.29685556046, 625.67019231240 },
   2353     { 0.00000973272, 4.09764549134, 95.97922721780 }
   2354 };
   2355 
   2356 static const vsop_term_t vsop_lon_Jupiter_1[] =
   2357 {
   2358     { 529.69096508814, 0.00000000000, 0.00000000000 },
   2359     { 0.00489503243, 4.22082939470, 529.69096509460 },
   2360     { 0.00228917222, 6.02646855621, 7.11354700080 },
   2361     { 0.00030099479, 4.54540782858, 1059.38193018920 },
   2362     { 0.00020720920, 5.45943156902, 522.57741809380 },
   2363     { 0.00012103653, 0.16994816098, 536.80451209540 },
   2364     { 0.00006067987, 4.42422292017, 103.09277421860 },
   2365     { 0.00005433968, 3.98480737746, 419.48464387520 },
   2366     { 0.00004237744, 5.89008707199, 14.22709400160 }
   2367 };
   2368 
   2369 static const vsop_term_t vsop_lon_Jupiter_2[] =
   2370 {
   2371     { 0.00047233601, 4.32148536482, 7.11354700080 },
   2372     { 0.00030649436, 2.92977788700, 529.69096509460 },
   2373     { 0.00014837605, 3.14159265359, 0.00000000000 }
   2374 };
   2375 
   2376 static const vsop_series_t vsop_lon_Jupiter[] =
   2377 {
   2378     { 29, vsop_lon_Jupiter_0 },
   2379     { 9, vsop_lon_Jupiter_1 },
   2380     { 3, vsop_lon_Jupiter_2 }
   2381 };
   2382 
   2383 static const vsop_term_t vsop_lat_Jupiter_0[] =
   2384 {
   2385     { 0.02268615702, 3.55852606721, 529.69096509460 },
   2386     { 0.00109971634, 3.90809347197, 1059.38193018920 },
   2387     { 0.00110090358, 0.00000000000, 0.00000000000 },
   2388     { 0.00008101428, 3.60509572885, 522.57741809380 },
   2389     { 0.00006043996, 4.25883108339, 1589.07289528380 },
   2390     { 0.00006437782, 0.30627119215, 536.80451209540 }
   2391 };
   2392 
   2393 static const vsop_term_t vsop_lat_Jupiter_1[] =
   2394 {
   2395     { 0.00078203446, 1.52377859742, 529.69096509460 }
   2396 };
   2397 
   2398 static const vsop_series_t vsop_lat_Jupiter[] =
   2399 {
   2400     { 6, vsop_lat_Jupiter_0 },
   2401     { 1, vsop_lat_Jupiter_1 }
   2402 };
   2403 
   2404 static const vsop_term_t vsop_rad_Jupiter_0[] =
   2405 {
   2406     { 5.20887429326, 0.00000000000, 0.00000000000 },
   2407     { 0.25209327119, 3.49108639871, 529.69096509460 },
   2408     { 0.00610599976, 3.84115365948, 1059.38193018920 },
   2409     { 0.00282029458, 2.57419881293, 632.78373931320 },
   2410     { 0.00187647346, 2.07590383214, 522.57741809380 },
   2411     { 0.00086792905, 0.71001145545, 419.48464387520 },
   2412     { 0.00072062974, 0.21465724607, 536.80451209540 },
   2413     { 0.00065517248, 5.97995884790, 316.39186965660 },
   2414     { 0.00029134542, 1.67759379655, 103.09277421860 },
   2415     { 0.00030135335, 2.16132003734, 949.17560896980 },
   2416     { 0.00023453271, 3.54023522184, 735.87651353180 },
   2417     { 0.00022283743, 4.19362594399, 1589.07289528380 },
   2418     { 0.00023947298, 0.27458037480, 7.11354700080 },
   2419     { 0.00013032614, 2.96042965363, 1162.47470440780 },
   2420     { 0.00009703360, 1.90669633585, 206.18554843720 },
   2421     { 0.00012749023, 2.71550286592, 1052.26838318840 },
   2422     { 0.00007057931, 2.18184839926, 1265.56747862640 },
   2423     { 0.00006137703, 6.26418240033, 846.08283475120 },
   2424     { 0.00002616976, 2.00994012876, 1581.95934828300 }
   2425 };
   2426 
   2427 static const vsop_term_t vsop_rad_Jupiter_1[] =
   2428 {
   2429     { 0.01271801520, 2.64937512894, 529.69096509460 },
   2430     { 0.00061661816, 3.00076460387, 1059.38193018920 },
   2431     { 0.00053443713, 3.89717383175, 522.57741809380 },
   2432     { 0.00031185171, 4.88276958012, 536.80451209540 },
   2433     { 0.00041390269, 0.00000000000, 0.00000000000 }
   2434 };
   2435 
   2436 static const vsop_series_t vsop_rad_Jupiter[] =
   2437 {
   2438     { 19, vsop_rad_Jupiter_0 },
   2439     { 5, vsop_rad_Jupiter_1 }
   2440 };
   2441 
   2442 ;
   2443 static const vsop_term_t vsop_lon_Saturn_0[] =
   2444 {
   2445     { 0.87401354025, 0.00000000000, 0.00000000000 },
   2446     { 0.11107659762, 3.96205090159, 213.29909543800 },
   2447     { 0.01414150957, 4.58581516874, 7.11354700080 },
   2448     { 0.00398379389, 0.52112032699, 206.18554843720 },
   2449     { 0.00350769243, 3.30329907896, 426.59819087600 },
   2450     { 0.00206816305, 0.24658372002, 103.09277421860 },
   2451     { 0.00079271300, 3.84007056878, 220.41264243880 },
   2452     { 0.00023990355, 4.66976924553, 110.20632121940 },
   2453     { 0.00016573588, 0.43719228296, 419.48464387520 },
   2454     { 0.00014906995, 5.76903183869, 316.39186965660 },
   2455     { 0.00015820290, 0.93809155235, 632.78373931320 },
   2456     { 0.00014609559, 1.56518472000, 3.93215326310 },
   2457     { 0.00013160301, 4.44891291899, 14.22709400160 },
   2458     { 0.00015053543, 2.71669915667, 639.89728631400 },
   2459     { 0.00013005299, 5.98119023644, 11.04570026390 },
   2460     { 0.00010725067, 3.12939523827, 202.25339517410 },
   2461     { 0.00005863206, 0.23656938524, 529.69096509460 },
   2462     { 0.00005227757, 4.20783365759, 3.18139373770 },
   2463     { 0.00006126317, 1.76328667907, 277.03499374140 },
   2464     { 0.00005019687, 3.17787728405, 433.71173787680 },
   2465     { 0.00004592550, 0.61977744975, 199.07200143640 },
   2466     { 0.00004005867, 2.24479718502, 63.73589830340 },
   2467     { 0.00002953796, 0.98280366998, 95.97922721780 },
   2468     { 0.00003873670, 3.22283226966, 138.51749687070 },
   2469     { 0.00002461186, 2.03163875071, 735.87651353180 },
   2470     { 0.00003269484, 0.77492638211, 949.17560896980 },
   2471     { 0.00001758145, 3.26580109940, 522.57741809380 },
   2472     { 0.00001640172, 5.50504453050, 846.08283475120 },
   2473     { 0.00001391327, 4.02333150505, 323.50541665740 },
   2474     { 0.00001580648, 4.37265307169, 309.27832265580 },
   2475     { 0.00001123498, 2.83726798446, 415.55249061210 },
   2476     { 0.00001017275, 3.71700135395, 227.52618943960 },
   2477     { 0.00000848642, 3.19150170830, 209.36694217490 }
   2478 };
   2479 
   2480 static const vsop_term_t vsop_lon_Saturn_1[] =
   2481 {
   2482     { 213.29909521690, 0.00000000000, 0.00000000000 },
   2483     { 0.01297370862, 1.82834923978, 213.29909543800 },
   2484     { 0.00564345393, 2.88499717272, 7.11354700080 },
   2485     { 0.00093734369, 1.06311793502, 426.59819087600 },
   2486     { 0.00107674962, 2.27769131009, 206.18554843720 },
   2487     { 0.00040244455, 2.04108104671, 220.41264243880 },
   2488     { 0.00019941774, 1.27954390470, 103.09277421860 },
   2489     { 0.00010511678, 2.74880342130, 14.22709400160 },
   2490     { 0.00006416106, 0.38238295041, 639.89728631400 },
   2491     { 0.00004848994, 2.43037610229, 419.48464387520 },
   2492     { 0.00004056892, 2.92133209468, 110.20632121940 },
   2493     { 0.00003768635, 3.64965330780, 3.93215326310 }
   2494 };
   2495 
   2496 static const vsop_term_t vsop_lon_Saturn_2[] =
   2497 {
   2498     { 0.00116441330, 1.17988132879, 7.11354700080 },
   2499     { 0.00091841837, 0.07325195840, 213.29909543800 },
   2500     { 0.00036661728, 0.00000000000, 0.00000000000 },
   2501     { 0.00015274496, 4.06493179167, 206.18554843720 }
   2502 };
   2503 
   2504 static const vsop_series_t vsop_lon_Saturn[] =
   2505 {
   2506     { 33, vsop_lon_Saturn_0 },
   2507     { 12, vsop_lon_Saturn_1 },
   2508     { 4, vsop_lon_Saturn_2 }
   2509 };
   2510 
   2511 static const vsop_term_t vsop_lat_Saturn_0[] =
   2512 {
   2513     { 0.04330678039, 3.60284428399, 213.29909543800 },
   2514     { 0.00240348302, 2.85238489373, 426.59819087600 },
   2515     { 0.00084745939, 0.00000000000, 0.00000000000 },
   2516     { 0.00030863357, 3.48441504555, 220.41264243880 },
   2517     { 0.00034116062, 0.57297307557, 206.18554843720 },
   2518     { 0.00014734070, 2.11846596715, 639.89728631400 },
   2519     { 0.00009916667, 5.79003188904, 419.48464387520 },
   2520     { 0.00006993564, 4.73604689720, 7.11354700080 },
   2521     { 0.00004807588, 5.43305312061, 316.39186965660 }
   2522 };
   2523 
   2524 static const vsop_term_t vsop_lat_Saturn_1[] =
   2525 {
   2526     { 0.00198927992, 4.93901017903, 213.29909543800 },
   2527     { 0.00036947916, 3.14159265359, 0.00000000000 },
   2528     { 0.00017966989, 0.51979431110, 426.59819087600 }
   2529 };
   2530 
   2531 static const vsop_series_t vsop_lat_Saturn[] =
   2532 {
   2533     { 9, vsop_lat_Saturn_0 },
   2534     { 3, vsop_lat_Saturn_1 }
   2535 };
   2536 
   2537 static const vsop_term_t vsop_rad_Saturn_0[] =
   2538 {
   2539     { 9.55758135486, 0.00000000000, 0.00000000000 },
   2540     { 0.52921382865, 2.39226219573, 213.29909543800 },
   2541     { 0.01873679867, 5.23549604660, 206.18554843720 },
   2542     { 0.01464663929, 1.64763042902, 426.59819087600 },
   2543     { 0.00821891141, 5.93520042303, 316.39186965660 },
   2544     { 0.00547506923, 5.01532618980, 103.09277421860 },
   2545     { 0.00371684650, 2.27114821115, 220.41264243880 },
   2546     { 0.00361778765, 3.13904301847, 7.11354700080 },
   2547     { 0.00140617506, 5.70406606781, 632.78373931320 },
   2548     { 0.00108974848, 3.29313390175, 110.20632121940 },
   2549     { 0.00069006962, 5.94099540992, 419.48464387520 },
   2550     { 0.00061053367, 0.94037691801, 639.89728631400 },
   2551     { 0.00048913294, 1.55733638681, 202.25339517410 },
   2552     { 0.00034143772, 0.19519102597, 277.03499374140 },
   2553     { 0.00032401773, 5.47084567016, 949.17560896980 },
   2554     { 0.00020936596, 0.46349251129, 735.87651353180 },
   2555     { 0.00009796004, 5.20477537945, 1265.56747862640 },
   2556     { 0.00011993338, 5.98050967385, 846.08283475120 },
   2557     { 0.00020839300, 1.52102476129, 433.71173787680 },
   2558     { 0.00015298404, 3.05943814940, 529.69096509460 },
   2559     { 0.00006465823, 0.17732249942, 1052.26838318840 },
   2560     { 0.00011380257, 1.73105427040, 522.57741809380 },
   2561     { 0.00003419618, 4.94550542171, 1581.95934828300 }
   2562 };
   2563 
   2564 static const vsop_term_t vsop_rad_Saturn_1[] =
   2565 {
   2566     { 0.06182981340, 0.25843511480, 213.29909543800 },
   2567     { 0.00506577242, 0.71114625261, 206.18554843720 },
   2568     { 0.00341394029, 5.79635741658, 426.59819087600 },
   2569     { 0.00188491195, 0.47215589652, 220.41264243880 },
   2570     { 0.00186261486, 3.14159265359, 0.00000000000 },
   2571     { 0.00143891146, 1.40744822888, 7.11354700080 }
   2572 };
   2573 
   2574 static const vsop_term_t vsop_rad_Saturn_2[] =
   2575 {
   2576     { 0.00436902572, 4.78671677509, 213.29909543800 }
   2577 };
   2578 
   2579 static const vsop_series_t vsop_rad_Saturn[] =
   2580 {
   2581     { 23, vsop_rad_Saturn_0 },
   2582     { 6, vsop_rad_Saturn_1 },
   2583     { 1, vsop_rad_Saturn_2 }
   2584 };
   2585 
   2586 ;
   2587 static const vsop_term_t vsop_lon_Uranus_0[] =
   2588 {
   2589     { 5.48129294297, 0.00000000000, 0.00000000000 },
   2590     { 0.09260408234, 0.89106421507, 74.78159856730 },
   2591     { 0.01504247898, 3.62719260920, 1.48447270830 },
   2592     { 0.00365981674, 1.89962179044, 73.29712585900 },
   2593     { 0.00272328168, 3.35823706307, 149.56319713460 },
   2594     { 0.00070328461, 5.39254450063, 63.73589830340 },
   2595     { 0.00068892678, 6.09292483287, 76.26607127560 },
   2596     { 0.00061998615, 2.26952066061, 2.96894541660 },
   2597     { 0.00061950719, 2.85098872691, 11.04570026390 },
   2598     { 0.00026468770, 3.14152083966, 71.81265315070 },
   2599     { 0.00025710476, 6.11379840493, 454.90936652730 },
   2600     { 0.00021078850, 4.36059339067, 148.07872442630 },
   2601     { 0.00017818647, 1.74436930289, 36.64856292950 },
   2602     { 0.00014613507, 4.73732166022, 3.93215326310 },
   2603     { 0.00011162509, 5.82681796350, 224.34479570190 },
   2604     { 0.00010997910, 0.48865004018, 138.51749687070 },
   2605     { 0.00009527478, 2.95516862826, 35.16409022120 },
   2606     { 0.00007545601, 5.23626582400, 109.94568878850 },
   2607     { 0.00004220241, 3.23328220918, 70.84944530420 },
   2608     { 0.00004051900, 2.27755017300, 151.04766984290 },
   2609     { 0.00003354596, 1.06549007380, 4.45341812490 },
   2610     { 0.00002926718, 4.62903718891, 9.56122755560 },
   2611     { 0.00003490340, 5.48306144511, 146.59425171800 },
   2612     { 0.00003144069, 4.75199570434, 77.75054398390 },
   2613     { 0.00002922333, 5.35235361027, 85.82729883120 },
   2614     { 0.00002272788, 4.36600400036, 70.32818044240 },
   2615     { 0.00002051219, 1.51773566586, 0.11187458460 },
   2616     { 0.00002148602, 0.60745949945, 38.13303563780 },
   2617     { 0.00001991643, 4.92437588682, 277.03499374140 },
   2618     { 0.00001376226, 2.04283539351, 65.22037101170 },
   2619     { 0.00001666902, 3.62744066769, 380.12776796000 },
   2620     { 0.00001284107, 3.11347961505, 202.25339517410 },
   2621     { 0.00001150429, 0.93343589092, 3.18139373770 },
   2622     { 0.00001533221, 2.58594681212, 52.69019803950 },
   2623     { 0.00001281604, 0.54271272721, 222.86032299360 },
   2624     { 0.00001372139, 4.19641530878, 111.43016149680 },
   2625     { 0.00001221029, 0.19900650030, 108.46121608020 },
   2626     { 0.00000946181, 1.19253165736, 127.47179660680 },
   2627     { 0.00001150989, 4.17898916639, 33.67961751290 }
   2628 };
   2629 
   2630 static const vsop_term_t vsop_lon_Uranus_1[] =
   2631 {
   2632     { 74.78159860910, 0.00000000000, 0.00000000000 },
   2633     { 0.00154332863, 5.24158770553, 74.78159856730 },
   2634     { 0.00024456474, 1.71260334156, 1.48447270830 },
   2635     { 0.00009258442, 0.42829732350, 11.04570026390 },
   2636     { 0.00008265977, 1.50218091379, 63.73589830340 },
   2637     { 0.00009150160, 1.41213765216, 149.56319713460 }
   2638 };
   2639 
   2640 static const vsop_series_t vsop_lon_Uranus[] =
   2641 {
   2642     { 39, vsop_lon_Uranus_0 },
   2643     { 6, vsop_lon_Uranus_1 }
   2644 };
   2645 
   2646 static const vsop_term_t vsop_lat_Uranus_0[] =
   2647 {
   2648     { 0.01346277648, 2.61877810547, 74.78159856730 },
   2649     { 0.00062341400, 5.08111189648, 149.56319713460 },
   2650     { 0.00061601196, 3.14159265359, 0.00000000000 },
   2651     { 0.00009963722, 1.61603805646, 76.26607127560 },
   2652     { 0.00009926160, 0.57630380333, 73.29712585900 }
   2653 };
   2654 
   2655 static const vsop_term_t vsop_lat_Uranus_1[] =
   2656 {
   2657     { 0.00034101978, 0.01321929936, 74.78159856730 }
   2658 };
   2659 
   2660 static const vsop_series_t vsop_lat_Uranus[] =
   2661 {
   2662     { 5, vsop_lat_Uranus_0 },
   2663     { 1, vsop_lat_Uranus_1 }
   2664 };
   2665 
   2666 static const vsop_term_t vsop_rad_Uranus_0[] =
   2667 {
   2668     { 19.21264847206, 0.00000000000, 0.00000000000 },
   2669     { 0.88784984413, 5.60377527014, 74.78159856730 },
   2670     { 0.03440836062, 0.32836099706, 73.29712585900 },
   2671     { 0.02055653860, 1.78295159330, 149.56319713460 },
   2672     { 0.00649322410, 4.52247285911, 76.26607127560 },
   2673     { 0.00602247865, 3.86003823674, 63.73589830340 },
   2674     { 0.00496404167, 1.40139935333, 454.90936652730 },
   2675     { 0.00338525369, 1.58002770318, 138.51749687070 },
   2676     { 0.00243509114, 1.57086606044, 71.81265315070 },
   2677     { 0.00190522303, 1.99809394714, 1.48447270830 },
   2678     { 0.00161858838, 2.79137786799, 148.07872442630 },
   2679     { 0.00143706183, 1.38368544947, 11.04570026390 },
   2680     { 0.00093192405, 0.17437220467, 36.64856292950 },
   2681     { 0.00071424548, 4.24509236074, 224.34479570190 },
   2682     { 0.00089806014, 3.66105364565, 109.94568878850 },
   2683     { 0.00039009723, 1.66971401684, 70.84944530420 },
   2684     { 0.00046677296, 1.39976401694, 35.16409022120 },
   2685     { 0.00039025624, 3.36234773834, 277.03499374140 },
   2686     { 0.00036755274, 3.88649278513, 146.59425171800 },
   2687     { 0.00030348723, 0.70100838798, 151.04766984290 },
   2688     { 0.00029156413, 3.18056336700, 77.75054398390 },
   2689     { 0.00022637073, 0.72518687029, 529.69096509460 },
   2690     { 0.00011959076, 1.75043392140, 984.60033162190 },
   2691     { 0.00025620756, 5.25656086672, 380.12776796000 }
   2692 };
   2693 
   2694 static const vsop_term_t vsop_rad_Uranus_1[] =
   2695 {
   2696     { 0.01479896629, 3.67205697578, 74.78159856730 }
   2697 };
   2698 
   2699 static const vsop_series_t vsop_rad_Uranus[] =
   2700 {
   2701     { 24, vsop_rad_Uranus_0 },
   2702     { 1, vsop_rad_Uranus_1 }
   2703 };
   2704 
   2705 ;
   2706 static const vsop_term_t vsop_lon_Neptune_0[] =
   2707 {
   2708     { 5.31188633046, 0.00000000000, 0.00000000000 },
   2709     { 0.01798475530, 2.90101273890, 38.13303563780 },
   2710     { 0.01019727652, 0.48580922867, 1.48447270830 },
   2711     { 0.00124531845, 4.83008090676, 36.64856292950 },
   2712     { 0.00042064466, 5.41054993053, 2.96894541660 },
   2713     { 0.00037714584, 6.09221808686, 35.16409022120 },
   2714     { 0.00033784738, 1.24488874087, 76.26607127560 },
   2715     { 0.00016482741, 0.00007727998, 491.55792945680 },
   2716     { 0.00009198584, 4.93747051954, 39.61750834610 },
   2717     { 0.00008994250, 0.27462171806, 175.16605980020 }
   2718 };
   2719 
   2720 static const vsop_term_t vsop_lon_Neptune_1[] =
   2721 {
   2722     { 38.13303563957, 0.00000000000, 0.00000000000 },
   2723     { 0.00016604172, 4.86323329249, 1.48447270830 },
   2724     { 0.00015744045, 2.27887427527, 38.13303563780 }
   2725 };
   2726 
   2727 static const vsop_series_t vsop_lon_Neptune[] =
   2728 {
   2729     { 10, vsop_lon_Neptune_0 },
   2730     { 3, vsop_lon_Neptune_1 }
   2731 };
   2732 
   2733 static const vsop_term_t vsop_lat_Neptune_0[] =
   2734 {
   2735     { 0.03088622933, 1.44104372644, 38.13303563780 },
   2736     { 0.00027780087, 5.91271884599, 76.26607127560 },
   2737     { 0.00027623609, 0.00000000000, 0.00000000000 },
   2738     { 0.00015355489, 2.52123799551, 36.64856292950 },
   2739     { 0.00015448133, 3.50877079215, 39.61750834610 }
   2740 };
   2741 
   2742 static const vsop_series_t vsop_lat_Neptune[] =
   2743 {
   2744     { 5, vsop_lat_Neptune_0 }
   2745 };
   2746 
   2747 static const vsop_term_t vsop_rad_Neptune_0[] =
   2748 {
   2749     { 30.07013205828, 0.00000000000, 0.00000000000 },
   2750     { 0.27062259632, 1.32999459377, 38.13303563780 },
   2751     { 0.01691764014, 3.25186135653, 36.64856292950 },
   2752     { 0.00807830553, 5.18592878704, 1.48447270830 },
   2753     { 0.00537760510, 4.52113935896, 35.16409022120 },
   2754     { 0.00495725141, 1.57105641650, 491.55792945680 },
   2755     { 0.00274571975, 1.84552258866, 175.16605980020 },
   2756     { 0.00012012320, 1.92059384991, 1021.24889455140 },
   2757     { 0.00121801746, 5.79754470298, 76.26607127560 },
   2758     { 0.00100896068, 0.37702724930, 73.29712585900 },
   2759     { 0.00135134092, 3.37220609835, 39.61750834610 },
   2760     { 0.00007571796, 1.07149207335, 388.46515523820 }
   2761 };
   2762 
   2763 static const vsop_series_t vsop_rad_Neptune[] =
   2764 {
   2765     { 12, vsop_rad_Neptune_0 }
   2766 };
   2767 
   2768 ;
   2769 
   2770 /** @cond DOXYGEN_SKIP */
   2771 #define VSOPFORMULA(x)    { ARRAYSIZE(x), x }
   2772 /** @endcond */
   2773 
   2774 static const vsop_model_t vsop[] =
   2775 {
   2776     { { VSOPFORMULA(vsop_lon_Mercury),  VSOPFORMULA(vsop_lat_Mercury),  VSOPFORMULA(vsop_rad_Mercury) } },
   2777     { { VSOPFORMULA(vsop_lon_Venus),    VSOPFORMULA(vsop_lat_Venus),    VSOPFORMULA(vsop_rad_Venus)   } },
   2778     { { VSOPFORMULA(vsop_lon_Earth),    VSOPFORMULA(vsop_lat_Earth),    VSOPFORMULA(vsop_rad_Earth)   } },
   2779     { { VSOPFORMULA(vsop_lon_Mars),     VSOPFORMULA(vsop_lat_Mars),     VSOPFORMULA(vsop_rad_Mars)    } },
   2780     { { VSOPFORMULA(vsop_lon_Jupiter),  VSOPFORMULA(vsop_lat_Jupiter),  VSOPFORMULA(vsop_rad_Jupiter) } },
   2781     { { VSOPFORMULA(vsop_lon_Saturn),   VSOPFORMULA(vsop_lat_Saturn),   VSOPFORMULA(vsop_rad_Saturn)  } },
   2782     { { VSOPFORMULA(vsop_lon_Uranus),   VSOPFORMULA(vsop_lat_Uranus),   VSOPFORMULA(vsop_rad_Uranus)  } },
   2783     { { VSOPFORMULA(vsop_lon_Neptune),  VSOPFORMULA(vsop_lat_Neptune),  VSOPFORMULA(vsop_rad_Neptune) } }
   2784 };
   2785 
   2786 /** @cond DOXYGEN_SKIP */
   2787 #define CalcEarth(time)     CalcVsop(&vsop[BODY_EARTH], (time))
   2788 #define LON_INDEX 0
   2789 #define LAT_INDEX 1
   2790 #define RAD_INDEX 2
   2791 /** @endcond */
   2792 
   2793 static void VsopCoords(const vsop_model_t *model, double t, double sphere[3])
   2794 {
   2795     int k, s, i;
   2796 
   2797     for (k=0; k < 3; ++k)
   2798     {
   2799         double tpower = 1.0;
   2800         const vsop_formula_t *formula = &model->formula[k];
   2801         sphere[k] = 0.0;
   2802         for (s=0; s < formula->nseries; ++s)
   2803         {
   2804             double sum = 0.0;
   2805             const vsop_series_t *series = &formula->series[s];
   2806             for (i=0; i < series->nterms; ++i)
   2807             {
   2808                 const vsop_term_t *term = &series->term[i];
   2809                 sum += term->amplitude * cos(term->phase + (t * term->frequency));
   2810             }
   2811             sphere[k] += tpower * sum;
   2812             tpower *= t;
   2813         }
   2814     }
   2815 }
   2816 
   2817 
   2818 static terse_vector_t VsopRotate(const double ecl[3])
   2819 {
   2820     terse_vector_t equ;
   2821 
   2822     /*
   2823         X        +1.000000000000  +0.000000440360  -0.000000190919   X
   2824         Y     =  -0.000000479966  +0.917482137087  -0.397776982902   Y
   2825         Z FK5     0.000000000000  +0.397776982902  +0.917482137087   Z VSOP87A
   2826     */
   2827 
   2828     equ.x = ecl[0] + 0.000000440360*ecl[1] - 0.000000190919*ecl[2];
   2829     equ.y = -0.000000479966*ecl[0] + 0.917482137087*ecl[1] - 0.397776982902*ecl[2];
   2830     equ.z = 0.397776982902*ecl[1] + 0.917482137087*ecl[2];
   2831 
   2832     return equ;
   2833 }
   2834 
   2835 
   2836 static void VsopSphereToRect(double lon, double lat, double radius, double pos[3])
   2837 {
   2838     double r_coslat = radius * cos(lat);
   2839     pos[0] = r_coslat * cos(lon);
   2840     pos[1] = r_coslat * sin(lon);
   2841     pos[2] = radius * sin(lat);
   2842 }
   2843 
   2844 static const double DAYS_PER_MILLENNIUM = 365250.0;
   2845 
   2846 
   2847 static astro_vector_t CalcVsop(const vsop_model_t *model, astro_time_t time)
   2848 {
   2849     double t = time.tt / DAYS_PER_MILLENNIUM;
   2850     double sphere[3];       /* lon, lat, rad */
   2851     double eclip[3];
   2852     astro_vector_t vector;
   2853     terse_vector_t pos;
   2854 
   2855     /* Calculate the VSOP "B" trigonometric series to obtain ecliptic spherical coordinates. */
   2856     VsopCoords(model, t, sphere);
   2857 
   2858     /* Convert ecliptic spherical coordinates to ecliptic Cartesian coordinates. */
   2859     VsopSphereToRect(sphere[LON_INDEX], sphere[LAT_INDEX], sphere[RAD_INDEX], eclip);
   2860 
   2861     /* Convert ecliptic Cartesian coordinates to equatorial Cartesian coordinates. */
   2862     pos = VsopRotate(eclip);
   2863 
   2864     /* Package the position as astro_vector_t. */
   2865     vector.status = ASTRO_SUCCESS;
   2866     vector.t = time;
   2867     vector.x = pos.x;
   2868     vector.y = pos.y;
   2869     vector.z = pos.z;
   2870 
   2871     return vector;
   2872 }
   2873 
   2874 
   2875 static void VsopDeriv(const vsop_model_t *model, double t, double deriv[3])
   2876 {
   2877     int k, s, i;
   2878 
   2879     for (k=0; k < 3; ++k)
   2880     {
   2881         double tpower = 1.0;        /* t^s */
   2882         double dpower = 0.0;        /* t^(s-1) */
   2883         const vsop_formula_t *formula = &model->formula[k];
   2884         deriv[k] = 0.0;
   2885         for (s=0; s < formula->nseries; ++s)
   2886         {
   2887             double sin_sum = 0.0;
   2888             double cos_sum = 0.0;
   2889             const vsop_series_t *series = &formula->series[s];
   2890             for (i=0; i < series->nterms; ++i)
   2891             {
   2892                 const vsop_term_t *term = &series->term[i];
   2893                 double angle = term->phase + (t * term->frequency);
   2894                 sin_sum += term->amplitude * term->frequency * sin(angle);
   2895                 if (s > 0)
   2896                     cos_sum += term->amplitude * cos(angle);
   2897             }
   2898             deriv[k] += (s * dpower * cos_sum) - (tpower * sin_sum);
   2899             dpower = tpower;
   2900             tpower *= t;
   2901         }
   2902     }
   2903 }
   2904 
   2905 
   2906 static body_state_t CalcVsopPosVel(const vsop_model_t *model, double tt)
   2907 {
   2908     body_state_t state;
   2909     double t = tt / DAYS_PER_MILLENNIUM;
   2910     double sphere[3];   /* lon, lat, r */
   2911     double deriv[3];    /* d(lon)/dt, d(lat)/dt, dr/dt */
   2912     double eclip[3];
   2913     double dr_dt, dlat_dt, dlon_dt;
   2914     double r, coslat, coslon, sinlat, sinlon;
   2915 
   2916     state.tt = tt;
   2917     VsopCoords(model, t, sphere);
   2918     VsopSphereToRect(sphere[LON_INDEX], sphere[LAT_INDEX], sphere[RAD_INDEX], eclip);
   2919     state.r = VsopRotate(eclip);
   2920 
   2921     VsopDeriv(model, t, deriv);
   2922 
   2923     /* Use spherical coords and spherical derivatives to calculate */
   2924     /* the velocity vector in rectangular coordinates. */
   2925 
   2926     /* Calculate mnemonic variables to help keep the math straight. */
   2927     coslon = cos(sphere[LON_INDEX]);
   2928     sinlon = sin(sphere[LON_INDEX]);
   2929     coslat = cos(sphere[LAT_INDEX]);
   2930     sinlat = sin(sphere[LAT_INDEX]);
   2931     r = sphere[RAD_INDEX];
   2932     dlon_dt = deriv[LON_INDEX];
   2933     dlat_dt = deriv[LAT_INDEX];
   2934     dr_dt   = deriv[RAD_INDEX];
   2935 
   2936     /* vx = dx/dt */
   2937     eclip[0] = (dr_dt * coslat * coslon) - (r * sinlat * coslon * dlat_dt) - (r * coslat * sinlon * dlon_dt);
   2938 
   2939     /* vy = dy/dt */
   2940     eclip[1] = (dr_dt * coslat * sinlon) - (r * sinlat * sinlon * dlat_dt) + (r * coslat * coslon * dlon_dt);
   2941 
   2942     /* vz = dz/dt */
   2943     eclip[2] = (dr_dt * sinlat) + (r * coslat * dlat_dt);
   2944 
   2945     /* Rotate the velocity vector from ecliptic to equatorial coordinates. */
   2946     state.v = VsopRotate(eclip);
   2947 
   2948     /* Convert speed units from [AU/millennium] to [AU/day]. */
   2949     VecScale(&state.v, 1 / DAYS_PER_MILLENNIUM);
   2950 
   2951     return state;
   2952 }
   2953 
   2954 
   2955 static double VsopHelioDistance(const vsop_model_t *model, astro_time_t time)
   2956 {
   2957     int s, i;
   2958     double t = time.tt / DAYS_PER_MILLENNIUM;
   2959     double distance = 0.0;
   2960     double tpower = 1.0;
   2961     const vsop_formula_t *formula = &model->formula[2];     /* [2] is the distance part of the formula */
   2962 
   2963     /*
   2964         The caller only wants to know the distance between the planet and the Sun.
   2965         So we only need to calculate the radial component of the spherical coordinates.
   2966     */
   2967 
   2968     for (s=0; s < formula->nseries; ++s)
   2969     {
   2970         double sum = 0.0;
   2971         const vsop_series_t *series = &formula->series[s];
   2972         for (i=0; i < series->nterms; ++i)
   2973         {
   2974             const vsop_term_t *term = &series->term[i];
   2975             sum += term->amplitude * cos(term->phase + (t * term->frequency));
   2976         }
   2977         distance += tpower * sum;
   2978         tpower *= t;
   2979     }
   2980 
   2981     return distance;
   2982 }
   2983 
   2984 
   2985 static void AdjustBarycenter(astro_vector_t *ssb, astro_time_t time, astro_body_t body, double planet_gm)
   2986 {
   2987     astro_vector_t planet;
   2988     double shift;
   2989 
   2990     shift = planet_gm / (planet_gm + SUN_GM);
   2991     planet = CalcVsop(&vsop[body], time);
   2992     ssb->x += shift * planet.x;
   2993     ssb->y += shift * planet.y;
   2994     ssb->z += shift * planet.z;
   2995 }
   2996 
   2997 
   2998 static astro_vector_t CalcSolarSystemBarycenter(astro_time_t time)
   2999 {
   3000     astro_vector_t ssb;
   3001 
   3002     ssb.status = ASTRO_SUCCESS;
   3003     ssb.t = time;
   3004     ssb.x = ssb.y = ssb.z = 0.0;
   3005 
   3006     AdjustBarycenter(&ssb, time, BODY_JUPITER, JUPITER_GM);
   3007     AdjustBarycenter(&ssb, time, BODY_SATURN,  SATURN_GM);
   3008     AdjustBarycenter(&ssb, time, BODY_URANUS,  URANUS_GM);
   3009     AdjustBarycenter(&ssb, time, BODY_NEPTUNE, NEPTUNE_GM);
   3010 
   3011     return ssb;
   3012 }
   3013 
   3014 /*------------------ begin Pluto integrator ------------------*/
   3015 
   3016 /** @cond DOXYGEN_SKIP */
   3017 typedef struct
   3018 {
   3019     double          tt;   /* J2000 terrestrial time [days] */
   3020     terse_vector_t  r;    /* position [au] */
   3021     terse_vector_t  v;    /* velocity [au/day] */
   3022     terse_vector_t  a;    /* acceleration [au/day^2] */
   3023 } body_grav_calc_t;
   3024 /** @endcond */
   3025 
   3026 #define PLUTO_NUM_STATES  41
   3027 #define PLUTO_TIME_STEP   36500
   3028 
   3029 static const body_state_t PlutoStateTable[] =
   3030 {
   3031     {  -730000.0, {-26.1182072321076, -14.3761681778250,   3.3844025152995}, { 1.6339372163656e-03, -2.7861699588508e-03, -1.3585880229445e-03} }
   3032 ,   {  -693500.0, { 43.6599275018261,  15.7782921408811,  -8.2269833881374}, {-2.5043046295860e-04,  2.1163039457238e-03,  7.3466073583102e-04} }
   3033 ,   {  -657000.0, {-17.0086014985033,  33.0590743876420,  15.4080189624259}, {-1.9676551946049e-03, -1.8337707766770e-03,  2.0125441459959e-05} }
   3034 ,   {  -620500.0, { 26.9005106893171, -21.5285596810214, -14.7987712668075}, { 2.2939261196998e-03,  1.7431871970059e-03, -1.4585639832643e-04} }
   3035 ,   {  -584000.0, { 20.2303809506997,  43.2669666571891,   7.3829660919234}, {-1.9754081700585e-03,  5.3457141292226e-04,  7.5929169129793e-04} }
   3036 ,   {  -547500.0, {-22.5571440338751, -19.2958112538447,   0.7806423603826}, { 2.1494578646505e-03, -2.4266772630044e-03, -1.4013084013574e-03} }
   3037 ,   {  -511000.0, { 43.0236236810360,  19.6179542007347,  -6.8406553041565}, {-4.7729923671058e-04,  2.0208979483877e-03,  7.7191815992131e-04} }
   3038 ,   {  -474500.0, {-20.4245105862934,  29.5157679318005,  15.3408675727018}, {-1.8003167284198e-03, -2.1025226687937e-03, -1.1262333332859e-04} }
   3039 ,   {  -438000.0, { 30.7746921076872, -18.2366370153037, -14.9455358798963}, { 2.0113162005465e-03,  1.9353827024189e-03, -2.0937793168297e-06} }
   3040 ,   {  -401500.0, { 16.7235440456361,  44.0505598318603,   8.6886113939440}, {-2.0565226049264e-03,  3.2710694138777e-04,  7.2006155046579e-04} }
   3041 ,   {  -365000.0, {-18.4891734360057, -23.1428732331142,  -1.6436720878799}, { 2.5524223225832e-03, -2.0035792463879e-03, -1.3910737531294e-03} }
   3042 ,   {  -328500.0, { 42.0853950560734,  22.9742531259520,  -5.5131410205412}, {-6.7105845193949e-04,  1.9177289500465e-03,  7.9770011059534e-04} }
   3043 ,   {  -292000.0, {-23.2753639151193,  25.8185142987694,  15.0553815885983}, {-1.6062295460975e-03, -2.3395961498533e-03, -2.4377362639479e-04} }
   3044 ,   {  -255500.0, { 33.9015793210130, -14.9421228983498, -14.8664994855707}, { 1.7455105487563e-03,  2.0655068871494e-03,  1.1695000657630e-04} }
   3045 ,   {  -219000.0, { 13.3770189322702,  44.4442211120183,   9.8260227015847}, {-2.1171882923251e-03,  1.3114714542921e-04,  6.7884578840323e-04} }
   3046 ,   {  -182500.0, {-14.1723844533379, -26.0054690135836,  -3.8387026446526}, { 2.8419751785822e-03, -1.5579441656564e-03, -1.3408416711060e-03} }
   3047 ,   {  -146000.0, { 40.9468572586403,  25.9049735920209,  -4.2563362404988}, {-8.3652705194051e-04,  1.8129497136404e-03,  8.1564228273060e-04} }
   3048 ,   {  -109500.0, {-25.5839689598009,  22.0699164999425,  14.5902026036780}, {-1.3923977856331e-03, -2.5442249745422e-03, -3.7169906721828e-04} }
   3049 ,   {   -73000.0, { 36.4035708396756, -11.7473067389593, -14.6304139635223}, { 1.5037714418941e-03,  2.1500325702247e-03,  2.1523781242948e-04} }
   3050 ,   {   -36500.0, { 10.2436041239517,  44.5280986402285,  10.8048664487066}, {-2.1615839201823e-03, -5.1418983893534e-05,  6.3687060751430e-04} }
   3051 ,   {        0.0, { -9.8753695807739, -27.9789262247367,  -5.7537118247043}, { 3.0287533248818e-03, -1.1276087003636e-03, -1.2651326732361e-03} }
   3052 ,   {    36500.0, { 39.7009143866164,  28.4327664903825,  -3.0906026170881}, {-9.7720559866138e-04,  1.7121518344796e-03,  8.2822409843551e-04} }
   3053 ,   {    73000.0, {-27.3620419812795,  18.4265651225706,  13.9975343005914}, {-1.1690934621340e-03, -2.7143131627458e-03, -4.9312695340367e-04} }
   3054 ,   {   109500.0, { 38.3556091850032,  -8.7643800131842, -14.2951819118807}, { 1.2922798115839e-03,  2.2032141141126e-03,  2.9606522103424e-04} }
   3055 ,   {   146000.0, {  7.3929490279056,  44.3826789515344,  11.6295002148543}, {-2.1932815453830e-03, -2.1751799585364e-04,  5.9556516201114e-04} }
   3056 ,   {   182500.0, { -5.8649529029432, -29.1987619981354,  -7.3502494912123}, { 3.1339384323665e-03, -7.4205968379701e-04, -1.1783357537604e-03} }
   3057 ,   {   219000.0, { 38.4269476345329,  30.5667598351632,  -2.0378379641214}, {-1.0958945370084e-03,  1.6194885149659e-03,  8.3705272532546e-04} }
   3058 ,   {   255500.0, {-28.6586488201636,  15.0309000931701,  13.3365724093667}, {-9.4611899595408e-04, -2.8506813871559e-03, -6.0508645822989e-04} }
   3059 ,   {   292000.0, { 39.8319806717528,  -6.0784057667647, -13.9098153586562}, { 1.1117769689167e-03,  2.2362097830152e-03,  3.6230548231153e-04} }
   3060 ,   {   328500.0, {  4.8371523764030,  44.0723119541530,  12.3146147867802}, {-2.2164547537724e-03, -3.6790365636785e-04,  5.5542723844616e-04} }
   3061 ,   {   365000.0, { -2.2619763759487, -29.8581508706765,  -8.6502366418978}, { 3.1821176368396e-03, -4.0915169873994e-04, -1.0895893040652e-03} }
   3062 ,   {   401500.0, { 37.1576590087419,  32.3528396259588,  -1.0950381786229}, {-1.1988412606830e-03,  1.5356290902995e-03,  8.4339118209852e-04} }
   3063 ,   {   438000.0, {-29.5767402292299,  11.8635359435865,  12.6313230398719}, {-7.2292830060955e-04, -2.9587820140709e-03, -7.0824296450300e-04} }
   3064 ,   {   474500.0, { 40.9541099577599,  -3.6589805945370, -13.4994699563950}, { 9.5387298337127e-04,  2.2572135462477e-03,  4.1826529781128e-04} }
   3065 ,   {   511000.0, {  2.4859523114116,  43.6181887566155,  12.8914184596699}, {-2.2339745420393e-03, -5.1034757181916e-04,  5.1485330196245e-04} }
   3066 ,   {   547500.0, {  1.0594791441638, -30.1357921778687,  -9.7458684762963}, { 3.1921591684898e-03, -1.1305312796150e-04, -9.9954096945965e-04} }
   3067 ,   {   584000.0, { 35.8778640130144,  33.8942263660709,  -0.2245246362769}, {-1.2941245730845e-03,  1.4560427668319e-03,  8.4762160640137e-04} }
   3068 ,   {   620500.0, {-30.2026537318923,   8.7794211940578,  11.8609238187578}, {-4.9002221381806e-04, -3.0438768469137e-03, -8.0605935262763e-04} }
   3069 ,   {   657000.0, { 41.8536204011376,  -1.3790965838042, -13.0624345337527}, { 8.0674627557124e-04,  2.2702374399791e-03,  4.6832587475465e-04} }
   3070 ,   {   693500.0, {  0.2468843977112,  43.0303960481227,  13.3909343344167}, {-2.2436121787266e-03, -6.5238074250728e-04,  4.7172729553196e-04} }
   3071 ,   {   730000.0, {  4.2432528370899, -30.1182016908248, -10.7074412313491}, { 3.1725847067411e-03,  1.6098461202270e-04, -9.0672150593868e-04} }
   3072 };
   3073 
   3074 
   3075 static terse_vector_t UpdatePosition(double dt, terse_vector_t r, terse_vector_t v, terse_vector_t a)
   3076 {
   3077     r.x += (v.x + a.x*dt/2) * dt;
   3078     r.y += (v.y + a.y*dt/2) * dt;
   3079     r.z += (v.z + a.z*dt/2) * dt;
   3080     return r;
   3081 }
   3082 
   3083 
   3084 static body_state_t AdjustBarycenterPosVel(body_state_t *ssb, double tt, astro_body_t body, double planet_gm)
   3085 {
   3086     body_state_t planet;
   3087     double shift;
   3088 
   3089     /*
   3090         This function does 2 important things:
   3091         1. Adjusts 'ssb' by the effect of one major body on the Solar System Barycenter.
   3092         2, Returns the heliocentric position of that major body.
   3093     */
   3094 
   3095     shift = planet_gm / (planet_gm + SUN_GM);
   3096     planet = CalcVsopPosVel(&vsop[body], tt);
   3097     VecIncr(&ssb->r, VecMul(shift, planet.r));
   3098     VecIncr(&ssb->v, VecMul(shift, planet.v));
   3099 
   3100     return planet;
   3101 }
   3102 
   3103 
   3104 static void MajorBodyBary(body_state_t bary[5], double tt)
   3105 {
   3106     int p;
   3107 
   3108     /* bary[0] starts out receiving the Solar System Barycenter. */
   3109     bary[0].tt = tt;
   3110     bary[0].r = VecZero;
   3111     bary[0].v = VecZero;
   3112 
   3113     /* Calculate heliocentric planet positions and SSB. */
   3114     bary[1] = AdjustBarycenterPosVel(&bary[0], tt, BODY_JUPITER, JUPITER_GM);
   3115     bary[2] = AdjustBarycenterPosVel(&bary[0], tt, BODY_SATURN,  SATURN_GM);
   3116     bary[3] = AdjustBarycenterPosVel(&bary[0], tt, BODY_URANUS,  URANUS_GM);
   3117     bary[4] = AdjustBarycenterPosVel(&bary[0], tt, BODY_NEPTUNE, NEPTUNE_GM);
   3118 
   3119     for (p=1; p < 5; ++p)
   3120     {
   3121         /* Convert major body [pos, vel] from heliocentric to barycentric. */
   3122         VecDecr(&bary[p].r, bary[0].r);
   3123         VecDecr(&bary[p].v, bary[0].v);
   3124     }
   3125 
   3126     /* Convert heliocentric SSB to barycentric Sun. */
   3127     VecScale(&bary[0].r, -1.0);
   3128     VecScale(&bary[0].v, -1.0);
   3129 }
   3130 
   3131 
   3132 static void AddAcceleration(terse_vector_t *acc, terse_vector_t small_pos, double gm, terse_vector_t major_pos)
   3133 {
   3134     double dx, dy, dz, r2, pull;
   3135 
   3136     dx = major_pos.x - small_pos.x;
   3137     dy = major_pos.y - small_pos.y;
   3138     dz = major_pos.z - small_pos.z;
   3139 
   3140     r2 = dx*dx + dy*dy + dz*dz;
   3141     pull = gm / (r2 * sqrt(r2));
   3142 
   3143     acc->x += dx * pull;
   3144     acc->y += dy * pull;
   3145     acc->z += dz * pull;
   3146 }
   3147 
   3148 
   3149 static terse_vector_t SmallBodyAcceleration(terse_vector_t small_pos, const body_state_t bary[5])
   3150 {
   3151     terse_vector_t acc = VecZero;
   3152 
   3153     /* Use barycentric coordinates of the Sun and major planets to calculate gravitational accelerations. */
   3154     AddAcceleration(&acc, small_pos, SUN_GM,     bary[0].r);
   3155     AddAcceleration(&acc, small_pos, JUPITER_GM, bary[1].r);
   3156     AddAcceleration(&acc, small_pos, SATURN_GM,  bary[2].r);
   3157     AddAcceleration(&acc, small_pos, URANUS_GM,  bary[3].r);
   3158     AddAcceleration(&acc, small_pos, NEPTUNE_GM, bary[4].r);
   3159 
   3160     return acc;
   3161 }
   3162 
   3163 
   3164 body_grav_calc_t GravSim(           /* out: [pos, vel, acc] of the simulated body at time tt2 */
   3165     body_state_t bary2[5],          /* out: major body barycentric positions at tt2 */
   3166     double tt2,                     /* in:  a target time to be calculated (either before or after tt1 */
   3167     const body_grav_calc_t *calc1)  /* in:  [pos, vel, acc] of the simulated body at time tt1 */
   3168 {
   3169     body_grav_calc_t calc2;
   3170     terse_vector_t approx_pos;
   3171     terse_vector_t acc;
   3172     const double dt = tt2 - calc1->tt;
   3173 
   3174     /* Calculate where the major bodies (Sun, Jupiter...Neptune) will be at the next time step. */
   3175     MajorBodyBary(bary2, tt2);
   3176 
   3177     /* Estimate position of small body as if current acceleration applies across the whole time interval. */
   3178     /* approx_pos = pos1 + vel1*dt + (1/2)acc*dt^2 */
   3179     approx_pos = UpdatePosition(dt, calc1->r, calc1->v, calc1->a);
   3180 
   3181     /* Calculate acceleration experienced by small body at approximate next location. */
   3182     acc = SmallBodyAcceleration(approx_pos, bary2);
   3183 
   3184     /* Calculate the average acceleration of the endpoints. */
   3185     /* This becomes our estimate of the mean effective acceleration over the whole interval. */
   3186     acc = VecMean(acc, calc1->a);
   3187 
   3188     /* Refine the estimates of [pos, vel, acc] at tt2 using the mean acceleration. */
   3189     calc2.r = UpdatePosition(dt, calc1->r, calc1->v, acc);
   3190     calc2.v = VecAdd(calc1->v, VecMul(dt, acc));
   3191     calc2.a = SmallBodyAcceleration(calc2.r, bary2);
   3192     calc2.tt = tt2;
   3193     return calc2;
   3194 }
   3195 
   3196 
   3197 #define PLUTO_DT 250
   3198 #if PLUTO_TIME_STEP % PLUTO_DT != 0
   3199     #error Invalid combination of Pluto time step, time increment.
   3200 #endif
   3201 
   3202 #define PLUTO_NSTEPS    ((PLUTO_TIME_STEP / PLUTO_DT) + 1)
   3203 
   3204 /** @cond DOXYGEN_SKIP */
   3205 typedef struct
   3206 {
   3207     body_grav_calc_t   step[PLUTO_NSTEPS];
   3208 }
   3209 body_segment_t;
   3210 /** @endcond */
   3211 
   3212 
   3213 /* FIXFIXFIX - Using a global is not thread-safe. Either add thread-locks or change API to accept a cache pointer. */
   3214 static body_segment_t *pluto_cache[PLUTO_NUM_STATES-1];
   3215 
   3216 
   3217 static int ClampIndex(double frac, int nsteps)
   3218 {
   3219     int index = (int) floor(frac);
   3220     if (index < 0)
   3221         return 0;
   3222     if (index >= nsteps)
   3223         return nsteps-1;
   3224     return index;
   3225 }
   3226 
   3227 
   3228 static body_grav_calc_t GravFromState(body_state_t bary[5], const body_state_t *state)
   3229 {
   3230     body_grav_calc_t calc;
   3231 
   3232     MajorBodyBary(bary, state->tt);
   3233 
   3234     calc.tt = state->tt;
   3235     calc.r  = VecAdd(state->r, bary[0].r);      /* convert heliocentric to barycentric */
   3236     calc.v  = VecAdd(state->v, bary[0].v);      /* convert heliocentric to barycentric */
   3237     calc.a  = SmallBodyAcceleration(calc.r, bary);
   3238 
   3239     return calc;
   3240 }
   3241 
   3242 
   3243 static astro_status_t GetSegment(int *seg_index, body_segment_t *cache[], double tt)
   3244 {
   3245     int i;
   3246     body_segment_t reverse;
   3247     body_segment_t *seg;
   3248     body_state_t bary[5];
   3249     double step_tt, ramp;
   3250 
   3251     if (tt < PlutoStateTable[0].tt || tt > PlutoStateTable[PLUTO_NUM_STATES-1].tt)
   3252     {
   3253         /* We don't bother calculating a segment. Let the caller crawl backward/forward to this time. */
   3254         *seg_index = -1;
   3255         return ASTRO_SUCCESS;
   3256     }
   3257 
   3258     /* See if we have a segment that straddles the requested time. */
   3259     /* If so, return it. Otherwise, calculate it and return it. */
   3260 
   3261     *seg_index = ClampIndex((tt - PlutoStateTable[0].tt) / PLUTO_TIME_STEP, PLUTO_NUM_STATES-1);
   3262     if (cache[*seg_index] == NULL)
   3263     {
   3264         /* Allocate memory for the segment (about 11K each). */
   3265         seg = cache[*seg_index] = calloc(1, sizeof(body_segment_t));
   3266         if (seg == NULL)
   3267             return ASTRO_OUT_OF_MEMORY;
   3268 
   3269         /* Calculate the segment. */
   3270         /* Pick the pair of bracketing body states to fill the segment. */
   3271 
   3272         /* Each endpoint is exact. */
   3273         seg->step[0] = GravFromState(bary, &PlutoStateTable[*seg_index]);
   3274         seg->step[PLUTO_NSTEPS-1] = GravFromState(bary, &PlutoStateTable[*seg_index + 1]);
   3275 
   3276         /* Simulate forwards from the lower time bound. */
   3277         step_tt = seg->step[0].tt;
   3278         for (i=1; i < PLUTO_NSTEPS-1; ++i)
   3279             seg->step[i] = GravSim(bary, step_tt += PLUTO_DT, &seg->step[i-1]);
   3280 
   3281         /* Simulate backwards from the upper time bound. */
   3282         step_tt = seg->step[PLUTO_NSTEPS-1].tt;
   3283         reverse.step[PLUTO_NSTEPS-1] = seg->step[PLUTO_NSTEPS-1];
   3284         for (i=PLUTO_NSTEPS-2; i > 0; --i)
   3285             reverse.step[i] = GravSim(bary, step_tt -= PLUTO_DT, &reverse.step[i+1]);
   3286 
   3287         /* Fade-mix the two series so that there are no discontinuities. */
   3288         for (i=PLUTO_NSTEPS-2; i > 0; --i)
   3289         {
   3290             ramp = (double)i / (PLUTO_NSTEPS-1);
   3291             seg->step[i].r = VecRamp(seg->step[i].r, reverse.step[i].r, ramp);
   3292             seg->step[i].v = VecRamp(seg->step[i].v, reverse.step[i].v, ramp);
   3293             seg->step[i].a = VecRamp(seg->step[i].a, reverse.step[i].a, ramp);
   3294         }
   3295     }
   3296 
   3297     return ASTRO_SUCCESS;
   3298 }
   3299 
   3300 
   3301 static terse_vector_t CalcPlutoOneWay(body_state_t bary[5], const body_state_t *init_state, double target_tt, double dt)
   3302 {
   3303     body_grav_calc_t calc;
   3304     int i, n;
   3305 
   3306     calc = GravFromState(bary, init_state);
   3307     n = (int) ceil((target_tt - calc.tt) / dt);
   3308     for (i=0; i < n; ++i)
   3309         calc = GravSim(bary, (i+1 == n) ? target_tt : (calc.tt + dt), &calc);
   3310 
   3311     return calc.r;
   3312 }
   3313 
   3314 
   3315 static astro_vector_t CalcPluto(astro_time_t time)
   3316 {
   3317     terse_vector_t acc, ra, rb, r;
   3318     body_state_t bary[5];
   3319     const body_segment_t *seg;
   3320     int seg_index, left;
   3321     const body_grav_calc_t *s1;
   3322     const body_grav_calc_t *s2;
   3323     astro_status_t status;
   3324 
   3325     status = GetSegment(&seg_index, pluto_cache, time.tt);
   3326     if (status != ASTRO_SUCCESS)
   3327         return VecError(status, time);
   3328 
   3329     if (seg_index < 0)
   3330     {
   3331         /* The target time is outside the year range 0000..4000. */
   3332         /* Calculate it by crawling backward from 0000 or forward from 4000. */
   3333         /* FIXFIXFIX - This is super slow. Could optimize this with extra caching if needed. */
   3334         if (time.tt < PlutoStateTable[0].tt)
   3335             r = CalcPlutoOneWay(bary, &PlutoStateTable[0], time.tt, -PLUTO_DT);
   3336         else
   3337             r = CalcPlutoOneWay(bary, &PlutoStateTable[PLUTO_NUM_STATES-1], time.tt, +PLUTO_DT);
   3338     }
   3339     else
   3340     {
   3341         seg = pluto_cache[seg_index];
   3342         left = ClampIndex((time.tt - seg->step[0].tt) / PLUTO_DT, PLUTO_NSTEPS-1);
   3343         s1 = &seg->step[left];
   3344         s2 = &seg->step[left+1];
   3345 
   3346         /* Find mean acceleration vector over the interval. */
   3347         acc = VecMean(s1->a, s2->a);
   3348 
   3349         /* Use Newtonian mechanics to extrapolate away from t1 in the positive time direction. */
   3350         ra = UpdatePosition(time.tt - s1->tt, s1->r, s1->v, acc);
   3351 
   3352         /* Use Newtonian mechanics to extrapolate away from t2 in the negative time direction. */
   3353         rb = UpdatePosition(time.tt - s2->tt, s2->r, s2->v, acc);
   3354 
   3355         /* Use fade in/out idea to blend the two position estimates. */
   3356         r = VecRamp(ra, rb, (time.tt - s1->tt)/PLUTO_DT);
   3357         MajorBodyBary(bary, time.tt);
   3358     }
   3359 
   3360     /* Convert barycentric coordinates back to heliocentric coordinates. */
   3361     return PublicVec(time, VecSub(r, bary[0].r));
   3362 }
   3363 
   3364 /*------------------ end Pluto integrator ------------------*/
   3365 
   3366 
   3367 /**
   3368  * @brief Calculates heliocentric Cartesian coordinates of a body in the J2000 equatorial system.
   3369  *
   3370  * This function calculates the position of the given celestial body as a vector,
   3371  * using the center of the Sun as the origin.  The result is expressed as a Cartesian
   3372  * vector in the J2000 equatorial system: the coordinates are based on the mean equator
   3373  * of the Earth at noon UTC on 1 January 2000.
   3374  *
   3375  * The position is not corrected for light travel time or aberration.
   3376  * This is different from the behavior of #Astronomy_GeoVector.
   3377  *
   3378  * If given an invalid value for `body`, this function will fail. The caller should always check
   3379  * the `status` field inside the returned #astro_vector_t for `ASTRO_SUCCESS` (success)
   3380  * or any other value (failure) before trusting the resulting vector.
   3381  *
   3382  * @param body
   3383  *      A body for which to calculate a heliocentric position: the Sun, Moon, any of the planets,
   3384  *      the Solar System Barycenter (SSB), or the Earth Moon Barycenter (EMB).
   3385  * @param time  The date and time for which to calculate the position.
   3386  * @return      A heliocentric position vector of the center of the given body.
   3387  */
   3388 astro_vector_t Astronomy_HelioVector(astro_body_t body, astro_time_t time)
   3389 {
   3390     astro_vector_t vector, earth;
   3391 
   3392     switch (body)
   3393     {
   3394     case BODY_SUN:
   3395         vector.status = ASTRO_SUCCESS;
   3396         vector.x = 0.0;
   3397         vector.y = 0.0;
   3398         vector.z = 0.0;
   3399         vector.t = time;
   3400         return vector;
   3401 
   3402     case BODY_MERCURY:
   3403     case BODY_VENUS:
   3404     case BODY_EARTH:
   3405     case BODY_MARS:
   3406     case BODY_JUPITER:
   3407     case BODY_SATURN:
   3408     case BODY_URANUS:
   3409     case BODY_NEPTUNE:
   3410         return CalcVsop(&vsop[body], time);
   3411 
   3412     case BODY_PLUTO:
   3413         return CalcPluto(time);
   3414 
   3415     case BODY_MOON:
   3416         vector = Astronomy_GeoMoon(time);
   3417         earth = CalcEarth(time);
   3418         vector.x += earth.x;
   3419         vector.y += earth.y;
   3420         vector.z += earth.z;
   3421         return vector;
   3422 
   3423     case BODY_EMB:
   3424         vector = Astronomy_GeoMoon(time);
   3425         earth = CalcEarth(time);
   3426         vector.x = earth.x + (vector.x / (1.0 + EARTH_MOON_MASS_RATIO));
   3427         vector.y = earth.y + (vector.y / (1.0 + EARTH_MOON_MASS_RATIO));
   3428         vector.z = earth.z + (vector.z / (1.0 + EARTH_MOON_MASS_RATIO));
   3429         return vector;
   3430 
   3431     case BODY_SSB:
   3432         return CalcSolarSystemBarycenter(time);
   3433 
   3434     default:
   3435         return VecError(ASTRO_INVALID_BODY, time);
   3436     }
   3437 }
   3438 
   3439 /**
   3440  * @brief Calculates the distance from a body to the Sun at a given time.
   3441  *
   3442  * Given a date and time, this function calculates the distance between
   3443  * the center of `body` and the center of the Sun.
   3444  * For the planets Mercury through Neptune, this function is significantly
   3445  * more efficient than calling #Astronomy_HelioVector followed by #Astronomy_VectorLength.
   3446  *
   3447  * @param body
   3448  *      A body for which to calculate a heliocentric distance: the Sun, Moon, or any of the planets.
   3449  *
   3450  * @param time
   3451  *      The date and time for which to calculate the heliocentric distance.
   3452  *
   3453  * @return
   3454  *      If successful, an #astro_func_result_t structure whose `status` is `ASTRO_SUCCESS`
   3455  *      and whose `value` holds the heliocentric distance in AU.
   3456  *      Otherwise, `status` reports an error condition.
   3457  */
   3458 astro_func_result_t Astronomy_HelioDistance(astro_body_t body, astro_time_t time)
   3459 {
   3460     astro_vector_t vector;
   3461     astro_func_result_t result;
   3462 
   3463     switch (body)
   3464     {
   3465     case BODY_SUN:
   3466         result.status = ASTRO_SUCCESS;
   3467         result.value = 0.0;
   3468         return result;
   3469 
   3470     case BODY_MERCURY:
   3471     case BODY_VENUS:
   3472     case BODY_EARTH:
   3473     case BODY_MARS:
   3474     case BODY_JUPITER:
   3475     case BODY_SATURN:
   3476     case BODY_URANUS:
   3477     case BODY_NEPTUNE:
   3478         result.status = ASTRO_SUCCESS;
   3479         result.value = VsopHelioDistance(&vsop[body], time);
   3480         return result;
   3481 
   3482     default:
   3483         /* For non-VSOP objects, fall back to taking the length of the heliocentric vector. */
   3484         vector = Astronomy_HelioVector(body, time);
   3485         if (vector.status != ASTRO_SUCCESS)
   3486             return FuncError(vector.status);
   3487         result.status = ASTRO_SUCCESS;
   3488         result.value = Astronomy_VectorLength(vector);
   3489         return result;
   3490     }
   3491 }
   3492 
   3493 
   3494 /**
   3495  * @brief Calculates geocentric Cartesian coordinates of a body in the J2000 equatorial system.
   3496  *
   3497  * This function calculates the position of the given celestial body as a vector,
   3498  * using the center of the Earth as the origin.  The result is expressed as a Cartesian
   3499  * vector in the J2000 equatorial system: the coordinates are based on the mean equator
   3500  * of the Earth at noon UTC on 1 January 2000.
   3501  *
   3502  * If given an invalid value for `body`, this function will fail. The caller should always check
   3503  * the `status` field inside the returned #astro_vector_t for `ASTRO_SUCCESS` (success)
   3504  * or any other value (failure) before trusting the resulting vector.
   3505  *
   3506  * Unlike #Astronomy_HelioVector, this function always corrects for light travel time.
   3507  * This means the position of the body is "back-dated" by the amount of time it takes
   3508  * light to travel from that body to an observer on the Earth.
   3509  *
   3510  * Also, the position can optionally be corrected for
   3511  * [aberration](https://en.wikipedia.org/wiki/Aberration_of_light), an effect
   3512  * causing the apparent direction of the body to be shifted due to transverse
   3513  * movement of the Earth with respect to the rays of light coming from that body.
   3514  *
   3515  * @param body          A body for which to calculate a heliocentric position: the Sun, Moon, or any of the planets.
   3516  * @param time          The date and time for which to calculate the position.
   3517  * @param aberration    `ABERRATION` to correct for aberration, or `NO_ABERRATION` to leave uncorrected.
   3518  * @return              A geocentric position vector of the center of the given body.
   3519  */
   3520 astro_vector_t Astronomy_GeoVector(astro_body_t body, astro_time_t time, astro_aberration_t aberration)
   3521 {
   3522     astro_vector_t vector;
   3523     astro_vector_t earth;
   3524     astro_time_t ltime;
   3525     astro_time_t ltime2;
   3526     double dt;
   3527     int iter;
   3528 
   3529     if (aberration != ABERRATION && aberration != NO_ABERRATION)
   3530         return VecError(ASTRO_INVALID_PARAMETER, time);
   3531 
   3532     switch (body)
   3533     {
   3534     case BODY_EARTH:
   3535         /* The Earth's geocentric coordinates are always (0,0,0). */
   3536         vector.status = ASTRO_SUCCESS;
   3537         vector.x = 0.0;
   3538         vector.y = 0.0;
   3539         vector.z = 0.0;
   3540         break;
   3541 
   3542     case BODY_MOON:
   3543         vector = Astronomy_GeoMoon(time);
   3544         break;
   3545 
   3546     default:
   3547         /* For all other bodies, apply light travel time correction. */
   3548 
   3549         if (aberration == NO_ABERRATION)
   3550         {
   3551             /* No aberration, so calculate Earth's position once, at the time of observation. */
   3552             earth = CalcEarth(time);
   3553             if (earth.status != ASTRO_SUCCESS)
   3554                 return earth;
   3555         }
   3556 
   3557         ltime = time;
   3558         for (iter=0; iter < 10; ++iter)
   3559         {
   3560             vector = Astronomy_HelioVector(body, ltime);
   3561             if (vector.status != ASTRO_SUCCESS)
   3562                 return vector;
   3563 
   3564             if (aberration == ABERRATION)
   3565             {
   3566                 /*
   3567                     Include aberration, so make a good first-order approximation
   3568                     by backdating the Earth's position also.
   3569                     This is confusing, but it works for objects within the Solar System
   3570                     because the distance the Earth moves in that small amount of light
   3571                     travel time (a few minutes to a few hours) is well approximated
   3572                     by a line segment that substends the angle seen from the remote
   3573                     body viewing Earth. That angle is pretty close to the aberration
   3574                     angle of the moving Earth viewing the remote body.
   3575                     In other words, both of the following approximate the aberration angle:
   3576                         (transverse distance Earth moves) / (distance to body)
   3577                         (transverse speed of Earth) / (speed of light).
   3578                 */
   3579                 earth = CalcEarth(ltime);
   3580                 if (earth.status != ASTRO_SUCCESS)
   3581                     return earth;
   3582             }
   3583 
   3584             /* Convert heliocentric vector to geocentric vector. */
   3585             vector.x -= earth.x;
   3586             vector.y -= earth.y;
   3587             vector.z -= earth.z;
   3588 
   3589             ltime2 = Astronomy_AddDays(time, -Astronomy_VectorLength(vector) / C_AUDAY);
   3590             dt = fabs(ltime2.tt - ltime.tt);
   3591             if (dt < 1.0e-9)
   3592                 goto finished;  /* Ensures we patch 'vector.t' with current time, not ante-dated time. */
   3593 
   3594             ltime = ltime2;
   3595         }
   3596         return VecError(ASTRO_NO_CONVERGE, time);   /* light travel time solver did not converge */
   3597     }
   3598 
   3599 finished:
   3600     vector.t = time;
   3601     return vector;
   3602 }
   3603 
   3604 /**
   3605  * @brief   Calculates equatorial coordinates of a celestial body as seen by an observer on the Earth's surface.
   3606  *
   3607  * Calculates topocentric equatorial coordinates in one of two different systems:
   3608  * J2000 or true-equator-of-date, depending on the value of the `equdate` parameter.
   3609  * Equatorial coordinates include right ascension, declination, and distance in astronomical units.
   3610  *
   3611  * This function corrects for light travel time: it adjusts the apparent location
   3612  * of the observed body based on how long it takes for light to travel from the body to the Earth.
   3613  *
   3614  * This function corrects for *topocentric parallax*, meaning that it adjusts for the
   3615  * angular shift depending on where the observer is located on the Earth. This is most
   3616  * significant for the Moon, because it is so close to the Earth. However, parallax corection
   3617  * has a small effect on the apparent positions of other bodies.
   3618  *
   3619  * Correction for aberration is optional, using the `aberration` parameter.
   3620  *
   3621  * @param body          The celestial body to be observed. Not allowed to be `BODY_EARTH`.
   3622  * @param time          The date and time at which the observation takes place.
   3623  * @param observer      A location on or near the surface of the Earth.
   3624  * @param equdate       Selects the date of the Earth's equator in which to express the equatorial coordinates.
   3625  * @param aberration    Selects whether or not to correct for aberration.
   3626  */
   3627 astro_equatorial_t Astronomy_Equator(
   3628     astro_body_t body,
   3629     astro_time_t *time,
   3630     astro_observer_t observer,
   3631     astro_equator_date_t equdate,
   3632     astro_aberration_t aberration)
   3633 {
   3634     astro_equatorial_t equ;
   3635     astro_vector_t gc;
   3636     double gc_observer[3];
   3637     double j2000[3];
   3638     double temp[3];
   3639     double datevect[3];
   3640 
   3641     geo_pos(time, observer, gc_observer);
   3642     gc = Astronomy_GeoVector(body, *time, aberration);
   3643     if (gc.status != ASTRO_SUCCESS)
   3644         return EquError(gc.status);
   3645 
   3646     j2000[0] = gc.x - gc_observer[0];
   3647     j2000[1] = gc.y - gc_observer[1];
   3648     j2000[2] = gc.z - gc_observer[2];
   3649 
   3650     switch (equdate)
   3651     {
   3652     case EQUATOR_OF_DATE:
   3653         precession(0.0, j2000, time->tt, temp);
   3654         nutation(time, 0, temp, datevect);
   3655         equ = vector2radec(datevect);
   3656         return equ;
   3657 
   3658     case EQUATOR_J2000:
   3659         equ = vector2radec(j2000);
   3660         return equ;
   3661 
   3662     default:
   3663         return EquError(ASTRO_INVALID_PARAMETER);
   3664     }
   3665 }
   3666 
   3667 /**
   3668  * @brief Calculates the apparent location of a body relative to the local horizon of an observer on the Earth.
   3669  *
   3670  * Given a date and time, the geographic location of an observer on the Earth, and
   3671  * equatorial coordinates (right ascension and declination) of a celestial body,
   3672  * this function returns horizontal coordinates (azimuth and altitude angles) for the body
   3673  * relative to the horizon at the geographic location.
   3674  *
   3675  * The right ascension `ra` and declination `dec` passed in must be *equator of date*
   3676  * coordinates, based on the Earth's true equator at the date and time of the observation.
   3677  * Otherwise the resulting horizontal coordinates will be inaccurate.
   3678  * Equator of date coordinates can be obtained by calling #Astronomy_Equator, passing in
   3679  * `EQUATOR_OF_DATE` as its `equdate` parameter. It is also recommended to enable
   3680  * aberration correction by passing in `ABERRATION` as the `aberration` parameter.
   3681  *
   3682  * This function optionally corrects for atmospheric refraction.
   3683  * For most uses, it is recommended to pass `REFRACTION_NORMAL` in the `refraction` parameter to
   3684  * correct for optical lensing of the Earth's atmosphere that causes objects
   3685  * to appear somewhat higher above the horizon than they actually are.
   3686  * However, callers may choose to avoid this correction by passing in `REFRACTION_NONE`.
   3687  * If refraction correction is enabled, the azimuth, altitude, right ascension, and declination
   3688  * in the #astro_horizon_t structure returned by this function will all be corrected for refraction.
   3689  * If refraction is disabled, none of these four coordinates will be corrected; in that case,
   3690  * the right ascension and declination in the returned structure will be numerically identical
   3691  * to the respective `ra` and `dec` values passed in.
   3692  *
   3693  * @param time
   3694  *      The date and time of the observation.
   3695  *
   3696  * @param observer
   3697  *      The geographic location of the observer.
   3698  *
   3699  * @param ra
   3700  *      The right ascension of the body in sidereal hours.
   3701  *      See function remarks for more details.
   3702  *
   3703  * @param dec
   3704  *      The declination of the body in degrees. See function remarks for more details.
   3705  *
   3706  * @param refraction
   3707  *      Selects whether to correct for atmospheric refraction, and if so, which model to use.
   3708  *      The recommended value for most uses is `REFRACTION_NORMAL`.
   3709  *      See function remarks for more details.
   3710  *
   3711  * @return
   3712  *      The body's apparent horizontal coordinates and equatorial coordinates, both optionally corrected for refraction.
   3713  */
   3714 astro_horizon_t Astronomy_Horizon(
   3715     astro_time_t *time, astro_observer_t observer, double ra, double dec, astro_refraction_t refraction)
   3716 {
   3717     astro_horizon_t hor;
   3718     double uze[3], une[3], uwe[3];
   3719     double uz[3], un[3], uw[3];
   3720     double p[3], pz, pn, pw, proj;
   3721     double az, zd;
   3722     double spin_angle;
   3723 
   3724     double sinlat = sin(observer.latitude * DEG2RAD);
   3725     double coslat = cos(observer.latitude * DEG2RAD);
   3726     double sinlon = sin(observer.longitude * DEG2RAD);
   3727     double coslon = cos(observer.longitude * DEG2RAD);
   3728     double sindc = sin(dec * DEG2RAD);
   3729     double cosdc = cos(dec * DEG2RAD);
   3730     double sinra = sin(ra * 15 * DEG2RAD);
   3731     double cosra = cos(ra * 15 * DEG2RAD);
   3732 
   3733     uze[0] = coslat * coslon;
   3734     uze[1] = coslat * sinlon;
   3735     uze[2] = sinlat;
   3736 
   3737     une[0] = -sinlat * coslon;
   3738     une[1] = -sinlat * sinlon;
   3739     une[2] = coslat;
   3740 
   3741     uwe[0] = sinlon;
   3742     uwe[1] = -coslon;
   3743     uwe[2] = 0.0;
   3744 
   3745     spin_angle = -15.0 * sidereal_time(time);
   3746     spin(spin_angle, uze, uz);
   3747     spin(spin_angle, une, un);
   3748     spin(spin_angle, uwe, uw);
   3749 
   3750     p[0] = cosdc * cosra;
   3751     p[1] = cosdc * sinra;
   3752     p[2] = sindc;
   3753 
   3754     pz = p[0]*uz[0] + p[1]*uz[1] + p[2]*uz[2];
   3755     pn = p[0]*un[0] + p[1]*un[1] + p[2]*un[2];
   3756     pw = p[0]*uw[0] + p[1]*uw[1] + p[2]*uw[2];
   3757 
   3758     proj = sqrt(pn*pn + pw*pw);
   3759     az = 0.0;
   3760     if (proj > 0.0)
   3761     {
   3762         az = -atan2(pw, pn) * RAD2DEG;
   3763         if (az < 0)
   3764             az += 360;
   3765         else if (az >= 360)
   3766             az -= 360;
   3767     }
   3768     zd = atan2(proj, pz) * RAD2DEG;
   3769     hor.ra = ra;
   3770     hor.dec = dec;
   3771 
   3772     if (refraction == REFRACTION_NORMAL || refraction == REFRACTION_JPLHOR)
   3773     {
   3774         double zd0, refr;
   3775 
   3776         zd0 = zd;
   3777         refr = Astronomy_Refraction(refraction, 90.0 - zd);
   3778         zd -= refr;
   3779 
   3780         if (refr > 0.0 && zd > 3.0e-4)
   3781         {
   3782             int j;
   3783             double sinzd = sin(zd * DEG2RAD);
   3784             double coszd = cos(zd * DEG2RAD);
   3785             double sinzd0 = sin(zd0 * DEG2RAD);
   3786             double coszd0 = cos(zd0 * DEG2RAD);
   3787             double pr[3];
   3788 
   3789             for (j=0; j<3; ++j)
   3790                 pr[j] = ((p[j] - coszd0 * uz[j]) / sinzd0)*sinzd + uz[j]*coszd;
   3791 
   3792             proj = sqrt(pr[0]*pr[0] + pr[1]*pr[1]);
   3793             if (proj > 0)
   3794             {
   3795                 hor.ra = atan2(pr[1], pr[0]) * (RAD2DEG / 15.0);
   3796                 if (hor.ra < 0.0)
   3797                     hor.ra += 24.0;
   3798                 else if (hor.ra >= 24.0)
   3799                     hor.ra -= 24.0;
   3800             }
   3801             else
   3802             {
   3803                 hor.ra = 0.0;
   3804             }
   3805             hor.dec = atan2(pr[2], proj) * RAD2DEG;
   3806         }
   3807     }
   3808 
   3809     hor.azimuth = az;
   3810     hor.altitude = 90.0 - zd;
   3811     return hor;
   3812 }
   3813 
   3814 /**
   3815  * @brief Calculates geocentric ecliptic coordinates for the Sun.
   3816  *
   3817  * This function calculates the position of the Sun as seen from the Earth.
   3818  * The returned value includes both Cartesian and spherical coordinates.
   3819  * The x-coordinate and longitude values in the returned structure are based
   3820  * on the *true equinox of date*: one of two points in the sky where the instantaneous
   3821  * plane of the Earth's equator at the given date and time (the *equatorial plane*)
   3822  * intersects with the plane of the Earth's orbit around the Sun (the *ecliptic plane*).
   3823  * By convention, the apparent location of the Sun at the March equinox is chosen
   3824  * as the longitude origin and x-axis direction, instead of the one for September.
   3825  *
   3826  * `Astronomy_SunPosition` corrects for precession and nutation of the Earth's axis
   3827  * in order to obtain the exact equatorial plane at the given time.
   3828  *
   3829  * This function can be used for calculating changes of seasons: equinoxes and solstices.
   3830  * In fact, the function #Astronomy_Seasons does use this function for that purpose.
   3831  *
   3832  * @param time
   3833  *      The date and time for which to calculate the Sun's position.
   3834  *
   3835  * @return
   3836  *      The ecliptic coordinates of the Sun using the Earth's true equator of date.
   3837  */
   3838 astro_ecliptic_t Astronomy_SunPosition(astro_time_t time)
   3839 {
   3840     astro_time_t adjusted_time;
   3841     astro_vector_t earth2000;
   3842     double sun2000[3];
   3843     double stemp[3];
   3844     double sun_ofdate[3];
   3845     double true_obliq;
   3846 
   3847     /* Correct for light travel time from the Sun. */
   3848     /* Otherwise season calculations (equinox, solstice) will all be early by about 8 minutes! */
   3849     adjusted_time = Astronomy_AddDays(time, -1.0 / C_AUDAY);
   3850 
   3851     earth2000 = CalcEarth(adjusted_time);
   3852     if (earth2000.status != ASTRO_SUCCESS)
   3853         return EclError(earth2000.status);
   3854 
   3855     /* Convert heliocentric location of Earth to geocentric location of Sun. */
   3856     sun2000[0] = -earth2000.x;
   3857     sun2000[1] = -earth2000.y;
   3858     sun2000[2] = -earth2000.z;
   3859 
   3860     /* Convert to equatorial Cartesian coordinates of date. */
   3861     precession(0.0, sun2000, adjusted_time.tt, stemp);
   3862     nutation(&adjusted_time, 0, stemp, sun_ofdate);
   3863 
   3864     /* Convert equatorial coordinates to ecliptic coordinates. */
   3865     true_obliq = DEG2RAD * e_tilt(&adjusted_time).tobl;
   3866     return RotateEquatorialToEcliptic(sun_ofdate, true_obliq);
   3867 }
   3868 
   3869 /**
   3870  * @brief Converts J2000 equatorial Cartesian coordinates to J2000 ecliptic coordinates.
   3871  *
   3872  * Given coordinates relative to the Earth's equator at J2000 (the instant of noon UTC
   3873  * on 1 January 2000), this function converts those coordinates to J2000 ecliptic coordinates,
   3874  * which are relative to the plane of the Earth's orbit around the Sun.
   3875  *
   3876  * @param equ
   3877  *      Equatorial coordinates in the J2000 frame of reference.
   3878  *      You can call #Astronomy_GeoVector to obtain suitable equatorial coordinates.
   3879  *
   3880  * @return
   3881  *      Ecliptic coordinates in the J2000 frame of reference.
   3882  */
   3883 astro_ecliptic_t Astronomy_Ecliptic(astro_vector_t equ)
   3884 {
   3885     /* Based on NOVAS functions equ2ecl() and equ2ecl_vec(). */
   3886     static const double ob2000 = 0.40909260059599012;   /* mean obliquity of the J2000 ecliptic in radians */
   3887     double pos[3];
   3888 
   3889     if (equ.status != ASTRO_SUCCESS)
   3890         return EclError(equ.status);
   3891 
   3892     pos[0] = equ.x;
   3893     pos[1] = equ.y;
   3894     pos[2] = equ.z;
   3895 
   3896     return RotateEquatorialToEcliptic(pos, ob2000);
   3897 }
   3898 
   3899 /**
   3900  * @brief   Calculates heliocentric ecliptic longitude of a body based on the J2000 equinox.
   3901  *
   3902  * This function calculates the angle around the plane of the Earth's orbit
   3903  * of a celestial body, as seen from the center of the Sun.
   3904  * The angle is measured prograde (in the direction of the Earth's orbit around the Sun)
   3905  * in degrees from the J2000 equinox. The ecliptic longitude is always in the range [0, 360).
   3906  *
   3907  * @param body
   3908  *      A body other than the Sun.
   3909  *
   3910  * @param time
   3911  *      The date and time at which the body's ecliptic longitude is to be calculated.
   3912  *
   3913  * @return
   3914  *      On success, returns a structure whose `status` is `ASTRO_SUCCESS` and whose
   3915  *      `angle` holds the ecliptic longitude in degrees.
   3916  *      On failure, `status` holds a value other than `ASTRO_SUCCESS`.
   3917  */
   3918 astro_angle_result_t Astronomy_EclipticLongitude(astro_body_t body, astro_time_t time)
   3919 {
   3920     astro_vector_t hv;
   3921     astro_ecliptic_t eclip;
   3922     astro_angle_result_t result;
   3923 
   3924     if (body == BODY_SUN)
   3925         return AngleError(ASTRO_INVALID_BODY);      /* cannot calculate heliocentric longitude of the Sun */
   3926 
   3927     hv = Astronomy_HelioVector(body, time);
   3928     eclip = Astronomy_Ecliptic(hv);     /* checks for errors in hv, so we don't have to here */
   3929     if (eclip.status != ASTRO_SUCCESS)
   3930         return AngleError(eclip.status);
   3931 
   3932     result.angle = eclip.elon;
   3933     result.status = ASTRO_SUCCESS;
   3934     return result;
   3935 }
   3936 
   3937 static astro_ecliptic_t RotateEquatorialToEcliptic(const double pos[3], double obliq_radians)
   3938 {
   3939     astro_ecliptic_t ecl;
   3940     double cos_ob, sin_ob;
   3941     double xyproj;
   3942 
   3943     cos_ob = cos(obliq_radians);
   3944     sin_ob = sin(obliq_radians);
   3945 
   3946     ecl.ex = +pos[0];
   3947     ecl.ey = +pos[1]*cos_ob + pos[2]*sin_ob;
   3948     ecl.ez = -pos[1]*sin_ob + pos[2]*cos_ob;
   3949 
   3950     xyproj = sqrt(ecl.ex*ecl.ex + ecl.ey*ecl.ey);
   3951     if (xyproj > 0.0)
   3952     {
   3953         ecl.elon = RAD2DEG * atan2(ecl.ey, ecl.ex);
   3954         if (ecl.elon < 0.0)
   3955             ecl.elon += 360.0;
   3956     }
   3957     else
   3958         ecl.elon = 0.0;
   3959 
   3960     ecl.elat = RAD2DEG * atan2(ecl.ez, xyproj);
   3961     ecl.status = ASTRO_SUCCESS;
   3962     return ecl;
   3963 }
   3964 
   3965 static astro_func_result_t sun_offset(void *context, astro_time_t time)
   3966 {
   3967     astro_func_result_t result;
   3968     double targetLon = *((double *)context);
   3969     astro_ecliptic_t ecl = Astronomy_SunPosition(time);
   3970     if (ecl.status != ASTRO_SUCCESS)
   3971         return FuncError(ecl.status);
   3972     result.value = LongitudeOffset(ecl.elon - targetLon);
   3973     result.status = ASTRO_SUCCESS;
   3974     return result;
   3975 }
   3976 
   3977 /**
   3978  * @brief
   3979  *      Searches for the time when the Sun reaches an apparent ecliptic longitude as seen from the Earth.
   3980  *
   3981  * This function finds the moment in time, if any exists in the given time window,
   3982  * that the center of the Sun reaches a specific ecliptic longitude as seen from the center of the Earth.
   3983  *
   3984  * This function can be used to determine equinoxes and solstices.
   3985  * However, it is usually more convenient and efficient to call #Astronomy_Seasons
   3986  * to calculate all equinoxes and solstices for a given calendar year.
   3987  *
   3988  * The function searches the window of time specified by `startTime` and `startTime+limitDays`.
   3989  * The search will return an error if the Sun never reaches the longitude `targetLon` or
   3990  * if the window is so large that the longitude ranges more than 180 degrees within it.
   3991  * It is recommended to keep the window smaller than 10 days when possible.
   3992  *
   3993  * @param targetLon
   3994  *      The desired ecliptic longitude in degrees, relative to the true equinox of date.
   3995  *      This may be any value in the range [0, 360), although certain values have
   3996  *      conventional meanings:
   3997  *      0 = March equinox, 90 = June solstice, 180 = September equinox, 270 = December solstice.
   3998  *
   3999  * @param startTime
   4000  *      The date and time for starting the search for the desired longitude event.
   4001  *
   4002  * @param limitDays
   4003  *      The real-valued number of days, which when added to `startTime`, limits the
   4004  *      range of time over which the search looks.
   4005  *      It is recommended to keep this value between 1 and 10 days.
   4006  *      See function remarks for more details.
   4007  *
   4008  * @return
   4009  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   4010  *      and the `time` field will contain the date and time the Sun reaches the target longitude.
   4011  *      Any other value indicates an error.
   4012  *      See remarks in #Astronomy_Search (which this function calls) for more information about possible error codes.
   4013  */
   4014 astro_search_result_t Astronomy_SearchSunLongitude(
   4015     double targetLon,
   4016     astro_time_t startTime,
   4017     double limitDays)
   4018 {
   4019     astro_time_t t2 = Astronomy_AddDays(startTime, limitDays);
   4020     return Astronomy_Search(sun_offset, &targetLon, startTime, t2, 1.0);
   4021 }
   4022 
   4023 /** @cond DOXYGEN_SKIP */
   4024 #define CALLFUNC(f,t)  \
   4025     do { \
   4026         funcres = func(context, (t)); \
   4027         if (funcres.status != ASTRO_SUCCESS) return SearchError(funcres.status); \
   4028         (f) = funcres.value; \
   4029     } while(0)
   4030 /** @endcond */
   4031 
   4032 /**
   4033  * @brief Searches for a time at which a function's value increases through zero.
   4034  *
   4035  * Certain astronomy calculations involve finding a time when an event occurs.
   4036  * Often such events can be defined as the root of a function:
   4037  * the time at which the function's value becomes zero.
   4038  *
   4039  * `Astronomy_Search` finds the *ascending root* of a function: the time at which
   4040  * the function's value becomes zero while having a positive slope. That is, as time increases,
   4041  * the function transitions from a negative value, through zero at a specific moment,
   4042  * to a positive value later. The goal of the search is to find that specific moment.
   4043  *
   4044  * The search function is specified by two parameters: `func` and `context`.
   4045  * The `func` parameter is a pointer to the function itself, which accepts a time
   4046  * and a context containing any other arguments needed to evaluate the function.
   4047  * The `context` parameter supplies that context for the given search.
   4048  * As an example, a caller may wish to find the moment a celestial body reaches a certain
   4049  * ecliptic longitude. In that case, the caller might create a structure that contains
   4050  * an #astro_body_t member to specify the body and a `double` to hold the target longitude.
   4051  * The function would cast the pointer `context` passed in as a pointer to that structure type.
   4052  * It could subtract the target longitude from the actual longitude at a given time;
   4053  * thus the difference would equal zero at the moment in time the planet reaches the
   4054  * desired longitude.
   4055  *
   4056  * The `func` returns an #astro_func_result_t structure every time it is called.
   4057  * If the returned structure has a value of `status` other than `ASTRO_SUCCESS`,
   4058  * the search immediately fails and reports that same error code in the `status`
   4059  * returned by `Astronomy_Search`. Otherwise, `status` is `ASTRO_SUCCESS` and
   4060  * `value` is the value of the function, and the search proceeds until it either
   4061  * finds the ascending root or fails for some reason.
   4062  *
   4063  * The search calls `func` repeatedly to rapidly narrow in on any ascending
   4064  * root within the time window specified by `t1` and `t2`. The search never
   4065  * reports a solution outside this time window.
   4066  *
   4067  * `Astronomy_Search` uses a combination of bisection and quadratic interpolation
   4068  * to minimize the number of function calls. However, it is critical that the
   4069  * supplied time window be small enough that there cannot be more than one root
   4070  * (ascedning or descending) within it; otherwise the search can fail.
   4071  * Beyond that, it helps to make the time window as small as possible, ideally
   4072  * such that the function itself resembles a smooth parabolic curve within that window.
   4073  *
   4074  * If an ascending root is not found, or more than one root
   4075  * (ascending and/or descending) exists within the window `t1`..`t2`,
   4076  * the search will fail with status code `ASTRO_SEARCH_FAILURE`.
   4077  *
   4078  * If the search does not converge within 20 iterations, it will fail
   4079  * with status code `ASTRO_NO_CONVERGE`.
   4080  *
   4081  * @param func
   4082  *      The function for which to find the time of an ascending root.
   4083  *      See function remarks for more details.
   4084  *
   4085  * @param context
   4086  *      Any ancillary data needed by the function `func` to calculate a value.
   4087  *      The data type varies depending on the function passed in.
   4088  *      For example, the function may involve a specific celestial body that
   4089  *      must be specified somehow.
   4090  *
   4091  * @param t1
   4092  *      The lower time bound of the search window.
   4093  *      See function remarks for more details.
   4094  *
   4095  * @param t2
   4096  *      The upper time bound of the search window.
   4097  *      See function remarks for more details.
   4098  *
   4099  * @param dt_tolerance_seconds
   4100  *      Specifies an amount of time in seconds within which a bounded ascending root
   4101  *      is considered accurate enough to stop. A typical value is 1 second.
   4102  *
   4103  * @return
   4104  *      If successful, the returned structure has `status` equal to `ASTRO_SUCCESS`
   4105  *      and `time` set to a value within `dt_tolerance_seconds` of an ascending root.
   4106  *      On success, the `time` value will always be in the inclusive range [`t1`, `t2`].
   4107  *      If the search fails, `status` will be set to a value other than `ASTRO_SUCCESS`.
   4108  *      See function remarks for more details.
   4109  */
   4110 astro_search_result_t Astronomy_Search(
   4111     astro_search_func_t func,
   4112     void *context,
   4113     astro_time_t t1,
   4114     astro_time_t t2,
   4115     double dt_tolerance_seconds)
   4116 {
   4117     astro_search_result_t result;
   4118     astro_time_t tmid;
   4119     astro_time_t tq;
   4120     astro_func_result_t funcres;
   4121     double f1, f2, fmid=0.0, fq, dt_days, dt, dt_guess;
   4122     double q_x, q_ut, q_df_dt;
   4123     const int iter_limit = 20;
   4124     int iter = 0;
   4125     int calc_fmid = 1;
   4126 
   4127     dt_days = fabs(dt_tolerance_seconds / SECONDS_PER_DAY);
   4128     CALLFUNC(f1, t1);
   4129     CALLFUNC(f2, t2);
   4130 
   4131     for(;;)
   4132     {
   4133         if (++iter > iter_limit)
   4134             return SearchError(ASTRO_NO_CONVERGE);
   4135 
   4136         dt = (t2.tt - t1.tt) / 2.0;
   4137         tmid = Astronomy_AddDays(t1, dt);
   4138         if (fabs(dt) < dt_days)
   4139         {
   4140             /* We are close enough to the event to stop the search. */
   4141             result.time = tmid;
   4142             result.status = ASTRO_SUCCESS;
   4143             return result;
   4144         }
   4145 
   4146         if (calc_fmid)
   4147             CALLFUNC(fmid, tmid);
   4148         else
   4149             calc_fmid = 1;      /* we already have the correct value of fmid from the previous loop */
   4150 
   4151         /* Quadratic interpolation: */
   4152         /* Try to find a parabola that passes through the 3 points we have sampled: */
   4153         /* (t1,f1), (tmid,fmid), (t2,f2) */
   4154 
   4155         if (QuadInterp(tmid.ut, t2.ut - tmid.ut, f1, fmid, f2, &q_x, &q_ut, &q_df_dt))
   4156         {
   4157             tq = Astronomy_TimeFromDays(q_ut);
   4158             CALLFUNC(fq, tq);
   4159             if (q_df_dt != 0.0)
   4160             {
   4161                 dt_guess = fabs(fq / q_df_dt);
   4162                 if (dt_guess < dt_days)
   4163                 {
   4164                     /* The estimated time error is small enough that we can quit now. */
   4165                     result.time = tq;
   4166                     result.status = ASTRO_SUCCESS;
   4167                     return result;
   4168                 }
   4169 
   4170                 /* Try guessing a tighter boundary with the interpolated root at the center. */
   4171                 dt_guess *= 1.2;
   4172                 if (dt_guess < dt/10.0)
   4173                 {
   4174                     astro_time_t tleft = Astronomy_AddDays(tq, -dt_guess);
   4175                     astro_time_t tright = Astronomy_AddDays(tq, +dt_guess);
   4176                     if ((tleft.ut - t1.ut)*(tleft.ut - t2.ut) < 0)
   4177                     {
   4178                         if ((tright.ut - t1.ut)*(tright.ut - t2.ut) < 0)
   4179                         {
   4180                             double fleft, fright;
   4181                             CALLFUNC(fleft, tleft);
   4182                             CALLFUNC(fright, tright);
   4183                             if (fleft<0.0 && fright>=0.0)
   4184                             {
   4185                                 f1 = fleft;
   4186                                 f2 = fright;
   4187                                 t1 = tleft;
   4188                                 t2 = tright;
   4189                                 fmid = fq;
   4190                                 calc_fmid = 0;  /* save a little work -- no need to re-calculate fmid next time around the loop */
   4191                                 continue;
   4192                             }
   4193                         }
   4194                     }
   4195                 }
   4196             }
   4197         }
   4198 
   4199         /* After quadratic interpolation attempt. */
   4200         /* Now just divide the region in two parts and pick whichever one appears to contain a root. */
   4201         if (f1 < 0.0 && fmid >= 0.0)
   4202         {
   4203             t2 = tmid;
   4204             f2 = fmid;
   4205             continue;
   4206         }
   4207 
   4208         if (fmid < 0.0 && f2 >= 0.0)
   4209         {
   4210             t1 = tmid;
   4211             f1 = fmid;
   4212             continue;
   4213         }
   4214 
   4215         /* Either there is no ascending zero-crossing in this range */
   4216         /* or the search window is too wide (more than one zero-crossing). */
   4217         return SearchError(ASTRO_SEARCH_FAILURE);
   4218     }
   4219 }
   4220 
   4221 static int QuadInterp(
   4222     double tm, double dt, double fa, double fm, double fb,
   4223     double *out_x, double *out_t, double *out_df_dt)
   4224 {
   4225     double Q, R, S;
   4226     double u, ru, x1, x2;
   4227 
   4228     Q = (fb + fa)/2.0 - fm;
   4229     R = (fb - fa)/2.0;
   4230     S = fm;
   4231 
   4232     if (Q == 0.0)
   4233     {
   4234         /* This is a line, not a parabola. */
   4235         if (R == 0.0)
   4236             return 0;       /* This is a HORIZONTAL line... can't make progress! */
   4237         *out_x = -S / R;
   4238         if (*out_x < -1.0 || *out_x > +1.0)
   4239             return 0;   /* out of bounds */
   4240     }
   4241     else
   4242     {
   4243         /* This really is a parabola. Find roots x1, x2. */
   4244         u = R*R - 4*Q*S;
   4245         if (u <= 0.0)
   4246             return 0;   /* can't solve if imaginary, or if vertex of parabola is tangent. */
   4247 
   4248         ru = sqrt(u);
   4249         x1 = (-R + ru) / (2.0 * Q);
   4250         x2 = (-R - ru) / (2.0 * Q);
   4251         if (-1.0 <= x1 && x1 <= +1.0)
   4252         {
   4253             if (-1.0 <= x2 && x2 <= +1.0)
   4254                 return 0;   /* two roots are within bounds; we require a unique zero-crossing. */
   4255             *out_x = x1;
   4256         }
   4257         else if (-1.0 <= x2 && x2 <= +1.0)
   4258             *out_x = x2;
   4259         else
   4260             return 0;   /* neither root is within bounds */
   4261     }
   4262 
   4263     *out_t = tm + (*out_x)*dt;
   4264     *out_df_dt = (2*Q*(*out_x) + R) / dt;
   4265     return 1;   /* success */
   4266 }
   4267 
   4268 static astro_status_t FindSeasonChange(double targetLon, int year, int month, int day, astro_time_t *time)
   4269 {
   4270     astro_time_t startTime = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
   4271     astro_search_result_t result = Astronomy_SearchSunLongitude(targetLon, startTime, 4.0);
   4272     *time = result.time;
   4273     return result.status;
   4274 }
   4275 
   4276 /**
   4277  * @brief Finds both equinoxes and both solstices for a given calendar year.
   4278  *
   4279  * The changes of seasons are defined by solstices and equinoxes.
   4280  * Given a calendar year number, this function calculates the
   4281  * March and September equinoxes and the June and December solstices.
   4282  *
   4283  * The equinoxes are the moments twice each year when the plane of the
   4284  * Earth's equator passes through the center of the Sun. In other words,
   4285  * the Sun's declination is zero at both equinoxes.
   4286  * The March equinox defines the beginning of spring in the northern hemisphere
   4287  * and the beginning of autumn in the southern hemisphere.
   4288  * The September equinox defines the beginning of autumn in the northern hemisphere
   4289  * and the beginning of spring in the southern hemisphere.
   4290  *
   4291  * The solstices are the moments twice each year when one of the Earth's poles
   4292  * is most tilted toward the Sun. More precisely, the Sun's declination reaches
   4293  * its minimum value at the December solstice, which defines the beginning of
   4294  * winter in the northern hemisphere and the beginning of summer in the southern
   4295  * hemisphere. The Sun's declination reaches its maximum value at the June solstice,
   4296  * which defines the beginning of summer in the northern hemisphere and the beginning
   4297  * of winter in the southern hemisphere.
   4298  *
   4299  * @param year
   4300  *      The calendar year number for which to calculate equinoxes and solstices.
   4301  *      The value may be any integer, but only the years 1800 through 2100 have been
   4302  *      validated for accuracy: unit testing against data from the
   4303  *      United States Naval Observatory confirms that all equinoxes and solstices
   4304  *      for that range of years are within 2 minutes of the correct time.
   4305  *
   4306  * @return
   4307  *      The times of the four seasonal changes in the given calendar year.
   4308  *      This function should always succeed. However, to be safe, callers
   4309  *      should check the `status` field of the returned structure to make sure
   4310  *      it contains `ASTRO_SUCCESS`. Any failures indicate a bug in the algorithm
   4311  *      and should be [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
   4312  */
   4313 astro_seasons_t Astronomy_Seasons(int year)
   4314 {
   4315     astro_seasons_t seasons;
   4316     astro_status_t  status;
   4317 
   4318     seasons.status = ASTRO_SUCCESS;
   4319 
   4320     status = FindSeasonChange(  0, year,  3, 19, &seasons.mar_equinox);
   4321     if (status != ASTRO_SUCCESS) seasons.status = status;
   4322 
   4323     status = FindSeasonChange( 90, year,  6, 19, &seasons.jun_solstice);
   4324     if (status != ASTRO_SUCCESS) seasons.status = status;
   4325 
   4326     status = FindSeasonChange(180, year,  9, 21, &seasons.sep_equinox);
   4327     if (status != ASTRO_SUCCESS) seasons.status = status;
   4328 
   4329     status = FindSeasonChange(270, year, 12, 20, &seasons.dec_solstice);
   4330     if (status != ASTRO_SUCCESS) seasons.status = status;
   4331 
   4332     return seasons;
   4333 }
   4334 
   4335 /**
   4336  * @brief   Returns the angle between the given body and the Sun, as seen from the Earth.
   4337  *
   4338  * This function calculates the angular separation between the given body and the Sun,
   4339  * as seen from the center of the Earth. This angle is helpful for determining how
   4340  * easy it is to see the body away from the glare of the Sun.
   4341  *
   4342  * @param body
   4343  *      The celestial body whose angle from the Sun is to be measured.
   4344  *      Not allowed to be `BODY_EARTH`.
   4345  *
   4346  * @param time
   4347  *      The time at which the observation is made.
   4348  *
   4349  * @return
   4350  *      If successful, the returned structure contains `ASTRO_SUCCESS` in the `status` field
   4351  *      and `angle` holds the angle in degrees between the Sun and the specified body as
   4352  *      seen from the center of the Earth.
   4353  *      If an error occurs, the `status` field contains a value other than `ASTRO_SUCCESS`
   4354  *      that indicates the error condition.
   4355  */
   4356 astro_angle_result_t Astronomy_AngleFromSun(astro_body_t body, astro_time_t time)
   4357 {
   4358     astro_vector_t sv, bv;
   4359 
   4360     if (body == BODY_EARTH)
   4361         return AngleError(ASTRO_EARTH_NOT_ALLOWED);
   4362 
   4363     sv = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
   4364     if (sv.status != ASTRO_SUCCESS)
   4365         return AngleError(sv.status);
   4366 
   4367     bv = Astronomy_GeoVector(body, time, ABERRATION);
   4368     if (bv.status != ASTRO_SUCCESS)
   4369         return AngleError(bv.status);
   4370 
   4371     return AngleBetween(sv, bv);
   4372 }
   4373 
   4374 /**
   4375  * @brief
   4376  *      Determines visibility of a celestial body relative to the Sun, as seen from the Earth.
   4377  *
   4378  * This function returns an #astro_elongation_t structure, which provides the following
   4379  * information about the given celestial body at the given time:
   4380  *
   4381  * - `visibility` is an enumerated type that specifies whether the body is more easily seen
   4382  *    in the morning before sunrise, or in the evening after sunset.
   4383  *
   4384  * - `elongation` is the angle in degrees between two vectors: one from the center of the Earth to the
   4385  *    center of the Sun, the other from the center of the Earth to the center of the specified body.
   4386  *    This angle indicates how far away the body is from the glare of the Sun.
   4387  *    The elongation angle is always in the range [0, 180].
   4388  *
   4389  * - `ecliptic_separation` is the absolute value of the difference between the body's ecliptic longitude
   4390  *   and the Sun's ecliptic longitude, both as seen from the center of the Earth. This angle measures
   4391  *   around the plane of the Earth's orbit, and ignores how far above or below that plane the body is.
   4392  *   The ecliptic separation is measured in degrees and is always in the range [0, 180].
   4393  *
   4394  * @param body
   4395  *      The celestial body whose visibility is to be calculated.
   4396  *
   4397  * @param time
   4398  *      The date and time of the observation.
   4399  *
   4400  * @return
   4401  *      If successful, the `status` field in the returned structure contains `ASTRO_SUCCESS`
   4402  *      and all the other fields in the structure are valid. On failure, `status` contains
   4403  *      some other value as an error code and the other fields contain invalid values.
   4404  */
   4405 astro_elongation_t Astronomy_Elongation(astro_body_t body, astro_time_t time)
   4406 {
   4407     astro_elongation_t result;
   4408     astro_angle_result_t angres;
   4409 
   4410     angres = Astronomy_LongitudeFromSun(body, time);
   4411     if (angres.status != ASTRO_SUCCESS)
   4412         return ElongError(angres.status);
   4413 
   4414     if (angres.angle > 180.0)
   4415     {
   4416         result.visibility = VISIBLE_MORNING;
   4417         result.ecliptic_separation = 360.0 - angres.angle;
   4418     }
   4419     else
   4420     {
   4421         result.visibility = VISIBLE_EVENING;
   4422         result.ecliptic_separation = angres.angle;
   4423     }
   4424 
   4425     angres = Astronomy_AngleFromSun(body, time);
   4426     if (angres.status != ASTRO_SUCCESS)
   4427         return ElongError(angres.status);
   4428 
   4429     result.elongation = angres.angle;
   4430     result.time = time;
   4431     result.status = ASTRO_SUCCESS;
   4432 
   4433     return result;
   4434 }
   4435 
   4436 static astro_func_result_t neg_elong_slope(void *context, astro_time_t time)
   4437 {
   4438     static const double dt = 0.1;
   4439     astro_angle_result_t e1, e2;
   4440     astro_func_result_t result;
   4441     astro_body_t body = *((astro_body_t *)context);
   4442     astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
   4443     astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
   4444 
   4445     e1 = Astronomy_AngleFromSun(body, t1);
   4446     if (e1.status != ASTRO_SUCCESS)
   4447         return FuncError(e1.status);
   4448 
   4449     e2 = Astronomy_AngleFromSun(body, t2);
   4450     if (e2.status)
   4451         return FuncError(e2.status);
   4452 
   4453     result.value = (e1.angle - e2.angle)/dt;
   4454     result.status = ASTRO_SUCCESS;
   4455     return result;
   4456 }
   4457 
   4458 /**
   4459  * @brief
   4460  *      Finds a date and time when Mercury or Venus reaches its maximum angle from the Sun as seen from the Earth.
   4461  *
   4462  * Mercury and Venus are are often difficult to observe because they are closer to the Sun than the Earth is.
   4463  * Mercury especially is almost always impossible to see because it gets lost in the Sun's glare.
   4464  * The best opportunities for spotting Mercury, and the best opportunities for viewing Venus through
   4465  * a telescope without atmospheric interference, are when these planets reach maximum elongation.
   4466  * These are events where the planets reach the maximum angle from the Sun as seen from the Earth.
   4467  *
   4468  * This function solves for those times, reporting the next maximum elongation event's date and time,
   4469  * the elongation value itself, the relative longitude with the Sun, and whether the planet is best
   4470  * observed in the morning or evening. See #Astronomy_Elongation for more details about the returned structure.
   4471  *
   4472  * @param body
   4473  *      Either `BODY_MERCURY` or `BODY_VENUS`. Any other value will fail with the error `ASTRO_INVALID_BODY`.
   4474  *      To find the best viewing opportunites for planets farther from the Sun than the Earth is (Mars through Pluto)
   4475  *      use #Astronomy_SearchRelativeLongitude to find the next opposition event.
   4476  *
   4477  * @param startTime
   4478  *      The date and time at which to begin the search. The maximum elongation event found will always
   4479  *      be the first one that occurs after this date and time.
   4480  *
   4481  * @return
   4482  *      If successful, the `status` field of the returned structure will be `ASTRO_SUCCESS`
   4483  *      and the other structure fields will be valid. Otherwise, `status` will contain
   4484  *      some other value indicating an error.
   4485  */
   4486 astro_elongation_t Astronomy_SearchMaxElongation(astro_body_t body, astro_time_t startTime)
   4487 {
   4488     double s1, s2;
   4489     int iter;
   4490     astro_angle_result_t plon, elon;
   4491     astro_time_t t_start;
   4492     double rlon, rlon_lo, rlon_hi, adjust_days;
   4493     astro_func_result_t syn;
   4494     astro_search_result_t search1, search2, searchx;
   4495     astro_time_t t1, t2;
   4496     astro_func_result_t m1, m2;
   4497 
   4498     /* Determine the range of relative longitudes within which maximum elongation can occur for this planet. */
   4499     switch (body)
   4500     {
   4501     case BODY_MERCURY:
   4502         s1 = 50.0;
   4503         s2 = 85.0;
   4504         break;
   4505 
   4506     case BODY_VENUS:
   4507         s1 = 40.0;
   4508         s2 = 50.0;
   4509         break;
   4510 
   4511     default:
   4512         /* SearchMaxElongation works for Mercury and Venus only. */
   4513         return ElongError(ASTRO_INVALID_BODY);
   4514     }
   4515 
   4516     syn = SynodicPeriod(body);
   4517     if (syn.status != ASTRO_SUCCESS)
   4518         return ElongError(syn.status);
   4519 
   4520     iter = 0;
   4521     while (++iter <= 2)
   4522     {
   4523         plon = Astronomy_EclipticLongitude(body, startTime);
   4524         if (plon.status != ASTRO_SUCCESS)
   4525             return ElongError(plon.status);
   4526 
   4527         elon = Astronomy_EclipticLongitude(BODY_EARTH, startTime);
   4528         if (elon.status != ASTRO_SUCCESS)
   4529             return ElongError(elon.status);
   4530 
   4531         rlon = LongitudeOffset(plon.angle - elon.angle);    /* clamp to (-180, +180] */
   4532 
   4533         /* The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees */
   4534         /* because there is a cusp there that causes a discontinuity in the derivative. */
   4535         /* So we need to guard against searching near such times. */
   4536         if (rlon >= -s1 && rlon < +s1)
   4537         {
   4538             /* Seek to the window [+s1, +s2]. */
   4539             adjust_days = 0.0;
   4540             /* Search forward for the time t1 when rel lon = +s1. */
   4541             rlon_lo = +s1;
   4542             /* Search forward for the time t2 when rel lon = +s2. */
   4543             rlon_hi = +s2;
   4544         }
   4545         else if (rlon > +s2 || rlon < -s2)
   4546         {
   4547             /* Seek to the next search window at [-s2, -s1]. */
   4548             adjust_days = 0.0;
   4549             /* Search forward for the time t1 when rel lon = -s2. */
   4550             rlon_lo = -s2;
   4551             /* Search forward for the time t2 when rel lon = -s1. */
   4552             rlon_hi = -s1;
   4553         }
   4554         else if (rlon >= 0.0)
   4555         {
   4556             /* rlon must be in the middle of the window [+s1, +s2]. */
   4557             /* Search BACKWARD for the time t1 when rel lon = +s1. */
   4558             adjust_days = -syn.value / 4.0;
   4559             rlon_lo = +s1;
   4560             rlon_hi = +s2;
   4561             /* Search forward from t1 to find t2 such that rel lon = +s2. */
   4562         }
   4563         else
   4564         {
   4565             /* rlon must be in the middle of the window [-s2, -s1]. */
   4566             /* Search BACKWARD for the time t1 when rel lon = -s2. */
   4567             adjust_days = -syn.value / 4.0;
   4568             rlon_lo = -s2;
   4569             /* Search forward from t1 to find t2 such that rel lon = -s1. */
   4570             rlon_hi = -s1;
   4571         }
   4572 
   4573         t_start = Astronomy_AddDays(startTime, adjust_days);
   4574 
   4575         search1 = Astronomy_SearchRelativeLongitude(body, rlon_lo, t_start);
   4576         if (search1.status != ASTRO_SUCCESS)
   4577             return ElongError(search1.status);
   4578         t1 = search1.time;
   4579 
   4580         search2 = Astronomy_SearchRelativeLongitude(body, rlon_hi, t1);
   4581         if (search2.status != ASTRO_SUCCESS)
   4582             return ElongError(search2.status);
   4583         t2 = search2.time;
   4584 
   4585         /* Now we have a time range [t1,t2] that brackets a maximum elongation event. */
   4586         /* Confirm the bracketing. */
   4587         m1 = neg_elong_slope(&body, t1);
   4588         if (m1.status != ASTRO_SUCCESS)
   4589             return ElongError(m1.status);
   4590 
   4591         if (m1.value >= 0)
   4592             return ElongError(ASTRO_INTERNAL_ERROR);    /* there is a bug in the bracketing algorithm! */
   4593 
   4594         m2 = neg_elong_slope(&body, t2);
   4595         if (m2.status != ASTRO_SUCCESS)
   4596             return ElongError(m2.status);
   4597 
   4598         if (m2.value <= 0)
   4599             return ElongError(ASTRO_INTERNAL_ERROR);    /* there is a bug in the bracketing algorithm! */
   4600 
   4601         /* Use the generic search algorithm to home in on where the slope crosses from negative to positive. */
   4602         searchx = Astronomy_Search(neg_elong_slope, &body, t1, t2, 10.0);
   4603         if (searchx.status != ASTRO_SUCCESS)
   4604             return ElongError(searchx.status);
   4605 
   4606         if (searchx.time.tt >= startTime.tt)
   4607             return Astronomy_Elongation(body, searchx.time);
   4608 
   4609         /* This event is in the past (earlier than startTime). */
   4610         /* We need to search forward from t2 to find the next possible window. */
   4611         /* We never need to search more than twice. */
   4612         startTime = Astronomy_AddDays(t2, 1.0);
   4613     }
   4614 
   4615     return ElongError(ASTRO_SEARCH_FAILURE);
   4616 }
   4617 
   4618 /**
   4619  * @brief
   4620  *      Returns a body's ecliptic longitude with respect to the Sun, as seen from the Earth.
   4621  *
   4622  * This function can be used to determine where a planet appears around the ecliptic plane
   4623  * (the plane of the Earth's orbit around the Sun) as seen from the Earth,
   4624  * relative to the Sun's apparent position.
   4625  *
   4626  * The angle starts at 0 when the body and the Sun are at the same ecliptic longitude
   4627  * as seen from the Earth. The angle increases in the prograde direction
   4628  * (the direction that the planets orbit the Sun and the Moon orbits the Earth).
   4629  *
   4630  * When the angle is 180 degrees, it means the Sun and the body appear on opposite sides
   4631  * of the sky for an Earthly observer. When `body` is a planet whose orbit around the
   4632  * Sun is farther than the Earth's, 180 degrees indicates opposition. For the Moon,
   4633  * it indicates a full moon.
   4634  *
   4635  * The angle keeps increasing up to 360 degrees as the body's apparent prograde
   4636  * motion continues relative to the Sun. When the angle reaches 360 degrees, it starts
   4637  * over at 0 degrees.
   4638  *
   4639  * Values between 0 and 180 degrees indicate that the body is visible in the evening sky
   4640  * after sunset.  Values between 180 degrees and 360 degrees indicate that the body
   4641  * is visible in the morning sky before sunrise.
   4642  *
   4643  * @param body
   4644  *      The celestial body for which to find longitude from the Sun.
   4645  *
   4646  * @param time
   4647  *      The date and time of the observation.
   4648  *
   4649  * @return
   4650  *      On success, the `status` field in the returned structure holds `ASTRO_SUCCESS` and
   4651  *      the `angle` field holds a value in the range [0, 360).
   4652  *      On failure, the `status` field contains some other value indicating an error condition.
   4653  */
   4654 astro_angle_result_t Astronomy_LongitudeFromSun(astro_body_t body, astro_time_t time)
   4655 {
   4656     astro_vector_t sv, bv;
   4657     astro_ecliptic_t se, be;
   4658     astro_angle_result_t result;
   4659 
   4660     if (body == BODY_EARTH)
   4661         return AngleError(ASTRO_EARTH_NOT_ALLOWED);
   4662 
   4663     sv = Astronomy_GeoVector(BODY_SUN, time, NO_ABERRATION);
   4664     se = Astronomy_Ecliptic(sv);        /* checks for errors in sv */
   4665     if (se.status != ASTRO_SUCCESS)
   4666         return AngleError(se.status);
   4667 
   4668     bv = Astronomy_GeoVector(body, time, NO_ABERRATION);
   4669     be = Astronomy_Ecliptic(bv);        /* checks for errors in bv */
   4670     if (be.status != ASTRO_SUCCESS)
   4671         return AngleError(be.status);
   4672 
   4673     result.status = ASTRO_SUCCESS;
   4674     result.angle = NormalizeLongitude(be.elon - se.elon);
   4675     return result;
   4676 }
   4677 
   4678 /**
   4679  * @brief
   4680  *      Returns the Moon's phase as an angle from 0 to 360 degrees.
   4681  *
   4682  * This function determines the phase of the Moon using its apparent
   4683  * ecliptic longitude relative to the Sun, as seen from the center of the Earth.
   4684  * Certain values of the angle have conventional definitions:
   4685  *
   4686  * - 0 = new moon
   4687  * - 90 = first quarter
   4688  * - 180 = full moon
   4689  * - 270 = third quarter
   4690  *
   4691  * @param time
   4692  *      The date and time of the observation.
   4693  *
   4694  * @return
   4695  *      On success, the function returns the angle as described in the function remarks
   4696  *      in the `angle` field and `ASTRO_SUCCESS` in the `status` field.
   4697  *      The function should always succeed, but it is a good idea for callers to check
   4698  *      the `status` field in the returned structure.
   4699  *      Any other value in `status` indicates a failure that should be
   4700  *      [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
   4701  */
   4702 astro_angle_result_t Astronomy_MoonPhase(astro_time_t time)
   4703 {
   4704     return Astronomy_LongitudeFromSun(BODY_MOON, time);
   4705 }
   4706 
   4707 static astro_func_result_t moon_offset(void *context, astro_time_t time)
   4708 {
   4709     astro_func_result_t result;
   4710     double targetLon = *((double *)context);
   4711     astro_angle_result_t angres = Astronomy_MoonPhase(time);
   4712     if (angres.status != ASTRO_SUCCESS)
   4713         return FuncError(angres.status);
   4714     result.value = LongitudeOffset(angres.angle - targetLon);
   4715     result.status = ASTRO_SUCCESS;
   4716     return result;
   4717 }
   4718 
   4719 /**
   4720  * @brief
   4721  *      Searches for the time that the Moon reaches a specified phase.
   4722  *
   4723  * Lunar phases are conventionally defined in terms of the Moon's geocentric ecliptic
   4724  * longitude with respect to the Sun's geocentric ecliptic longitude.
   4725  * When the Moon and the Sun have the same longitude, that is defined as a new moon.
   4726  * When their longitudes are 180 degrees apart, that is defined as a full moon.
   4727  *
   4728  * This function searches for any value of the lunar phase expressed as an
   4729  * angle in degrees in the range [0, 360).
   4730  *
   4731  * If you want to iterate through lunar quarters (new moon, first quarter, full moon, third quarter)
   4732  * it is much easier to call the functions #Astronomy_SearchMoonQuarter and #Astronomy_NextMoonQuarter.
   4733  * This function is useful for finding general phase angles outside those four quarters.
   4734  *
   4735  * @param targetLon
   4736  *      The difference in geocentric longitude between the Sun and Moon
   4737  *      that specifies the lunar phase being sought. This can be any value
   4738  *      in the range [0, 360).  Certain values have conventional names:
   4739  *      0 = new moon, 90 = first quarter, 180 = full moon, 270 = third quarter.
   4740  *
   4741  * @param startTime
   4742  *      The beginning of the time window in which to search for the Moon reaching the specified phase.
   4743  *
   4744  * @param limitDays
   4745  *      The number of days after `startTime` that limits the time window for the search.
   4746  *
   4747  * @return
   4748  *      On success, the `status` field in the returned structure holds `ASTRO_SUCCESS` and
   4749  *      the `time` field holds the date and time when the Moon reaches the target longitude.
   4750  *      On failure, `status` holds some other value as an error code.
   4751  *      One possible error code is `ASTRO_NO_MOON_QUARTER` if `startTime` and `limitDays`
   4752  *      do not enclose the desired event. See remarks in #Astronomy_Search for other possible
   4753  *      error codes.
   4754  */
   4755 astro_search_result_t Astronomy_SearchMoonPhase(double targetLon, astro_time_t startTime, double limitDays)
   4756 {
   4757     /*
   4758         To avoid discontinuities in the moon_offset function causing problems,
   4759         we need to approximate when that function will next return 0.
   4760         We probe it with the start time and take advantage of the fact
   4761         that every lunar phase repeats roughly every 29.5 days.
   4762         There is a surprising uncertainty in the quarter timing,
   4763         due to the eccentricity of the moon's orbit.
   4764         I have seen up to 0.826 days away from the simple prediction.
   4765         To be safe, we take the predicted time of the event and search
   4766         +/-0.9 days around it (a 1.8-day wide window).
   4767         Return ASTRO_NO_MOON_QUARTER if the final result goes beyond limitDays after startTime.
   4768     */
   4769     const double uncertainty = 0.9;
   4770     astro_func_result_t funcres;
   4771     double ya, est_dt, dt1, dt2;
   4772     astro_time_t t1, t2;
   4773 
   4774     funcres = moon_offset(&targetLon, startTime);
   4775     if (funcres.status != ASTRO_SUCCESS)
   4776         return SearchError(funcres.status);
   4777 
   4778     ya = funcres.value;
   4779     if (ya > 0.0) ya -= 360.0;  /* force searching forward in time, not backward */
   4780     est_dt = -(MEAN_SYNODIC_MONTH * ya) / 360.0;
   4781     dt1 = est_dt - uncertainty;
   4782     if (dt1 > limitDays)
   4783         return SearchError(ASTRO_NO_MOON_QUARTER);    /* not possible for moon phase to occur within specified window (too short) */
   4784     dt2 = est_dt + uncertainty;
   4785     if (limitDays < dt2)
   4786         dt2 = limitDays;
   4787     t1 = Astronomy_AddDays(startTime, dt1);
   4788     t2 = Astronomy_AddDays(startTime, dt2);
   4789     return Astronomy_Search(moon_offset, &targetLon, t1, t2, 1.0);
   4790 }
   4791 
   4792 /**
   4793  * @brief
   4794  *      Finds the first lunar quarter after the specified date and time.
   4795  *
   4796  * A lunar quarter is one of the following four lunar phase events:
   4797  * new moon, first quarter, full moon, third quarter.
   4798  * This function finds the lunar quarter that happens soonest
   4799  * after the specified date and time.
   4800  *
   4801  * To continue iterating through consecutive lunar quarters, call this function once,
   4802  * followed by calls to #Astronomy_NextMoonQuarter as many times as desired.
   4803  *
   4804  * @param startTime
   4805  *      The date and time at which to start the search.
   4806  *
   4807  * @return
   4808  *      This function should always succeed, indicated by the `status` field
   4809  *      in the returned structure holding `ASTRO_SUCCESS`. Any other value indicates
   4810  *      an internal error, which should be [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
   4811  *      To be safe, calling code should always check the `status` field for errors.
   4812  */
   4813 astro_moon_quarter_t Astronomy_SearchMoonQuarter(astro_time_t startTime)
   4814 {
   4815     astro_moon_quarter_t mq;
   4816     astro_angle_result_t angres;
   4817     astro_search_result_t srchres;
   4818 
   4819     /* Determine what the next quarter phase will be. */
   4820     angres = Astronomy_MoonPhase(startTime);
   4821     if (angres.status != ASTRO_SUCCESS)
   4822         return MoonQuarterError(angres.status);
   4823 
   4824     mq.quarter = (1 + (int)floor(angres.angle / 90.0)) % 4;
   4825     srchres = Astronomy_SearchMoonPhase(90.0 * mq.quarter, startTime, 10.0);
   4826     if (srchres.status != ASTRO_SUCCESS)
   4827         return MoonQuarterError(srchres.status);
   4828 
   4829     mq.status = ASTRO_SUCCESS;
   4830     mq.time = srchres.time;
   4831     return mq;
   4832 }
   4833 
   4834 /**
   4835  * @brief
   4836  *      Continues searching for lunar quarters from a previous search.
   4837  *
   4838  * After calling #Astronomy_SearchMoonQuarter, this function can be called
   4839  * one or more times to continue finding consecutive lunar quarters.
   4840  * This function finds the next consecutive moon quarter event after the one passed in as the parameter `mq`.
   4841  *
   4842  * @param mq
   4843  *      A value returned by a prior call to #Astronomy_SearchMoonQuarter or #Astronomy_NextMoonQuarter.
   4844  *
   4845  * @return
   4846  *      If `mq` is valid, this function should always succeed, indicated by the `status` field
   4847  *      in the returned structure holding `ASTRO_SUCCESS`. Any other value indicates
   4848  *      an internal error, which (after confirming that `mq` is valid) should be
   4849  *      [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
   4850  *      To be safe, calling code should always check the `status` field for errors.
   4851  */
   4852 astro_moon_quarter_t Astronomy_NextMoonQuarter(astro_moon_quarter_t mq)
   4853 {
   4854     astro_time_t time;
   4855     astro_moon_quarter_t next_mq;
   4856 
   4857     if (mq.status != ASTRO_SUCCESS)
   4858         return MoonQuarterError(ASTRO_INVALID_PARAMETER);
   4859 
   4860     /* Skip 6 days past the previous found moon quarter to find the next one. */
   4861     /* This is less than the minimum possible increment. */
   4862     /* So far I have seen the interval well contained by the range (6.5, 8.3) days. */
   4863 
   4864     time = Astronomy_AddDays(mq.time, 6.0);
   4865     next_mq = Astronomy_SearchMoonQuarter(time);
   4866     if (next_mq.status == ASTRO_SUCCESS)
   4867     {
   4868         /* Verify that we found the expected moon quarter. */
   4869         if (next_mq.quarter != (1 + mq.quarter) % 4)
   4870             return MoonQuarterError(ASTRO_WRONG_MOON_QUARTER);  /* internal error! we found the wrong moon quarter */
   4871     }
   4872     return next_mq;
   4873 }
   4874 
   4875 static astro_func_result_t rlon_offset(astro_body_t body, astro_time_t time, int direction, double targetRelLon)
   4876 {
   4877     astro_func_result_t result;
   4878     astro_angle_result_t plon, elon;
   4879     double diff;
   4880 
   4881     plon = Astronomy_EclipticLongitude(body, time);
   4882     if (plon.status != ASTRO_SUCCESS)
   4883         return FuncError(plon.status);
   4884 
   4885     elon = Astronomy_EclipticLongitude(BODY_EARTH, time);
   4886     if (elon.status != ASTRO_SUCCESS)
   4887         return FuncError(elon.status);
   4888 
   4889     diff = direction * (elon.angle - plon.angle);
   4890     result.value = LongitudeOffset(diff - targetRelLon);
   4891     result.status = ASTRO_SUCCESS;
   4892     return result;
   4893 }
   4894 
   4895 /**
   4896  * @brief
   4897  *      Searches for the time when the Earth and another planet are separated by a specified angle
   4898  *      in ecliptic longitude, as seen from the Sun.
   4899  *
   4900  * A relative longitude is the angle between two bodies measured in the plane of the Earth's orbit
   4901  * (the ecliptic plane). The distance of the bodies above or below the ecliptic plane is ignored.
   4902  * If you imagine the shadow of the body cast onto the ecliptic plane, and the angle measured around
   4903  * that plane from one body to the other in the direction the planets orbit the Sun, you will get an
   4904  * angle somewhere between 0 and 360 degrees. This is the relative longitude.
   4905  *
   4906  * Given a planet other than the Earth in `body` and a time to start the search in `startTime`,
   4907  * this function searches for the next time that the relative longitude measured from the planet
   4908  * to the Earth is `targetRelLon`.
   4909  *
   4910  * Certain astronomical events are defined in terms of relative longitude between the Earth and another planet:
   4911  *
   4912  * - When the relative longitude is 0 degrees, it means both planets are in the same direction from the Sun.
   4913  *   For planets that orbit closer to the Sun (Mercury and Venus), this is known as *inferior conjunction*,
   4914  *   a time when the other planet becomes very difficult to see because of being lost in the Sun's glare.
   4915  *   (The only exception is in the rare event of a transit, when we see the silhouette of the planet passing
   4916  *   between the Earth and the Sun.)
   4917  *
   4918  * - When the relative longitude is 0 degrees and the other planet orbits farther from the Sun,
   4919  *   this is known as *opposition*.  Opposition is when the planet is closest to the Earth, and
   4920  *   also when it is visible for most of the night, so it is considered the best time to observe the planet.
   4921  *
   4922  * - When the relative longitude is 180 degrees, it means the other planet is on the opposite side of the Sun
   4923  *   from the Earth. This is called *superior conjunction*. Like inferior conjunction, the planet is
   4924  *   very difficult to see from the Earth. Superior conjunction is possible for any planet other than the Earth.
   4925  *
   4926  * @param body
   4927  *      A planet other than the Earth. If `body` is not a planet other than the Earth, an error occurs.
   4928  *
   4929  * @param targetRelLon
   4930  *      The desired relative longitude, expressed in degrees. Must be in the range [0, 360).
   4931  *
   4932  * @param startTime
   4933  *      The date and time at which to begin the search.
   4934  *
   4935  * @return
   4936  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   4937  *      and `time` will hold the date and time of the relative longitude event.
   4938  *      Otherwise `status` will hold some other value that indicates an error condition.
   4939  */
   4940 astro_search_result_t Astronomy_SearchRelativeLongitude(astro_body_t body, double targetRelLon, astro_time_t startTime)
   4941 {
   4942     astro_search_result_t result;
   4943     astro_func_result_t syn;
   4944     astro_func_result_t error_angle;
   4945     double prev_angle;
   4946     astro_time_t time;
   4947     int iter, direction;
   4948 
   4949     if (body == BODY_EARTH)
   4950         return SearchError(ASTRO_EARTH_NOT_ALLOWED);
   4951 
   4952     if (body == BODY_MOON || body == BODY_SUN)
   4953         return SearchError(ASTRO_INVALID_BODY);
   4954 
   4955     syn = SynodicPeriod(body);
   4956     if (syn.status != ASTRO_SUCCESS)
   4957         return SearchError(syn.status);
   4958 
   4959     direction = IsSuperiorPlanet(body) ? +1 : -1;
   4960 
   4961     /* Iterate until we converge on the desired event. */
   4962     /* Calculate the error angle, which will be a negative number of degrees, */
   4963     /* meaning we are "behind" the target relative longitude. */
   4964 
   4965     error_angle = rlon_offset(body, startTime, direction, targetRelLon);
   4966     if (error_angle.status != ASTRO_SUCCESS)
   4967         return SearchError(error_angle.status);
   4968 
   4969     if (error_angle.value > 0)
   4970         error_angle.value -= 360;    /* force searching forward in time */
   4971 
   4972     time = startTime;
   4973     for (iter = 0; iter < 100; ++iter)
   4974     {
   4975         /* Estimate how many days in the future (positive) or past (negative) */
   4976         /* we have to go to get closer to the target relative longitude. */
   4977         double day_adjust = (-error_angle.value/360.0) * syn.value;
   4978         time = Astronomy_AddDays(time, day_adjust);
   4979         if (fabs(day_adjust) * SECONDS_PER_DAY < 1.0)
   4980         {
   4981             result.time = time;
   4982             result.status = ASTRO_SUCCESS;
   4983             return result;
   4984         }
   4985 
   4986         prev_angle = error_angle.value;
   4987         error_angle = rlon_offset(body, time, direction, targetRelLon);
   4988         if (error_angle.status != ASTRO_SUCCESS)
   4989             return SearchError(error_angle.status);
   4990 
   4991         if (fabs(prev_angle) < 30.0 && (prev_angle != error_angle.value))
   4992         {
   4993             /* Improve convergence for Mercury/Mars (eccentric orbits) */
   4994             /* by adjusting the synodic period to more closely match the */
   4995             /* variable speed of both planets in this part of their respective orbits. */
   4996             double ratio = prev_angle / (prev_angle - error_angle.value);
   4997             if (ratio > 0.5 && ratio < 2.0)
   4998                 syn.value *= ratio;
   4999         }
   5000     }
   5001 
   5002     return SearchError(ASTRO_NO_CONVERGE);
   5003 }
   5004 
   5005 /**
   5006  * @brief
   5007  *      Searches for the time when a celestial body reaches a specified hour angle as seen by an observer on the Earth.
   5008  *
   5009  * The *hour angle* of a celestial body indicates its position in the sky with respect
   5010  * to the Earth's rotation. The hour angle depends on the location of the observer on the Earth.
   5011  * The hour angle is 0 when the body reaches its highest angle above the horizon in a given day.
   5012  * The hour angle increases by 1 unit for every sidereal hour that passes after that point, up
   5013  * to 24 sidereal hours when it reaches the highest point again. So the hour angle indicates
   5014  * the number of hours that have passed since the most recent time that the body has culminated,
   5015  * or reached its highest point.
   5016  *
   5017  * This function searches for the next time a celestial body reaches the given hour angle
   5018  * after the date and time specified by `startTime`.
   5019  * To find when a body culminates, pass 0 for `hourAngle`.
   5020  * To find when a body reaches its lowest point in the sky, pass 12 for `hourAngle`.
   5021  *
   5022  * Note that, especially close to the Earth's poles, a body as seen on a given day
   5023  * may always be above the horizon or always below the horizon, so the caller cannot
   5024  * assume that a culminating object is visible nor that an object is below the horizon
   5025  * at its minimum altitude.
   5026  *
   5027  * On success, the function reports the date and time, along with the horizontal coordinates
   5028  * of the body at that time, as seen by the given observer.
   5029  *
   5030  * @param body
   5031  *      The celestial body, which can the Sun, the Moon, or any planet other than the Earth.
   5032  *
   5033  * @param observer
   5034  *      Indicates a location on or near the surface of the Earth where the observer is located.
   5035  *      Call #Astronomy_MakeObserver to create an observer structure.
   5036  *
   5037  * @param hourAngle
   5038  *      An hour angle value in the range [0, 24) indicating the number of sidereal hours after the
   5039  *      body's most recent culmination.
   5040  *
   5041  * @param startTime
   5042  *      The date and time at which to start the search.
   5043  *
   5044  * @return
   5045  *      If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
   5046  *      and the other structure fields are valid. Otherwise, `status` holds some other value
   5047  *      that indicates an error condition.
   5048  */
   5049 astro_hour_angle_t Astronomy_SearchHourAngle(
   5050     astro_body_t body,
   5051     astro_observer_t observer,
   5052     double hourAngle,
   5053     astro_time_t startTime)
   5054 {
   5055     int iter = 0;
   5056     astro_time_t time;
   5057     astro_equatorial_t ofdate;
   5058     astro_hour_angle_t result;
   5059     double delta_sidereal_hours, delta_days, gast;
   5060 
   5061     if (body < MIN_BODY || body > MAX_BODY)
   5062         return HourAngleError(ASTRO_INVALID_BODY);
   5063 
   5064     if (body == BODY_EARTH)
   5065         return HourAngleError(ASTRO_EARTH_NOT_ALLOWED);
   5066 
   5067     if (hourAngle < 0.0 || hourAngle >= 24.0)
   5068         return HourAngleError(ASTRO_INVALID_PARAMETER);
   5069 
   5070     time = startTime;
   5071     for(;;)
   5072     {
   5073         ++iter;
   5074 
   5075         /* Calculate Greenwich Apparent Sidereal Time (GAST) at the given time. */
   5076         gast = sidereal_time(&time);
   5077 
   5078         /* Obtain equatorial coordinates of date for the body. */
   5079         ofdate = Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION);
   5080         if (ofdate.status != ASTRO_SUCCESS)
   5081             return HourAngleError(ofdate.status);
   5082 
   5083         /* Calculate the adjustment needed in sidereal time */
   5084         /* to bring the hour angle to the desired value. */
   5085 
   5086         delta_sidereal_hours = fmod((hourAngle + ofdate.ra - observer.longitude/15) - gast, 24.0);
   5087         if (iter == 1)
   5088         {
   5089             /* On the first iteration, always search forward in time. */
   5090             if (delta_sidereal_hours < 0)
   5091                 delta_sidereal_hours += 24;
   5092         }
   5093         else
   5094         {
   5095             /* On subsequent iterations, we make the smallest possible adjustment, */
   5096             /* either forward or backward in time. */
   5097             if (delta_sidereal_hours < -12.0)
   5098                 delta_sidereal_hours += 24.0;
   5099             else if (delta_sidereal_hours > +12.0)
   5100                 delta_sidereal_hours -= 24.0;
   5101         }
   5102 
   5103         /* If the error is tolerable (less than 0.1 seconds), the search has succeeded. */
   5104         if (fabs(delta_sidereal_hours) * 3600.0 < 0.1)
   5105         {
   5106             result.hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NORMAL);
   5107             result.time = time;
   5108             result.status = ASTRO_SUCCESS;
   5109             return result;
   5110         }
   5111 
   5112         /* We need to loop another time to get more accuracy. */
   5113         /* Update the terrestrial time (in solar days) adjusting by sidereal time (sidereal hours). */
   5114         delta_days = (delta_sidereal_hours / 24.0) * SOLAR_DAYS_PER_SIDEREAL_DAY;
   5115         time = Astronomy_AddDays(time, delta_days);
   5116     }
   5117 }
   5118 
   5119 /** @cond DOXYGEN_SKIP */
   5120 typedef struct
   5121 {
   5122     astro_body_t        body;
   5123     int                 direction;
   5124     astro_observer_t    observer;
   5125     double              body_radius_au;
   5126 }
   5127 context_peak_altitude_t;
   5128 /** @endcond */
   5129 
   5130 static astro_func_result_t peak_altitude(void *context, astro_time_t time)
   5131 {
   5132     astro_func_result_t result;
   5133     astro_equatorial_t ofdate;
   5134     astro_horizon_t hor;
   5135     const context_peak_altitude_t *p = context;
   5136 
   5137     /*
   5138         Return the angular altitude above or below the horizon
   5139         of the highest part (the peak) of the given object.
   5140         This is defined as the apparent altitude of the center of the body plus
   5141         the body's angular radius.
   5142         The 'direction' parameter controls whether the angle is measured
   5143         positive above the horizon or positive below the horizon,
   5144         depending on whether the caller wants rise times or set times, respectively.
   5145     */
   5146 
   5147     ofdate = Astronomy_Equator(p->body, &time, p->observer, EQUATOR_OF_DATE, ABERRATION);
   5148     if (ofdate.status != ASTRO_SUCCESS)
   5149         return FuncError(ofdate.status);
   5150 
   5151     /* We calculate altitude without refraction, then add fixed refraction near the horizon. */
   5152     /* This gives us the time of rise/set without the extra work. */
   5153     hor = Astronomy_Horizon(&time, p->observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
   5154     result.value = p->direction * (hor.altitude + RAD2DEG*(p->body_radius_au / ofdate.dist) + REFRACTION_NEAR_HORIZON);
   5155     result.status = ASTRO_SUCCESS;
   5156     return result;
   5157 }
   5158 
   5159 /**
   5160  * @brief
   5161  *      Searches for the next time a celestial body rises or sets as seen by an observer on the Earth.
   5162  *
   5163  * This function finds the next rise or set time of the Sun, Moon, or planet other than the Earth.
   5164  * Rise time is when the body first starts to be visible above the horizon.
   5165  * For example, sunrise is the moment that the top of the Sun first appears to peek above the horizon.
   5166  * Set time is the moment when the body appears to vanish below the horizon.
   5167  *
   5168  * This function corrects for typical atmospheric refraction, which causes celestial
   5169  * bodies to appear higher above the horizon than they would if the Earth had no atmosphere.
   5170  * It also adjusts for the apparent angular radius of the observed body (significant only for the Sun and Moon).
   5171  *
   5172  * Note that rise or set may not occur in every 24 hour period.
   5173  * For example, near the Earth's poles, there are long periods of time where
   5174  * the Sun stays below the horizon, never rising.
   5175  * Also, it is possible for the Moon to rise just before midnight but not set during the subsequent 24-hour day.
   5176  * This is because the Moon sets nearly an hour later each day due to orbiting the Earth a
   5177  * significant amount during each rotation of the Earth.
   5178  * Therefore callers must not assume that the function will always succeed.
   5179  *
   5180  * @param body
   5181  *      The Sun, Moon, or any planet other than the Earth.
   5182  *
   5183  * @param observer
   5184  *      The location where observation takes place.
   5185  *      You can create an observer structure by calling #Astronomy_MakeObserver.
   5186  *
   5187  * @param direction
   5188  *      Either `DIRECTION_RISE` to find a rise time or `DIRECTION_SET` to find a set time.
   5189  *
   5190  * @param startTime
   5191  *      The date and time at which to start the search.
   5192  *
   5193  * @param limitDays
   5194  *      Limits how many days to search for a rise or set time.
   5195  *      To limit a rise or set time to the same day, you can use a value of 1 day.
   5196  *      In cases where you want to find the next rise or set time no matter how far
   5197  *      in the future (for example, for an observer near the south pole), you can
   5198  *      pass in a larger value like 365.
   5199  *
   5200  * @return
   5201  *      On success, the `status` field in the returned structure contains `ASTRO_SUCCESS`
   5202  *      and the `time` field contains the date and time of the rise or set time as requested.
   5203  *      If the `status` field contains `ASTRO_SEARCH_FAILURE`, it means the rise or set
   5204  *      event does not occur within `limitDays` days of `startTime`. This is a normal condition,
   5205  *      not an error. Any other value of `status` indicates an error of some kind.
   5206  */
   5207 astro_search_result_t Astronomy_SearchRiseSet(
   5208     astro_body_t body,
   5209     astro_observer_t observer,
   5210     astro_direction_t direction,
   5211     astro_time_t startTime,
   5212     double limitDays)
   5213 {
   5214     context_peak_altitude_t context;
   5215     double ha_before, ha_after;
   5216     astro_time_t time_start, time_before;
   5217     astro_func_result_t alt_before, alt_after;
   5218     astro_hour_angle_t evt_before, evt_after;
   5219 
   5220     if (body == BODY_EARTH)
   5221         return SearchError(ASTRO_EARTH_NOT_ALLOWED);
   5222 
   5223     switch (direction)
   5224     {
   5225     case DIRECTION_RISE:
   5226         ha_before = 12.0;   /* minimum altitude (bottom) happens BEFORE the body rises. */
   5227         ha_after = 0.0;     /* maximum altitude (culmination) happens AFTER the body rises. */
   5228         break;
   5229 
   5230     case DIRECTION_SET:
   5231         ha_before = 0.0;    /* culmination happens BEFORE the body sets. */
   5232         ha_after = 12.0;    /* bottom happens AFTER the body sets. */
   5233         break;
   5234 
   5235     default:
   5236         return SearchError(ASTRO_INVALID_PARAMETER);
   5237     }
   5238 
   5239     /* Set up the context structure for the search function 'peak_altitude'. */
   5240     context.body = body;
   5241     context.direction = (int)direction;
   5242     context.observer = observer;
   5243     switch (body)
   5244     {
   5245     case BODY_SUN:  context.body_radius_au = SUN_RADIUS_AU;                 break;
   5246     case BODY_MOON: context.body_radius_au = MOON_EQUATORIAL_RADIUS_AU;     break;
   5247     default:        context.body_radius_au = 0.0;                           break;
   5248     }
   5249 
   5250     /*
   5251         See if the body is currently above/below the horizon.
   5252         If we are looking for next rise time and the body is below the horizon,
   5253         we use the current time as the lower time bound and the next culmination
   5254         as the upper bound.
   5255         If the body is above the horizon, we search for the next bottom and use it
   5256         as the lower bound and the next culmination after that bottom as the upper bound.
   5257         The same logic applies for finding set times, only we swap the hour angles.
   5258     */
   5259 
   5260     time_start = startTime;
   5261     alt_before = peak_altitude(&context, time_start);
   5262     if (alt_before.status != ASTRO_SUCCESS)
   5263         return SearchError(alt_before.status);
   5264 
   5265     if (alt_before.value > 0.0)
   5266     {
   5267         /* We are past the sought event, so we have to wait for the next "before" event (culm/bottom). */
   5268         evt_before = Astronomy_SearchHourAngle(body, observer, ha_before, time_start);
   5269         if (evt_before.status != ASTRO_SUCCESS)
   5270             return SearchError(evt_before.status);
   5271 
   5272         time_before = evt_before.time;
   5273 
   5274         alt_before = peak_altitude(&context, time_before);
   5275         if (alt_before.status != ASTRO_SUCCESS)
   5276             return SearchError(alt_before.status);
   5277     }
   5278     else
   5279     {
   5280         /* We are before or at the sought event, so we find the next "after" event (bottom/culm), */
   5281         /* and use the current time as the "before" event. */
   5282         time_before = time_start;
   5283     }
   5284 
   5285     evt_after = Astronomy_SearchHourAngle(body, observer, ha_after, time_before);
   5286     if (evt_after.status != ASTRO_SUCCESS)
   5287         return SearchError(evt_after.status);
   5288 
   5289     alt_after = peak_altitude(&context, evt_after.time);
   5290     if (alt_after.status != ASTRO_SUCCESS)
   5291         return SearchError(alt_after.status);
   5292 
   5293     for(;;)
   5294     {
   5295         if (alt_before.value <= 0.0 && alt_after.value > 0.0)
   5296         {
   5297             /* Search between evt_before and evt_after for the desired event. */
   5298             astro_search_result_t result = Astronomy_Search(peak_altitude, &context, time_before, evt_after.time, 1.0);
   5299 
   5300             /* ASTRO_SEARCH_FAILURE is a special error that indicates a normal lack of finding a solution. */
   5301             /* If successful, or any other error, return immediately. */
   5302             if (result.status != ASTRO_SEARCH_FAILURE)
   5303                 return result;
   5304         }
   5305 
   5306         /* If we didn't find the desired event, use evt_after.time to find the next before-event. */
   5307         evt_before = Astronomy_SearchHourAngle(body, observer, ha_before, evt_after.time);
   5308         if (evt_before.status != ASTRO_SUCCESS)
   5309             return SearchError(evt_before.status);
   5310 
   5311         evt_after = Astronomy_SearchHourAngle(body, observer, ha_after, evt_before.time);
   5312         if (evt_after.status != ASTRO_SUCCESS)
   5313             return SearchError(evt_after.status);
   5314 
   5315         if (evt_before.time.ut >= time_start.ut + limitDays)
   5316             return SearchError(ASTRO_SEARCH_FAILURE);
   5317 
   5318         time_before = evt_before.time;
   5319 
   5320         alt_before = peak_altitude(&context, evt_before.time);
   5321         if (alt_before.status != ASTRO_SUCCESS)
   5322             return SearchError(alt_before.status);
   5323 
   5324         alt_after = peak_altitude(&context, evt_after.time);
   5325         if (alt_after.status != ASTRO_SUCCESS)
   5326             return SearchError(alt_after.status);
   5327     }
   5328 }
   5329 
   5330 static double MoonMagnitude(double phase, double helio_dist, double geo_dist)
   5331 {
   5332     /* https://astronomy.stackexchange.com/questions/10246/is-there-a-simple-analytical-formula-for-the-lunar-phase-brightness-curve */
   5333     double rad = phase * DEG2RAD;
   5334     double rad2 = rad * rad;
   5335     double rad4 = rad2 * rad2;
   5336     double mag = -12.717 + 1.49*fabs(rad) + 0.0431*rad4;
   5337     double moon_mean_distance_au = 385000.6 / KM_PER_AU;
   5338     double geo_au = geo_dist / moon_mean_distance_au;
   5339     mag += 5*log10(helio_dist * geo_au);
   5340     return mag;
   5341 }
   5342 
   5343 static astro_status_t SaturnMagnitude(
   5344     double phase,
   5345     double helio_dist,
   5346     double geo_dist,
   5347     astro_vector_t gc,
   5348     astro_time_t time,
   5349     double *mag,
   5350     double *ring_tilt)
   5351 {
   5352     astro_ecliptic_t eclip;
   5353     double ir, Nr, lat, lon, tilt, sin_tilt;
   5354 
   5355     *mag = *ring_tilt = NAN;
   5356 
   5357     /* Based on formulas by Paul Schlyter found here: */
   5358     /* http://www.stjarnhimlen.se/comp/ppcomp.html#15 */
   5359 
   5360     /* We must handle Saturn's rings as a major component of its visual magnitude. */
   5361     /* Find geocentric ecliptic coordinates of Saturn. */
   5362     eclip = Astronomy_Ecliptic(gc);
   5363     if (eclip.status != ASTRO_SUCCESS)
   5364         return eclip.status;
   5365 
   5366     ir = DEG2RAD * 28.06;   /* tilt of Saturn's rings to the ecliptic, in radians */
   5367     Nr = DEG2RAD * (169.51 + (3.82e-5 * time.tt));    /* ascending node of Saturn's rings, in radians */
   5368 
   5369     /* Find tilt of Saturn's rings, as seen from Earth. */
   5370     lat = DEG2RAD * eclip.elat;
   5371     lon = DEG2RAD * eclip.elon;
   5372     tilt = asin(sin(lat)*cos(ir) - cos(lat)*sin(ir)*sin(lon-Nr));
   5373     sin_tilt = sin(fabs(tilt));
   5374 
   5375     *mag = -9.0 + 0.044*phase;
   5376     *mag += sin_tilt*(-2.6 + 1.2*sin_tilt);
   5377     *mag += 5.0 * log10(helio_dist * geo_dist);
   5378 
   5379     *ring_tilt = RAD2DEG * tilt;
   5380 
   5381     return ASTRO_SUCCESS;
   5382 }
   5383 
   5384 static astro_status_t VisualMagnitude(
   5385     astro_body_t body,
   5386     double phase,
   5387     double helio_dist,
   5388     double geo_dist,
   5389     double *mag)
   5390 {
   5391     /* For Mercury and Venus, see:  https://iopscience.iop.org/article/10.1086/430212 */
   5392     double c0, c1=0, c2=0, c3=0, x;
   5393     *mag = NAN;
   5394     switch (body)
   5395     {
   5396     case BODY_MERCURY:  c0 = -0.60, c1 = +4.98, c2 = -4.88, c3 = +3.02; break;
   5397     case BODY_VENUS:
   5398         if (phase < 163.6)
   5399             c0 = -4.47, c1 = +1.03, c2 = +0.57, c3 = +0.13;
   5400         else
   5401             c0 = 0.98, c1 = -1.02;
   5402         break;
   5403     case BODY_MARS:        c0 = -1.52, c1 = +1.60;   break;
   5404     case BODY_JUPITER:     c0 = -9.40, c1 = +0.50;   break;
   5405     case BODY_URANUS:      c0 = -7.19, c1 = +0.25;   break;
   5406     case BODY_NEPTUNE:     c0 = -6.87;               break;
   5407     case BODY_PLUTO:       c0 = -1.00, c1 = +4.00;   break;
   5408     default: return ASTRO_INVALID_BODY;
   5409     }
   5410 
   5411     x = phase / 100;
   5412     *mag = c0 + x*(c1 + x*(c2 + x*c3));
   5413     *mag += 5.0 * log10(helio_dist * geo_dist);
   5414     return ASTRO_SUCCESS;
   5415 }
   5416 
   5417 /**
   5418  * @brief
   5419  *      Finds visual magnitude, phase angle, and other illumination information about a celestial body.
   5420  *
   5421  * This function calculates information about how bright a celestial body appears from the Earth,
   5422  * reported as visual magnitude, which is a smaller (or even negative) number for brighter objects
   5423  * and a larger number for dimmer objects.
   5424  *
   5425  * For bodies other than the Sun, it reports a phase angle, which is the angle in degrees between
   5426  * the Sun and the Earth, as seen from the center of the body. Phase angle indicates what fraction
   5427  * of the body appears illuminated as seen from the Earth. For example, when the phase angle is
   5428  * near zero, it means the body appears "full" as seen from the Earth.  A phase angle approaching
   5429  * 180 degrees means the body appears as a thin crescent as seen from the Earth.  A phase angle
   5430  * of 90 degrees means the body appears "half full".
   5431  * For the Sun, the phase angle is always reported as 0; the Sun emits light rather than reflecting it,
   5432  * so it doesn't have a phase angle.
   5433  *
   5434  * When the body is Saturn, the returned structure contains a field `ring_tilt` that holds
   5435  * the tilt angle in degrees of Saturn's rings as seen from the Earth. A value of 0 means
   5436  * the rings appear edge-on, and are thus nearly invisible from the Earth. The `ring_tilt` holds
   5437  * 0 for all bodies other than Saturn.
   5438  *
   5439  * @param body
   5440  *      The Sun, Moon, or any planet other than the Earth.
   5441  *
   5442  * @param time
   5443  *      The date and time of the observation.
   5444  *
   5445  * @return
   5446  *      On success, the `status` field of the return structure holds `ASTRO_SUCCESS`
   5447  *      and the other structure fields are valid.
   5448  *      Any other value indicates an error, in which case the remaining structure fields are not valid.
   5449  */
   5450 astro_illum_t Astronomy_Illumination(astro_body_t body, astro_time_t time)
   5451 {
   5452     astro_vector_t earth;   /* vector from Sun to Earth */
   5453     astro_vector_t hc;      /* vector from Sun to body */
   5454     astro_vector_t gc;      /* vector from Earth to body */
   5455     double mag;             /* visual magnitude */
   5456     astro_angle_result_t phase;     /* phase angle in degrees between Earth and Sun as seen from body */
   5457     double helio_dist;      /* distance from Sun to body */
   5458     double geo_dist;        /* distance from Earth to body */
   5459     double ring_tilt = 0.0; /* Saturn's ring tilt (0 for all other bodies) */
   5460     astro_illum_t illum;
   5461     astro_status_t status;
   5462 
   5463     if (body == BODY_EARTH)
   5464         return IllumError(ASTRO_EARTH_NOT_ALLOWED);
   5465 
   5466     earth = CalcEarth(time);
   5467     if (earth.status != ASTRO_SUCCESS)
   5468         return IllumError(earth.status);
   5469 
   5470     if (body == BODY_SUN)
   5471     {
   5472         gc.status = ASTRO_SUCCESS;
   5473         gc.t = time;
   5474         gc.x = -earth.x;
   5475         gc.y = -earth.y;
   5476         gc.z = -earth.z;
   5477 
   5478         hc.status = ASTRO_SUCCESS;
   5479         hc.t = time;
   5480         hc.x = 0.0;
   5481         hc.y = 0.0;
   5482         hc.z = 0.0;
   5483 
   5484         /* The Sun emits light instead of reflecting it, */
   5485         /* so we report a placeholder phase angle of 0. */
   5486         phase.status = ASTRO_SUCCESS;
   5487         phase.angle = 0.0;
   5488     }
   5489     else
   5490     {
   5491         if (body == BODY_MOON)
   5492         {
   5493             /* For extra numeric precision, use geocentric Moon formula directly. */
   5494             gc = Astronomy_GeoMoon(time);
   5495             if (gc.status != ASTRO_SUCCESS)
   5496                 return IllumError(gc.status);
   5497 
   5498             hc.status = ASTRO_SUCCESS;
   5499             hc.t = time;
   5500             hc.x = earth.x + gc.x;
   5501             hc.y = earth.y + gc.y;
   5502             hc.z = earth.z + gc.z;
   5503         }
   5504         else
   5505         {
   5506             /* For planets, the heliocentric vector is more direct to calculate. */
   5507             hc = Astronomy_HelioVector(body, time);
   5508             if (hc.status != ASTRO_SUCCESS)
   5509                 return IllumError(hc.status);
   5510 
   5511             gc.status = ASTRO_SUCCESS;
   5512             gc.t = time;
   5513             gc.x = hc.x - earth.x;
   5514             gc.y = hc.y - earth.y;
   5515             gc.z = hc.z - earth.z;
   5516         }
   5517 
   5518         phase = AngleBetween(gc, hc);
   5519         if (phase.status != ASTRO_SUCCESS)
   5520             return IllumError(phase.status);
   5521     }
   5522 
   5523     geo_dist = Astronomy_VectorLength(gc);
   5524     helio_dist = Astronomy_VectorLength(hc);
   5525 
   5526     switch (body)
   5527     {
   5528     case BODY_SUN:
   5529         mag = -0.17 + 5.0*log10(geo_dist / AU_PER_PARSEC);
   5530         break;
   5531 
   5532     case BODY_MOON:
   5533         mag = MoonMagnitude(phase.angle, helio_dist, geo_dist);
   5534         break;
   5535 
   5536     case BODY_SATURN:
   5537         status = SaturnMagnitude(phase.angle, helio_dist, geo_dist, gc, time, &mag, &ring_tilt);
   5538         if (status != ASTRO_SUCCESS)
   5539             return IllumError(status);
   5540         break;
   5541 
   5542     default:
   5543         status = VisualMagnitude(body, phase.angle, helio_dist, geo_dist, &mag);
   5544         break;
   5545     }
   5546 
   5547     illum.status = ASTRO_SUCCESS;
   5548     illum.time = time;
   5549     illum.mag = mag;
   5550     illum.phase_angle = phase.angle;
   5551     illum.helio_dist = helio_dist;
   5552     illum.ring_tilt = ring_tilt;
   5553 
   5554     return illum;
   5555 }
   5556 
   5557 static astro_func_result_t mag_slope(void *context, astro_time_t time)
   5558 {
   5559     /*
   5560         The Search() function finds a transition from negative to positive values.
   5561         The derivative of magnitude y with respect to time t (dy/dt)
   5562         is negative as an object gets brighter, because the magnitude numbers
   5563         get smaller. At peak magnitude dy/dt = 0, then as the object gets dimmer,
   5564         dy/dt > 0.
   5565     */
   5566     static const double dt = 0.01;
   5567     astro_illum_t y1, y2;
   5568     astro_body_t body = *((astro_body_t *)context);
   5569     astro_time_t t1 = Astronomy_AddDays(time, -dt/2);
   5570     astro_time_t t2 = Astronomy_AddDays(time, +dt/2);
   5571     astro_func_result_t result;
   5572 
   5573     y1 = Astronomy_Illumination(body, t1);
   5574     if (y1.status != ASTRO_SUCCESS)
   5575         return FuncError(y1.status);
   5576 
   5577     y2 = Astronomy_Illumination(body, t2);
   5578     if (y2.status != ASTRO_SUCCESS)
   5579         return FuncError(y2.status);
   5580 
   5581     result.value = (y2.mag - y1.mag) / dt;
   5582     result.status = ASTRO_SUCCESS;
   5583     return result;
   5584 }
   5585 
   5586 /**
   5587  * @brief
   5588  *      Searches for the date and time Venus will next appear brightest as seen from the Earth.
   5589  *
   5590  * This function searches for the date and time Venus appears brightest as seen from the Earth.
   5591  * Currently only Venus is supported for the `body` parameter, though this could change in the future.
   5592  * Mercury's peak magnitude occurs at superior conjunction, when it is virtually impossible to see from the Earth,
   5593  * so peak magnitude events have little practical value for that planet.
   5594  * Planets other than Venus and Mercury reach peak magnitude at opposition, which can
   5595  * be found using #Astronomy_SearchRelativeLongitude.
   5596  * The Moon reaches peak magnitude at full moon, which can be found using
   5597  * #Astronomy_SearchMoonQuarter or #Astronomy_SearchMoonPhase.
   5598  * The Sun reaches peak magnitude at perihelion, which occurs each year in January.
   5599  * However, the difference is minor and has little practical value.
   5600  *
   5601  * @param body
   5602  *      Currently only `BODY_VENUS` is allowed. Any other value results in the error `ASTRO_INVALID_BODY`.
   5603  *      See function remarks for more details.
   5604  *
   5605  * @param startTime
   5606  *      The date and time to start searching for the next peak magnitude event.
   5607  *
   5608  * @return
   5609  *      See documentation about the return value from #Astronomy_Illumination.
   5610  */
   5611 astro_illum_t Astronomy_SearchPeakMagnitude(astro_body_t body, astro_time_t startTime)
   5612 {
   5613     /* s1 and s2 are relative longitudes within which peak magnitude of Venus can occur. */
   5614     static const double s1 = 10.0;
   5615     static const double s2 = 30.0;
   5616     int iter;
   5617     astro_angle_result_t plon, elon;
   5618     astro_search_result_t t1, t2, tx;
   5619     astro_func_result_t syn, m1, m2;
   5620     astro_time_t t_start;
   5621     double rlon, rlon_lo, rlon_hi, adjust_days;
   5622 
   5623     if (body != BODY_VENUS)
   5624         return IllumError(ASTRO_INVALID_BODY);
   5625 
   5626     iter = 0;
   5627     while (++iter <= 2)
   5628     {
   5629         /* Find current heliocentric relative longitude between the */
   5630         /* inferior planet and the Earth. */
   5631         plon = Astronomy_EclipticLongitude(body, startTime);
   5632         if (plon.status != ASTRO_SUCCESS)
   5633             return IllumError(plon.status);
   5634 
   5635         elon = Astronomy_EclipticLongitude(BODY_EARTH, startTime);
   5636         if (elon.status != ASTRO_SUCCESS)
   5637             return IllumError(elon.status);
   5638 
   5639         rlon = LongitudeOffset(plon.angle - elon.angle);    /* clamp to (-180, +180]. */
   5640 
   5641         /* The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees */
   5642         /* because there is a cusp there that causes a discontinuity in the derivative. */
   5643         /* So we need to guard against searching near such times. */
   5644 
   5645         if (rlon >= -s1 && rlon < +s1)
   5646         {
   5647             /* Seek to the window [+s1, +s2]. */
   5648             adjust_days = 0.0;
   5649             /* Search forward for the time t1 when rel lon = +s1. */
   5650             rlon_lo = +s1;
   5651             /* Search forward for the time t2 when rel lon = +s2. */
   5652             rlon_hi = +s2;
   5653         }
   5654         else if (rlon >= +s2 || rlon < -s2)
   5655         {
   5656             /* Seek to the next search window at [-s2, -s1]. */
   5657             adjust_days = 0.0;
   5658             /* Search forward for the time t1 when rel lon = -s2. */
   5659             rlon_lo = -s2;
   5660             /* Search forward for the time t2 when rel lon = -s1. */
   5661             rlon_hi = -s1;
   5662         }
   5663         else if (rlon >= 0)
   5664         {
   5665             /* rlon must be in the middle of the window [+s1, +s2]. */
   5666             /* Search BACKWARD for the time t1 when rel lon = +s1. */
   5667             syn = SynodicPeriod(body);
   5668             if (syn.status != ASTRO_SUCCESS)
   5669                 return IllumError(syn.status);
   5670             adjust_days = -syn.value / 4;
   5671             rlon_lo = +s1;
   5672             /* Search forward from t1 to find t2 such that rel lon = +s2. */
   5673             rlon_hi = +s2;
   5674         }
   5675         else
   5676         {
   5677             /* rlon must be in the middle of the window [-s2, -s1]. */
   5678             /* Search BACKWARD for the time t1 when rel lon = -s2. */
   5679             syn = SynodicPeriod(body);
   5680             if (syn.status != ASTRO_SUCCESS)
   5681                 return IllumError(syn.status);
   5682             adjust_days = -syn.value / 4;
   5683             rlon_lo = -s2;
   5684             /* Search forward from t1 to find t2 such that rel lon = -s1. */
   5685             rlon_hi = -s1;
   5686         }
   5687         t_start = Astronomy_AddDays(startTime, adjust_days);
   5688         t1 = Astronomy_SearchRelativeLongitude(body, rlon_lo, t_start);
   5689         if (t1.status != ASTRO_SUCCESS)
   5690             return IllumError(t1.status);
   5691         t2 = Astronomy_SearchRelativeLongitude(body, rlon_hi, t1.time);
   5692         if (t2.status != ASTRO_SUCCESS)
   5693             return IllumError(t2.status);
   5694 
   5695         /* Now we have a time range [t1,t2] that brackets a maximum magnitude event. */
   5696         /* Confirm the bracketing. */
   5697         m1 = mag_slope(&body, t1.time);
   5698         if (m1.status != ASTRO_SUCCESS)
   5699             return IllumError(m1.status);
   5700         if (m1.value >= 0.0)
   5701             return IllumError(ASTRO_INTERNAL_ERROR);    /* should never happen! */
   5702 
   5703         m2 = mag_slope(&body, t2.time);
   5704         if (m2.status != ASTRO_SUCCESS)
   5705             return IllumError(m2.status);
   5706         if (m2.value <= 0.0)
   5707             return IllumError(ASTRO_INTERNAL_ERROR);    /* should never happen! */
   5708 
   5709         /* Use the generic search algorithm to home in on where the slope crosses from negative to positive. */
   5710         tx = Astronomy_Search(mag_slope, &body, t1.time, t2.time, 10.0);
   5711         if (tx.status != ASTRO_SUCCESS)
   5712             return IllumError(tx.status);
   5713 
   5714         if (tx.time.tt >= startTime.tt)
   5715             return Astronomy_Illumination(body, tx.time);
   5716 
   5717         /* This event is in the past (earlier than startTime). */
   5718         /* We need to search forward from t2 to find the next possible window. */
   5719         /* We never need to search more than twice. */
   5720         startTime = Astronomy_AddDays(t2.time, 1.0);
   5721     }
   5722 
   5723     return IllumError(ASTRO_SEARCH_FAILURE);
   5724 }
   5725 
   5726 static double MoonDistance(astro_time_t t)
   5727 {
   5728     double lon, lat, dist;
   5729     CalcMoon(t.tt / 36525.0, &lon, &lat, &dist);
   5730     return dist;
   5731 }
   5732 
   5733 static astro_func_result_t moon_distance_slope(void *context, astro_time_t time)
   5734 {
   5735     static const double dt = 0.001;
   5736     astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
   5737     astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
   5738     double dist1, dist2;
   5739     int direction = *((int *)context);
   5740     astro_func_result_t result;
   5741 
   5742     dist1 = MoonDistance(t1);
   5743     dist2 = MoonDistance(t2);
   5744     result.value = direction * (dist2 - dist1) / dt;
   5745     result.status = ASTRO_SUCCESS;
   5746     return result;
   5747 }
   5748 
   5749 /**
   5750  * @brief
   5751  *      Finds the date and time of the Moon's closest distance (perigee)
   5752  *      or farthest distance (apogee) with respect to the Earth.
   5753  *
   5754  * Given a date and time to start the search in `startTime`, this function finds the
   5755  * next date and time that the center of the Moon reaches the closest or farthest point
   5756  * in its orbit with respect to the center of the Earth, whichever comes first
   5757  * after `startTime`.
   5758  *
   5759  * The closest point is called *perigee* and the farthest point is called *apogee*.
   5760  * The word *apsis* refers to either event.
   5761  *
   5762  * To iterate through consecutive alternating perigee and apogee events, call `Astronomy_SearchLunarApsis`
   5763  * once, then use the return value to call #Astronomy_NextLunarApsis. After that,
   5764  * keep feeding the previous return value from `Astronomy_NextLunarApsis` into another
   5765  * call of `Astronomy_NextLunarApsis` as many times as desired.
   5766  *
   5767  * @param startTime
   5768  *      The date and time at which to start searching for the next perigee or apogee.
   5769  *
   5770  * @return
   5771  *      If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`,
   5772  *      `time` holds the date and time of the next lunar apsis, `kind` holds either
   5773  *      `APSIS_PERICENTER` for perigee or `APSIS_APOCENTER` for apogee, and the distance
   5774  *      values `dist_au` (astronomical units) and `dist_km` (kilometers) are valid.
   5775  *      If the function fails, `status` holds some value other than `ASTRO_SUCCESS` that
   5776  *      indicates what went wrong, and the other structure fields are invalid.
   5777  */
   5778 astro_apsis_t Astronomy_SearchLunarApsis(astro_time_t startTime)
   5779 {
   5780     astro_time_t t1, t2;
   5781     astro_search_result_t search;
   5782     astro_func_result_t m1, m2;
   5783     int positive_direction = +1;
   5784     int negative_direction = -1;
   5785     const double increment = 5.0;   /* number of days to skip in each iteration */
   5786     astro_apsis_t result;
   5787     int iter;
   5788 
   5789     /*
   5790         Check the rate of change of the distance dr/dt at the start time.
   5791         If it is positive, the Moon is currently getting farther away,
   5792         so start looking for apogee.
   5793         Conversely, if dr/dt < 0, start looking for perigee.
   5794         Either way, the polarity of the slope will change, so the product will be negative.
   5795         Handle the crazy corner case of exactly touching zero by checking for m1*m2 <= 0.
   5796     */
   5797 
   5798     t1 = startTime;
   5799     m1 = moon_distance_slope(&positive_direction, t1);
   5800     if (m1.status != ASTRO_SUCCESS)
   5801         return ApsisError(m1.status);
   5802 
   5803     for (iter=0; iter * increment < 2.0 * MEAN_SYNODIC_MONTH; ++iter)
   5804     {
   5805         t2 = Astronomy_AddDays(t1, increment);
   5806         m2 = moon_distance_slope(&positive_direction, t2);
   5807         if (m2.status != ASTRO_SUCCESS)
   5808             return ApsisError(m2.status);
   5809 
   5810         if (m1.value * m2.value <= 0.0)
   5811         {
   5812             /* There is a change of slope polarity within the time range [t1, t2]. */
   5813             /* Therefore this time range contains an apsis. */
   5814             /* Figure out whether it is perigee or apogee. */
   5815 
   5816             if (m1.value < 0.0 || m2.value > 0.0)
   5817             {
   5818                 /* We found a minimum-distance event: perigee. */
   5819                 /* Search the time range for the time when the slope goes from negative to positive. */
   5820                 search = Astronomy_Search(moon_distance_slope, &positive_direction, t1, t2, 1.0);
   5821                 result.kind = APSIS_PERICENTER;
   5822             }
   5823             else if (m1.value > 0.0 || m2.value < 0.0)
   5824             {
   5825                 /* We found a maximum-distance event: apogee. */
   5826                 /* Search the time range for the time when the slope goes from positive to negative. */
   5827                 search = Astronomy_Search(moon_distance_slope, &negative_direction, t1, t2, 1.0);
   5828                 result.kind = APSIS_APOCENTER;
   5829             }
   5830             else
   5831             {
   5832                 /* This should never happen. It should not be possible for both slopes to be zero. */
   5833                 return ApsisError(ASTRO_INTERNAL_ERROR);
   5834             }
   5835 
   5836             if (search.status != ASTRO_SUCCESS)
   5837                 return ApsisError(search.status);
   5838 
   5839             result.status = ASTRO_SUCCESS;
   5840             result.time = search.time;
   5841             result.dist_au = MoonDistance(search.time);
   5842             result.dist_km = result.dist_au * KM_PER_AU;
   5843             return result;
   5844         }
   5845 
   5846         /* We have not yet found a slope polarity change. Keep searching. */
   5847         t1 = t2;
   5848         m1 = m2;
   5849     }
   5850 
   5851     /* It should not be possible to fail to find an apsis within 2 synodic months. */
   5852     return ApsisError(ASTRO_INTERNAL_ERROR);
   5853 }
   5854 
   5855 /**
   5856  * @brief
   5857  *      Finds the next lunar perigee or apogee event in a series.
   5858  *
   5859  * This function requires an #astro_apsis_t value obtained from a call
   5860  * to #Astronomy_SearchLunarApsis or `Astronomy_NextLunarApsis`. Given
   5861  * an apogee event, this function finds the next perigee event, and vice versa.
   5862  *
   5863  * See #Astronomy_SearchLunarApsis for more details.
   5864  *
   5865  * @param apsis
   5866  *      An apsis event obtained from a call to #Astronomy_SearchLunarApsis or `Astronomy_NextLunarApsis`.
   5867  *      See #Astronomy_SearchLunarApsis for more details.
   5868  *
   5869  * @return
   5870  *      Same as the return value for #Astronomy_SearchLunarApsis.
   5871  */
   5872 astro_apsis_t Astronomy_NextLunarApsis(astro_apsis_t apsis)
   5873 {
   5874     static const double skip = 11.0;    /* number of days to skip to start looking for next apsis event */
   5875     astro_apsis_t next;
   5876     astro_time_t time;
   5877 
   5878     if (apsis.status != ASTRO_SUCCESS)
   5879         return ApsisError(ASTRO_INVALID_PARAMETER);
   5880 
   5881     if (apsis.kind != APSIS_APOCENTER && apsis.kind != APSIS_PERICENTER)
   5882         return ApsisError(ASTRO_INVALID_PARAMETER);
   5883 
   5884     time = Astronomy_AddDays(apsis.time, skip);
   5885     next = Astronomy_SearchLunarApsis(time);
   5886     if (next.status == ASTRO_SUCCESS)
   5887     {
   5888         /* Verify that we found the opposite apsis from the previous one. */
   5889         if (next.kind + apsis.kind != 1)
   5890             return ApsisError(ASTRO_INTERNAL_ERROR);
   5891     }
   5892     return next;
   5893 }
   5894 
   5895 
   5896 /** @cond DOXYGEN_SKIP */
   5897 typedef struct
   5898 {
   5899     int direction;
   5900     astro_body_t body;
   5901 }
   5902 planet_distance_context_t;
   5903 /** @endcond */
   5904 
   5905 
   5906 static astro_func_result_t planet_distance_slope(void *context, astro_time_t time)
   5907 {
   5908     static const double dt = 0.001;
   5909     const planet_distance_context_t *pc = context;
   5910     astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
   5911     astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
   5912     astro_func_result_t dist1, dist2, result;
   5913 
   5914     dist1 = Astronomy_HelioDistance(pc->body, t1);
   5915     if (dist1.status != ASTRO_SUCCESS)
   5916         return dist1;
   5917 
   5918     dist2 = Astronomy_HelioDistance(pc->body, t2);
   5919     if (dist2.status != ASTRO_SUCCESS)
   5920         return dist2;
   5921 
   5922     result.value = pc->direction * (dist2.value - dist1.value) / dt;
   5923     result.status = ASTRO_SUCCESS;
   5924     return result;
   5925 }
   5926 
   5927 static astro_apsis_t PlanetExtreme(
   5928     astro_body_t body,
   5929     astro_apsis_kind_t kind,
   5930     astro_time_t start_time,
   5931     double dayspan)
   5932 {
   5933     astro_apsis_t apsis;
   5934     const double direction = (kind == APSIS_APOCENTER) ? +1.0 : -1.0;
   5935     const int npoints = 10;
   5936     int i, best_i;
   5937     double interval;
   5938     double dist, best_dist;
   5939     astro_time_t time;
   5940     astro_func_result_t result;
   5941 
   5942     for(;;)
   5943     {
   5944         interval = dayspan / (npoints - 1);
   5945 
   5946         if (interval < 1.0 / 1440.0)    /* iterate until uncertainty is less than one minute */
   5947         {
   5948             apsis.status = ASTRO_SUCCESS;
   5949             apsis.kind = kind;
   5950             apsis.time = Astronomy_AddDays(start_time, interval / 2.0);
   5951             result = Astronomy_HelioDistance(body, apsis.time);
   5952             if (result.status != ASTRO_SUCCESS)
   5953                 return ApsisError(result.status);
   5954             apsis.dist_au = result.value;
   5955             apsis.dist_km = apsis.dist_au * KM_PER_AU;
   5956             return apsis;
   5957         }
   5958 
   5959         best_i = -1;
   5960         best_dist = 0.0;
   5961         for (i=0; i < npoints; ++i)
   5962         {
   5963             time = Astronomy_AddDays(start_time, i * interval);
   5964             result = Astronomy_HelioDistance(body, time);
   5965             if (result.status != ASTRO_SUCCESS)
   5966                 return ApsisError(result.status);
   5967             dist = direction * result.value;
   5968             if (i==0 || dist > best_dist)
   5969             {
   5970                 best_i = i;
   5971                 best_dist = dist;
   5972             }
   5973         }
   5974 
   5975         /* Narrow in on the extreme point. */
   5976         start_time = Astronomy_AddDays(start_time, (best_i - 1) * interval);
   5977         dayspan = 2.0 * interval;
   5978     }
   5979 }
   5980 
   5981 
   5982 static astro_apsis_t BruteSearchPlanetApsis(astro_body_t body, astro_time_t startTime)
   5983 {
   5984     const int npoints = 100;
   5985     int i;
   5986     astro_time_t t1, t2, time, t_min, t_max;
   5987     double dist, max_dist, min_dist;
   5988     astro_apsis_t perihelion, aphelion;
   5989     double interval;
   5990     double period;
   5991     astro_func_result_t result;
   5992 
   5993     /*
   5994         Neptune is a special case for two reasons:
   5995         1. Its orbit is nearly circular (low orbital eccentricity).
   5996         2. It is so distant from the Sun that the orbital period is very long.
   5997         Put together, this causes wobbling of the Sun around the Solar System Barycenter (SSB)
   5998         to be so significant that there are 3 local minima in the distance-vs-time curve
   5999         near each apsis. Therefore, unlike for other planets, we can't use an optimized
   6000         algorithm for finding dr/dt = 0.
   6001         Instead, we use a dumb, brute-force algorithm of sampling and finding min/max
   6002         heliocentric distance.
   6003 
   6004         There is a similar problem in the TOP2013 model for Pluto:
   6005         Its position vector has high-frequency oscillations that confuse the
   6006         slope-based determination of apsides.
   6007     */
   6008 
   6009     /*
   6010         Rewind approximately 30 degrees in the orbit,
   6011         then search forward for 270 degrees.
   6012         This is a very cautious way to prevent missing an apsis.
   6013         Typically we will find two apsides, and we pick whichever
   6014         apsis is ealier, but after startTime.
   6015         Sample points around this orbital arc and find when the distance
   6016         is greatest and smallest.
   6017     */
   6018     period = PlanetOrbitalPeriod(body);
   6019     t1 = Astronomy_AddDays(startTime, period * ( -30.0 / 360.0));
   6020     t2 = Astronomy_AddDays(startTime, period * (+270.0 / 360.0));
   6021     t_min = t_max = t1;
   6022     min_dist = max_dist = -1.0;     /* prevent warning about uninitialized variables */
   6023     interval = (t2.ut - t1.ut) / (npoints - 1.0);
   6024 
   6025     for (i=0; i < npoints; ++i)
   6026     {
   6027         double ut = t1.ut + (i * interval);
   6028         time = Astronomy_TimeFromDays(ut);
   6029         result = Astronomy_HelioDistance(body, time);
   6030         if (result.status != ASTRO_SUCCESS)
   6031             return ApsisError(result.status);
   6032         dist = result.value;
   6033         if (i == 0)
   6034         {
   6035             max_dist = min_dist = dist;
   6036         }
   6037         else
   6038         {
   6039             if (dist > max_dist)
   6040             {
   6041                 max_dist = dist;
   6042                 t_max = time;
   6043             }
   6044             if (dist < min_dist)
   6045             {
   6046                 min_dist = dist;
   6047                 t_min = time;
   6048             }
   6049         }
   6050     }
   6051 
   6052     t1 = Astronomy_AddDays(t_min, -2 * interval);
   6053     perihelion = PlanetExtreme(body, APSIS_PERICENTER, t1, 4 * interval);
   6054 
   6055     t1 = Astronomy_AddDays(t_max, -2 * interval);
   6056     aphelion = PlanetExtreme(body, APSIS_APOCENTER, t1, 4 * interval);
   6057 
   6058     if (perihelion.status == ASTRO_SUCCESS && perihelion.time.tt >= startTime.tt)
   6059     {
   6060         if (aphelion.status == ASTRO_SUCCESS && aphelion.time.tt >= startTime.tt)
   6061         {
   6062             /* Perihelion and aphelion are both valid. Pick the one that comes first. */
   6063             if (aphelion.time.tt < perihelion.time.tt)
   6064                 return aphelion;
   6065         }
   6066         return perihelion;
   6067     }
   6068 
   6069     if (aphelion.status == ASTRO_SUCCESS && aphelion.time.tt >= startTime.tt)
   6070         return aphelion;
   6071 
   6072     return ApsisError(ASTRO_FAIL_APSIS);
   6073 }
   6074 
   6075 
   6076 /**
   6077  * @brief
   6078  *      Finds the date and time of a planet's perihelion (closest approach to the Sun)
   6079  *      or aphelion (farthest distance from the Sun) after a given time.
   6080  *
   6081  * Given a date and time to start the search in `startTime`, this function finds the
   6082  * next date and time that the center of the specified planet reaches the closest or farthest point
   6083  * in its orbit with respect to the center of the Sun, whichever comes first
   6084  * after `startTime`.
   6085  *
   6086  * The closest point is called *perihelion* and the farthest point is called *aphelion*.
   6087  * The word *apsis* refers to either event.
   6088  *
   6089  * To iterate through consecutive alternating perihelion and aphelion events,
   6090  * call `Astronomy_SearchPlanetApsis` once, then use the return value to call
   6091  * #Astronomy_NextPlanetApsis. After that, keep feeding the previous return value
   6092  * from `Astronomy_NextPlanetApsis` into another call of `Astronomy_NextPlanetApsis`
   6093  * as many times as desired.
   6094  *
   6095  * @param body
   6096  *      The planet for which to find the next perihelion/aphelion event.
   6097  *      Not allowed to be `BODY_SUN` or `BODY_MOON`.
   6098  *
   6099  * @param startTime
   6100  *      The date and time at which to start searching for the next perihelion or aphelion.
   6101  *
   6102  * @return
   6103  *      If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`,
   6104  *      `time` holds the date and time of the next planetary apsis, `kind` holds either
   6105  *      `APSIS_PERICENTER` for perihelion or `APSIS_APOCENTER` for aphelion, and the distance
   6106  *      values `dist_au` (astronomical units) and `dist_km` (kilometers) are valid.
   6107  *      If the function fails, `status` holds some value other than `ASTRO_SUCCESS` that
   6108  *      indicates what went wrong, and the other structure fields are invalid.
   6109  */
   6110 astro_apsis_t Astronomy_SearchPlanetApsis(astro_body_t body, astro_time_t startTime)
   6111 {
   6112     astro_time_t t1, t2;
   6113     astro_search_result_t search;
   6114     astro_func_result_t m1, m2;
   6115     planet_distance_context_t context;
   6116     astro_apsis_t result;
   6117     int iter;
   6118     double orbit_period_days;
   6119     double increment;   /* number of days to skip in each iteration */
   6120     astro_func_result_t dist;
   6121 
   6122     if (body == BODY_NEPTUNE || body == BODY_PLUTO)
   6123         return BruteSearchPlanetApsis(body, startTime);
   6124 
   6125     orbit_period_days = PlanetOrbitalPeriod(body);
   6126     if (orbit_period_days == 0.0)
   6127         return ApsisError(ASTRO_INVALID_BODY);      /* The body must be a planet. */
   6128 
   6129     increment = orbit_period_days / 6.0;
   6130 
   6131     context.body = body;
   6132 
   6133     t1 = startTime;
   6134     context.direction = +1;
   6135     m1 = planet_distance_slope(&context, t1);
   6136     if (m1.status != ASTRO_SUCCESS)
   6137         return ApsisError(m1.status);
   6138 
   6139     for (iter=0; iter * increment < 2.0 * orbit_period_days; ++iter)
   6140     {
   6141         t2 = Astronomy_AddDays(t1, increment);
   6142         context.direction = +1;
   6143         m2 = planet_distance_slope(&context, t2);
   6144         if (m2.status != ASTRO_SUCCESS)
   6145             return ApsisError(m2.status);
   6146 
   6147         if (m1.value * m2.value <= 0.0)
   6148         {
   6149             /* There is a change of slope polarity within the time range [t1, t2]. */
   6150             /* Therefore this time range contains an apsis. */
   6151             /* Figure out whether it is perihelion or aphelion. */
   6152 
   6153             if (m1.value < 0.0 || m2.value > 0.0)
   6154             {
   6155                 /* We found a minimum-distance event: perihelion. */
   6156                 /* Search the time range for the time when the slope goes from negative to positive. */
   6157                 context.direction = +1;
   6158                 result.kind = APSIS_PERICENTER;
   6159             }
   6160             else if (m1.value > 0.0 || m2.value < 0.0)
   6161             {
   6162                 /* We found a maximum-distance event: aphelion. */
   6163                 /* Search the time range for the time when the slope goes from positive to negative. */
   6164                 context.direction = -1;
   6165                 result.kind = APSIS_APOCENTER;
   6166             }
   6167             else
   6168             {
   6169                 /* This should never happen. It should not be possible for both slopes to be zero. */
   6170                 return ApsisError(ASTRO_INTERNAL_ERROR);
   6171             }
   6172 
   6173             search = Astronomy_Search(planet_distance_slope, &context, t1, t2, 1.0);
   6174             if (search.status != ASTRO_SUCCESS)
   6175                 return ApsisError(search.status);
   6176 
   6177             dist = Astronomy_HelioDistance(body, search.time);
   6178             if (dist.status != ASTRO_SUCCESS)
   6179                 return ApsisError(dist.status);
   6180 
   6181             result.status = ASTRO_SUCCESS;
   6182             result.time = search.time;
   6183             result.dist_au = dist.value;
   6184             result.dist_km = dist.value * KM_PER_AU;
   6185             return result;
   6186         }
   6187 
   6188         /* We have not yet found a slope polarity change. Keep searching. */
   6189         t1 = t2;
   6190         m1 = m2;
   6191     }
   6192 
   6193     /* It should not be possible to fail to find an apsis within 2 orbits. */
   6194     return ApsisError(ASTRO_INTERNAL_ERROR);
   6195 }
   6196 
   6197 /**
   6198  * @brief
   6199  *      Finds the next planetary perihelion or aphelion event in a series.
   6200  *
   6201  * This function requires an #astro_apsis_t value obtained from a call
   6202  * to #Astronomy_SearchPlanetApsis or `Astronomy_NextPlanetApsis`.
   6203  * Given an aphelion event, this function finds the next perihelion event, and vice versa.
   6204  *
   6205  * See #Astronomy_SearchPlanetApsis for more details.
   6206  *
   6207  * @param body
   6208  *      The planet for which to find the next perihelion/aphelion event.
   6209  *      Not allowed to be `BODY_SUN` or `BODY_MOON`.
   6210  *      Must match the body passed into the call that produced the `apsis` parameter.
   6211  *
   6212  * @param apsis
   6213  *      An apsis event obtained from a call to #Astronomy_SearchPlanetApsis or `Astronomy_NextPlanetApsis`.
   6214  *
   6215  * @return
   6216  *      Same as the return value for #Astronomy_SearchPlanetApsis.
   6217  */
   6218 astro_apsis_t Astronomy_NextPlanetApsis(astro_body_t body, astro_apsis_t apsis)
   6219 {
   6220     double skip;    /* number of days to skip to start looking for next apsis event */
   6221     astro_apsis_t next;
   6222     astro_time_t time;
   6223 
   6224     if (apsis.status != ASTRO_SUCCESS)
   6225         return ApsisError(ASTRO_INVALID_PARAMETER);
   6226 
   6227     if (apsis.kind != APSIS_APOCENTER && apsis.kind != APSIS_PERICENTER)
   6228         return ApsisError(ASTRO_INVALID_PARAMETER);
   6229 
   6230     skip = 0.25 * PlanetOrbitalPeriod(body);        /* skip 1/4 of an orbit before starting search again */
   6231     if (skip <= 0.0)
   6232         return ApsisError(ASTRO_INVALID_BODY);      /* body must be a planet */
   6233 
   6234     time = Astronomy_AddDays(apsis.time, skip);
   6235     next = Astronomy_SearchPlanetApsis(body, time);
   6236     if (next.status == ASTRO_SUCCESS)
   6237     {
   6238         /* Verify that we found the opposite apsis from the previous one. */
   6239         if (next.kind + apsis.kind != 1)
   6240             return ApsisError(ASTRO_INTERNAL_ERROR);
   6241     }
   6242     return next;
   6243 }
   6244 
   6245 
   6246 /**
   6247  * @brief Calculates the inverse of a rotation matrix.
   6248  *
   6249  * Given a rotation matrix that performs some coordinate transform,
   6250  * this function returns the matrix that reverses that trasnform.
   6251  *
   6252  * @param rotation
   6253  *      The rotation matrix to be inverted.
   6254  *
   6255  * @return
   6256  *      A rotation matrix that performs the opposite transformation.
   6257  */
   6258 astro_rotation_t Astronomy_InverseRotation(astro_rotation_t rotation)
   6259 {
   6260     astro_rotation_t inverse;
   6261 
   6262     if (rotation.status != ASTRO_SUCCESS)
   6263         return RotationErr(ASTRO_INVALID_PARAMETER);
   6264 
   6265     inverse.status = ASTRO_SUCCESS;
   6266     inverse.rot[0][0] = rotation.rot[0][0];
   6267     inverse.rot[0][1] = rotation.rot[1][0];
   6268     inverse.rot[0][2] = rotation.rot[2][0];
   6269     inverse.rot[1][0] = rotation.rot[0][1];
   6270     inverse.rot[1][1] = rotation.rot[1][1];
   6271     inverse.rot[1][2] = rotation.rot[2][1];
   6272     inverse.rot[2][0] = rotation.rot[0][2];
   6273     inverse.rot[2][1] = rotation.rot[1][2];
   6274     inverse.rot[2][2] = rotation.rot[2][2];
   6275 
   6276     return inverse;
   6277 }
   6278 
   6279 /**
   6280  * @brief Creates a rotation based on applying one rotation followed by another.
   6281  *
   6282  * Given two rotation matrices, returns a combined rotation matrix that is
   6283  * equivalent to rotating based on the first matrix, followed by the second.
   6284  *
   6285  * @param a
   6286  *      The first rotation to apply.
   6287  *
   6288  * @param b
   6289  *      The second rotation to apply.
   6290  *
   6291  * @return
   6292  *      The combined rotation matrix.
   6293  */
   6294 astro_rotation_t Astronomy_CombineRotation(astro_rotation_t a, astro_rotation_t b)
   6295 {
   6296     astro_rotation_t c;
   6297 
   6298     if (a.status != ASTRO_SUCCESS || b.status != ASTRO_SUCCESS)
   6299         return RotationErr(ASTRO_INVALID_PARAMETER);
   6300 
   6301     /*
   6302         Use matrix multiplication: c = b*a.
   6303         We put 'b' on the left and 'a' on the right because,
   6304         just like when you use a matrix M to rotate a vector V,
   6305         you put the M on the left in the product M*V.
   6306         We can think of this as 'b' rotating all the 3 column vectors in 'a'.
   6307     */
   6308     c.rot[0][0] = b.rot[0][0]*a.rot[0][0] + b.rot[1][0]*a.rot[0][1] + b.rot[2][0]*a.rot[0][2];
   6309     c.rot[1][0] = b.rot[0][0]*a.rot[1][0] + b.rot[1][0]*a.rot[1][1] + b.rot[2][0]*a.rot[1][2];
   6310     c.rot[2][0] = b.rot[0][0]*a.rot[2][0] + b.rot[1][0]*a.rot[2][1] + b.rot[2][0]*a.rot[2][2];
   6311     c.rot[0][1] = b.rot[0][1]*a.rot[0][0] + b.rot[1][1]*a.rot[0][1] + b.rot[2][1]*a.rot[0][2];
   6312     c.rot[1][1] = b.rot[0][1]*a.rot[1][0] + b.rot[1][1]*a.rot[1][1] + b.rot[2][1]*a.rot[1][2];
   6313     c.rot[2][1] = b.rot[0][1]*a.rot[2][0] + b.rot[1][1]*a.rot[2][1] + b.rot[2][1]*a.rot[2][2];
   6314     c.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];
   6315     c.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];
   6316     c.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];
   6317 
   6318     c.status = ASTRO_SUCCESS;
   6319     return c;
   6320 }
   6321 
   6322 /**
   6323  * @brief Converts spherical coordinates to Cartesian coordinates.
   6324  *
   6325  * Given spherical coordinates and a time at which they are valid,
   6326  * returns a vector of Cartesian coordinates. The returned value
   6327  * includes the time, as required by the type #astro_vector_t.
   6328  *
   6329  * @param sphere
   6330  *      Spherical coordinates to be converted.
   6331  *
   6332  * @param time
   6333  *      The time that should be included in the return value.
   6334  *
   6335  * @return
   6336  *      The vector form of the supplied spherical coordinates.
   6337  */
   6338 astro_vector_t Astronomy_VectorFromSphere(astro_spherical_t sphere, astro_time_t time)
   6339 {
   6340     astro_vector_t vector;
   6341     double radlat, radlon, rcoslat;
   6342 
   6343     if (sphere.status != ASTRO_SUCCESS)
   6344         return VecError(ASTRO_INVALID_PARAMETER, time);
   6345 
   6346     radlat = sphere.lat * DEG2RAD;
   6347     radlon = sphere.lon * DEG2RAD;
   6348     rcoslat = sphere.dist * cos(radlat);
   6349 
   6350     vector.status = ASTRO_SUCCESS;
   6351     vector.t = time;
   6352     vector.x = rcoslat * cos(radlon);
   6353     vector.y = rcoslat * sin(radlon);
   6354     vector.z = sphere.dist * sin(radlat);
   6355 
   6356     return vector;
   6357 }
   6358 
   6359 
   6360 /**
   6361  * @brief Converts Cartesian coordinates to spherical coordinates.
   6362  *
   6363  * Given a Cartesian vector, returns latitude, longitude, and distance.
   6364  *
   6365  * @param vector
   6366  *      Cartesian vector to be converted to spherical coordinates.
   6367  *
   6368  * @return
   6369  *      Spherical coordinates that are equivalent to the given vector.
   6370  */
   6371 astro_spherical_t Astronomy_SphereFromVector(astro_vector_t vector)
   6372 {
   6373     double xyproj;
   6374     astro_spherical_t sphere;
   6375 
   6376     xyproj = vector.x*vector.x + vector.y*vector.y;
   6377     sphere.dist = sqrt(xyproj + vector.z*vector.z);
   6378     if (xyproj == 0.0)
   6379     {
   6380         if (vector.z == 0.0)
   6381         {
   6382             /* Indeterminate coordinates; pos vector has zero length. */
   6383             return SphereError(ASTRO_INVALID_PARAMETER);
   6384         }
   6385 
   6386         sphere.lon = 0.0;
   6387         sphere.lat = (vector.z < 0.0) ? -90.0 : +90.0;
   6388     }
   6389     else
   6390     {
   6391         sphere.lon = RAD2DEG * atan2(vector.y, vector.x);
   6392         if (sphere.lon < 0.0)
   6393             sphere.lon += 360.0;
   6394 
   6395         sphere.lat = RAD2DEG * atan2(vector.z, sqrt(xyproj));
   6396     }
   6397 
   6398     sphere.status = ASTRO_SUCCESS;
   6399     return sphere;
   6400 }
   6401 
   6402 
   6403 /**
   6404  * @brief
   6405  *      Given angular equatorial coordinates in `equ`, calculates equatorial vector.
   6406  *
   6407  * @param equ
   6408  *      Angular equatorial coordinates to be converted to a vector.
   6409  *
   6410  * @param time
   6411  *      The date and time of the observation. This is needed because the returned
   6412  *      vector requires a valid time value when passed to certain other functions.
   6413  *
   6414  * @return
   6415  *      A vector in the equatorial system.
   6416  */
   6417 astro_vector_t Astronomy_VectorFromEquator(astro_equatorial_t equ, astro_time_t time)
   6418 {
   6419     astro_spherical_t sphere;
   6420 
   6421     if (equ.status != ASTRO_SUCCESS)
   6422         return VecError(ASTRO_INVALID_PARAMETER, time);
   6423 
   6424     sphere.status = ASTRO_SUCCESS;
   6425     sphere.lat = equ.dec;
   6426     sphere.lon = 15.0 * equ.ra;     /* convert sidereal hours to degrees */
   6427     sphere.dist = equ.dist;
   6428 
   6429     return Astronomy_VectorFromSphere(sphere, time);
   6430 }
   6431 
   6432 
   6433 /**
   6434  * @brief
   6435  *      Given an equatorial vector, calculates equatorial angular coordinates.
   6436  *
   6437  * @param vector
   6438  *      A vector in an equatorial coordinate system.
   6439  *
   6440  * @return
   6441  *      Angular coordinates expressed in the same equatorial system as `vector`.
   6442  */
   6443 astro_equatorial_t Astronomy_EquatorFromVector(astro_vector_t vector)
   6444 {
   6445     astro_equatorial_t equ;
   6446     astro_spherical_t sphere;
   6447 
   6448     sphere = Astronomy_SphereFromVector(vector);
   6449     if (sphere.status != ASTRO_SUCCESS)
   6450         return EquError(sphere.status);
   6451 
   6452     equ.status = ASTRO_SUCCESS;
   6453     equ.dec = sphere.lat;
   6454     equ.ra = sphere.lon / 15.0;     /* convert degrees to sidereal hours */
   6455     equ.dist = sphere.dist;
   6456 
   6457     return equ;
   6458 }
   6459 
   6460 
   6461 static double ToggleAzimuthDirection(double az)
   6462 {
   6463     az = 360.0 - az;
   6464     if (az >= 360.0)
   6465         az -= 360.0;
   6466     else if (az < 0.0)
   6467         az += 360.0;
   6468     return az;
   6469 }
   6470 
   6471 /**
   6472  * @brief Converts Cartesian coordinates to horizontal coordinates.
   6473  *
   6474  * Given a horizontal Cartesian vector, returns horizontal azimuth and altitude.
   6475  *
   6476  * *IMPORTANT:* This function differs from #Astronomy_SphereFromVector in two ways:
   6477  * - `Astronomy_SphereFromVector` returns a `lon` value that represents azimuth defined counterclockwise
   6478  *   from north (e.g., west = +90), but this function represents a clockwise rotation
   6479  *   (e.g., east = +90). The difference is because `Astronomy_SphereFromVector` is intended
   6480  *   to preserve the vector "right-hand rule", while this function defines azimuth in a more
   6481  *   traditional way as used in navigation and cartography.
   6482  * - This function optionally corrects for atmospheric refraction, while `Astronomy_SphereFromVector`
   6483  *   does not.
   6484  *
   6485  * The returned structure contains the azimuth in `lon`.
   6486  * It is measured in degrees clockwise from north: east = +90 degrees, west = +270 degrees.
   6487  *
   6488  * The altitude is stored in `lat`.
   6489  *
   6490  * The distance to the observed object is stored in `dist`,
   6491  * and is expressed in astronomical units (AU).
   6492  *
   6493  * @param vector
   6494  *      Cartesian vector to be converted to horizontal coordinates.
   6495  *
   6496  * @param refraction
   6497  *      `REFRACTION_NORMAL`: correct altitude for atmospheric refraction (recommended).
   6498  *      `REFRACTION_NONE`: no atmospheric refraction correction is performed.
   6499  *      `REFRACTION_JPLHOR`: for JPL Horizons compatibility testing only; not recommended for normal use.
   6500  *
   6501  * @return
   6502  *      If successful, `status` holds `ASTRO_SUCCESS` and the other fields are valid as described
   6503  *      in the function remarks.
   6504  *      Otherwise `status` holds an error code and the other fields are undefined.
   6505  */
   6506 astro_spherical_t Astronomy_HorizonFromVector(astro_vector_t vector, astro_refraction_t refraction)
   6507 {
   6508     astro_spherical_t sphere;
   6509 
   6510     sphere = Astronomy_SphereFromVector(vector);
   6511     if (sphere.status == ASTRO_SUCCESS)
   6512     {
   6513         /* Convert azimuth from counterclockwise-from-north to clockwise-from-north. */
   6514         sphere.lon = ToggleAzimuthDirection(sphere.lon);
   6515         sphere.lat += Astronomy_Refraction(refraction, sphere.lat);
   6516     }
   6517 
   6518     return sphere;
   6519 }
   6520 
   6521 
   6522 /**
   6523  * @brief
   6524  *      Given apparent angular horizontal coordinates in `sphere`, calculate horizontal vector.
   6525  *
   6526  * @param sphere
   6527  *      A structure that contains apparent horizontal coordinates:
   6528  *      `lat` holds the refracted azimuth angle,
   6529  *      `lon` holds the azimuth in degrees clockwise from north,
   6530  *      and `dist` holds the distance from the observer to the object in AU.
   6531  *
   6532  * @param time
   6533  *      The date and time of the observation. This is needed because the returned
   6534  *      #astro_vector_t structure requires a valid time value when passed to certain other functions.
   6535  *
   6536  * @param refraction
   6537  *      The refraction option used to model atmospheric lensing. See #Astronomy_Refraction.
   6538  *      This specifies how refraction is to be removed from the altitude stored in `sphere.lat`.
   6539  *
   6540  * @return
   6541  *      A vector in the horizontal system: `x` = north, `y` = west, and `z` = zenith (up).
   6542  */
   6543 astro_vector_t Astronomy_VectorFromHorizon(astro_spherical_t sphere, astro_time_t time, astro_refraction_t refraction)
   6544 {
   6545     if (sphere.status != ASTRO_SUCCESS)
   6546         return VecError(ASTRO_INVALID_PARAMETER, time);
   6547 
   6548     /* Convert azimuth from clockwise-from-north to counterclockwise-from-north. */
   6549     sphere.lon = ToggleAzimuthDirection(sphere.lon);
   6550 
   6551     /* Reverse any applied refraction. */
   6552     sphere.lat += Astronomy_InverseRefraction(refraction, sphere.lat);
   6553 
   6554     return Astronomy_VectorFromSphere(sphere, time);
   6555 }
   6556 
   6557 
   6558 /**
   6559  * @brief
   6560  *      Calculates the amount of "lift" to an altitude angle caused by atmospheric refraction.
   6561  *
   6562  * Given an altitude angle and a refraction option, calculates
   6563  * the amount of "lift" caused by atmospheric refraction.
   6564  * This is the number of degrees higher in the sky an object appears
   6565  * due to the lensing of the Earth's atmosphere.
   6566  *
   6567  * @param refraction
   6568  *      The option selecting which refraction correction to use.
   6569  *      If `REFRACTION_NORMAL`, uses a well-behaved refraction model that works well for
   6570  *      all valid values (-90 to +90) of `altitude`.
   6571  *      If `REFRACTION_JPLHOR`, this function returns a compatible value with the JPL Horizons tool.
   6572  *      If any other value (including `REFRACTION_NONE`), this function returns 0.
   6573  *
   6574  * @param altitude
   6575  *      An altitude angle in a horizontal coordinate system. Must be a value between -90 and +90.
   6576  *
   6577  * @return
   6578  *      The angular adjustment in degrees to be added to the altitude angle to correct for atmospheric lensing.
   6579  */
   6580 double Astronomy_Refraction(astro_refraction_t refraction, double altitude)
   6581 {
   6582     double refr, hd;
   6583 
   6584     if (altitude < -90.0 || altitude > +90.0)
   6585         return 0.0;     /* no attempt to correct an invalid altitude */
   6586 
   6587     if (refraction == REFRACTION_NORMAL || refraction == REFRACTION_JPLHOR)
   6588     {
   6589         // http://extras.springer.com/1999/978-1-4471-0555-8/chap4/horizons/horizons.pdf
   6590         // JPL Horizons says it uses refraction algorithm from
   6591         // Meeus "Astronomical Algorithms", 1991, p. 101-102.
   6592         // I found the following Go implementation:
   6593         // https://github.com/soniakeys/meeus/blob/master/v3/refraction/refract.go
   6594         // This is a translation from the function "Saemundsson" there.
   6595         // I found experimentally that JPL Horizons clamps the angle to 1 degree below the horizon.
   6596         // This is important because the 'refr' formula below goes crazy near hd = -5.11.
   6597         hd = altitude;
   6598         if (hd < -1.0)
   6599             hd = -1.0;
   6600 
   6601         refr = (1.02 / tan((hd+10.3/(hd+5.11))*DEG2RAD)) / 60.0;
   6602 
   6603         if (refraction == REFRACTION_NORMAL && altitude < -1.0)
   6604         {
   6605             // In "normal" mode we gradually reduce refraction toward the nadir
   6606             // so that we never get an altitude angle less than -90 degrees.
   6607             // When horizon angle is -1 degrees, the factor is exactly 1.
   6608             // As altitude approaches -90 (the nadir), the fraction approaches 0 linearly.
   6609             refr *= (altitude + 90.0) / 89.0;
   6610         }
   6611     }
   6612     else
   6613     {
   6614         /* No refraction, or the refraction option is invalid. */
   6615         refr = 0.0;
   6616     }
   6617 
   6618     return refr;
   6619 }
   6620 
   6621 
   6622 /**
   6623  * @brief
   6624  *      Calculates the inverse of an atmospheric refraction angle.
   6625  *
   6626  * Given an observed altitude angle that includes atmospheric refraction,
   6627  * calculate the negative angular correction to obtain the unrefracted
   6628  * altitude. This is useful for cases where observed horizontal
   6629  * coordinates are to be converted to another orientation system,
   6630  * but refraction first must be removed from the observed position.
   6631  *
   6632  * @param refraction
   6633  *      The option selecting which refraction correction to use.
   6634  *      See #Astronomy_Refraction.
   6635  *
   6636  * @param bent_altitude
   6637  *      The apparent altitude that includes atmospheric refraction.
   6638  *
   6639  * @return
   6640  *      The angular adjustment in degrees to be added to the
   6641  *      altitude angle to correct for atmospheric lensing.
   6642  *      This will be less than or equal to zero.
   6643  */
   6644 double Astronomy_InverseRefraction(astro_refraction_t refraction, double bent_altitude)
   6645 {
   6646     double altitude, diff;
   6647 
   6648     if (bent_altitude < -90.0 || bent_altitude > +90.0)
   6649         return 0.0;     /* no attempt to correct an invalid altitude */
   6650 
   6651     /* Find the pre-adjusted altitude whose refraction correction leads to 'altitude'. */
   6652     altitude = bent_altitude - Astronomy_Refraction(refraction, bent_altitude);
   6653     for(;;)
   6654     {
   6655         /* See how close we got. */
   6656         diff = (altitude + Astronomy_Refraction(refraction, altitude)) - bent_altitude;
   6657         if (fabs(diff) < 1.0e-14)
   6658             return altitude - bent_altitude;
   6659 
   6660         altitude -= diff;
   6661     }
   6662 }
   6663 
   6664 /**
   6665  * @brief
   6666  *      Applies a rotation to a vector, yielding a rotated vector.
   6667  *
   6668  * This function transforms a vector in one orientation to a vector
   6669  * in another orientation.
   6670  *
   6671  * @param rotation
   6672  *      A rotation matrix that specifies how the orientation of the vector is to be changed.
   6673  *
   6674  * @param vector
   6675  *      The vector whose orientation is to be changed.
   6676  *
   6677  * @return
   6678  *      A vector in the orientation specified by `rotation`.
   6679  */
   6680 astro_vector_t Astronomy_RotateVector(astro_rotation_t rotation, astro_vector_t vector)
   6681 {
   6682     astro_vector_t target;
   6683 
   6684     if (rotation.status != ASTRO_SUCCESS || vector.status != ASTRO_SUCCESS)
   6685         return VecError(ASTRO_INVALID_PARAMETER, vector.t);
   6686 
   6687     target.status = ASTRO_SUCCESS;
   6688     target.t = vector.t;
   6689     target.x = rotation.rot[0][0]*vector.x + rotation.rot[1][0]*vector.y + rotation.rot[2][0]*vector.z;
   6690     target.y = rotation.rot[0][1]*vector.x + rotation.rot[1][1]*vector.y + rotation.rot[2][1]*vector.z;
   6691     target.z = rotation.rot[0][2]*vector.x + rotation.rot[1][2]*vector.y + rotation.rot[2][2]*vector.z;
   6692 
   6693     return target;
   6694 }
   6695 
   6696 
   6697 /**
   6698  * @brief
   6699  *      Calculates a rotation matrix from equatorial J2000 (EQJ) to ecliptic J2000 (ECL).
   6700  *
   6701  * This is one of the family of functions that returns a rotation matrix
   6702  * for converting from one orientation to another.
   6703  * Source: EQJ = equatorial system, using equator at J2000 epoch.
   6704  * Target: ECL = ecliptic system, using equator at J2000 epoch.
   6705  *
   6706  * @return
   6707  *      A rotation matrix that converts EQJ to ECL.
   6708  */
   6709 astro_rotation_t Astronomy_Rotation_EQJ_ECL(void)
   6710 {
   6711     /* ob = mean obliquity of the J2000 ecliptic = 0.40909260059599012 radians. */
   6712     static const double c = 0.9174821430670688;    /* cos(ob) */
   6713     static const double s = 0.3977769691083922;    /* sin(ob) */
   6714     astro_rotation_t r;
   6715 
   6716     r.status = ASTRO_SUCCESS;
   6717     r.rot[0][0] = 1.0;  r.rot[1][0] = 0.0;  r.rot[2][0] = 0.0;
   6718     r.rot[0][1] = 0.0;  r.rot[1][1] = +c;   r.rot[2][1] = +s;
   6719     r.rot[0][2] = 0.0;  r.rot[1][2] = -s;   r.rot[2][2] = +c;
   6720     return r;
   6721 }
   6722 
   6723 /**
   6724  * @brief
   6725  *      Calculates a rotation matrix from ecliptic J2000 (ECL) to equatorial J2000 (EQJ).
   6726  *
   6727  * This is one of the family of functions that returns a rotation matrix
   6728  * for converting from one orientation to another.
   6729  * Source: ECL = ecliptic system, using equator at J2000 epoch.
   6730  * Target: EQJ = equatorial system, using equator at J2000 epoch.
   6731  *
   6732  * @return
   6733  *      A rotation matrix that converts ECL to EQJ.
   6734  */
   6735 astro_rotation_t Astronomy_Rotation_ECL_EQJ(void)
   6736 {
   6737     /* ob = mean obliquity of the J2000 ecliptic = 0.40909260059599012 radians. */
   6738     static const double c = 0.9174821430670688;    /* cos(ob) */
   6739     static const double s = 0.3977769691083922;    /* sin(ob) */
   6740     astro_rotation_t r;
   6741 
   6742     r.status = ASTRO_SUCCESS;
   6743     r.rot[0][0] = 1.0;  r.rot[1][0] = 0.0;  r.rot[2][0] = 0.0;
   6744     r.rot[0][1] = 0.0;  r.rot[1][1] = +c;   r.rot[2][1] = -s;
   6745     r.rot[0][2] = 0.0;  r.rot[1][2] = +s;   r.rot[2][2] = +c;
   6746     return r;
   6747 }
   6748 
   6749 /**
   6750  * @brief
   6751  *      Calculates a rotation matrix from equatorial J2000 (EQJ) to equatorial of-date (EQD).
   6752  *
   6753  * This is one of the family of functions that returns a rotation matrix
   6754  * for converting from one orientation to another.
   6755  * Source: EQJ = equatorial system, using equator at J2000 epoch.
   6756  * Target: EQD = equatorial system, using equator of the specified date/time.
   6757  *
   6758  * @param time
   6759  *      The date and time at which the Earth's equator defines the target orientation.
   6760  *
   6761  * @return
   6762  *      A rotation matrix that converts EQJ to EQD at `time`.
   6763  */
   6764 astro_rotation_t Astronomy_Rotation_EQJ_EQD(astro_time_t time)
   6765 {
   6766     astro_rotation_t prec, nut;
   6767 
   6768     prec = precession_rot(0.0, time.tt);
   6769     nut = nutation_rot(&time, 0);
   6770     return Astronomy_CombineRotation(prec, nut);
   6771 }
   6772 
   6773 /**
   6774  * @brief
   6775  *      Calculates a rotation matrix from equatorial of-date (EQD) to equatorial J2000 (EQJ).
   6776  *
   6777  * This is one of the family of functions that returns a rotation matrix
   6778  * for converting from one orientation to another.
   6779  * Source: EQD = equatorial system, using equator of the specified date/time.
   6780  * Target: EQJ = equatorial system, using equator at J2000 epoch.
   6781  *
   6782  * @param time
   6783  *      The date and time at which the Earth's equator defines the source orientation.
   6784  *
   6785  * @return
   6786  *      A rotation matrix that converts EQD at `time` to EQJ.
   6787  */
   6788 astro_rotation_t Astronomy_Rotation_EQD_EQJ(astro_time_t time)
   6789 {
   6790     astro_rotation_t prec, nut;
   6791 
   6792     nut = nutation_rot(&time, 1);
   6793     prec = precession_rot(time.tt, 0.0);
   6794     return Astronomy_CombineRotation(nut, prec);
   6795 }
   6796 
   6797 
   6798 /**
   6799  * @brief
   6800  *      Calculates a rotation matrix from equatorial of-date (EQD) to horizontal (HOR).
   6801  *
   6802  * This is one of the family of functions that returns a rotation matrix
   6803  * for converting from one orientation to another.
   6804  * Source: EQD = equatorial system, using equator of the specified date/time.
   6805  * Target: HOR = horizontal system.
   6806  *
   6807  * @param time
   6808  *      The date and time at which the Earth's equator applies.
   6809  *
   6810  * @param observer
   6811  *      A location near the Earth's mean sea level that defines the observer's horizon.
   6812  *
   6813  * @return
   6814  *      A rotation matrix that converts EQD to HOR at `time` and for `observer`.
   6815  *      The components of the horizontal vector are:
   6816  *      x = north, y = west, z = zenith (straight up from the observer).
   6817  *      These components are chosen so that the "right-hand rule" works for the vector
   6818  *      and so that north represents the direction where azimuth = 0.
   6819  */
   6820 astro_rotation_t Astronomy_Rotation_EQD_HOR(astro_time_t time, astro_observer_t observer)
   6821 {
   6822     astro_rotation_t rot;
   6823     double uze[3], une[3], uwe[3];
   6824     double uz[3], un[3], uw[3];
   6825     double spin_angle;
   6826 
   6827     double sinlat = sin(observer.latitude * DEG2RAD);
   6828     double coslat = cos(observer.latitude * DEG2RAD);
   6829     double sinlon = sin(observer.longitude * DEG2RAD);
   6830     double coslon = cos(observer.longitude * DEG2RAD);
   6831 
   6832     uze[0] = coslat * coslon;
   6833     uze[1] = coslat * sinlon;
   6834     uze[2] = sinlat;
   6835 
   6836     une[0] = -sinlat * coslon;
   6837     une[1] = -sinlat * sinlon;
   6838     une[2] = coslat;
   6839 
   6840     uwe[0] = sinlon;
   6841     uwe[1] = -coslon;
   6842     uwe[2] = 0.0;
   6843 
   6844     spin_angle = -15.0 * sidereal_time(&time);
   6845     spin(spin_angle, uze, uz);
   6846     spin(spin_angle, une, un);
   6847     spin(spin_angle, uwe, uw);
   6848 
   6849     rot.rot[0][0] = un[0]; rot.rot[1][0] = un[1]; rot.rot[2][0] = un[2];
   6850     rot.rot[0][1] = uw[0]; rot.rot[1][1] = uw[1]; rot.rot[2][1] = uw[2];
   6851     rot.rot[0][2] = uz[0]; rot.rot[1][2] = uz[1]; rot.rot[2][2] = uz[2];
   6852 
   6853     rot.status = ASTRO_SUCCESS;
   6854     return rot;
   6855 }
   6856 
   6857 
   6858 /**
   6859  * @brief
   6860  *      Calculates a rotation matrix from horizontal (HOR) to equatorial of-date (EQD).
   6861  *
   6862  * This is one of the family of functions that returns a rotation matrix
   6863  * for converting from one orientation to another.
   6864  * Source: HOR = horizontal system (x=North, y=West, z=Zenith).
   6865  * Target: EQD = equatorial system, using equator of the specified date/time.
   6866  *
   6867  * @param time
   6868  *      The date and time at which the Earth's equator applies.
   6869  *
   6870  * @param observer
   6871  *      A location near the Earth's mean sea level that defines the observer's horizon.
   6872  *
   6873  * @return
   6874  *      A rotation matrix that converts HOR to EQD at `time` and for `observer`.
   6875  */
   6876 astro_rotation_t Astronomy_Rotation_HOR_EQD(astro_time_t time, astro_observer_t observer)
   6877 {
   6878     astro_rotation_t rot = Astronomy_Rotation_EQD_HOR(time, observer);
   6879     return Astronomy_InverseRotation(rot);
   6880 }
   6881 
   6882 
   6883 /**
   6884  * @brief
   6885  *      Calculates a rotation matrix from horizontal (HOR) to J2000 equatorial (EQJ).
   6886  *
   6887  * This is one of the family of functions that returns a rotation matrix
   6888  * for converting from one orientation to another.
   6889  * Source: HOR = horizontal system (x=North, y=West, z=Zenith).
   6890  * Target: EQJ = equatorial system, using equator at the J2000 epoch.
   6891  *
   6892  * @param time
   6893  *      The date and time of the observation.
   6894  *
   6895  * @param observer
   6896  *      A location near the Earth's mean sea level that defines the observer's horizon.
   6897  *
   6898  * @return
   6899  *      A rotation matrix that converts HOR to EQD at `time` and for `observer`.
   6900  */
   6901 astro_rotation_t Astronomy_Rotation_HOR_EQJ(astro_time_t time, astro_observer_t observer)
   6902 {
   6903     astro_rotation_t hor_eqd, eqd_eqj;
   6904 
   6905     hor_eqd = Astronomy_Rotation_HOR_EQD(time, observer);
   6906     eqd_eqj = Astronomy_Rotation_EQD_EQJ(time);
   6907     return Astronomy_CombineRotation(hor_eqd, eqd_eqj);
   6908 }
   6909 
   6910 
   6911 /**
   6912  * @brief
   6913  *      Calculates a rotation matrix from equatorial J2000 (EQJ) to horizontal (HOR).
   6914  *
   6915  * This is one of the family of functions that returns a rotation matrix
   6916  * for converting from one orientation to another.
   6917  * Source: EQJ = equatorial system, using the equator at the J2000 epoch.
   6918  * Target: HOR = horizontal system.
   6919  *
   6920  * @param time
   6921  *      The date and time of the desired horizontal orientation.
   6922  *
   6923  * @param observer
   6924  *      A location near the Earth's mean sea level that defines the observer's horizon.
   6925  *
   6926  * @return
   6927  *      A rotation matrix that converts EQJ to HOR at `time` and for `observer`.
   6928  *      The components of the horizontal vector are:
   6929  *      x = north, y = west, z = zenith (straight up from the observer).
   6930  *      These components are chosen so that the "right-hand rule" works for the vector
   6931  *      and so that north represents the direction where azimuth = 0.
   6932  */
   6933 astro_rotation_t Astronomy_Rotation_EQJ_HOR(astro_time_t time, astro_observer_t observer)
   6934 {
   6935     astro_rotation_t rot = Astronomy_Rotation_HOR_EQJ(time, observer);
   6936     return Astronomy_InverseRotation(rot);
   6937 }
   6938 
   6939 
   6940 /**
   6941  * @brief
   6942  *      Calculates a rotation matrix from equatorial of-date (EQD) to ecliptic J2000 (ECL).
   6943  *
   6944  * This is one of the family of functions that returns a rotation matrix
   6945  * for converting from one orientation to another.
   6946  * Source: EQD = equatorial system, using equator of date.
   6947  * Target: ECL = ecliptic system, using equator at J2000 epoch.
   6948  *
   6949  * @param time
   6950  *      The date and time of the source equator.
   6951  *
   6952  * @return
   6953  *      A rotation matrix that converts EQD to ECL.
   6954  */
   6955 astro_rotation_t Astronomy_Rotation_EQD_ECL(astro_time_t time)
   6956 {
   6957     astro_rotation_t eqd_eqj;
   6958     astro_rotation_t eqj_ecl;
   6959 
   6960     eqd_eqj = Astronomy_Rotation_EQD_EQJ(time);
   6961     eqj_ecl = Astronomy_Rotation_EQJ_ECL();
   6962     return Astronomy_CombineRotation(eqd_eqj, eqj_ecl);
   6963 }
   6964 
   6965 
   6966 /**
   6967  * @brief
   6968  *      Calculates a rotation matrix from ecliptic J2000 (ECL) to equatorial of-date (EQD).
   6969  *
   6970  * This is one of the family of functions that returns a rotation matrix
   6971  * for converting from one orientation to another.
   6972  * Source: ECL = ecliptic system, using equator at J2000 epoch.
   6973  * Target: EQD = equatorial system, using equator of date.
   6974  *
   6975  * @param time
   6976  *      The date and time of the desired equator.
   6977  *
   6978  * @return
   6979  *      A rotation matrix that converts ECL to EQD.
   6980  */
   6981 astro_rotation_t Astronomy_Rotation_ECL_EQD(astro_time_t time)
   6982 {
   6983     astro_rotation_t rot = Astronomy_Rotation_EQD_ECL(time);
   6984     return Astronomy_InverseRotation(rot);
   6985 }
   6986 
   6987 /**
   6988  * @brief
   6989  *      Calculates a rotation matrix from ecliptic J2000 (ECL) to horizontal (HOR).
   6990  *
   6991  * This is one of the family of functions that returns a rotation matrix
   6992  * for converting from one orientation to another.
   6993  * Source: ECL = ecliptic system, using equator at J2000 epoch.
   6994  * Target: HOR = horizontal system.
   6995  *
   6996  * @param time
   6997  *      The date and time of the desired horizontal orientation.
   6998  *
   6999  * @param observer
   7000  *      A location near the Earth's mean sea level that defines the observer's horizon.
   7001  *
   7002  * @return
   7003  *      A rotation matrix that converts ECL to HOR at `time` and for `observer`.
   7004  *      The components of the horizontal vector are:
   7005  *      x = north, y = west, z = zenith (straight up from the observer).
   7006  *      These components are chosen so that the "right-hand rule" works for the vector
   7007  *      and so that north represents the direction where azimuth = 0.
   7008  */
   7009 astro_rotation_t Astronomy_Rotation_ECL_HOR(astro_time_t time, astro_observer_t observer)
   7010 {
   7011     astro_rotation_t ecl_eqd = Astronomy_Rotation_ECL_EQD(time);
   7012     astro_rotation_t eqd_hor = Astronomy_Rotation_EQD_HOR(time, observer);
   7013     return Astronomy_CombineRotation(ecl_eqd, eqd_hor);
   7014 }
   7015 
   7016 /**
   7017  * @brief
   7018  *      Calculates a rotation matrix from horizontal (HOR) to ecliptic J2000 (ECL).
   7019  *
   7020  * This is one of the family of functions that returns a rotation matrix
   7021  * for converting from one orientation to another.
   7022  * Source: HOR = horizontal system.
   7023  * Target: ECL = ecliptic system, using equator at J2000 epoch.
   7024  *
   7025  * @param time
   7026  *      The date and time of the horizontal observation.
   7027  *
   7028  * @param observer
   7029  *      The location of the horizontal observer.
   7030  *
   7031  * @return
   7032  *      A rotation matrix that converts HOR to ECL.
   7033  */
   7034 astro_rotation_t Astronomy_Rotation_HOR_ECL(astro_time_t time, astro_observer_t observer)
   7035 {
   7036     astro_rotation_t rot = Astronomy_Rotation_ECL_HOR(time, observer);
   7037     return Astronomy_InverseRotation(rot);
   7038 }
   7039 
   7040 
   7041 /** @cond DOXYGEN_SKIP */
   7042 typedef struct
   7043 {
   7044     const char *symbol;
   7045     const char *name;
   7046 }
   7047 constel_info_t;
   7048 
   7049 
   7050 typedef struct
   7051 {
   7052     int    index;
   7053     double ra_lo;
   7054     double ra_hi;
   7055     double dec_lo;
   7056 }
   7057 constel_boundary_t;
   7058 /** @endcond */
   7059 
   7060 #define NUM_CONSTELLATIONS   88
   7061 
   7062 static const constel_info_t ConstelInfo[] = {
   7063     /*  0 */ { "And", "Andromeda"            }
   7064 ,   /*  1 */ { "Ant", "Antila"               }
   7065 ,   /*  2 */ { "Aps", "Apus"                 }
   7066 ,   /*  3 */ { "Aql", "Aquila"               }
   7067 ,   /*  4 */ { "Aqr", "Aquarius"             }
   7068 ,   /*  5 */ { "Ara", "Ara"                  }
   7069 ,   /*  6 */ { "Ari", "Aries"                }
   7070 ,   /*  7 */ { "Aur", "Auriga"               }
   7071 ,   /*  8 */ { "Boo", "Bootes"               }
   7072 ,   /*  9 */ { "Cae", "Caelum"               }
   7073 ,   /* 10 */ { "Cam", "Camelopardis"         }
   7074 ,   /* 11 */ { "Cap", "Capricornus"          }
   7075 ,   /* 12 */ { "Car", "Carina"               }
   7076 ,   /* 13 */ { "Cas", "Cassiopeia"           }
   7077 ,   /* 14 */ { "Cen", "Centaurus"            }
   7078 ,   /* 15 */ { "Cep", "Cepheus"              }
   7079 ,   /* 16 */ { "Cet", "Cetus"                }
   7080 ,   /* 17 */ { "Cha", "Chamaeleon"           }
   7081 ,   /* 18 */ { "Cir", "Circinus"             }
   7082 ,   /* 19 */ { "CMa", "Canis Major"          }
   7083 ,   /* 20 */ { "CMi", "Canis Minor"          }
   7084 ,   /* 21 */ { "Cnc", "Cancer"               }
   7085 ,   /* 22 */ { "Col", "Columba"              }
   7086 ,   /* 23 */ { "Com", "Coma Berenices"       }
   7087 ,   /* 24 */ { "CrA", "Corona Australis"     }
   7088 ,   /* 25 */ { "CrB", "Corona Borealis"      }
   7089 ,   /* 26 */ { "Crt", "Crater"               }
   7090 ,   /* 27 */ { "Cru", "Crux"                 }
   7091 ,   /* 28 */ { "Crv", "Corvus"               }
   7092 ,   /* 29 */ { "CVn", "Canes Venatici"       }
   7093 ,   /* 30 */ { "Cyg", "Cygnus"               }
   7094 ,   /* 31 */ { "Del", "Delphinus"            }
   7095 ,   /* 32 */ { "Dor", "Dorado"               }
   7096 ,   /* 33 */ { "Dra", "Draco"                }
   7097 ,   /* 34 */ { "Equ", "Equuleus"             }
   7098 ,   /* 35 */ { "Eri", "Eridanus"             }
   7099 ,   /* 36 */ { "For", "Fornax"               }
   7100 ,   /* 37 */ { "Gem", "Gemini"               }
   7101 ,   /* 38 */ { "Gru", "Grus"                 }
   7102 ,   /* 39 */ { "Her", "Hercules"             }
   7103 ,   /* 40 */ { "Hor", "Horologium"           }
   7104 ,   /* 41 */ { "Hya", "Hydra"                }
   7105 ,   /* 42 */ { "Hyi", "Hydrus"               }
   7106 ,   /* 43 */ { "Ind", "Indus"                }
   7107 ,   /* 44 */ { "Lac", "Lacerta"              }
   7108 ,   /* 45 */ { "Leo", "Leo"                  }
   7109 ,   /* 46 */ { "Lep", "Lepus"                }
   7110 ,   /* 47 */ { "Lib", "Libra"                }
   7111 ,   /* 48 */ { "LMi", "Leo Minor"            }
   7112 ,   /* 49 */ { "Lup", "Lupus"                }
   7113 ,   /* 50 */ { "Lyn", "Lynx"                 }
   7114 ,   /* 51 */ { "Lyr", "Lyra"                 }
   7115 ,   /* 52 */ { "Men", "Mensa"                }
   7116 ,   /* 53 */ { "Mic", "Microscopium"         }
   7117 ,   /* 54 */ { "Mon", "Monoceros"            }
   7118 ,   /* 55 */ { "Mus", "Musca"                }
   7119 ,   /* 56 */ { "Nor", "Norma"                }
   7120 ,   /* 57 */ { "Oct", "Octans"               }
   7121 ,   /* 58 */ { "Oph", "Ophiuchus"            }
   7122 ,   /* 59 */ { "Ori", "Orion"                }
   7123 ,   /* 60 */ { "Pav", "Pavo"                 }
   7124 ,   /* 61 */ { "Peg", "Pegasus"              }
   7125 ,   /* 62 */ { "Per", "Perseus"              }
   7126 ,   /* 63 */ { "Phe", "Phoenix"              }
   7127 ,   /* 64 */ { "Pic", "Pictor"               }
   7128 ,   /* 65 */ { "PsA", "Pisces Austrinus"     }
   7129 ,   /* 66 */ { "Psc", "Pisces"               }
   7130 ,   /* 67 */ { "Pup", "Puppis"               }
   7131 ,   /* 68 */ { "Pyx", "Pyxis"                }
   7132 ,   /* 69 */ { "Ret", "Reticulum"            }
   7133 ,   /* 70 */ { "Scl", "Sculptor"             }
   7134 ,   /* 71 */ { "Sco", "Scorpius"             }
   7135 ,   /* 72 */ { "Sct", "Scutum"               }
   7136 ,   /* 73 */ { "Ser", "Serpens"              }
   7137 ,   /* 74 */ { "Sex", "Sextans"              }
   7138 ,   /* 75 */ { "Sge", "Sagitta"              }
   7139 ,   /* 76 */ { "Sgr", "Sagittarius"          }
   7140 ,   /* 77 */ { "Tau", "Taurus"               }
   7141 ,   /* 78 */ { "Tel", "Telescopium"          }
   7142 ,   /* 79 */ { "TrA", "Triangulum Australe"  }
   7143 ,   /* 80 */ { "Tri", "Triangulum"           }
   7144 ,   /* 81 */ { "Tuc", "Tucana"               }
   7145 ,   /* 82 */ { "UMa", "Ursa Major"           }
   7146 ,   /* 83 */ { "UMi", "Ursa Minor"           }
   7147 ,   /* 84 */ { "Vel", "Vela"                 }
   7148 ,   /* 85 */ { "Vir", "Virgo"                }
   7149 ,   /* 86 */ { "Vol", "Volans"               }
   7150 ,   /* 87 */ { "Vul", "Vulpecula"            }
   7151 };
   7152 
   7153 static const constel_boundary_t ConstelBounds[] = {
   7154     { 83,  0.00000000000000, 24.00000000000000, 88.00000000000000 }    /* UMi */
   7155 ,   { 83,  8.00000000000000, 14.50000000000000, 86.50000000000000 }    /* UMi */
   7156 ,   { 83, 21.00000000000000, 23.00000000000000, 86.16666666666667 }    /* UMi */
   7157 ,   { 83, 18.00000000000000, 21.00000000000000, 86.00000000000000 }    /* UMi */
   7158 ,   { 15,  0.00000000000000,  8.00000000000000, 85.00000000000000 }    /* Cep */
   7159 ,   { 10,  9.16666666666667, 10.66666666666667, 82.00000000000000 }    /* Cam */
   7160 ,   { 15,  0.00000000000000,  5.00000000000000, 80.00000000000000 }    /* Cep */
   7161 ,   { 10, 10.66666666666667, 14.50000000000000, 80.00000000000000 }    /* Cam */
   7162 ,   { 83, 17.50000000000000, 18.00000000000000, 80.00000000000000 }    /* UMi */
   7163 ,   { 33, 20.16666666666667, 21.00000000000000, 80.00000000000000 }    /* Dra */
   7164 ,   { 15,  0.00000000000000,  3.50833333333333, 77.00000000000000 }    /* Cep */
   7165 ,   { 10, 11.50000000000000, 13.58333333333333, 77.00000000000000 }    /* Cam */
   7166 ,   { 83, 16.53333333333333, 17.50000000000000, 75.00000000000000 }    /* UMi */
   7167 ,   { 15, 20.16666666666667, 20.66666666666667, 75.00000000000000 }    /* Cep */
   7168 ,   { 10,  7.96666666666667,  9.16666666666667, 73.50000000000000 }    /* Cam */
   7169 ,   { 33,  9.16666666666667, 11.33333333333333, 73.50000000000000 }    /* Dra */
   7170 ,   { 83, 13.00000000000000, 16.53333333333333, 70.00000000000000 }    /* UMi */
   7171 ,   { 13,  3.10000000000000,  3.41666666666667, 68.00000000000000 }    /* Cas */
   7172 ,   { 33, 20.41666666666667, 20.66666666666667, 67.00000000000000 }    /* Dra */
   7173 ,   { 33, 11.33333333333333, 12.00000000000000, 66.50000000000000 }    /* Dra */
   7174 ,   { 15,  0.00000000000000,  0.33333333333333, 66.00000000000000 }    /* Cep */
   7175 ,   { 83, 14.00000000000000, 15.66666666666667, 66.00000000000000 }    /* UMi */
   7176 ,   { 15, 23.58333333333333, 24.00000000000000, 66.00000000000000 }    /* Cep */
   7177 ,   { 33, 12.00000000000000, 13.50000000000000, 64.00000000000000 }    /* Dra */
   7178 ,   { 33, 13.50000000000000, 14.41666666666667, 63.00000000000000 }    /* Dra */
   7179 ,   { 15, 23.16666666666667, 23.58333333333333, 63.00000000000000 }    /* Cep */
   7180 ,   { 10,  6.10000000000000,  7.00000000000000, 62.00000000000000 }    /* Cam */
   7181 ,   { 33, 20.00000000000000, 20.41666666666667, 61.50000000000000 }    /* Dra */
   7182 ,   { 15, 20.53666666666667, 20.60000000000000, 60.91666666666666 }    /* Cep */
   7183 ,   { 10,  7.00000000000000,  7.96666666666667, 60.00000000000000 }    /* Cam */
   7184 ,   { 82,  7.96666666666667,  8.41666666666667, 60.00000000000000 }    /* UMa */
   7185 ,   { 33, 19.76666666666667, 20.00000000000000, 59.50000000000000 }    /* Dra */
   7186 ,   { 15, 20.00000000000000, 20.53666666666667, 59.50000000000000 }    /* Cep */
   7187 ,   { 15, 22.86666666666667, 23.16666666666667, 59.08333333333334 }    /* Cep */
   7188 ,   { 13,  0.00000000000000,  2.43333333333333, 58.50000000000000 }    /* Cas */
   7189 ,   { 33, 19.41666666666667, 19.76666666666667, 58.00000000000000 }    /* Dra */
   7190 ,   { 13,  1.70000000000000,  1.90833333333333, 57.50000000000000 }    /* Cas */
   7191 ,   { 13,  2.43333333333333,  3.10000000000000, 57.00000000000000 }    /* Cas */
   7192 ,   { 10,  3.10000000000000,  3.16666666666667, 57.00000000000000 }    /* Cam */
   7193 ,   { 15, 22.31666666666667, 22.86666666666667, 56.25000000000000 }    /* Cep */
   7194 ,   { 10,  5.00000000000000,  6.10000000000000, 56.00000000000000 }    /* Cam */
   7195 ,   { 82, 14.03333333333333, 14.41666666666667, 55.50000000000000 }    /* UMa */
   7196 ,   { 33, 14.41666666666667, 19.41666666666667, 55.50000000000000 }    /* Dra */
   7197 ,   { 10,  3.16666666666667,  3.33333333333333, 55.00000000000000 }    /* Cam */
   7198 ,   { 15, 22.13333333333333, 22.31666666666667, 55.00000000000000 }    /* Cep */
   7199 ,   { 15, 20.60000000000000, 21.96666666666667, 54.83333333333334 }    /* Cep */
   7200 ,   { 13,  0.00000000000000,  1.70000000000000, 54.00000000000000 }    /* Cas */
   7201 ,   { 50,  6.10000000000000,  6.50000000000000, 54.00000000000000 }    /* Lyn */
   7202 ,   { 82, 12.08333333333333, 13.50000000000000, 53.00000000000000 }    /* UMa */
   7203 ,   { 33, 15.25000000000000, 15.75000000000000, 53.00000000000000 }    /* Dra */
   7204 ,   { 15, 21.96666666666667, 22.13333333333333, 52.75000000000000 }    /* Cep */
   7205 ,   { 10,  3.33333333333333,  5.00000000000000, 52.50000000000000 }    /* Cam */
   7206 ,   { 13, 22.86666666666667, 23.33333333333333, 52.50000000000000 }    /* Cas */
   7207 ,   { 33, 15.75000000000000, 17.00000000000000, 51.50000000000000 }    /* Dra */
   7208 ,   { 62,  2.04166666666667,  2.51666666666667, 50.50000000000000 }    /* Per */
   7209 ,   { 33, 17.00000000000000, 18.23333333333333, 50.50000000000000 }    /* Dra */
   7210 ,   { 13,  0.00000000000000,  1.36666666666667, 50.00000000000000 }    /* Cas */
   7211 ,   { 62,  1.36666666666667,  1.66666666666667, 50.00000000000000 }    /* Per */
   7212 ,   { 50,  6.50000000000000,  6.80000000000000, 50.00000000000000 }    /* Lyn */
   7213 ,   { 13, 23.33333333333333, 24.00000000000000, 50.00000000000000 }    /* Cas */
   7214 ,   { 82, 13.50000000000000, 14.03333333333333, 48.50000000000000 }    /* UMa */
   7215 ,   { 13,  0.00000000000000,  1.11666666666667, 48.00000000000000 }    /* Cas */
   7216 ,   { 13, 23.58333333333333, 24.00000000000000, 48.00000000000000 }    /* Cas */
   7217 ,   { 39, 18.17500000000000, 18.23333333333333, 47.50000000000000 }    /* Her */
   7218 ,   { 33, 18.23333333333333, 19.08333333333333, 47.50000000000000 }    /* Dra */
   7219 ,   { 30, 19.08333333333333, 19.16666666666667, 47.50000000000000 }    /* Cyg */
   7220 ,   { 62,  1.66666666666667,  2.04166666666667, 47.00000000000000 }    /* Per */
   7221 ,   { 82,  8.41666666666667,  9.16666666666667, 47.00000000000000 }    /* UMa */
   7222 ,   { 13,  0.16666666666667,  0.86666666666667, 46.00000000000000 }    /* Cas */
   7223 ,   { 82, 12.00000000000000, 12.08333333333333, 45.00000000000000 }    /* UMa */
   7224 ,   { 50,  6.80000000000000,  7.36666666666667, 44.50000000000000 }    /* Lyn */
   7225 ,   { 30, 21.90833333333333, 21.96666666666667, 44.00000000000000 }    /* Cyg */
   7226 ,   { 30, 21.87500000000000, 21.90833333333333, 43.75000000000000 }    /* Cyg */
   7227 ,   { 30, 19.16666666666667, 19.40000000000000, 43.50000000000000 }    /* Cyg */
   7228 ,   { 82,  9.16666666666667, 10.16666666666667, 42.00000000000000 }    /* UMa */
   7229 ,   { 82, 10.16666666666667, 10.78333333333333, 40.00000000000000 }    /* UMa */
   7230 ,   {  8, 15.43333333333333, 15.75000000000000, 40.00000000000000 }    /* Boo */
   7231 ,   { 39, 15.75000000000000, 16.33333333333333, 40.00000000000000 }    /* Her */
   7232 ,   { 50,  9.25000000000000,  9.58333333333333, 39.75000000000000 }    /* Lyn */
   7233 ,   {  0,  0.00000000000000,  2.51666666666667, 36.75000000000000 }    /* And */
   7234 ,   { 62,  2.51666666666667,  2.56666666666667, 36.75000000000000 }    /* Per */
   7235 ,   { 51, 19.35833333333333, 19.40000000000000, 36.50000000000000 }    /* Lyr */
   7236 ,   { 62,  4.50000000000000,  4.69166666666667, 36.00000000000000 }    /* Per */
   7237 ,   { 30, 21.73333333333333, 21.87500000000000, 36.00000000000000 }    /* Cyg */
   7238 ,   { 44, 21.87500000000000, 22.00000000000000, 36.00000000000000 }    /* Lac */
   7239 ,   {  7,  6.53333333333333,  7.36666666666667, 35.50000000000000 }    /* Aur */
   7240 ,   { 50,  7.36666666666667,  7.75000000000000, 35.50000000000000 }    /* Lyn */
   7241 ,   {  0,  0.00000000000000,  2.00000000000000, 35.00000000000000 }    /* And */
   7242 ,   { 44, 22.00000000000000, 22.81666666666667, 35.00000000000000 }    /* Lac */
   7243 ,   { 44, 22.81666666666667, 22.86666666666667, 34.50000000000000 }    /* Lac */
   7244 ,   {  0, 22.86666666666667, 23.50000000000000, 34.50000000000000 }    /* And */
   7245 ,   { 62,  2.56666666666667,  2.71666666666667, 34.00000000000000 }    /* Per */
   7246 ,   { 82, 10.78333333333333, 11.00000000000000, 34.00000000000000 }    /* UMa */
   7247 ,   { 29, 12.00000000000000, 12.33333333333333, 34.00000000000000 }    /* CVn */
   7248 ,   { 50,  7.75000000000000,  9.25000000000000, 33.50000000000000 }    /* Lyn */
   7249 ,   { 48,  9.25000000000000,  9.88333333333333, 33.50000000000000 }    /* LMi */
   7250 ,   {  0,  0.71666666666667,  1.40833333333333, 33.00000000000000 }    /* And */
   7251 ,   {  8, 15.18333333333333, 15.43333333333333, 33.00000000000000 }    /* Boo */
   7252 ,   {  0, 23.50000000000000, 23.75000000000000, 32.08333333333334 }    /* And */
   7253 ,   { 29, 12.33333333333333, 13.25000000000000, 32.00000000000000 }    /* CVn */
   7254 ,   {  0, 23.75000000000000, 24.00000000000000, 31.33333333333333 }    /* And */
   7255 ,   { 29, 13.95833333333333, 14.03333333333333, 30.75000000000000 }    /* CVn */
   7256 ,   { 80,  2.41666666666667,  2.71666666666667, 30.66666666666667 }    /* Tri */
   7257 ,   { 62,  2.71666666666667,  4.50000000000000, 30.66666666666667 }    /* Per */
   7258 ,   {  7,  4.50000000000000,  4.75000000000000, 30.00000000000000 }    /* Aur */
   7259 ,   { 51, 18.17500000000000, 19.35833333333333, 30.00000000000000 }    /* Lyr */
   7260 ,   { 82, 11.00000000000000, 12.00000000000000, 29.00000000000000 }    /* UMa */
   7261 ,   { 30, 19.66666666666667, 20.91666666666667, 29.00000000000000 }    /* Cyg */
   7262 ,   {  7,  4.75000000000000,  5.88333333333333, 28.50000000000000 }    /* Aur */
   7263 ,   { 48,  9.88333333333333, 10.50000000000000, 28.50000000000000 }    /* LMi */
   7264 ,   { 29, 13.25000000000000, 13.95833333333333, 28.50000000000000 }    /* CVn */
   7265 ,   {  0,  0.00000000000000,  0.06666666666667, 28.00000000000000 }    /* And */
   7266 ,   { 80,  1.40833333333333,  1.66666666666667, 28.00000000000000 }    /* Tri */
   7267 ,   {  7,  5.88333333333333,  6.53333333333333, 28.00000000000000 }    /* Aur */
   7268 ,   { 37,  7.88333333333333,  8.00000000000000, 28.00000000000000 }    /* Gem */
   7269 ,   { 30, 20.91666666666667, 21.73333333333333, 28.00000000000000 }    /* Cyg */
   7270 ,   { 30, 19.25833333333333, 19.66666666666667, 27.50000000000000 }    /* Cyg */
   7271 ,   { 80,  1.91666666666667,  2.41666666666667, 27.25000000000000 }    /* Tri */
   7272 ,   { 25, 16.16666666666667, 16.33333333333333, 27.00000000000000 }    /* CrB */
   7273 ,   {  8, 15.08333333333333, 15.18333333333333, 26.00000000000000 }    /* Boo */
   7274 ,   { 25, 15.18333333333333, 16.16666666666667, 26.00000000000000 }    /* CrB */
   7275 ,   { 51, 18.36666666666667, 18.86666666666667, 26.00000000000000 }    /* Lyr */
   7276 ,   { 48, 10.75000000000000, 11.00000000000000, 25.50000000000000 }    /* LMi */
   7277 ,   { 51, 18.86666666666667, 19.25833333333333, 25.50000000000000 }    /* Lyr */
   7278 ,   { 80,  1.66666666666667,  1.91666666666667, 25.00000000000000 }    /* Tri */
   7279 ,   { 66,  0.71666666666667,  0.85000000000000, 23.75000000000000 }    /* Psc */
   7280 ,   { 48, 10.50000000000000, 10.75000000000000, 23.50000000000000 }    /* LMi */
   7281 ,   { 87, 21.25000000000000, 21.41666666666667, 23.50000000000000 }    /* Vul */
   7282 ,   { 77,  5.70000000000000,  5.88333333333333, 22.83333333333333 }    /* Tau */
   7283 ,   {  0,  0.06666666666667,  0.14166666666667, 22.00000000000000 }    /* And */
   7284 ,   { 73, 15.91666666666667, 16.03333333333333, 22.00000000000000 }    /* Ser */
   7285 ,   { 37,  5.88333333333333,  6.21666666666667, 21.50000000000000 }    /* Gem */
   7286 ,   { 87, 19.83333333333333, 20.25000000000000, 21.25000000000000 }    /* Vul */
   7287 ,   { 87, 18.86666666666667, 19.25000000000000, 21.08333333333333 }    /* Vul */
   7288 ,   {  0,  0.14166666666667,  0.85000000000000, 21.00000000000000 }    /* And */
   7289 ,   { 87, 20.25000000000000, 20.56666666666667, 20.50000000000000 }    /* Vul */
   7290 ,   { 37,  7.80833333333333,  7.88333333333333, 20.00000000000000 }    /* Gem */
   7291 ,   { 87, 20.56666666666667, 21.25000000000000, 19.50000000000000 }    /* Vul */
   7292 ,   { 87, 19.25000000000000, 19.83333333333333, 19.16666666666667 }    /* Vul */
   7293 ,   {  6,  3.28333333333333,  3.36666666666667, 19.00000000000000 }    /* Ari */
   7294 ,   { 75, 18.86666666666667, 19.00000000000000, 18.50000000000000 }    /* Sge */
   7295 ,   { 59,  5.70000000000000,  5.76666666666667, 18.00000000000000 }    /* Ori */
   7296 ,   { 37,  6.21666666666667,  6.30833333333333, 17.50000000000000 }    /* Gem */
   7297 ,   { 75, 19.00000000000000, 19.83333333333333, 16.16666666666667 }    /* Sge */
   7298 ,   { 77,  4.96666666666667,  5.33333333333333, 16.00000000000000 }    /* Tau */
   7299 ,   { 39, 15.91666666666667, 16.08333333333333, 16.00000000000000 }    /* Her */
   7300 ,   { 75, 19.83333333333333, 20.25000000000000, 15.75000000000000 }    /* Sge */
   7301 ,   { 77,  4.61666666666667,  4.96666666666667, 15.50000000000000 }    /* Tau */
   7302 ,   { 77,  5.33333333333333,  5.60000000000000, 15.50000000000000 }    /* Tau */
   7303 ,   { 23, 12.83333333333333, 13.50000000000000, 15.00000000000000 }    /* Com */
   7304 ,   { 39, 17.25000000000000, 18.25000000000000, 14.33333333333333 }    /* Her */
   7305 ,   { 23, 11.86666666666667, 12.83333333333333, 14.00000000000000 }    /* Com */
   7306 ,   { 37,  7.50000000000000,  7.80833333333333, 13.50000000000000 }    /* Gem */
   7307 ,   { 39, 16.75000000000000, 17.25000000000000, 12.83333333333333 }    /* Her */
   7308 ,   { 61,  0.00000000000000,  0.14166666666667, 12.50000000000000 }    /* Peg */
   7309 ,   { 77,  5.60000000000000,  5.76666666666667, 12.50000000000000 }    /* Tau */
   7310 ,   { 37,  7.00000000000000,  7.50000000000000, 12.50000000000000 }    /* Gem */
   7311 ,   { 61, 21.11666666666667, 21.33333333333333, 12.50000000000000 }    /* Peg */
   7312 ,   { 37,  6.30833333333333,  6.93333333333333, 12.00000000000000 }    /* Gem */
   7313 ,   { 39, 18.25000000000000, 18.86666666666667, 12.00000000000000 }    /* Her */
   7314 ,   { 31, 20.87500000000000, 21.05000000000000, 11.83333333333333 }    /* Del */
   7315 ,   { 61, 21.05000000000000, 21.11666666666667, 11.83333333333333 }    /* Peg */
   7316 ,   { 45, 11.51666666666667, 11.86666666666667, 11.00000000000000 }    /* Leo */
   7317 ,   { 59,  6.24166666666667,  6.30833333333333, 10.00000000000000 }    /* Ori */
   7318 ,   { 37,  6.93333333333333,  7.00000000000000, 10.00000000000000 }    /* Gem */
   7319 ,   { 21,  7.80833333333333,  7.92500000000000, 10.00000000000000 }    /* Cnc */
   7320 ,   { 61, 23.83333333333333, 24.00000000000000, 10.00000000000000 }    /* Peg */
   7321 ,   {  6,  1.66666666666667,  3.28333333333333,  9.91666666666667 }    /* Ari */
   7322 ,   { 31, 20.14166666666667, 20.30000000000000,  8.50000000000000 }    /* Del */
   7323 ,   {  8, 13.50000000000000, 15.08333333333333,  8.00000000000000 }    /* Boo */
   7324 ,   { 61, 22.75000000000000, 23.83333333333333,  7.50000000000000 }    /* Peg */
   7325 ,   { 21,  7.92500000000000,  9.25000000000000,  7.00000000000000 }    /* Cnc */
   7326 ,   { 45,  9.25000000000000, 10.75000000000000,  7.00000000000000 }    /* Leo */
   7327 ,   { 58, 18.25000000000000, 18.66222222222222,  6.25000000000000 }    /* Oph */
   7328 ,   {  3, 18.66222222222222, 18.86666666666667,  6.25000000000000 }    /* Aql */
   7329 ,   { 31, 20.83333333333333, 20.87500000000000,  6.00000000000000 }    /* Del */
   7330 ,   { 20,  7.00000000000000,  7.01666666666667,  5.50000000000000 }    /* CMi */
   7331 ,   { 73, 18.25000000000000, 18.42500000000000,  4.50000000000000 }    /* Ser */
   7332 ,   { 39, 16.08333333333333, 16.75000000000000,  4.00000000000000 }    /* Her */
   7333 ,   { 58, 18.25000000000000, 18.42500000000000,  3.00000000000000 }    /* Oph */
   7334 ,   { 61, 21.46666666666667, 21.66666666666667,  2.75000000000000 }    /* Peg */
   7335 ,   { 66,  0.00000000000000,  2.00000000000000,  2.00000000000000 }    /* Psc */
   7336 ,   { 73, 18.58333333333333, 18.86666666666667,  2.00000000000000 }    /* Ser */
   7337 ,   { 31, 20.30000000000000, 20.83333333333333,  2.00000000000000 }    /* Del */
   7338 ,   { 34, 20.83333333333333, 21.33333333333333,  2.00000000000000 }    /* Equ */
   7339 ,   { 61, 21.33333333333333, 21.46666666666667,  2.00000000000000 }    /* Peg */
   7340 ,   { 61, 22.00000000000000, 22.75000000000000,  2.00000000000000 }    /* Peg */
   7341 ,   { 61, 21.66666666666667, 22.00000000000000,  1.75000000000000 }    /* Peg */
   7342 ,   { 20,  7.01666666666667,  7.20000000000000,  1.50000000000000 }    /* CMi */
   7343 ,   { 77,  3.58333333333333,  4.61666666666667,  0.00000000000000 }    /* Tau */
   7344 ,   { 59,  4.61666666666667,  4.66666666666667,  0.00000000000000 }    /* Ori */
   7345 ,   { 20,  7.20000000000000,  8.08333333333333,  0.00000000000000 }    /* CMi */
   7346 ,   { 85, 14.66666666666667, 15.08333333333333,  0.00000000000000 }    /* Vir */
   7347 ,   { 58, 17.83333333333333, 18.25000000000000,  0.00000000000000 }    /* Oph */
   7348 ,   { 16,  2.65000000000000,  3.28333333333333, -1.75000000000000 }    /* Cet */
   7349 ,   { 77,  3.28333333333333,  3.58333333333333, -1.75000000000000 }    /* Tau */
   7350 ,   { 73, 15.08333333333333, 16.26666666666667, -3.25000000000000 }    /* Ser */
   7351 ,   { 59,  4.66666666666667,  5.08333333333333, -4.00000000000000 }    /* Ori */
   7352 ,   { 59,  5.83333333333333,  6.24166666666667, -4.00000000000000 }    /* Ori */
   7353 ,   { 73, 17.83333333333333, 17.96666666666667, -4.00000000000000 }    /* Ser */
   7354 ,   { 73, 18.25000000000000, 18.58333333333333, -4.00000000000000 }    /* Ser */
   7355 ,   {  3, 18.58333333333333, 18.86666666666667, -4.00000000000000 }    /* Aql */
   7356 ,   { 66, 22.75000000000000, 23.83333333333333, -4.00000000000000 }    /* Psc */
   7357 ,   { 45, 10.75000000000000, 11.51666666666667, -6.00000000000000 }    /* Leo */
   7358 ,   { 85, 11.51666666666667, 11.83333333333333, -6.00000000000000 }    /* Vir */
   7359 ,   { 66,  0.00000000000000,  0.33333333333333, -7.00000000000000 }    /* Psc */
   7360 ,   { 66, 23.83333333333333, 24.00000000000000, -7.00000000000000 }    /* Psc */
   7361 ,   { 85, 14.25000000000000, 14.66666666666667, -8.00000000000000 }    /* Vir */
   7362 ,   { 58, 15.91666666666667, 16.26666666666667, -8.00000000000000 }    /* Oph */
   7363 ,   {  3, 20.00000000000000, 20.53333333333333, -9.00000000000000 }    /* Aql */
   7364 ,   {  4, 21.33333333333333, 21.86666666666667, -9.00000000000000 }    /* Aqr */
   7365 ,   { 58, 17.16666666666667, 17.96666666666667, -10.00000000000000 }    /* Oph */
   7366 ,   { 54,  5.83333333333333,  8.08333333333333, -11.00000000000000 }    /* Mon */
   7367 ,   { 35,  4.91666666666667,  5.08333333333333, -11.00000000000000 }    /* Eri */
   7368 ,   { 59,  5.08333333333333,  5.83333333333333, -11.00000000000000 }    /* Ori */
   7369 ,   { 41,  8.08333333333333,  8.36666666666667, -11.00000000000000 }    /* Hya */
   7370 ,   { 74,  9.58333333333333, 10.75000000000000, -11.00000000000000 }    /* Sex */
   7371 ,   { 85, 11.83333333333333, 12.83333333333333, -11.00000000000000 }    /* Vir */
   7372 ,   { 58, 17.58333333333333, 17.66666666666667, -11.66666666666667 }    /* Oph */
   7373 ,   {  3, 18.86666666666667, 20.00000000000000, -12.03333333333333 }    /* Aql */
   7374 ,   { 35,  4.83333333333333,  4.91666666666667, -14.50000000000000 }    /* Eri */
   7375 ,   {  4, 20.53333333333333, 21.33333333333333, -15.00000000000000 }    /* Aqr */
   7376 ,   { 73, 17.16666666666667, 18.25000000000000, -16.00000000000000 }    /* Ser */
   7377 ,   { 72, 18.25000000000000, 18.86666666666667, -16.00000000000000 }    /* Sct */
   7378 ,   { 41,  8.36666666666667,  8.58333333333333, -17.00000000000000 }    /* Hya */
   7379 ,   { 58, 16.26666666666667, 16.37500000000000, -18.25000000000000 }    /* Oph */
   7380 ,   { 41,  8.58333333333333,  9.08333333333333, -19.00000000000000 }    /* Hya */
   7381 ,   { 26, 10.75000000000000, 10.83333333333333, -19.00000000000000 }    /* Crt */
   7382 ,   { 71, 16.26666666666667, 16.37500000000000, -19.25000000000000 }    /* Sco */
   7383 ,   { 47, 15.66666666666667, 15.91666666666667, -20.00000000000000 }    /* Lib */
   7384 ,   { 28, 12.58333333333333, 12.83333333333333, -22.00000000000000 }    /* Crv */
   7385 ,   { 85, 12.83333333333333, 14.25000000000000, -22.00000000000000 }    /* Vir */
   7386 ,   { 41,  9.08333333333333,  9.75000000000000, -24.00000000000000 }    /* Hya */
   7387 ,   { 16,  1.66666666666667,  2.65000000000000, -24.38333333333333 }    /* Cet */
   7388 ,   { 35,  2.65000000000000,  3.75000000000000, -24.38333333333333 }    /* Eri */
   7389 ,   { 26, 10.83333333333333, 11.83333333333333, -24.50000000000000 }    /* Crt */
   7390 ,   { 28, 11.83333333333333, 12.58333333333333, -24.50000000000000 }    /* Crv */
   7391 ,   { 47, 14.25000000000000, 14.91666666666667, -24.50000000000000 }    /* Lib */
   7392 ,   { 58, 16.26666666666667, 16.75000000000000, -24.58333333333333 }    /* Oph */
   7393 ,   { 16,  0.00000000000000,  1.66666666666667, -25.50000000000000 }    /* Cet */
   7394 ,   { 11, 21.33333333333333, 21.86666666666667, -25.50000000000000 }    /* Cap */
   7395 ,   {  4, 21.86666666666667, 23.83333333333333, -25.50000000000000 }    /* Aqr */
   7396 ,   { 16, 23.83333333333333, 24.00000000000000, -25.50000000000000 }    /* Cet */
   7397 ,   { 41,  9.75000000000000, 10.25000000000000, -26.50000000000000 }    /* Hya */
   7398 ,   { 35,  4.70000000000000,  4.83333333333333, -27.25000000000000 }    /* Eri */
   7399 ,   { 46,  4.83333333333333,  6.11666666666667, -27.25000000000000 }    /* Lep */
   7400 ,   { 11, 20.00000000000000, 21.33333333333333, -28.00000000000000 }    /* Cap */
   7401 ,   { 41, 10.25000000000000, 10.58333333333333, -29.16666666666667 }    /* Hya */
   7402 ,   { 41, 12.58333333333333, 14.91666666666667, -29.50000000000000 }    /* Hya */
   7403 ,   { 47, 14.91666666666667, 15.66666666666667, -29.50000000000000 }    /* Lib */
   7404 ,   { 71, 15.66666666666667, 16.00000000000000, -29.50000000000000 }    /* Sco */
   7405 ,   { 35,  4.58333333333333,  4.70000000000000, -30.00000000000000 }    /* Eri */
   7406 ,   { 58, 16.75000000000000, 17.60000000000000, -30.00000000000000 }    /* Oph */
   7407 ,   { 76, 17.60000000000000, 17.83333333333333, -30.00000000000000 }    /* Sgr */
   7408 ,   { 41, 10.58333333333333, 10.83333333333333, -31.16666666666667 }    /* Hya */
   7409 ,   { 19,  6.11666666666667,  7.36666666666667, -33.00000000000000 }    /* CMa */
   7410 ,   { 41, 12.25000000000000, 12.58333333333333, -33.00000000000000 }    /* Hya */
   7411 ,   { 41, 10.83333333333333, 12.25000000000000, -35.00000000000000 }    /* Hya */
   7412 ,   { 36,  3.50000000000000,  3.75000000000000, -36.00000000000000 }    /* For */
   7413 ,   { 68,  8.36666666666667,  9.36666666666667, -36.75000000000000 }    /* Pyx */
   7414 ,   { 35,  4.26666666666667,  4.58333333333333, -37.00000000000000 }    /* Eri */
   7415 ,   { 76, 17.83333333333333, 19.16666666666667, -37.00000000000000 }    /* Sgr */
   7416 ,   { 65, 21.33333333333333, 23.00000000000000, -37.00000000000000 }    /* PsA */
   7417 ,   { 70, 23.00000000000000, 23.33333333333333, -37.00000000000000 }    /* Scl */
   7418 ,   { 36,  3.00000000000000,  3.50000000000000, -39.58333333333334 }    /* For */
   7419 ,   {  1,  9.36666666666667, 11.00000000000000, -39.75000000000000 }    /* Ant */
   7420 ,   { 70,  0.00000000000000,  1.66666666666667, -40.00000000000000 }    /* Scl */
   7421 ,   { 36,  1.66666666666667,  3.00000000000000, -40.00000000000000 }    /* For */
   7422 ,   { 35,  3.86666666666667,  4.26666666666667, -40.00000000000000 }    /* Eri */
   7423 ,   { 70, 23.33333333333333, 24.00000000000000, -40.00000000000000 }    /* Scl */
   7424 ,   { 14, 14.16666666666667, 14.91666666666667, -42.00000000000000 }    /* Cen */
   7425 ,   { 49, 15.66666666666667, 16.00000000000000, -42.00000000000000 }    /* Lup */
   7426 ,   { 71, 16.00000000000000, 16.42083333333333, -42.00000000000000 }    /* Sco */
   7427 ,   {  9,  4.83333333333333,  5.00000000000000, -43.00000000000000 }    /* Cae */
   7428 ,   { 22,  5.00000000000000,  6.58333333333333, -43.00000000000000 }    /* Col */
   7429 ,   { 67,  8.00000000000000,  8.36666666666667, -43.00000000000000 }    /* Pup */
   7430 ,   { 35,  3.41666666666667,  3.86666666666667, -44.00000000000000 }    /* Eri */
   7431 ,   { 71, 16.42083333333333, 17.83333333333333, -45.50000000000000 }    /* Sco */
   7432 ,   { 24, 17.83333333333333, 19.16666666666667, -45.50000000000000 }    /* CrA */
   7433 ,   { 76, 19.16666666666667, 20.33333333333333, -45.50000000000000 }    /* Sgr */
   7434 ,   { 53, 20.33333333333333, 21.33333333333333, -45.50000000000000 }    /* Mic */
   7435 ,   { 35,  3.00000000000000,  3.41666666666667, -46.00000000000000 }    /* Eri */
   7436 ,   {  9,  4.50000000000000,  4.83333333333333, -46.50000000000000 }    /* Cae */
   7437 ,   { 49, 15.33333333333333, 15.66666666666667, -48.00000000000000 }    /* Lup */
   7438 ,   { 63,  0.00000000000000,  2.33333333333333, -48.16666666666666 }    /* Phe */
   7439 ,   { 35,  2.66666666666667,  3.00000000000000, -49.00000000000000 }    /* Eri */
   7440 ,   { 40,  4.08333333333333,  4.26666666666667, -49.00000000000000 }    /* Hor */
   7441 ,   {  9,  4.26666666666667,  4.50000000000000, -49.00000000000000 }    /* Cae */
   7442 ,   { 38, 21.33333333333333, 22.00000000000000, -50.00000000000000 }    /* Gru */
   7443 ,   { 67,  6.00000000000000,  8.00000000000000, -50.75000000000000 }    /* Pup */
   7444 ,   { 84,  8.00000000000000,  8.16666666666667, -50.75000000000000 }    /* Vel */
   7445 ,   { 35,  2.41666666666667,  2.66666666666667, -51.00000000000000 }    /* Eri */
   7446 ,   { 40,  3.83333333333333,  4.08333333333333, -51.00000000000000 }    /* Hor */
   7447 ,   { 63,  0.00000000000000,  1.83333333333333, -51.50000000000000 }    /* Phe */
   7448 ,   { 12,  6.00000000000000,  6.16666666666667, -52.50000000000000 }    /* Car */
   7449 ,   { 84,  8.16666666666667,  8.45000000000000, -53.00000000000000 }    /* Vel */
   7450 ,   { 40,  3.50000000000000,  3.83333333333333, -53.16666666666666 }    /* Hor */
   7451 ,   { 32,  3.83333333333333,  4.00000000000000, -53.16666666666666 }    /* Dor */
   7452 ,   { 63,  0.00000000000000,  1.58333333333333, -53.50000000000000 }    /* Phe */
   7453 ,   { 35,  2.16666666666667,  2.41666666666667, -54.00000000000000 }    /* Eri */
   7454 ,   { 64,  4.50000000000000,  5.00000000000000, -54.00000000000000 }    /* Pic */
   7455 ,   { 49, 15.05000000000000, 15.33333333333333, -54.00000000000000 }    /* Lup */
   7456 ,   { 84,  8.45000000000000,  8.83333333333333, -54.50000000000000 }    /* Vel */
   7457 ,   { 12,  6.16666666666667,  6.50000000000000, -55.00000000000000 }    /* Car */
   7458 ,   { 14, 11.83333333333333, 12.83333333333333, -55.00000000000000 }    /* Cen */
   7459 ,   { 49, 14.16666666666667, 15.05000000000000, -55.00000000000000 }    /* Lup */
   7460 ,   { 56, 15.05000000000000, 15.33333333333333, -55.00000000000000 }    /* Nor */
   7461 ,   { 32,  4.00000000000000,  4.33333333333333, -56.50000000000000 }    /* Dor */
   7462 ,   { 84,  8.83333333333333, 11.00000000000000, -56.50000000000000 }    /* Vel */
   7463 ,   { 14, 11.00000000000000, 11.25000000000000, -56.50000000000000 }    /* Cen */
   7464 ,   {  5, 17.50000000000000, 18.00000000000000, -57.00000000000000 }    /* Ara */
   7465 ,   { 78, 18.00000000000000, 20.33333333333333, -57.00000000000000 }    /* Tel */
   7466 ,   { 38, 22.00000000000000, 23.33333333333333, -57.00000000000000 }    /* Gru */
   7467 ,   { 40,  3.20000000000000,  3.50000000000000, -57.50000000000000 }    /* Hor */
   7468 ,   { 64,  5.00000000000000,  5.50000000000000, -57.50000000000000 }    /* Pic */
   7469 ,   { 12,  6.50000000000000,  6.83333333333333, -58.00000000000000 }    /* Car */
   7470 ,   { 63,  0.00000000000000,  1.33333333333333, -58.50000000000000 }    /* Phe */
   7471 ,   { 35,  1.33333333333333,  2.16666666666667, -58.50000000000000 }    /* Eri */
   7472 ,   { 63, 23.33333333333333, 24.00000000000000, -58.50000000000000 }    /* Phe */
   7473 ,   { 32,  4.33333333333333,  4.58333333333333, -59.00000000000000 }    /* Dor */
   7474 ,   { 56, 15.33333333333333, 16.42083333333333, -60.00000000000000 }    /* Nor */
   7475 ,   { 43, 20.33333333333333, 21.33333333333333, -60.00000000000000 }    /* Ind */
   7476 ,   { 64,  5.50000000000000,  6.00000000000000, -61.00000000000000 }    /* Pic */
   7477 ,   { 18, 15.16666666666667, 15.33333333333333, -61.00000000000000 }    /* Cir */
   7478 ,   {  5, 16.42083333333333, 16.58333333333333, -61.00000000000000 }    /* Ara */
   7479 ,   { 18, 14.91666666666667, 15.16666666666667, -63.58333333333334 }    /* Cir */
   7480 ,   {  5, 16.58333333333333, 16.75000000000000, -63.58333333333334 }    /* Ara */
   7481 ,   { 64,  6.00000000000000,  6.83333333333333, -64.00000000000000 }    /* Pic */
   7482 ,   { 12,  6.83333333333333,  9.03333333333333, -64.00000000000000 }    /* Car */
   7483 ,   { 14, 11.25000000000000, 11.83333333333333, -64.00000000000000 }    /* Cen */
   7484 ,   { 27, 11.83333333333333, 12.83333333333333, -64.00000000000000 }    /* Cru */
   7485 ,   { 14, 12.83333333333333, 14.53333333333333, -64.00000000000000 }    /* Cen */
   7486 ,   { 18, 13.50000000000000, 13.66666666666667, -65.00000000000000 }    /* Cir */
   7487 ,   {  5, 16.75000000000000, 16.83333333333333, -65.00000000000000 }    /* Ara */
   7488 ,   { 40,  2.16666666666667,  3.20000000000000, -67.50000000000000 }    /* Hor */
   7489 ,   { 69,  3.20000000000000,  4.58333333333333, -67.50000000000000 }    /* Ret */
   7490 ,   { 18, 14.75000000000000, 14.91666666666667, -67.50000000000000 }    /* Cir */
   7491 ,   {  5, 16.83333333333333, 17.50000000000000, -67.50000000000000 }    /* Ara */
   7492 ,   { 60, 17.50000000000000, 18.00000000000000, -67.50000000000000 }    /* Pav */
   7493 ,   { 81, 22.00000000000000, 23.33333333333333, -67.50000000000000 }    /* Tuc */
   7494 ,   { 32,  4.58333333333333,  6.58333333333333, -70.00000000000000 }    /* Dor */
   7495 ,   { 18, 13.66666666666667, 14.75000000000000, -70.00000000000000 }    /* Cir */
   7496 ,   { 79, 14.75000000000000, 17.00000000000000, -70.00000000000000 }    /* TrA */
   7497 ,   { 81,  0.00000000000000,  1.33333333333333, -75.00000000000000 }    /* Tuc */
   7498 ,   { 42,  3.50000000000000,  4.58333333333333, -75.00000000000000 }    /* Hyi */
   7499 ,   { 86,  6.58333333333333,  9.03333333333333, -75.00000000000000 }    /* Vol */
   7500 ,   { 12,  9.03333333333333, 11.25000000000000, -75.00000000000000 }    /* Car */
   7501 ,   { 55, 11.25000000000000, 13.66666666666667, -75.00000000000000 }    /* Mus */
   7502 ,   { 60, 18.00000000000000, 21.33333333333333, -75.00000000000000 }    /* Pav */
   7503 ,   { 43, 21.33333333333333, 23.33333333333333, -75.00000000000000 }    /* Ind */
   7504 ,   { 81, 23.33333333333333, 24.00000000000000, -75.00000000000000 }    /* Tuc */
   7505 ,   { 81,  0.75000000000000,  1.33333333333333, -76.00000000000000 }    /* Tuc */
   7506 ,   { 42,  0.00000000000000,  3.50000000000000, -82.50000000000000 }    /* Hyi */
   7507 ,   { 17,  7.66666666666667, 13.66666666666667, -82.50000000000000 }    /* Cha */
   7508 ,   {  2, 13.66666666666667, 18.00000000000000, -82.50000000000000 }    /* Aps */
   7509 ,   { 52,  3.50000000000000,  7.66666666666667, -85.00000000000000 }    /* Men */
   7510 ,   { 57,  0.00000000000000, 24.00000000000000, -90.00000000000000 }    /* Oct */
   7511 };
   7512 
   7513 #define NUM_CONSTEL_BOUNDARIES  357
   7514 
   7515 
   7516 
   7517 /**
   7518  * @brief
   7519  *      Determines the constellation that contains the given point in the sky.
   7520  *
   7521  * Given J2000 equatorial (EQJ) coordinates of a point in the sky, determines the
   7522  * constellation that contains that point.
   7523  *
   7524  * @param ra
   7525  *      The right ascension (RA) of a point in the sky, using the J2000 equatorial system.
   7526  *
   7527  * @param dec
   7528  *      The declination (DEC) of a point in the sky, using the J2000 equatorial system.
   7529  *
   7530  * @return
   7531  *      If successful, `status` holds `ASTRO_SUCCESS`,
   7532  *      `symbol` holds a pointer to a 3-character string like "Ori", and
   7533  *      `name` holds a pointer to the full constellation name like "Orion".
   7534  */
   7535 astro_constellation_t Astronomy_Constellation(double ra, double dec)
   7536 {
   7537     static astro_time_t epoch2000;
   7538     static astro_rotation_t rot = { ASTRO_NOT_INITIALIZED };
   7539     astro_constellation_t constel;
   7540     astro_equatorial_t j2000, b1875;
   7541     astro_vector_t vec2000, vec1875;
   7542     int i, c;
   7543 
   7544     if (dec < -90.0 || dec > +90.0)
   7545         return ConstelErr(ASTRO_INVALID_PARAMETER);
   7546 
   7547     /* Allow right ascension to "wrap around". Clamp to [0, 24) sidereal hours. */
   7548     ra = fmod(ra, 24.0);
   7549     if (ra < 0.0)
   7550         ra += 24.0;
   7551 
   7552     /* Lazy-initialize the rotation matrix for converting J2000 to B1875. */
   7553     if (rot.status != ASTRO_SUCCESS)
   7554     {
   7555         /*
   7556             Need to calculate the B1875 epoch. Based on this:
   7557             https://en.wikipedia.org/wiki/Epoch_(astronomy)#Besselian_years
   7558             B = 1900 + (JD - 2415020.31352) / 365.242198781
   7559             I'm interested in using TT instead of JD, giving:
   7560             B = 1900 + ((TT+2451545) - 2415020.31352) / 365.242198781
   7561             B = 1900 + (TT + 36524.68648) / 365.242198781
   7562             TT = 365.242198781*(B - 1900) - 36524.68648 = -45655.741449525
   7563             But Astronomy_TimeFromDays() wants UT, not TT.
   7564             Near that date, I get a historical correction of ut-tt = 3.2 seconds.
   7565             That gives UT = -45655.74141261017 for the B1875 epoch,
   7566             or 1874-12-31T18:12:21.950Z.
   7567         */
   7568         astro_time_t time = Astronomy_TimeFromDays(-45655.74141261017);
   7569         rot = Astronomy_Rotation_EQJ_EQD(time);
   7570         if (rot.status != ASTRO_SUCCESS)
   7571             return ConstelErr(rot.status);
   7572 
   7573         epoch2000 = Astronomy_TimeFromDays(0.0);
   7574     }
   7575 
   7576     /* Convert coordinates from J2000 to year 1875. */
   7577     j2000.status = ASTRO_SUCCESS;
   7578     j2000.ra = ra;
   7579     j2000.dec = dec;
   7580     j2000.dist = 1.0;
   7581     vec2000 = Astronomy_VectorFromEquator(j2000, epoch2000);
   7582     if (vec2000.status != ASTRO_SUCCESS)
   7583         return ConstelErr(vec2000.status);
   7584 
   7585     vec1875 = Astronomy_RotateVector(rot, vec2000);
   7586     if (vec1875.status != ASTRO_SUCCESS)
   7587         return ConstelErr(vec1875.status);
   7588 
   7589     b1875 = Astronomy_EquatorFromVector(vec1875);
   7590     if (b1875.status != ASTRO_SUCCESS)
   7591         return ConstelErr(b1875.status);
   7592 
   7593     /* Search for the constellation using the B1875 coordinates. */
   7594     c = -1;     /* constellation not (yet) found */
   7595     for (i=0; i < NUM_CONSTEL_BOUNDARIES; ++i)
   7596     {
   7597         const constel_boundary_t *b = &ConstelBounds[i];
   7598         if ((b->dec_lo <= b1875.dec) && (b->ra_hi > b1875.ra) && (b->ra_lo <= b1875.ra))
   7599         {
   7600             c = b->index;
   7601             break;
   7602         }
   7603     }
   7604 
   7605     if (c < 0 || c >= NUM_CONSTELLATIONS)
   7606         return ConstelErr(ASTRO_INTERNAL_ERROR);    /* should have been able to find the constellation */
   7607 
   7608     constel.status = ASTRO_SUCCESS;
   7609     constel.symbol = ConstelInfo[c].symbol;
   7610     constel.name = ConstelInfo[c].name;
   7611     constel.ra_1875 = b1875.ra;
   7612     constel.dec_1875 = b1875.dec;
   7613     return constel;
   7614 }
   7615 
   7616 
   7617 static astro_lunar_eclipse_t LunarEclipseError(astro_status_t status)
   7618 {
   7619     astro_lunar_eclipse_t eclipse;
   7620     eclipse.status = status;
   7621     eclipse.kind = ECLIPSE_NONE;
   7622     eclipse.peak = TimeError();
   7623     eclipse.sd_penum = eclipse.sd_partial = eclipse.sd_total = NAN;
   7624     return eclipse;
   7625 }
   7626 
   7627 
   7628 /** @cond DOXYGEN_SKIP */
   7629 typedef struct
   7630 {
   7631     astro_status_t status;
   7632     astro_time_t time;
   7633     double  u;              /* dot product of (heliocentric earth) and (geocentric moon): defines the shadow plane where the Moon is */
   7634     double  r;              /* km distance between center of Moon/Earth (shaded body) and the line passing through the centers of the Sun and Earth/Moon (casting body). */
   7635     double  k;              /* umbra radius in km, at the shadow plane */
   7636     double  p;              /* penumbra radius in km, at the shadow plane */
   7637     astro_vector_t target;  /* coordinates of target body relative to shadow-casting body at 'time' */
   7638     astro_vector_t dir;     /* heliocentric coordinates of shadow-casting body at 'time' */
   7639 }
   7640 shadow_t;               /* Represents alignment of the Moon/Earth with the Earth's/Moon's shadow, for finding eclipses. */
   7641 
   7642 typedef struct
   7643 {
   7644     double radius_limit;
   7645     double direction;
   7646 }
   7647 shadow_context_t;
   7648 /** @endcond */
   7649 
   7650 
   7651 static shadow_t ShadowError(astro_status_t status)
   7652 {
   7653     shadow_t shadow;
   7654     memset(&shadow, 0, sizeof(shadow));
   7655     shadow.status = status;
   7656     return shadow;
   7657 }
   7658 
   7659 
   7660 static shadow_t CalcShadow(
   7661     double body_radius_km,
   7662     astro_time_t time,
   7663     astro_vector_t target,
   7664     astro_vector_t dir)
   7665 {
   7666     double dx, dy, dz;
   7667     shadow_t shadow;
   7668 
   7669     shadow.target = target;
   7670     shadow.dir = dir;
   7671 
   7672     shadow.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);
   7673 
   7674     dx = (shadow.u * dir.x) - target.x;
   7675     dy = (shadow.u * dir.y) - target.y;
   7676     dz = (shadow.u * dir.z) - target.z;
   7677     shadow.r = KM_PER_AU * sqrt(dx*dx + dy*dy + dz*dz);
   7678 
   7679     shadow.k = +SUN_RADIUS_KM - (1.0 + shadow.u)*(SUN_RADIUS_KM - body_radius_km);
   7680     shadow.p = -SUN_RADIUS_KM + (1.0 + shadow.u)*(SUN_RADIUS_KM + body_radius_km);
   7681     shadow.status = ASTRO_SUCCESS;
   7682     shadow.time = time;
   7683 
   7684     return shadow;
   7685 }
   7686 
   7687 
   7688 static shadow_t PlanetShadow(astro_body_t body, double planet_radius_km, astro_time_t time)
   7689 {
   7690     astro_vector_t e, p, g;
   7691 
   7692     /* Calculate light-travel-corrected vector from Earth to planet. */
   7693     g = Astronomy_GeoVector(body, time, NO_ABERRATION);
   7694     if (g.status != ASTRO_SUCCESS)
   7695         return ShadowError(g.status);
   7696 
   7697     /* Calculate light-travel-corrected vector from Earth to Sun. */
   7698     e = Astronomy_GeoVector(BODY_SUN, time, NO_ABERRATION);
   7699     if (e.status != ASTRO_SUCCESS)
   7700         return ShadowError(e.status);
   7701 
   7702     /* Deduce light-travel-corrected vector from Sun to planet. */
   7703     p.t = time;
   7704     p.x = g.x - e.x;
   7705     p.y = g.y - e.y;
   7706     p.z = g.z - e.z;
   7707 
   7708     /* Calcluate Earth's position from the planet's point of view. */
   7709     e.x = -g.x;
   7710     e.y = -g.y;
   7711     e.z = -g.z;
   7712 
   7713     return CalcShadow(planet_radius_km, time, e, p);
   7714 }
   7715 
   7716 
   7717 static shadow_t EarthShadow(astro_time_t time)
   7718 {
   7719     /* This function helps find when the Earth's shadow falls upon the Moon. */
   7720     astro_vector_t e, m;
   7721 
   7722     e = CalcEarth(time);            /* This function never fails; no need to check return value */
   7723     m = Astronomy_GeoMoon(time);    /* This function never fails; no need to check return value */
   7724 
   7725     return CalcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, e);
   7726 }
   7727 
   7728 
   7729 static shadow_t MoonShadow(astro_time_t time)
   7730 {
   7731     /* This function helps find when the Moon's shadow falls upon the Earth. */
   7732 
   7733     astro_vector_t h, e, m;
   7734 
   7735     /*
   7736         This is a variation on the logic in EarthShadow().
   7737         Instead of a heliocentric Earth and a geocentric Moon,
   7738         we want a heliocentric Moon and a lunacentric Earth.
   7739     */
   7740 
   7741     h = CalcEarth(time);            /* heliocentric Earth */
   7742     m = Astronomy_GeoMoon(time);    /* geocentric Moon */
   7743 
   7744     /* Calculate lunacentric Earth. */
   7745     e.status = m.status;
   7746     e.x = -m.x;
   7747     e.y = -m.y;
   7748     e.z = -m.z;
   7749     e.t = m.t;
   7750 
   7751     /* Convert geocentric moon to heliocentric Moon. */
   7752     m.x += h.x;
   7753     m.y += h.y;
   7754     m.z += h.z;
   7755 
   7756     return CalcShadow(MOON_MEAN_RADIUS_KM, time, e, m);
   7757 }
   7758 
   7759 
   7760 /** @cond DOXYGEN_SKIP */
   7761 typedef shadow_t (* shadow_func_t) (astro_time_t time);
   7762 /** @endcond */
   7763 
   7764 
   7765 static astro_func_result_t shadow_distance_slope(void *context, astro_time_t time)
   7766 {
   7767     const double dt = 1.0 / 86400.0;
   7768     astro_time_t t1, t2;
   7769     astro_func_result_t result;
   7770     shadow_t shadow1, shadow2;
   7771     shadow_func_t shadowfunc = context;
   7772 
   7773     t1 = Astronomy_AddDays(time, -dt);
   7774     t2 = Astronomy_AddDays(time, +dt);
   7775 
   7776     shadow1 = shadowfunc(t1);
   7777     if (shadow1.status != ASTRO_SUCCESS)
   7778         return FuncError(shadow1.status);
   7779 
   7780     shadow2 = shadowfunc(t2);
   7781     if (shadow2.status != ASTRO_SUCCESS)
   7782         return FuncError(shadow2.status);
   7783 
   7784     result.value = (shadow2.r - shadow1.r) / dt;
   7785     result.status = ASTRO_SUCCESS;
   7786     return result;
   7787 }
   7788 
   7789 
   7790 static shadow_t PeakEarthShadow(astro_time_t search_center_time)
   7791 {
   7792     /* Search for when the Earth's shadow axis is closest to the center of the Moon. */
   7793 
   7794     astro_time_t t1, t2;
   7795     astro_search_result_t result;
   7796     const double window = 0.03;        /* days before/after full moon to search for minimum shadow distance */
   7797 
   7798     t1 = Astronomy_AddDays(search_center_time, -window);
   7799     t2 = Astronomy_AddDays(search_center_time, +window);
   7800 
   7801     result = Astronomy_Search(shadow_distance_slope, EarthShadow, t1, t2, 1.0);
   7802     if (result.status != ASTRO_SUCCESS)
   7803         return ShadowError(result.status);
   7804 
   7805     return EarthShadow(result.time);
   7806 }
   7807 
   7808 
   7809 static shadow_t PeakMoonShadow(astro_time_t search_center_time)
   7810 {
   7811     /* Search for when the Moon's shadow axis is closest to the center of the Earth. */
   7812 
   7813     astro_time_t t1, t2;
   7814     astro_search_result_t result;
   7815     const double window = 0.03;     /* days before/after new moon to search for minimum shadow distance */
   7816 
   7817     t1 = Astronomy_AddDays(search_center_time, -window);
   7818     t2 = Astronomy_AddDays(search_center_time, +window);
   7819 
   7820     result = Astronomy_Search(shadow_distance_slope, MoonShadow, t1, t2, 1.0);
   7821     if (result.status != ASTRO_SUCCESS)
   7822         return ShadowError(result.status);
   7823 
   7824     return MoonShadow(result.time);
   7825 }
   7826 
   7827 
   7828 /** @cond DOXYGEN_SKIP */
   7829 typedef struct
   7830 {
   7831     astro_body_t    body;
   7832     double          planet_radius_km;
   7833     double          direction;          /* used for transit start/finish search only */
   7834 }
   7835 planet_shadow_context_t;
   7836 /** @endcond */
   7837 
   7838 
   7839 static astro_func_result_t planet_shadow_distance_slope(void *context, astro_time_t time)
   7840 {
   7841     const double dt = 1.0 / 86400.0;
   7842     astro_time_t t1, t2;
   7843     astro_func_result_t result;
   7844     shadow_t shadow1, shadow2;
   7845     const planet_shadow_context_t *p = context;
   7846 
   7847     t1 = Astronomy_AddDays(time, -dt);
   7848     t2 = Astronomy_AddDays(time, +dt);
   7849 
   7850     shadow1 = PlanetShadow(p->body, p->planet_radius_km, t1);
   7851     if (shadow1.status != ASTRO_SUCCESS)
   7852         return FuncError(shadow1.status);
   7853 
   7854     shadow2 = PlanetShadow(p->body, p->planet_radius_km, t2);
   7855     if (shadow2.status != ASTRO_SUCCESS)
   7856         return FuncError(shadow2.status);
   7857 
   7858     result.value = (shadow2.r - shadow1.r) / dt;
   7859     result.status = ASTRO_SUCCESS;
   7860     return result;
   7861 }
   7862 
   7863 
   7864 static shadow_t PeakPlanetShadow(astro_body_t body, double planet_radius_km, astro_time_t search_center_time)
   7865 {
   7866     /* Search for when the body's shadow is closest to the center of the Earth. */
   7867 
   7868     astro_time_t t1, t2;
   7869     astro_search_result_t result;
   7870     planet_shadow_context_t context;
   7871     const double window = 1.0;     /* days before/after inferior conjunction to search for minimum shadow distance */
   7872 
   7873     t1 = Astronomy_AddDays(search_center_time, -window);
   7874     t2 = Astronomy_AddDays(search_center_time, +window);
   7875 
   7876     context.body = body;
   7877     context.planet_radius_km = planet_radius_km;
   7878     context.direction = 0.0;    /* not used in this search */
   7879 
   7880     result = Astronomy_Search(planet_shadow_distance_slope, &context, t1, t2, 1.0);
   7881     if (result.status != ASTRO_SUCCESS)
   7882         return ShadowError(result.status);
   7883 
   7884     return PlanetShadow(body, planet_radius_km, result.time);
   7885 }
   7886 
   7887 
   7888 static astro_func_result_t shadow_distance(void *context, astro_time_t time)
   7889 {
   7890     astro_func_result_t result;
   7891     const shadow_context_t *p = context;
   7892     shadow_t shadow = EarthShadow(time);
   7893     if (shadow.status != ASTRO_SUCCESS)
   7894         return FuncError(shadow.status);
   7895 
   7896     result.value = p->direction * (shadow.r - p->radius_limit);
   7897     result.status = ASTRO_SUCCESS;
   7898     return result;
   7899 }
   7900 
   7901 
   7902 static double ShadowSemiDurationMinutes(astro_time_t center_time, double radius_limit, double window_minutes)
   7903 {
   7904     /* Search backwards and forwards from the center time until shadow axis distance crosses radius limit. */
   7905     double window = window_minutes / (24.0 * 60.0);
   7906     shadow_context_t context;
   7907     astro_search_result_t s1, s2;
   7908     astro_time_t before, after;
   7909 
   7910     before = Astronomy_AddDays(center_time, -window);
   7911     after  = Astronomy_AddDays(center_time, +window);
   7912 
   7913     context.radius_limit = radius_limit;
   7914     context.direction = -1.0;
   7915     s1 = Astronomy_Search(shadow_distance, &context, before, center_time, 1.0);
   7916 
   7917     context.direction = +1.0;
   7918     s2 = Astronomy_Search(shadow_distance, &context, center_time, after, 1.0);
   7919 
   7920     if (s1.status != ASTRO_SUCCESS || s2.status != ASTRO_SUCCESS)
   7921         return -1.0;    /* something went wrong! */
   7922 
   7923     return (s2.time.ut - s1.time.ut) * ((24.0 * 60.0) / 2.0);       /* convert days to minutes and average the semi-durations. */
   7924 }
   7925 
   7926 
   7927 /**
   7928  * @brief Searches for a lunar eclipse.
   7929  *
   7930  * This function finds the first lunar eclipse that occurs after `startTime`.
   7931  * A lunar eclipse may be penumbral, partial, or total.
   7932  * See #astro_lunar_eclipse_t for more information.
   7933  * To find a series of lunar eclipses, call this function once,
   7934  * then keep calling #Astronomy_NextLunarEclipse as many times as desired,
   7935  * passing in the `peak` value returned from the previous call.
   7936  *
   7937  * @param startTime
   7938  *      The date and time for starting the search for a lunar eclipse.
   7939  *
   7940  * @return
   7941  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   7942  *      and the remaining structure fields will be valid.
   7943  *      Any other value indicates an error.
   7944  */
   7945 astro_lunar_eclipse_t Astronomy_SearchLunarEclipse(astro_time_t startTime)
   7946 {
   7947     const double PruneLatitude = 1.8;   /* full Moon's ecliptic latitude above which eclipse is impossible */
   7948     astro_time_t fmtime;
   7949     astro_lunar_eclipse_t eclipse;
   7950     astro_search_result_t fullmoon;
   7951     shadow_t shadow;
   7952     int fmcount;
   7953     double eclip_lat, eclip_lon, distance;
   7954 
   7955     /* Iterate through consecutive full moons until we find any kind of lunar eclipse. */
   7956     fmtime = startTime;
   7957     for (fmcount=0; fmcount < 12; ++fmcount)
   7958     {
   7959         /* Search for the next full moon. Any eclipse will be near it. */
   7960         fullmoon = Astronomy_SearchMoonPhase(180.0, fmtime, 40.0);
   7961         if (fullmoon.status != ASTRO_SUCCESS)
   7962             return LunarEclipseError(fullmoon.status);
   7963 
   7964         /* Pruning: if the full Moon's ecliptic latitude is too large, a lunar eclipse is not possible. */
   7965         CalcMoon(fullmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
   7966         if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
   7967         {
   7968             /* Search near the full moon for the time when the center of the Moon */
   7969             /* is closest to the line passing through the centers of the Sun and Earth. */
   7970             shadow = PeakEarthShadow(fullmoon.time);
   7971             if (shadow.status != ASTRO_SUCCESS)
   7972                 return LunarEclipseError(shadow.status);
   7973 
   7974             if (shadow.r < shadow.p + MOON_MEAN_RADIUS_KM)
   7975             {
   7976                 /* This is at least a penumbral eclipse. We will return a result. */
   7977                 eclipse.status = ASTRO_SUCCESS;
   7978                 eclipse.kind = ECLIPSE_PENUMBRAL;
   7979                 eclipse.peak = shadow.time;
   7980                 eclipse.sd_total = 0.0;
   7981                 eclipse.sd_partial = 0.0;
   7982                 eclipse.sd_penum = ShadowSemiDurationMinutes(shadow.time, shadow.p + MOON_MEAN_RADIUS_KM, 200.0);
   7983                 if (eclipse.sd_penum <= 0.0)
   7984                     return LunarEclipseError(ASTRO_SEARCH_FAILURE);
   7985 
   7986                 if (shadow.r < shadow.k + MOON_MEAN_RADIUS_KM)
   7987                 {
   7988                     /* This is at least a partial eclipse. */
   7989                     eclipse.kind = ECLIPSE_PARTIAL;
   7990                     eclipse.sd_partial = ShadowSemiDurationMinutes(shadow.time, shadow.k + MOON_MEAN_RADIUS_KM, eclipse.sd_penum);
   7991                     if (eclipse.sd_partial <= 0.0)
   7992                         return LunarEclipseError(ASTRO_SEARCH_FAILURE);
   7993 
   7994                     if (shadow.r + MOON_MEAN_RADIUS_KM < shadow.k)
   7995                     {
   7996                         /* This is a total eclipse. */
   7997                         eclipse.kind = ECLIPSE_TOTAL;
   7998                         eclipse.sd_total = ShadowSemiDurationMinutes(shadow.time, shadow.k - MOON_MEAN_RADIUS_KM, eclipse.sd_partial);
   7999                         if (eclipse.sd_total <= 0.0)
   8000                             return LunarEclipseError(ASTRO_SEARCH_FAILURE);
   8001                     }
   8002                 }
   8003                 return eclipse;
   8004             }
   8005         }
   8006 
   8007         /* We didn't find an eclipse on this full moon, so search for the next one. */
   8008         fmtime = Astronomy_AddDays(fullmoon.time, 10.0);
   8009     }
   8010 
   8011     /* Safety valve to prevent infinite loop. */
   8012     /* This should never happen, because at least 2 lunar eclipses happen per year. */
   8013     return LunarEclipseError(ASTRO_INTERNAL_ERROR);
   8014 }
   8015 
   8016 /**
   8017  * @brief Searches for the next lunar eclipse in a series.
   8018  *
   8019  * After using #Astronomy_SearchLunarEclipse to find the first lunar eclipse
   8020  * in a series, you can call this function to find the next consecutive lunar eclipse.
   8021  * Pass in the `peak` value from the #astro_lunar_eclipse_t returned by the
   8022  * previous call to `Astronomy_SearchLunarEclipse` or `Astronomy_NextLunarEclipse`
   8023  * to find the next lunar eclipse.
   8024  *
   8025  * @param prevEclipseTime
   8026  *      A date and time near a full moon. Lunar eclipse search will start at the next full moon.
   8027  *
   8028  * @return
   8029  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   8030  *      and the remaining structure fields will be valid.
   8031  *      Any other value indicates an error.
   8032  */
   8033 astro_lunar_eclipse_t Astronomy_NextLunarEclipse(astro_time_t prevEclipseTime)
   8034 {
   8035     astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
   8036     return Astronomy_SearchLunarEclipse(startTime);
   8037 }
   8038 
   8039 
   8040 static astro_global_solar_eclipse_t GlobalSolarEclipseError(astro_status_t status)
   8041 {
   8042     astro_global_solar_eclipse_t eclipse;
   8043 
   8044     eclipse.status = status;
   8045     eclipse.kind = ECLIPSE_NONE;
   8046     eclipse.peak = TimeError();
   8047     eclipse.distance = eclipse.latitude = eclipse.longitude = NAN;
   8048 
   8049     return eclipse;
   8050 }
   8051 
   8052 /* The umbra radius tells us what kind of eclipse the observer sees. */
   8053 /* If the umbra radius is positive, this is a total eclipse. Otherwise, it's annular. */
   8054 /* HACK: I added a tiny bias (14 meters) to match Espenak test data. */
   8055 #define EclipseKindFromUmbra(k)     (((k) > 0.014) ? ECLIPSE_TOTAL : ECLIPSE_ANNULAR)
   8056 
   8057 static astro_global_solar_eclipse_t GeoidIntersect(shadow_t shadow)
   8058 {
   8059     astro_global_solar_eclipse_t eclipse;
   8060     astro_rotation_t rot, inv;
   8061     astro_vector_t v, e, o;
   8062     shadow_t surface;
   8063     double A, B, C, radic, u, R;
   8064     double px, py, pz, proj;
   8065     double gast;
   8066 
   8067     eclipse.status = ASTRO_SUCCESS;
   8068     eclipse.kind = ECLIPSE_PARTIAL;
   8069     eclipse.peak = shadow.time;
   8070     eclipse.distance = shadow.r;
   8071     eclipse.latitude = eclipse.longitude = NAN;
   8072 
   8073     /*
   8074         We want to calculate the intersection of the shadow axis with the Earth's geoid.
   8075         First we must convert EQJ (equator of J2000) coordinates to EQD (equator of date)
   8076         coordinates that are perfectly aligned with the Earth's equator at this
   8077         moment in time.
   8078     */
   8079     rot = Astronomy_Rotation_EQJ_EQD(shadow.time);
   8080     if (rot.status != ASTRO_SUCCESS)
   8081         return GlobalSolarEclipseError(rot.status);
   8082 
   8083     v = Astronomy_RotateVector(rot, shadow.dir);        /* shadow-axis vector in equator-of-date coordinates */
   8084     if (v.status != ASTRO_SUCCESS)
   8085         return GlobalSolarEclipseError(v.status);
   8086 
   8087     e = Astronomy_RotateVector(rot, shadow.target);     /* lunacentric Earth in equator-of-date coordinates */
   8088     if (e.status != ASTRO_SUCCESS)
   8089         return GlobalSolarEclipseError(e.status);
   8090 
   8091     /*
   8092         Convert all distances from AU to km.
   8093         But dilate the z-coordinates so that the Earth becomes a perfect sphere.
   8094         Then find the intersection of the vector with the sphere.
   8095         See p 184 in Montenbruck & Pfleger's "Astronomy on the Personal Computer", second edition.
   8096     */
   8097     v.x *= KM_PER_AU;
   8098     v.y *= KM_PER_AU;
   8099     v.z *= KM_PER_AU / EARTH_FLATTENING;
   8100 
   8101     e.x *= KM_PER_AU;
   8102     e.y *= KM_PER_AU;
   8103     e.z *= KM_PER_AU / EARTH_FLATTENING;
   8104 
   8105     /*
   8106         Solve the quadratic equation that finds whether and where
   8107         the shadow axis intersects with the Earth in the dilated coordinate system.
   8108     */
   8109     R = EARTH_EQUATORIAL_RADIUS_KM;
   8110     A = v.x*v.x + v.y*v.y + v.z*v.z;
   8111     B = -2.0 * (v.x*e.x + v.y*e.y + v.z*e.z);
   8112     C = (e.x*e.x + e.y*e.y + e.z*e.z) - R*R;
   8113     radic = B*B - 4*A*C;
   8114 
   8115     if (radic > 0.0)
   8116     {
   8117         /* Calculate the closer of the two intersection points. */
   8118         /* This will be on the day side of the Earth. */
   8119         u = (-B - sqrt(radic)) / (2 * A);
   8120 
   8121         /* Convert lunacentric dilated coordinates to geocentric coordinates. */
   8122         px = u*v.x - e.x;
   8123         py = u*v.y - e.y;
   8124         pz = (u*v.z - e.z) * EARTH_FLATTENING;
   8125 
   8126         /* Convert cartesian coordinates into geodetic latitude/longitude. */
   8127         proj = sqrt(px*px + py*py) * (EARTH_FLATTENING * EARTH_FLATTENING);
   8128         if (proj == 0.0)
   8129             eclipse.latitude = (pz > 0.0) ? +90.0 : -90.0;
   8130         else
   8131             eclipse.latitude = RAD2DEG * atan(pz / proj);
   8132 
   8133         /* Adjust longitude for Earth's rotation at the given UT. */
   8134         gast = sidereal_time(&eclipse.peak);
   8135         eclipse.longitude = fmod((RAD2DEG*atan2(py, px)) - (15*gast), 360.0);
   8136         if (eclipse.longitude <= -180.0)
   8137             eclipse.longitude += 360.0;
   8138         else if (eclipse.longitude > +180.0)
   8139             eclipse.longitude -= 360.0;
   8140 
   8141         /* We want to determine whether the observer sees a total eclipse or an annular eclipse. */
   8142         /* We need to perform a series of vector calculations... */
   8143         /* Calculate the inverse rotation matrix, so we can convert EQD to EQJ. */
   8144         inv = Astronomy_InverseRotation(rot);
   8145         if (inv.status != ASTRO_SUCCESS)
   8146             return GlobalSolarEclipseError(inv.status);
   8147 
   8148         /* Put the EQD geocentric coordinates of the observer into the vector 'o'. */
   8149         /* Also convert back from kilometers to astronomical units. */
   8150         o.status = ASTRO_SUCCESS;
   8151         o.t = shadow.time;
   8152         o.x = px / KM_PER_AU;
   8153         o.y = py / KM_PER_AU;
   8154         o.z = pz / KM_PER_AU;
   8155 
   8156         /* Rotate the observer's geocentric EQD back to the EQJ system. */
   8157         o = Astronomy_RotateVector(inv, o);
   8158 
   8159         /* Convert geocentric vector to lunacentric vector. */
   8160         o.x += shadow.target.x;
   8161         o.y += shadow.target.y;
   8162         o.z += shadow.target.z;
   8163 
   8164         /* Recalculate the shadow using a vector from the Moon's center toward the observer. */
   8165         surface = CalcShadow(MOON_POLAR_RADIUS_KM, shadow.time, o, shadow.dir);
   8166 
   8167         /* If we did everything right, the shadow distance should be very close to zero. */
   8168         /* That's because we already determined the observer 'o' is on the shadow axis! */
   8169         if (surface.r > 1.0e-9 || surface.r < 0.0)
   8170             return GlobalSolarEclipseError(ASTRO_INTERNAL_ERROR);
   8171 
   8172         eclipse.kind = EclipseKindFromUmbra(surface.k);
   8173     }
   8174 
   8175     return eclipse;
   8176 }
   8177 
   8178 
   8179 /**
   8180  * @brief Searches for a solar eclipse visible anywhere on the Earth's surface.
   8181  *
   8182  * This function finds the first solar eclipse that occurs after `startTime`.
   8183  * A solar eclipse may be partial, annular, or total.
   8184  * See #astro_global_solar_eclipse_t for more information.
   8185  * To find a series of solar eclipses, call this function once,
   8186  * then keep calling #Astronomy_NextGlobalSolarEclipse as many times as desired,
   8187  * passing in the `peak` value returned from the previous call.
   8188  *
   8189  * @param startTime
   8190  *      The date and time for starting the search for a solar eclipse.
   8191  *
   8192  * @return
   8193  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   8194  *      and the remaining structure fields are as described in #astro_global_solar_eclipse_t.
   8195  *      Any other value indicates an error.
   8196  */
   8197 astro_global_solar_eclipse_t Astronomy_SearchGlobalSolarEclipse(astro_time_t startTime)
   8198 {
   8199     const double PruneLatitude = 1.8;   /* Moon's ecliptic latitude beyond which eclipse is impossible */
   8200     astro_time_t nmtime;
   8201     astro_search_result_t newmoon;
   8202     shadow_t shadow;
   8203     int nmcount;
   8204     double eclip_lat, eclip_lon, distance;
   8205 
   8206     /* Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth. */
   8207     nmtime = startTime;
   8208     for (nmcount=0; nmcount < 12; ++nmcount)
   8209     {
   8210         /* Search for the next new moon. Any eclipse will be near it. */
   8211         newmoon = Astronomy_SearchMoonPhase(0.0, nmtime, 40.0);
   8212         if (newmoon.status != ASTRO_SUCCESS)
   8213             return GlobalSolarEclipseError(newmoon.status);
   8214 
   8215         /* Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible. */
   8216         CalcMoon(newmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
   8217         if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
   8218         {
   8219             /* Search near the new moon for the time when the center of the Earth */
   8220             /* is closest to the line passing through the centers of the Sun and Moon. */
   8221             shadow = PeakMoonShadow(newmoon.time);
   8222             if (shadow.status != ASTRO_SUCCESS)
   8223                 return GlobalSolarEclipseError(shadow.status);
   8224 
   8225             if (shadow.r < shadow.p + EARTH_MEAN_RADIUS_KM)
   8226             {
   8227                 /* This is at least a partial solar eclipse visible somewhere on Earth. */
   8228                 /* Try to find an intersection between the shadow axis and the Earth's oblate geoid. */
   8229                 return GeoidIntersect(shadow);
   8230             }
   8231         }
   8232 
   8233         /* We didn't find an eclipse on this new moon, so search for the next one. */
   8234         nmtime = Astronomy_AddDays(newmoon.time, 10.0);
   8235     }
   8236 
   8237     /* Safety valve to prevent infinite loop. */
   8238     /* This should never happen, because at least 2 solar eclipses happen per year. */
   8239     return GlobalSolarEclipseError(ASTRO_INTERNAL_ERROR);
   8240 }
   8241 
   8242 
   8243 /**
   8244  * @brief Searches for the next global solar eclipse in a series.
   8245  *
   8246  * After using #Astronomy_SearchGlobalSolarEclipse to find the first solar eclipse
   8247  * in a series, you can call this function to find the next consecutive solar eclipse.
   8248  * Pass in the `peak` value from the #astro_global_solar_eclipse_t returned by the
   8249  * previous call to `Astronomy_SearchGlobalSolarEclipse` or `Astronomy_NextGlobalSolarEclipse`
   8250  * to find the next solar eclipse.
   8251  *
   8252  * @param prevEclipseTime
   8253  *      A date and time near a new moon. Solar eclipse search will start at the next new moon.
   8254  *
   8255  * @return
   8256  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   8257  *      and the remaining structure fields are as described in #astro_global_solar_eclipse_t.
   8258  *      Any other value indicates an error.
   8259  */
   8260 astro_global_solar_eclipse_t Astronomy_NextGlobalSolarEclipse(astro_time_t prevEclipseTime)
   8261 {
   8262     astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
   8263     return Astronomy_SearchGlobalSolarEclipse(startTime);
   8264 }
   8265 
   8266 
   8267 static astro_eclipse_event_t EclipseEventError(void)
   8268 {
   8269     astro_eclipse_event_t evt;
   8270     evt.time = TimeError();
   8271     evt.altitude = NAN;
   8272     return evt;
   8273 }
   8274 
   8275 
   8276 static astro_local_solar_eclipse_t LocalSolarEclipseError(astro_status_t status)
   8277 {
   8278     astro_local_solar_eclipse_t eclipse;
   8279 
   8280     eclipse.status = status;
   8281     eclipse.kind = ECLIPSE_NONE;
   8282 
   8283     eclipse.partial_begin = EclipseEventError();
   8284     eclipse.total_begin   = EclipseEventError();
   8285     eclipse.peak          = EclipseEventError();
   8286     eclipse.total_end     = EclipseEventError();
   8287     eclipse.partial_end   = EclipseEventError();
   8288 
   8289     return eclipse;
   8290 }
   8291 
   8292 
   8293 static shadow_t LocalMoonShadow(astro_time_t time, astro_observer_t observer)
   8294 {
   8295     astro_vector_t h, o, m;
   8296     double pos[3];
   8297 
   8298     /* Calculate observer's geocentric position. */
   8299     /* For efficiency, do this first, to populate the earth rotation parameters in 'time'. */
   8300     /* That way they can be recycled instead of recalculated. */
   8301     geo_pos(&time, observer, pos);
   8302 
   8303     h = CalcEarth(time);            /* heliocentric Earth */
   8304     m = Astronomy_GeoMoon(time);    /* geocentric Moon */
   8305 
   8306     /* Calculate lunacentric location of an observer on the Earth's surface. */
   8307     o.status = m.status;
   8308     o.x = pos[0] - m.x;
   8309     o.y = pos[1] - m.y;
   8310     o.z = pos[2] - m.z;
   8311     o.t = m.t;
   8312 
   8313     /* Convert geocentric moon to heliocentric Moon. */
   8314     m.x += h.x;
   8315     m.y += h.y;
   8316     m.z += h.z;
   8317 
   8318     return CalcShadow(MOON_MEAN_RADIUS_KM, time, o, m);
   8319 }
   8320 
   8321 
   8322 static astro_func_result_t local_shadow_distance_slope(void *context, astro_time_t time)
   8323 {
   8324     const double dt = 1.0 / 86400.0;
   8325     astro_time_t t1, t2;
   8326     astro_func_result_t result;
   8327     shadow_t shadow1, shadow2;
   8328     const astro_observer_t *observer = context;
   8329 
   8330     t1 = Astronomy_AddDays(time, -dt);
   8331     t2 = Astronomy_AddDays(time, +dt);
   8332 
   8333     shadow1 = LocalMoonShadow(t1, *observer);
   8334     if (shadow1.status != ASTRO_SUCCESS)
   8335         return FuncError(shadow1.status);
   8336 
   8337     shadow2 = LocalMoonShadow(t2, *observer);
   8338     if (shadow2.status != ASTRO_SUCCESS)
   8339         return FuncError(shadow2.status);
   8340 
   8341     result.value = (shadow2.r - shadow1.r) / dt;
   8342     result.status = ASTRO_SUCCESS;
   8343     return result;
   8344 }
   8345 
   8346 
   8347 static shadow_t PeakLocalMoonShadow(astro_time_t search_center_time, astro_observer_t observer)
   8348 {
   8349     astro_time_t t1, t2;
   8350     astro_search_result_t result;
   8351     const double window = 0.2;
   8352 
   8353     /*
   8354         Search for the time near search_center_time that the Moon's shadow comes
   8355         closest to the given observer.
   8356     */
   8357 
   8358     t1 = Astronomy_AddDays(search_center_time, -window);
   8359     t2 = Astronomy_AddDays(search_center_time, +window);
   8360 
   8361     result = Astronomy_Search(local_shadow_distance_slope, &observer, t1, t2, 1.0);
   8362     if (result.status != ASTRO_SUCCESS)
   8363         return ShadowError(result.status);
   8364 
   8365     return LocalMoonShadow(result.time, observer);
   8366 }
   8367 
   8368 
   8369 static double local_partial_distance(const shadow_t *shadow)
   8370 {
   8371     return shadow->p - shadow->r;
   8372 }
   8373 
   8374 static double local_total_distance(const shadow_t *shadow)
   8375 {
   8376     /* Must take the absolute value of the umbra radius 'k' */
   8377     /* because it can be negative for an annular eclipse. */
   8378     return fabs(shadow->k) - shadow->r;
   8379 }
   8380 
   8381 /** @cond DOXYGEN_SKIP */
   8382 typedef double (* local_distance_func) (const shadow_t *shadow);
   8383 
   8384 typedef struct
   8385 {
   8386     local_distance_func     func;
   8387     double                  direction;
   8388     astro_observer_t        observer;
   8389 }
   8390 eclipse_transition_t;
   8391 /* @endcond */
   8392 
   8393 
   8394 static astro_func_result_t local_eclipse_func(void *context, astro_time_t time)
   8395 {
   8396     const eclipse_transition_t *trans = context;
   8397     shadow_t shadow;
   8398     astro_func_result_t result;
   8399 
   8400     shadow = LocalMoonShadow(time, trans->observer);
   8401     if (shadow.status != ASTRO_SUCCESS)
   8402         return FuncError(shadow.status);
   8403 
   8404     result.value = trans->direction * trans->func(&shadow);
   8405     result.status = ASTRO_SUCCESS;
   8406     return result;
   8407 }
   8408 
   8409 
   8410 astro_func_result_t SunAltitude(
   8411     astro_time_t time,
   8412     astro_observer_t observer)
   8413 {
   8414     astro_equatorial_t equ;
   8415     astro_horizon_t hor;
   8416     astro_func_result_t result;
   8417 
   8418     equ = Astronomy_Equator(BODY_SUN, &time, observer, EQUATOR_OF_DATE, ABERRATION);
   8419     if (equ.status != ASTRO_SUCCESS)
   8420         return FuncError(equ.status);
   8421 
   8422     hor = Astronomy_Horizon(&time, observer, equ.ra, equ.dec, REFRACTION_NORMAL);
   8423     result.value = hor.altitude;
   8424     result.status = ASTRO_SUCCESS;
   8425     return result;
   8426 }
   8427 
   8428 
   8429 static astro_status_t CalcEvent(
   8430     astro_observer_t observer,
   8431     astro_time_t time,
   8432     astro_eclipse_event_t *evt)
   8433 {
   8434     astro_func_result_t result;
   8435 
   8436     result = SunAltitude(time, observer);
   8437     if (result.status != ASTRO_SUCCESS)
   8438     {
   8439         evt->time = TimeError();
   8440         evt->altitude = NAN;
   8441         return result.status;
   8442     }
   8443 
   8444     evt->time = time;
   8445     evt->altitude = result.value;
   8446     return ASTRO_SUCCESS;
   8447 }
   8448 
   8449 
   8450 static astro_status_t LocalEclipseTransition(
   8451     astro_observer_t observer,
   8452     double direction,
   8453     local_distance_func func,
   8454     astro_time_t t1,
   8455     astro_time_t t2,
   8456     astro_eclipse_event_t *evt)
   8457 {
   8458     eclipse_transition_t trans;
   8459     astro_search_result_t search;
   8460 
   8461     trans.func = func;
   8462     trans.direction = direction;
   8463     trans.observer = observer;
   8464 
   8465     search = Astronomy_Search(local_eclipse_func, &trans, t1, t2, 1.0);
   8466     if (search.status != ASTRO_SUCCESS)
   8467     {
   8468         evt->time = TimeError();
   8469         evt->altitude = NAN;
   8470         return search.status;
   8471     }
   8472 
   8473     return CalcEvent(observer, search.time, evt);
   8474 }
   8475 
   8476 
   8477 static astro_local_solar_eclipse_t LocalEclipse(
   8478     shadow_t shadow,
   8479     astro_observer_t observer)
   8480 {
   8481     const double PARTIAL_WINDOW = 0.2;
   8482     const double TOTAL_WINDOW = 0.01;
   8483     astro_local_solar_eclipse_t eclipse;
   8484     astro_time_t t1, t2;
   8485     astro_status_t status;
   8486 
   8487     status = CalcEvent(observer, shadow.time, &eclipse.peak);
   8488     if (status != ASTRO_SUCCESS)
   8489         return LocalSolarEclipseError(status);
   8490 
   8491     t1 = Astronomy_AddDays(shadow.time, -PARTIAL_WINDOW);
   8492     t2 = Astronomy_AddDays(shadow.time, +PARTIAL_WINDOW);
   8493 
   8494     status = LocalEclipseTransition(observer, +1.0, local_partial_distance, t1, shadow.time, &eclipse.partial_begin);
   8495     if (status != ASTRO_SUCCESS)
   8496         return LocalSolarEclipseError(status);
   8497 
   8498     status = LocalEclipseTransition(observer, -1.0, local_partial_distance, shadow.time, t2, &eclipse.partial_end);
   8499     if (status != ASTRO_SUCCESS)
   8500         return LocalSolarEclipseError(status);
   8501 
   8502     if (shadow.r < fabs(shadow.k))      /* take absolute value of 'k' to handle annular eclipses too. */
   8503     {
   8504         t1 = Astronomy_AddDays(shadow.time, -TOTAL_WINDOW);
   8505         t2 = Astronomy_AddDays(shadow.time, +TOTAL_WINDOW);
   8506 
   8507         status = LocalEclipseTransition(observer, +1.0, local_total_distance, t1, shadow.time, &eclipse.total_begin);
   8508         if (status != ASTRO_SUCCESS)
   8509             return LocalSolarEclipseError(status);
   8510 
   8511         status = LocalEclipseTransition(observer, -1.0, local_total_distance, shadow.time, t2, &eclipse.total_end);
   8512         if (status != ASTRO_SUCCESS)
   8513             return LocalSolarEclipseError(status);
   8514 
   8515         eclipse.kind = EclipseKindFromUmbra(shadow.k);
   8516     }
   8517     else
   8518     {
   8519         eclipse.total_begin = eclipse.total_end = EclipseEventError();
   8520         eclipse.kind = ECLIPSE_PARTIAL;
   8521     }
   8522 
   8523     eclipse.status = ASTRO_SUCCESS;
   8524     return eclipse;
   8525 }
   8526 
   8527 
   8528 /**
   8529  * @brief Searches for a solar eclipse visible at a specific location on the Earth's surface.
   8530  *
   8531  * This function finds the first solar eclipse that occurs after `startTime`.
   8532  * A solar eclipse may be partial, annular, or total.
   8533  * See #astro_local_solar_eclipse_t for more information.
   8534  * To find a series of solar eclipses, call this function once,
   8535  * then keep calling #Astronomy_NextLocalSolarEclipse as many times as desired,
   8536  * passing in the `peak` value returned from the previous call.
   8537  *
   8538  * IMPORTANT: An eclipse reported by this function might be partly or
   8539  * completely invisible to the observer due to the time of day.
   8540  *
   8541  * @param startTime
   8542  *      The date and time for starting the search for a solar eclipse.
   8543  *
   8544  * @param observer
   8545  *      The geographic location of the observer.
   8546  *
   8547  * @return
   8548  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   8549  *      and the remaining structure fields are as described in #astro_local_solar_eclipse_t.
   8550  *      Any other value indicates an error.
   8551  */
   8552 astro_local_solar_eclipse_t Astronomy_SearchLocalSolarEclipse(
   8553     astro_time_t startTime,
   8554     astro_observer_t observer)
   8555 {
   8556     const double PruneLatitude = 1.8;   /* Moon's ecliptic latitude beyond which eclipse is impossible */
   8557     astro_time_t nmtime;
   8558     astro_search_result_t newmoon;
   8559     shadow_t shadow;
   8560     double eclip_lat, eclip_lon, distance;
   8561     astro_local_solar_eclipse_t eclipse;
   8562 
   8563     /* Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth. */
   8564     nmtime = startTime;
   8565     for(;;)
   8566     {
   8567         /* Search for the next new moon. Any eclipse will be near it. */
   8568         newmoon = Astronomy_SearchMoonPhase(0.0, nmtime, 40.0);
   8569         if (newmoon.status != ASTRO_SUCCESS)
   8570             return LocalSolarEclipseError(newmoon.status);
   8571 
   8572         /* Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible. */
   8573         CalcMoon(newmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
   8574         if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
   8575         {
   8576             /* Search near the new moon for the time when the observer */
   8577             /* is closest to the line passing through the centers of the Sun and Moon. */
   8578             shadow = PeakLocalMoonShadow(newmoon.time, observer);
   8579             if (shadow.status != ASTRO_SUCCESS)
   8580                 return LocalSolarEclipseError(shadow.status);
   8581 
   8582             if (shadow.r < shadow.p)
   8583             {
   8584                 /* This is at least a partial solar eclipse for the observer. */
   8585                 eclipse = LocalEclipse(shadow, observer);
   8586 
   8587                 /* If any error occurs, something is really wrong and we should bail out. */
   8588                 if (eclipse.status != ASTRO_SUCCESS)
   8589                     return eclipse;
   8590 
   8591                 /* Ignore any eclipse that happens completely at night. */
   8592                 /* More precisely, the center of the Sun must be above the horizon */
   8593                 /* at the beginning or the end of the eclipse, or we skip the event. */
   8594                 if (eclipse.partial_begin.altitude > 0.0 || eclipse.partial_end.altitude > 0.0)
   8595                     return eclipse;
   8596             }
   8597         }
   8598 
   8599         /* We didn't find an eclipse on this new moon, so search for the next one. */
   8600         nmtime = Astronomy_AddDays(newmoon.time, 10.0);
   8601     }
   8602 }
   8603 
   8604 
   8605 /**
   8606  * @brief Searches for the next local solar eclipse in a series.
   8607  *
   8608  * After using #Astronomy_SearchLocalSolarEclipse to find the first solar eclipse
   8609  * in a series, you can call this function to find the next consecutive solar eclipse.
   8610  * Pass in the `peak` value from the #astro_local_solar_eclipse_t returned by the
   8611  * previous call to `Astronomy_SearchLocalSolarEclipse` or `Astronomy_NextLocalSolarEclipse`
   8612  * to find the next solar eclipse.
   8613  *
   8614  * @param prevEclipseTime
   8615  *      A date and time near a new moon. Solar eclipse search will start at the next new moon.
   8616  *
   8617  * @param observer
   8618  *      The geographic location of the observer.
   8619  *
   8620  * @return
   8621  *      If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
   8622  *      and the remaining structure fields are as described in #astro_local_solar_eclipse_t.
   8623  *      Any other value indicates an error.
   8624  */
   8625 astro_local_solar_eclipse_t Astronomy_NextLocalSolarEclipse(
   8626     astro_time_t prevEclipseTime,
   8627     astro_observer_t observer)
   8628 {
   8629     astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
   8630     return Astronomy_SearchLocalSolarEclipse(startTime, observer);
   8631 }
   8632 
   8633 
   8634 static astro_func_result_t planet_transit_bound(void *context, astro_time_t time)
   8635 {
   8636     shadow_t shadow;
   8637     astro_func_result_t result;
   8638     const planet_shadow_context_t *p = context;
   8639 
   8640     shadow = PlanetShadow(p->body, p->planet_radius_km, time);
   8641     if (shadow.status != ASTRO_SUCCESS)
   8642         return FuncError(shadow.status);
   8643 
   8644     result.status = ASTRO_SUCCESS;
   8645     result.value = p->direction * (shadow.r - shadow.p);
   8646     return result;
   8647 }
   8648 
   8649 
   8650 static astro_search_result_t PlanetTransitBoundary(
   8651     astro_body_t body,
   8652     double planet_radius_km,
   8653     astro_time_t t1,
   8654     astro_time_t t2,
   8655     double direction)
   8656 {
   8657     /* Search for the time the planet's penumbra begins/ends making contact with the center of the Earth. */
   8658     planet_shadow_context_t context;
   8659 
   8660     context.body = body;
   8661     context.planet_radius_km = planet_radius_km;
   8662     context.direction = direction;
   8663 
   8664     return Astronomy_Search(planet_transit_bound, &context, t1, t2, 1.0);
   8665 }
   8666 
   8667 
   8668 /**
   8669  * @brief Searches for the first transit of Mercury or Venus after a given date.
   8670  *
   8671  * Finds the first transit of Mercury or Venus after a specified date.
   8672  * A transit is when an inferior planet passes between the Sun and the Earth
   8673  * so that the silhouette of the planet is visible against the Sun in the background.
   8674  * To continue the search, pass the `finish` time in the returned structure to
   8675  * #Astronomy_NextTransit.
   8676  *
   8677  * @param body
   8678  *      The planet whose transit is to be found. Must be `BODY_MERCURY` or `BODY_VENUS`.
   8679  *
   8680  * @param startTime
   8681  *      The date and time for starting the search for a transit.
   8682  *
   8683  * @return
   8684  *      If successful, the `status` field in the returned structure hold `ASTRO_SUCCESS`
   8685  *      and the other fields are as documented in #astro_transit_t.
   8686  *      Otherwise, `status` holds an error code and the other structure members are undefined.
   8687  */
   8688 astro_transit_t Astronomy_SearchTransit(astro_body_t body, astro_time_t startTime)
   8689 {
   8690     astro_time_t search_time;
   8691     astro_transit_t transit;
   8692     astro_search_result_t conj, search;
   8693     astro_angle_result_t conj_separation, min_separation;
   8694     shadow_t shadow;
   8695     double planet_radius_km;
   8696     astro_time_t tx;
   8697     const double threshold_angle = 0.4;     /* maximum angular separation to attempt transit calculation */
   8698     const double dt_days = 1.0;
   8699 
   8700     /* Validate the planet and find its mean radius. */
   8701     switch (body)
   8702     {
   8703     case BODY_MERCURY:  planet_radius_km = 2439.7;  break;
   8704     case BODY_VENUS:    planet_radius_km = 6051.8;  break;
   8705     default:
   8706         return TransitErr(ASTRO_INVALID_BODY);
   8707     }
   8708 
   8709     search_time = startTime;
   8710     for(;;)
   8711     {
   8712         /*
   8713             Search for the next inferior conjunction of the given planet.
   8714             This is the next time the Earth and the other planet have the same
   8715             ecliptic longitude as seen from the Sun.
   8716         */
   8717         conj = Astronomy_SearchRelativeLongitude(body, 0.0, search_time);
   8718         if (conj.status != ASTRO_SUCCESS)
   8719             return TransitErr(conj.status);
   8720 
   8721         /* Calculate the angular separation between the body and the Sun at this time. */
   8722         conj_separation = Astronomy_AngleFromSun(body, conj.time);
   8723         if (conj_separation.status != ASTRO_SUCCESS)
   8724             return TransitErr(conj_separation.status);
   8725 
   8726         if (conj_separation.angle < threshold_angle)
   8727         {
   8728             /*
   8729                 The planet's angular separation from the Sun is small enough
   8730                 to consider it a transit candidate.
   8731                 Search for the moment when the line passing through the Sun
   8732                 and planet are closest to the Earth's center.
   8733             */
   8734             shadow = PeakPlanetShadow(body, planet_radius_km, conj.time);
   8735             if (shadow.status != ASTRO_SUCCESS)
   8736                 return TransitErr(shadow.status);
   8737 
   8738             if (shadow.r < shadow.p)        /* does the planet's penumbra touch the Earth's center? */
   8739             {
   8740                 /* Find the beginning and end of the penumbral contact. */
   8741                 tx = Astronomy_AddDays(shadow.time, -dt_days);
   8742                 search = PlanetTransitBoundary(body, planet_radius_km, tx, shadow.time, -1.0);
   8743                 if (search.status != ASTRO_SUCCESS)
   8744                     return TransitErr(search.status);
   8745                 transit.start = search.time;
   8746 
   8747                 tx = Astronomy_AddDays(shadow.time, +dt_days);
   8748                 search = PlanetTransitBoundary(body, planet_radius_km, shadow.time, tx, +1.0);
   8749                 if (search.status != ASTRO_SUCCESS)
   8750                     return TransitErr(search.status);
   8751                 transit.finish = search.time;
   8752                 transit.status = ASTRO_SUCCESS;
   8753                 transit.peak = shadow.time;
   8754 
   8755                 min_separation = Astronomy_AngleFromSun(body, shadow.time);
   8756                 if (min_separation.status != ASTRO_SUCCESS)
   8757                     return TransitErr(min_separation.status);
   8758 
   8759                 transit.separation = 60.0 * min_separation.angle;   /* convert degrees to arcminutes */
   8760                 return transit;
   8761             }
   8762         }
   8763 
   8764         /* This inferior conjunction was not a transit. Try the next inferior conjunction. */
   8765         search_time = Astronomy_AddDays(conj.time, 10.0);
   8766     }
   8767 }
   8768 
   8769 
   8770 /**
   8771  * @brief Searches for another transit of Mercury or Venus.
   8772  *
   8773  * After calling #Astronomy_SearchTransit to find a transit of Mercury or Venus,
   8774  * this function finds the next transit after that.
   8775  * Keep calling this function as many times as you want to keep finding more transits.
   8776  *
   8777  * @param body
   8778  *      The planet whose transit is to be found. Must be `BODY_MERCURY` or `BODY_VENUS`.
   8779  *
   8780  * @param prevTransitTime
   8781  *      A date and time near the previous transit.
   8782  *
   8783  * @return
   8784  *      If successful, the `status` field in the returned structure hold `ASTRO_SUCCESS`
   8785  *      and the other fields are as documented in #astro_transit_t.
   8786  *      Otherwise, `status` holds an error code and the other structure members are undefined.
   8787  */
   8788 astro_transit_t Astronomy_NextTransit(astro_body_t body, astro_time_t prevTransitTime)
   8789 {
   8790     astro_time_t startTime;
   8791 
   8792     startTime = Astronomy_AddDays(prevTransitTime, 100.0);
   8793     return Astronomy_SearchTransit(body, startTime);
   8794 }
   8795 
   8796 
   8797 /**
   8798  * @brief Frees up all dynamic memory allocated by Astronomy Engine.
   8799  *
   8800  * Astronomy Engine uses dynamic memory allocation in only one place:
   8801  * it makes calculation of Pluto's orbit more efficient by caching 11 KB
   8802  * segments recycling them. To force purging this cache and
   8803  * freeing all the dynamic memory, you can call this function at any time.
   8804  * It is always safe to call, although it will slow down the very next
   8805  * calculation of Pluto's position for a nearby time value.
   8806  * Calling this function before your program exits is optional, but
   8807  * it will be helpful for leak-checkers like valgrind.
   8808  */
   8809 void Astronomy_Reset(void)
   8810 {
   8811     int i;
   8812     for (i=0; i < PLUTO_NUM_STATES-1; ++i)
   8813     {
   8814         free(pluto_cache[i]);
   8815         pluto_cache[i] = NULL;
   8816     }
   8817 }
   8818 
   8819 
   8820 #ifdef __cplusplus
   8821 }
   8822 #endif