use super::provider::AltitudePeriodsProvider;
use super::search::{SearchOpts, DEFAULT_SCAN_STEP, EXTREMA_SCAN_STEP};
use super::types::{CrossingDirection, CrossingEvent, CulminationEvent, CulminationKind};
use crate::calculus::math_core::{extrema, intervals};
use crate::coordinates::centers::Geodetic;
use crate::coordinates::frames::ECEF;
use crate::time::{complement_within, ModifiedJulianDate, Period, MJD};
use qtty::*;
fn make_altitude_fn<'a, T: AltitudePeriodsProvider>(
target: &'a T,
site: &'a Geodetic<ECEF>,
) -> impl Fn(ModifiedJulianDate) -> Radians + 'a {
let site = *site;
move |t: ModifiedJulianDate| target.altitude_at(&site, t)
}
fn scan_step_for<T: AltitudePeriodsProvider>(target: &T, opts: &SearchOpts) -> Days {
opts.scan_step_days
.or_else(|| target.scan_step_hint())
.unwrap_or(DEFAULT_SCAN_STEP)
}
pub fn crossings<T: AltitudePeriodsProvider>(
target: &T,
observer: &Geodetic<ECEF>,
window: Period<MJD>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<CrossingEvent> {
let f = make_altitude_fn(target, observer);
let thr_rad = threshold.to::<Radian>();
let step = scan_step_for(target, &opts);
let mut raw_crossings = intervals::find_crossings(window, step, &f, thr_rad);
let labeled = intervals::label_crossings(&mut raw_crossings, &f, thr_rad);
labeled
.iter()
.map(|lc| CrossingEvent {
mjd: lc.t,
direction: if lc.direction > 0 {
CrossingDirection::Rising
} else {
CrossingDirection::Setting
},
})
.collect()
}
pub fn culminations<T: AltitudePeriodsProvider>(
target: &T,
observer: &Geodetic<ECEF>,
window: Period<MJD>,
opts: SearchOpts,
) -> Vec<CulminationEvent> {
let f = make_altitude_fn(target, observer);
let step = opts
.scan_step_days
.or_else(|| target.scan_step_hint())
.unwrap_or(EXTREMA_SCAN_STEP);
let tol = opts.time_tolerance;
let raw: Vec<extrema::Extremum<Radian>> = extrema::find_extrema_tol(window, step, &f, tol);
raw.iter()
.map(|ext| {
let alt_deg = ext.value.to::<Degree>();
CulminationEvent {
mjd: ext.t,
altitude: alt_deg,
kind: match ext.kind {
extrema::ExtremumKind::Maximum => CulminationKind::Max,
extrema::ExtremumKind::Minimum => CulminationKind::Min,
},
}
})
.collect()
}
pub fn altitude_ranges<T: AltitudePeriodsProvider>(
target: &T,
observer: &Geodetic<ECEF>,
window: Period<MJD>,
h_min: Degrees,
h_max: Degrees,
opts: SearchOpts,
) -> Vec<Period<MJD>> {
let f = make_altitude_fn(target, observer);
let min_rad = h_min.to::<Radian>();
let max_rad = h_max.to::<Radian>();
let step = scan_step_for(target, &opts);
intervals::in_range_periods(window, step, &f, min_rad, max_rad)
}
pub fn above_threshold<T: AltitudePeriodsProvider>(
target: &T,
observer: &Geodetic<ECEF>,
window: Period<MJD>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Period<MJD>> {
let f = make_altitude_fn(target, observer);
let thr_rad = threshold.to::<Radian>();
let step = scan_step_for(target, &opts);
intervals::above_threshold_periods(window, step, &f, thr_rad)
}
pub fn below_threshold<T: AltitudePeriodsProvider>(
target: &T,
observer: &Geodetic<ECEF>,
window: Period<MJD>,
threshold: Degrees,
opts: SearchOpts,
) -> Vec<Period<MJD>> {
let above = above_threshold(target, observer, window, threshold, opts);
complement_within(window, &above)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bodies::solar_system::{Moon, Sun};
fn greenwich() -> Geodetic<ECEF> {
Geodetic::<ECEF>::new(
Degrees::new(0.0),
Degrees::new(51.4769),
Quantity::<Meter>::new(0.0),
)
}
#[test]
fn crossings_finds_sun_rise_set() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60001.0);
let window = Period::new(mjd_start, mjd_end);
let events = crossings(
&Sun,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!events.is_empty(), "should find crossings");
let rises = events
.iter()
.filter(|e| e.direction == CrossingDirection::Rising)
.count();
let sets = events
.iter()
.filter(|e| e.direction == CrossingDirection::Setting)
.count();
assert!(
rises >= 1 || sets >= 1,
"should find at least one rise or set"
);
}
#[test]
fn culminations_finds_sun_extrema() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60001.0);
let window = Period::new(mjd_start, mjd_end);
let culms = culminations(&Sun, &site, window, SearchOpts::default());
assert!(!culms.is_empty(), "should find culminations");
let maxima = culms
.iter()
.filter(|c| c.kind == CulminationKind::Max)
.count();
let minima = culms
.iter()
.filter(|c| c.kind == CulminationKind::Min)
.count();
assert!(maxima >= 1, "should find at least one upper culmination");
assert!(minima >= 1, "should find at least one lower culmination");
}
#[test]
fn above_threshold_sun_day_periods() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60007.0);
let window = Period::new(mjd_start, mjd_end);
let days = above_threshold(
&Sun,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(!days.is_empty(), "should find daytime periods in 7 days");
for p in &days {
assert!(p.duration_days() > 0.0);
assert!(p.duration_days() < 1.0, "each day period < 24h");
}
}
#[test]
fn below_threshold_sun_night_periods() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60007.0);
let window = Period::new(mjd_start, mjd_end);
let nights = below_threshold(
&Sun,
&site,
window,
Degrees::new(-18.0), SearchOpts::default(),
);
assert!(!nights.is_empty(), "should find night periods");
}
#[test]
fn altitude_ranges_twilight_band() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60002.0);
let window = Period::new(mjd_start, mjd_end);
let twilight = altitude_ranges(
&Sun,
&site,
window,
Degrees::new(-18.0),
Degrees::new(-12.0),
SearchOpts::default(),
);
assert!(!twilight.is_empty(), "should find twilight bands");
}
#[test]
fn moon_above_horizon_7_days() {
let site = greenwich();
let mjd_start = ModifiedJulianDate::new(60000.0);
let mjd_end = ModifiedJulianDate::new(60007.0);
let window = Period::new(mjd_start, mjd_end);
let periods = above_threshold(
&Moon,
&site,
window,
Degrees::new(0.0),
SearchOpts::default(),
);
assert!(
!periods.is_empty(),
"should find moon-up periods over 7 days"
);
}
}