use crate::time::Epoch;
use crate::types::Ecef4;
use geo3d::{Ecef, Helmert7, Vec3};
const MAS_PER_YEAR_TO_RAD_PER_YEAR: f64 = core::f64::consts::PI / (180.0 * 3600.0 * 1000.0);
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PlateMotion {
omega: Vec3,
}
impl PlateMotion {
#[inline]
pub const fn from_rad_per_year(omega: Vec3) -> Self {
PlateMotion { omega }
}
#[inline]
pub const fn from_mas_per_year(wx: f64, wy: f64, wz: f64) -> Self {
PlateMotion {
omega: Vec3::new(
wx * MAS_PER_YEAR_TO_RAD_PER_YEAR,
wy * MAS_PER_YEAR_TO_RAD_PER_YEAR,
wz * MAS_PER_YEAR_TO_RAD_PER_YEAR,
),
}
}
pub fn from_pole(pole_lat_deg: f64, pole_lon_deg: f64, rate_deg_per_myr: f64) -> Self {
let rate_rad_per_year = rate_deg_per_myr.to_radians() / 1.0e6;
let (slat, clat) = pole_lat_deg.to_radians().sin_cos();
let (slon, clon) = pole_lon_deg.to_radians().sin_cos();
PlateMotion {
omega: Vec3::new(clat * clon, clat * slon, slat) * rate_rad_per_year,
}
}
#[inline]
pub const fn angular_velocity(self) -> Vec3 {
self.omega
}
#[inline]
pub fn velocity_at(self, position: Ecef) -> Vec3 {
self.omega.cross(position.vec())
}
#[inline]
pub fn displacement(self, position: Ecef, dt: crate::time::Duration) -> Vec3 {
self.velocity_at(position) * dt.as_julian_years()
}
pub fn propagate(self, from: Ecef4, to: Epoch) -> Ecef4 {
let v = self.velocity_at(from.position);
let dt = (to - from.epoch).as_julian_years();
Ecef4::new(from.position.offset(v * dt), to)
}
pub const AMUR: PlateMotion = PlateMotion::from_mas_per_year(-0.1310, -0.5515, 0.8370);
pub const ANTARCTICA: PlateMotion = PlateMotion::from_mas_per_year(-0.2686, -0.3118, 0.6775);
pub const ARABIA: PlateMotion = PlateMotion::from_mas_per_year(1.1286, -0.1462, 1.4378);
pub const AUSTRALIA: PlateMotion = PlateMotion::from_mas_per_year(1.4875, 1.1754, 1.2233);
pub const CARIBBEAN: PlateMotion = PlateMotion::from_mas_per_year(0.2074, -1.4216, 0.7261);
pub const EURASIA: PlateMotion = PlateMotion::from_mas_per_year(-0.0853, -0.5191, 0.7528);
pub const INDIA: PlateMotion = PlateMotion::from_mas_per_year(1.1372, 0.0133, 1.4436);
pub const NAZCA: PlateMotion = PlateMotion::from_mas_per_year(-0.3265, -1.5610, 1.6052);
pub const NORTH_AMERICA: PlateMotion = PlateMotion::from_mas_per_year(0.0454, -0.6656, -0.0979);
pub const NUBIA: PlateMotion = PlateMotion::from_mas_per_year(0.0900, -0.5850, 0.7168);
pub const PACIFIC: PlateMotion = PlateMotion::from_mas_per_year(-0.4039, 1.0210, -2.1542);
pub const SOUTH_AMERICA: PlateMotion = PlateMotion::from_mas_per_year(-0.2610, -0.2822, -0.1573);
pub const SOMALIA: PlateMotion = PlateMotion::from_mas_per_year(-0.0810, -0.7189, 0.8644);
pub const ITRF2020_ORIGIN_RATE_BIAS: Vec3 = Vec3::new(0.000_37, 0.000_35, 0.000_74);
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Helmert14 {
pub reference_epoch: Epoch,
pub value: Helmert7,
pub rate: Helmert7,
}
impl Helmert14 {
#[inline]
pub const fn from_parts(reference_epoch: Epoch, value: Helmert7, rate: Helmert7) -> Self {
Helmert14 {
reference_epoch,
value,
rate,
}
}
pub fn position_vector(
reference_epoch: Epoch,
translation_mm: [f64; 3],
rotation_mas: [f64; 3],
scale_ppb: f64,
translation_rate_mm_yr: [f64; 3],
rotation_rate_mas_yr: [f64; 3],
scale_rate_ppb_yr: f64,
) -> Self {
Helmert14 {
reference_epoch,
value: pv(translation_mm, rotation_mas, scale_ppb),
rate: pv(translation_rate_mm_yr, rotation_rate_mas_yr, scale_rate_ppb_yr),
}
}
pub fn coordinate_frame(
reference_epoch: Epoch,
translation_mm: [f64; 3],
rotation_mas: [f64; 3],
scale_ppb: f64,
translation_rate_mm_yr: [f64; 3],
rotation_rate_mas_yr: [f64; 3],
scale_rate_ppb_yr: f64,
) -> Self {
Helmert14 {
reference_epoch,
value: pv(
translation_mm,
[-rotation_mas[0], -rotation_mas[1], -rotation_mas[2]],
scale_ppb,
),
rate: pv(
translation_rate_mm_yr,
[
-rotation_rate_mas_yr[0],
-rotation_rate_mas_yr[1],
-rotation_rate_mas_yr[2],
],
scale_rate_ppb_yr,
),
}
}
pub fn at(self, epoch: Epoch) -> Helmert7 {
let dt = (epoch - self.reference_epoch).as_julian_years();
let (v, r) = (self.value, self.rate);
Helmert7::new(
v.tx + r.tx * dt,
v.ty + r.ty * dt,
v.tz + r.tz * dt,
v.rx + r.rx * dt,
v.ry + r.ry * dt,
v.rz + r.rz * dt,
v.scale + r.scale * dt,
)
}
#[inline]
pub fn apply(self, p: Ecef4) -> Ecef4 {
Ecef4::new(self.at(p.epoch).apply(p.position), p.epoch)
}
#[inline]
pub fn apply_inverse(self, p: Ecef4) -> Ecef4 {
Ecef4::new(self.at(p.epoch).apply_inverse(p.position), p.epoch)
}
pub fn itrf2020_to_itrf2014() -> Self {
Helmert14::position_vector(
Epoch::from_decimal_year(2015.0),
[-1.4, -0.9, 1.4],
[0.0, 0.0, 0.0],
-0.42,
[0.0, -0.1, 0.2],
[0.0, 0.0, 0.0],
0.0,
)
}
pub fn itrf2020_to_itrf2008() -> Self {
Helmert14::position_vector(
Epoch::from_decimal_year(2015.0),
[0.2, 1.0, 3.3],
[0.0, 0.0, 0.0],
-0.29,
[0.0, -0.1, 0.1],
[0.0, 0.0, 0.0],
0.03,
)
}
pub fn itrf2014_to_gda2020() -> Self {
Helmert14::coordinate_frame(
Epoch::from_decimal_year(2020.0),
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
0.0,
[0.0, 0.0, 0.0],
[1.503_79, 1.183_46, 1.207_16],
0.0,
)
}
pub fn gda94_to_gda2020() -> Self {
Helmert14::coordinate_frame(
Epoch::from_decimal_year(2020.0), [61.55, -10.87, -40.19],
[-39.4924, -32.7221, -32.8979],
-9.994,
[0.0, 0.0, 0.0],
[0.0, 0.0, 0.0],
0.0,
)
}
}
fn pv(translation_mm: [f64; 3], rotation_mas: [f64; 3], scale_ppb: f64) -> Helmert7 {
Helmert7::position_vector(
translation_mm[0] / 1000.0,
translation_mm[1] / 1000.0,
translation_mm[2] / 1000.0,
rotation_mas[0] / 1000.0,
rotation_mas[1] / 1000.0,
rotation_mas[2] / 1000.0,
scale_ppb / 1000.0,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Duration, Geodetic4};
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn australian_plate_moves_seven_cm_per_year_northeast() {
let expected_omega =
(1.4875f64.powi(2) + 1.1754f64.powi(2) + 1.2233f64.powi(2)).sqrt() * MAS_PER_YEAR_TO_RAD_PER_YEAR;
assert!(close(
PlateMotion::AUSTRALIA.angular_velocity().norm(),
expected_omega,
1e-20
));
let syd = Geodetic4::from_lat_lon(-33.87, 151.21, 20.0, Epoch::from_decimal_year(2020.0)).to_ecef4();
let v = PlateMotion::AUSTRALIA.velocity_at(syd.position);
assert!(close(v.norm(), 0.057_131_9, 1e-6), "{}", v.norm()); let enu = geo3d::LocalFrame::new(syd.to_geodetic4().position).dir_to_enu(v);
assert!(enu.north > 0.0 && enu.east > 0.0, "{enu:?}"); assert!(enu.up.abs() < 0.02); }
#[test]
fn propagation_is_linear_and_reversible() {
let p0 = Ecef4::new(
Ecef::new(-4_052_051.0, 4_212_836.0, -2_545_106.0),
Epoch::from_decimal_year(2020.0),
);
let t1 = Epoch::from_decimal_year(2030.0);
let moved = PlateMotion::AUSTRALIA.propagate(p0, t1);
assert!(close(moved.position.distance_to(p0.position), 0.70, 0.05));
let back = PlateMotion::AUSTRALIA.propagate(moved, p0.epoch);
assert!(back.position.distance_to(p0.position) < 1e-6);
assert_eq!(back.epoch, p0.epoch);
let d = PlateMotion::AUSTRALIA.displacement(p0.position, Duration::from_julian_years(10.0));
assert!(close(d.norm(), 0.70, 0.05));
}
#[test]
fn pole_and_component_constructors_agree() {
let by_pole = PlateMotion::from_pole(-63.58, 114.70, 0.651);
assert!(by_pole.angular_velocity().norm() > 0.0);
let raw = PlateMotion::from_mas_per_year(1.0, 2.0, 3.0);
assert_eq!(PlateMotion::from_rad_per_year(raw.angular_velocity()), raw);
}
#[test]
fn helmert14_evaluates_at_the_coordinate_epoch() {
let h = Helmert14::itrf2020_to_itrf2014();
let at2015 = h.at(Epoch::from_decimal_year(2015.0));
assert!(close(at2015.tz, 1.4 / 1000.0, 1e-12));
let at2025 = h.at(Epoch::from_decimal_year(2025.0));
assert!(
close(at2025.tz - at2015.tz, 2.0 / 1000.0, 1e-6),
"{}",
at2025.tz - at2015.tz
);
}
#[test]
fn helmert14_round_trips_and_preserves_epoch() {
let p = Ecef4::new(
Ecef::new(-4_052_051.0, 4_212_836.0, -2_545_106.0),
Epoch::from_decimal_year(2024.6),
);
let there = Helmert14::itrf2014_to_gda2020().apply(p);
let back = Helmert14::itrf2014_to_gda2020().apply_inverse(there);
assert!(back.position.distance_to(p.position) < 1e-6);
assert_eq!(there.epoch, p.epoch);
let g94 = Ecef4::new(
Ecef::new(-4_052_051.0, 4_212_836.0, -2_545_106.0),
Epoch::from_decimal_year(2020.0),
);
let g20 = Helmert14::gda94_to_gda2020().apply(g94);
assert!(
close(g20.position.distance_to(g94.position), 1.8, 0.3),
"{}",
g20.position.distance_to(g94.position)
);
}
}