use crate::bodies::solar_system::Moon;
use crate::calculus::math_core::intervals;
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::time::{complement_within, JulianDate, ModifiedJulianDate, Period, MJD};
use qtty::*;
use super::moon_cache::{find_and_label_crossings, MoonAltitudeContext};
const SCAN_STEP: Days = Quantity::new(2.0 / 24.0);
pub(crate) fn moon_altitude_rad(
mjd: ModifiedJulianDate,
site: &Geodetic<ECEF>,
) -> Quantity<Radian> {
let jd: JulianDate = mjd.into();
Moon::get_horizontal::<Kilometer>(jd, *site)
.alt()
.to::<Radian>()
}
pub(crate) fn find_moon_above_horizon(
site: Geodetic<ECEF>,
period: Period<MJD>,
threshold: Degrees,
) -> Vec<Period<MJD>> {
let thr = threshold.to::<Radian>();
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let f = |t: ModifiedJulianDate| -> Radians { ctx.altitude_rad(t) };
let (labeled, start_above) = find_and_label_crossings(period, SCAN_STEP, &f, thr);
intervals::build_above_periods(&labeled, period, start_above, &f, thr)
}
pub(crate) fn find_moon_below_horizon(
site: Geodetic<ECEF>,
period: Period<MJD>,
threshold: Degrees,
) -> Vec<Period<MJD>> {
let above = find_moon_above_horizon(site, period, threshold);
complement_within(period, &above)
}
pub(crate) fn find_moon_altitude_range(
site: Geodetic<ECEF>,
period: Period<MJD>,
range: (Degrees, Degrees),
) -> Vec<Period<MJD>> {
let h_min = range.0.to::<Radian>();
let h_max = range.1.to::<Radian>();
let ctx = MoonAltitudeContext::new(period.start, period.end, site);
let f = |t: ModifiedJulianDate| -> Radians { ctx.altitude_rad(t) };
intervals::in_range_periods(period, SCAN_STEP, &f, h_min, h_max)
}
#[cfg(test)]
const SCAN_STEP_10MIN: Days = Quantity::new(10.0 / 1440.0);
#[cfg(test)]
fn find_moon_above_horizon_scan(
site: Geodetic<ECEF>,
period: Period<MJD>,
threshold: Degrees,
) -> Vec<Period<MJD>> {
let thr = threshold.to::<Radian>();
let f = |t: ModifiedJulianDate| -> Radians { moon_altitude_rad(t, &site) };
intervals::above_threshold_periods(period, SCAN_STEP_10MIN, &f, thr)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{observatories::ROQUE_DE_LOS_MUCHACHOS, time::JulianDate};
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 = JulianDate::J2000.into();
let alt = moon_altitude_rad(mjd, &site);
assert!(
alt > -std::f64::consts::FRAC_PI_2 * RAD && alt < std::f64::consts::FRAC_PI_2 * RAD
);
}
#[test]
fn test_find_moon_above_horizon() {
let site = greenwich_site();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60007.0);
let period = Period::new(mjd_start, mjd_end);
let periods = find_moon_above_horizon(site, period, Degrees::new(0.0));
assert!(
!periods.is_empty(),
"Should find moon-up periods over 7 days"
);
for p in &periods {
assert!(
p.duration_days() > 0.0,
"Period duration should be positive"
);
}
}
#[test]
fn test_find_moon_below_horizon() {
let site = ROQUE_DE_LOS_MUCHACHOS;
let mjd_start = ModifiedJulianDate::new(60676.0);
let mjd_end = ModifiedJulianDate::new(60683.0);
let period = Period::new(mjd_start, mjd_end);
let periods = find_moon_below_horizon(site, period, Degrees::new(-0.5));
assert!(!periods.is_empty(), "Should find moon-down periods");
}
#[test]
fn test_above_vs_scan_consistency() {
let site = greenwich_site();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60003.0);
let period = Period::new(mjd_start, mjd_end);
let main_result = find_moon_above_horizon(site, period, Degrees::new(0.0));
let scan_result = find_moon_above_horizon_scan(site, 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 - s.start).abs() < tolerance,
"Start times should match within 1 minute"
);
assert!(
(m.end - s.end).abs() < tolerance,
"End times should match within 1 minute"
);
}
}
}