use ::time::OffsetDateTime;
use crate::angle::Angle;
use crate::coords::{precess, Epoch, Equatorial};
use crate::time::julian_epoch_of;
const NGP_RA_DEG: f64 = 192.859_48;
const NGP_DEC_DEG: f64 = 27.128_25;
const NCP_L_DEG: f64 = 122.931_92;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Galactic {
pub l: Angle,
pub b: Angle,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ecliptic {
pub lambda: Angle,
pub beta: Angle,
}
pub fn to_galactic(eq: Equatorial) -> Galactic {
let eq = precess(eq, Epoch::J2000);
let (a, d) = (eq.ra().radians(), eq.dec().radians());
let (a_g, d_g) = (NGP_RA_DEG.to_radians(), NGP_DEC_DEG.to_radians());
let da = a - a_g;
let sin_b = d_g.sin() * d.sin() + d_g.cos() * d.cos() * da.cos();
let b = sin_b.clamp(-1.0, 1.0).asin();
let l = NCP_L_DEG.to_radians()
- (d.cos() * da.sin()).atan2(d.sin() * d_g.cos() - d.cos() * d_g.sin() * da.cos());
Galactic {
l: Angle::from_radians(l).normalized_0_360(),
b: Angle::from_radians(b),
}
}
pub fn from_galactic(g: Galactic) -> Equatorial {
let (l, b) = (g.l.radians(), g.b.radians());
let (a_g, d_g) = (NGP_RA_DEG.to_radians(), NGP_DEC_DEG.to_radians());
let dl = NCP_L_DEG.to_radians() - l;
let sin_d = d_g.sin() * b.sin() + d_g.cos() * b.cos() * dl.cos();
let dec = sin_d.clamp(-1.0, 1.0).asin();
let ra = a_g + (b.cos() * dl.sin()).atan2(b.sin() * d_g.cos() - b.cos() * d_g.sin() * dl.cos());
Equatorial::j2000(
Angle::from_radians(ra).normalized_0_360(),
Angle::from_radians(dec),
)
.expect("rotation output is in domain by construction")
}
pub(crate) fn mean_obliquity(at: OffsetDateTime) -> f64 {
let t = match julian_epoch_of(at) {
Epoch::OfDate(year) => (year - 2_000.0) / 100.0,
Epoch::J2000 => 0.0,
};
let arcsec = 21.448 - 46.815_0 * t - 0.000_59 * t * t + 0.001_813 * t * t * t;
(23.0 + 26.0 / 60.0 + arcsec / 3_600.0).to_radians()
}
pub fn to_ecliptic(eq: Equatorial, at: OffsetDateTime) -> Ecliptic {
let eq = precess(eq, julian_epoch_of(at));
let (a, d) = (eq.ra().radians(), eq.dec().radians());
let eps = mean_obliquity(at);
let lambda = (a.sin() * eps.cos() + d.tan() * eps.sin()).atan2(a.cos());
let sin_beta = d.sin() * eps.cos() - d.cos() * eps.sin() * a.sin();
let beta = sin_beta.clamp(-1.0, 1.0).asin();
Ecliptic {
lambda: Angle::from_radians(lambda).normalized_0_360(),
beta: Angle::from_radians(beta),
}
}
pub fn from_ecliptic(e: Ecliptic, at: OffsetDateTime) -> Equatorial {
let (l, b) = (e.lambda.radians(), e.beta.radians());
let eps = mean_obliquity(at);
let ra = (l.sin() * eps.cos() - b.tan() * eps.sin()).atan2(l.cos());
let sin_dec = b.sin() * eps.cos() + b.cos() * eps.sin() * l.sin();
let dec = sin_dec.clamp(-1.0, 1.0).asin();
let of_date = 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");
precess(of_date, Epoch::J2000)
}
#[cfg(test)]
mod tests {
use super::*;
use ::time::macros::datetime;
#[test]
fn mean_obliquity_at_j2000_is_23_4393() {
let eps = mean_obliquity(datetime!(2000-01-01 12:00 UTC)).to_degrees();
assert!((eps - 23.439_291_1).abs() < 1e-6, "ε = {eps}°");
}
}