use crate::time::{Duration, Epoch};
use geo3d::{eci, Ecef, Eci, Geodetic, LocalFrame, Track, Vec3, WGS84};
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Geodetic4 {
pub position: Geodetic,
pub epoch: Epoch,
}
impl Geodetic4 {
#[inline]
pub const fn new(position: Geodetic, epoch: Epoch) -> Self {
Geodetic4 { position, epoch }
}
#[inline]
pub const fn from_lat_lon(lat_deg: f64, lon_deg: f64, height_m: f64, epoch: Epoch) -> Self {
Geodetic4::new(Geodetic::new(lat_deg, lon_deg, height_m), epoch)
}
#[inline]
pub fn to_ecef4(self) -> Ecef4 {
Ecef4::new(WGS84.to_ecef(self.position), self.epoch)
}
#[inline]
pub const fn at_epoch(self, epoch: Epoch) -> Self {
Geodetic4 { epoch, ..self }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Ecef4 {
pub position: Ecef,
pub epoch: Epoch,
}
impl Ecef4 {
#[inline]
pub const fn new(position: Ecef, epoch: Epoch) -> Self {
Ecef4 { position, epoch }
}
#[inline]
pub fn to_geodetic4(self) -> Geodetic4 {
Geodetic4::new(WGS84.to_geodetic(self.position), self.epoch)
}
#[inline]
pub fn to_eci(self) -> Eci {
eci::ecef_to_eci(self.position, self.epoch.earth_rotation_angle_rad())
}
#[inline]
pub fn distance_to(self, o: Ecef4) -> f64 {
self.position.distance_to(o.position)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StateVector {
pub epoch: Epoch,
pub position: Ecef,
pub velocity: Vec3,
}
impl StateVector {
#[inline]
pub const fn new(epoch: Epoch, position: Ecef, velocity: Vec3) -> Self {
StateVector {
epoch,
position,
velocity,
}
}
pub fn from_fixes(earlier: Ecef4, later: Ecef4) -> Self {
let dt = (later.epoch - earlier.epoch).as_seconds();
let velocity = if dt > 0.0 {
(later.position.vec() - earlier.position.vec()) / dt
} else {
Vec3::ZERO
};
StateVector::new(later.epoch, later.position, velocity)
}
#[inline]
pub fn at(self, epoch: Epoch) -> Self {
let dt = (epoch - self.epoch).as_seconds();
StateVector::new(epoch, self.position.offset(self.velocity * dt), self.velocity)
}
#[inline]
pub fn position_at(self, epoch: Epoch) -> Ecef4 {
let s = self.at(epoch);
Ecef4::new(s.position, s.epoch)
}
#[inline]
pub fn speed_mps(self) -> f64 {
self.velocity.norm()
}
#[inline]
pub fn ecef4(self) -> Ecef4 {
Ecef4::new(self.position, self.epoch)
}
#[inline]
pub fn to_track(self) -> Track {
Track::new(self.position, self.velocity)
}
#[inline]
pub fn dt_to(self, other: StateVector) -> Duration {
other.epoch - self.epoch
}
}
impl From<StateVector> for Track {
#[inline]
fn from(s: StateVector) -> Track {
s.to_track()
}
}
impl Geodetic4 {
pub fn state_from_course(self, course: geo3d::Course) -> StateVector {
let frame = LocalFrame::new(self.position);
StateVector::new(
self.epoch,
WGS84.to_ecef(self.position),
frame.velocity_of_course(course),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::TimeScale;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn geodetic4_ecef4_round_trip() {
let t = Epoch::from_gregorian_utc(2026, 1, 2, 3, 4, 5.0);
let g = Geodetic4::from_lat_lon(-33.87, 151.21, 20.0, t);
let back = g.to_ecef4().to_geodetic4();
assert!(close(back.position.lat_deg, g.position.lat_deg, 1e-9));
assert!(close(back.position.height_m, g.position.height_m, 1e-4));
assert_eq!(back.epoch, t);
}
#[test]
fn state_vector_dead_reckons_and_infers() {
let t0 = Epoch::from_unix_seconds(1_700_000_000.0);
let s = StateVector::new(t0, Ecef::new(0.0, 0.0, 0.0), Vec3::new(10.0, 0.0, 0.0));
let s10 = s.at(t0 + Duration::from_seconds(10.0));
assert_eq!(s10.position, Ecef::new(100.0, 0.0, 0.0));
assert!(close(s10.speed_mps(), 10.0, 1e-12));
let a = Ecef4::new(Ecef::new(0.0, 0.0, 0.0), t0);
let b = Ecef4::new(Ecef::new(50.0, 0.0, 0.0), t0 + Duration::from_seconds(5.0));
let inferred = StateVector::from_fixes(a, b);
assert_eq!(inferred.velocity, Vec3::new(10.0, 0.0, 0.0));
assert_eq!(inferred.epoch, b.epoch);
assert_eq!(StateVector::from_fixes(b, a).velocity, Vec3::ZERO);
}
#[test]
fn eci_conversion_uses_the_epoch() {
let t = Epoch::from_julian_date(TimeScale::Utc, 2_451_545.0);
let p = Ecef4::new(Ecef::new(7_000_000.0, 0.0, 0.0), t);
let eci = p.to_eci();
let back = eci::eci_to_ecef(eci, t.earth_rotation_angle_rad());
assert!(close(back.x, p.position.x, 1e-6) && close(back.y, p.position.y, 1e-6));
}
#[test]
fn state_from_course_matches_speed() {
let t = Epoch::now();
let g = Geodetic4::from_lat_lon(-33.9, 151.2, 100.0, t);
let s = g.state_from_course(geo3d::Course::new(90.0, 200.0, 0.0)); assert!(close(s.speed_mps(), 200.0, 1e-6));
assert_eq!(s.epoch, t);
}
}