unit_sphere/vector/
intersection.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//! 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//! the mormalized centroid of the arc midpoints is used instead of the
38//! intersection point.
39//!
40//! Otherwise `closest_intersection_point` calls `use_antipodal_point`to determine
41//! which intersection point is closer to the
42//! [centroid](https://en.wikipedia.org/wiki/Centroid) of the `Arc`s midpoints.
43
44use super::{
45    MIN_SQ_NORM, Vector3d, calculate_great_circle_atd, normalise, normalise_centroid, sq_distance,
46};
47use angle_sc::{Angle, Radians};
48
49/// Calculate an intersection point between the poles of two Great Circles.
50/// See: <http://www.movable-type.co.uk/scripts/latlong-vectors.html#intersection>
51/// * `pole_0`, `pole_1` the poles.
52/// * `min_sq_value` the minimum square of a vector length to normalize.
53///
54/// return an intersection point or None if the poles represent coincident Great Circles.
55#[must_use]
56pub fn calculate_intersection(
57    pole_0: &Vector3d,
58    pole_1: &Vector3d,
59    min_sq_value: f64,
60) -> Option<Vector3d> {
61    normalise(&pole_0.cross(pole_1), min_sq_value)
62}
63
64/// Determine whether the antipodal point is closer to the centroid of the
65/// `Arc`s.
66///
67/// * `point` a great-circle intersection point.
68/// * `centroid` the centroid of the `Arc`s mid points.
69///
70/// returns true if the antipodal intersection is closer to the `centroid`
71/// of the `Arc`s otherwise returns false.
72#[must_use]
73pub fn use_antipodal_point(point: &Vector3d, centroid: &Vector3d) -> bool {
74    sq_distance(centroid, &(-*point)) < sq_distance(centroid, point)
75}
76
77/// Return the closer intersection point to the centroid of the `Arc`s.
78///
79/// * `point` a great-circle intersection point.
80/// * `centroid` the centroid of the `Arc`s mid points.
81///
82/// returns the antipodal point if it is closer to the `centroid`,
83/// otherwise returns the point.
84#[must_use]
85pub fn closest_intersection_point(point: &Vector3d, centroid: &Vector3d) -> Vector3d {
86    if use_antipodal_point(point, centroid) {
87        -*point
88    } else {
89        *point
90    }
91}
92
93/// Determine the reference point of a pair of arcs.
94/// I.e. the closest intersection point if they intersect or the
95/// centroid normalized to lie on the unit sphere if they don't.
96///
97/// * `mid_point_0`, `mid_point_1` the mid points of the `Arc`s.
98/// * `pole_0`, `pole_1` the poles of the `Arc` great circles.
99///
100/// returns the closest intersection point or normalized centroid and the
101/// sine of the angle between the arcs, zero if the arcs are coincident.
102/// And the absolute relative angle at the intersection point or centroid.
103#[must_use]
104pub fn calculate_reference_point_and_angle(
105    mid_point_0: &Vector3d,
106    pole_0: &Vector3d,
107    mid_point_1: &Vector3d,
108    pole_1: &Vector3d,
109) -> (Vector3d, Angle) {
110    let centroid = mid_point_0 + mid_point_1;
111    // calculate the intersection point between the great circles
112    let point = pole_0.cross(pole_1);
113    normalise(&point, MIN_SQ_NORM).map_or_else(
114        || {
115            // the great circles are coincident
116
117            let c = normalise_centroid(&centroid, mid_point_0, pole_0);
118
119            // determine whether the great circles oppose each other
120            let angle = if pole_0.dot(pole_1).is_sign_negative() {
121                Angle::default().opposite()
122            } else {
123                Angle::default()
124            };
125
126            (c, angle)
127        },
128        |p| {
129            // the great circles intersect
130
131            let x = closest_intersection_point(&p, &centroid);
132            (x, Angle::from_y_x(point.norm(), pole_0.dot(pole_1)))
133        },
134    )
135}
136
137/// Calculate signed great circle distances from two arc mid points to their
138/// closest intersection point or normalized centroid if the arcs are on coincident
139/// great circles.
140///
141/// * `mid_point_0`, `mid_point_1` the mid points of the arcs.
142/// * `pole_0`, `pole_1` the poles of the arc great circles.
143///
144/// returns the signed great circle distances of the closest intersection
145/// point or centroid  from the arc mid points in `Radians`,
146/// and the relative angle between the arc great circles.
147#[must_use]
148pub fn calculate_arc_reference_distances_and_angle(
149    mid_point_0: &Vector3d,
150    pole_0: &Vector3d,
151    mid_point_1: &Vector3d,
152    pole_1: &Vector3d,
153) -> (Radians, Radians, Angle) {
154    let (point, angle) =
155        calculate_reference_point_and_angle(mid_point_0, pole_0, mid_point_1, pole_1);
156    let distance_0 = calculate_great_circle_atd(mid_point_0, pole_0, &point);
157    let distance_1 = calculate_great_circle_atd(mid_point_1, pole_1, &point);
158
159    (distance_0, distance_1, angle)
160}
161
162#[cfg(test)]
163mod tests {
164    use std::f64;
165
166    use super::*;
167    use crate::{LatLong, vector};
168    use angle_sc::{Angle, Degrees, is_within_tolerance};
169
170    #[test]
171    fn test_calculate_intersection() {
172        let lat_lon_south = LatLong::new(Degrees(-90.0), Degrees(0.0));
173        let south_pole = Vector3d::from(&lat_lon_south);
174
175        let lat_lon_north = LatLong::new(Degrees(90.0), Degrees(0.0));
176        let north_pole = Vector3d::from(&lat_lon_north);
177
178        let lat_lon_idl = LatLong::new(Degrees(0.0), Degrees(180.0));
179        let idl = Vector3d::from(&lat_lon_idl);
180
181        let equator_intersection = calculate_intersection(&south_pole, &north_pole, MIN_SQ_NORM);
182        assert!(equator_intersection.is_none());
183
184        let gc_intersection1 = calculate_intersection(&idl, &north_pole, MIN_SQ_NORM).unwrap();
185        let gc_intersection2 = calculate_intersection(&idl, &south_pole, MIN_SQ_NORM).unwrap();
186
187        assert_eq!(gc_intersection1, -gc_intersection2);
188    }
189
190    #[test]
191    fn test_calculate_arc_reference_distances_and_angle_coincident_great_circles() {
192        let point_1 = Vector3d::new(1.0, 0.0, 0.0);
193        let pole_1 = Vector3d::new(0.0, 0.0, 1.0);
194
195        // same mid points and great circles
196        let result =
197            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_1, &pole_1);
198        assert_eq!(Radians(0.0), result.0);
199        assert_eq!(Radians(0.0), result.1);
200        assert_eq!(Degrees(0.0), Degrees::from(result.2));
201
202        // opposite mid points and same great circles
203        let point_m1 = -point_1;
204        let result =
205            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_m1, &pole_1);
206        assert!(is_within_tolerance(
207            -f64::consts::FRAC_PI_2,
208            result.0.0,
209            f64::EPSILON
210        ));
211        assert!(is_within_tolerance(
212            f64::consts::FRAC_PI_2,
213            result.1.0,
214            f64::EPSILON
215        ));
216        assert_eq!(Degrees(0.0), Degrees::from(result.2));
217
218        // opposite mid points and great circles
219        let pole_m1 = -pole_1;
220        let result =
221            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_m1, &pole_m1);
222        assert!(is_within_tolerance(
223            -f64::consts::FRAC_PI_2,
224            result.0.0,
225            f64::EPSILON
226        ));
227        assert!(is_within_tolerance(
228            -f64::consts::FRAC_PI_2,
229            result.1.0,
230            f64::EPSILON
231        ));
232        assert_eq!(Degrees(180.0), Degrees::from(result.2));
233    }
234
235    #[test]
236    fn test_calculate_arc_reference_distances_and_angle_intersecting_great_circles() {
237        let point_1 = Vector3d::new(1.0, 0.0, 0.0);
238        let pole_1 = Vector3d::new(0.0, 0.0, 1.0);
239        let pole_2 = Vector3d::new(0.0, 1.0, 0.0);
240
241        // intersection, same mid points
242        let result =
243            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_1, &pole_2);
244        assert_eq!(Radians(0.0), result.0);
245        assert_eq!(Radians(0.0), result.1);
246        assert_eq!(Degrees(90.0), Degrees::from(result.2));
247
248        // intersection, same mid points, acute angle
249        let pole_3 = (pole_1 + pole_2).normalize();
250        let result =
251            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_1, &pole_3);
252        assert_eq!(Radians(0.0), result.0);
253        assert_eq!(Radians(0.0), result.1);
254        assert_eq!(Degrees(45.0), Degrees::from(result.2));
255
256        // intersection, same mid points, obtuse angle
257        let pole_m3 = -pole_3;
258        let result =
259            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_1, &pole_m3);
260        assert_eq!(Radians(0.0), result.0);
261        assert_eq!(Radians(0.0), result.1);
262        assert_eq!(Degrees(135.0), Degrees::from(result.2));
263
264        // intersection, different mid points, acute angle
265        let point_2 = vector::position(
266            &point_1,
267            &vector::direction(&point_1, &pole_3),
268            Angle::default().quarter_turn_cw(),
269        );
270        let result =
271            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_2, &pole_3);
272        assert_eq!(Radians(0.0), result.0);
273        assert!(is_within_tolerance(
274            -f64::consts::FRAC_PI_2,
275            result.1.0,
276            f64::EPSILON
277        ));
278        assert_eq!(Degrees(45.0), Degrees::from(result.2));
279
280        // intersection, different mid points, obtuse angle
281        let result =
282            calculate_arc_reference_distances_and_angle(&point_1, &pole_1, &point_2, &pole_m3);
283        assert_eq!(Radians(0.0), result.0);
284        assert!(is_within_tolerance(
285            f64::consts::FRAC_PI_2,
286            result.1.0,
287            f64::EPSILON
288        ));
289        assert_eq!(Degrees(135.0), Degrees::from(result.2));
290    }
291}