unit_sphere/
lib.rs

1// Copyright (c) 2024-2025 Ken Barker
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"),
5// to deal in the Software without restriction, including without limitation the
6// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7// sell 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//! # unit-sphere
22//!
23//! [![crates.io](https://img.shields.io/crates/v/unit-sphere.svg)](https://crates.io/crates/unit-sphere)
24//! [![docs.io](https://docs.rs/unit-sphere/badge.svg)](https://docs.rs/unit-sphere/)
25//! [![License](https://img.shields.io/badge/License-MIT-blue)](https://opensource.org/license/mit/)
26//! [![Rust](https://github.com/kenba/unit-sphere-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/kenba/unit-sphere-rs/actions)
27//! [![codecov](https://codecov.io/gh/kenba/unit-sphere-rs/graph/badge.svg?token=G1H1XINERW)](https://codecov.io/gh/kenba/unit-sphere-rs)
28//!
29//! A library for performing geometric calculations on the surface of a sphere.
30//!
31//! The library uses a combination of spherical trigonometry and vector geometry
32//! to perform [great-circle navigation](https://en.wikipedia.org/wiki/Great-circle_navigation)
33//! on the surface of a unit sphere, see *Figure 1*.
34//!
35//! ![great circle arc](https://via-technology.aero/img/navigation/sphere/great_circle_arc.svg)
36//!
37//! *Figure 1 A Great Circle Arc*
38//!
39//! A [great circle](https://en.wikipedia.org/wiki/Great_circle) is the
40//! shortest path between positions on the surface of a sphere.
41//! It is the spherical equivalent of a straight line in planar geometry.
42//!
43//! ## Spherical trigonometry
44//!
45//! A great circle path between positions may be found using
46//! [spherical trigonometry](https://en.wikipedia.org/wiki/Spherical_trigonometry).
47//!
48//! The [course](https://en.wikipedia.org/wiki/Great-circle_navigation#Course)
49//! (initial azimuth) of a great circle can be calculated from the
50//! latitudes and longitudes of the start and end points.
51//! While great circle distance can also be calculated from the latitudes and
52//! longitudes of the start and end points using the
53//! [haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
54//! The resulting distance in `Radians` can be converted to the required units by
55//! multiplying the distance by the Earth radius measured in the required units.
56//!
57//! ## Vector geometry
58//!
59//! Points on the surface of a sphere and great circle poles may be represented
60//! by 3D [vectors](https://www.movable-type.co.uk/scripts/latlong-vectors.html).
61//! Many calculations are simpler using vectors than spherical trigonometry.
62//!
63//! ![Spherical Vector Coordinates](https://via-technology.aero/img/navigation/sphere/ecef_coordinates.svg)
64//!
65//! *Figure 2 Spherical Vector Coordinates*
66//!
67//! For example, the across track distance of a point from a great circle can
68//! be calculated from the [dot product](https://en.wikipedia.org/wiki/Dot_product)
69//! of the point and the great circle pole vectors.
70//! While intersection points of great circles can simply be calculated from
71//! the [cross product](https://en.wikipedia.org/wiki/Cross_product) of their
72//! pole vectors.
73//!
74//! ## Design
75//!
76//! The `great_circle` module performs spherical trigonometric calculations
77//! and the `vector` module performs vector geometry calculations.
78//! See: [spherical vector geometry](https://via-technology.aero/navigation/spherical-vector-geometry/).
79//!
80//! The software uses types: `Angle`, `Degrees` and `Radians` from the
81//! [angle-sc](https://crates.io/crates/angle-sc) crate.
82//!
83//! The library is declared [no_std](https://docs.rust-embedded.org/book/intro/no-std.html)
84//! so it can be used in embedded applications.
85//!
86//! ## Example
87//!
88//! The following example calculates the intersection between two Great Circle `Arc`s
89//! it is taken from Charles Karney's original solution to
90//! [Intersection between two geodesic lines](https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a).
91//!
92//! ```rust
93//! use unit_sphere::{Arc, Degrees, LatLong, calculate_intersection_point};
94//! use angle_sc::is_within_tolerance;
95//!
96//! let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
97//! let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
98//! let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
99//! let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
100//!
101//! let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
102//! let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
103//!
104//! let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
105//! let lat_long = LatLong::from(&intersection_point);
106//! // Geodesic intersection latitude is 54.7170296089477
107//! assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
108//! // Geodesic intersection longitude is -14.56385574430775
109//! assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
110//! ```
111
112#![cfg_attr(not(test), no_std)]
113
114extern crate angle_sc;
115extern crate nalgebra as na;
116
117pub mod great_circle;
118pub mod vector;
119
120use angle_sc::min;
121pub use angle_sc::{Angle, Degrees, Radians, Validate};
122use thiserror::Error;
123
124/// Test whether a latitude in degrees is a valid latitude.
125///
126/// I.e. whether it lies in the range: -90.0 <= degrees <= 90.0
127#[must_use]
128pub fn is_valid_latitude(degrees: f64) -> bool {
129    (-90.0..=90.0).contains(&degrees)
130}
131
132/// Test whether a longitude in degrees is a valid longitude.
133///
134/// I.e. whether it lies in the range: -180.0 <= degrees <= 180.0
135#[must_use]
136pub fn is_valid_longitude(degrees: f64) -> bool {
137    (-180.0..=180.0).contains(&degrees)
138}
139
140/// A position as a latitude and longitude pair of `Degrees`.
141#[derive(Clone, Copy, Debug, PartialEq)]
142pub struct LatLong {
143    lat: Degrees,
144    lon: Degrees,
145}
146
147impl Validate for LatLong {
148    /// Test whether a `LatLong` is valid.
149    ///
150    /// I.e. whether the latitude lies in the range: -90.0 <= lat <= 90.0
151    /// and the longitude lies in the range: -180.0 <= lon <= 180.0
152    fn is_valid(&self) -> bool {
153        is_valid_latitude(self.lat.0) && is_valid_longitude(self.lon.0)
154    }
155}
156
157impl LatLong {
158    #[must_use]
159    pub const fn new(lat: Degrees, lon: Degrees) -> Self {
160        Self { lat, lon }
161    }
162
163    #[must_use]
164    pub const fn lat(&self) -> Degrees {
165        self.lat
166    }
167
168    #[must_use]
169    pub const fn lon(&self) -> Degrees {
170        self.lon
171    }
172
173    /// Determine whether the `LatLong` is South of  a.
174    ///
175    /// It compares the latitude of the two points.
176    /// * `a` - the other `LatLong`.
177    ///
178    /// returns true if South of a, false otherwise.
179    #[must_use]
180    pub fn is_south_of(&self, a: &Self) -> bool {
181        self.lat.0 < a.lat.0
182    }
183
184    /// Determine whether the `LatLong` is West of `LatLong` a.
185    ///
186    /// It compares the longitude difference of the two points.
187    /// * `a`, `b` - the points.
188    ///
189    /// returns true if a is West of b, false otherwise.
190    #[must_use]
191    pub fn is_west_of(&self, a: &Self) -> bool {
192        (a.lon() - self.lon).0 < 0.0
193    }
194}
195
196/// A Error type for an invalid `LatLong`.
197#[derive(Error, Debug, PartialEq)]
198pub enum LatLongError {
199    #[error("invalid latitude value: `{0}`")]
200    Latitude(f64),
201    #[error("invalid longitude value: `{0}`")]
202    Longitude(f64),
203}
204
205impl TryFrom<(f64, f64)> for LatLong {
206    type Error = LatLongError;
207
208    /// Attempt to convert a pair of f64 values in latitude, longitude order.
209    ///
210    /// return a valid `LatLong` or a `LatLongError`.
211    fn try_from(lat_long: (f64, f64)) -> Result<Self, Self::Error> {
212        if !is_valid_latitude(lat_long.0) {
213            Err(LatLongError::Latitude(lat_long.0))
214        } else if !is_valid_longitude(lat_long.1) {
215            Err(LatLongError::Longitude(lat_long.1))
216        } else {
217            Ok(Self::new(Degrees(lat_long.0), Degrees(lat_long.1)))
218        }
219    }
220}
221
222/// Calculate the azimuth and distance along the great circle of point b from
223/// point a.
224/// * `a`, `b` - the start and end positions
225///
226/// returns the great-circle azimuth relative to North and distance of point b
227/// from point a.
228#[must_use]
229pub fn calculate_azimuth_and_distance(a: &LatLong, b: &LatLong) -> (Angle, Radians) {
230    let a_lat = Angle::from(a.lat);
231    let b_lat = Angle::from(b.lat);
232    let delta_long = Angle::from((b.lon, a.lon));
233    (
234        great_circle::calculate_gc_azimuth(a_lat, b_lat, delta_long),
235        great_circle::calculate_gc_distance(a_lat, b_lat, delta_long),
236    )
237}
238
239/// Calculate the distance along the great circle of point b from point a.
240///
241/// See: [Haversine formula](https://en.wikipedia.org/wiki/Haversine_formula).
242/// This function is less accurate than `calculate_azimuth_and_distance`.
243/// * `a`, `b` - the start and end positions
244///
245/// returns the great-circle distance of point b from point a in `Radians`.
246#[must_use]
247pub fn haversine_distance(a: &LatLong, b: &LatLong) -> Radians {
248    let a_lat = Angle::from(a.lat);
249    let b_lat = Angle::from(b.lat);
250    let delta_lat = Angle::from((b.lat, a.lat));
251    let delta_long = Angle::from(b.lon - a.lon);
252    great_circle::calculate_haversine_distance(a_lat, b_lat, delta_long, delta_lat)
253}
254
255/// A `Vector3d` is a [nalgebra](https://crates.io/crates/nalgebra) `Vector3<f64>`.
256#[allow(clippy::module_name_repetitions)]
257pub type Vector3d = na::Vector3<f64>;
258
259impl From<&LatLong> for Vector3d {
260    /// Convert a `LatLong` to a point on the unit sphere.
261    ///
262    /// @pre |lat| <= 90.0 degrees.
263    /// * `lat` - the latitude.
264    /// * `lon` - the longitude.
265    ///
266    /// returns a `Vector3d` of the point on the unit sphere.
267    fn from(a: &LatLong) -> Self {
268        vector::to_point(Angle::from(a.lat), Angle::from(a.lon))
269    }
270}
271
272impl From<&Vector3d> for LatLong {
273    /// Convert a point to a `LatLong`
274    fn from(value: &Vector3d) -> Self {
275        Self::new(
276            Degrees::from(vector::latitude(value)),
277            Degrees::from(vector::longitude(value)),
278        )
279    }
280}
281
282/// An `Arc` of a Great Circle on a unit sphere.
283#[derive(Clone, Copy, Debug, PartialEq)]
284pub struct Arc {
285    /// The start point of the `Arc`.
286    a: Vector3d,
287    /// The right hand pole of the Great Circle of the `Arc`.
288    pole: Vector3d,
289    /// The length of the `Arc`.
290    length: Radians,
291    /// The half width of the `Arc`.
292    half_width: Radians,
293}
294
295impl Validate for Arc {
296    /// Test whether an `Arc` is valid.
297    ///
298    /// I.e. both a and pole are on the unit sphere and are orthogonal and
299    /// both length and `half_width` are not negative.
300    fn is_valid(&self) -> bool {
301        vector::is_unit(&self.a)
302            && vector::is_unit(&self.pole)
303            && vector::are_orthogonal(&self.a, &self.pole)
304            && !self.length.0.is_sign_negative()
305            && !self.half_width.0.is_sign_negative()
306    }
307}
308
309impl Arc {
310    /// Construct an `Arc`
311    ///
312    /// * `a` - the start point of the `Arc`.
313    /// * `pole` - the right hand pole of the Great Circle of the `Arc`.
314    /// * `length` - the length of the `Arc`.
315    /// * `half_width` - the half width of the `Arc`.
316    #[must_use]
317    pub const fn new(a: Vector3d, pole: Vector3d, length: Radians, half_width: Radians) -> Self {
318        Self {
319            a,
320            pole,
321            length,
322            half_width,
323        }
324    }
325
326    /// Construct an `Arc`
327    ///
328    /// * `a` - the start position
329    /// * `azimuth` - the azimuth at a.
330    /// * `length` - the length of the `Arc`.
331    #[must_use]
332    pub fn from_lat_lon_azi_length(a: &LatLong, azimuth: Angle, length: Radians) -> Self {
333        Self::new(
334            Vector3d::from(a),
335            vector::calculate_pole(Angle::from(a.lat()), Angle::from(a.lon()), azimuth),
336            length,
337            Radians(0.0),
338        )
339    }
340
341    /// Construct an `Arc` from the start and end positions.
342    ///
343    /// Note: if the points are the same or antipodal, the pole will be invalid.
344    /// * `a`, `b` - the start and end positions
345    #[must_use]
346    pub fn between_positions(a: &LatLong, b: &LatLong) -> Self {
347        let (azimuth, length) = calculate_azimuth_and_distance(a, b);
348        let a_lat = Angle::from(a.lat());
349        // if a is at the North or South pole
350        if a_lat.cos().0 < great_circle::MIN_VALUE {
351            // use b's longitude
352            Self::from_lat_lon_azi_length(&LatLong::new(a.lat(), b.lon()), azimuth, length)
353        } else {
354            Self::from_lat_lon_azi_length(a, azimuth, length)
355        }
356    }
357
358    /// Set the `half_width` of an `Arc`.
359    ///
360    /// * `half_width` - the half width of the `Arc`.
361    #[must_use]
362    pub const fn set_half_width(&mut self, half_width: Radians) -> &mut Self {
363        self.half_width = half_width;
364        self
365    }
366
367    /// The start point of the `Arc`.
368    #[must_use]
369    pub const fn a(&self) -> Vector3d {
370        self.a
371    }
372
373    /// The right hand pole of the Great Circle at the start point of the `Arc`.
374    #[must_use]
375    pub const fn pole(&self) -> Vector3d {
376        self.pole
377    }
378
379    /// The length of the `Arc`.
380    #[must_use]
381    pub const fn length(&self) -> Radians {
382        self.length
383    }
384
385    /// The half width of the `Arc`.
386    #[must_use]
387    pub const fn half_width(&self) -> Radians {
388        self.half_width
389    }
390
391    /// The azimuth at the start point.
392    #[must_use]
393    pub fn azimuth(&self) -> Angle {
394        vector::calculate_azimuth(&self.a, &self.pole)
395    }
396
397    /// The direction vector of the `Arc` at the start point.
398    #[must_use]
399    pub fn direction(&self) -> Vector3d {
400        vector::direction(&self.a, &self.pole)
401    }
402
403    /// A position vector at distance along the `Arc`.
404    #[must_use]
405    pub fn position(&self, distance: Radians) -> Vector3d {
406        vector::position(&self.a, &self.direction(), Angle::from(distance))
407    }
408
409    /// The end point of the `Arc`.
410    #[must_use]
411    pub fn b(&self) -> Vector3d {
412        self.position(self.length)
413    }
414
415    /// The mid point of the `Arc`.
416    #[must_use]
417    pub fn mid_point(&self) -> Vector3d {
418        self.position(Radians(0.5 * self.length.0))
419    }
420
421    /// The position of a perpendicular point at distance from the `Arc`.
422    ///
423    /// * `point` a point on the `Arc`'s great circle.
424    /// * `distance` the perpendicular distance from the `Arc`'s great circle.
425    ///
426    /// returns the point at perpendicular distance from point.
427    #[must_use]
428    pub fn perp_position(&self, point: &Vector3d, distance: Radians) -> Vector3d {
429        vector::position(point, &self.pole, Angle::from(distance))
430    }
431
432    /// The position of a point at angle from the `Arc` start, at `Arc` length.
433    ///
434    /// * `angle` the angle from the `Arc` start.
435    ///
436    /// returns the point at angle from the `Arc` start, at `Arc` length.
437    #[must_use]
438    pub fn angle_position(&self, angle: Angle) -> Vector3d {
439        vector::rotate_position(&self.a, &self.pole, angle, Angle::from(self.length))
440    }
441
442    /// The `Arc` at the end of an `Arc`, just the point if `half_width` is zero.
443    ///
444    /// @param `at_b` if true the `Arc` at b, else the `Arc` at a.
445    ///
446    /// @return the end `Arc` at a or b.
447    #[must_use]
448    pub fn end_arc(&self, at_b: bool) -> Self {
449        let p = if at_b { self.b() } else { self.a };
450        let pole = vector::direction(&p, &self.pole);
451        if self.half_width.0 < great_circle::MIN_VALUE {
452            Self::new(p, pole, Radians(0.0), Radians(0.0))
453        } else {
454            let a = self.perp_position(&p, self.half_width);
455            Self::new(a, pole, self.half_width + self.half_width, Radians(0.0))
456        }
457    }
458
459    /// Calculate great-circle along and across track distances of point from
460    /// the `Arc`.
461    ///
462    /// * `point` - the point.
463    ///
464    /// returns the along and across track distances of the point in Radians.
465    #[must_use]
466    pub fn calculate_atd_and_xtd(&self, point: &Vector3d) -> (Radians, Radians) {
467        vector::calculate_atd_and_xtd(&self.a, &self.pole(), point)
468    }
469
470    /// Calculate the shortest great-circle distance of a point from the `Arc`.
471    ///
472    /// * `point` - the point.
473    ///
474    /// returns the shortest distance of a point from the `Arc` in Radians.
475    #[must_use]
476    pub fn shortest_distance(&self, point: &Vector3d) -> Radians {
477        let (atd, xtd) = self.calculate_atd_and_xtd(point);
478        if vector::intersection::is_alongside(atd, self.length, Radians(4.0 * f64::EPSILON)) {
479            xtd.abs()
480        } else {
481            let sq_a = vector::sq_distance(&self.a, point);
482            let sq_b = vector::sq_distance(&self.b(), point);
483            let sq_d = min(sq_a, sq_b);
484            great_circle::e2gc_distance(libm::sqrt(sq_d))
485        }
486    }
487}
488
489/// A Error type for an invalid `Arc`.
490#[derive(Error, Debug, PartialEq)]
491pub enum ArcError {
492    #[error("positions are too close: `{0}`")]
493    PositionsTooClose(f64),
494    #[error("positions are too far apart: `{0}`")]
495    PositionsTooFar(f64),
496}
497
498impl TryFrom<(&LatLong, &LatLong)> for Arc {
499    type Error = ArcError;
500
501    /// Construct an `Arc` from a pair of positions.
502    ///
503    /// * `params` - the start and end positions
504    fn try_from(params: (&LatLong, &LatLong)) -> Result<Self, Self::Error> {
505        // Convert positions to vectors
506        let a = Vector3d::from(params.0);
507        let b = Vector3d::from(params.1);
508        // Calculate the great circle pole
509        vector::normalise(&a.cross(&b), vector::MIN_SQ_NORM).map_or_else(
510            || {
511                let sq_d = vector::sq_distance(&a, &b);
512                if sq_d < 1.0 {
513                    Err(ArcError::PositionsTooClose(sq_d))
514                } else {
515                    Err(ArcError::PositionsTooFar(sq_d))
516                }
517            },
518            |pole| {
519                Ok(Self::new(
520                    a,
521                    pole,
522                    great_circle::e2gc_distance(vector::distance(&a, &b)),
523                    Radians(0.0),
524                ))
525            },
526        )
527    }
528}
529
530/// Calculate the great-circle distances along a pair of `Arc`s to their
531/// closest intersection point or their coincident arc distances if the
532/// `Arc`s are on coincident Great Circles.
533///
534/// * `arc1`, `arc2` the `Arc`s.
535///
536/// returns the distances along the first `Arc` and second `Arc` to the intersection
537/// point or to their coincident arc distances if the `Arc`s do not intersect.
538#[must_use]
539pub fn calculate_intersection_distances(arc1: &Arc, arc2: &Arc) -> (Radians, Radians) {
540    vector::intersection::calculate_intersection_point_distances(
541        &arc1.a,
542        &arc1.pole,
543        arc1.length(),
544        &arc2.a,
545        &arc2.pole,
546        arc2.length(),
547        &(0.5 * (arc1.mid_point() + arc2.mid_point())),
548    )
549}
550
551/// Calculate whether a pair of `Arc`s intersect and (if so) where.
552///
553/// * `arc1`, `arc2` the `Arc`s.
554///
555/// returns the distance along the first `Arc` to the second `Arc` or None if they
556/// don't intersect.
557///
558/// # Examples
559/// ```
560/// use unit_sphere::{Arc, Degrees, LatLong, calculate_intersection_point};
561/// use angle_sc::is_within_tolerance;
562///
563/// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
564/// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
565/// let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
566/// let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
567///
568/// let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
569/// let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
570///
571/// // Calculate the intersection point position
572/// let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
573/// let lat_long = LatLong::from(&intersection_point);
574///
575/// // The expected latitude and longitude are from:
576/// // <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
577///
578/// // Geodesic intersection latitude is 54.7170296089477
579/// assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
580/// // Geodesic intersection longitude is -14.56385574430775
581/// assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
582/// ```
583#[must_use]
584pub fn calculate_intersection_point(arc1: &Arc, arc2: &Arc) -> Option<Vector3d> {
585    let (distance1, distance2) = calculate_intersection_distances(arc1, arc2);
586
587    // Determine whether both distances are within both arcs
588    if vector::intersection::is_alongside(distance1, arc1.length(), Radians(4.0 * f64::EPSILON))
589        && vector::intersection::is_alongside(distance2, arc2.length(), Radians(4.0 * f64::EPSILON))
590    {
591        Some(arc1.position(distance1.clamp(arc1.length())))
592    } else {
593        None
594    }
595}
596
597#[cfg(test)]
598mod tests {
599    use super::*;
600    use angle_sc::{is_within_tolerance, Degrees};
601
602    #[test]
603    fn test_is_valid_latitude() {
604        // value < -90
605        assert!(!is_valid_latitude(-90.0001));
606        // value = -90
607        assert!(is_valid_latitude(-90.0));
608        // value = 90
609        assert!(is_valid_latitude(90.0));
610        // value > 90
611        assert!(!is_valid_latitude(90.0001));
612    }
613
614    #[test]
615    fn test_is_valid_longitude() {
616        // value < -180
617        assert!(!is_valid_longitude(-180.0001));
618        // value = -180
619        assert!(is_valid_longitude(-180.0));
620        // value = 180
621        assert!(is_valid_longitude(180.0));
622        // value > 180
623        assert!(!is_valid_longitude(180.0001));
624    }
625
626    #[test]
627    fn test_latlong_traits() {
628        let a = LatLong::try_from((0.0, 90.0)).unwrap();
629
630        assert!(a.is_valid());
631
632        let a_clone = a.clone();
633        assert!(a_clone == a);
634
635        assert_eq!(Degrees(0.0), a.lat());
636        assert_eq!(Degrees(90.0), a.lon());
637
638        assert!(!a.is_south_of(&a));
639        assert!(!a.is_west_of(&a));
640
641        let b = LatLong::try_from((-10.0, -91.0)).unwrap();
642        assert!(b.is_south_of(&a));
643        assert!(b.is_west_of(&a));
644
645        println!("LatLong: {:?}", a);
646
647        let invalid_lat = LatLong::try_from((91.0, 0.0));
648        assert_eq!(Err(LatLongError::Latitude(91.0)), invalid_lat);
649        println!("invalid_lat: {:?}", invalid_lat);
650
651        let invalid_lon = LatLong::try_from((0.0, 181.0));
652        assert_eq!(Err(LatLongError::Longitude(181.0)), invalid_lon);
653        println!("invalid_lon: {:?}", invalid_lon);
654    }
655
656    #[test]
657    fn test_vector3d_traits() {
658        let a = LatLong::try_from((0.0, 90.0)).unwrap();
659        let point = Vector3d::from(&a);
660
661        assert_eq!(0.0, point.x);
662        assert_eq!(1.0, point.y);
663        assert_eq!(0.0, point.z);
664
665        assert_eq!(Degrees(0.0), Degrees::from(vector::latitude(&point)));
666        assert_eq!(Degrees(90.0), Degrees::from(vector::longitude(&point)));
667
668        let result = LatLong::from(&point);
669        assert_eq!(a, result);
670    }
671
672    #[test]
673    fn test_great_circle_90n_0n_0e() {
674        let a = LatLong::new(Degrees(90.0), Degrees(0.0));
675        let b = LatLong::new(Degrees(0.0), Degrees(0.0));
676        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
677
678        assert!(is_within_tolerance(
679            core::f64::consts::FRAC_PI_2,
680            dist.0,
681            f64::EPSILON
682        ));
683        assert_eq!(180.0, Degrees::from(azimuth).0);
684
685        let dist = haversine_distance(&a, &b);
686        assert!(is_within_tolerance(
687            core::f64::consts::FRAC_PI_2,
688            dist.0,
689            f64::EPSILON
690        ));
691    }
692
693    #[test]
694    fn test_great_circle_90s_0n_50e() {
695        let a = LatLong::new(Degrees(-90.0), Degrees(0.0));
696        let b = LatLong::new(Degrees(0.0), Degrees(50.0));
697        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
698
699        assert!(is_within_tolerance(
700            core::f64::consts::FRAC_PI_2,
701            dist.0,
702            f64::EPSILON
703        ));
704        assert_eq!(0.0, Degrees::from(azimuth).0);
705
706        let dist = haversine_distance(&a, &b);
707        assert!(is_within_tolerance(
708            core::f64::consts::FRAC_PI_2,
709            dist.0,
710            f64::EPSILON
711        ));
712    }
713
714    #[test]
715    fn test_great_circle_0n_60e_0n_60w() {
716        let a = LatLong::new(Degrees(0.0), Degrees(60.0));
717        let b = LatLong::new(Degrees(0.0), Degrees(-60.0));
718        let (azimuth, dist) = calculate_azimuth_and_distance(&a, &b);
719
720        assert!(is_within_tolerance(
721            2.0 * core::f64::consts::FRAC_PI_3,
722            dist.0,
723            2.0 * f64::EPSILON
724        ));
725        assert_eq!(-90.0, Degrees::from(azimuth).0);
726
727        let dist = haversine_distance(&a, &b);
728        assert!(is_within_tolerance(
729            2.0 * core::f64::consts::FRAC_PI_3,
730            dist.0,
731            2.0 * f64::EPSILON
732        ));
733    }
734
735    #[test]
736    fn test_arc() {
737        // Greenwich equator
738        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
739
740        // 90 degrees East on the equator
741        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
742
743        let mut arc = Arc::between_positions(&g_eq, &e_eq);
744        let arc = arc.set_half_width(Radians(0.01));
745        assert!(arc.is_valid());
746        assert_eq!(Radians(0.01), arc.half_width());
747
748        assert_eq!(Vector3d::from(&g_eq), arc.a());
749        assert_eq!(Vector3d::new(0.0, 0.0, 1.0), arc.pole());
750        assert!(is_within_tolerance(
751            core::f64::consts::FRAC_PI_2,
752            arc.length().0,
753            f64::EPSILON
754        ));
755        assert_eq!(Angle::from(Degrees(90.0)), arc.azimuth());
756        let b = Vector3d::from(&e_eq);
757        assert!(is_within_tolerance(
758            0.0,
759            vector::distance(&b, &arc.b()),
760            f64::EPSILON
761        ));
762
763        let mid_point = arc.mid_point();
764        assert_eq!(0.0, mid_point.z);
765        assert!(is_within_tolerance(
766            45.0,
767            Degrees::from(vector::longitude(&mid_point)).0,
768            32.0 * f64::EPSILON
769        ));
770
771        let start_arc = arc.end_arc(false);
772        assert_eq!(0.02, start_arc.length().0);
773
774        let start_arc_a = start_arc.a();
775        assert_eq!(start_arc_a, arc.perp_position(&arc.a(), Radians(0.01)));
776
777        let angle_90 = Angle::from(Degrees(90.0));
778        let pole_0 = Vector3d::new(0.0, 0.0, 1.0);
779        assert!(vector::distance(&pole_0, &arc.angle_position(angle_90)) <= f64::EPSILON);
780
781        let end_arc = arc.end_arc(true);
782        assert_eq!(0.02, end_arc.length().0);
783
784        let end_arc_a = end_arc.a();
785        assert_eq!(end_arc_a, arc.perp_position(&arc.b(), Radians(0.01)));
786    }
787
788    #[test]
789    fn test_north_and_south_poles() {
790        let north_pole = LatLong::new(Degrees(90.0), Degrees(0.0));
791        let south_pole = LatLong::new(Degrees(-90.0), Degrees(0.0));
792
793        let (azimuth, distance) = calculate_azimuth_and_distance(&south_pole, &north_pole);
794        assert_eq!(0.0, Degrees::from(azimuth).0);
795        assert_eq!(core::f64::consts::PI, distance.0);
796
797        let (azimuth, distance) = calculate_azimuth_and_distance(&north_pole, &south_pole);
798        assert_eq!(180.0, Degrees::from(azimuth).0);
799        assert_eq!(core::f64::consts::PI, distance.0);
800
801        // 90 degrees East on the equator
802        let e_eq = LatLong::new(Degrees(0.0), Degrees(50.0));
803
804        let arc = Arc::between_positions(&north_pole, &e_eq);
805        assert!(is_within_tolerance(
806            e_eq.lat().0,
807            LatLong::from(&arc.b()).lat().abs().0,
808            1e-13
809        ));
810        assert!(is_within_tolerance(
811            e_eq.lon().0,
812            LatLong::from(&arc.b()).lon().0,
813            50.0 * f64::EPSILON
814        ));
815
816        let arc = Arc::between_positions(&south_pole, &e_eq);
817        assert!(is_within_tolerance(
818            e_eq.lat().0,
819            LatLong::from(&arc.b()).lat().abs().0,
820            1e-13
821        ));
822        assert!(is_within_tolerance(
823            e_eq.lon().0,
824            LatLong::from(&arc.b()).lon().0,
825            50.0 * f64::EPSILON
826        ));
827
828        let w_eq = LatLong::new(Degrees(0.0), Degrees(-140.0));
829
830        let arc = Arc::between_positions(&north_pole, &w_eq);
831        assert!(is_within_tolerance(
832            w_eq.lat().0,
833            LatLong::from(&arc.b()).lat().abs().0,
834            1e-13
835        ));
836        assert!(is_within_tolerance(
837            w_eq.lon().0,
838            LatLong::from(&arc.b()).lon().0,
839            256.0 * f64::EPSILON
840        ));
841
842        let arc = Arc::between_positions(&south_pole, &w_eq);
843        assert!(is_within_tolerance(
844            w_eq.lat().0,
845            LatLong::from(&arc.b()).lat().abs().0,
846            1e-13
847        ));
848        assert!(is_within_tolerance(
849            w_eq.lon().0,
850            LatLong::from(&arc.b()).lon().0,
851            256.0 * f64::EPSILON
852        ));
853
854        let invalid_arc = Arc::try_from((&north_pole, &north_pole));
855        assert_eq!(Err(ArcError::PositionsTooClose(0.0)), invalid_arc);
856        println!("invalid_arc: {:?}", invalid_arc);
857
858        let arc = Arc::between_positions(&north_pole, &north_pole);
859        assert_eq!(north_pole, LatLong::from(&arc.b()));
860
861        let invalid_arc = Arc::try_from((&north_pole, &south_pole));
862        assert_eq!(Err(ArcError::PositionsTooFar(4.0)), invalid_arc);
863        println!("invalid_arc: {:?}", invalid_arc);
864
865        let arc = Arc::between_positions(&north_pole, &south_pole);
866        assert_eq!(south_pole, LatLong::from(&arc.b()));
867
868        let arc = Arc::between_positions(&south_pole, &north_pole);
869        assert_eq!(north_pole, LatLong::from(&arc.b()));
870
871        let arc = Arc::between_positions(&south_pole, &south_pole);
872        assert_eq!(south_pole, LatLong::from(&arc.b()));
873    }
874
875    #[test]
876    fn test_arc_atd_and_xtd() {
877        // Greenwich equator
878        let g_eq = LatLong::new(Degrees(0.0), Degrees(0.0));
879
880        // 90 degrees East on the equator
881        let e_eq = LatLong::new(Degrees(0.0), Degrees(90.0));
882
883        let arc = Arc::try_from((&g_eq, &e_eq)).unwrap();
884        assert!(arc.is_valid());
885
886        let start_arc = arc.end_arc(false);
887        assert_eq!(0.0, start_arc.length().0);
888
889        let start_arc_a = start_arc.a();
890        assert_eq!(arc.a(), start_arc_a);
891
892        let longitude = Degrees(1.0);
893
894        // Test across track distance
895        // Accuracy drops off outside of this range
896        for lat in -83..84 {
897            let latitude = Degrees(lat as f64);
898            let latlong = LatLong::new(latitude, longitude);
899            let point = Vector3d::from(&latlong);
900
901            let expected = (lat as f64).to_radians();
902            let (atd, xtd) = arc.calculate_atd_and_xtd(&point);
903            assert!(is_within_tolerance(1_f64.to_radians(), atd.0, f64::EPSILON));
904            assert!(is_within_tolerance(expected, xtd.0, 2.0 * f64::EPSILON));
905
906            let d = arc.shortest_distance(&point);
907            assert!(is_within_tolerance(
908                libm::fabs(expected),
909                d.0,
910                2.0 * f64::EPSILON
911            ));
912        }
913
914        let point = Vector3d::from(&g_eq);
915        let d = arc.shortest_distance(&point);
916        assert_eq!(0.0, d.0);
917
918        let point = Vector3d::from(&e_eq);
919        let d = arc.shortest_distance(&point);
920        assert_eq!(0.0, d.0);
921
922        let latlong = LatLong::new(Degrees(0.0), Degrees(-1.0));
923        let point = Vector3d::from(&latlong);
924        let d = arc.shortest_distance(&point);
925        assert!(is_within_tolerance(1_f64.to_radians(), d.0, f64::EPSILON));
926
927        let point = -point;
928        let d = arc.shortest_distance(&point);
929        assert!(is_within_tolerance(89_f64.to_radians(), d.0, f64::EPSILON));
930    }
931
932    #[test]
933    fn test_arc_intersection_point() {
934        // Karney's example:
935        // Istanbul, Washington, Reyjavik and Accra
936        // from: <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
937        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
938        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
939        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
940        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
941
942        let arc1 = Arc::try_from((&istanbul, &washington)).unwrap();
943        let arc2 = Arc::try_from((&reyjavik, &accra)).unwrap();
944
945        let intersection_point = calculate_intersection_point(&arc1, &arc2).unwrap();
946        let lat_long = LatLong::from(&intersection_point);
947        // Geodesic intersection latitude is 54.7170296089477
948        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
949        // Geodesic intersection longitude is -14.56385574430775
950        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
951
952        // Switch arcs
953        let intersection_point = calculate_intersection_point(&arc2, &arc1).unwrap();
954        let lat_long = LatLong::from(&intersection_point);
955        // Geodesic intersection latitude is 54.7170296089477
956        assert!(is_within_tolerance(54.72, lat_long.lat().0, 0.05));
957        // Geodesic intersection longitude is -14.56385574430775
958        assert!(is_within_tolerance(-14.56, lat_long.lon().0, 0.02));
959    }
960
961    #[test]
962    fn test_arc_intersection_same_great_circles() {
963        let south_pole_1 = LatLong::new(Degrees(-88.0), Degrees(-180.0));
964        let south_pole_2 = LatLong::new(Degrees(-87.0), Degrees(0.0));
965
966        let arc1 = Arc::try_from((&south_pole_1, &south_pole_2)).unwrap();
967
968        let intersection_lengths = calculate_intersection_distances(&arc1, &arc1);
969        assert_eq!(Radians(0.0), intersection_lengths.0);
970        assert_eq!(Radians(0.0), intersection_lengths.1);
971
972        let intersection_point = calculate_intersection_point(&arc1, &arc1).unwrap();
973        assert_eq!(0.0, vector::sq_distance(&arc1.a(), &intersection_point));
974
975        let south_pole_3 = LatLong::new(Degrees(-85.0), Degrees(0.0));
976        let south_pole_4 = LatLong::new(Degrees(-86.0), Degrees(0.0));
977        let arc2 = Arc::try_from((&south_pole_3, &south_pole_4)).unwrap();
978        let intersection_point = calculate_intersection_point(&arc1, &arc2);
979        assert!(intersection_point.is_none());
980    }
981}