1use core::fmt;
28
29use super::{EcefPoint, Ellipsoid, GeodeticPoint, Height};
30use crate::error::{ensure_finite, KernelError, Result};
31use crate::math;
32use crate::position::Position;
33use crate::units::Distance;
34
35const RADIANS_PER_ARC_SECOND: f64 = core::f64::consts::PI / (180.0 * 3600.0);
37
38#[derive(Debug, Clone, Copy, PartialEq)]
49#[cfg_attr(
50 feature = "serde",
51 derive(serde::Serialize, serde::Deserialize),
52 serde(try_from = "StoredHelmert", into = "StoredHelmert")
53)]
54pub struct Helmert {
55 translation: [f64; 3],
57 rotation: [f64; 3],
59 scale_ppm: f64,
61}
62
63impl Helmert {
64 pub const IDENTITY: Self = Self {
66 translation: [0.0; 3],
67 rotation: [0.0; 3],
68 scale_ppm: 0.0,
69 };
70
71 #[must_use]
74 pub const fn translation(dx: f64, dy: f64, dz: f64) -> Self {
75 Self {
76 translation: [dx, dy, dz],
77 rotation: [0.0; 3],
78 scale_ppm: 0.0,
79 }
80 }
81
82 pub fn position_vector(
92 translation: [f64; 3],
93 rotation: [f64; 3],
94 scale_ppm: f64,
95 ) -> Result<Self> {
96 for value in translation {
97 ensure_finite("Helmert translation", value)?;
98 }
99 for value in rotation {
100 ensure_finite("Helmert rotation", value)?;
101 }
102 ensure_finite("Helmert scale difference", scale_ppm)?;
103 if scale_ppm <= -1e6 {
104 return Err(KernelError::OutOfRange {
105 parameter: "Helmert scale difference",
106 value: scale_ppm,
107 min: -1e6,
108 max: f64::INFINITY,
109 });
110 }
111 Ok(Self {
112 translation,
113 rotation,
114 scale_ppm,
115 })
116 }
117
118 pub fn coordinate_frame(
126 translation: [f64; 3],
127 rotation: [f64; 3],
128 scale_ppm: f64,
129 ) -> Result<Self> {
130 Self::position_vector(
131 translation,
132 [-rotation[0], -rotation[1], -rotation[2]],
133 scale_ppm,
134 )
135 }
136
137 #[must_use]
139 pub const fn translation_metres(&self) -> [f64; 3] {
140 self.translation
141 }
142
143 #[must_use]
145 pub const fn rotation_arc_seconds(&self) -> [f64; 3] {
146 self.rotation
147 }
148
149 #[must_use]
151 pub const fn scale_ppm(&self) -> f64 {
152 self.scale_ppm
153 }
154
155 #[must_use]
157 pub fn is_identity(&self) -> bool {
158 *self == Self::IDENTITY
159 }
160
161 fn rotation_radians(&self) -> [f64; 3] {
162 [
163 self.rotation[0] * RADIANS_PER_ARC_SECOND,
164 self.rotation[1] * RADIANS_PER_ARC_SECOND,
165 self.rotation[2] * RADIANS_PER_ARC_SECOND,
166 ]
167 }
168
169 fn scale(&self) -> f64 {
170 1.0 + self.scale_ppm * 1e-6
171 }
172
173 #[must_use]
175 pub fn apply(&self, point: EcefPoint) -> EcefPoint {
176 let [rx, ry, rz] = self.rotation_radians();
177 let [tx, ty, tz] = self.translation;
178 let scale = self.scale();
179 let (x, y, z) = (point.x, point.y, point.z);
180 EcefPoint {
181 x: tx + scale * (x - rz * y + ry * z),
182 y: ty + scale * (rz * x + y - rx * z),
183 z: tz + scale * (-ry * x + rx * y + z),
184 }
185 }
186
187 #[must_use]
192 pub fn apply_inverse(&self, point: EcefPoint) -> EcefPoint {
193 let [rx, ry, rz] = self.rotation_radians();
194 let [tx, ty, tz] = self.translation;
195 let scale = self.scale();
196 let (x, y, z) = (
197 (point.x - tx) / scale,
198 (point.y - ty) / scale,
199 (point.z - tz) / scale,
200 );
201 let along = rx * x + ry * y + rz * z;
202 let norm = 1.0 + rx * rx + ry * ry + rz * rz;
203 EcefPoint {
204 x: (x + rz * y - ry * z + rx * along) / norm,
205 y: (-rz * x + y + rx * z + ry * along) / norm,
206 z: (ry * x - rx * y + z + rz * along) / norm,
207 }
208 }
209}
210
211#[derive(Debug, Clone, Copy, PartialEq)]
219pub struct Datum {
220 name: &'static str,
221 ellipsoid: Ellipsoid,
222 to_wgs84: Helmert,
223 accuracy_metres: f64,
224}
225
226#[cfg(feature = "serde")]
229#[derive(serde::Serialize, serde::Deserialize)]
230struct StoredHelmert {
231 translation: [f64; 3],
232 rotation: [f64; 3],
233 scale_ppm: f64,
234}
235
236#[cfg(feature = "serde")]
237impl TryFrom<StoredHelmert> for Helmert {
238 type Error = KernelError;
239
240 fn try_from(stored: StoredHelmert) -> Result<Self> {
241 Self::position_vector(stored.translation, stored.rotation, stored.scale_ppm)
242 }
243}
244
245#[cfg(feature = "serde")]
246impl From<Helmert> for StoredHelmert {
247 fn from(helmert: Helmert) -> Self {
248 Self {
249 translation: helmert.translation,
250 rotation: helmert.rotation,
251 scale_ppm: helmert.scale_ppm,
252 }
253 }
254}
255
256impl Datum {
257 pub const WGS84: Self = Self {
259 name: "WGS 84",
260 ellipsoid: Ellipsoid::WGS84,
261 to_wgs84: Helmert::IDENTITY,
262 accuracy_metres: 0.0,
263 };
264
265 pub const NAD83: Self = Self {
268 name: "NAD83",
269 ellipsoid: Ellipsoid::GRS80,
270 to_wgs84: Helmert::IDENTITY,
271 accuracy_metres: 4.0,
272 };
273
274 pub const ED50: Self = Self {
277 name: "ED50",
278 ellipsoid: Ellipsoid::INTERNATIONAL_1924,
279 to_wgs84: Helmert::translation(-87.0, -98.0, -121.0),
280 accuracy_metres: 10.0,
281 };
282
283 pub const NAD27: Self = Self {
285 name: "NAD27",
286 ellipsoid: Ellipsoid::CLARKE_1866,
287 to_wgs84: Helmert::translation(-8.0, 160.0, 176.0),
288 accuracy_metres: 10.0,
289 };
290
291 pub const OSGB36: Self = Self {
293 name: "OSGB36",
294 ellipsoid: Ellipsoid::AIRY_1830,
295 to_wgs84: Helmert {
296 translation: [446.448, -125.157, 542.06],
297 rotation: [0.15, 0.247, 0.842],
298 scale_ppm: -20.489,
299 },
300 accuracy_metres: 2.0,
301 };
302
303 pub const PULKOVO_1942: Self = Self {
307 name: "Pulkovo 1942",
308 ellipsoid: Ellipsoid::KRASSOWSKY_1940,
309 to_wgs84: Helmert {
310 translation: [23.57, -140.95, -79.8],
311 rotation: [0.0, 0.35, 0.79],
312 scale_ppm: -0.22,
313 },
314 accuracy_metres: 3.0,
315 };
316
317 pub const TOKYO: Self = Self {
320 name: "Tokyo",
321 ellipsoid: Ellipsoid::BESSEL_1841,
322 to_wgs84: Helmert::translation(-148.0, 507.0, 685.0),
323 accuracy_metres: 29.0,
324 };
325
326 pub const DHDN: Self = Self {
329 name: "DHDN",
330 ellipsoid: Ellipsoid::BESSEL_1841,
331 to_wgs84: Helmert {
332 translation: [598.1, 73.7, 418.2],
333 rotation: [0.202, 0.045, -2.455],
334 scale_ppm: 6.7,
335 },
336 accuracy_metres: 3.0,
337 };
338
339 pub const AGD66: Self = Self {
342 name: "AGD66",
343 ellipsoid: Ellipsoid::AUSTRALIAN_NATIONAL,
344 to_wgs84: Helmert::translation(-127.8, -52.3, 152.9),
345 accuracy_metres: 5.0,
346 };
347
348 pub const SAD69: Self = Self {
350 name: "SAD69",
351 ellipsoid: Ellipsoid::AUSTRALIAN_NATIONAL,
352 to_wgs84: Helmert::translation(-57.0, 1.0, -41.0),
353 accuracy_metres: 19.0,
354 };
355
356 #[must_use]
359 pub fn new(
360 name: &'static str,
361 ellipsoid: Ellipsoid,
362 to_wgs84: Helmert,
363 accuracy: Distance,
364 ) -> Self {
365 Self {
366 name,
367 ellipsoid,
368 to_wgs84,
369 accuracy_metres: math::abs(accuracy.metres()),
370 }
371 }
372
373 #[must_use]
375 pub const fn name(&self) -> &'static str {
376 self.name
377 }
378
379 #[must_use]
381 pub const fn ellipsoid(&self) -> &Ellipsoid {
382 &self.ellipsoid
383 }
384
385 #[must_use]
387 pub const fn to_wgs84_helmert(&self) -> &Helmert {
388 &self.to_wgs84
389 }
390
391 #[must_use]
393 pub fn accuracy(&self) -> Distance {
394 Distance::from_metres(self.accuracy_metres).unwrap_or(Distance::ZERO)
395 }
396
397 pub fn to_wgs84(&self, position: Position) -> Result<Position> {
409 if self.to_wgs84.is_identity() && self.ellipsoid == Ellipsoid::WGS84 {
410 return Ok(position);
411 }
412 let point = GeodeticPoint::new(position, Height::above_ellipsoid(Distance::ZERO));
413 let geocentric = EcefPoint::from_geodetic(point, &self.ellipsoid)?;
414 let shifted = self.to_wgs84.apply(geocentric);
415 Ok(shifted.to_geodetic(&Ellipsoid::WGS84)?.position())
416 }
417
418 pub fn from_wgs84(&self, position: Position) -> Result<Position> {
430 if self.to_wgs84.is_identity() && self.ellipsoid == Ellipsoid::WGS84 {
431 return Ok(position);
432 }
433 let point = GeodeticPoint::new(position, Height::above_ellipsoid(Distance::ZERO));
434 let geocentric = EcefPoint::from_geodetic(point, &Ellipsoid::WGS84)?;
435 let shifted = self.to_wgs84.apply_inverse(geocentric);
436 Ok(shifted.to_geodetic(&self.ellipsoid)?.position())
437 }
438}
439
440impl fmt::Display for Datum {
441 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442 f.write_str(self.name)
443 }
444}
445
446#[cfg(test)]
447#[allow(clippy::unwrap_used, clippy::float_cmp)]
448mod tests {
449 use super::*;
450 use crate::position::{Latitude, Longitude};
451
452 fn position(latitude: f64, longitude: f64) -> Position {
453 Position::new(
454 Latitude::from_degrees(latitude).unwrap(),
455 Longitude::from_degrees(longitude).unwrap(),
456 )
457 }
458
459 fn metres_apart(a: Position, b: Position) -> f64 {
461 let dlat = math::to_radians(b.latitude().degrees() - a.latitude().degrees());
462 let dlon = b.longitude_difference(a).radians();
463 let cos = math::cos(math::to_radians(a.latitude().degrees()));
464 6_371_000.0 * math::hypot(dlat, dlon * cos)
465 }
466
467 #[test]
468 fn the_identity_leaves_a_point_alone_and_a_translation_moves_it() {
469 let point = EcefPoint::new(
470 Distance::from_metres(1.0).unwrap(),
471 Distance::from_metres(2.0).unwrap(),
472 Distance::from_metres(3.0).unwrap(),
473 );
474 assert_eq!(Helmert::IDENTITY.apply(point), point);
475 assert!(Helmert::IDENTITY.is_identity());
476 let moved = Helmert::translation(10.0, -20.0, 30.0).apply(point);
477 assert_eq!(moved.x().metres(), 11.0);
478 assert_eq!(moved.y().metres(), -18.0);
479 assert_eq!(moved.z().metres(), 33.0);
480 }
481
482 #[test]
483 fn the_two_conventions_differ_by_the_sign_of_the_rotation() {
484 let pv = Helmert::position_vector([1.0, 2.0, 3.0], [0.1, -0.2, 0.3], 1.5).unwrap();
485 let cf = Helmert::coordinate_frame([1.0, 2.0, 3.0], [-0.1, 0.2, -0.3], 1.5).unwrap();
486 assert_eq!(pv, cf);
487 assert_eq!(pv.rotation_arc_seconds(), [0.1, -0.2, 0.3]);
488 assert_eq!(pv.translation_metres(), [1.0, 2.0, 3.0]);
489 assert_eq!(pv.scale_ppm(), 1.5);
490 }
491
492 #[test]
493 fn wild_parameters_are_refused() {
494 assert!(Helmert::position_vector([f64::NAN, 0.0, 0.0], [0.0; 3], 0.0).is_err());
495 assert!(Helmert::position_vector([0.0; 3], [0.0, f64::INFINITY, 0.0], 0.0).is_err());
496 assert!(Helmert::position_vector([0.0; 3], [0.0; 3], f64::NAN).is_err());
497 assert!(matches!(
498 Helmert::coordinate_frame([0.0; 3], [0.0; 3], -1e6),
499 Err(KernelError::OutOfRange { .. })
500 ));
501 }
502
503 #[test]
504 fn the_inverse_is_exact_not_the_reversed_parameters() {
505 let helmert = Datum::OSGB36.to_wgs84;
506 let point = EcefPoint::new(
507 Distance::from_metres(3_874_938.849).unwrap(),
508 Distance::from_metres(116_218.624).unwrap(),
509 Distance::from_metres(5_047_168.208).unwrap(),
510 );
511 let back = helmert.apply_inverse(helmert.apply(point));
512 assert!(back.chord_to(point).metres() < 1e-9, "{back:?}");
513
514 let reversed = Helmert {
518 translation: [-446.448, 125.157, -542.06],
519 rotation: [-0.15, -0.247, -0.842],
520 scale_ppm: 20.489,
521 };
522 let approximate = reversed.apply(helmert.apply(point));
523 assert!(approximate.chord_to(point).metres() > 1e-6);
524 }
525
526 #[test]
527 fn wgs84_and_nad83_shift_nothing() {
528 let here = position(38.9, -77.0);
529 assert_eq!(Datum::WGS84.to_wgs84(here).unwrap(), here);
530 assert_eq!(Datum::WGS84.from_wgs84(here).unwrap(), here);
531 assert_eq!(Datum::WGS84.accuracy(), Distance::ZERO);
532 let shifted = Datum::NAD83.to_wgs84(here).unwrap();
535 assert!(metres_apart(here, shifted) < 1e-3);
536 }
537
538 #[test]
539 fn the_datums_are_named_and_carry_their_accuracy() {
540 assert_eq!(Datum::OSGB36.name(), "OSGB36");
541 assert_eq!(alloc::format!("{}", Datum::PULKOVO_1942), "Pulkovo 1942");
542 assert!((Datum::TOKYO.accuracy().metres() - 29.0).abs() < 1e-9);
543 assert_eq!(*Datum::ED50.ellipsoid(), Ellipsoid::INTERNATIONAL_1924);
544 assert_eq!(
545 Datum::NAD27.to_wgs84_helmert().translation_metres(),
546 [-8.0, 160.0, 176.0]
547 );
548 let own = Datum::new(
549 "chart note",
550 Ellipsoid::INTERNATIONAL_1924,
551 Helmert::translation(-84.0, -97.0, -117.0),
552 Distance::from_metres(-5.0).unwrap(),
553 );
554 assert_eq!(own.name(), "chart note");
555 assert!((own.accuracy().metres() - 5.0).abs() < 1e-9);
556 }
557}