use oxiproj_core::ProjResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RotationConvention {
PositionVector,
CoordinateFrame,
}
#[derive(Debug, Clone, Copy)]
pub struct FrameTransform {
pub from: &'static str,
pub to: &'static str,
pub ref_epoch: f64,
pub tx_mm: f64,
pub ty_mm: f64,
pub tz_mm: f64,
pub dtx_mm: f64,
pub dty_mm: f64,
pub dtz_mm: f64,
pub scale_ppb: f64,
pub dscale_ppb: f64,
pub rx_mas: f64,
pub ry_mas: f64,
pub rz_mas: f64,
pub drx_mas: f64,
pub dry_mas: f64,
pub drz_mas: f64,
pub convention: RotationConvention,
}
use RotationConvention::{CoordinateFrame, PositionVector};
pub const FRAME_TRANSFORMS: &[FrameTransform] = &[
FrameTransform {
from: "ITRF2020",
to: "ITRF2014",
ref_epoch: 2015.0,
tx_mm: -1.4,
ty_mm: -0.9,
tz_mm: 1.4,
dtx_mm: 0.0,
dty_mm: -0.1,
dtz_mm: 0.2,
scale_ppb: -0.42,
dscale_ppb: 0.0,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 0.0,
dry_mas: 0.0,
drz_mas: 0.0,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2020",
to: "ITRF2008",
ref_epoch: 2015.0,
tx_mm: 0.2,
ty_mm: 1.0,
tz_mm: 3.3,
dtx_mm: 0.0,
dty_mm: -0.1,
dtz_mm: 0.1,
scale_ppb: -0.29,
dscale_ppb: 0.03,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 0.0,
dry_mas: 0.0,
drz_mas: 0.0,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2020",
to: "ITRF2005",
ref_epoch: 2015.0,
tx_mm: 2.7,
ty_mm: 0.1,
tz_mm: -1.4,
dtx_mm: 0.3,
dty_mm: -0.1,
dtz_mm: 0.1,
scale_ppb: 0.65,
dscale_ppb: 0.03,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 0.0,
dry_mas: 0.0,
drz_mas: 0.0,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2020",
to: "ITRF2000",
ref_epoch: 2015.0,
tx_mm: -0.2,
ty_mm: 0.8,
tz_mm: -34.2,
dtx_mm: 0.1,
dty_mm: 0.0,
dtz_mm: -1.7,
scale_ppb: 2.25,
dscale_ppb: 0.11,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 0.0,
dry_mas: 0.0,
drz_mas: 0.0,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2014",
to: "ITRF2008",
ref_epoch: 2010.0,
tx_mm: 1.6,
ty_mm: 1.9,
tz_mm: 2.4,
dtx_mm: 0.0,
dty_mm: 0.0,
dtz_mm: -0.1,
scale_ppb: -0.02,
dscale_ppb: 0.03,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 0.0,
dry_mas: 0.0,
drz_mas: 0.0,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2000",
to: "ETRF2000",
ref_epoch: 2000.0,
tx_mm: 54.0,
ty_mm: 51.0,
tz_mm: -48.0,
dtx_mm: 0.0,
dty_mm: 0.0,
dtz_mm: 0.0,
scale_ppb: 0.0,
dscale_ppb: 0.0,
rx_mas: 0.891,
ry_mas: 5.39,
rz_mas: -8.712,
drx_mas: 0.081,
dry_mas: 0.49,
drz_mas: -0.792,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2014",
to: "ETRF2014",
ref_epoch: 2010.0,
tx_mm: 0.0,
ty_mm: 0.0,
tz_mm: 0.0,
dtx_mm: 0.0,
dty_mm: 0.0,
dtz_mm: 0.0,
scale_ppb: 0.0,
dscale_ppb: 0.0,
rx_mas: 1.785,
ry_mas: 11.151,
rz_mas: -16.17,
drx_mas: 0.085,
dry_mas: 0.531,
drz_mas: -0.77,
convention: PositionVector,
},
FrameTransform {
from: "ITRF2014",
to: "NAD83(2011)",
ref_epoch: 2010.0,
tx_mm: 1005.3,
ty_mm: -1909.21,
tz_mm: -541.57,
dtx_mm: 0.79,
dty_mm: -0.6,
dtz_mm: -1.44,
scale_ppb: 0.36891,
dscale_ppb: -0.07201,
rx_mas: 26.78138,
ry_mas: -0.42027,
rz_mas: 10.93206,
drx_mas: 0.06667,
dry_mas: -0.75744,
drz_mas: -0.05133,
convention: CoordinateFrame,
},
FrameTransform {
from: "ITRF2014",
to: "GDA2020",
ref_epoch: 2020.0,
tx_mm: 0.0,
ty_mm: 0.0,
tz_mm: 0.0,
dtx_mm: 0.0,
dty_mm: 0.0,
dtz_mm: 0.0,
scale_ppb: 0.0,
dscale_ppb: 0.0,
rx_mas: 0.0,
ry_mas: 0.0,
rz_mas: 0.0,
drx_mas: 1.50379,
dry_mas: 1.18346,
drz_mas: 1.20716,
convention: CoordinateFrame,
},
];
pub fn find_frame_transform<'a>(from: &str, to: &str) -> Option<&'a FrameTransform> {
FRAME_TRANSFORMS
.iter()
.find(|ft| ft.from.eq_ignore_ascii_case(from) && ft.to.eq_ignore_ascii_case(to))
}
#[derive(Debug, Clone, Copy)]
pub struct FramePathStep {
pub transform: &'static FrameTransform,
pub inverse: bool,
}
#[must_use]
pub fn find_frame_path(from: &str, to: &str) -> Option<Vec<FramePathStep>> {
if from.eq_ignore_ascii_case(to) {
return Some(Vec::new());
}
let mut visited: Vec<&str> = Vec::new();
let mut queue: Vec<(&str, Vec<FramePathStep>)> = Vec::new();
visited.push(from);
queue.push((from, Vec::new()));
let mut head = 0usize;
while head < queue.len() {
let (node, path) = queue[head].clone();
head += 1;
for ft in FRAME_TRANSFORMS {
let (neighbour, inverse) = if ft.from.eq_ignore_ascii_case(node) {
(ft.to, false)
} else if ft.to.eq_ignore_ascii_case(node) {
(ft.from, true)
} else {
continue;
};
if visited.iter().any(|v| v.eq_ignore_ascii_case(neighbour)) {
continue;
}
let mut next_path = path.clone();
next_path.push(FramePathStep {
transform: ft,
inverse,
});
if neighbour.eq_ignore_ascii_case(to) {
return Some(next_path);
}
visited.push(neighbour);
queue.push((neighbour, next_path));
}
}
None
}
pub fn apply_frame_transform(
transform: &FrameTransform,
x: f64,
y: f64,
z: f64,
epoch: f64,
) -> ProjResult<(f64, f64, f64)> {
let dt = epoch - transform.ref_epoch;
let tx = (transform.tx_mm + transform.dtx_mm * dt) * 1e-3;
let ty = (transform.ty_mm + transform.dty_mm * dt) * 1e-3;
let tz = (transform.tz_mm + transform.dtz_mm * dt) * 1e-3;
let scale = 1.0 + (transform.scale_ppb + transform.dscale_ppb * dt) * 1e-9;
const MAS_TO_RAD: f64 = core::f64::consts::PI / (180.0 * 3_600_000.0);
let conv_sign = match transform.convention {
RotationConvention::PositionVector => 1.0,
RotationConvention::CoordinateFrame => -1.0,
};
let rx = conv_sign * (transform.rx_mas + transform.drx_mas * dt) * MAS_TO_RAD;
let ry = conv_sign * (transform.ry_mas + transform.dry_mas * dt) * MAS_TO_RAD;
let rz = conv_sign * (transform.rz_mas + transform.drz_mas * dt) * MAS_TO_RAD;
let xout = tx + scale * (x - rz * y + ry * z);
let yout = ty + scale * (rz * x + y - rx * z);
let zout = tz + scale * (-ry * x + rx * y + z);
Ok((xout, yout, zout))
}
pub fn apply_frame_transform_inverse(
transform: &FrameTransform,
x: f64,
y: f64,
z: f64,
epoch: f64,
) -> ProjResult<(f64, f64, f64)> {
let inv = FrameTransform {
from: transform.from,
to: transform.to,
ref_epoch: transform.ref_epoch,
tx_mm: -transform.tx_mm,
ty_mm: -transform.ty_mm,
tz_mm: -transform.tz_mm,
dtx_mm: -transform.dtx_mm,
dty_mm: -transform.dty_mm,
dtz_mm: -transform.dtz_mm,
scale_ppb: -transform.scale_ppb,
dscale_ppb: -transform.dscale_ppb,
rx_mas: -transform.rx_mas,
ry_mas: -transform.ry_mas,
rz_mas: -transform.rz_mas,
drx_mas: -transform.drx_mas,
dry_mas: -transform.dry_mas,
drz_mas: -transform.drz_mas,
convention: transform.convention,
};
apply_frame_transform(&inv, x, y, z, epoch)
}
pub fn apply_frame_path(
path: &[FramePathStep],
x: f64,
y: f64,
z: f64,
epoch: f64,
) -> ProjResult<(f64, f64, f64)> {
let mut cur = (x, y, z);
for step in path {
cur = if step.inverse {
apply_frame_transform_inverse(step.transform, cur.0, cur.1, cur.2, epoch)?
} else {
apply_frame_transform(step.transform, cur.0, cur.1, cur.2, epoch)?
};
}
Ok(cur)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn frame_transform_itrf2020_to_itrf2014_found() {
let ft = find_frame_transform("ITRF2020", "ITRF2014");
assert!(ft.is_some(), "ITRF2020->ITRF2014 transform not found");
}
#[test]
fn catalogue_itrf2020_to_itrf2014_matches_published_iers() {
let ft = find_frame_transform("ITRF2020", "ITRF2014").unwrap();
assert_eq!(ft.ref_epoch, 2015.0);
assert_eq!(ft.tx_mm, -1.4);
assert_eq!(ft.ty_mm, -0.9, "ITRF2020->ITRF2014 T2 must be -0.9 mm");
assert_eq!(ft.tz_mm, 1.4);
assert_eq!(ft.dty_mm, -0.1);
assert_eq!(ft.dtz_mm, 0.2);
assert_eq!(ft.scale_ppb, -0.42);
assert_eq!(ft.convention, RotationConvention::PositionVector);
}
#[test]
fn frame_transform_itrf2020_to_itrf2014_matches_cct() {
let ft = find_frame_transform("ITRF2020", "ITRF2014").unwrap();
let (x, y, z) =
apply_frame_transform(ft, 4_627_798.0, 119_795.0, 4_369_668.0, 2015.0).expect("apply");
assert!((x - 4_627_797.996_656).abs() < 5e-6, "x={x}");
assert!((y - 119_794.999_050).abs() < 5e-6, "y={y}");
assert!((z - 4_369_667.999_565).abs() < 5e-6, "z={z}");
}
#[test]
fn frame_transform_itrf2020_star_matches_cct() {
let cases: &[(&str, (f64, f64, f64))] = &[
(
"ITRF2008",
(4_627_797.998_858, 119_795.000_965, 4_369_668.002_033),
),
(
"ITRF2005",
(4_627_798.005_708, 119_795.000_178, 4_369_668.001_440),
),
(
"ITRF2000",
(4_627_798.010_213, 119_795.001_070, 4_369_667.975_632),
),
];
for (target, (ex, ey, ez)) in cases {
let ft = find_frame_transform("ITRF2020", target).unwrap();
let (x, y, z) =
apply_frame_transform(ft, 4_627_798.0, 119_795.0, 4_369_668.0, 2015.0).unwrap();
assert!((x - ex).abs() < 5e-6, "{target} x={x}");
assert!((y - ey).abs() < 5e-6, "{target} y={y}");
assert!((z - ez).abs() < 5e-6, "{target} z={z}");
}
}
#[test]
fn frame_transform_inverse_reverses_forward() {
let ft = find_frame_transform("ITRF2020", "ITRF2014").unwrap();
let x0 = 4_627_798.0_f64;
let y0 = 119_795.0_f64;
let z0 = 4_369_668.0_f64;
let (xf, yf, zf) = apply_frame_transform(ft, x0, y0, z0, 2015.0).unwrap();
let (xr, yr, zr) = apply_frame_transform_inverse(ft, xf, yf, zf, 2015.0).unwrap();
assert!(
(xr - x0).abs() < 0.001,
"x roundtrip error: {}",
(xr - x0).abs()
);
assert!(
(yr - y0).abs() < 0.001,
"y roundtrip error: {}",
(yr - y0).abs()
);
assert!(
(zr - z0).abs() < 0.001,
"z roundtrip error: {}",
(zr - z0).abs()
);
}
#[test]
fn frame_transform_unknown_returns_none() {
assert!(find_frame_transform("ITRF1900", "ITRF2014").is_none());
}
#[test]
fn find_frame_path_same_frame_is_empty() {
let p = find_frame_path("ITRF2020", "itrf2020").expect("same frame path");
assert!(p.is_empty());
}
#[test]
fn find_frame_path_direct_is_one_hop() {
let p = find_frame_path("ITRF2020", "ITRF2014").expect("direct");
assert_eq!(p.len(), 1);
assert!(!p[0].inverse);
assert_eq!(p[0].transform.to, "ITRF2014");
}
#[test]
fn find_frame_path_inverse_direct() {
let p = find_frame_path("ITRF2014", "ITRF2020").expect("inverse direct");
assert_eq!(p.len(), 1);
assert!(p[0].inverse);
assert_eq!(p[0].transform.from, "ITRF2020");
}
#[test]
fn find_frame_path_two_hop_itrf2014_to_itrf2000() {
let p = find_frame_path("ITRF2014", "ITRF2000").expect("2-hop");
assert_eq!(p.len(), 2);
}
#[test]
fn find_frame_path_reaches_gda2020_and_etrf_and_nad83() {
assert!(find_frame_path("ITRF2020", "GDA2020").is_some());
assert!(find_frame_path("ITRF2020", "NAD83(2011)").is_some());
assert!(find_frame_path("ITRF2020", "ETRF2000").is_some());
assert!(find_frame_path("ITRF2020", "ETRF2014").is_some());
assert_eq!(
find_frame_path("NAD83(2011)", "GDA2020").map(|p| p.len()),
Some(2)
);
}
#[test]
fn find_frame_path_disconnected_returns_none() {
assert!(find_frame_path("ITRF2020", "MADE_UP_FRAME").is_none());
}
#[test]
fn apply_frame_path_two_hop_matches_cct_pipeline() {
let path = find_frame_path("ITRF2014", "ITRF2000").expect("2-hop");
let (x, y, z) =
apply_frame_path(&path, 4_627_798.0, 119_795.0, 4_369_668.0, 2015.0).unwrap();
assert!((x - 4_627_798.013_556).abs() < 1e-5, "x={x}");
assert!((y - 119_795.002_020).abs() < 1e-5, "y={y}");
assert!((z - 4_369_667.976_067).abs() < 1e-5, "z={z}");
}
#[test]
fn coordinate_frame_nad83_matches_cct() {
let ft = find_frame_transform("ITRF2014", "NAD83(2011)").unwrap();
let (x, y, z) =
apply_frame_transform(ft, -1_300_000.0, -4_700_000.0, 4_000_000.0, 2010.0).unwrap();
assert!((x - (-1_299_999.236_130)).abs() < 5e-6, "x2010={x}");
assert!((y - (-4_700_001.322_685)).abs() < 5e-6, "y2010={y}");
assert!((z - 4_000_000.072_801).abs() < 5e-6, "z2010={z}");
let (x2, y2, z2) =
apply_frame_transform(ft, -1_300_000.0, -4_700_000.0, 4_000_000.0, 2020.0).unwrap();
assert!((x2 - (-1_299_999.068_711)).abs() < 5e-6, "x2020={x2}");
assert!((y2 - (-4_700_001.315_606)).abs() < 5e-6, "y2020={y2}");
assert!((z2 - 4_000_000.118_451).abs() < 5e-6, "z2020={z2}");
}
#[test]
fn coordinate_frame_gda2020_matches_cct() {
let ft = find_frame_transform("ITRF2014", "GDA2020").unwrap();
let (x, y, z) =
apply_frame_transform(ft, -4_000_000.0, 2_500_000.0, -3_800_000.0, 2015.0).unwrap();
assert!((x - (-4_000_000.182_170)).abs() < 5e-6, "x={x}");
assert!((y - 2_500_000.021_471).abs() < 5e-6, "y={y}");
assert!((z - (-3_799_999.794_116)).abs() < 5e-6, "z={z}");
}
#[test]
fn apply_frame_path_nad83_to_gda2020_matches_cct_pipeline() {
let path = find_frame_path("NAD83(2011)", "GDA2020").expect("path");
let (x, y, z) =
apply_frame_path(&path, -4_000_000.0, 2_500_000.0, -3_800_000.0, 2020.0).unwrap();
assert!((x - (-4_000_000.993_599)).abs() < 1e-4, "x={x}");
assert!((y - 2_500_002.219_715).abs() < 1e-4, "y={y}");
assert!((z - (-3_799_999.267_721)).abs() < 1e-4, "z={z}");
}
}