1use core::fmt;
54use core::marker::PhantomData;
55use core::ops::{Add, Div, Mul, Neg, Sub};
56
57use crate::angle::TrueCourse;
58use crate::error::Result;
59use crate::geodesy::{EcefPoint, Ellipsoid, GeodeticPoint};
60use crate::math;
61use crate::units::{Distance, Speed};
62
63mod sealed {
64 pub trait Sealed {}
65}
66
67pub trait VectorFrame:
71 sealed::Sealed + Copy + Clone + fmt::Debug + Eq + core::hash::Hash + Default + 'static
72{
73 const NAME: &'static str;
75 const AXES: [&'static str; 3];
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
84#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
85pub struct Ned;
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
89#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
90pub struct Enu;
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
98#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
99pub struct Body;
100
101impl sealed::Sealed for Ned {}
102impl sealed::Sealed for Enu {}
103impl sealed::Sealed for Body {}
104
105impl VectorFrame for Ned {
106 const NAME: &'static str = "NED";
107 const AXES: [&'static str; 3] = ["north", "east", "down"];
108}
109
110impl VectorFrame for Enu {
111 const NAME: &'static str = "ENU";
112 const AXES: [&'static str; 3] = ["east", "north", "up"];
113}
114
115impl VectorFrame for Body {
116 const NAME: &'static str = "body";
117 const AXES: [&'static str; 3] = ["forward", "right", "down"];
118}
119
120pub trait VectorUnit:
124 sealed::Sealed
125 + Copy
126 + fmt::Debug
127 + PartialEq
128 + Add<Output = Self>
129 + Sub<Output = Self>
130 + Neg<Output = Self>
131 + Mul<f64, Output = Self>
132{
133 fn si(self) -> f64;
135 fn from_si(value: f64) -> Self;
137}
138
139impl sealed::Sealed for Distance {}
140impl sealed::Sealed for Speed {}
141
142impl VectorUnit for Distance {
143 fn si(self) -> f64 {
144 self.metres()
145 }
146
147 fn from_si(value: f64) -> Self {
148 Self::from_metres(value).unwrap_or(Self::ZERO)
149 }
150}
151
152impl VectorUnit for Speed {
153 fn si(self) -> f64 {
154 self.metres_per_second()
155 }
156
157 fn from_si(value: f64) -> Self {
158 Self::from_metres_per_second(value).unwrap_or(Self::ZERO)
159 }
160}
161
162#[derive(Clone, Copy, PartialEq)]
167pub struct Vector3<F: VectorFrame, U: VectorUnit> {
168 components: [U; 3],
169 frame: PhantomData<F>,
170}
171
172impl<F: VectorFrame, U: VectorUnit> Vector3<F, U> {
173 #[must_use]
175 pub const fn new(first: U, second: U, third: U) -> Self {
176 Self {
177 components: [first, second, third],
178 frame: PhantomData,
179 }
180 }
181
182 #[must_use]
184 pub const fn components(&self) -> [U; 3] {
185 self.components
186 }
187
188 #[must_use]
190 pub fn magnitude(&self) -> U {
191 let [a, b, c] = self.si();
192 U::from_si(math::hypot(math::hypot(a, b), c))
193 }
194
195 #[must_use]
198 pub fn horizontal_magnitude(&self) -> U {
199 let [a, b, _] = self.si();
200 U::from_si(math::hypot(a, b))
201 }
202
203 fn si(&self) -> [f64; 3] {
204 self.components.map(U::si)
205 }
206
207 fn from_si(components: [f64; 3]) -> Self {
208 Self {
209 components: components.map(U::from_si),
210 frame: PhantomData,
211 }
212 }
213}
214
215impl<U: VectorUnit> Vector3<Ned, U> {
216 #[must_use]
218 pub const fn north(&self) -> U {
219 self.components[0]
220 }
221
222 #[must_use]
224 pub const fn east(&self) -> U {
225 self.components[1]
226 }
227
228 #[must_use]
230 pub const fn down(&self) -> U {
231 self.components[2]
232 }
233
234 #[must_use]
236 pub fn to_enu(self) -> Vector3<Enu, U> {
237 Vector3::new(self.east(), self.north(), -self.down())
238 }
239
240 #[must_use]
244 pub fn horizontal_direction(&self) -> Option<TrueCourse> {
245 horizontal_direction(self.north().si(), self.east().si())
246 }
247}
248
249impl<U: VectorUnit> Vector3<Enu, U> {
250 #[must_use]
252 pub const fn east(&self) -> U {
253 self.components[0]
254 }
255
256 #[must_use]
258 pub const fn north(&self) -> U {
259 self.components[1]
260 }
261
262 #[must_use]
264 pub const fn up(&self) -> U {
265 self.components[2]
266 }
267
268 #[must_use]
270 pub fn to_ned(self) -> Vector3<Ned, U> {
271 Vector3::new(self.north(), self.east(), -self.up())
272 }
273
274 #[must_use]
276 pub fn horizontal_direction(&self) -> Option<TrueCourse> {
277 horizontal_direction(self.north().si(), self.east().si())
278 }
279}
280
281impl<U: VectorUnit> Vector3<Body, U> {
282 #[must_use]
284 pub const fn forward(&self) -> U {
285 self.components[0]
286 }
287
288 #[must_use]
290 pub const fn right(&self) -> U {
291 self.components[1]
292 }
293
294 #[must_use]
296 pub const fn down(&self) -> U {
297 self.components[2]
298 }
299}
300
301fn horizontal_direction(north: f64, east: f64) -> Option<TrueCourse> {
304 let scale = math::abs(north).max(math::abs(east));
305 if scale < f64::MIN_POSITIVE {
306 return None;
307 }
308 TrueCourse::wrap(math::to_degrees(math::atan2(east, north))).ok()
309}
310
311impl<F: VectorFrame, U: VectorUnit> Add for Vector3<F, U> {
312 type Output = Self;
313
314 fn add(self, other: Self) -> Self {
315 let [first, second, third] = self.components;
316 let [x, y, z] = other.components;
317 Self::new(first + x, second + y, third + z)
318 }
319}
320
321impl<F: VectorFrame, U: VectorUnit> Sub for Vector3<F, U> {
322 type Output = Self;
323
324 fn sub(self, other: Self) -> Self {
325 let [first, second, third] = self.components;
326 let [x, y, z] = other.components;
327 Self::new(first - x, second - y, third - z)
328 }
329}
330
331impl<F: VectorFrame, U: VectorUnit> Neg for Vector3<F, U> {
332 type Output = Self;
333
334 fn neg(self) -> Self {
335 let [a, b, c] = self.components;
336 Self::new(-a, -b, -c)
337 }
338}
339
340impl<F: VectorFrame, U: VectorUnit> Mul<f64> for Vector3<F, U> {
341 type Output = Self;
342
343 fn mul(self, factor: f64) -> Self {
344 let [a, b, c] = self.components;
345 Self::new(a * factor, b * factor, c * factor)
346 }
347}
348
349impl<F: VectorFrame, U: VectorUnit> Div<f64> for Vector3<F, U> {
350 type Output = Self;
351
352 fn div(self, divisor: f64) -> Self {
355 Self::from_si(self.si().map(|value| value / divisor))
356 }
357}
358
359impl<F: VectorFrame, U: VectorUnit> fmt::Debug for Vector3<F, U> {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 let mut debug = f.debug_struct(F::NAME);
362 for (axis, component) in F::AXES.iter().zip(&self.components) {
363 debug.field(axis, component);
364 }
365 debug.finish()
366 }
367}
368
369impl<F: VectorFrame, U: VectorUnit + fmt::Display> fmt::Display for Vector3<F, U> {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
372 f.write_str("(")?;
373 for (index, (axis, component)) in F::AXES.iter().zip(&self.components).enumerate() {
374 if index > 0 {
375 f.write_str(", ")?;
376 }
377 write!(f, "{axis} {component}")?;
378 }
379 f.write_str(")")
380 }
381}
382
383#[cfg(feature = "serde")]
384impl<F: VectorFrame, U: VectorUnit + serde::Serialize> serde::Serialize for Vector3<F, U> {
385 fn serialize<S: serde::Serializer>(
387 &self,
388 serializer: S,
389 ) -> core::result::Result<S::Ok, S::Error> {
390 self.components.serialize(serializer)
391 }
392}
393
394#[cfg(feature = "serde")]
395impl<'de, F: VectorFrame, U: VectorUnit + serde::Deserialize<'de>> serde::Deserialize<'de>
396 for Vector3<F, U>
397{
398 fn deserialize<D: serde::Deserializer<'de>>(
399 deserializer: D,
400 ) -> core::result::Result<Self, D::Error> {
401 let [a, b, c] = <[U; 3]>::deserialize(deserializer)?;
402 Ok(Self::new(a, b, c))
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq)]
412pub struct LocalFrame {
413 origin: GeodeticPoint,
414 origin_ecef: EcefPoint,
415 ellipsoid: Ellipsoid,
416 rotation: [[f64; 3]; 3],
418}
419
420impl LocalFrame {
421 pub fn at(origin: GeodeticPoint, ellipsoid: &Ellipsoid) -> Result<Self> {
427 let origin_ecef = EcefPoint::from_geodetic(origin, ellipsoid)?;
428 let (sin_lat, cos_lat) = sin_cos(origin.position().latitude().radians());
429 let (sin_lon, cos_lon) = sin_cos(origin.position().longitude().radians());
430 Ok(Self {
431 origin,
432 origin_ecef,
433 ellipsoid: *ellipsoid,
434 rotation: [
435 [-sin_lat * cos_lon, -sin_lat * sin_lon, cos_lat],
436 [-sin_lon, cos_lon, 0.0],
437 [-cos_lat * cos_lon, -cos_lat * sin_lon, -sin_lat],
438 ],
439 })
440 }
441
442 #[must_use]
444 pub const fn origin(&self) -> GeodeticPoint {
445 self.origin
446 }
447
448 #[must_use]
450 pub const fn ellipsoid(&self) -> &Ellipsoid {
451 &self.ellipsoid
452 }
453
454 pub fn ned_of(&self, point: GeodeticPoint) -> Result<Vector3<Ned, Distance>> {
460 let ecef = EcefPoint::from_geodetic(point, &self.ellipsoid)?;
461 let delta = [
462 ecef.x().metres() - self.origin_ecef.x().metres(),
463 ecef.y().metres() - self.origin_ecef.y().metres(),
464 ecef.z().metres() - self.origin_ecef.z().metres(),
465 ];
466 Ok(Vector3::from_si(self.rotation.map(|row| dot(row, delta))))
467 }
468
469 pub fn enu_of(&self, point: GeodeticPoint) -> Result<Vector3<Enu, Distance>> {
475 self.ned_of(point).map(Vector3::to_enu)
476 }
477
478 pub fn point_from_ned(&self, displacement: Vector3<Ned, Distance>) -> Result<GeodeticPoint> {
488 let local = displacement.si();
489 let column = |index: usize| {
491 self.rotation
492 .iter()
493 .zip(local)
494 .map(|(row, value)| row.get(index).copied().unwrap_or(0.0) * value)
495 .sum::<f64>()
496 };
497 let ecef = EcefPoint::new(
498 Distance::from_si(self.origin_ecef.x().metres() + column(0)),
499 Distance::from_si(self.origin_ecef.y().metres() + column(1)),
500 Distance::from_si(self.origin_ecef.z().metres() + column(2)),
501 );
502 ecef.to_geodetic(&self.ellipsoid)
503 }
504
505 pub fn point_from_enu(&self, displacement: Vector3<Enu, Distance>) -> Result<GeodeticPoint> {
511 self.point_from_ned(displacement.to_ned())
512 }
513}
514
515fn sin_cos(radians: f64) -> (f64, f64) {
516 (math::sin(radians), math::cos(radians))
517}
518
519fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {
520 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
521}
522
523#[cfg(test)]
524#[allow(clippy::unwrap_used, clippy::float_cmp)]
525mod tests {
526 use super::*;
527 use crate::geodesy::Height;
528 use crate::position::{Latitude, Longitude, Position};
529 use alloc::format;
530
531 fn metres(value: f64) -> Distance {
532 Distance::from_metres(value).unwrap()
533 }
534
535 fn point(latitude: f64, longitude: f64, height: f64) -> GeodeticPoint {
536 GeodeticPoint::new(
537 Position::new(
538 Latitude::from_degrees(latitude).unwrap(),
539 Longitude::from_degrees(longitude).unwrap(),
540 ),
541 Height::above_ellipsoid(metres(height)),
542 )
543 }
544
545 #[test]
546 fn vectors_add_scale_and_measure_within_one_frame_and_unit() {
547 let a: Vector3<Ned, Distance> = Vector3::new(metres(3.0), metres(4.0), metres(12.0));
548 let b = Vector3::new(metres(1.0), metres(1.0), metres(1.0));
549 let close = |vector: Vector3<Ned, Distance>, wanted: [f64; 3]| {
550 vector
551 .components()
552 .iter()
553 .zip(wanted)
554 .all(|(got, wanted)| (got.metres() - wanted).abs() < 1e-9)
555 };
556 assert!(close(a + b, [4.0, 5.0, 13.0]));
557 assert!(close(a - b, [2.0, 3.0, 11.0]));
558 assert!(close(-a, [-3.0, -4.0, -12.0]));
559 assert!(close(a * 2.0, [6.0, 8.0, 24.0]));
560 assert!(close(a / 2.0, [1.5, 2.0, 6.0]));
561 assert!((a.magnitude().metres() - 13.0).abs() < 1e-9);
562 assert!((a.horizontal_magnitude().metres() - 5.0).abs() < 1e-9);
563 let printed = format!("{a:?}");
564 assert!(printed.starts_with("NED { north: "), "{printed}");
565 assert!(format!("{a}").starts_with("(north "), "{a}");
566 }
567
568 #[test]
569 fn ned_and_enu_are_the_same_vector_written_differently() {
570 let ned: Vector3<Ned, Speed> = Vector3::new(
571 Speed::from_metres_per_second(4.0).unwrap(),
572 Speed::from_metres_per_second(1.0).unwrap(),
573 Speed::from_metres_per_second(-0.5).unwrap(),
574 );
575 let enu = ned.to_enu();
576 assert_eq!(enu.east(), ned.east());
577 assert_eq!(enu.north(), ned.north());
578 assert_eq!(enu.up().metres_per_second(), 0.5);
579 assert_eq!(enu.to_ned(), ned);
580 let course = ned.horizontal_direction().unwrap();
581 assert!((course.degrees() - 14.036_243_467_926_479).abs() < 1e-9);
582 assert_eq!(enu.horizontal_direction(), Some(course));
583 let still: Vector3<Ned, Speed> = Vector3::new(Speed::ZERO, Speed::ZERO, Speed::ZERO);
584 assert_eq!(still.horizontal_direction(), None);
585 }
586
587 #[test]
588 fn a_local_frame_measures_displacements_from_its_origin() {
589 let frame = LocalFrame::at(point(50.0, 0.0, 0.0), &Ellipsoid::WGS84).unwrap();
590 let zero = frame.ned_of(point(50.0, 0.0, 0.0)).unwrap();
592 assert!(zero.magnitude().metres() < 1e-6);
593 let above = frame.ned_of(point(50.0, 0.0, 100.0)).unwrap();
595 assert!(above.north().metres().abs() < 1e-6);
596 assert!(above.east().metres().abs() < 1e-6);
597 assert!((above.down().metres() + 100.0).abs() < 1e-6);
598 let east = frame.ned_of(point(50.0, 0.01, 0.0)).unwrap();
600 assert!(east.east().metres() > 700.0 && east.east().metres() < 720.0);
601 assert!(east.north().metres().abs() < 0.1);
602 assert!(east.down().metres() > 0.0);
603 assert_eq!(east.horizontal_direction().unwrap().degrees().round(), 90.0);
604 }
605
606 #[test]
607 fn displacements_round_trip_through_the_frame() {
608 let origins = [
609 point(50.0, 0.0, 0.0),
610 point(89.99, 179.99, 10.0),
611 point(-33.9, 151.2, 50.0),
612 point(0.0, -180.0, -30.0),
613 ];
614 for origin in origins {
615 let frame = LocalFrame::at(origin, &Ellipsoid::WGS84).unwrap();
616 let displacement: Vector3<Ned, Distance> =
617 Vector3::new(metres(12_345.6), metres(-9_876.5), metres(432.1));
618 let there = frame.point_from_ned(displacement).unwrap();
619 let back = frame.ned_of(there).unwrap();
620 let error = (back - displacement).magnitude().metres();
621 assert!(error < 1e-4, "{origin}: off by {error} m, {back:?}");
622 let enu_back = frame.enu_of(there).unwrap();
623 let again = frame.point_from_enu(enu_back).unwrap();
624 assert!(
625 (again.position().latitude().degrees() - there.position().latitude().degrees())
626 .abs()
627 < 1e-9
628 );
629 }
630 }
631
632 #[test]
633 fn the_frame_wants_an_ellipsoidal_origin() {
634 let msl = GeodeticPoint::new(
635 point(50.0, 0.0, 0.0).position(),
636 Height::above_mean_sea_level(Distance::ZERO),
637 );
638 assert!(LocalFrame::at(msl, &Ellipsoid::WGS84).is_err());
639 }
640}