use crate::astro::{nutation, sidereal};
use crate::coordinates::cartesian::{Direction, Position};
use crate::coordinates::centers::{Geodetic, Topocentric};
use crate::coordinates::frames::{EquatorialTrueOfDate, Horizontal, ECEF};
use crate::time::JulianDate;
use qtty::{Degrees, LengthUnit, Radians};
use std::f64::consts::TAU;
pub trait ToHorizontal {
fn to_horizontal(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<Horizontal>;
}
impl ToHorizontal for Direction<EquatorialTrueOfDate> {
#[inline]
fn to_horizontal(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<Horizontal> {
let spherical = self.to_spherical();
let ra = Radians::from(spherical.azimuth);
let dec = Radians::from(spherical.polar);
let obs_lon = Radians::from(site.lon);
let obs_lat = Radians::from(site.lat);
let nut = nutation::nutation_iau2000b(*jd_tt);
let gast = sidereal::gast_iau2006(*jd_ut1, *jd_tt, nut.dpsi, nut.true_obliquity());
let ha = gast.value() + obs_lon.value() - ra.value();
let (sh, ch) = ha.sin_cos();
let (sd, cd) = dec.sin_cos();
let (sp, cp) = obs_lat.sin_cos();
let x = -ch * cd * sp + sd * cp;
let y = -sh * cd;
let z = ch * cd * cp + sd * sp;
let r = (x * x + y * y).sqrt();
let az = if r != 0.0 { y.atan2(x) } else { 0.0 }.rem_euclid(TAU);
let alt = z.atan2(r);
let spherical_horiz = affn::spherical::Direction::<Horizontal>::new_raw(
Degrees::new(alt.to_degrees()),
Degrees::new(az.to_degrees()),
);
spherical_horiz.to_cartesian()
}
}
pub trait FromHorizontal {
fn to_equatorial(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<EquatorialTrueOfDate>;
}
impl FromHorizontal for Direction<Horizontal> {
#[inline]
fn to_equatorial(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<EquatorialTrueOfDate> {
let spherical = self.to_spherical();
let az = Radians::from(spherical.azimuth);
let alt = Radians::from(spherical.polar);
let obs_lon = Radians::from(site.lon);
let obs_lat = Radians::from(site.lat);
let nut = nutation::nutation_iau2000b(*jd_tt);
let gast = sidereal::gast_iau2006(*jd_ut1, *jd_tt, nut.dpsi, nut.true_obliquity());
let last = gast.value() + obs_lon.value();
let (sp, cp) = obs_lat.sin_cos();
let (sa, ca) = alt.sin_cos();
let (sz, cz) = az.sin_cos();
let dec = (sp * sa + cp * ca * cz).clamp(-1.0, 1.0).asin();
let ha = (-sz * ca).atan2(sa * cp - ca * cz * sp);
let ra = (last - ha).rem_euclid(TAU);
let spherical_equ = affn::spherical::Direction::<EquatorialTrueOfDate>::new_raw(
Degrees::new(dec.to_degrees()),
Degrees::new(ra.to_degrees()),
);
spherical_equ.to_cartesian()
}
}
pub trait TopocentricEquatorialExt<U: LengthUnit> {
fn to_horizontal_position(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, Horizontal, U>;
}
impl<U: LengthUnit> TopocentricEquatorialExt<U> for Position<Topocentric, EquatorialTrueOfDate, U> {
#[inline]
fn to_horizontal_position(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, Horizontal, U> {
let site = *self.center_params();
let sph = affn::Position::<Topocentric, EquatorialTrueOfDate, U>::to_spherical(self);
let equ_dir = sph.direction().to_cartesian();
let distance = sph.distance;
let horiz_cart = equ_dir.to_horizontal(jd_ut1, jd_tt, &site);
let horiz_sph = horiz_cart.to_spherical();
let result_sph = horiz_sph.position_with_params::<Topocentric, U>(site, distance);
affn::Position::<Topocentric, Horizontal, U>::from_spherical(&result_sph)
}
}
pub trait TopocentricHorizontalExt<U: LengthUnit> {
fn to_equatorial_position(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, EquatorialTrueOfDate, U>;
}
impl<U: LengthUnit> TopocentricHorizontalExt<U> for Position<Topocentric, Horizontal, U> {
#[inline]
fn to_equatorial_position(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, EquatorialTrueOfDate, U> {
let site = *self.center_params();
let sph = affn::Position::<Topocentric, Horizontal, U>::to_spherical(self);
let horiz_dir = sph.direction().to_cartesian();
let distance = sph.distance;
let equ_cart = horiz_dir.to_equatorial(jd_ut1, jd_tt, &site);
let equ_sph = equ_cart.to_spherical();
let result_sph = equ_sph.position_with_params::<Topocentric, U>(site, distance);
affn::Position::<Topocentric, EquatorialTrueOfDate, U>::from_spherical(&result_sph)
}
}
#[cfg(test)]
mod tests {
use super::*;
use qtty::*;
#[test]
fn roundtrip_equatorial_horizontal_is_stable() {
let jd_ut1 = JulianDate::new(2_451_545.0);
let jd_tt = JulianDate::new(2_451_545.000_800_741);
let ra = 1.0 * RAD;
let dec = 0.5 * RAD;
let site = Geodetic::<ECEF>::new(
0.0 * DEG,
Degrees::new((0.6 * RAD).value().to_degrees()),
0.0 * M,
);
let spherical_equ = affn::spherical::Direction::<EquatorialTrueOfDate>::new_raw(
Degrees::new(dec.value().to_degrees()),
Degrees::new(ra.value().to_degrees()),
);
let equatorial = spherical_equ.to_cartesian();
let horizontal = equatorial.to_horizontal(&jd_ut1, &jd_tt, &site);
let back = horizontal.to_equatorial(&jd_ut1, &jd_tt, &site);
let spherical = equatorial.to_spherical();
let back_spherical = back.to_spherical();
let dra = (Radians::from(back_spherical.azimuth).value()
- Radians::from(spherical.azimuth).value())
.abs();
let dra = dra.min(TAU - dra);
let ddec = (Radians::from(back_spherical.polar).value()
- Radians::from(spherical.polar).value())
.abs();
assert!(dra < 1e-12, "RA roundtrip error too large: {dra}");
assert!(ddec < 1e-12, "Dec roundtrip error too large: {ddec}");
}
#[test]
fn matches_erfa_reference_case() {
let jd_ut1 = JulianDate::new(2_451_545.0);
let jd_tt = JulianDate::new(2_451_545.000_800_741);
let ra = 1.0 * RAD;
let dec = 0.5 * RAD;
let site = Geodetic::<ECEF>::new(
0.0 * DEG,
Degrees::new((0.6 * RAD).value().to_degrees()),
0.0 * M,
);
let spherical_equ = affn::spherical::Direction::<EquatorialTrueOfDate>::new_raw(
Degrees::new(dec.value().to_degrees()),
Degrees::new(ra.value().to_degrees()),
);
let equatorial = spherical_equ.to_cartesian();
let horizontal = equatorial.to_horizontal(&jd_ut1, &jd_tt, &site);
let spherical = horizontal.to_spherical();
let az = Radians::from(spherical.azimuth).value();
let alt = Radians::from(spherical.polar).value();
let az_ref = 0.670_382_099_942_421_8_f64;
let alt_ref = -0.260_561_249_223_089_5_f64;
assert!((az - az_ref).abs() < 1e-8, "Az mismatch: {az} vs {az_ref}");
assert!(
(alt - alt_ref).abs() < 1e-8,
"Alt mismatch: {alt} vs {alt_ref}"
);
}
#[test]
fn topocentric_position_transformation() {
use affn::spherical;
use qtty::Kilometer;
let jd_ut1 = JulianDate::new(2_451_545.0);
let jd_tt = JulianDate::new(2_451_545.000_800_741);
let site = Geodetic::<ECEF>::new(0.0 * DEG, 51.5 * DEG, 100.0 * M);
let ra = 45.0 * DEG;
let dec = 30.0 * DEG;
let distance = 1000.0 * KM;
let equ_sph_dir = spherical::Direction::<EquatorialTrueOfDate>::new_raw(dec, ra);
let equ_pos_sph =
equ_sph_dir.position_with_params::<Topocentric, Kilometer>(site, distance);
let equ_pos =
affn::Position::<Topocentric, EquatorialTrueOfDate, Kilometer>::from_spherical(
&equ_pos_sph,
);
let horiz_pos = equ_pos.to_horizontal_position(&jd_ut1, &jd_tt);
assert_eq!(horiz_pos.center_params(), &site);
let horiz_dist = horiz_pos.distance();
assert!(
(horiz_dist.value() - distance.value()).abs() < 1e-10,
"Distance mismatch"
);
let back_pos = horiz_pos.to_equatorial_position(&jd_ut1, &jd_tt);
assert_eq!(back_pos.center_params(), &site);
let back_dist = back_pos.distance();
assert!(
(back_dist.value() - distance.value()).abs() < 1e-10,
"Roundtrip distance mismatch"
);
}
}