use super::events;
use super::types::AzimuthQuery;
use crate::bodies::solar_system;
use crate::bodies::Star;
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::coordinates::spherical::direction;
use crate::time::{complement_within, ModifiedJulianDate, Period, MJD};
use qtty::*;
use crate::calculus::horizontal;
use crate::coordinates::transform::Transform;
use crate::coordinates::{cartesian, centers::Geocentric, frames};
use crate::time::JulianDate;
pub trait AzimuthProvider {
fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians;
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>>;
fn in_azimuth_range(
&self,
observer: Geodetic<ECEF>,
window: Period<MJD>,
min_az: Degrees,
max_az: Degrees,
) -> Vec<Period<MJD>> {
self.azimuth_periods(&AzimuthQuery {
observer,
window,
min_azimuth: min_az,
max_azimuth: max_az,
})
}
fn outside_azimuth_range(
&self,
observer: Geodetic<ECEF>,
window: Period<MJD>,
min_az: Degrees,
max_az: Degrees,
) -> Vec<Period<MJD>> {
let inside = self.in_azimuth_range(observer, window, min_az, max_az);
complement_within(window, &inside)
}
fn scan_step_hint(&self) -> Option<Days> {
None
}
}
#[inline]
pub fn azimuth_periods<B: AzimuthProvider>(body: &B, query: &AzimuthQuery) -> Vec<Period<MJD>> {
body.azimuth_periods(query)
}
impl AzimuthProvider for solar_system::Sun {
fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::calculus::solar::sun_azimuth_rad(mjd, observer)
}
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>> {
if query.window.duration() <= Days::zero() {
return Vec::new();
}
events::azimuth_range_periods(self, query)
}
fn scan_step_hint(&self) -> Option<Days> {
Some(Hours::new(2.0).to::<Day>())
}
}
impl AzimuthProvider for solar_system::Moon {
fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::calculus::lunar::moon_azimuth_rad(mjd, observer)
}
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>> {
if query.window.duration() <= Days::zero() {
return Vec::new();
}
events::azimuth_range_periods(self, query)
}
fn scan_step_hint(&self) -> Option<Days> {
Some(Hours::new(2.0).to::<Day>())
}
}
impl AzimuthProvider for Star<'_> {
fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
let dir = direction::ICRS::from(self);
dir.azimuth_at(observer, mjd)
}
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>> {
let dir = direction::ICRS::from(self);
dir.azimuth_periods(query)
}
}
impl AzimuthProvider for direction::ICRS {
fn azimuth_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::calculus::stellar::fixed_star_azimuth_rad(mjd, observer, self.ra(), self.dec())
}
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>> {
if query.window.duration() <= Days::zero() {
return Vec::new();
}
events::azimuth_range_periods(self, query)
}
}
fn vsop87_planet_azimuth_rad<F>(
vsop87e_fn: F,
mjd: ModifiedJulianDate,
site: &Geodetic<ECEF>,
) -> Radians
where
F: Fn(
JulianDate,
) -> cartesian::Position<
crate::coordinates::centers::Barycentric,
frames::EclipticMeanJ2000,
AstronomicalUnit,
>,
{
let jd: JulianDate = mjd.into();
let bary_ecl = vsop87e_fn(jd);
let geo_equ: cartesian::Position<Geocentric, frames::EquatorialMeanJ2000, AstronomicalUnit> =
bary_ecl.transform(jd);
let topo = horizontal::geocentric_j2000_to_apparent_topocentric(&geo_equ, *site, jd);
let horiz = horizontal::equatorial_to_horizontal(&topo, *site, jd);
horiz.az().to::<Radian>()
}
macro_rules! impl_azimuth_provider_vsop87 {
($($Planet:ident),+ $(,)?) => {
$(
impl AzimuthProvider for solar_system::$Planet {
fn azimuth_at(
&self,
observer: &Geodetic<ECEF>,
mjd: ModifiedJulianDate,
) -> Radians {
vsop87_planet_azimuth_rad(
solar_system::$Planet::vsop87e, mjd, observer,
)
}
fn azimuth_periods(&self, query: &AzimuthQuery) -> Vec<Period<MJD>> {
if query.window.duration() <= Days::zero() {
return Vec::new();
}
events::azimuth_range_periods(self, query)
}
fn scan_step_hint(&self) -> Option<Days> {
Some(Hours::new(2.0).to::<Day>())
}
}
)+
};
}
impl_azimuth_provider_vsop87!(Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune);
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::catalog;
use crate::bodies::solar_system::Moon;
fn greenwich() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.4769), Meters::new(0.0))
}
fn one_day_window() -> Period<MJD> {
Period::new(
ModifiedJulianDate::new(60000.0),
ModifiedJulianDate::new(60001.0),
)
}
#[test]
fn sun_azimuth_at_returns_valid_range() {
let az = solar_system::Sun.azimuth_at(
&greenwich(),
ModifiedJulianDate::new(60000.5), );
assert!(az.value() >= 0.0, "azimuth must be ≥ 0");
assert!(az.value() < std::f64::consts::TAU, "azimuth must be < 2π");
}
#[test]
fn moon_azimuth_at_returns_valid_range() {
let az = Moon.azimuth_at(&greenwich(), ModifiedJulianDate::new(60000.5));
assert!(az.value() >= 0.0);
assert!(az.value() < std::f64::consts::TAU);
}
#[test]
fn star_azimuth_at_returns_valid_range() {
let sirius = &catalog::SIRIUS;
let az = sirius.azimuth_at(&greenwich(), ModifiedJulianDate::new(60000.5));
assert!(az.value() >= 0.0);
assert!(az.value() < std::f64::consts::TAU);
}
#[test]
fn star_and_icrs_agree() {
let sirius = &catalog::SIRIUS;
let dir = direction::ICRS::from(sirius);
let mjd = ModifiedJulianDate::new(60000.5);
let az_star = sirius.azimuth_at(&greenwich(), mjd);
let az_dir = dir.azimuth_at(&greenwich(), mjd);
assert!(
(az_star.value() - az_dir.value()).abs() < 1e-10,
"Star and direction::ICRS must agree"
);
}
#[test]
fn sun_azimuth_periods_eastern_half() {
let query = AzimuthQuery {
observer: greenwich(),
window: one_day_window(),
min_azimuth: Degrees::new(90.0),
max_azimuth: Degrees::new(270.0),
};
let periods = solar_system::Sun.azimuth_periods(&query);
assert!(
!periods.is_empty(),
"Sun should cross the eastern half in 24h"
);
}
#[test]
fn outside_azimuth_range_is_complement() {
let observer = greenwich();
let window = one_day_window();
let inside = solar_system::Sun.in_azimuth_range(
observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
);
let outside = solar_system::Sun.outside_azimuth_range(
observer,
window,
Degrees::new(90.0),
Degrees::new(270.0),
);
let total_inside: f64 = inside.iter().map(|p| p.duration_days().value()).sum();
let total_outside: f64 = outside.iter().map(|p| p.duration_days().value()).sum();
let window_len = window.duration_days().value();
assert!(
(total_inside + total_outside - window_len).abs() < 1e-6,
"inside + outside should equal the full window"
);
}
#[test]
fn mars_azimuth_at_returns_valid_range() {
let az = solar_system::Mars.azimuth_at(&greenwich(), ModifiedJulianDate::new(60000.5));
assert!(az.value() >= 0.0, "azimuth must be ≥ 0, got {}", az);
assert!(
az.value() < std::f64::consts::TAU,
"azimuth must be < 2π, got {}",
az
);
}
#[test]
fn all_planets_azimuth_valid() {
let observer = greenwich();
let mjd = ModifiedJulianDate::new(60000.5);
let mercury_az = solar_system::Mercury.azimuth_at(&observer, mjd);
let venus_az = solar_system::Venus.azimuth_at(&observer, mjd);
let mars_az = solar_system::Mars.azimuth_at(&observer, mjd);
let jupiter_az = solar_system::Jupiter.azimuth_at(&observer, mjd);
let saturn_az = solar_system::Saturn.azimuth_at(&observer, mjd);
let uranus_az = solar_system::Uranus.azimuth_at(&observer, mjd);
let neptune_az = solar_system::Neptune.azimuth_at(&observer, mjd);
for (name, az) in [
("Mercury", mercury_az),
("Venus", venus_az),
("Mars", mars_az),
("Jupiter", jupiter_az),
("Saturn", saturn_az),
("Uranus", uranus_az),
("Neptune", neptune_az),
] {
assert!(
az.value() >= 0.0 && az.value() < std::f64::consts::TAU,
"{name} azimuth out of range: {az}"
);
}
}
}