Skip to main content

pleiades_types/
motion.rs

1//! Motion types: [`MotionDirection`], [`Motion`], and [`MotionValidationError`].
2
3use core::fmt;
4
5/// The coarse direction of longitudinal motion.
6#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
8pub enum MotionDirection {
9    /// Motion is prograde or direct.
10    Direct,
11    /// Motion is effectively stationary at the chosen precision.
12    Stationary,
13    /// Motion is retrograde.
14    Retrograde,
15}
16
17impl fmt::Display for MotionDirection {
18    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19        let label = match self {
20            Self::Direct => "Direct",
21            Self::Stationary => "Stationary",
22            Self::Retrograde => "Retrograde",
23        };
24        f.write_str(label)
25    }
26}
27
28/// Apparent motion data for a position sample.
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30#[derive(Clone, Copy, Debug, PartialEq)]
31pub struct Motion {
32    /// Longitude speed in degrees per day.
33    pub longitude_deg_per_day: Option<f64>,
34    /// Latitude speed in degrees per day.
35    pub latitude_deg_per_day: Option<f64>,
36    /// Distance speed in astronomical units per day.
37    pub distance_au_per_day: Option<f64>,
38}
39
40/// Errors returned when motion samples contain non-finite values.
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub enum MotionValidationError {
43    /// A motion component contained a non-finite value.
44    NonFiniteSpeed {
45        /// Motion field name.
46        field: &'static str,
47        /// Observed value.
48        value: f64,
49    },
50}
51
52impl fmt::Display for MotionValidationError {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Self::NonFiniteSpeed { field, value } => {
56                write!(f, "motion field `{field}` must be finite, got {value}")
57            }
58        }
59    }
60}
61
62impl std::error::Error for MotionValidationError {}
63
64impl Motion {
65    /// Creates a new motion sample.
66    pub const fn new(
67        longitude_deg_per_day: Option<f64>,
68        latitude_deg_per_day: Option<f64>,
69        distance_au_per_day: Option<f64>,
70    ) -> Self {
71        Self {
72            longitude_deg_per_day,
73            latitude_deg_per_day,
74            distance_au_per_day,
75        }
76    }
77
78    /// Returns the longitudinal motion speed when available.
79    pub const fn longitude_speed(self) -> Option<f64> {
80        self.longitude_deg_per_day
81    }
82
83    /// Returns the latitudinal motion speed when available.
84    pub const fn latitude_speed(self) -> Option<f64> {
85        self.latitude_deg_per_day
86    }
87
88    /// Returns the radial motion speed when available.
89    pub const fn distance_speed(self) -> Option<f64> {
90        self.distance_au_per_day
91    }
92
93    /// Returns a compact one-line summary of the motion sample.
94    pub fn summary_line(self) -> String {
95        let longitude = self
96            .longitude_speed()
97            .map(|value| format!("{value} deg/day"))
98            .unwrap_or_else(|| "n/a".to_string());
99        let latitude = self
100            .latitude_speed()
101            .map(|value| format!("{value} deg/day"))
102            .unwrap_or_else(|| "n/a".to_string());
103        let distance = self
104            .distance_speed()
105            .map(|value| format!("{value} au/day"))
106            .unwrap_or_else(|| "n/a".to_string());
107
108        format!("longitude={longitude}; latitude={latitude}; distance={distance}")
109    }
110
111    /// Validates that every populated motion component is finite.
112    pub fn validate(self) -> Result<(), MotionValidationError> {
113        for (field, value) in [
114            ("longitude_deg_per_day", self.longitude_deg_per_day),
115            ("latitude_deg_per_day", self.latitude_deg_per_day),
116            ("distance_au_per_day", self.distance_au_per_day),
117        ] {
118            if let Some(value) = value {
119                if !value.is_finite() {
120                    return Err(MotionValidationError::NonFiniteSpeed { field, value });
121                }
122            }
123        }
124
125        Ok(())
126    }
127
128    /// Returns the coarse longitudinal motion direction when that speed is available.
129    ///
130    /// The classification is sign-based: positive speed is direct, negative speed is retrograde,
131    /// and an exact zero speed is stationary. Non-finite longitudinal speeds are treated as
132    /// unknown until the sample is validated.
133    pub fn longitude_direction(self) -> Option<MotionDirection> {
134        let speed = self.longitude_speed()?;
135        if !speed.is_finite() {
136            return None;
137        }
138
139        Some(if speed > 0.0 {
140            MotionDirection::Direct
141        } else if speed < 0.0 {
142            MotionDirection::Retrograde
143        } else {
144            MotionDirection::Stationary
145        })
146    }
147}
148
149impl fmt::Display for Motion {
150    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151        f.write_str(&self.summary_line())
152    }
153}