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//! \
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(¢roid, 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, ¢roid);
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}