Packages/MoonriseKit/Sources/CAstronomy/astronomy.c
13058 lines · 481221 bytes
1/*
2 Astronomy Engine for C/C++.
3 https://github.com/cosinekitty/astronomy
4
5 MIT License
6
7 Copyright (c) 2019-2023 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 <math.h>
29#include <stdint.h>
30#include <stdio.h>
31#include <stdlib.h>
32#include <string.h>
33
34#if !defined(ASTRONOMY_ENGINE_NO_CURRENT_TIME)
35#if defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))
36#include <sys/time.h>
37#elif defined(_WIN32)
38#define WIN32_LEAN_AND_MEAN
39#include <windows.h>
40#else
41#include <time.h>
42#endif
43#endif
44
45#include "astronomy.h"
46
47#ifdef __FAST_MATH__
48#error Astronomy Engine does not support "fast math" optimization because it causes incorrect behavior. See: https://github.com/cosinekitty/astronomy/issues/245
49#endif
50
51#ifdef __cplusplus
52extern "C" {
53#endif
54
55/** @cond DOXYGEN_SKIP */
56#define PI 3.14159265358979323846
57
58#define PLUTO_NUM_STATES 51
59#define PLUTO_TIME_STEP 29200
60#define PLUTO_DT 146
61
62#define PLUTO_NSTEPS 201
63
64
65
66typedef enum
67{
68 FROM_2000,
69 INTO_2000
70}
71precess_dir_t;
72
73typedef struct
74{
75 double x;
76 double y;
77 double z;
78}
79terse_vector_t;
80
81typedef struct
82{
83 double tt; /* J2000 terrestrial time [days] */
84 terse_vector_t r; /* position [au] */
85 terse_vector_t v; /* velocity [au/day] */
86 terse_vector_t a; /* acceleration [au/day^2] */
87}
88body_grav_calc_t;
89
90typedef struct
91{
92 body_grav_calc_t step[PLUTO_NSTEPS];
93}
94body_segment_t;
95
96typedef struct
97{
98 double tt; /* Terrestrial Time in J2000 days */
99 terse_vector_t r; /* position [au] */
100 terse_vector_t v; /* velocity [au/day] */
101}
102body_state_t;
103
104typedef struct
105{
106 body_state_t Sun;
107 body_state_t Jupiter;
108 body_state_t Saturn;
109 body_state_t Uranus;
110 body_state_t Neptune;
111}
112major_bodies_t;
113
114typedef struct
115{
116 astro_time_t time;
117 body_state_t gravitators[1 + BODY_SUN];
118 body_grav_calc_t *bodies;
119}
120gravsim_endpoint_t;
121
122struct astro_grav_sim_s
123{
124 astro_body_t originBody;
125 int numBodies;
126 gravsim_endpoint_t endpoint[2];
127 gravsim_endpoint_t *prev;
128 gravsim_endpoint_t *curr;
129};
130
131typedef struct
132{
133 double ra;
134 double dec;
135 double dist;
136}
137stardef_t;
138
139/* Mean obliquity of the J2000 ecliptic in radians. */
140#define OBLIQ_2000 0.40909260059599012
141#define COS_OBLIQ_2000 0.9174821430670688
142#define SIN_OBLIQ_2000 0.3977769691083922
143
144/** @endcond */
145
146#define NSTARS 8
147static stardef_t StarTable[NSTARS];
148
149#define GetStarPointer(body) (((body) >= BODY_STAR1) && ((body) <= BODY_STAR8) ? &StarTable[(body) - BODY_STAR1] : NULL)
150
151static stardef_t *UserDefinedStar(astro_body_t body)
152{
153 stardef_t *star = GetStarPointer(body);
154 if (star != NULL && star->dist > 0.0)
155 return star;
156 return NULL;
157}
158
159
160/**
161 * @brief Assign equatorial coordinates to a user-defined star.
162 *
163 * Some Astronomy Engine functions allow their `body` parameter to
164 * be a user-defined fixed point in the sky, loosely called a "star".
165 * This function assigns a right ascension, declination, and distance
166 * to one of the eight user-defined stars `BODY_STAR1` .. `BODY_STAR8`.
167 *
168 * Stars are not valid until defined. Once defined, they retain their
169 * definition until re-defined by another call to `Astronomy_DefineStar`.
170 *
171 * @param body
172 * One of the eight user-defined star identifiers: `BODY_STAR1` .. `BODY_STAR8`.
173 *
174 * @param ra
175 * The right ascension to be assigned to the star, expressed in J2000 equatorial coordinates (EQJ).
176 * The value is in units of sidereal hours, and must be within the half-open range [0, 24).
177 *
178 * @param dec
179 * The declination to be assigned to the star, expressed in J2000 equatorial coordinates (EQJ).
180 * The value is in units of degrees north (positive) or south (negative) of the J2000 equator,
181 * and must be within the closed range [-90, +90].
182 *
183 * @param distanceLightYears
184 * The distance between the star and the Sun, expressed in light-years.
185 * This value is used to calculate the tiny parallax shift as seen by an observer on Earth.
186 * If you don't know the distance to the star, using a large value like 1000 will generally work well.
187 * The minimum allowed distance is 1 light-year, which is required to provide certain internal optimizations.
188 *
189 * @return
190 * `ASTRO_SUCCESS` indicates the star has been defined. Any other value indicates an error,
191 * in which case no change has taken place to any of the star definitions.
192 */
193astro_status_t Astronomy_DefineStar(astro_body_t body, double ra, double dec, double distanceLightYears)
194{
195 stardef_t *star = GetStarPointer(body);
196
197 if (star == NULL)
198 return ASTRO_INVALID_BODY;
199
200 if (!isfinite(ra) || ra < 0.0 || ra >= 24.0)
201 return ASTRO_INVALID_PARAMETER;
202
203 if (!isfinite(dec) || dec < -90.0 || dec > +90.0)
204 return ASTRO_INVALID_PARAMETER;
205
206 if (!isfinite(distanceLightYears) || distanceLightYears < 1.0)
207 return ASTRO_INVALID_PARAMETER;
208
209 star->ra = ra;
210 star->dec = dec;
211 star->dist = distanceLightYears * AU_PER_LY;
212
213 return ASTRO_SUCCESS;
214}
215
216static const terse_vector_t VecZero = { 0.0, 0.0, 0.0 };
217
218static terse_vector_t VecAdd(terse_vector_t a, terse_vector_t b)
219{
220 terse_vector_t c;
221 c.x = a.x + b.x;
222 c.y = a.y + b.y;
223 c.z = a.z + b.z;
224 return c;
225}
226
227static void VecIncr(terse_vector_t *target, terse_vector_t source)
228{
229 target->x += source.x;
230 target->y += source.y;
231 target->z += source.z;
232}
233
234static void VecDecr(terse_vector_t *target, terse_vector_t source)
235{
236 target->x -= source.x;
237 target->y -= source.y;
238 target->z -= source.z;
239}
240
241static terse_vector_t VecMul(double s, terse_vector_t v)
242{
243 terse_vector_t p;
244 p.x = s * v.x;
245 p.y = s * v.y;
246 p.z = s * v.z;
247 return p;
248}
249
250static void VecScale(terse_vector_t *target, double scalar)
251{
252 target->x *= scalar;
253 target->y *= scalar;
254 target->z *= scalar;
255}
256
257static terse_vector_t VecRamp(terse_vector_t a, terse_vector_t b, double ramp)
258{
259 terse_vector_t c;
260 c.x = (1-ramp)*a.x + ramp*b.x;
261 c.y = (1-ramp)*a.y + ramp*b.y;
262 c.z = (1-ramp)*a.z + ramp*b.z;
263 return c;
264}
265
266static terse_vector_t VecMean(terse_vector_t a, terse_vector_t b)
267{
268 terse_vector_t c;
269 c.x = (a.x + b.x) / 2;
270 c.y = (a.y + b.y) / 2;
271 c.z = (a.z + b.z) / 2;
272 return c;
273}
274
275static astro_state_vector_t ExportState(body_state_t terse, astro_time_t time)
276{
277 astro_state_vector_t state;
278
279 state.status = ASTRO_SUCCESS;
280 state.x = terse.r.x;
281 state.y = terse.r.y;
282 state.z = terse.r.z;
283 state.vx = terse.v.x;
284 state.vy = terse.v.y;
285 state.vz = terse.v.z;
286 state.t = time;
287
288 return state;
289}
290
291static astro_state_vector_t ExportGravCalc(body_grav_calc_t calc, astro_time_t time)
292{
293 astro_state_vector_t state;
294
295 state.status = ASTRO_SUCCESS;
296 state.x = calc.r.x;
297 state.y = calc.r.y;
298 state.z = calc.r.z;
299 state.vx = calc.v.x;
300 state.vy = calc.v.y;
301 state.vz = calc.v.z;
302 state.t = time;
303
304 return state;
305}
306
307static const double DAYS_PER_TROPICAL_YEAR = 365.24217;
308static const double ASEC360 = 1296000.0;
309static const double ASEC2RAD = 4.848136811095359935899141e-6;
310static const double PI2 = 2.0 * PI;
311static const double ARC = 3600.0 * 180.0 / PI; /* arcseconds per radian */
312static const double SECONDS_PER_DAY = 24.0 * 3600.0;
313static const double SOLAR_DAYS_PER_SIDEREAL_DAY = 0.9972695717592592;
314static const double MEAN_SYNODIC_MONTH = 29.530588; /* average number of days for Moon to return to the same phase */
315static const double EARTH_ORBITAL_PERIOD = 365.256;
316static const double NEPTUNE_ORBITAL_PERIOD = 60189.0;
317
318/*
319 Degrees of refractive "lift" seen for objects near horizon.
320 More precisely, the angle below the horizon a point has to be, at sea level,
321 to appear to be exactly on the horizon.
322 If the ground plane is higher than sea level, this angle
323 needs to be corrected for decreased atmospheric density.
324*/
325static const double REFRACTION_NEAR_HORIZON = 34.0 / 60.0;
326
327
328#define SUN_RADIUS_AU (SUN_RADIUS_KM / KM_PER_AU)
329
330#define EARTH_MEAN_RADIUS_KM 6371.0 /* mean radius of the Earth's geoid, without atmosphere */
331#define EARTH_ATMOSPHERE_KM 88.0 /* effective atmosphere thickness for lunar eclipses. see: https://eclipse.gsfc.nasa.gov/LEcat5/shadow.html */
332#define EARTH_ECLIPSE_RADIUS_KM (EARTH_MEAN_RADIUS_KM + EARTH_ATMOSPHERE_KM)
333#define EARTH_EQUATORIAL_RADIUS_AU (EARTH_EQUATORIAL_RADIUS_KM / KM_PER_AU)
334
335#define MOON_MEAN_RADIUS_KM 1737.4
336#define MOON_EQUATORIAL_RADIUS_AU (MOON_EQUATORIAL_RADIUS_KM / KM_PER_AU)
337#define MOON_POLAR_RADIUS_AU (MOON_POLAR_RADIUS_KM / KM_PER_AU)
338
339/* The inclination of the moon's rotation axis to the ecliptic plane, in radians. */
340#define MOON_AXIS_INCLINATION_RADIANS (DEG2RAD * 1.543)
341
342static const double ASEC180 = 180.0 * 60.0 * 60.0; /* arcseconds per 180 degrees (or pi radians) */
343static const double EARTH_MOON_MASS_RATIO = 81.30056;
344
345/*
346 Masses of the Sun and planets, used for:
347 (1) Calculating the Solar System Barycenter
348 (2) Integrating the movement of Pluto
349
350 https://web.archive.org/web/20120220062549/http://iau-comm4.jpl.nasa.gov/de405iom/de405iom.pdf
351
352 Page 10 in the above document describes the constants used in the DE405 ephemeris.
353 The following are GM values (gravity constant * mass) in [au^3 / day^2].
354 This side-steps issues of not knowing the exact values of G and masses M[i];
355 the products GM[i] are known extremely accurately.
356*/
357static const double SUN_GM = 0.2959122082855911e-03;
358static const double MERCURY_GM = 0.4912547451450812e-10;
359static const double VENUS_GM = 0.7243452486162703e-09;
360static const double EARTH_GM = 0.8887692390113509e-09;
361static const double MARS_GM = 0.9549535105779258e-10;
362static const double JUPITER_GM = 0.2825345909524226e-06;
363static const double SATURN_GM = 0.8459715185680659e-07;
364static const double URANUS_GM = 0.1292024916781969e-07;
365static const double NEPTUNE_GM = 0.1524358900784276e-07;
366static const double PLUTO_GM = 0.2188699765425970e-11;
367
368#define MOON_GM (EARTH_GM / EARTH_MOON_MASS_RATIO)
369
370/** @cond DOXYGEN_SKIP */
371#define ASTRO_ARRAYSIZE(x) (sizeof(x) / sizeof(x[0]))
372#define AU_PER_PARSEC (ASEC180 / PI) /* exact definition of how many AU = one parsec */
373#define Y2000_IN_MJD (T0 - MJD_BASIS)
374/** @endcond */
375
376static astro_ecliptic_t RotateEquatorialToEcliptic(const double pos[3], double obliq_radians, astro_time_t time);
377static int QuadInterp(
378 double tm, double dt, double fa, double fm, double fb,
379 double *t, double *df_dt);
380
381static double LongitudeOffset(double diff)
382{
383 double offset = diff;
384
385 while (offset <= -180.0)
386 offset += 360.0;
387
388 while (offset > 180.0)
389 offset -= 360.0;
390
391 return offset;
392}
393
394static double NormalizeLongitude(double lon)
395{
396 while (lon < 0.0)
397 lon += 360.0;
398
399 while (lon >= 360.0)
400 lon -= 360.0;
401
402 return lon;
403}
404
405/**
406 * @brief Calculates the length of the given vector.
407 *
408 * Calculates the non-negative length of the given vector.
409 * The length is expressed in the same units as the vector's components,
410 * usually astronomical units (AU).
411 *
412 * @param vector The vector whose length is to be calculated.
413 * @return The length of the vector.
414 */
415double Astronomy_VectorLength(astro_vector_t vector)
416{
417 return sqrt(vector.x*vector.x + vector.y*vector.y + vector.z*vector.z);
418}
419
420/**
421 * @brief Finds the name of a celestial body.
422 * @param body The celestial body whose name is to be found.
423 * @return The English-language name of the celestial body, or "" if the body is not valid.
424 */
425const char *Astronomy_BodyName(astro_body_t body)
426{
427 switch (body)
428 {
429 case BODY_MERCURY: return "Mercury";
430 case BODY_VENUS: return "Venus";
431 case BODY_EARTH: return "Earth";
432 case BODY_MARS: return "Mars";
433 case BODY_JUPITER: return "Jupiter";
434 case BODY_SATURN: return "Saturn";
435 case BODY_URANUS: return "Uranus";
436 case BODY_NEPTUNE: return "Neptune";
437 case BODY_PLUTO: return "Pluto";
438 case BODY_SUN: return "Sun";
439 case BODY_MOON: return "Moon";
440 case BODY_EMB: return "EMB";
441 case BODY_SSB: return "SSB";
442 default: return "";
443 }
444}
445
446/**
447 * @brief Returns the #astro_body_t value corresponding to the given English name.
448 * @param name One of the following strings: Sun, Moon, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, Pluto, EMB, SSB.
449 * @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`.
450 */
451astro_body_t Astronomy_BodyCode(const char *name)
452{
453 if (name != NULL)
454 {
455 if (!strcmp(name, "Mercury")) return BODY_MERCURY;
456 if (!strcmp(name, "Venus")) return BODY_VENUS;
457 if (!strcmp(name, "Earth")) return BODY_EARTH;
458 if (!strcmp(name, "Mars")) return BODY_MARS;
459 if (!strcmp(name, "Jupiter")) return BODY_JUPITER;
460 if (!strcmp(name, "Saturn")) return BODY_SATURN;
461 if (!strcmp(name, "Uranus")) return BODY_URANUS;
462 if (!strcmp(name, "Neptune")) return BODY_NEPTUNE;
463 if (!strcmp(name, "Pluto")) return BODY_PLUTO;
464 if (!strcmp(name, "Sun")) return BODY_SUN;
465 if (!strcmp(name, "Moon")) return BODY_MOON;
466 if (!strcmp(name, "EMB")) return BODY_EMB;
467 if (!strcmp(name, "SSB")) return BODY_SSB;
468 }
469 return BODY_INVALID;
470}
471
472/**
473 * @brief Returns 1 for planets that are farther from the Sun than the Earth is, 0 otherwise.
474 */
475static int IsSuperiorPlanet(astro_body_t body)
476{
477 switch (body)
478 {
479 case BODY_MARS:
480 case BODY_JUPITER:
481 case BODY_SATURN:
482 case BODY_URANUS:
483 case BODY_NEPTUNE:
484 case BODY_PLUTO:
485 return 1;
486
487 default:
488 return 0;
489 }
490}
491
492/**
493 * @brief Returns the average number of days it takes for a planet to orbit the Sun.
494 * @param body One of the planets: Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto.
495 * @return The mean orbital period of the body, or 0.0 if the `body` parameter is not valid.
496 */
497double Astronomy_PlanetOrbitalPeriod(astro_body_t body)
498{
499 switch (body)
500 {
501 case BODY_MERCURY: return 87.969;
502 case BODY_VENUS: return 224.701;
503 case BODY_EARTH: return EARTH_ORBITAL_PERIOD;
504 case BODY_MARS: return 686.980;
505 case BODY_JUPITER: return 4332.589;
506 case BODY_SATURN: return 10759.22;
507 case BODY_URANUS: return 30685.4;
508 case BODY_NEPTUNE: return NEPTUNE_ORBITAL_PERIOD;
509 case BODY_PLUTO: return 90560.0;
510 default: return 0.0; /* invalid body */
511 }
512}
513
514static astro_vector_t VecError(astro_status_t status, astro_time_t time)
515{
516 astro_vector_t vec;
517 vec.x = vec.y = vec.z = NAN;
518 vec.t = time;
519 vec.status = status;
520 return vec;
521}
522
523static astro_state_vector_t StateVecError(astro_status_t status, astro_time_t time)
524{
525 astro_state_vector_t vec;
526 vec.x = vec.y = vec.z = NAN;
527 vec.vx = vec.vy = vec.vz = NAN;
528 vec.t = time;
529 vec.status = status;
530 return vec;
531}
532
533static astro_spherical_t SphereError(astro_status_t status)
534{
535 astro_spherical_t sphere;
536 sphere.status = status;
537 sphere.dist = sphere.lat = sphere.lon = NAN;
538 return sphere;
539}
540
541static astro_time_t TimeError(void)
542{
543 astro_time_t time;
544 time.tt = time.ut = time.eps = time.psi = time.st = NAN;
545 return time;
546}
547
548static astro_equatorial_t EquError(astro_status_t status)
549{
550 astro_equatorial_t equ;
551 equ.vec = VecError(status, TimeError());
552 equ.ra = equ.dec = equ.dist = NAN;
553 equ.status = status;
554 return equ;
555}
556
557static astro_ecliptic_t EclError(astro_status_t status)
558{
559 astro_ecliptic_t ecl;
560 ecl.status = status;
561 ecl.elon = ecl.elat = NAN;
562 ecl.vec = VecError(status, TimeError());
563 return ecl;
564}
565
566static astro_angle_result_t AngleError(astro_status_t status)
567{
568 astro_angle_result_t result;
569 result.status = status;
570 result.angle = NAN;
571 return result;
572}
573
574static astro_func_result_t FuncError(astro_status_t status)
575{
576 astro_func_result_t result;
577 result.status = status;
578 result.value = NAN;
579 return result;
580}
581
582static astro_rotation_t RotationErr(astro_status_t status)
583{
584 astro_rotation_t rotation;
585 int i, j;
586
587 rotation.status = status;
588 for (i=0; i<3; ++i)
589 for (j=0; j<3; ++j)
590 rotation.rot[i][j] = NAN;
591
592 return rotation;
593}
594
595static astro_moon_quarter_t MoonQuarterError(astro_status_t status)
596{
597 astro_moon_quarter_t result;
598 result.status = status;
599 result.quarter = -1;
600 result.time = TimeError();
601 return result;
602}
603
604static astro_elongation_t ElongError(astro_status_t status)
605{
606 astro_elongation_t result;
607
608 result.status = status;
609 result.elongation = NAN;
610 result.ecliptic_separation = NAN;
611 result.time = TimeError();
612 result.visibility = (astro_visibility_t)(-1);
613
614 return result;
615}
616
617static astro_hour_angle_t HourAngleError(astro_status_t status)
618{
619 astro_hour_angle_t result;
620
621 result.status = status;
622 result.time = TimeError();
623 result.hor.altitude = result.hor.azimuth = result.hor.dec = result.hor.ra = NAN;
624
625 return result;
626}
627
628static astro_illum_t IllumError(astro_status_t status)
629{
630 astro_illum_t result;
631
632 result.status = status;
633 result.time = TimeError();
634 result.mag = NAN;
635 result.phase_angle = NAN;
636 result.phase_fraction = NAN;
637 result.helio_dist = NAN;
638 result.ring_tilt = NAN;
639
640 return result;
641}
642
643static astro_apsis_t ApsisError(astro_status_t status)
644{
645 astro_apsis_t result;
646
647 result.status = status;
648 result.time = TimeError();
649 result.kind = APSIS_INVALID;
650 result.dist_km = result.dist_au = NAN;
651
652 return result;
653}
654
655static astro_search_result_t SearchError(astro_status_t status)
656{
657 astro_search_result_t result;
658 result.time = TimeError();
659 result.status = status;
660 return result;
661}
662
663static astro_constellation_t ConstelErr(astro_status_t status)
664{
665 astro_constellation_t constel;
666 constel.status = status;
667 constel.symbol = constel.name = NULL;
668 constel.ra_1875 = constel.dec_1875 = NAN;
669 return constel;
670}
671
672static astro_transit_t TransitErr(astro_status_t status)
673{
674 astro_transit_t transit;
675 transit.status = status;
676 transit.start = transit.peak = transit.finish = TimeError();
677 transit.separation = NAN;
678 return transit;
679}
680
681static astro_axis_t AxisErr(astro_status_t status, astro_time_t time)
682{
683 astro_axis_t axis;
684 axis.status = status;
685 axis.ra = axis.dec = axis.spin = NAN;
686 axis.north = VecError(status, time);
687 return axis;
688}
689
690static astro_func_result_t SynodicPeriod(astro_body_t body)
691{
692 double Tp; /* planet's orbital period in days */
693 astro_func_result_t result;
694
695 /* The Earth does not have a synodic period as seen from itself. */
696 if (body == BODY_EARTH)
697 return FuncError(ASTRO_EARTH_NOT_ALLOWED);
698
699 if (body == BODY_MOON)
700 {
701 result.status = ASTRO_SUCCESS;
702 result.value = MEAN_SYNODIC_MONTH;
703 return result;
704 }
705
706 Tp = Astronomy_PlanetOrbitalPeriod(body);
707 if (Tp <= 0.0)
708 return FuncError(ASTRO_INVALID_BODY);
709
710 result.status = ASTRO_SUCCESS;
711 result.value = fabs(EARTH_ORBITAL_PERIOD / (EARTH_ORBITAL_PERIOD/Tp - 1.0));
712 return result;
713}
714
715
716/**
717 * @brief Calculates the angle between two vectors.
718 *
719 * Given a pair of vectors, this function returns the angle in degrees
720 * between the two vectors in 3D space.
721 * The angle is measured in the plane that contains both vectors.
722 *
723 * @param a
724 * The first vector.
725 *
726 * @param b
727 * The second vector.
728 *
729 * @returns
730 * On success, the `status` field holds `ASTRO_SUCCESS` and `angle` holds
731 * a number of degrees in the range [0, 180].
732 * If either vector has a zero magnitude or contains NAN (not a number)
733 * components, the `status` will hold the error code `ASTRO_BAD_VECTOR`.
734 */
735astro_angle_result_t Astronomy_AngleBetween(astro_vector_t a, astro_vector_t b)
736{
737 double r, dot;
738 astro_angle_result_t result;
739
740 r = Astronomy_VectorLength(a) * Astronomy_VectorLength(b);
741 if (r < 1.0e-8 || !isfinite(r))
742 return AngleError(ASTRO_BAD_VECTOR);
743
744 dot = (a.x*b.x + a.y*b.y + a.z*b.z) / r;
745
746 if (dot <= -1.0)
747 result.angle = 180.0;
748 else if (dot >= +1.0)
749 result.angle = 0.0;
750 else
751 result.angle = RAD2DEG * acos(dot);
752
753 result.status = ASTRO_SUCCESS;
754 return result;
755}
756
757/**
758 * @brief The default Delta T function used by Astronomy Engine.
759 *
760 * Espenak and Meeus use a series of piecewise polynomials to
761 * approximate DeltaT of the Earth in their "Five Millennium Canon of Solar Eclipses".
762 * See: https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html
763 * This is the default Delta T function used by Astronomy Engine.
764 *
765 * @param ut
766 * The floating point number of days since noon UTC on January 1, 2000.
767 *
768 * @returns
769 * The estimated difference TT-UT on the given date, expressed in seconds.
770 */
771double Astronomy_DeltaT_EspenakMeeus(double ut)
772{
773 double y, u, u2, u3, u4, u5, u6, u7;
774
775 /*
776 Fred Espenak writes about Delta-T generically here:
777 https://eclipse.gsfc.nasa.gov/SEhelp/deltaT.html
778 https://eclipse.gsfc.nasa.gov/SEhelp/deltat2004.html
779
780 He provides polynomial approximations for distant years here:
781 https://eclipse.gsfc.nasa.gov/SEhelp/deltatpoly2004.html
782
783 They start with a year value 'y' such that y=2000 corresponds
784 to the UTC Date 15-January-2000. Convert difference in days
785 to mean tropical years.
786 */
787
788 y = 2000 + ((ut - 14) / DAYS_PER_TROPICAL_YEAR);
789
790 if (y < -500)
791 {
792 u = (y - 1820) / 100;
793 return -20 + (32 * u*u);
794 }
795 if (y < 500)
796 {
797 u = y / 100;
798 u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3;
799 return 10583.6 - 1014.41*u + 33.78311*u2 - 5.952053*u3 - 0.1798452*u4 + 0.022174192*u5 + 0.0090316521*u6;
800 }
801 if (y < 1600)
802 {
803 u = (y - 1000) / 100;
804 u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3;
805 return 1574.2 - 556.01*u + 71.23472*u2 + 0.319781*u3 - 0.8503463*u4 - 0.005050998*u5 + 0.0083572073*u6;
806 }
807 if (y < 1700)
808 {
809 u = y - 1600;
810 u2 = u*u; u3 = u*u2;
811 return 120 - 0.9808*u - 0.01532*u2 + u3/7129.0;
812 }
813 if (y < 1800)
814 {
815 u = y - 1700;
816 u2 = u*u; u3 = u*u2; u4 = u2*u2;
817 return 8.83 + 0.1603*u - 0.0059285*u2 + 0.00013336*u3 - u4/1174000;
818 }
819 if (y < 1860)
820 {
821 u = y - 1800;
822 u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3; u6 = u3*u3; u7 = u3*u4;
823 return 13.72 - 0.332447*u + 0.0068612*u2 + 0.0041116*u3 - 0.00037436*u4 + 0.0000121272*u5 - 0.0000001699*u6 + 0.000000000875*u7;
824 }
825 if (y < 1900)
826 {
827 u = y - 1860;
828 u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3;
829 return 7.62 + 0.5737*u - 0.251754*u2 + 0.01680668*u3 - 0.0004473624*u4 + u5/233174;
830 }
831 if (y < 1920)
832 {
833 u = y - 1900;
834 u2 = u*u; u3 = u*u2; u4 = u2*u2;
835 return -2.79 + 1.494119*u - 0.0598939*u2 + 0.0061966*u3 - 0.000197*u4;
836 }
837 if (y < 1941)
838 {
839 u = y - 1920;
840 u2 = u*u; u3 = u*u2;
841 return 21.20 + 0.84493*u - 0.076100*u2 + 0.0020936*u3;
842 }
843 if (y < 1961)
844 {
845 u = y - 1950;
846 u2 = u*u; u3 = u*u2;
847 return 29.07 + 0.407*u - u2/233 + u3/2547;
848 }
849 if (y < 1986)
850 {
851 u = y - 1975;
852 u2 = u*u; u3 = u*u2;
853 return 45.45 + 1.067*u - u2/260 - u3/718;
854 }
855 if (y < 2005)
856 {
857 u = y - 2000;
858 u2 = u*u; u3 = u*u2; u4 = u2*u2; u5 = u2*u3;
859 return 63.86 + 0.3345*u - 0.060374*u2 + 0.0017275*u3 + 0.000651814*u4 + 0.00002373599*u5;
860 }
861 if (y < 2050)
862 {
863 u = y - 2000;
864 return 62.92 + 0.32217*u + 0.005589*u*u;
865 }
866 if (y < 2150)
867 {
868 u = (y-1820)/100;
869 return -20 + 32*u*u - 0.5628*(2150 - y);
870 }
871
872 /* all years after 2150 */
873 u = (y - 1820) / 100;
874 return -20 + (32 * u*u);
875}
876
877/**
878 * @brief A Delta T function that approximates the one used by the JPL Horizons tool.
879 *
880 * In order to support unit tests based on data generated by the JPL Horizons online
881 * tool, I had to reverse engineer their Delta T function by generating a table that
882 * contained it. The main difference between their tool and the Espenak/Meeus function
883 * is that they stop extrapolating the Earth's deceleration after the year 2017.
884 *
885 * @param ut
886 * The floating point number of days since noon UTC on January 1, 2000.
887 *
888 * @returns
889 * The estimated difference TT-UT on the given date, expressed in seconds.
890 */
891double Astronomy_DeltaT_JplHorizons(double ut)
892{
893 if (ut > 17.0 * DAYS_PER_TROPICAL_YEAR)
894 ut = 17.0 * DAYS_PER_TROPICAL_YEAR;
895
896 return Astronomy_DeltaT_EspenakMeeus(ut);
897}
898
899static astro_deltat_func DeltaTFunc = Astronomy_DeltaT_EspenakMeeus;
900
901/**
902 * @brief Changes the function Astronomy Engine uses to calculate Delta T.
903 *
904 * Most programs should not call this function. It is for advanced use cases only.
905 * By default, Astronomy Engine uses the function #Astronomy_DeltaT_EspenakMeeus
906 * to estimate changes in the Earth's rotation rate over time.
907 * However, for the sake of unit tests that compare calculations against
908 * external data sources that use alternative models for Delta T,
909 * it is sometimes useful to replace the Delta T model to match.
910 * This function allows replacing the Delta T model with any other
911 * desired model.
912 *
913 * @param func
914 * A pointer to a function to convert UT values to DeltaT values.
915 */
916void Astronomy_SetDeltaTFunction(astro_deltat_func func)
917{
918 DeltaTFunc = func;
919}
920
921static double TerrestrialTime(double ut)
922{
923 return ut + DeltaTFunc(ut)/86400.0;
924}
925
926/**
927 * @brief Converts a J2000 day value to an #astro_time_t value.
928 *
929 * This function can be useful for reproducing an #astro_time_t structure
930 * from its `ut` field only.
931 *
932 * @param ut
933 * The floating point number of days since noon UTC on January 1, 2000.
934 * This time is based on UTC/UT1 civil time.
935 * See #Astronomy_TerrestrialTime if you instead want to create
936 * a time value based on atomic Terrestrial Time (TT).
937 *
938 * @returns
939 * An #astro_time_t value for the given `ut` value.
940 */
941astro_time_t Astronomy_TimeFromDays(double ut)
942{
943 astro_time_t time;
944 time.ut = ut;
945 time.tt = TerrestrialTime(ut);
946 time.psi = time.eps = time.st = NAN;
947 return time;
948}
949
950
951/**
952 * @brief Converts a terrestrial time value into an #astro_time_t value.
953 *
954 * This function can be used in rare cases where a time must be based
955 * on Terrestrial Time (TT) rather than Universal Time (UT).
956 * Most developers will want to call #Astronomy_TimeFromDays instead of
957 * this function, because usually time is based on civil time adjusted
958 * by leap seconds to match the Earth's rotation, rather than the uniformly
959 * flowing TT used to calculate solar system dynamics. In rare cases
960 * where the caller already knows TT, this function is provided to create
961 * an #astro_time_t value that can be passed to Astronomy Engine functions.
962 *
963 * @param tt
964 * The floating point number of days of uniformly flowing
965 * Terrestrial Time since the J2000 epoch.
966 *
967 * @returns
968 * An #astro_time_t value for the given `tt` value.
969 */
970astro_time_t Astronomy_TerrestrialTime(double tt)
971{
972 /* Iterate to solve to find the correct ut for a given tt, and create an astro_time_t for that time. */
973 astro_time_t time = Astronomy_TimeFromDays(tt);
974 for(;;)
975 {
976 double err = tt - time.tt;
977 if (fabs(err) < 1.0e-12)
978 return time;
979 time = Astronomy_AddDays(time, err);
980 }
981}
982
983#if !defined(ASTRONOMY_ENGINE_NO_CURRENT_TIME)
984/**
985 * @brief Returns the computer's current date and time in the form of an #astro_time_t.
986 *
987 * Uses the computer's system clock to find the current UTC date and time.
988 * Converts that date and time to an #astro_time_t value and returns the result.
989 * Callers can pass this value to other Astronomy Engine functions to calculate
990 * current observational conditions.
991 *
992 * On supported platforms (Linux/Unix, Mac, Windows), the time is measured with
993 * microsecond resolution.
994 *
995 * On unsupported platforms, a compiler error will occur due to lack of
996 * microsecond resolution support. However, if whole second resolution is good
997 * enough for your application, you can define the preprocessor symbol
998 * `ASTRONOMY_ENGINE_WHOLE_SECOND` to use the portable function `time(NULL)`.
999 * Alternatively, if you do not need to use `Astronomy_CurrentTime`, you can
1000 * define the preprocessor symbol `ASTRONOMY_ENGINE_NO_CURRENT_TIME` to
1001 * exclude this function from your code.
1002 */
1003astro_time_t Astronomy_CurrentTime(void)
1004{
1005 astro_time_t t;
1006 double sec; /* Seconds since midnight January 1, 1970. */
1007
1008#if defined(__unix__) || defined(__unix) || (defined(__APPLE__) && defined(__MACH__))
1009 struct timeval tv;
1010 gettimeofday(&tv, NULL);
1011 sec = (double)tv.tv_sec + tv.tv_usec/1.0e+6;
1012#elif defined(_WIN32)
1013 FILETIME ft;
1014 ULARGE_INTEGER large;
1015 /* Get time in 100-nanosecond units from January 1, 1601. */
1016 GetSystemTimePreciseAsFileTime(&ft);
1017 large.u.LowPart = ft.dwLowDateTime;
1018 large.u.HighPart = ft.dwHighDateTime;
1019 sec = (large.QuadPart - 116444736000000000ULL) / 1.0e+7;
1020#elif defined(ASTRONOMY_ENGINE_WHOLE_SECOND)
1021 sec = time(NULL);
1022#else
1023 #error Microsecond time resolution is not supported on this platform. Define ASTRONOMY_ENGINE_WHOLE_SECOND to use second resolution instead.
1024#endif
1025
1026 /* Convert seconds to days, then subtract to get days since noon on January 1, 2000. */
1027 t.ut = (sec / SECONDS_PER_DAY) - 10957.5;
1028 t.tt = TerrestrialTime(t.ut);
1029 t.psi = t.eps = t.st = NAN;
1030 return t;
1031}
1032#endif
1033
1034/**
1035 * @brief Creates an #astro_time_t value from a given calendar date and time.
1036 *
1037 * Given a UTC calendar date and time, calculates an #astro_time_t value that can
1038 * be passed to other Astronomy Engine functions for performing various calculations
1039 * relating to that date and time.
1040 *
1041 * It is the caller's responsibility to ensure that the parameter values are correct.
1042 * The parameters are not checked for validity,
1043 * and this function never returns any indication of an error.
1044 * Invalid values, for example passing in February 31, may cause unexpected return values.
1045 *
1046 * @param year The UTC calendar year, e.g. 2019.
1047 * @param month The UTC calendar month in the range 1..12.
1048 * @param day The UTC calendar day in the range 1..31.
1049 * @param hour The UTC hour of the day in the range 0..23.
1050 * @param minute The UTC minute in the range 0..59.
1051 * @param second The UTC floating-point second in the range [0, 60).
1052 *
1053 * @return An #astro_time_t value that represents the given calendar date and time.
1054 */
1055astro_time_t Astronomy_MakeTime(int year, int month, int day, int hour, int minute, double second)
1056{
1057 astro_time_t time;
1058 int64_t y = (int64_t)year;
1059 int64_t m = (int64_t)month;
1060 int64_t d = (int64_t)day;
1061 int64_t f = (14 - m) / 12;
1062
1063 /*
1064 This formula is adapted from NOVAS C 3.1 function julian_date(),
1065 which in turn comes from Henry F. Fliegel & Thomas C. Van Flendern:
1066 Communications of the ACM, Vol 11, No 10, October 1968, p. 657.
1067 See: https://dl.acm.org/doi/pdf/10.1145/364096.364097
1068
1069 [Don Cross - 2023-02-25] I modified the formula so that it will
1070 work correctly with years as far back as -999999.
1071 */
1072 int64_t y2000 = (
1073 (d - 365972956)
1074 + (1461*(y + 1000000 - f))/4
1075 + (367*(m - 2 + 12*f))/12
1076 - (3*((y + 1000100 - f) / 100))/4
1077 );
1078
1079 time.ut = (y2000 - 0.5) + (hour / 24.0) + (minute / 1440.0) + (second / 86400.0);
1080 time.tt = TerrestrialTime(time.ut);
1081 time.psi = time.eps = time.st = NAN;
1082
1083 return time;
1084}
1085
1086/**
1087 * @brief Calculates the sum or difference of an #astro_time_t with a specified floating point number of days.
1088 *
1089 * Sometimes we need to adjust a given #astro_time_t value by a certain amount of time.
1090 * This function adds the given real number of days in `days` to the date and time in `time`.
1091 *
1092 * More precisely, the result's Universal Time field `ut` is exactly adjusted by `days` and
1093 * the Terrestrial Time field `tt` is adjusted correctly for the resulting UTC date and time,
1094 * according to the historical and predictive Delta-T model provided by the
1095 * [United States Naval Observatory](http://maia.usno.navy.mil/ser7/).
1096 *
1097 * The value stored in `time` will not be modified; it is passed by value.
1098 *
1099 * @param time A date and time for which to calculate an adjusted date and time.
1100 * @param days A floating point number of days by which to adjust `time`. May be negative, 0, or positive.
1101 * @return A date and time that is conceptually equal to `time + days`.
1102 */
1103astro_time_t Astronomy_AddDays(astro_time_t time, double days)
1104{
1105 /*
1106 This is slightly wrong, but the error is tiny.
1107 We really should be adding to TT, not to UT.
1108 But using TT would require creating an inverse function for DeltaT,
1109 which would be quite a bit of extra calculation.
1110 I estimate the error is in practice on the order of 10^(-7)
1111 times the value of 'days'.
1112 This is based on a typical drift of 1 second per year between UT and TT.
1113 */
1114
1115 astro_time_t sum;
1116
1117 sum.ut = time.ut + days;
1118 sum.tt = TerrestrialTime(sum.ut);
1119 sum.eps = sum.psi = sum.st = NAN;
1120
1121 return sum;
1122}
1123
1124/**
1125 * @brief Creates an #astro_time_t value from a given calendar date and time.
1126 *
1127 * This function is similar to #Astronomy_MakeTime, only it receives a
1128 * UTC calendar date and time in the form of an #astro_utc_t structure instead of
1129 * as separate numeric parameters. Astronomy_TimeFromUtc is the inverse of
1130 * #Astronomy_UtcFromTime.
1131 *
1132 * @param utc The UTC calendar date and time to be converted to #astro_time_t.
1133 * @return A value that can be used for astronomical calculations for the given date and time.
1134 */
1135astro_time_t Astronomy_TimeFromUtc(astro_utc_t utc)
1136{
1137 return Astronomy_MakeTime(utc.year, utc.month, utc.day, utc.hour, utc.minute, utc.second);
1138}
1139
1140/**
1141 * @brief Determines the calendar year, month, day, and time from an #astro_time_t value.
1142 *
1143 * After calculating the date and time of an astronomical event in the form of
1144 * an #astro_time_t value, it is often useful to display the result in a human-readable
1145 * form. This function converts the linear time scales in the `ut` field of #astro_time_t
1146 * into a calendar date and time: year, month, day, hours, minutes, and seconds, expressed
1147 * in UTC.
1148 *
1149 * @param time The astronomical time value to be converted to calendar date and time.
1150 * @return A date and time broken out into conventional year, month, day, hour, minute, and second.
1151 */
1152astro_utc_t Astronomy_UtcFromTime(astro_time_t time)
1153{
1154 /* Adapted from the NOVAS C 3.1 function cal_date() */
1155 astro_utc_t utc;
1156 int64_t jd, k, m, n;
1157 double djd, x;
1158 const int64_t c = 2500;
1159
1160 djd = time.ut + 2451545.5;
1161 jd = (int64_t)floor(djd);
1162
1163 x = 24.0 * fmod(djd, 1.0);
1164 if (x < 0.0)
1165 x += 24.0;
1166 utc.hour = (int)x;
1167 x = 60.0 * fmod(x, 1.0);
1168 utc.minute = (int)x;
1169 utc.second = 60.0 * fmod(x, 1.0);
1170
1171 /*
1172 This is my own adjustment to the NOVAS cal_date logic
1173 so that it can handle dates much farther back in the past.
1174 I add c*400 years worth of days at the front,
1175 then subtract c*400 years at the back,
1176 which avoids negative values in the formulas that mess up
1177 the calendar date calculations.
1178 Any multiple of 400 years has the same number of days,
1179 because it eliminates all the special cases for leap years.
1180 */
1181 k = jd + (68569 + c*146097);
1182 n = (4 * k) / 146097;
1183 k = k - (146097 * n + 3)/4;
1184 m = (4000 * (k+1)) / 1461001;
1185 k = k - (1461 * m)/4 + 31;
1186
1187 utc.month = (int) ((80 * k) / 2447);
1188 utc.day = (int) (k - (2447*utc.month)/80);
1189 k = utc.month / 11;
1190
1191 utc.month = (int) (utc.month + 2 - 12*k);
1192 utc.year = (int) (100 * (n - 49) + m + k - 400*c);
1193
1194 return utc;
1195}
1196
1197
1198/**
1199 * @brief Formats an #astro_time_t value as an ISO 8601 string.
1200 *
1201 * Given an #astro_time_t value `time`, formats it as an ISO 8601
1202 * string to the resolution specified by the `format` parameter.
1203 * The result is stored in the `text` buffer whose capacity in bytes
1204 * is specified by `size`.
1205 *
1206 * @param time
1207 * The date and time whose civil time `time.ut` is to be formatted as an ISO 8601 string.
1208 * If the civil time is outside the year range -999999 to +999999, the function fails
1209 * and returns `ASTRO_BAD_TIME`. Years prior to 1583 are treated as if they are
1210 * using the modern Gregorian calendar, even when the Julian calendar was actually in effect.
1211 * The year before 1 AD, commonly known as 1 BC, is represented by the value 0.
1212 * The year 2 BC is represented by -1, etc.
1213 *
1214 * @param format
1215 * Specifies the resolution to which the date and time should be formatted,
1216 * as explained at #astro_time_format_t.
1217 * If the value of `format` is not recognized, the function fails and
1218 * returns `ASTRO_INVALID_PARAMETER`.
1219 *
1220 * @param text
1221 * A pointer to a text buffer to receive the output.
1222 * If `text` is `NULL`, this function returns `ASTRO_INVALID_PARAMETER`.
1223 * If the function fails for any reason, and `text` is not `NULL`,
1224 * and `size` is greater than 0, the `text` buffer is set to an empty string.
1225 *
1226 * @param size
1227 * The size in bytes of the buffer pointed to by `text`. The buffer must
1228 * be large enough to accomodate the output format selected by the
1229 * `format` parameter, as specified at #astro_time_format_t.
1230 * If `size` is too small to hold the string as specified by `format`,
1231 * the `text` buffer is set to `""` (if possible)
1232 * and the function returns `ASTRO_BUFFER_TOO_SMALL`.
1233 * A buffer that is `TIME_TEXT_BYTES` (28) bytes or larger is always large enough for this function.
1234 *
1235 * @return `ASTRO_SUCCESS` on success; otherwise an error as described in the parameter notes.
1236 */
1237astro_status_t Astronomy_FormatTime(
1238 astro_time_t time,
1239 astro_time_format_t format,
1240 char *text,
1241 size_t size)
1242{
1243 int nprinted;
1244 double rounding;
1245 size_t min_size;
1246 astro_utc_t utc;
1247 char ytext[20]; /* worst case: "+999999" = 8 characters including terminal '\0'. But gcc 12.2 still complains! */
1248
1249 if (text == NULL)
1250 return ASTRO_INVALID_PARAMETER;
1251
1252 if (size == 0)
1253 return ASTRO_BUFFER_TOO_SMALL;
1254
1255 text[0] = '\0'; /* initialize to empty string, in case an error occurs */
1256
1257 /* Validate 'size' parameter and perform date/time rounding. */
1258 switch (format)
1259 {
1260 case TIME_FORMAT_DAY:
1261 min_size = 11; /* "2020-12-31" */
1262 rounding = 0.0; /* no rounding */
1263 break;
1264
1265 case TIME_FORMAT_MINUTE:
1266 min_size = 18; /* "2020-12-31T15:47Z" */
1267 rounding = 0.5 / (24.0 * 60.0); /* round to nearest minute */
1268 break;
1269
1270 case TIME_FORMAT_SECOND:
1271 min_size = 21; /* "2020-12-31T15:47:59Z" */
1272 rounding = 0.5 / (24.0 * 3600.0); /* round to nearest second */
1273 break;
1274
1275 case TIME_FORMAT_MILLI:
1276 min_size = 25; /* "2020-12-31T15:47:59.123Z" */
1277 rounding = 0.5 / (24.0 * 3600000.0); /* round to nearest millisecond */
1278 break;
1279
1280 default:
1281 return ASTRO_INVALID_PARAMETER;
1282 }
1283
1284 /* Perform rounding. */
1285 time.ut += rounding;
1286
1287 /* Convert linear J2000 days to Gregorian UTC date/time. */
1288 utc = Astronomy_UtcFromTime(time);
1289 if (utc.year < -999999 || utc.year > +999999)
1290 return ASTRO_BAD_TIME;
1291
1292 if (utc.year < 0)
1293 {
1294 snprintf(ytext, sizeof(ytext), "-%06d", -utc.year);
1295 min_size += 3; /* '-' prefix and two extra year digits. */
1296 }
1297 else if (utc.year <= 9999)
1298 {
1299 snprintf(ytext, sizeof(ytext), "%04d", utc.year);
1300 }
1301 else
1302 {
1303 snprintf(ytext, sizeof(ytext), "+%06d", utc.year);
1304 min_size += 3; /* '+' prefix and two extra year digits. */
1305 }
1306
1307 /* Check for insufficient buffer size. */
1308 if (size < min_size)
1309 return ASTRO_BUFFER_TOO_SMALL;
1310
1311 /* Format the string. */
1312 switch (format)
1313 {
1314 case TIME_FORMAT_DAY:
1315 nprinted = snprintf(text, size, "%s-%02d-%02d",
1316 ytext, utc.month, utc.day);
1317 break;
1318
1319 case TIME_FORMAT_MINUTE:
1320 nprinted = snprintf(text, size, "%s-%02d-%02dT%02d:%02dZ",
1321 ytext, utc.month, utc.day,
1322 utc.hour, utc.minute);
1323 break;
1324
1325 case TIME_FORMAT_SECOND:
1326 nprinted = snprintf(text, size, "%s-%02d-%02dT%02d:%02d:%02.0lfZ",
1327 ytext, utc.month, utc.day,
1328 utc.hour, utc.minute, floor(utc.second));
1329 break;
1330
1331 case TIME_FORMAT_MILLI:
1332 nprinted = snprintf(text, size, "%s-%02d-%02dT%02d:%02d:%06.3lfZ",
1333 ytext, utc.month, utc.day,
1334 utc.hour, utc.minute, floor(1000.0 * utc.second) / 1000.0);
1335 break;
1336
1337 default:
1338 /* We should have already failed for any unknown 'format' value. */
1339 return ASTRO_INTERNAL_ERROR;
1340 }
1341
1342 if (nprinted < 0)
1343 return ASTRO_INTERNAL_ERROR; /* should not be possible for snprintf to return a negative number */
1344
1345 if ((size_t)(1+nprinted) != min_size)
1346 return ASTRO_INTERNAL_ERROR; /* there must be a bug calculating min_size or formatting the string */
1347
1348 return ASTRO_SUCCESS;
1349}
1350
1351
1352/**
1353 * @brief Creates an observer object that represents a location on or near the surface of the Earth.
1354 *
1355 * Some Astronomy Engine functions calculate values pertaining to an observer on the Earth.
1356 * These functions require a value of type #astro_observer_t that represents the location
1357 * of such an observer.
1358 *
1359 * @param latitude The geographic latitude of the observer in degrees north (positive) or south (negative) of the equator.
1360 * @param longitude The geographic longitude of the observer in degrees east (positive) or west (negative) of the prime meridian at Greenwich, England.
1361 * @param height The height of the observer in meters above mean sea level.
1362 * @return An observer object that can be passed to astronomy functions that require a geographic location.
1363 */
1364astro_observer_t Astronomy_MakeObserver(double latitude, double longitude, double height)
1365{
1366 astro_observer_t observer;
1367
1368 observer.latitude = latitude;
1369 observer.longitude = longitude;
1370 observer.height = height;
1371
1372 return observer;
1373}
1374
1375static void iau2000b(astro_time_t *time)
1376{
1377 /* Truncated and hand-optimized nutation model. */
1378
1379 if ((time != NULL) && isnan(time->psi))
1380 {
1381 double t, elp, f, d, om, arg, dp, de, sarg, carg;
1382
1383 t = time->tt / 36525.0;
1384 elp = fmod(1287104.79305 + t * 129596581.0481, ASEC360) * ASEC2RAD;
1385 f = fmod(335779.526232 + t * 1739527262.8478, ASEC360) * ASEC2RAD;
1386 d = fmod(1072260.70369 + t * 1602961601.2090, ASEC360) * ASEC2RAD;
1387 om = fmod(450160.398036 - t * 6962890.5431, ASEC360) * ASEC2RAD;
1388
1389 sarg = sin(om);
1390 carg = cos(om);
1391 dp = (-172064161.0 - 174666.0*t)*sarg + 33386.0*carg;
1392 de = (92052331.0 + 9086.0*t)*carg + 15377.0*sarg;
1393
1394 arg = 2.0*(f - d + om);
1395 sarg = sin(arg);
1396 carg = cos(arg);
1397 dp += (-13170906.0 - 1675.0*t)*sarg - 13696.0*carg;
1398 de += (5730336.0 - 3015.0*t)*carg - 4587.0*sarg;
1399
1400 arg = 2.0*(f + om);
1401 sarg = sin(arg);
1402 carg = cos(arg);
1403 dp += (-2276413.0 - 234.0*t)*sarg + 2796.0*carg;
1404 de += (978459.0 - 485.0*t)*carg + 1374.0*sarg;
1405
1406 arg = 2.0*om;
1407 sarg = sin(arg);
1408 carg = cos(arg);
1409 dp += (2074554.0 + 207.0*t)*sarg - 698.0*carg;
1410 de += (-897492.0 + 470.0*t)*carg - 291.0*sarg;
1411
1412 sarg = sin(elp);
1413 carg = cos(elp);
1414 dp += (1475877.0 - 3633.0*t)*sarg + 11817.0*carg;
1415 de += (73871.0 - 184.0*t)*carg - 1924.0*sarg;
1416
1417 time->psi = -0.000135 + (dp * 1.0e-7);
1418 time->eps = +0.000388 + (de * 1.0e-7);
1419 }
1420}
1421
1422static double mean_obliq(double tt)
1423{
1424 double t = tt / 36525.0;
1425 double asec =
1426 (((( - 0.0000000434 * t
1427 - 0.000000576 ) * t
1428 + 0.00200340 ) * t
1429 - 0.0001831 ) * t
1430 - 46.836769 ) * t + 84381.406;
1431
1432 return asec / 3600.0;
1433}
1434
1435/** @cond DOXYGEN_SKIP */
1436typedef struct
1437{
1438 double tt;
1439 double dpsi;
1440 double deps;
1441 double ee;
1442 double mobl;
1443 double tobl;
1444}
1445earth_tilt_t;
1446/** @endcond */
1447
1448static earth_tilt_t e_tilt(astro_time_t *time)
1449{
1450 earth_tilt_t et;
1451
1452 /* There is no good answer for what to do if time==NULL. Callers must prevent! */
1453
1454 iau2000b(time);
1455 et.dpsi = time->psi;
1456 et.deps = time->eps;
1457 et.mobl = mean_obliq(time->tt);
1458 et.tobl = et.mobl + (et.deps / 3600.0);
1459 et.tt = time->tt;
1460 et.ee = et.dpsi * cos(et.mobl * DEG2RAD) / 15.0;
1461
1462 return et;
1463}
1464
1465static void obl_ecl2equ_vec(double obl, astro_time_t time, const double ecl[3], double equ[3])
1466{
1467 double obl_rad = obl * DEG2RAD;
1468 double cos_obl = cos(obl_rad);
1469 double sin_obl = sin(obl_rad);
1470
1471 equ[0] = ecl[0];
1472 equ[1] = ecl[1]*cos_obl - ecl[2]*sin_obl;
1473 equ[2] = ecl[1]*sin_obl + ecl[2]*cos_obl;
1474}
1475
1476static void ecl2equ_vec(astro_time_t time, const double ecl[3], double equ[3])
1477{
1478 double obl = mean_obliq(time.tt);
1479 obl_ecl2equ_vec(obl, time, ecl, equ);
1480}
1481
1482static astro_rotation_t precession_rot(astro_time_t time, precess_dir_t dir)
1483{
1484 /*
1485 dir==INTO_2000: converts mean equator of date (EQM) to J2000 mean equator (EQJ).
1486 dir==FROM_2000: converts J2000 mean equator (EQJ) to mean equator of date (EQM).
1487 */
1488
1489 astro_rotation_t rotation;
1490 double xx, yx, zx, xy, yy, zy, xz, yz, zz;
1491 double t, psia, omegaa, chia, sa, ca, sb, cb, sc, cc, sd, cd;
1492 double eps0 = 84381.406;
1493
1494 t = time.tt / 36525;
1495
1496 psia = (((((- 0.0000000951 * t
1497 + 0.000132851 ) * t
1498 - 0.00114045 ) * t
1499 - 1.0790069 ) * t
1500 + 5038.481507 ) * t);
1501
1502 omegaa = (((((+ 0.0000003337 * t
1503 - 0.000000467 ) * t
1504 - 0.00772503 ) * t
1505 + 0.0512623 ) * t
1506 - 0.025754 ) * t + eps0);
1507
1508 chia = (((((- 0.0000000560 * t
1509 + 0.000170663 ) * t
1510 - 0.00121197 ) * t
1511 - 2.3814292 ) * t
1512 + 10.556403 ) * t);
1513
1514 eps0 = eps0 * ASEC2RAD;
1515 psia = psia * ASEC2RAD;
1516 omegaa = omegaa * ASEC2RAD;
1517 chia = chia * ASEC2RAD;
1518
1519 sa = sin(eps0);
1520 ca = cos(eps0);
1521 sb = sin(-psia);
1522 cb = cos(-psia);
1523 sc = sin(-omegaa);
1524 cc = cos(-omegaa);
1525 sd = sin(chia);
1526 cd = cos(chia);
1527
1528 xx = cd * cb - sb * sd * cc;
1529 yx = cd * sb * ca + sd * cc * cb * ca - sa * sd * sc;
1530 zx = cd * sb * sa + sd * cc * cb * sa + ca * sd * sc;
1531 xy = -sd * cb - sb * cd * cc;
1532 yy = -sd * sb * ca + cd * cc * cb * ca - sa * cd * sc;
1533 zy = -sd * sb * sa + cd * cc * cb * sa + ca * cd * sc;
1534 xz = sb * sc;
1535 yz = -sc * cb * ca - sa * cc;
1536 zz = -sc * cb * sa + cc * ca;
1537
1538 if (dir == INTO_2000)
1539 {
1540 /* Perform rotation from other epoch to J2000.0. */
1541 rotation.rot[0][0] = xx;
1542 rotation.rot[0][1] = yx;
1543 rotation.rot[0][2] = zx;
1544 rotation.rot[1][0] = xy;
1545 rotation.rot[1][1] = yy;
1546 rotation.rot[1][2] = zy;
1547 rotation.rot[2][0] = xz;
1548 rotation.rot[2][1] = yz;
1549 rotation.rot[2][2] = zz;
1550 }
1551 else
1552 {
1553 /* Perform rotation from J2000.0 to other epoch. */
1554 rotation.rot[0][0] = xx;
1555 rotation.rot[0][1] = xy;
1556 rotation.rot[0][2] = xz;
1557 rotation.rot[1][0] = yx;
1558 rotation.rot[1][1] = yy;
1559 rotation.rot[1][2] = yz;
1560 rotation.rot[2][0] = zx;
1561 rotation.rot[2][1] = zy;
1562 rotation.rot[2][2] = zz;
1563 }
1564
1565 rotation.status = ASTRO_SUCCESS;
1566 return rotation;
1567}
1568
1569
1570static void rotate(const double invec[3], const double rot[3][3], double outvec[3])
1571{
1572 outvec[0] = rot[0][0]*invec[0] + rot[1][0]*invec[1] + rot[2][0]*invec[2];
1573 outvec[1] = rot[0][1]*invec[0] + rot[1][1]*invec[1] + rot[2][1]*invec[2];
1574 outvec[2] = rot[0][2]*invec[0] + rot[1][2]*invec[1] + rot[2][2]*invec[2];
1575}
1576
1577static void precession(
1578 const double pos1[3],
1579 astro_time_t time,
1580 precess_dir_t dir,
1581 double pos2[3])
1582{
1583 astro_rotation_t r = precession_rot(time, dir);
1584 rotate(pos1, r.rot, pos2);
1585}
1586
1587static void precession_posvel(
1588 const double pos1[3],
1589 const double vel1[3],
1590 astro_time_t time,
1591 precess_dir_t dir,
1592 double pos2[3],
1593 double vel2[3])
1594{
1595 astro_rotation_t r = precession_rot(time, dir);
1596 rotate(pos1, r.rot, pos2);
1597 rotate(vel1, r.rot, vel2);
1598}
1599
1600
1601static astro_equatorial_t vector2radec(const double pos[3], astro_time_t time)
1602{
1603 astro_equatorial_t equ;
1604 double xyproj;
1605
1606 /* Copy the cartesian coordinates from the input into the returned structure. */
1607 equ.vec.status = ASTRO_SUCCESS;
1608 equ.vec.t = time;
1609 equ.vec.x = pos[0];
1610 equ.vec.y = pos[1];
1611 equ.vec.z = pos[2];
1612
1613 /* Calculate spherical coordinates: RA, DEC, distance. */
1614 xyproj = pos[0]*pos[0] + pos[1]*pos[1];
1615 equ.dist = sqrt(xyproj + pos[2]*pos[2]);
1616 equ.status = ASTRO_SUCCESS;
1617 if (xyproj == 0.0)
1618 {
1619 if (pos[2] == 0.0)
1620 {
1621 /* Indeterminate coordinates; pos vector has zero length. */
1622 equ = EquError(ASTRO_BAD_VECTOR);
1623 }
1624 else if (pos[2] < 0)
1625 {
1626 equ.ra = 0.0;
1627 equ.dec = -90.0;
1628 }
1629 else
1630 {
1631 equ.ra = 0.0;
1632 equ.dec = +90.0;
1633 }
1634 }
1635 else
1636 {
1637 equ.ra = RAD2HOUR * atan2(pos[1], pos[0]);
1638 if (equ.ra < 0)
1639 equ.ra += 24.0;
1640
1641 equ.dec = RAD2DEG * atan2(pos[2], sqrt(xyproj));
1642 }
1643
1644 return equ;
1645}
1646
1647
1648static astro_rotation_t nutation_rot(astro_time_t *time, precess_dir_t dir)
1649{
1650 /*
1651 Creates a rotation matrix that adds/removes nutation from
1652 an equatorial vector of date:
1653
1654 The `dir` parameter is a little misleading, but reflects the
1655 common task of working together with precession_rot() to
1656 convert mean equator of 2000 to/from true equator of date.
1657 Here is the actual result of `dir`:
1658
1659 dir==INTO_2000: Subtract nutation from true equator of date (EQD)
1660 to produce mean equator of date (EQM).
1661
1662 dir==FROM_2000: Add nutation from mean equator of date (EQM) to
1663 produce true equator of date (EQD).
1664 */
1665
1666 astro_rotation_t rotation;
1667 earth_tilt_t tilt;
1668 double oblm, oblt, psi, cobm, sobm, cobt, sobt, cpsi, spsi;
1669 double xx, yx, zx, xy, yy, zy, xz, yz, zz;
1670
1671 if (time == NULL)
1672 return RotationErr(ASTRO_INVALID_PARAMETER);
1673
1674 tilt = e_tilt(time);
1675 oblm = tilt.mobl * DEG2RAD;
1676 oblt = tilt.tobl * DEG2RAD;
1677 psi = tilt.dpsi * ASEC2RAD;
1678 cobm = cos(oblm);
1679 sobm = sin(oblm);
1680 cobt = cos(oblt);
1681 sobt = sin(oblt);
1682 cpsi = cos(psi);
1683 spsi = sin(psi);
1684
1685 xx = cpsi;
1686 yx = -spsi * cobm;
1687 zx = -spsi * sobm;
1688 xy = spsi * cobt;
1689 yy = cpsi * cobm * cobt + sobm * sobt;
1690 zy = cpsi * sobm * cobt - cobm * sobt;
1691 xz = spsi * sobt;
1692 yz = cpsi * cobm * sobt - sobm * cobt;
1693 zz = cpsi * sobm * sobt + cobm * cobt;
1694
1695 if (dir == FROM_2000)
1696 {
1697 /* convert J2000 to of-date */
1698 rotation.rot[0][0] = xx;
1699 rotation.rot[0][1] = xy;
1700 rotation.rot[0][2] = xz;
1701 rotation.rot[1][0] = yx;
1702 rotation.rot[1][1] = yy;
1703 rotation.rot[1][2] = yz;
1704 rotation.rot[2][0] = zx;
1705 rotation.rot[2][1] = zy;
1706 rotation.rot[2][2] = zz;
1707 }
1708 else
1709 {
1710 /* convert of-date to J2000 */
1711 rotation.rot[0][0] = xx;
1712 rotation.rot[0][1] = yx;
1713 rotation.rot[0][2] = zx;
1714 rotation.rot[1][0] = xy;
1715 rotation.rot[1][1] = yy;
1716 rotation.rot[1][2] = zy;
1717 rotation.rot[2][0] = xz;
1718 rotation.rot[2][1] = yz;
1719 rotation.rot[2][2] = zz;
1720 }
1721
1722 rotation.status = ASTRO_SUCCESS;
1723 return rotation;
1724}
1725
1726static void nutation(
1727 const double inpos[3],
1728 astro_time_t *time,
1729 precess_dir_t dir,
1730 double outpos[3])
1731{
1732 astro_rotation_t r = nutation_rot(time, dir);
1733 rotate(inpos, r.rot, outpos);
1734}
1735
1736static void nutation_posvel(
1737 const double inpos[3],
1738 const double invel[3],
1739 astro_time_t *time,
1740 precess_dir_t dir,
1741 double outpos[3],
1742 double outvel[3])
1743{
1744 astro_rotation_t r = nutation_rot(time, dir);
1745 rotate(inpos, r.rot, outpos);
1746 rotate(invel, r.rot, outvel);
1747}
1748
1749static double era(double ut) /* Earth Rotation Angle */
1750{
1751 double thet1 = 0.7790572732640 + 0.00273781191135448 * ut;
1752 double thet3 = fmod(ut, 1.0);
1753 double theta = 360.0 * fmod(thet1 + thet3, 1.0);
1754 if (theta < 0.0)
1755 theta += 360.0;
1756
1757 return theta;
1758}
1759
1760/**
1761 * @brief Calculates Greenwich Apparent Sidereal Time (GAST).
1762 *
1763 * Given a date and time, this function calculates the rotation of the
1764 * Earth, represented by the equatorial angle of the Greenwich prime meridian
1765 * with respect to distant stars (not the Sun, which moves relative to background
1766 * stars by almost one degree per day).
1767 * This angle is called Greenwich Apparent Sidereal Time (GAST).
1768 * GAST is measured in sidereal hours in the half-open range [0, 24).
1769 * When GAST = 0, it means the prime meridian is aligned with the of-date equinox,
1770 * corrected at that time for precession and nutation of the Earth's axis.
1771 * In this context, the "equinox" is the direction in space where the Earth's
1772 * orbital plane (the ecliptic) intersects with the plane of the Earth's equator,
1773 * at the location on the Earth's orbit of the (seasonal) March equinox.
1774 * As the Earth rotates, GAST increases from 0 up to 24 sidereal hours,
1775 * then starts over at 0.
1776 * To convert to degrees, multiply the return value by 15.
1777 *
1778 * @param time
1779 * The date and time for which to find GAST.
1780 * The parameter is passed by address because it can be modified by the call:
1781 * As an optimization, this function caches the sidereal time value in `time`,
1782 * unless it has already been cached, in which case the cached value is reused.
1783 * If the `time` pointer is NULL, this function returns a NAN value.
1784 *
1785 * @returns {number}
1786 */
1787double Astronomy_SiderealTime(astro_time_t *time)
1788{
1789 if (time == NULL)
1790 return NAN;
1791
1792 if (isnan(time->st))
1793 {
1794 double t = time->tt / 36525.0;
1795 double eqeq = 15.0 * e_tilt(time).ee; /* Replace with eqeq=0 to get GMST instead of GAST (if we ever need it) */
1796 double theta = era(time->ut);
1797 double st = (eqeq + 0.014506 +
1798 (((( - 0.0000000368 * t
1799 - 0.000029956 ) * t
1800 - 0.00000044 ) * t
1801 + 1.3915817 ) * t
1802 + 4612.156534 ) * t);
1803
1804 double gst = fmod(st/3600.0 + theta, 360.0) / 15.0;
1805 if (gst < 0.0)
1806 gst += 24.0;
1807
1808 time->st = gst;
1809 }
1810
1811 return time->st; /* return sidereal hours in the half-open range [0, 24). */
1812}
1813
1814static astro_observer_t inverse_terra(const double ovec[3], double st)
1815{
1816 double x, y, z, p, F, W, D, c, s, c2, s2;
1817 double lon_deg, lat_deg, lat, radicand, factor, denom, adjust;
1818 double height_km, stlocl;
1819 astro_observer_t observer;
1820 int count;
1821
1822 /* Convert from AU to kilometers. */
1823 x = ovec[0] * KM_PER_AU;
1824 y = ovec[1] * KM_PER_AU;
1825 z = ovec[2] * KM_PER_AU;
1826 p = hypot(x, y);
1827 if (p < 1.0e-6)
1828 {
1829 /* Special case: within 1 millimeter of a pole! */
1830 /* Use arbitrary longitude, and latitude determined by polarity of z. */
1831 lon_deg = 0.0;
1832 lat_deg = (z > 0.0) ? +90.0 : -90.0;
1833 /* Elevation is calculated directly from z */
1834 height_km = fabs(z) - EARTH_POLAR_RADIUS_KM;
1835 }
1836 else
1837 {
1838 stlocl = atan2(y, x);
1839 /* Calculate exact longitude. */
1840 lon_deg = RAD2DEG*stlocl - (15.0 * st);
1841 /* Normalize longitude to the range (-180, +180]. */
1842 while (lon_deg <= -180.0)
1843 lon_deg += 360.0;
1844 while (lon_deg > +180.0)
1845 lon_deg -= 360.0;
1846 /* Numerically solve for exact latitude, using Newton's Method. */
1847 F = EARTH_FLATTENING * EARTH_FLATTENING;
1848 /* Start with initial latitude estimate, based on a spherical Earth. */
1849 lat = atan2(z, p);
1850 for (count = 0; ; ++count)
1851 {
1852 if (count > 10)
1853 {
1854 fprintf(stderr, "\nFATAL(inverse_terra): did not converge!\n");
1855 exit(1);
1856 }
1857
1858 /* Calculate the error function W(lat). */
1859 /* We try to find the root of W, meaning where the error is 0. */
1860 c = cos(lat);
1861 s = sin(lat);
1862 factor = (F-1)*EARTH_EQUATORIAL_RADIUS_KM;
1863 c2 = c*c;
1864 s2 = s*s;
1865 radicand = c2 + F*s2;
1866 denom = sqrt(radicand);
1867 W = (factor*s*c)/denom - z*c + p*s;
1868 if (fabs(W) < 1.0e-8)
1869 break; /* The error is now negligible. */
1870 /* Error is still too large. Find the next estimate. */
1871 /* Calculate D = the derivative of W with respect to lat. */
1872 D = factor*((c2 - s2)/denom - s2*c2*(F-1)/(factor*radicand)) + z*s + p*c;
1873 lat -= W/D;
1874 }
1875 /* We now have a solution for the latitude in radians. */
1876 lat_deg = lat * RAD2DEG;
1877 /* Solve for exact height in meters. */
1878 /* There are two formulas I can use. Use whichever has the less risky denominator. */
1879 adjust = EARTH_EQUATORIAL_RADIUS_KM / denom;
1880 if (fabs(s) > fabs(c))
1881 height_km = z/s - F*adjust;
1882 else
1883 height_km = p/c - adjust;
1884 }
1885
1886 observer.latitude = lat_deg;
1887 observer.longitude = lon_deg;
1888 observer.height = 1000.0 * height_km;
1889 return observer;
1890}
1891
1892static void terra(astro_observer_t observer, double st, double pos[3], double vel[3])
1893{
1894 static const double ANGVEL = 7.2921150e-5;
1895
1896 double phi = observer.latitude * DEG2RAD;
1897 double sinphi = sin(phi);
1898 double cosphi = cos(phi);
1899 double c = 1.0 / hypot(cosphi, sinphi*EARTH_FLATTENING);
1900 double s = c * (EARTH_FLATTENING * EARTH_FLATTENING);
1901 double ht_km = observer.height / 1000.0;
1902 double ach = EARTH_EQUATORIAL_RADIUS_KM*c + ht_km;
1903 double ash = EARTH_EQUATORIAL_RADIUS_KM*s + ht_km;
1904 double stlocl = (15.0*st + observer.longitude) * DEG2RAD;
1905 double sinst = sin(stlocl);
1906 double cosst = cos(stlocl);
1907
1908 if (pos != NULL)
1909 {
1910 pos[0] = ach * cosphi * cosst / KM_PER_AU;
1911 pos[1] = ach * cosphi * sinst / KM_PER_AU;
1912 pos[2] = ash * sinphi / KM_PER_AU;
1913 }
1914
1915 if (vel != NULL)
1916 {
1917 vel[0] = -(ANGVEL * 86400.0 / KM_PER_AU) * ach * cosphi * sinst;
1918 vel[1] = +(ANGVEL * 86400.0 / KM_PER_AU) * ach * cosphi * cosst;
1919 vel[2] = 0.0;
1920 }
1921}
1922
1923static void geo_pos(astro_time_t *time, astro_observer_t observer, double pos[3])
1924{
1925 double gast;
1926 double pos1[3], pos2[3];
1927
1928 if (time == NULL)
1929 {
1930 pos[0] = pos[1] = pos[2] = NAN;
1931 }
1932 else
1933 {
1934 gast = Astronomy_SiderealTime(time);
1935 terra(observer, gast, pos1, NULL);
1936 nutation(pos1, time, INTO_2000, pos2);
1937 precession(pos2, *time, INTO_2000, pos);
1938 }
1939}
1940
1941static void spin(double angle, const double pos1[3], double vec2[3])
1942{
1943 double angr = angle * DEG2RAD;
1944 double cosang = cos(angr);
1945 double sinang = sin(angr);
1946 vec2[0] = +cosang*pos1[0] + sinang*pos1[1];
1947 vec2[1] = -sinang*pos1[0] + cosang*pos1[1];
1948 vec2[2] = pos1[2];
1949}
1950
1951/*------------------ CalcMoon ------------------*/
1952
1953/** @cond DOXYGEN_SKIP */
1954
1955#define DECLARE_PASCAL_ARRAY_1(elemtype,name,xmin,xmax) \
1956 elemtype name[(xmax)-(xmin)+1]
1957
1958#define DECLARE_PASCAL_ARRAY_2(elemtype,name,xmin,xmax,ymin,ymax) \
1959 elemtype name[(xmax)-(xmin)+1][(ymax)-(ymin)+1]
1960
1961#define ACCESS_PASCAL_ARRAY_1(name,xmin,x) \
1962 ((name)[(x)-(xmin)])
1963
1964#define ACCESS_PASCAL_ARRAY_2(name,xmin,ymin,x,y) \
1965 ((name)[(x)-(xmin)][(y)-(ymin)])
1966
1967typedef struct
1968{
1969 double t;
1970 double dgam;
1971 double dlam, n, gam1c, sinpi;
1972 double l0, l, ls, f, d, s;
1973 double dl0, dl, dls, df, dd, ds;
1974 DECLARE_PASCAL_ARRAY_2(double,co,-6,6,1,4); /* ARRAY[-6..6,1..4] OF REAL */
1975 DECLARE_PASCAL_ARRAY_2(double,si,-6,6,1,4); /* ARRAY[-6..6,1..4] OF REAL */
1976}
1977MoonContext;
1978
1979#define T (ctx->t)
1980#define DGAM (ctx->dgam)
1981#define DLAM (ctx->dlam)
1982#define N (ctx->n)
1983#define GAM1C (ctx->gam1c)
1984#define SINPI (ctx->sinpi)
1985#define L0 (ctx->l0)
1986#define L (ctx->l)
1987#define LS (ctx->ls)
1988#define F (ctx->f)
1989#define D (ctx->d)
1990#define S (ctx->s)
1991#define DL0 (ctx->dl0)
1992#define DL (ctx->dl)
1993#define DLS (ctx->dls)
1994#define DF (ctx->df)
1995#define DD (ctx->dd)
1996#define DS (ctx->ds)
1997#define CO(x,y) ACCESS_PASCAL_ARRAY_2(ctx->co,-6,1,x,y)
1998#define SI(x,y) ACCESS_PASCAL_ARRAY_2(ctx->si,-6,1,x,y)
1999
2000static double Frac(double x)
2001{
2002 return x - floor(x);
2003}
2004
2005static void AddThe(
2006 double c1, double s1, double c2, double s2,
2007 double *c, double *s)
2008{
2009 *c = c1*c2 - s1*s2;
2010 *s = s1*c2 + c1*s2;
2011}
2012
2013static double Sine(double phi)
2014{
2015 /* sine, of phi in revolutions, not radians */
2016 return sin(PI2 * phi);
2017}
2018
2019static void LongPeriodic(MoonContext *ctx)
2020{
2021 double S1 = Sine(0.19833+0.05611*T);
2022 double S2 = Sine(0.27869+0.04508*T);
2023 double S3 = Sine(0.16827-0.36903*T);
2024 double S4 = Sine(0.34734-5.37261*T);
2025 double S5 = Sine(0.10498-5.37899*T);
2026 double S6 = Sine(0.42681-0.41855*T);
2027 double S7 = Sine(0.14943-5.37511*T);
2028
2029 DL0 = 0.84*S1+0.31*S2+14.27*S3+ 7.26*S4+ 0.28*S5+0.24*S6;
2030 DL = 2.94*S1+0.31*S2+14.27*S3+ 9.34*S4+ 1.12*S5+0.83*S6;
2031 DLS =-6.40*S1 -1.89*S6;
2032 DF = 0.21*S1+0.31*S2+14.27*S3-88.70*S4-15.30*S5+0.24*S6-1.86*S7;
2033 DD = DL0-DLS;
2034 DGAM = -3332E-9 * Sine(0.59734-5.37261*T)
2035 -539E-9 * Sine(0.35498-5.37899*T)
2036 -64E-9 * Sine(0.39943-5.37511*T);
2037}
2038
2039static void Init(MoonContext *ctx)
2040{
2041 int I, J, MAX;
2042 double T2, ARG, FAC;
2043
2044 T2 = T*T;
2045 DLAM = 0;
2046 DS = 0;
2047 GAM1C = 0;
2048 SINPI = 3422.7000;
2049 LongPeriodic(ctx);
2050 L0 = PI2*Frac(0.60643382+1336.85522467*T-0.00000313*T2) + DL0/ARC;
2051 L = PI2*Frac(0.37489701+1325.55240982*T+0.00002565*T2) + DL /ARC;
2052 LS = PI2*Frac(0.99312619+ 99.99735956*T-0.00000044*T2) + DLS/ARC;
2053 F = PI2*Frac(0.25909118+1342.22782980*T-0.00000892*T2) + DF /ARC;
2054 D = PI2*Frac(0.82736186+1236.85308708*T-0.00000397*T2) + DD /ARC;
2055 for (I=1; I<=4; ++I)
2056 {
2057 switch(I)
2058 {
2059 case 1: ARG=L; MAX=4; FAC=1.000002208; break;
2060 case 2: ARG=LS; MAX=3; FAC=0.997504612-0.002495388*T; break;
2061 case 3: ARG=F; MAX=4; FAC=1.000002708+139.978*DGAM; break;
2062 default: ARG=D; MAX=6; FAC=1.0; break;
2063 }
2064 CO(0,I) = 1.0;
2065 CO(1,I) = cos(ARG)*FAC;
2066 SI(0,I) = 0.0;
2067 SI(1,I) = sin(ARG)*FAC;
2068 for (J=2; J<=MAX; ++J)
2069 AddThe(CO(J-1,I), SI(J-1,I), CO(1,I), SI(1,I), &CO(J,I), &SI(J,I));
2070
2071 for (J=1; J<=MAX; ++J)
2072 {
2073 CO(-J,I) = CO(J,I);
2074 SI(-J,I) = -SI(J,I);
2075 }
2076 }
2077}
2078
2079static void Term(const MoonContext *ctx, int p, int q, int r, int s, double *x, double *y)
2080{
2081 int k;
2082 DECLARE_PASCAL_ARRAY_1(int, i, 1, 4);
2083 #define I(n) ACCESS_PASCAL_ARRAY_1(i,1,n)
2084
2085 I(1) = p;
2086 I(2) = q;
2087 I(3) = r;
2088 I(4) = s;
2089 *x = 1.0;
2090 *y = 0.0;
2091
2092 for (k=1; k<=4; ++k)
2093 if (I(k) != 0.0)
2094 AddThe(*x, *y, CO(I(k), k), SI(I(k), k), x, y);
2095
2096 #undef I
2097}
2098
2099static void AddSol(
2100 MoonContext *ctx,
2101 double coeffl,
2102 double coeffs,
2103 double coeffg,
2104 double coeffp,
2105 int p,
2106 int q,
2107 int r,
2108 int s)
2109{
2110 double x, y;
2111 Term(ctx, p, q, r, s, &x, &y);
2112 DLAM += coeffl*y;
2113 DS += coeffs*y;
2114 GAM1C += coeffg*x;
2115 SINPI += coeffp*x;
2116}
2117
2118#define ADDN(coeffn,p,q,r,s) ( Term(ctx, (p),(q),(r),(s),&x,&y), (N += (coeffn)*y) )
2119
2120static void SolarN(MoonContext *ctx)
2121{
2122 double x, y;
2123
2124 N = 0.0;
2125 ADDN(-526.069, 0, 0,1,-2);
2126 ADDN( -3.352, 0, 0,1,-4);
2127 ADDN( +44.297,+1, 0,1,-2);
2128 ADDN( -6.000,+1, 0,1,-4);
2129 ADDN( +20.599,-1, 0,1, 0);
2130 ADDN( -30.598,-1, 0,1,-2);
2131 ADDN( -24.649,-2, 0,1, 0);
2132 ADDN( -2.000,-2, 0,1,-2);
2133 ADDN( -22.571, 0,+1,1,-2);
2134 ADDN( +10.985, 0,-1,1,-2);
2135}
2136
2137static void Planetary(MoonContext *ctx)
2138{
2139 DLAM +=
2140 +0.82*Sine(0.7736 -62.5512*T)+0.31*Sine(0.0466 -125.1025*T)
2141 +0.35*Sine(0.5785 -25.1042*T)+0.66*Sine(0.4591+1335.8075*T)
2142 +0.64*Sine(0.3130 -91.5680*T)+1.14*Sine(0.1480+1331.2898*T)
2143 +0.21*Sine(0.5918+1056.5859*T)+0.44*Sine(0.5784+1322.8595*T)
2144 +0.24*Sine(0.2275 -5.7374*T)+0.28*Sine(0.2965 +2.6929*T)
2145 +0.33*Sine(0.3132 +6.3368*T);
2146}
2147
2148int _CalcMoonCount; /* Undocumented global for performance tuning. */
2149
2150static void CalcMoon(
2151 double centuries_since_j2000,
2152 double *geo_eclip_lon, /* (LAMBDA) equinox of date */
2153 double *geo_eclip_lat, /* (BETA) equinox of date */
2154 double *distance_au) /* (R) */
2155{
2156 double lat_seconds;
2157 MoonContext context;
2158 MoonContext *ctx = &context; /* goofy, but makes macros work inside this function */
2159
2160 context.t = centuries_since_j2000;
2161 Init(ctx);
2162
2163 AddSol(ctx, 13.9020, 14.0600, -0.0010, 0.2607, 0, 0, 0, 4);
2164 AddSol(ctx, 0.4030, -4.0100, 0.3940, 0.0023, 0, 0, 0, 3);
2165 AddSol(ctx, 2369.9120, 2373.3600, 0.6010, 28.2333, 0, 0, 0, 2);
2166 AddSol(ctx, -125.1540, -112.7900, -0.7250, -0.9781, 0, 0, 0, 1);
2167 AddSol(ctx, 1.9790, 6.9800, -0.4450, 0.0433, 1, 0, 0, 4);
2168 AddSol(ctx, 191.9530, 192.7200, 0.0290, 3.0861, 1, 0, 0, 2);
2169 AddSol(ctx, -8.4660, -13.5100, 0.4550, -0.1093, 1, 0, 0, 1);
2170 AddSol(ctx, 22639.5000, 22609.0700, 0.0790, 186.5398, 1, 0, 0, 0);
2171 AddSol(ctx, 18.6090, 3.5900, -0.0940, 0.0118, 1, 0, 0,-1);
2172 AddSol(ctx, -4586.4650, -4578.1300, -0.0770, 34.3117, 1, 0, 0,-2);
2173 AddSol(ctx, 3.2150, 5.4400, 0.1920, -0.0386, 1, 0, 0,-3);
2174 AddSol(ctx, -38.4280, -38.6400, 0.0010, 0.6008, 1, 0, 0,-4);
2175 AddSol(ctx, -0.3930, -1.4300, -0.0920, 0.0086, 1, 0, 0,-6);
2176 AddSol(ctx, -0.2890, -1.5900, 0.1230, -0.0053, 0, 1, 0, 4);
2177 AddSol(ctx, -24.4200, -25.1000, 0.0400, -0.3000, 0, 1, 0, 2);
2178 AddSol(ctx, 18.0230, 17.9300, 0.0070, 0.1494, 0, 1, 0, 1);
2179 AddSol(ctx, -668.1460, -126.9800, -1.3020, -0.3997, 0, 1, 0, 0);
2180 AddSol(ctx, 0.5600, 0.3200, -0.0010, -0.0037, 0, 1, 0,-1);
2181 AddSol(ctx, -165.1450, -165.0600, 0.0540, 1.9178, 0, 1, 0,-2);
2182 AddSol(ctx, -1.8770, -6.4600, -0.4160, 0.0339, 0, 1, 0,-4);
2183 AddSol(ctx, 0.2130, 1.0200, -0.0740, 0.0054, 2, 0, 0, 4);
2184 AddSol(ctx, 14.3870, 14.7800, -0.0170, 0.2833, 2, 0, 0, 2);
2185 AddSol(ctx, -0.5860, -1.2000, 0.0540, -0.0100, 2, 0, 0, 1);
2186 AddSol(ctx, 769.0160, 767.9600, 0.1070, 10.1657, 2, 0, 0, 0);
2187 AddSol(ctx, 1.7500, 2.0100, -0.0180, 0.0155, 2, 0, 0,-1);
2188 AddSol(ctx, -211.6560, -152.5300, 5.6790, -0.3039, 2, 0, 0,-2);
2189 AddSol(ctx, 1.2250, 0.9100, -0.0300, -0.0088, 2, 0, 0,-3);
2190 AddSol(ctx, -30.7730, -34.0700, -0.3080, 0.3722, 2, 0, 0,-4);
2191 AddSol(ctx, -0.5700, -1.4000, -0.0740, 0.0109, 2, 0, 0,-6);
2192 AddSol(ctx, -2.9210, -11.7500, 0.7870, -0.0484, 1, 1, 0, 2);
2193 AddSol(ctx, 1.2670, 1.5200, -0.0220, 0.0164, 1, 1, 0, 1);
2194 AddSol(ctx, -109.6730, -115.1800, 0.4610, -0.9490, 1, 1, 0, 0);
2195 AddSol(ctx, -205.9620, -182.3600, 2.0560, 1.4437, 1, 1, 0,-2);
2196 AddSol(ctx, 0.2330, 0.3600, 0.0120, -0.0025, 1, 1, 0,-3);
2197 AddSol(ctx, -4.3910, -9.6600, -0.4710, 0.0673, 1, 1, 0,-4);
2198 AddSol(ctx, 0.2830, 1.5300, -0.1110, 0.0060, 1,-1, 0, 4);
2199 AddSol(ctx, 14.5770, 31.7000, -1.5400, 0.2302, 1,-1, 0, 2);
2200 AddSol(ctx, 147.6870, 138.7600, 0.6790, 1.1528, 1,-1, 0, 0);
2201 AddSol(ctx, -1.0890, 0.5500, 0.0210, 0.0000, 1,-1, 0,-1);
2202 AddSol(ctx, 28.4750, 23.5900, -0.4430, -0.2257, 1,-1, 0,-2);
2203 AddSol(ctx, -0.2760, -0.3800, -0.0060, -0.0036, 1,-1, 0,-3);
2204 AddSol(ctx, 0.6360, 2.2700, 0.1460, -0.0102, 1,-1, 0,-4);
2205 AddSol(ctx, -0.1890, -1.6800, 0.1310, -0.0028, 0, 2, 0, 2);
2206 AddSol(ctx, -7.4860, -0.6600, -0.0370, -0.0086, 0, 2, 0, 0);
2207 AddSol(ctx, -8.0960, -16.3500, -0.7400, 0.0918, 0, 2, 0,-2);
2208 AddSol(ctx, -5.7410, -0.0400, 0.0000, -0.0009, 0, 0, 2, 2);
2209 AddSol(ctx, 0.2550, 0.0000, 0.0000, 0.0000, 0, 0, 2, 1);
2210 AddSol(ctx, -411.6080, -0.2000, 0.0000, -0.0124, 0, 0, 2, 0);
2211 AddSol(ctx, 0.5840, 0.8400, 0.0000, 0.0071, 0, 0, 2,-1);
2212 AddSol(ctx, -55.1730, -52.1400, 0.0000, -0.1052, 0, 0, 2,-2);
2213 AddSol(ctx, 0.2540, 0.2500, 0.0000, -0.0017, 0, 0, 2,-3);
2214 AddSol(ctx, 0.0250, -1.6700, 0.0000, 0.0031, 0, 0, 2,-4);
2215 AddSol(ctx, 1.0600, 2.9600, -0.1660, 0.0243, 3, 0, 0, 2);
2216 AddSol(ctx, 36.1240, 50.6400, -1.3000, 0.6215, 3, 0, 0, 0);
2217 AddSol(ctx, -13.1930, -16.4000, 0.2580, -0.1187, 3, 0, 0,-2);
2218 AddSol(ctx, -1.1870, -0.7400, 0.0420, 0.0074, 3, 0, 0,-4);
2219 AddSol(ctx, -0.2930, -0.3100, -0.0020, 0.0046, 3, 0, 0,-6);
2220 AddSol(ctx, -0.2900, -1.4500, 0.1160, -0.0051, 2, 1, 0, 2);
2221 AddSol(ctx, -7.6490, -10.5600, 0.2590, -0.1038, 2, 1, 0, 0);
2222 AddSol(ctx, -8.6270, -7.5900, 0.0780, -0.0192, 2, 1, 0,-2);
2223 AddSol(ctx, -2.7400, -2.5400, 0.0220, 0.0324, 2, 1, 0,-4);
2224 AddSol(ctx, 1.1810, 3.3200, -0.2120, 0.0213, 2,-1, 0, 2);
2225 AddSol(ctx, 9.7030, 11.6700, -0.1510, 0.1268, 2,-1, 0, 0);
2226 AddSol(ctx, -0.3520, -0.3700, 0.0010, -0.0028, 2,-1, 0,-1);
2227 AddSol(ctx, -2.4940, -1.1700, -0.0030, -0.0017, 2,-1, 0,-2);
2228 AddSol(ctx, 0.3600, 0.2000, -0.0120, -0.0043, 2,-1, 0,-4);
2229 AddSol(ctx, -1.1670, -1.2500, 0.0080, -0.0106, 1, 2, 0, 0);
2230 AddSol(ctx, -7.4120, -6.1200, 0.1170, 0.0484, 1, 2, 0,-2);
2231 AddSol(ctx, -0.3110, -0.6500, -0.0320, 0.0044, 1, 2, 0,-4);
2232 AddSol(ctx, 0.7570, 1.8200, -0.1050, 0.0112, 1,-2, 0, 2);
2233 AddSol(ctx, 2.5800, 2.3200, 0.0270, 0.0196, 1,-2, 0, 0);
2234 AddSol(ctx, 2.5330, 2.4000, -0.0140, -0.0212, 1,-2, 0,-2);
2235 AddSol(ctx, -0.3440, -0.5700, -0.0250, 0.0036, 0, 3, 0,-2);
2236 AddSol(ctx, -0.9920, -0.0200, 0.0000, 0.0000, 1, 0, 2, 2);
2237 AddSol(ctx, -45.0990, -0.0200, 0.0000, -0.0010, 1, 0, 2, 0);
2238 AddSol(ctx, -0.1790, -9.5200, 0.0000, -0.0833, 1, 0, 2,-2);
2239 AddSol(ctx, -0.3010, -0.3300, 0.0000, 0.0014, 1, 0, 2,-4);
2240 AddSol(ctx, -6.3820, -3.3700, 0.0000, -0.0481, 1, 0,-2, 2);
2241 AddSol(ctx, 39.5280, 85.1300, 0.0000, -0.7136, 1, 0,-2, 0);
2242 AddSol(ctx, 9.3660, 0.7100, 0.0000, -0.0112, 1, 0,-2,-2);
2243 AddSol(ctx, 0.2020, 0.0200, 0.0000, 0.0000, 1, 0,-2,-4);
2244 AddSol(ctx, 0.4150, 0.1000, 0.0000, 0.0013, 0, 1, 2, 0);
2245 AddSol(ctx, -2.1520, -2.2600, 0.0000, -0.0066, 0, 1, 2,-2);
2246 AddSol(ctx, -1.4400, -1.3000, 0.0000, 0.0014, 0, 1,-2, 2);
2247 AddSol(ctx, 0.3840, -0.0400, 0.0000, 0.0000, 0, 1,-2,-2);
2248 AddSol(ctx, 1.9380, 3.6000, -0.1450, 0.0401, 4, 0, 0, 0);
2249 AddSol(ctx, -0.9520, -1.5800, 0.0520, -0.0130, 4, 0, 0,-2);
2250 AddSol(ctx, -0.5510, -0.9400, 0.0320, -0.0097, 3, 1, 0, 0);
2251 AddSol(ctx, -0.4820, -0.5700, 0.0050, -0.0045, 3, 1, 0,-2);
2252 AddSol(ctx, 0.6810, 0.9600, -0.0260, 0.0115, 3,-1, 0, 0);
2253 AddSol(ctx, -0.2970, -0.2700, 0.0020, -0.0009, 2, 2, 0,-2);
2254 AddSol(ctx, 0.2540, 0.2100, -0.0030, 0.0000, 2,-2, 0,-2);
2255 AddSol(ctx, -0.2500, -0.2200, 0.0040, 0.0014, 1, 3, 0,-2);
2256 AddSol(ctx, -3.9960, 0.0000, 0.0000, 0.0004, 2, 0, 2, 0);
2257 AddSol(ctx, 0.5570, -0.7500, 0.0000, -0.0090, 2, 0, 2,-2);
2258 AddSol(ctx, -0.4590, -0.3800, 0.0000, -0.0053, 2, 0,-2, 2);
2259 AddSol(ctx, -1.2980, 0.7400, 0.0000, 0.0004, 2, 0,-2, 0);
2260 AddSol(ctx, 0.5380, 1.1400, 0.0000, -0.0141, 2, 0,-2,-2);
2261 AddSol(ctx, 0.2630, 0.0200, 0.0000, 0.0000, 1, 1, 2, 0);
2262 AddSol(ctx, 0.4260, 0.0700, 0.0000, -0.0006, 1, 1,-2,-2);
2263 AddSol(ctx, -0.3040, 0.0300, 0.0000, 0.0003, 1,-1, 2, 0);
2264 AddSol(ctx, -0.3720, -0.1900, 0.0000, -0.0027, 1,-1,-2, 2);
2265 AddSol(ctx, 0.4180, 0.0000, 0.0000, 0.0000, 0, 0, 4, 0);
2266 AddSol(ctx, -0.3300, -0.0400, 0.0000, 0.0000, 3, 0, 2, 0);
2267
2268 SolarN(ctx);
2269 Planetary(ctx);
2270 S = F + DS/ARC;
2271
2272 lat_seconds = (1.000002708 + 139.978*DGAM)*(18518.511+1.189+GAM1C)*sin(S)-6.24*sin(3*S) + N;
2273
2274 *geo_eclip_lon = PI2 * Frac((L0+DLAM/ARC) / PI2);
2275 *geo_eclip_lat = lat_seconds * (DEG2RAD / 3600.0);
2276 *distance_au = (ARC * EARTH_EQUATORIAL_RADIUS_AU) / (0.999953253 * SINPI);
2277 ++_CalcMoonCount;
2278}
2279
2280#undef T
2281#undef DGAM
2282#undef DLAM
2283#undef N
2284#undef GAM1C
2285#undef SINPI
2286#undef L0
2287#undef L
2288#undef LS
2289#undef F
2290#undef D
2291#undef S
2292#undef DL0
2293#undef DL
2294#undef DLS
2295#undef DF
2296#undef DD
2297#undef DS
2298#undef CO
2299#undef SI
2300
2301/** @endcond */
2302
2303/**
2304 * @brief Calculates equatorial geocentric position of the Moon at a given time.
2305 *
2306 * Given a time of observation, calculates the Moon's position as a vector.
2307 * The vector gives the location of the Moon's center relative to the Earth's center
2308 * with x-, y-, and z-components measured in astronomical units.
2309 * The coordinates are oriented with respect to the Earth's equator at the J2000 epoch.
2310 * In Astronomy Engine, this orientation is called EQJ.
2311 *
2312 * This algorithm is based on the Nautical Almanac Office's *Improved Lunar Ephemeris* of 1954,
2313 * which in turn derives from E. W. Brown's lunar theories from the early twentieth century.
2314 * It is adapted from Turbo Pascal code from the book
2315 * [Astronomy on the Personal Computer](https://www.springer.com/us/book/9783540672210)
2316 * by Montenbruck and Pfleger.
2317 *
2318 * To calculate ecliptic spherical coordinates instead, see #Astronomy_EclipticGeoMoon.
2319 *
2320 * @param time The date and time for which to calculate the Moon's position.
2321 * @return The Moon's position as a vector in J2000 Cartesian equatorial (EQJ) coordinates.
2322 */
2323astro_vector_t Astronomy_GeoMoon(astro_time_t time)
2324{
2325 double geo_eclip_lon, geo_eclip_lat, distance_au;
2326 double dist_cos_lat;
2327 astro_vector_t vector;
2328 double gepos[3];
2329 double mpos1[3];
2330 double mpos2[3];
2331
2332 CalcMoon(time.tt / 36525.0, &geo_eclip_lon, &geo_eclip_lat, &distance_au);
2333
2334 /* Convert geocentric ecliptic spherical coordinates to Cartesian coordinates. */
2335 dist_cos_lat = distance_au * cos(geo_eclip_lat);
2336 gepos[0] = dist_cos_lat * cos(geo_eclip_lon);
2337 gepos[1] = dist_cos_lat * sin(geo_eclip_lon);
2338 gepos[2] = distance_au * sin(geo_eclip_lat);
2339
2340 /* Convert ecliptic coordinates to equatorial coordinates, both in mean equinox of date. */
2341 ecl2equ_vec(time, gepos, mpos1);
2342
2343 /* Convert equatorial coordinates from mean equinox of date to J2000 mean equinox. */
2344 precession(mpos1, time, INTO_2000, mpos2);
2345
2346 vector.status = ASTRO_SUCCESS;
2347 vector.x = mpos2[0];
2348 vector.y = mpos2[1];
2349 vector.z = mpos2[2];
2350 vector.t = time;
2351 return vector;
2352}
2353
2354
2355/**
2356 * @brief Calculates spherical ecliptic geocentric position of the Moon.
2357 *
2358 * Given a time of observation, calculates the Moon's geocentric position
2359 * in ecliptic spherical coordinates. Provides the ecliptic latitude and
2360 * longitude in degrees, and the geocentric distance in astronomical units (AU).
2361 *
2362 * The ecliptic angles are measured in "ECT": relative to the true ecliptic plane and
2363 * equatorial plane at the specified time. This means the Earth's equator
2364 * is corrected for precession and nutation, and the plane of the Earth's
2365 * orbit is corrected for gradual obliquity drift.
2366 *
2367 * This algorithm is based on the Nautical Almanac Office's *Improved Lunar Ephemeris* of 1954,
2368 * which in turn derives from E. W. Brown's lunar theories from the early twentieth century.
2369 * It is adapted from Turbo Pascal code from the book
2370 * [Astronomy on the Personal Computer](https://www.springer.com/us/book/9783540672210)
2371 * by Montenbruck and Pfleger.
2372 *
2373 * To calculate a J2000 mean equator vector instead, use #Astronomy_GeoMoon.
2374 *
2375 * @param time The date and time for which to calculate the Moon's position.
2376 * @return The Moon's position expressed in ecliptic coordinates using the true equinox of date (ECT).
2377 */
2378astro_spherical_t Astronomy_EclipticGeoMoon(astro_time_t time)
2379{
2380 astro_spherical_t sphere;
2381 astro_ecliptic_t eclip;
2382 earth_tilt_t et;
2383 double dist_cos_lat, ecm[3], eqm[3], eqd[3];
2384
2385 /* CalcMoon produces ecliptic coordinates in mean equinox of date (ECM). */
2386 CalcMoon(time.tt / 36525.0, &sphere.lon, &sphere.lat, &sphere.dist);
2387
2388 /* Calculate vector in ecliptic coordinates (ECM). */
2389 dist_cos_lat = sphere.dist * cos(sphere.lat);
2390 ecm[0] = dist_cos_lat * cos(sphere.lon);
2391 ecm[1] = dist_cos_lat * sin(sphere.lon);
2392 ecm[2] = sphere.dist * sin(sphere.lat);
2393
2394 /* Obtain true and mean obliquity angles for the given time. */
2395 /* This serves to pre-calculate the nutation also, and cache it in `time`. */
2396 et = e_tilt(&time);
2397
2398 /* Convert ecliptic coordinates to equatorial coordinates, both in mean equinox of date. */
2399 /* In other words, convert ECM to EQM. */
2400 obl_ecl2equ_vec(et.mobl, time, ecm, eqm);
2401
2402 /* Add nutation to convert ECM to true equatorial coordinates of date (EQD). */
2403 nutation(eqm, &time, FROM_2000, eqd);
2404
2405 /* Convert back to ecliptic, this time in true equinox of date (ECT). */
2406 eclip = RotateEquatorialToEcliptic(eqd, DEG2RAD * et.tobl, time);
2407
2408 /* Package the return value. */
2409 /* CalcMoon() already set sphere.dist to the correct value. */
2410 sphere.status = eclip.status;
2411 sphere.lat = eclip.elat;
2412 sphere.lon = eclip.elon;
2413 return sphere;
2414}
2415
2416
2417/**
2418 * @brief Calculates equatorial geocentric position and velocity of the Moon at a given time.
2419 *
2420 * Given a time of observation, calculates the Moon's position and velocity vectors.
2421 * The position and velocity are of the Moon's center relative to the Earth's center.
2422 * The position (x, y, z) components are expressed in AU (astronomical units).
2423 * The velocity (vx, vy, vz) components are expressed in AU/day.
2424 * The coordinates are oriented with respect to the Earth's equator at the J2000 epoch.
2425 * In Astronomy Engine, this orientation is called EQJ.
2426 *
2427 * If you need the Moon's position only, and not its velocity,
2428 * it is much more efficient to use #Astronomy_GeoMoon instead.
2429 *
2430 * @param time The date and time for which to calculate the Moon's position and velocity.
2431 * @return The Moon's position and velocity vectors in J2000 equatorial coordinates (EQJ).
2432 */
2433astro_state_vector_t Astronomy_GeoMoonState(astro_time_t time)
2434{
2435 /*
2436 This is a hack, because trying to figure out how to derive a time
2437 derivative for CalcMoon() would be extremely painful!
2438 Calculate just before and just after the given time.
2439 Average to find position, subtract to find velocity.
2440 */
2441 const double dt = 1.0e-5; /* 0.864 seconds */
2442 astro_vector_t r1, r2;
2443 astro_time_t t1, t2;
2444 astro_state_vector_t s;
2445
2446 t1 = Astronomy_AddDays(time, -dt);
2447 t2 = Astronomy_AddDays(time, +dt);
2448
2449 r1 = Astronomy_GeoMoon(t1);
2450 r2 = Astronomy_GeoMoon(t2);
2451
2452 /* The desired position is the average of the two calculated positions. */
2453 s.x = (r1.x + r2.x) / 2;
2454 s.y = (r1.y + r2.y) / 2;
2455 s.z = (r1.z + r2.z) / 2;
2456
2457 /* The difference of the position vectors divided by the time span gives the velocity vector. */
2458 s.vx = (r2.x - r1.x) / (2 * dt);
2459 s.vy = (r2.y - r1.y) / (2 * dt);
2460 s.vz = (r2.z - r1.z) / (2 * dt);
2461 s.t = time;
2462 s.status = ASTRO_SUCCESS;
2463
2464 return s;
2465}
2466
2467
2468/**
2469 * @brief Calculates the geocentric position and velocity of the Earth/Moon barycenter.
2470 *
2471 * Given a time of observation, calculates the geocentric position and velocity vectors
2472 * of the Earth/Moon barycenter (EMB).
2473 * The position (x, y, z) components are expressed in AU (astronomical units).
2474 * The velocity (vx, vy, vz) components are expressed in AU/day.
2475 * The coordinates are oriented with respect to the Earth's equator at the J2000 epoch.
2476 * In Astronomy Engine, this orientation is called EQJ.
2477 *
2478 * @param time The date and time for which to calculate the EMB vectors.
2479 * @return The EMB's position and velocity vectors in geocentric J2000 equatorial coordinates.
2480 */
2481astro_state_vector_t Astronomy_GeoEmbState(astro_time_t time)
2482{
2483 astro_state_vector_t s = Astronomy_GeoMoonState(time);
2484 const double d = 1.0 + EARTH_MOON_MASS_RATIO;
2485 s.x /= d;
2486 s.y /= d;
2487 s.z /= d;
2488 s.vx /= d;
2489 s.vy /= d;
2490 s.vz /= d;
2491 return s;
2492}
2493
2494
2495/**
2496 * @brief Calculates the Moon's libration angles at a given moment in time.
2497 *
2498 * Libration is an observed back-and-forth wobble of the portion of the
2499 * Moon visible from the Earth. It is caused by the imperfect tidal locking
2500 * of the Moon's fixed rotation rate, compared to its variable angular speed
2501 * of orbit around the Earth.
2502 *
2503 * This function calculates a pair of perpendicular libration angles,
2504 * one representing rotation of the Moon in ecliptic longitude `elon`, the other
2505 * in ecliptic latitude `elat`, both relative to the Moon's mean Earth-facing position.
2506 *
2507 * This function also returns the geocentric position of the Moon
2508 * expressed in ecliptic longitude `mlon`, ecliptic latitude `mlat`, the
2509 * distance `dist_km` between the centers of the Earth and Moon expressed in kilometers,
2510 * and the apparent angular diameter of the Moon `diam_deg`.
2511 *
2512 * @param time The date and time for which to calculate libration angles.
2513 * @return The Moon's ecliptic position and libration angles as seen from the Earth.
2514 */
2515astro_libration_t Astronomy_Libration(astro_time_t time)
2516{
2517 astro_libration_t lib;
2518 double t, t2, t3, t4;
2519 double f, omega, w, a, ldash, ldash2, bdash, bdash2;
2520 double k1, k2, m, mdash, d, e, rho, sigma, tau;
2521 const double sin_I = sin(MOON_AXIS_INCLINATION_RADIANS);
2522 const double cos_I = cos(MOON_AXIS_INCLINATION_RADIANS);
2523
2524 t = time.tt / 36525.0;
2525 t2 = t * t;
2526 t3 = t2 * t;
2527 t4 = t2 * t2;
2528
2529 double mlon; /* Moon's ecliptic longitude in radians. */
2530 double mlat; /* Moon's ecliptic latitude in radians. */
2531 CalcMoon(t, &mlon, &mlat, &lib.dist_km);
2532 lib.mlon = RAD2DEG * mlon;
2533 lib.mlat = RAD2DEG * mlat;
2534 lib.dist_km *= KM_PER_AU;
2535 lib.diam_deg = (2.0 * RAD2DEG) * atan(MOON_MEAN_RADIUS_KM / sqrt(lib.dist_km*lib.dist_km - MOON_MEAN_RADIUS_KM*MOON_MEAN_RADIUS_KM));
2536
2537 /* Moon's argument of latitude in radians. */
2538 f = DEG2RAD * NormalizeLongitude(93.2720950 + 483202.0175233*t - 0.0036539*t2 - t3/3526000 + t4/863310000);
2539
2540 /* Moon's ascending node's mean longitude in radians. */
2541 omega = DEG2RAD * NormalizeLongitude(125.0445479 - 1934.1362891*t + 0.0020754*t2 + t3/467441 - t4/60616000);
2542
2543 /* Sun's mean anomaly. */
2544 m = DEG2RAD * NormalizeLongitude(357.5291092 + 35999.0502909*t - 0.0001536*t2 + t3/24490000);
2545
2546 /* Moon's mean anomaly. */
2547 mdash = DEG2RAD * NormalizeLongitude(134.9633964 + 477198.8675055*t + 0.0087414*t2 + t3/69699 - t4/14712000);
2548
2549 /* Moon's mean elongation. */
2550 d = DEG2RAD * NormalizeLongitude(297.8501921 + 445267.1114034*t - 0.0018819*t2 + t3/545868 - t4/113065000);
2551
2552 /* Eccentricity of the Earth's orbit. */
2553 e = 1.0 - 0.002516*t - 0.0000074*t2;
2554
2555 /* Optical librations */
2556 w = mlon - omega;
2557 a = atan2(sin(w)*cos(mlat)*cos_I - sin(mlat)*sin_I, cos(w)*cos(mlat));
2558 ldash = LongitudeOffset(RAD2DEG * (a - f));
2559 bdash = asin(-sin(w)*cos(mlat)*sin_I - sin(mlat)*cos_I);
2560
2561 /* Physical librations */
2562 k1 = DEG2RAD*(119.75 + 131.849*t);
2563 k2 = DEG2RAD*(72.56 + 20.186*t);
2564
2565 rho = (
2566 -0.02752*cos(mdash) +
2567 -0.02245*sin(f) +
2568 +0.00684*cos(mdash - 2*f) +
2569 -0.00293*cos(2*f) +
2570 -0.00085*cos(2*f - 2*d) +
2571 -0.00054*cos(mdash - 2*d) +
2572 -0.00020*sin(mdash + f) +
2573 -0.00020*cos(mdash + 2*f) +
2574 -0.00020*cos(mdash - f) +
2575 +0.00014*cos(mdash + 2*f - 2*d)
2576 );
2577
2578 sigma = (
2579 -0.02816*sin(mdash) +
2580 +0.02244*cos(f) +
2581 -0.00682*sin(mdash - 2*f) +
2582 -0.00279*sin(2*f) +
2583 -0.00083*sin(2*f - 2*d) +
2584 +0.00069*sin(mdash - 2*d) +
2585 +0.00040*cos(mdash + f) +
2586 -0.00025*sin(2*mdash) +
2587 -0.00023*sin(mdash + 2*f) +
2588 +0.00020*cos(mdash - f) +
2589 +0.00019*sin(mdash - f) +
2590 +0.00013*sin(mdash + 2*f - 2*d) +
2591 -0.00010*cos(mdash - 3*f)
2592 );
2593
2594 tau = (
2595 +0.02520*e*sin(m) +
2596 +0.00473*sin(2*mdash - 2*f) +
2597 -0.00467*sin(mdash) +
2598 +0.00396*sin(k1) +
2599 +0.00276*sin(2*mdash - 2*d) +
2600 +0.00196*sin(omega) +
2601 -0.00183*cos(mdash - f) +
2602 +0.00115*sin(mdash - 2*d) +
2603 -0.00096*sin(mdash - d) +
2604 +0.00046*sin(2*f - 2*d) +
2605 -0.00039*sin(mdash - f) +
2606 -0.00032*sin(mdash - m - d) +
2607 +0.00027*sin(2*mdash - m - 2*d) +
2608 +0.00023*sin(k2) +
2609 -0.00014*sin(2*d) +
2610 +0.00014*cos(2*mdash - 2*f) +
2611 -0.00012*sin(mdash - 2*f) +
2612 -0.00012*sin(2*mdash) +
2613 +0.00011*sin(2*mdash - 2*m - 2*d)
2614 );
2615
2616 ldash2 = -tau + (rho*cos(a) + sigma*sin(a))*tan(bdash);
2617 bdash *= RAD2DEG;
2618 bdash2 = sigma*cos(a) - rho*sin(a);
2619
2620 lib.elon = ldash + ldash2;
2621 lib.elat = bdash + bdash2;
2622
2623 return lib;
2624}
2625
2626
2627/*------------------ VSOP ------------------*/
2628
2629/** @cond DOXYGEN_SKIP */
2630typedef struct
2631{
2632 double amplitude;
2633 double phase;
2634 double frequency;
2635}
2636vsop_term_t;
2637
2638typedef struct
2639{
2640 int nterms;
2641 const vsop_term_t *term;
2642}
2643vsop_series_t;
2644
2645typedef struct
2646{
2647 int nseries;
2648 const vsop_series_t *series;
2649}
2650vsop_formula_t;
2651
2652typedef struct
2653{
2654 const vsop_formula_t formula[3];
2655}
2656vsop_model_t;
2657
2658typedef struct
2659{
2660 double mu;
2661 double al[2];
2662 vsop_series_t a;
2663 vsop_series_t l;
2664 vsop_series_t z;
2665 vsop_series_t zeta;
2666}
2667jupiter_moon_t;
2668/** @endcond */
2669
2670static const vsop_term_t vsop_lon_Mercury_0[] =
2671{
2672 { 4.40250710144, 0.00000000000, 0.00000000000 },
2673 { 0.40989414977, 1.48302034195, 26087.90314157420 },
2674 { 0.05046294200, 4.47785489551, 52175.80628314840 },
2675 { 0.00855346844, 1.16520322459, 78263.70942472259 },
2676 { 0.00165590362, 4.11969163423, 104351.61256629678 },
2677 { 0.00034561897, 0.77930768443, 130439.51570787099 },
2678 { 0.00007583476, 3.71348404924, 156527.41884944518 }
2679};
2680
2681static const vsop_term_t vsop_lon_Mercury_1[] =
2682{
2683 { 26087.90313685529, 0.00000000000, 0.00000000000 },
2684 { 0.01131199811, 6.21874197797, 26087.90314157420 },
2685 { 0.00292242298, 3.04449355541, 52175.80628314840 },
2686 { 0.00075775081, 6.08568821653, 78263.70942472259 },
2687 { 0.00019676525, 2.80965111777, 104351.61256629678 }
2688};
2689
2690static const vsop_series_t vsop_lon_Mercury[] =
2691{
2692 { 7, vsop_lon_Mercury_0 },
2693 { 5, vsop_lon_Mercury_1 }
2694};
2695
2696static const vsop_term_t vsop_lat_Mercury_0[] =
2697{
2698 { 0.11737528961, 1.98357498767, 26087.90314157420 },
2699 { 0.02388076996, 5.03738959686, 52175.80628314840 },
2700 { 0.01222839532, 3.14159265359, 0.00000000000 },
2701 { 0.00543251810, 1.79644363964, 78263.70942472259 },
2702 { 0.00129778770, 4.83232503958, 104351.61256629678 },
2703 { 0.00031866927, 1.58088495658, 130439.51570787099 },
2704 { 0.00007963301, 4.60972126127, 156527.41884944518 }
2705};
2706
2707static const vsop_term_t vsop_lat_Mercury_1[] =
2708{
2709 { 0.00274646065, 3.95008450011, 26087.90314157420 },
2710 { 0.00099737713, 3.14159265359, 0.00000000000 }
2711};
2712
2713static const vsop_series_t vsop_lat_Mercury[] =
2714{
2715 { 7, vsop_lat_Mercury_0 },
2716 { 2, vsop_lat_Mercury_1 }
2717};
2718
2719static const vsop_term_t vsop_rad_Mercury_0[] =
2720{
2721 { 0.39528271651, 0.00000000000, 0.00000000000 },
2722 { 0.07834131818, 6.19233722598, 26087.90314157420 },
2723 { 0.00795525558, 2.95989690104, 52175.80628314840 },
2724 { 0.00121281764, 6.01064153797, 78263.70942472259 },
2725 { 0.00021921969, 2.77820093972, 104351.61256629678 },
2726 { 0.00004354065, 5.82894543774, 130439.51570787099 }
2727};
2728
2729static const vsop_term_t vsop_rad_Mercury_1[] =
2730{
2731 { 0.00217347740, 4.65617158665, 26087.90314157420 },
2732 { 0.00044141826, 1.42385544001, 52175.80628314840 }
2733};
2734
2735static const vsop_series_t vsop_rad_Mercury[] =
2736{
2737 { 6, vsop_rad_Mercury_0 },
2738 { 2, vsop_rad_Mercury_1 }
2739};
2740
2741;
2742static const vsop_term_t vsop_lon_Venus_0[] =
2743{
2744 { 3.17614666774, 0.00000000000, 0.00000000000 },
2745 { 0.01353968419, 5.59313319619, 10213.28554621100 },
2746 { 0.00089891645, 5.30650047764, 20426.57109242200 },
2747 { 0.00005477194, 4.41630661466, 7860.41939243920 },
2748 { 0.00003455741, 2.69964447820, 11790.62908865880 },
2749 { 0.00002372061, 2.99377542079, 3930.20969621960 },
2750 { 0.00001317168, 5.18668228402, 26.29831979980 },
2751 { 0.00001664146, 4.25018630147, 1577.34354244780 },
2752 { 0.00001438387, 4.15745084182, 9683.59458111640 },
2753 { 0.00001200521, 6.15357116043, 30639.85663863300 }
2754};
2755
2756static const vsop_term_t vsop_lon_Venus_1[] =
2757{
2758 { 10213.28554621638, 0.00000000000, 0.00000000000 },
2759 { 0.00095617813, 2.46406511110, 10213.28554621100 },
2760 { 0.00007787201, 0.62478482220, 20426.57109242200 }
2761};
2762
2763static const vsop_series_t vsop_lon_Venus[] =
2764{
2765 { 10, vsop_lon_Venus_0 },
2766 { 3, vsop_lon_Venus_1 }
2767};
2768
2769static const vsop_term_t vsop_lat_Venus_0[] =
2770{
2771 { 0.05923638472, 0.26702775812, 10213.28554621100 },
2772 { 0.00040107978, 1.14737178112, 20426.57109242200 },
2773 { 0.00032814918, 3.14159265359, 0.00000000000 }
2774};
2775
2776static const vsop_term_t vsop_lat_Venus_1[] =
2777{
2778 { 0.00287821243, 1.88964962838, 10213.28554621100 }
2779};
2780
2781static const vsop_series_t vsop_lat_Venus[] =
2782{
2783 { 3, vsop_lat_Venus_0 },
2784 { 1, vsop_lat_Venus_1 }
2785};
2786
2787static const vsop_term_t vsop_rad_Venus_0[] =
2788{
2789 { 0.72334820891, 0.00000000000, 0.00000000000 },
2790 { 0.00489824182, 4.02151831717, 10213.28554621100 },
2791 { 0.00001658058, 4.90206728031, 20426.57109242200 },
2792 { 0.00001378043, 1.12846591367, 11790.62908865880 },
2793 { 0.00001632096, 2.84548795207, 7860.41939243920 },
2794 { 0.00000498395, 2.58682193892, 9683.59458111640 },
2795 { 0.00000221985, 2.01346696541, 19367.18916223280 },
2796 { 0.00000237454, 2.55136053886, 15720.83878487840 }
2797};
2798
2799static const vsop_term_t vsop_rad_Venus_1[] =
2800{
2801 { 0.00034551041, 0.89198706276, 10213.28554621100 }
2802};
2803
2804static const vsop_series_t vsop_rad_Venus[] =
2805{
2806 { 8, vsop_rad_Venus_0 },
2807 { 1, vsop_rad_Venus_1 }
2808};
2809
2810;
2811static const vsop_term_t vsop_lon_Earth_0[] =
2812{
2813 { 1.75347045673, 0.00000000000, 0.00000000000 },
2814 { 0.03341656453, 4.66925680415, 6283.07584999140 },
2815 { 0.00034894275, 4.62610242189, 12566.15169998280 },
2816 { 0.00003417572, 2.82886579754, 3.52311834900 },
2817 { 0.00003497056, 2.74411783405, 5753.38488489680 },
2818 { 0.00003135899, 3.62767041756, 77713.77146812050 },
2819 { 0.00002676218, 4.41808345438, 7860.41939243920 },
2820 { 0.00002342691, 6.13516214446, 3930.20969621960 },
2821 { 0.00001273165, 2.03709657878, 529.69096509460 },
2822 { 0.00001324294, 0.74246341673, 11506.76976979360 },
2823 { 0.00000901854, 2.04505446477, 26.29831979980 },
2824 { 0.00001199167, 1.10962946234, 1577.34354244780 },
2825 { 0.00000857223, 3.50849152283, 398.14900340820 },
2826 { 0.00000779786, 1.17882681962, 5223.69391980220 },
2827 { 0.00000990250, 5.23268072088, 5884.92684658320 },
2828 { 0.00000753141, 2.53339052847, 5507.55323866740 },
2829 { 0.00000505267, 4.58292599973, 18849.22754997420 },
2830 { 0.00000492392, 4.20505711826, 775.52261132400 },
2831 { 0.00000356672, 2.91954114478, 0.06731030280 },
2832 { 0.00000284125, 1.89869240932, 796.29800681640 },
2833 { 0.00000242879, 0.34481445893, 5486.77784317500 },
2834 { 0.00000317087, 5.84901948512, 11790.62908865880 },
2835 { 0.00000271112, 0.31486255375, 10977.07880469900 },
2836 { 0.00000206217, 4.80646631478, 2544.31441988340 },
2837 { 0.00000205478, 1.86953770281, 5573.14280143310 },
2838 { 0.00000202318, 2.45767790232, 6069.77675455340 },
2839 { 0.00000126225, 1.08295459501, 20.77539549240 },
2840 { 0.00000155516, 0.83306084617, 213.29909543800 }
2841};
2842
2843static const vsop_term_t vsop_lon_Earth_1[] =
2844{
2845 { 6283.07584999140, 0.00000000000, 0.00000000000 },
2846 { 0.00206058863, 2.67823455808, 6283.07584999140 },
2847 { 0.00004303419, 2.63512233481, 12566.15169998280 }
2848};
2849
2850static const vsop_term_t vsop_lon_Earth_2[] =
2851{
2852 { 0.00008721859, 1.07253635559, 6283.07584999140 }
2853};
2854
2855static const vsop_series_t vsop_lon_Earth[] =
2856{
2857 { 28, vsop_lon_Earth_0 },
2858 { 3, vsop_lon_Earth_1 },
2859 { 1, vsop_lon_Earth_2 }
2860};
2861
2862static const vsop_term_t vsop_lat_Earth_1[] =
2863{
2864 { 0.00227777722, 3.41376620530, 6283.07584999140 },
2865 { 0.00003805678, 3.37063423795, 12566.15169998280 }
2866};
2867
2868static const vsop_series_t vsop_lat_Earth[] =
2869{
2870 { 0, NULL },
2871 { 2, vsop_lat_Earth_1 }
2872};
2873
2874static const vsop_term_t vsop_rad_Earth_0[] =
2875{
2876 { 1.00013988784, 0.00000000000, 0.00000000000 },
2877 { 0.01670699632, 3.09846350258, 6283.07584999140 },
2878 { 0.00013956024, 3.05524609456, 12566.15169998280 },
2879 { 0.00003083720, 5.19846674381, 77713.77146812050 },
2880 { 0.00001628463, 1.17387558054, 5753.38488489680 },
2881 { 0.00001575572, 2.84685214877, 7860.41939243920 },
2882 { 0.00000924799, 5.45292236722, 11506.76976979360 },
2883 { 0.00000542439, 4.56409151453, 3930.20969621960 },
2884 { 0.00000472110, 3.66100022149, 5884.92684658320 },
2885 { 0.00000085831, 1.27079125277, 161000.68573767410 },
2886 { 0.00000057056, 2.01374292245, 83996.84731811189 },
2887 { 0.00000055736, 5.24159799170, 71430.69561812909 },
2888 { 0.00000174844, 3.01193636733, 18849.22754997420 },
2889 { 0.00000243181, 4.27349530790, 11790.62908865880 }
2890};
2891
2892static const vsop_term_t vsop_rad_Earth_1[] =
2893{
2894 { 0.00103018607, 1.10748968172, 6283.07584999140 },
2895 { 0.00001721238, 1.06442300386, 12566.15169998280 }
2896};
2897
2898static const vsop_term_t vsop_rad_Earth_2[] =
2899{
2900 { 0.00004359385, 5.78455133808, 6283.07584999140 }
2901};
2902
2903static const vsop_series_t vsop_rad_Earth[] =
2904{
2905 { 14, vsop_rad_Earth_0 },
2906 { 2, vsop_rad_Earth_1 },
2907 { 1, vsop_rad_Earth_2 }
2908};
2909
2910;
2911static const vsop_term_t vsop_lon_Mars_0[] =
2912{
2913 { 6.20347711581, 0.00000000000, 0.00000000000 },
2914 { 0.18656368093, 5.05037100270, 3340.61242669980 },
2915 { 0.01108216816, 5.40099836344, 6681.22485339960 },
2916 { 0.00091798406, 5.75478744667, 10021.83728009940 },
2917 { 0.00027744987, 5.97049513147, 3.52311834900 },
2918 { 0.00010610235, 2.93958560338, 2281.23049651060 },
2919 { 0.00012315897, 0.84956094002, 2810.92146160520 },
2920 { 0.00008926784, 4.15697846427, 0.01725365220 },
2921 { 0.00008715691, 6.11005153139, 13362.44970679920 },
2922 { 0.00006797556, 0.36462229657, 398.14900340820 },
2923 { 0.00007774872, 3.33968761376, 5621.84292321040 },
2924 { 0.00003575078, 1.66186505710, 2544.31441988340 },
2925 { 0.00004161108, 0.22814971327, 2942.46342329160 },
2926 { 0.00003075252, 0.85696614132, 191.44826611160 },
2927 { 0.00002628117, 0.64806124465, 3337.08930835080 },
2928 { 0.00002937546, 6.07893711402, 0.06731030280 },
2929 { 0.00002389414, 5.03896442664, 796.29800681640 },
2930 { 0.00002579844, 0.02996736156, 3344.13554504880 },
2931 { 0.00001528141, 1.14979301996, 6151.53388830500 },
2932 { 0.00001798806, 0.65634057445, 529.69096509460 },
2933 { 0.00001264357, 3.62275122593, 5092.15195811580 },
2934 { 0.00001286228, 3.06796065034, 2146.16541647520 },
2935 { 0.00001546404, 2.91579701718, 1751.53953141600 },
2936 { 0.00001024902, 3.69334099279, 8962.45534991020 },
2937 { 0.00000891566, 0.18293837498, 16703.06213349900 },
2938 { 0.00000858759, 2.40093811940, 2914.01423582380 },
2939 { 0.00000832715, 2.46418619474, 3340.59517304760 },
2940 { 0.00000832720, 4.49495782139, 3340.62968035200 },
2941 { 0.00000712902, 3.66335473479, 1059.38193018920 },
2942 { 0.00000748723, 3.82248614017, 155.42039943420 },
2943 { 0.00000723861, 0.67497311481, 3738.76143010800 },
2944 { 0.00000635548, 2.92182225127, 8432.76438481560 },
2945 { 0.00000655162, 0.48864064125, 3127.31333126180 },
2946 { 0.00000550474, 3.81001042328, 0.98032106820 },
2947 { 0.00000552750, 4.47479317037, 1748.01641306700 },
2948 { 0.00000425966, 0.55364317304, 6283.07584999140 },
2949 { 0.00000415131, 0.49662285038, 213.29909543800 },
2950 { 0.00000472167, 3.62547124025, 1194.44701022460 },
2951 { 0.00000306551, 0.38052848348, 6684.74797174860 },
2952 { 0.00000312141, 0.99853944405, 6677.70173505060 },
2953 { 0.00000293198, 4.22131299634, 20.77539549240 },
2954 { 0.00000302375, 4.48618007156, 3532.06069281140 },
2955 { 0.00000274027, 0.54222167059, 3340.54511639700 },
2956 { 0.00000281079, 5.88163521788, 1349.86740965880 },
2957 { 0.00000231183, 1.28242156993, 3870.30339179440 },
2958 { 0.00000283602, 5.76885434940, 3149.16416058820 },
2959 { 0.00000236117, 5.75503217933, 3333.49887969900 },
2960 { 0.00000274033, 0.13372524985, 3340.67973700260 },
2961 { 0.00000299395, 2.78323740866, 6254.62666252360 }
2962};
2963
2964static const vsop_term_t vsop_lon_Mars_1[] =
2965{
2966 { 3340.61242700512, 0.00000000000, 0.00000000000 },
2967 { 0.01457554523, 3.60433733236, 3340.61242669980 },
2968 { 0.00168414711, 3.92318567804, 6681.22485339960 },
2969 { 0.00020622975, 4.26108844583, 10021.83728009940 },
2970 { 0.00003452392, 4.73210393190, 3.52311834900 },
2971 { 0.00002586332, 4.60670058555, 13362.44970679920 },
2972 { 0.00000841535, 4.45864030426, 2281.23049651060 }
2973};
2974
2975static const vsop_term_t vsop_lon_Mars_2[] =
2976{
2977 { 0.00058152577, 2.04961712429, 3340.61242669980 },
2978 { 0.00013459579, 2.45738706163, 6681.22485339960 }
2979};
2980
2981static const vsop_series_t vsop_lon_Mars[] =
2982{
2983 { 49, vsop_lon_Mars_0 },
2984 { 7, vsop_lon_Mars_1 },
2985 { 2, vsop_lon_Mars_2 }
2986};
2987
2988static const vsop_term_t vsop_lat_Mars_0[] =
2989{
2990 { 0.03197134986, 3.76832042431, 3340.61242669980 },
2991 { 0.00298033234, 4.10616996305, 6681.22485339960 },
2992 { 0.00289104742, 0.00000000000, 0.00000000000 },
2993 { 0.00031365539, 4.44651053090, 10021.83728009940 },
2994 { 0.00003484100, 4.78812549260, 13362.44970679920 }
2995};
2996
2997static const vsop_term_t vsop_lat_Mars_1[] =
2998{
2999 { 0.00217310991, 6.04472194776, 3340.61242669980 },
3000 { 0.00020976948, 3.14159265359, 0.00000000000 },
3001 { 0.00012834709, 1.60810667915, 6681.22485339960 }
3002};
3003
3004static const vsop_series_t vsop_lat_Mars[] =
3005{
3006 { 5, vsop_lat_Mars_0 },
3007 { 3, vsop_lat_Mars_1 }
3008};
3009
3010static const vsop_term_t vsop_rad_Mars_0[] =
3011{
3012 { 1.53033488271, 0.00000000000, 0.00000000000 },
3013 { 0.14184953160, 3.47971283528, 3340.61242669980 },
3014 { 0.00660776362, 3.81783443019, 6681.22485339960 },
3015 { 0.00046179117, 4.15595316782, 10021.83728009940 },
3016 { 0.00008109733, 5.55958416318, 2810.92146160520 },
3017 { 0.00007485318, 1.77239078402, 5621.84292321040 },
3018 { 0.00005523191, 1.36436303770, 2281.23049651060 },
3019 { 0.00003825160, 4.49407183687, 13362.44970679920 },
3020 { 0.00002306537, 0.09081579001, 2544.31441988340 },
3021 { 0.00001999396, 5.36059617709, 3337.08930835080 },
3022 { 0.00002484394, 4.92545639920, 2942.46342329160 },
3023 { 0.00001960195, 4.74249437639, 3344.13554504880 },
3024 { 0.00001167119, 2.11260868341, 5092.15195811580 },
3025 { 0.00001102816, 5.00908403998, 398.14900340820 },
3026 { 0.00000899066, 4.40791133207, 529.69096509460 },
3027 { 0.00000992252, 5.83861961952, 6151.53388830500 },
3028 { 0.00000807354, 2.10217065501, 1059.38193018920 },
3029 { 0.00000797915, 3.44839203899, 796.29800681640 },
3030 { 0.00000740975, 1.49906336885, 2146.16541647520 }
3031};
3032
3033static const vsop_term_t vsop_rad_Mars_1[] =
3034{
3035 { 0.01107433345, 2.03250524857, 3340.61242669980 },
3036 { 0.00103175887, 2.37071847807, 6681.22485339960 },
3037 { 0.00012877200, 0.00000000000, 0.00000000000 },
3038 { 0.00010815880, 2.70888095665, 10021.83728009940 }
3039};
3040
3041static const vsop_term_t vsop_rad_Mars_2[] =
3042{
3043 { 0.00044242249, 0.47930604954, 3340.61242669980 },
3044 { 0.00008138042, 0.86998389204, 6681.22485339960 }
3045};
3046
3047static const vsop_series_t vsop_rad_Mars[] =
3048{
3049 { 19, vsop_rad_Mars_0 },
3050 { 4, vsop_rad_Mars_1 },
3051 { 2, vsop_rad_Mars_2 }
3052};
3053
3054;
3055static const vsop_term_t vsop_lon_Jupiter_0[] =
3056{
3057 { 0.59954691494, 0.00000000000, 0.00000000000 },
3058 { 0.09695898719, 5.06191793158, 529.69096509460 },
3059 { 0.00573610142, 1.44406205629, 7.11354700080 },
3060 { 0.00306389205, 5.41734730184, 1059.38193018920 },
3061 { 0.00097178296, 4.14264726552, 632.78373931320 },
3062 { 0.00072903078, 3.64042916389, 522.57741809380 },
3063 { 0.00064263975, 3.41145165351, 103.09277421860 },
3064 { 0.00039806064, 2.29376740788, 419.48464387520 },
3065 { 0.00038857767, 1.27231755835, 316.39186965660 },
3066 { 0.00027964629, 1.78454591820, 536.80451209540 },
3067 { 0.00013589730, 5.77481040790, 1589.07289528380 },
3068 { 0.00008246349, 3.58227925840, 206.18554843720 },
3069 { 0.00008768704, 3.63000308199, 949.17560896980 },
3070 { 0.00007368042, 5.08101194270, 735.87651353180 },
3071 { 0.00006263150, 0.02497628807, 213.29909543800 },
3072 { 0.00006114062, 4.51319998626, 1162.47470440780 },
3073 { 0.00004905396, 1.32084470588, 110.20632121940 },
3074 { 0.00005305285, 1.30671216791, 14.22709400160 },
3075 { 0.00005305441, 4.18625634012, 1052.26838318840 },
3076 { 0.00004647248, 4.69958103684, 3.93215326310 },
3077 { 0.00003045023, 4.31676431084, 426.59819087600 },
3078 { 0.00002609999, 1.56667394063, 846.08283475120 },
3079 { 0.00002028191, 1.06376530715, 3.18139373770 },
3080 { 0.00001764763, 2.14148655117, 1066.49547719000 },
3081 { 0.00001722972, 3.88036268267, 1265.56747862640 },
3082 { 0.00001920945, 0.97168196472, 639.89728631400 },
3083 { 0.00001633223, 3.58201833555, 515.46387109300 },
3084 { 0.00001431999, 4.29685556046, 625.67019231240 },
3085 { 0.00000973272, 4.09764549134, 95.97922721780 }
3086};
3087
3088static const vsop_term_t vsop_lon_Jupiter_1[] =
3089{
3090 { 529.69096508814, 0.00000000000, 0.00000000000 },
3091 { 0.00489503243, 4.22082939470, 529.69096509460 },
3092 { 0.00228917222, 6.02646855621, 7.11354700080 },
3093 { 0.00030099479, 4.54540782858, 1059.38193018920 },
3094 { 0.00020720920, 5.45943156902, 522.57741809380 },
3095 { 0.00012103653, 0.16994816098, 536.80451209540 },
3096 { 0.00006067987, 4.42422292017, 103.09277421860 },
3097 { 0.00005433968, 3.98480737746, 419.48464387520 },
3098 { 0.00004237744, 5.89008707199, 14.22709400160 }
3099};
3100
3101static const vsop_term_t vsop_lon_Jupiter_2[] =
3102{
3103 { 0.00047233601, 4.32148536482, 7.11354700080 },
3104 { 0.00030649436, 2.92977788700, 529.69096509460 },
3105 { 0.00014837605, 3.14159265359, 0.00000000000 }
3106};
3107
3108static const vsop_series_t vsop_lon_Jupiter[] =
3109{
3110 { 29, vsop_lon_Jupiter_0 },
3111 { 9, vsop_lon_Jupiter_1 },
3112 { 3, vsop_lon_Jupiter_2 }
3113};
3114
3115static const vsop_term_t vsop_lat_Jupiter_0[] =
3116{
3117 { 0.02268615702, 3.55852606721, 529.69096509460 },
3118 { 0.00109971634, 3.90809347197, 1059.38193018920 },
3119 { 0.00110090358, 0.00000000000, 0.00000000000 },
3120 { 0.00008101428, 3.60509572885, 522.57741809380 },
3121 { 0.00006043996, 4.25883108339, 1589.07289528380 },
3122 { 0.00006437782, 0.30627119215, 536.80451209540 }
3123};
3124
3125static const vsop_term_t vsop_lat_Jupiter_1[] =
3126{
3127 { 0.00078203446, 1.52377859742, 529.69096509460 }
3128};
3129
3130static const vsop_series_t vsop_lat_Jupiter[] =
3131{
3132 { 6, vsop_lat_Jupiter_0 },
3133 { 1, vsop_lat_Jupiter_1 }
3134};
3135
3136static const vsop_term_t vsop_rad_Jupiter_0[] =
3137{
3138 { 5.20887429326, 0.00000000000, 0.00000000000 },
3139 { 0.25209327119, 3.49108639871, 529.69096509460 },
3140 { 0.00610599976, 3.84115365948, 1059.38193018920 },
3141 { 0.00282029458, 2.57419881293, 632.78373931320 },
3142 { 0.00187647346, 2.07590383214, 522.57741809380 },
3143 { 0.00086792905, 0.71001145545, 419.48464387520 },
3144 { 0.00072062974, 0.21465724607, 536.80451209540 },
3145 { 0.00065517248, 5.97995884790, 316.39186965660 },
3146 { 0.00029134542, 1.67759379655, 103.09277421860 },
3147 { 0.00030135335, 2.16132003734, 949.17560896980 },
3148 { 0.00023453271, 3.54023522184, 735.87651353180 },
3149 { 0.00022283743, 4.19362594399, 1589.07289528380 },
3150 { 0.00023947298, 0.27458037480, 7.11354700080 },
3151 { 0.00013032614, 2.96042965363, 1162.47470440780 },
3152 { 0.00009703360, 1.90669633585, 206.18554843720 },
3153 { 0.00012749023, 2.71550286592, 1052.26838318840 },
3154 { 0.00007057931, 2.18184839926, 1265.56747862640 },
3155 { 0.00006137703, 6.26418240033, 846.08283475120 },
3156 { 0.00002616976, 2.00994012876, 1581.95934828300 }
3157};
3158
3159static const vsop_term_t vsop_rad_Jupiter_1[] =
3160{
3161 { 0.01271801520, 2.64937512894, 529.69096509460 },
3162 { 0.00061661816, 3.00076460387, 1059.38193018920 },
3163 { 0.00053443713, 3.89717383175, 522.57741809380 },
3164 { 0.00031185171, 4.88276958012, 536.80451209540 },
3165 { 0.00041390269, 0.00000000000, 0.00000000000 }
3166};
3167
3168static const vsop_series_t vsop_rad_Jupiter[] =
3169{
3170 { 19, vsop_rad_Jupiter_0 },
3171 { 5, vsop_rad_Jupiter_1 }
3172};
3173
3174;
3175static const vsop_term_t vsop_lon_Saturn_0[] =
3176{
3177 { 0.87401354025, 0.00000000000, 0.00000000000 },
3178 { 0.11107659762, 3.96205090159, 213.29909543800 },
3179 { 0.01414150957, 4.58581516874, 7.11354700080 },
3180 { 0.00398379389, 0.52112032699, 206.18554843720 },
3181 { 0.00350769243, 3.30329907896, 426.59819087600 },
3182 { 0.00206816305, 0.24658372002, 103.09277421860 },
3183 { 0.00079271300, 3.84007056878, 220.41264243880 },
3184 { 0.00023990355, 4.66976924553, 110.20632121940 },
3185 { 0.00016573588, 0.43719228296, 419.48464387520 },
3186 { 0.00014906995, 5.76903183869, 316.39186965660 },
3187 { 0.00015820290, 0.93809155235, 632.78373931320 },
3188 { 0.00014609559, 1.56518472000, 3.93215326310 },
3189 { 0.00013160301, 4.44891291899, 14.22709400160 },
3190 { 0.00015053543, 2.71669915667, 639.89728631400 },
3191 { 0.00013005299, 5.98119023644, 11.04570026390 },
3192 { 0.00010725067, 3.12939523827, 202.25339517410 },
3193 { 0.00005863206, 0.23656938524, 529.69096509460 },
3194 { 0.00005227757, 4.20783365759, 3.18139373770 },
3195 { 0.00006126317, 1.76328667907, 277.03499374140 },
3196 { 0.00005019687, 3.17787728405, 433.71173787680 },
3197 { 0.00004592550, 0.61977744975, 199.07200143640 },
3198 { 0.00004005867, 2.24479718502, 63.73589830340 },
3199 { 0.00002953796, 0.98280366998, 95.97922721780 },
3200 { 0.00003873670, 3.22283226966, 138.51749687070 },
3201 { 0.00002461186, 2.03163875071, 735.87651353180 },
3202 { 0.00003269484, 0.77492638211, 949.17560896980 },
3203 { 0.00001758145, 3.26580109940, 522.57741809380 },
3204 { 0.00001640172, 5.50504453050, 846.08283475120 },
3205 { 0.00001391327, 4.02333150505, 323.50541665740 },
3206 { 0.00001580648, 4.37265307169, 309.27832265580 },
3207 { 0.00001123498, 2.83726798446, 415.55249061210 },
3208 { 0.00001017275, 3.71700135395, 227.52618943960 },
3209 { 0.00000848642, 3.19150170830, 209.36694217490 }
3210};
3211
3212static const vsop_term_t vsop_lon_Saturn_1[] =
3213{
3214 { 213.29909521690, 0.00000000000, 0.00000000000 },
3215 { 0.01297370862, 1.82834923978, 213.29909543800 },
3216 { 0.00564345393, 2.88499717272, 7.11354700080 },
3217 { 0.00093734369, 1.06311793502, 426.59819087600 },
3218 { 0.00107674962, 2.27769131009, 206.18554843720 },
3219 { 0.00040244455, 2.04108104671, 220.41264243880 },
3220 { 0.00019941774, 1.27954390470, 103.09277421860 },
3221 { 0.00010511678, 2.74880342130, 14.22709400160 },
3222 { 0.00006416106, 0.38238295041, 639.89728631400 },
3223 { 0.00004848994, 2.43037610229, 419.48464387520 },
3224 { 0.00004056892, 2.92133209468, 110.20632121940 },
3225 { 0.00003768635, 3.64965330780, 3.93215326310 }
3226};
3227
3228static const vsop_term_t vsop_lon_Saturn_2[] =
3229{
3230 { 0.00116441330, 1.17988132879, 7.11354700080 },
3231 { 0.00091841837, 0.07325195840, 213.29909543800 },
3232 { 0.00036661728, 0.00000000000, 0.00000000000 },
3233 { 0.00015274496, 4.06493179167, 206.18554843720 }
3234};
3235
3236static const vsop_series_t vsop_lon_Saturn[] =
3237{
3238 { 33, vsop_lon_Saturn_0 },
3239 { 12, vsop_lon_Saturn_1 },
3240 { 4, vsop_lon_Saturn_2 }
3241};
3242
3243static const vsop_term_t vsop_lat_Saturn_0[] =
3244{
3245 { 0.04330678039, 3.60284428399, 213.29909543800 },
3246 { 0.00240348302, 2.85238489373, 426.59819087600 },
3247 { 0.00084745939, 0.00000000000, 0.00000000000 },
3248 { 0.00030863357, 3.48441504555, 220.41264243880 },
3249 { 0.00034116062, 0.57297307557, 206.18554843720 },
3250 { 0.00014734070, 2.11846596715, 639.89728631400 },
3251 { 0.00009916667, 5.79003188904, 419.48464387520 },
3252 { 0.00006993564, 4.73604689720, 7.11354700080 },
3253 { 0.00004807588, 5.43305312061, 316.39186965660 }
3254};
3255
3256static const vsop_term_t vsop_lat_Saturn_1[] =
3257{
3258 { 0.00198927992, 4.93901017903, 213.29909543800 },
3259 { 0.00036947916, 3.14159265359, 0.00000000000 },
3260 { 0.00017966989, 0.51979431110, 426.59819087600 }
3261};
3262
3263static const vsop_series_t vsop_lat_Saturn[] =
3264{
3265 { 9, vsop_lat_Saturn_0 },
3266 { 3, vsop_lat_Saturn_1 }
3267};
3268
3269static const vsop_term_t vsop_rad_Saturn_0[] =
3270{
3271 { 9.55758135486, 0.00000000000, 0.00000000000 },
3272 { 0.52921382865, 2.39226219573, 213.29909543800 },
3273 { 0.01873679867, 5.23549604660, 206.18554843720 },
3274 { 0.01464663929, 1.64763042902, 426.59819087600 },
3275 { 0.00821891141, 5.93520042303, 316.39186965660 },
3276 { 0.00547506923, 5.01532618980, 103.09277421860 },
3277 { 0.00371684650, 2.27114821115, 220.41264243880 },
3278 { 0.00361778765, 3.13904301847, 7.11354700080 },
3279 { 0.00140617506, 5.70406606781, 632.78373931320 },
3280 { 0.00108974848, 3.29313390175, 110.20632121940 },
3281 { 0.00069006962, 5.94099540992, 419.48464387520 },
3282 { 0.00061053367, 0.94037691801, 639.89728631400 },
3283 { 0.00048913294, 1.55733638681, 202.25339517410 },
3284 { 0.00034143772, 0.19519102597, 277.03499374140 },
3285 { 0.00032401773, 5.47084567016, 949.17560896980 },
3286 { 0.00020936596, 0.46349251129, 735.87651353180 },
3287 { 0.00009796004, 5.20477537945, 1265.56747862640 },
3288 { 0.00011993338, 5.98050967385, 846.08283475120 },
3289 { 0.00020839300, 1.52102476129, 433.71173787680 },
3290 { 0.00015298404, 3.05943814940, 529.69096509460 },
3291 { 0.00006465823, 0.17732249942, 1052.26838318840 },
3292 { 0.00011380257, 1.73105427040, 522.57741809380 },
3293 { 0.00003419618, 4.94550542171, 1581.95934828300 }
3294};
3295
3296static const vsop_term_t vsop_rad_Saturn_1[] =
3297{
3298 { 0.06182981340, 0.25843511480, 213.29909543800 },
3299 { 0.00506577242, 0.71114625261, 206.18554843720 },
3300 { 0.00341394029, 5.79635741658, 426.59819087600 },
3301 { 0.00188491195, 0.47215589652, 220.41264243880 },
3302 { 0.00186261486, 3.14159265359, 0.00000000000 },
3303 { 0.00143891146, 1.40744822888, 7.11354700080 }
3304};
3305
3306static const vsop_term_t vsop_rad_Saturn_2[] =
3307{
3308 { 0.00436902572, 4.78671677509, 213.29909543800 }
3309};
3310
3311static const vsop_series_t vsop_rad_Saturn[] =
3312{
3313 { 23, vsop_rad_Saturn_0 },
3314 { 6, vsop_rad_Saturn_1 },
3315 { 1, vsop_rad_Saturn_2 }
3316};
3317
3318;
3319static const vsop_term_t vsop_lon_Uranus_0[] =
3320{
3321 { 5.48129294297, 0.00000000000, 0.00000000000 },
3322 { 0.09260408234, 0.89106421507, 74.78159856730 },
3323 { 0.01504247898, 3.62719260920, 1.48447270830 },
3324 { 0.00365981674, 1.89962179044, 73.29712585900 },
3325 { 0.00272328168, 3.35823706307, 149.56319713460 },
3326 { 0.00070328461, 5.39254450063, 63.73589830340 },
3327 { 0.00068892678, 6.09292483287, 76.26607127560 },
3328 { 0.00061998615, 2.26952066061, 2.96894541660 },
3329 { 0.00061950719, 2.85098872691, 11.04570026390 },
3330 { 0.00026468770, 3.14152083966, 71.81265315070 },
3331 { 0.00025710476, 6.11379840493, 454.90936652730 },
3332 { 0.00021078850, 4.36059339067, 148.07872442630 },
3333 { 0.00017818647, 1.74436930289, 36.64856292950 },
3334 { 0.00014613507, 4.73732166022, 3.93215326310 },
3335 { 0.00011162509, 5.82681796350, 224.34479570190 },
3336 { 0.00010997910, 0.48865004018, 138.51749687070 },
3337 { 0.00009527478, 2.95516862826, 35.16409022120 },
3338 { 0.00007545601, 5.23626582400, 109.94568878850 },
3339 { 0.00004220241, 3.23328220918, 70.84944530420 },
3340 { 0.00004051900, 2.27755017300, 151.04766984290 },
3341 { 0.00003354596, 1.06549007380, 4.45341812490 },
3342 { 0.00002926718, 4.62903718891, 9.56122755560 },
3343 { 0.00003490340, 5.48306144511, 146.59425171800 },
3344 { 0.00003144069, 4.75199570434, 77.75054398390 },
3345 { 0.00002922333, 5.35235361027, 85.82729883120 },
3346 { 0.00002272788, 4.36600400036, 70.32818044240 },
3347 { 0.00002051219, 1.51773566586, 0.11187458460 },
3348 { 0.00002148602, 0.60745949945, 38.13303563780 },
3349 { 0.00001991643, 4.92437588682, 277.03499374140 },
3350 { 0.00001376226, 2.04283539351, 65.22037101170 },
3351 { 0.00001666902, 3.62744066769, 380.12776796000 },
3352 { 0.00001284107, 3.11347961505, 202.25339517410 },
3353 { 0.00001150429, 0.93343589092, 3.18139373770 },
3354 { 0.00001533221, 2.58594681212, 52.69019803950 },
3355 { 0.00001281604, 0.54271272721, 222.86032299360 },
3356 { 0.00001372139, 4.19641530878, 111.43016149680 },
3357 { 0.00001221029, 0.19900650030, 108.46121608020 },
3358 { 0.00000946181, 1.19253165736, 127.47179660680 },
3359 { 0.00001150989, 4.17898916639, 33.67961751290 }
3360};
3361
3362static const vsop_term_t vsop_lon_Uranus_1[] =
3363{
3364 { 74.78159860910, 0.00000000000, 0.00000000000 },
3365 { 0.00154332863, 5.24158770553, 74.78159856730 },
3366 { 0.00024456474, 1.71260334156, 1.48447270830 },
3367 { 0.00009258442, 0.42829732350, 11.04570026390 },
3368 { 0.00008265977, 1.50218091379, 63.73589830340 },
3369 { 0.00009150160, 1.41213765216, 149.56319713460 }
3370};
3371
3372static const vsop_series_t vsop_lon_Uranus[] =
3373{
3374 { 39, vsop_lon_Uranus_0 },
3375 { 6, vsop_lon_Uranus_1 }
3376};
3377
3378static const vsop_term_t vsop_lat_Uranus_0[] =
3379{
3380 { 0.01346277648, 2.61877810547, 74.78159856730 },
3381 { 0.00062341400, 5.08111189648, 149.56319713460 },
3382 { 0.00061601196, 3.14159265359, 0.00000000000 },
3383 { 0.00009963722, 1.61603805646, 76.26607127560 },
3384 { 0.00009926160, 0.57630380333, 73.29712585900 }
3385};
3386
3387static const vsop_term_t vsop_lat_Uranus_1[] =
3388{
3389 { 0.00034101978, 0.01321929936, 74.78159856730 }
3390};
3391
3392static const vsop_series_t vsop_lat_Uranus[] =
3393{
3394 { 5, vsop_lat_Uranus_0 },
3395 { 1, vsop_lat_Uranus_1 }
3396};
3397
3398static const vsop_term_t vsop_rad_Uranus_0[] =
3399{
3400 { 19.21264847206, 0.00000000000, 0.00000000000 },
3401 { 0.88784984413, 5.60377527014, 74.78159856730 },
3402 { 0.03440836062, 0.32836099706, 73.29712585900 },
3403 { 0.02055653860, 1.78295159330, 149.56319713460 },
3404 { 0.00649322410, 4.52247285911, 76.26607127560 },
3405 { 0.00602247865, 3.86003823674, 63.73589830340 },
3406 { 0.00496404167, 1.40139935333, 454.90936652730 },
3407 { 0.00338525369, 1.58002770318, 138.51749687070 },
3408 { 0.00243509114, 1.57086606044, 71.81265315070 },
3409 { 0.00190522303, 1.99809394714, 1.48447270830 },
3410 { 0.00161858838, 2.79137786799, 148.07872442630 },
3411 { 0.00143706183, 1.38368544947, 11.04570026390 },
3412 { 0.00093192405, 0.17437220467, 36.64856292950 },
3413 { 0.00071424548, 4.24509236074, 224.34479570190 },
3414 { 0.00089806014, 3.66105364565, 109.94568878850 },
3415 { 0.00039009723, 1.66971401684, 70.84944530420 },
3416 { 0.00046677296, 1.39976401694, 35.16409022120 },
3417 { 0.00039025624, 3.36234773834, 277.03499374140 },
3418 { 0.00036755274, 3.88649278513, 146.59425171800 },
3419 { 0.00030348723, 0.70100838798, 151.04766984290 },
3420 { 0.00029156413, 3.18056336700, 77.75054398390 },
3421 { 0.00022637073, 0.72518687029, 529.69096509460 },
3422 { 0.00011959076, 1.75043392140, 984.60033162190 },
3423 { 0.00025620756, 5.25656086672, 380.12776796000 }
3424};
3425
3426static const vsop_term_t vsop_rad_Uranus_1[] =
3427{
3428 { 0.01479896629, 3.67205697578, 74.78159856730 }
3429};
3430
3431static const vsop_series_t vsop_rad_Uranus[] =
3432{
3433 { 24, vsop_rad_Uranus_0 },
3434 { 1, vsop_rad_Uranus_1 }
3435};
3436
3437;
3438static const vsop_term_t vsop_lon_Neptune_0[] =
3439{
3440 { 5.31188633046, 0.00000000000, 0.00000000000 },
3441 { 0.01798475530, 2.90101273890, 38.13303563780 },
3442 { 0.01019727652, 0.48580922867, 1.48447270830 },
3443 { 0.00124531845, 4.83008090676, 36.64856292950 },
3444 { 0.00042064466, 5.41054993053, 2.96894541660 },
3445 { 0.00037714584, 6.09221808686, 35.16409022120 },
3446 { 0.00033784738, 1.24488874087, 76.26607127560 },
3447 { 0.00016482741, 0.00007727998, 491.55792945680 },
3448 { 0.00009198584, 4.93747051954, 39.61750834610 },
3449 { 0.00008994250, 0.27462171806, 175.16605980020 }
3450};
3451
3452static const vsop_term_t vsop_lon_Neptune_1[] =
3453{
3454 { 38.13303563957, 0.00000000000, 0.00000000000 },
3455 { 0.00016604172, 4.86323329249, 1.48447270830 },
3456 { 0.00015744045, 2.27887427527, 38.13303563780 }
3457};
3458
3459static const vsop_series_t vsop_lon_Neptune[] =
3460{
3461 { 10, vsop_lon_Neptune_0 },
3462 { 3, vsop_lon_Neptune_1 }
3463};
3464
3465static const vsop_term_t vsop_lat_Neptune_0[] =
3466{
3467 { 0.03088622933, 1.44104372644, 38.13303563780 },
3468 { 0.00027780087, 5.91271884599, 76.26607127560 },
3469 { 0.00027623609, 0.00000000000, 0.00000000000 },
3470 { 0.00015355489, 2.52123799551, 36.64856292950 },
3471 { 0.00015448133, 3.50877079215, 39.61750834610 }
3472};
3473
3474static const vsop_series_t vsop_lat_Neptune[] =
3475{
3476 { 5, vsop_lat_Neptune_0 }
3477};
3478
3479static const vsop_term_t vsop_rad_Neptune_0[] =
3480{
3481 { 30.07013205828, 0.00000000000, 0.00000000000 },
3482 { 0.27062259632, 1.32999459377, 38.13303563780 },
3483 { 0.01691764014, 3.25186135653, 36.64856292950 },
3484 { 0.00807830553, 5.18592878704, 1.48447270830 },
3485 { 0.00537760510, 4.52113935896, 35.16409022120 },
3486 { 0.00495725141, 1.57105641650, 491.55792945680 },
3487 { 0.00274571975, 1.84552258866, 175.16605980020 },
3488 { 0.00012012320, 1.92059384991, 1021.24889455140 },
3489 { 0.00121801746, 5.79754470298, 76.26607127560 },
3490 { 0.00100896068, 0.37702724930, 73.29712585900 },
3491 { 0.00135134092, 3.37220609835, 39.61750834610 },
3492 { 0.00007571796, 1.07149207335, 388.46515523820 }
3493};
3494
3495static const vsop_series_t vsop_rad_Neptune[] =
3496{
3497 { 12, vsop_rad_Neptune_0 }
3498};
3499
3500;
3501
3502/** @cond DOXYGEN_SKIP */
3503#define VSOPFORMULA(x) { ASTRO_ARRAYSIZE(x), x }
3504/** @endcond */
3505
3506static const vsop_model_t vsop[] =
3507{
3508 { { VSOPFORMULA(vsop_lon_Mercury), VSOPFORMULA(vsop_lat_Mercury), VSOPFORMULA(vsop_rad_Mercury) } },
3509 { { VSOPFORMULA(vsop_lon_Venus), VSOPFORMULA(vsop_lat_Venus), VSOPFORMULA(vsop_rad_Venus) } },
3510 { { VSOPFORMULA(vsop_lon_Earth), VSOPFORMULA(vsop_lat_Earth), VSOPFORMULA(vsop_rad_Earth) } },
3511 { { VSOPFORMULA(vsop_lon_Mars), VSOPFORMULA(vsop_lat_Mars), VSOPFORMULA(vsop_rad_Mars) } },
3512 { { VSOPFORMULA(vsop_lon_Jupiter), VSOPFORMULA(vsop_lat_Jupiter), VSOPFORMULA(vsop_rad_Jupiter) } },
3513 { { VSOPFORMULA(vsop_lon_Saturn), VSOPFORMULA(vsop_lat_Saturn), VSOPFORMULA(vsop_rad_Saturn) } },
3514 { { VSOPFORMULA(vsop_lon_Uranus), VSOPFORMULA(vsop_lat_Uranus), VSOPFORMULA(vsop_rad_Uranus) } },
3515 { { VSOPFORMULA(vsop_lon_Neptune), VSOPFORMULA(vsop_lat_Neptune), VSOPFORMULA(vsop_rad_Neptune) } }
3516};
3517
3518/** @cond DOXYGEN_SKIP */
3519#define CalcEarth(time) CalcVsop(&vsop[BODY_EARTH], (time))
3520#define LON_INDEX 0
3521#define LAT_INDEX 1
3522#define RAD_INDEX 2
3523/** @endcond */
3524
3525static void VsopCoords(const vsop_model_t *model, double t, double sphere[3])
3526{
3527 int k, s, i;
3528 double incr;
3529
3530 for (k=0; k < 3; ++k)
3531 {
3532 double tpower = 1.0;
3533 const vsop_formula_t *formula = &model->formula[k];
3534 sphere[k] = 0.0;
3535 for (s=0; s < formula->nseries; ++s)
3536 {
3537 double sum = 0.0;
3538 const vsop_series_t *series = &formula->series[s];
3539 for (i=0; i < series->nterms; ++i)
3540 {
3541 const vsop_term_t *term = &series->term[i];
3542 sum += term->amplitude * cos(term->phase + (t * term->frequency));
3543 }
3544 incr = tpower * sum;
3545 if (k == LON_INDEX)
3546 incr = fmod(incr, PI2); /* improve precision for longitudes, which can be hundreds of radians */
3547 sphere[k] += incr;
3548 tpower *= t;
3549 }
3550 }
3551}
3552
3553
3554static terse_vector_t VsopRotate(const double ecl[3])
3555{
3556 terse_vector_t equ;
3557
3558 /*
3559 X +1.000000000000 +0.000000440360 -0.000000190919 X
3560 Y = -0.000000479966 +0.917482137087 -0.397776982902 Y
3561 Z FK5 0.000000000000 +0.397776982902 +0.917482137087 Z VSOP87A
3562 */
3563
3564 equ.x = ecl[0] + 0.000000440360*ecl[1] - 0.000000190919*ecl[2];
3565 equ.y = -0.000000479966*ecl[0] + 0.917482137087*ecl[1] - 0.397776982902*ecl[2];
3566 equ.z = 0.397776982902*ecl[1] + 0.917482137087*ecl[2];
3567
3568 return equ;
3569}
3570
3571
3572static void VsopSphereToRect(double lon, double lat, double radius, double pos[3])
3573{
3574 double r_coslat = radius * cos(lat);
3575 double coslon = cos(lon);
3576 double sinlon = sin(lon);
3577 pos[0] = r_coslat * coslon;
3578 pos[1] = r_coslat * sinlon;
3579 pos[2] = radius * sin(lat);
3580}
3581
3582static const double DAYS_PER_MILLENNIUM = 365250.0;
3583
3584
3585static astro_vector_t CalcVsop(const vsop_model_t *model, astro_time_t time)
3586{
3587 double t = time.tt / DAYS_PER_MILLENNIUM;
3588 double sphere[3]; /* lon, lat, rad */
3589 double eclip[3];
3590 astro_vector_t vector;
3591 terse_vector_t pos;
3592
3593 /* Calculate the VSOP "B" trigonometric series to obtain ecliptic spherical coordinates. */
3594 VsopCoords(model, t, sphere);
3595
3596 /* Convert ecliptic spherical coordinates to ecliptic Cartesian coordinates. */
3597 VsopSphereToRect(sphere[LON_INDEX], sphere[LAT_INDEX], sphere[RAD_INDEX], eclip);
3598
3599 /* Convert ecliptic Cartesian coordinates to equatorial Cartesian coordinates. */
3600 pos = VsopRotate(eclip);
3601
3602 /* Package the position as astro_vector_t. */
3603 vector.status = ASTRO_SUCCESS;
3604 vector.t = time;
3605 vector.x = pos.x;
3606 vector.y = pos.y;
3607 vector.z = pos.z;
3608
3609 return vector;
3610}
3611
3612
3613static void VsopDeriv(const vsop_model_t *model, double t, double deriv[3])
3614{
3615 int k, s, i;
3616
3617 for (k=0; k < 3; ++k)
3618 {
3619 double tpower = 1.0; /* t^s */
3620 double dpower = 0.0; /* t^(s-1) */
3621 const vsop_formula_t *formula = &model->formula[k];
3622 deriv[k] = 0.0;
3623 for (s=0; s < formula->nseries; ++s)
3624 {
3625 double sin_sum = 0.0;
3626 double cos_sum = 0.0;
3627 const vsop_series_t *series = &formula->series[s];
3628 for (i=0; i < series->nterms; ++i)
3629 {
3630 const vsop_term_t *term = &series->term[i];
3631 double angle = term->phase + (t * term->frequency);
3632 sin_sum += term->amplitude * term->frequency * sin(angle);
3633 if (s > 0)
3634 cos_sum += term->amplitude * cos(angle);
3635 }
3636 deriv[k] += (s * dpower * cos_sum) - (tpower * sin_sum);
3637 dpower = tpower;
3638 tpower *= t;
3639 }
3640 }
3641}
3642
3643
3644static body_state_t CalcVsopPosVel(const vsop_model_t *model, double tt)
3645{
3646 body_state_t state;
3647 double t = tt / DAYS_PER_MILLENNIUM;
3648 double sphere[3]; /* lon, lat, r */
3649 double deriv[3]; /* d(lon)/dt, d(lat)/dt, dr/dt */
3650 double eclip[3];
3651 double dr_dt, dlat_dt, dlon_dt;
3652 double r, coslat, coslon, sinlat, sinlon;
3653
3654 state.tt = tt;
3655 VsopCoords(model, t, sphere);
3656 VsopSphereToRect(sphere[LON_INDEX], sphere[LAT_INDEX], sphere[RAD_INDEX], eclip);
3657 state.r = VsopRotate(eclip);
3658
3659 VsopDeriv(model, t, deriv);
3660
3661 /* Use spherical coords and spherical derivatives to calculate */
3662 /* the velocity vector in rectangular coordinates. */
3663
3664 /* Calculate mnemonic variables to help keep the math straight. */
3665 coslon = cos(sphere[LON_INDEX]);
3666 sinlon = sin(sphere[LON_INDEX]);
3667 coslat = cos(sphere[LAT_INDEX]);
3668 sinlat = sin(sphere[LAT_INDEX]);
3669 r = sphere[RAD_INDEX];
3670 dlon_dt = deriv[LON_INDEX];
3671 dlat_dt = deriv[LAT_INDEX];
3672 dr_dt = deriv[RAD_INDEX];
3673
3674 /* vx = dx/dt */
3675 eclip[0] = (dr_dt * coslat * coslon) - (r * sinlat * coslon * dlat_dt) - (r * coslat * sinlon * dlon_dt);
3676
3677 /* vy = dy/dt */
3678 eclip[1] = (dr_dt * coslat * sinlon) - (r * sinlat * sinlon * dlat_dt) + (r * coslat * coslon * dlon_dt);
3679
3680 /* vz = dz/dt */
3681 eclip[2] = (dr_dt * sinlat) + (r * coslat * dlat_dt);
3682
3683 /* Rotate the velocity vector from ecliptic to equatorial coordinates. */
3684 state.v = VsopRotate(eclip);
3685
3686 /* Convert speed units from [AU/millennium] to [AU/day]. */
3687 VecScale(&state.v, 1 / DAYS_PER_MILLENNIUM);
3688
3689 return state;
3690}
3691
3692
3693static double VsopHelioDistance(const vsop_model_t *model, astro_time_t time)
3694{
3695 int s, i;
3696 double t = time.tt / DAYS_PER_MILLENNIUM;
3697 double distance = 0.0;
3698 double tpower = 1.0;
3699 const vsop_formula_t *formula = &model->formula[2]; /* [2] is the distance part of the formula */
3700
3701 /*
3702 The caller only wants to know the distance between the planet and the Sun.
3703 So we only need to calculate the radial component of the spherical coordinates.
3704 */
3705
3706 for (s=0; s < formula->nseries; ++s)
3707 {
3708 double sum = 0.0;
3709 const vsop_series_t *series = &formula->series[s];
3710 for (i=0; i < series->nterms; ++i)
3711 {
3712 const vsop_term_t *term = &series->term[i];
3713 sum += term->amplitude * cos(term->phase + (t * term->frequency));
3714 }
3715 distance += tpower * sum;
3716 tpower *= t;
3717 }
3718
3719 return distance;
3720}
3721
3722
3723static void AdjustBarycenter(astro_vector_t *ssb, astro_time_t time, astro_body_t body, double planet_gm)
3724{
3725 astro_vector_t planet;
3726 double shift;
3727
3728 shift = planet_gm / (planet_gm + SUN_GM);
3729 planet = CalcVsop(&vsop[body], time);
3730 ssb->x += shift * planet.x;
3731 ssb->y += shift * planet.y;
3732 ssb->z += shift * planet.z;
3733}
3734
3735
3736static astro_vector_t CalcSolarSystemBarycenter(astro_time_t time)
3737{
3738 astro_vector_t ssb;
3739
3740 ssb.status = ASTRO_SUCCESS;
3741 ssb.t = time;
3742 ssb.x = ssb.y = ssb.z = 0.0;
3743
3744 AdjustBarycenter(&ssb, time, BODY_JUPITER, JUPITER_GM);
3745 AdjustBarycenter(&ssb, time, BODY_SATURN, SATURN_GM);
3746 AdjustBarycenter(&ssb, time, BODY_URANUS, URANUS_GM);
3747 AdjustBarycenter(&ssb, time, BODY_NEPTUNE, NEPTUNE_GM);
3748
3749 return ssb;
3750}
3751
3752/*------------------ begin general gravity simulator ------------------*/
3753
3754static terse_vector_t UpdatePosition(double dt, terse_vector_t r, terse_vector_t v, terse_vector_t a)
3755{
3756 r.x += (v.x + a.x*dt/2) * dt;
3757 r.y += (v.y + a.y*dt/2) * dt;
3758 r.z += (v.z + a.z*dt/2) * dt;
3759 return r;
3760}
3761
3762
3763static terse_vector_t UpdateVelocity(double dt, terse_vector_t v, terse_vector_t a)
3764{
3765 v.x += dt * a.x;
3766 v.y += dt * a.y;
3767 v.z += dt * a.z;
3768 return v;
3769}
3770
3771
3772static body_state_t AdjustBarycenterPosVel(body_state_t *ssb, double tt, astro_body_t body, double planet_gm)
3773{
3774 body_state_t planet;
3775 double shift;
3776
3777 /*
3778 This function does 2 important things:
3779 1. Adjusts 'ssb' by the effect of one major body on the Solar System Barycenter.
3780 2, Returns the heliocentric position of that major body.
3781 */
3782
3783 shift = planet_gm / (planet_gm + SUN_GM);
3784 planet = CalcVsopPosVel(&vsop[body], tt);
3785 VecIncr(&ssb->r, VecMul(shift, planet.r));
3786 VecIncr(&ssb->v, VecMul(shift, planet.v));
3787
3788 return planet;
3789}
3790
3791
3792static void MajorBodyBary(major_bodies_t *bary, double tt)
3793{
3794 /* Initialize the Sun's position as a zero vector, then adjust it from pulls from the planets. */
3795 bary->Sun.tt = tt;
3796 bary->Sun.r = VecZero;
3797 bary->Sun.v = VecZero;
3798
3799 /* Calculate heliocentric planet positions and SSB. */
3800 bary->Jupiter = AdjustBarycenterPosVel(&bary->Sun, tt, BODY_JUPITER, JUPITER_GM);
3801 bary->Saturn = AdjustBarycenterPosVel(&bary->Sun, tt, BODY_SATURN, SATURN_GM);
3802 bary->Uranus = AdjustBarycenterPosVel(&bary->Sun, tt, BODY_URANUS, URANUS_GM);
3803 bary->Neptune = AdjustBarycenterPosVel(&bary->Sun, tt, BODY_NEPTUNE, NEPTUNE_GM);
3804
3805 /* Convert planet [pos, vel] from heliocentric to barycentric. */
3806 VecDecr(&bary->Jupiter.r, bary->Sun.r); VecDecr(&bary->Jupiter.v, bary->Sun.v);
3807 VecDecr(&bary->Saturn.r, bary->Sun.r); VecDecr(&bary->Saturn.v, bary->Sun.v);
3808 VecDecr(&bary->Uranus.r, bary->Sun.r); VecDecr(&bary->Uranus.v, bary->Sun.v);
3809 VecDecr(&bary->Neptune.r, bary->Sun.r); VecDecr(&bary->Neptune.v, bary->Sun.v);
3810
3811 /* Convert heliocentric SSB to barycentric Sun. */
3812 VecScale(&bary->Sun.r, -1.0);
3813 VecScale(&bary->Sun.v, -1.0);
3814}
3815
3816
3817static void AddAcceleration(terse_vector_t *acc, terse_vector_t small_pos, double gm, terse_vector_t major_pos)
3818{
3819 double dx, dy, dz, r2, pull;
3820
3821 dx = major_pos.x - small_pos.x;
3822 dy = major_pos.y - small_pos.y;
3823 dz = major_pos.z - small_pos.z;
3824
3825 r2 = dx*dx + dy*dy + dz*dz;
3826 pull = gm / (r2 * sqrt(r2));
3827
3828 acc->x += dx * pull;
3829 acc->y += dy * pull;
3830 acc->z += dz * pull;
3831}
3832
3833
3834static terse_vector_t SmallBodyAcceleration(terse_vector_t small_pos, const major_bodies_t *bary)
3835{
3836 terse_vector_t acc = VecZero;
3837
3838 /* Use barycentric coordinates of the Sun and major planets to calculate gravitational accelerations. */
3839 AddAcceleration(&acc, small_pos, SUN_GM, bary->Sun.r);
3840 AddAcceleration(&acc, small_pos, JUPITER_GM, bary->Jupiter.r);
3841 AddAcceleration(&acc, small_pos, SATURN_GM, bary->Saturn.r);
3842 AddAcceleration(&acc, small_pos, URANUS_GM, bary->Uranus.r);
3843 AddAcceleration(&acc, small_pos, NEPTUNE_GM, bary->Neptune.r);
3844
3845 return acc;
3846}
3847
3848
3849body_grav_calc_t GravSim( /* out: [pos, vel, acc] of the simulated body at time tt2 */
3850 major_bodies_t *bary2, /* temp: work area for major body barycentric state */
3851 double tt2, /* in: a target time to be calculated (either before or after tt1) */
3852 const body_grav_calc_t *calc1) /* in: [pos, vel, acc] of the simulated body at time tt1 */
3853{
3854 body_grav_calc_t calc2;
3855 terse_vector_t approx_pos;
3856 terse_vector_t acc;
3857 const double dt = tt2 - calc1->tt;
3858
3859 /* Calculate where the major bodies (Sun, Jupiter...Neptune) will be at the next time step. */
3860 MajorBodyBary(bary2, tt2);
3861
3862 /* Estimate position of small body as if current acceleration applies across the whole time interval. */
3863 /* approx_pos = pos1 + vel1*dt + (1/2)acc*dt^2 */
3864 approx_pos = UpdatePosition(dt, calc1->r, calc1->v, calc1->a);
3865
3866 /* Calculate acceleration experienced by small body at approximate next location. */
3867 acc = SmallBodyAcceleration(approx_pos, bary2);
3868
3869 /* Calculate the average acceleration of the endpoints. */
3870 /* This becomes our estimate of the mean effective acceleration over the whole interval. */
3871 acc = VecMean(acc, calc1->a);
3872
3873 /* Refine the estimates of [pos, vel, acc] at tt2 using the mean acceleration. */
3874 calc2.r = UpdatePosition(dt, calc1->r, calc1->v, acc);
3875 calc2.v = UpdateVelocity(dt, calc1->v, acc);
3876 calc2.a = SmallBodyAcceleration(calc2.r, bary2);
3877 calc2.tt = tt2;
3878 return calc2;
3879}
3880
3881
3882static body_grav_calc_t GravFromState(major_bodies_t *bary, const body_state_t *state)
3883{
3884 body_grav_calc_t calc;
3885
3886 MajorBodyBary(bary, state->tt);
3887
3888 calc.tt = state->tt;
3889 calc.r = VecAdd(state->r, bary->Sun.r); /* convert heliocentric to barycentric */
3890 calc.v = VecAdd(state->v, bary->Sun.v); /* convert heliocentric to barycentric */
3891 calc.a = SmallBodyAcceleration(calc.r, bary);
3892
3893 return calc;
3894}
3895
3896
3897static void CalcSolarSystem(astro_grav_sim_t *sim)
3898{
3899 int body;
3900 double tt = sim->curr->time.tt;
3901 body_state_t *grav = sim->curr->gravitators;
3902 body_state_t *sun = &grav[BODY_SUN];
3903
3904 /* Initialize the Sun's position/velocity as zero vectors, then adjust from pulls from the planets. */
3905 sun->tt = tt;
3906 sun->r = VecZero;
3907 sun->v = VecZero;
3908
3909 /* Calculate the position of each planet, and adjust the SSB position accordingly. */
3910 grav[BODY_MERCURY] = AdjustBarycenterPosVel(sun, tt, BODY_MERCURY, MERCURY_GM);
3911 grav[BODY_VENUS ] = AdjustBarycenterPosVel(sun, tt, BODY_VENUS, VENUS_GM);
3912 grav[BODY_EARTH ] = AdjustBarycenterPosVel(sun, tt, BODY_EARTH, EARTH_GM + MOON_GM);
3913 grav[BODY_MARS ] = AdjustBarycenterPosVel(sun, tt, BODY_MARS, MARS_GM);
3914 grav[BODY_JUPITER] = AdjustBarycenterPosVel(sun, tt, BODY_JUPITER, JUPITER_GM);
3915 grav[BODY_SATURN ] = AdjustBarycenterPosVel(sun, tt, BODY_SATURN, SATURN_GM);
3916 grav[BODY_URANUS ] = AdjustBarycenterPosVel(sun, tt, BODY_URANUS, URANUS_GM);
3917 grav[BODY_NEPTUNE] = AdjustBarycenterPosVel(sun, tt, BODY_NEPTUNE, NEPTUNE_GM);
3918
3919 /* Convert planet state vectors from heliocentric to barycentric. */
3920 for (body = BODY_MERCURY; body <= BODY_NEPTUNE; ++body)
3921 {
3922 VecDecr(&grav[body].r, sun->r);
3923 VecDecr(&grav[body].v, sun->v);
3924 }
3925
3926 /* Convert heliocentric SSB to barycentric Sun. */
3927 VecScale(&sun->r, -1.0);
3928 VecScale(&sun->v, -1.0);
3929}
3930
3931
3932static void CalcBodyAccelerations(astro_grav_sim_t *sim)
3933{
3934 int i;
3935 const body_state_t *grav = sim->curr->gravitators;
3936
3937 /* Calculate the gravitational acceleration experienced by the simulated bodies. */
3938 for (i = 0; i < sim->numBodies; ++i)
3939 {
3940 body_grav_calc_t *calc = &sim->curr->bodies[i];
3941
3942 calc->a = VecZero;
3943
3944 AddAcceleration(&calc->a, calc->r, SUN_GM, grav[BODY_SUN ].r);
3945 AddAcceleration(&calc->a, calc->r, MERCURY_GM, grav[BODY_MERCURY].r);
3946 AddAcceleration(&calc->a, calc->r, VENUS_GM, grav[BODY_VENUS ].r);
3947 AddAcceleration(&calc->a, calc->r, EARTH_GM + MOON_GM, grav[BODY_EARTH ].r);
3948 AddAcceleration(&calc->a, calc->r, MARS_GM, grav[BODY_MARS ].r);
3949 AddAcceleration(&calc->a, calc->r, JUPITER_GM, grav[BODY_JUPITER].r);
3950 AddAcceleration(&calc->a, calc->r, SATURN_GM, grav[BODY_SATURN ].r);
3951 AddAcceleration(&calc->a, calc->r, URANUS_GM, grav[BODY_URANUS ].r);
3952 AddAcceleration(&calc->a, calc->r, NEPTUNE_GM, grav[BODY_NEPTUNE].r);
3953 }
3954}
3955
3956
3957static body_state_t *GravSimBodyStatePtr(astro_grav_sim_t *sim, astro_body_t body)
3958{
3959 /*
3960 Return a pointer to the place where we cache the barycentric
3961 state of the given body, or NULL if this body is not one
3962 that we use for calculating gravitational interactions.
3963 */
3964
3965 if ((body == BODY_SUN) || (body >= BODY_MERCURY && body <= BODY_NEPTUNE))
3966 return &sim->curr->gravitators[body];
3967
3968 return NULL;
3969}
3970
3971
3972static astro_state_vector_t GravSimOriginState(astro_grav_sim_t *sim)
3973{
3974 body_state_t *optr;
3975 astro_time_t time = sim->curr->time;
3976
3977 if (sim->originBody == BODY_SSB)
3978 {
3979 /* The barycentric state of the SSB is zero, by definition. */
3980 astro_state_vector_t state;
3981 state.status = ASTRO_SUCCESS;
3982 state.t = time;
3983 state.x = 0.0;
3984 state.y = 0.0;
3985 state.z = 0.0;
3986 state.vx = 0.0;
3987 state.vy = 0.0;
3988 state.vz = 0.0;
3989 return state;
3990 }
3991
3992 optr = GravSimBodyStatePtr(sim, sim->originBody);
3993 if (optr != NULL)
3994 return ExportState(*optr, time);
3995
3996 /* We only support the VSOP bodies, for efficiency. */
3997 return StateVecError(ASTRO_INVALID_BODY, time);
3998}
3999
4000
4001static void GravSimDuplicate(astro_grav_sim_t *sim)
4002{
4003 /* Copy the current state into the previous state, so that both become the same moment in time. */
4004 sim->prev->time = sim->curr->time;
4005 memcpy(sim->prev->gravitators, sim->curr->gravitators, sizeof(sim->prev->gravitators));
4006 memcpy(sim->prev->bodies, sim->curr->bodies, ((size_t)sim->numBodies) * sizeof(sim->prev->bodies[0]));
4007}
4008
4009
4010/**
4011 * @brief Allocate and initialize a gravity step simulator.
4012 *
4013 * Prepares to simulate a series of incremental time steps,
4014 * simulating the movement of zero or more small bodies through the Solar System
4015 * acting under gravitational attraction from the Sun and planets.
4016 *
4017 * After calling this function, you can call #Astronomy_GravSimUpdate
4018 * as many times as desired to advance the simulation by small time steps.
4019 *
4020 * If this function succeeds (returns `ASTRO_SUCCESS`), `sim`
4021 * will be set to a dynamically allocated object. The caller is
4022 * then responsible for eventually calling #Astronomy_GravSimFree
4023 * to release the memory.
4024 *
4025 * @param simOut
4026 * The address of a pointer to store the newly allocated simulation object.
4027 * The type #astro_grav_sim_t is an opaque type, so its internal structure is not documented.
4028 *
4029 * @param originBody
4030 * Specifies the origin of the reference frame.
4031 * All position vectors and velocity vectors will use `originBody`
4032 * as the origin of the coordinate system.
4033 * This origin applies to all the input vectors provided in the
4034 * `bodyStateArray` parameter of this function, along with all
4035 * output vectors returned by #Astronomy_GravSimUpdate.
4036 * Most callers will want to provide one of the following:
4037 * `BODY_SUN` for heliocentric coordinates,
4038 * `BODY_SSB` for solar system barycentric coordinates,
4039 * or `BODY_EARTH` for geocentric coordinates. Note that the
4040 * gravity simulator does not correct for light travel time;
4041 * all state vectors are tied to a Newtonian "instantaneous" time.
4042 *
4043 * @param time
4044 * The initial time at which to start the simulation.
4045 *
4046 * @param numBodies
4047 * The number of small bodies to be simulated. This may be any non-negative integer.
4048 *
4049 * @param bodyStateArray
4050 * An array of initial state vectors (positions and velocities) of the small bodies to be simulated.
4051 * The caller must know the positions and velocities of the small bodies at an initial moment in time.
4052 * Their positions and velocities are expressed with respect to `originBody`, using J2000 mean equator orientation (EQJ).
4053 * Positions are expressed in astronomical units (AU). Velocities are expressed in AU/day.
4054 * All the times embedded within the state vectors must be exactly equal to `time`,
4055 * or this function will fail with the error `ASTRO_INCONSISTENT_TIMES`.
4056 *
4057 * @return
4058 * `ASTRO_SUCCESS` on success, with `*sim` set to a non-NULL value. Otherwise an error code with `*sim` set to NULL.
4059 */
4060astro_status_t Astronomy_GravSimInit(
4061 astro_grav_sim_t **simOut,
4062 astro_body_t originBody,
4063 astro_time_t time,
4064 int numBodies,
4065 const astro_state_vector_t *bodyStateArray)
4066{
4067 astro_grav_sim_t *sim;
4068 astro_status_t status;
4069 body_grav_calc_t *array;
4070 int i;
4071
4072 /* Validate parameters before attempting to allocate memory. */
4073
4074 if (simOut == NULL)
4075 return ASTRO_INVALID_PARAMETER;
4076
4077 *simOut = NULL;
4078
4079 if (numBodies < 0)
4080 return ASTRO_INVALID_PARAMETER;
4081
4082 if (numBodies > 0 && bodyStateArray == NULL)
4083 return ASTRO_INVALID_PARAMETER;
4084
4085 if (originBody < BODY_MERCURY || originBody > BODY_SSB)
4086 return ASTRO_INVALID_BODY;
4087
4088 /* Verify that all the state vectors are valid and have matching times. */
4089 for (i = 0; i < numBodies; ++i)
4090 {
4091 if (bodyStateArray[i].status != ASTRO_SUCCESS)
4092 return ASTRO_INVALID_PARAMETER;
4093
4094 if (bodyStateArray[i].t.tt != time.tt)
4095 return ASTRO_INCONSISTENT_TIMES;
4096 }
4097
4098 *simOut = sim = (astro_grav_sim_t *) calloc(1, sizeof(astro_grav_sim_t));
4099 if (sim == NULL)
4100 return ASTRO_OUT_OF_MEMORY;
4101
4102 sim->originBody = originBody;
4103 sim->numBodies = numBodies;
4104 sim->prev = &(sim->endpoint[0]);
4105 sim->curr = &(sim->endpoint[1]);
4106 sim->curr->time = time;
4107
4108 if (numBodies > 0)
4109 {
4110 sim->prev->bodies = (body_grav_calc_t *) calloc(numBodies, sizeof(body_grav_calc_t));
4111 sim->curr->bodies = (body_grav_calc_t *) calloc(numBodies, sizeof(body_grav_calc_t));
4112 if (sim->prev->bodies == NULL || sim->curr->bodies == NULL)
4113 {
4114 status = ASTRO_OUT_OF_MEMORY;
4115 goto fail;
4116 }
4117 }
4118
4119 /* Remember the initial states of all the bodies as "current". */
4120 /* Convert from the public type astro_state_t to our internal type body_grav_calc_t. */
4121 array = sim->curr->bodies;
4122 for (i = 0; i < numBodies; ++i)
4123 {
4124 array[i].tt = bodyStateArray[i].t.tt;
4125 array[i].r.x = bodyStateArray[i].x;
4126 array[i].r.y = bodyStateArray[i].y;
4127 array[i].r.z = bodyStateArray[i].z;
4128 array[i].v.x = bodyStateArray[i].vx;
4129 array[i].v.y = bodyStateArray[i].vy;
4130 array[i].v.z = bodyStateArray[i].vz;
4131 }
4132
4133 /* Calculate the state of the Sun and planets. */
4134 CalcSolarSystem(sim);
4135
4136 /*
4137 We need to do all the physics calculations in barycentric coordinates.
4138 But the caller can provide the input vectors with respect to any body.
4139 Correct the input body state vectors for the specified coordinate origin.
4140 */
4141 if (originBody != BODY_SSB)
4142 {
4143 /* Determine the barycentric state of the origin body. */
4144 astro_state_vector_t originState = GravSimOriginState(sim);
4145 if (originState.status != ASTRO_SUCCESS)
4146 {
4147 status = originState.status;
4148 goto fail;
4149 }
4150
4151 /* Add barycentric origin to origin-centric body to obtain barycentric body. */
4152 for (i = 0; i < numBodies; ++i)
4153 {
4154 array[i].r.x += originState.x;
4155 array[i].r.y += originState.y;
4156 array[i].r.z += originState.z;
4157 array[i].v.x += originState.vx;
4158 array[i].v.y += originState.vy;
4159 array[i].v.z += originState.vz;
4160 }
4161 }
4162
4163 /* Calculate the net acceleration experienced by the small bodies. */
4164 CalcBodyAccelerations(sim);
4165
4166 /* To prepare for a possible swap operation, duplicate the current state into the previous state. */
4167 GravSimDuplicate(sim);
4168
4169 return ASTRO_SUCCESS;
4170
4171fail:
4172 Astronomy_GravSimFree(sim);
4173 *simOut = NULL;
4174 return status;
4175}
4176
4177
4178/**
4179 * @brief Advances a gravity simulation by a small time step.
4180 *
4181 * @param sim
4182 * A simulation object that was created by a prior call to #Astronomy_GravSimInit.
4183 *
4184 * @param time
4185 * A time that is a small increment away from the current simulation time.
4186 * It is up to the developer to figure out an appropriate time increment.
4187 * Depending on the trajectories, a smaller or larger increment
4188 * may be needed for the desired accuracy. Some experimentation may be needed.
4189 * Generally, bodies that stay in the outer Solar System and move slowly can
4190 * use larger time steps. Bodies that pass into the inner Solar System and
4191 * move faster will need a smaller time step to maintain accuracy.
4192 * The `time` value may be after or before the current simulation time
4193 * to move forward or backward in time.
4194 *
4195 * @param numBodies
4196 * The number of bodies whose state vectors are to be updated.
4197 * This is the number of elements in the `bodyStateArray`.
4198 * This parameter is passed as a sanity check, and must be equal
4199 * to the value passed to #Astronomy_GravSimInit when `sim` was created.
4200 *
4201 * @param bodyStateArray
4202 * An array big enough to hold `numBodies` state vectors, to receive
4203 * the updated positions and velocities of the simulated small bodies.
4204 * Alternatively, `bodyStateArray` may be NULL if the output of this
4205 * simulation step is not needed. This makes the call slightly faster.
4206 *
4207 * @return
4208 * `ASTRO_SUCCESS` if the calculation was successful.
4209 * Otherwise, an error code if something went wrong, in which case
4210 * the simulation should be considered "broken". This means there
4211 * is no reliable output in `bodyStateArray` and that no more calculations
4212 * can be performed with `sim`.
4213 */
4214astro_status_t Astronomy_GravSimUpdate(
4215 astro_grav_sim_t *sim,
4216 astro_time_t time,
4217 int numBodies,
4218 astro_state_vector_t *bodyStateArray)
4219{
4220 terse_vector_t acc;
4221 double dt; /* terrestrial time increment */
4222 int i;
4223
4224 /*
4225 The caller's understanding of the number of bodies must match the actual
4226 array size in `sim`, or we risk corrupting/accessing invalid memory.
4227 */
4228 if (numBodies != sim->numBodies)
4229 return ASTRO_INVALID_PARAMETER;
4230
4231 dt = time.tt - sim->curr->time.tt;
4232
4233 if (dt == 0.0)
4234 {
4235 /*
4236 Special case: the time has not changed, so skip the usual physics calculations.
4237 This allows a way for the caller to query the current state if desired.
4238 It is also necessary to avoid dividing by `dt` if `dt` is zero.
4239 To prepare for a possible swap operation, duplicate the current state into the previous state.
4240 */
4241 GravSimDuplicate(sim);
4242 }
4243 else
4244 {
4245 /* Swap the current state and the previous state. Then calculate the new current state. */
4246 Astronomy_GravSimSwap(sim);
4247
4248 /* Update the current time. This is the only place we have a full (tt,ut) pair. */
4249 /* All of the Newtonian dynamics are calculated using tt only. */
4250 sim->curr->time = time;
4251
4252 /* Now that sim->time is set, it is safe to call `CalcSolarSystem`. */
4253 CalcSolarSystem(sim);
4254
4255 for (i = 0; i < numBodies; ++i)
4256 {
4257 /*
4258 Estimate the positions of the small bodies as if their
4259 current accelerations apply across the whole time interval.
4260 approx_pos = pos1 + vel1*dt + (1/2)acc*dt^2
4261 */
4262 const body_grav_calc_t *prev = &sim->prev->bodies[i];
4263 sim->curr->bodies[i].r = UpdatePosition(dt, prev->r, prev->v, prev->a);
4264 }
4265
4266 /*
4267 Calculate the acceleration experienced by the small bodies
4268 at their respective approximate next locations.
4269 */
4270 CalcBodyAccelerations(sim);
4271
4272 for (i = 0; i < numBodies; ++i)
4273 {
4274 const body_grav_calc_t *prev = &sim->prev->bodies[i];
4275 body_grav_calc_t *curr = &sim->curr->bodies[i];
4276
4277 /*
4278 Calculate the average of the acceleration vectors
4279 experienced by the previous body positions and
4280 their estimated next positions.
4281 These become estimates of the mean effective accelerations over the whole interval.
4282 */
4283 acc = VecMean(prev->a, curr->a);
4284
4285 /*
4286 Refine the estimates of position and velocity at the next time step,
4287 using the mean acceleration as a better approximation of the
4288 continuously changing acceleration acting on each body.
4289 */
4290 curr->tt = time.tt;
4291 curr->r = UpdatePosition(dt, prev->r, prev->v, acc);
4292 curr->v = UpdateVelocity(dt, prev->v, acc);
4293 }
4294
4295 /*
4296 Re-calculate accelerations experienced by each body.
4297 These will be needed for the next simulation step (if any).
4298 Also, they will be potentially useful if some day we add
4299 a function to query the acceleration vectors for the bodies.
4300 */
4301 CalcBodyAccelerations(sim);
4302 }
4303
4304 /*
4305 Translate our internal calculations of body positions
4306 and velocities into state vectors that the caller can understand.
4307 But if the output buffer `bodyStateArray` is NULL, it means
4308 the caller wanted us to update the simulation state without
4309 returning any output.
4310 */
4311 if (bodyStateArray != NULL)
4312 {
4313 for (i = 0; i < numBodies; ++i)
4314 bodyStateArray[i] = ExportGravCalc(sim->curr->bodies[i], time);
4315
4316 if (sim->originBody != BODY_SSB)
4317 {
4318 /* Determine the barycentric state of the origin body. */
4319 astro_state_vector_t originState = GravSimOriginState(sim);
4320 if (originState.status != ASTRO_SUCCESS)
4321 return originState.status;
4322
4323 /* Subtract vectors to convert barycentric states to origin-centric states. */
4324 for (i = 0; i < numBodies; ++i)
4325 {
4326 bodyStateArray[i].x -= originState.x;
4327 bodyStateArray[i].y -= originState.y;
4328 bodyStateArray[i].z -= originState.z;
4329 bodyStateArray[i].vx -= originState.vx;
4330 bodyStateArray[i].vy -= originState.vy;
4331 bodyStateArray[i].vz -= originState.vz;
4332 }
4333 }
4334 }
4335
4336 return ASTRO_SUCCESS;
4337}
4338
4339
4340/**
4341 * @brief Get the position and velocity of a Solar System body included in the simulation.
4342 *
4343 * In order to simulate the movement of small bodies through the Solar System,
4344 * the simulator needs to calculate the state vectors for the Sun and planets.
4345 *
4346 * If an application wants to know the positions of one or more of the planets
4347 * in addition to the small bodies, this function provides a way to obtain
4348 * their state vectors. This is provided for the sake of efficiency, to avoid
4349 * redundant calculations.
4350 *
4351 * @param sim
4352 * A gravity simulator object created by a successful call to #Astronomy_GravSimInit.
4353 *
4354 * @param body
4355 * The Sun, Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, or Neptune.
4356 *
4357 * @return
4358 * If the given body is part of the set of calculated bodies
4359 * (Sun and planets), returns the current time step's state
4360 * vector for that body, expressed in the coordinate system
4361 * that was specified by the `originBody` parameter to #Astronomy_GravSimInit.
4362 * Success is indicated by the returned structure's `status` field holding
4363 * `ASTRO_SUCCESS`. Any other `status` value indicates an error, meaning
4364 * the returned state vector is invalid.
4365 */
4366astro_state_vector_t Astronomy_GravSimBodyState(
4367 astro_grav_sim_t *sim,
4368 astro_body_t body)
4369{
4370 astro_state_vector_t state;
4371 body_state_t *bptr;
4372
4373 bptr = GravSimBodyStatePtr(sim, body);
4374 if (bptr == NULL)
4375 return StateVecError(ASTRO_INVALID_BODY, sim->curr->time);
4376
4377 state = GravSimOriginState(sim);
4378 if (state.status != ASTRO_SUCCESS)
4379 return state;
4380
4381 /*
4382 Subtract the origin's state from the body's barycentric
4383 state to get the body in the desired reference frame.
4384 state.t and state.status are already initialized correctly.
4385 */
4386
4387 state.x = bptr->r.x - state.x;
4388 state.y = bptr->r.y - state.y;
4389 state.z = bptr->r.z - state.z;
4390 state.vx = bptr->v.x - state.vx;
4391 state.vy = bptr->v.y - state.vy;
4392 state.vz = bptr->v.z - state.vz;
4393
4394 return state;
4395}
4396
4397
4398/**
4399 * @brief Exchange the current time step with the previous time step.
4400 *
4401 * Sometimes it is helpful to "explore" various times near a given
4402 * simulation time step, while repeatedly returning to the original
4403 * time step. For example, when backdating a position for light travel
4404 * time, the caller may wish to repeatedly try different amounts of
4405 * backdating. When the backdating solver has converged, the caller
4406 * wants to leave the simulation in its original state.
4407 *
4408 * This function allows a single "undo" of a simulation, and does so
4409 * very efficiently.
4410 *
4411 * Usually this function will be called immediately after a matching
4412 * call to #Astronomy_GravSimUpdate. It has the effect of rolling
4413 * back the most recent update. If called twice in a row, it reverts
4414 * the swap and thus has no net effect.
4415 *
4416 * #Astronomy_GravSimInit initializes the current state and previous
4417 * state to be identical. Both states represent the `time` parameter that was
4418 * passed into the initializer. Therefore, `Astronomy_GravSimSwap` will
4419 * have no effect from the caller's point of view when passed a simulator
4420 * that has not yet been updated by a call to #Astronomy_GravSimUpdate.
4421 *
4422 * @param sim
4423 * A gravity simulator object that was created by a prior call to #Astronomy_GravSimInit.
4424 */
4425void Astronomy_GravSimSwap(astro_grav_sim_t *sim)
4426{
4427 gravsim_endpoint_t *swap = sim->prev;
4428 sim->prev = sim->curr;
4429 sim->curr = swap;
4430}
4431
4432
4433/**
4434 * @brief Returns the time of the current simulation step.
4435 *
4436 * @param sim
4437 * A gravity simulator object that was created by a prior call to #Astronomy_GravSimInit.
4438 */
4439astro_time_t Astronomy_GravSimTime(const astro_grav_sim_t *sim)
4440{
4441 return sim->curr->time;
4442}
4443
4444
4445/**
4446 * @brief Returns the number of small bodies represented in this simulation.
4447 *
4448 * When a simulation is created by a call to #Astronomy_GravSimInit,
4449 * the caller specifies the number of small bodies.
4450 * This function returns that same number, which may be convenient for a caller,
4451 * so that it does not need to track the body count separately.
4452 *
4453 * @param sim
4454 * A gravity simulator object that was created by a prior call to #Astronomy_GravSimInit.
4455 */
4456int Astronomy_GravSimNumBodies(const astro_grav_sim_t *sim)
4457{
4458 return sim->numBodies;
4459}
4460
4461
4462/**
4463 * @brief Returns the body whose center is the coordinate origin that small bodies are referenced to.
4464 *
4465 * When a simulation is created by a call to #Astronomy_GravSimInit,
4466 * the caller specifies an `originBody` to indicate the coordinate origin
4467 * used to represent the small bodies being simulated.
4468 * This function returns that same #astro_body_t value.
4469 *
4470 * @param sim
4471 * A gravity simulator object that was created by a prior call to #Astronomy_GravSimInit.
4472 */
4473astro_body_t Astronomy_GravSimOrigin(const astro_grav_sim_t *sim)
4474{
4475 return sim->originBody;
4476}
4477
4478
4479/**
4480 * @brief Releases memory allocated to a gravity simulator object.
4481 *
4482 * To avoid memory leaks, any successful call to #Astronomy_GravSimInit
4483 * must be paired with a matching call to `Astronomy_GravSimFree`.
4484 *
4485 * @param sim
4486 * A gravity simulator object that was created by a prior call to #Astronomy_GravSimInit.
4487 */
4488void Astronomy_GravSimFree(astro_grav_sim_t *sim)
4489{
4490 if (sim != NULL)
4491 {
4492 free(sim->endpoint[0].bodies);
4493 free(sim->endpoint[1].bodies);
4494 free(sim);
4495 }
4496}
4497
4498
4499/*------------------ begin Pluto integrator ------------------*/
4500
4501static const body_state_t PlutoStateTable[] =
4502{
4503 { -730000.0, {-26.118207232108, -14.376168177825, 3.384402515299}, { 1.6339372163656e-03, -2.7861699588508e-03, -1.3585880229445e-03} }
4504, { -700800.0, { 41.974905202127, -0.448502952929, -12.770351505989}, { 7.3458569351457e-04, 2.2785014891658e-03, 4.8619778602049e-04} }
4505, { -671600.0, { 14.706930780744, 44.269110540027, 9.353698474772}, {-2.1000147999800e-03, 2.2295915939915e-04, 7.0143443551414e-04} }
4506, { -642400.0, {-29.441003929957, -6.430161530570, 6.858481011305}, { 8.4495803960544e-04, -3.0783914758711e-03, -1.2106305981192e-03} }
4507, { -613200.0, { 39.444396946234, -6.557989760571, -13.913760296463}, { 1.1480029005873e-03, 2.2400006880665e-03, 3.5168075922288e-04} }
4508, { -584000.0, { 20.230380950700, 43.266966657189, 7.382966091923}, {-1.9754081700585e-03, 5.3457141292226e-04, 7.5929169129793e-04} }
4509, { -554800.0, {-30.658325364620, 2.093818874552, 9.880531138071}, { 6.1010603013347e-05, -3.1326500935382e-03, -9.9346125151067e-04} }
4510, { -525600.0, { 35.737703251673, -12.587706024764, -14.677847247563}, { 1.5802939375649e-03, 2.1347678412429e-03, 1.9074436384343e-04} }
4511, { -496400.0, { 25.466295188546, 41.367478338417, 5.216476873382}, {-1.8054401046468e-03, 8.3283083599510e-04, 8.0260156912107e-04} }
4512, { -467200.0, {-29.847174904071, 10.636426313081, 12.297904180106}, {-6.3257063052907e-04, -2.9969577578221e-03, -7.4476074151596e-04} }
4513, { -438000.0, { 30.774692107687, -18.236637015304, -14.945535879896}, { 2.0113162005465e-03, 1.9353827024189e-03, -2.0937793168297e-06} }
4514, { -408800.0, { 30.243153324028, 38.656267888503, 2.938501750218}, {-1.6052508674468e-03, 1.1183495337525e-03, 8.3333973416824e-04} }
4515, { -379600.0, {-27.288984772533, 18.643162147874, 14.023633623329}, {-1.1856388898191e-03, -2.7170609282181e-03, -4.9015526126399e-04} }
4516, { -350400.0, { 24.519605196774, -23.245756064727, -14.626862367368}, { 2.4322321483154e-03, 1.6062008146048e-03, -2.3369181613312e-04} }
4517, { -321200.0, { 34.505274805875, 35.125338586954, 0.557361475637}, {-1.3824391637782e-03, 1.3833397561817e-03, 8.4823598806262e-04} }
4518, { -292000.0, {-23.275363915119, 25.818514298769, 15.055381588598}, {-1.6062295460975e-03, -2.3395961498533e-03, -2.4377362639479e-04} }
4519, { -262800.0, { 17.050384798092, -27.180376290126, -13.608963321694}, { 2.8175521080578e-03, 1.1358749093955e-03, -4.9548725258825e-04} }
4520, { -233600.0, { 38.093671910285, 30.880588383337, -1.843688067413}, {-1.1317697153459e-03, 1.6128814698472e-03, 8.4177586176055e-04} }
4521, { -204400.0, {-18.197852930878, 31.932869934309, 15.438294826279}, {-1.9117272501813e-03, -1.9146495909842e-03, -1.9657304369835e-05} }
4522, { -175200.0, { 8.528924039997, -29.618422200048, -11.805400994258}, { 3.1034370787005e-03, 5.1393633292430e-04, -7.7293066202546e-04} }
4523, { -146000.0, { 40.946857258640, 25.904973592021, -4.256336240499}, {-8.3652705194051e-04, 1.8129497136404e-03, 8.1564228273060e-04} }
4524, { -116800.0, {-12.326958895325, 36.881883446292, 15.217158258711}, {-2.1166103705038e-03, -1.4814420035990e-03, 1.7401209844705e-04} }
4525, { -87600.0, { -0.633258375909, -30.018759794709, -9.171932874950}, { 3.2016994581737e-03, -2.5279858672148e-04, -1.0411088271861e-03} }
4526, { -58400.0, { 42.936048423883, 20.344685584452, -6.588027007912}, {-5.0525450073192e-04, 1.9910074335507e-03, 7.7440196540269e-04} }
4527, { -29200.0, { -5.975910552974, 40.611809958460, 14.470131723673}, {-2.2184202156107e-03, -1.0562361130164e-03, 3.3652250216211e-04} }
4528, { 0.0, { -9.875369580774, -27.978926224737, -5.753711824704}, { 3.0287533248818e-03, -1.1276087003636e-03, -1.2651326732361e-03} }
4529, { 29200.0, { 43.958831986165, 14.214147973292, -8.808306227163}, {-1.4717608981871e-04, 2.1404187242141e-03, 7.1486567806614e-04} }
4530, { 58400.0, { 0.678136763520, 43.094461639362, 13.243238780721}, {-2.2358226110718e-03, -6.3233636090933e-04, 4.7664798895648e-04} }
4531, { 87600.0, {-18.282602096834, -23.305039586660, -1.766620508028}, { 2.5567245263557e-03, -1.9902940754171e-03, -1.3943491701082e-03} }
4532, { 116800.0, { 43.873338744526, 7.700705617215, -10.814273666425}, { 2.3174803055677e-04, 2.2402163127924e-03, 6.2988756452032e-04} }
4533, { 146000.0, { 7.392949027906, 44.382678951534, 11.629500214854}, {-2.1932815453830e-03, -2.1751799585364e-04, 5.9556516201114e-04} }
4534, { 175200.0, {-24.981690229261, -16.204012851426, 2.466457544298}, { 1.8193989149580e-03, -2.6765419531201e-03, -1.3848283502247e-03} }
4535, { 204400.0, { 42.530187039511, 0.845935508021, -12.554907527683}, { 6.5059779150669e-04, 2.2725657282262e-03, 5.1133743202822e-04} }
4536, { 233600.0, { 13.999526486822, 44.462363044894, 9.669418486465}, {-2.1079296569252e-03, 1.7533423831993e-04, 6.9128485798076e-04} }
4537, { 262800.0, {-29.184024803031, -7.371243995762, 6.493275957928}, { 9.3581363109681e-04, -3.0610357109184e-03, -1.2364201089345e-03} }
4538, { 292000.0, { 39.831980671753, -6.078405766765, -13.909815358656}, { 1.1117769689167e-03, 2.2362097830152e-03, 3.6230548231153e-04} }
4539, { 321200.0, { 20.294955108476, 43.417190420251, 7.450091985932}, {-1.9742157451535e-03, 5.3102050468554e-04, 7.5938408813008e-04} }
4540, { 350400.0, {-30.669992302160, 2.318743558955, 9.973480913858}, { 4.5605107450676e-05, -3.1308219926928e-03, -9.9066533301924e-04} }
4541, { 379600.0, { 35.626122155983, -12.897647509224, -14.777586508444}, { 1.6015684949743e-03, 2.1171931182284e-03, 1.8002516202204e-04} }
4542, { 408800.0, { 26.133186148561, 41.232139187599, 5.006401326220}, {-1.7857704419579e-03, 8.6046232702817e-04, 8.0614690298954e-04} }
4543, { 438000.0, {-29.576740229230, 11.863535943587, 12.631323039872}, {-7.2292830060955e-04, -2.9587820140709e-03, -7.0824296450300e-04} }
4544, { 467200.0, { 29.910805787391, -19.159019294000, -15.013363865194}, { 2.0871080437997e-03, 1.8848372554514e-03, -3.8528655083926e-05} }
4545, { 496400.0, { 31.375957451819, 38.050372720763, 2.433138343754}, {-1.5546055556611e-03, 1.1699815465629e-03, 8.3565439266001e-04} }
4546, { 525600.0, {-26.360071336928, 20.662505904952, 14.414696258958}, {-1.3142373118349e-03, -2.6236647854842e-03, -4.2542017598193e-04} }
4547, { 554800.0, { 22.599441488648, -24.508879898306, -14.484045731468}, { 2.5454108304806e-03, 1.4917058755191e-03, -3.0243665086079e-04} }
4548, { 584000.0, { 35.877864013014, 33.894226366071, -0.224524636277}, {-1.2941245730845e-03, 1.4560427668319e-03, 8.4762160640137e-04} }
4549, { 613200.0, {-21.538149762417, 28.204068269761, 15.321973799534}, {-1.7312117409010e-03, -2.1939631314577e-03, -1.6316913275180e-04} }
4550, { 642400.0, { 13.971521374415, -28.339941764789, -13.083792871886}, { 2.9334630526035e-03, 9.1860931752944e-04, -5.9939422488627e-04} }
4551, { 671600.0, { 39.526942044143, 28.939897360110, -2.872799527539}, {-1.0068481658095e-03, 1.7021132888090e-03, 8.3578230511981e-04} }
4552, { 700800.0, {-15.576200701394, 34.399412961275, 15.466033737854}, {-2.0098814612884e-03, -1.7191109825989e-03, 7.0414782780416e-05} }
4553, { 730000.0, { 4.243252837090, -30.118201690825, -10.707441231349}, { 3.1725847067411e-03, 1.6098461202270e-04, -9.0672150593868e-04} }
4554};
4555
4556/* FIXFIXFIX - Using a global is not thread-safe. Either add thread-locks or change API to accept a cache pointer. */
4557static body_segment_t *pluto_cache[PLUTO_NUM_STATES-1];
4558
4559
4560static int ClampIndex(double frac, int nsteps)
4561{
4562 int index = (int) floor(frac);
4563 if (index < 0)
4564 return 0;
4565 if (index >= nsteps)
4566 return nsteps-1;
4567 return index;
4568}
4569
4570
4571static astro_status_t GetSegment(int *seg_index, body_segment_t *cache[], double tt)
4572{
4573 int i;
4574 body_segment_t reverse;
4575 body_segment_t *seg;
4576 major_bodies_t bary;
4577 double step_tt, ramp;
4578
4579 if (tt < PlutoStateTable[0].tt || tt > PlutoStateTable[PLUTO_NUM_STATES-1].tt)
4580 {
4581 /* We don't bother calculating a segment. Let the caller crawl backward/forward to this time. */
4582 *seg_index = -1;
4583 return ASTRO_SUCCESS;
4584 }
4585
4586 /* See if we have a segment that straddles the requested time. */
4587 /* If so, return it. Otherwise, calculate it and return it. */
4588
4589 *seg_index = ClampIndex((tt - PlutoStateTable[0].tt) / PLUTO_TIME_STEP, PLUTO_NUM_STATES-1);
4590 if (cache[*seg_index] == NULL)
4591 {
4592 /* Allocate memory for the segment (about 11K each). */
4593 seg = cache[*seg_index] = (body_segment_t *) calloc(1, sizeof(body_segment_t));
4594 if (seg == NULL)
4595 return ASTRO_OUT_OF_MEMORY;
4596
4597 /* Calculate the segment. */
4598 /* Pick the pair of bracketing body states to fill the segment. */
4599
4600 /* Each endpoint is exact. */
4601 seg->step[0] = GravFromState(&bary, &PlutoStateTable[*seg_index]);
4602 seg->step[PLUTO_NSTEPS-1] = GravFromState(&bary, &PlutoStateTable[*seg_index + 1]);
4603
4604 /* Simulate forwards from the lower time bound. */
4605 step_tt = seg->step[0].tt;
4606 for (i=1; i < PLUTO_NSTEPS-1; ++i)
4607 seg->step[i] = GravSim(&bary, step_tt += PLUTO_DT, &seg->step[i-1]);
4608
4609 /* Simulate backwards from the upper time bound. */
4610 step_tt = seg->step[PLUTO_NSTEPS-1].tt;
4611 reverse.step[PLUTO_NSTEPS-1] = seg->step[PLUTO_NSTEPS-1];
4612 for (i=PLUTO_NSTEPS-2; i > 0; --i)
4613 reverse.step[i] = GravSim(&bary, step_tt -= PLUTO_DT, &reverse.step[i+1]);
4614
4615 /* Fade-mix the two series so that there are no discontinuities. */
4616 for (i=PLUTO_NSTEPS-2; i > 0; --i)
4617 {
4618 ramp = (double)i / (PLUTO_NSTEPS-1);
4619 seg->step[i].r = VecRamp(seg->step[i].r, reverse.step[i].r, ramp);
4620 seg->step[i].v = VecRamp(seg->step[i].v, reverse.step[i].v, ramp);
4621 seg->step[i].a = VecRamp(seg->step[i].a, reverse.step[i].a, ramp);
4622 }
4623 }
4624
4625 return ASTRO_SUCCESS;
4626}
4627
4628
4629static body_grav_calc_t CalcPlutoOneWay(major_bodies_t *bary, const body_state_t *init_state, double target_tt, double dt)
4630{
4631 body_grav_calc_t calc;
4632 int i, n;
4633
4634 calc = GravFromState(bary, init_state);
4635 n = (int) ceil((target_tt - calc.tt) / dt);
4636 for (i=0; i < n; ++i)
4637 calc = GravSim(bary, (i+1 == n) ? target_tt : (calc.tt + dt), &calc);
4638
4639 return calc;
4640}
4641
4642
4643static astro_status_t CalcPluto(body_state_t *bstate, astro_time_t time, int helio)
4644{
4645 terse_vector_t acc, ra, rb, va, vb;
4646 major_bodies_t bary;
4647 const body_segment_t *seg;
4648 int seg_index, left;
4649 const body_grav_calc_t *s1;
4650 const body_grav_calc_t *s2;
4651 body_grav_calc_t calc;
4652 astro_status_t status;
4653 double ramp;
4654
4655 memset(bstate, 0, sizeof(body_state_t));
4656 bstate->tt = time.tt;
4657
4658 status = GetSegment(&seg_index, pluto_cache, time.tt);
4659 if (status != ASTRO_SUCCESS)
4660 return status;
4661
4662 if (seg_index < 0)
4663 {
4664 /* The target time is outside the year range 0000..4000. */
4665 /* Calculate it by crawling backward from 0000 or forward from 4000. */
4666 /* FIXFIXFIX - This is super slow. Could optimize this with extra caching if needed. */
4667 if (time.tt < PlutoStateTable[0].tt)
4668 calc = CalcPlutoOneWay(&bary, &PlutoStateTable[0], time.tt, -PLUTO_DT);
4669 else
4670 calc = CalcPlutoOneWay(&bary, &PlutoStateTable[PLUTO_NUM_STATES-1], time.tt, +PLUTO_DT);
4671
4672 bstate->r = calc.r;
4673 bstate->v = calc.v;
4674 }
4675 else
4676 {
4677 seg = pluto_cache[seg_index];
4678 left = ClampIndex((time.tt - seg->step[0].tt) / PLUTO_DT, PLUTO_NSTEPS-1);
4679 s1 = &seg->step[left];
4680 s2 = &seg->step[left+1];
4681
4682 /* Find mean acceleration vector over the interval. */
4683 acc = VecMean(s1->a, s2->a);
4684
4685 /* Use Newtonian mechanics to extrapolate away from t1 in the positive time direction. */
4686 ra = UpdatePosition(time.tt - s1->tt, s1->r, s1->v, acc);
4687 va = UpdateVelocity(time.tt - s1->tt, s1->v, acc);
4688
4689 /* Use Newtonian mechanics to extrapolate away from t2 in the negative time direction. */
4690 rb = UpdatePosition(time.tt - s2->tt, s2->r, s2->v, acc);
4691 vb = UpdateVelocity(time.tt - s2->tt, s2->v, acc);
4692
4693 /* Use fade in/out idea to blend the two position estimates. */
4694 ramp = (time.tt - s1->tt)/PLUTO_DT;
4695 bstate->r = VecRamp(ra, rb, ramp);
4696 bstate->v = VecRamp(va, vb, ramp);
4697
4698 if (helio)
4699 MajorBodyBary(&bary, time.tt);
4700 }
4701
4702 if (helio)
4703 {
4704 /* Convert barycentric coordinates back to heliocentric coordinates. */
4705 VecDecr(&bstate->r, bary.Sun.r);
4706 VecDecr(&bstate->v, bary.Sun.v);
4707 }
4708
4709 return ASTRO_SUCCESS;
4710}
4711
4712/*------------------ end Pluto integrator ------------------*/
4713
4714
4715/*---------------------- begin Jupiter moons ----------------------*/
4716
4717static const astro_rotation_t Rotation_JUP_EQJ =
4718{
4719 ASTRO_SUCCESS,
4720 {
4721 { 9.99432765338654e-01, -3.36771074697641e-02, 0.00000000000000e+00 },
4722 { 3.03959428906285e-02, 9.02057912352809e-01, 4.30543388542295e-01 },
4723 { -1.44994559663353e-02, -4.30299169409101e-01, 9.02569881273754e-01 }
4724 }
4725};
4726
4727static const vsop_term_t jm_Io_a[] =
4728{
4729 { 0.0028210960212903, 0.0000000000000000e+00, 0.0000000000000000e+00 }
4730};
4731
4732static const vsop_term_t jm_Io_l[] =
4733{
4734 { -0.0001925258348666, 4.9369589722644998e+00, 1.3584836583050000e-02 },
4735 { -0.0000970803596076, 4.3188796477322002e+00, 1.3034138432430000e-02 },
4736 { -0.0000898817416500, 1.9080016428616999e+00, 3.0506486715799999e-03 },
4737 { -0.0000553101050262, 1.4936156681568999e+00, 1.2938928911549999e-02 }
4738};
4739
4740static const vsop_term_t jm_Io_z[] =
4741{
4742 { 0.0041510849668155, 4.0899396355450000e+00, -1.2906864146660001e-02 },
4743 { 0.0006260521444113, 1.4461888986270000e+00, 3.5515522949801999e+00 },
4744 { 0.0000352747346169, 2.1256287034577999e+00, 1.2727416566999999e-04 }
4745};
4746
4747static const vsop_term_t jm_Io_zeta[] =
4748{
4749 { 0.0003142172466014, 2.7964219722923001e+00, -2.3150960980000000e-03 },
4750 { 0.0000904169207946, 1.0477061879627001e+00, -5.6920638196000003e-04 }
4751};
4752
4753static const vsop_term_t jm_Europa_a[] =
4754{
4755 { 0.0044871037804314, 0.0000000000000000e+00, 0.0000000000000000e+00 },
4756 { 0.0000004324367498, 1.8196456062910000e+00, 1.7822295777568000e+00 }
4757};
4758
4759static const vsop_term_t jm_Europa_l[] =
4760{
4761 { 0.0008576433172936, 4.3188693178264002e+00, 1.3034138308049999e-02 },
4762 { 0.0004549582875086, 1.4936531751079001e+00, 1.2938928819619999e-02 },
4763 { 0.0003248939825174, 1.8196494533458001e+00, 1.7822295777568000e+00 },
4764 { -0.0003074250079334, 4.9377037005910998e+00, 1.3584832867240000e-02 },
4765 { 0.0001982386144784, 1.9079869054759999e+00, 3.0510121286900001e-03 },
4766 { 0.0001834063551804, 2.1402853388529000e+00, 1.4500978933800000e-03 },
4767 { -0.0001434383188452, 5.6222140366630002e+00, 8.9111478887838003e-01 },
4768 { -0.0000771939140944, 4.3002724372349999e+00, 2.6733443704265998e+00 }
4769};
4770
4771static const vsop_term_t jm_Europa_z[] =
4772{
4773 { -0.0093589104136341, 4.0899396509038999e+00, -1.2906864146660001e-02 },
4774 { 0.0002988994545555, 5.9097265185595003e+00, 1.7693227079461999e+00 },
4775 { 0.0002139036390350, 2.1256289300016000e+00, 1.2727418406999999e-04 },
4776 { 0.0001980963564781, 2.7435168292649998e+00, 6.7797343008999997e-04 },
4777 { 0.0001210388158965, 5.5839943711203004e+00, 3.2056614899999997e-05 },
4778 { 0.0000837042048393, 1.6094538368039000e+00, -9.0402165808846002e-01 },
4779 { 0.0000823525166369, 1.4461887708689001e+00, 3.5515522949801999e+00 }
4780};
4781
4782static const vsop_term_t jm_Europa_zeta[] =
4783{
4784 { 0.0040404917832303, 1.0477063169425000e+00, -5.6920640539999997e-04 },
4785 { 0.0002200421034564, 3.3368857864364001e+00, -1.2491307306999999e-04 },
4786 { 0.0001662544744719, 2.4134862374710999e+00, 0.0000000000000000e+00 },
4787 { 0.0000590282470983, 5.9719930968366004e+00, -3.0561602250000000e-05 }
4788};
4789
4790static const vsop_term_t jm_Ganymede_a[] =
4791{
4792 { 0.0071566594572575, 0.0000000000000000e+00, 0.0000000000000000e+00 },
4793 { 0.0000013930299110, 1.1586745884981000e+00, 2.6733443704265998e+00 }
4794};
4795
4796static const vsop_term_t jm_Ganymede_l[] =
4797{
4798 { 0.0002310797886226, 2.1402987195941998e+00, 1.4500978438400001e-03 },
4799 { -0.0001828635964118, 4.3188672736968003e+00, 1.3034138282630000e-02 },
4800 { 0.0001512378778204, 4.9373102372298003e+00, 1.3584834812520000e-02 },
4801 { -0.0001163720969778, 4.3002659861490002e+00, 2.6733443704265998e+00 },
4802 { -0.0000955478069846, 1.4936612842567001e+00, 1.2938928798570001e-02 },
4803 { 0.0000815246854464, 5.6222137132535002e+00, 8.9111478887838003e-01 },
4804 { -0.0000801219679602, 1.2995922951532000e+00, 1.0034433456728999e+00 },
4805 { -0.0000607017260182, 6.4978769669238001e-01, 5.0172167043264004e-01 }
4806};
4807
4808static const vsop_term_t jm_Ganymede_z[] =
4809{
4810 { 0.0014289811307319, 2.1256295942738999e+00, 1.2727413029000001e-04 },
4811 { 0.0007710931226760, 5.5836330003496002e+00, 3.2064341100000001e-05 },
4812 { 0.0005925911780766, 4.0899396636447998e+00, -1.2906864146660001e-02 },
4813 { 0.0002045597496146, 5.2713683670371996e+00, -1.2523544076106000e-01 },
4814 { 0.0001785118648258, 2.8743156721063001e-01, 8.7820792442520001e-01 },
4815 { 0.0001131999784893, 1.4462127277818000e+00, 3.5515522949801999e+00 },
4816 { -0.0000658778169210, 2.2702423990985001e+00, -1.7951364394536999e+00 },
4817 { 0.0000497058888328, 5.9096792204858000e+00, 1.7693227129285001e+00 }
4818};
4819
4820static const vsop_term_t jm_Ganymede_zeta[] =
4821{
4822 { 0.0015932721570848, 3.3368862796665000e+00, -1.2491307058000000e-04 },
4823 { 0.0008533093128905, 2.4133881688166001e+00, 0.0000000000000000e+00 },
4824 { 0.0003513347911037, 5.9720789850126996e+00, -3.0561017709999999e-05 },
4825 { -0.0001441929255483, 1.0477061764435001e+00, -5.6920632124000004e-04 }
4826};
4827
4828static const vsop_term_t jm_Callisto_a[] =
4829{
4830 { 0.0125879701715314, 0.0000000000000000e+00, 0.0000000000000000e+00 },
4831 { 0.0000035952049470, 6.4965776007116005e-01, 5.0172168165034003e-01 },
4832 { 0.0000027580210652, 1.8084235781510001e+00, 3.1750660413359002e+00 }
4833};
4834
4835static const vsop_term_t jm_Callisto_l[] =
4836{
4837 { 0.0005586040123824, 2.1404207189814999e+00, 1.4500979323100001e-03 },
4838 { -0.0003805813868176, 2.7358844897852999e+00, 2.9729650620000000e-05 },
4839 { 0.0002205152863262, 6.4979652596399995e-01, 5.0172167243580001e-01 },
4840 { 0.0001877895151158, 1.8084787604004999e+00, 3.1750660413359002e+00 },
4841 { 0.0000766916975242, 6.2720114319754998e+00, 1.3928364636651001e+00 },
4842 { 0.0000747056855106, 1.2995916202344000e+00, 1.0034433456728999e+00 }
4843};
4844
4845static const vsop_term_t jm_Callisto_z[] =
4846{
4847 { 0.0073755808467977, 5.5836071576083999e+00, 3.2065099140000001e-05 },
4848 { 0.0002065924169942, 5.9209831565786004e+00, 3.7648624194703001e-01 },
4849 { 0.0001589869764021, 2.8744006242622999e-01, 8.7820792442520001e-01 },
4850 { -0.0001561131605348, 2.1257397865089001e+00, 1.2727441285000001e-04 },
4851 { 0.0001486043380971, 1.4462134301023000e+00, 3.5515522949801999e+00 },
4852 { 0.0000635073108731, 5.9096803285953996e+00, 1.7693227129285001e+00 },
4853 { 0.0000599351698525, 4.1125517584797997e+00, -2.7985797954588998e+00 },
4854 { 0.0000540660842731, 5.5390350845569003e+00, 2.8683408228299999e-03 },
4855 { -0.0000489596900866, 4.6218149483337996e+00, -6.2695712529518999e-01 }
4856};
4857
4858static const vsop_term_t jm_Callisto_zeta[] =
4859{
4860 { 0.0038422977898495, 2.4133922085556998e+00, 0.0000000000000000e+00 },
4861 { 0.0022453891791894, 5.9721736773277003e+00, -3.0561255249999997e-05 },
4862 { -0.0002604479450559, 3.3368746306408998e+00, -1.2491309972000001e-04 },
4863 { 0.0000332112143230, 5.5604137742336999e+00, 2.9003768850700000e-03 }
4864};
4865
4866static const jupiter_moon_t JupiterMoonModel[] =
4867{
4868 { 2.8248942843381399e-07, { 1.4462132960212239e+00, 3.5515522861824000e+00 }
4869 , { 1, jm_Io_a }
4870 , { 4, jm_Io_l }
4871 , { 3, jm_Io_z }
4872 , { 2, jm_Io_zeta } },
4873
4874 { 2.8248327439289299e-07, { -3.7352634374713622e-01, 1.7693227111234699e+00 }
4875 , { 2, jm_Europa_a }
4876 , { 8, jm_Europa_l }
4877 , { 7, jm_Europa_z }
4878 , { 4, jm_Europa_zeta } },
4879
4880 { 2.8249818418472298e-07, { 2.8740893911433479e-01, 8.7820792358932798e-01 }
4881 , { 2, jm_Ganymede_a }
4882 , { 8, jm_Ganymede_l }
4883 , { 8, jm_Ganymede_z }
4884 , { 4, jm_Ganymede_zeta } },
4885
4886 { 2.8249214488990899e-07, { -3.6203412913757038e-01, 3.7648623343382798e-01 }
4887 , { 3, jm_Callisto_a }
4888 , { 6, jm_Callisto_l }
4889 , { 9, jm_Callisto_z }
4890 , { 4, jm_Callisto_zeta } }
4891};
4892
4893static astro_state_vector_t JupiterMoon_elem2pv(astro_time_t time, double mu, const double elem[6])
4894{
4895 /* Translation of FORTRAN subroutine ELEM2PV from: */
4896 /* https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/ */
4897 astro_state_vector_t state;
4898 double EE, DE, CE, SE, DLE, RSAM1, ASR, PHI, PSI, X1, Y1, VX1, VY1, F2, P2, Q2, PQ;
4899
4900 const double A = elem[0];
4901 const double AL = elem[1];
4902 const double K = elem[2];
4903 const double H = elem[3];
4904 const double Q = elem[4];
4905 const double P = elem[5];
4906
4907 const double AN = sqrt(mu / (A*A*A));
4908
4909 EE = AL + K*sin(AL) - H*cos(AL);
4910 do
4911 {
4912 CE = cos(EE);
4913 SE = sin(EE);
4914 DE = (AL - EE + K*SE - H*CE) / (1.0 - K*CE - H*SE);
4915 EE += DE;
4916 }
4917 while (fabs(DE) >= 1.0e-12);
4918
4919 CE = cos(EE);
4920 SE = sin(EE);
4921 DLE = H*CE - K*SE;
4922 RSAM1 = -K*CE - H*SE;
4923 ASR = 1.0/(1.0 + RSAM1);
4924 PHI = sqrt(1.0 - K*K - H*H);
4925 PSI = 1.0/(1.0 + PHI);
4926 X1 = A*(CE - K - PSI*H*DLE);
4927 Y1 = A*(SE - H + PSI*K*DLE);
4928 VX1 = AN*ASR*A*(-SE - PSI*H*RSAM1);
4929 VY1 = AN*ASR*A*(+CE + PSI*K*RSAM1);
4930 F2 = 2.0*sqrt(1.0 - Q*Q - P*P);
4931 P2 = 1.0 - 2.0*P*P;
4932 Q2 = 1.0 - 2.0*Q*Q;
4933 PQ = 2.0*P*Q;
4934
4935 state.x = X1*P2 + Y1*PQ;
4936 state.y = X1*PQ + Y1*Q2;
4937 state.z = (Q*Y1 - X1*P)*F2;
4938
4939 state.vx = VX1*P2 + VY1*PQ;
4940 state.vy = VX1*PQ + VY1*Q2;
4941 state.vz = (Q*VY1 - VX1*P)*F2;
4942
4943 state.t = time;
4944 state.status = ASTRO_SUCCESS;
4945 return state;
4946}
4947
4948static astro_state_vector_t CalcJupiterMoon(astro_time_t time, int mindex)
4949{
4950 /* This is a translation of FORTRAN code by Duriez, Lainey, and Vienne: */
4951 /* https://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/ */
4952
4953 astro_state_vector_t state;
4954 int k;
4955 double arg;
4956 double elem[6];
4957 const jupiter_moon_t *m = &JupiterMoonModel[mindex];
4958 const double t = time.tt + 18262.5; /* t = time since 1950-01-01T00:00:00Z */
4959
4960 /* Calculate 6 orbital elements at the given time t. */
4961
4962 elem[0] = 0.0;
4963 for (k = 0; k < m->a.nterms; ++k)
4964 {
4965 arg = m->a.term[k].phase + (t * m->a.term[k].frequency);
4966 elem[0] += m->a.term[k].amplitude * cos(arg);
4967 }
4968
4969 elem[1] = m->al[0] + (t * m->al[1]);
4970 for (k = 0; k < m->l.nterms; ++k)
4971 {
4972 arg = m->l.term[k].phase + (t * m->l.term[k].frequency);
4973 elem[1] += m->l.term[k].amplitude * sin(arg);
4974 }
4975 elem[1] = fmod(elem[1], PI2);
4976 if (elem[1] < 0.0)
4977 elem[1] += PI2;
4978
4979 elem[2] = elem[3] = 0.0;
4980 for (k = 0; k < m->z.nterms; ++k)
4981 {
4982 arg = m->z.term[k].phase + (t * m->z.term[k].frequency);
4983 elem[2] += m->z.term[k].amplitude * cos(arg);
4984 elem[3] += m->z.term[k].amplitude * sin(arg);
4985 }
4986
4987 elem[4] = elem[5] = 0.0;
4988 for (k = 0; k < m->zeta.nterms; ++k)
4989 {
4990 arg = m->zeta.term[k].phase + (t * m->zeta.term[k].frequency);
4991 elem[4] += m->zeta.term[k].amplitude * cos(arg);
4992 elem[5] += m->zeta.term[k].amplitude * sin(arg);
4993 }
4994
4995 /* Convert the oribital elements into position vectors in the Jupiter equatorial system (JUP). */
4996 state = JupiterMoon_elem2pv(time, m->mu, elem);
4997
4998 /* Re-orient position and velocity vectors from Jupiter-equatorial (JUP) to Earth-equatorial in J2000 (EQJ). */
4999 return Astronomy_RotateState(Rotation_JUP_EQJ, state);
5000}
5001
5002
5003/**
5004 * @brief Calculates jovicentric positions and velocities of Jupiter's largest 4 moons.
5005 *
5006 * Calculates position and velocity vectors for Jupiter's moons
5007 * Io, Europa, Ganymede, and Callisto, at the given date and time.
5008 * The vectors are jovicentric (relative to the center of Jupiter).
5009 * Their orientation is the Earth's equatorial system at the J2000 epoch (EQJ).
5010 * The position components are expressed in astronomical units (AU), and the
5011 * velocity components are in AU/day.
5012 *
5013 * To convert to heliocentric position vectors, call #Astronomy_HelioVector
5014 * with `BODY_JUPITER` to get Jupiter's heliocentric position, then
5015 * add the jovicentric positions.
5016 *
5017 * Likewise, you can call #Astronomy_GeoVector
5018 * with `BODY_JUPITER` to convert to geocentric positions.
5019 * However, you will have to manually correct for light travel time
5020 * from the Jupiter system to Earth to figure out what time to pass
5021 * to `Astronomy_JupiterMoons` to get an accurate picture
5022 * of how Jupiter and its moons look from Earth.
5023 *
5024 * @param time The date and time for which to calculate the position vectors.
5025 * @return Position vectors of Jupiter's largest 4 moons, as described above.
5026 */
5027astro_jupiter_moons_t Astronomy_JupiterMoons(astro_time_t time)
5028{
5029 astro_jupiter_moons_t jm;
5030
5031 jm.io = CalcJupiterMoon(time, 0);
5032 jm.europa = CalcJupiterMoon(time, 1);
5033 jm.ganymede = CalcJupiterMoon(time, 2);
5034 jm.callisto = CalcJupiterMoon(time, 3);
5035
5036 return jm;
5037}
5038
5039/*---------------------- end Jupiter moons ----------------------*/
5040
5041
5042/**
5043 * @brief Calculates heliocentric Cartesian coordinates of a body in the J2000 equatorial system.
5044 *
5045 * This function calculates the position of the given celestial body as a vector,
5046 * using the center of the Sun as the origin. The result is expressed as a Cartesian
5047 * vector in the J2000 equatorial system: the coordinates are based on the mean equator
5048 * of the Earth at noon UTC on 1 January 2000.
5049 *
5050 * The position is not corrected for light travel time or aberration.
5051 * This is different from the behavior of #Astronomy_GeoVector.
5052 *
5053 * If given an invalid value for `body`, this function will fail. The caller should always check
5054 * the `status` field inside the returned #astro_vector_t for `ASTRO_SUCCESS` (success)
5055 * or any other value (failure) before trusting the resulting vector.
5056 *
5057 * @param body
5058 * A body for which to calculate a heliocentric position: the Sun, Moon, any of the planets,
5059 * the Solar System Barycenter (SSB), or the Earth Moon Barycenter (EMB).
5060 * Can also be a star defined by #Astronomy_DefineStar.
5061 * @param time The date and time for which to calculate the position.
5062 * @return A heliocentric position vector of the center of the given body.
5063 */
5064astro_vector_t Astronomy_HelioVector(astro_body_t body, astro_time_t time)
5065{
5066 astro_vector_t vector, earth;
5067 body_state_t bstate;
5068 stardef_t *star;
5069
5070 star = UserDefinedStar(body);
5071 if (star != NULL)
5072 {
5073 astro_spherical_t sphere;
5074 sphere.lat = star->dec;
5075 sphere.lon = 15.0 * star->ra;
5076 sphere.dist = star->dist;
5077 sphere.status = ASTRO_SUCCESS;
5078 return Astronomy_VectorFromSphere(sphere, time);
5079 }
5080
5081 switch (body)
5082 {
5083 case BODY_SUN:
5084 vector.status = ASTRO_SUCCESS;
5085 vector.x = 0.0;
5086 vector.y = 0.0;
5087 vector.z = 0.0;
5088 vector.t = time;
5089 return vector;
5090
5091 case BODY_MERCURY:
5092 case BODY_VENUS:
5093 case BODY_EARTH:
5094 case BODY_MARS:
5095 case BODY_JUPITER:
5096 case BODY_SATURN:
5097 case BODY_URANUS:
5098 case BODY_NEPTUNE:
5099 return CalcVsop(&vsop[body], time);
5100
5101 case BODY_PLUTO:
5102 vector.t = time;
5103 vector.status = CalcPluto(&bstate, time, 1);
5104 if (vector.status != ASTRO_SUCCESS)
5105 {
5106 vector.x = vector.y = vector.z = NAN;
5107 }
5108 else
5109 {
5110 vector.x = bstate.r.x;
5111 vector.y = bstate.r.y;
5112 vector.z = bstate.r.z;
5113 }
5114 return vector;
5115
5116 case BODY_MOON:
5117 vector = Astronomy_GeoMoon(time);
5118 earth = CalcEarth(time);
5119 vector.x += earth.x;
5120 vector.y += earth.y;
5121 vector.z += earth.z;
5122 return vector;
5123
5124 case BODY_EMB:
5125 vector = Astronomy_GeoMoon(time);
5126 earth = CalcEarth(time);
5127 vector.x = earth.x + (vector.x / (1.0 + EARTH_MOON_MASS_RATIO));
5128 vector.y = earth.y + (vector.y / (1.0 + EARTH_MOON_MASS_RATIO));
5129 vector.z = earth.z + (vector.z / (1.0 + EARTH_MOON_MASS_RATIO));
5130 return vector;
5131
5132 case BODY_SSB:
5133 return CalcSolarSystemBarycenter(time);
5134
5135 default:
5136 return VecError(ASTRO_INVALID_BODY, time);
5137 }
5138}
5139
5140/**
5141 * @brief Calculates the distance from a body to the Sun at a given time.
5142 *
5143 * Given a date and time, this function calculates the distance between
5144 * the center of `body` and the center of the Sun.
5145 * For the planets Mercury through Neptune, this function is significantly
5146 * more efficient than calling #Astronomy_HelioVector followed by #Astronomy_VectorLength.
5147 *
5148 * @param body
5149 * A body for which to calculate a heliocentric distance:
5150 * the Sun, Moon, any of the planets, or a user-defined star.
5151 *
5152 * @param time
5153 * The date and time for which to calculate the heliocentric distance.
5154 *
5155 * @return
5156 * If successful, an #astro_func_result_t structure whose `status` is `ASTRO_SUCCESS`
5157 * and whose `value` holds the heliocentric distance in AU.
5158 * Otherwise, `status` reports an error condition.
5159 */
5160astro_func_result_t Astronomy_HelioDistance(astro_body_t body, astro_time_t time)
5161{
5162 astro_vector_t vector;
5163 astro_func_result_t result;
5164 stardef_t *star;
5165
5166 star = UserDefinedStar(body);
5167 if (star != NULL)
5168 {
5169 result.status = ASTRO_SUCCESS;
5170 result.value = star->dist;
5171 return result;
5172 }
5173
5174 switch (body)
5175 {
5176 case BODY_SUN:
5177 result.status = ASTRO_SUCCESS;
5178 result.value = 0.0;
5179 return result;
5180
5181 case BODY_MERCURY:
5182 case BODY_VENUS:
5183 case BODY_EARTH:
5184 case BODY_MARS:
5185 case BODY_JUPITER:
5186 case BODY_SATURN:
5187 case BODY_URANUS:
5188 case BODY_NEPTUNE:
5189 result.status = ASTRO_SUCCESS;
5190 result.value = VsopHelioDistance(&vsop[body], time);
5191 return result;
5192
5193 default:
5194 /* For non-VSOP objects, fall back to taking the length of the heliocentric vector. */
5195 vector = Astronomy_HelioVector(body, time);
5196 if (vector.status != ASTRO_SUCCESS)
5197 return FuncError(vector.status);
5198 result.status = ASTRO_SUCCESS;
5199 result.value = Astronomy_VectorLength(vector);
5200 return result;
5201 }
5202}
5203
5204
5205/**
5206 * @brief Solve for light travel time of a vector function.
5207 *
5208 * When observing a distant object, for example Jupiter as seen from Earth,
5209 * the amount of time it takes for light to travel from the object to the
5210 * observer can significantly affect the object's apparent position.
5211 * This function is a generic solver that figures out how long in the
5212 * past light must have left the observed object to reach the observer
5213 * at the specified observation time. It uses a context/function pair
5214 * as a generic interface that expresses an arbitrary position vector
5215 * as a function of time.
5216 *
5217 * This function repeatedly calls `func`, passing `context` and a series of time
5218 * estimates in the past. Then `func` must return a relative position vector between
5219 * the observer and the target. `Astronomy_CorrectLightTravel` keeps calling
5220 * `func` with more and more refined estimates of the time light must have
5221 * left the target to arrive at the observer.
5222 *
5223 * For common use cases, it is simpler to use #Astronomy_BackdatePosition
5224 * for calculating the light travel time correction of one body observing another body.
5225 *
5226 * For geocentric calculations, #Astronomy_GeoVector also backdates the returned
5227 * position vector for light travel time, only it returns the observation time in
5228 * the returned vector's `t` field rather than the backdated time.
5229 *
5230 * @param context Holds any parameters needed by `func`.
5231 * @param func Pointer to a function that returns a relative position vector as a function of time.
5232 * @param time The observation time for which to solve for light travel delay.
5233 * @return
5234 * The position vector returned by `func` at the solved backdated time.
5235 * On success, the vector will hold `ASTRO_SUCCESS` in its `status` field,
5236 * the backdated time in its `t` field, along with the apparent relative position.
5237 * If an error occurs, `status` will hold an error code and the remaining fields
5238 * should be ignored.
5239 */
5240astro_vector_t Astronomy_CorrectLightTravel(
5241 void *context,
5242 astro_position_func_t func,
5243 astro_time_t time)
5244{
5245 int iter;
5246 astro_time_t ltime, ltime2;
5247 astro_vector_t pos;
5248 double distance, dt;
5249
5250 ltime = time;
5251 for (iter = 0; iter < 10; ++iter)
5252 {
5253 pos = func(context, ltime);
5254 if (pos.status != ASTRO_SUCCESS)
5255 return pos;
5256
5257 distance = Astronomy_VectorLength(pos);
5258
5259 /*
5260 This solver does not support more than one light-day of distance,
5261 because that would cause convergence problems and inaccurate
5262 values for stellar aberration angles.
5263 */
5264 if (distance > C_AUDAY)
5265 return VecError(ASTRO_INVALID_PARAMETER, time);
5266
5267 ltime2 = Astronomy_AddDays(time, -distance/C_AUDAY);
5268 dt = fabs(ltime2.tt - ltime.tt);
5269 if (dt < 1.0e-9) /* 86.4 microseconds */
5270 return pos;
5271
5272 ltime = ltime2;
5273 }
5274 return VecError(ASTRO_NO_CONVERGE, time); /* light travel time solver did not converge */
5275}
5276
5277
5278/** @cond DOXYGEN_SKIP */
5279typedef struct
5280{
5281 astro_body_t observerBody;
5282 astro_body_t targetBody;
5283 astro_aberration_t aberration;
5284 astro_vector_t observerPos; /* used only when aberration == NO_ABERRATION */
5285}
5286backdate_context_t;
5287/** @endcond */
5288
5289
5290static astro_vector_t BodyPosition(void *context, astro_time_t time)
5291{
5292 const backdate_context_t *b = (const backdate_context_t *)context;
5293 astro_vector_t observerPos, pos;
5294
5295 if (b->aberration == NO_ABERRATION)
5296 {
5297 /* No aberration, so use the pre-calculated initial position of the observer body. */
5298 observerPos = b->observerPos;
5299 }
5300 else
5301 {
5302 /*
5303 The following discussion is worded with the observer body being the Earth,
5304 which is often the case. However, the same reasoning applies to any observer body
5305 without loss of generality.
5306
5307 To include aberration, make a good first-order approximation
5308 by backdating the Earth's position also.
5309 This is confusing, but it works for objects within the Solar System
5310 because the distance the Earth moves in that small amount of light
5311 travel time (a few minutes to a few hours) is well approximated
5312 by a line segment that substends the angle seen from the remote
5313 body viewing Earth. That angle is pretty close to the aberration
5314 angle of the moving Earth viewing the remote body.
5315 In other words, both of the following approximate the aberration angle:
5316 (transverse distance Earth moves) / (distance to body)
5317 (transverse speed of Earth) / (speed of light).
5318 */
5319 observerPos = Astronomy_HelioVector(b->observerBody, time);
5320 }
5321
5322 if (observerPos.status != ASTRO_SUCCESS)
5323 return observerPos;
5324
5325 pos = Astronomy_HelioVector(b->targetBody, time);
5326 if (pos.status == ASTRO_SUCCESS)
5327 {
5328 /* Convert heliocentric body position to observer-centric position. */
5329 pos.x -= observerPos.x;
5330 pos.y -= observerPos.y;
5331 pos.z -= observerPos.z;
5332 }
5333 return pos;
5334}
5335
5336
5337/**
5338 * @brief Solve for light travel time correction of apparent position.
5339 *
5340 * When observing a distant object, for example Jupiter as seen from Earth,
5341 * the amount of time it takes for light to travel from the object to the
5342 * observer can significantly affect the object's apparent position.
5343 *
5344 * This function solves the light travel time correction for the apparent
5345 * relative position vector of a target body as seen by an observer body
5346 * at a given observation time.
5347 *
5348 * For geocentric calculations, #Astronomy_GeoVector also includes light
5349 * travel time correction, but the time `t` embedded in its returned vector
5350 * refers to the observation time, not the backdated time that light left
5351 * the observed body. Thus `Astronomy_BackdatePosition` provides direct
5352 * access to the light departure time for callers that need it.
5353 *
5354 * For a more generalized light travel correction solver, see #Astronomy_CorrectLightTravel.
5355 *
5356 * @param time The time of observation.
5357 * @param observerBody The body to be used as the observation location.
5358 * @param targetBody The body to be observed.
5359 * @param aberration `ABERRATION` to correct for aberration, or `NO_ABERRATION` to leave uncorrected.
5360 *
5361 * @return
5362 * On success, the position vector at the solved backdated time.
5363 * The returned vector will hold `ASTRO_SUCCESS` in its `status` field,
5364 * the backdated time in its `t` field, along with the apparent relative position.
5365 * If an error occurs, `status` will hold an error code and the remaining fields should be ignored.
5366 */
5367astro_vector_t Astronomy_BackdatePosition(
5368 astro_time_t time,
5369 astro_body_t observerBody,
5370 astro_body_t targetBody,
5371 astro_aberration_t aberration)
5372{
5373 if (UserDefinedStar(targetBody))
5374 {
5375 /*
5376 This is a user-defined star, which must be treated as a special case.
5377 First, we assume its heliocentric position does not change with time.
5378 Second, we assume its heliocentric position has already been corrected
5379 for light-travel time, its coordinates given as it appears on Earth at the present.
5380 Therefore, no backdating is applied.
5381 */
5382 astro_vector_t ovec, tvec, vec;
5383 double rx, ry, rz, s;
5384 astro_state_vector_t ostate;
5385
5386 tvec = Astronomy_HelioVector(targetBody, time);
5387 if (tvec.status != ASTRO_SUCCESS)
5388 return tvec;
5389
5390 switch (aberration)
5391 {
5392 case NO_ABERRATION:
5393 /* Return the star's position as seen from the observer. */
5394 ovec = Astronomy_HelioVector(observerBody, time);
5395 if (ovec.status != ASTRO_SUCCESS)
5396 return ovec;
5397 vec.x = tvec.x - ovec.x;
5398 vec.y = tvec.y - ovec.y;
5399 vec.z = tvec.z - ovec.z;
5400 vec.t = time;
5401 vec.status = ASTRO_SUCCESS;
5402 return vec;
5403
5404 case ABERRATION:
5405 /*
5406 (Observer velocity) - (light vector) = (Aberration-corrected direction to target body).
5407 Note that this is an approximation, because technically the light vector should
5408 be measured in barycentric coordinates, not heliocentric. The error is very small.
5409 */
5410 ostate = Astronomy_HelioState(observerBody, time);
5411 if (ostate.status != ASTRO_SUCCESS)
5412 return VecError(ostate.status, time);
5413
5414 rx = tvec.x - ostate.x;
5415 ry = tvec.y - ostate.y;
5416 rz = tvec.z - ostate.z;
5417 s = C_AUDAY / sqrt(rx*rx + ry*ry + rz*rz);
5418
5419 vec.x = rx + ostate.vx/s;
5420 vec.y = ry + ostate.vy/s;
5421 vec.z = rz + ostate.vz/s;
5422 vec.t = time;
5423 vec.status = ASTRO_SUCCESS;
5424 return vec;
5425
5426 default:
5427 return VecError(ASTRO_INVALID_PARAMETER, time);
5428 }
5429 }
5430 else
5431 {
5432 backdate_context_t context;
5433
5434 context.observerBody = observerBody;
5435 context.targetBody = targetBody;
5436 context.aberration = aberration;
5437 switch (aberration)
5438 {
5439 case NO_ABERRATION:
5440 /* Without aberration, we need the observer body position at the observation time only. */
5441 /* For efficiency, calculate it once and hold onto it, so `BodyPosition` can keep using it. */
5442 context.observerPos = Astronomy_HelioVector(observerBody, time);
5443 break;
5444
5445 case ABERRATION:
5446 /* With aberration, `BackdatePosition` will calculate the observer body state at different times. */
5447 /* Therefore, do not waste time calculating it at the observation time. */
5448 /* Initialize the memory with an explicitly invalid value. */
5449 context.observerPos = VecError(ASTRO_NOT_INITIALIZED, time);
5450 break;
5451
5452 default:
5453 return VecError(ASTRO_INVALID_PARAMETER, time);
5454 }
5455
5456 return Astronomy_CorrectLightTravel(&context, BodyPosition, time);
5457 }
5458}
5459
5460
5461/**
5462 * @brief Calculates geocentric Cartesian coordinates of a body in the J2000 equatorial system.
5463 *
5464 * This function calculates the position of the given celestial body as a vector,
5465 * using the center of the Earth as the origin. The result is expressed as a Cartesian
5466 * vector in the J2000 equatorial system: the coordinates are based on the mean equator
5467 * of the Earth at noon UTC on 1 January 2000.
5468 *
5469 * If given an invalid value for `body`, this function will fail. The caller should always check
5470 * the `status` field inside the returned #astro_vector_t for `ASTRO_SUCCESS` (success)
5471 * or any other value (failure) before trusting the resulting vector.
5472 *
5473 * Unlike #Astronomy_HelioVector, this function corrects for light travel time.
5474 * This means the position of the body is "back-dated" by the amount of time it takes
5475 * light to travel from that body to an observer on the Earth.
5476 *
5477 * Also, the position can optionally be corrected for
5478 * [aberration](https://en.wikipedia.org/wiki/Aberration_of_light), an effect
5479 * causing the apparent direction of the body to be shifted due to transverse
5480 * movement of the Earth with respect to the rays of light coming from that body.
5481 *
5482 * @param body
5483 * A body for which to calculate a heliocentric position: the Sun, Moon, or any of the planets.
5484 * Can also be a star defined by #Astronomy_DefineStar.
5485 * @param time The date and time for which to calculate the position.
5486 * @param aberration `ABERRATION` to correct for aberration, or `NO_ABERRATION` to leave uncorrected.
5487 * @return A geocentric position vector of the center of the given body.
5488 */
5489astro_vector_t Astronomy_GeoVector(astro_body_t body, astro_time_t time, astro_aberration_t aberration)
5490{
5491 astro_vector_t vector;
5492
5493 switch (body)
5494 {
5495 case BODY_EARTH:
5496 /* The Earth's geocentric coordinates are always (0,0,0). */
5497 vector.status = ASTRO_SUCCESS;
5498 vector.x = 0.0;
5499 vector.y = 0.0;
5500 vector.z = 0.0;
5501 break;
5502
5503 case BODY_MOON:
5504 /* The moon is so close, aberration and light travel time don't matter. */
5505 vector = Astronomy_GeoMoon(time);
5506 break;
5507
5508 default:
5509 /* For all other bodies, apply light travel time correction. */
5510 vector = Astronomy_BackdatePosition(time, BODY_EARTH, body, aberration);
5511 break;
5512 }
5513
5514 vector.t = time; /* tricky: return the observation time, not the backdated time */
5515 return vector;
5516}
5517
5518
5519/**
5520 * @brief Calculates barycentric position and velocity vectors for the given body.
5521 *
5522 * Given a body and a time, calculates the barycentric position and velocity
5523 * vectors for the center of that body at that time.
5524 * The vectors are expressed in J2000 mean equator coordinates (EQJ).
5525 *
5526 * @param body
5527 * The celestial body whose barycentric state vector is to be calculated.
5528 * Supported values are `BODY_SUN`, `BODY_MOON`, `BODY_EMB`, `BODY_SSB`, and all planets:
5529 * `BODY_MERCURY`, `BODY_VENUS`, `BODY_EARTH`, `BODY_MARS`, `BODY_JUPITER`,
5530 * `BODY_SATURN`, `BODY_URANUS`, `BODY_NEPTUNE`, `BODY_PLUTO`.
5531 * @param time
5532 * The date and time for which to calculate position and velocity.
5533 * @return
5534 * A structure that contains barycentric position and velocity vectors.
5535 */
5536astro_state_vector_t Astronomy_BaryState(astro_body_t body, astro_time_t time)
5537{
5538 astro_state_vector_t state;
5539 major_bodies_t bary;
5540 body_state_t planet, earth;
5541
5542 if (body == BODY_SSB)
5543 {
5544 /* Trivial case: the solar system barycenter itself. */
5545 state.status = ASTRO_SUCCESS;
5546 state.x = state.y = state.z = 0.0;
5547 state.vx = state.vy = state.vz = 0.0;
5548 state.t = time;
5549 return state;
5550 }
5551
5552 if (body == BODY_PLUTO)
5553 {
5554 astro_status_t status = CalcPluto(&planet, time, 0);
5555 if (status != ASTRO_SUCCESS)
5556 return StateVecError(status, time);
5557 return ExportState(planet, time);
5558 }
5559
5560 MajorBodyBary(&bary, time.tt);
5561
5562 switch (body)
5563 {
5564 /* If the caller is asking for one of the major bodies, we can immediately return the answer. */
5565 case BODY_SUN: return ExportState(bary.Sun, time);
5566 case BODY_JUPITER: return ExportState(bary.Jupiter, time);
5567 case BODY_SATURN: return ExportState(bary.Saturn, time);
5568 case BODY_URANUS: return ExportState(bary.Uranus, time);
5569 case BODY_NEPTUNE: return ExportState(bary.Neptune, time);
5570
5571 /* Handle the remaining VSOP bodies: Mercury, Venus, Earth, Mars. */
5572 case BODY_MERCURY:
5573 case BODY_VENUS:
5574 case BODY_EARTH:
5575 case BODY_MARS:
5576 planet = CalcVsopPosVel(&vsop[body], time.tt);
5577 /* BarySun + HelioBody = BaryBody */
5578 state.x = bary.Sun.r.x + planet.r.x;
5579 state.y = bary.Sun.r.y + planet.r.y;
5580 state.z = bary.Sun.r.z + planet.r.z;
5581 state.vx = bary.Sun.v.x + planet.v.x;
5582 state.vy = bary.Sun.v.y + planet.v.y;
5583 state.vz = bary.Sun.v.z + planet.v.z;
5584 state.t = time;
5585 state.status = ASTRO_SUCCESS;
5586 return state;
5587
5588 case BODY_MOON:
5589 case BODY_EMB:
5590 earth = CalcVsopPosVel(&vsop[BODY_EARTH], time.tt);
5591 if (body == BODY_MOON)
5592 state = Astronomy_GeoMoonState(time);
5593 else
5594 state = Astronomy_GeoEmbState(time);
5595 state.x += bary.Sun.r.x + earth.r.x;
5596 state.y += bary.Sun.r.y + earth.r.y;
5597 state.z += bary.Sun.r.z + earth.r.z;
5598 state.vx += bary.Sun.v.x + earth.v.x;
5599 state.vy += bary.Sun.v.y + earth.v.y;
5600 state.vz += bary.Sun.v.z + earth.v.z;
5601 return state;
5602
5603 default:
5604 return StateVecError(ASTRO_INVALID_BODY, time);
5605 }
5606}
5607
5608
5609/**
5610 * @brief Calculates heliocentric position and velocity vectors for the given body.
5611 *
5612 * Given a body and a time, calculates the position and velocity
5613 * vectors for the center of that body at that time, relative to the center of the Sun.
5614 * The vectors are expressed in J2000 mean equator coordinates (EQJ).
5615 * If you need the position vector only, it is more efficient to call #Astronomy_HelioVector.
5616 * The Sun's center is a non-inertial frame of reference. In other words, the Sun
5617 * experiences acceleration due to gravitational forces, mostly from the larger
5618 * planets (Jupiter, Saturn, Uranus, and Neptune). If you want to calculate momentum,
5619 * kinetic energy, or other quantities that require a non-accelerating frame
5620 * of reference, consider using #Astronomy_BaryState instead.
5621 *
5622 * @param body
5623 * The celestial body whose heliocentric state vector is to be calculated.
5624 * Supported values are `BODY_SUN`, `BODY_MOON`, `BODY_EMB`, `BODY_SSB`, and all planets:
5625 * `BODY_MERCURY`, `BODY_VENUS`, `BODY_EARTH`, `BODY_MARS`, `BODY_JUPITER`,
5626 * `BODY_SATURN`, `BODY_URANUS`, `BODY_NEPTUNE`, `BODY_PLUTO`.
5627 * Also allowed to be a user-defined star created by #Astronomy_DefineStar.
5628 * @param time
5629 * The date and time for which to calculate position and velocity.
5630 * @return
5631 * A structure that contains heliocentric position and velocity vectors.
5632 */
5633astro_state_vector_t Astronomy_HelioState(astro_body_t body, astro_time_t time)
5634{
5635 astro_status_t status;
5636 astro_state_vector_t state;
5637 major_bodies_t bary;
5638 body_state_t planet, earth;
5639
5640 if (UserDefinedStar(body))
5641 {
5642 astro_vector_t vec = Astronomy_HelioVector(body, time);
5643 state.x = vec.x;
5644 state.y = vec.y;
5645 state.z = vec.z;
5646 state.vx = state.vy = state.vz = 0.0;
5647 state.t = time;
5648 state.status = vec.status;
5649 return state;
5650 }
5651
5652 switch (body)
5653 {
5654 case BODY_SUN:
5655 /* Trivial case: the Sun is the origin of the heliocentric frame. */
5656 state.status = ASTRO_SUCCESS;
5657 state.x = state.y = state.z = 0.0;
5658 state.vx = state.vy = state.vz = 0.0;
5659 state.t = time;
5660 return state;
5661
5662 case BODY_SSB:
5663 /* Calculate the barycentric Sun. Then the negative of that is the heliocentric SSB. */
5664 MajorBodyBary(&bary, time.tt);
5665 state.x = -bary.Sun.r.x;
5666 state.y = -bary.Sun.r.y;
5667 state.z = -bary.Sun.r.z;
5668 state.vx = -bary.Sun.v.x;
5669 state.vy = -bary.Sun.v.y;
5670 state.vz = -bary.Sun.v.z;
5671 state.t = time;
5672 state.status = ASTRO_SUCCESS;
5673 return state;
5674
5675 case BODY_MERCURY:
5676 case BODY_VENUS:
5677 case BODY_EARTH:
5678 case BODY_MARS:
5679 case BODY_JUPITER:
5680 case BODY_SATURN:
5681 case BODY_URANUS:
5682 case BODY_NEPTUNE:
5683 /* Planets included in the VSOP87 model. */
5684 planet = CalcVsopPosVel(&vsop[body], time.tt);
5685 return ExportState(planet, time);
5686
5687 case BODY_PLUTO:
5688 status = CalcPluto(&planet, time, 1);
5689 if (status != ASTRO_SUCCESS)
5690 return StateVecError(status, time);
5691 return ExportState(planet, time);
5692
5693 case BODY_MOON:
5694 case BODY_EMB:
5695 earth = CalcVsopPosVel(&vsop[BODY_EARTH], time.tt);
5696 if (body == BODY_MOON)
5697 state = Astronomy_GeoMoonState(time);
5698 else
5699 state = Astronomy_GeoEmbState(time);
5700 state.x += earth.r.x;
5701 state.y += earth.r.y;
5702 state.z += earth.r.z;
5703 state.vx += earth.v.x;
5704 state.vy += earth.v.y;
5705 state.vz += earth.v.z;
5706 return state;
5707
5708 default:
5709 return StateVecError(ASTRO_INVALID_BODY, time);
5710 }
5711}
5712
5713
5714/**
5715 * @brief Returns the product of mass and universal gravitational constant of a Solar System body.
5716 *
5717 * For problems involving the gravitational interactions of Solar System bodies,
5718 * it is helpful to know the product GM, where G = the universal gravitational constant
5719 * and M = the mass of the body. In practice, GM is known to a higher precision than
5720 * either G or M alone, and thus using the product results in the most accurate results.
5721 * This function returns the product GM in the units au^3/day^2, or 0 for invalid bodies.
5722 * The values come from page 10 of a
5723 * [JPL memorandum regarding the DE405/LE405 ephemeris](https://web.archive.org/web/20120220062549/http://iau-comm4.jpl.nasa.gov/de405iom/de405iom.pdf).
5724 *
5725 * @param body The body for which to find the GM product.
5726 * @return The mass product of the given body in au^3/day^2.
5727 */
5728double Astronomy_MassProduct(astro_body_t body)
5729{
5730 switch (body)
5731 {
5732 case BODY_SUN: return SUN_GM;
5733 case BODY_MERCURY: return MERCURY_GM;
5734 case BODY_VENUS: return VENUS_GM;
5735 case BODY_EARTH: return EARTH_GM;
5736 case BODY_MOON: return MOON_GM;
5737 case BODY_EMB: return EARTH_GM + MOON_GM;
5738 case BODY_MARS: return MARS_GM;
5739 case BODY_JUPITER: return JUPITER_GM;
5740 case BODY_SATURN: return SATURN_GM;
5741 case BODY_URANUS: return URANUS_GM;
5742 case BODY_NEPTUNE: return NEPTUNE_GM;
5743 case BODY_PLUTO: return PLUTO_GM;
5744 default: return 0.0; /* invalid body */
5745 }
5746}
5747
5748
5749/**
5750 * @brief Calculates one of the 5 Lagrange points for a pair of co-orbiting bodies.
5751 *
5752 * Given a more massive "major" body and a much less massive "minor" body,
5753 * calculates one of the five Lagrange points in relation to the minor body's
5754 * orbit around the major body. The parameter `point` is an integer that
5755 * selects the Lagrange point as follows:
5756 *
5757 * 1 = the Lagrange point between the major body and minor body.
5758 * 2 = the Lagrange point on the far side of the minor body.
5759 * 3 = the Lagrange point on the far side of the major body.
5760 * 4 = the Lagrange point 60 degrees ahead of the minor body's orbital position.
5761 * 5 = the Lagrange point 60 degrees behind the minor body's orbital position.
5762 *
5763 * The function returns the state vector for the selected Lagrange point
5764 * in J2000 mean equator coordinates (EQJ), with respect to the center of the
5765 * major body.
5766 *
5767 * To calculate Sun/Earth Lagrange points, pass in `BODY_SUN` for `major_body`
5768 * and `BODY_EMB` (Earth/Moon barycenter) for `minor_body`.
5769 * For Lagrange points of the Sun and any other planet, pass in that planet
5770 * (e.g. `BODY_JUPITER`) for `minor_body`.
5771 * To calculate Earth/Moon Lagrange points, pass in `BODY_EARTH` and `BODY_MOON`
5772 * for the major and minor bodies respectively.
5773 *
5774 * In some cases, it may be more efficient to call #Astronomy_LagrangePointFast,
5775 * especially when the state vectors have already been calculated, or are needed
5776 * for some other purpose.
5777 *
5778 * @param point A value 1..5 that selects which of the Lagrange points to calculate.
5779 * @param time The time at which the Lagrange point is to be calculated.
5780 * @param major_body The more massive of the co-orbiting bodies: `BODY_SUN` or `BODY_EARTH`.
5781 * @param minor_body The less massive of the co-orbiting bodies. See main remarks.
5782 * @return The position and velocity of the selected Lagrange point with respect to the major body's center.
5783 */
5784astro_state_vector_t Astronomy_LagrangePoint(
5785 int point,
5786 astro_time_t time,
5787 astro_body_t major_body,
5788 astro_body_t minor_body)
5789{
5790 astro_state_vector_t major_state, minor_state;
5791 double major_mass, minor_mass;
5792
5793 major_mass = Astronomy_MassProduct(major_body);
5794 if (major_mass <= 0.0)
5795 return StateVecError(ASTRO_INVALID_BODY, time);
5796
5797 minor_mass = Astronomy_MassProduct(minor_body);
5798 if (minor_mass <= 0.0)
5799 return StateVecError(ASTRO_INVALID_BODY, time);
5800
5801 /* Calculate the state vectors for the major and minor bodies. */
5802 if (major_body == BODY_EARTH && minor_body == BODY_MOON)
5803 {
5804 /* Use geocentric calculations for more precision. */
5805
5806 /* The Earth's geocentric state is trivial. */
5807 major_state.status = ASTRO_SUCCESS;
5808 major_state.t = time;
5809 major_state.x = major_state.y = major_state.z = 0.0;
5810 major_state.vx = major_state.vy = major_state.vz = 0.0;
5811
5812 minor_state = Astronomy_GeoMoonState(time);
5813 if (minor_state.status != ASTRO_SUCCESS)
5814 return minor_state;
5815 }
5816 else
5817 {
5818 major_state = Astronomy_HelioState(major_body, time);
5819 if (major_state.status != ASTRO_SUCCESS)
5820 return major_state;
5821
5822 minor_state = Astronomy_HelioState(minor_body, time);
5823 if (minor_state.status != ASTRO_SUCCESS)
5824 return minor_state;
5825 }
5826
5827 return Astronomy_LagrangePointFast(
5828 point,
5829 major_state,
5830 major_mass,
5831 minor_state,
5832 minor_mass
5833 );
5834}
5835
5836
5837/**
5838 * @brief Calculates one of the 5 Lagrange points from body masses and state vectors.
5839 *
5840 * Given a more massive "major" body and a much less massive "minor" body,
5841 * calculates one of the five Lagrange points in relation to the minor body's
5842 * orbit around the major body. The parameter `point` is an integer that
5843 * selects the Lagrange point as follows:
5844 *
5845 * 1 = the Lagrange point between the major body and minor body.
5846 * 2 = the Lagrange point on the far side of the minor body.
5847 * 3 = the Lagrange point on the far side of the major body.
5848 * 4 = the Lagrange point 60 degrees ahead of the minor body's orbital position.
5849 * 5 = the Lagrange point 60 degrees behind the minor body's orbital position.
5850 *
5851 * The caller passes in the state vector and mass for both bodies.
5852 * The state vectors can be in any orientation and frame of reference.
5853 * The body masses are expressed as GM products, where G = the universal
5854 * gravitation constant and M = the body's mass. Thus the units for
5855 * `major_mass` and `minor_mass` must be au^3/day^2.
5856 * Use #Astronomy_MassProduct to obtain GM values for various solar system bodies.
5857 *
5858 * The function returns the state vector for the selected Lagrange point
5859 * using the same orientation as the state vector parameters `major_state` and `minor_state`,
5860 * and the position and velocity components are with respect to the major body's center.
5861 *
5862 * Consider calling #Astronomy_LagrangePoint, instead of this function, for simpler usage in most cases.
5863 *
5864 * @param point A value 1..5 that selects which of the Lagrange points to calculate.
5865 * @param major_state The state vector of the major (more massive) of the pair of bodies.
5866 * @param major_mass The mass product GM of the major body.
5867 * @param minor_state The state vector of the minor (less massive) of the pair of bodies.
5868 * @param minor_mass The mass product GM of the minor body.
5869 * @return The position and velocity of the selected Lagrange point with respect to the major body's center.
5870 */
5871astro_state_vector_t Astronomy_LagrangePointFast(
5872 int point,
5873 astro_state_vector_t major_state,
5874 double major_mass,
5875 astro_state_vector_t minor_state,
5876 double minor_mass)
5877{
5878 const double cos_60 = 0.5;
5879 const double sin_60 = 0.8660254037844386; /* sqrt(3) / 2 */
5880 double scale, dx, dy, dz;
5881 double vx, vy, vz;
5882 double R2, R, r1, r2, x, deltax, dr1, dr2, numer1, numer2, omega2, accel, deriv;
5883 astro_state_vector_t p;
5884
5885 if (point < 1 || point > 5)
5886 return StateVecError(ASTRO_INVALID_PARAMETER, major_state.t);
5887
5888 if (major_state.status != ASTRO_SUCCESS || minor_state.status != ASTRO_SUCCESS)
5889 return StateVecError(ASTRO_INVALID_PARAMETER, major_state.t);
5890
5891 if (!isfinite(major_mass) || major_mass <= 0.0)
5892 return StateVecError(ASTRO_INVALID_PARAMETER, major_state.t);
5893
5894 if (!isfinite(minor_mass) || minor_mass <= 0.0)
5895 return StateVecError(ASTRO_INVALID_PARAMETER, major_state.t);
5896
5897 /* Find the relative position vector <dx, dy, dz>. */
5898 dx = minor_state.x - major_state.x;
5899 dy = minor_state.y - major_state.y;
5900 dz = minor_state.z - major_state.z;
5901 R2 = (dx*dx + dy*dy + dz*dz);
5902
5903 /* R = Total distance between the bodies. */
5904 R = sqrt(R2);
5905
5906 /* Find the velocity vector <vx, vy, vz>. */
5907 vx = minor_state.vx - major_state.vx;
5908 vy = minor_state.vy - major_state.vy;
5909 vz = minor_state.vz - major_state.vz;
5910
5911 if (point == 4 || point == 5)
5912 {
5913 double nx, ny, nz;
5914 double ux, uy, uz, U;
5915 double Dx, Dy, Dz;
5916 double Ux, Uy, Uz;
5917 double vert, vrad, vtan;
5918
5919 /*
5920 For L4 and L5, we need to find points 60 degrees away from the
5921 line connecting the two bodies and in the instantaneous orbital plane.
5922 Define the instantaneous orbital plane as the unique plane that contains
5923 both the relative position vector and the relative velocity vector.
5924 */
5925
5926 /* Take the cross product of position and velocity to find a normal vector <nx, ny, nz>. */
5927 nx = dy*vz - dz*vy;
5928 ny = dz*vx - dx*vz;
5929 nz = dx*vy - dy*vx;
5930
5931 /* Take the cross product normal*position to get a tangential vector <ux, uy, uz>. */
5932 ux = ny*dz - nz*dy;
5933 uy = nz*dx - nx*dz;
5934 uz = nx*dy - ny*dx;
5935
5936 /* Convert the tangential direction vector to a unit vector. */
5937 U = sqrt(ux*ux + uy*uy + uz*uz);
5938 ux /= U;
5939 uy /= U;
5940 uz /= U;
5941
5942 /* Convert the relative position vector into a unit vector. */
5943 dx /= R;
5944 dy /= R;
5945 dz /= R;
5946
5947 /* Now we have two perpendicular unit vectors in the orbital plane: 'd' and 'u'. */
5948
5949 /* Create new unit vectors rotated (+/-)60 degrees from the radius/tangent directions. */
5950 vert = (point == 4) ? +sin_60 : -sin_60;
5951
5952 /* Rotated radial vector */
5953 Dx = cos_60*dx + vert*ux;
5954 Dy = cos_60*dy + vert*uy;
5955 Dz = cos_60*dz + vert*uz;
5956
5957 /* Rotated tangent vector */
5958 Ux = cos_60*ux - vert*dx;
5959 Uy = cos_60*uy - vert*dy;
5960 Uz = cos_60*uz - vert*dz;
5961
5962 /* Calculate L4/L5 positions relative to the major body. */
5963 p.x = R * Dx;
5964 p.y = R * Dy;
5965 p.z = R * Dz;
5966
5967 /* Use dot products to find radial and tangential components of the relative velocity. */
5968 vrad = vx*dx + vy*dy + vz*dz;
5969 vtan = vx*ux + vy*uy + vz*uz;
5970
5971 /* Calculate L4/L5 velocities. */
5972 p.vx = vrad*Dx + vtan*Ux;
5973 p.vy = vrad*Dy + vtan*Uy;
5974 p.vz = vrad*Dz + vtan*Uz;
5975 }
5976 else
5977 {
5978 /*
5979 Calculate the distances of each body from their mutual barycenter.
5980 r1 = negative distance of major mass from barycenter (e.g. Sun to the left of barycenter)
5981 r2 = positive distance of minor mass from barycenter (e.g. Earth to the right of barycenter)
5982 */
5983 r1 = -R * (minor_mass / (major_mass + minor_mass));
5984 r2 = +R * (major_mass / (major_mass + minor_mass));
5985
5986 /* Calculate the square of the angular orbital speed in [rad^2 / day^2]. */
5987 omega2 = (major_mass + minor_mass) / (R2*R);
5988
5989 /*
5990 Use Newton's Method to numerically solve for the location where
5991 outward centrifugal acceleration in the rotating frame of reference
5992 is equal to net inward gravitational acceleration.
5993 First derive a good initial guess based on approximate analysis.
5994 */
5995 if (point == 1 || point == 2)
5996 {
5997 scale = (major_mass / (major_mass + minor_mass)) * cbrt(minor_mass / (3.0 * major_mass));
5998 numer1 = -major_mass; /* The major mass is to the left of L1 and L2 */
5999 if (point == 1)
6000 {
6001 scale = 1.0 - scale;
6002 numer2 = +minor_mass; /* The minor mass is to the right of L1. */
6003 }
6004 else
6005 {
6006 scale = 1.0 + scale;
6007 numer2 = -minor_mass; /* The minor mass is to the left of L2. */
6008 }
6009 }
6010 else /* point == 3 */
6011 {
6012 scale = ((7.0/12.0)*minor_mass - major_mass) / (minor_mass + major_mass);
6013 numer1 = +major_mass; /* major mass is to the right of L3. */
6014 numer2 = +minor_mass; /* minor mass is to the right of L3. */
6015 }
6016
6017 /* Iterate Newton's Method until it converges. */
6018 x = R*scale - r1;
6019 do
6020 {
6021 dr1 = x - r1;
6022 dr2 = x - r2;
6023 accel = omega2*x + numer1/(dr1*dr1) + numer2/(dr2*dr2);
6024 deriv = omega2 - 2*numer1/(dr1*dr1*dr1) - 2*numer2/(dr2*dr2*dr2);
6025 deltax = accel/deriv;
6026 x -= deltax;
6027 }
6028 while (fabs(deltax/R) > 1.0e-14);
6029 scale = (x - r1) / R;
6030
6031 p.x = scale * dx;
6032 p.y = scale * dy;
6033 p.z = scale * dz;
6034 p.vx = scale * vx;
6035 p.vy = scale * vy;
6036 p.vz = scale * vz;
6037 }
6038 p.t = major_state.t;
6039 p.status = ASTRO_SUCCESS;
6040 return p;
6041}
6042
6043
6044/**
6045 * @brief Calculates equatorial coordinates of a celestial body as seen by an observer on the Earth's surface.
6046 *
6047 * Calculates topocentric equatorial coordinates in one of two different systems:
6048 * J2000 or true-equator-of-date, depending on the value of the `equdate` parameter.
6049 * Equatorial coordinates include right ascension, declination, and distance in astronomical units.
6050 *
6051 * This function corrects for light travel time: it adjusts the apparent location
6052 * of the observed body based on how long it takes for light to travel from the body to the Earth.
6053 *
6054 * This function corrects for *topocentric parallax*, meaning that it adjusts for the
6055 * angular shift depending on where the observer is located on the Earth. This is most
6056 * significant for the Moon, because it is so close to the Earth. However, parallax corection
6057 * has a small effect on the apparent positions of other bodies.
6058 *
6059 * Correction for aberration is optional, using the `aberration` parameter.
6060 *
6061 * @param body The celestial body to be observed. Not allowed to be `BODY_EARTH`.
6062 * @param time The date and time at which the observation takes place.
6063 * @param observer A location on or near the surface of the Earth.
6064 * @param equdate Selects the date of the Earth's equator in which to express the equatorial coordinates.
6065 * @param aberration Selects whether or not to correct for aberration.
6066 * @return Topocentric equatorial coordinates of the celestial body.
6067 */
6068astro_equatorial_t Astronomy_Equator(
6069 astro_body_t body,
6070 astro_time_t *time,
6071 astro_observer_t observer,
6072 astro_equator_date_t equdate,
6073 astro_aberration_t aberration)
6074{
6075 astro_equatorial_t equ;
6076 astro_vector_t gc;
6077 double gc_observer[3];
6078 double j2000[3];
6079 double temp[3];
6080 double datevect[3];
6081
6082 if (time == NULL)
6083 return EquError(ASTRO_INVALID_PARAMETER);
6084
6085 /* Calculate the geocentric location of the observer. */
6086 geo_pos(time, observer, gc_observer);
6087
6088 /* Calculate the geocentric location of the body. */
6089 gc = Astronomy_GeoVector(body, *time, aberration);
6090 if (gc.status != ASTRO_SUCCESS)
6091 return EquError(gc.status);
6092
6093 /* Convert geocentric coordinates to topocentric coordinates. */
6094 j2000[0] = gc.x - gc_observer[0];
6095 j2000[1] = gc.y - gc_observer[1];
6096 j2000[2] = gc.z - gc_observer[2];
6097
6098 switch (equdate)
6099 {
6100 case EQUATOR_OF_DATE:
6101 precession(j2000, *time, FROM_2000, temp);
6102 nutation(temp, time, FROM_2000, datevect);
6103 equ = vector2radec(datevect, *time);
6104 return equ;
6105
6106 case EQUATOR_J2000:
6107 equ = vector2radec(j2000, *time);
6108 return equ;
6109
6110 default:
6111 return EquError(ASTRO_INVALID_PARAMETER);
6112 }
6113}
6114
6115/**
6116 * @brief Calculates geocentric equatorial coordinates of an observer on the surface of the Earth.
6117 *
6118 * This function calculates a vector from the center of the Earth to
6119 * a point on or near the surface of the Earth, expressed in equatorial
6120 * coordinates. It takes into account the rotation of the Earth at the given
6121 * time, along with the given latitude, longitude, and elevation of the observer.
6122 *
6123 * The caller may pass a value in `equdate` to select either `EQUATOR_J2000`
6124 * for using J2000 coordinates, or `EQUATOR_OF_DATE` for using coordinates relative
6125 * to the Earth's equator at the specified time.
6126 *
6127 * The returned vector has components expressed in astronomical units (AU).
6128 * To convert to kilometers, multiply the `x`, `y`, and `z` values by
6129 * the constant value #KM_PER_AU.
6130 *
6131 * The inverse of this function is also available: #Astronomy_VectorObserver.
6132 *
6133 * @param time
6134 * The date and time for which to calculate the observer's position vector.
6135 *
6136 * @param observer
6137 * The geographic location of a point on or near the surface of the Earth.
6138 *
6139 * @param equdate
6140 * Selects the date of the Earth's equator in which to express the equatorial coordinates.
6141 * The caller may select `EQUATOR_J2000` to use the orientation of the Earth's equator
6142 * at noon UTC on January 1, 2000, in which case this function corrects for precession
6143 * and nutation of the Earth as it was at the moment specified by the `time` parameter.
6144 * Or the caller may select `EQUATOR_OF_DATE` to use the Earth's equator at `time`
6145 * as the orientation.
6146 *
6147 * @return
6148 * If successful, the returned vector holds `ASTRO_SUCCESS` in its `status` field,
6149 * and is an equatorial vector from the center of the Earth to the specified location
6150 * on (or near) the Earth's surface. Otherwise, `status` holds an error code.
6151 */
6152astro_vector_t Astronomy_ObserverVector(
6153 astro_time_t *time,
6154 astro_observer_t observer,
6155 astro_equator_date_t equdate)
6156{
6157 astro_vector_t vec;
6158 double gast, pos[3], temp[3];
6159
6160 if (time == NULL)
6161 return VecError(ASTRO_INVALID_PARAMETER, TimeError());
6162
6163 gast = Astronomy_SiderealTime(time);
6164 terra(observer, gast, pos, NULL);
6165
6166 switch (equdate)
6167 {
6168 case EQUATOR_OF_DATE:
6169 /* 'pos' already contains equator-of-date coordinates. */
6170 break;
6171
6172 case EQUATOR_J2000:
6173 /* Convert 'pos' from equator-of-date to J2000. */
6174 nutation(pos, time, INTO_2000, temp);
6175 precession(temp, *time, INTO_2000, pos);
6176 break;
6177
6178 default:
6179 /* This is not a valid value of the 'equdate' parameter. */
6180 return VecError(ASTRO_INVALID_PARAMETER, *time);
6181 }
6182
6183 vec.x = pos[0];
6184 vec.y = pos[1];
6185 vec.z = pos[2];
6186 vec.t = *time;
6187 vec.status = ASTRO_SUCCESS;
6188 return vec;
6189}
6190
6191
6192/**
6193 * @brief Calculates geocentric equatorial position and velocity of an observer on the surface of the Earth.
6194 *
6195 * This function calculates position and velocity vectors of an observer
6196 * on or near the surface of the Earth, expressed in equatorial
6197 * coordinates. It takes into account the rotation of the Earth at the given
6198 * time, along with the given latitude, longitude, and elevation of the observer.
6199 *
6200 * The caller may pass a value in `equdate` to select either `EQUATOR_J2000`
6201 * for using J2000 coordinates, or `EQUATOR_OF_DATE` for using coordinates relative
6202 * to the Earth's equator at the specified time.
6203 *
6204 * The returned position vector has components expressed in astronomical units (AU).
6205 * To convert to kilometers, multiply the `x`, `y`, and `z` values by
6206 * the constant value #KM_PER_AU.
6207 *
6208 * The returned velocity vector is measured in AU/day.
6209 *
6210 * If you need the position only, and not the velocity, #Astronomy_ObserverVector
6211 * is slightly more efficient than this function.
6212 *
6213 * @param time
6214 * The date and time for which to calculate the observer's geocentric state vector.
6215 *
6216 * @param observer
6217 * The geographic location of a point on or near the surface of the Earth.
6218 *
6219 * @param equdate
6220 * Selects the date of the Earth's equator in which to express the equatorial coordinates.
6221 * The caller may select `EQUATOR_J2000` to use the orientation of the Earth's equator
6222 * at noon UTC on January 1, 2000, in which case this function corrects for precession
6223 * and nutation of the Earth as it was at the moment specified by the `time` parameter.
6224 * Or the caller may select `EQUATOR_OF_DATE` to use the Earth's equator at `time`
6225 * as the orientation.
6226 *
6227 * @return
6228 * If successful, the returned state vector holds `ASTRO_SUCCESS` in its `status` field,
6229 * and the position (x, y, z) and velocity (vx, vy, vz) vectors are valid.
6230 * Otherwise, `status` holds an error code.
6231 */
6232astro_state_vector_t Astronomy_ObserverState(
6233 astro_time_t *time,
6234 astro_observer_t observer,
6235 astro_equator_date_t equdate)
6236{
6237 astro_state_vector_t state;
6238 double gast, pos[3], vel[3], postemp[3], veltemp[3];
6239
6240 if (time == NULL)
6241 return StateVecError(ASTRO_INVALID_PARAMETER, TimeError());
6242
6243 gast = Astronomy_SiderealTime(time);
6244 terra(observer, gast, pos, vel);
6245
6246 switch (equdate)
6247 {
6248 case EQUATOR_OF_DATE:
6249 /* 'pos' and 'vel' already contain equator-of-date coordinates. */
6250 break;
6251
6252 case EQUATOR_J2000:
6253 /* Convert 'pos' from equator-of-date to J2000. */
6254 nutation_posvel(pos, vel, time, INTO_2000, postemp, veltemp);
6255 precession_posvel(postemp, veltemp, *time, INTO_2000, pos, vel);
6256 break;
6257
6258 default:
6259 /* This is not a valid value of the 'equdate' parameter. */
6260 return StateVecError(ASTRO_INVALID_PARAMETER, *time);
6261 }
6262
6263 state.x = pos[0];
6264 state.y = pos[1];
6265 state.z = pos[2];
6266 state.vx = vel[0];
6267 state.vy = vel[1];
6268 state.vz = vel[2];
6269 state.t = *time;
6270 state.status = ASTRO_SUCCESS;
6271 return state;
6272}
6273
6274
6275/**
6276 * @brief Calculates the geographic location corresponding to an equatorial vector.
6277 *
6278 * This is the inverse function of #Astronomy_ObserverVector.
6279 * Given a geocentric equatorial vector, it returns the geographic
6280 * latitude, longitude, and elevation for that vector.
6281 *
6282 * @param vector
6283 * The geocentric equatorial position vector for which to find geographic coordinates.
6284 * The components are expressed in Astronomical Units (AU).
6285 * You can calculate AU by dividing kilometers by the constant #KM_PER_AU.
6286 * The time `vector.t` determines the Earth's rotation. The caller must set `vector.t` to a valid time.
6287 * The vector is passed by reference (using a pointer) so that nutation calculations
6288 * can be cached inside `vector.t` as an optimization.
6289 *
6290 * @param equdate
6291 * Selects the date of the Earth's equator in which `vector` is expressed.
6292 * The caller may select `EQUATOR_J2000` to use the orientation of the Earth's equator
6293 * at noon UTC on January 1, 2000, in which case this function corrects for precession
6294 * and nutation of the Earth as it was at the moment specified by `vector.t`.
6295 * Or the caller may select `EQUATOR_OF_DATE` to use the Earth's equator at `vector.t`
6296 * as the orientation.
6297 *
6298 * @return
6299 * The geographic latitude, longitude, and elevation above sea level
6300 * that corresponds to the given equatorial vector.
6301 */
6302astro_observer_t Astronomy_VectorObserver(
6303 astro_vector_t *vector,
6304 astro_equator_date_t equdate)
6305{
6306 double gast;
6307 double pos1[3];
6308 double pos2[3];
6309
6310 gast = Astronomy_SiderealTime(&vector->t);
6311 pos1[0] = vector->x;
6312 pos1[1] = vector->y;
6313 pos1[2] = vector->z;
6314 if (equdate == EQUATOR_J2000)
6315 {
6316 precession(pos1, vector->t, FROM_2000, pos2);
6317 nutation(pos2, &vector->t, FROM_2000, pos1);
6318 }
6319 return inverse_terra(pos1, gast);
6320}
6321
6322
6323/**
6324 * @brief Calculates the gravitational acceleration experienced by an observer on the Earth.
6325 *
6326 * This function implements the WGS 84 Ellipsoidal Gravity Formula.
6327 * The result is a combination of inward gravitational acceleration
6328 * with outward centrifugal acceleration, as experienced by an observer
6329 * in the Earth's rotating frame of reference.
6330 * The resulting value increases toward the Earth's poles and decreases
6331 * toward the equator, consistent with changes of the weight measured
6332 * by a spring scale of a fixed mass moved to different latitudes and heights
6333 * on the Earth.
6334 *
6335 * @param latitude
6336 * The latitude of the observer in degrees north or south of the equator.
6337 * By formula symmetry, positive latitudes give the same answer as negative
6338 * latitudes, so the sign does not matter.
6339 *
6340 * @param height
6341 * The height above the sea level geoid in meters.
6342 * No range checking is done; however, accuracy is only valid in the
6343 * range 0 to 100000 meters.
6344 *
6345 * @return
6346 * The effective gravitational acceleration expressed in meters per second squared [m/s^2].
6347 */
6348double Astronomy_ObserverGravity(double latitude, double height)
6349{
6350 double s = sin(latitude * DEG2RAD);
6351 double s2 = s*s;
6352 double g0 = 9.7803253359 * (1.0 + 0.00193185265241*s2) / sqrt(1.0 - 0.00669437999013*s2);
6353 return g0 * (1.0 - (3.15704e-07 - 2.10269e-09*s2)*height + 7.37452e-14*height*height);
6354}
6355
6356
6357/**
6358 * @brief Calculates the apparent location of a body relative to the local horizon of an observer on the Earth.
6359 *
6360 * Given a date and time, the geographic location of an observer on the Earth, and
6361 * equatorial coordinates (right ascension and declination) of a celestial body,
6362 * this function returns horizontal coordinates (azimuth and altitude angles) for the body
6363 * relative to the horizon at the geographic location.
6364 *
6365 * The right ascension `ra` and declination `dec` passed in must be *equator of date*
6366 * coordinates, based on the Earth's true equator at the date and time of the observation.
6367 * Otherwise the resulting horizontal coordinates will be inaccurate.
6368 * Equator of date coordinates can be obtained by calling #Astronomy_Equator, passing in
6369 * `EQUATOR_OF_DATE` as its `equdate` parameter. It is also recommended to enable
6370 * aberration correction by passing in `ABERRATION` as the `aberration` parameter.
6371 *
6372 * This function optionally corrects for atmospheric refraction.
6373 * For most uses, it is recommended to pass `REFRACTION_NORMAL` in the `refraction` parameter to
6374 * correct for optical lensing of the Earth's atmosphere that causes objects
6375 * to appear somewhat higher above the horizon than they actually are.
6376 * However, callers may choose to avoid this correction by passing in `REFRACTION_NONE`.
6377 * If refraction correction is enabled, the azimuth, altitude, right ascension, and declination
6378 * in the #astro_horizon_t structure returned by this function will all be corrected for refraction.
6379 * If refraction is disabled, none of these four coordinates will be corrected; in that case,
6380 * the right ascension and declination in the returned structure will be numerically identical
6381 * to the respective `ra` and `dec` values passed in.
6382 *
6383 * @param time
6384 * The date and time of the observation.
6385 *
6386 * @param observer
6387 * The geographic location of the observer.
6388 *
6389 * @param ra
6390 * The right ascension of the body in sidereal hours.
6391 * See function remarks for more details.
6392 *
6393 * @param dec
6394 * The declination of the body in degrees. See function remarks for more details.
6395 *
6396 * @param refraction
6397 * Selects whether to correct for atmospheric refraction, and if so, which model to use.
6398 * The recommended value for most uses is `REFRACTION_NORMAL`.
6399 * See function remarks for more details.
6400 *
6401 * @return
6402 * The body's apparent horizontal coordinates and equatorial coordinates, both optionally corrected for refraction.
6403 */
6404astro_horizon_t Astronomy_Horizon(
6405 astro_time_t *time,
6406 astro_observer_t observer,
6407 double ra,
6408 double dec,
6409 astro_refraction_t refraction)
6410{
6411 astro_horizon_t hor;
6412 double latrad, lonrad, decrad, rarad;
6413 double uze[3], une[3], uwe[3];
6414 double uz[3], un[3], uw[3];
6415 double p[3], pz, pn, pw, proj;
6416 double az, zd;
6417 double spin_angle;
6418
6419 if (time == NULL)
6420 {
6421 /* The best we can do is return an invalid state. */
6422 /* It would break external dependencies to expect them to check for errors. */
6423 hor.altitude = NAN;
6424 hor.azimuth = NAN;
6425 hor.ra = NAN;
6426 hor.dec = NAN;
6427 return hor;
6428 }
6429
6430 latrad = observer.latitude * DEG2RAD;
6431 lonrad = observer.longitude * DEG2RAD;
6432 decrad = dec * DEG2RAD;
6433 rarad = ra * HOUR2RAD;
6434
6435 double sinlat = sin(latrad);
6436 double coslat = cos(latrad);
6437 double sinlon = sin(lonrad);
6438 double coslon = cos(lonrad);
6439 double sindc = sin(decrad);
6440 double cosdc = cos(decrad);
6441 double sinra = sin(rarad);
6442 double cosra = cos(rarad);
6443
6444 /*
6445 Calculate three mutually perpendicular unit vectors
6446 in equatorial coordinates: uze, une, uwe.
6447
6448 uze = The direction of the observer's local zenith (straight up).
6449 une = The direction toward due north on the observer's horizon.
6450 uwe = The direction toward due west on the observer's horizon.
6451
6452 HOWEVER, these are uncorrected for the Earth's rotation due to the time of day.
6453
6454 The components of these 3 vectors are as follows:
6455 [0] = x = direction from center of Earth toward 0 degrees longitude (the prime meridian) on equator.
6456 [1] = y = direction from center of Earth toward 90 degrees west longitude on equator.
6457 [2] = z = direction from center of Earth toward the north pole.
6458 */
6459
6460 uze[0] = coslat * coslon;
6461 uze[1] = coslat * sinlon;
6462 uze[2] = sinlat;
6463
6464 une[0] = -sinlat * coslon;
6465 une[1] = -sinlat * sinlon;
6466 une[2] = coslat;
6467
6468 uwe[0] = sinlon;
6469 uwe[1] = -coslon;
6470 uwe[2] = 0.0;
6471
6472 /*
6473 Correct the vectors uze, une, uwe for the Earth's rotation by calculating
6474 sidereal time. Call spin() for each uncorrected vector to rotate about
6475 the Earth's axis to yield corrected unit vectors uz, un, uw.
6476 Multiply sidereal hours by -15 to convert to degrees and flip eastward
6477 rotation of the Earth to westward apparent movement of objects with time.
6478 */
6479
6480 spin_angle = -15.0 * Astronomy_SiderealTime(time);
6481 spin(spin_angle, uze, uz);
6482 spin(spin_angle, une, un);
6483 spin(spin_angle, uwe, uw);
6484
6485 /*
6486 Convert angular equatorial coordinates (RA, DEC) to
6487 cartesian equatorial coordinates in 'p', using the
6488 same orientation system as uze, une, uwe.
6489 */
6490
6491 p[0] = cosdc * cosra;
6492 p[1] = cosdc * sinra;
6493 p[2] = sindc;
6494
6495 /*
6496 Use dot products of p with the zenith, north, and west
6497 vectors to obtain the cartesian coordinates of the body in
6498 the observer's horizontal orientation system.
6499
6500 pn = north component [-1, +1]
6501 pw = west component [-1, +1]
6502 pz = zenith component [-1, +1]
6503 */
6504
6505 pn = p[0]*un[0] + p[1]*un[1] + p[2]*un[2];
6506 pw = p[0]*uw[0] + p[1]*uw[1] + p[2]*uw[2];
6507 pz = p[0]*uz[0] + p[1]*uz[1] + p[2]*uz[2];
6508
6509 /* proj is the "shadow" of the body vector along the observer's flat ground. */
6510 proj = hypot(pn, pw);
6511 if (proj > 0.0)
6512 {
6513 /* If the body is not exactly straight up/down, it has an azimuth. */
6514 /* Invert the angle to produce degrees eastward from north. */
6515 az = -atan2(pw, pn) * RAD2DEG;
6516 if (az < 0.0)
6517 az += 360;
6518 }
6519 else
6520 {
6521 /* The body is straight up/down, so it does not have an azimuth. */
6522 /* Report an arbitrary but reasonable value. */
6523 az = 0.0;
6524 }
6525
6526 /* zd = the angle of the body away from the observer's zenith, in degrees. */
6527 zd = atan2(proj, pz) * RAD2DEG;
6528 hor.ra = ra;
6529 hor.dec = dec;
6530
6531 if (refraction == REFRACTION_NORMAL || refraction == REFRACTION_JPLHOR)
6532 {
6533 double zd0, refr;
6534
6535 zd0 = zd;
6536 refr = Astronomy_Refraction(refraction, 90.0 - zd);
6537 zd -= refr;
6538
6539 if (refr > 0.0 && zd > 3.0e-4)
6540 {
6541 int j;
6542 double sinzd = sin(zd * DEG2RAD);
6543 double coszd = cos(zd * DEG2RAD);
6544 double sinzd0 = sin(zd0 * DEG2RAD);
6545 double coszd0 = cos(zd0 * DEG2RAD);
6546 double pr[3];
6547
6548 for (j=0; j<3; ++j)
6549 pr[j] = ((p[j] - coszd0 * uz[j]) / sinzd0)*sinzd + uz[j]*coszd;
6550
6551 proj = hypot(pr[0], pr[1]);
6552 if (proj > 0)
6553 {
6554 hor.ra = RAD2HOUR * atan2(pr[1], pr[0]);
6555 if (hor.ra < 0.0)
6556 hor.ra += 24.0;
6557 }
6558 else
6559 {
6560 hor.ra = 0.0;
6561 }
6562 hor.dec = RAD2DEG * atan2(pr[2], proj);
6563 }
6564 }
6565
6566 hor.azimuth = az;
6567 hor.altitude = 90.0 - zd;
6568 return hor;
6569}
6570
6571/**
6572 * @brief Calculates geocentric ecliptic coordinates for the Sun.
6573 *
6574 * This function calculates the position of the Sun as seen from the Earth.
6575 * The returned value includes both Cartesian and spherical coordinates.
6576 * The x-coordinate and longitude values in the returned structure are based
6577 * on the *true equinox of date*: one of two points in the sky where the instantaneous
6578 * plane of the Earth's equator at the given date and time (the *equatorial plane*)
6579 * intersects with the plane of the Earth's orbit around the Sun (the *ecliptic plane*).
6580 * By convention, the apparent location of the Sun at the March equinox is chosen
6581 * as the longitude origin and x-axis direction, instead of the one for September.
6582 *
6583 * `Astronomy_SunPosition` corrects for precession and nutation of the Earth's axis
6584 * in order to obtain the exact equatorial plane at the given time.
6585 *
6586 * This function can be used for calculating changes of seasons: equinoxes and solstices.
6587 * In fact, the function #Astronomy_Seasons does use this function for that purpose.
6588 *
6589 * @param time
6590 * The date and time for which to calculate the Sun's position.
6591 *
6592 * @return
6593 * The ecliptic coordinates of the Sun using the Earth's true equator of date.
6594 */
6595astro_ecliptic_t Astronomy_SunPosition(astro_time_t time)
6596{
6597 astro_time_t adjusted_time;
6598 astro_vector_t earth2000;
6599 double sun2000[3];
6600 double stemp[3];
6601 double sun_ofdate[3];
6602 double true_obliq;
6603
6604 /* Correct for light travel time from the Sun. */
6605 /* Otherwise season calculations (equinox, solstice) will all be early by about 8 minutes! */
6606 adjusted_time = Astronomy_AddDays(time, -1.0 / C_AUDAY);
6607
6608 earth2000 = CalcEarth(adjusted_time);
6609 if (earth2000.status != ASTRO_SUCCESS)
6610 return EclError(earth2000.status);
6611
6612 /* Convert heliocentric location of Earth to geocentric location of Sun. */
6613 sun2000[0] = -earth2000.x;
6614 sun2000[1] = -earth2000.y;
6615 sun2000[2] = -earth2000.z;
6616
6617 /* Convert to equatorial Cartesian coordinates of date. */
6618 precession(sun2000, adjusted_time, FROM_2000, stemp);
6619 nutation(stemp, &adjusted_time, FROM_2000, sun_ofdate);
6620
6621 /* Convert equatorial coordinates to ecliptic coordinates. */
6622 true_obliq = DEG2RAD * e_tilt(&adjusted_time).tobl;
6623 return RotateEquatorialToEcliptic(sun_ofdate, true_obliq, time);
6624}
6625
6626/**
6627 * @brief Converts a J2000 mean equator (EQJ) vector to a true ecliptic of date (ETC) vector and angles.
6628 *
6629 * Given coordinates relative to the Earth's equator at J2000 (the instant of noon UTC
6630 * on 1 January 2000), this function converts those coordinates to true ecliptic coordinates
6631 * that are relative to the plane of the Earth's orbit around the Sun on that date.
6632 *
6633 * @param eqj
6634 * Equatorial coordinates in the EQJ frame of reference.
6635 * You can call #Astronomy_GeoVector to obtain suitable equatorial coordinates.
6636 *
6637 * @return
6638 * Spherical and vector coordinates expressed in true ecliptic coordinates of date (ECT).
6639 */
6640astro_ecliptic_t Astronomy_Ecliptic(astro_vector_t eqj)
6641{
6642 earth_tilt_t et;
6643 double eqj_pos[3];
6644 double mean_pos[3];
6645 double eqd_pos[3];
6646
6647 if (eqj.status != ASTRO_SUCCESS)
6648 return EclError(eqj.status);
6649
6650 /* Calculate nutation and obliquity for this time. */
6651 /* As an optimization, the nutation angles are cached in `time`, */
6652 /* and reused below when the `nutation` function is called. */
6653 et = e_tilt(&eqj.t);
6654
6655 /* Convert mean J2000 equator (EQJ) to true equator of date (EQD). */
6656 eqj_pos[0] = eqj.x;
6657 eqj_pos[1] = eqj.y;
6658 eqj_pos[2] = eqj.z;
6659 precession(eqj_pos, eqj.t, FROM_2000, mean_pos);
6660 nutation(mean_pos, &eqj.t, FROM_2000, eqd_pos);
6661
6662 /* Rotate from EQD to true ecliptic of date (ECT). */
6663 return RotateEquatorialToEcliptic(eqd_pos, et.tobl * DEG2RAD, eqj.t);
6664}
6665
6666/**
6667 * @brief Calculates heliocentric ecliptic longitude of a body.
6668 *
6669 * This function calculates the angle around the plane of the Earth's orbit
6670 * of a celestial body, as seen from the center of the Sun.
6671 * The angle is measured prograde (in the direction of the Earth's orbit around the Sun)
6672 * in degrees from the true equinox of date. The ecliptic longitude is always in the range [0, 360).
6673 *
6674 * @param body
6675 * A body other than the Sun.
6676 *
6677 * @param time
6678 * The date and time at which the body's ecliptic longitude is to be calculated.
6679 *
6680 * @return
6681 * On success, returns a structure whose `status` is `ASTRO_SUCCESS` and whose
6682 * `angle` holds the ecliptic longitude in degrees.
6683 * On failure, `status` holds a value other than `ASTRO_SUCCESS`.
6684 */
6685astro_angle_result_t Astronomy_EclipticLongitude(astro_body_t body, astro_time_t time)
6686{
6687 astro_vector_t hv;
6688 astro_ecliptic_t eclip;
6689 astro_angle_result_t result;
6690
6691 if (body == BODY_SUN)
6692 return AngleError(ASTRO_INVALID_BODY); /* cannot calculate heliocentric longitude of the Sun */
6693
6694 hv = Astronomy_HelioVector(body, time);
6695 eclip = Astronomy_Ecliptic(hv); /* checks for errors in hv, so we don't have to here */
6696 if (eclip.status != ASTRO_SUCCESS)
6697 return AngleError(eclip.status);
6698
6699 result.angle = eclip.elon;
6700 result.status = ASTRO_SUCCESS;
6701 return result;
6702}
6703
6704static astro_ecliptic_t RotateEquatorialToEcliptic(const double pos[3], double obliq_radians, astro_time_t time)
6705{
6706 astro_ecliptic_t ecl;
6707 double cos_ob, sin_ob;
6708 double xyproj;
6709
6710 cos_ob = cos(obliq_radians);
6711 sin_ob = sin(obliq_radians);
6712
6713 ecl.vec.status = ASTRO_SUCCESS;
6714 ecl.vec.t = time;
6715 ecl.vec.x = +pos[0];
6716 ecl.vec.y = +pos[1]*cos_ob + pos[2]*sin_ob;
6717 ecl.vec.z = -pos[1]*sin_ob + pos[2]*cos_ob;
6718
6719 xyproj = hypot(ecl.vec.x, ecl.vec.y);
6720 if (xyproj > 0.0)
6721 {
6722 ecl.elon = RAD2DEG * atan2(ecl.vec.y, ecl.vec.x);
6723 if (ecl.elon < 0.0)
6724 ecl.elon += 360.0;
6725 }
6726 else
6727 ecl.elon = 0.0;
6728
6729 ecl.elat = RAD2DEG * atan2(ecl.vec.z, xyproj);
6730 ecl.status = ASTRO_SUCCESS;
6731 return ecl;
6732}
6733
6734static astro_func_result_t sun_offset(void *context, astro_time_t time)
6735{
6736 astro_func_result_t result;
6737 double targetLon = *((double *)context);
6738 astro_ecliptic_t ecl = Astronomy_SunPosition(time);
6739 if (ecl.status != ASTRO_SUCCESS)
6740 return FuncError(ecl.status);
6741 result.value = LongitudeOffset(ecl.elon - targetLon);
6742 result.status = ASTRO_SUCCESS;
6743 return result;
6744}
6745
6746/**
6747 * @brief
6748 * Searches for the time when the Sun reaches an apparent ecliptic longitude as seen from the Earth.
6749 *
6750 * This function finds the moment in time, if any exists in the given time window,
6751 * that the center of the Sun reaches a specific ecliptic longitude as seen from the center of the Earth.
6752 *
6753 * This function can be used to determine equinoxes and solstices.
6754 * However, it is usually more convenient and efficient to call #Astronomy_Seasons
6755 * to calculate all equinoxes and solstices for a given calendar year.
6756 *
6757 * The function searches the window of time specified by `startTime` and `startTime+limitDays`.
6758 * The search will return an error if the Sun never reaches the longitude `targetLon` or
6759 * if the window is so large that the longitude ranges more than 180 degrees within it.
6760 * It is recommended to keep the window smaller than 10 days when possible.
6761 *
6762 * @param targetLon
6763 * The desired ecliptic longitude in degrees, relative to the true equinox of date.
6764 * This may be any value in the range [0, 360), although certain values have
6765 * conventional meanings:
6766 * 0 = March equinox, 90 = June solstice, 180 = September equinox, 270 = December solstice.
6767 *
6768 * @param startTime
6769 * The date and time for starting the search for the desired longitude event.
6770 *
6771 * @param limitDays
6772 * The real-valued number of days, which when added to `startTime`, limits the
6773 * range of time over which the search looks.
6774 * It is recommended to keep this value between 1 and 10 days.
6775 * See function remarks for more details.
6776 *
6777 * @return
6778 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
6779 * and the `time` field will contain the date and time the Sun reaches the target longitude.
6780 * Any other value indicates an error.
6781 * See remarks in #Astronomy_Search (which this function calls) for more information about possible error codes.
6782 */
6783astro_search_result_t Astronomy_SearchSunLongitude(
6784 double targetLon,
6785 astro_time_t startTime,
6786 double limitDays)
6787{
6788 astro_time_t t2 = Astronomy_AddDays(startTime, limitDays);
6789 return Astronomy_Search(sun_offset, &targetLon, startTime, t2, 0.01);
6790}
6791
6792/** @cond DOXYGEN_SKIP */
6793#define CALLFUNC(f,t) \
6794 do { \
6795 funcres = func(context, (t)); \
6796 if (funcres.status != ASTRO_SUCCESS) return SearchError(funcres.status); \
6797 (f) = funcres.value; \
6798 } while(0)
6799/** @endcond */
6800
6801/**
6802 * @brief Searches for a time at which a function's value increases through zero.
6803 *
6804 * Certain astronomy calculations involve finding a time when an event occurs.
6805 * Often such events can be defined as the root of a function:
6806 * the time at which the function's value becomes zero.
6807 *
6808 * `Astronomy_Search` finds the *ascending root* of a function: the time at which
6809 * the function's value becomes zero while having a positive slope. That is, as time increases,
6810 * the function transitions from a negative value, through zero at a specific moment,
6811 * to a positive value later. The goal of the search is to find that specific moment.
6812 *
6813 * The search function is specified by two parameters: `func` and `context`.
6814 * The `func` parameter is a pointer to the function itself, which accepts a time
6815 * and a context containing any other arguments needed to evaluate the function.
6816 * The `context` parameter supplies that context for the given search.
6817 * As an example, a caller may wish to find the moment a celestial body reaches a certain
6818 * ecliptic longitude. In that case, the caller might create a structure that contains
6819 * an #astro_body_t member to specify the body and a `double` to hold the target longitude.
6820 * The function would cast the pointer `context` passed in as a pointer to that structure type.
6821 * It could subtract the target longitude from the actual longitude at a given time;
6822 * thus the difference would equal zero at the moment in time the planet reaches the
6823 * desired longitude.
6824 *
6825 * The `func` returns an #astro_func_result_t structure every time it is called.
6826 * If the returned structure has a value of `status` other than `ASTRO_SUCCESS`,
6827 * the search immediately fails and reports that same error code in the `status`
6828 * returned by `Astronomy_Search`. Otherwise, `status` is `ASTRO_SUCCESS` and
6829 * `value` is the value of the function, and the search proceeds until it either
6830 * finds the ascending root or fails for some reason.
6831 *
6832 * The search calls `func` repeatedly to rapidly narrow in on any ascending
6833 * root within the time window specified by `t1` and `t2`. The search never
6834 * reports a solution outside this time window.
6835 *
6836 * `Astronomy_Search` uses a combination of bisection and quadratic interpolation
6837 * to minimize the number of function calls. However, it is critical that the
6838 * supplied time window be small enough that there cannot be more than one root
6839 * (ascedning or descending) within it; otherwise the search can fail.
6840 * Beyond that, it helps to make the time window as small as possible, ideally
6841 * such that the function itself resembles a smooth parabolic curve within that window.
6842 *
6843 * If an ascending root is not found, or more than one root
6844 * (ascending and/or descending) exists within the window `t1`..`t2`,
6845 * the search will fail with status code `ASTRO_SEARCH_FAILURE`.
6846 *
6847 * If the search does not converge within 20 iterations, it will fail
6848 * with status code `ASTRO_NO_CONVERGE`.
6849 *
6850 * @param func
6851 * The function for which to find the time of an ascending root.
6852 * See function remarks for more details.
6853 *
6854 * @param context
6855 * Any ancillary data needed by the function `func` to calculate a value.
6856 * The data type varies depending on the function passed in.
6857 * For example, the function may involve a specific celestial body that
6858 * must be specified somehow.
6859 *
6860 * @param t1
6861 * The lower time bound of the search window.
6862 * See function remarks for more details.
6863 *
6864 * @param t2
6865 * The upper time bound of the search window.
6866 * See function remarks for more details.
6867 *
6868 * @param dt_tolerance_seconds
6869 * Specifies an amount of time in seconds within which a bounded ascending root
6870 * is considered accurate enough to stop. A typical value is 1 second.
6871 *
6872 * @return
6873 * If successful, the returned structure has `status` equal to `ASTRO_SUCCESS`
6874 * and `time` set to a value within `dt_tolerance_seconds` of an ascending root.
6875 * On success, the `time` value will always be in the inclusive range [`t1`, `t2`].
6876 * If the search fails, `status` will be set to a value other than `ASTRO_SUCCESS`.
6877 * See function remarks for more details.
6878 */
6879astro_search_result_t Astronomy_Search(
6880 astro_search_func_t func,
6881 void *context,
6882 astro_time_t t1,
6883 astro_time_t t2,
6884 double dt_tolerance_seconds)
6885{
6886 astro_search_result_t result;
6887 astro_time_t tmid;
6888 astro_time_t tq;
6889 astro_func_result_t funcres;
6890 double f1, f2, fmid=0.0, fq, dt_days, dt, dt_guess;
6891 double q_ut, q_df_dt;
6892 const int iter_limit = 20;
6893 int iter = 0;
6894 int calc_fmid = 1;
6895
6896 dt_days = fabs(dt_tolerance_seconds / SECONDS_PER_DAY);
6897 CALLFUNC(f1, t1);
6898 CALLFUNC(f2, t2);
6899
6900 for(;;)
6901 {
6902 if (++iter > iter_limit)
6903 return SearchError(ASTRO_NO_CONVERGE);
6904
6905 dt = (t2.tt - t1.tt) / 2.0;
6906 tmid = Astronomy_AddDays(t1, dt);
6907 if (fabs(dt) < dt_days)
6908 {
6909 /* We are close enough to the event to stop the search. */
6910 result.time = tmid;
6911 result.status = ASTRO_SUCCESS;
6912 return result;
6913 }
6914
6915 if (calc_fmid)
6916 CALLFUNC(fmid, tmid);
6917 else
6918 calc_fmid = 1; /* we already have the correct value of fmid from the previous loop */
6919
6920 /* Quadratic interpolation: */
6921 /* Try to find a parabola that passes through the 3 points we have sampled: */
6922 /* (t1,f1), (tmid,fmid), (t2,f2) */
6923
6924 if (QuadInterp(tmid.ut, t2.ut - tmid.ut, f1, fmid, f2, &q_ut, &q_df_dt))
6925 {
6926 tq = Astronomy_TimeFromDays(q_ut);
6927 CALLFUNC(fq, tq);
6928 if (q_df_dt != 0.0)
6929 {
6930 dt_guess = fabs(fq / q_df_dt);
6931 if (dt_guess < dt_days)
6932 {
6933 /* The estimated time error is small enough that we can quit now. */
6934 result.time = tq;
6935 result.status = ASTRO_SUCCESS;
6936 return result;
6937 }
6938
6939 /* Try guessing a tighter boundary with the interpolated root at the center. */
6940 dt_guess *= 1.2;
6941 if (dt_guess < dt/10.0)
6942 {
6943 astro_time_t tleft = Astronomy_AddDays(tq, -dt_guess);
6944 astro_time_t tright = Astronomy_AddDays(tq, +dt_guess);
6945 if ((tleft.ut - t1.ut)*(tleft.ut - t2.ut) < 0)
6946 {
6947 if ((tright.ut - t1.ut)*(tright.ut - t2.ut) < 0)
6948 {
6949 double fleft, fright;
6950 CALLFUNC(fleft, tleft);
6951 CALLFUNC(fright, tright);
6952 if (fleft<0.0 && fright>=0.0)
6953 {
6954 f1 = fleft;
6955 f2 = fright;
6956 t1 = tleft;
6957 t2 = tright;
6958 fmid = fq;
6959 calc_fmid = 0; /* save a little work -- no need to re-calculate fmid next time around the loop */
6960 continue;
6961 }
6962 }
6963 }
6964 }
6965 }
6966 }
6967
6968 /* After quadratic interpolation attempt. */
6969 /* Now just divide the region in two parts and pick whichever one appears to contain a root. */
6970 if (f1 < 0.0 && fmid >= 0.0)
6971 {
6972 t2 = tmid;
6973 f2 = fmid;
6974 continue;
6975 }
6976
6977 if (fmid < 0.0 && f2 >= 0.0)
6978 {
6979 t1 = tmid;
6980 f1 = fmid;
6981 continue;
6982 }
6983
6984 /* Either there is no ascending zero-crossing in this range */
6985 /* or the search window is too wide (more than one zero-crossing). */
6986 return SearchError(ASTRO_SEARCH_FAILURE);
6987 }
6988}
6989
6990static int QuadInterp(
6991 double tm, double dt, double fa, double fm, double fb,
6992 double *out_t, double *out_df_dt)
6993{
6994 double Q, R, S;
6995 double x, u, ru, x1, x2;
6996
6997 Q = (fb + fa)/2.0 - fm;
6998 R = (fb - fa)/2.0;
6999 S = fm;
7000
7001 if (Q == 0.0)
7002 {
7003 /* This is a line, not a parabola. */
7004 if (R == 0.0)
7005 return 0; /* This is a HORIZONTAL line... can't make progress! */
7006 x = -S / R;
7007 if (x < -1.0 || x > +1.0)
7008 return 0; /* out of bounds */
7009 }
7010 else
7011 {
7012 /* This really is a parabola. Find roots x1, x2. */
7013 u = R*R - 4*Q*S;
7014 if (u <= 0.0)
7015 return 0; /* can't solve if imaginary, or if vertex of parabola is tangent. */
7016
7017 ru = sqrt(u);
7018 x1 = (-R + ru) / (2.0 * Q);
7019 x2 = (-R - ru) / (2.0 * Q);
7020 if (-1.0 <= x1 && x1 <= +1.0)
7021 {
7022 if (-1.0 <= x2 && x2 <= +1.0)
7023 return 0; /* two roots are within bounds; we require a unique zero-crossing. */
7024 x = x1;
7025 }
7026 else if (-1.0 <= x2 && x2 <= +1.0)
7027 x = x2;
7028 else
7029 return 0; /* neither root is within bounds */
7030 }
7031
7032 *out_t = tm + x*dt;
7033 *out_df_dt = (2*Q*x + R) / dt;
7034 return 1; /* success */
7035}
7036
7037static astro_status_t FindSeasonChange(double targetLon, int year, int month, int day, astro_time_t *time)
7038{
7039 astro_time_t startTime;
7040 astro_search_result_t result;
7041
7042 startTime = Astronomy_MakeTime(year, month, day, 0, 0, 0.0);
7043 result = Astronomy_SearchSunLongitude(targetLon, startTime, 20.0);
7044 *time = result.time;
7045 return result.status;
7046}
7047
7048/**
7049 * @brief Finds both equinoxes and both solstices for a given calendar year.
7050 *
7051 * The changes of seasons are defined by solstices and equinoxes.
7052 * Given a calendar year number, this function calculates the
7053 * March and September equinoxes and the June and December solstices.
7054 *
7055 * The equinoxes are the moments twice each year when the plane of the
7056 * Earth's equator passes through the center of the Sun. In other words,
7057 * the Sun's declination is zero at both equinoxes.
7058 * The March equinox defines the beginning of spring in the northern hemisphere
7059 * and the beginning of autumn in the southern hemisphere.
7060 * The September equinox defines the beginning of autumn in the northern hemisphere
7061 * and the beginning of spring in the southern hemisphere.
7062 *
7063 * The solstices are the moments twice each year when one of the Earth's poles
7064 * is most tilted toward the Sun. More precisely, the Sun's declination reaches
7065 * its minimum value at the December solstice, which defines the beginning of
7066 * winter in the northern hemisphere and the beginning of summer in the southern
7067 * hemisphere. The Sun's declination reaches its maximum value at the June solstice,
7068 * which defines the beginning of summer in the northern hemisphere and the beginning
7069 * of winter in the southern hemisphere.
7070 *
7071 * @param year
7072 * The calendar year number for which to calculate equinoxes and solstices.
7073 * The value may be any integer, but only the years 1800 through 2100 have been
7074 * validated for accuracy: unit testing against data from the
7075 * United States Naval Observatory confirms that all equinoxes and solstices
7076 * for that range of years are within 2 minutes of the correct time.
7077 *
7078 * @return
7079 * The times of the four seasonal changes in the given calendar year.
7080 * This function should always succeed. However, to be safe, callers
7081 * should check the `status` field of the returned structure to make sure
7082 * it contains `ASTRO_SUCCESS`. Any failures indicate a bug in the algorithm
7083 * and should be [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
7084 */
7085astro_seasons_t Astronomy_Seasons(int year)
7086{
7087 astro_seasons_t seasons;
7088 astro_status_t status;
7089
7090 seasons.status = ASTRO_SUCCESS;
7091
7092 /*
7093 https://github.com/cosinekitty/astronomy/issues/187
7094 Solstices and equinoxes drift over long spans of time,
7095 due to precession of the Earth's axis.
7096 Therefore, we have to search a wider range of time than
7097 one might expect. It turns out this has very little
7098 effect on efficiency, thanks to the quick convergence
7099 of quadratic interpolation inside Astronomy_Search().
7100 */
7101
7102 status = FindSeasonChange( 0, year, 3, 10, &seasons.mar_equinox);
7103 if (status != ASTRO_SUCCESS) seasons.status = status;
7104
7105 status = FindSeasonChange( 90, year, 6, 10, &seasons.jun_solstice);
7106 if (status != ASTRO_SUCCESS) seasons.status = status;
7107
7108 status = FindSeasonChange(180, year, 9, 10, &seasons.sep_equinox);
7109 if (status != ASTRO_SUCCESS) seasons.status = status;
7110
7111 status = FindSeasonChange(270, year, 12, 10, &seasons.dec_solstice);
7112 if (status != ASTRO_SUCCESS) seasons.status = status;
7113
7114 return seasons;
7115}
7116
7117/**
7118 * @brief Returns the angle between the given body and the Sun, as seen from the Earth.
7119 *
7120 * This function calculates the angular separation between the given body and the Sun,
7121 * as seen from the center of the Earth. This angle is helpful for determining how
7122 * easy it is to see the body away from the glare of the Sun.
7123 *
7124 * @param body
7125 * The celestial body whose angle from the Sun is to be measured.
7126 * Not allowed to be `BODY_EARTH`.
7127 *
7128 * @param time
7129 * The time at which the observation is made.
7130 *
7131 * @return
7132 * If successful, the returned structure contains `ASTRO_SUCCESS` in the `status` field
7133 * and `angle` holds the angle in degrees between the Sun and the specified body as
7134 * seen from the center of the Earth.
7135 * If an error occurs, the `status` field contains a value other than `ASTRO_SUCCESS`
7136 * that indicates the error condition.
7137 */
7138astro_angle_result_t Astronomy_AngleFromSun(astro_body_t body, astro_time_t time)
7139{
7140 astro_vector_t sv, bv;
7141
7142 if (body == BODY_EARTH)
7143 return AngleError(ASTRO_EARTH_NOT_ALLOWED);
7144
7145 sv = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
7146 if (sv.status != ASTRO_SUCCESS)
7147 return AngleError(sv.status);
7148
7149 bv = Astronomy_GeoVector(body, time, ABERRATION);
7150 if (bv.status != ASTRO_SUCCESS)
7151 return AngleError(bv.status);
7152
7153 return Astronomy_AngleBetween(sv, bv);
7154}
7155
7156/**
7157 * @brief
7158 * Determines visibility of a celestial body relative to the Sun, as seen from the Earth.
7159 *
7160 * This function returns an #astro_elongation_t structure, which provides the following
7161 * information about the given celestial body at the given time:
7162 *
7163 * - `visibility` is an enumerated type that specifies whether the body is more easily seen
7164 * in the morning before sunrise, or in the evening after sunset.
7165 *
7166 * - `elongation` is the angle in degrees between two vectors: one from the center of the Earth to the
7167 * center of the Sun, the other from the center of the Earth to the center of the specified body.
7168 * This angle indicates how far away the body is from the glare of the Sun.
7169 * The elongation angle is always in the range [0, 180].
7170 *
7171 * - `ecliptic_separation` is the absolute value of the difference between the body's ecliptic longitude
7172 * and the Sun's ecliptic longitude, both as seen from the center of the Earth. This angle measures
7173 * around the plane of the Earth's orbit, and ignores how far above or below that plane the body is.
7174 * The ecliptic separation is measured in degrees and is always in the range [0, 180].
7175 *
7176 * @param body
7177 * The celestial body whose visibility is to be calculated.
7178 *
7179 * @param time
7180 * The date and time of the observation.
7181 *
7182 * @return
7183 * If successful, the `status` field in the returned structure contains `ASTRO_SUCCESS`
7184 * and all the other fields in the structure are valid. On failure, `status` contains
7185 * some other value as an error code and the other fields contain invalid values.
7186 */
7187astro_elongation_t Astronomy_Elongation(astro_body_t body, astro_time_t time)
7188{
7189 astro_elongation_t result;
7190 astro_angle_result_t angres;
7191
7192 angres = Astronomy_PairLongitude(body, BODY_SUN, time);
7193 if (angres.status != ASTRO_SUCCESS)
7194 return ElongError(angres.status);
7195
7196 if (angres.angle > 180.0)
7197 {
7198 result.visibility = VISIBLE_MORNING;
7199 result.ecliptic_separation = 360.0 - angres.angle;
7200 }
7201 else
7202 {
7203 result.visibility = VISIBLE_EVENING;
7204 result.ecliptic_separation = angres.angle;
7205 }
7206
7207 angres = Astronomy_AngleFromSun(body, time);
7208 if (angres.status != ASTRO_SUCCESS)
7209 return ElongError(angres.status);
7210
7211 result.elongation = angres.angle;
7212 result.time = time;
7213 result.status = ASTRO_SUCCESS;
7214
7215 return result;
7216}
7217
7218static astro_func_result_t neg_elong_slope(void *context, astro_time_t time)
7219{
7220 static const double dt = 0.1;
7221 astro_angle_result_t e1, e2;
7222 astro_func_result_t result;
7223 astro_body_t body = *((astro_body_t *)context);
7224 astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
7225 astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
7226
7227 e1 = Astronomy_AngleFromSun(body, t1);
7228 if (e1.status != ASTRO_SUCCESS)
7229 return FuncError(e1.status);
7230
7231 e2 = Astronomy_AngleFromSun(body, t2);
7232 if (e2.status)
7233 return FuncError(e2.status);
7234
7235 result.value = (e1.angle - e2.angle)/dt;
7236 result.status = ASTRO_SUCCESS;
7237 return result;
7238}
7239
7240/**
7241 * @brief
7242 * Finds a date and time when Mercury or Venus reaches its maximum angle from the Sun as seen from the Earth.
7243 *
7244 * Mercury and Venus are are often difficult to observe because they are closer to the Sun than the Earth is.
7245 * Mercury especially is almost always impossible to see because it gets lost in the Sun's glare.
7246 * The best opportunities for spotting Mercury, and the best opportunities for viewing Venus through
7247 * a telescope without atmospheric interference, are when these planets reach maximum elongation.
7248 * These are events where the planets reach the maximum angle from the Sun as seen from the Earth.
7249 *
7250 * This function solves for those times, reporting the next maximum elongation event's date and time,
7251 * the elongation value itself, the relative longitude with the Sun, and whether the planet is best
7252 * observed in the morning or evening. See #Astronomy_Elongation for more details about the returned structure.
7253 *
7254 * @param body
7255 * Either `BODY_MERCURY` or `BODY_VENUS`. Any other value will fail with the error `ASTRO_INVALID_BODY`.
7256 * To find the best viewing opportunites for planets farther from the Sun than the Earth is (Mars through Pluto)
7257 * use #Astronomy_SearchRelativeLongitude to find the next opposition event.
7258 *
7259 * @param startTime
7260 * The date and time at which to begin the search. The maximum elongation event found will always
7261 * be the first one that occurs after this date and time.
7262 *
7263 * @return
7264 * If successful, the `status` field of the returned structure will be `ASTRO_SUCCESS`
7265 * and the other structure fields will be valid. Otherwise, `status` will contain
7266 * some other value indicating an error.
7267 */
7268astro_elongation_t Astronomy_SearchMaxElongation(astro_body_t body, astro_time_t startTime)
7269{
7270 double s1, s2;
7271 int iter;
7272 astro_angle_result_t plon, elon;
7273 astro_time_t t_start;
7274 double rlon, rlon_lo, rlon_hi, adjust_days;
7275 astro_func_result_t syn;
7276 astro_search_result_t search1, search2, searchx;
7277 astro_time_t t1, t2;
7278 astro_func_result_t m1, m2;
7279
7280 /* Determine the range of relative longitudes within which maximum elongation can occur for this planet. */
7281 switch (body)
7282 {
7283 case BODY_MERCURY:
7284 s1 = 50.0;
7285 s2 = 85.0;
7286 break;
7287
7288 case BODY_VENUS:
7289 s1 = 40.0;
7290 s2 = 50.0;
7291 break;
7292
7293 default:
7294 /* SearchMaxElongation works for Mercury and Venus only. */
7295 return ElongError(ASTRO_INVALID_BODY);
7296 }
7297
7298 syn = SynodicPeriod(body);
7299 if (syn.status != ASTRO_SUCCESS)
7300 return ElongError(syn.status);
7301
7302 iter = 0;
7303 while (++iter <= 2)
7304 {
7305 plon = Astronomy_EclipticLongitude(body, startTime);
7306 if (plon.status != ASTRO_SUCCESS)
7307 return ElongError(plon.status);
7308
7309 elon = Astronomy_EclipticLongitude(BODY_EARTH, startTime);
7310 if (elon.status != ASTRO_SUCCESS)
7311 return ElongError(elon.status);
7312
7313 rlon = LongitudeOffset(plon.angle - elon.angle); /* clamp to (-180, +180] */
7314
7315 /* The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees */
7316 /* because there is a cusp there that causes a discontinuity in the derivative. */
7317 /* So we need to guard against searching near such times. */
7318 if (rlon >= -s1 && rlon < +s1)
7319 {
7320 /* Seek to the window [+s1, +s2]. */
7321 adjust_days = 0.0;
7322 /* Search forward for the time t1 when rel lon = +s1. */
7323 rlon_lo = +s1;
7324 /* Search forward for the time t2 when rel lon = +s2. */
7325 rlon_hi = +s2;
7326 }
7327 else if (rlon > +s2 || rlon < -s2)
7328 {
7329 /* Seek to the next search window at [-s2, -s1]. */
7330 adjust_days = 0.0;
7331 /* Search forward for the time t1 when rel lon = -s2. */
7332 rlon_lo = -s2;
7333 /* Search forward for the time t2 when rel lon = -s1. */
7334 rlon_hi = -s1;
7335 }
7336 else if (rlon >= 0.0)
7337 {
7338 /* rlon must be in the middle of the window [+s1, +s2]. */
7339 /* Search BACKWARD for the time t1 when rel lon = +s1. */
7340 adjust_days = -syn.value / 4.0;
7341 rlon_lo = +s1;
7342 rlon_hi = +s2;
7343 /* Search forward from t1 to find t2 such that rel lon = +s2. */
7344 }
7345 else
7346 {
7347 /* rlon must be in the middle of the window [-s2, -s1]. */
7348 /* Search BACKWARD for the time t1 when rel lon = -s2. */
7349 adjust_days = -syn.value / 4.0;
7350 rlon_lo = -s2;
7351 /* Search forward from t1 to find t2 such that rel lon = -s1. */
7352 rlon_hi = -s1;
7353 }
7354
7355 t_start = Astronomy_AddDays(startTime, adjust_days);
7356
7357 search1 = Astronomy_SearchRelativeLongitude(body, rlon_lo, t_start);
7358 if (search1.status != ASTRO_SUCCESS)
7359 return ElongError(search1.status);
7360 t1 = search1.time;
7361
7362 search2 = Astronomy_SearchRelativeLongitude(body, rlon_hi, t1);
7363 if (search2.status != ASTRO_SUCCESS)
7364 return ElongError(search2.status);
7365 t2 = search2.time;
7366
7367 /* Now we have a time range [t1,t2] that brackets a maximum elongation event. */
7368 /* Confirm the bracketing. */
7369 m1 = neg_elong_slope(&body, t1);
7370 if (m1.status != ASTRO_SUCCESS)
7371 return ElongError(m1.status);
7372
7373 if (m1.value >= 0)
7374 return ElongError(ASTRO_INTERNAL_ERROR); /* there is a bug in the bracketing algorithm! */
7375
7376 m2 = neg_elong_slope(&body, t2);
7377 if (m2.status != ASTRO_SUCCESS)
7378 return ElongError(m2.status);
7379
7380 if (m2.value <= 0)
7381 return ElongError(ASTRO_INTERNAL_ERROR); /* there is a bug in the bracketing algorithm! */
7382
7383 /* Use the generic search algorithm to home in on where the slope crosses from negative to positive. */
7384 searchx = Astronomy_Search(neg_elong_slope, &body, t1, t2, 10.0);
7385 if (searchx.status != ASTRO_SUCCESS)
7386 return ElongError(searchx.status);
7387
7388 if (searchx.time.tt >= startTime.tt)
7389 return Astronomy_Elongation(body, searchx.time);
7390
7391 /* This event is in the past (earlier than startTime). */
7392 /* We need to search forward from t2 to find the next possible window. */
7393 /* We never need to search more than twice. */
7394 startTime = Astronomy_AddDays(t2, 1.0);
7395 }
7396
7397 return ElongError(ASTRO_SEARCH_FAILURE);
7398}
7399
7400
7401/**
7402 * @brief Returns one body's ecliptic longitude with respect to another, as seen from the Earth.
7403 *
7404 * This function determines where one body appears around the ecliptic plane
7405 * (the plane of the Earth's orbit around the Sun) as seen from the Earth,
7406 * relative to the another body's apparent position.
7407 * The function returns an angle in the half-open range [0, 360) degrees.
7408 * The value is the ecliptic longitude of `body1` relative to the ecliptic
7409 * longitude of `body2`.
7410 *
7411 * The angle is 0 when the two bodies are at the same ecliptic longitude
7412 * as seen from the Earth. The angle increases in the prograde direction
7413 * (the direction that the planets orbit the Sun and the Moon orbits the Earth).
7414 *
7415 * When the angle is 180 degrees, it means the two bodies appear on opposite sides
7416 * of the sky for an Earthly observer.
7417 *
7418 * Neither `body1` nor `body2` is allowed to be `BODY_EARTH`.
7419 * If this happens, the function fails with the error code `ASTRO_EARTH_NOT_ALLOWED`.
7420 *
7421 * @param body1
7422 * The first body, whose longitude is to be found relative to the second body.
7423 *
7424 * @param body2
7425 * The second body, relative to which the longitude of the first body is to be found.
7426 *
7427 * @param time
7428 * The date and time of the observation.
7429 *
7430 * @return
7431 * On success, the `status` field in the returned structure holds `ASTRO_SUCCESS` and
7432 * the `angle` field holds a value in the range [0, 360).
7433 * On failure, the `status` field contains some other value indicating an error condition.
7434 */
7435astro_angle_result_t Astronomy_PairLongitude(
7436 astro_body_t body1,
7437 astro_body_t body2,
7438 astro_time_t time)
7439{
7440 astro_vector_t vector1, vector2;
7441 astro_ecliptic_t eclip1, eclip2;
7442 astro_angle_result_t result;
7443
7444 if (body1 == BODY_EARTH || body2 == BODY_EARTH)
7445 return AngleError(ASTRO_EARTH_NOT_ALLOWED);
7446
7447 vector1 = Astronomy_GeoVector(body1, time, NO_ABERRATION);
7448 eclip1 = Astronomy_Ecliptic(vector1); /* checks for errors in vector1 */
7449 if (eclip1.status != ASTRO_SUCCESS)
7450 return AngleError(eclip1.status);
7451
7452 vector2 = Astronomy_GeoVector(body2, time, NO_ABERRATION);
7453 eclip2 = Astronomy_Ecliptic(vector2); /* checks for errors in vector2 */
7454 if (eclip2.status != ASTRO_SUCCESS)
7455 return AngleError(eclip2.status);
7456
7457 result.status = ASTRO_SUCCESS;
7458 result.angle = NormalizeLongitude(eclip1.elon - eclip2.elon);
7459 return result;
7460}
7461
7462
7463/**
7464 * @brief
7465 * Returns the Moon's phase as an angle from 0 to 360 degrees.
7466 *
7467 * This function determines the phase of the Moon using its apparent
7468 * ecliptic longitude relative to the Sun, as seen from the center of the Earth.
7469 * Certain values of the angle have conventional definitions:
7470 *
7471 * - 0 = new moon
7472 * - 90 = first quarter
7473 * - 180 = full moon
7474 * - 270 = third quarter
7475 *
7476 * @param time
7477 * The date and time of the observation.
7478 *
7479 * @return
7480 * On success, the function returns the angle as described in the function remarks
7481 * in the `angle` field and `ASTRO_SUCCESS` in the `status` field.
7482 * The function should always succeed, but it is a good idea for callers to check
7483 * the `status` field in the returned structure.
7484 * Any other value in `status` indicates a failure that should be
7485 * [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
7486 */
7487astro_angle_result_t Astronomy_MoonPhase(astro_time_t time)
7488{
7489 return Astronomy_PairLongitude(BODY_MOON, BODY_SUN, time);
7490}
7491
7492static astro_func_result_t moon_offset(void *context, astro_time_t time)
7493{
7494 astro_func_result_t result;
7495 double targetLon = *((double *)context);
7496 astro_angle_result_t angres = Astronomy_MoonPhase(time);
7497 if (angres.status != ASTRO_SUCCESS)
7498 return FuncError(angres.status);
7499 result.value = LongitudeOffset(angres.angle - targetLon);
7500 result.status = ASTRO_SUCCESS;
7501 return result;
7502}
7503
7504/**
7505 * @brief
7506 * Searches for the time that the Moon reaches a specified phase.
7507 *
7508 * Lunar phases are conventionally defined in terms of the Moon's geocentric ecliptic
7509 * longitude with respect to the Sun's geocentric ecliptic longitude.
7510 * When the Moon and the Sun have the same longitude, that is defined as a new moon.
7511 * When their longitudes are 180 degrees apart, that is defined as a full moon.
7512 *
7513 * This function searches for any value of the lunar phase expressed as an
7514 * angle in degrees in the range [0, 360).
7515 *
7516 * If you want to iterate through lunar quarters (new moon, first quarter, full moon, third quarter)
7517 * it is much easier to call the functions #Astronomy_SearchMoonQuarter and #Astronomy_NextMoonQuarter.
7518 * This function is useful for finding general phase angles outside those four quarters.
7519 *
7520 * @param targetLon
7521 * The difference in geocentric longitude between the Sun and Moon
7522 * that specifies the lunar phase being sought. This can be any value
7523 * in the range [0, 360). Certain values have conventional names:
7524 * 0 = new moon, 90 = first quarter, 180 = full moon, 270 = third quarter.
7525 *
7526 * @param startTime
7527 * The beginning of the time window in which to search for the Moon reaching the specified phase.
7528 *
7529 * @param limitDays
7530 * The number of days away from `startTime` that limits the time window for the search.
7531 * If the value is negative, the search is performed into the past from `startTime`.
7532 * Otherwise, the search is performed into the future from `startTime`.
7533 *
7534 * @return
7535 * On success, the `status` field in the returned structure holds `ASTRO_SUCCESS` and
7536 * the `time` field holds the date and time when the Moon reaches the target longitude.
7537 * On failure, `status` holds some other value as an error code.
7538 * One possible error code is `ASTRO_NO_MOON_QUARTER` if `startTime` and `limitDays`
7539 * do not enclose the desired event. See remarks in #Astronomy_Search for other possible
7540 * error codes.
7541 */
7542astro_search_result_t Astronomy_SearchMoonPhase(double targetLon, astro_time_t startTime, double limitDays)
7543{
7544 /*
7545 To avoid discontinuities in the moon_offset function causing problems,
7546 we need to approximate when that function will next return 0.
7547 We probe it with the start time and take advantage of the fact
7548 that every lunar phase repeats roughly every 29.5 days.
7549 There is a surprising uncertainty in the quarter timing,
7550 due to the eccentricity of the moon's orbit.
7551 I have seen more than 0.9 days away from the simple prediction.
7552 To be safe, we take the predicted time of the event and search
7553 +/-1.5 days around it (a 3-day wide window).
7554 Return ASTRO_NO_MOON_QUARTER if the final result goes beyond limitDays after startTime.
7555 */
7556 const double uncertainty = 1.5;
7557 astro_func_result_t funcres;
7558 double ya, est_dt, dt1, dt2;
7559 astro_time_t t1, t2;
7560
7561 funcres = moon_offset(&targetLon, startTime);
7562 if (funcres.status != ASTRO_SUCCESS)
7563 return SearchError(funcres.status);
7564
7565 ya = funcres.value;
7566 if (limitDays < 0.0)
7567 {
7568 /* Search backward in time. */
7569 if (ya < 0.0) ya += 360.0;
7570 est_dt = -(MEAN_SYNODIC_MONTH * ya) / 360.0;
7571 dt1 = est_dt - uncertainty;
7572 dt2 = est_dt + uncertainty;
7573 if (dt2 < limitDays)
7574 return SearchError(ASTRO_NO_MOON_QUARTER); /* not possible for moon phase to occur within specified window (too short) */
7575 if (dt1 < limitDays)
7576 dt1 = limitDays;
7577 }
7578 else
7579 {
7580 /* Search forward in time. */
7581 if (ya > 0.0) ya -= 360.0;
7582 est_dt = -(MEAN_SYNODIC_MONTH * ya) / 360.0;
7583 dt1 = est_dt - uncertainty;
7584 dt2 = est_dt + uncertainty;
7585 if (dt1 > limitDays)
7586 return SearchError(ASTRO_NO_MOON_QUARTER); /* not possible for moon phase to occur within specified window (too short) */
7587 if (dt2 > limitDays)
7588 dt2 = limitDays;
7589 }
7590 t1 = Astronomy_AddDays(startTime, dt1);
7591 t2 = Astronomy_AddDays(startTime, dt2);
7592 return Astronomy_Search(moon_offset, &targetLon, t1, t2, 0.1);
7593}
7594
7595/**
7596 * @brief
7597 * Finds the first lunar quarter after the specified date and time.
7598 *
7599 * A lunar quarter is one of the following four lunar phase events:
7600 * new moon, first quarter, full moon, third quarter.
7601 * This function finds the lunar quarter that happens soonest
7602 * after the specified date and time.
7603 *
7604 * To continue iterating through consecutive lunar quarters, call this function once,
7605 * followed by calls to #Astronomy_NextMoonQuarter as many times as desired.
7606 *
7607 * @param startTime
7608 * The date and time at which to start the search.
7609 *
7610 * @return
7611 * This function should always succeed, indicated by the `status` field
7612 * in the returned structure holding `ASTRO_SUCCESS`. Any other value indicates
7613 * an internal error, which should be [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
7614 * To be safe, calling code should always check the `status` field for errors.
7615 */
7616astro_moon_quarter_t Astronomy_SearchMoonQuarter(astro_time_t startTime)
7617{
7618 astro_moon_quarter_t mq;
7619 astro_angle_result_t angres;
7620 astro_search_result_t srchres;
7621
7622 /* Determine what the next quarter phase will be. */
7623 angres = Astronomy_MoonPhase(startTime);
7624 if (angres.status != ASTRO_SUCCESS)
7625 return MoonQuarterError(angres.status);
7626
7627 mq.quarter = (1 + (int)floor(angres.angle / 90.0)) % 4;
7628 srchres = Astronomy_SearchMoonPhase(90.0 * mq.quarter, startTime, 10.0);
7629 if (srchres.status != ASTRO_SUCCESS)
7630 return MoonQuarterError(srchres.status);
7631
7632 mq.status = ASTRO_SUCCESS;
7633 mq.time = srchres.time;
7634 return mq;
7635}
7636
7637/**
7638 * @brief
7639 * Continues searching for lunar quarters from a previous search.
7640 *
7641 * After calling #Astronomy_SearchMoonQuarter, this function can be called
7642 * one or more times to continue finding consecutive lunar quarters.
7643 * This function finds the next consecutive moon quarter event after the one passed in as the parameter `mq`.
7644 *
7645 * @param mq
7646 * A value returned by a prior call to #Astronomy_SearchMoonQuarter or #Astronomy_NextMoonQuarter.
7647 *
7648 * @return
7649 * If `mq` is valid, this function should always succeed, indicated by the `status` field
7650 * in the returned structure holding `ASTRO_SUCCESS`. Any other value indicates
7651 * an internal error, which (after confirming that `mq` is valid) should be
7652 * [reported as an issue](https://github.com/cosinekitty/astronomy/issues).
7653 * To be safe, calling code should always check the `status` field for errors.
7654 */
7655astro_moon_quarter_t Astronomy_NextMoonQuarter(astro_moon_quarter_t mq)
7656{
7657 astro_time_t time;
7658 astro_moon_quarter_t next_mq;
7659
7660 if (mq.status != ASTRO_SUCCESS)
7661 return MoonQuarterError(ASTRO_INVALID_PARAMETER);
7662
7663 /* Skip 6 days past the previous found moon quarter to find the next one. */
7664 /* This is less than the minimum possible increment. */
7665 /* So far I have seen the interval well contained by the range (6.5, 8.3) days. */
7666
7667 time = Astronomy_AddDays(mq.time, 6.0);
7668 next_mq = Astronomy_SearchMoonQuarter(time);
7669 if (next_mq.status == ASTRO_SUCCESS)
7670 {
7671 /* Verify that we found the expected moon quarter. */
7672 if (next_mq.quarter != (1 + mq.quarter) % 4)
7673 return MoonQuarterError(ASTRO_WRONG_MOON_QUARTER); /* internal error! we found the wrong moon quarter */
7674 }
7675 return next_mq;
7676}
7677
7678static astro_func_result_t rlon_offset(astro_body_t body, astro_time_t time, int direction, double targetRelLon)
7679{
7680 astro_func_result_t result;
7681 astro_angle_result_t plon, elon;
7682 double diff;
7683
7684 plon = Astronomy_EclipticLongitude(body, time);
7685 if (plon.status != ASTRO_SUCCESS)
7686 return FuncError(plon.status);
7687
7688 elon = Astronomy_EclipticLongitude(BODY_EARTH, time);
7689 if (elon.status != ASTRO_SUCCESS)
7690 return FuncError(elon.status);
7691
7692 diff = direction * (elon.angle - plon.angle);
7693 result.value = LongitudeOffset(diff - targetRelLon);
7694 result.status = ASTRO_SUCCESS;
7695 return result;
7696}
7697
7698/**
7699 * @brief
7700 * Searches for the time when the Earth and another planet are separated by a specified angle
7701 * in ecliptic longitude, as seen from the Sun.
7702 *
7703 * A relative longitude is the angle between two bodies measured in the plane of the Earth's orbit
7704 * (the ecliptic plane). The distance of the bodies above or below the ecliptic plane is ignored.
7705 * If you imagine the shadow of the body cast onto the ecliptic plane, and the angle measured around
7706 * that plane from one body to the other in the direction the planets orbit the Sun, you will get an
7707 * angle somewhere between 0 and 360 degrees. This is the relative longitude.
7708 *
7709 * Given a planet other than the Earth in `body` and a time to start the search in `startTime`,
7710 * this function searches for the next time that the relative longitude measured from the planet
7711 * to the Earth is `targetRelLon`.
7712 *
7713 * Certain astronomical events are defined in terms of relative longitude between the Earth and another planet:
7714 *
7715 * - When the relative longitude is 0 degrees, it means both planets are in the same direction from the Sun.
7716 * For planets that orbit closer to the Sun (Mercury and Venus), this is known as *inferior conjunction*,
7717 * a time when the other planet becomes very difficult to see because of being lost in the Sun's glare.
7718 * (The only exception is in the rare event of a transit, when we see the silhouette of the planet passing
7719 * between the Earth and the Sun.)
7720 *
7721 * - When the relative longitude is 0 degrees and the other planet orbits farther from the Sun,
7722 * this is known as *opposition*. Opposition is when the planet is closest to the Earth, and
7723 * also when it is visible for most of the night, so it is considered the best time to observe the planet.
7724 *
7725 * - When the relative longitude is 180 degrees, it means the other planet is on the opposite side of the Sun
7726 * from the Earth. This is called *superior conjunction*. Like inferior conjunction, the planet is
7727 * very difficult to see from the Earth. Superior conjunction is possible for any planet other than the Earth.
7728 *
7729 * @param body
7730 * A planet other than the Earth. If `body` is not a planet other than the Earth, an error occurs.
7731 *
7732 * @param targetRelLon
7733 * The desired relative longitude, expressed in degrees. Must be in the range [0, 360).
7734 *
7735 * @param startTime
7736 * The date and time at which to begin the search.
7737 *
7738 * @return
7739 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
7740 * and `time` will hold the date and time of the relative longitude event.
7741 * Otherwise `status` will hold some other value that indicates an error condition.
7742 */
7743astro_search_result_t Astronomy_SearchRelativeLongitude(astro_body_t body, double targetRelLon, astro_time_t startTime)
7744{
7745 astro_search_result_t result;
7746 astro_func_result_t syn;
7747 astro_func_result_t error_angle;
7748 double prev_angle;
7749 astro_time_t time;
7750 int iter, direction;
7751
7752 if (body == BODY_EARTH)
7753 return SearchError(ASTRO_EARTH_NOT_ALLOWED);
7754
7755 if (body == BODY_MOON || body == BODY_SUN)
7756 return SearchError(ASTRO_INVALID_BODY);
7757
7758 syn = SynodicPeriod(body);
7759 if (syn.status != ASTRO_SUCCESS)
7760 return SearchError(syn.status);
7761
7762 direction = IsSuperiorPlanet(body) ? +1 : -1;
7763
7764 /* Iterate until we converge on the desired event. */
7765 /* Calculate the error angle, which will be a negative number of degrees, */
7766 /* meaning we are "behind" the target relative longitude. */
7767
7768 error_angle = rlon_offset(body, startTime, direction, targetRelLon);
7769 if (error_angle.status != ASTRO_SUCCESS)
7770 return SearchError(error_angle.status);
7771
7772 if (error_angle.value > 0)
7773 error_angle.value -= 360; /* force searching forward in time */
7774
7775 time = startTime;
7776 for (iter = 0; iter < 100; ++iter)
7777 {
7778 /* Estimate how many days in the future (positive) or past (negative) */
7779 /* we have to go to get closer to the target relative longitude. */
7780 double day_adjust = (-error_angle.value/360.0) * syn.value;
7781 time = Astronomy_AddDays(time, day_adjust);
7782 if (fabs(day_adjust) * SECONDS_PER_DAY < 1.0)
7783 {
7784 result.time = time;
7785 result.status = ASTRO_SUCCESS;
7786 return result;
7787 }
7788
7789 prev_angle = error_angle.value;
7790 error_angle = rlon_offset(body, time, direction, targetRelLon);
7791 if (error_angle.status != ASTRO_SUCCESS)
7792 return SearchError(error_angle.status);
7793
7794 if (fabs(prev_angle) < 30.0 && (prev_angle != error_angle.value))
7795 {
7796 /* Improve convergence for Mercury/Mars (eccentric orbits) */
7797 /* by adjusting the synodic period to more closely match the */
7798 /* variable speed of both planets in this part of their respective orbits. */
7799 double ratio = prev_angle / (prev_angle - error_angle.value);
7800 if (ratio > 0.5 && ratio < 2.0)
7801 syn.value *= ratio;
7802 }
7803 }
7804
7805 return SearchError(ASTRO_NO_CONVERGE);
7806}
7807
7808/**
7809 * @brief Searches for the time when the center of a body reaches a specified hour angle as seen by an observer on the Earth.
7810 *
7811 * The *hour angle* of a celestial body indicates its position in the sky with respect
7812 * to the Earth's rotation. The hour angle depends on the location of the observer on the Earth.
7813 * The hour angle is 0 when the body's center reaches its highest angle above the horizon in a given day.
7814 * The hour angle increases by 1 unit for every sidereal hour that passes after that point, up
7815 * to 24 sidereal hours when it reaches the highest point again. So the hour angle indicates
7816 * the number of hours that have passed since the most recent time that the body has culminated,
7817 * or reached its highest point.
7818 *
7819 * This function searches for the next or previous time a celestial body reaches the given hour angle
7820 * relative to the date and time specified by `startTime`.
7821 * To find when a body culminates, pass 0 for `hourAngle`.
7822 * To find when a body reaches its lowest point in the sky, pass 12 for `hourAngle`.
7823 *
7824 * Note that, especially close to the Earth's poles, a body as seen on a given day
7825 * may always be above the horizon or always below the horizon, so the caller cannot
7826 * assume that a culminating object is visible nor that an object is below the horizon
7827 * at its minimum altitude.
7828 *
7829 * On success, the function reports the date and time, along with the horizontal coordinates
7830 * of the body at that time, as seen by the given observer.
7831 *
7832 * @param body
7833 * The Sun, Moon, any planet other than the Earth,
7834 * or a user-defined star that was created by a call to #Astronomy_DefineStar.
7835 *
7836 * @param observer
7837 * Indicates a location on or near the surface of the Earth where the observer is located.
7838 * Call #Astronomy_MakeObserver to create an observer structure.
7839 *
7840 * @param hourAngle
7841 * An hour angle value in the range [0, 24) indicating the number of sidereal hours after the
7842 * body's most recent culmination.
7843 *
7844 * @param startTime
7845 * The date and time at which to start the search.
7846 *
7847 * @param direction
7848 * The direction in time to perform the search: a positive value
7849 * searches forward in time, a negative value searches backward in time.
7850 * The function will fail with `ASTRO_INVALID_PARAMETER` if `direction` is zero.
7851 *
7852 * @return
7853 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
7854 * and the other structure fields are valid. Otherwise, `status` holds some other value
7855 * that indicates an error condition.
7856 */
7857astro_hour_angle_t Astronomy_SearchHourAngleEx(
7858 astro_body_t body,
7859 astro_observer_t observer,
7860 double hourAngle,
7861 astro_time_t startTime,
7862 int direction)
7863{
7864 int iter = 0;
7865 astro_time_t time;
7866 astro_equatorial_t ofdate;
7867 astro_hour_angle_t result;
7868 double delta_sidereal_hours, delta_days, gast;
7869
7870 if (body == BODY_EARTH)
7871 return HourAngleError(ASTRO_EARTH_NOT_ALLOWED);
7872
7873 if (hourAngle < 0.0 || hourAngle >= 24.0)
7874 return HourAngleError(ASTRO_INVALID_PARAMETER);
7875
7876 if (direction == 0)
7877 return HourAngleError(ASTRO_INVALID_PARAMETER);
7878
7879 time = startTime;
7880 for(;;)
7881 {
7882 ++iter;
7883
7884 /* Calculate Greenwich Apparent Sidereal Time (GAST) at the given time. */
7885 gast = Astronomy_SiderealTime(&time);
7886
7887 /* Obtain equatorial coordinates of date for the body. */
7888 ofdate = Astronomy_Equator(body, &time, observer, EQUATOR_OF_DATE, ABERRATION);
7889 if (ofdate.status != ASTRO_SUCCESS)
7890 return HourAngleError(ofdate.status);
7891
7892 /* Calculate the adjustment needed in sidereal time */
7893 /* to bring the hour angle to the desired value. */
7894
7895 delta_sidereal_hours = fmod((hourAngle + ofdate.ra - observer.longitude/15) - gast, 24.0);
7896 if (iter == 1)
7897 {
7898 /* On the first iteration, always search the requested time direction. */
7899 if (direction > 0)
7900 {
7901 /* Search forward in time. */
7902 if (delta_sidereal_hours < 0.0)
7903 delta_sidereal_hours += 24.0;
7904 }
7905 else
7906 {
7907 /* Search backward in time. */
7908 if (delta_sidereal_hours > 0.0)
7909 delta_sidereal_hours -= 24.0;
7910 }
7911 }
7912 else
7913 {
7914 /* On subsequent iterations, we make the smallest possible adjustment, */
7915 /* either forward or backward in time. */
7916 if (delta_sidereal_hours < -12.0)
7917 delta_sidereal_hours += 24.0;
7918 else if (delta_sidereal_hours > +12.0)
7919 delta_sidereal_hours -= 24.0;
7920 }
7921
7922 /* If the error is tolerable (less than 0.1 seconds), the search has succeeded. */
7923 if (fabs(delta_sidereal_hours) * 3600.0 < 0.1)
7924 {
7925 result.hor = Astronomy_Horizon(&time, observer, ofdate.ra, ofdate.dec, REFRACTION_NORMAL);
7926 result.time = time;
7927 result.status = ASTRO_SUCCESS;
7928 return result;
7929 }
7930
7931 /* We need to loop another time to get more accuracy. */
7932 /* Update the terrestrial time (in solar days) adjusting by sidereal time (sidereal hours). */
7933 delta_days = (delta_sidereal_hours / 24.0) * SOLAR_DAYS_PER_SIDEREAL_DAY;
7934 time = Astronomy_AddDays(time, delta_days);
7935 }
7936}
7937
7938
7939/**
7940 * @brief Finds the hour angle of a body for a given observer and time.
7941 *
7942 * The *hour angle* of a celestial body indicates its position in the sky with respect
7943 * to the Earth's rotation. The hour angle depends on the location of the observer on the Earth.
7944 * The hour angle is 0 when the body's center reaches its highest angle above the horizon in a given day.
7945 * The hour angle increases by 1 unit for every sidereal hour that passes after that point, up
7946 * to 24 sidereal hours when it reaches the highest point again. So the hour angle indicates
7947 * the number of hours that have passed since the most recent time that the body has culminated,
7948 * or reached its highest point.
7949 *
7950 * @param body
7951 * The body whose observed hour angle is to be found.
7952 *
7953 * @param time
7954 * The time of the observation.
7955 *
7956 * @param observer
7957 * The geographic location where the observation takes place.
7958 *
7959 * @return astro_func_result_t
7960 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
7961 * and `value` holds the hour angle in the half-open range [0, 24).
7962 * Otherwise, `status` is an error code that indicates failure.
7963 */
7964astro_func_result_t Astronomy_HourAngle(astro_body_t body, astro_time_t *time, astro_observer_t observer)
7965{
7966 astro_func_result_t result;
7967 astro_equatorial_t ofdate;
7968 double gast;
7969
7970 if (time == NULL)
7971 return FuncError(ASTRO_INVALID_PARAMETER);
7972
7973 /* Calculate Greenwich Apparent Sidereal Time (GAST) at the given time. */
7974 gast = Astronomy_SiderealTime(time);
7975
7976 /* Obtain equatorial coordinates of date for the body. */
7977 ofdate = Astronomy_Equator(body, time, observer, EQUATOR_OF_DATE, ABERRATION);
7978 if (ofdate.status != ASTRO_SUCCESS)
7979 return FuncError(ofdate.status);
7980
7981 result.value = fmod(observer.longitude/15 + gast - ofdate.ra, 24.0);
7982 if (result.value < 0.0)
7983 result.value += 24.0;
7984 result.status = ASTRO_SUCCESS;
7985
7986 return result;
7987}
7988
7989
7990/** @cond DOXYGEN_SKIP */
7991
7992typedef struct
7993{
7994 astro_body_t body;
7995 int direction; // search option: +1 = rise, -1 = set
7996 astro_observer_t observer;
7997 double body_radius_au;
7998 double target_altitude;
7999}
8000context_altitude_t;
8001
8002static const double RISE_SET_DT = 0.42; /* 10.08 hours: Nyquist-safe for 22-hour period. */
8003
8004typedef struct
8005{
8006 astro_status_t status;
8007 astro_time_t tx;
8008 astro_time_t ty;
8009 double ax;
8010 double ay;
8011}
8012ascent_t;
8013
8014int _AltitudeDiffCallCount;
8015int _FindAscentMaxRecursionDepth;
8016
8017/** @endcond */
8018
8019static astro_func_result_t altitude_diff(void *context, astro_time_t time)
8020{
8021 astro_func_result_t result;
8022 astro_equatorial_t ofdate;
8023 astro_horizon_t hor;
8024 double altitude;
8025 const context_altitude_t *p = (const context_altitude_t *)context;
8026
8027 ++_AltitudeDiffCallCount; /* for internal performance testing */
8028
8029 ofdate = Astronomy_Equator(p->body, &time, p->observer, EQUATOR_OF_DATE, ABERRATION);
8030 if (ofdate.status != ASTRO_SUCCESS)
8031 return FuncError(ofdate.status);
8032
8033 hor = Astronomy_Horizon(&time, p->observer, ofdate.ra, ofdate.dec, REFRACTION_NONE);
8034 altitude = hor.altitude + RAD2DEG*asin(p->body_radius_au / ofdate.dist);
8035 result.value = p->direction*(altitude - p->target_altitude);
8036 result.status = ASTRO_SUCCESS;
8037 return result;
8038}
8039
8040
8041static ascent_t AscentError(astro_status_t status)
8042{
8043 ascent_t ascent;
8044 ascent.ax = ascent.ay = NAN;
8045 ascent.tx = ascent.ty = TimeError();
8046 ascent.status = status;
8047 return ascent;
8048}
8049
8050
8051static ascent_t FindAscent(
8052 int depth,
8053 context_altitude_t *context,
8054 double max_deriv_alt,
8055 astro_time_t t1,
8056 astro_time_t t2,
8057 double a1,
8058 double a2)
8059{
8060 ascent_t ascent;
8061 double da, dt, abs_a1, abs_a2;
8062 astro_time_t tm;
8063 astro_func_result_t alt;
8064
8065 /* For internal performance testing. */
8066 if (depth > _FindAscentMaxRecursionDepth)
8067 _FindAscentMaxRecursionDepth = depth;
8068
8069 /* See if we can find any time interval where the altitude-diff function */
8070 /* rises from non-positive to positive. */
8071 /* Return ASTRO_SUCCESS if we do, ASTRO_SEARCH_FAILURE if we don't, or some other status for error cases. */
8072
8073 if (a1 < 0.0 && a2 >= 0.0)
8074 {
8075 /* Trivial success case: the endpoints already rise through zero. */
8076 ascent.status = ASTRO_SUCCESS;
8077 ascent.tx = t1;
8078 ascent.ty = t2;
8079 ascent.ax = a1;
8080 ascent.ay = a2;
8081 return ascent;
8082 }
8083
8084 if (a1 >= 0.0 && a2 < 0.0)
8085 {
8086 /* Trivial failure case: Assume Nyquist condition prevents an ascent. */
8087 return AscentError(ASTRO_SEARCH_FAILURE);
8088 }
8089
8090 if (depth > 17)
8091 {
8092 /*
8093 Safety valve: do not allow unlimited recursion.
8094 This should never happen if the rest of the logic is working correctly,
8095 so fail the whole search if it does happen. It's a bug!
8096 */
8097 return AscentError(ASTRO_NO_CONVERGE);
8098 }
8099
8100 /*
8101 Both altitudes are on the same side of zero: both are negative, or both are non-negative.
8102 There could be a convex "hill" or a concave "valley" that passes through zero.
8103 In polar regions sometimes there is a rise/set or set/rise pair within minutes of each other.
8104 For example, the Moon can be below the horizon, then the very top of it becomes
8105 visible (moonrise) for a few minutes, then it moves sideways and down below
8106 the horizon again (moonset). We want to catch these cases.
8107 However, for efficiency and practicality concerns, because the rise/set search itself
8108 has a 0.1 second threshold, we do not worry about rise/set pairs that are less than
8109 one second apart. These are marginal cases that are rendered highly uncertain
8110 anyway, due to unpredictable atmospheric refraction conditions (air temperature and pressure).
8111 */
8112 dt = (t2.ut - t1.ut) / 2;
8113 if (dt * SECONDS_PER_DAY < 1.0)
8114 return AscentError(ASTRO_SEARCH_FAILURE);
8115
8116 /* Is it possible to reach zero from the altitude that is closer to zero? */
8117 abs_a1 = fabs(a1);
8118 abs_a2 = fabs(a2);
8119 da = (abs_a1 < abs_a2) ? abs_a1 : abs_a2;
8120
8121 /*
8122 Without loss of generality, assume |a1| <= |a2|.
8123 (Reverse the argument in the case |a2| < |a1|.)
8124 Imagine you have to "drive" from a1 to 0, then back to a2.
8125 You can't go faster than max_deriv_alt. If you can't reach 0 in half the time,
8126 you certainly don't have time to reach 0, turn around, and still make your way
8127 back up to a2 (which is at least as far from 0 than a1 is) in the time interval dt.
8128 Therefore, the time threshold is half the time interval, or dt/2.
8129 */
8130 if (da > max_deriv_alt*(dt / 2))
8131 {
8132 /* Prune: the altitude cannot change fast enough to reach zero. */
8133 return AscentError(ASTRO_SEARCH_FAILURE);
8134 }
8135
8136 /* Bisect the time interval and evaluate the altitude at the midpoint. */
8137 tm = Astronomy_TimeFromDays((t1.ut + t2.ut)/2);
8138 alt = altitude_diff(context, tm);
8139 if (alt.status != ASTRO_SUCCESS)
8140 return AscentError(ASTRO_SEARCH_FAILURE);
8141
8142 /* Recurse to the left interval. */
8143 ascent = FindAscent(1+depth, context, max_deriv_alt, t1, tm, a1, alt.value);
8144 if (ascent.status == ASTRO_SEARCH_FAILURE)
8145 {
8146 /* Recurse to the right interval. */
8147 ascent = FindAscent(1+depth, context, max_deriv_alt, tm, t2, alt.value, a2);
8148 }
8149
8150 return ascent;
8151}
8152
8153
8154static astro_func_result_t MaxAltitudeSlope(astro_body_t body, double latitude)
8155{
8156 astro_func_result_t result;
8157 double deriv_ra, deriv_dec, latrad;
8158
8159 if (!isfinite(latitude) || latitude < -90.0 || latitude > +90.0)
8160 {
8161 result.value = NAN;
8162 result.status = ASTRO_INVALID_PARAMETER;
8163 return result;
8164 }
8165
8166 /*
8167 Calculate the maximum possible rate that this body's altitude
8168 could change [degrees/day] as seen by this observer.
8169 First use experimentally determined extreme bounds for this body
8170 of how much topocentric RA and DEC can ever change per rate of time.
8171 We need minimum possible d(RA)/dt, and maximum possible magnitude of d(DEC)/dt.
8172 Conservatively, we round d(RA)/dt down, d(DEC)/dt up.
8173 Then calculate the resulting maximum possible altitude change rate.
8174 */
8175
8176 switch (body)
8177 {
8178 case BODY_MOON:
8179 deriv_ra = +4.5;
8180 deriv_dec = +8.2;
8181 break;
8182
8183 case BODY_SUN:
8184 deriv_ra = +0.8;
8185 deriv_dec = +0.5;
8186 break;
8187
8188 case BODY_MERCURY:
8189 deriv_ra = -1.6;
8190 deriv_dec = +1.0;
8191 break;
8192
8193 case BODY_VENUS:
8194 deriv_ra = -0.8;
8195 deriv_dec = +0.6;
8196 break;
8197
8198 case BODY_MARS:
8199 deriv_ra = -0.5;
8200 deriv_dec = +0.4;
8201 break;
8202
8203 case BODY_JUPITER:
8204 case BODY_SATURN:
8205 case BODY_URANUS:
8206 case BODY_NEPTUNE:
8207 case BODY_PLUTO:
8208 deriv_ra = -0.2;
8209 deriv_dec = +0.2;
8210 break;
8211
8212 case BODY_EARTH:
8213 result.value = NAN;
8214 result.status = ASTRO_EARTH_NOT_ALLOWED;
8215 return result;
8216
8217 default:
8218 if (UserDefinedStar(body))
8219 {
8220 /*
8221 The minimum allowed heliocentric distance of a user-defined star
8222 is one light-year. This can cause a tiny amount of parallax (about 0.001 degrees).
8223 Also, including stellar aberration (22 arcsec = 0.006 degrees), we provide a
8224 generous safety buffer of 0.008 degrees.
8225 */
8226 deriv_ra = -0.008;
8227 deriv_dec = +0.008;
8228 break;
8229 }
8230 result.value = NAN;
8231 result.status = ASTRO_INVALID_BODY;
8232 return result;
8233 }
8234
8235 latrad = DEG2RAD * latitude;
8236 result.value = fabs(((360.0 / SOLAR_DAYS_PER_SIDEREAL_DAY) - deriv_ra)*cos(latrad)) + fabs(deriv_dec*sin(latrad));
8237 result.status = isfinite(result.value) ? ASTRO_SUCCESS : ASTRO_INTERNAL_ERROR;
8238 return result;
8239}
8240
8241
8242static astro_search_result_t InternalSearchAltitude(
8243 astro_body_t body,
8244 astro_observer_t observer,
8245 astro_direction_t direction,
8246 astro_time_t startTime,
8247 double limitDays,
8248 double bodyRadiusAu,
8249 double targetAltitude)
8250{
8251 astro_search_result_t search_result;
8252 astro_func_result_t func_result;
8253 context_altitude_t context;
8254 ascent_t ascent;
8255 astro_time_t t1, t2;
8256 double a1, a2, max_deriv_alt;
8257
8258 if (!isfinite(targetAltitude) || targetAltitude < -90.0 || targetAltitude > +90.0)
8259 return SearchError(ASTRO_INVALID_PARAMETER);
8260
8261 func_result = MaxAltitudeSlope(body, observer.latitude);
8262 if (func_result.status != ASTRO_SUCCESS)
8263 return SearchError(func_result.status);
8264 max_deriv_alt = func_result.value;
8265
8266 context.body = body;
8267 context.direction = (int)direction;
8268 context.observer = observer;
8269 context.body_radius_au = bodyRadiusAu;
8270 context.target_altitude = targetAltitude;
8271
8272 /* We allow searching forward or backward in time. */
8273 /* But we want to keep t1 < t2, so we need a few if/else statements. */
8274 t1 = t2 = startTime;
8275 func_result = altitude_diff(&context, t2);
8276 if (func_result.status != ASTRO_SUCCESS)
8277 return SearchError(func_result.status);
8278 a1 = a2 = func_result.value;
8279
8280 for(;;)
8281 {
8282 if (limitDays < 0.0)
8283 {
8284 t1 = Astronomy_AddDays(t2, -RISE_SET_DT);
8285 func_result = altitude_diff(&context, t1);
8286 if (func_result.status != ASTRO_SUCCESS)
8287 return SearchError(func_result.status);
8288 a1 = func_result.value;
8289 }
8290 else
8291 {
8292 t2 = Astronomy_AddDays(t1, +RISE_SET_DT);
8293 func_result = altitude_diff(&context, t2);
8294 if (func_result.status != ASTRO_SUCCESS)
8295 return SearchError(func_result.status);
8296 a2 = func_result.value;
8297 }
8298
8299 ascent = FindAscent(0, &context, max_deriv_alt, t1, t2, a1, a2);
8300 if (ascent.status == ASTRO_SUCCESS)
8301 {
8302 /* We found a time interval [t1, t2] that contains an alt-diff */
8303 /* rising from negative a1 to non-negative a2. */
8304 /* Search for the time where the root occurs. */
8305 search_result = Astronomy_Search(altitude_diff, &context, ascent.tx, ascent.ty, 0.1);
8306 if (search_result.status == ASTRO_SUCCESS)
8307 {
8308 /* Now that we have a solution, we have to check whether it goes outside the time bounds. */
8309 if (limitDays < 0.0)
8310 {
8311 if (search_result.time.ut < startTime.ut + limitDays)
8312 return SearchError(ASTRO_SEARCH_FAILURE);
8313 }
8314 else
8315 {
8316 if (search_result.time.ut > startTime.ut + limitDays)
8317 return SearchError(ASTRO_SEARCH_FAILURE);
8318 }
8319 return search_result; /* success! */
8320 }
8321
8322 /* The search should have succeeded. Something is wrong with FindAscent! */
8323 return SearchError(ASTRO_INTERNAL_ERROR);
8324 }
8325 else if (ascent.status == ASTRO_SEARCH_FAILURE)
8326 {
8327 /* There is no ascent in this interval, so keep searching. */
8328 }
8329 else
8330 {
8331 /* An unexpected error occurred. Fail the search. */
8332 return SearchError(ascent.status);
8333 }
8334
8335 if (limitDays < 0.0)
8336 {
8337 if (t1.ut < startTime.ut + limitDays)
8338 return SearchError(ASTRO_SEARCH_FAILURE);
8339 t2 = t1;
8340 a2 = a1;
8341 }
8342 else
8343 {
8344 if (t2.ut > startTime.ut + limitDays)
8345 return SearchError(ASTRO_SEARCH_FAILURE);
8346 t1 = t2;
8347 a1 = a2;
8348 }
8349 }
8350}
8351
8352
8353/**
8354 * @brief Calculates U.S. Standard Atmosphere (1976) variables as a function of elevation.
8355 *
8356 * This function calculates idealized values of pressure, temperature, and density
8357 * using the U.S. Standard Atmosphere (1976) model.
8358 *
8359 * See:
8360 * https://hbcp.chemnetbase.com/faces/documents/14_12/14_12_0001.xhtml
8361 * https://ntrs.nasa.gov/api/citations/19770009539/downloads/19770009539.pdf
8362 * https://www.ngdc.noaa.gov/stp/space-weather/online-publications/miscellaneous/us-standard-atmosphere-1976/us-standard-atmosphere_st76-1562_noaa.pdf
8363 *
8364 * @param elevationMeters
8365 * The elevation above sea level at which to calculate atmospheric variables.
8366 * The value must be at least -500 to +100000, or the function will
8367 * fail with status `ASTRO_INVALID_PARAMETER`.
8368 *
8369 * @return astro_atmosphere_tp0
8370 */
8371astro_atmosphere_t Astronomy_Atmosphere(double elevationMeters)
8372{
8373 astro_atmosphere_t atmos;
8374 const double P0 = 101325.0; /* pressure at sea level [pascals] */
8375 const double T0 = 288.15; /* temperature at sea level [kelvins] */
8376 const double T1 = 216.65; /* temperature between 20 km and 32 km [kelvins] */
8377
8378 /*
8379 Formulas for air temperature and pressure at a height of `h` meters
8380 were found at:
8381 https://hbcp.chemnetbase.com/faces/documents/14_12/14_12_0001.xhtml
8382
8383 These in turn come from:
8384 1. COESA, U.S. Standard Atmosphere, 1976, U.S. Government Printing Office, Washington, DC, 1976.
8385 2. Jursa, A. S., Ed., Handbook of Geophysics and the Space Environment, Air Force Geophysics Laboratory, 1985.
8386 */
8387
8388 if (!isfinite(elevationMeters) || elevationMeters < -500.0 || elevationMeters > 100000.0)
8389 {
8390 /* Invalid elevation. */
8391 atmos.status = ASTRO_INVALID_PARAMETER;
8392 atmos.pressure = atmos.temperature = atmos.density = NAN;
8393 }
8394 else
8395 {
8396 if (elevationMeters <= 11000.0)
8397 {
8398 atmos.temperature = T0 - 0.0065*elevationMeters;
8399 atmos.pressure = P0 * pow(T0 / atmos.temperature, -5.25577);
8400 }
8401 else if (elevationMeters <= 20000.0)
8402 {
8403 atmos.temperature = T1;
8404 atmos.pressure = 22632.0 * exp(-0.00015768832 * (elevationMeters - 11000.0));
8405 }
8406 else
8407 {
8408 atmos.temperature = T1 + 0.001*(elevationMeters - 20000.0);
8409 atmos.pressure = 5474.87 * pow(T1 / atmos.temperature, 34.16319);
8410 }
8411 /* The density is calculated relative to the sea level value. */
8412 /* Using the ideal gas law PV=nRT, we deduce that density is proportional to P/T. */
8413 atmos.density = (atmos.pressure / atmos.temperature) / (P0 / T0);
8414 atmos.status = ASTRO_SUCCESS;
8415 }
8416
8417 return atmos;
8418}
8419
8420
8421static double HorizonDipAngle(
8422 astro_observer_t observer,
8423 double metersAboveGround)
8424{
8425 double phi, sinphi, cosphi, c, s, ht_km, ach, ash, radius_m;
8426 double k, dip;
8427
8428 /* Calculate the effective radius of the Earth at ground level below the observer. */
8429 /* Correct for the Earth's oblateness. */
8430 phi = observer.latitude * DEG2RAD;
8431 sinphi = sin(phi);
8432 cosphi = cos(phi);
8433 c = 1.0 / hypot(cosphi, sinphi*EARTH_FLATTENING);
8434 s = c * (EARTH_FLATTENING * EARTH_FLATTENING);
8435 ht_km = (observer.height - metersAboveGround) / 1000.0; /* height of ground above sea level */
8436 ach = EARTH_EQUATORIAL_RADIUS_KM*c + ht_km;
8437 ash = EARTH_EQUATORIAL_RADIUS_KM*s + ht_km;
8438 radius_m = 1000.0 * hypot(ach*cosphi, ash*sinphi);
8439
8440 /*
8441 Correct refraction of a ray of light traveling tangent to the Earth's surface.
8442 Based on: https://www.largeformatphotography.info/sunmooncalc/SMCalc.js
8443 which in turn derives from:
8444 Sweer, John. 1938. The Path of a Ray of Light Tangent to the Surface of the Earth.
8445 Journal of the Optical Society of America 28 (September):327-329.
8446 */
8447
8448 /* k = refraction index */
8449 k = 0.175 * pow(1.0 - (6.5e-3/283.15)*(observer.height - (2.0/3.0)*metersAboveGround), 3.256);
8450
8451 /* Calculate how far below the observer's horizontal plane the observed horizon dips. */
8452 dip = RAD2DEG * -(sqrt(2*(1 - k)*metersAboveGround / radius_m) / (1 - k));
8453 return dip;
8454}
8455
8456
8457
8458/**
8459 * @brief Searches for the next time a celestial body rises or sets as seen by an observer on the Earth.
8460 *
8461 * This function finds the next rise or set time of the Sun, Moon, or planet other than the Earth.
8462 * Rise time is when the body first starts to be visible above the horizon.
8463 * For example, sunrise is the moment that the top of the Sun first appears to peek above the horizon.
8464 * Set time is the moment when the body appears to vanish below the horizon.
8465 * Therefore, this function adjusts for the apparent angular radius of the observed body
8466 * (significant only for the Sun and Moon).
8467 *
8468 * This function corrects for a typical value of atmospheric refraction, which causes celestial
8469 * bodies to appear higher above the horizon than they would if the Earth had no atmosphere.
8470 * Astronomy Engine uses a correction of 34 arcminutes. Real-world refraction varies based
8471 * on air temperature, pressure, and humidity; such weather-based conditions are outside
8472 * the scope of Astronomy Engine.
8473 *
8474 * Note that rise or set may not occur in every 24 hour period.
8475 * For example, near the Earth's poles, there are long periods of time where
8476 * the Sun stays below the horizon, never rising.
8477 * Also, it is possible for the Moon to rise just before midnight but not set during the subsequent 24-hour day.
8478 * This is because the Moon sets nearly an hour later each day due to orbiting the Earth a
8479 * significant amount during each rotation of the Earth.
8480 * Therefore callers must not assume that the function will always succeed.
8481 *
8482 * @param body
8483 * The Sun, Moon, any planet other than the Earth,
8484 * or a user-defined star that was created by a call to #Astronomy_DefineStar.
8485 *
8486 * @param observer
8487 * The location where observation takes place.
8488 * You can create an observer structure by calling #Astronomy_MakeObserver.
8489 *
8490 * @param direction
8491 * Either `DIRECTION_RISE` to find a rise time or `DIRECTION_SET` to find a set time.
8492 *
8493 * @param startTime
8494 * The date and time at which to start the search.
8495 *
8496 * @param limitDays
8497 * Limits how many days to search for a rise or set time, and defines
8498 * the direction in time to search. When `limitDays` is positive, the
8499 * search is performed into the future, after `startTime`.
8500 * When negative, the search is performed into the past, before `startTime`.
8501 * To limit a rise or set time to the same day, you can use a value of 1 day.
8502 * In cases where you want to find the next rise or set time no matter how far
8503 * in the future (for example, for an observer near the south pole), you can
8504 * pass in a larger value like 365.
8505 *
8506 * @param metersAboveGround
8507 * Usually the observer is located at ground level. Then this parameter
8508 * should be zero. But if the observer is significantly higher than ground
8509 * level, for example in an airplane, this parameter should be a positive
8510 * number indicating how far above the ground the observer is.
8511 * An error occurs if `metersAboveGround` is negative.
8512 *
8513 * @return
8514 * On success, the `status` field in the returned structure contains `ASTRO_SUCCESS`
8515 * and the `time` field contains the date and time of the rise or set time as requested.
8516 * If the `status` field contains `ASTRO_SEARCH_FAILURE`, it means the rise or set
8517 * event does not occur within `limitDays` days of `startTime`. This is a normal condition,
8518 * not an error. Any other value of `status` indicates an error of some kind.
8519 */
8520astro_search_result_t Astronomy_SearchRiseSetEx(
8521 astro_body_t body,
8522 astro_observer_t observer,
8523 astro_direction_t direction,
8524 astro_time_t startTime,
8525 double limitDays,
8526 double metersAboveGround)
8527{
8528 double altitude, dip;
8529 double body_radius_au;
8530 astro_atmosphere_t atmos;
8531
8532 if (!isfinite(metersAboveGround) || (metersAboveGround < 0.0))
8533 return SearchError(ASTRO_INVALID_PARAMETER);
8534
8535 switch (body)
8536 {
8537 case BODY_SUN: body_radius_au = SUN_RADIUS_AU; break;
8538 case BODY_MOON: body_radius_au = MOON_EQUATORIAL_RADIUS_AU; break;
8539 default: body_radius_au = 0.0; break;
8540 }
8541
8542 /* Calculate atmospheric density at ground level. */
8543 atmos = Astronomy_Atmosphere(observer.height - metersAboveGround);
8544 if (atmos.status != ASTRO_SUCCESS)
8545 return SearchError(atmos.status);
8546
8547 /* Calculate the apparent angular dip of the horizon. */
8548 dip = HorizonDipAngle(observer, metersAboveGround);
8549
8550 /* Correct refraction for objects near the horizon, using atmospheric density at the ground. */
8551 altitude = dip - (REFRACTION_NEAR_HORIZON * atmos.density);
8552
8553 /* Search for the top of the body crossing the corrected altitude angle. */
8554 return InternalSearchAltitude(body, observer, direction, startTime, limitDays, body_radius_au, altitude);
8555}
8556
8557
8558/**
8559 * @brief Finds the next time the center of a body passes through a given altitude.
8560 *
8561 * Finds when the center of the given body ascends or descends through a given
8562 * altitude angle, as seen by an observer at the specified location on the Earth.
8563 * By using the appropriate combination of `direction` and `altitude` parameters,
8564 * this function can be used to find when civil, nautical, or astronomical twilight
8565 * begins (dawn) or ends (dusk).
8566 *
8567 * Civil dawn begins before sunrise when the Sun ascends through 6 degrees below
8568 * the horizon. To find civil dawn, pass `DIRECTION_RISE` for `direction` and -6 for `altitude`.
8569 *
8570 * Civil dusk ends after sunset when the Sun descends through 6 degrees below the horizon.
8571 * To find civil dusk, pass `DIRECTION_SET` for `direction` and -6 for `altitude`.
8572 *
8573 * Nautical twilight is similar to civil twilight, only the `altitude` value should be -12 degrees.
8574 *
8575 * Astronomical twilight uses -18 degrees as the `altitude` value.
8576 *
8577 * By convention for twilight time calculations, the altitude is not corrected for
8578 * atmospheric refraction. This is because the target altitudes are below the horizon,
8579 * and refraction is not directly observable.
8580 *
8581 * `Astronomy_SearchAltitude` is not intended to find rise/set times of a body for two reasons:
8582 * (1) Rise/set times of the Sun or Moon are defined by their topmost visible portion, not their centers.
8583 * (2) Rise/set times are affected significantly by atmospheric refraction.
8584 * Therefore, it is better to use #Astronomy_SearchRiseSetEx to find rise/set times, which
8585 * corrects for both of these considerations.
8586 *
8587 * `Astronomy_SearchAltitude` will not work reliably for altitudes at or near the body's
8588 * maximum or minimum altitudes. To find the time a body reaches minimum or maximum altitude
8589 * angles, use #Astronomy_SearchHourAngleEx.
8590 *
8591 * @param body
8592 * The Sun, Moon, any planet other than the Earth,
8593 * or a user-defined star that was created by a call to #Astronomy_DefineStar.
8594 *
8595 * @param observer
8596 * The location where observation takes place.
8597 * You can create an observer structure by calling #Astronomy_MakeObserver.
8598 *
8599 * @param direction
8600 * Either `DIRECTION_RISE` to find when the body ascends through the altitude,
8601 * or `DIRECTION_SET` for when the body descends through the altitude.
8602 *
8603 * @param startTime
8604 * The date and time at which to start the search.
8605 *
8606 * @param limitDays
8607 * Limits how many days to search for the body reaching the altitude angle,
8608 * and defines the direction in time to search. When `limitDays` is positive, the
8609 * search is performed into the future, after `startTime`.
8610 * When negative, the search is performed into the past, before `startTime`.
8611 * To limit the search to the same day, you can use a value of 1 day.
8612 * In cases where you want to find the altitude event no matter how far
8613 * in the future (for example, for an observer near the south pole), you can
8614 * pass in a larger value like 365.
8615 *
8616 * @param altitude
8617 * The desired altitude angle of the body's center above (positive)
8618 * or below (negative) the observer's local horizon, expressed in degrees.
8619 * Must be in the range [-90, +90].
8620 *
8621 * @return
8622 * On success, the `status` field in the returned structure contains `ASTRO_SUCCESS`
8623 * and the `time` field contains the date and time of the requested altitude event.
8624 * If the `status` field contains `ASTRO_SEARCH_FAILURE`, it means the altitude
8625 * event does not occur within `limitDays` days of `startTime`. This is a normal condition,
8626 * not an error. Any other value of `status` indicates an error of some kind.
8627 */
8628astro_search_result_t Astronomy_SearchAltitude(
8629 astro_body_t body,
8630 astro_observer_t observer,
8631 astro_direction_t direction,
8632 astro_time_t startTime,
8633 double limitDays,
8634 double altitude)
8635{
8636 return InternalSearchAltitude(body, observer, direction, startTime, limitDays, 0.0, altitude);
8637}
8638
8639
8640static double MoonMagnitude(double phase, double helio_dist, double geo_dist)
8641{
8642 /* https://astronomy.stackexchange.com/questions/10246/is-there-a-simple-analytical-formula-for-the-lunar-phase-brightness-curve */
8643 double rad = phase * DEG2RAD;
8644 double rad2 = rad * rad;
8645 double rad4 = rad2 * rad2;
8646 double mag = -12.717 + 1.49*fabs(rad) + 0.0431*rad4;
8647 double moon_mean_distance_au = 385000.6 / KM_PER_AU;
8648 double geo_au = geo_dist / moon_mean_distance_au;
8649 mag += 5*log10(helio_dist * geo_au);
8650 return mag;
8651}
8652
8653static astro_status_t SaturnMagnitude(
8654 double phase,
8655 double helio_dist,
8656 double geo_dist,
8657 astro_vector_t gc,
8658 astro_time_t time,
8659 double *mag,
8660 double *ring_tilt)
8661{
8662 astro_ecliptic_t eclip;
8663 double ir, Nr, lat, lon, tilt, sin_tilt;
8664
8665 *mag = *ring_tilt = NAN;
8666
8667 /* Based on formulas by Paul Schlyter found here: */
8668 /* http://www.stjarnhimlen.se/comp/ppcomp.html#15 */
8669
8670 /* We must handle Saturn's rings as a major component of its visual magnitude. */
8671 /* Find geocentric ecliptic coordinates of Saturn. */
8672 eclip = Astronomy_Ecliptic(gc);
8673 if (eclip.status != ASTRO_SUCCESS)
8674 return eclip.status;
8675
8676 ir = DEG2RAD * 28.06; /* tilt of Saturn's rings to the ecliptic, in radians */
8677 Nr = DEG2RAD * (169.51 + (3.82e-5 * time.tt)); /* ascending node of Saturn's rings, in radians */
8678
8679 /* Find tilt of Saturn's rings, as seen from Earth. */
8680 lat = DEG2RAD * eclip.elat;
8681 lon = DEG2RAD * eclip.elon;
8682 tilt = asin(sin(lat)*cos(ir) - cos(lat)*sin(ir)*sin(lon-Nr));
8683 sin_tilt = sin(fabs(tilt));
8684
8685 *mag = -9.0 + 0.044*phase;
8686 *mag += sin_tilt*(-2.6 + 1.2*sin_tilt);
8687 *mag += 5.0 * log10(helio_dist * geo_dist);
8688
8689 *ring_tilt = RAD2DEG * tilt;
8690
8691 return ASTRO_SUCCESS;
8692}
8693
8694static astro_status_t VisualMagnitude(
8695 astro_body_t body,
8696 double phase,
8697 double helio_dist,
8698 double geo_dist,
8699 double *mag)
8700{
8701 /* For Mercury and Venus, see: https://iopscience.iop.org/article/10.1086/430212 */
8702 double c0, c1=0, c2=0, c3=0, x;
8703 *mag = NAN;
8704 switch (body)
8705 {
8706 case BODY_MERCURY: c0 = -0.60, c1 = +4.98, c2 = -4.88, c3 = +3.02; break;
8707 case BODY_VENUS:
8708 if (phase < 163.6)
8709 c0 = -4.47, c1 = +1.03, c2 = +0.57, c3 = +0.13;
8710 else
8711 c0 = 0.98, c1 = -1.02;
8712 break;
8713 case BODY_MARS: c0 = -1.52, c1 = +1.60; break;
8714 case BODY_JUPITER: c0 = -9.40, c1 = +0.50; break;
8715 case BODY_URANUS: c0 = -7.19, c1 = +0.25; break;
8716 case BODY_NEPTUNE: c0 = -6.87; break;
8717 case BODY_PLUTO: c0 = -1.00, c1 = +4.00; break;
8718 default: return ASTRO_INVALID_BODY;
8719 }
8720
8721 x = phase / 100;
8722 *mag = c0 + x*(c1 + x*(c2 + x*c3));
8723 *mag += 5.0 * log10(helio_dist * geo_dist);
8724 return ASTRO_SUCCESS;
8725}
8726
8727/**
8728 * @brief
8729 * Finds visual magnitude, phase angle, and other illumination information about a celestial body.
8730 *
8731 * This function calculates information about how bright a celestial body appears from the Earth,
8732 * reported as visual magnitude, which is a smaller (or even negative) number for brighter objects
8733 * and a larger number for dimmer objects.
8734 *
8735 * For bodies other than the Sun, it reports a phase angle, which is the angle in degrees between
8736 * the Sun and the Earth, as seen from the center of the body. Phase angle indicates what fraction
8737 * of the body appears illuminated as seen from the Earth. For example, when the phase angle is
8738 * near zero, it means the body appears "full" as seen from the Earth. A phase angle approaching
8739 * 180 degrees means the body appears as a thin crescent as seen from the Earth. A phase angle
8740 * of 90 degrees means the body appears "half full".
8741 * For the Sun, the phase angle is always reported as 0; the Sun emits light rather than reflecting it,
8742 * so it doesn't have a phase angle.
8743 *
8744 * When the body is Saturn, the returned structure contains a field `ring_tilt` that holds
8745 * the tilt angle in degrees of Saturn's rings as seen from the Earth. A value of 0 means
8746 * the rings appear edge-on, and are thus nearly invisible from the Earth. The `ring_tilt` holds
8747 * 0 for all bodies other than Saturn.
8748 *
8749 * @param body
8750 * The Sun, Moon, or any planet other than the Earth.
8751 *
8752 * @param time
8753 * The date and time of the observation.
8754 *
8755 * @return
8756 * On success, the `status` field of the return structure holds `ASTRO_SUCCESS`
8757 * and the other structure fields are valid.
8758 * Any other value indicates an error, in which case the remaining structure fields are not valid.
8759 */
8760astro_illum_t Astronomy_Illumination(astro_body_t body, astro_time_t time)
8761{
8762 astro_vector_t earth; /* vector from Sun to Earth */
8763 astro_vector_t hc; /* vector from Sun to body */
8764 astro_vector_t gc; /* vector from Earth to body */
8765 double mag; /* visual magnitude */
8766 astro_angle_result_t phase; /* phase angle in degrees between Earth and Sun as seen from body */
8767 double helio_dist; /* distance from Sun to body */
8768 double geo_dist; /* distance from Earth to body */
8769 double ring_tilt = 0.0; /* Saturn's ring tilt (0 for all other bodies) */
8770 astro_illum_t illum;
8771 astro_status_t status;
8772
8773 if (body == BODY_EARTH)
8774 return IllumError(ASTRO_EARTH_NOT_ALLOWED);
8775
8776 earth = CalcEarth(time);
8777 if (earth.status != ASTRO_SUCCESS)
8778 return IllumError(earth.status);
8779
8780 if (body == BODY_SUN)
8781 {
8782 gc.status = ASTRO_SUCCESS;
8783 gc.t = time;
8784 gc.x = -earth.x;
8785 gc.y = -earth.y;
8786 gc.z = -earth.z;
8787
8788 hc.status = ASTRO_SUCCESS;
8789 hc.t = time;
8790 hc.x = 0.0;
8791 hc.y = 0.0;
8792 hc.z = 0.0;
8793
8794 /* The Sun emits light instead of reflecting it, */
8795 /* so we report a placeholder phase angle of 0. */
8796 phase.status = ASTRO_SUCCESS;
8797 phase.angle = 0.0;
8798 }
8799 else
8800 {
8801 if (body == BODY_MOON)
8802 {
8803 /* For extra numeric precision, use geocentric Moon formula directly. */
8804 gc = Astronomy_GeoMoon(time);
8805 if (gc.status != ASTRO_SUCCESS)
8806 return IllumError(gc.status);
8807
8808 hc.status = ASTRO_SUCCESS;
8809 hc.t = time;
8810 hc.x = earth.x + gc.x;
8811 hc.y = earth.y + gc.y;
8812 hc.z = earth.z + gc.z;
8813 }
8814 else
8815 {
8816 /* For planets, the heliocentric vector is more direct to calculate. */
8817 hc = Astronomy_HelioVector(body, time);
8818 if (hc.status != ASTRO_SUCCESS)
8819 return IllumError(hc.status);
8820
8821 gc.status = ASTRO_SUCCESS;
8822 gc.t = time;
8823 gc.x = hc.x - earth.x;
8824 gc.y = hc.y - earth.y;
8825 gc.z = hc.z - earth.z;
8826 }
8827
8828 phase = Astronomy_AngleBetween(gc, hc);
8829 if (phase.status != ASTRO_SUCCESS)
8830 return IllumError(phase.status);
8831 }
8832
8833 geo_dist = Astronomy_VectorLength(gc);
8834 helio_dist = Astronomy_VectorLength(hc);
8835
8836 switch (body)
8837 {
8838 case BODY_SUN:
8839 mag = -0.17 + 5.0*log10(geo_dist / AU_PER_PARSEC);
8840 break;
8841
8842 case BODY_MOON:
8843 mag = MoonMagnitude(phase.angle, helio_dist, geo_dist);
8844 break;
8845
8846 case BODY_SATURN:
8847 status = SaturnMagnitude(phase.angle, helio_dist, geo_dist, gc, time, &mag, &ring_tilt);
8848 if (status != ASTRO_SUCCESS)
8849 return IllumError(status);
8850 break;
8851
8852 default:
8853 status = VisualMagnitude(body, phase.angle, helio_dist, geo_dist, &mag);
8854 if (status != ASTRO_SUCCESS)
8855 return IllumError(status);
8856 break;
8857 }
8858
8859 illum.status = ASTRO_SUCCESS;
8860 illum.time = time;
8861 illum.mag = mag;
8862 illum.phase_angle = phase.angle;
8863 illum.phase_fraction = (1.0 + cos(DEG2RAD * phase.angle)) / 2.0;
8864 illum.helio_dist = helio_dist;
8865 illum.ring_tilt = ring_tilt;
8866
8867 return illum;
8868}
8869
8870static astro_func_result_t mag_slope(void *context, astro_time_t time)
8871{
8872 /*
8873 The Search() function finds a transition from negative to positive values.
8874 The derivative of magnitude y with respect to time t (dy/dt)
8875 is negative as an object gets brighter, because the magnitude numbers
8876 get smaller. At peak magnitude dy/dt = 0, then as the object gets dimmer,
8877 dy/dt > 0.
8878 */
8879 static const double dt = 0.01;
8880 astro_illum_t y1, y2;
8881 astro_body_t body = *((astro_body_t *)context);
8882 astro_time_t t1 = Astronomy_AddDays(time, -dt/2);
8883 astro_time_t t2 = Astronomy_AddDays(time, +dt/2);
8884 astro_func_result_t result;
8885
8886 y1 = Astronomy_Illumination(body, t1);
8887 if (y1.status != ASTRO_SUCCESS)
8888 return FuncError(y1.status);
8889
8890 y2 = Astronomy_Illumination(body, t2);
8891 if (y2.status != ASTRO_SUCCESS)
8892 return FuncError(y2.status);
8893
8894 result.value = (y2.mag - y1.mag) / dt;
8895 result.status = ASTRO_SUCCESS;
8896 return result;
8897}
8898
8899/**
8900 * @brief
8901 * Searches for the date and time Venus will next appear brightest as seen from the Earth.
8902 *
8903 * This function searches for the date and time Venus appears brightest as seen from the Earth.
8904 * Currently only Venus is supported for the `body` parameter, though this could change in the future.
8905 * Mercury's peak magnitude occurs at superior conjunction, when it is virtually impossible to see from the Earth,
8906 * so peak magnitude events have little practical value for that planet.
8907 * Planets other than Venus and Mercury reach peak magnitude at opposition, which can
8908 * be found using #Astronomy_SearchRelativeLongitude.
8909 * The Moon reaches peak magnitude at full moon, which can be found using
8910 * #Astronomy_SearchMoonQuarter or #Astronomy_SearchMoonPhase.
8911 * The Sun reaches peak magnitude at perihelion, which occurs each year in January.
8912 * However, the difference is minor and has little practical value.
8913 *
8914 * @param body
8915 * Currently only `BODY_VENUS` is allowed. Any other value results in the error `ASTRO_INVALID_BODY`.
8916 * See function remarks for more details.
8917 *
8918 * @param startTime
8919 * The date and time to start searching for the next peak magnitude event.
8920 *
8921 * @return
8922 * See documentation about the return value from #Astronomy_Illumination.
8923 */
8924astro_illum_t Astronomy_SearchPeakMagnitude(astro_body_t body, astro_time_t startTime)
8925{
8926 /* s1 and s2 are relative longitudes within which peak magnitude of Venus can occur. */
8927 static const double s1 = 10.0;
8928 static const double s2 = 30.0;
8929 int iter;
8930 astro_angle_result_t plon, elon;
8931 astro_search_result_t t1, t2, tx;
8932 astro_func_result_t syn, m1, m2;
8933 astro_time_t t_start;
8934 double rlon, rlon_lo, rlon_hi, adjust_days;
8935
8936 if (body != BODY_VENUS)
8937 return IllumError(ASTRO_INVALID_BODY);
8938
8939 iter = 0;
8940 while (++iter <= 2)
8941 {
8942 /* Find current heliocentric relative longitude between the */
8943 /* inferior planet and the Earth. */
8944 plon = Astronomy_EclipticLongitude(body, startTime);
8945 if (plon.status != ASTRO_SUCCESS)
8946 return IllumError(plon.status);
8947
8948 elon = Astronomy_EclipticLongitude(BODY_EARTH, startTime);
8949 if (elon.status != ASTRO_SUCCESS)
8950 return IllumError(elon.status);
8951
8952 rlon = LongitudeOffset(plon.angle - elon.angle); /* clamp to (-180, +180]. */
8953
8954 /* The slope function is not well-behaved when rlon is near 0 degrees or 180 degrees */
8955 /* because there is a cusp there that causes a discontinuity in the derivative. */
8956 /* So we need to guard against searching near such times. */
8957
8958 if (rlon >= -s1 && rlon < +s1)
8959 {
8960 /* Seek to the window [+s1, +s2]. */
8961 adjust_days = 0.0;
8962 /* Search forward for the time t1 when rel lon = +s1. */
8963 rlon_lo = +s1;
8964 /* Search forward for the time t2 when rel lon = +s2. */
8965 rlon_hi = +s2;
8966 }
8967 else if (rlon >= +s2 || rlon < -s2)
8968 {
8969 /* Seek to the next search window at [-s2, -s1]. */
8970 adjust_days = 0.0;
8971 /* Search forward for the time t1 when rel lon = -s2. */
8972 rlon_lo = -s2;
8973 /* Search forward for the time t2 when rel lon = -s1. */
8974 rlon_hi = -s1;
8975 }
8976 else if (rlon >= 0)
8977 {
8978 /* rlon must be in the middle of the window [+s1, +s2]. */
8979 /* Search BACKWARD for the time t1 when rel lon = +s1. */
8980 syn = SynodicPeriod(body);
8981 if (syn.status != ASTRO_SUCCESS)
8982 return IllumError(syn.status);
8983 adjust_days = -syn.value / 4;
8984 rlon_lo = +s1;
8985 /* Search forward from t1 to find t2 such that rel lon = +s2. */
8986 rlon_hi = +s2;
8987 }
8988 else
8989 {
8990 /* rlon must be in the middle of the window [-s2, -s1]. */
8991 /* Search BACKWARD for the time t1 when rel lon = -s2. */
8992 syn = SynodicPeriod(body);
8993 if (syn.status != ASTRO_SUCCESS)
8994 return IllumError(syn.status);
8995 adjust_days = -syn.value / 4;
8996 rlon_lo = -s2;
8997 /* Search forward from t1 to find t2 such that rel lon = -s1. */
8998 rlon_hi = -s1;
8999 }
9000 t_start = Astronomy_AddDays(startTime, adjust_days);
9001 t1 = Astronomy_SearchRelativeLongitude(body, rlon_lo, t_start);
9002 if (t1.status != ASTRO_SUCCESS)
9003 return IllumError(t1.status);
9004 t2 = Astronomy_SearchRelativeLongitude(body, rlon_hi, t1.time);
9005 if (t2.status != ASTRO_SUCCESS)
9006 return IllumError(t2.status);
9007
9008 /* Now we have a time range [t1,t2] that brackets a maximum magnitude event. */
9009 /* Confirm the bracketing. */
9010 m1 = mag_slope(&body, t1.time);
9011 if (m1.status != ASTRO_SUCCESS)
9012 return IllumError(m1.status);
9013 if (m1.value >= 0.0)
9014 return IllumError(ASTRO_INTERNAL_ERROR); /* should never happen! */
9015
9016 m2 = mag_slope(&body, t2.time);
9017 if (m2.status != ASTRO_SUCCESS)
9018 return IllumError(m2.status);
9019 if (m2.value <= 0.0)
9020 return IllumError(ASTRO_INTERNAL_ERROR); /* should never happen! */
9021
9022 /* Use the generic search algorithm to home in on where the slope crosses from negative to positive. */
9023 tx = Astronomy_Search(mag_slope, &body, t1.time, t2.time, 10.0);
9024 if (tx.status != ASTRO_SUCCESS)
9025 return IllumError(tx.status);
9026
9027 if (tx.time.tt >= startTime.tt)
9028 return Astronomy_Illumination(body, tx.time);
9029
9030 /* This event is in the past (earlier than startTime). */
9031 /* We need to search forward from t2 to find the next possible window. */
9032 /* We never need to search more than twice. */
9033 startTime = Astronomy_AddDays(t2.time, 1.0);
9034 }
9035
9036 return IllumError(ASTRO_SEARCH_FAILURE);
9037}
9038
9039static double MoonDistance(astro_time_t t)
9040{
9041 double lon, lat, dist;
9042 CalcMoon(t.tt / 36525.0, &lon, &lat, &dist);
9043 return dist;
9044}
9045
9046static astro_func_result_t moon_distance_slope(void *context, astro_time_t time)
9047{
9048 static const double dt = 0.001;
9049 astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
9050 astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
9051 double dist1, dist2;
9052 int direction = *((int *)context);
9053 astro_func_result_t result;
9054
9055 dist1 = MoonDistance(t1);
9056 dist2 = MoonDistance(t2);
9057 result.value = direction * (dist2 - dist1) / dt;
9058 result.status = ASTRO_SUCCESS;
9059 return result;
9060}
9061
9062/**
9063 * @brief
9064 * Finds the date and time of the Moon's closest distance (perigee)
9065 * or farthest distance (apogee) with respect to the Earth.
9066 *
9067 * Given a date and time to start the search in `startTime`, this function finds the
9068 * next date and time that the center of the Moon reaches the closest or farthest point
9069 * in its orbit with respect to the center of the Earth, whichever comes first
9070 * after `startTime`.
9071 *
9072 * The closest point is called *perigee* and the farthest point is called *apogee*.
9073 * The word *apsis* refers to either event.
9074 *
9075 * To iterate through consecutive alternating perigee and apogee events, call `Astronomy_SearchLunarApsis`
9076 * once, then use the return value to call #Astronomy_NextLunarApsis. After that,
9077 * keep feeding the previous return value from `Astronomy_NextLunarApsis` into another
9078 * call of `Astronomy_NextLunarApsis` as many times as desired.
9079 *
9080 * @param startTime
9081 * The date and time at which to start searching for the next perigee or apogee.
9082 *
9083 * @return
9084 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`,
9085 * `time` holds the date and time of the next lunar apsis, `kind` holds either
9086 * `APSIS_PERICENTER` for perigee or `APSIS_APOCENTER` for apogee, and the distance
9087 * values `dist_au` (astronomical units) and `dist_km` (kilometers) are valid.
9088 * If the function fails, `status` holds some value other than `ASTRO_SUCCESS` that
9089 * indicates what went wrong, and the other structure fields are invalid.
9090 */
9091astro_apsis_t Astronomy_SearchLunarApsis(astro_time_t startTime)
9092{
9093 astro_time_t t1, t2;
9094 astro_search_result_t search;
9095 astro_func_result_t m1, m2;
9096 int positive_direction = +1;
9097 int negative_direction = -1;
9098 const double increment = 5.0; /* number of days to skip in each iteration */
9099 astro_apsis_t result;
9100 int iter;
9101
9102 /*
9103 Check the rate of change of the distance dr/dt at the start time.
9104 If it is positive, the Moon is currently getting farther away,
9105 so start looking for apogee.
9106 Conversely, if dr/dt < 0, start looking for perigee.
9107 Either way, the polarity of the slope will change, so the product will be negative.
9108 Handle the crazy corner case of exactly touching zero by checking for m1*m2 <= 0.
9109 */
9110
9111 t1 = startTime;
9112 m1 = moon_distance_slope(&positive_direction, t1);
9113 if (m1.status != ASTRO_SUCCESS)
9114 return ApsisError(m1.status);
9115
9116 for (iter=0; iter * increment < 2.0 * MEAN_SYNODIC_MONTH; ++iter)
9117 {
9118 t2 = Astronomy_AddDays(t1, increment);
9119 m2 = moon_distance_slope(&positive_direction, t2);
9120 if (m2.status != ASTRO_SUCCESS)
9121 return ApsisError(m2.status);
9122
9123 if (m1.value * m2.value <= 0.0)
9124 {
9125 /* There is a change of slope polarity within the time range [t1, t2]. */
9126 /* Therefore this time range contains an apsis. */
9127 /* Figure out whether it is perigee or apogee. */
9128
9129 if (m1.value < 0.0 || m2.value > 0.0)
9130 {
9131 /* We found a minimum-distance event: perigee. */
9132 /* Search the time range for the time when the slope goes from negative to positive. */
9133 search = Astronomy_Search(moon_distance_slope, &positive_direction, t1, t2, 1.0);
9134 result.kind = APSIS_PERICENTER;
9135 }
9136 else if (m1.value > 0.0 || m2.value < 0.0)
9137 {
9138 /* We found a maximum-distance event: apogee. */
9139 /* Search the time range for the time when the slope goes from positive to negative. */
9140 search = Astronomy_Search(moon_distance_slope, &negative_direction, t1, t2, 1.0);
9141 result.kind = APSIS_APOCENTER;
9142 }
9143 else
9144 {
9145 /* This should never happen. It should not be possible for both slopes to be zero. */
9146 return ApsisError(ASTRO_INTERNAL_ERROR);
9147 }
9148
9149 if (search.status != ASTRO_SUCCESS)
9150 return ApsisError(search.status);
9151
9152 result.status = ASTRO_SUCCESS;
9153 result.time = search.time;
9154 result.dist_au = MoonDistance(search.time);
9155 result.dist_km = result.dist_au * KM_PER_AU;
9156 return result;
9157 }
9158
9159 /* We have not yet found a slope polarity change. Keep searching. */
9160 t1 = t2;
9161 m1 = m2;
9162 }
9163
9164 /* It should not be possible to fail to find an apsis within 2 synodic months. */
9165 return ApsisError(ASTRO_INTERNAL_ERROR);
9166}
9167
9168/**
9169 * @brief
9170 * Finds the next lunar perigee or apogee event in a series.
9171 *
9172 * This function requires an #astro_apsis_t value obtained from a call
9173 * to #Astronomy_SearchLunarApsis or `Astronomy_NextLunarApsis`. Given
9174 * an apogee event, this function finds the next perigee event, and vice versa.
9175 *
9176 * See #Astronomy_SearchLunarApsis for more details.
9177 *
9178 * @param apsis
9179 * An apsis event obtained from a call to #Astronomy_SearchLunarApsis or `Astronomy_NextLunarApsis`.
9180 * See #Astronomy_SearchLunarApsis for more details.
9181 *
9182 * @return
9183 * Same as the return value for #Astronomy_SearchLunarApsis.
9184 */
9185astro_apsis_t Astronomy_NextLunarApsis(astro_apsis_t apsis)
9186{
9187 static const double skip = 11.0; /* number of days to skip to start looking for next apsis event */
9188 astro_apsis_t next;
9189 astro_time_t time;
9190
9191 if (apsis.status != ASTRO_SUCCESS)
9192 return ApsisError(ASTRO_INVALID_PARAMETER);
9193
9194 if (apsis.kind != APSIS_APOCENTER && apsis.kind != APSIS_PERICENTER)
9195 return ApsisError(ASTRO_INVALID_PARAMETER);
9196
9197 time = Astronomy_AddDays(apsis.time, skip);
9198 next = Astronomy_SearchLunarApsis(time);
9199 if (next.status == ASTRO_SUCCESS)
9200 {
9201 /* Verify that we found the opposite apsis from the previous one. */
9202 if (next.kind + apsis.kind != 1)
9203 return ApsisError(ASTRO_INTERNAL_ERROR);
9204 }
9205 return next;
9206}
9207
9208
9209/** @cond DOXYGEN_SKIP */
9210typedef struct
9211{
9212 int direction;
9213 astro_body_t body;
9214}
9215planet_distance_context_t;
9216/** @endcond */
9217
9218
9219static astro_func_result_t planet_distance_slope(void *context, astro_time_t time)
9220{
9221 static const double dt = 0.001;
9222 const planet_distance_context_t *pc = (const planet_distance_context_t *) context;
9223 astro_time_t t1 = Astronomy_AddDays(time, -dt/2.0);
9224 astro_time_t t2 = Astronomy_AddDays(time, +dt/2.0);
9225 astro_func_result_t dist1, dist2, result;
9226
9227 dist1 = Astronomy_HelioDistance(pc->body, t1);
9228 if (dist1.status != ASTRO_SUCCESS)
9229 return dist1;
9230
9231 dist2 = Astronomy_HelioDistance(pc->body, t2);
9232 if (dist2.status != ASTRO_SUCCESS)
9233 return dist2;
9234
9235 result.value = pc->direction * (dist2.value - dist1.value) / dt;
9236 result.status = ASTRO_SUCCESS;
9237 return result;
9238}
9239
9240static astro_apsis_t PlanetExtreme(
9241 astro_body_t body,
9242 astro_apsis_kind_t kind,
9243 astro_time_t start_time,
9244 double dayspan)
9245{
9246 astro_apsis_t apsis;
9247 const double direction = (kind == APSIS_APOCENTER) ? +1.0 : -1.0;
9248 const int npoints = 10;
9249 int i, best_i;
9250 double interval;
9251 double dist, best_dist;
9252 astro_time_t time;
9253 astro_func_result_t result;
9254
9255 for(;;)
9256 {
9257 interval = dayspan / (npoints - 1);
9258
9259 if (interval < 1.0 / 1440.0) /* iterate until uncertainty is less than one minute */
9260 {
9261 apsis.status = ASTRO_SUCCESS;
9262 apsis.kind = kind;
9263 apsis.time = Astronomy_AddDays(start_time, interval / 2.0);
9264 result = Astronomy_HelioDistance(body, apsis.time);
9265 if (result.status != ASTRO_SUCCESS)
9266 return ApsisError(result.status);
9267 apsis.dist_au = result.value;
9268 apsis.dist_km = apsis.dist_au * KM_PER_AU;
9269 return apsis;
9270 }
9271
9272 best_i = -1;
9273 best_dist = 0.0;
9274 for (i=0; i < npoints; ++i)
9275 {
9276 time = Astronomy_AddDays(start_time, i * interval);
9277 result = Astronomy_HelioDistance(body, time);
9278 if (result.status != ASTRO_SUCCESS)
9279 return ApsisError(result.status);
9280 dist = direction * result.value;
9281 if (i==0 || dist > best_dist)
9282 {
9283 best_i = i;
9284 best_dist = dist;
9285 }
9286 }
9287
9288 /* Narrow in on the extreme point. */
9289 start_time = Astronomy_AddDays(start_time, (best_i - 1) * interval);
9290 dayspan = 2.0 * interval;
9291 }
9292}
9293
9294
9295static astro_apsis_t BruteSearchPlanetApsis(astro_body_t body, astro_time_t startTime)
9296{
9297 const int npoints = 100;
9298 int i;
9299 astro_time_t t1, t2, time, t_min, t_max;
9300 double dist, max_dist, min_dist;
9301 astro_apsis_t perihelion, aphelion;
9302 double interval;
9303 double period;
9304 astro_func_result_t result;
9305
9306 /*
9307 Neptune is a special case for two reasons:
9308 1. Its orbit is nearly circular (low orbital eccentricity).
9309 2. It is so distant from the Sun that the orbital period is very long.
9310 Put together, this causes wobbling of the Sun around the Solar System Barycenter (SSB)
9311 to be so significant that there are 3 local minima in the distance-vs-time curve
9312 near each apsis. Therefore, unlike for other planets, we can't use an optimized
9313 algorithm for finding dr/dt = 0.
9314 Instead, we use a dumb, brute-force algorithm of sampling and finding min/max
9315 heliocentric distance.
9316
9317 There is a similar problem in the TOP2013 model for Pluto:
9318 Its position vector has high-frequency oscillations that confuse the
9319 slope-based determination of apsides.
9320 */
9321
9322 /*
9323 Rewind approximately 30 degrees in the orbit,
9324 then search forward for 270 degrees.
9325 This is a very cautious way to prevent missing an apsis.
9326 Typically we will find two apsides, and we pick whichever
9327 apsis is ealier, but after startTime.
9328 Sample points around this orbital arc and find when the distance
9329 is greatest and smallest.
9330 */
9331 period = Astronomy_PlanetOrbitalPeriod(body);
9332 t1 = Astronomy_AddDays(startTime, period * ( -30.0 / 360.0));
9333 t2 = Astronomy_AddDays(startTime, period * (+270.0 / 360.0));
9334 t_min = t_max = t1;
9335 min_dist = max_dist = -1.0; /* prevent warning about uninitialized variables */
9336 interval = (t2.ut - t1.ut) / (npoints - 1.0);
9337
9338 for (i=0; i < npoints; ++i)
9339 {
9340 double ut = t1.ut + (i * interval);
9341 time = Astronomy_TimeFromDays(ut);
9342 result = Astronomy_HelioDistance(body, time);
9343 if (result.status != ASTRO_SUCCESS)
9344 return ApsisError(result.status);
9345 dist = result.value;
9346 if (i == 0)
9347 {
9348 max_dist = min_dist = dist;
9349 }
9350 else
9351 {
9352 if (dist > max_dist)
9353 {
9354 max_dist = dist;
9355 t_max = time;
9356 }
9357 if (dist < min_dist)
9358 {
9359 min_dist = dist;
9360 t_min = time;
9361 }
9362 }
9363 }
9364
9365 t1 = Astronomy_AddDays(t_min, -2 * interval);
9366 perihelion = PlanetExtreme(body, APSIS_PERICENTER, t1, 4 * interval);
9367
9368 t1 = Astronomy_AddDays(t_max, -2 * interval);
9369 aphelion = PlanetExtreme(body, APSIS_APOCENTER, t1, 4 * interval);
9370
9371 if (perihelion.status == ASTRO_SUCCESS && perihelion.time.tt >= startTime.tt)
9372 {
9373 if (aphelion.status == ASTRO_SUCCESS && aphelion.time.tt >= startTime.tt)
9374 {
9375 /* Perihelion and aphelion are both valid. Pick the one that comes first. */
9376 if (aphelion.time.tt < perihelion.time.tt)
9377 return aphelion;
9378 }
9379 return perihelion;
9380 }
9381
9382 if (aphelion.status == ASTRO_SUCCESS && aphelion.time.tt >= startTime.tt)
9383 return aphelion;
9384
9385 return ApsisError(ASTRO_FAIL_APSIS);
9386}
9387
9388
9389/**
9390 * @brief
9391 * Finds the date and time of a planet's perihelion (closest approach to the Sun)
9392 * or aphelion (farthest distance from the Sun) after a given time.
9393 *
9394 * Given a date and time to start the search in `startTime`, this function finds the
9395 * next date and time that the center of the specified planet reaches the closest or farthest point
9396 * in its orbit with respect to the center of the Sun, whichever comes first
9397 * after `startTime`.
9398 *
9399 * The closest point is called *perihelion* and the farthest point is called *aphelion*.
9400 * The word *apsis* refers to either event.
9401 *
9402 * To iterate through consecutive alternating perihelion and aphelion events,
9403 * call `Astronomy_SearchPlanetApsis` once, then use the return value to call
9404 * #Astronomy_NextPlanetApsis. After that, keep feeding the previous return value
9405 * from `Astronomy_NextPlanetApsis` into another call of `Astronomy_NextPlanetApsis`
9406 * as many times as desired.
9407 *
9408 * @param body
9409 * The planet for which to find the next perihelion/aphelion event.
9410 * Not allowed to be `BODY_SUN` or `BODY_MOON`.
9411 *
9412 * @param startTime
9413 * The date and time at which to start searching for the next perihelion or aphelion.
9414 *
9415 * @return
9416 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`,
9417 * `time` holds the date and time of the next planetary apsis, `kind` holds either
9418 * `APSIS_PERICENTER` for perihelion or `APSIS_APOCENTER` for aphelion, and the distance
9419 * values `dist_au` (astronomical units) and `dist_km` (kilometers) are valid.
9420 * If the function fails, `status` holds some value other than `ASTRO_SUCCESS` that
9421 * indicates what went wrong, and the other structure fields are invalid.
9422 */
9423astro_apsis_t Astronomy_SearchPlanetApsis(astro_body_t body, astro_time_t startTime)
9424{
9425 astro_time_t t1, t2;
9426 astro_search_result_t search;
9427 astro_func_result_t m1, m2;
9428 planet_distance_context_t context;
9429 astro_apsis_t result;
9430 int iter;
9431 double orbit_period_days;
9432 double increment; /* number of days to skip in each iteration */
9433 astro_func_result_t dist;
9434
9435 if (body == BODY_NEPTUNE || body == BODY_PLUTO)
9436 return BruteSearchPlanetApsis(body, startTime);
9437
9438 orbit_period_days = Astronomy_PlanetOrbitalPeriod(body);
9439 if (orbit_period_days == 0.0)
9440 return ApsisError(ASTRO_INVALID_BODY); /* The body must be a planet. */
9441
9442 increment = orbit_period_days / 6.0;
9443
9444 context.body = body;
9445
9446 t1 = startTime;
9447 context.direction = +1;
9448 m1 = planet_distance_slope(&context, t1);
9449 if (m1.status != ASTRO_SUCCESS)
9450 return ApsisError(m1.status);
9451
9452 for (iter=0; iter * increment < 2.0 * orbit_period_days; ++iter)
9453 {
9454 t2 = Astronomy_AddDays(t1, increment);
9455 context.direction = +1;
9456 m2 = planet_distance_slope(&context, t2);
9457 if (m2.status != ASTRO_SUCCESS)
9458 return ApsisError(m2.status);
9459
9460 if (m1.value * m2.value <= 0.0)
9461 {
9462 /* There is a change of slope polarity within the time range [t1, t2]. */
9463 /* Therefore this time range contains an apsis. */
9464 /* Figure out whether it is perihelion or aphelion. */
9465
9466 if (m1.value < 0.0 || m2.value > 0.0)
9467 {
9468 /* We found a minimum-distance event: perihelion. */
9469 /* Search the time range for the time when the slope goes from negative to positive. */
9470 context.direction = +1;
9471 result.kind = APSIS_PERICENTER;
9472 }
9473 else if (m1.value > 0.0 || m2.value < 0.0)
9474 {
9475 /* We found a maximum-distance event: aphelion. */
9476 /* Search the time range for the time when the slope goes from positive to negative. */
9477 context.direction = -1;
9478 result.kind = APSIS_APOCENTER;
9479 }
9480 else
9481 {
9482 /* This should never happen. It should not be possible for both slopes to be zero. */
9483 return ApsisError(ASTRO_INTERNAL_ERROR);
9484 }
9485
9486 search = Astronomy_Search(planet_distance_slope, &context, t1, t2, 1.0);
9487 if (search.status != ASTRO_SUCCESS)
9488 return ApsisError(search.status);
9489
9490 dist = Astronomy_HelioDistance(body, search.time);
9491 if (dist.status != ASTRO_SUCCESS)
9492 return ApsisError(dist.status);
9493
9494 result.status = ASTRO_SUCCESS;
9495 result.time = search.time;
9496 result.dist_au = dist.value;
9497 result.dist_km = dist.value * KM_PER_AU;
9498 return result;
9499 }
9500
9501 /* We have not yet found a slope polarity change. Keep searching. */
9502 t1 = t2;
9503 m1 = m2;
9504 }
9505
9506 /* It should not be possible to fail to find an apsis within 2 orbits. */
9507 return ApsisError(ASTRO_INTERNAL_ERROR);
9508}
9509
9510/**
9511 * @brief
9512 * Finds the next planetary perihelion or aphelion event in a series.
9513 *
9514 * This function requires an #astro_apsis_t value obtained from a call
9515 * to #Astronomy_SearchPlanetApsis or `Astronomy_NextPlanetApsis`.
9516 * Given an aphelion event, this function finds the next perihelion event, and vice versa.
9517 *
9518 * See #Astronomy_SearchPlanetApsis for more details.
9519 *
9520 * @param body
9521 * The planet for which to find the next perihelion/aphelion event.
9522 * Not allowed to be `BODY_SUN` or `BODY_MOON`.
9523 * Must match the body passed into the call that produced the `apsis` parameter.
9524 *
9525 * @param apsis
9526 * An apsis event obtained from a call to #Astronomy_SearchPlanetApsis or `Astronomy_NextPlanetApsis`.
9527 *
9528 * @return
9529 * Same as the return value for #Astronomy_SearchPlanetApsis.
9530 */
9531astro_apsis_t Astronomy_NextPlanetApsis(astro_body_t body, astro_apsis_t apsis)
9532{
9533 double skip; /* number of days to skip to start looking for next apsis event */
9534 astro_apsis_t next;
9535 astro_time_t time;
9536
9537 if (apsis.status != ASTRO_SUCCESS)
9538 return ApsisError(ASTRO_INVALID_PARAMETER);
9539
9540 if (apsis.kind != APSIS_APOCENTER && apsis.kind != APSIS_PERICENTER)
9541 return ApsisError(ASTRO_INVALID_PARAMETER);
9542
9543 skip = 0.25 * Astronomy_PlanetOrbitalPeriod(body); /* skip 1/4 of an orbit before starting search again */
9544 if (skip <= 0.0)
9545 return ApsisError(ASTRO_INVALID_BODY); /* body must be a planet */
9546
9547 time = Astronomy_AddDays(apsis.time, skip);
9548 next = Astronomy_SearchPlanetApsis(body, time);
9549 if (next.status == ASTRO_SUCCESS)
9550 {
9551 /* Verify that we found the opposite apsis from the previous one. */
9552 if (next.kind + apsis.kind != 1)
9553 return ApsisError(ASTRO_INTERNAL_ERROR);
9554 }
9555 return next;
9556}
9557
9558
9559/**
9560 * @brief Calculates the inverse of a rotation matrix.
9561 *
9562 * Given a rotation matrix that performs some coordinate transform,
9563 * this function returns the matrix that reverses that transform.
9564 *
9565 * @param rotation
9566 * The rotation matrix to be inverted.
9567 *
9568 * @return
9569 * A rotation matrix that performs the opposite transformation.
9570 */
9571astro_rotation_t Astronomy_InverseRotation(astro_rotation_t rotation)
9572{
9573 astro_rotation_t inverse;
9574
9575 if (rotation.status != ASTRO_SUCCESS)
9576 return RotationErr(ASTRO_INVALID_PARAMETER);
9577
9578 inverse.status = ASTRO_SUCCESS;
9579 inverse.rot[0][0] = rotation.rot[0][0];
9580 inverse.rot[0][1] = rotation.rot[1][0];
9581 inverse.rot[0][2] = rotation.rot[2][0];
9582 inverse.rot[1][0] = rotation.rot[0][1];
9583 inverse.rot[1][1] = rotation.rot[1][1];
9584 inverse.rot[1][2] = rotation.rot[2][1];
9585 inverse.rot[2][0] = rotation.rot[0][2];
9586 inverse.rot[2][1] = rotation.rot[1][2];
9587 inverse.rot[2][2] = rotation.rot[2][2];
9588
9589 return inverse;
9590}
9591
9592/**
9593 * @brief Creates a rotation based on applying one rotation followed by another.
9594 *
9595 * Given two rotation matrices, returns a combined rotation matrix that is
9596 * equivalent to rotating based on the first matrix, followed by the second.
9597 *
9598 * @param a
9599 * The first rotation to apply.
9600 *
9601 * @param b
9602 * The second rotation to apply.
9603 *
9604 * @return
9605 * The combined rotation matrix.
9606 */
9607astro_rotation_t Astronomy_CombineRotation(astro_rotation_t a, astro_rotation_t b)
9608{
9609 astro_rotation_t c;
9610
9611 if (a.status != ASTRO_SUCCESS || b.status != ASTRO_SUCCESS)
9612 return RotationErr(ASTRO_INVALID_PARAMETER);
9613
9614 /*
9615 Use matrix multiplication: c = b*a.
9616 We put 'b' on the left and 'a' on the right because,
9617 just like when you use a matrix M to rotate a vector V,
9618 you put the M on the left in the product M*V.
9619 We can think of this as 'b' rotating all the 3 column vectors in 'a'.
9620 */
9621 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];
9622 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];
9623 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];
9624 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];
9625 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];
9626 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];
9627 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];
9628 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];
9629 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];
9630
9631 c.status = ASTRO_SUCCESS;
9632 return c;
9633}
9634
9635/**
9636 * @brief Creates an identity rotation matrix.
9637 *
9638 * Returns a rotation matrix that has no effect on orientation.
9639 * This matrix can be the starting point for other operations,
9640 * such as using a series of calls to #Astronomy_Pivot to
9641 * create a custom rotation matrix.
9642 *
9643 * @return
9644 * The identity matrix.
9645 */
9646astro_rotation_t Astronomy_IdentityMatrix(void)
9647{
9648 astro_rotation_t r;
9649
9650 r.rot[0][0] = 1.0; r.rot[1][0] = 0.0; r.rot[2][0] = 0.0;
9651 r.rot[0][1] = 0.0; r.rot[1][1] = 1.0; r.rot[2][1] = 0.0;
9652 r.rot[0][2] = 0.0; r.rot[1][2] = 0.0; r.rot[2][2] = 1.0;
9653
9654 r.status = ASTRO_SUCCESS;
9655
9656 return r;
9657}
9658
9659/**
9660 * @brief Re-orients a rotation matrix by pivoting it by an angle around one of its axes.
9661 *
9662 * Given a rotation matrix, a selected coordinate axis, and an angle in degrees,
9663 * this function pivots the rotation matrix by that angle around that coordinate axis.
9664 *
9665 * For example, if you have rotation matrix that converts ecliptic coordinates (ECL)
9666 * to horizontal coordinates (HOR), but you really want to convert ECL to the orientation
9667 * of a telescope camera pointed at a given body, you can use `Astronomy_Pivot` twice:
9668 * (1) pivot around the zenith axis by the body's azimuth, then (2) pivot around the
9669 * western axis by the body's altitude angle. The resulting rotation matrix will then
9670 * reorient ECL coordinates to the orientation of your telescope camera.
9671 *
9672 * @param rotation
9673 * The input rotation matrix.
9674 *
9675 * @param axis
9676 * An integer that selects which coordinate axis to rotate around:
9677 * 0 = x, 1 = y, 2 = z. Any other value will fail with the error code
9678 * `ASTRO_INVALID_PARAMETER` in the `status` field of the return value.
9679 *
9680 * @param angle
9681 * An angle in degrees indicating the amount of rotation around the specified axis.
9682 * Positive angles indicate rotation counterclockwise as seen from the positive
9683 * direction along that axis, looking towards the origin point of the orientation system.
9684 * If `angle` is NAN or infinite, the function will fail with the error code
9685 * `ASTRO_INVALID_PARAMETER`. Any finite number of degrees is allowed, but best
9686 * precision will result from keeping `angle` in the range [-360, +360].
9687 *
9688 * @return
9689 * If successful, the return value will have `ASTRO_SUCCESS` in the `status`
9690 * field, along with a pivoted rotation matrix. Otherwise, `status` holds
9691 * an appropriate error code and the rotation matrix is invalid.
9692 */
9693astro_rotation_t Astronomy_Pivot(astro_rotation_t rotation, int axis, double angle)
9694{
9695 astro_rotation_t p;
9696 double radians, c, s;
9697 int i, j, k;
9698
9699 /* Check for an invalid input matrix. */
9700 if (rotation.status != ASTRO_SUCCESS)
9701 return RotationErr(ASTRO_INVALID_PARAMETER);
9702
9703 /* Check for an invalid coordinate axis. */
9704 if (axis < 0 || axis > 2)
9705 return RotationErr(ASTRO_INVALID_PARAMETER);
9706
9707 /* Check for an invalid angle value. */
9708 if (!isfinite(angle))
9709 return RotationErr(ASTRO_INVALID_PARAMETER);
9710
9711 radians = angle * DEG2RAD;
9712 c = cos(radians);
9713 s = sin(radians);
9714
9715 /*
9716 We need to maintain the "right-hand" rule, no matter which
9717 axis was selected. That means we pick (i, j, k) axis order
9718 such that the following vector cross product is satisfied:
9719 i x j = k
9720 */
9721 i = (axis + 1) % 3;
9722 j = (axis + 2) % 3;
9723 k = axis;
9724
9725 p.rot[i][i] = c*rotation.rot[i][i] - s*rotation.rot[i][j];
9726 p.rot[i][j] = s*rotation.rot[i][i] + c*rotation.rot[i][j];
9727 p.rot[i][k] = rotation.rot[i][k];
9728
9729 p.rot[j][i] = c*rotation.rot[j][i] - s*rotation.rot[j][j];
9730 p.rot[j][j] = s*rotation.rot[j][i] + c*rotation.rot[j][j];
9731 p.rot[j][k] = rotation.rot[j][k];
9732
9733 p.rot[k][i] = c*rotation.rot[k][i] - s*rotation.rot[k][j];
9734 p.rot[k][j] = s*rotation.rot[k][i] + c*rotation.rot[k][j];
9735 p.rot[k][k] = rotation.rot[k][k];
9736
9737 p.status = ASTRO_SUCCESS;
9738 return p;
9739}
9740
9741/**
9742 * @brief Converts spherical coordinates to Cartesian coordinates.
9743 *
9744 * Given spherical coordinates and a time at which they are valid,
9745 * returns a vector of Cartesian coordinates. The returned value
9746 * includes the time, as required by the type #astro_vector_t.
9747 *
9748 * @param sphere
9749 * Spherical coordinates to be converted.
9750 *
9751 * @param time
9752 * The time that should be included in the return value.
9753 *
9754 * @return
9755 * The vector form of the supplied spherical coordinates.
9756 */
9757astro_vector_t Astronomy_VectorFromSphere(astro_spherical_t sphere, astro_time_t time)
9758{
9759 astro_vector_t vector;
9760 double radlat, radlon, rcoslat;
9761
9762 if (sphere.status != ASTRO_SUCCESS)
9763 return VecError(ASTRO_INVALID_PARAMETER, time);
9764
9765 radlat = sphere.lat * DEG2RAD;
9766 radlon = sphere.lon * DEG2RAD;
9767 rcoslat = sphere.dist * cos(radlat);
9768
9769 vector.status = ASTRO_SUCCESS;
9770 vector.t = time;
9771 vector.x = rcoslat * cos(radlon);
9772 vector.y = rcoslat * sin(radlon);
9773 vector.z = sphere.dist * sin(radlat);
9774
9775 return vector;
9776}
9777
9778
9779/**
9780 * @brief Converts Cartesian coordinates to spherical coordinates.
9781 *
9782 * Given a Cartesian vector, returns latitude, longitude, and distance.
9783 *
9784 * @param vector
9785 * Cartesian vector to be converted to spherical coordinates.
9786 *
9787 * @return
9788 * Spherical coordinates that are equivalent to the given vector.
9789 */
9790astro_spherical_t Astronomy_SphereFromVector(astro_vector_t vector)
9791{
9792 double xyproj;
9793 astro_spherical_t sphere;
9794
9795 if (vector.status != ASTRO_SUCCESS)
9796 return SphereError(vector.status);
9797
9798 xyproj = vector.x*vector.x + vector.y*vector.y;
9799 sphere.dist = sqrt(xyproj + vector.z*vector.z);
9800 if (xyproj == 0.0)
9801 {
9802 if (vector.z == 0.0)
9803 {
9804 /* Indeterminate coordinates; pos vector has zero length. */
9805 return SphereError(ASTRO_INVALID_PARAMETER);
9806 }
9807
9808 sphere.lon = 0.0;
9809 sphere.lat = (vector.z < 0.0) ? -90.0 : +90.0;
9810 }
9811 else
9812 {
9813 sphere.lon = RAD2DEG * atan2(vector.y, vector.x);
9814 if (sphere.lon < 0.0)
9815 sphere.lon += 360.0;
9816
9817 sphere.lat = RAD2DEG * atan2(vector.z, sqrt(xyproj));
9818 }
9819
9820 sphere.status = ASTRO_SUCCESS;
9821 return sphere;
9822}
9823
9824
9825/**
9826 * @brief
9827 * Given an equatorial vector, calculates equatorial angular coordinates.
9828 *
9829 * @param vector
9830 * A vector in an equatorial coordinate system.
9831 *
9832 * @return
9833 * Angular coordinates expressed in the same equatorial system as `vector`.
9834 */
9835astro_equatorial_t Astronomy_EquatorFromVector(astro_vector_t vector)
9836{
9837 astro_equatorial_t equ;
9838 astro_spherical_t sphere;
9839
9840 sphere = Astronomy_SphereFromVector(vector);
9841 if (sphere.status != ASTRO_SUCCESS)
9842 return EquError(sphere.status);
9843
9844 equ.status = ASTRO_SUCCESS;
9845 equ.dec = sphere.lat;
9846 equ.ra = sphere.lon / 15.0; /* convert degrees to sidereal hours */
9847 equ.dist = sphere.dist;
9848 equ.vec = vector;
9849
9850 return equ;
9851}
9852
9853
9854static double ToggleAzimuthDirection(double az)
9855{
9856 az = 360.0 - az;
9857 if (az >= 360.0)
9858 az -= 360.0;
9859 else if (az < 0.0)
9860 az += 360.0;
9861 return az;
9862}
9863
9864/**
9865 * @brief Converts Cartesian coordinates to horizontal coordinates.
9866 *
9867 * Given a horizontal Cartesian vector, returns horizontal azimuth and altitude.
9868 *
9869 * *IMPORTANT:* This function differs from #Astronomy_SphereFromVector in two ways:
9870 * - `Astronomy_SphereFromVector` returns a `lon` value that represents azimuth defined counterclockwise
9871 * from north (e.g., west = +90), but this function represents a clockwise rotation
9872 * (e.g., east = +90). The difference is because `Astronomy_SphereFromVector` is intended
9873 * to preserve the vector "right-hand rule", while this function defines azimuth in a more
9874 * traditional way as used in navigation and cartography.
9875 * - This function optionally corrects for atmospheric refraction, while `Astronomy_SphereFromVector`
9876 * does not.
9877 *
9878 * The returned structure contains the azimuth in `lon`.
9879 * It is measured in degrees clockwise from north: east = +90 degrees, west = +270 degrees.
9880 *
9881 * The altitude is stored in `lat`.
9882 *
9883 * The distance to the observed object is stored in `dist`,
9884 * and is expressed in astronomical units (AU).
9885 *
9886 * @param vector
9887 * Cartesian vector to be converted to horizontal coordinates.
9888 *
9889 * @param refraction
9890 * `REFRACTION_NORMAL`: correct altitude for atmospheric refraction (recommended).
9891 * `REFRACTION_NONE`: no atmospheric refraction correction is performed.
9892 * `REFRACTION_JPLHOR`: for JPL Horizons compatibility testing only; not recommended for normal use.
9893 *
9894 * @return
9895 * If successful, `status` holds `ASTRO_SUCCESS` and the other fields are valid as described
9896 * in the function remarks.
9897 * Otherwise `status` holds an error code and the other fields are undefined.
9898 */
9899astro_spherical_t Astronomy_HorizonFromVector(astro_vector_t vector, astro_refraction_t refraction)
9900{
9901 astro_spherical_t sphere;
9902
9903 sphere = Astronomy_SphereFromVector(vector);
9904 if (sphere.status == ASTRO_SUCCESS)
9905 {
9906 /* Convert azimuth from counterclockwise-from-north to clockwise-from-north. */
9907 sphere.lon = ToggleAzimuthDirection(sphere.lon);
9908 sphere.lat += Astronomy_Refraction(refraction, sphere.lat);
9909 }
9910
9911 return sphere;
9912}
9913
9914
9915/**
9916 * @brief
9917 * Given apparent angular horizontal coordinates in `sphere`, calculate horizontal vector.
9918 *
9919 * @param sphere
9920 * A structure that contains apparent horizontal coordinates:
9921 * `lat` holds the refracted altitude angle,
9922 * `lon` holds the azimuth in degrees clockwise from north,
9923 * and `dist` holds the distance from the observer to the object in AU.
9924 *
9925 * @param time
9926 * The date and time of the observation. This is needed because the returned
9927 * #astro_vector_t structure requires a valid time value when passed to certain other functions.
9928 *
9929 * @param refraction
9930 * The refraction option used to model atmospheric lensing. See #Astronomy_Refraction.
9931 * This specifies how refraction is to be removed from the altitude stored in `sphere.lat`.
9932 *
9933 * @return
9934 * A vector in the horizontal system: `x` = north, `y` = west, and `z` = zenith (up).
9935 */
9936astro_vector_t Astronomy_VectorFromHorizon(astro_spherical_t sphere, astro_time_t time, astro_refraction_t refraction)
9937{
9938 if (sphere.status != ASTRO_SUCCESS)
9939 return VecError(ASTRO_INVALID_PARAMETER, time);
9940
9941 /* Convert azimuth from clockwise-from-north to counterclockwise-from-north. */
9942 sphere.lon = ToggleAzimuthDirection(sphere.lon);
9943
9944 /* Reverse any applied refraction. */
9945 sphere.lat += Astronomy_InverseRefraction(refraction, sphere.lat);
9946
9947 return Astronomy_VectorFromSphere(sphere, time);
9948}
9949
9950
9951/**
9952 * @brief
9953 * Calculates the amount of "lift" to an altitude angle caused by atmospheric refraction.
9954 *
9955 * Given an altitude angle and a refraction option, calculates
9956 * the amount of "lift" caused by atmospheric refraction.
9957 * This is the number of degrees higher in the sky an object appears
9958 * due to the lensing of the Earth's atmosphere.
9959 * This function works best near sea level.
9960 * To correct for higher elevations, call #Astronomy_Atmosphere for that
9961 * elevation and multiply the refraction angle by the resulting relative density.
9962 *
9963 * @param refraction
9964 * The option selecting which refraction correction to use.
9965 * If `REFRACTION_NORMAL`, uses a well-behaved refraction model that works well for
9966 * all valid values (-90 to +90) of `altitude`.
9967 * If `REFRACTION_JPLHOR`, this function returns a compatible value with the JPL Horizons tool.
9968 * If any other value (including `REFRACTION_NONE`), this function returns 0.
9969 *
9970 * @param altitude
9971 * An altitude angle in a horizontal coordinate system. Must be a value between -90 and +90.
9972 *
9973 * @return
9974 * The angular adjustment in degrees to be added to the altitude angle to correct for atmospheric lensing.
9975 */
9976double Astronomy_Refraction(astro_refraction_t refraction, double altitude)
9977{
9978 double refr, hd;
9979
9980 if (altitude < -90.0 || altitude > +90.0)
9981 return 0.0; /* no attempt to correct an invalid altitude */
9982
9983 if (refraction == REFRACTION_NORMAL || refraction == REFRACTION_JPLHOR)
9984 {
9985 /*
9986 http://extras.springer.com/1999/978-1-4471-0555-8/chap4/horizons/horizons.pdf
9987 JPL Horizons says it uses refraction algorithm from
9988 Meeus "Astronomical Algorithms", 1991, p. 101-102.
9989 I found the following Go implementation:
9990 https://github.com/soniakeys/meeus/blob/master/v3/refraction/refract.go
9991 This is a translation from the function "Saemundsson" there.
9992 I found experimentally that JPL Horizons clamps the angle to 1 degree below the horizon.
9993 This is important because the 'refr' formula below goes crazy near hd = -5.11.
9994 */
9995
9996 hd = altitude;
9997 if (hd < -1.0)
9998 hd = -1.0;
9999
10000 refr = (1.02 / tan((hd+10.3/(hd+5.11))*DEG2RAD)) / 60.0;
10001
10002 if (refraction == REFRACTION_NORMAL && altitude < -1.0)
10003 {
10004 /*
10005 In "normal" mode we gradually reduce refraction toward the nadir
10006 so that we never get an altitude angle less than -90 degrees.
10007 When horizon angle is -1 degrees, the factor is exactly 1.
10008 As altitude approaches -90 (the nadir), the fraction approaches 0 linearly.
10009 */
10010 refr *= (altitude + 90.0) / 89.0;
10011 }
10012 }
10013 else
10014 {
10015 /* No refraction, or the refraction option is invalid. */
10016 refr = 0.0;
10017 }
10018
10019 return refr;
10020}
10021
10022
10023/**
10024 * @brief
10025 * Calculates the inverse of an atmospheric refraction angle.
10026 *
10027 * Given an observed altitude angle that includes atmospheric refraction,
10028 * calculates the negative angular correction to obtain the unrefracted
10029 * altitude. This is useful for cases where observed horizontal
10030 * coordinates are to be converted to another orientation system,
10031 * but refraction first must be removed from the observed position.
10032 *
10033 * @param refraction
10034 * The option selecting which refraction correction to use.
10035 * See #Astronomy_Refraction.
10036 *
10037 * @param bent_altitude
10038 * The apparent altitude that includes atmospheric refraction.
10039 *
10040 * @return
10041 * The angular adjustment in degrees to be added to the
10042 * altitude angle to correct for atmospheric lensing.
10043 * This will be less than or equal to zero.
10044 */
10045double Astronomy_InverseRefraction(astro_refraction_t refraction, double bent_altitude)
10046{
10047 double altitude, diff;
10048
10049 if (bent_altitude < -90.0 || bent_altitude > +90.0)
10050 return 0.0; /* no attempt to correct an invalid altitude */
10051
10052 /* Find the pre-adjusted altitude whose refraction correction leads to 'altitude'. */
10053 altitude = bent_altitude - Astronomy_Refraction(refraction, bent_altitude);
10054 for(;;)
10055 {
10056 /* See how close we got. */
10057 diff = (altitude + Astronomy_Refraction(refraction, altitude)) - bent_altitude;
10058 if (fabs(diff) < 1.0e-14)
10059 return altitude - bent_altitude;
10060
10061 altitude -= diff;
10062 }
10063}
10064
10065/**
10066 * @brief Applies a rotation to a vector, yielding a rotated vector.
10067 *
10068 * This function transforms a vector in one orientation to a vector
10069 * in another orientation.
10070 *
10071 * @param rotation
10072 * A rotation matrix that specifies how the orientation of the vector is to be changed.
10073 *
10074 * @param vector
10075 * The vector whose orientation is to be changed.
10076 *
10077 * @return
10078 * A vector in the orientation specified by `rotation`.
10079 */
10080astro_vector_t Astronomy_RotateVector(astro_rotation_t rotation, astro_vector_t vector)
10081{
10082 astro_vector_t target;
10083
10084 if (rotation.status != ASTRO_SUCCESS || vector.status != ASTRO_SUCCESS)
10085 return VecError(ASTRO_INVALID_PARAMETER, vector.t);
10086
10087 target.status = ASTRO_SUCCESS;
10088 target.t = vector.t;
10089 target.x = rotation.rot[0][0]*vector.x + rotation.rot[1][0]*vector.y + rotation.rot[2][0]*vector.z;
10090 target.y = rotation.rot[0][1]*vector.x + rotation.rot[1][1]*vector.y + rotation.rot[2][1]*vector.z;
10091 target.z = rotation.rot[0][2]*vector.x + rotation.rot[1][2]*vector.y + rotation.rot[2][2]*vector.z;
10092
10093 return target;
10094}
10095
10096
10097/**
10098 * @brief Applies a rotation to a state vector, yielding a rotated vector.
10099 *
10100 * This function transforms a state vector in one orientation to a vector
10101 * in another orientation.
10102 *
10103 * @param rotation
10104 * A rotation matrix that specifies how the orientation of the state vector is to be changed.
10105 *
10106 * @param state
10107 * The state vector whose orientation is to be changed.
10108 * Both the position and velocity components are transformed.
10109 *
10110 * @return
10111 * A state vector in the orientation specified by `rotation`.
10112 */
10113astro_state_vector_t Astronomy_RotateState(astro_rotation_t rotation, astro_state_vector_t state)
10114{
10115 astro_state_vector_t target;
10116
10117 if (rotation.status != ASTRO_SUCCESS || state.status != ASTRO_SUCCESS)
10118 return StateVecError(ASTRO_INVALID_PARAMETER, state.t);
10119
10120 target.status = ASTRO_SUCCESS;
10121 target.t = state.t;
10122
10123 target.x = rotation.rot[0][0]*state.x + rotation.rot[1][0]*state.y + rotation.rot[2][0]*state.z;
10124 target.y = rotation.rot[0][1]*state.x + rotation.rot[1][1]*state.y + rotation.rot[2][1]*state.z;
10125 target.z = rotation.rot[0][2]*state.x + rotation.rot[1][2]*state.y + rotation.rot[2][2]*state.z;
10126
10127 target.vx = rotation.rot[0][0]*state.vx + rotation.rot[1][0]*state.vy + rotation.rot[2][0]*state.vz;
10128 target.vy = rotation.rot[0][1]*state.vx + rotation.rot[1][1]*state.vy + rotation.rot[2][1]*state.vz;
10129 target.vz = rotation.rot[0][2]*state.vx + rotation.rot[1][2]*state.vy + rotation.rot[2][2]*state.vz;
10130
10131 return target;
10132}
10133
10134
10135/**
10136 * @brief
10137 * Calculates a rotation matrix from J2000 mean equator (EQJ) to J2000 mean ecliptic (ECL).
10138 *
10139 * This is one of the family of functions that returns a rotation matrix
10140 * for converting from one orientation to another.
10141 * Source: EQJ = equatorial system, using equator at J2000 epoch.
10142 * Target: ECL = ecliptic system, using equator at J2000 epoch.
10143 *
10144 * @return
10145 * A rotation matrix that converts EQJ to ECL.
10146 */
10147astro_rotation_t Astronomy_Rotation_EQJ_ECL(void)
10148{
10149 static const double c = COS_OBLIQ_2000;
10150 static const double s = SIN_OBLIQ_2000;
10151 astro_rotation_t r;
10152
10153 r.status = ASTRO_SUCCESS;
10154 r.rot[0][0] = 1.0; r.rot[1][0] = 0.0; r.rot[2][0] = 0.0;
10155 r.rot[0][1] = 0.0; r.rot[1][1] = +c; r.rot[2][1] = +s;
10156 r.rot[0][2] = 0.0; r.rot[1][2] = -s; r.rot[2][2] = +c;
10157 return r;
10158}
10159
10160/**
10161 * @brief
10162 * Calculates a rotation matrix from J2000 mean ecliptic (ECL) to J2000 mean equator (EQJ).
10163 *
10164 * This is one of the family of functions that returns a rotation matrix
10165 * for converting from one orientation to another.
10166 * Source: ECL = ecliptic system, using equator at J2000 epoch.
10167 * Target: EQJ = equatorial system, using equator at J2000 epoch.
10168 *
10169 * @return
10170 * A rotation matrix that converts ECL to EQJ.
10171 */
10172astro_rotation_t Astronomy_Rotation_ECL_EQJ(void)
10173{
10174 static const double c = COS_OBLIQ_2000;
10175 static const double s = SIN_OBLIQ_2000;
10176 astro_rotation_t r;
10177
10178 r.status = ASTRO_SUCCESS;
10179 r.rot[0][0] = 1.0; r.rot[1][0] = 0.0; r.rot[2][0] = 0.0;
10180 r.rot[0][1] = 0.0; r.rot[1][1] = +c; r.rot[2][1] = -s;
10181 r.rot[0][2] = 0.0; r.rot[1][2] = +s; r.rot[2][2] = +c;
10182 return r;
10183}
10184
10185/**
10186 * @brief
10187 * Calculates a rotation matrix from J2000 mean equator (EQJ) to equatorial of-date (EQD).
10188 *
10189 * This is one of the family of functions that returns a rotation matrix
10190 * for converting from one orientation to another.
10191 * Source: EQJ = equatorial system, using equator at J2000 epoch.
10192 * Target: EQD = equatorial system, using equator of the specified date/time.
10193 *
10194 * @param time
10195 * The date and time at which the Earth's equator defines the target orientation.
10196 *
10197 * @return
10198 * A rotation matrix that converts EQJ to EQD at `time`.
10199 */
10200astro_rotation_t Astronomy_Rotation_EQJ_EQD(astro_time_t *time)
10201{
10202 astro_rotation_t prec, nut;
10203
10204 if (time == NULL)
10205 return RotationErr(ASTRO_INVALID_PARAMETER);
10206
10207 prec = precession_rot(*time, FROM_2000);
10208 nut = nutation_rot(time, FROM_2000);
10209 return Astronomy_CombineRotation(prec, nut);
10210}
10211
10212
10213/**
10214 * @brief
10215 * Calculates a rotation matrix from J2000 mean equator (EQJ) to true ecliptic of date (ECT).
10216 *
10217 * This is one of the family of functions that returns a rotation matrix
10218 * for converting from one orientation to another.
10219 * Source: EQJ = equatorial system, using mean equator at J2000 epoch.
10220 * Target: ECT = ecliptic system, using true equinox of the specified date/time.
10221 *
10222 * @param time
10223 * The date and time at which the Earth's equator defines the target orientation.
10224 *
10225 * @return
10226 * A rotation matrix that converts EQJ to ECT at `time`.
10227 */
10228astro_rotation_t Astronomy_Rotation_EQJ_ECT(astro_time_t *time)
10229{
10230 astro_rotation_t rot, step;
10231
10232 if (time == NULL)
10233 return RotationErr(ASTRO_INVALID_PARAMETER);
10234
10235 rot = Astronomy_Rotation_EQJ_EQD(time);
10236 step = Astronomy_Rotation_EQD_ECT(time);
10237 return Astronomy_CombineRotation(rot, step);
10238}
10239
10240
10241/**
10242 * @brief
10243 * Calculates a rotation matrix from true ecliptic of date (ECT) to J2000 mean equator (EQJ).
10244 *
10245 * This is one of the family of functions that returns a rotation matrix
10246 * for converting from one orientation to another.
10247 * Source: ECT = ecliptic system, using true equinox of the specified date/time.
10248 * Target: EQJ = equatorial system, using mean equator at J2000 epoch.
10249 *
10250 * @param time
10251 * The date and time at which the Earth's equator defines the target orientation.
10252 *
10253 * @return
10254 * A rotation matrix that converts ECT to EQJ at `time`.
10255 */
10256astro_rotation_t Astronomy_Rotation_ECT_EQJ(astro_time_t *time)
10257{
10258 astro_rotation_t rot, step;
10259
10260 if (time == NULL)
10261 return RotationErr(ASTRO_INVALID_PARAMETER);
10262
10263 rot = Astronomy_Rotation_ECT_EQD(time);
10264 step = Astronomy_Rotation_EQD_EQJ(time);
10265 return Astronomy_CombineRotation(rot, step);
10266}
10267
10268
10269/**
10270 * @brief
10271 * Calculates a rotation matrix from equatorial of-date (EQD) to J2000 mean equator (EQJ).
10272 *
10273 * This is one of the family of functions that returns a rotation matrix
10274 * for converting from one orientation to another.
10275 * Source: EQD = equatorial system, using equator of the specified date/time.
10276 * Target: EQJ = equatorial system, using equator at J2000 epoch.
10277 *
10278 * @param time
10279 * The date and time at which the Earth's equator defines the source orientation.
10280 *
10281 * @return
10282 * A rotation matrix that converts EQD at `time` to EQJ.
10283 */
10284astro_rotation_t Astronomy_Rotation_EQD_EQJ(astro_time_t *time)
10285{
10286 astro_rotation_t prec, nut;
10287
10288 if (time == NULL)
10289 return RotationErr(ASTRO_INVALID_PARAMETER);
10290
10291 nut = nutation_rot(time, INTO_2000);
10292 prec = precession_rot(*time, INTO_2000);
10293 return Astronomy_CombineRotation(nut, prec);
10294}
10295
10296
10297/**
10298 * @brief
10299 * Calculates a rotation matrix from equatorial of-date (EQD) to horizontal (HOR).
10300 *
10301 * This is one of the family of functions that returns a rotation matrix
10302 * for converting from one orientation to another.
10303 * Source: EQD = equatorial system, using equator of the specified date/time.
10304 * Target: HOR = horizontal system.
10305 *
10306 * @param time
10307 * The date and time at which the Earth's equator applies.
10308 *
10309 * @param observer
10310 * A location near the Earth's mean sea level that defines the observer's horizon.
10311 *
10312 * @return
10313 * A rotation matrix that converts EQD to HOR at `time` and for `observer`.
10314 * The components of the horizontal vector are:
10315 * x = north, y = west, z = zenith (straight up from the observer).
10316 * These components are chosen so that the "right-hand rule" works for the vector
10317 * and so that north represents the direction where azimuth = 0.
10318 */
10319astro_rotation_t Astronomy_Rotation_EQD_HOR(astro_time_t *time, astro_observer_t observer)
10320{
10321 astro_rotation_t rot;
10322 double uze[3], une[3], uwe[3];
10323 double uz[3], un[3], uw[3];
10324 double spin_angle;
10325
10326 if (time == NULL)
10327 return RotationErr(ASTRO_INVALID_PARAMETER);
10328
10329 double sinlat = sin(observer.latitude * DEG2RAD);
10330 double coslat = cos(observer.latitude * DEG2RAD);
10331 double sinlon = sin(observer.longitude * DEG2RAD);
10332 double coslon = cos(observer.longitude * DEG2RAD);
10333
10334 uze[0] = coslat * coslon;
10335 uze[1] = coslat * sinlon;
10336 uze[2] = sinlat;
10337
10338 une[0] = -sinlat * coslon;
10339 une[1] = -sinlat * sinlon;
10340 une[2] = coslat;
10341
10342 uwe[0] = sinlon;
10343 uwe[1] = -coslon;
10344 uwe[2] = 0.0;
10345
10346 spin_angle = -15.0 * Astronomy_SiderealTime(time);
10347 spin(spin_angle, uze, uz);
10348 spin(spin_angle, une, un);
10349 spin(spin_angle, uwe, uw);
10350
10351 rot.rot[0][0] = un[0]; rot.rot[1][0] = un[1]; rot.rot[2][0] = un[2];
10352 rot.rot[0][1] = uw[0]; rot.rot[1][1] = uw[1]; rot.rot[2][1] = uw[2];
10353 rot.rot[0][2] = uz[0]; rot.rot[1][2] = uz[1]; rot.rot[2][2] = uz[2];
10354
10355 rot.status = ASTRO_SUCCESS;
10356 return rot;
10357}
10358
10359
10360/**
10361 * @brief
10362 * Calculates a rotation matrix from horizontal (HOR) to equatorial of-date (EQD).
10363 *
10364 * This is one of the family of functions that returns a rotation matrix
10365 * for converting from one orientation to another.
10366 * Source: HOR = horizontal system (x=North, y=West, z=Zenith).
10367 * Target: EQD = equatorial system, using equator of the specified date/time.
10368 *
10369 * @param time
10370 * The date and time at which the Earth's equator applies.
10371 *
10372 * @param observer
10373 * A location near the Earth's mean sea level that defines the observer's horizon.
10374 *
10375 * @return
10376 * A rotation matrix that converts HOR to EQD at `time` and for `observer`.
10377 */
10378astro_rotation_t Astronomy_Rotation_HOR_EQD(astro_time_t *time, astro_observer_t observer)
10379{
10380 astro_rotation_t rot = Astronomy_Rotation_EQD_HOR(time, observer);
10381 return Astronomy_InverseRotation(rot);
10382}
10383
10384
10385/**
10386 * @brief
10387 * Calculates a rotation matrix from horizontal (HOR) to J2000 equatorial (EQJ).
10388 *
10389 * This is one of the family of functions that returns a rotation matrix
10390 * for converting from one orientation to another.
10391 * Source: HOR = horizontal system (x=North, y=West, z=Zenith).
10392 * Target: EQJ = equatorial system, using equator at the J2000 epoch.
10393 *
10394 * @param time
10395 * The date and time of the observation.
10396 *
10397 * @param observer
10398 * A location near the Earth's mean sea level that defines the observer's horizon.
10399 *
10400 * @return
10401 * A rotation matrix that converts HOR to EQJ at `time` and for `observer`.
10402 */
10403astro_rotation_t Astronomy_Rotation_HOR_EQJ(astro_time_t *time, astro_observer_t observer)
10404{
10405 astro_rotation_t hor_eqd, eqd_eqj;
10406
10407 if (time == NULL)
10408 return RotationErr(ASTRO_INVALID_PARAMETER);
10409
10410 hor_eqd = Astronomy_Rotation_HOR_EQD(time, observer);
10411 eqd_eqj = Astronomy_Rotation_EQD_EQJ(time);
10412 return Astronomy_CombineRotation(hor_eqd, eqd_eqj);
10413}
10414
10415
10416/**
10417 * @brief
10418 * Calculates a rotation matrix from J2000 mean equator (EQJ) to horizontal (HOR).
10419 *
10420 * This is one of the family of functions that returns a rotation matrix
10421 * for converting from one orientation to another.
10422 * Source: EQJ = equatorial system, using the equator at the J2000 epoch.
10423 * Target: HOR = horizontal system.
10424 *
10425 * @param time
10426 * The date and time of the desired horizontal orientation.
10427 *
10428 * @param observer
10429 * A location near the Earth's mean sea level that defines the observer's horizon.
10430 *
10431 * @return
10432 * A rotation matrix that converts EQJ to HOR at `time` and for `observer`.
10433 * The components of the horizontal vector are:
10434 * x = north, y = west, z = zenith (straight up from the observer).
10435 * These components are chosen so that the "right-hand rule" works for the vector
10436 * and so that north represents the direction where azimuth = 0.
10437 */
10438astro_rotation_t Astronomy_Rotation_EQJ_HOR(astro_time_t *time, astro_observer_t observer)
10439{
10440 astro_rotation_t rot = Astronomy_Rotation_HOR_EQJ(time, observer);
10441 return Astronomy_InverseRotation(rot);
10442}
10443
10444
10445/**
10446 * @brief
10447 * Calculates a rotation matrix from equatorial of-date (EQD) to J2000 mean ecliptic (ECL).
10448 *
10449 * This is one of the family of functions that returns a rotation matrix
10450 * for converting from one orientation to another.
10451 * Source: EQD = equatorial system, using equator of date.
10452 * Target: ECL = ecliptic system, using equator at J2000 epoch.
10453 *
10454 * @param time
10455 * The date and time of the source equator.
10456 *
10457 * @return
10458 * A rotation matrix that converts EQD to ECL.
10459 */
10460astro_rotation_t Astronomy_Rotation_EQD_ECL(astro_time_t *time)
10461{
10462 astro_rotation_t eqd_eqj;
10463 astro_rotation_t eqj_ecl;
10464
10465 if (time == NULL)
10466 return RotationErr(ASTRO_INVALID_PARAMETER);
10467
10468 eqd_eqj = Astronomy_Rotation_EQD_EQJ(time);
10469 eqj_ecl = Astronomy_Rotation_EQJ_ECL();
10470 return Astronomy_CombineRotation(eqd_eqj, eqj_ecl);
10471}
10472
10473
10474/**
10475 * @brief
10476 * Calculates a rotation matrix from J2000 mean ecliptic (ECL) to equatorial of-date (EQD).
10477 *
10478 * This is one of the family of functions that returns a rotation matrix
10479 * for converting from one orientation to another.
10480 * Source: ECL = ecliptic system, using equator at J2000 epoch.
10481 * Target: EQD = equatorial system, using equator of date.
10482 *
10483 * @param time
10484 * The date and time of the desired equator.
10485 *
10486 * @return
10487 * A rotation matrix that converts ECL to EQD.
10488 */
10489astro_rotation_t Astronomy_Rotation_ECL_EQD(astro_time_t *time)
10490{
10491 astro_rotation_t rot = Astronomy_Rotation_EQD_ECL(time);
10492 return Astronomy_InverseRotation(rot);
10493}
10494
10495/**
10496 * @brief
10497 * Calculates a rotation matrix from J2000 mean ecliptic (ECL) to horizontal (HOR).
10498 *
10499 * This is one of the family of functions that returns a rotation matrix
10500 * for converting from one orientation to another.
10501 * Source: ECL = ecliptic system, using equator at J2000 epoch.
10502 * Target: HOR = horizontal system.
10503 *
10504 * @param time
10505 * The date and time of the desired horizontal orientation.
10506 *
10507 * @param observer
10508 * A location near the Earth's mean sea level that defines the observer's horizon.
10509 *
10510 * @return
10511 * A rotation matrix that converts ECL to HOR at `time` and for `observer`.
10512 * The components of the horizontal vector are:
10513 * x = north, y = west, z = zenith (straight up from the observer).
10514 * These components are chosen so that the "right-hand rule" works for the vector
10515 * and so that north represents the direction where azimuth = 0.
10516 */
10517astro_rotation_t Astronomy_Rotation_ECL_HOR(astro_time_t *time, astro_observer_t observer)
10518{
10519 astro_rotation_t ecl_eqd;
10520 astro_rotation_t eqd_hor;
10521
10522 if (time == NULL)
10523 return RotationErr(ASTRO_INVALID_PARAMETER);
10524
10525 ecl_eqd = Astronomy_Rotation_ECL_EQD(time);
10526 eqd_hor = Astronomy_Rotation_EQD_HOR(time, observer);
10527 return Astronomy_CombineRotation(ecl_eqd, eqd_hor);
10528}
10529
10530/**
10531 * @brief
10532 * Calculates a rotation matrix from horizontal (HOR) to J2000 mean ecliptic (ECL).
10533 *
10534 * This is one of the family of functions that returns a rotation matrix
10535 * for converting from one orientation to another.
10536 * Source: HOR = horizontal system.
10537 * Target: ECL = ecliptic system, using equator at J2000 epoch.
10538 *
10539 * @param time
10540 * The date and time of the horizontal observation.
10541 *
10542 * @param observer
10543 * The location of the horizontal observer.
10544 *
10545 * @return
10546 * A rotation matrix that converts HOR to ECL.
10547 */
10548astro_rotation_t Astronomy_Rotation_HOR_ECL(astro_time_t *time, astro_observer_t observer)
10549{
10550 astro_rotation_t rot = Astronomy_Rotation_ECL_HOR(time, observer);
10551 return Astronomy_InverseRotation(rot);
10552}
10553
10554/**
10555 * @brief
10556 * Returns a rotation matrix from J2000 mean ecliptic (EQJ) to galactic (GAL).
10557 *
10558 * This is one of the family of functions that returns a rotation matrix
10559 * for converting from one orientation to another.
10560 * Source: EQJ = equatorial system, using the equator at the J2000 epoch.
10561 * Target: GAL = galactic system (IAU 1958 definition).
10562 *
10563 * @return
10564 * A rotation matrix that converts EQJ to GAL.
10565 */
10566astro_rotation_t Astronomy_Rotation_EQJ_GAL(void)
10567{
10568 astro_rotation_t rot;
10569
10570 /*
10571 This rotation matrix was calculated by the following script
10572 in this same source code repository:
10573 demo/python/galeqj_matrix.py
10574 */
10575
10576 rot.rot[0][0] = -0.0548624779711344;
10577 rot.rot[0][1] = +0.4941095946388765;
10578 rot.rot[0][2] = -0.8676668813529025;
10579
10580 rot.rot[1][0] = -0.8734572784246782;
10581 rot.rot[1][1] = -0.4447938112296831;
10582 rot.rot[1][2] = -0.1980677870294097;
10583
10584 rot.rot[2][0] = -0.4838000529948520;
10585 rot.rot[2][1] = +0.7470034631630423;
10586 rot.rot[2][2] = +0.4559861124470794;
10587
10588 rot.status = ASTRO_SUCCESS;
10589
10590 return rot;
10591}
10592
10593/**
10594 * @brief
10595 * Returns a rotation matrix from ecliptic galactic (GAL) to J2000 (EQJ).
10596 *
10597 * This is one of the family of functions that returns a rotation matrix
10598 * for converting from one orientation to another.
10599 * Source: GAL = galactic system (IAU 1958 definition).
10600 * Target: EQJ = equatorial system, using the equator at the J2000 epoch.
10601 *
10602 * @return
10603 * A rotation matrix that converts GAL to EQJ.
10604 */
10605astro_rotation_t Astronomy_Rotation_GAL_EQJ(void)
10606{
10607 astro_rotation_t rot;
10608
10609 /*
10610 This rotation matrix was calculated by the following script
10611 in this same source code repository:
10612 demo/python/galeqj_matrix.py
10613 */
10614
10615 rot.rot[0][0] = -0.0548624779711344;
10616 rot.rot[0][1] = -0.8734572784246782;
10617 rot.rot[0][2] = -0.4838000529948520;
10618
10619 rot.rot[1][0] = +0.4941095946388765;
10620 rot.rot[1][1] = -0.4447938112296831;
10621 rot.rot[1][2] = +0.7470034631630423;
10622
10623 rot.rot[2][0] = -0.8676668813529025;
10624 rot.rot[2][1] = -0.1980677870294097;
10625 rot.rot[2][2] = +0.4559861124470794;
10626
10627 rot.status = ASTRO_SUCCESS;
10628
10629 return rot;
10630}
10631
10632
10633/**
10634 * @brief
10635 * Returns a rotation matrix from true ecliptic of date (ECT) to equator of date (EQD).
10636 *
10637 * This is one of the family of functions that returns a rotation matrix
10638 * for converting from one orientation to another.
10639 * Source: ECT = true ecliptic of date.
10640 * Target: EQD = equator of date.
10641 *
10642 * @param time
10643 * The date and time of the ecliptic/equator conversion.
10644 *
10645 * @return
10646 * A rotation matrix that converts ECT to EQD.
10647 */
10648astro_rotation_t Astronomy_Rotation_ECT_EQD(astro_time_t *time)
10649{
10650 astro_rotation_t m;
10651 earth_tilt_t et;
10652 double tobl, cos_tobl, sin_tobl;
10653
10654 if (time == NULL)
10655 return RotationErr(ASTRO_INVALID_PARAMETER);
10656
10657 /* Find true ecliptic obliquity for this time. */
10658 et = e_tilt(time);
10659 tobl = et.tobl * DEG2RAD;
10660 cos_tobl = cos(tobl);
10661 sin_tobl = sin(tobl);
10662
10663 /* EQD.x = ECT.x */
10664 m.rot[0][0] = 1.0;
10665 m.rot[1][0] = 0.0;
10666 m.rot[2][0] = 0.0;
10667
10668 /* EQD.y = +cos*ECT.y - sin*ECT.z */
10669 m.rot[0][1] = 0.0;
10670 m.rot[1][1] = +cos_tobl;
10671 m.rot[2][1] = -sin_tobl;
10672
10673 /* EQD.z = +sin*ECT.y + cos*ECT.z */
10674 m.rot[0][2] = 0.0;
10675 m.rot[1][2] = +sin_tobl;
10676 m.rot[2][2] = +cos_tobl;
10677
10678 m.status = ASTRO_SUCCESS;
10679 return m;
10680}
10681
10682
10683/**
10684 * @brief
10685 * Returns a rotation matrix from equator of date (EQD) to true ecliptic of date (ECT).
10686 *
10687 * This is one of the family of functions that returns a rotation matrix
10688 * for converting from one orientation to another.
10689 * Source: EQD = equator of date.
10690 * Target: ECT = true ecliptic of date.
10691 *
10692 * @param time
10693 * The date and time of the equator/ecliptic conversion.
10694 *
10695 * @return
10696 * A rotation matrix that converts EQD to ECT.
10697 */
10698astro_rotation_t Astronomy_Rotation_EQD_ECT(astro_time_t *time)
10699{
10700 astro_rotation_t m;
10701 earth_tilt_t et;
10702 double tobl, cos_tobl, sin_tobl;
10703
10704 if (time == NULL)
10705 return RotationErr(ASTRO_INVALID_PARAMETER);
10706
10707 /* Find true ecliptic obliquity for this time. */
10708 et = e_tilt(time);
10709 tobl = et.tobl * DEG2RAD;
10710 cos_tobl = cos(tobl);
10711 sin_tobl = sin(tobl);
10712
10713 /* ECT.x = EQD.x */
10714 m.rot[0][0] = 1.0;
10715 m.rot[1][0] = 0.0;
10716 m.rot[2][0] = 0.0;
10717
10718 /* ECT.y = +cos*EQJ.y + sin*EQJ.z */
10719 m.rot[0][1] = 0.0;
10720 m.rot[1][1] = +cos_tobl;
10721 m.rot[2][1] = +sin_tobl;
10722
10723 /* ECT.z = -sin*EQJ.y + cos*EQJ.z */
10724 m.rot[0][2] = 0.0;
10725 m.rot[1][2] = -sin_tobl;
10726 m.rot[2][2] = +cos_tobl;
10727
10728 m.status = ASTRO_SUCCESS;
10729 return m;
10730}
10731
10732
10733/** @cond DOXYGEN_SKIP */
10734typedef struct
10735{
10736 const char *symbol;
10737 const char *name;
10738}
10739constel_info_t;
10740
10741
10742typedef struct
10743{
10744 int index;
10745 double ra_lo;
10746 double ra_hi;
10747 double dec_lo;
10748}
10749constel_boundary_t;
10750/** @endcond */
10751
10752#define NUM_CONSTELLATIONS 88
10753
10754static const constel_info_t ConstelInfo[] = {
10755 /* 0 */ { "And", "Andromeda" }
10756, /* 1 */ { "Ant", "Antila" }
10757, /* 2 */ { "Aps", "Apus" }
10758, /* 3 */ { "Aql", "Aquila" }
10759, /* 4 */ { "Aqr", "Aquarius" }
10760, /* 5 */ { "Ara", "Ara" }
10761, /* 6 */ { "Ari", "Aries" }
10762, /* 7 */ { "Aur", "Auriga" }
10763, /* 8 */ { "Boo", "Bootes" }
10764, /* 9 */ { "Cae", "Caelum" }
10765, /* 10 */ { "Cam", "Camelopardis" }
10766, /* 11 */ { "Cap", "Capricornus" }
10767, /* 12 */ { "Car", "Carina" }
10768, /* 13 */ { "Cas", "Cassiopeia" }
10769, /* 14 */ { "Cen", "Centaurus" }
10770, /* 15 */ { "Cep", "Cepheus" }
10771, /* 16 */ { "Cet", "Cetus" }
10772, /* 17 */ { "Cha", "Chamaeleon" }
10773, /* 18 */ { "Cir", "Circinus" }
10774, /* 19 */ { "CMa", "Canis Major" }
10775, /* 20 */ { "CMi", "Canis Minor" }
10776, /* 21 */ { "Cnc", "Cancer" }
10777, /* 22 */ { "Col", "Columba" }
10778, /* 23 */ { "Com", "Coma Berenices" }
10779, /* 24 */ { "CrA", "Corona Australis" }
10780, /* 25 */ { "CrB", "Corona Borealis" }
10781, /* 26 */ { "Crt", "Crater" }
10782, /* 27 */ { "Cru", "Crux" }
10783, /* 28 */ { "Crv", "Corvus" }
10784, /* 29 */ { "CVn", "Canes Venatici" }
10785, /* 30 */ { "Cyg", "Cygnus" }
10786, /* 31 */ { "Del", "Delphinus" }
10787, /* 32 */ { "Dor", "Dorado" }
10788, /* 33 */ { "Dra", "Draco" }
10789, /* 34 */ { "Equ", "Equuleus" }
10790, /* 35 */ { "Eri", "Eridanus" }
10791, /* 36 */ { "For", "Fornax" }
10792, /* 37 */ { "Gem", "Gemini" }
10793, /* 38 */ { "Gru", "Grus" }
10794, /* 39 */ { "Her", "Hercules" }
10795, /* 40 */ { "Hor", "Horologium" }
10796, /* 41 */ { "Hya", "Hydra" }
10797, /* 42 */ { "Hyi", "Hydrus" }
10798, /* 43 */ { "Ind", "Indus" }
10799, /* 44 */ { "Lac", "Lacerta" }
10800, /* 45 */ { "Leo", "Leo" }
10801, /* 46 */ { "Lep", "Lepus" }
10802, /* 47 */ { "Lib", "Libra" }
10803, /* 48 */ { "LMi", "Leo Minor" }
10804, /* 49 */ { "Lup", "Lupus" }
10805, /* 50 */ { "Lyn", "Lynx" }
10806, /* 51 */ { "Lyr", "Lyra" }
10807, /* 52 */ { "Men", "Mensa" }
10808, /* 53 */ { "Mic", "Microscopium" }
10809, /* 54 */ { "Mon", "Monoceros" }
10810, /* 55 */ { "Mus", "Musca" }
10811, /* 56 */ { "Nor", "Norma" }
10812, /* 57 */ { "Oct", "Octans" }
10813, /* 58 */ { "Oph", "Ophiuchus" }
10814, /* 59 */ { "Ori", "Orion" }
10815, /* 60 */ { "Pav", "Pavo" }
10816, /* 61 */ { "Peg", "Pegasus" }
10817, /* 62 */ { "Per", "Perseus" }
10818, /* 63 */ { "Phe", "Phoenix" }
10819, /* 64 */ { "Pic", "Pictor" }
10820, /* 65 */ { "PsA", "Pisces Austrinus" }
10821, /* 66 */ { "Psc", "Pisces" }
10822, /* 67 */ { "Pup", "Puppis" }
10823, /* 68 */ { "Pyx", "Pyxis" }
10824, /* 69 */ { "Ret", "Reticulum" }
10825, /* 70 */ { "Scl", "Sculptor" }
10826, /* 71 */ { "Sco", "Scorpius" }
10827, /* 72 */ { "Sct", "Scutum" }
10828, /* 73 */ { "Ser", "Serpens" }
10829, /* 74 */ { "Sex", "Sextans" }
10830, /* 75 */ { "Sge", "Sagitta" }
10831, /* 76 */ { "Sgr", "Sagittarius" }
10832, /* 77 */ { "Tau", "Taurus" }
10833, /* 78 */ { "Tel", "Telescopium" }
10834, /* 79 */ { "TrA", "Triangulum Australe" }
10835, /* 80 */ { "Tri", "Triangulum" }
10836, /* 81 */ { "Tuc", "Tucana" }
10837, /* 82 */ { "UMa", "Ursa Major" }
10838, /* 83 */ { "UMi", "Ursa Minor" }
10839, /* 84 */ { "Vel", "Vela" }
10840, /* 85 */ { "Vir", "Virgo" }
10841, /* 86 */ { "Vol", "Volans" }
10842, /* 87 */ { "Vul", "Vulpecula" }
10843};
10844
10845static const constel_boundary_t ConstelBounds[] = {
10846 { 83, 0, 8640, 2112 } /* UMi */
10847, { 83, 2880, 5220, 2076 } /* UMi */
10848, { 83, 7560, 8280, 2068 } /* UMi */
10849, { 83, 6480, 7560, 2064 } /* UMi */
10850, { 15, 0, 2880, 2040 } /* Cep */
10851, { 10, 3300, 3840, 1968 } /* Cam */
10852, { 15, 0, 1800, 1920 } /* Cep */
10853, { 10, 3840, 5220, 1920 } /* Cam */
10854, { 83, 6300, 6480, 1920 } /* UMi */
10855, { 33, 7260, 7560, 1920 } /* Dra */
10856, { 15, 0, 1263, 1848 } /* Cep */
10857, { 10, 4140, 4890, 1848 } /* Cam */
10858, { 83, 5952, 6300, 1800 } /* UMi */
10859, { 15, 7260, 7440, 1800 } /* Cep */
10860, { 10, 2868, 3300, 1764 } /* Cam */
10861, { 33, 3300, 4080, 1764 } /* Dra */
10862, { 83, 4680, 5952, 1680 } /* UMi */
10863, { 13, 1116, 1230, 1632 } /* Cas */
10864, { 33, 7350, 7440, 1608 } /* Dra */
10865, { 33, 4080, 4320, 1596 } /* Dra */
10866, { 15, 0, 120, 1584 } /* Cep */
10867, { 83, 5040, 5640, 1584 } /* UMi */
10868, { 15, 8490, 8640, 1584 } /* Cep */
10869, { 33, 4320, 4860, 1536 } /* Dra */
10870, { 33, 4860, 5190, 1512 } /* Dra */
10871, { 15, 8340, 8490, 1512 } /* Cep */
10872, { 10, 2196, 2520, 1488 } /* Cam */
10873, { 33, 7200, 7350, 1476 } /* Dra */
10874, { 15, 7393.2, 7416, 1462 } /* Cep */
10875, { 10, 2520, 2868, 1440 } /* Cam */
10876, { 82, 2868, 3030, 1440 } /* UMa */
10877, { 33, 7116, 7200, 1428 } /* Dra */
10878, { 15, 7200, 7393.2, 1428 } /* Cep */
10879, { 15, 8232, 8340, 1418 } /* Cep */
10880, { 13, 0, 876, 1404 } /* Cas */
10881, { 33, 6990, 7116, 1392 } /* Dra */
10882, { 13, 612, 687, 1380 } /* Cas */
10883, { 13, 876, 1116, 1368 } /* Cas */
10884, { 10, 1116, 1140, 1368 } /* Cam */
10885, { 15, 8034, 8232, 1350 } /* Cep */
10886, { 10, 1800, 2196, 1344 } /* Cam */
10887, { 82, 5052, 5190, 1332 } /* UMa */
10888, { 33, 5190, 6990, 1332 } /* Dra */
10889, { 10, 1140, 1200, 1320 } /* Cam */
10890, { 15, 7968, 8034, 1320 } /* Cep */
10891, { 15, 7416, 7908, 1316 } /* Cep */
10892, { 13, 0, 612, 1296 } /* Cas */
10893, { 50, 2196, 2340, 1296 } /* Lyn */
10894, { 82, 4350, 4860, 1272 } /* UMa */
10895, { 33, 5490, 5670, 1272 } /* Dra */
10896, { 15, 7908, 7968, 1266 } /* Cep */
10897, { 10, 1200, 1800, 1260 } /* Cam */
10898, { 13, 8232, 8400, 1260 } /* Cas */
10899, { 33, 5670, 6120, 1236 } /* Dra */
10900, { 62, 735, 906, 1212 } /* Per */
10901, { 33, 6120, 6564, 1212 } /* Dra */
10902, { 13, 0, 492, 1200 } /* Cas */
10903, { 62, 492, 600, 1200 } /* Per */
10904, { 50, 2340, 2448, 1200 } /* Lyn */
10905, { 13, 8400, 8640, 1200 } /* Cas */
10906, { 82, 4860, 5052, 1164 } /* UMa */
10907, { 13, 0, 402, 1152 } /* Cas */
10908, { 13, 8490, 8640, 1152 } /* Cas */
10909, { 39, 6543, 6564, 1140 } /* Her */
10910, { 33, 6564, 6870, 1140 } /* Dra */
10911, { 30, 6870, 6900, 1140 } /* Cyg */
10912, { 62, 600, 735, 1128 } /* Per */
10913, { 82, 3030, 3300, 1128 } /* UMa */
10914, { 13, 60, 312, 1104 } /* Cas */
10915, { 82, 4320, 4350, 1080 } /* UMa */
10916, { 50, 2448, 2652, 1068 } /* Lyn */
10917, { 30, 7887, 7908, 1056 } /* Cyg */
10918, { 30, 7875, 7887, 1050 } /* Cyg */
10919, { 30, 6900, 6984, 1044 } /* Cyg */
10920, { 82, 3300, 3660, 1008 } /* UMa */
10921, { 82, 3660, 3882, 960 } /* UMa */
10922, { 8, 5556, 5670, 960 } /* Boo */
10923, { 39, 5670, 5880, 960 } /* Her */
10924, { 50, 3330, 3450, 954 } /* Lyn */
10925, { 0, 0, 906, 882 } /* And */
10926, { 62, 906, 924, 882 } /* Per */
10927, { 51, 6969, 6984, 876 } /* Lyr */
10928, { 62, 1620, 1689, 864 } /* Per */
10929, { 30, 7824, 7875, 864 } /* Cyg */
10930, { 44, 7875, 7920, 864 } /* Lac */
10931, { 7, 2352, 2652, 852 } /* Aur */
10932, { 50, 2652, 2790, 852 } /* Lyn */
10933, { 0, 0, 720, 840 } /* And */
10934, { 44, 7920, 8214, 840 } /* Lac */
10935, { 44, 8214, 8232, 828 } /* Lac */
10936, { 0, 8232, 8460, 828 } /* And */
10937, { 62, 924, 978, 816 } /* Per */
10938, { 82, 3882, 3960, 816 } /* UMa */
10939, { 29, 4320, 4440, 816 } /* CVn */
10940, { 50, 2790, 3330, 804 } /* Lyn */
10941, { 48, 3330, 3558, 804 } /* LMi */
10942, { 0, 258, 507, 792 } /* And */
10943, { 8, 5466, 5556, 792 } /* Boo */
10944, { 0, 8460, 8550, 770 } /* And */
10945, { 29, 4440, 4770, 768 } /* CVn */
10946, { 0, 8550, 8640, 752 } /* And */
10947, { 29, 5025, 5052, 738 } /* CVn */
10948, { 80, 870, 978, 736 } /* Tri */
10949, { 62, 978, 1620, 736 } /* Per */
10950, { 7, 1620, 1710, 720 } /* Aur */
10951, { 51, 6543, 6969, 720 } /* Lyr */
10952, { 82, 3960, 4320, 696 } /* UMa */
10953, { 30, 7080, 7530, 696 } /* Cyg */
10954, { 7, 1710, 2118, 684 } /* Aur */
10955, { 48, 3558, 3780, 684 } /* LMi */
10956, { 29, 4770, 5025, 684 } /* CVn */
10957, { 0, 0, 24, 672 } /* And */
10958, { 80, 507, 600, 672 } /* Tri */
10959, { 7, 2118, 2352, 672 } /* Aur */
10960, { 37, 2838, 2880, 672 } /* Gem */
10961, { 30, 7530, 7824, 672 } /* Cyg */
10962, { 30, 6933, 7080, 660 } /* Cyg */
10963, { 80, 690, 870, 654 } /* Tri */
10964, { 25, 5820, 5880, 648 } /* CrB */
10965, { 8, 5430, 5466, 624 } /* Boo */
10966, { 25, 5466, 5820, 624 } /* CrB */
10967, { 51, 6612, 6792, 624 } /* Lyr */
10968, { 48, 3870, 3960, 612 } /* LMi */
10969, { 51, 6792, 6933, 612 } /* Lyr */
10970, { 80, 600, 690, 600 } /* Tri */
10971, { 66, 258, 306, 570 } /* Psc */
10972, { 48, 3780, 3870, 564 } /* LMi */
10973, { 87, 7650, 7710, 564 } /* Vul */
10974, { 77, 2052, 2118, 548 } /* Tau */
10975, { 0, 24, 51, 528 } /* And */
10976, { 73, 5730, 5772, 528 } /* Ser */
10977, { 37, 2118, 2238, 516 } /* Gem */
10978, { 87, 7140, 7290, 510 } /* Vul */
10979, { 87, 6792, 6930, 506 } /* Vul */
10980, { 0, 51, 306, 504 } /* And */
10981, { 87, 7290, 7404, 492 } /* Vul */
10982, { 37, 2811, 2838, 480 } /* Gem */
10983, { 87, 7404, 7650, 468 } /* Vul */
10984, { 87, 6930, 7140, 460 } /* Vul */
10985, { 6, 1182, 1212, 456 } /* Ari */
10986, { 75, 6792, 6840, 444 } /* Sge */
10987, { 59, 2052, 2076, 432 } /* Ori */
10988, { 37, 2238, 2271, 420 } /* Gem */
10989, { 75, 6840, 7140, 388 } /* Sge */
10990, { 77, 1788, 1920, 384 } /* Tau */
10991, { 39, 5730, 5790, 384 } /* Her */
10992, { 75, 7140, 7290, 378 } /* Sge */
10993, { 77, 1662, 1788, 372 } /* Tau */
10994, { 77, 1920, 2016, 372 } /* Tau */
10995, { 23, 4620, 4860, 360 } /* Com */
10996, { 39, 6210, 6570, 344 } /* Her */
10997, { 23, 4272, 4620, 336 } /* Com */
10998, { 37, 2700, 2811, 324 } /* Gem */
10999, { 39, 6030, 6210, 308 } /* Her */
11000, { 61, 0, 51, 300 } /* Peg */
11001, { 77, 2016, 2076, 300 } /* Tau */
11002, { 37, 2520, 2700, 300 } /* Gem */
11003, { 61, 7602, 7680, 300 } /* Peg */
11004, { 37, 2271, 2496, 288 } /* Gem */
11005, { 39, 6570, 6792, 288 } /* Her */
11006, { 31, 7515, 7578, 284 } /* Del */
11007, { 61, 7578, 7602, 284 } /* Peg */
11008, { 45, 4146, 4272, 264 } /* Leo */
11009, { 59, 2247, 2271, 240 } /* Ori */
11010, { 37, 2496, 2520, 240 } /* Gem */
11011, { 21, 2811, 2853, 240 } /* Cnc */
11012, { 61, 8580, 8640, 240 } /* Peg */
11013, { 6, 600, 1182, 238 } /* Ari */
11014, { 31, 7251, 7308, 204 } /* Del */
11015, { 8, 4860, 5430, 192 } /* Boo */
11016, { 61, 8190, 8580, 180 } /* Peg */
11017, { 21, 2853, 3330, 168 } /* Cnc */
11018, { 45, 3330, 3870, 168 } /* Leo */
11019, { 58, 6570, 6718.4, 150 } /* Oph */
11020, { 3, 6718.4, 6792, 150 } /* Aql */
11021, { 31, 7500, 7515, 144 } /* Del */
11022, { 20, 2520, 2526, 132 } /* CMi */
11023, { 73, 6570, 6633, 108 } /* Ser */
11024, { 39, 5790, 6030, 96 } /* Her */
11025, { 58, 6570, 6633, 72 } /* Oph */
11026, { 61, 7728, 7800, 66 } /* Peg */
11027, { 66, 0, 720, 48 } /* Psc */
11028, { 73, 6690, 6792, 48 } /* Ser */
11029, { 31, 7308, 7500, 48 } /* Del */
11030, { 34, 7500, 7680, 48 } /* Equ */
11031, { 61, 7680, 7728, 48 } /* Peg */
11032, { 61, 7920, 8190, 48 } /* Peg */
11033, { 61, 7800, 7920, 42 } /* Peg */
11034, { 20, 2526, 2592, 36 } /* CMi */
11035, { 77, 1290, 1662, 0 } /* Tau */
11036, { 59, 1662, 1680, 0 } /* Ori */
11037, { 20, 2592, 2910, 0 } /* CMi */
11038, { 85, 5280, 5430, 0 } /* Vir */
11039, { 58, 6420, 6570, 0 } /* Oph */
11040, { 16, 954, 1182, -42 } /* Cet */
11041, { 77, 1182, 1290, -42 } /* Tau */
11042, { 73, 5430, 5856, -78 } /* Ser */
11043, { 59, 1680, 1830, -96 } /* Ori */
11044, { 59, 2100, 2247, -96 } /* Ori */
11045, { 73, 6420, 6468, -96 } /* Ser */
11046, { 73, 6570, 6690, -96 } /* Ser */
11047, { 3, 6690, 6792, -96 } /* Aql */
11048, { 66, 8190, 8580, -96 } /* Psc */
11049, { 45, 3870, 4146, -144 } /* Leo */
11050, { 85, 4146, 4260, -144 } /* Vir */
11051, { 66, 0, 120, -168 } /* Psc */
11052, { 66, 8580, 8640, -168 } /* Psc */
11053, { 85, 5130, 5280, -192 } /* Vir */
11054, { 58, 5730, 5856, -192 } /* Oph */
11055, { 3, 7200, 7392, -216 } /* Aql */
11056, { 4, 7680, 7872, -216 } /* Aqr */
11057, { 58, 6180, 6468, -240 } /* Oph */
11058, { 54, 2100, 2910, -264 } /* Mon */
11059, { 35, 1770, 1830, -264 } /* Eri */
11060, { 59, 1830, 2100, -264 } /* Ori */
11061, { 41, 2910, 3012, -264 } /* Hya */
11062, { 74, 3450, 3870, -264 } /* Sex */
11063, { 85, 4260, 4620, -264 } /* Vir */
11064, { 58, 6330, 6360, -280 } /* Oph */
11065, { 3, 6792, 7200, -288.8 } /* Aql */
11066, { 35, 1740, 1770, -348 } /* Eri */
11067, { 4, 7392, 7680, -360 } /* Aqr */
11068, { 73, 6180, 6570, -384 } /* Ser */
11069, { 72, 6570, 6792, -384 } /* Sct */
11070, { 41, 3012, 3090, -408 } /* Hya */
11071, { 58, 5856, 5895, -438 } /* Oph */
11072, { 41, 3090, 3270, -456 } /* Hya */
11073, { 26, 3870, 3900, -456 } /* Crt */
11074, { 71, 5856, 5895, -462 } /* Sco */
11075, { 47, 5640, 5730, -480 } /* Lib */
11076, { 28, 4530, 4620, -528 } /* Crv */
11077, { 85, 4620, 5130, -528 } /* Vir */
11078, { 41, 3270, 3510, -576 } /* Hya */
11079, { 16, 600, 954, -585.2 } /* Cet */
11080, { 35, 954, 1350, -585.2 } /* Eri */
11081, { 26, 3900, 4260, -588 } /* Crt */
11082, { 28, 4260, 4530, -588 } /* Crv */
11083, { 47, 5130, 5370, -588 } /* Lib */
11084, { 58, 5856, 6030, -590 } /* Oph */
11085, { 16, 0, 600, -612 } /* Cet */
11086, { 11, 7680, 7872, -612 } /* Cap */
11087, { 4, 7872, 8580, -612 } /* Aqr */
11088, { 16, 8580, 8640, -612 } /* Cet */
11089, { 41, 3510, 3690, -636 } /* Hya */
11090, { 35, 1692, 1740, -654 } /* Eri */
11091, { 46, 1740, 2202, -654 } /* Lep */
11092, { 11, 7200, 7680, -672 } /* Cap */
11093, { 41, 3690, 3810, -700 } /* Hya */
11094, { 41, 4530, 5370, -708 } /* Hya */
11095, { 47, 5370, 5640, -708 } /* Lib */
11096, { 71, 5640, 5760, -708 } /* Sco */
11097, { 35, 1650, 1692, -720 } /* Eri */
11098, { 58, 6030, 6336, -720 } /* Oph */
11099, { 76, 6336, 6420, -720 } /* Sgr */
11100, { 41, 3810, 3900, -748 } /* Hya */
11101, { 19, 2202, 2652, -792 } /* CMa */
11102, { 41, 4410, 4530, -792 } /* Hya */
11103, { 41, 3900, 4410, -840 } /* Hya */
11104, { 36, 1260, 1350, -864 } /* For */
11105, { 68, 3012, 3372, -882 } /* Pyx */
11106, { 35, 1536, 1650, -888 } /* Eri */
11107, { 76, 6420, 6900, -888 } /* Sgr */
11108, { 65, 7680, 8280, -888 } /* PsA */
11109, { 70, 8280, 8400, -888 } /* Scl */
11110, { 36, 1080, 1260, -950 } /* For */
11111, { 1, 3372, 3960, -954 } /* Ant */
11112, { 70, 0, 600, -960 } /* Scl */
11113, { 36, 600, 1080, -960 } /* For */
11114, { 35, 1392, 1536, -960 } /* Eri */
11115, { 70, 8400, 8640, -960 } /* Scl */
11116, { 14, 5100, 5370, -1008 } /* Cen */
11117, { 49, 5640, 5760, -1008 } /* Lup */
11118, { 71, 5760, 5911.5, -1008 } /* Sco */
11119, { 9, 1740, 1800, -1032 } /* Cae */
11120, { 22, 1800, 2370, -1032 } /* Col */
11121, { 67, 2880, 3012, -1032 } /* Pup */
11122, { 35, 1230, 1392, -1056 } /* Eri */
11123, { 71, 5911.5, 6420, -1092 } /* Sco */
11124, { 24, 6420, 6900, -1092 } /* CrA */
11125, { 76, 6900, 7320, -1092 } /* Sgr */
11126, { 53, 7320, 7680, -1092 } /* Mic */
11127, { 35, 1080, 1230, -1104 } /* Eri */
11128, { 9, 1620, 1740, -1116 } /* Cae */
11129, { 49, 5520, 5640, -1152 } /* Lup */
11130, { 63, 0, 840, -1156 } /* Phe */
11131, { 35, 960, 1080, -1176 } /* Eri */
11132, { 40, 1470, 1536, -1176 } /* Hor */
11133, { 9, 1536, 1620, -1176 } /* Cae */
11134, { 38, 7680, 7920, -1200 } /* Gru */
11135, { 67, 2160, 2880, -1218 } /* Pup */
11136, { 84, 2880, 2940, -1218 } /* Vel */
11137, { 35, 870, 960, -1224 } /* Eri */
11138, { 40, 1380, 1470, -1224 } /* Hor */
11139, { 63, 0, 660, -1236 } /* Phe */
11140, { 12, 2160, 2220, -1260 } /* Car */
11141, { 84, 2940, 3042, -1272 } /* Vel */
11142, { 40, 1260, 1380, -1276 } /* Hor */
11143, { 32, 1380, 1440, -1276 } /* Dor */
11144, { 63, 0, 570, -1284 } /* Phe */
11145, { 35, 780, 870, -1296 } /* Eri */
11146, { 64, 1620, 1800, -1296 } /* Pic */
11147, { 49, 5418, 5520, -1296 } /* Lup */
11148, { 84, 3042, 3180, -1308 } /* Vel */
11149, { 12, 2220, 2340, -1320 } /* Car */
11150, { 14, 4260, 4620, -1320 } /* Cen */
11151, { 49, 5100, 5418, -1320 } /* Lup */
11152, { 56, 5418, 5520, -1320 } /* Nor */
11153, { 32, 1440, 1560, -1356 } /* Dor */
11154, { 84, 3180, 3960, -1356 } /* Vel */
11155, { 14, 3960, 4050, -1356 } /* Cen */
11156, { 5, 6300, 6480, -1368 } /* Ara */
11157, { 78, 6480, 7320, -1368 } /* Tel */
11158, { 38, 7920, 8400, -1368 } /* Gru */
11159, { 40, 1152, 1260, -1380 } /* Hor */
11160, { 64, 1800, 1980, -1380 } /* Pic */
11161, { 12, 2340, 2460, -1392 } /* Car */
11162, { 63, 0, 480, -1404 } /* Phe */
11163, { 35, 480, 780, -1404 } /* Eri */
11164, { 63, 8400, 8640, -1404 } /* Phe */
11165, { 32, 1560, 1650, -1416 } /* Dor */
11166, { 56, 5520, 5911.5, -1440 } /* Nor */
11167, { 43, 7320, 7680, -1440 } /* Ind */
11168, { 64, 1980, 2160, -1464 } /* Pic */
11169, { 18, 5460, 5520, -1464 } /* Cir */
11170, { 5, 5911.5, 5970, -1464 } /* Ara */
11171, { 18, 5370, 5460, -1526 } /* Cir */
11172, { 5, 5970, 6030, -1526 } /* Ara */
11173, { 64, 2160, 2460, -1536 } /* Pic */
11174, { 12, 2460, 3252, -1536 } /* Car */
11175, { 14, 4050, 4260, -1536 } /* Cen */
11176, { 27, 4260, 4620, -1536 } /* Cru */
11177, { 14, 4620, 5232, -1536 } /* Cen */
11178, { 18, 4860, 4920, -1560 } /* Cir */
11179, { 5, 6030, 6060, -1560 } /* Ara */
11180, { 40, 780, 1152, -1620 } /* Hor */
11181, { 69, 1152, 1650, -1620 } /* Ret */
11182, { 18, 5310, 5370, -1620 } /* Cir */
11183, { 5, 6060, 6300, -1620 } /* Ara */
11184, { 60, 6300, 6480, -1620 } /* Pav */
11185, { 81, 7920, 8400, -1620 } /* Tuc */
11186, { 32, 1650, 2370, -1680 } /* Dor */
11187, { 18, 4920, 5310, -1680 } /* Cir */
11188, { 79, 5310, 6120, -1680 } /* TrA */
11189, { 81, 0, 480, -1800 } /* Tuc */
11190, { 42, 1260, 1650, -1800 } /* Hyi */
11191, { 86, 2370, 3252, -1800 } /* Vol */
11192, { 12, 3252, 4050, -1800 } /* Car */
11193, { 55, 4050, 4920, -1800 } /* Mus */
11194, { 60, 6480, 7680, -1800 } /* Pav */
11195, { 43, 7680, 8400, -1800 } /* Ind */
11196, { 81, 8400, 8640, -1800 } /* Tuc */
11197, { 81, 270, 480, -1824 } /* Tuc */
11198, { 42, 0, 1260, -1980 } /* Hyi */
11199, { 17, 2760, 4920, -1980 } /* Cha */
11200, { 2, 4920, 6480, -1980 } /* Aps */
11201, { 52, 1260, 2760, -2040 } /* Men */
11202, { 57, 0, 8640, -2160 } /* Oct */
11203};
11204
11205#define NUM_CONSTEL_BOUNDARIES 357
11206
11207
11208
11209/**
11210 * @brief
11211 * Determines the constellation that contains the given point in the sky.
11212 *
11213 * Given J2000 equatorial (EQJ) coordinates of a point in the sky, determines the
11214 * constellation that contains that point.
11215 *
11216 * @param ra
11217 * The right ascension (RA) of a point in the sky, using the J2000 equatorial system.
11218 *
11219 * @param dec
11220 * The declination (DEC) of a point in the sky, using the J2000 equatorial system.
11221 *
11222 * @return
11223 * If successful, `status` holds `ASTRO_SUCCESS`,
11224 * `symbol` holds a pointer to a 3-character string like "Ori", and
11225 * `name` holds a pointer to the full constellation name like "Orion".
11226 */
11227astro_constellation_t Astronomy_Constellation(double ra, double dec)
11228{
11229 static astro_time_t epoch2000;
11230 static astro_rotation_t rot = { ASTRO_NOT_INITIALIZED };
11231 astro_constellation_t constel;
11232 astro_spherical_t s2000;
11233 astro_equatorial_t b1875;
11234 astro_vector_t vec2000, vec1875;
11235 double x_ra, x_dec;
11236 int i, c;
11237
11238 if (dec < -90.0 || dec > +90.0)
11239 return ConstelErr(ASTRO_INVALID_PARAMETER);
11240
11241 /* Allow right ascension to "wrap around". Clamp to [0, 24) sidereal hours. */
11242 ra = fmod(ra, 24.0);
11243 if (ra < 0.0)
11244 ra += 24.0;
11245
11246 /* Lazy-initialize the rotation matrix for converting J2000 to B1875. */
11247 if (rot.status != ASTRO_SUCCESS)
11248 {
11249 /*
11250 Need to calculate the B1875 epoch. Based on this:
11251 https://en.wikipedia.org/wiki/Epoch_(astronomy)#Besselian_years
11252 B = 1900 + (JD - 2415020.31352) / 365.242198781
11253 I'm interested in using TT instead of JD, giving:
11254 B = 1900 + ((TT+2451545) - 2415020.31352) / 365.242198781
11255 B = 1900 + (TT + 36524.68648) / 365.242198781
11256 TT = 365.242198781*(B - 1900) - 36524.68648 = -45655.741449525
11257 But Astronomy_TimeFromDays() wants UT, not TT.
11258 Near that date, I get a historical correction of ut-tt = 3.2 seconds.
11259 That gives UT = -45655.74141261017 for the B1875 epoch,
11260 or 1874-12-31T18:12:21.950Z.
11261 */
11262 astro_time_t time = Astronomy_TimeFromDays(-45655.74141261017);
11263 rot = Astronomy_Rotation_EQJ_EQD(&time);
11264 if (rot.status != ASTRO_SUCCESS)
11265 return ConstelErr(rot.status);
11266
11267 epoch2000 = Astronomy_TimeFromDays(0.0);
11268 }
11269
11270 /* Convert coordinates from J2000 to year 1875. */
11271 s2000.status = ASTRO_SUCCESS;
11272 s2000.lon = ra * 15.0;
11273 s2000.lat = dec;
11274 s2000.dist = 1.0;
11275 vec2000 = Astronomy_VectorFromSphere(s2000, epoch2000);
11276 if (vec2000.status != ASTRO_SUCCESS)
11277 return ConstelErr(vec2000.status);
11278
11279 vec1875 = Astronomy_RotateVector(rot, vec2000);
11280 if (vec1875.status != ASTRO_SUCCESS)
11281 return ConstelErr(vec1875.status);
11282
11283 b1875 = Astronomy_EquatorFromVector(vec1875);
11284 if (b1875.status != ASTRO_SUCCESS)
11285 return ConstelErr(b1875.status);
11286
11287 /* Convert DEC from degrees, and RA from hours, to compact angle units used in the ContelBounds table. */
11288 x_ra = (24.0 * 15.0) * b1875.ra;
11289 x_dec = 24.0 * b1875.dec;
11290
11291 /* Search for the constellation using the B1875 coordinates. */
11292 c = -1; /* constellation not (yet) found */
11293 for (i=0; i < NUM_CONSTEL_BOUNDARIES; ++i)
11294 {
11295 const constel_boundary_t *b = &ConstelBounds[i];
11296 if ((b->dec_lo <= x_dec) && (b->ra_hi > x_ra) && (b->ra_lo <= x_ra))
11297 {
11298 c = b->index;
11299 break;
11300 }
11301 }
11302
11303 if (c < 0 || c >= NUM_CONSTELLATIONS)
11304 return ConstelErr(ASTRO_INTERNAL_ERROR); /* should have been able to find the constellation */
11305
11306 constel.status = ASTRO_SUCCESS;
11307 constel.symbol = ConstelInfo[c].symbol;
11308 constel.name = ConstelInfo[c].name;
11309 constel.ra_1875 = b1875.ra;
11310 constel.dec_1875 = b1875.dec;
11311 return constel;
11312}
11313
11314
11315static astro_lunar_eclipse_t LunarEclipseError(astro_status_t status)
11316{
11317 astro_lunar_eclipse_t eclipse;
11318 eclipse.status = status;
11319 eclipse.kind = ECLIPSE_NONE;
11320 eclipse.peak = TimeError();
11321 eclipse.obscuration = eclipse.sd_penum = eclipse.sd_partial = eclipse.sd_total = NAN;
11322 return eclipse;
11323}
11324
11325
11326/** @cond DOXYGEN_SKIP */
11327typedef struct
11328{
11329 astro_status_t status;
11330 astro_time_t time;
11331 double u; /* dot product of (heliocentric earth) and (geocentric moon): defines the shadow plane where the Moon is */
11332 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). */
11333 double k; /* umbra radius in km, at the shadow plane */
11334 double p; /* penumbra radius in km, at the shadow plane */
11335 astro_vector_t target; /* coordinates of target body relative to shadow-casting body at 'time' */
11336 astro_vector_t dir; /* heliocentric coordinates of shadow-casting body at 'time' */
11337}
11338shadow_t; /* Represents alignment of the Moon/Earth with the Earth's/Moon's shadow, for finding eclipses. */
11339
11340typedef struct
11341{
11342 double radius_limit;
11343 double direction;
11344}
11345shadow_context_t;
11346/** @endcond */
11347
11348
11349static double Obscuration( /* returns area of intersection of the two discs, divided by area of first disc. */
11350 double a, /* radius of first disc */
11351 double b, /* radius of second disc */
11352 double c) /* distance between the centers of the discs */
11353{
11354 double x; /* Horizontal location of intersection point on both circumferences */
11355 double y; /* Positive vertical location of intersection point on both circumferences */
11356 double radicand, lens1, lens2, obs;
11357
11358 if (a <= 0.0 || b <= 0.0)
11359 return 0.0; /* invalid radius */
11360
11361 if (c < 0.0)
11362 return 0.0; /* invalid distance between centers */
11363
11364 if (c >= a + b)
11365 return 0.0; /* the discs are too far apart to have any overlapping area */
11366
11367 if (c == 0.0)
11368 {
11369 /* The discs have a common center. Therefore, one disc is inside the other. */
11370 return (a <= b) ? 1.0 : (b*b)/(a*a);
11371 }
11372
11373 x = (a*a - b*b + c*c) / (2 * c);
11374
11375 radicand = a*a - x*x;
11376 if (radicand <= 0.0)
11377 {
11378 /* The circumferences do not intersect, or are tangent. */
11379 /* We already ruled out the case of non-overlapping discs. */
11380 /* Therefore, one disc is inside the other. */
11381 return (a <= b) ? 1.0 : (b*b)/(a*a);
11382 }
11383
11384 /* The discs overlap fractionally in a pair of lens-shaped areas. */
11385
11386 y = sqrt(radicand);
11387
11388 /* Return the overlapping fractional area. */
11389 /* There are two lens-shaped areas, one to the left of x, the other to the right of x. */
11390 /* Each part is calculated by subtracting a triangular area from a sector's area. */
11391 lens1 = a*a*acos(x/a) - x*y;
11392 lens2 = b*b*acos((c-x)/b) - (c-x)*y;
11393
11394 /* Find the fractional area with respect to the first disc. */
11395 obs = (lens1 + lens2) / (PI*a*a);
11396 return obs;
11397}
11398
11399
11400static shadow_t ShadowError(astro_status_t status)
11401{
11402 shadow_t shadow;
11403 memset(&shadow, 0, sizeof(shadow));
11404 shadow.status = status;
11405 return shadow;
11406}
11407
11408
11409static shadow_t CalcShadow(
11410 double body_radius_km,
11411 astro_time_t time,
11412 astro_vector_t target,
11413 astro_vector_t dir)
11414{
11415 double dx, dy, dz;
11416 shadow_t shadow;
11417
11418 shadow.target = target;
11419 shadow.dir = dir;
11420
11421 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);
11422
11423 dx = (shadow.u * dir.x) - target.x;
11424 dy = (shadow.u * dir.y) - target.y;
11425 dz = (shadow.u * dir.z) - target.z;
11426 shadow.r = KM_PER_AU * sqrt(dx*dx + dy*dy + dz*dz);
11427
11428 shadow.k = +SUN_RADIUS_KM - (1.0 + shadow.u)*(SUN_RADIUS_KM - body_radius_km);
11429 shadow.p = -SUN_RADIUS_KM + (1.0 + shadow.u)*(SUN_RADIUS_KM + body_radius_km);
11430 shadow.status = ASTRO_SUCCESS;
11431 shadow.time = time;
11432
11433 return shadow;
11434}
11435
11436
11437static shadow_t PlanetShadow(astro_body_t body, double planet_radius_km, astro_time_t time)
11438{
11439 astro_vector_t e, p, g;
11440
11441 /* Calculate light-travel-corrected vector from Earth to planet. */
11442 g = Astronomy_GeoVector(body, time, ABERRATION);
11443 if (g.status != ASTRO_SUCCESS)
11444 return ShadowError(g.status);
11445
11446 /* Calculate light-travel-corrected vector from Earth to Sun. */
11447 e = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
11448 if (e.status != ASTRO_SUCCESS)
11449 return ShadowError(e.status);
11450
11451 /* Deduce light-travel-corrected vector from Sun to planet. */
11452 p.status = ASTRO_SUCCESS;
11453 p.t = time;
11454 p.x = g.x - e.x;
11455 p.y = g.y - e.y;
11456 p.z = g.z - e.z;
11457
11458 /* Calcluate Earth's position from the planet's point of view. */
11459 e.x = -g.x;
11460 e.y = -g.y;
11461 e.z = -g.z;
11462
11463 return CalcShadow(planet_radius_km, time, e, p);
11464}
11465
11466
11467static shadow_t EarthShadow(astro_time_t time)
11468{
11469 /* This function helps find when the Earth's shadow falls upon the Moon. */
11470 astro_vector_t e, m;
11471
11472 /* Find geocentric Sun with aberration correction. */
11473 e = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
11474 if (e.status != ASTRO_SUCCESS)
11475 return ShadowError(e.status);
11476
11477 /* Convert geocentric Sun to heliocentric Earth. */
11478 /* Thus `e` points in the direction of sunlight heading toward the Earth's center. */
11479 e.x = -e.x;
11480 e.y = -e.y;
11481 e.z = -e.z;
11482
11483 /* Find geocentric Moon. */
11484 m = Astronomy_GeoMoon(time); /* This function never fails; no need to check return value */
11485
11486 return CalcShadow(EARTH_ECLIPSE_RADIUS_KM, time, m, e);
11487}
11488
11489
11490static shadow_t MoonShadow(astro_time_t time)
11491{
11492 /* This function helps find when the Moon's shadow falls upon the Earth. */
11493
11494 astro_vector_t s, e, m;
11495
11496 /* Calculate geocentric Sun with aberration correction. */
11497 s = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
11498 if (s.status != ASTRO_SUCCESS)
11499 return ShadowError(s.status);
11500
11501 m = Astronomy_GeoMoon(time); /* geocentric Moon */
11502
11503 /* Calculate lunacentric Earth. */
11504 e.status = m.status;
11505 e.x = -m.x;
11506 e.y = -m.y;
11507 e.z = -m.z;
11508 e.t = m.t;
11509
11510 /* Convert geocentric moon to heliocentric Moon. */
11511 m.x -= s.x;
11512 m.y -= s.y;
11513 m.z -= s.z;
11514
11515 return CalcShadow(MOON_MEAN_RADIUS_KM, time, e, m);
11516}
11517
11518
11519/** @cond DOXYGEN_SKIP */
11520typedef shadow_t (* shadow_func_t) (astro_time_t time);
11521/** @endcond */
11522
11523
11524static astro_func_result_t shadow_distance_slope(void *context, astro_time_t time)
11525{
11526 const double dt = 1.0 / 86400.0;
11527 astro_time_t t1, t2;
11528 astro_func_result_t result;
11529 shadow_t shadow1, shadow2;
11530 shadow_func_t shadowfunc = (shadow_func_t) context;
11531
11532 t1 = Astronomy_AddDays(time, -dt);
11533 t2 = Astronomy_AddDays(time, +dt);
11534
11535 shadow1 = shadowfunc(t1);
11536 if (shadow1.status != ASTRO_SUCCESS)
11537 return FuncError(shadow1.status);
11538
11539 shadow2 = shadowfunc(t2);
11540 if (shadow2.status != ASTRO_SUCCESS)
11541 return FuncError(shadow2.status);
11542
11543 result.value = (shadow2.r - shadow1.r) / dt;
11544 result.status = ASTRO_SUCCESS;
11545 return result;
11546}
11547
11548
11549static shadow_t PeakEarthShadow(astro_time_t search_center_time)
11550{
11551 /* Search for when the Earth's shadow axis is closest to the center of the Moon. */
11552
11553 astro_time_t t1, t2;
11554 astro_search_result_t result;
11555 const double window = 0.03; /* days before/after full moon to search for minimum shadow distance */
11556
11557 t1 = Astronomy_AddDays(search_center_time, -window);
11558 t2 = Astronomy_AddDays(search_center_time, +window);
11559
11560 result = Astronomy_Search(shadow_distance_slope, (void *)EarthShadow, t1, t2, 1.0);
11561 if (result.status != ASTRO_SUCCESS)
11562 return ShadowError(result.status);
11563
11564 return EarthShadow(result.time);
11565}
11566
11567
11568static shadow_t PeakMoonShadow(astro_time_t search_center_time)
11569{
11570 /* Search for when the Moon's shadow axis is closest to the center of the Earth. */
11571
11572 astro_time_t t1, t2;
11573 astro_search_result_t result;
11574 const double window = 0.03; /* days before/after new moon to search for minimum shadow distance */
11575
11576 t1 = Astronomy_AddDays(search_center_time, -window);
11577 t2 = Astronomy_AddDays(search_center_time, +window);
11578
11579 result = Astronomy_Search(shadow_distance_slope, (void *)MoonShadow, t1, t2, 1.0);
11580 if (result.status != ASTRO_SUCCESS)
11581 return ShadowError(result.status);
11582
11583 return MoonShadow(result.time);
11584}
11585
11586
11587/** @cond DOXYGEN_SKIP */
11588typedef struct
11589{
11590 astro_body_t body;
11591 double planet_radius_km;
11592 double direction; /* used for transit start/finish search only */
11593}
11594planet_shadow_context_t;
11595/** @endcond */
11596
11597
11598static astro_func_result_t planet_shadow_distance_slope(void *context, astro_time_t time)
11599{
11600 const double dt = 1.0 / 86400.0;
11601 astro_time_t t1, t2;
11602 astro_func_result_t result;
11603 shadow_t shadow1, shadow2;
11604 const planet_shadow_context_t *p = (const planet_shadow_context_t *) context;
11605
11606 t1 = Astronomy_AddDays(time, -dt);
11607 t2 = Astronomy_AddDays(time, +dt);
11608
11609 shadow1 = PlanetShadow(p->body, p->planet_radius_km, t1);
11610 if (shadow1.status != ASTRO_SUCCESS)
11611 return FuncError(shadow1.status);
11612
11613 shadow2 = PlanetShadow(p->body, p->planet_radius_km, t2);
11614 if (shadow2.status != ASTRO_SUCCESS)
11615 return FuncError(shadow2.status);
11616
11617 result.value = (shadow2.r - shadow1.r) / dt;
11618 result.status = ASTRO_SUCCESS;
11619 return result;
11620}
11621
11622
11623static shadow_t PeakPlanetShadow(astro_body_t body, double planet_radius_km, astro_time_t search_center_time)
11624{
11625 /* Search for when the body's shadow is closest to the center of the Earth. */
11626
11627 astro_time_t t1, t2;
11628 astro_search_result_t result;
11629 planet_shadow_context_t context;
11630 const double window = 1.0; /* days before/after inferior conjunction to search for minimum shadow distance */
11631
11632 t1 = Astronomy_AddDays(search_center_time, -window);
11633 t2 = Astronomy_AddDays(search_center_time, +window);
11634
11635 context.body = body;
11636 context.planet_radius_km = planet_radius_km;
11637 context.direction = 0.0; /* not used in this search */
11638
11639 result = Astronomy_Search(planet_shadow_distance_slope, &context, t1, t2, 1.0);
11640 if (result.status != ASTRO_SUCCESS)
11641 return ShadowError(result.status);
11642
11643 return PlanetShadow(body, planet_radius_km, result.time);
11644}
11645
11646
11647static astro_func_result_t shadow_distance(void *context, astro_time_t time)
11648{
11649 astro_func_result_t result;
11650 const shadow_context_t *p = (const shadow_context_t *) context;
11651 shadow_t shadow = EarthShadow(time);
11652 if (shadow.status != ASTRO_SUCCESS)
11653 return FuncError(shadow.status);
11654
11655 result.value = p->direction * (shadow.r - p->radius_limit);
11656 result.status = ASTRO_SUCCESS;
11657 return result;
11658}
11659
11660
11661static double ShadowSemiDurationMinutes(astro_time_t center_time, double radius_limit, double window_minutes)
11662{
11663 /* Search backwards and forwards from the center time until shadow axis distance crosses radius limit. */
11664 double window = window_minutes / (24.0 * 60.0);
11665 shadow_context_t context;
11666 astro_search_result_t s1, s2;
11667 astro_time_t before, after;
11668
11669 before = Astronomy_AddDays(center_time, -window);
11670 after = Astronomy_AddDays(center_time, +window);
11671
11672 context.radius_limit = radius_limit;
11673 context.direction = -1.0;
11674 s1 = Astronomy_Search(shadow_distance, &context, before, center_time, 1.0);
11675
11676 context.direction = +1.0;
11677 s2 = Astronomy_Search(shadow_distance, &context, center_time, after, 1.0);
11678
11679 if (s1.status != ASTRO_SUCCESS || s2.status != ASTRO_SUCCESS)
11680 return -1.0; /* something went wrong! */
11681
11682 return (s2.time.ut - s1.time.ut) * ((24.0 * 60.0) / 2.0); /* convert days to minutes and average the semi-durations. */
11683}
11684
11685
11686/**
11687 * @brief Searches for a lunar eclipse.
11688 *
11689 * This function finds the first lunar eclipse that occurs after `startTime`.
11690 * A lunar eclipse may be penumbral, partial, or total.
11691 * See #astro_lunar_eclipse_t for more information.
11692 * To find a series of lunar eclipses, call this function once,
11693 * then keep calling #Astronomy_NextLunarEclipse as many times as desired,
11694 * passing in the `peak` value returned from the previous call.
11695 *
11696 * @param startTime
11697 * The date and time for starting the search for a lunar eclipse.
11698 *
11699 * @return
11700 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
11701 * and the remaining structure fields will be valid.
11702 * Any other value indicates an error.
11703 */
11704astro_lunar_eclipse_t Astronomy_SearchLunarEclipse(astro_time_t startTime)
11705{
11706 const double PruneLatitude = 1.8; /* full Moon's ecliptic latitude above which eclipse is impossible */
11707 astro_time_t fmtime;
11708 astro_lunar_eclipse_t eclipse;
11709 astro_search_result_t fullmoon;
11710 shadow_t shadow;
11711 int fmcount;
11712 double eclip_lat, eclip_lon, distance;
11713
11714 /* Iterate through consecutive full moons until we find any kind of lunar eclipse. */
11715 fmtime = startTime;
11716 for (fmcount=0; fmcount < 12; ++fmcount)
11717 {
11718 /* Search for the next full moon. Any eclipse will be near it. */
11719 fullmoon = Astronomy_SearchMoonPhase(180.0, fmtime, 40.0);
11720 if (fullmoon.status != ASTRO_SUCCESS)
11721 return LunarEclipseError(fullmoon.status);
11722
11723 /* Pruning: if the full Moon's ecliptic latitude is too large, a lunar eclipse is not possible. */
11724 CalcMoon(fullmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
11725 if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
11726 {
11727 /* Search near the full moon for the time when the center of the Moon */
11728 /* is closest to the line passing through the centers of the Sun and Earth. */
11729 shadow = PeakEarthShadow(fullmoon.time);
11730 if (shadow.status != ASTRO_SUCCESS)
11731 return LunarEclipseError(shadow.status);
11732
11733 if (shadow.r < shadow.p + MOON_MEAN_RADIUS_KM)
11734 {
11735 /* This is at least a penumbral eclipse. We will return a result. */
11736 eclipse.status = ASTRO_SUCCESS;
11737 eclipse.kind = ECLIPSE_PENUMBRAL;
11738 eclipse.obscuration = 0.0;
11739 eclipse.peak = shadow.time;
11740 eclipse.sd_total = 0.0;
11741 eclipse.sd_partial = 0.0;
11742 eclipse.sd_penum = ShadowSemiDurationMinutes(shadow.time, shadow.p + MOON_MEAN_RADIUS_KM, 200.0);
11743 if (eclipse.sd_penum <= 0.0)
11744 return LunarEclipseError(ASTRO_SEARCH_FAILURE);
11745
11746 if (shadow.r < shadow.k + MOON_MEAN_RADIUS_KM)
11747 {
11748 /* This is at least a partial eclipse. */
11749 eclipse.kind = ECLIPSE_PARTIAL;
11750 eclipse.sd_partial = ShadowSemiDurationMinutes(shadow.time, shadow.k + MOON_MEAN_RADIUS_KM, eclipse.sd_penum);
11751 if (eclipse.sd_partial <= 0.0)
11752 return LunarEclipseError(ASTRO_SEARCH_FAILURE);
11753
11754 if (shadow.r + MOON_MEAN_RADIUS_KM < shadow.k)
11755 {
11756 /* This is a total eclipse. */
11757 eclipse.kind = ECLIPSE_TOTAL;
11758 eclipse.obscuration = 1.0;
11759 eclipse.sd_total = ShadowSemiDurationMinutes(shadow.time, shadow.k - MOON_MEAN_RADIUS_KM, eclipse.sd_partial);
11760 if (eclipse.sd_total <= 0.0)
11761 return LunarEclipseError(ASTRO_SEARCH_FAILURE);
11762 }
11763 else
11764 {
11765 /* For lunar eclipses, we calculate the fraction of the Moon's disc covered by the Earth's umbra. */
11766 eclipse.obscuration = Obscuration(MOON_MEAN_RADIUS_KM, shadow.k, shadow.r);
11767 }
11768 }
11769 return eclipse;
11770 }
11771 }
11772
11773 /* We didn't find an eclipse on this full moon, so search for the next one. */
11774 fmtime = Astronomy_AddDays(fullmoon.time, 10.0);
11775 }
11776
11777 /* Safety valve to prevent infinite loop. */
11778 /* This should never happen, because at least 2 lunar eclipses happen per year. */
11779 return LunarEclipseError(ASTRO_INTERNAL_ERROR);
11780}
11781
11782/**
11783 * @brief Searches for the next lunar eclipse in a series.
11784 *
11785 * After using #Astronomy_SearchLunarEclipse to find the first lunar eclipse
11786 * in a series, you can call this function to find the next consecutive lunar eclipse.
11787 * Pass in the `peak` value from the #astro_lunar_eclipse_t returned by the
11788 * previous call to `Astronomy_SearchLunarEclipse` or `Astronomy_NextLunarEclipse`
11789 * to find the next lunar eclipse.
11790 *
11791 * @param prevEclipseTime
11792 * A date and time near a full moon. Lunar eclipse search will start at the next full moon.
11793 *
11794 * @return
11795 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
11796 * and the remaining structure fields will be valid.
11797 * Any other value indicates an error.
11798 */
11799astro_lunar_eclipse_t Astronomy_NextLunarEclipse(astro_time_t prevEclipseTime)
11800{
11801 astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
11802 return Astronomy_SearchLunarEclipse(startTime);
11803}
11804
11805
11806static astro_global_solar_eclipse_t GlobalSolarEclipseError(astro_status_t status)
11807{
11808 astro_global_solar_eclipse_t eclipse;
11809
11810 eclipse.status = status;
11811 eclipse.kind = ECLIPSE_NONE;
11812 eclipse.peak = TimeError();
11813 eclipse.obscuration = eclipse.distance = eclipse.latitude = eclipse.longitude = NAN;
11814
11815 return eclipse;
11816}
11817
11818/* The umbra radius tells us what kind of eclipse the observer sees. */
11819/* If the umbra radius is positive, this is a total eclipse. Otherwise, it's annular. */
11820/* HACK: I added a tiny bias (14 meters) to match Espenak test data. */
11821#define EclipseKindFromUmbra(k) (((k) > 0.014) ? ECLIPSE_TOTAL : ECLIPSE_ANNULAR)
11822
11823static double SolarEclipseObscuration(
11824 astro_vector_t hm, /* heliocentric Moon */
11825 astro_vector_t lo) /* lunacentric observer */
11826{
11827 astro_vector_t ho; /* heliocentric observer */
11828 astro_angle_result_t sun_moon_separation;
11829 double obscuration, sun_au, sun_radius, moon_radius;
11830
11831 /* Find heliocentric observer. */
11832 ho.status = ASTRO_SUCCESS;
11833 ho.t = lo.t;
11834 ho.x = hm.x + lo.x;
11835 ho.y = hm.y + lo.y;
11836 ho.z = hm.z + lo.z;
11837
11838 /* Find the distance from the Sun's center to the observer. */
11839 sun_au = Astronomy_VectorLength(ho);
11840
11841 /* Calculate the apparent angular radius of the Sun for the observer. */
11842 sun_radius = asin(SUN_RADIUS_AU / sun_au);
11843
11844 /* Calculate the apparent angular radius of the Moon for the observer. */
11845 moon_radius = asin(MOON_POLAR_RADIUS_AU / Astronomy_VectorLength(lo));
11846
11847 /* Calculate the apparent angular separation between the Sun's center and the Moon's center. */
11848 sun_moon_separation = Astronomy_AngleBetween(lo, ho);
11849
11850 if (sun_moon_separation.status != ASTRO_SUCCESS)
11851 return -1.0; /* internal error! should never happen. */
11852
11853 obscuration = Obscuration(sun_radius, moon_radius, sun_moon_separation.angle * DEG2RAD);
11854
11855 /* HACK: In marginal cases, we need to clamp obscuration to less than 1.0. */
11856 if (obscuration > 0.9999)
11857 obscuration = 0.9999;
11858
11859 return obscuration;
11860}
11861
11862static astro_global_solar_eclipse_t GeoidIntersect(shadow_t shadow)
11863{
11864 astro_global_solar_eclipse_t eclipse;
11865 astro_rotation_t rot, inv;
11866 astro_vector_t v, e, o;
11867 shadow_t surface;
11868 double A, B, C, radic, u, R;
11869 double px, py, pz, proj;
11870 double gast;
11871
11872 eclipse.status = ASTRO_SUCCESS;
11873 eclipse.kind = ECLIPSE_PARTIAL;
11874 eclipse.peak = shadow.time;
11875 eclipse.distance = shadow.r;
11876 eclipse.latitude = eclipse.longitude = NAN;
11877
11878 /*
11879 We want to calculate the intersection of the shadow axis with the Earth's geoid.
11880 First we must convert EQJ (equator of J2000) coordinates to EQD (equator of date)
11881 coordinates that are perfectly aligned with the Earth's equator at this
11882 moment in time.
11883 */
11884 rot = Astronomy_Rotation_EQJ_EQD(&shadow.time);
11885 if (rot.status != ASTRO_SUCCESS)
11886 return GlobalSolarEclipseError(rot.status);
11887
11888 v = Astronomy_RotateVector(rot, shadow.dir); /* shadow-axis vector in equator-of-date coordinates */
11889 if (v.status != ASTRO_SUCCESS)
11890 return GlobalSolarEclipseError(v.status);
11891
11892 e = Astronomy_RotateVector(rot, shadow.target); /* lunacentric Earth in equator-of-date coordinates */
11893 if (e.status != ASTRO_SUCCESS)
11894 return GlobalSolarEclipseError(e.status);
11895
11896 /*
11897 Convert all distances from AU to km.
11898 But dilate the z-coordinates so that the Earth becomes a perfect sphere.
11899 Then find the intersection of the vector with the sphere.
11900 See p 184 in Montenbruck & Pfleger's "Astronomy on the Personal Computer", second edition.
11901 */
11902 v.x *= KM_PER_AU;
11903 v.y *= KM_PER_AU;
11904 v.z *= KM_PER_AU / EARTH_FLATTENING;
11905
11906 e.x *= KM_PER_AU;
11907 e.y *= KM_PER_AU;
11908 e.z *= KM_PER_AU / EARTH_FLATTENING;
11909
11910 /*
11911 Solve the quadratic equation that finds whether and where
11912 the shadow axis intersects with the Earth in the dilated coordinate system.
11913 */
11914 R = EARTH_EQUATORIAL_RADIUS_KM;
11915 A = v.x*v.x + v.y*v.y + v.z*v.z;
11916 B = -2.0 * (v.x*e.x + v.y*e.y + v.z*e.z);
11917 C = (e.x*e.x + e.y*e.y + e.z*e.z) - R*R;
11918 radic = B*B - 4*A*C;
11919
11920 if (radic > 0.0)
11921 {
11922 /* Calculate the closer of the two intersection points. */
11923 /* This will be on the day side of the Earth. */
11924 u = (-B - sqrt(radic)) / (2 * A);
11925
11926 /* Convert lunacentric dilated coordinates to geocentric coordinates. */
11927 px = u*v.x - e.x;
11928 py = u*v.y - e.y;
11929 pz = (u*v.z - e.z) * EARTH_FLATTENING;
11930
11931 /* Convert cartesian coordinates into geodetic latitude/longitude. */
11932 proj = hypot(px, py) * (EARTH_FLATTENING * EARTH_FLATTENING);
11933 if (proj == 0.0)
11934 eclipse.latitude = (pz > 0.0) ? +90.0 : -90.0;
11935 else
11936 eclipse.latitude = RAD2DEG * atan(pz / proj);
11937
11938 /* Adjust longitude for Earth's rotation at the given UT. */
11939 gast = Astronomy_SiderealTime(&eclipse.peak);
11940 eclipse.longitude = fmod((RAD2DEG*atan2(py, px)) - (15*gast), 360.0);
11941 if (eclipse.longitude <= -180.0)
11942 eclipse.longitude += 360.0;
11943 else if (eclipse.longitude > +180.0)
11944 eclipse.longitude -= 360.0;
11945
11946 /* We want to determine whether the observer sees a total eclipse or an annular eclipse. */
11947 /* We need to perform a series of vector calculations... */
11948 /* Calculate the inverse rotation matrix, so we can convert EQD to EQJ. */
11949 inv = Astronomy_InverseRotation(rot);
11950 if (inv.status != ASTRO_SUCCESS)
11951 return GlobalSolarEclipseError(inv.status);
11952
11953 /* Put the EQD geocentric coordinates of the observer into the vector 'o'. */
11954 /* Also convert back from kilometers to astronomical units. */
11955 o.status = ASTRO_SUCCESS;
11956 o.t = shadow.time;
11957 o.x = px / KM_PER_AU;
11958 o.y = py / KM_PER_AU;
11959 o.z = pz / KM_PER_AU;
11960
11961 /* Rotate the observer's geocentric EQD back to the EQJ system. */
11962 o = Astronomy_RotateVector(inv, o);
11963
11964 /* Convert geocentric vector to lunacentric vector. */
11965 o.x += shadow.target.x;
11966 o.y += shadow.target.y;
11967 o.z += shadow.target.z;
11968
11969 /* Recalculate the shadow using a vector from the Moon's center toward the observer. */
11970 surface = CalcShadow(MOON_POLAR_RADIUS_KM, shadow.time, o, shadow.dir);
11971
11972 /* If we did everything right, the shadow distance should be very close to zero. */
11973 /* That's because we already determined the observer 'o' is on the shadow axis! */
11974 if (surface.r > 1.0e-9 || surface.r < 0.0)
11975 return GlobalSolarEclipseError(ASTRO_INTERNAL_ERROR);
11976
11977 eclipse.kind = EclipseKindFromUmbra(surface.k);
11978 if (eclipse.kind == ECLIPSE_TOTAL)
11979 eclipse.obscuration = 1.0;
11980 else
11981 eclipse.obscuration = SolarEclipseObscuration(shadow.dir, o);
11982 }
11983 else
11984 {
11985 /* This is a partial solar eclipse. It does not make practical sense to calculate obscuration. */
11986 /* Anyone who wants obscuration should use Astronomy_SearchLocalSolarEclipse for a specific location on the Earth. */
11987 eclipse.obscuration = NAN;
11988 }
11989
11990 return eclipse;
11991}
11992
11993
11994/**
11995 * @brief Searches for a solar eclipse visible anywhere on the Earth's surface.
11996 *
11997 * This function finds the first solar eclipse that occurs after `startTime`.
11998 * A solar eclipse may be partial, annular, or total.
11999 * See #astro_global_solar_eclipse_t for more information.
12000 * To find a series of solar eclipses, call this function once,
12001 * then keep calling #Astronomy_NextGlobalSolarEclipse as many times as desired,
12002 * passing in the `peak` value returned from the previous call.
12003 *
12004 * @param startTime
12005 * The date and time for starting the search for a solar eclipse.
12006 *
12007 * @return
12008 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
12009 * and the remaining structure fields are as described in #astro_global_solar_eclipse_t.
12010 * Any other value indicates an error.
12011 */
12012astro_global_solar_eclipse_t Astronomy_SearchGlobalSolarEclipse(astro_time_t startTime)
12013{
12014 const double PruneLatitude = 1.8; /* Moon's ecliptic latitude beyond which eclipse is impossible */
12015 astro_time_t nmtime;
12016 astro_search_result_t newmoon;
12017 shadow_t shadow;
12018 int nmcount;
12019 double eclip_lat, eclip_lon, distance;
12020
12021 /* Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth. */
12022 nmtime = startTime;
12023 for (nmcount=0; nmcount < 12; ++nmcount)
12024 {
12025 /* Search for the next new moon. Any eclipse will be near it. */
12026 newmoon = Astronomy_SearchMoonPhase(0.0, nmtime, 40.0);
12027 if (newmoon.status != ASTRO_SUCCESS)
12028 return GlobalSolarEclipseError(newmoon.status);
12029
12030 /* Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible. */
12031 CalcMoon(newmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
12032 if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
12033 {
12034 /* Search near the new moon for the time when the center of the Earth */
12035 /* is closest to the line passing through the centers of the Sun and Moon. */
12036 shadow = PeakMoonShadow(newmoon.time);
12037 if (shadow.status != ASTRO_SUCCESS)
12038 return GlobalSolarEclipseError(shadow.status);
12039
12040 if (shadow.r < shadow.p + EARTH_MEAN_RADIUS_KM)
12041 {
12042 /* This is at least a partial solar eclipse visible somewhere on Earth. */
12043 /* Try to find an intersection between the shadow axis and the Earth's oblate geoid. */
12044 return GeoidIntersect(shadow);
12045 }
12046 }
12047
12048 /* We didn't find an eclipse on this new moon, so search for the next one. */
12049 nmtime = Astronomy_AddDays(newmoon.time, 10.0);
12050 }
12051
12052 /* Safety valve to prevent infinite loop. */
12053 /* This should never happen, because at least 2 solar eclipses happen per year. */
12054 return GlobalSolarEclipseError(ASTRO_INTERNAL_ERROR);
12055}
12056
12057
12058/**
12059 * @brief Searches for the next global solar eclipse in a series.
12060 *
12061 * After using #Astronomy_SearchGlobalSolarEclipse to find the first solar eclipse
12062 * in a series, you can call this function to find the next consecutive solar eclipse.
12063 * Pass in the `peak` value from the #astro_global_solar_eclipse_t returned by the
12064 * previous call to `Astronomy_SearchGlobalSolarEclipse` or `Astronomy_NextGlobalSolarEclipse`
12065 * to find the next solar eclipse.
12066 *
12067 * @param prevEclipseTime
12068 * A date and time near a new moon. Solar eclipse search will start at the next new moon.
12069 *
12070 * @return
12071 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
12072 * and the remaining structure fields are as described in #astro_global_solar_eclipse_t.
12073 * Any other value indicates an error.
12074 */
12075astro_global_solar_eclipse_t Astronomy_NextGlobalSolarEclipse(astro_time_t prevEclipseTime)
12076{
12077 astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
12078 return Astronomy_SearchGlobalSolarEclipse(startTime);
12079}
12080
12081
12082static astro_eclipse_event_t EclipseEventError(void)
12083{
12084 astro_eclipse_event_t evt;
12085 evt.time = TimeError();
12086 evt.altitude = NAN;
12087 return evt;
12088}
12089
12090
12091static astro_local_solar_eclipse_t LocalSolarEclipseError(astro_status_t status)
12092{
12093 astro_local_solar_eclipse_t eclipse;
12094
12095 eclipse.status = status;
12096 eclipse.kind = ECLIPSE_NONE;
12097 eclipse.obscuration = NAN;
12098
12099 eclipse.partial_begin = EclipseEventError();
12100 eclipse.total_begin = EclipseEventError();
12101 eclipse.peak = EclipseEventError();
12102 eclipse.total_end = EclipseEventError();
12103 eclipse.partial_end = EclipseEventError();
12104
12105 return eclipse;
12106}
12107
12108
12109static shadow_t LocalMoonShadow(astro_time_t time, astro_observer_t observer)
12110{
12111 astro_vector_t s, o, m;
12112 double pos[3];
12113
12114 /* Calculate observer's geocentric position. */
12115 /* For efficiency, do this first, to populate the earth rotation parameters in 'time'. */
12116 /* That way they can be recycled instead of recalculated. */
12117 geo_pos(&time, observer, pos);
12118
12119 /* Calculate geocentric Sun with aberration correction. */
12120 s = Astronomy_GeoVector(BODY_SUN, time, ABERRATION);
12121 if (s.status != ASTRO_SUCCESS)
12122 return ShadowError(s.status);
12123
12124 m = Astronomy_GeoMoon(time); /* geocentric Moon */
12125
12126 /* Calculate lunacentric location of an observer on the Earth's surface. */
12127 o.status = m.status;
12128 o.x = pos[0] - m.x;
12129 o.y = pos[1] - m.y;
12130 o.z = pos[2] - m.z;
12131 o.t = m.t;
12132
12133 /* Convert geocentric moon to heliocentric Moon. */
12134 m.x -= s.x;
12135 m.y -= s.y;
12136 m.z -= s.z;
12137
12138 return CalcShadow(MOON_MEAN_RADIUS_KM, time, o, m);
12139}
12140
12141
12142static astro_func_result_t local_shadow_distance_slope(void *context, astro_time_t time)
12143{
12144 const double dt = 1.0 / 86400.0;
12145 astro_time_t t1, t2;
12146 astro_func_result_t result;
12147 shadow_t shadow1, shadow2;
12148 const astro_observer_t *observer = (const astro_observer_t *) context;
12149
12150 t1 = Astronomy_AddDays(time, -dt);
12151 t2 = Astronomy_AddDays(time, +dt);
12152
12153 shadow1 = LocalMoonShadow(t1, *observer);
12154 if (shadow1.status != ASTRO_SUCCESS)
12155 return FuncError(shadow1.status);
12156
12157 shadow2 = LocalMoonShadow(t2, *observer);
12158 if (shadow2.status != ASTRO_SUCCESS)
12159 return FuncError(shadow2.status);
12160
12161 result.value = (shadow2.r - shadow1.r) / dt;
12162 result.status = ASTRO_SUCCESS;
12163 return result;
12164}
12165
12166
12167static shadow_t PeakLocalMoonShadow(astro_time_t search_center_time, astro_observer_t observer)
12168{
12169 astro_time_t t1, t2;
12170 astro_search_result_t result;
12171 const double window = 0.2;
12172
12173 /*
12174 Search for the time near search_center_time that the Moon's shadow comes
12175 closest to the given observer.
12176 */
12177
12178 t1 = Astronomy_AddDays(search_center_time, -window);
12179 t2 = Astronomy_AddDays(search_center_time, +window);
12180
12181 result = Astronomy_Search(local_shadow_distance_slope, &observer, t1, t2, 1.0);
12182 if (result.status != ASTRO_SUCCESS)
12183 return ShadowError(result.status);
12184
12185 return LocalMoonShadow(result.time, observer);
12186}
12187
12188
12189static double local_partial_distance(const shadow_t *shadow)
12190{
12191 return shadow->p - shadow->r;
12192}
12193
12194static double local_total_distance(const shadow_t *shadow)
12195{
12196 /* Must take the absolute value of the umbra radius 'k' */
12197 /* because it can be negative for an annular eclipse. */
12198 return fabs(shadow->k) - shadow->r;
12199}
12200
12201/** @cond DOXYGEN_SKIP */
12202typedef double (* local_distance_func) (const shadow_t *shadow);
12203
12204typedef struct
12205{
12206 local_distance_func func;
12207 double direction;
12208 astro_observer_t observer;
12209}
12210eclipse_transition_t;
12211/* @endcond */
12212
12213
12214static astro_func_result_t local_eclipse_func(void *context, astro_time_t time)
12215{
12216 const eclipse_transition_t *trans = (const eclipse_transition_t *) context;
12217 shadow_t shadow;
12218 astro_func_result_t result;
12219
12220 shadow = LocalMoonShadow(time, trans->observer);
12221 if (shadow.status != ASTRO_SUCCESS)
12222 return FuncError(shadow.status);
12223
12224 result.value = trans->direction * trans->func(&shadow);
12225 result.status = ASTRO_SUCCESS;
12226 return result;
12227}
12228
12229
12230astro_func_result_t SunAltitude(
12231 astro_time_t time,
12232 astro_observer_t observer)
12233{
12234 astro_equatorial_t equ;
12235 astro_horizon_t hor;
12236 astro_func_result_t result;
12237
12238 equ = Astronomy_Equator(BODY_SUN, &time, observer, EQUATOR_OF_DATE, ABERRATION);
12239 if (equ.status != ASTRO_SUCCESS)
12240 return FuncError(equ.status);
12241
12242 hor = Astronomy_Horizon(&time, observer, equ.ra, equ.dec, REFRACTION_NORMAL);
12243 result.value = hor.altitude;
12244 result.status = ASTRO_SUCCESS;
12245 return result;
12246}
12247
12248
12249static astro_status_t CalcEvent(
12250 astro_observer_t observer,
12251 astro_time_t time,
12252 astro_eclipse_event_t *evt)
12253{
12254 astro_func_result_t result;
12255
12256 result = SunAltitude(time, observer);
12257 if (result.status != ASTRO_SUCCESS)
12258 {
12259 evt->time = TimeError();
12260 evt->altitude = NAN;
12261 return result.status;
12262 }
12263
12264 evt->time = time;
12265 evt->altitude = result.value;
12266 return ASTRO_SUCCESS;
12267}
12268
12269
12270static astro_status_t LocalEclipseTransition(
12271 astro_observer_t observer,
12272 double direction,
12273 local_distance_func func,
12274 astro_time_t t1,
12275 astro_time_t t2,
12276 astro_eclipse_event_t *evt)
12277{
12278 eclipse_transition_t trans;
12279 astro_search_result_t search;
12280
12281 trans.func = func;
12282 trans.direction = direction;
12283 trans.observer = observer;
12284
12285 search = Astronomy_Search(local_eclipse_func, &trans, t1, t2, 1.0);
12286 if (search.status != ASTRO_SUCCESS)
12287 {
12288 evt->time = TimeError();
12289 evt->altitude = NAN;
12290 return search.status;
12291 }
12292
12293 return CalcEvent(observer, search.time, evt);
12294}
12295
12296
12297static astro_local_solar_eclipse_t LocalEclipse(
12298 shadow_t shadow,
12299 astro_observer_t observer)
12300{
12301 const double PARTIAL_WINDOW = 0.2;
12302 const double TOTAL_WINDOW = 0.01;
12303 astro_local_solar_eclipse_t eclipse;
12304 astro_time_t t1, t2;
12305 astro_status_t status;
12306
12307 status = CalcEvent(observer, shadow.time, &eclipse.peak);
12308 if (status != ASTRO_SUCCESS)
12309 return LocalSolarEclipseError(status);
12310
12311 t1 = Astronomy_AddDays(shadow.time, -PARTIAL_WINDOW);
12312 t2 = Astronomy_AddDays(shadow.time, +PARTIAL_WINDOW);
12313
12314 status = LocalEclipseTransition(observer, +1.0, local_partial_distance, t1, shadow.time, &eclipse.partial_begin);
12315 if (status != ASTRO_SUCCESS)
12316 return LocalSolarEclipseError(status);
12317
12318 status = LocalEclipseTransition(observer, -1.0, local_partial_distance, shadow.time, t2, &eclipse.partial_end);
12319 if (status != ASTRO_SUCCESS)
12320 return LocalSolarEclipseError(status);
12321
12322 if (shadow.r < fabs(shadow.k)) /* take absolute value of 'k' to handle annular eclipses too. */
12323 {
12324 t1 = Astronomy_AddDays(shadow.time, -TOTAL_WINDOW);
12325 t2 = Astronomy_AddDays(shadow.time, +TOTAL_WINDOW);
12326
12327 status = LocalEclipseTransition(observer, +1.0, local_total_distance, t1, shadow.time, &eclipse.total_begin);
12328 if (status != ASTRO_SUCCESS)
12329 return LocalSolarEclipseError(status);
12330
12331 status = LocalEclipseTransition(observer, -1.0, local_total_distance, shadow.time, t2, &eclipse.total_end);
12332 if (status != ASTRO_SUCCESS)
12333 return LocalSolarEclipseError(status);
12334
12335 eclipse.kind = EclipseKindFromUmbra(shadow.k);
12336 if (eclipse.kind == ECLIPSE_TOTAL)
12337 eclipse.obscuration = 1.0;
12338 else
12339 eclipse.obscuration = SolarEclipseObscuration(shadow.dir, shadow.target);
12340 }
12341 else
12342 {
12343 eclipse.total_begin = eclipse.total_end = EclipseEventError();
12344 eclipse.kind = ECLIPSE_PARTIAL;
12345 eclipse.obscuration = SolarEclipseObscuration(shadow.dir, shadow.target);
12346 }
12347
12348 eclipse.status = ASTRO_SUCCESS;
12349 return eclipse;
12350}
12351
12352
12353/**
12354 * @brief Searches for a solar eclipse visible at a specific location on the Earth's surface.
12355 *
12356 * This function finds the first solar eclipse that occurs after `startTime`.
12357 * A solar eclipse may be partial, annular, or total.
12358 * See #astro_local_solar_eclipse_t for more information.
12359 * To find a series of solar eclipses, call this function once,
12360 * then keep calling #Astronomy_NextLocalSolarEclipse as many times as desired,
12361 * passing in the `peak` value returned from the previous call.
12362 *
12363 * IMPORTANT: An eclipse reported by this function might be partly or
12364 * completely invisible to the observer due to the time of day.
12365 *
12366 * @param startTime
12367 * The date and time for starting the search for a solar eclipse.
12368 *
12369 * @param observer
12370 * The geographic location of the observer.
12371 *
12372 * @return
12373 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
12374 * and the remaining structure fields are as described in #astro_local_solar_eclipse_t.
12375 * Any other value indicates an error.
12376 */
12377astro_local_solar_eclipse_t Astronomy_SearchLocalSolarEclipse(
12378 astro_time_t startTime,
12379 astro_observer_t observer)
12380{
12381 const double PruneLatitude = 1.8; /* Moon's ecliptic latitude beyond which eclipse is impossible */
12382 astro_time_t nmtime;
12383 astro_search_result_t newmoon;
12384 shadow_t shadow;
12385 double eclip_lat, eclip_lon, distance;
12386 astro_local_solar_eclipse_t eclipse;
12387
12388 /* Iterate through consecutive new moons until we find a solar eclipse visible somewhere on Earth. */
12389 nmtime = startTime;
12390 for(;;)
12391 {
12392 /* Search for the next new moon. Any eclipse will be near it. */
12393 newmoon = Astronomy_SearchMoonPhase(0.0, nmtime, 40.0);
12394 if (newmoon.status != ASTRO_SUCCESS)
12395 return LocalSolarEclipseError(newmoon.status);
12396
12397 /* Pruning: if the new moon's ecliptic latitude is too large, a solar eclipse is not possible. */
12398 CalcMoon(newmoon.time.tt / 36525.0, &eclip_lon, &eclip_lat, &distance);
12399 if (RAD2DEG * fabs(eclip_lat) < PruneLatitude)
12400 {
12401 /* Search near the new moon for the time when the observer */
12402 /* is closest to the line passing through the centers of the Sun and Moon. */
12403 shadow = PeakLocalMoonShadow(newmoon.time, observer);
12404 if (shadow.status != ASTRO_SUCCESS)
12405 return LocalSolarEclipseError(shadow.status);
12406
12407 if (shadow.r < shadow.p)
12408 {
12409 /* This is at least a partial solar eclipse for the observer. */
12410 eclipse = LocalEclipse(shadow, observer);
12411
12412 /* If any error occurs, something is really wrong and we should bail out. */
12413 if (eclipse.status != ASTRO_SUCCESS)
12414 return eclipse;
12415
12416 /* Ignore any eclipse that happens completely at night. */
12417 /* More precisely, the center of the Sun must be above the horizon */
12418 /* at the beginning or the end of the eclipse, or we skip the event. */
12419 if (eclipse.partial_begin.altitude > 0.0 || eclipse.partial_end.altitude > 0.0)
12420 return eclipse;
12421 }
12422 }
12423
12424 /* We didn't find an eclipse on this new moon, so search for the next one. */
12425 nmtime = Astronomy_AddDays(newmoon.time, 10.0);
12426 }
12427}
12428
12429
12430/**
12431 * @brief Searches for the next local solar eclipse in a series.
12432 *
12433 * After using #Astronomy_SearchLocalSolarEclipse to find the first solar eclipse
12434 * in a series, you can call this function to find the next consecutive solar eclipse.
12435 * Pass in the `peak` value from the #astro_local_solar_eclipse_t returned by the
12436 * previous call to `Astronomy_SearchLocalSolarEclipse` or `Astronomy_NextLocalSolarEclipse`
12437 * to find the next solar eclipse.
12438 *
12439 * @param prevEclipseTime
12440 * A date and time near a new moon. Solar eclipse search will start at the next new moon.
12441 *
12442 * @param observer
12443 * The geographic location of the observer.
12444 *
12445 * @return
12446 * If successful, the `status` field in the returned structure will contain `ASTRO_SUCCESS`
12447 * and the remaining structure fields are as described in #astro_local_solar_eclipse_t.
12448 * Any other value indicates an error.
12449 */
12450astro_local_solar_eclipse_t Astronomy_NextLocalSolarEclipse(
12451 astro_time_t prevEclipseTime,
12452 astro_observer_t observer)
12453{
12454 astro_time_t startTime = Astronomy_AddDays(prevEclipseTime, 10.0);
12455 return Astronomy_SearchLocalSolarEclipse(startTime, observer);
12456}
12457
12458
12459static astro_func_result_t planet_transit_bound(void *context, astro_time_t time)
12460{
12461 shadow_t shadow;
12462 astro_func_result_t result;
12463 const planet_shadow_context_t *p = (const planet_shadow_context_t *) context;
12464
12465 shadow = PlanetShadow(p->body, p->planet_radius_km, time);
12466 if (shadow.status != ASTRO_SUCCESS)
12467 return FuncError(shadow.status);
12468
12469 result.status = ASTRO_SUCCESS;
12470 result.value = p->direction * (shadow.r - shadow.p);
12471 return result;
12472}
12473
12474
12475static astro_search_result_t PlanetTransitBoundary(
12476 astro_body_t body,
12477 double planet_radius_km,
12478 astro_time_t t1,
12479 astro_time_t t2,
12480 double direction)
12481{
12482 /* Search for the time the planet's penumbra begins/ends making contact with the center of the Earth. */
12483 planet_shadow_context_t context;
12484
12485 context.body = body;
12486 context.planet_radius_km = planet_radius_km;
12487 context.direction = direction;
12488
12489 return Astronomy_Search(planet_transit_bound, &context, t1, t2, 1.0);
12490}
12491
12492
12493/**
12494 * @brief Searches for the first transit of Mercury or Venus after a given date.
12495 *
12496 * Finds the first transit of Mercury or Venus after a specified date.
12497 * A transit is when an inferior planet passes between the Sun and the Earth
12498 * so that the silhouette of the planet is visible against the Sun in the background.
12499 * To continue the search, pass the `finish` time in the returned structure to
12500 * #Astronomy_NextTransit.
12501 *
12502 * @param body
12503 * The planet whose transit is to be found. Must be `BODY_MERCURY` or `BODY_VENUS`.
12504 *
12505 * @param startTime
12506 * The date and time for starting the search for a transit.
12507 *
12508 * @return
12509 * If successful, the `status` field in the returned structure hold `ASTRO_SUCCESS`
12510 * and the other fields are as documented in #astro_transit_t.
12511 * Otherwise, `status` holds an error code and the other structure members are undefined.
12512 */
12513astro_transit_t Astronomy_SearchTransit(astro_body_t body, astro_time_t startTime)
12514{
12515 astro_time_t search_time;
12516 astro_transit_t transit;
12517 astro_search_result_t conj, search;
12518 astro_angle_result_t conj_separation, min_separation;
12519 shadow_t shadow;
12520 double planet_radius_km;
12521 astro_time_t tx;
12522 const double threshold_angle = 0.4; /* maximum angular separation to attempt transit calculation */
12523 const double dt_days = 1.0;
12524
12525 /* Validate the planet and find its mean radius. */
12526 switch (body)
12527 {
12528 case BODY_MERCURY: planet_radius_km = 2439.7; break;
12529 case BODY_VENUS: planet_radius_km = 6051.8; break;
12530 default:
12531 return TransitErr(ASTRO_INVALID_BODY);
12532 }
12533
12534 search_time = startTime;
12535 for(;;)
12536 {
12537 /*
12538 Search for the next inferior conjunction of the given planet.
12539 This is the next time the Earth and the other planet have the same
12540 ecliptic longitude as seen from the Sun.
12541 */
12542 conj = Astronomy_SearchRelativeLongitude(body, 0.0, search_time);
12543 if (conj.status != ASTRO_SUCCESS)
12544 return TransitErr(conj.status);
12545
12546 /* Calculate the angular separation between the body and the Sun at this time. */
12547 conj_separation = Astronomy_AngleFromSun(body, conj.time);
12548 if (conj_separation.status != ASTRO_SUCCESS)
12549 return TransitErr(conj_separation.status);
12550
12551 if (conj_separation.angle < threshold_angle)
12552 {
12553 /*
12554 The planet's angular separation from the Sun is small enough
12555 to consider it a transit candidate.
12556 Search for the moment when the line passing through the Sun
12557 and planet are closest to the Earth's center.
12558 */
12559 shadow = PeakPlanetShadow(body, planet_radius_km, conj.time);
12560 if (shadow.status != ASTRO_SUCCESS)
12561 return TransitErr(shadow.status);
12562
12563 if (shadow.r < shadow.p) /* does the planet's penumbra touch the Earth's center? */
12564 {
12565 /* Find the beginning and end of the penumbral contact. */
12566 tx = Astronomy_AddDays(shadow.time, -dt_days);
12567 search = PlanetTransitBoundary(body, planet_radius_km, tx, shadow.time, -1.0);
12568 if (search.status != ASTRO_SUCCESS)
12569 return TransitErr(search.status);
12570 transit.start = search.time;
12571
12572 tx = Astronomy_AddDays(shadow.time, +dt_days);
12573 search = PlanetTransitBoundary(body, planet_radius_km, shadow.time, tx, +1.0);
12574 if (search.status != ASTRO_SUCCESS)
12575 return TransitErr(search.status);
12576 transit.finish = search.time;
12577 transit.status = ASTRO_SUCCESS;
12578 transit.peak = shadow.time;
12579
12580 min_separation = Astronomy_AngleFromSun(body, shadow.time);
12581 if (min_separation.status != ASTRO_SUCCESS)
12582 return TransitErr(min_separation.status);
12583
12584 transit.separation = 60.0 * min_separation.angle; /* convert degrees to arcminutes */
12585 return transit;
12586 }
12587 }
12588
12589 /* This inferior conjunction was not a transit. Try the next inferior conjunction. */
12590 search_time = Astronomy_AddDays(conj.time, 10.0);
12591 }
12592}
12593
12594
12595/**
12596 * @brief Searches for another transit of Mercury or Venus.
12597 *
12598 * After calling #Astronomy_SearchTransit to find a transit of Mercury or Venus,
12599 * this function finds the next transit after that.
12600 * Keep calling this function as many times as you want to keep finding more transits.
12601 *
12602 * @param body
12603 * The planet whose transit is to be found. Must be `BODY_MERCURY` or `BODY_VENUS`.
12604 *
12605 * @param prevTransitTime
12606 * A date and time near the previous transit.
12607 *
12608 * @return
12609 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
12610 * and the other fields are as documented in #astro_transit_t.
12611 * Otherwise, `status` holds an error code and the other structure members are undefined.
12612 */
12613astro_transit_t Astronomy_NextTransit(astro_body_t body, astro_time_t prevTransitTime)
12614{
12615 astro_time_t startTime;
12616
12617 startTime = Astronomy_AddDays(prevTransitTime, 100.0);
12618 return Astronomy_SearchTransit(body, startTime);
12619}
12620
12621
12622static astro_node_event_t NodeError(astro_status_t status)
12623{
12624 astro_node_event_t node;
12625
12626 node.status = status;
12627 node.time = TimeError();
12628 node.kind = INVALID_NODE;
12629
12630 return node;
12631}
12632
12633static astro_func_result_t MoonNodeSearchFunc(void *context, astro_time_t time)
12634{
12635 astro_func_result_t result;
12636 astro_spherical_t eclip;
12637 astro_node_kind_t kind = *((astro_node_kind_t *)context);
12638
12639 eclip = Astronomy_EclipticGeoMoon(time);
12640
12641 result.value = eclip.lat * (double)kind;
12642 result.status = ASTRO_SUCCESS;
12643 return result;
12644}
12645
12646static const double MOON_NODE_STEP_DAYS = +10.0; /* a safe number of days to step without missing a Moon node */
12647
12648/**
12649 * @brief Searches for a time when the Moon's center crosses through the ecliptic plane.
12650 *
12651 * Searches for the first ascending or descending node of the Moon after `startTime`.
12652 * An ascending node is when the Moon's center passes through the ecliptic plane
12653 * (the plane of the Earth's orbit around the Sun) from south to north.
12654 * A descending node is when the Moon's center passes through the ecliptic plane
12655 * from north to south. Nodes indicate possible times of solar or lunar eclipses,
12656 * if the Moon also happens to be in the correct phase (new or full, respectively).
12657 *
12658 * Call `Astronomy_SearchMoonNode` to find the first of a series of nodes.
12659 * Then call #Astronomy_NextMoonNode to find as many more consecutive nodes as desired.
12660 *
12661 * @param startTime
12662 * The date and time for starting the search for an ascending or descending node of the Moon.
12663 *
12664 * @return
12665 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
12666 * and the other fields are as documented in #astro_node_event_t.
12667 * Otherwise, `status` holds an error code and the other structure members are undefined.
12668 */
12669astro_node_event_t Astronomy_SearchMoonNode(astro_time_t startTime)
12670{
12671 astro_node_event_t node;
12672 astro_time_t time1, time2;
12673 astro_spherical_t eclip1, eclip2;
12674 astro_node_kind_t kind;
12675 astro_search_result_t result;
12676
12677 /* Start at the given moment in time and sample the Moon's ecliptic latitude. */
12678 /* Step 10 days at a time, searching for an interval where that latitude crosses zero. */
12679 time1 = startTime;
12680 eclip1 = Astronomy_EclipticGeoMoon(time1); /* never returns a failure code */
12681
12682 for(;;)
12683 {
12684 time2 = Astronomy_AddDays(time1, MOON_NODE_STEP_DAYS);
12685 eclip2 = Astronomy_EclipticGeoMoon(time2); /* never returns a failure code */
12686 if (eclip1.lat * eclip2.lat <= 0.0)
12687 {
12688 /* There is a node somewhere inside this closed time interval. */
12689 /* Figure out whether it is an ascending node or a descending node. */
12690 kind = (eclip2.lat > eclip1.lat) ? ASCENDING_NODE : DESCENDING_NODE;
12691 result = Astronomy_Search(MoonNodeSearchFunc, &kind, time1, time2, 1.0);
12692 if (result.status != ASTRO_SUCCESS)
12693 return NodeError(result.status);
12694
12695 node.status = ASTRO_SUCCESS;
12696 node.time = result.time;
12697 node.kind = kind;
12698 return node;
12699 }
12700 time1 = time2;
12701 eclip1 = eclip2;
12702 }
12703}
12704
12705
12706/**
12707 * @brief Searches for the next time when the Moon's center crosses through the ecliptic plane.
12708 *
12709 * Call #Astronomy_SearchMoonNode to find the first of a series of nodes.
12710 * Then call `Astronomy_NextMoonNode` to find as many more consecutive nodes as desired.
12711 *
12712 * @param prevNode
12713 * The previous node found from calling #Astronomy_SearchMoonNode or `Astronomy_NextMoonNode`.
12714 *
12715 * @return
12716 * If successful, the `status` field in the returned structure holds `ASTRO_SUCCESS`
12717 * and the other fields are as documented in #astro_node_event_t.
12718 * Otherwise, `status` holds an error code and the other structure members are undefined.
12719 */
12720astro_node_event_t Astronomy_NextMoonNode(astro_node_event_t prevNode)
12721{
12722 astro_time_t time;
12723 astro_node_event_t node;
12724
12725 if (prevNode.status != ASTRO_SUCCESS)
12726 return NodeError(ASTRO_INVALID_PARAMETER);
12727
12728 if (prevNode.kind != ASCENDING_NODE && prevNode.kind != DESCENDING_NODE)
12729 return NodeError(ASTRO_INVALID_PARAMETER);
12730
12731 time = Astronomy_AddDays(prevNode.time, MOON_NODE_STEP_DAYS);
12732 node = Astronomy_SearchMoonNode(time);
12733 if (node.status == ASTRO_SUCCESS)
12734 {
12735 /* Verify nodes are alternating as expected. */
12736 if (prevNode.kind == ASCENDING_NODE)
12737 {
12738 if (node.kind != DESCENDING_NODE)
12739 return NodeError(ASTRO_INTERNAL_ERROR);
12740 }
12741 else
12742 {
12743 if (node.kind != ASCENDING_NODE)
12744 return NodeError(ASTRO_INTERNAL_ERROR);
12745 }
12746 }
12747 return node;
12748}
12749
12750
12751/**
12752 * @brief Frees up all dynamic memory allocated by Astronomy Engine.
12753 *
12754 * Astronomy Engine uses dynamic memory allocation in only one place:
12755 * it makes calculation of Pluto's orbit more efficient by caching 11 KB
12756 * segments and recycling them. To force purging this cache and
12757 * freeing all the dynamic memory, you can call this function at any time.
12758 * It is always safe to call, although it will slow down the very next
12759 * calculation of Pluto's position for a nearby time value.
12760 * Calling this function before your program exits is optional, but
12761 * it will be helpful for leak-checkers like valgrind.
12762 */
12763void Astronomy_Reset(void)
12764{
12765 int i;
12766 for (i=0; i < PLUTO_NUM_STATES-1; ++i)
12767 {
12768 free(pluto_cache[i]);
12769 pluto_cache[i] = NULL;
12770 }
12771}
12772
12773
12774static astro_axis_t EarthRotationAxis(astro_time_t *time)
12775{
12776 astro_axis_t axis;
12777 double pos1[3];
12778 double pos2[3];
12779 astro_equatorial_t equ;
12780
12781 if (time == NULL)
12782 return AxisErr(ASTRO_INVALID_PARAMETER, TimeError());
12783
12784 /*
12785 Unlike the other planets, we have a model of precession and nutation
12786 for the Earth's axis that provides a north pole vector.
12787 So calculate the vector first, then derive the (RA,DEC) angles from the vector.
12788 */
12789
12790 /* Start with a north pole vector in equator-of-date coordinates: (0,0,1). */
12791 pos1[0] = 0.0;
12792 pos1[1] = 0.0;
12793 pos1[2] = 1.0;
12794
12795 /* Convert the vector into J2000 coordinates. */
12796 nutation(pos1, time, INTO_2000, pos2);
12797 precession(pos2, *time, INTO_2000, pos1);
12798 axis.north.x = pos1[0];
12799 axis.north.y = pos1[1];
12800 axis.north.z = pos1[2];
12801 axis.north.t = *time;
12802 axis.north.status = ASTRO_SUCCESS;
12803
12804 /* Derive angular values: right ascension and declination. */
12805 equ = Astronomy_EquatorFromVector(axis.north);
12806 if (equ.status != ASTRO_SUCCESS)
12807 return AxisErr(equ.status, *time);
12808 axis.ra = equ.ra;
12809 axis.dec = equ.dec;
12810
12811 /* Use a modified version of the era() function that does not trim to 0..360 degrees. */
12812 /* This expression is also corrected to give the correct angle at the J2000 epoch. */
12813 axis.spin = 190.41375788700253 + (360.9856122880876 * time->ut);
12814
12815 axis.status = ASTRO_SUCCESS;
12816
12817 return axis;
12818}
12819
12820
12821/**
12822 * @brief Calculates information about a body's rotation axis at a given time.
12823 *
12824 * Calculates the orientation of a body's rotation axis, along with
12825 * the rotation angle of its prime meridian, at a given moment in time.
12826 *
12827 * This function uses formulas standardized by the IAU Working Group
12828 * on Cartographics and Rotational Elements 2015 report, as described
12829 * in the following document:
12830 *
12831 * https://astropedia.astrogeology.usgs.gov/download/Docs/WGCCRE/WGCCRE2015reprint.pdf
12832 *
12833 * See #astro_axis_t for more detailed information.
12834 *
12835 * @param body
12836 * The body whose rotation axis is to be found. The supported bodies are:
12837 * `BODY_SUN`, `BODY_MOON`, `BODY_MERCURY`, `BODY_VENUS`, `BODY_EARTH`, `BODY_MARS`,
12838 * `BODY_JUPITER`, `BODY_SATURN`, `BODY_URANUS`, `BODY_NEPTUNE`, `BODY_PLUTO`.
12839 *
12840 * @param time
12841 * The time for which the body's rotation axis is to be found.
12842 *
12843 * @return astro_axis_t
12844 */
12845astro_axis_t Astronomy_RotationAxis(astro_body_t body, astro_time_t *time)
12846{
12847 astro_axis_t axis;
12848 double ra, dec, w;
12849 double radlat, radlon, rcoslat;
12850 double Ja, Jb, Jc, Jd, Je, N;
12851 double E1, E2, E3, E4, E5, E6, E7, E8, E9, E10, E11, E12, E13;
12852 double d, T;
12853
12854 if (time == NULL)
12855 return AxisErr(ASTRO_INVALID_PARAMETER, TimeError());
12856
12857 d = time->tt;
12858 T = d / 36525.0;
12859
12860 switch (body)
12861 {
12862 case BODY_SUN:
12863 ra = 286.13;
12864 dec = 63.87;
12865 w = 84.176 + (14.1844 * d);
12866 break;
12867
12868 case BODY_MERCURY:
12869 ra = 281.0103 - (0.0328 * T);
12870 dec = 61.4155 - (0.0049 * T);
12871 w = (
12872 329.5988
12873 + (6.1385108 * d)
12874 + (0.01067257 * sin(DEG2RAD*(174.7910857 + 4.092335*d)))
12875 - (0.00112309 * sin(DEG2RAD*(349.5821714 + 8.184670*d)))
12876 - (0.00011040 * sin(DEG2RAD*(164.3732571 + 12.277005*d)))
12877 - (0.00002539 * sin(DEG2RAD*(339.1643429 + 16.369340*d)))
12878 - (0.00000571 * sin(DEG2RAD*(153.9554286 + 20.461675*d)))
12879 );
12880 break;
12881
12882 case BODY_VENUS:
12883 ra = 272.76;
12884 dec = 67.16;
12885 w = 160.20 - (1.4813688 * d);
12886 break;
12887
12888 case BODY_EARTH:
12889 return EarthRotationAxis(time);
12890
12891 case BODY_MOON:
12892 /*
12893 https://astropedia.astrogeology.usgs.gov/alfresco/d/d/workspace/SpacesStore/28fd9e81-1964-44d6-a58b-fbbf61e64e15/WGCCRE2009reprint.pdf
12894 Page 8, Table 2.
12895 */
12896 E1 = DEG2RAD * (125.045 - 0.0529921*d);
12897 E2 = DEG2RAD * (250.089 - 0.1059842*d);
12898 E3 = DEG2RAD * (260.008 + 13.0120009*d);
12899 E4 = DEG2RAD * (176.625 + 13.3407154*d);
12900 E5 = DEG2RAD * (357.529 + 0.9856003*d);
12901 E6 = DEG2RAD * (311.589 + 26.4057084*d);
12902 E7 = DEG2RAD * (134.963 + 13.0649930*d);
12903 E8 = DEG2RAD * (276.617 + 0.3287146*d);
12904 E9 = DEG2RAD * (34.226 + 1.7484877*d);
12905 E10 = DEG2RAD * (15.134 - 0.1589763*d);
12906 E11 = DEG2RAD * (119.743 + 0.0036096*d);
12907 E12 = DEG2RAD * (239.961 + 0.1643573*d);
12908 E13 = DEG2RAD * (25.053 + 12.9590088*d);
12909
12910 ra = (
12911 269.9949 + 0.0031*T
12912 - 3.8787*sin(E1)
12913 - 0.1204*sin(E2)
12914 + 0.0700*sin(E3)
12915 - 0.0172*sin(E4)
12916 + 0.0072*sin(E6)
12917 - 0.0052*sin(E10)
12918 + 0.0043*sin(E13)
12919 );
12920
12921 dec = (
12922 66.5392 + 0.0130*T
12923 + 1.5419*cos(E1)
12924 + 0.0239*cos(E2)
12925 - 0.0278*cos(E3)
12926 + 0.0068*cos(E4)
12927 - 0.0029*cos(E6)
12928 + 0.0009*cos(E7)
12929 + 0.0008*cos(E10)
12930 - 0.0009*cos(E13)
12931 );
12932
12933 w = (
12934 38.3213 + (13.17635815 - 1.4e-12*d)*d
12935 + 3.5610*sin(E1)
12936 + 0.1208*sin(E2)
12937 - 0.0642*sin(E3)
12938 + 0.0158*sin(E4)
12939 + 0.0252*sin(E5)
12940 - 0.0066*sin(E6)
12941 - 0.0047*sin(E7)
12942 - 0.0046*sin(E8)
12943 + 0.0028*sin(E9)
12944 + 0.0052*sin(E10)
12945 + 0.0040*sin(E11)
12946 + 0.0019*sin(E12)
12947 - 0.0044*sin(E13)
12948 );
12949 break;
12950
12951 case BODY_MARS:
12952 ra = (
12953 317.269202 - 0.10927547*T
12954 + 0.000068 * sin(DEG2RAD*(198.991226 + 19139.4819985*T))
12955 + 0.000238 * sin(DEG2RAD*(226.292679 + 38280.8511281*T))
12956 + 0.000052 * sin(DEG2RAD*(249.663391 + 57420.7251593*T))
12957 + 0.000009 * sin(DEG2RAD*(266.183510 + 76560.6367950*T))
12958 + 0.419057 * sin(DEG2RAD*(79.398797 + 0.5042615*T))
12959 );
12960
12961 dec = (
12962 54.432516 - 0.05827105*T
12963 + 0.000051*cos(DEG2RAD*(122.433576 + 19139.9407476*T))
12964 + 0.000141*cos(DEG2RAD*(43.058401 + 38280.8753272*T))
12965 + 0.000031*cos(DEG2RAD*(57.663379 + 57420.7517205*T))
12966 + 0.000005*cos(DEG2RAD*(79.476401 + 76560.6495004*T))
12967 + 1.591274*cos(DEG2RAD*(166.325722 + 0.5042615*T))
12968 );
12969
12970 w = (
12971 176.049863 + 350.891982443297*d
12972 + 0.000145*sin(DEG2RAD*(129.071773 + 19140.0328244*T))
12973 + 0.000157*sin(DEG2RAD*(36.352167 + 38281.0473591*T))
12974 + 0.000040*sin(DEG2RAD*(56.668646 + 57420.9295360*T))
12975 + 0.000001*sin(DEG2RAD*(67.364003 + 76560.2552215*T))
12976 + 0.000001*sin(DEG2RAD*(104.792680 + 95700.4387578*T))
12977 + 0.584542*sin(DEG2RAD*(95.391654 + 0.5042615*T))
12978 );
12979 break;
12980
12981 case BODY_JUPITER:
12982 Ja = DEG2RAD*(99.360714 + 4850.4046*T);
12983 Jb = DEG2RAD*(175.895369 + 1191.9605*T);
12984 Jc = DEG2RAD*(300.323162 + 262.5475*T);
12985 Jd = DEG2RAD*(114.012305 + 6070.2476*T);
12986 Je = DEG2RAD*(49.511251 + 64.3000*T);
12987
12988 ra = (
12989 268.056595 - 0.006499*T
12990 + 0.000117*sin(Ja)
12991 + 0.000938*sin(Jb)
12992 + 0.001432*sin(Jc)
12993 + 0.000030*sin(Jd)
12994 + 0.002150*sin(Je)
12995 );
12996
12997 dec = (
12998 64.495303 + 0.002413*T
12999 + 0.000050*cos(Ja)
13000 + 0.000404*cos(Jb)
13001 + 0.000617*cos(Jc)
13002 - 0.000013*cos(Jd)
13003 + 0.000926*cos(Je)
13004 );
13005
13006 w = 284.95 + 870.536*d;
13007 break;
13008
13009 case BODY_SATURN:
13010 ra = 40.589 - 0.036*T;
13011 dec = 83.537 - 0.004*T;
13012 w = 38.90 + 810.7939024*d;
13013 break;
13014
13015 case BODY_URANUS:
13016 ra = 257.311;
13017 dec = -15.175;
13018 w = 203.81 - 501.1600928*d;
13019 break;
13020
13021 case BODY_NEPTUNE:
13022 N = DEG2RAD*(357.85 + 52.316*T);
13023 ra = 299.36 + 0.70*sin(N);
13024 dec = 43.46 - 0.51*cos(N);
13025 w = 249.978 + 541.1397757*d - 0.48*sin(N);
13026 break;
13027
13028 case BODY_PLUTO:
13029 ra = 132.993;
13030 dec = -6.163;
13031 w = 302.695 + 56.3625225*d;
13032 break;
13033
13034 default:
13035 return AxisErr(ASTRO_INVALID_BODY, *time);
13036 }
13037
13038 axis.ra = ra / 15.0; /* convert degrees to sidereal hours */
13039 axis.dec = dec;
13040 axis.spin = w;
13041
13042 /* calculate the north pole vector using the given angles. */
13043 radlat = dec * DEG2RAD;
13044 radlon = ra * DEG2RAD;
13045 rcoslat = cos(radlat);
13046 axis.north.x = rcoslat * cos(radlon);
13047 axis.north.y = rcoslat * sin(radlon);
13048 axis.north.z = sin(radlat);
13049 axis.north.t = *time;
13050 axis.north.status = ASTRO_SUCCESS;
13051
13052 axis.status = ASTRO_SUCCESS;
13053 return axis;
13054}
13055
13056#ifdef __cplusplus
13057}
13058#endif