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