Skip to main content

pleiades_types/
observer.rs

1//! Observer location types: [`ObserverLocation`] and [`ObserverLocationValidationError`].
2
3use core::fmt;
4
5use crate::angles::{Latitude, Longitude};
6
7/// A geographic observer location.
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
9#[derive(Clone, Debug, PartialEq)]
10pub struct ObserverLocation {
11    /// Geographic latitude, north of the equator positive (degrees).
12    pub latitude: Latitude,
13    /// Geographic longitude, expressed in degrees east of Greenwich.
14    pub longitude: Longitude,
15    /// Optional elevation above sea level in meters.
16    pub elevation_m: Option<f64>,
17}
18
19impl ObserverLocation {
20    /// Creates a new observer location.
21    pub const fn new(latitude: Latitude, longitude: Longitude, elevation_m: Option<f64>) -> Self {
22        Self {
23            latitude,
24            longitude,
25            elevation_m,
26        }
27    }
28
29    /// Validates that the stored observer location is finite and within the
30    /// latitude range expected by house calculations.
31    pub fn validate(&self) -> Result<(), ObserverLocationValidationError> {
32        let latitude = self.latitude.degrees();
33        if !latitude.is_finite() {
34            return Err(ObserverLocationValidationError::NonFiniteLatitude { value: latitude });
35        }
36        if !(-90.0..=90.0).contains(&latitude) {
37            return Err(ObserverLocationValidationError::LatitudeOutOfRange { value: latitude });
38        }
39
40        let longitude = self.longitude.degrees();
41        if !longitude.is_finite() {
42            return Err(ObserverLocationValidationError::NonFiniteLongitude { value: longitude });
43        }
44
45        if let Some(elevation_m) = self.elevation_m {
46            if !elevation_m.is_finite() {
47                return Err(ObserverLocationValidationError::NonFiniteElevation {
48                    value: elevation_m,
49                });
50            }
51        }
52
53        Ok(())
54    }
55
56    /// Returns a compact one-line rendering of the observer location.
57    pub fn summary_line(&self) -> String {
58        let elevation = self
59            .elevation_m
60            .map(|value| format!("{value:.3} m"))
61            .unwrap_or_else(|| "n/a".to_string());
62
63        format!(
64            "latitude={}, longitude={}, elevation={}",
65            self.latitude, self.longitude, elevation
66        )
67    }
68
69    /// Returns a compact one-line rendering after validating the stored data.
70    pub fn validated_summary_line(&self) -> Result<String, ObserverLocationValidationError> {
71        self.validate()?;
72        Ok(self.summary_line())
73    }
74}
75
76impl fmt::Display for ObserverLocation {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        f.write_str(&self.summary_line())
79    }
80}
81
82/// Validation errors for observer locations.
83#[derive(Clone, Copy, Debug, PartialEq)]
84#[non_exhaustive]
85pub enum ObserverLocationValidationError {
86    /// The observer latitude was not finite.
87    NonFiniteLatitude {
88        /// The offending value.
89        value: f64,
90    },
91    /// The observer latitude fell outside the valid range.
92    LatitudeOutOfRange {
93        /// The offending value.
94        value: f64,
95    },
96    /// The observer longitude was not finite.
97    NonFiniteLongitude {
98        /// The offending value.
99        value: f64,
100    },
101    /// The observer elevation was not finite.
102    NonFiniteElevation {
103        /// The offending value.
104        value: f64,
105    },
106}
107
108impl ObserverLocationValidationError {
109    /// Returns a compact one-line rendering of the validation failure.
110    pub fn summary_line(&self) -> String {
111        match self {
112            Self::NonFiniteLatitude { value } => {
113                format!("observer latitude must be finite, got {value}")
114            }
115            Self::LatitudeOutOfRange { value } => {
116                format!("observer latitude must stay within [-90, 90], got {value}")
117            }
118            Self::NonFiniteLongitude { value } => {
119                format!("observer longitude must be finite, got {value}")
120            }
121            Self::NonFiniteElevation { value } => {
122                format!("observer elevation must be finite, got {value}")
123            }
124        }
125    }
126}
127
128impl fmt::Display for ObserverLocationValidationError {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str(&self.summary_line())
131    }
132}
133
134impl std::error::Error for ObserverLocationValidationError {}