icao_wgs84/
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//! icao-wgs84
22//!
23//! [![crates.io](https://img.shields.io/crates/v/icao-wgs84.svg)](https://crates.io/crates/icao-wgs84)
24//! [![docs.io](https://docs.rs/icao-wgs84/badge.svg)](https://docs.rs/icao-wgs84/)
25//! [![License](https://img.shields.io/badge/License-MIT-blue)](https://opensource.org/license/mit/)
26//! [![Rust](https://github.com/kenba/icao-wgs84-rs/actions/workflows/rust.yml/badge.svg)](https://github.com/kenba/icao-wgs84-rs/actions)
27//! [![codecov](https://codecov.io/gh/kenba/icao-wgs84-rs/graph/badge.svg?token=85TJX5VAHF)](https://codecov.io/gh/kenba/icao-wgs84-rs)
28//!
29//! A library for performing geometric calculations on the
30//! [WGS-84](https://via-technology.aero/img/navigation/REF08-Doc9674.pdf)
31//! ellipsoid, see *Figure 1*.
32//!
33//! <img src="https://upload.wikimedia.org/wikipedia/commons/3/3e/WGS84_mean_Earth_radius.svg" width="400">\
34//! *Figure 1 The WGS-84 Ellipsoid (not to scale)
35//! [Cmglee](https://commons.wikimedia.org/wiki/User:Cmglee), [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0), via Wikimedia Commons*
36//!
37//! [WGS-84](https://via-technology.aero/img/navigation/REF08-Doc9674.pdf)
38//! has become the de facto standard for satellite navigation since its adoption
39//! by the Navstar Global Positioning System
40//! ([GPS](https://www.gps.gov/systems/gps/performance/accuracy/))
41//! and the USA making GPS available for civilian use in 1983.
42//!
43//! This library uses the WGS-84 primary parameters defined in Tab. 3-1 of the
44//! [ICAO WGS-84 Implementation Manual](https://via-technology.aero/img/navigation/REF08-Doc9674.pdf).
45//!
46//! ## Geodesic navigation
47//!
48//! The shortest path between two points on the surface of an ellipsoid is a
49//! [geodesic segment](https://en.wikipedia.org/wiki/Geodesics_on_an_ellipsoid).
50//! It is the equivalent of a straight line segment in planar geometry or a
51//! [great circle arc](https://en.wikipedia.org/wiki/Great_circle) on the
52//! surface of a sphere, see *Figure 2*.
53//!
54//! <img src="https://via-technology.aero/img/navigation/ellipsoid/sphere_mercator_long_geodesic.png" width="400">
55//!
56//! *Figure 2 A geodesic segment (orange) and a great circle  arc (blue)*
57//!
58//! This library uses the correspondence between geodesic segments on an ellipsoid
59//! and great-circle arcs on a unit sphere, together with 3D vectors to calculate:
60//!
61//! - the length and azimuths of a geodesic segment between two positions;
62//! - the along track and across track distances of a position relative to a geodesic segment;
63//! - and the intersection of two geodesic segments.
64//!
65//! See: [geodesic algorithms](https://via-technology.aero/navigation/geodesic-algorithms/).
66//!
67//! ## Design
68//!
69//! The library is based on Charles Karney's [GeographicLib](https://geographiclib.sourceforge.io/) library.
70//!
71//! Like `GeographicLib`, it models geodesic segments as great circle arcs on
72//! the surface of a unit sphere. However, it also uses vectors to perform
73//! calculations between geodesic segments.
74//!
75//! The `Ellipsoid` class represents an ellipsoid of revolution.
76//! The static `WGS84_ELLIPSOID` represents the WGS-84 `Ellipsoid` which is used
77//! by the `GeodesicSegment` `From` traits to create `GeodesicSegment`s on the WGS-84 `Ellipsoid`.
78//!
79//! The library depends upon the following crates:
80//!
81//! - [angle-sc](https://crates.io/crates/angle-sc) - to define `Angle`,
82//!   `Degrees` and `Radians` and perform trigonometric calculations;
83//! - [unit-sphere](https://crates.io/crates/unit-sphere) - to define `LatLong`
84//!   and perform great-circle and vector calculations.
85//! - [icao_units](https://crates.io/crates/icao-units) - to define `Metres` and
86//!   `NauticalMiles` and perform conversions between them.
87//!
88//! <img src="https://via-technology.aero/img/software/ellipsoid_class_diagram.svg" width="400">
89//!
90//! *Figure 3 Class Diagram*
91//!
92//! The library is declared [no_std](https://docs.rust-embedded.org/book/intro/no-std.html)
93//! so it can be used in embedded applications.
94
95#![cfg_attr(not(test), no_std)]
96
97extern crate angle_sc;
98extern crate icao_units;
99extern crate unit_sphere;
100
101pub mod ellipsoid;
102pub mod geodesic;
103pub mod intersection;
104
105pub use angle_sc::{Angle, Degrees, Radians, Validate};
106pub use icao_units::non_si::NauticalMiles;
107pub use icao_units::si::Metres;
108pub use unit_sphere::LatLong;
109
110use angle_sc::trig;
111use once_cell::sync::Lazy;
112use unit_sphere::{Vector3d, great_circle};
113
114/// The parameters of an `Ellipsoid`.
115#[derive(Clone, Debug, PartialEq)]
116pub struct Ellipsoid {
117    /// The Semimajor axis of the ellipsoid.
118    a: Metres,
119    /// The flattening of the ellipsoid, a ratio.
120    f: f64,
121
122    /// The Semiminor axis of the ellipsoid.
123    b: Metres,
124    /// One minus the flattening ratio.
125    one_minus_f: f64,
126    /// The reciprocal of one minus the flattening ratio.
127    recip_one_minus_f: f64,
128    /// The square of the Eccentricity of the ellipsoid.
129    e_2: f64,
130    /// The square of the second Eccentricity of the ellipsoid.
131    ep_2: f64,
132    /// The third flattening of the ellipsoid.
133    n: f64,
134
135    /// The A3 series `coefficients` of the ellipsoid.
136    a3: [f64; 6],
137    /// The C3x series `coefficients` of the ellipsoid.
138    c3x: [f64; 15],
139}
140
141impl Ellipsoid {
142    /// Constructor.
143    /// * `a` - the Semimajor axis of the `Ellipsoid`.
144    /// * `f` - the flattening of the `Ellipsoid`, a ratio.
145    #[must_use]
146    pub fn new(a: Metres, f: f64) -> Self {
147        let one_minus_f = 1.0 - f;
148        let n = ellipsoid::calculate_3rd_flattening(f);
149        Self {
150            a,
151            f,
152            b: ellipsoid::calculate_minor_axis(a, f),
153            one_minus_f,
154            recip_one_minus_f: 1.0 / one_minus_f,
155            e_2: ellipsoid::calculate_sq_eccentricity(f),
156            ep_2: ellipsoid::calculate_sq_2nd_eccentricity(f),
157            n,
158            a3: ellipsoid::coefficients::evaluate_coeffs_a3(n),
159            c3x: ellipsoid::coefficients::evaluate_coeffs_c3x(n),
160        }
161    }
162
163    /// Construct an `Ellipsoid` with the WGS-84 parameters.
164    #[must_use]
165    pub fn wgs84() -> Self {
166        Self::new(ellipsoid::wgs84::A, ellipsoid::wgs84::F)
167    }
168
169    /// The Semimajor axis of the ellipsoid.
170    #[must_use]
171    pub const fn a(&self) -> Metres {
172        self.a
173    }
174
175    /// The flattening of the ellipsoid, a ratio.
176    #[must_use]
177    pub const fn f(&self) -> f64 {
178        self.f
179    }
180
181    /// The Semiminor axis of the ellipsoid.
182    #[must_use]
183    pub const fn b(&self) -> Metres {
184        self.b
185    }
186
187    /// One minus the flattening ratio.
188    #[must_use]
189    pub const fn one_minus_f(&self) -> f64 {
190        self.one_minus_f
191    }
192
193    /// The reciprocal of one minus the flattening ratio.
194    #[must_use]
195    pub const fn recip_one_minus_f(&self) -> f64 {
196        self.recip_one_minus_f
197    }
198
199    /// The square of the Eccentricity of the ellipsoid.
200    #[must_use]
201    pub const fn e_2(&self) -> f64 {
202        self.e_2
203    }
204
205    /// The square of the second Eccentricity of the ellipsoid.
206    #[must_use]
207    pub const fn ep_2(&self) -> f64 {
208        self.ep_2
209    }
210
211    /// The third flattening of the ellipsoid.
212    #[must_use]
213    pub const fn n(&self) -> f64 {
214        self.n
215    }
216
217    /// Calculate epsilon, the variable used in series expansions.
218    /// Note: epsilon is positive and small.
219    /// * `clairaut` - Clairaut's constant.
220    #[must_use]
221    pub fn calculate_epsilon(&self, clairaut: trig::UnitNegRange) -> f64 {
222        ellipsoid::calculate_epsilon(clairaut, self.ep_2)
223    }
224
225    /// Calculate a3f from the A3 series `coefficients` of the ellipsoid.
226    /// * `eps` - epsilon
227    #[must_use]
228    pub fn calculate_a3f(&self, eps: f64) -> f64 {
229        ellipsoid::coefficients::evaluate_polynomial(&self.a3, eps)
230    }
231
232    /// Calculate a3c from the A3 series `coefficients` of the ellipsoid.
233    /// * `clairaut` - Clairaut's constant.
234    /// * `eps` - epsilon
235    #[must_use]
236    pub fn calculate_a3c(&self, clairaut: trig::UnitNegRange, eps: f64) -> f64 {
237        self.f * clairaut.0 * self.calculate_a3f(eps)
238    }
239
240    /// Calculate the coefficients `C3[l]` in the Fourier expansion of `C3`.
241    /// * `eps` - epsilon
242    #[must_use]
243    pub fn calculate_c3y(&self, eps: f64) -> [f64; 6] {
244        ellipsoid::coefficients::evaluate_coeffs_c3y(&self.c3x, eps)
245    }
246
247    /// Convert a geodetic Latitude to a parametric Latitude on the
248    /// auxiliary sphere.
249    /// * `lat` - the geodetic Latitude
250    #[must_use]
251    pub fn calculate_parametric_latitude(&self, lat: Angle) -> Angle {
252        ellipsoid::calculate_parametric_latitude(lat, self.one_minus_f)
253    }
254
255    /// Convert a parametric Latitude on the auxiliary sphere to a
256    /// geodetic Latitude.
257    /// * `beta` - the parametric Latitude
258    #[must_use]
259    pub fn calculate_geodetic_latitude(&self, beta: Angle) -> Angle {
260        ellipsoid::calculate_geodetic_latitude(beta, self.one_minus_f)
261    }
262
263    /// Convert a geodetic latitude and longitude to a point on the
264    /// auxiliary sphere.
265    /// @pre |lat| <= 90.0 degrees.
266    /// * `lat` - the latitude.
267    /// * `lon` - the longitude.
268    ///
269    /// returns a `Vector3d` of the point on the auxiliary sphere.
270    #[must_use]
271    pub fn to_arc_point(&self, lat: Angle, lon: Angle) -> Vector3d {
272        let beta = self.calculate_parametric_latitude(lat);
273        unit_sphere::vector::to_point(beta, lon)
274    }
275}
276
277/// A static instance of the WGS-84 `Ellipsoid`.
278pub static WGS84_ELLIPSOID: Lazy<Ellipsoid> = Lazy::new(Ellipsoid::wgs84);
279
280/// Calculate the azimuths and geodesic length (in metres) between a pair
281/// of positions on the ellipsoid.
282/// * `a`, `b` - the start and finish positions in geodetic coordinates.
283/// * `tolerance` - the tolerance to perform the calculation to.
284/// * `ellipsoid` - the `Ellipsoid`.
285///
286/// returns the azimuth at the start and end positions and the length of
287/// the geodesic segment on the ellipsoid in metres.
288///
289/// # Examples
290/// ```
291/// use icao_wgs84::*;
292/// use unit_sphere::great_circle;
293///
294/// let tolerance = Radians(great_circle::MIN_VALUE);
295///
296/// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
297/// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
298/// let (azimuth, length, end_azimuth) = calculate_azimuths_and_geodesic_length(&istanbul, &washington, tolerance, &WGS84_ELLIPSOID);
299///
300/// let azimuth_degrees = Degrees::from(azimuth);
301/// println!("Istanbul-Washington initial azimuth: {:?}", azimuth_degrees.0);
302///
303/// let distance_nm = NauticalMiles::from(length);
304/// println!("Istanbul-Washington distance: {:?}", distance_nm);
305///
306/// let azimuth_degrees = Degrees::from(end_azimuth.opposite());
307/// println!("Washington-Istanbul initial azimuth: {:?}", azimuth_degrees.0);
308#[must_use]
309pub fn calculate_azimuths_and_geodesic_length(
310    a: &LatLong,
311    b: &LatLong,
312    tolerance: Radians,
313    ellipsoid: &Ellipsoid,
314) -> (Angle, Metres, Angle) {
315    let (alpha1, arc_length, alpha2, _) =
316        geodesic::calculate_azimuths_arc_length(a, b, tolerance, ellipsoid);
317    let beta1 =
318        ellipsoid::calculate_parametric_latitude(Angle::from(a.lat()), ellipsoid.one_minus_f());
319    (
320        alpha1,
321        geodesic::convert_radians_to_metres(beta1, alpha1, arc_length, ellipsoid),
322        alpha2,
323    )
324}
325
326/// A geodesic segment on the surface of an ellipsoid.
327///
328/// A geodesic segment on an ellipsoid is the shortest path between two points.
329/// It is represented by a great circle arc on the auxiliary sphere.
330#[derive(Clone, Debug, PartialEq)]
331pub struct GeodesicSegment<'a> {
332    /// The parametric start latitude on the auxiliary sphere.
333    beta: Angle,
334    /// The start longitude.
335    lon: Angle,
336    /// The start azimuth.
337    azi: Angle,
338    /// Azimuth at the Equator.
339    azi0: Angle,
340    /// Great circle arc distance to the first Equator crossing.
341    sigma1: Angle,
342    /// Great circle arc length on the auxiliary sphere in radians.
343    arc_length: Radians,
344    /// The half width of a Geodesic Rectangle in metres.
345    half_width: Metres,
346    /// Integration constant: epsilon, derived from Clairaut's constant.
347    eps: f64,
348    /// constant used to convert geodesic/great circle longitudes.
349    a3c: f64,
350    /// Start parameter for geodesic/great circle longitudes.
351    b31: Radians,
352    /// A reference to the underlying `Ellipsoid`.
353    ellipsoid: &'a Ellipsoid,
354}
355
356impl Validate for GeodesicSegment<'_> {
357    /// Test whether a `GeodesicSegment` is valid.
358    /// Whether 0° <= `latitude` <= 90° and 0 <= `arc_length` <= π.
359    fn is_valid(&self) -> bool {
360        self.beta.cos().0 >= 0.0
361            && (0.0..core::f64::consts::PI).contains(&self.arc_length.0)
362            && self.half_width.0 >= 0.0
363    }
364}
365
366impl<'a> GeodesicSegment<'a> {
367    /// Construct a `GeodesicSegment`
368    /// * `beta` - the start point parametric latitude on the auxiliary sphere.
369    /// * `lon` - the start point longitude.
370    /// * `azi` - the start azimuth.
371    /// * `arc_length` - the great circle arc length on the auxiliary sphere in radians.
372    /// * `half_width` - the `GeodesicSegment` half width in Metres.
373    /// * `ellipsoid` - a reference to the `Ellipsoid`.
374    #[must_use]
375    pub fn new(
376        beta: Angle,
377        lon: Angle,
378        azi: Angle,
379        arc_length: Radians,
380        half_width: Metres,
381        ellipsoid: &'a Ellipsoid,
382    ) -> Self {
383        // Calculate the azimuth at the first Equator crossing
384        let clairaut = trig::UnitNegRange(azi.sin().0 * beta.cos().0);
385        let azi0 = Angle::new(clairaut, trig::swap_sin_cos(clairaut));
386
387        // Calculate the distance to the first Equator crossing
388        let sigma1 = Angle::from_y_x(beta.sin().0, beta.cos().0 * azi.cos().0);
389
390        // Calculate eps and c3 for calculating coefficients
391        let eps = ellipsoid.calculate_epsilon(azi0.sin());
392        let c3 = ellipsoid.calculate_c3y(eps);
393        Self {
394            beta,
395            lon,
396            azi,
397            azi0,
398            sigma1,
399            arc_length,
400            half_width,
401            eps,
402            a3c: ellipsoid.calculate_a3c(azi0.sin(), eps),
403            b31: ellipsoid::coefficients::sin_cos_series(&c3, sigma1),
404            ellipsoid,
405        }
406    }
407
408    /// Construct a `GeodesicSegment` using the "direct" method.
409    /// @pre |lat| <= 90.0 degrees.
410    /// * `a` - the start position in geodetic coordinates.
411    /// * `azimuth` - the azimuth at the start position.
412    /// * `arc_length` - the Great Circle arc length on the auxiliary sphere in radians.
413    /// * `half_width` - the `GeodesicSegment` half width in Metres.
414    /// * `ellipsoid` - a reference to the `Ellipsoid`.
415    #[must_use]
416    pub fn from_lat_lon_azi_arc_length_half_width(
417        a: &LatLong,
418        azimuth: Angle,
419        arc_length: Radians,
420        half_width: Metres,
421        ellipsoid: &'a Ellipsoid,
422    ) -> Self {
423        let a_lat = Angle::from(a.lat());
424        let a_lon = Angle::from(a.lon());
425        GeodesicSegment::new(
426            ellipsoid.calculate_parametric_latitude(a_lat),
427            a_lon,
428            azimuth,
429            arc_length,
430            half_width,
431            ellipsoid,
432        )
433    }
434
435    /// Construct a `GeodesicSegment` using the "direct" method.
436    /// @pre |lat| <= 90.0 degrees.
437    /// * `a` - the start position in geodetic coordinates.
438    /// * `azimuth` - the azimuth at the start position.
439    /// * `arc_length` - the Great Circle arc length on the auxiliary sphere in radians.
440    /// * `ellipsoid` - a reference to the `Ellipsoid`.
441    #[must_use]
442    pub fn from_lat_lon_azi_arc_length(
443        a: &LatLong,
444        azimuth: Angle,
445        arc_length: Radians,
446        ellipsoid: &'a Ellipsoid,
447    ) -> Self {
448        GeodesicSegment::from_lat_lon_azi_arc_length_half_width(
449            a,
450            azimuth,
451            arc_length,
452            Metres(0.0),
453            ellipsoid,
454        )
455    }
456
457    /// Construct a `GeodesicSegment` using the "direct" method with the length in metres.
458    /// @pre |lat| <= 90.0 degrees.
459    /// * `a` - the start position in geodetic coordinates.
460    /// * `azimuth` - the azimuth at the start position.
461    /// * `length` - the length on the `Ellipsoid` in metres.
462    /// * `ellipsoid` - a reference to the `Ellipsoid`.
463    #[must_use]
464    pub fn from_lat_lon_azi_length(
465        a: &LatLong,
466        azimuth: Angle,
467        length: Metres,
468        ellipsoid: &'a Ellipsoid,
469    ) -> Self {
470        let mut arc =
471            GeodesicSegment::from_lat_lon_azi_arc_length(a, azimuth, Radians(0.0), ellipsoid);
472        arc.set_arc_length(arc.metres_to_radians(length));
473        arc
474    }
475
476    /// Construct a `GeodesicSegment` between a pair of positions, the "indirect" method.
477    /// @pre |lat| <= 90.0 degrees.
478    /// * `a`, `b` - the start and finish positions in geodetic coordinates.
479    /// * `half_width` - the `GeodesicSegment` half width in Metres.
480    /// * `tolerance` - the tolerance to perform the calculation to.
481    /// * `ellipsoid` - a reference to the `Ellipsoid`.
482    #[must_use]
483    pub fn between_positions(
484        a: &LatLong,
485        b: &LatLong,
486        half_width: Metres,
487        tolerance: Radians,
488        ellipsoid: &'a Ellipsoid,
489    ) -> Self {
490        let (azimuth, arc_length, _, _) =
491            geodesic::calculate_azimuths_arc_length(a, b, tolerance, ellipsoid);
492        let a_lat = Angle::from(a.lat());
493        // if a is at the North or South pole
494        if a_lat.cos().0 < great_circle::MIN_VALUE {
495            // use b's longitude
496            Self::from_lat_lon_azi_arc_length_half_width(
497                &LatLong::new(a.lat(), b.lon()),
498                azimuth,
499                arc_length,
500                half_width,
501                ellipsoid,
502            )
503        } else {
504            Self::from_lat_lon_azi_arc_length_half_width(
505                a, azimuth, arc_length, half_width, ellipsoid,
506            )
507        }
508    }
509
510    /// Accessor for the start parametric latitude on the auxiliary sphere.
511    #[must_use]
512    pub const fn beta(&self) -> Angle {
513        self.beta
514    }
515
516    /// Accessor for the start longitude.
517    #[must_use]
518    pub const fn lon(&self) -> Angle {
519        self.lon
520    }
521
522    /// Accessor for the start azimuth.
523    #[must_use]
524    pub const fn azi(&self) -> Angle {
525        self.azi
526    }
527
528    /// Set the `arc_length` of a `GeodesicSegment`
529    /// * `arc_length` - the great circle arc length of the `GeodesicSegment`.
530    pub const fn set_arc_length(&mut self, arc_length: Radians) -> &mut Self {
531        self.arc_length = arc_length;
532        self
533    }
534
535    /// Accessor for the arc length on the auxiliary sphere in radians.
536    #[must_use]
537    pub const fn arc_length(&self) -> Radians {
538        self.arc_length
539    }
540
541    /// Method to set the half width in metres.
542    /// Set the `half_width` of a `GeodesicSegment`
543    /// * `half_width` - the half width of the `GeodesicSegment`.
544    pub const fn set_half_width(&mut self, half_width: Metres) -> &mut Self {
545        self.half_width = half_width;
546        self
547    }
548
549    /// Accessor for the half width in metres.
550    #[must_use]
551    pub const fn half_width(&self) -> Metres {
552        self.half_width
553    }
554
555    /// Accessor for the reference to the underlying `Ellipsoid`.
556    #[must_use]
557    pub const fn ellipsoid(&self) -> &Ellipsoid {
558        self.ellipsoid
559    }
560
561    /// Accessor for the start point on the unit sphere.
562    #[must_use]
563    pub fn a(&self) -> Vector3d {
564        unit_sphere::vector::to_point(self.beta, self.lon)
565    }
566
567    /// Convert a distance in metres on the ellipsoid to radians on the
568    /// auxiliary sphere.
569    /// * `distance` - the distance along the `GeodesicSegment` in metres.
570    ///
571    /// returns the distance along the great circle arc in radians.
572    #[must_use]
573    pub fn metres_to_radians(&self, distance: Metres) -> Radians {
574        if distance.0.abs() < great_circle::MIN_VALUE {
575            Radians(0.0)
576        } else {
577            let a1 = ellipsoid::coefficients::evaluate_a1(self.eps) + 1.0;
578            let tau12 = Radians(distance.0 / (self.ellipsoid.b().0 * a1));
579            let c1 = ellipsoid::coefficients::evaluate_coeffs_c1(self.eps);
580            let b11 = ellipsoid::coefficients::sin_cos_series(&c1, self.sigma1);
581            let tau_sum = Angle::from(b11 + tau12);
582            let c1p = ellipsoid::coefficients::evaluate_coeffs_c1p(self.eps);
583            let b12 = ellipsoid::coefficients::sin_cos_series(&c1p, self.sigma1 + tau_sum);
584
585            tau12 + b12 + b11
586        }
587    }
588
589    /// Accessor for the length of the `GeodesicSegment` in metres.
590    #[must_use]
591    pub fn length(&self) -> Metres {
592        geodesic::convert_radians_to_metres(self.beta, self.azi, self.arc_length, self.ellipsoid)
593    }
594
595    /// Calculate the parametric latitude at the great circle length.
596    /// * `sigma` - the arc distance on the auxiliary sphere as an Angle.
597    ///
598    /// return the parametric latitude of the position at sigma.
599    #[must_use]
600    pub fn arc_beta(&self, sigma: Angle) -> Angle {
601        great_circle::calculate_latitude(self.beta, self.azi, sigma)
602    }
603
604    /// Calculate the geodetic latitude at the great circle arc distance.
605    /// * `arc_distance` - the great circle arc distance on the auxiliary sphere.
606    ///
607    /// return the geodetic latitude of the position at `arc_distance`.
608    #[must_use]
609    pub fn arc_latitude(&self, arc_distance: Radians) -> Angle {
610        let sigma = Angle::from(arc_distance);
611        self.ellipsoid
612            .calculate_geodetic_latitude(self.arc_beta(sigma))
613    }
614
615    /// Calculate the geodetic latitude at the length along the geodesic.
616    /// * `distance` - the distance along the `GeodesicSegment`, in metres.
617    ///
618    /// return the geodetic latitude of the position at distance.
619    #[must_use]
620    pub fn latitude(&self, distance: Metres) -> Angle {
621        let arc_distance = self.metres_to_radians(distance);
622        self.arc_latitude(arc_distance)
623    }
624
625    /// Calculate the azimuth at the great circle length.
626    /// * `sigma` - the arc distance on the auxiliary sphere as an Angle.
627    ///
628    /// return the azimuth at `sigma`.
629    #[must_use]
630    pub fn arc_azimuth(&self, sigma: Angle) -> Angle {
631        const MAX_LAT: f64 = 1.0 - great_circle::MIN_VALUE;
632
633        let sigma_sum = self.sigma1 + sigma;
634        let sin_beta = self.azi0.cos().0 * sigma_sum.sin().0;
635
636        // if at North pole, only valid azimuth is due South
637        if MAX_LAT < sin_beta {
638            Angle::new(trig::UnitNegRange(0.0), trig::UnitNegRange(-1.0))
639        } else {
640            Angle::from_y_x(self.azi0.sin().0, self.azi0.cos().0 * sigma_sum.cos().0)
641        }
642    }
643
644    /// Calculate the azimuth at the length along the geodesic.
645    /// * `distance` - the distance along the `GeodesicSegment`, in metres.
646    ///
647    /// return the azimuth of the geodesic/great circle at length.
648    #[must_use]
649    pub fn azimuth(&self, distance: Metres) -> Angle {
650        let sigma = Angle::from(self.metres_to_radians(distance));
651        self.arc_azimuth(sigma)
652    }
653
654    /// Calculate the geodesic longitude difference at arc distance
655    /// along the auxiliary sphere.
656    /// * `arc_distance` - the great circle arc distance on the auxiliary sphere.
657    /// * `sigma` - the arc distance as an Angle.
658    ///
659    /// return the longitude difference from the start point.
660    #[must_use]
661    pub fn delta_longitude(&self, arc_distance: Radians, sigma: Angle) -> Angle {
662        if arc_distance.abs().0 < great_circle::MIN_VALUE {
663            Angle::default()
664        } else {
665            // The great circle distance from Northward Equator crossing.
666            let sigma_sum = self.sigma1 + sigma;
667
668            // The longitude difference on the auxiliary sphere, omega12.
669            let omega12 = Angle::from_y_x(self.azi0.sin().0 * sigma_sum.sin().0, sigma_sum.cos().0)
670                - Angle::from_y_x(
671                    self.azi0.sin().0 * self.beta.sin().0,
672                    self.beta.cos().0 * self.azi.cos().0,
673                );
674
675            let c3 = self.ellipsoid.calculate_c3y(self.eps);
676            let b32 = ellipsoid::coefficients::sin_cos_series(&c3, sigma_sum);
677
678            omega12 - Angle::from(Radians(self.a3c * (arc_distance.0 + (b32.0 - self.b31.0))))
679        }
680    }
681
682    /// Calculate the geodesic longitude at the great circle length along
683    /// the auxiliary sphere.
684    /// * `arc_distance` - the great circle arc distance on the auxiliary sphere.
685    ///
686    /// return the longitude of the geodesic at `arc_distance`.
687    #[must_use]
688    pub fn arc_longitude(&self, arc_distance: Radians) -> Angle {
689        let sigma = Angle::from(arc_distance);
690        self.lon + self.delta_longitude(arc_distance, sigma)
691    }
692
693    /// Calculate the geodesic longitude at the distance along the geodesic.
694    /// * `distance` - the distance along the `GeodesicSegment`, in metres.
695    ///
696    /// return the longitude of the geodesic at distance.
697    #[must_use]
698    pub fn longitude(&self, distance: Metres) -> Angle {
699        self.arc_longitude(self.metres_to_radians(distance))
700    }
701
702    /// Calculate the parametric latitude and longitude at the arc distance.
703    ///
704    /// * `arc_distance` - the arc distance on the auxiliary sphere in Radians.
705    ///
706    /// return the parametric latitude and longitude at `arc_distance`.
707    #[must_use]
708    pub fn arc_beta_long(&self, arc_distance: Radians) -> (Angle, Angle) {
709        let sigma = Angle::from(arc_distance);
710        (
711            self.arc_beta(sigma),
712            self.lon + self.delta_longitude(arc_distance, sigma),
713        )
714    }
715
716    /// Calculate the geodesic `LatLong` at the arc distance along
717    /// the auxiliary sphere.
718    /// * `arc_distance` - the great circle arc distance on the auxiliary sphere.
719    ///
720    /// return the `LatLong` of the geodesic position at `arc_distance`.
721    #[must_use]
722    pub fn arc_lat_long(&self, arc_distance: Radians) -> LatLong {
723        let (beta, lon) = self.arc_beta_long(arc_distance);
724        LatLong::new(
725            angle_sc::Degrees::from(self.ellipsoid.calculate_geodetic_latitude(beta)),
726            angle_sc::Degrees::from(lon),
727        )
728    }
729
730    /// Calculate the geodesic `LatLong` at the distance along the `GeodesicSegment`.
731    /// * `distance` - the distance in `Metres`.
732    ///
733    /// return the `LatLong` of the geodesic position at `distance`.
734    #[must_use]
735    pub fn lat_long(&self, distance: Metres) -> LatLong {
736        let arc_distance = self.metres_to_radians(distance);
737        self.arc_lat_long(arc_distance)
738    }
739
740    /// Calculate the parametric latitude, longitude and azimuth at the arc distance.
741    ///
742    /// * `arc_distance` - the arc distance on the auxiliary sphere in Radians.
743    ///
744    /// return the parametric latitude, longitude and azimuth at `arc_distance`.
745    #[must_use]
746    pub fn arc_angles(&self, arc_distance: Radians) -> (Angle, Angle, Angle) {
747        let sigma = Angle::from(arc_distance);
748        let beta: Angle = self.arc_beta(sigma);
749        let lon = self.lon + self.delta_longitude(arc_distance, sigma);
750        let azimuth = self.arc_azimuth(sigma);
751
752        (beta, lon, azimuth)
753    }
754
755    /// Calculate the vector on the auxiliary sphere at `arc_distance` in `Radians`.
756    /// * `arc_distance` the great circle distance on the auxiliary sphere
757    ///
758    /// returns the point on the auxiliary sphere at `arc_distance`.
759    #[must_use]
760    pub fn arc_point(&self, arc_distance: Radians) -> Vector3d {
761        if arc_distance.abs().0 < great_circle::MIN_VALUE {
762            unit_sphere::vector::to_point(self.beta, self.lon)
763        } else {
764            let (beta, lon) = self.arc_beta_long(arc_distance);
765            unit_sphere::vector::to_point(beta, lon)
766        }
767    }
768
769    /// Calculate the vector on the auxiliary sphere at the mid point of the `GeodesicSegment`.
770    ///
771    /// returns the mid point vector of the `GeodesicSegment`.
772    #[must_use]
773    pub fn mid_point(&self) -> Vector3d {
774        self.arc_point(self.metres_to_radians(self.length().half()))
775    }
776
777    /// Calculate the geodesic and pole at the great circle arc distance.
778    /// * `arc_distance` the great circle arc distance on the auxiliary sphere
779    ///
780    /// returns the point and pole on the auxiliary sphere at `arc_distance`.
781    #[must_use]
782    pub fn arc_pole(&self, arc_distance: Radians) -> Vector3d {
783        // if point is on a meridional GeodesicSegment use auxiliary sphere point and pole
784        if self.azi0.sin().abs().0 < great_circle::MIN_VALUE {
785            unit_sphere::vector::calculate_pole(self.beta, self.lon, self.azi)
786        } else {
787            let (beta, lon, azimuth) = self.arc_angles(arc_distance);
788            unit_sphere::vector::calculate_pole(beta, lon, azimuth)
789        }
790    }
791
792    /// Calculate the geodesic point and pole at the arc distance along the great circle.
793    /// * `arc_distance` the great circle arc distance on the auxiliary sphere
794    ///
795    /// returns the point and pole on the auxiliary sphere at `arc_distance`.
796    #[must_use]
797    pub fn arc_point_and_pole(&self, arc_distance: Radians) -> (Vector3d, Vector3d) {
798        let (beta, lon, azimuth) = self.arc_angles(arc_distance);
799
800        // if point is on a meridional GeodesicSegment use auxiliary sphere point and pole
801        let pole = if self.azi0.sin().abs().0 < great_circle::MIN_VALUE {
802            unit_sphere::vector::calculate_pole(self.beta, self.lon, self.azi)
803        } else {
804            unit_sphere::vector::calculate_pole(beta, lon, azimuth)
805        };
806
807        (unit_sphere::vector::to_point(beta, lon), pole)
808    }
809
810    /// The reverse `GeodesicSegment` from end to start.
811    ///
812    /// returns the reverse `GeodesicSegment` from end to start.
813    #[must_use]
814    pub fn reverse(&self) -> GeodesicSegment<'_> {
815        let sigma = Angle::from(self.arc_length);
816        let mut segment = GeodesicSegment::new(
817            self.arc_beta(sigma),
818            self.lon + self.delta_longitude(self.arc_length, sigma),
819            self.arc_azimuth(sigma).opposite(),
820            self.arc_length,
821            self.half_width,
822            self.ellipsoid,
823        );
824        segment.set_half_width(self.half_width);
825        segment.clone()
826    }
827
828    /// Calculate along and across track distances to a position from a geodesic segment.
829    /// * `beta` the latitude of the position
830    /// * `lon` the longitude of the position
831    /// * `precision` the required precision
832    ///
833    /// returns the along and across track distances to the position in `Radians`.
834    #[allow(clippy::similar_names)]
835    #[must_use]
836    pub fn calculate_sphere_atd_and_xtd(
837        &self,
838        beta: Angle,
839        lon: Angle,
840        precision: Radians,
841    ) -> (Radians, Radians, u32) {
842        const MAX_ITERATIONS: u32 = 10;
843
844        // calculate the position as a point on the unit sphere
845        let point = unit_sphere::vector::to_point(beta, lon);
846
847        // calculate the start point and pole of the geodesic on the unit sphere
848        let (a, pole) = self.arc_point_and_pole(Radians(0.0));
849        let gc_d =
850            unit_sphere::great_circle::e2gc_distance(unit_sphere::vector::distance(&a, &point));
851
852        // if the point is close to the start point of the GeodesicSegment
853        if gc_d < precision {
854            (Radians(0.0), Radians(0.0), 0)
855        } else {
856            // estimate initial along track distance on the unit sphere
857            let (mut atd, mut xtd) = unit_sphere::vector::calculate_atd_and_xtd(&a, &pole, &point);
858            let mut iterations = 1;
859            while iterations < MAX_ITERATIONS {
860                // calculate the position and azimuth at atd along the GeodesicSegment
861                let (beta_x, lon_x, azi_x) = self.arc_angles(atd);
862
863                // calculate the geodesic azimuth and length to the point from the GeodesicSegment position at atd
864                let (azi_p, length, _, _) = geodesic::aux_sphere_azimuths_length(
865                    beta_x,
866                    beta,
867                    lon - lon_x,
868                    Radians(great_circle::MIN_VALUE),
869                    self.ellipsoid,
870                );
871                let delta_azi = azi_x - azi_p;
872                let delta_atd = trig::spherical_cosine_rule(delta_azi.cos(), length);
873                atd += delta_atd;
874                xtd = length;
875
876                if delta_atd.abs().0 < precision.0 {
877                    break;
878                }
879
880                iterations += 1;
881            }
882            // get the cross track distance (and sign) at the along track distance
883            xtd = if xtd < precision {
884                Radians(0.0)
885            } else {
886                let pole = self.arc_pole(atd);
887                let sign = pole.dot(&point);
888                Radians(xtd.0.copysign(sign))
889            };
890            (atd, xtd, iterations)
891        }
892    }
893
894    /// Calculate the shortest geodesic distance of point from the `GeodesicSegment`.
895    ///
896    /// * `beta` - the parametric latitude of the point.
897    /// * `lon` - the longitude of the point.
898    /// * `precision` the required precision in `Radians`
899    ///
900    /// returns the shortest distance of the point from the `GeodesicSegment` in Metres.
901    #[allow(clippy::similar_names)]
902    #[must_use]
903    pub fn calculate_sphere_shortest_distance(
904        &self,
905        beta: Angle,
906        lon: Angle,
907        precision: Radians,
908    ) -> Metres {
909        let (atd, xtd, _) = self.calculate_sphere_atd_and_xtd(beta, lon, precision);
910
911        // if the position is beside the geodesic segment
912        if (-precision <= atd) && (atd <= self.arc_length + precision) {
913            if xtd.abs() < precision {
914                Metres(0.0)
915            } else {
916                // convert cross track distance to Metres
917                let (beta_x, _lon, azi) = self.arc_angles(atd);
918                let alpha = azi.quarter_turn_ccw();
919                let distance =
920                    geodesic::convert_radians_to_metres(beta_x, alpha, xtd, self.ellipsoid);
921                // return the abs cross track distance in Metres
922                Metres(distance.0.abs())
923            }
924        } else {
925            // adjust atd to measure the distance from the centre of the Arc to the point
926            let atd_centre = atd - self.arc_length.half();
927            if atd_centre.0.is_sign_negative() {
928                // calculate the geodesic distance from the start of the segment
929                let delta_long = lon - self.lon;
930                let (alpha, distance, _, _) = geodesic::aux_sphere_azimuths_length(
931                    self.beta,
932                    beta,
933                    delta_long,
934                    precision,
935                    self.ellipsoid,
936                );
937                geodesic::convert_radians_to_metres(self.beta, alpha, distance, self.ellipsoid)
938            } else {
939                // calculate the geodesic distance from the end of the segment
940                let (arc_beta, arc_lon, _azi) = self.arc_angles(self.arc_length());
941                let delta_long = lon - arc_lon;
942                let (alpha, distance, _, _) = geodesic::aux_sphere_azimuths_length(
943                    arc_beta,
944                    beta,
945                    delta_long,
946                    precision,
947                    self.ellipsoid,
948                );
949                geodesic::convert_radians_to_metres(arc_beta, alpha, distance, self.ellipsoid)
950            }
951        }
952    }
953
954    /// Calculate along and across track distances to a position from a geodesic.
955    /// * `position` the position as a `LatLong`
956    /// * `precision` the required precision
957    ///
958    /// returns the along and across track distances to the position in `Metres`.
959    /// # Examples
960    /// ```
961    /// use icao_wgs84::*;
962    /// use angle_sc::is_within_tolerance;
963    /// use unit_sphere::great_circle;
964    ///
965    /// let tolerance = Radians(great_circle::MIN_VALUE);
966    ///
967    /// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
968    /// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
969    /// let g_0 = GeodesicSegment::between_positions(&istanbul, &washington, Metres(0.0), tolerance, &WGS84_ELLIPSOID);
970    ///
971    /// let azimuth_degrees = Degrees::from(g_0.azimuth(Metres(0.0)));
972    /// println!("Istanbul-Washington initial azimuth: {:?}", azimuth_degrees.0);
973    ///
974    /// let distance_nm = NauticalMiles::from(g_0.length());
975    /// println!("Istanbul-Washington distance: {:?}", distance_nm);
976    ///
977    /// let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
978    ///
979    /// // Calculate geodesic along track and across track distances to 1mm precision.
980    /// let (atd, xtd, iterations) = g_0.calculate_atd_and_xtd(&reyjavik, Metres(1e-3));
981    /// assert!(is_within_tolerance(3928788.572, atd.0, 1e-3));
982    /// assert!(is_within_tolerance(-1010585.9988368, xtd.0, 1e-3));
983    /// println!("calculate_atd_and_xtd iterations: {:?}", iterations);
984    ///
985    /// // The expected latitude and longitude are from:
986    /// // <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#8a93>
987    /// let position = g_0.lat_long(atd);
988    /// assert!(is_within_tolerance(
989    ///     54.92853149711691,
990    ///     Degrees::from(position.lat()).0,
991    ///     128.0 * f64::EPSILON
992    /// ));
993    /// assert!(is_within_tolerance(
994    ///     -21.93729106604878,
995    ///     Degrees::from(position.lon()).0,
996    ///     2048.0 * f64::EPSILON
997    /// ));
998    /// ```
999    #[allow(clippy::similar_names)]
1000    #[must_use]
1001    pub fn calculate_atd_and_xtd(
1002        &self,
1003        position: &LatLong,
1004        precision: Metres,
1005    ) -> (Metres, Metres, u32) {
1006        // convert precision to Radians
1007        let precision = Radians(precision.0 / self.ellipsoid.a().0);
1008
1009        // calculate the parametric latitude and longitude of the position
1010        let beta = self
1011            .ellipsoid
1012            .calculate_parametric_latitude(Angle::from(position.lat()));
1013        let lon = Angle::from(position.lon());
1014
1015        let (atd, xtd, iterations) = self.calculate_sphere_atd_and_xtd(beta, lon, precision);
1016
1017        // calculate the parametric latitude and azimuth at the abeam point
1018        let atd_angle = Angle::from(atd);
1019        let beta = self.arc_beta(atd_angle);
1020        let alpha = self.arc_azimuth(atd_angle).quarter_turn_ccw();
1021        (
1022            geodesic::convert_radians_to_metres(self.beta, self.azi, atd, self.ellipsoid),
1023            geodesic::convert_radians_to_metres(beta, alpha, xtd, self.ellipsoid),
1024            iterations,
1025        )
1026    }
1027
1028    /// Calculate the shortest geodesic distance of point from the `GeodesicSegment`.
1029    ///
1030    /// * `position` - the point.
1031    /// * `precision` the required precision in `Metres`
1032    ///
1033    /// returns the shortest distance of the point from the `GeodesicSegment` in Metres.
1034    #[allow(clippy::similar_names)]
1035    #[must_use]
1036    pub fn shortest_distance(&self, position: &LatLong, precision: Metres) -> Metres {
1037        // calculate the parametric latitude and longitude of the position
1038        let beta = self
1039            .ellipsoid
1040            .calculate_parametric_latitude(Angle::from(position.lat()));
1041        let lon = Angle::from(position.lon());
1042        // convert precision to Radians
1043        let precision = Radians(precision.0 / self.ellipsoid.a().0);
1044        self.calculate_sphere_shortest_distance(beta, lon, precision)
1045    }
1046}
1047
1048impl From<(&LatLong, Angle, Radians)> for GeodesicSegment<'_> {
1049    /// Construct a `GeodesicSegment` on the WGS-84  `Ellipsoid` using the "direct"
1050    /// method with the length in `Radians`.
1051    /// @pre |lat| <= 90.0 degrees.
1052    /// * `a` - the start position in geodetic coordinates.
1053    /// * `azimuth` - the azimuth at the start position.
1054    /// * `arc_length` - the great circle arc length on the auxiliary sphere in radians.
1055    fn from(params: (&LatLong, Angle, Radians)) -> Self {
1056        GeodesicSegment::from_lat_lon_azi_arc_length(params.0, params.1, params.2, &WGS84_ELLIPSOID)
1057    }
1058}
1059
1060impl From<(&LatLong, Angle, Metres)> for GeodesicSegment<'_> {
1061    /// Construct a `GeodesicSegment` on the WGS-84 `Ellipsoid` using the "direct"
1062    /// method with the length in metres.
1063    /// @pre |lat| <= 90.0 degrees.
1064    /// * `a` - the start position in geodetic coordinates.
1065    /// * `azimuth` - the azimuth at the start position.
1066    /// * `length` - the length on the `Ellipsoid` in metres.
1067    fn from(params: (&LatLong, Angle, Metres)) -> Self {
1068        GeodesicSegment::from_lat_lon_azi_length(params.0, params.1, params.2, &WGS84_ELLIPSOID)
1069    }
1070}
1071
1072impl From<(&LatLong, &LatLong)> for GeodesicSegment<'_> {
1073    /// Construct a `GeodesicSegment` between a pair of positions on the WGS-84
1074    /// `Ellipsoid`, the "indirect" method.
1075    /// @pre |lat| <= 90.0 degrees.
1076    /// * `a`, `b` - the start and finish positions in geodetic coordinates.
1077    fn from(params: (&LatLong, &LatLong)) -> Self {
1078        Self::between_positions(
1079            params.0,
1080            params.1,
1081            Metres(0.0),
1082            Radians(great_circle::MIN_VALUE),
1083            &WGS84_ELLIPSOID,
1084        )
1085    }
1086}
1087
1088/// Calculate the distances along a pair of `GeodesicSegment`s (in Radians)
1089/// to their closest intersection or reference points.
1090/// * `g_0`, `g_1` the Geodesic`GeodesicSegment`s.
1091/// * `precision` the precision in `Metres`
1092///
1093/// returns the distances along the `GeodesicSegment`s to the intersection
1094/// point or to their closest (reference) points if the `GeodesicSegment`s
1095/// do not intersect and the number of iterations required.
1096///
1097/// # Panics
1098///
1099/// The function will panic if the Geodesics are **not** on the same `Ellipsoid`.
1100#[must_use]
1101pub fn calculate_intersection_distances(
1102    g_0: &GeodesicSegment,
1103    g_1: &GeodesicSegment,
1104    precision: Metres,
1105) -> (Radians, Radians) {
1106    let precision = Radians(precision.0 / g_0.ellipsoid().a().0);
1107    let (distance1, distance2, _, _) =
1108        intersection::calculate_arc_reference_distances_and_angle(g_0, g_1, precision);
1109    (
1110        distance1 + g_0.arc_length().half(),
1111        distance2 + g_1.arc_length().half(),
1112    )
1113}
1114
1115/// Calculate the position (Latitude and Longitude) where a pair of `GeodesicSegment`s
1116/// intersect, or None if the `GeodesicSegment`s do not intersect.
1117/// * `g_0`, `g_1` the `GeodesicSegment`s.
1118/// * `precision` the precision in `Metres`
1119///
1120/// returns the distances along the `GeodesicSegment`s to the intersection point
1121/// or to their closest (reference) points if the `GeodesicSegment`s do not intersect.
1122///
1123/// # Panics
1124///
1125/// The function will panic if the `GeodesicSegment`s are **not** on the same `Ellipsoid`.
1126///
1127/// # Examples
1128/// ```
1129/// use icao_wgs84::*;
1130/// use angle_sc::is_within_tolerance;
1131///
1132/// let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1133/// let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1134/// let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
1135/// let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
1136///
1137/// let g_0 = GeodesicSegment::from((&istanbul, &washington));
1138/// let g_1 = GeodesicSegment::from((&reyjavik, &accra));
1139///
1140/// // Calculate the intersection point position
1141/// // The expected latitude and longitude are from:
1142/// // <https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#fe0a>
1143/// let result = calculate_intersection_point(&g_0, &g_1, Metres(1e-3));
1144/// let lat_lon = result.unwrap();
1145///
1146/// assert!(is_within_tolerance(54.7170296089477, lat_lon.lat().0, 1e-6));
1147/// assert!(is_within_tolerance(-14.56385574430775, lat_lon.lon().0, 1e-6));
1148/// ```
1149#[must_use]
1150pub fn calculate_intersection_point(
1151    g_0: &GeodesicSegment,
1152    g_1: &GeodesicSegment,
1153    precision: Metres,
1154) -> Option<LatLong> {
1155    let precision = Radians(precision.0 / g_0.ellipsoid().a().0);
1156    let (distance1, distance2, angle, _) =
1157        intersection::calculate_arc_reference_distances_and_angle(g_0, g_1, precision);
1158
1159    let segments_are_coincident = angle.sin().0 == 0.0;
1160    let segments_intersect_or_overlap = if segments_are_coincident {
1161        // do coincident segments overlap?
1162        distance1.abs() + distance2.abs()
1163            <= g_0.arc_length().half() + g_1.arc_length().half() + precision
1164    } else {
1165        // do geodesic paths intersect inside both segments
1166        (distance1.abs() <= g_0.arc_length().half() + precision)
1167            && distance2.abs() <= (g_1.arc_length().half() + precision)
1168    };
1169
1170    if segments_intersect_or_overlap {
1171        let distance = (distance1 + g_0.arc_length().half()).clamp(g_0.arc_length());
1172        Some(g_0.arc_lat_long(distance))
1173    } else {
1174        None
1175    }
1176}
1177
1178#[cfg(test)]
1179mod tests {
1180    use super::*;
1181    use angle_sc::is_within_tolerance;
1182    use core::mem::size_of;
1183    use unit_sphere::{LatLong, great_circle};
1184
1185    #[test]
1186    fn test_ellipsoid_wgs84() {
1187        let geoid = Ellipsoid::wgs84();
1188        assert_eq!(ellipsoid::wgs84::A, geoid.a());
1189        assert_eq!(ellipsoid::wgs84::F, geoid.f());
1190        assert_eq!(
1191            ellipsoid::calculate_minor_axis(ellipsoid::wgs84::A, ellipsoid::wgs84::F),
1192            geoid.b()
1193        );
1194        assert_eq!(1.0 - ellipsoid::wgs84::F, geoid.one_minus_f());
1195        assert_eq!(1.0 / (1.0 - ellipsoid::wgs84::F), geoid.recip_one_minus_f());
1196        assert_eq!(
1197            ellipsoid::calculate_sq_eccentricity(ellipsoid::wgs84::F),
1198            geoid.e_2()
1199        );
1200        assert_eq!(
1201            ellipsoid::calculate_sq_2nd_eccentricity(ellipsoid::wgs84::F),
1202            geoid.ep_2()
1203        );
1204        assert_eq!(
1205            ellipsoid::calculate_3rd_flattening(ellipsoid::wgs84::F),
1206            geoid.n()
1207        );
1208
1209        let point = geoid.to_arc_point(Angle::from(Degrees(45.0)), Angle::from(Degrees(45.0)));
1210        assert!(is_within_tolerance(
1211            44.903787849420226,
1212            Degrees::from(unit_sphere::vector::latitude(&point)).0,
1213            f64::EPSILON
1214        ));
1215        assert_eq!(
1216            45.0,
1217            Degrees::from(unit_sphere::vector::longitude(&point)).0
1218        );
1219    }
1220
1221    #[test]
1222    fn test_ellipsoid_traits() {
1223        let geoid = Ellipsoid::wgs84();
1224
1225        let geoid_clone = geoid.clone();
1226        assert!(geoid_clone == geoid);
1227
1228        println!("Ellipsoid: {:?}", geoid);
1229    }
1230
1231    #[test]
1232    fn test_calculate_azimuth_arc_length_normal_05() {
1233        // GeodTest.dat line 2874
1234        // 5.421025561218 0 84.846843174846
1235        // 3.027329237478900117 109.666857465735641205 96.826992198613537236
1236        // 12161089.9991805 109.607910081857488806 5988906.6319258056178 8449589948776.249238
1237
1238        // North East bound, straddle Equator
1239        let latlon1 = LatLong::new(Degrees(5.421025561218), Degrees(0.0));
1240        let latlon2 = LatLong::new(
1241            Degrees(3.027329237478900117),
1242            Degrees(109.666857465735641205),
1243        );
1244
1245        let result = calculate_azimuths_and_geodesic_length(
1246            &latlon1,
1247            &latlon2,
1248            Radians(great_circle::MIN_VALUE),
1249            &WGS84_ELLIPSOID,
1250        );
1251        assert_eq!(84.846843174846, Degrees::from(result.0).0);
1252        assert!(is_within_tolerance(12161089.9991805, (result.1).0, 1e-8));
1253        // assert_eq!(84.846843174846, Degrees::from(result.2).0);
1254    }
1255
1256    #[test]
1257    fn test_calculate_azimuth_arc_length_nearly_antipodal_1() {
1258        // GeodTest.dat line 100001
1259        // 8.226828747671 0 111.1269645725
1260        // -8.516119211674268968 178.688979582629224039 68.982798544955243193
1261        // 19886305.6710041 179.197987814300505446 97496.4436255989712 -29736790544759.340534
1262
1263        let latlon1 = LatLong::new(Degrees(8.226828747671), Degrees(0.0));
1264        let latlon2 = LatLong::new(
1265            Degrees(-8.516119211674268968),
1266            Degrees(178.688979582629224039),
1267        );
1268
1269        let result = calculate_azimuths_and_geodesic_length(
1270            &latlon1,
1271            &latlon2,
1272            Radians(great_circle::MIN_VALUE),
1273            &WGS84_ELLIPSOID,
1274        );
1275        assert!(is_within_tolerance(
1276            111.1269645725,
1277            Degrees::from(result.0).0,
1278            1e-9
1279        ));
1280        assert!(is_within_tolerance(19886305.6710041, (result.1).0, 1e-8));
1281    }
1282
1283    #[test]
1284    fn test_calculate_azimuth_arc_length_nearly_antipodal_2() {
1285        // GeodTest.dat line 100017
1286        // .322440123063 0 100.319048368176
1287        // -.367465171996537868 179.160624688175359763 79.682430612745621077
1288        // 19943611.6727803 179.749470297545372441 29954.0028615773743 -14555544282075.683105
1289
1290        let latlon1 = LatLong::new(Degrees(0.322440123063), Degrees(0.0));
1291        let latlon2 = LatLong::new(
1292            Degrees(-0.367465171996537868),
1293            Degrees(179.160624688175359763),
1294        );
1295
1296        let result = calculate_azimuths_and_geodesic_length(
1297            &latlon1,
1298            &latlon2,
1299            Radians(great_circle::MIN_VALUE),
1300            &WGS84_ELLIPSOID,
1301        );
1302        assert!(is_within_tolerance(
1303            100.319048368176,
1304            Degrees::from(result.0).0,
1305            1e-9
1306        ));
1307        assert!(is_within_tolerance(19943611.6727803, (result.1).0, 1e-8));
1308    }
1309
1310    #[test]
1311    fn test_calculate_azimuth_and_geodesic_length_karney() {
1312        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1313        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1314        let (azimuth, length, end_azimuth) = calculate_azimuths_and_geodesic_length(
1315            &istanbul,
1316            &washington,
1317            Radians(great_circle::MIN_VALUE),
1318            &WGS84_ELLIPSOID,
1319        );
1320
1321        let azimuth_degrees = Degrees::from(azimuth);
1322        assert_eq!(-50.69375304113997, azimuth_degrees.0);
1323        assert_eq!(8339863.136005359, length.0);
1324
1325        println!("Istanbul-Washington azimuth: {:?}", azimuth_degrees.0);
1326
1327        let distance_nm = NauticalMiles::from(length);
1328        println!("Istanbul-Washington distance: {:?}", distance_nm);
1329
1330        let azimuth_degrees = Degrees::from(end_azimuth.opposite());
1331        assert_eq!(47.735339288362425, azimuth_degrees.0);
1332        println!("Washington-Istanbul azimuth: {:?}", azimuth_degrees.0);
1333    }
1334
1335    #[test]
1336    fn test_geodesicarc_direct_constructors() {
1337        let length = Metres(9_000_000.0);
1338        let arc_length = Radians(core::f64::consts::FRAC_PI_2);
1339
1340        // Ensure that two Geodesics can fit on a cache line.
1341        assert_eq!(128, size_of::<GeodesicSegment>());
1342
1343        let a = LatLong::new(Degrees(45.0), Degrees(45.0));
1344
1345        // Increase azimuth around compass from due South to due North
1346        for i in -180..180 {
1347            let azi = i as f64;
1348            let azimuth = Angle::from(Degrees(azi));
1349
1350            let geodesic1 = GeodesicSegment::from((&a, azimuth, length));
1351            assert!(geodesic1.is_valid());
1352            assert_eq!(Metres(0.0), geodesic1.half_width());
1353            let azi0 = geodesic1.azimuth(Metres(0.0));
1354            assert!(is_within_tolerance(
1355                Radians::from(azimuth).0,
1356                Radians::from(azi0).0,
1357                2.0 * f64::EPSILON
1358            ));
1359
1360            let len0 = geodesic1.length();
1361            assert!(is_within_tolerance(length.0, len0.0, 1.0e-8));
1362
1363            let geodesic2 = GeodesicSegment::from((&a, azimuth, arc_length));
1364            assert!(geodesic2.is_valid());
1365            assert_eq!(Metres(0.0), geodesic2.half_width());
1366            let azi0 = geodesic2.azimuth(Metres(0.0));
1367            assert!(is_within_tolerance(
1368                Radians::from(azimuth).0,
1369                Radians::from(azi0).0,
1370                2.0 * f64::EPSILON
1371            ));
1372
1373            let len0 = geodesic2.arc_length();
1374            assert!(is_within_tolerance(arc_length.0, len0.0, 1.0e-8));
1375        }
1376    }
1377
1378    #[test]
1379    fn test_geodesicarc_between_positions() {
1380        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1381        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1382
1383        let tolerance = Radians(great_circle::MIN_VALUE);
1384
1385        let g_0 = GeodesicSegment::between_positions(
1386            &istanbul,
1387            &washington,
1388            Metres(0.0),
1389            tolerance,
1390            &WGS84_ELLIPSOID,
1391        );
1392        assert!(g_0.is_valid());
1393        assert_eq!(Metres(0.0), g_0.half_width());
1394
1395        let end_azimuth = Degrees::from(g_0.azimuth(g_0.length()));
1396        assert_eq!(-132.2646607116376, end_azimuth.0);
1397
1398        let mut g1_clone = g_0.clone();
1399        assert_eq!(g1_clone, g_0);
1400
1401        let g1_clone = g1_clone.set_arc_length(Radians(1.0));
1402        assert_eq!(Radians(1.0), g1_clone.arc_length());
1403        println!("GeodesicSegment: {:?}", &g1_clone);
1404
1405        let g1_clone = g1_clone.set_half_width(Metres(2.0));
1406        assert_eq!(Metres(2.0), g1_clone.half_width());
1407
1408        // test start position
1409        assert!(is_within_tolerance(
1410            42.0,
1411            Degrees::from(WGS84_ELLIPSOID.calculate_geodetic_latitude(g_0.beta())).0,
1412            32.0 * f64::EPSILON
1413        ));
1414        assert!(is_within_tolerance(
1415            29.0,
1416            Degrees::from(g_0.lon()).0,
1417            16.0 * f64::EPSILON
1418        ));
1419
1420        let lat_long: LatLong = g_0.lat_long(Metres(0.0));
1421        // assert_eq!(istanbul, lat_long);
1422        assert!(is_within_tolerance(
1423            istanbul.lat().0,
1424            lat_long.lat().0,
1425            64.0 * f64::EPSILON
1426        ));
1427        assert!(is_within_tolerance(
1428            istanbul.lon().0,
1429            lat_long.lon().0,
1430            32.0 * f64::EPSILON
1431        ));
1432
1433        // test start point
1434        let start_point = g_0.arc_point(Radians(0.0));
1435        assert!(is_within_tolerance(
1436            istanbul.lat().0,
1437            Degrees::from(
1438                g_0.ellipsoid()
1439                    .calculate_geodetic_latitude(unit_sphere::vector::latitude(&start_point))
1440            )
1441            .0,
1442            64.0 * f64::EPSILON
1443        ));
1444        assert!(is_within_tolerance(
1445            istanbul.lon().0,
1446            Degrees::from(unit_sphere::vector::longitude(&start_point)).0,
1447            16.0 * f64::EPSILON
1448        ));
1449
1450        let precision = Metres(1e-3);
1451        let (atd, xtd, iterations) = g_0.calculate_atd_and_xtd(&istanbul, precision);
1452        println!("calculate_atd_and_xtd iterations: {:?}", iterations);
1453        assert_eq!(0.0, atd.0);
1454        assert_eq!(0.0, xtd.0);
1455
1456        let distance = g_0.shortest_distance(&istanbul, precision);
1457        assert_eq!(0.0, distance.0);
1458
1459        // test end position
1460        let arc_length = g_0.arc_length();
1461        assert_eq!(1.309412846249522, arc_length.0);
1462
1463        let length = g_0.length();
1464        assert_eq!(8339863.136005359, length.0);
1465
1466        // let end_position = g_0.arc_lat_long(arc_length);
1467        assert!(is_within_tolerance(
1468            39.0,
1469            Degrees::from(g_0.latitude(length)).0,
1470            32.0 * f64::EPSILON
1471        ));
1472        assert!(is_within_tolerance(
1473            -77.0,
1474            Degrees::from(g_0.longitude(length)).0,
1475            256.0 * f64::EPSILON
1476        ));
1477
1478        // test mid position
1479        let half_length = length.half();
1480        let mid_position = g_0.lat_long(half_length);
1481
1482        assert!(is_within_tolerance(
1483            54.86379153725445,
1484            Degrees::from(mid_position.lat()).0,
1485            64.0 * f64::EPSILON
1486        ));
1487
1488        assert!(is_within_tolerance(
1489            -25.694568908316413,
1490            Degrees::from(mid_position.lon()).0,
1491            128.0 * f64::EPSILON
1492        ));
1493
1494        let mid_length = g_0.metres_to_radians(half_length);
1495        assert_eq!(0.654673165141749, mid_length.0);
1496        let mid_point = g_0.mid_point();
1497        let mid_beta = unit_sphere::vector::latitude(&mid_point);
1498        let mid_lat = g_0.ellipsoid().calculate_geodetic_latitude(mid_beta);
1499        assert!(is_within_tolerance(
1500            54.86379153725445,
1501            Degrees::from(mid_lat).0,
1502            64.0 * f64::EPSILON
1503        ));
1504
1505        let mid_lon = unit_sphere::vector::longitude(&mid_point);
1506        assert!(is_within_tolerance(
1507            -25.694568908316413,
1508            Degrees::from(mid_lon).0,
1509            128.0 * f64::EPSILON
1510        ));
1511
1512        let precision_r = Radians(1e-3 / WGS84_ELLIPSOID.a().0);
1513        let (atd, xtd, iterations) =
1514            g_0.calculate_sphere_atd_and_xtd(mid_beta, mid_lon, precision_r);
1515        assert!(is_within_tolerance(mid_length.0, atd.0, f64::EPSILON));
1516        assert_eq!(0.0, xtd.0);
1517        println!("calculate_sphere_atd_and_xtd iterations: {:?}", iterations);
1518
1519        let (atd, xtd, iterations) = g_0.calculate_atd_and_xtd(&mid_position, precision);
1520        assert!(is_within_tolerance(half_length.0, atd.0, 1e-3));
1521        assert!(xtd.0.abs() < 1e-3);
1522        println!("calculate_atd_and_xtd iterations: {:?}", iterations);
1523
1524        let distance = g_0.shortest_distance(&mid_position, precision);
1525        assert_eq!(0.0, distance.0);
1526
1527        let g_1 = g_0.reverse();
1528        assert_eq!(g_0.arc_length(), g_1.arc_length());
1529        assert_eq!(g_0.length(), g_1.length());
1530        assert_eq!(
1531            washington.lat().0,
1532            Degrees::from(g_1.arc_latitude(Radians::default())).0
1533        );
1534        assert_eq!(
1535            washington.lon().0,
1536            Degrees::from(g_1.arc_longitude(Radians(0.0))).0
1537        );
1538        assert!(is_within_tolerance(
1539            istanbul.lat().0,
1540            Degrees::from(g_1.arc_latitude(g_1.arc_length())).0,
1541            64.0 * f64::EPSILON
1542        ));
1543        assert!(is_within_tolerance(
1544            istanbul.lon().0,
1545            Degrees::from(g_1.arc_longitude(g_1.arc_length())).0,
1546            64.0 * f64::EPSILON
1547        ));
1548    }
1549
1550    #[test]
1551    fn test_meridonal_geodesicarc() {
1552        // A GeodesicSegment along the Greenwich meridian, over the North pole and down the IDL
1553        let a = LatLong::new(Degrees(45.0), Degrees(0.0));
1554        let b = LatLong::new(Degrees(45.0), Degrees(180.0));
1555        let g_0 = GeodesicSegment::from((&a, &b));
1556        let pole0 = g_0.arc_pole(Radians(0.0));
1557
1558        // Calculate the azimuth at the North pole
1559        let mid_length = g_0.arc_length().half();
1560        let azimuth = Degrees::from(g_0.arc_azimuth(Angle::from(mid_length)));
1561        assert_eq!(180.0, azimuth.0);
1562
1563        // Calculate the point and great circle pole at the North pole
1564        let (point1, pole1) = g_0.arc_point_and_pole(mid_length);
1565        assert_eq!(Vector3d::new(0.5 * f64::EPSILON, 0.0, 1.0), point1);
1566        assert_eq!(pole0, pole1);
1567    }
1568
1569    #[test]
1570    fn test_geodesicarc_90n_0n_0e() {
1571        let a = LatLong::new(Degrees(90.0), Degrees(0.0));
1572        let b = LatLong::new(Degrees(0.0), Degrees(0.0));
1573        let g_0 = GeodesicSegment::from((&a, &b));
1574
1575        assert!(is_within_tolerance(
1576            core::f64::consts::FRAC_PI_2,
1577            g_0.arc_length().0,
1578            f64::EPSILON
1579        ));
1580        assert_eq!(180.0, Degrees::from(g_0.azi()).0);
1581    }
1582
1583    #[test]
1584    fn test_geodesicarc_90s_0n_50e() {
1585        let a = LatLong::new(Degrees(-90.0), Degrees(0.0));
1586        let b = LatLong::new(Degrees(0.0), Degrees(50.0));
1587        let g_0 = GeodesicSegment::from((&a, &b));
1588
1589        assert!(is_within_tolerance(
1590            core::f64::consts::FRAC_PI_2,
1591            g_0.arc_length().0,
1592            f64::EPSILON
1593        ));
1594        assert_eq!(0.0, Degrees::from(g_0.azi()).0);
1595    }
1596
1597    #[test]
1598    fn test_calculate_atd_and_xtd() {
1599        // Karney's example
1600        // Istanbul, Washington and Reyjavik
1601        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1602        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1603        let g_0 = GeodesicSegment::from((&istanbul, &washington));
1604
1605        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
1606
1607        // Calculate geodesic along track and across track distances to 1mm precision.
1608        let precision = Metres(1e-3);
1609
1610        let (atd, xtd, iterations) = g_0.calculate_atd_and_xtd(&reyjavik, precision);
1611        assert!(is_within_tolerance(3928788.572, atd.0, precision.0));
1612        assert!(is_within_tolerance(-1010585.9988368, xtd.0, precision.0));
1613        println!("calculate_atd_and_xtd iterations: {:?}", iterations);
1614
1615        // Karney's latitude and longitude from Final result at:
1616        // https://sourceforge.net/p/geographiclib/discussion/1026621/thread/21aaff9f/#8a93
1617        let position = g_0.lat_long(atd);
1618        assert!(is_within_tolerance(
1619            54.92853149711691,
1620            Degrees::from(position.lat()).0,
1621            128.0 * f64::EPSILON
1622        ));
1623        assert!(is_within_tolerance(
1624            -21.93729106604878,
1625            Degrees::from(position.lon()).0,
1626            2048.0 * f64::EPSILON
1627        ));
1628
1629        // Test delta_azimuth at interception, should be PI/2
1630        let azimuth_1 = g_0.azimuth(atd);
1631        let g_1 = GeodesicSegment::from((&position, &reyjavik));
1632        let azimuth_2 = g_1.azi();
1633        let delta_azimuth = azimuth_2 - azimuth_1;
1634        assert!(is_within_tolerance(
1635            core::f64::consts::FRAC_PI_2,
1636            Radians::from(delta_azimuth).0,
1637            128.0 * f64::EPSILON
1638        ));
1639
1640        // opposite geodesic
1641        let g_0 = GeodesicSegment::from((&washington, &istanbul));
1642        let (atd, xtd, iterations) = g_0.calculate_atd_and_xtd(&reyjavik, precision);
1643        assert!(is_within_tolerance(
1644            g_0.length().0 - 3928788.572,
1645            atd.0,
1646            precision.0
1647        ));
1648        assert!(is_within_tolerance(1010585.9988368, xtd.0, 1e-3));
1649        println!("calculate_atd_and_xtd iterations: {:?}", iterations);
1650
1651        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
1652        let distance = g_0.shortest_distance(&reyjavik, precision);
1653        assert!(is_within_tolerance(
1654            1010585.998836817,
1655            distance.0,
1656            precision.0
1657        ));
1658
1659        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
1660        let distance = g_0.shortest_distance(&accra, precision);
1661        assert!(is_within_tolerance(
1662            4891211.398445355,
1663            distance.0,
1664            precision.0
1665        ));
1666
1667        let chicago = LatLong::new(Degrees(42.0), Degrees(-88.0));
1668        let distance = g_0.shortest_distance(&chicago, precision);
1669        assert!(is_within_tolerance(
1670            989277.1859906457,
1671            distance.0,
1672            precision.0
1673        ));
1674
1675        let singapore = LatLong::new(Degrees(1.0), Degrees(104.0));
1676        let distance = g_0.shortest_distance(&singapore, precision);
1677        assert!(is_within_tolerance(
1678            8699538.22763653,
1679            distance.0,
1680            precision.0
1681        ));
1682    }
1683
1684    #[test]
1685    fn test_intersection_point_distance() {
1686        // Karney's example
1687        // Istanbul, Washington, Reyjavik and Accra
1688        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1689        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1690        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
1691        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
1692
1693        let g_0 = GeodesicSegment::from((&istanbul, &washington));
1694        let g_1 = GeodesicSegment::from((&reyjavik, &accra));
1695
1696        let (d1, d2) = calculate_intersection_distances(&g_0, &g_1, Metres(1e-3));
1697        assert!(is_within_tolerance(0.5423772210673643, d1.0, 1e-6));
1698        assert!(is_within_tolerance(0.17504915893226164, d2.0, 1e-6));
1699
1700        let result = calculate_intersection_point(&g_0, &g_1, Metres(1e-3));
1701        let lat_lon = result.unwrap();
1702
1703        assert!(is_within_tolerance(54.7170296089477, lat_lon.lat().0, 1e-6));
1704        assert!(is_within_tolerance(
1705            -14.56385574430775,
1706            lat_lon.lon().0,
1707            1e-6
1708        ));
1709
1710        // Swap geodesics
1711        let result = calculate_intersection_point(&g_1, &g_0, Metres(1e-3));
1712        let lat_lon = result.unwrap();
1713
1714        assert!(is_within_tolerance(54.7170296089477, lat_lon.lat().0, 1e-6));
1715        assert!(is_within_tolerance(
1716            -14.56385574430775,
1717            lat_lon.lon().0,
1718            1e-6
1719        ));
1720    }
1721
1722    #[test]
1723    fn test_intersection_point_non_intersecting() {
1724        let istanbul = LatLong::new(Degrees(42.0), Degrees(29.0));
1725        let washington = LatLong::new(Degrees(39.0), Degrees(-77.0));
1726        let reyjavik = LatLong::new(Degrees(64.0), Degrees(-22.0));
1727        let accra = LatLong::new(Degrees(6.0), Degrees(0.0));
1728
1729        let g_0 = GeodesicSegment::from((&istanbul, &accra));
1730        let g_1 = GeodesicSegment::from((&reyjavik, &washington));
1731
1732        let result = calculate_intersection_point(&g_0, &g_1, Metres(1e-3));
1733        assert!(result.is_none());
1734    }
1735
1736    #[test]
1737    fn test_intersection_coincident_geodesic_paths() {
1738        let south_pole_1 = LatLong::new(Degrees(-88.0), Degrees(-180.0));
1739        let south_pole_2 = LatLong::new(Degrees(-87.0), Degrees(0.0));
1740
1741        let g_0 = GeodesicSegment::from((&south_pole_1, &south_pole_2));
1742
1743        let intersection_lengths = calculate_intersection_distances(&g_0, &g_0, Metres(1e-3));
1744        assert_eq!(g_0.arc_length().half(), intersection_lengths.0);
1745        assert_eq!(g_0.arc_length().half(), intersection_lengths.1);
1746
1747        let intersection_point = calculate_intersection_point(&g_0, &g_0, Metres(1e-3));
1748        let lat_lon = intersection_point.unwrap();
1749        assert!(is_within_tolerance(-89.5, lat_lon.lat().0, 1e-5));
1750        assert_eq!(0.0, lat_lon.lon().0);
1751
1752        let south_pole_3 = LatLong::new(Degrees(-85.0), Degrees(0.0));
1753        let south_pole_4 = LatLong::new(Degrees(-86.0), Degrees(0.0));
1754        let g_1 = GeodesicSegment::from((&south_pole_3, &south_pole_4));
1755        let intersection_point = calculate_intersection_point(&g_0, &g_1, Metres(1e-3));
1756        assert!(intersection_point.is_none());
1757    }
1758}