Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions examples/19_horizontal_coordinates.rs
Original file line number Diff line number Diff line change
@@ -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::<Topocentric, Kilometer>(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<Topocentric, Horizontal, Kilometer> = eq_sph.transform(jd);
let back_sph: spherical::Position<Topocentric, EquatorialMeanOfDate, Kilometer> =
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::<Topocentric, EquatorialMeanOfDate, Kilometer>::from_spherical(&eq_sph);

let hz_cart: Position<Topocentric, Horizontal, Kilometer> = eq_cart.transform(jd);
let back_cart: Position<Topocentric, EquatorialMeanOfDate, Kilometer> = 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 ===");
}
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
136 changes: 129 additions & 7 deletions src/coordinates/transform/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -138,11 +139,7 @@ pub trait DirectionAstroExt<F: ReferenceFrame> {
/// value.
///
/// [`to_horizontal_precise`]: Self::to_horizontal_precise
fn to_horizontal(
&self,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<crate::coordinates::frames::Horizontal>
fn to_horizontal(&self, jd_tt: &JulianDate, site: &Geodetic<ECEF>) -> Direction<Horizontal>
where
Self: crate::coordinates::transform::horizontal::ToHorizontal,
{
Expand All @@ -162,14 +159,47 @@ pub trait DirectionAstroExt<F: ReferenceFrame> {
jd_tt: &JulianDate,
jd_ut1: &JulianDate,
site: &Geodetic<ECEF>,
) -> Direction<crate::coordinates::frames::Horizontal>
) -> Direction<Horizontal>
where
Self: crate::coordinates::transform::horizontal::ToHorizontal,
{
crate::coordinates::transform::horizontal::ToHorizontal::to_horizontal(
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<ECEF>,
) -> Direction<EquatorialTrueOfDate>
where
Self: FromHorizontal,
{
let ctx: AstroContext<DefaultEphemeris, DefaultEop> = 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<ECEF>,
) -> Direction<EquatorialTrueOfDate>
where
Self: FromHorizontal,
{
FromHorizontal::to_equatorial(self, jd_ut1, jd_tt, site)
}
}

impl<F: ReferenceFrame> DirectionAstroExt<F> for Direction<F> {
Expand Down Expand Up @@ -218,6 +248,44 @@ pub trait SphericalDirectionAstroExt<F: ReferenceFrame> {
where
Ctx: TransformContext,
(): FrameRotationProvider<F, F2>;

/// Converts this spherical direction to horizontal coordinates using TT only.
fn to_horizontal(
&self,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> spherical::Direction<Horizontal>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<Horizontal>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<EquatorialTrueOfDate>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<EquatorialTrueOfDate>
where
Direction<F>: FromHorizontal;
}

impl<F: ReferenceFrame> SphericalDirectionAstroExt<F> for spherical::Direction<F> {
Expand All @@ -242,6 +310,60 @@ impl<F: ReferenceFrame> SphericalDirectionAstroExt<F> for spherical::Direction<F
let cart_f2: Direction<F2> = cart.to_frame_with(jd, ctx);
spherical::Direction::from_cartesian(&cart_f2)
}

fn to_horizontal(
&self,
jd_tt: &JulianDate,
site: &Geodetic<ECEF>,
) -> spherical::Direction<Horizontal>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<Horizontal>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<EquatorialTrueOfDate>
where
Direction<F>: 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<ECEF>,
) -> spherical::Direction<EquatorialTrueOfDate>
where
Direction<F>: 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)
}
}

// =============================================================================
Expand Down
20 changes: 20 additions & 0 deletions src/coordinates/transform/horizontal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,16 @@ pub trait TopocentricEquatorialExt<U: LengthUnit> {
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, Horizontal, U>;

/// Alias for [`to_horizontal_position`](Self::to_horizontal_position).
#[inline]
fn to_horizontal(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, Horizontal, U> {
self.to_horizontal_position(jd_ut1, jd_tt)
}
}

impl<U: LengthUnit> TopocentricEquatorialExt<U> for Position<Topocentric, EquatorialTrueOfDate, U> {
Expand Down Expand Up @@ -279,6 +289,16 @@ pub trait TopocentricHorizontalExt<U: LengthUnit> {
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, EquatorialTrueOfDate, U>;

/// Alias for [`to_equatorial_position`](Self::to_equatorial_position).
#[inline]
fn to_equatorial(
&self,
jd_ut1: &JulianDate,
jd_tt: &JulianDate,
) -> Position<Topocentric, EquatorialTrueOfDate, U> {
self.to_equatorial_position(jd_ut1, jd_tt)
}
}

impl<U: LengthUnit> TopocentricHorizontalExt<U> for Position<Topocentric, Horizontal, U> {
Expand Down
Loading