use ::time::OffsetDateTime;
use crate::angle::Angle;
use crate::coords::Equatorial;
use crate::frames::mean_obliquity;
use crate::observer::{moving_body_crossings, CrossingOutcome, Location};
use crate::time::{julian_date, julian_epoch_of};
const J2000_JD: f64 = 2_451_545.0;
pub(crate) fn solar_coords(at: OffsetDateTime) -> (f64, f64, f64) {
let t = (julian_date(at) - J2000_JD) / 36_525.0;
let l0 = 280.466_46 + 36_000.769_83 * t + 0.000_303_2 * t * t;
let m = (357.529_11 + 35_999.050_29 * t - 0.000_153_7 * t * t).to_radians();
let e = 0.016_708_634 - 0.000_042_037 * t - 0.000_000_126_7 * t * t;
let c = (1.914_602 - 0.004_817 * t - 0.000_014 * t * t) * m.sin()
+ (0.019_993 - 0.000_101 * t) * (2.0 * m).sin()
+ 0.000_289 * (3.0 * m).sin();
let true_longitude = l0 + c;
let nu = m + c.to_radians();
let r = 1.000_001_018 * (1.0 - e * e) / (1.0 + e * nu.cos());
let omega = (125.04 - 1_934.136 * t).to_radians();
let lambda = (true_longitude - 0.005_69 - 0.004_78 * omega.sin()).rem_euclid(360.0);
let eps = mean_obliquity(at) + (0.002_56 * omega.cos()).to_radians();
(lambda, r, eps)
}
pub fn sun_position(at: OffsetDateTime) -> Equatorial {
let (lambda, _, eps) = solar_coords(at);
let lambda = lambda.to_radians();
let ra = (lambda.sin() * eps.cos()).atan2(lambda.cos());
let dec = (eps.sin() * lambda.sin()).asin();
Equatorial::at_epoch(
Angle::from_radians(ra).normalized_0_360(),
Angle::from_radians(dec),
julian_epoch_of(at),
)
.expect("rotation output is in domain by construction")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Twilight {
Civil,
Nautical,
Astronomical,
}
impl Twilight {
#[must_use]
pub fn threshold(self) -> Angle {
Angle::from_degrees(match self {
Twilight::Civil => -6.0,
Twilight::Nautical => -12.0,
Twilight::Astronomical => -18.0,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum TwilightOutcome {
Night {
dusk: OffsetDateTime,
dawn: OffsetDateTime,
},
NeverDark,
AlwaysDark,
}
pub fn twilight(kind: Twilight, night_of: OffsetDateTime, site: &Location) -> TwilightOutcome {
match moving_body_crossings(sun_position, kind.threshold(), night_of, site, 180.0) {
CrossingOutcome::AlwaysAbove => TwilightOutcome::NeverDark,
CrossingOutcome::NeverAbove => TwilightOutcome::AlwaysDark,
CrossingOutcome::Crosses { rise, set } => TwilightOutcome::Night {
dusk: set,
dawn: rise,
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use ::time::macros::datetime;
#[test]
fn meeus_25a_solar_position() {
let at = datetime!(1992-10-13 00:00 UTC);
let (lambda, r, _) = solar_coords(at);
assert!((lambda - 199.908_95).abs() < 5e-4, "λ = {lambda}");
assert!((r - 0.997_66).abs() < 1e-5, "R = {r}");
let p = sun_position(at);
assert!(
(p.ra().degrees() - 198.380_83).abs() < 5e-3,
"α {}",
p.ra().degrees()
);
assert!(
(p.dec().degrees() + 7.785_07).abs() < 5e-3,
"δ {}",
p.dec().degrees()
);
}
#[test]
fn twilight_thresholds() {
for (kind, expect) in [
(Twilight::Civil, -6.0),
(Twilight::Nautical, -12.0),
(Twilight::Astronomical, -18.0),
] {
assert!((kind.threshold().degrees() - expect).abs() < 1e-12);
}
}
}