Skip to main content

siderust/event/azimuth/
types.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! # Azimuth Type Definitions
5//!
6//! ## Scientific scope
7//!
8//! Pure data structures expressing the *result* of an azimuth analysis:
9//! bearing‑crossing events (instants when *A(t)* sweeps through a fixed
10//! compass bearing) and azimuth extrema (turning points of *A(t)*),
11//! together with a query descriptor for range searches over the circular
12//! `[0°, 360°)` domain. Wrap‑around ranges spanning North are encoded by
13//! `min_azimuth > max_azimuth`; this is a convention, not a constraint
14//! enforced at the type level.
15//!
16//! ## Technical scope
17//!
18//! No functions. Defines:
19//! - [`AzimuthCrossingDirection`] (re‑exported from `altitude`),
20//! - [`AzimuthCrossingEvent`],
21//! - [`AzimuthExtremumKind`] / [`AzimuthExtremum`],
22//! - [`AzimuthQuery`].
23//!
24//! ## References
25//! None.
26
27use crate::astro::apparent::CorrectionPolicy;
28use crate::event::altitude::{CrossingDirection, SearchOpts};
29use crate::qtty::*;
30use crate::time::{Interval, ModifiedJulianDate};
31
32// Re-export CrossingDirection so consumers only need to import from this module.
33pub use crate::event::altitude::CrossingDirection as AzimuthCrossingDirection;
34
35// ---------------------------------------------------------------------------
36// Bearing Crossing Types
37// ---------------------------------------------------------------------------
38
39/// A bearing-crossing event: the moment a body's azimuth passes through a
40/// specific compass bearing.
41///
42/// `direction` is [`CrossingDirection::Rising`] when the azimuth is increasing
43/// (moving clockwise / eastward through the bearing), and
44/// [`CrossingDirection::Setting`] when decreasing (westward).
45#[derive(Debug, Clone, Copy)]
46pub struct AzimuthCrossingEvent {
47    /// Modified Julian Date of the crossing (TT axis).
48    pub mjd: ModifiedJulianDate,
49    /// Whether azimuth was increasing (Rising) or decreasing (Setting).
50    pub direction: CrossingDirection,
51}
52
53impl std::fmt::Display for AzimuthCrossingEvent {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        write!(f, "Azimuth {} at {}", self.direction, self.mjd)
56    }
57}
58
59// ---------------------------------------------------------------------------
60// Azimuth Extremum Types
61// ---------------------------------------------------------------------------
62
63/// Kind of azimuth extremum.
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum AzimuthExtremumKind {
66    /// Local maximum azimuth (southernmost / easternmost bearing).
67    Max,
68    /// Local minimum azimuth (northernmost / westernmost bearing).
69    Min,
70}
71
72impl std::fmt::Display for AzimuthExtremumKind {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::Max => write!(f, "Max Azimuth"),
76            Self::Min => write!(f, "Min Azimuth"),
77        }
78    }
79}
80
81/// An azimuth extremum event.
82#[derive(Debug, Clone, Copy)]
83pub struct AzimuthExtremum {
84    /// Modified Julian Date of the extremum (TT axis).
85    pub mjd: ModifiedJulianDate,
86    /// Azimuth at the extremum (North-clockwise, `[0°, 360°)`).
87    pub azimuth: Degrees,
88    /// Maximum or minimum.
89    pub kind: AzimuthExtremumKind,
90}
91
92impl std::fmt::Display for AzimuthExtremum {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{} at {} (az: {})", self.kind, self.mjd, self.azimuth)
95    }
96}
97
98// ---------------------------------------------------------------------------
99// Query Type
100// ---------------------------------------------------------------------------
101
102/// Describes what to search for: observer, time window, and the azimuth
103/// range of interest.
104///
105/// All fields use strongly-typed `qtty` quantities; time is MJD on the TT axis.
106///
107/// ## Wrap-around ranges
108///
109/// If `min_azimuth > max_azimuth` the query is interpreted as a
110/// **wrap-around** (North-crossing) range: the body is "in range" when
111/// `az ≥ min_azimuth  OR  az ≤ max_azimuth`.
112///
113/// For example `{ min: 350°, max: 10° }` matches azimuths from 350° through
114/// North (0°) to 10°.
115#[derive(Debug, Clone, Copy)]
116pub struct AzimuthQuery {
117    /// Observer location on Earth.
118    pub observer: crate::coordinates::centers::Geodetic<crate::coordinates::frames::ECEF>,
119    /// Time window to search (MJD on the TT axis).
120    pub window: Interval<ModifiedJulianDate>,
121    /// Lower (or start-of-wrap) bound of the azimuth band.
122    pub min_azimuth: Degrees,
123    /// Upper (or end-of-wrap) bound of the azimuth band.
124    pub max_azimuth: Degrees,
125    /// Numerical search options (scan step, tolerance).
126    pub opts: SearchOpts,
127    /// Apparent-position correction policy for the target pipeline.
128    pub correction_policy: CorrectionPolicy,
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn azimuth_crossing_event_display() {
137        let event = AzimuthCrossingEvent {
138            mjd: ModifiedJulianDate::new(60_000.0),
139            direction: CrossingDirection::Setting,
140        };
141        let text = event.to_string();
142        assert!(text.contains("Azimuth"));
143        assert!(text.contains("Setting"));
144    }
145
146    #[test]
147    fn azimuth_extremum_kind_display() {
148        assert_eq!(AzimuthExtremumKind::Max.to_string(), "Max Azimuth");
149        assert_eq!(AzimuthExtremumKind::Min.to_string(), "Min Azimuth");
150    }
151
152    #[test]
153    fn azimuth_extremum_display_includes_kind_and_azimuth() {
154        let event = AzimuthExtremum {
155            mjd: ModifiedJulianDate::new(60_002.0),
156            azimuth: Degrees::new(180.0),
157            kind: AzimuthExtremumKind::Min,
158        };
159        let text = event.to_string();
160        assert!(text.contains("Min Azimuth"));
161        assert!(text.contains("180"));
162    }
163}