use core::fmt;
use super::{EcefPoint, Ellipsoid, GeodeticPoint, Height};
use crate::error::{ensure_finite, KernelError, Result};
use crate::math;
use crate::position::Position;
use crate::units::Distance;
const RADIANS_PER_ARC_SECOND: f64 = core::f64::consts::PI / (180.0 * 3600.0);
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "StoredHelmert", into = "StoredHelmert")
)]
pub struct Helmert {
translation: [f64; 3],
rotation: [f64; 3],
scale_ppm: f64,
}
impl Helmert {
pub const IDENTITY: Self = Self {
translation: [0.0; 3],
rotation: [0.0; 3],
scale_ppm: 0.0,
};
#[must_use]
pub const fn translation(dx: f64, dy: f64, dz: f64) -> Self {
Self {
translation: [dx, dy, dz],
rotation: [0.0; 3],
scale_ppm: 0.0,
}
}
pub fn position_vector(
translation: [f64; 3],
rotation: [f64; 3],
scale_ppm: f64,
) -> Result<Self> {
for value in translation {
ensure_finite("Helmert translation", value)?;
}
for value in rotation {
ensure_finite("Helmert rotation", value)?;
}
ensure_finite("Helmert scale difference", scale_ppm)?;
if scale_ppm <= -1e6 {
return Err(KernelError::OutOfRange {
parameter: "Helmert scale difference",
value: scale_ppm,
min: -1e6,
max: f64::INFINITY,
});
}
Ok(Self {
translation,
rotation,
scale_ppm,
})
}
pub fn coordinate_frame(
translation: [f64; 3],
rotation: [f64; 3],
scale_ppm: f64,
) -> Result<Self> {
Self::position_vector(
translation,
[-rotation[0], -rotation[1], -rotation[2]],
scale_ppm,
)
}
#[must_use]
pub const fn translation_metres(&self) -> [f64; 3] {
self.translation
}
#[must_use]
pub const fn rotation_arc_seconds(&self) -> [f64; 3] {
self.rotation
}
#[must_use]
pub const fn scale_ppm(&self) -> f64 {
self.scale_ppm
}
#[must_use]
pub fn is_identity(&self) -> bool {
*self == Self::IDENTITY
}
fn rotation_radians(&self) -> [f64; 3] {
[
self.rotation[0] * RADIANS_PER_ARC_SECOND,
self.rotation[1] * RADIANS_PER_ARC_SECOND,
self.rotation[2] * RADIANS_PER_ARC_SECOND,
]
}
fn scale(&self) -> f64 {
1.0 + self.scale_ppm * 1e-6
}
#[must_use]
pub fn apply(&self, point: EcefPoint) -> EcefPoint {
let [rx, ry, rz] = self.rotation_radians();
let [tx, ty, tz] = self.translation;
let scale = self.scale();
let (x, y, z) = (point.x, point.y, point.z);
EcefPoint {
x: tx + scale * (x - rz * y + ry * z),
y: ty + scale * (rz * x + y - rx * z),
z: tz + scale * (-ry * x + rx * y + z),
}
}
#[must_use]
pub fn apply_inverse(&self, point: EcefPoint) -> EcefPoint {
let [rx, ry, rz] = self.rotation_radians();
let [tx, ty, tz] = self.translation;
let scale = self.scale();
let (x, y, z) = (
(point.x - tx) / scale,
(point.y - ty) / scale,
(point.z - tz) / scale,
);
let along = rx * x + ry * y + rz * z;
let norm = 1.0 + rx * rx + ry * ry + rz * rz;
EcefPoint {
x: (x + rz * y - ry * z + rx * along) / norm,
y: (-rz * x + y + rx * z + ry * along) / norm,
z: (ry * x - rx * y + z + rz * along) / norm,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Datum {
name: &'static str,
ellipsoid: Ellipsoid,
to_wgs84: Helmert,
accuracy_metres: f64,
}
#[cfg(feature = "serde")]
#[derive(serde::Serialize, serde::Deserialize)]
struct StoredHelmert {
translation: [f64; 3],
rotation: [f64; 3],
scale_ppm: f64,
}
#[cfg(feature = "serde")]
impl TryFrom<StoredHelmert> for Helmert {
type Error = KernelError;
fn try_from(stored: StoredHelmert) -> Result<Self> {
Self::position_vector(stored.translation, stored.rotation, stored.scale_ppm)
}
}
#[cfg(feature = "serde")]
impl From<Helmert> for StoredHelmert {
fn from(helmert: Helmert) -> Self {
Self {
translation: helmert.translation,
rotation: helmert.rotation,
scale_ppm: helmert.scale_ppm,
}
}
}
impl Datum {
pub const WGS84: Self = Self {
name: "WGS 84",
ellipsoid: Ellipsoid::WGS84,
to_wgs84: Helmert::IDENTITY,
accuracy_metres: 0.0,
};
pub const NAD83: Self = Self {
name: "NAD83",
ellipsoid: Ellipsoid::GRS80,
to_wgs84: Helmert::IDENTITY,
accuracy_metres: 4.0,
};
pub const ED50: Self = Self {
name: "ED50",
ellipsoid: Ellipsoid::INTERNATIONAL_1924,
to_wgs84: Helmert::translation(-87.0, -98.0, -121.0),
accuracy_metres: 10.0,
};
pub const NAD27: Self = Self {
name: "NAD27",
ellipsoid: Ellipsoid::CLARKE_1866,
to_wgs84: Helmert::translation(-8.0, 160.0, 176.0),
accuracy_metres: 10.0,
};
pub const OSGB36: Self = Self {
name: "OSGB36",
ellipsoid: Ellipsoid::AIRY_1830,
to_wgs84: Helmert {
translation: [446.448, -125.157, 542.06],
rotation: [0.15, 0.247, 0.842],
scale_ppm: -20.489,
},
accuracy_metres: 2.0,
};
pub const PULKOVO_1942: Self = Self {
name: "Pulkovo 1942",
ellipsoid: Ellipsoid::KRASSOWSKY_1940,
to_wgs84: Helmert {
translation: [23.57, -140.95, -79.8],
rotation: [0.0, 0.35, 0.79],
scale_ppm: -0.22,
},
accuracy_metres: 3.0,
};
pub const TOKYO: Self = Self {
name: "Tokyo",
ellipsoid: Ellipsoid::BESSEL_1841,
to_wgs84: Helmert::translation(-148.0, 507.0, 685.0),
accuracy_metres: 29.0,
};
pub const DHDN: Self = Self {
name: "DHDN",
ellipsoid: Ellipsoid::BESSEL_1841,
to_wgs84: Helmert {
translation: [598.1, 73.7, 418.2],
rotation: [0.202, 0.045, -2.455],
scale_ppm: 6.7,
},
accuracy_metres: 3.0,
};
pub const AGD66: Self = Self {
name: "AGD66",
ellipsoid: Ellipsoid::AUSTRALIAN_NATIONAL,
to_wgs84: Helmert::translation(-127.8, -52.3, 152.9),
accuracy_metres: 5.0,
};
pub const SAD69: Self = Self {
name: "SAD69",
ellipsoid: Ellipsoid::AUSTRALIAN_NATIONAL,
to_wgs84: Helmert::translation(-57.0, 1.0, -41.0),
accuracy_metres: 19.0,
};
#[must_use]
pub fn new(
name: &'static str,
ellipsoid: Ellipsoid,
to_wgs84: Helmert,
accuracy: Distance,
) -> Self {
Self {
name,
ellipsoid,
to_wgs84,
accuracy_metres: math::abs(accuracy.metres()),
}
}
#[must_use]
pub const fn name(&self) -> &'static str {
self.name
}
#[must_use]
pub const fn ellipsoid(&self) -> &Ellipsoid {
&self.ellipsoid
}
#[must_use]
pub const fn to_wgs84_helmert(&self) -> &Helmert {
&self.to_wgs84
}
#[must_use]
pub fn accuracy(&self) -> Distance {
Distance::from_metres(self.accuracy_metres).unwrap_or(Distance::ZERO)
}
pub fn to_wgs84(&self, position: Position) -> Result<Position> {
if self.to_wgs84.is_identity() && self.ellipsoid == Ellipsoid::WGS84 {
return Ok(position);
}
let point = GeodeticPoint::new(position, Height::above_ellipsoid(Distance::ZERO));
let geocentric = EcefPoint::from_geodetic(point, &self.ellipsoid)?;
let shifted = self.to_wgs84.apply(geocentric);
Ok(shifted.to_geodetic(&Ellipsoid::WGS84)?.position())
}
pub fn from_wgs84(&self, position: Position) -> Result<Position> {
if self.to_wgs84.is_identity() && self.ellipsoid == Ellipsoid::WGS84 {
return Ok(position);
}
let point = GeodeticPoint::new(position, Height::above_ellipsoid(Distance::ZERO));
let geocentric = EcefPoint::from_geodetic(point, &Ellipsoid::WGS84)?;
let shifted = self.to_wgs84.apply_inverse(geocentric);
Ok(shifted.to_geodetic(&self.ellipsoid)?.position())
}
}
impl fmt::Display for Datum {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::float_cmp)]
mod tests {
use super::*;
use crate::position::{Latitude, Longitude};
fn position(latitude: f64, longitude: f64) -> Position {
Position::new(
Latitude::from_degrees(latitude).unwrap(),
Longitude::from_degrees(longitude).unwrap(),
)
}
fn metres_apart(a: Position, b: Position) -> f64 {
let dlat = math::to_radians(b.latitude().degrees() - a.latitude().degrees());
let dlon = b.longitude_difference(a).radians();
let cos = math::cos(math::to_radians(a.latitude().degrees()));
6_371_000.0 * math::hypot(dlat, dlon * cos)
}
#[test]
fn the_identity_leaves_a_point_alone_and_a_translation_moves_it() {
let point = EcefPoint::new(
Distance::from_metres(1.0).unwrap(),
Distance::from_metres(2.0).unwrap(),
Distance::from_metres(3.0).unwrap(),
);
assert_eq!(Helmert::IDENTITY.apply(point), point);
assert!(Helmert::IDENTITY.is_identity());
let moved = Helmert::translation(10.0, -20.0, 30.0).apply(point);
assert_eq!(moved.x().metres(), 11.0);
assert_eq!(moved.y().metres(), -18.0);
assert_eq!(moved.z().metres(), 33.0);
}
#[test]
fn the_two_conventions_differ_by_the_sign_of_the_rotation() {
let pv = Helmert::position_vector([1.0, 2.0, 3.0], [0.1, -0.2, 0.3], 1.5).unwrap();
let cf = Helmert::coordinate_frame([1.0, 2.0, 3.0], [-0.1, 0.2, -0.3], 1.5).unwrap();
assert_eq!(pv, cf);
assert_eq!(pv.rotation_arc_seconds(), [0.1, -0.2, 0.3]);
assert_eq!(pv.translation_metres(), [1.0, 2.0, 3.0]);
assert_eq!(pv.scale_ppm(), 1.5);
}
#[test]
fn wild_parameters_are_refused() {
assert!(Helmert::position_vector([f64::NAN, 0.0, 0.0], [0.0; 3], 0.0).is_err());
assert!(Helmert::position_vector([0.0; 3], [0.0, f64::INFINITY, 0.0], 0.0).is_err());
assert!(Helmert::position_vector([0.0; 3], [0.0; 3], f64::NAN).is_err());
assert!(matches!(
Helmert::coordinate_frame([0.0; 3], [0.0; 3], -1e6),
Err(KernelError::OutOfRange { .. })
));
}
#[test]
fn the_inverse_is_exact_not_the_reversed_parameters() {
let helmert = Datum::OSGB36.to_wgs84;
let point = EcefPoint::new(
Distance::from_metres(3_874_938.849).unwrap(),
Distance::from_metres(116_218.624).unwrap(),
Distance::from_metres(5_047_168.208).unwrap(),
);
let back = helmert.apply_inverse(helmert.apply(point));
assert!(back.chord_to(point).metres() < 1e-9, "{back:?}");
let reversed = Helmert {
translation: [-446.448, 125.157, -542.06],
rotation: [-0.15, -0.247, -0.842],
scale_ppm: 20.489,
};
let approximate = reversed.apply(helmert.apply(point));
assert!(approximate.chord_to(point).metres() > 1e-6);
}
#[test]
fn wgs84_and_nad83_shift_nothing() {
let here = position(38.9, -77.0);
assert_eq!(Datum::WGS84.to_wgs84(here).unwrap(), here);
assert_eq!(Datum::WGS84.from_wgs84(here).unwrap(), here);
assert_eq!(Datum::WGS84.accuracy(), Distance::ZERO);
let shifted = Datum::NAD83.to_wgs84(here).unwrap();
assert!(metres_apart(here, shifted) < 1e-3);
}
#[test]
fn the_datums_are_named_and_carry_their_accuracy() {
assert_eq!(Datum::OSGB36.name(), "OSGB36");
assert_eq!(alloc::format!("{}", Datum::PULKOVO_1942), "Pulkovo 1942");
assert!((Datum::TOKYO.accuracy().metres() - 29.0).abs() < 1e-9);
assert_eq!(*Datum::ED50.ellipsoid(), Ellipsoid::INTERNATIONAL_1924);
assert_eq!(
Datum::NAD27.to_wgs84_helmert().translation_metres(),
[-8.0, 160.0, 176.0]
);
let own = Datum::new(
"chart note",
Ellipsoid::INTERNATIONAL_1924,
Helmert::translation(-84.0, -97.0, -117.0),
Distance::from_metres(-5.0).unwrap(),
);
assert_eq!(own.name(), "chart note");
assert!((own.accuracy().metres() - 5.0).abs() < 1e-9);
}
}