unit_sphere/vector/
intersection.rs

1// Copyright (c) 2024-2025 Ken Barker
2
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"),
5// to deal in the Software without restriction, including without limitation the
6// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7// sell copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21//! The `intersection` module contains functions for calculating great-circle
22//! intersections using vectors.
23//!
24//! A pair of great circles intersect at two points unless they are coincident.\
25//! For example, points `u` and `v` in *Figure1*.
26//!
27//! ![great circle path](https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/Illustration_of_great-circle_distance.svg/220px-Illustration_of_great-circle_distance.svg.png)\
28//! *Figure 1 A pair of intersecting great circles*
29//!
30//! A great circle intersection point can simply be calculated by normalizing
31//! the [cross product](https://en.wikipedia.org/wiki/Cross_product) of their
32//! pole vectors.\
33//! If the resulting vector is too small to normalize, then the great circles
34//! are coincident, in which case they effectively *intersect* everywhere.
35//!
36//! If a pair of `Arc`s are on coincident great circles,
37//! `calculate_coincident_arc_distances` calculates the distances between
38//! `Arc` ends, zero if the `Arc`s overlap.
39//!
40//! Otherwise `use_antipodal_point` determines which intersection point
41//! is closer to the [centroid](https://en.wikipedia.org/wiki/Centroid)
42//! of the `Arc`s midpoints.
43//! `calculate_intersection_distances` then calculates great-circle distances
44//! along the `Arc`s to the intersection point.
45
46use super::{
47    MIN_SQ_DISTANCE, MIN_SQ_NORM, Vector3d, calculate_great_circle_atd, normalise, sq_distance,
48};
49use angle_sc::{Radians, max};
50
51/// Calculate an intersection point between the poles of two Great Circles.
52/// See: <http://www.movable-type.co.uk/scripts/latlong-vectors.html#intersection>
53/// * `pole1`, `pole2` the poles.
54/// * `min_sq_value` the minimum square of a vector length to normalize.
55///
56/// return an intersection point or None if the poles represent coincident Great Circles.
57#[must_use]
58pub fn calculate_intersection(
59    pole1: &Vector3d,
60    pole2: &Vector3d,
61    min_sq_value: f64,
62) -> Option<Vector3d> {
63    normalise(&pole1.cross(pole2), min_sq_value)
64}
65
66/// Calculate the great circle distances to an intersection point from the
67/// start points of a pair of great circle arcs, on different great circles.
68///
69/// * `a1`, `a2` the start points of the great circle arcs
70/// * `pole1`, `pole2` the poles of the great circle arcs
71/// * `c` the intersection point
72///
73/// returns a pair of great circle distances along the arcs to the
74/// intersection point in `Radians`.
75#[must_use]
76pub fn calculate_intersection_distances(
77    a1: &Vector3d,
78    pole1: &Vector3d,
79    a2: &Vector3d,
80    pole2: &Vector3d,
81    c: &Vector3d,
82) -> (Radians, Radians) {
83    (
84        calculate_great_circle_atd(a1, pole1, c),
85        calculate_great_circle_atd(a2, pole2, c),
86    )
87}
88
89/// Whether an along track distance is within an `Arc` length including tolerance.
90///
91/// * `distance` - the along track distance from the start of the `Arc`.
92/// * `length` the length of the `Arc`.
93/// * `tolerance` the distance tolerance.
94///
95/// return true if the along track distance is within the length including tolerance,
96/// false otherwise.
97#[must_use]
98pub fn is_alongside(distance: Radians, length: Radians, tolerance: Radians) -> bool {
99    (-tolerance <= distance) && (distance <= length + tolerance)
100}
101
102/// Whether an intersection point is within an `Arc`.
103///
104/// * `distance` - the along track distance to the point from the start of the `Arc`.
105/// * `length` the length of the `Arc`.
106///
107/// return true if the intersection point is within the `Arc`, false otherwise.
108#[must_use]
109pub fn is_within(distance: f64, length: f64) -> bool {
110    (-f64::EPSILON <= distance) && (distance <= length + (f64::EPSILON * (1.0 + length)))
111}
112
113/// Calculate the great-circle distances along a pair of `Arc`s on coincident
114/// Great Circles to their closest (reference) points.
115///
116/// * `gc_d` the great-circle distance between the arc start points.
117/// * `reciprocal` whether the arcs are in reciprocal directions.
118/// * `arc1_length`, `arc2_length` the `Arc` lengths in `Radians`.
119///
120/// returns the distances along the first `Arc` and second `Arc` to their closest
121/// (reference) points in `Radians`.
122#[must_use]
123pub fn calculate_coincident_arc_distances(
124    gc_d: Radians,
125    reciprocal: bool,
126    arc1_length: Radians,
127    arc2_length: Radians,
128) -> (Radians, Radians) {
129    if reciprocal {
130        // if the arcs intersect
131        if is_alongside(
132            gc_d,
133            max(arc1_length, arc2_length),
134            Radians(4.0 * f64::EPSILON),
135        ) {
136            if gc_d <= arc2_length {
137                // The start of the first `Arc` is within the second `Arc`
138                (Radians(0.0), gc_d.clamp(arc2_length))
139            } else {
140                // The start of the second `Arc` is within the first `Arc`
141                (gc_d.clamp(arc1_length), Radians(0.0))
142            }
143        } else {
144            let abs_d = gc_d.abs();
145
146            // The distance between the `Arc` b ends
147            let b_d = abs_d.0 - arc1_length.0 - arc2_length.0;
148            // The distance between the `Arc` b ends around the Great Circle
149            let b_gc_d = if Radians(0.0) < gc_d {
150                b_d
151            } else {
152                core::f64::consts::TAU - b_d
153            };
154            if b_gc_d < abs_d.0 {
155                // The end of the second `Arc` is beyond the end of first `Arc`
156                (Radians(b_gc_d) + arc1_length, arc2_length)
157            } else {
158                // The start of the second `Arc` is before the start of first `Arc`
159                (-abs_d, Radians(0.0))
160            }
161        }
162    } else {
163        // The distance to the start of arc2 from the end of arc1
164        let b1a2 = if Radians(0.0) < gc_d {
165            gc_d.0 - arc1_length.0
166        } else {
167            core::f64::consts::TAU + gc_d.0 - arc1_length.0
168        };
169        // The distance to the start of arc1 from the end of arc2
170        let b2a1 = if Radians(0.0) < gc_d {
171            core::f64::consts::TAU - gc_d.0 - arc2_length.0
172        } else {
173            -gc_d.0 - arc2_length.0
174        };
175        if b2a1 < b1a2 {
176            // The start of the first arc is within the second arc
177            (Radians(0.0), Radians(b2a1 + arc2_length.0))
178        } else {
179            // The start of the second arc relative to the start of first arc.
180            (Radians(b1a2 + arc1_length.0), Radians(0.0))
181        }
182    }
183}
184
185/// Determine whether the antipodal point is closer to the centroid of the
186/// `Arc`s.
187///
188/// * `point` a great-circle intersection point.
189/// * `centroid` the centroid (geometric mean) of the `Arc`s mid points.
190///
191/// returns true if the antipodal intersection is closer to the `centroid`
192/// of the `Arc`s otherwise returns false.
193#[must_use]
194pub fn use_antipodal_point(point: &Vector3d, centroid: &Vector3d) -> bool {
195    sq_distance(centroid, &(-*point)) < sq_distance(centroid, point)
196}
197
198/// Calculate the great-circle distances along a pair of arcs to their
199/// closest intersection point or their coincident arc distances if the
200/// `Arc`s are on coincident Great Circles.
201///
202/// * `a1`, `a2` the `Arc` start points.
203/// * `pole1`, `pole1` the `Arc` poles.
204/// * `length1`, `length2` the `Arc` lengths.
205/// * `centroid` the centroid (geometric mean) of the `Arc`s mid points.
206///
207/// returns the distances along the first arc and second arc to the intersection
208/// point or to their coincident arc distances if the arcs do not intersect.
209#[must_use]
210pub fn calculate_intersection_point_distances(
211    a1: &Vector3d,
212    pole1: &Vector3d,
213    length1: Radians,
214    a2: &Vector3d,
215    pole2: &Vector3d,
216    length2: Radians,
217    centroid: &Vector3d,
218) -> (Radians, Radians) {
219    // Calculate the square of the Euclidean distance between the start points.
220    let sq_d = sq_distance(a1, a2);
221    if sq_d < MIN_SQ_DISTANCE {
222        (Radians(0.0), Radians(0.0))
223    } else {
224        calculate_intersection(pole1, pole2, MIN_SQ_NORM).map_or_else(
225            || {
226                calculate_coincident_arc_distances(
227                    calculate_great_circle_atd(a1, pole1, a2),
228                    pole1.dot(pole2) < 0.0,
229                    length1,
230                    length2,
231                )
232            },
233            |c| {
234                // Find the closest intersection point
235                let c = if use_antipodal_point(&c, centroid) {
236                    -c
237                } else {
238                    c
239                };
240                calculate_intersection_distances(a1, pole1, a2, pole2, &c)
241            },
242        )
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::{LatLong, vector};
250    use angle_sc::{Angle, Degrees, is_within_tolerance};
251
252    #[test]
253    fn test_calculate_intersection() {
254        let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(0.0));
255        let south_pole = Vector3d::from(&lat_lon_south);
256
257        let lat_lon_north = LatLong::new(Degrees(90.0), Degrees(0.0));
258        let north_pole = Vector3d::from(&lat_lon_north);
259
260        let lat_lon_idl = LatLong::new(Degrees(0.0), Degrees(180.0));
261        let idl = Vector3d::from(&lat_lon_idl);
262
263        let equator_intersection = calculate_intersection(&south_pole, &north_pole, MIN_SQ_NORM);
264        assert!(equator_intersection.is_none());
265
266        let gc_intersection1 = calculate_intersection(&idl, &north_pole, MIN_SQ_NORM).unwrap();
267        let gc_intersection2 = calculate_intersection(&idl, &south_pole, MIN_SQ_NORM).unwrap();
268
269        assert_eq!(gc_intersection1, -gc_intersection2);
270    }
271
272    #[test]
273    fn test_calculate_intersection_distances() {
274        let start1 = LatLong::new(Degrees(-1.0), Degrees(-1.0));
275        let a1 = Vector3d::from(&start1);
276        let azimuth1 = Angle::from(Degrees(45.0));
277        let pole1 = vector::calculate_pole(
278            Angle::from(start1.lat()),
279            Angle::from(start1.lon()),
280            azimuth1,
281        );
282
283        let start2 = LatLong::new(Degrees(1.0), Degrees(-1.0));
284        let a2 = Vector3d::from(&start2);
285        let azimuth2 = Angle::from(Degrees(135.0));
286        let pole2 = vector::calculate_pole(
287            Angle::from(start2.lat()),
288            Angle::from(start2.lon()),
289            azimuth2,
290        );
291
292        let c = calculate_intersection(&pole1, &pole2, MIN_SQ_NORM).unwrap();
293        let (c1, c2) = calculate_intersection_distances(&a1, &pole1, &a2, &pole2, &c);
294        assert!(is_within_tolerance(-3.1169124762478333, c1.0, f64::EPSILON));
295        assert!(is_within_tolerance(-3.1169124762478333, c2.0, f64::EPSILON));
296
297        // Calculate the centre of the arc start points
298        let centre_point = vector::normalise(&(a1 + a2), MIN_SQ_NORM).unwrap();
299        assert!(sq_distance(&c, &centre_point) > 2.0);
300
301        // opposite intersection point
302        let d = -c;
303        assert!(sq_distance(&d, &centre_point) <= 2.0);
304
305        let (d1, d2) = calculate_intersection_distances(&a1, &pole1, &a2, &pole2, &d);
306        assert!(is_within_tolerance(
307            0.024680177341956263,
308            d1.0,
309            f64::EPSILON
310        ));
311        assert!(is_within_tolerance(
312            0.024680177341956263,
313            d2.0,
314            f64::EPSILON
315        ));
316
317        // Same start points and intersection point
318        let (e1, e2) = calculate_intersection_distances(&a1, &pole1, &a1, &pole2, &a1);
319        assert_eq!(0.0, e1.0);
320        assert_eq!(0.0, e2.0);
321    }
322
323    #[test]
324    fn test_is_alongside() {
325        assert!(!is_alongside(
326            Radians(-5.0 * f64::EPSILON),
327            Radians(3.0),
328            Radians(4.0 * f64::EPSILON)
329        ));
330        assert!(is_alongside(
331            Radians(-4.0 * f64::EPSILON),
332            Radians(3.0),
333            Radians(4.0 * f64::EPSILON)
334        ));
335        assert!(is_alongside(
336            Radians(3.0 + 4.0 * f64::EPSILON),
337            Radians(3.0),
338            Radians(4.0 * f64::EPSILON)
339        ));
340        assert!(!is_alongside(
341            Radians(3.0 + 6.0 + f64::EPSILON),
342            Radians(3.0),
343            Radians(4.0 * f64::EPSILON)
344        ));
345    }
346
347    #[test]
348    fn test_is_within() {
349        assert!(!is_within(-2.0 * f64::EPSILON, 2.0));
350        assert!(is_within(-f64::EPSILON, 2.0));
351        assert!(is_within(2.0 * (1.0 + f64::EPSILON), 2.0));
352        assert!(!is_within(2.0 * (1.0 + 3.0 * f64::EPSILON), 2.0));
353    }
354
355    #[test]
356    fn test_calculate_coincident_arc_distances() {
357        let zero = Radians(0.0);
358        let length1 = Radians(0.25);
359        let length2 = Radians(0.75);
360
361        let result0 = calculate_coincident_arc_distances(length2, true, length2, length1);
362        assert_eq!(length2, result0.0);
363        assert_eq!(zero, result0.1);
364
365        let result1 = calculate_coincident_arc_distances(length2, true, length1, length2);
366        assert_eq!(zero, result1.0);
367        assert_eq!(length2, result1.1);
368
369        let result2 = calculate_coincident_arc_distances(Radians(1.0), true, length1, length2);
370        assert_eq!(length1, result2.0);
371        assert_eq!(length2, result2.1);
372
373        let result3 = calculate_coincident_arc_distances(Radians(1.5), true, length1, length2);
374        assert_eq!(length2, result3.0);
375        assert_eq!(length2, result3.1);
376
377        let result4 = calculate_coincident_arc_distances(Radians(-1.5), true, length1, length2);
378        assert_eq!(Radians(-1.5), result4.0);
379        assert_eq!(zero, result4.1);
380
381        let result5 = calculate_coincident_arc_distances(Radians(-1.0), false, length1, length2);
382        assert_eq!(zero, result5.0);
383        assert_eq!(Radians(1.0), result5.1);
384
385        let result6 = calculate_coincident_arc_distances(Radians(1.0), false, length1, length2);
386        assert_eq!(Radians(1.0), result6.0);
387        assert_eq!(zero, result6.1);
388
389        let result7 = calculate_coincident_arc_distances(-length2, false, length1, length2);
390        assert_eq!(zero, result7.0);
391        assert_eq!(length2, result7.1);
392
393        let result8 = calculate_coincident_arc_distances(length1, false, length1, length2);
394        assert_eq!(length1, result8.0);
395        assert_eq!(zero, result8.1);
396    }
397}