unit_sphere/
lib.rs

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