geoconv/
lib.rs

1// {{{ Copyright (c) Paul R. Tagliamonte <paultag@gmail.com>, 2023-2024
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE. }}}
20
21#![forbid(unsafe_code)]
22#![deny(missing_docs)]
23#![deny(missing_copy_implementations)]
24#![deny(trivial_casts)]
25#![deny(trivial_numeric_casts)]
26#![deny(unused_import_braces)]
27#![deny(unused_qualifications)]
28#![deny(rustdoc::broken_intra_doc_links)]
29#![deny(rustdoc::private_intra_doc_links)]
30#![cfg_attr(not(feature = "std"), no_std)]
31
32//! `geoconv` implements support for converting between some basic coordinate
33//! systems. This package contains support for [Wgs84]
34//! Latitude/Longitude/Elevation ([Lle]) geodetic coordinates, Earth-centered,
35//! Earth-fixed ([Xyz]), and local tangent plane (both East/North/Up ([Enu])
36//! and Azimuth/Elevation/Range ([Aer])) systems.
37//!
38//! <div class="warning">
39//! <b>This is also absolutely not ready to be used for navigational purposes</b>.
40//! Please do not use this library in any situation that may cause harm to life
41//! or property.
42//! </div>
43//!
44//! This package is particularly useful if you know your location on Earth,
45//! and want to geolocate an object that you can observe, or if you know your
46//! location on Earth, and an object's location on Earth and want to determine
47//! your relative positions.
48//!
49//! This also includes Haversine-distance using the circumference of the earth
50//! for approximate great-circle distance between two Lat/Lon points, but only
51//! on the "Earth's surface". In some cases (long distances) this is a way
52//! better approach for distance. If both points are line-of-sight, you likely
53//! care more about using the local tangent plane to work out the Range
54//! to the target in 3-space by converting both lat/lons to Earth-North-Up or
55//! an Azimuth/Elevation/Range.
56//!
57//! # Supported Coordinate Systems
58//!
59//! I only implemented what I was interested in ([Wgs84]), but I threw
60//! in another system ([Wgs72]) since it should be pretty straight-forward to
61//! support both. I have no reference data, so some additional testing for
62//! that coordinate system would be most welcome.
63//!
64//! | Coordinate System | State       | Note                     |
65//! | ----------------- | ----------- | ------------------------ |
66//! | [Wgs84]           | Tested      | **You likely want this** |
67//! | [Wgs72]           | Implemented |                          |
68//!
69//! # Types
70//!
71//! Within `geoconv`, the main types that you'll be interacting with for location
72//! data are listed below, along with a short description.
73//!
74//! | Name  | Cartesian or Angular | Where is `0`, `0`, `0`?               | Description                                                  |
75//! | ----- | -------------------- | ------------------------------------- | ------------------------------------------------------------ |
76//! | [Lle] | Angular              | Null Island                           | Latitude, Longitude, and Elevation                           |
77//! | [Xyz] | Cartesian            | Earth's Core                          | X, Y, Z (sometimes called ECEF, Earth-centered, Earth-fixed) |
78//! | [Aer] | Angular              | Some point on the local tangent plane | Azimuth, Elevation and Range                                 |
79//! | [Enu] | Cartesian            | Some point on the local tangent plane | East-North-Up                                                |
80//!
81//! # Determining the Azimuth, Elevation and Rage between two points
82//!
83//! Using `geoconv`, we can take some Latitude, Longitude and Elevation ([Lle])
84//! and figure out where another [Lle] point is in reference to our local
85//! tangent plane -- for instance, what the bearing (azimuth), elevation (up
86//! and down) and range to the "other" point is from where "we" are.
87//!
88//! ```rust
89//! use geoconv::{Lle, Wgs84, Degrees, Meters, Aer};
90//!
91//! // Alias for a lat/lon/elevation in the Wgs84 coordinate system,
92//! // used by (among many others), GPS -- this is usually what you
93//! // want when you're processing lat/lon information.
94//! type LleW84 = Lle<Wgs84>;
95//!
96//! // "My" point on earth.
97//! let me = LleW84::new(
98//!     Degrees::new(42.352211),
99//!     Degrees::new(-71.051315),
100//!     Meters::new(0.0),
101//! );
102//!
103//! // "Your" point on earth.
104//! let you = LleW84::new(
105//!     Degrees::new(42.320239),
106//!     Degrees::new(-70.929482),
107//!     Meters::new(100.0),
108//! );
109//!
110//! // Compute in what direction I'd need to look to see you.
111//! let look: Aer<Degrees> = me.aer_to(&you);
112//! ```
113//!
114//! # Determine the coordinates of something you can range
115//!
116//! Using `geoconv`, we can take some observation taken from a point,
117//! and figure out where that point is. Let's work through
118//! taking a reading in Azimuth, Elevation and Range ([Aer]) and
119//! turning that back into Latitude, Longitude and Elevation ([Lle])
120//! given our location.
121//!
122//! ```rust
123//! use geoconv::{Lle, Wgs84, Degrees, Meters, Aer};
124//!
125//! type LleW84 = Lle<Wgs84>;
126//!
127//! // "My" point on earth.
128//! let me = LleW84::new(
129//!     Degrees::new(42.352211),
130//!     Degrees::new(-71.051315),
131//!     Meters::new(0.0),
132//! );
133//!
134//! // I see something straight ahead of me, 45 degrees in elevation (up),
135//! // and 30 meters away.
136//! let observation = Aer {
137//!     azimuth: Degrees::new(0.0),
138//!     elevation: Degrees::new(45.0),
139//!     range: Meters::new(30.0),
140//! };
141//!
142//! // Assuming I have a perfect reading on where that object is, where
143//! // is that object as a latitude/longitude?
144//! let observed_lle: LleW84 = observation.to_lle(&me);
145//! ```
146//!
147//! # Haversine (Great Circle) distance between two points
148//!
149//! In addition to operations on the local tangent plane, sometimes
150//! the two points being measured are far enough away where the range
151//! between two points is *not* the distance you'd travel between them.
152//!
153//! For instance, when traving, from San Francisco to Paris, we need to
154//! travel along the curve of the earth, rather than directly through
155//! the crust of Earth.
156//!
157//! To compute distance beyond line of sight between to latitude and longitude
158//! points, we can use the Haversine formula to approximate the distance.
159//!
160//! Haversine distance **does not** easily take into account elevation,
161//! so the [haversine_distance] function does not accept a [Lle], rather,
162//! a tuple of AngularMeasures, such as [Degrees] or [Radians].
163//!
164//! ```rust
165//! use geoconv::{Degrees, haversine_distance, Meters};
166//!
167//! // Latitude, then Longitude (in that order) for the tuples
168//! let lat_lon_a = (Degrees::new(22.55), Degrees::new(43.12));
169//! let lat_lob_b = (Degrees::new(13.45), Degrees::new(100.28));
170//!
171//! let result: Meters = haversine_distance(lat_lon_a, lat_lob_b);
172//! ```
173
174pub mod maidenhead;
175mod wgs;
176mod wgs72;
177mod wgs84;
178
179pub use wgs72::Wgs72;
180pub use wgs84::Wgs84;
181
182pub(crate) mod std {
183    // pub use alloc::*;
184    pub use core::*;
185}
186
187pub(crate) mod math {
188    #[cfg(feature = "libm")]
189    pub use libm::{atan2, cos, pow, sin, sqrt};
190
191    #[cfg(all(not(feature = "libm"), feature = "std"))]
192    mod _std_math {
193        pub fn cos(x: f64) -> f64 {
194            x.cos()
195        }
196        pub fn sin(x: f64) -> f64 {
197            x.sin()
198        }
199        pub fn sqrt(x: f64) -> f64 {
200            x.sqrt()
201        }
202        pub fn atan2(x: f64, y: f64) -> f64 {
203            x.atan2(y)
204        }
205        pub fn pow(x: f64, y: f64) -> f64 {
206            x.powf(y)
207        }
208    }
209
210    #[cfg(all(not(feature = "libm"), feature = "std"))]
211    pub use _std_math::*;
212
213    #[cfg(not(any(feature = "std", feature = "libm")))]
214    compile_error!("need one of libm or std for a math backend");
215}
216
217use math::{atan2, cos, pow, sin, sqrt};
218use std::marker::PhantomData;
219
220/// [Meters] represent the SI unit of measure, Meter
221#[derive(Debug, Copy, Clone, PartialEq)]
222pub struct Meters(f64);
223
224impl Meters {
225    /// Create a new Meters struct, initialized with the provided
226    /// distance, in meters.
227    pub const fn new(meters: f64) -> Meters {
228        Meters(meters)
229    }
230
231    /// Return the number of meters as a floating point number.
232    /// This number may be less than 0 (for distances less than a meter), or
233    /// very large (for kilometers, etc).
234    pub const fn as_float(self) -> f64 {
235        self.0
236    }
237}
238
239/// [Degrees] is an angular measure that ranges from 0 to 360 (sometimes negative)
240#[derive(Debug, Copy, Clone, PartialEq)]
241pub struct Degrees(f64);
242
243impl Degrees {
244    /// Create a new [Degrees] struct, initialized with the provided
245    /// number of degrees, either between 0 and 360 or -180 to +180.
246    pub const fn new(degrees: f64) -> Degrees {
247        Degrees(degrees)
248    }
249
250    /// Return the [Degrees] as a floating point number.
251    pub const fn as_float(self) -> f64 {
252        self.0
253    }
254}
255
256impl From<Degrees> for Radians {
257    fn from(deg: Degrees) -> Self {
258        Radians::new(std::f64::consts::PI / 180.0 * deg.as_float())
259    }
260}
261
262/// [Radians] is an angular measure that ranges from 0 to 2Ο€ (𝜏).
263#[derive(Debug, Copy, Clone, PartialEq)]
264pub struct Radians(f64);
265
266impl Radians {
267    /// Create a new [Radians] struct, initialized with the provided
268    /// number of radians, ranging between 0 and to 2Ο€ (𝜏).
269    pub const fn new(radians: f64) -> Radians {
270        // TODO(paultag): put this into the range 0 -> 𝜏
271        Radians(radians)
272    }
273
274    /// Return the [Radians] as a floating point number from 0
275    /// to 2Ο€ (𝜏).
276    pub const fn as_float(self) -> f64 {
277        self.0
278    }
279}
280
281impl From<Radians> for Degrees {
282    fn from(rad: Radians) -> Self {
283        Degrees::new(180.0 / std::f64::consts::PI * rad.as_float())
284    }
285}
286
287/// [Lle] or Latitude, Longitude, Elevation, is a location somewhere around
288/// Earth, in refernce to some [CoordinateSystem]'s ellipsoid.
289///
290/// Relatedly, this means that the `elevation` is the number of [Meters]
291/// above the [CoordinateSystem]'s ellipsoid, *not* the altitude above or
292/// below the true surface of the Earth.
293///
294/// The provided `CoordinateSystem` should be the [CoordinateSystem]
295/// that the Latitude, Longitude and Elevation are in refernce to, usually
296/// [Wgs84]. If you don't know otherwise, it's a safe to assume that you
297/// want to use `Lle<Wgs84>`. If you know otherwise, you may not have
298/// needed this warning.
299///
300/// Additionally, the `latitude` and `longitude` fields can be specified in
301/// either [Degrees] or [Radians]. Unless you know otherwise, you likely
302/// want to work with [Degrees], and is the default if nothing is specified.
303#[derive(Debug, Copy, Clone, PartialEq)]
304pub struct Lle<CoordinateSystem, AngularMeasure = Degrees>
305where
306    Radians: From<AngularMeasure> + Copy,
307    AngularMeasure: From<Radians> + Copy,
308    CoordinateSystem: crate::CoordinateSystem<AngularMeasure>,
309{
310    _unused: PhantomData<CoordinateSystem>,
311
312    /// Latitude, in degrees, for some geodetic coordinate system.
313    pub latitude: AngularMeasure,
314
315    /// Longitude, in degrees, for some geodetic coordinate system.
316    pub longitude: AngularMeasure,
317
318    /// Elevation above the ellipsoid for some geodetic coordinate system.
319    /// This is *not* the same as altitude above the surface.
320    pub elevation: Meters,
321}
322
323/// Alias for [LLE]. This struct was renamed to better match Rust style,
324/// and this alias will be removed in a future release.
325#[deprecated]
326pub type LLE<CoordinateSystem> = Lle<CoordinateSystem>;
327
328impl<CoordinateSystem, AngularMeasure> Lle<CoordinateSystem, AngularMeasure>
329where
330    Radians: From<AngularMeasure> + Copy,
331    AngularMeasure: From<Radians> + Copy,
332    CoordinateSystem: crate::CoordinateSystem<AngularMeasure>,
333{
334    /// Create a new Lle (latitude, longitude, elevation) from parts.
335    pub const fn new(
336        latitude: AngularMeasure,
337        longitude: AngularMeasure,
338        elevation: Meters,
339    ) -> Lle<CoordinateSystem, AngularMeasure> {
340        Lle {
341            latitude,
342            longitude,
343            elevation,
344            _unused: PhantomData,
345        }
346    }
347
348    /// Compute the East-North-Up of some provided point (`other`) given
349    /// some refernce location `self`.
350    pub fn enu_to(&self, other: &Lle<CoordinateSystem, AngularMeasure>) -> Enu {
351        CoordinateSystem::lle_to_enu(self, other)
352    }
353
354    /// Compute the Az-El-Range of some provided point (`other`) given
355    /// some reference location `self`.
356    pub fn aer_to<AerAngularMeasure>(
357        &self,
358        other: &Lle<CoordinateSystem, AngularMeasure>,
359    ) -> Aer<AerAngularMeasure>
360    where
361        Aer<AerAngularMeasure>: From<Enu>,
362    {
363        self.enu_to(other).into()
364    }
365}
366
367/// [Xyz] is the earth-centric Xyz point system.
368///
369/// `{ x: 0.0, y: 0.0, z: 0.0 }` is the center of the CoordinateSystem (which
370/// is to say, inside Earth's Core). X/Y/Z coordinates are cartesian, and
371/// as they grow larger in absolute value, will get further from the center of
372/// Earth.
373///
374/// Xyz locations can be turned into points in reference to Earth's ellipsoid
375/// by using some [CoordinateSystem], such as [Wgs84], either directly
376/// or via [Lle].
377#[derive(Debug, Copy, Clone, PartialEq)]
378pub struct Xyz {
379    /// Distance from the center of the ECEF coordinate system. The X axis
380    /// intersects the equator along the semi-major axis.
381    pub x: Meters,
382
383    /// Distance from the center of the ECEF coordinate system. The Y axis
384    /// intersects the equator along the semi-minor axis.
385    pub y: Meters,
386
387    /// Distance from the center of the ECEF coordinate system. The Z axis
388    /// intersects the north and south poles.
389    pub z: Meters,
390}
391
392/// Alias for [XYZ]. This struct was renamed to better match Rust style,
393/// and this alias will be removed in a future release.
394#[deprecated]
395pub type XYZ = Xyz;
396
397// Implemnt conversions to/from Lle and Xyz coordinates.
398
399impl<CoordinateSystem, AngularMeasure> From<Lle<CoordinateSystem, AngularMeasure>> for Xyz
400where
401    Radians: From<AngularMeasure> + Copy,
402    AngularMeasure: From<Radians> + Copy,
403    CoordinateSystem: crate::CoordinateSystem<AngularMeasure>,
404{
405    /// Convert some [Lle] into an [Xyz] coordinate using [Lle]'s
406    /// [CoordinateSystem].
407    fn from(lle: Lle<CoordinateSystem, AngularMeasure>) -> Self {
408        CoordinateSystem::lle_to_xyz(&lle)
409    }
410}
411
412impl<CoordinateSystem, AngularMeasure> From<Xyz> for Lle<CoordinateSystem, AngularMeasure>
413where
414    Radians: From<AngularMeasure> + Copy,
415    AngularMeasure: From<Radians> + Copy,
416    CoordinateSystem: crate::CoordinateSystem<AngularMeasure>,
417{
418    /// Convert some [Xyz] into an [Lle] coordinate using [Lle]'s
419    /// [CoordinateSystem].
420    fn from(xyz: Xyz) -> Self {
421        CoordinateSystem::xyz_to_lle(&xyz)
422    }
423}
424
425/// [Aer] represents an Azimuth, Elevation, and Range measurement.
426///
427/// Azimuth/Elevation (or Az/El) is a common way of locating objects measured
428/// at a specific location on Earth in the local tangent plane
429/// (for instance, from a RADAR). That reference location is not included in
430/// the [Aer] struct, so re-contextualizing this requires knowing the point
431/// at which the observation was taken from or was in reference to.
432///
433/// An [Aer] can be passed an "`AngularMeasure`" generic argument, which means,
434/// for all practical purposes, either [Degrees] or [Radians]. The default is
435/// [Degrees], but can be overriden by passing [Radians] into the [Aer].
436/// [Degrees] tend to be a lot easier to use as an API and for humans, but
437/// when passing data between third party code and this library, you may find
438/// it's less work to skip conversions through [Degrees] when reading or
439/// writing [Aer] data. If you don't have some overriding conviction, using
440/// [Degrees] is a good idea.
441#[derive(Debug, Copy, Clone, PartialEq)]
442pub struct Aer<AngularMeasure = Degrees> {
443    /// Angle in azimuth (left and right) in relation to some fixed point and
444    /// orientation.
445    pub azimuth: AngularMeasure,
446
447    /// Angle in elevation (up and down) in relation to some fixed point and
448    /// orientation.
449    pub elevation: AngularMeasure,
450
451    /// Range to some point in space along the defined azimuth and elevation,
452    /// in Meters.
453    pub range: Meters,
454}
455
456/// Alias for [AER]. This struct was renamed to better match Rust style,
457/// and this alias will be removed in a future release.
458#[deprecated]
459pub type AER = Aer;
460
461impl<AngularMeasure> Aer<AngularMeasure>
462where
463    Enu: From<Aer<AngularMeasure>>,
464    Self: Copy,
465{
466    /// Take some observation (`self`), and contextualize it into some
467    /// absolute [Lle] given some reference point `obs` that this [Aer] is
468    /// in reference to.
469    pub fn to_lle<CoordinateSystem, LleAngularMeasure>(
470        &self,
471        obs: &Lle<CoordinateSystem, LleAngularMeasure>,
472    ) -> Lle<CoordinateSystem, LleAngularMeasure>
473    where
474        Radians: From<LleAngularMeasure> + Copy,
475        LleAngularMeasure: From<Radians> + Copy,
476        CoordinateSystem: crate::CoordinateSystem<LleAngularMeasure>,
477    {
478        let enu: Enu = (*self).into();
479        enu.to_lle(obs)
480    }
481}
482
483// Here we need to bite the bullet and just implement manual conversions.
484// We can't do a blanket implementation using `From` and generics since
485// the stdlib implements From<X> for X, which winds up getting a conflicting
486// implementation.
487//
488// As a result, every new AngularMeasure is going to need to grow stupid
489// conversions. If I need to add a new angle type, this should all be
490// turned into macros. It's the literal same code.
491
492impl From<Aer<Degrees>> for Aer<Radians> {
493    /// Convert an [Aer] in [Degrees] into [Radians]
494    fn from(aer: Aer<Degrees>) -> Self {
495        Self {
496            azimuth: aer.azimuth.into(),
497            elevation: aer.elevation.into(),
498            range: aer.range,
499        }
500    }
501}
502
503impl From<Aer<Radians>> for Aer<Degrees> {
504    /// Convert an [Aer] in [Radians] into [Degrees].
505    fn from(aer: Aer<Radians>) -> Self {
506        Self {
507            azimuth: aer.azimuth.into(),
508            elevation: aer.elevation.into(),
509            range: aer.range,
510        }
511    }
512}
513
514// Implement conversions from Aer -> Enu; for Radians and Degrees. Again,
515// this can't be done easily using generics, so I'm just going to make it
516// explicit.
517
518impl From<Aer<Radians>> for Enu {
519    /// Convert to [Enu] cartesian local tangent plane coordinates from an
520    /// [Aer] local tangent plane angular measurement in [Radians].
521    fn from(aer: Aer<Radians>) -> Self {
522        let az_rad: Radians = aer.azimuth;
523        let el_rad: Radians = aer.elevation;
524        let r = Meters::new(aer.range.as_float() * cos(el_rad.as_float()));
525        Enu {
526            east: Meters::new(r.as_float() * sin(az_rad.as_float())),
527            north: Meters::new(r.as_float() * cos(az_rad.as_float())),
528            up: Meters::new(aer.range.as_float() * sin(el_rad.as_float())),
529        }
530    }
531}
532
533impl From<Aer<Degrees>> for Enu {
534    /// Convert to [Enu] cartesian local tangent plane coordinates from an
535    /// [Aer] local tangent plane angular measurement in [Degrees].
536    fn from(aer: Aer<Degrees>) -> Self {
537        let aer: Aer<Radians> = aer.into();
538        aer.into()
539    }
540}
541
542/// [Enu] is East, North, Up in Meters.
543///
544/// East-North-Up are cartesian coordinates rooted at some reference
545/// point's local tangent plane. That reference location is not included in
546/// the [Enu] struct, so re-contextualizing an [Enu] to a [Lle] or [Xyz]
547/// requires knowing the point at which the observation was taken from or
548/// was in reference to.
549///
550/// These measures are cartesian in the local tangent plane, which is to say,
551/// increasing "North" will continue straight ahead, but not following the
552/// curve of the Earth -- so you'll slowly get further and further away from
553/// Earth's surface.
554#[derive(Debug, Copy, Clone, PartialEq)]
555pub struct Enu {
556    /// East is the distance in the easterly direction on some local tangent
557    /// plane reference. This may be negative to indicate a westerly
558    /// distance.
559    pub east: Meters,
560
561    /// North is the distance in the northernly direction on some local tangent
562    /// plane reference. This may be negative to indicate a southern
563    /// distance.
564    pub north: Meters,
565
566    /// Up is the distance above the local tangent plane that intersects with
567    /// the coordinate system ellipsoid (this is *not* the same as altitude
568    /// above the surface, this is elevation above the ellipsoid).
569    pub up: Meters,
570}
571
572/// Alias for [Enu]. This struct was renamed to better match Rust style,
573/// and this alias will be removed in a future release.
574#[deprecated]
575pub type ENU = Enu;
576
577impl Enu {
578    /// Take some observation (`self`), and contextualize it into some
579    /// absolute [Lle] given some reference point `obs` that this [Aer]
580    /// is in reference to.
581    pub fn to_lle<CoordinateSystem, LleAngularMeasure>(
582        &self,
583        obs: &Lle<CoordinateSystem, LleAngularMeasure>,
584    ) -> Lle<CoordinateSystem, LleAngularMeasure>
585    where
586        Radians: From<LleAngularMeasure> + Copy,
587        LleAngularMeasure: From<Radians> + Copy,
588        CoordinateSystem: crate::CoordinateSystem<LleAngularMeasure>,
589    {
590        CoordinateSystem::enu_to_lle(obs, self)
591    }
592}
593
594// Implement conversions from Aer -> Enu; for Radians and Degrees. Again,
595// this can't be done easily using generics, so I'm just going to make it
596// explicit.
597
598impl From<Enu> for Aer<Radians> {
599    /// Convert to [Aer] cartesian local tangent plane angular measurement in
600    /// [Radians] from [Enu] local tangent plane coordinates.
601    fn from(enu: Enu) -> Self {
602        let r = sqrt(
603            enu.east.as_float() * enu.east.as_float() + enu.north.as_float() * enu.north.as_float(),
604        );
605
606        let tau = std::f64::consts::PI * 2.0;
607        Self {
608            azimuth: Radians::new(atan2(enu.east.as_float(), enu.north.as_float()) % tau),
609            elevation: Radians::new(atan2(enu.up.as_float(), r)),
610            range: Meters::new(sqrt(r * r + enu.up.as_float() * enu.up.as_float())),
611        }
612    }
613}
614
615impl From<Enu> for Aer<Degrees> {
616    /// Convert to [Aer] cartesian local tangent plane angular measurement in
617    /// [Degrees] from [Enu] local tangent plane coordinates.
618    fn from(enu: Enu) -> Self {
619        let aer: Aer<Radians> = enu.into();
620        aer.into()
621    }
622}
623
624/// [CoordinateSystem] is a trait to enable converstion between locations
625/// (usually Latitude and Longitude, in the form of [Lle] objects) to absolute
626/// points in space and vice versa.
627///
628/// Different systems have different measurements of Earth's surface, and
629/// a Latitude / Longitude must be understood within its [CoordinateSystem],
630/// or significant errors can be introduced.
631pub trait CoordinateSystem<AngularMeasure>
632where
633    Radians: From<AngularMeasure> + Copy,
634    AngularMeasure: From<Radians> + Copy,
635    Self: Sized,
636{
637    /// Convert [Xyz] to [Lle] in our [CoordinateSystem].
638    fn xyz_to_lle(c: &Xyz) -> Lle<Self, AngularMeasure>;
639
640    /// Convert [Xyz] to [Enu], referenced to `refr` ([Lle]) in our
641    /// [CoordinateSystem].
642    fn xyz_to_enu(refr: &Lle<Self, AngularMeasure>, c: &Xyz) -> Enu;
643
644    /// Convert [Enu] to [Xyz], referenced to `refr` ([Lle]) in our
645    /// [CoordinateSystem].
646    fn enu_to_xyz(refr: &Lle<Self, AngularMeasure>, c: &Enu) -> Xyz;
647
648    /// Convert [Lle] to [Xyz] in our [CoordinateSystem].
649    fn lle_to_xyz(geo: &Lle<Self, AngularMeasure>) -> Xyz;
650
651    /// Convert [Enu] to [Lle], referenced to `refr` ([Lle]) in our
652    /// [CoordinateSystem].
653    fn enu_to_lle(refr: &Lle<Self, AngularMeasure>, c: &Enu) -> Lle<Self, AngularMeasure> {
654        let xyz = Self::enu_to_xyz(refr, c);
655        Self::xyz_to_lle(&xyz)
656    }
657
658    /// Convert [Lle] to [Enu], referenced to `refr` ([Lle]) in our
659    /// [CoordinateSystem].
660    fn lle_to_enu(r: &Lle<Self, AngularMeasure>, geo: &Lle<Self, AngularMeasure>) -> Enu {
661        let xyz = Self::lle_to_xyz(geo);
662        Self::xyz_to_enu(r, &xyz)
663    }
664}
665
666impl<FromCoordinateSystem, FromAngularMeasure> Lle<FromCoordinateSystem, FromAngularMeasure>
667where
668    Radians: From<FromAngularMeasure> + Copy,
669    FromAngularMeasure: From<Radians> + Copy,
670    FromCoordinateSystem: CoordinateSystem<FromAngularMeasure>,
671    Self: Copy,
672{
673    /// Translate from one [CoordinateSystem] to another [CoordinateSystem].
674    ///
675    /// This isn't a From trait since Rust doesn't have the ability to implement
676    /// conversions between the same type. In the future when Rust can handle
677    /// specialization or negating a blanket generic implementation
678    /// (such as `where FromCoordinateSystem != ToCoordinateSystem`), this
679    /// method will be deprecated for a plain `.into()`.
680    ///
681    /// <div class="warning">
682    /// It's not clear to me that, as things are implemented right now,
683    /// that <code>Xyz</code>'s <code>(0.0, 0.0, 0.0)</code> is the same
684    /// exact point in space for all the <code>CoordinateSystem</code>s,
685    /// although this library <u>will assume they are</u>. As such, I'm not
686    /// confident these conversions are implemented properly, and
687    /// <b>must not be relied upon for anything approaching important</b>.
688    /// </div>
689    pub fn translate<ToCoordinateSystem, ToAngularMeasure>(
690        &self,
691    ) -> Lle<ToCoordinateSystem, ToAngularMeasure>
692    where
693        Radians: From<ToAngularMeasure> + Copy,
694        ToAngularMeasure: From<Radians> + Copy,
695        ToCoordinateSystem: CoordinateSystem<ToAngularMeasure>,
696    {
697        let xyz = FromCoordinateSystem::lle_to_xyz(self);
698        ToCoordinateSystem::xyz_to_lle(&xyz)
699    }
700}
701
702/// For both [haversine_distance] and [bearing] we need to know these values
703/// in this format, and writing this every time is a bit tedious.
704fn angular_measure_to_radian_delta<AngularMeasure>(
705    self_lat_lon: (AngularMeasure, AngularMeasure),
706    other_lat_lon: (AngularMeasure, AngularMeasure),
707) -> ((f64, f64), (f64, f64), (f64, f64))
708where
709    Radians: From<AngularMeasure> + Copy,
710{
711    let (self_lat, self_lon) = self_lat_lon;
712    let (self_lat, self_lon): (Radians, Radians) = (self_lat.into(), self_lon.into());
713    let (self_lat, self_lon) = (self_lat.as_float(), self_lon.as_float());
714
715    let (other_lat, other_lon) = other_lat_lon;
716    let (other_lat, other_lon): (Radians, Radians) = (other_lat.into(), other_lon.into());
717    let (other_lat, other_lon) = (other_lat.as_float(), other_lon.as_float());
718
719    let delta_lat = other_lat - self_lat;
720    let delta_lon = other_lon - self_lon;
721
722    (
723        (delta_lat, delta_lon),
724        (self_lat, self_lon),
725        (other_lat, other_lon),
726    )
727}
728
729/// Haversine (Great Circle) distance between two points returns the
730/// distance between to Latitude/Longitude points along the great circle
731/// path along Earth's "surface", in either [Degrees] or [Radians].
732///
733/// "Surface" has quotes around it beacuse great circle paths as
734/// implemented by the haversine formula (and therefore [haversine_distance])
735/// will treat earth as a perfect sphere, sure to annoy both flat-earthers
736/// as well as globeheads. This means that, in reality, the point on the
737/// sphere's latitude/longitude at elevation `0` may be above or below the
738/// true surface of earth, some [CoordinateSystem] ellipsoid (such as the
739/// very likely case these are [Wgs84] coordinates) -- and absolutely won't
740/// account for changes in the true elevation on Earth's surface.
741///
742/// But hey, what's [804672](https://www.youtube.com/watch?v=tbNlMtqrYS0)
743/// [Meters] between friends?
744pub fn haversine_distance<AngularMeasure>(
745    self_lat_lon: (AngularMeasure, AngularMeasure),
746    other_lat_lon: (AngularMeasure, AngularMeasure),
747) -> Meters
748where
749    Radians: From<AngularMeasure> + Copy,
750{
751    const EARTH_RADIUS: Meters = Meters(6371000.0);
752
753    let (
754        (delta_lat, delta_lon),
755        // f64 of the measure in radians.
756        (self_lat, _self_lon),
757        (other_lat, _other_lon),
758    ) = angular_measure_to_radian_delta(self_lat_lon, other_lat_lon);
759
760    let a = pow(sin(delta_lat / 2.0), 2.0)
761        + cos(self_lat) * cos(other_lat) * pow(sin(delta_lon / 2.0), 2.0);
762    let c = 2.0 * atan2(sqrt(a), sqrt(1.0 - a));
763
764    Meters::new(EARTH_RADIUS.as_float() * c)
765}
766
767/// Compute the bearing (compass heading) from one point to another, provided
768/// in Latitude/Longitude points along Earth's "surface" in either [Degrees]
769/// or [Radians].
770pub fn bearing<AngularMeasure>(
771    self_lat_lon: (AngularMeasure, AngularMeasure),
772    other_lat_lon: (AngularMeasure, AngularMeasure),
773) -> AngularMeasure
774where
775    Radians: From<AngularMeasure> + Copy,
776    AngularMeasure: From<Radians> + Copy,
777{
778    let (
779        (_delta_lat, delta_lon),
780        // f64 of the measure in radians.
781        (self_lat, _self_lon),
782        (other_lat, _other_lon),
783    ) = angular_measure_to_radian_delta(self_lat_lon, other_lat_lon);
784
785    let x = cos(self_lat) * sin(other_lat) - sin(self_lat) * cos(other_lat) * cos(delta_lon);
786    let y = sin(delta_lon) * cos(other_lat);
787
788    Radians::new((atan2(y, x) + std::f64::consts::TAU) % std::f64::consts::TAU).into()
789}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    #[test]
796    fn degrees_as_radians() {
797        let result: Radians = Degrees::new(180.0).into();
798        assert_eq!(result.as_float(), std::f64::consts::PI);
799    }
800
801    #[test]
802    fn radians_as_degrees() {
803        let result: Degrees = Radians::new(std::f64::consts::PI).into();
804        assert_eq!(result.as_float(), 180.0);
805    }
806
807    macro_rules! assert_in_eps {
808        ($x:expr, $y:expr, $d:expr) => {
809            if !($x - $y < $d && $y - $x < $d) {
810                panic!("{} is not ~= to {}", $x, $y);
811            }
812        };
813    }
814
815    #[test]
816    fn haversine_known() {
817        let result = haversine_distance(
818            (Degrees::new(22.55), Degrees::new(43.12)),
819            (Degrees::new(13.45), Degrees::new(100.28)),
820        );
821        assert_in_eps!(6094544.408786774, result.as_float(), 1e-6);
822
823        let result = haversine_distance(
824            (Degrees::new(51.510357), Degrees::new(-0.116773)),
825            (Degrees::new(38.889931), Degrees::new(-77.009003)),
826        );
827        assert_in_eps!(5897658.288856054, result.as_float(), 1e-6);
828    }
829
830    #[test]
831    fn bearing_known() {
832        // somewhere in south america's bearing to null island (strictly
833        // due east).
834        let result = bearing(
835            (Degrees::new(0.0), Degrees::new(-88.0)),
836            (Degrees::new(0.0), Degrees::new(0.0)),
837        );
838        assert_in_eps!(90.0, result.as_float(), 1e-6);
839
840        let result = bearing(
841            (Degrees::new(0.0), Degrees::new(0.0)),
842            (Degrees::new(0.0), Degrees::new(-88.0)),
843        );
844        assert_in_eps!(270.0, result.as_float(), 1e-6);
845
846        // the US Supreme Court building to The White House πŸ‡ΊπŸ‡Έ
847        let result = bearing(
848            (Degrees::new(38.890591), Degrees::new(-77.004745)),
849            (Degrees::new(38.897957), Degrees::new(-77.036560)),
850        );
851        assert_in_eps!(286.576, result.as_float(), 1e-3);
852    }
853}
854
855// vim: foldmethod=marker