1use std::cell::RefCell;
4use std::fmt;
5
6use crate::astro::apparent::apparent_geocentric;
7use crate::astro::events::{EventFinder, EventFinderError};
8use crate::astro::frames::transforms::{geodetic_to_itrs, FrameTransformError, GeodeticStationKm};
9use crate::astro::math::vec3::{dot3, norm3};
10use crate::astro::passes::UtcInstant;
11use crate::astro::spk::{Spk, SpkError};
12use crate::astro::{
13 constants::{
14 time::{SECONDS_PER_DAY, SECONDS_PER_HOUR},
15 units::MICROSECONDS_PER_SECOND,
16 },
17 events::CrossingEvent,
18};
19use crate::validate;
20
21mod eclipse;
22mod ecliptic;
23mod phases;
24mod planets;
25mod seasons;
26#[cfg(test)]
27mod tests;
28mod transits;
29
30pub use eclipse::lunar_solar_eclipses;
31pub use ecliptic::{geocentric_ecliptic, EclipticLonLat};
32pub use phases::{moon_phase_deg, moon_phases};
33pub use planets::planetary_events;
34pub use seasons::seasons;
35pub use transits::meridian_transits;
36
37pub(crate) const NAIF_SUN: i32 = 10;
38pub(crate) const NAIF_MOON: i32 = 301;
39
40pub(crate) const SEASON_PLANET_STEP_MAX_SECONDS: f64 = SECONDS_PER_DAY;
41pub(crate) const PHASE_STEP_MAX_SECONDS: f64 = 3.0 * SECONDS_PER_DAY;
42pub(crate) const TRANSIT_STEP_MAX_SECONDS: f64 = SECONDS_PER_HOUR;
43
44#[derive(Clone, Copy)]
46pub enum EphemerisSource<'a> {
47 Spk(&'a Spk),
49 Analytic,
51}
52
53#[non_exhaustive]
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum SeasonKind {
57 MarchEquinox,
58 JuneSolstice,
59 SeptemberEquinox,
60 DecemberSolstice,
61}
62
63#[non_exhaustive]
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum MoonPhaseKind {
67 New,
68 FirstQuarter,
69 Full,
70 LastQuarter,
71}
72
73#[non_exhaustive]
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum PlanetaryEventKind {
77 Conjunction,
78 Opposition,
79}
80
81#[non_exhaustive]
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum CulminationKind {
85 Upper,
86 Lower,
87}
88
89#[non_exhaustive]
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub enum EclipseKind {
93 LunarPenumbral,
94 LunarPartial,
95 LunarTotal,
96 SolarPartial,
97 SolarAnnular,
98 SolarTotal,
99 SolarHybrid,
100}
101
102#[non_exhaustive]
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Planet {
106 Mercury,
107 Venus,
108 Mars,
109 Jupiter,
110 Saturn,
111 Uranus,
112 Neptune,
113}
114
115#[non_exhaustive]
117#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum TransitBody {
119 Sun,
120 Moon,
121 Planet(Planet),
122}
123
124#[non_exhaustive]
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub struct SeasonEvent {
128 pub time: UtcInstant,
129 pub kind: SeasonKind,
130}
131
132#[non_exhaustive]
134#[derive(Debug, Clone, Copy, PartialEq, Eq)]
135pub struct MoonPhaseEvent {
136 pub time: UtcInstant,
137 pub kind: MoonPhaseKind,
138}
139
140#[non_exhaustive]
142#[derive(Debug, Clone, Copy, PartialEq)]
143pub struct PlanetaryEvent {
144 pub time: UtcInstant,
145 pub planet: Planet,
146 pub kind: PlanetaryEventKind,
147 pub elongation_deg: f64,
148}
149
150#[non_exhaustive]
152#[derive(Debug, Clone, Copy, PartialEq)]
153pub struct CulminationEvent {
154 pub time: UtcInstant,
155 pub kind: CulminationKind,
156 pub altitude_deg: f64,
157}
158
159#[non_exhaustive]
161#[derive(Debug, Clone, Copy, PartialEq)]
162pub struct EclipseEvent {
163 pub time_maximum: UtcInstant,
164 pub kind: EclipseKind,
165 pub magnitude: f64,
166 pub moon_latitude_deg: f64,
167 pub gamma: f64,
168 pub uncertain: bool,
169}
170
171#[non_exhaustive]
173#[derive(Debug, Clone, PartialEq)]
174pub enum AlmanacError {
175 Finder(EventFinderError),
176 Spk(SpkError),
177 Frame(&'static str),
178 EphemerisRequired,
179 InferiorPlanetOpposition,
180 InvalidInput {
181 field: &'static str,
182 reason: &'static str,
183 },
184}
185
186impl fmt::Display for AlmanacError {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 match self {
189 Self::Finder(error) => write!(f, "event finder failed: {error}"),
190 Self::Spk(error) => write!(f, "SPK failed: {error}"),
191 Self::Frame(label) => write!(f, "frame reduction failed: {label}"),
192 Self::EphemerisRequired => write!(f, "SPK ephemeris is required"),
193 Self::InferiorPlanetOpposition => {
194 write!(f, "opposition is not defined for an inferior planet")
195 }
196 Self::InvalidInput { field, reason } => {
197 write!(f, "invalid almanac input {field}: {reason}")
198 }
199 }
200 }
201}
202
203impl std::error::Error for AlmanacError {}
204
205pub(crate) fn validate_scan_controls(
206 step_seconds: f64,
207 time_tolerance_seconds: f64,
208 step_max_seconds: f64,
209) -> Result<(), AlmanacError> {
210 validate::positive_step(step_seconds, "step_seconds").map_err(map_field_error)?;
211 validate::positive_step(time_tolerance_seconds, "time_tolerance_seconds")
212 .map_err(map_field_error)?;
213 if step_seconds > step_max_seconds {
214 return Err(AlmanacError::InvalidInput {
215 field: "step_seconds",
216 reason: "exceeds maximum",
217 });
218 }
219 Ok(())
220}
221
222pub(crate) fn validate_station(station: &GeodeticStationKm) -> Result<(), AlmanacError> {
223 geodetic_to_itrs(
224 station.latitude_deg,
225 station.longitude_deg,
226 station.altitude_km,
227 )
228 .map(|_| ())
229 .map_err(map_frame_input)
230}
231
232pub(crate) fn event_finder(
233 start: UtcInstant,
234 end: UtcInstant,
235 step_seconds: f64,
236 time_tolerance_seconds: f64,
237) -> Result<EventFinder, AlmanacError> {
238 EventFinder::new(
239 0.0,
240 seconds_between(start, end)?,
241 step_seconds,
242 time_tolerance_seconds,
243 )
244 .map_err(AlmanacError::Finder)
245}
246
247pub(crate) fn seconds_between(start: UtcInstant, end: UtcInstant) -> Result<f64, AlmanacError> {
248 let span = end
249 .unix_microseconds()
250 .checked_sub(start.unix_microseconds())
251 .ok_or(AlmanacError::Finder(EventFinderError::InvalidInput {
252 field: "time_window",
253 reason: "start/end span overflows i64 microseconds",
254 }))?;
255 Ok(span as f64 / MICROSECONDS_PER_SECOND)
256}
257
258pub(crate) fn instant_at_offset_seconds(start: UtcInstant, offset_seconds: f64) -> UtcInstant {
259 UtcInstant::from_unix_microseconds(
260 start.unix_microseconds() + (offset_seconds * MICROSECONDS_PER_SECOND).floor() as i64,
261 )
262}
263
264pub(crate) fn offset_instant(start: UtcInstant, offset_seconds: f64) -> UtcInstant {
265 instant_at_offset_seconds(start, offset_seconds)
266}
267
268pub(crate) fn body_ecliptic(
269 source: EphemerisSource<'_>,
270 target_naif: i32,
271 time: UtcInstant,
272) -> Result<EclipticLonLat, AlmanacError> {
273 let ts = time.time_scales();
274 let pos = apparent_geocentric(target_naif, &ts, source)?;
275 geocentric_ecliptic(pos, &ts)
276}
277
278pub(crate) fn apparent_km(
279 source: EphemerisSource<'_>,
280 target_naif: i32,
281 time: UtcInstant,
282) -> Result<[f64; 3], AlmanacError> {
283 let pos_m = apparent_geocentric(target_naif, &time.time_scales(), source)?;
284 Ok([pos_m[0] * 1.0e-3, pos_m[1] * 1.0e-3, pos_m[2] * 1.0e-3])
285}
286
287pub(crate) fn find_angle_crossing_times<F>(
288 start: UtcInstant,
289 end: UtcInstant,
290 step_seconds: f64,
291 time_tolerance_seconds: f64,
292 target_deg: f64,
293 angle_fn: F,
294) -> Result<Vec<UtcInstant>, AlmanacError>
295where
296 F: Fn(UtcInstant) -> Result<f64, AlmanacError>,
297{
298 let finder = event_finder(start, end, step_seconds, time_tolerance_seconds)?;
299 let latch = RefCell::new(None);
300 let crossings = finder
301 .find_crossings(
302 |offset_seconds| {
303 latch_scalar(&latch, || {
304 let time = instant_at_offset_seconds(start, offset_seconds);
305 let angle = angle_fn(time)?;
306 Ok((angle - target_deg).to_radians().sin())
307 })
308 },
309 0.0,
310 )
311 .map_err(|error| latched_or_finder(error, &latch))?;
312
313 let mut times = Vec::new();
314 for crossing in crossings {
315 let time = instant_at_offset_seconds(start, crossing.time_seconds);
316 let angle = angle_fn(time)?;
317 if (angle - target_deg).to_radians().cos() > 0.0 {
318 times.push(time);
319 }
320 }
321 Ok(times)
322}
323
324pub(crate) fn latch_scalar<F>(latch: &RefCell<Option<AlmanacError>>, f: F) -> f64
325where
326 F: FnOnce() -> Result<f64, AlmanacError>,
327{
328 match f() {
329 Ok(value) if value.is_finite() => value,
330 Ok(_) => {
331 latch_error(
332 latch,
333 AlmanacError::InvalidInput {
334 field: "predicate",
335 reason: "not finite",
336 },
337 );
338 f64::NAN
339 }
340 Err(error) => {
341 latch_error(latch, error);
342 f64::NAN
343 }
344 }
345}
346
347pub(crate) fn latched_or_finder(
348 error: EventFinderError,
349 latch: &RefCell<Option<AlmanacError>>,
350) -> AlmanacError {
351 latch
352 .borrow()
353 .clone()
354 .unwrap_or(AlmanacError::Finder(error))
355}
356
357pub(crate) fn latch_error(latch: &RefCell<Option<AlmanacError>>, error: AlmanacError) {
358 if latch.borrow().is_none() {
359 *latch.borrow_mut() = Some(error);
360 }
361}
362
363pub(crate) fn crossing_time(start: UtcInstant, crossing: CrossingEvent) -> UtcInstant {
364 instant_at_offset_seconds(start, crossing.time_seconds)
365}
366
367pub(crate) fn planet_naif(planet: Planet) -> i32 {
368 match planet {
369 Planet::Mercury => 1,
370 Planet::Venus => 2,
371 Planet::Mars => 4,
372 Planet::Jupiter => 5,
373 Planet::Saturn => 6,
374 Planet::Uranus => 7,
375 Planet::Neptune => 8,
376 }
377}
378
379pub(crate) fn transit_body_naif(body: TransitBody) -> i32 {
380 match body {
381 TransitBody::Sun => NAIF_SUN,
382 TransitBody::Moon => NAIF_MOON,
383 TransitBody::Planet(planet) => planet_naif(planet),
384 }
385}
386
387pub(crate) fn is_inferior(planet: Planet) -> bool {
388 matches!(planet, Planet::Mercury | Planet::Venus)
389}
390
391pub(crate) fn wrap360(degrees: f64) -> f64 {
392 degrees.rem_euclid(360.0)
393}
394
395pub(crate) fn angular_separation_rad(a: [f64; 3], b: [f64; 3]) -> Result<f64, AlmanacError> {
396 let na = norm_checked(a, "a")?;
397 let nb = norm_checked(b, "b")?;
398 let cos_sep = (dot3(a, b) / (na * nb)).clamp(-1.0, 1.0);
399 Ok(cos_sep.acos())
400}
401
402pub(crate) fn norm_checked(vector: [f64; 3], field: &'static str) -> Result<f64, AlmanacError> {
403 if vector.iter().any(|value| !value.is_finite()) {
404 return Err(AlmanacError::InvalidInput {
405 field,
406 reason: "components must be finite",
407 });
408 }
409 let norm = norm3(vector);
410 if !norm.is_finite() {
411 return Err(AlmanacError::InvalidInput {
412 field,
413 reason: "norm must be finite",
414 });
415 }
416 if norm == 0.0 {
417 return Err(AlmanacError::InvalidInput {
418 field,
419 reason: "degenerate",
420 });
421 }
422 Ok(norm)
423}
424
425pub(crate) fn map_field_error(error: validate::FieldError) -> AlmanacError {
426 AlmanacError::InvalidInput {
427 field: error.field(),
428 reason: error.reason(),
429 }
430}
431
432fn map_frame_input(error: FrameTransformError) -> AlmanacError {
433 let FrameTransformError::InvalidInput { field, reason } = error;
434 AlmanacError::InvalidInput { field, reason }
435}