use chrono::{DateTime, Duration, Utc};
use crate::astronomy::solar::SolarTime;
use crate::astronomy::unit::Coordinates;
use crate::models::madhab::Madhab;
#[derive(Debug, Default, Clone, Copy, PartialEq)]
pub enum PolarFallback {
NearestLatitude,
Reference45,
#[default]
None,
}
impl PolarFallback {
#[must_use]
pub fn recommended(coordinates: Coordinates) -> Self {
if coordinates.latitude.abs() > 66.5 {
Self::NearestLatitude
} else {
Self::None
}
}
#[must_use]
pub fn resolve_latitude(
self,
date: DateTime<Utc>,
coordinates: Coordinates,
madhab: Madhab,
) -> Option<f64> {
let shadow = madhab.shadow() as f64;
let asr_reachable = |st: SolarTime| st.time_for_shadow(shadow).is_some();
let yesterday = date - Duration::days(1);
let tomorrow = date + Duration::days(1);
fn undisturbed(
date: DateTime<Utc>,
yesterday: DateTime<Utc>,
tomorrow: DateTime<Utc>,
coordinates: Coordinates,
asr_reachable: impl Fn(SolarTime) -> bool,
) -> bool {
SolarTime::new(date, coordinates).is_ok_and(&asr_reachable)
&& SolarTime::new(yesterday, coordinates).is_ok()
&& SolarTime::new(tomorrow, coordinates).is_ok()
}
match self {
Self::NearestLatitude => {
if undisturbed(date, yesterday, tomorrow, coordinates, asr_reachable) {
Some(coordinates.latitude)
} else {
Some(nearest_working_latitude(date, &coordinates, shadow))
}
}
Self::Reference45 => {
if undisturbed(date, yesterday, tomorrow, coordinates, asr_reachable) {
Some(coordinates.latitude)
} else {
Some(45.0 * coordinates.latitude.signum())
}
}
Self::None => {
if undisturbed(date, yesterday, tomorrow, coordinates, asr_reachable) {
Some(coordinates.latitude)
} else {
None
}
}
}
}
}
fn nearest_working_latitude(date: DateTime<Utc>, coords: &Coordinates, shadow: f64) -> f64 {
fn check(
date: DateTime<Utc>,
coords: &Coordinates,
shadow: f64,
require_adjacent: bool,
) -> Option<f64> {
let sign = coords.latitude.signum();
let mut lo = 0.0_f64;
let mut hi = coords.latitude.abs();
let yesterday = date - Duration::days(1);
let tomorrow = date + Duration::days(1);
for _ in 0..24 {
let mid = (lo + hi) / 2.0;
let test = Coordinates::new(mid * sign, coords.longitude);
let today_ok =
SolarTime::new(date, test).is_ok_and(|st| st.time_for_shadow(shadow).is_some());
let ok = if require_adjacent {
today_ok
&& SolarTime::new(yesterday, test).is_ok()
&& SolarTime::new(tomorrow, test).is_ok()
} else {
today_ok
};
if ok {
lo = mid;
} else {
hi = mid;
}
}
if lo > 0.0 { Some(lo * sign) } else { None }
}
check(date, coords, shadow, true)
.or_else(|| check(date, coords, shadow, false))
.unwrap_or(0.0)
}