Skip to main content

siderust/coordinates/transform/
ext.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 Vallés Puig, Ramon
3
4//! # Coordinate Extension Traits
5//!
6//! This module provides extension traits that add ergonomic transformation
7//! methods to `affn` coordinate types. These traits enable method-chaining
8//! style transformations with compile-time type safety.
9//!
10//! ## Design
11//!
12//! The default API uses IAU models with no context argument required:
13//!
14//! - `to_frame::<F2>(jd_tt)`, Rotate to a new reference frame.
15//! - `to::<C2, F2>(jd_tt)`, Combined center and frame transformation.
16//!
17//! Center-only transforms are exposed on the [`TransformCenter`] trait:
18//!
19//! - `pos.to_center(params, jd)`, Shift to a new reference center (all variants).
20//! - `pos.to_center_with(params, jd, &ctx)`, Same with a custom context.
21//!
22//! For expert overrides, a `_with` suffix variant accepts any
23//! [`crate::coordinates::transform::context::TransformContext`]:
24//!
25//! - `to_frame_with::<F2>(jd_tt, &ctx)`, Frame rotation with custom context.
26//! - `to_with::<C2, F2>(jd_tt, &ctx)`, Combined transform with custom context.
27//!
28//! A plain [`AstroContext`] uses the default IAU 2006A model. To bind another
29//! model at compile time, derive a [`ModelContext`](crate::coordinates::transform::context::ModelContext)
30//! with [`AstroContext::with_model`](crate::coordinates::transform::context::AstroContext::with_model):
31//!
32//! ```rust,ignore
33//! use siderust::astro::nutation::Iau2000B;
34//!
35//! let ctx = AstroContext::new();
36//! let low_cost = ctx.with_model::<Iau2000B>();
37//! let dir = dir.to_frame_with::<EclipticMeanJ2000>(&jd, &low_cost);
38//! ```
39//!
40//! Alternatively, wrap a coordinate with a custom context using
41//! [`WithEngine`] and use the same method names:
42//!
43//! ```rust,ignore
44//! coord.using(&engine).to_frame::<F2>(&jd);
45//! ```
46//!
47//! ## Transformation Order
48//!
49//! For combined transformations (`to`), the order is:
50//! 1. **Center first** (in source frame): Translate the position.
51//! 2. **Then frame**: Rotate to the target frame.
52//!
53//! This order is chosen because:
54//! - Center shifts depend on body positions which are frame-dependent.
55//! - Shifting in the source frame before rotating is more intuitive.
56//!
57//! ## Example
58//!
59//! ```rust
60//! use siderust::coordinates::transform::ext::PositionAstroExt;
61//! use siderust::coordinates::cartesian::Position;
62//! use siderust::coordinates::centers::{Barycentric, Geocentric};
63//! use siderust::coordinates::frames::{EclipticMeanJ2000, ICRS};
64//! use siderust::time::JulianDate;
65//! use siderust::qtty::AstronomicalUnit;
66//!
67//! let pos = Position::<Barycentric, EclipticMeanJ2000, AstronomicalUnit>::new(1.0, 0.5, 0.2);
68//! let jd = siderust::J2000;
69//!
70//! // Transform to Geocentric ICRS, no context needed
71//! let geo_icrs: Position<Geocentric, ICRS, AstronomicalUnit> = pos.to(&jd);
72//! ```
73
74use crate::coordinates::cartesian::{Direction, Position, Vector};
75use crate::coordinates::centers::{Geodetic, ReferenceCenter};
76use crate::coordinates::frames::{EquatorialTrueOfDate, Horizontal, ReferenceFrame, ECEF};
77use crate::coordinates::spherical;
78use crate::coordinates::transform::centers::TransformCenter;
79use crate::coordinates::transform::context::{
80    AstroContext, DefaultEop, DefaultEphemeris, TransformContext,
81};
82use crate::coordinates::transform::horizontal::FromHorizontal;
83use crate::coordinates::transform::providers::{
84    frame_rotation_with, CenterShiftProvider, FrameRotationProvider,
85};
86use crate::qtty::{LengthUnit, Unit};
87#[cfg(not(feature = "std"))]
88use crate::qtty::{Real, Scalar};
89use crate::time::JulianDate;
90use affn::Rotation3;
91
92// =============================================================================
93// DirectionAstroExt - Extension trait for Direction<F>
94// =============================================================================
95
96/// Extension trait for `Direction<F>` providing frame transformations.
97///
98/// Directions are unit vectors (translation-invariant), so only frame
99/// rotations apply. Center transformations are not meaningful for directions.
100pub trait DirectionAstroExt<F: ReferenceFrame> {
101    /// Rotates this direction to a new reference frame using IAU defaults.
102    ///
103    /// # Type Parameters
104    ///
105    /// - `F2`: The target reference frame.
106    ///
107    /// # Arguments
108    ///
109    /// - `jd`: The Julian Date (TT) for time-dependent rotations.
110    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Direction<F2>
111    where
112        (): FrameRotationProvider<F, F2>;
113
114    /// Rotates this direction to a new reference frame with a custom context.
115    fn to_frame_with<F2: ReferenceFrame, Ctx>(&self, jd: &JulianDate, ctx: &Ctx) -> Direction<F2>
116    where
117        Ctx: TransformContext,
118        (): FrameRotationProvider<F, F2>;
119
120    /// Converts this direction to ecliptic-of-date coordinates (convenience).
121    ///
122    /// Available for ICRS and GCRS directions via the provider system.
123    fn to_ecliptic_of_date(
124        &self,
125        jd_tt: &JulianDate,
126    ) -> Direction<crate::coordinates::frames::EclipticTrueOfDate>
127    where
128        Self: crate::coordinates::transform::ecliptic_of_date::ToEclipticTrueOfDate,
129    {
130        crate::coordinates::transform::ecliptic_of_date::ToEclipticTrueOfDate::to_ecliptic_of_date(
131            self, jd_tt,
132        )
133    }
134
135    /// Converts this direction to horizontal coordinates using TT only.
136    ///
137    /// UT1 is derived from TT using the context's EOP provider
138    /// (`IersEop` by default), which applies the IERS `dUT1 = UT1 − UTC`
139    /// correction on top of tempoch's leap-second chain. For sub-second
140    /// precision, prefer [`to_horizontal_precise`] with an explicit UT1
141    /// value.
142    ///
143    /// [`to_horizontal_precise`]: Self::to_horizontal_precise
144    fn to_horizontal(&self, jd_tt: &JulianDate, site: &Geodetic<ECEF>) -> Direction<Horizontal>
145    where
146        Self: crate::coordinates::transform::horizontal::ToHorizontal,
147    {
148        let ctx: AstroContext<DefaultEphemeris, DefaultEop> = AstroContext::default();
149        let eop = ctx.eop_at_tt(*jd_tt);
150        let jd_ut1 = crate::astro::earth_rotation::jd_ut1_from_tt_eop(*jd_tt, &eop);
151        crate::coordinates::transform::horizontal::ToHorizontal::to_horizontal(
152            self, &jd_ut1, jd_tt, site,
153        )
154    }
155
156    /// Converts this direction to horizontal coordinates with explicit UT1+TT.
157    ///
158    /// Use this when you have a precise UT1 value (e.g. from IERS EOP).
159    fn to_horizontal_precise(
160        &self,
161        jd_tt: &JulianDate,
162        jd_ut1: &JulianDate,
163        site: &Geodetic<ECEF>,
164    ) -> Direction<Horizontal>
165    where
166        Self: crate::coordinates::transform::horizontal::ToHorizontal,
167    {
168        crate::coordinates::transform::horizontal::ToHorizontal::to_horizontal(
169            self, jd_ut1, jd_tt, site,
170        )
171    }
172
173    /// Converts this horizontal direction to equatorial (true of date) using TT only.
174    ///
175    /// UT1 is derived from TT using the context's EOP provider. For sub-second
176    /// precision, prefer [`to_equatorial_precise`] with an explicit UT1 value.
177    ///
178    /// [`to_equatorial_precise`]: Self::to_equatorial_precise
179    fn to_equatorial(
180        &self,
181        jd_tt: &JulianDate,
182        site: &Geodetic<ECEF>,
183    ) -> Direction<EquatorialTrueOfDate>
184    where
185        Self: FromHorizontal,
186    {
187        let ctx: AstroContext<DefaultEphemeris, DefaultEop> = AstroContext::default();
188        let eop = ctx.eop_at_tt(*jd_tt);
189        let jd_ut1 = crate::astro::earth_rotation::jd_ut1_from_tt_eop(*jd_tt, &eop);
190        FromHorizontal::to_equatorial(self, &jd_ut1, jd_tt, site)
191    }
192
193    /// Converts this horizontal direction to equatorial (true of date) with explicit UT1+TT.
194    fn to_equatorial_precise(
195        &self,
196        jd_tt: &JulianDate,
197        jd_ut1: &JulianDate,
198        site: &Geodetic<ECEF>,
199    ) -> Direction<EquatorialTrueOfDate>
200    where
201        Self: FromHorizontal,
202    {
203        FromHorizontal::to_equatorial(self, jd_ut1, jd_tt, site)
204    }
205}
206
207impl<F: ReferenceFrame> DirectionAstroExt<F> for Direction<F> {
208    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Direction<F2>
209    where
210        (): FrameRotationProvider<F, F2>,
211    {
212        let ctx: AstroContext = AstroContext::default();
213        self.to_frame_with(jd, &ctx)
214    }
215
216    fn to_frame_with<F2: ReferenceFrame, Ctx>(&self, jd: &JulianDate, ctx: &Ctx) -> Direction<F2>
217    where
218        Ctx: TransformContext,
219        (): FrameRotationProvider<F, F2>,
220    {
221        let rot: Rotation3 = frame_rotation_with::<F, F2, Ctx>(*jd, ctx);
222        let [x, y, z] = rot.apply_array([self.x(), self.y(), self.z()]);
223        // The result is still normalized (rotations preserve length)
224        Direction::new_unchecked(x, y, z)
225    }
226}
227
228// =============================================================================
229// SphericalDirectionAstroExt - Extension trait for spherical::Direction<F>
230// =============================================================================
231
232/// Extension trait for `spherical::Direction<F>` providing time-dependent
233/// frame transformations via the provider system.
234///
235/// This is the spherical counterpart of [`DirectionAstroExt`]. Internally,
236/// it converts to a cartesian [`Direction`], applies the rotation, and converts
237/// back.
238pub trait SphericalDirectionAstroExt<F: ReferenceFrame> {
239    /// Rotates this spherical direction to a new reference frame (IAU defaults).
240    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> spherical::Direction<F2>
241    where
242        (): FrameRotationProvider<F, F2>;
243
244    /// Rotates this spherical direction to a new reference frame with custom context.
245    fn to_frame_with<F2: ReferenceFrame, Ctx>(
246        &self,
247        jd: &JulianDate,
248        ctx: &Ctx,
249    ) -> spherical::Direction<F2>
250    where
251        Ctx: TransformContext,
252        (): FrameRotationProvider<F, F2>;
253
254    /// Converts this spherical direction to horizontal coordinates using TT only.
255    fn to_horizontal(
256        &self,
257        jd_tt: &JulianDate,
258        site: &Geodetic<ECEF>,
259    ) -> spherical::Direction<Horizontal>
260    where
261        Direction<F>: crate::coordinates::transform::horizontal::ToHorizontal;
262
263    /// Converts this spherical direction to horizontal coordinates with explicit UT1+TT.
264    fn to_horizontal_precise(
265        &self,
266        jd_tt: &JulianDate,
267        jd_ut1: &JulianDate,
268        site: &Geodetic<ECEF>,
269    ) -> spherical::Direction<Horizontal>
270    where
271        Direction<F>: crate::coordinates::transform::horizontal::ToHorizontal;
272
273    /// Converts this horizontal spherical direction to equatorial (true of date) using TT only.
274    fn to_equatorial(
275        &self,
276        jd_tt: &JulianDate,
277        site: &Geodetic<ECEF>,
278    ) -> spherical::Direction<EquatorialTrueOfDate>
279    where
280        Direction<F>: FromHorizontal;
281
282    /// Converts this horizontal spherical direction to equatorial (true of date) with explicit UT1+TT.
283    fn to_equatorial_precise(
284        &self,
285        jd_tt: &JulianDate,
286        jd_ut1: &JulianDate,
287        site: &Geodetic<ECEF>,
288    ) -> spherical::Direction<EquatorialTrueOfDate>
289    where
290        Direction<F>: FromHorizontal;
291}
292
293impl<F: ReferenceFrame> SphericalDirectionAstroExt<F> for spherical::Direction<F> {
294    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> spherical::Direction<F2>
295    where
296        (): FrameRotationProvider<F, F2>,
297    {
298        let ctx: AstroContext = AstroContext::default();
299        self.to_frame_with(jd, &ctx)
300    }
301
302    fn to_frame_with<F2: ReferenceFrame, Ctx>(
303        &self,
304        jd: &JulianDate,
305        ctx: &Ctx,
306    ) -> spherical::Direction<F2>
307    where
308        Ctx: TransformContext,
309        (): FrameRotationProvider<F, F2>,
310    {
311        let cart: Direction<F> = self.to_cartesian();
312        let cart_f2: Direction<F2> = cart.to_frame_with(jd, ctx);
313        spherical::Direction::from_cartesian(&cart_f2)
314    }
315
316    fn to_horizontal(
317        &self,
318        jd_tt: &JulianDate,
319        site: &Geodetic<ECEF>,
320    ) -> spherical::Direction<Horizontal>
321    where
322        Direction<F>: crate::coordinates::transform::horizontal::ToHorizontal,
323    {
324        let cart = self.to_cartesian();
325        let horiz_cart = cart.to_horizontal(jd_tt, site);
326        spherical::Direction::from_cartesian(&horiz_cart)
327    }
328
329    fn to_horizontal_precise(
330        &self,
331        jd_tt: &JulianDate,
332        jd_ut1: &JulianDate,
333        site: &Geodetic<ECEF>,
334    ) -> spherical::Direction<Horizontal>
335    where
336        Direction<F>: crate::coordinates::transform::horizontal::ToHorizontal,
337    {
338        let cart = self.to_cartesian();
339        let horiz_cart = cart.to_horizontal_precise(jd_tt, jd_ut1, site);
340        spherical::Direction::from_cartesian(&horiz_cart)
341    }
342
343    fn to_equatorial(
344        &self,
345        jd_tt: &JulianDate,
346        site: &Geodetic<ECEF>,
347    ) -> spherical::Direction<EquatorialTrueOfDate>
348    where
349        Direction<F>: FromHorizontal,
350    {
351        let cart = self.to_cartesian();
352        let equ_cart = DirectionAstroExt::to_equatorial(&cart, jd_tt, site);
353        spherical::Direction::from_cartesian(&equ_cart)
354    }
355
356    fn to_equatorial_precise(
357        &self,
358        jd_tt: &JulianDate,
359        jd_ut1: &JulianDate,
360        site: &Geodetic<ECEF>,
361    ) -> spherical::Direction<EquatorialTrueOfDate>
362    where
363        Direction<F>: FromHorizontal,
364    {
365        let cart = self.to_cartesian();
366        let equ_cart = DirectionAstroExt::to_equatorial_precise(&cart, jd_tt, jd_ut1, site);
367        spherical::Direction::from_cartesian(&equ_cart)
368    }
369}
370
371// =============================================================================
372// VectorAstroExt - Extension trait for Vector<F, U>
373// =============================================================================
374
375/// Extension trait for `Vector<F, U>` providing frame transformations.
376///
377/// Vectors (displacements, velocities) are free vectors that are
378/// translation-invariant. Only frame rotations apply.
379pub trait VectorAstroExt<F: ReferenceFrame, U: Unit> {
380    /// Rotates this vector to a new reference frame (IAU defaults).
381    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Vector<F2, U>
382    where
383        (): FrameRotationProvider<F, F2>;
384
385    /// Rotates this vector to a new reference frame with custom context.
386    fn to_frame_with<F2: ReferenceFrame, Ctx>(&self, jd: &JulianDate, ctx: &Ctx) -> Vector<F2, U>
387    where
388        Ctx: TransformContext,
389        (): FrameRotationProvider<F, F2>;
390}
391
392impl<F: ReferenceFrame, U: Unit> VectorAstroExt<F, U> for Vector<F, U> {
393    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Vector<F2, U>
394    where
395        (): FrameRotationProvider<F, F2>,
396    {
397        let ctx: AstroContext = AstroContext::default();
398        self.to_frame_with(jd, &ctx)
399    }
400
401    fn to_frame_with<F2: ReferenceFrame, Ctx>(&self, jd: &JulianDate, ctx: &Ctx) -> Vector<F2, U>
402    where
403        Ctx: TransformContext,
404        (): FrameRotationProvider<F, F2>,
405    {
406        let rot: Rotation3 = frame_rotation_with::<F, F2, Ctx>(*jd, ctx);
407        let [x, y, z] = rot * [self.x(), self.y(), self.z()];
408        Vector::new(x, y, z)
409    }
410}
411
412// =============================================================================
413// PositionAstroExt - Extension trait for Position<C, F, U>
414// =============================================================================
415
416/// Extension trait for `Position<C, F, U>` providing frame transformations
417/// and combined center+frame transformations.
418///
419/// Positions are affine points that can undergo both frame rotations and
420/// center translations.
421///
422/// **Center-only transforms** are provided by [`TransformCenter`] (use
423/// `pos.to_center(params, jd)`).
424///
425/// Default methods use IAU models with no context argument.
426/// `_with` variants accept an [`AstroContext`] for expert overrides.
427pub trait PositionAstroExt<C: ReferenceCenter, F: ReferenceFrame, U: LengthUnit> {
428    /// Rotates this position to a new reference frame (IAU defaults).
429    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Position<C, F2, U>
430    where
431        (): FrameRotationProvider<F, F2>;
432
433    /// Rotates this position to a new reference frame with custom context.
434    fn to_frame_with<F2: ReferenceFrame, Ctx>(
435        &self,
436        jd: &JulianDate,
437        ctx: &Ctx,
438    ) -> Position<C, F2, U>
439    where
440        Ctx: TransformContext,
441        (): FrameRotationProvider<F, F2>;
442
443    /// Transforms this position to a new center and frame (IAU defaults).
444    ///
445    /// # Transformation Order
446    ///
447    /// 1. Center shift (in source frame F): `C → C2`
448    /// 2. Frame rotation: `F → F2`
449    fn to<C2: ReferenceCenter<Params = ()>, F2: ReferenceFrame>(
450        &self,
451        jd: &JulianDate,
452    ) -> Position<C2, F2, U>
453    where
454        (): CenterShiftProvider<C, C2, F>,
455        (): FrameRotationProvider<F, F2>;
456
457    /// Transforms this position to a new center and frame with custom context.
458    fn to_with<C2: ReferenceCenter<Params = ()>, F2: ReferenceFrame, Ctx>(
459        &self,
460        jd: &JulianDate,
461        ctx: &Ctx,
462    ) -> Position<C2, F2, U>
463    where
464        Ctx: TransformContext,
465        Ctx::Eph: crate::ephemeris::Ephemeris,
466        (): CenterShiftProvider<C, C2, F>,
467        (): FrameRotationProvider<F, F2>;
468}
469
470impl<C, F, U> PositionAstroExt<C, F, U> for Position<C, F, U>
471where
472    C: ReferenceCenter<Params = ()>,
473    F: ReferenceFrame,
474    U: LengthUnit,
475{
476    fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Position<C, F2, U>
477    where
478        (): FrameRotationProvider<F, F2>,
479    {
480        let ctx: AstroContext = AstroContext::default();
481        self.to_frame_with(jd, &ctx)
482    }
483
484    fn to_frame_with<F2: ReferenceFrame, Ctx>(
485        &self,
486        jd: &JulianDate,
487        ctx: &Ctx,
488    ) -> Position<C, F2, U>
489    where
490        Ctx: TransformContext,
491        (): FrameRotationProvider<F, F2>,
492    {
493        let rot: Rotation3 = frame_rotation_with::<F, F2, Ctx>(*jd, ctx);
494        let [x, y, z] = rot * [self.x(), self.y(), self.z()];
495        Position::new(x, y, z)
496    }
497
498    fn to<C2: ReferenceCenter<Params = ()>, F2: ReferenceFrame>(
499        &self,
500        jd: &JulianDate,
501    ) -> Position<C2, F2, U>
502    where
503        (): CenterShiftProvider<C, C2, F>,
504        (): FrameRotationProvider<F, F2>,
505    {
506        let ctx: AstroContext = AstroContext::default();
507        self.to_with(jd, &ctx)
508    }
509
510    fn to_with<C2: ReferenceCenter<Params = ()>, F2: ReferenceFrame, Ctx>(
511        &self,
512        jd: &JulianDate,
513        ctx: &Ctx,
514    ) -> Position<C2, F2, U>
515    where
516        Ctx: TransformContext,
517        Ctx::Eph: crate::ephemeris::Ephemeris,
518        (): CenterShiftProvider<C, C2, F>,
519        (): FrameRotationProvider<F, F2>,
520    {
521        // Order: center first (in source frame), then rotate
522        <Self as TransformCenter<C2, F, U>>::to_center_with(self, (), *jd, ctx)
523            .to_frame_with::<F2, Ctx>(jd, ctx)
524    }
525}
526
527// =============================================================================
528// WithEngine - Builder for custom context
529// =============================================================================
530
531/// A wrapper that pairs a coordinate reference with a custom transform
532/// context,
533/// enabling `.using(&engine).to_frame::<F2>(&jd)` style calls.
534///
535/// # Example
536///
537/// ```rust,ignore
538/// let engine = AstroContext::new();
539/// let result = direction.using(&engine).to_frame::<EclipticMeanJ2000>(&jd);
540/// ```
541pub struct WithEngine<'a, T, Ctx> {
542    inner: &'a T,
543    ctx: &'a Ctx,
544}
545
546/// Helper trait to create [`WithEngine`] wrappers.
547pub trait UsingEngine: Sized {
548    /// Wrap this coordinate with a custom transform context for the next
549    /// transformation call.
550    fn using<'a, Ctx>(&'a self, engine: &'a Ctx) -> WithEngine<'a, Self, Ctx>
551    where
552        Ctx: TransformContext,
553    {
554        WithEngine {
555            inner: self,
556            ctx: engine,
557        }
558    }
559}
560
561// Blanket impl: every type gets `.using()`
562impl<T> UsingEngine for T {}
563
564// --- WithEngine impls for Direction<F> ---
565
566impl<'a, F: ReferenceFrame, Ctx> WithEngine<'a, Direction<F>, Ctx>
567where
568    Ctx: TransformContext,
569{
570    /// Rotates this direction to a new reference frame using the wrapped context.
571    pub fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Direction<F2>
572    where
573        (): FrameRotationProvider<F, F2>,
574    {
575        self.inner.to_frame_with(jd, self.ctx)
576    }
577}
578
579// --- WithEngine impls for Position<C, F, U> ---
580
581impl<'a, C, F, U, Ctx> WithEngine<'a, Position<C, F, U>, Ctx>
582where
583    C: ReferenceCenter<Params = ()>,
584    Ctx: TransformContext,
585    Ctx::Eph: crate::ephemeris::Ephemeris,
586    F: ReferenceFrame,
587    U: LengthUnit,
588{
589    /// Rotates this position to a new reference frame using the wrapped context.
590    pub fn to_frame<F2: ReferenceFrame>(&self, jd: &JulianDate) -> Position<C, F2, U>
591    where
592        (): FrameRotationProvider<F, F2>,
593    {
594        self.inner.to_frame_with(jd, self.ctx)
595    }
596
597    /// Translates this position to a new reference center using the wrapped context.
598    pub fn to_center<C2: ReferenceCenter<Params = ()>>(&self, jd: &JulianDate) -> Position<C2, F, U>
599    where
600        (): CenterShiftProvider<C, C2, F>,
601    {
602        <Position<C, F, U> as TransformCenter<C2, F, U>>::to_center_with(
603            self.inner,
604            (),
605            *jd,
606            self.ctx,
607        )
608    }
609
610    /// Combined center + frame transform using the wrapped context.
611    pub fn to<C2: ReferenceCenter<Params = ()>, F2: ReferenceFrame>(
612        &self,
613        jd: &JulianDate,
614    ) -> Position<C2, F2, U>
615    where
616        (): CenterShiftProvider<C, C2, F>,
617        (): FrameRotationProvider<F, F2>,
618    {
619        self.inner.to_with(jd, self.ctx)
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626    use crate::coordinates::centers::{Barycentric, Geocentric};
627    use crate::coordinates::frames::{EclipticMeanJ2000, ICRS};
628    use crate::qtty::{AstronomicalUnit, AstronomicalUnits};
629
630    const EPSILON: f64 = 1e-10;
631    const AU_EPS: AstronomicalUnits = AstronomicalUnits::new(EPSILON);
632    const AU_TIGHT: AstronomicalUnits = AstronomicalUnits::new(1e-15);
633
634    #[test]
635    fn test_direction_frame_transform() {
636        let dir = Direction::<ICRS>::new(1.0, 0.0, 0.0);
637        let jd = crate::J2000;
638
639        // ICRS to EclipticMeanJ2000 includes a small frame-bias; don't assume exact axis invariance.
640        let dir_ecl: Direction<EclipticMeanJ2000> = dir.to_frame(&jd);
641
642        // Must be finite and length-preserving.
643        assert!(dir_ecl.x().is_finite() && dir_ecl.y().is_finite() && dir_ecl.z().is_finite());
644        let n0 = (dir.x() * dir.x() + dir.y() * dir.y() + dir.z() * dir.z()).sqrt();
645        let n1 =
646            (dir_ecl.x() * dir_ecl.x() + dir_ecl.y() * dir_ecl.y() + dir_ecl.z() * dir_ecl.z())
647                .sqrt();
648        assert!((n0 - n1).abs() < 1e-12);
649    }
650
651    #[test]
652    fn test_direction_frame_transform_with_ctx() {
653        let dir = Direction::<ICRS>::new(1.0, 0.0, 0.0);
654        let ctx: AstroContext = AstroContext::default();
655        let jd = crate::J2000;
656
657        let dir_ecl: Direction<EclipticMeanJ2000> = dir.to_frame_with(&jd, &ctx);
658        let dir_ecl_default: Direction<EclipticMeanJ2000> = dir.to_frame(&jd);
659
660        assert!((dir_ecl.x() - dir_ecl_default.x()).abs() < 1e-15);
661        assert!((dir_ecl.y() - dir_ecl_default.y()).abs() < 1e-15);
662        assert!((dir_ecl.z() - dir_ecl_default.z()).abs() < 1e-15);
663    }
664
665    #[test]
666    fn test_direction_frame_roundtrip() {
667        let dir = Direction::<ICRS>::new(1.0, 2.0, 3.0);
668        let jd = crate::J2000;
669
670        let dir_ecl: Direction<EclipticMeanJ2000> = dir.to_frame(&jd);
671        let dir_back: Direction<ICRS> = dir_ecl.to_frame(&jd);
672
673        assert!((dir_back.x() - dir.x()).abs() < EPSILON);
674        assert!((dir_back.y() - dir.y()).abs() < EPSILON);
675        assert!((dir_back.z() - dir.z()).abs() < EPSILON);
676    }
677
678    #[test]
679    fn test_position_frame_transform() {
680        let pos = Position::<Barycentric, ICRS, AstronomicalUnit>::new(1.0, 0.0, 0.0);
681        let jd = crate::J2000;
682
683        let pos_ecl: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> = pos.to_frame(&jd);
684
685        assert!(pos_ecl.x().is_finite() && pos_ecl.y().is_finite() && pos_ecl.z().is_finite());
686
687        // Length must be preserved under pure rotation.
688        let n0 =
689            (pos.x().value().powi(2) + pos.y().value().powi(2) + pos.z().value().powi(2)).sqrt();
690        let n1 = (pos_ecl.x().value().powi(2)
691            + pos_ecl.y().value().powi(2)
692            + pos_ecl.z().value().powi(2))
693        .sqrt();
694        assert!((n0 - n1).abs() < 1e-12);
695    }
696
697    #[test]
698    fn test_position_center_transform() {
699        let jd = crate::J2000;
700
701        // A point at the Geocentric origin should map to Earth's position in Barycentric
702        let geo_origin =
703            Position::<Geocentric, EclipticMeanJ2000, AstronomicalUnit>::new(0.0, 0.0, 0.0);
704        let bary: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> =
705            geo_origin.to_center(jd);
706
707        // Should be non-zero (Earth is ~1 AU from barycenter)
708        let dist = bary.distance();
709        assert!(
710            dist.value() > 0.9 && dist.value() < 1.1,
711            "Earth should be ~1 AU from barycenter, got {}",
712            dist
713        );
714    }
715
716    #[test]
717    fn test_position_combined_transform() {
718        let jd = crate::J2000;
719
720        let pos = Position::<Barycentric, EclipticMeanJ2000, AstronomicalUnit>::new(1.0, 0.5, 0.2);
721
722        // Combined transform: Barycentric EclipticMeanJ2000 -> Geocentric ICRS
723        let result: Position<Geocentric, ICRS, AstronomicalUnit> = pos.to(&jd);
724
725        // Verify it's not the same as the input (transformation happened)
726        assert!(
727            (result.x() - pos.x()).abs() > AU_EPS
728                || (result.y() - pos.y()).abs() > AU_EPS
729                || (result.z() - pos.z()).abs() > AU_EPS
730        );
731    }
732
733    #[test]
734    fn test_position_identity_transforms() {
735        let jd = crate::J2000;
736
737        let pos = Position::<Barycentric, ICRS, AstronomicalUnit>::new(1.5, 2.5, 3.5);
738
739        // Identity frame transform
740        let same_frame: Position<Barycentric, ICRS, AstronomicalUnit> = pos.to_frame(&jd);
741        assert!((same_frame.x() - pos.x()).abs() < AU_EPS);
742        assert!((same_frame.y() - pos.y()).abs() < AU_EPS);
743        assert!((same_frame.z() - pos.z()).abs() < AU_EPS);
744
745        // Identity center transform (via ShiftCenter)
746        let same_center: Position<Barycentric, ICRS, AstronomicalUnit> = pos.to_center(jd);
747        assert!((same_center.x() - pos.x()).abs() < AU_EPS);
748        assert!((same_center.y() - pos.y()).abs() < AU_EPS);
749        assert!((same_center.z() - pos.z()).abs() < AU_EPS);
750    }
751
752    #[test]
753    fn test_using_engine() {
754        let dir = Direction::<ICRS>::new(1.0, 0.0, 0.0);
755        let engine: AstroContext = AstroContext::default();
756        let jd = crate::J2000;
757
758        let dir_ecl: Direction<EclipticMeanJ2000> = dir.using(&engine).to_frame(&jd);
759        let dir_ecl_direct: Direction<EclipticMeanJ2000> = dir.to_frame(&jd);
760
761        assert!((dir_ecl.x() - dir_ecl_direct.x()).abs() < 1e-15);
762        assert!((dir_ecl.y() - dir_ecl_direct.y()).abs() < 1e-15);
763        assert!((dir_ecl.z() - dir_ecl_direct.z()).abs() < 1e-15);
764    }
765
766    #[test]
767    fn test_phantom_model_selection_affects_true_of_date_rotation() {
768        use crate::astro::nutation::{Iau2006, Iau2006A};
769
770        let dir = Direction::<ICRS>::new(1.0, 0.0, 0.0);
771        let jd = crate::time::JulianDate::new(2_458_850.0);
772        let ctx: AstroContext<DefaultEphemeris, DefaultEop> = AstroContext::default();
773
774        let with_nutation = dir
775            .to_frame_with::<crate::coordinates::frames::EquatorialTrueOfDate, _>(
776                &jd,
777                &ctx.with_model::<Iau2006A>(),
778            );
779        let precession_only = dir
780            .to_frame_with::<crate::coordinates::frames::EquatorialTrueOfDate, _>(
781                &jd,
782                &ctx.with_model::<Iau2006>(),
783            );
784
785        let delta = ((with_nutation.x() - precession_only.x()).powi(2)
786            + (with_nutation.y() - precession_only.y()).powi(2)
787            + (with_nutation.z() - precession_only.z()).powi(2))
788        .sqrt();
789
790        assert!(
791            delta > 1e-9,
792            "model presets should produce distinct rotations"
793        );
794    }
795
796    // =====================================================================
797    // SphericalDirectionAstroExt tests
798    // =====================================================================
799
800    #[test]
801    fn test_spherical_direction_frame_transform() {
802        use super::SphericalDirectionAstroExt;
803        use crate::coordinates::spherical;
804        use crate::qtty::DEG;
805
806        let sph_dir = spherical::Direction::<ICRS>::new(45.0 * DEG, 30.0 * DEG);
807        let jd = crate::J2000;
808
809        let sph_ecl: spherical::Direction<EclipticMeanJ2000> = sph_dir.to_frame(&jd);
810
811        // Should be finite
812        assert!(sph_ecl.azimuth.is_finite());
813        assert!(sph_ecl.polar.is_finite());
814    }
815
816    #[test]
817    fn test_spherical_direction_roundtrip() {
818        use super::SphericalDirectionAstroExt;
819        use crate::coordinates::spherical;
820        use crate::qtty::DEG;
821
822        let sph_dir = spherical::Direction::<ICRS>::new(123.0 * DEG, -45.0 * DEG);
823        let jd = crate::J2000;
824
825        let sph_ecl: spherical::Direction<EclipticMeanJ2000> = sph_dir.to_frame(&jd);
826        let sph_back: spherical::Direction<ICRS> = sph_ecl.to_frame(&jd);
827
828        assert!((sph_back.azimuth - sph_dir.azimuth).abs().value() < 1e-8);
829        assert!((sph_back.polar - sph_dir.polar).abs().value() < 1e-8);
830    }
831
832    #[test]
833    fn test_spherical_direction_with_ctx() {
834        use super::SphericalDirectionAstroExt;
835        use crate::coordinates::spherical;
836        use crate::qtty::DEG;
837
838        let sph_dir = spherical::Direction::<ICRS>::new(90.0 * DEG, 0.0 * DEG);
839        let ctx: AstroContext = AstroContext::default();
840        let jd = crate::J2000;
841
842        let with_ctx: spherical::Direction<EclipticMeanJ2000> = sph_dir.to_frame_with(&jd, &ctx);
843        let without_ctx: spherical::Direction<EclipticMeanJ2000> = sph_dir.to_frame(&jd);
844
845        assert!((with_ctx.azimuth - without_ctx.azimuth).abs().value() < 1e-15);
846        assert!((with_ctx.polar - without_ctx.polar).abs().value() < 1e-15);
847    }
848
849    // =====================================================================
850    // VectorAstroExt tests
851    // =====================================================================
852
853    #[test]
854    fn test_vector_frame_transform() {
855        use super::VectorAstroExt;
856
857        let vec = Vector::<ICRS, AstronomicalUnit>::new(
858            AstronomicalUnits::new(1.0),
859            AstronomicalUnits::new(2.0),
860            AstronomicalUnits::new(3.0),
861        );
862        let jd = crate::J2000;
863
864        let vec_ecl: Vector<EclipticMeanJ2000, AstronomicalUnit> = vec.to_frame(&jd);
865        assert!(vec_ecl.x().is_finite() && vec_ecl.y().is_finite() && vec_ecl.z().is_finite());
866
867        // Length should be preserved
868        let n0 = (vec.x() * vec.x() + vec.y() * vec.y() + vec.z() * vec.z()).scalar_sqrt();
869        let n1 =
870            (vec_ecl.x() * vec_ecl.x() + vec_ecl.y() * vec_ecl.y() + vec_ecl.z() * vec_ecl.z())
871                .scalar_sqrt();
872        assert!((n0 - n1).abs() < 1e-12);
873    }
874
875    #[test]
876    fn test_vector_frame_roundtrip() {
877        use super::VectorAstroExt;
878
879        let vec = Vector::<ICRS, AstronomicalUnit>::new(
880            AstronomicalUnits::new(0.5),
881            AstronomicalUnits::new(-0.3),
882            AstronomicalUnits::new(0.8),
883        );
884        let jd = crate::J2000;
885
886        let vec_ecl: Vector<EclipticMeanJ2000, AstronomicalUnit> = vec.to_frame(&jd);
887        let vec_back: Vector<ICRS, AstronomicalUnit> = vec_ecl.to_frame(&jd);
888
889        assert!((vec_back.x() - vec.x()).abs() < AU_EPS);
890        assert!((vec_back.y() - vec.y()).abs() < AU_EPS);
891        assert!((vec_back.z() - vec.z()).abs() < AU_EPS);
892    }
893
894    #[test]
895    fn test_vector_frame_with_ctx() {
896        use super::VectorAstroExt;
897
898        let vec = Vector::<ICRS, AstronomicalUnit>::new(
899            AstronomicalUnits::new(1.0),
900            AstronomicalUnits::new(0.0),
901            AstronomicalUnits::new(0.0),
902        );
903        let ctx: AstroContext = AstroContext::default();
904        let jd = crate::J2000;
905
906        let with_ctx: Vector<EclipticMeanJ2000, AstronomicalUnit> = vec.to_frame_with(&jd, &ctx);
907        let without_ctx: Vector<EclipticMeanJ2000, AstronomicalUnit> = vec.to_frame(&jd);
908
909        assert!((with_ctx.x() - without_ctx.x()).abs() < AU_TIGHT);
910        assert!((with_ctx.y() - without_ctx.y()).abs() < AU_TIGHT);
911        assert!((with_ctx.z() - without_ctx.z()).abs() < AU_TIGHT);
912    }
913
914    // =====================================================================
915    // WithEngine for positions
916    // =====================================================================
917
918    #[test]
919    fn test_using_engine_position_frame() {
920        let pos = Position::<Barycentric, ICRS, AstronomicalUnit>::new(1.0, 0.5, 0.2);
921        let engine: AstroContext = AstroContext::default();
922        let jd = crate::J2000;
923
924        let via_engine: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> =
925            pos.using(&engine).to_frame(&jd);
926        let direct: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> = pos.to_frame(&jd);
927
928        assert!((via_engine.x() - direct.x()).abs() < AU_TIGHT);
929        assert!((via_engine.y() - direct.y()).abs() < AU_TIGHT);
930        assert!((via_engine.z() - direct.z()).abs() < AU_TIGHT);
931    }
932
933    #[test]
934    fn test_using_engine_position_center() {
935        let pos = Position::<Geocentric, EclipticMeanJ2000, AstronomicalUnit>::new(0.0, 0.0, 0.0);
936        let engine: AstroContext = AstroContext::default();
937        let jd = crate::J2000;
938
939        let via_engine: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> =
940            pos.using(&engine).to_center(&jd);
941        let direct: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> = pos.to_center(jd);
942
943        assert!((via_engine.x() - direct.x()).abs() < AU_TIGHT);
944        assert!((via_engine.y() - direct.y()).abs() < AU_TIGHT);
945        assert!((via_engine.z() - direct.z()).abs() < AU_TIGHT);
946    }
947
948    #[test]
949    fn test_using_engine_position_combined() {
950        let pos = Position::<Barycentric, EclipticMeanJ2000, AstronomicalUnit>::new(1.0, 0.5, 0.2);
951        let engine: AstroContext = AstroContext::default();
952        let jd = crate::J2000;
953
954        let via_engine: Position<Geocentric, ICRS, AstronomicalUnit> = pos.using(&engine).to(&jd);
955        let direct: Position<Geocentric, ICRS, AstronomicalUnit> = pos.to(&jd);
956
957        assert!((via_engine.x() - direct.x()).abs() < AU_TIGHT);
958        assert!((via_engine.y() - direct.y()).abs() < AU_TIGHT);
959        assert!((via_engine.z() - direct.z()).abs() < AU_TIGHT);
960    }
961
962    // =====================================================================
963    // Position with_ctx variants
964    // =====================================================================
965
966    #[test]
967    fn test_position_frame_with_ctx() {
968        let pos = Position::<Barycentric, ICRS, AstronomicalUnit>::new(1.0, 2.0, 3.0);
969        let ctx: AstroContext = AstroContext::default();
970        let jd = crate::J2000;
971
972        let with_ctx: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> =
973            pos.to_frame_with(&jd, &ctx);
974        let default_ctx: Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> =
975            pos.to_frame(&jd);
976
977        assert!((with_ctx.x() - default_ctx.x()).abs() < AU_TIGHT);
978        assert!((with_ctx.y() - default_ctx.y()).abs() < AU_TIGHT);
979        assert!((with_ctx.z() - default_ctx.z()).abs() < AU_TIGHT);
980    }
981
982    #[test]
983    fn test_position_combined_with_ctx() {
984        let pos = Position::<Barycentric, EclipticMeanJ2000, AstronomicalUnit>::new(1.0, 0.5, 0.2);
985        let ctx: AstroContext = AstroContext::default();
986        let jd = crate::J2000;
987
988        let with_ctx: Position<Geocentric, ICRS, AstronomicalUnit> = pos.to_with(&jd, &ctx);
989        let default_ctx: Position<Geocentric, ICRS, AstronomicalUnit> = pos.to(&jd);
990
991        assert!((with_ctx.x() - default_ctx.x()).abs() < AU_TIGHT);
992        assert!((with_ctx.y() - default_ctx.y()).abs() < AU_TIGHT);
993        assert!((with_ctx.z() - default_ctx.z()).abs() < AU_TIGHT);
994    }
995}