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}