Skip to main content

proj_core/
datum.rs

1use crate::ellipsoid::{self, Ellipsoid};
2
3/// A geodetic datum, defined by a reference ellipsoid and its relationship to WGS84.
4#[derive(Debug, Clone, Copy)]
5pub struct Datum {
6    /// The reference ellipsoid.
7    pub ellipsoid: Ellipsoid,
8    /// 7-parameter Helmert transformation from this datum to WGS84.
9    /// `None` means this datum is WGS84 (or is functionally identical, e.g. NAD83).
10    pub to_wgs84: Option<HelmertParams>,
11}
12
13impl Datum {
14    /// Returns true if this datum is WGS84 or functionally identical (no Helmert shift needed).
15    pub fn is_wgs84_compatible(&self) -> bool {
16        self.to_wgs84.is_none()
17    }
18
19    /// Returns true if two datums are the same (same ellipsoid, same Helmert parameters).
20    pub fn same_datum(&self, other: &Datum) -> bool {
21        // If both are WGS84-compatible with the same ellipsoid, they're the same.
22        // If both have Helmert params, compare them.
23        let same_ellipsoid = (self.ellipsoid.a - other.ellipsoid.a).abs() < 1e-6
24            && (self.ellipsoid.f - other.ellipsoid.f).abs() < 1e-12;
25
26        match (&self.to_wgs84, &other.to_wgs84) {
27            (None, None) => same_ellipsoid,
28            (Some(a), Some(b)) => same_ellipsoid && a.approx_eq(b),
29            _ => false,
30        }
31    }
32}
33
34/// 7-parameter Helmert (Bursa-Wolf) transformation parameters.
35///
36/// Defines the transformation from one datum to WGS84 geocentric coordinates:
37/// ```text
38/// [X']   [dx]         [  1  -rz   ry] [X]
39/// [Y'] = [dy] + (1+ds)[  rz   1  -rx] [Y]
40/// [Z']   [dz]         [ -ry  rx   1 ] [Z]
41/// ```
42#[derive(Debug, Clone, Copy)]
43pub struct HelmertParams {
44    /// X-axis translation in meters.
45    pub dx: f64,
46    /// Y-axis translation in meters.
47    pub dy: f64,
48    /// Z-axis translation in meters.
49    pub dz: f64,
50    /// X-axis rotation in arc-seconds.
51    pub rx: f64,
52    /// Y-axis rotation in arc-seconds.
53    pub ry: f64,
54    /// Z-axis rotation in arc-seconds.
55    pub rz: f64,
56    /// Scale difference in parts-per-million (ppm).
57    pub ds: f64,
58}
59
60impl HelmertParams {
61    /// Create a translation-only (3-parameter) transformation.
62    pub const fn translation(dx: f64, dy: f64, dz: f64) -> Self {
63        Self {
64            dx,
65            dy,
66            dz,
67            rx: 0.0,
68            ry: 0.0,
69            rz: 0.0,
70            ds: 0.0,
71        }
72    }
73
74    /// Return the inverse parameters (WGS84 → this datum).
75    pub fn inverse(&self) -> Self {
76        Self {
77            dx: -self.dx,
78            dy: -self.dy,
79            dz: -self.dz,
80            rx: -self.rx,
81            ry: -self.ry,
82            rz: -self.rz,
83            ds: -self.ds,
84        }
85    }
86
87    fn approx_eq(&self, other: &Self) -> bool {
88        (self.dx - other.dx).abs() < 1e-6
89            && (self.dy - other.dy).abs() < 1e-6
90            && (self.dz - other.dz).abs() < 1e-6
91            && (self.rx - other.rx).abs() < 1e-9
92            && (self.ry - other.ry).abs() < 1e-9
93            && (self.rz - other.rz).abs() < 1e-9
94            && (self.ds - other.ds).abs() < 1e-9
95    }
96}
97
98// ---------------------------------------------------------------------------
99// Well-known datums
100// ---------------------------------------------------------------------------
101
102/// WGS 84 datum.
103pub const WGS84: Datum = Datum {
104    ellipsoid: ellipsoid::WGS84,
105    to_wgs84: None,
106};
107
108/// NAD83 datum (functionally identical to WGS84 for sub-meter work).
109pub const NAD83: Datum = Datum {
110    ellipsoid: ellipsoid::GRS80,
111    to_wgs84: None,
112};
113
114/// NAD27 datum (Clarke 1866 ellipsoid).
115/// Helmert parameters from EPSG dataset (approximate continental US average).
116pub const NAD27: Datum = Datum {
117    ellipsoid: ellipsoid::CLARKE1866,
118    to_wgs84: Some(HelmertParams::translation(-8.0, 160.0, 176.0)),
119};
120
121/// ETRS89 datum (European Terrestrial Reference System 1989).
122/// Functionally identical to WGS84 for most purposes.
123pub const ETRS89: Datum = Datum {
124    ellipsoid: ellipsoid::GRS80,
125    to_wgs84: None,
126};
127
128/// OSGB36 datum (Ordnance Survey Great Britain 1936).
129pub const OSGB36: Datum = Datum {
130    ellipsoid: ellipsoid::AIRY1830,
131    to_wgs84: Some(HelmertParams {
132        dx: 446.448,
133        dy: -125.157,
134        dz: 542.060,
135        rx: 0.1502,
136        ry: 0.2470,
137        rz: 0.8421,
138        ds: -20.4894,
139    }),
140};
141
142/// Pulkovo 1942 datum (used in Russia and former Soviet states).
143pub const PULKOVO1942: Datum = Datum {
144    ellipsoid: ellipsoid::KRASSOWSKY,
145    to_wgs84: Some(HelmertParams::translation(23.92, -141.27, -80.9)),
146};
147
148/// ED50 datum (European Datum 1950).
149pub const ED50: Datum = Datum {
150    ellipsoid: ellipsoid::INTL1924,
151    to_wgs84: Some(HelmertParams::translation(-87.0, -98.0, -121.0)),
152};
153
154/// Tokyo datum (used in Japan).
155pub const TOKYO: Datum = Datum {
156    ellipsoid: ellipsoid::BESSEL1841,
157    to_wgs84: Some(HelmertParams::translation(-146.414, 507.337, 680.507)),
158};
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn wgs84_is_wgs84_compatible() {
166        assert!(WGS84.is_wgs84_compatible());
167        assert!(NAD83.is_wgs84_compatible());
168        assert!(ETRS89.is_wgs84_compatible());
169    }
170
171    #[test]
172    fn nad27_is_not_wgs84_compatible() {
173        assert!(!NAD27.is_wgs84_compatible());
174        assert!(!OSGB36.is_wgs84_compatible());
175    }
176
177    #[test]
178    fn same_datum_identity() {
179        assert!(WGS84.same_datum(&WGS84));
180        assert!(NAD27.same_datum(&NAD27));
181    }
182
183    #[test]
184    fn different_datums() {
185        assert!(!WGS84.same_datum(&NAD27));
186        assert!(!NAD27.same_datum(&OSGB36));
187    }
188
189    #[test]
190    fn helmert_inverse_negates() {
191        let h = HelmertParams {
192            dx: 1.0,
193            dy: 2.0,
194            dz: 3.0,
195            rx: 0.1,
196            ry: 0.2,
197            rz: 0.3,
198            ds: 0.5,
199        };
200        let inv = h.inverse();
201        assert_eq!(inv.dx, -1.0);
202        assert_eq!(inv.rx, -0.1);
203        assert_eq!(inv.ds, -0.5);
204    }
205}