astrodynamics-gnss 0.14.0

GNSS domain layer (SP3, broadcast ephemeris, multi-GNSS single-point positioning, ionosphere/troposphere, DOP) built on the astrodynamics core
Documentation
//! Frame-tagged position types.
//!
//! Every position value encodes its reference frame and datum in the **type
//! name**, never a bare `position_m` that hides the frame. SP3 products give
//! satellite states
//! in the ITRF/IGS-realization ECEF frame, in meters; that fact is carried by
//! [`ItrfPositionM`] so a consumer cannot accidentally mix it with, say, a
//! GCRS/TEME state from the core crate (which is in kilometers).

use core::fmt;

/// A position in the ITRF / IGS-realization Earth-Centered-Earth-Fixed frame,
/// expressed in **meters**.
///
/// SP3 ephemerides are published in an IGS realization of the ITRF (e.g.
/// `IGS14`, `IGS20`); the exact realization string is carried on the SP3
/// header ([`crate::sp3::Sp3Header::coordinate_system`]), while this type fixes
/// the *kind* of frame (ITRF/IGS ECEF) and the *unit* (meters) in the type
/// system. There is intentionally no implicit conversion to the core crate's
/// kilometer states; conversion happens explicitly at an API boundary.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ItrfPositionM {
    /// ECEF X coordinate in meters.
    pub x_m: f64,
    /// ECEF Y coordinate in meters.
    pub y_m: f64,
    /// ECEF Z coordinate in meters.
    pub z_m: f64,
}

impl ItrfPositionM {
    /// Construct an ITRF ECEF position from meter components.
    pub const fn new(x_m: f64, y_m: f64, z_m: f64) -> Self {
        Self { x_m, y_m, z_m }
    }

    /// The components as a `[x, y, z]` meter array.
    pub const fn as_array(self) -> [f64; 3] {
        [self.x_m, self.y_m, self.z_m]
    }
}

impl fmt::Display for ItrfPositionM {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "ITRF[{:.3}, {:.3}, {:.3}] m",
            self.x_m, self.y_m, self.z_m
        )
    }
}

/// A geodetic (ellipsoidal) position on the WGS84 datum.
///
/// Latitude and longitude are geodetic angles in **radians**; height is the
/// ellipsoidal height in **meters**. This is the receiver-position type the
/// atmospheric (ionosphere/troposphere) models take: they need the geodetic
/// latitude/longitude of the antenna and, for the troposphere, the height.
///
/// The frame and units are fixed in the type so a caller cannot mix this with
/// the ECEF [`ItrfPositionM`] or pass degrees where radians are expected.
/// Longitude is positive east; latitude is positive north.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Wgs84Geodetic {
    /// Geodetic latitude in radians, positive north, range `[-pi/2, pi/2]`.
    pub lat_rad: f64,
    /// Geodetic longitude in radians, positive east, range `(-pi, pi]`.
    pub lon_rad: f64,
    /// Ellipsoidal height above the WGS84 ellipsoid in meters.
    pub height_m: f64,
}

impl Wgs84Geodetic {
    /// Construct a WGS84 geodetic position from radians and meters.
    pub const fn new(lat_rad: f64, lon_rad: f64, height_m: f64) -> Self {
        Self {
            lat_rad,
            lon_rad,
            height_m,
        }
    }
}

impl fmt::Display for Wgs84Geodetic {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "WGS84[lat {:.6} rad, lon {:.6} rad, h {:.3} m]",
            self.lat_rad, self.lon_rad, self.height_m
        )
    }
}

/// A velocity in the ITRF / IGS-realization ECEF frame, in **meters per
/// second**.
///
/// Present only when the SP3 file is a velocity product (`#?V...` header /
/// `V`-records). The frame/unit are fixed in the type for the same reason as
/// [`ItrfPositionM`].
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ItrfVelocityMS {
    /// ECEF X velocity in meters per second.
    pub vx_m_s: f64,
    /// ECEF Y velocity in meters per second.
    pub vy_m_s: f64,
    /// ECEF Z velocity in meters per second.
    pub vz_m_s: f64,
}

impl ItrfVelocityMS {
    /// Construct an ITRF ECEF velocity from meter-per-second components.
    pub const fn new(vx_m_s: f64, vy_m_s: f64, vz_m_s: f64) -> Self {
        Self {
            vx_m_s,
            vy_m_s,
            vz_m_s,
        }
    }

    /// The components as a `[vx, vy, vz]` m/s array.
    pub const fn as_array(self) -> [f64; 3] {
        [self.vx_m_s, self.vy_m_s, self.vz_m_s]
    }
}