1use crate::identity::BackendId;
2use core::fmt;
3use pleiades_types::{
4 Apparentness, CelestialBody, CoordinateFrame, CoordinateValidationError, EclipticCoordinates,
5 EquatorialCoordinates, Instant, Motion, MotionValidationError, ZodiacMode,
6};
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11#[non_exhaustive]
12pub enum QualityAnnotation {
13 Exact,
15 Interpolated,
17 Approximate,
19 Unknown,
21}
22
23impl QualityAnnotation {
24 pub const fn label(self) -> &'static str {
26 match self {
27 Self::Exact => "Exact",
28 Self::Interpolated => "Interpolated",
29 Self::Approximate => "Approximate",
30 Self::Unknown => "Unknown",
31 }
32 }
33}
34
35impl fmt::Display for QualityAnnotation {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 f.write_str(self.label())
38 }
39}
40
41#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43#[derive(Clone, Debug, PartialEq)]
44pub struct EphemerisResult {
45 pub backend_id: BackendId,
47 pub body: CelestialBody,
49 pub instant: Instant,
51 pub frame: CoordinateFrame,
53 pub zodiac_mode: ZodiacMode,
55 pub apparent: Apparentness,
57 pub ecliptic: Option<EclipticCoordinates>,
59 pub equatorial: Option<EquatorialCoordinates>,
69 pub motion: Option<Motion>,
71 pub quality: QualityAnnotation,
73}
74
75#[derive(Clone, Debug, PartialEq)]
77pub enum EphemerisResultValidationError {
78 InvalidEcliptic(CoordinateValidationError),
80 InvalidEquatorial(CoordinateValidationError),
82 InvalidMotion(MotionValidationError),
84}
85
86impl fmt::Display for EphemerisResultValidationError {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::InvalidEcliptic(error) => {
90 write!(f, "backend result ecliptic is invalid: {error}")
91 }
92 Self::InvalidEquatorial(error) => {
93 write!(f, "backend result equatorial is invalid: {error}")
94 }
95 Self::InvalidMotion(error) => write!(f, "backend result motion is invalid: {error}"),
96 }
97 }
98}
99
100impl std::error::Error for EphemerisResultValidationError {}
101
102impl EphemerisResult {
103 pub fn new(
105 backend_id: BackendId,
106 body: CelestialBody,
107 instant: Instant,
108 frame: CoordinateFrame,
109 zodiac_mode: ZodiacMode,
110 apparent: Apparentness,
111 ) -> Self {
112 Self {
113 backend_id,
114 body,
115 instant,
116 frame,
117 zodiac_mode,
118 apparent,
119 ecliptic: None,
120 equatorial: None,
121 motion: None,
122 quality: QualityAnnotation::Unknown,
123 }
124 }
125
126 pub fn validate(&self) -> Result<(), EphemerisResultValidationError> {
128 if let Some(ecliptic) = &self.ecliptic {
129 ecliptic
130 .validate()
131 .map_err(EphemerisResultValidationError::InvalidEcliptic)?;
132 }
133
134 if let Some(equatorial) = &self.equatorial {
135 equatorial
136 .validate()
137 .map_err(EphemerisResultValidationError::InvalidEquatorial)?;
138 }
139
140 if let Some(motion) = &self.motion {
141 motion
142 .validate()
143 .map_err(EphemerisResultValidationError::InvalidMotion)?;
144 }
145
146 Ok(())
147 }
148
149 pub fn summary_line(&self) -> String {
155 format!(
156 "backend={}; body={}; instant={}; frame={}; zodiac={}; apparent={}; quality={}; ecliptic={}; equatorial={}; motion={}",
157 self.backend_id,
158 self.body,
159 self.instant,
160 self.frame,
161 self.zodiac_mode,
162 self.apparent,
163 self.quality,
164 format_optional_ecliptic_coordinates(self.ecliptic.as_ref()),
165 format_optional_equatorial_coordinates(self.equatorial.as_ref()),
166 format_optional_motion(self.motion.as_ref()),
167 )
168 }
169
170 pub fn validated_summary_line(&self) -> Result<String, EphemerisResultValidationError> {
172 self.validate()?;
173 Ok(self.summary_line())
174 }
175}
176
177impl fmt::Display for EphemerisResult {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 f.write_str(&self.summary_line())
180 }
181}
182
183pub(crate) fn format_optional_ecliptic_coordinates(value: Option<&EclipticCoordinates>) -> String {
184 value
185 .map(|coordinates| {
186 let distance = coordinates
187 .distance_au
188 .map(|distance| format!("{distance} AU"))
189 .unwrap_or_else(|| "n/a".to_string());
190
191 format!(
192 "longitude={}, latitude={}, distance={}",
193 coordinates.longitude, coordinates.latitude, distance
194 )
195 })
196 .unwrap_or_else(|| "absent".to_string())
197}
198
199pub(crate) fn format_optional_equatorial_coordinates(
200 value: Option<&EquatorialCoordinates>,
201) -> String {
202 value
203 .map(|coordinates| {
204 let distance = coordinates
205 .distance_au
206 .map(|distance| format!("{distance} AU"))
207 .unwrap_or_else(|| "n/a".to_string());
208
209 format!(
210 "right_ascension={}, declination={}, distance={}",
211 coordinates.right_ascension, coordinates.declination, distance
212 )
213 })
214 .unwrap_or_else(|| "absent".to_string())
215}
216
217pub(crate) fn format_optional_motion(value: Option<&Motion>) -> String {
218 value
219 .map(|motion| {
220 let longitude_speed = motion
221 .longitude_deg_per_day
222 .map(|speed| format!("{speed} deg/day"))
223 .unwrap_or_else(|| "n/a".to_string());
224 let latitude_speed = motion
225 .latitude_deg_per_day
226 .map(|speed| format!("{speed} deg/day"))
227 .unwrap_or_else(|| "n/a".to_string());
228 let distance_speed = motion
229 .distance_au_per_day
230 .map(|speed| format!("{speed} AU/day"))
231 .unwrap_or_else(|| "n/a".to_string());
232
233 format!(
234 "longitude_speed={}, latitude_speed={}, distance_speed={}",
235 longitude_speed, latitude_speed, distance_speed
236 )
237 })
238 .unwrap_or_else(|| "absent".to_string())
239}
240
241#[cfg(test)]
242#[path = "result_tests.rs"]
243mod tests;