Skip to main content

sidereon_core/astro/
apparent.rs

1//! Shared apparent-place reducers for body observations and almanac events.
2
3use crate::astro::almanac::{AlmanacError, EphemerisSource};
4use crate::astro::bodies::observe::{
5    apparent_geocentric_analytic_true_of_date_m, apparent_geocentric_spk_true_of_date_m,
6    observe_with_time_scales, ObserveError, ObserveOptions, Target,
7};
8use crate::astro::frames::transforms::{FrameTransformError, GeodeticStationKm};
9use crate::astro::time::scales::TimeScales;
10
11const NAIF_SUN: i32 = 10;
12const NAIF_MOON: i32 = 301;
13
14/// Apparent topocentric right ascension and declination of a body.
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct RaDec {
17    /// Right ascension in degrees on `[0, 360)`.
18    pub right_ascension_deg: f64,
19    /// Declination in degrees on `[-90, 90]`.
20    pub declination_deg: f64,
21    /// Distance from the observer, kilometres.
22    pub distance_km: f64,
23}
24
25/// Apparent topocentric reduction used by meridian transit finders.
26#[derive(Debug, Clone, Copy, PartialEq)]
27pub struct TopocentricApparent {
28    /// Topocentric apparent right ascension and declination.
29    pub radec: RaDec,
30    /// Local apparent hour angle in degrees on `(-180, 180]`.
31    pub hour_angle_deg: f64,
32    /// Topocentric apparent geometric altitude, degrees, without refraction.
33    pub altitude_deg: f64,
34}
35
36/// Geocentric apparent position of a body, true equator and equinox of date, metres.
37pub fn apparent_geocentric(
38    target_naif: i32,
39    ts: &TimeScales,
40    source: EphemerisSource<'_>,
41) -> Result<[f64; 3], AlmanacError> {
42    match source {
43        EphemerisSource::Spk(kernel) => {
44            apparent_geocentric_spk_true_of_date_m(target_naif, ts, kernel).map_err(map_observe)
45        }
46        EphemerisSource::Analytic => match target_naif {
47            NAIF_SUN | NAIF_MOON => {
48                apparent_geocentric_analytic_true_of_date_m(target_naif, ts).map_err(map_observe)
49            }
50            _ => Err(AlmanacError::EphemerisRequired),
51        },
52    }
53}
54
55/// Topocentric apparent right ascension, declination, hour angle, and altitude.
56pub fn topocentric_apparent(
57    target_naif: i32,
58    station: &GeodeticStationKm,
59    ts: &TimeScales,
60    source: EphemerisSource<'_>,
61) -> Result<TopocentricApparent, AlmanacError> {
62    let target = match source {
63        EphemerisSource::Spk(kernel) => Target::Spk {
64            kernel,
65            naif_id: target_naif,
66        },
67        EphemerisSource::Analytic => match target_naif {
68            NAIF_SUN => Target::Sun,
69            NAIF_MOON => Target::Moon,
70            _ => return Err(AlmanacError::EphemerisRequired),
71        },
72    };
73    let observation = observe_with_time_scales(station, ts, target, ObserveOptions::default())
74        .map_err(map_observe)?;
75    Ok(TopocentricApparent {
76        radec: RaDec {
77            right_ascension_deg: observation.apparent.right_ascension_deg,
78            declination_deg: observation.apparent.declination_deg,
79            distance_km: observation.apparent.distance_km,
80        },
81        hour_angle_deg: observation.hour_angle_deg,
82        altitude_deg: observation.horizontal.elevation_deg,
83    })
84}
85
86fn map_observe(error: ObserveError) -> AlmanacError {
87    match error {
88        ObserveError::Spk(error) => AlmanacError::Spk(error),
89        ObserveError::FrameTransform(FrameTransformError::InvalidInput { field, reason }) => {
90            AlmanacError::InvalidInput { field, reason }
91        }
92        ObserveError::SunMoon(_) => AlmanacError::Frame("sun_moon"),
93        ObserveError::Angle(_) => AlmanacError::Frame("angle"),
94        ObserveError::UnsupportedSpkFrame { .. } => AlmanacError::Frame("spk_frame"),
95        ObserveError::NonFinite => AlmanacError::InvalidInput {
96            field: "geometry",
97            reason: "must be finite",
98        },
99        ObserveError::DegenerateGeometry => AlmanacError::InvalidInput {
100            field: "geometry",
101            reason: "degenerate",
102        },
103    }
104}