use crate::identity::BackendId;
use core::fmt;
use pleiades_types::{
Apparentness, CelestialBody, CoordinateFrame, CoordinateValidationError, EclipticCoordinates,
EquatorialCoordinates, Instant, Motion, MotionValidationError, ZodiacMode,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum QualityAnnotation {
Exact,
Interpolated,
Approximate,
Unknown,
}
impl QualityAnnotation {
pub const fn label(self) -> &'static str {
match self {
Self::Exact => "Exact",
Self::Interpolated => "Interpolated",
Self::Approximate => "Approximate",
Self::Unknown => "Unknown",
}
}
}
impl fmt::Display for QualityAnnotation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Debug, PartialEq)]
pub struct EphemerisResult {
pub backend_id: BackendId,
pub body: CelestialBody,
pub instant: Instant,
pub frame: CoordinateFrame,
pub zodiac_mode: ZodiacMode,
pub apparent: Apparentness,
pub ecliptic: Option<EclipticCoordinates>,
pub equatorial: Option<EquatorialCoordinates>,
pub motion: Option<Motion>,
pub quality: QualityAnnotation,
}
#[derive(Clone, Debug, PartialEq)]
pub enum EphemerisResultValidationError {
InvalidEcliptic(CoordinateValidationError),
InvalidEquatorial(CoordinateValidationError),
InvalidMotion(MotionValidationError),
}
impl fmt::Display for EphemerisResultValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEcliptic(error) => {
write!(f, "backend result ecliptic is invalid: {error}")
}
Self::InvalidEquatorial(error) => {
write!(f, "backend result equatorial is invalid: {error}")
}
Self::InvalidMotion(error) => write!(f, "backend result motion is invalid: {error}"),
}
}
}
impl std::error::Error for EphemerisResultValidationError {}
impl EphemerisResult {
pub fn new(
backend_id: BackendId,
body: CelestialBody,
instant: Instant,
frame: CoordinateFrame,
zodiac_mode: ZodiacMode,
apparent: Apparentness,
) -> Self {
Self {
backend_id,
body,
instant,
frame,
zodiac_mode,
apparent,
ecliptic: None,
equatorial: None,
motion: None,
quality: QualityAnnotation::Unknown,
}
}
pub fn validate(&self) -> Result<(), EphemerisResultValidationError> {
if let Some(ecliptic) = &self.ecliptic {
ecliptic
.validate()
.map_err(EphemerisResultValidationError::InvalidEcliptic)?;
}
if let Some(equatorial) = &self.equatorial {
equatorial
.validate()
.map_err(EphemerisResultValidationError::InvalidEquatorial)?;
}
if let Some(motion) = &self.motion {
motion
.validate()
.map_err(EphemerisResultValidationError::InvalidMotion)?;
}
Ok(())
}
pub fn summary_line(&self) -> String {
format!(
"backend={}; body={}; instant={}; frame={}; zodiac={}; apparent={}; quality={}; ecliptic={}; equatorial={}; motion={}",
self.backend_id,
self.body,
self.instant,
self.frame,
self.zodiac_mode,
self.apparent,
self.quality,
format_optional_ecliptic_coordinates(self.ecliptic.as_ref()),
format_optional_equatorial_coordinates(self.equatorial.as_ref()),
format_optional_motion(self.motion.as_ref()),
)
}
pub fn validated_summary_line(&self) -> Result<String, EphemerisResultValidationError> {
self.validate()?;
Ok(self.summary_line())
}
}
impl fmt::Display for EphemerisResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.summary_line())
}
}
pub(crate) fn format_optional_ecliptic_coordinates(value: Option<&EclipticCoordinates>) -> String {
value
.map(|coordinates| {
let distance = coordinates
.distance_au
.map(|distance| format!("{distance} AU"))
.unwrap_or_else(|| "n/a".to_string());
format!(
"longitude={}, latitude={}, distance={}",
coordinates.longitude, coordinates.latitude, distance
)
})
.unwrap_or_else(|| "absent".to_string())
}
pub(crate) fn format_optional_equatorial_coordinates(
value: Option<&EquatorialCoordinates>,
) -> String {
value
.map(|coordinates| {
let distance = coordinates
.distance_au
.map(|distance| format!("{distance} AU"))
.unwrap_or_else(|| "n/a".to_string());
format!(
"right_ascension={}, declination={}, distance={}",
coordinates.right_ascension, coordinates.declination, distance
)
})
.unwrap_or_else(|| "absent".to_string())
}
pub(crate) fn format_optional_motion(value: Option<&Motion>) -> String {
value
.map(|motion| {
let longitude_speed = motion
.longitude_deg_per_day
.map(|speed| format!("{speed} deg/day"))
.unwrap_or_else(|| "n/a".to_string());
let latitude_speed = motion
.latitude_deg_per_day
.map(|speed| format!("{speed} deg/day"))
.unwrap_or_else(|| "n/a".to_string());
let distance_speed = motion
.distance_au_per_day
.map(|speed| format!("{speed} AU/day"))
.unwrap_or_else(|| "n/a".to_string());
format!(
"longitude_speed={}, latitude_speed={}, distance_speed={}",
longitude_speed, latitude_speed, distance_speed
)
})
.unwrap_or_else(|| "absent".to_string())
}
#[cfg(test)]
#[path = "result_tests.rs"]
mod tests;