use crate::bodies::solar_system::Moon;
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::event::altitude::search::{InternalSearchConfig, DIURNAL_CHEBY_SCAN_STEP};
use crate::event::altitude::{CrossingDirection, CrossingEvent};
use crate::event::search::crossings;
use crate::event::search::intervals;
use crate::event::search::periods as threshold_periods;
use crate::qtty::*;
use crate::time::{Interval, JulianDate, ModifiedJulianDate};
use alloc::vec::Vec;
use super::moon_cache::MoonAltitudeContext;
pub(crate) fn moon_altitude_rad(
mjd: ModifiedJulianDate,
site: &Geodetic<ECEF>,
) -> Quantity<Radian> {
let jd: JulianDate = mjd.to::<crate::JD>();
Moon::get_horizontal::<Kilometer>(jd, *site)
.alt()
.to::<Radian>()
}
pub(crate) fn lunar_above_threshold_impl(
site: Geodetic<ECEF>,
period: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: InternalSearchConfig,
) -> Vec<Interval<ModifiedJulianDate>> {
let thr = threshold.to::<Radian>();
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let (labeled, start_above) = find_moon_labeled_crossings_with_context(&ctx, period, thr, opts);
threshold_periods::assemble_above_threshold_periods(&labeled, period, start_above)
}
pub(crate) fn lunar_below_threshold_impl(
site: Geodetic<ECEF>,
period: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: InternalSearchConfig,
) -> Vec<Interval<ModifiedJulianDate>> {
let above = lunar_above_threshold_impl(site, period, threshold, opts);
threshold_periods::complement_threshold_periods(period, &above)
}
pub(crate) fn lunar_altitude_ranges_impl(
site: Geodetic<ECEF>,
period: Interval<ModifiedJulianDate>,
range: (Degrees, Degrees),
opts: InternalSearchConfig,
) -> Vec<Interval<ModifiedJulianDate>> {
let h_min = range.0.to::<Radian>();
let h_max = range.1.to::<Radian>();
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let (min_crossings, start_above_min) =
find_moon_labeled_crossings_with_context(&ctx, period, h_min, opts);
let (max_crossings, start_above_max) =
find_moon_labeled_crossings_with_context(&ctx, period, h_max, opts);
threshold_periods::assemble_in_range_periods(
&min_crossings,
start_above_min,
&max_crossings,
start_above_max,
period,
)
}
pub(crate) fn lunar_crossings_impl(
site: Geodetic<ECEF>,
period: Interval<ModifiedJulianDate>,
threshold: Degrees,
opts: InternalSearchConfig,
) -> Vec<CrossingEvent> {
let thr = threshold.to::<Radian>();
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let (labeled, _) = find_moon_labeled_crossings_with_context(&ctx, period, thr, opts);
labeled
.iter()
.map(|crossing| CrossingEvent {
mjd: crossing.t,
direction: if crossing.direction > 0 {
CrossingDirection::Rising
} else {
CrossingDirection::Setting
},
})
.collect()
}
fn find_moon_labeled_crossings_with_context(
ctx: &MoonAltitudeContext,
period: Interval<ModifiedJulianDate>,
threshold: Radians,
opts: InternalSearchConfig,
) -> (Vec<intervals::LabeledCrossing>, bool) {
let signal = |t: ModifiedJulianDate| -> f64 { ctx.altitude_rad(t).sin() };
let (labeled, start_above, _) = crossings::find_labelled_crossings(
period,
DIURNAL_CHEBY_SCAN_STEP,
&signal,
threshold.sin(),
opts,
);
(labeled, start_above)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::catalogs::observatories::ROQUE_DE_LOS_MUCHACHOS;
fn greenwich_site() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(Degrees::new(0.0), Degrees::new(51.4769), Meters::new(0.0))
}
#[test]
fn test_moon_altitude_basic() {
let site = greenwich_site();
let mjd: ModifiedJulianDate = crate::J2000.to::<crate::MJD>();
let alt = moon_altitude_rad(mjd, &site);
assert!(
alt > -core::f64::consts::FRAC_PI_2 * RAD && alt < core::f64::consts::FRAC_PI_2 * RAD
);
}
#[test]
fn test_lunar_above_threshold() {
let site = greenwich_site();
let mjd_start = crate::time::ModifiedJulianDate::new(60000.0);
let mjd_end = crate::time::ModifiedJulianDate::new(60007.0);
let period = Interval::new(mjd_start, mjd_end);
let periods = lunar_above_threshold_impl(
site,
period,
Degrees::new(0.0),
InternalSearchConfig::default(),
);
assert!(
!periods.is_empty(),
"Should find moon-up periods over 7 days"
);
for p in &periods {
assert!(
p.length() > Days::new(0.0),
"Period duration should be positive"
);
}
}
#[test]
fn test_lunar_below_threshold() {
let site = ROQUE_DE_LOS_MUCHACHOS.geodetic();
let mjd_start = crate::time::ModifiedJulianDate::new(60676.0);
let mjd_end = crate::time::ModifiedJulianDate::new(60683.0);
let period = Interval::new(mjd_start, mjd_end);
let periods = lunar_below_threshold_impl(
site,
period,
Degrees::new(-0.5),
InternalSearchConfig::default(),
);
assert!(!periods.is_empty(), "Should find moon-down periods");
}
fn scan_baseline_above_horizon(
ctx: &MoonAltitudeContext,
period: Interval<ModifiedJulianDate>,
threshold: Degrees,
) -> Vec<Interval<ModifiedJulianDate>> {
let f = |t: ModifiedJulianDate| -> Radians { ctx.altitude_rad(t) };
let (labeled, start_above) = crate::event::lunar::moon_cache::find_and_label_crossings(
period,
DIURNAL_CHEBY_SCAN_STEP,
&f,
threshold.to::<Radian>(),
);
intervals::build_above_periods_directed(&labeled, period, start_above)
}
#[test]
fn chebyshev_lunar_above_threshold_matches_scan_baseline() {
let site = greenwich_site();
let mjd_start = crate::time::ModifiedJulianDate::new(60000.0);
let mjd_end = crate::time::ModifiedJulianDate::new(60003.0);
let period = Interval::new(mjd_start, mjd_end);
let main_result = lunar_above_threshold_impl(
site,
period,
Degrees::new(0.0),
InternalSearchConfig::default(),
);
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let scan_result = scan_baseline_above_horizon(&ctx, period, Degrees::new(0.0));
assert!(!main_result.is_empty());
assert!(!scan_result.is_empty());
assert_eq!(
main_result.len(),
scan_result.len(),
"Should find same number of periods"
);
let tolerance = Days::new(1.0 / 1440.0);
for (m, s) in main_result.iter().zip(scan_result.iter()) {
assert!(
(m.start.raw() - s.start.raw()).abs() < tolerance,
"Start times should match within 1 minute"
);
assert!(
(m.end.raw() - s.end.raw()).abs() < tolerance,
"End times should match within 1 minute"
);
}
}
}