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//! \
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, ¢re_point) > 2.0);
300
301 // opposite intersection point
302 let d = -c;
303 assert!(sq_distance(&d, ¢re_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}