diff --git a/examples/19_horizontal_coordinates.rs b/examples/19_horizontal_coordinates.rs new file mode 100644 index 00000000..8db0c9a6 --- /dev/null +++ b/examples/19_horizontal_coordinates.rs @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright (C) 2026 Vallés Puig, Ramon + +//! Horizontal Coordinates Example +//! +//! Demonstrates equatorial ↔ horizontal transforms for spherical and cartesian +//! directions and positions, with round-trip checks. +//! +//! Positions use **mean-of-date + GMST** via [`.transform(jd)`]. Directions use +//! **true-of-date + GAST** via [`.to_horizontal`] / [`.to_equatorial`] (observer +//! site passed explicitly). See `doc/frames_and_centers.md` for frame details. +//! +//! Run with: `cargo run --example 19_horizontal_coordinates` +#![allow(clippy::print_stdout)] + +use siderust::catalogs::observatories::ROQUE_DE_LOS_MUCHACHOS; +use siderust::coordinates::cartesian::Position; +use siderust::coordinates::centers::Topocentric; +use siderust::coordinates::frames::{EquatorialMeanOfDate, Horizontal}; +use siderust::coordinates::spherical; +use siderust::coordinates::transform::{DirectionAstroExt, SphericalDirectionAstroExt, Transform}; +use siderust::qtty::*; +use siderust::time::JulianDate; + +fn main() { + println!("=== Horizontal Coordinates Example ===\n"); + + let site = ROQUE_DE_LOS_MUCHACHOS.geodetic(); + let jd = JulianDate::new(2_459_015.5); // 2020-01-01 12:00 TT + + // Sirius apparent equatorial coordinates (degrees). + let ra = Degrees::new(101.287); + let dec = Degrees::new(-16.716); + let distance = 384_400.0 * KM; + + println!("Observer: {}", ROQUE_DE_LOS_MUCHACHOS.name); + println!("Epoch (TT): {jd:.6}\n"); + + // ========================================================================= + // 1. Spherical positions (EquatorialMeanOfDate → Horizontal, GMST) + // ========================================================================= + println!("1. SPHERICAL POSITIONS (GMST, .transform)"); + println!("-------------------------------------------"); + + let eq_mod_dir = spherical::direction::EquatorialMeanOfDate::new(ra, dec); + let eq_sph = eq_mod_dir.position_with_params::(site, distance); + + println!("EquatorialMeanOfDate (topocentric):"); + println!( + " RA = {:.4}, Dec = {:.4}, dist = {}", + eq_sph.ra(), + eq_sph.dec(), + eq_sph.distance + ); + + let hz_sph: spherical::Position = eq_sph.transform(jd); + let back_sph: spherical::Position = + hz_sph.transform(jd); + + println!("Horizontal:"); + println!( + " Alt = {:.4}, Az = {:.4}, dist = {}", + hz_sph.alt(), + hz_sph.az(), + hz_sph.distance + ); + println!( + " Round-trip distance residual: {}\n", + (eq_sph.distance - back_sph.distance).abs() + ); + + // ========================================================================= + // 2. Cartesian positions (EquatorialMeanOfDate → Horizontal, GMST) + // ========================================================================= + println!("2. CARTESIAN POSITIONS (GMST, .transform)"); + println!("-------------------------------------------"); + + let eq_cart = Position::::from_spherical(&eq_sph); + + let hz_cart: Position = eq_cart.transform(jd); + let back_cart: Position = hz_cart.transform(jd); + + println!( + "EquatorialMeanOfDate (cartesian): dist = {}", + eq_cart.distance() + ); + println!( + "Horizontal (cartesian): Alt = {:.4}, Az = {:.4}, dist = {}", + hz_cart.to_spherical().alt(), + hz_cart.to_spherical().az(), + hz_cart.distance() + ); + println!( + " Round-trip distance residual: {}\n", + (eq_cart.distance() - back_cart.distance()).abs() + ); + + // ========================================================================= + // 3. Spherical directions (EquatorialTrueOfDate → Horizontal, GAST) + // ========================================================================= + println!("3. SPHERICAL DIRECTIONS (GAST, .to_horizontal / .to_equatorial)"); + println!("----------------------------------------------------------------"); + + let eq_tod = spherical::direction::EquatorialTrueOfDate::new(ra, dec); + + println!("EquatorialTrueOfDate:"); + println!(" RA = {:.4}, Dec = {:.4}", eq_tod.ra(), eq_tod.dec()); + + let hz_tod = eq_tod.to_horizontal(&jd, &site); + let back_tod = hz_tod.to_equatorial(&jd, &site); + + println!("Horizontal:"); + println!(" Alt = {:.4}, Az = {:.4}", hz_tod.alt(), hz_tod.az()); + println!( + " Round-trip angular separation: {}\n", + eq_tod.angular_separation(&back_tod) + ); + + // ========================================================================= + // 4. Cartesian directions (EquatorialTrueOfDate → Horizontal, GAST) + // ========================================================================= + println!("4. CARTESIAN DIRECTIONS (GAST, .to_horizontal / .to_equatorial)"); + println!("-----------------------------------------------------------------"); + + let eq_cart_dir = eq_tod.to_cartesian(); + let hz_cart_dir = eq_cart_dir.to_horizontal(&jd, &site); + let back_cart_dir = hz_cart_dir.to_equatorial(&jd, &site); + + let hz_cart_sph = hz_cart_dir.to_spherical(); + let back_cart_sph = back_cart_dir.to_spherical(); + + println!("Horizontal (cartesian):"); + println!( + " Alt = {:.4}, Az = {:.4}", + hz_cart_sph.alt(), + hz_cart_sph.az() + ); + println!( + " Round-trip angular separation: {}\n", + eq_tod.angular_separation(&back_cart_sph) + ); + + println!("=== Example Complete ==="); +} diff --git a/examples/README.md b/examples/README.md index 81dece57..ec7e5de3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,6 +16,7 @@ Feature-gated examples list the required feature in the command. - `04_all_center_conversions`: supported center shifts and round-trip checks. - `13_coordinate_operations`: angular separation, distances, and displacement algebra. - `14_nutation_models`: default and custom nutation-model transform paths. +- `19_horizontal_coordinates`: equatorial ↔ horizontal for spherical/cartesian directions and positions. ```bash cargo run --example 01_basic_coordinates @@ -24,6 +25,7 @@ cargo run --example 03_all_frames_conversions cargo run --example 04_all_center_conversions cargo run --example 13_coordinate_operations cargo run --example 14_nutation_models +cargo run --example 19_horizontal_coordinates ``` ## Observing Workflows diff --git a/src/coordinates/transform/ext.rs b/src/coordinates/transform/ext.rs index efd34677..d8fb3014 100644 --- a/src/coordinates/transform/ext.rs +++ b/src/coordinates/transform/ext.rs @@ -73,12 +73,13 @@ use crate::coordinates::cartesian::{Direction, Position, Vector}; use crate::coordinates::centers::{Geodetic, ReferenceCenter}; -use crate::coordinates::frames::{ReferenceFrame, ECEF}; +use crate::coordinates::frames::{EquatorialTrueOfDate, Horizontal, ReferenceFrame, ECEF}; use crate::coordinates::spherical; use crate::coordinates::transform::centers::TransformCenter; use crate::coordinates::transform::context::{ AstroContext, DefaultEop, DefaultEphemeris, TransformContext, }; +use crate::coordinates::transform::horizontal::FromHorizontal; use crate::coordinates::transform::providers::{ frame_rotation_with, CenterShiftProvider, FrameRotationProvider, }; @@ -138,11 +139,7 @@ pub trait DirectionAstroExt { /// value. /// /// [`to_horizontal_precise`]: Self::to_horizontal_precise - fn to_horizontal( - &self, - jd_tt: &JulianDate, - site: &Geodetic, - ) -> Direction + fn to_horizontal(&self, jd_tt: &JulianDate, site: &Geodetic) -> Direction where Self: crate::coordinates::transform::horizontal::ToHorizontal, { @@ -162,7 +159,7 @@ pub trait DirectionAstroExt { jd_tt: &JulianDate, jd_ut1: &JulianDate, site: &Geodetic, - ) -> Direction + ) -> Direction where Self: crate::coordinates::transform::horizontal::ToHorizontal, { @@ -170,6 +167,39 @@ pub trait DirectionAstroExt { self, jd_ut1, jd_tt, site, ) } + + /// Converts this horizontal direction to equatorial (true of date) using TT only. + /// + /// UT1 is derived from TT using the context's EOP provider. For sub-second + /// precision, prefer [`to_equatorial_precise`] with an explicit UT1 value. + /// + /// [`to_equatorial_precise`]: Self::to_equatorial_precise + fn to_equatorial( + &self, + jd_tt: &JulianDate, + site: &Geodetic, + ) -> Direction + where + Self: FromHorizontal, + { + let ctx: AstroContext = AstroContext::default(); + let eop = ctx.eop_at_tt(*jd_tt); + let jd_ut1 = crate::astro::earth_rotation::jd_ut1_from_tt_eop(*jd_tt, &eop); + FromHorizontal::to_equatorial(self, &jd_ut1, jd_tt, site) + } + + /// Converts this horizontal direction to equatorial (true of date) with explicit UT1+TT. + fn to_equatorial_precise( + &self, + jd_tt: &JulianDate, + jd_ut1: &JulianDate, + site: &Geodetic, + ) -> Direction + where + Self: FromHorizontal, + { + FromHorizontal::to_equatorial(self, jd_ut1, jd_tt, site) + } } impl DirectionAstroExt for Direction { @@ -218,6 +248,44 @@ pub trait SphericalDirectionAstroExt { where Ctx: TransformContext, (): FrameRotationProvider; + + /// Converts this spherical direction to horizontal coordinates using TT only. + fn to_horizontal( + &self, + jd_tt: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: crate::coordinates::transform::horizontal::ToHorizontal; + + /// Converts this spherical direction to horizontal coordinates with explicit UT1+TT. + fn to_horizontal_precise( + &self, + jd_tt: &JulianDate, + jd_ut1: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: crate::coordinates::transform::horizontal::ToHorizontal; + + /// Converts this horizontal spherical direction to equatorial (true of date) using TT only. + fn to_equatorial( + &self, + jd_tt: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: FromHorizontal; + + /// Converts this horizontal spherical direction to equatorial (true of date) with explicit UT1+TT. + fn to_equatorial_precise( + &self, + jd_tt: &JulianDate, + jd_ut1: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: FromHorizontal; } impl SphericalDirectionAstroExt for spherical::Direction { @@ -242,6 +310,60 @@ impl SphericalDirectionAstroExt for spherical::Direction = cart.to_frame_with(jd, ctx); spherical::Direction::from_cartesian(&cart_f2) } + + fn to_horizontal( + &self, + jd_tt: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: crate::coordinates::transform::horizontal::ToHorizontal, + { + let cart = self.to_cartesian(); + let horiz_cart = cart.to_horizontal(jd_tt, site); + spherical::Direction::from_cartesian(&horiz_cart) + } + + fn to_horizontal_precise( + &self, + jd_tt: &JulianDate, + jd_ut1: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: crate::coordinates::transform::horizontal::ToHorizontal, + { + let cart = self.to_cartesian(); + let horiz_cart = cart.to_horizontal_precise(jd_tt, jd_ut1, site); + spherical::Direction::from_cartesian(&horiz_cart) + } + + fn to_equatorial( + &self, + jd_tt: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: FromHorizontal, + { + let cart = self.to_cartesian(); + let equ_cart = DirectionAstroExt::to_equatorial(&cart, jd_tt, site); + spherical::Direction::from_cartesian(&equ_cart) + } + + fn to_equatorial_precise( + &self, + jd_tt: &JulianDate, + jd_ut1: &JulianDate, + site: &Geodetic, + ) -> spherical::Direction + where + Direction: FromHorizontal, + { + let cart = self.to_cartesian(); + let equ_cart = DirectionAstroExt::to_equatorial_precise(&cart, jd_tt, jd_ut1, site); + spherical::Direction::from_cartesian(&equ_cart) + } } // ============================================================================= diff --git a/src/coordinates/transform/horizontal.rs b/src/coordinates/transform/horizontal.rs index dc48830c..49d24ff9 100644 --- a/src/coordinates/transform/horizontal.rs +++ b/src/coordinates/transform/horizontal.rs @@ -231,6 +231,16 @@ pub trait TopocentricEquatorialExt { jd_ut1: &JulianDate, jd_tt: &JulianDate, ) -> Position; + + /// Alias for [`to_horizontal_position`](Self::to_horizontal_position). + #[inline] + fn to_horizontal( + &self, + jd_ut1: &JulianDate, + jd_tt: &JulianDate, + ) -> Position { + self.to_horizontal_position(jd_ut1, jd_tt) + } } impl TopocentricEquatorialExt for Position { @@ -279,6 +289,16 @@ pub trait TopocentricHorizontalExt { jd_ut1: &JulianDate, jd_tt: &JulianDate, ) -> Position; + + /// Alias for [`to_equatorial_position`](Self::to_equatorial_position). + #[inline] + fn to_equatorial( + &self, + jd_ut1: &JulianDate, + jd_tt: &JulianDate, + ) -> Position { + self.to_equatorial_position(jd_ut1, jd_tt) + } } impl TopocentricHorizontalExt for Position {