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::event::altitude::search::{InternalSearchConfig, SearchOpts};
use crate::event::altitude::types::{CrossingEvent, CulminationEvent};
use crate::qtty::*;
use crate::time::{Interval, ModifiedJulianDate};
use crate::coordinates::{cartesian, centers::Geocentric, frames};
use crate::event::horizontal;
use crate::time::JulianDate;
pub trait AltitudeProvider {
fn altitude_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians;
fn altitude_at_with_policy(
&self,
observer: &Geodetic<ECEF>,
mjd: ModifiedJulianDate,
policy: crate::astro::apparent::CorrectionPolicy,
) -> Radians {
let _ = policy;
self.altitude_at(observer, mjd)
}
fn scan_step_hint(&self) -> Option<Days> {
None
}
#[doc(hidden)]
fn event_above_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
super::events::generic_above_threshold(self, observer, window, threshold, opts)
}
#[doc(hidden)]
fn event_below_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
super::events::generic_below_threshold(self, observer, window, threshold, opts)
}
#[doc(hidden)]
fn event_altitude_ranges(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
h_min: Degrees,
h_max: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
super::events::generic_altitude_ranges(self, observer, window, h_min, h_max, opts)
}
#[doc(hidden)]
fn event_crossings(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<CrossingEvent> {
super::events::generic_crossings(self, observer, window, threshold, opts)
}
#[doc(hidden)]
fn event_culminations(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
opts: SearchOpts,
) -> Vec<CulminationEvent> {
super::events::generic_culminations(self, observer, window, opts)
}
}
impl AltitudeProvider for solar_system::Sun {
fn altitude_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::event::solar::sun_altitude_rad(mjd, observer)
}
fn event_above_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::solar::solar_above_threshold_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_below_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::solar::solar_below_threshold_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_altitude_ranges(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
h_min: Degrees,
h_max: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::solar::solar_altitude_ranges_impl(
*observer,
window,
(h_min, h_max),
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_crossings(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<CrossingEvent> {
crate::event::solar::solar_crossings_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
}
impl AltitudeProvider for solar_system::Moon {
fn altitude_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::event::lunar::moon_altitude_rad(mjd, observer)
}
fn scan_step_hint(&self) -> Option<Days> {
Some(Hours::new(2.0).to::<Day>())
}
fn event_above_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::lunar::lunar_above_threshold_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_below_threshold(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::lunar::lunar_below_threshold_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_altitude_ranges(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
h_min: Degrees,
h_max: Degrees,
opts: SearchOpts,
) -> Vec<Interval<ModifiedJulianDate>> {
crate::event::lunar::lunar_altitude_ranges_impl(
*observer,
window,
(h_min, h_max),
InternalSearchConfig::from_public_opts(opts),
)
}
fn event_crossings(
&self,
observer: &Geodetic<ECEF>,
window: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<CrossingEvent> {
crate::event::lunar::lunar_crossings_impl(
*observer,
window,
threshold,
InternalSearchConfig::from_public_opts(opts),
)
}
}
impl AltitudeProvider for Star<'_> {
fn altitude_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
direction::ICRS::from(self).altitude_at(observer, mjd)
}
fn altitude_at_with_policy(
&self,
observer: &Geodetic<ECEF>,
mjd: ModifiedJulianDate,
policy: crate::astro::apparent::CorrectionPolicy,
) -> Radians {
direction::ICRS::from(self).altitude_at_with_policy(observer, mjd, policy)
}
}
impl AltitudeProvider for direction::ICRS {
fn altitude_at(&self, observer: &Geodetic<ECEF>, mjd: ModifiedJulianDate) -> Radians {
crate::event::stellar::fixed_star_altitude_rad(mjd, observer, self.ra(), self.dec())
}
fn altitude_at_with_policy(
&self,
observer: &Geodetic<ECEF>,
mjd: ModifiedJulianDate,
policy: crate::astro::apparent::CorrectionPolicy,
) -> Radians {
crate::event::stellar::fixed_star_altitude_rad_with_policy(
mjd,
observer,
self.ra(),
self.dec(),
policy,
)
}
}
use crate::coordinates::transform::Transform;
const PLANET_SCAN_STEP: Days = Quantity::<Hour>::new(2.0).to_const::<Day>();
fn vsop87_planet_altitude_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.to::<crate::JD>();
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.alt().to::<Radian>()
}
macro_rules! impl_altitude_provider_vsop87 {
($($Planet:ident),+ $(,)?) => {
$(
impl AltitudeProvider for solar_system::$Planet {
fn altitude_at(
&self,
observer: &Geodetic<ECEF>,
mjd: ModifiedJulianDate,
) -> Radians {
vsop87_planet_altitude_rad(
solar_system::$Planet::vsop87e, mjd, observer,
)
}
fn scan_step_hint(&self) -> Option<Days> {
Some(PLANET_SCAN_STEP)
}
}
)+
};
}
impl_altitude_provider_vsop87!(Mercury, Venus, Mars, Jupiter, Saturn, Uranus, Neptune);
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::catalog;
use crate::event::altitude::{
above_threshold, altitude_ranges, below_threshold, AltitudeEventsExt, SearchOpts,
};
use crate::time::Interval;
struct FixedAltitudeTarget {
alt: Radians,
}
struct BorrowedAltitudeTarget<'a> {
alt: &'a Radians,
}
impl AltitudeProvider for FixedAltitudeTarget {
fn altitude_at(&self, _observer: &Geodetic<ECEF>, _mjd: ModifiedJulianDate) -> Radians {
self.alt
}
}
impl AltitudeProvider for BorrowedAltitudeTarget<'_> {
fn altitude_at(&self, _observer: &Geodetic<ECEF>, _mjd: ModifiedJulianDate) -> Radians {
*self.alt
}
}
fn greenwich() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.4769), Meters::new(0.0))
}
fn one_day_window() -> Interval<ModifiedJulianDate> {
Interval::new(
crate::time::ModifiedJulianDate::new(60000.0),
crate::time::ModifiedJulianDate::new(60001.0),
)
}
fn one_week_window() -> Interval<ModifiedJulianDate> {
Interval::new(
crate::time::ModifiedJulianDate::new(60000.0),
crate::time::ModifiedJulianDate::new(60007.0),
)
}
#[test]
fn custom_altitude_provider_uses_generic_search_path() {
let target = FixedAltitudeTarget {
alt: Degrees::new(45.0).to::<Radian>(),
};
let periods = above_threshold(
&target,
&greenwich(),
one_day_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert_eq!(periods.len(), 1);
assert!(
((periods[0].end.raw() - periods[0].start.raw()) - Days::new(1.0)).abs()
< Days::new(0.01)
);
}
#[test]
fn borrowed_custom_altitude_provider_works_with_above_threshold() {
let alt = Degrees::new(45.0).to::<Radian>();
let target = BorrowedAltitudeTarget { alt: &alt };
let periods = above_threshold(
&target,
&greenwich(),
one_day_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert_eq!(periods.len(), 1);
}
#[test]
fn sun_above_horizon_via_standard_api() {
let periods = above_threshold(
&solar_system::Sun,
&greenwich(),
one_day_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!periods.is_empty());
}
#[test]
fn moon_above_horizon_via_standard_api() {
let periods = above_threshold(
&solar_system::Moon,
&greenwich(),
one_week_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!periods.is_empty());
}
#[test]
fn extension_trait_exposes_method_style_events() {
let periods = solar_system::Sun.above_threshold(
&greenwich(),
one_day_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!periods.is_empty());
}
#[test]
fn star_above_horizon_via_standard_api() {
let periods = above_threshold(
&catalog::SIRIUS,
&greenwich(),
one_day_window(),
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!periods.is_empty());
}
#[test]
fn below_threshold_sun_night_via_standard_api() {
let nights = below_threshold(
&solar_system::Sun,
&greenwich(),
one_week_window(),
Degrees::new(-18.0),
SearchOpts::default(),
);
assert!(!nights.is_empty());
}
#[test]
fn altitude_range_twilight_via_standard_api() {
let bands = altitude_ranges(
&solar_system::Sun,
&greenwich(),
Interval::new(
crate::time::ModifiedJulianDate::new(60000.0),
crate::time::ModifiedJulianDate::new(60002.0),
),
Degrees::new(-18.0),
Degrees::new(-12.0),
SearchOpts::default(),
);
assert!(bands.len() >= 2);
}
}