use crate::datum::HelmertParams;
const ARCSEC_TO_RAD: f64 = std::f64::consts::PI / (180.0 * 3600.0);
pub(crate) fn helmert_forward(params: &HelmertParams, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let s = 1.0 + params.ds() * 1e-6; let rx = params.rx() * ARCSEC_TO_RAD;
let ry = params.ry() * ARCSEC_TO_RAD;
let rz = params.rz() * ARCSEC_TO_RAD;
let xo = params.dx() + s * (x - rz * y + ry * z);
let yo = params.dy() + s * (rz * x + y - rx * z);
let zo = params.dz() + s * (-ry * x + rx * y + z);
(xo, yo, zo)
}
pub(crate) fn helmert_inverse(params: &HelmertParams, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
let s = 1.0 + params.ds() * 1e-6;
let rx = params.rx() * ARCSEC_TO_RAD;
let ry = params.ry() * ARCSEC_TO_RAD;
let rz = params.rz() * ARCSEC_TO_RAD;
let ux = (x - params.dx()) / s;
let uy = (y - params.dy()) / s;
let uz = (z - params.dz()) / s;
let det = 1.0 + rx * rx + ry * ry + rz * rz;
let xo = ((1.0 + rx * rx) * ux + (rz + rx * ry) * uy + (rx * rz - ry) * uz) / det;
let yo = ((rx * ry - rz) * ux + (1.0 + ry * ry) * uy + (rx + ry * rz) * uz) / det;
let zo = ((ry + rx * rz) * ux + (ry * rz - rx) * uy + (1.0 + rz * rz) * uz) / det;
(xo, yo, zo)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::datum;
use crate::ellipsoid;
use crate::geocentric::{geocentric_to_geodetic, geodetic_to_geocentric};
#[test]
fn identity_with_zero_params() {
let params = HelmertParams::translation(0.0, 0.0, 0.0).unwrap();
let (xo, yo, zo) = helmert_forward(¶ms, 1000.0, 2000.0, 3000.0);
assert_eq!(xo, 1000.0);
assert_eq!(yo, 2000.0);
assert_eq!(zo, 3000.0);
}
#[test]
fn translation_only() {
let params = HelmertParams::translation(10.0, 20.0, 30.0).unwrap();
let (xo, yo, zo) = helmert_forward(¶ms, 1000.0, 2000.0, 3000.0);
assert!((xo - 1010.0).abs() < 1e-6);
assert!((yo - 2020.0).abs() < 1e-6);
assert!((zo - 3030.0).abs() < 1e-6);
}
#[test]
fn roundtrip_forward_inverse() {
let datum = datum::OSGB36;
let params = datum.helmert_to_wgs84().unwrap();
let x = 3_980_000.0;
let y = -100_000.0;
let z = 4_960_000.0;
let (x2, y2, z2) = helmert_forward(params, x, y, z);
let (x3, y3, z3) = helmert_inverse(params, x2, y2, z2);
assert!((x3 - x).abs() < 1e-8, "x: {x3} vs {x}");
assert!((y3 - y).abs() < 1e-8, "y: {y3} vs {y}");
assert!((z3 - z).abs() < 1e-8, "z: {z3} vs {z}");
}
#[test]
fn roundtrip_is_exact_for_large_rotations() {
let params = HelmertParams::new(120.0, -75.0, 310.0, 300.0, -450.0, 600.0, 25.0).unwrap();
let x = 3_980_000.0;
let y = -100_000.0;
let z = 4_960_000.0;
let (x2, y2, z2) = helmert_forward(¶ms, x, y, z);
let (x3, y3, z3) = helmert_inverse(¶ms, x2, y2, z2);
assert!((x3 - x).abs() < 1e-8, "x: {x3} vs {x}");
assert!((y3 - y).abs() < 1e-8, "y: {y3} vs {y}");
assert!((z3 - z).abs() < 1e-8, "z: {z3} vs {z}");
}
#[test]
fn inverse_matches_negation_to_first_order() {
let datum = datum::OSGB36;
let params = datum.helmert_to_wgs84().unwrap();
let x = 3_980_000.0;
let y = -100_000.0;
let z = 4_960_000.0;
let (ex, ey, ez) = helmert_inverse(params, x, y, z);
let (nx, ny, nz) = helmert_forward(¶ms.inverse(), x, y, z);
assert!((ex - nx).abs() < 0.01, "x: {ex} vs {nx}");
assert!((ey - ny).abs() < 0.01, "y: {ey} vs {ny}");
assert!((ez - nz).abs() < 0.01, "z: {ez} vs {nz}");
}
#[test]
fn nad27_to_wgs84_known_point() {
let nad27 = datum::NAD27;
let nad27_params = nad27.helmert_to_wgs84().unwrap();
let lon = (-90.0_f64).to_radians();
let lat = 45.0_f64.to_radians();
let h = 0.0;
let (x, y, z) = geodetic_to_geocentric(&ellipsoid::CLARKE1866, lon, lat, h);
let (x2, y2, z2) = helmert_forward(nad27_params, x, y, z);
let (lon2, lat2, _h2) = geocentric_to_geodetic(&ellipsoid::WGS84, x2, y2, z2).unwrap();
let d_lon = (lon2 - lon).to_degrees() * 3600.0; let d_lat = (lat2 - lat).to_degrees() * 3600.0;
assert!(d_lon.abs() < 10.0, "lon shift: {d_lon} arcseconds");
assert!(d_lat.abs() < 10.0, "lat shift: {d_lat} arcseconds");
}
}