geometry_strategy/geographic/
geodesic_intersection.rs1use geometry_cs::Spheroid;
11
12use super::{KarneyDirect, KarneyInverse};
13
14#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct GeodesicIntersection {
17 pub longitude: f64,
19 pub latitude: f64,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub struct Gnomonic {
26 pub spheroid: Spheroid,
28}
29
30impl Gnomonic {
31 pub const WGS84: Self = Self {
33 spheroid: Spheroid::WGS84,
34 };
35
36 #[cfg(feature = "std")]
38 #[must_use]
39 pub fn forward(&self, center: [f64; 2], point: [f64; 2]) -> Option<[f64; 2]> {
40 let inverse = KarneyInverse {
41 spheroid: self.spheroid,
42 ..KarneyInverse::WGS84
43 }
44 .apply(center[0], center[1], point[0], point[1]);
45 if inverse.geodesic_scale <= 0.0 {
46 return None;
47 }
48 let rho = inverse.reduced_length / inverse.geodesic_scale;
49 Some([inverse.azimuth.sin() * rho, inverse.azimuth.cos() * rho])
50 }
51
52 #[cfg(feature = "std")]
54 #[must_use]
55 pub fn reverse(&self, center: [f64; 2], point: [f64; 2]) -> Option<[f64; 2]> {
56 let azimuth = point[0].atan2(point[1]);
57 let rho = point[0].hypot(point[1]);
58 let radius = self.spheroid.equatorial_radius;
59 let mut distance = radius * (rho / radius).atan();
60 let direct = KarneyDirect {
61 spheroid: self.spheroid,
62 };
63 let mut found = false;
64 for _ in 0..10 {
65 let result = direct.apply(center[0], center[1], distance, azimuth);
66 if result.geodesic_scale <= 0.0 {
67 return None;
68 }
69 let rho_error = result.reduced_length / result.geodesic_scale - rho;
70 let distance_update = rho_error * result.geodesic_scale * result.geodesic_scale;
71 distance -= distance_update;
72 if distance_update.abs() <= radius * f64::EPSILON {
73 found = true;
74 break;
75 }
76 }
77 found.then(|| {
78 let result = direct.apply(center[0], center[1], distance, azimuth);
79 [result.lon2, result.lat2]
80 })
81 }
82
83 #[cfg(feature = "std")]
85 #[must_use]
86 pub fn intersection(
87 &self,
88 first1: [f64; 2],
89 first2: [f64; 2],
90 second1: [f64; 2],
91 second2: [f64; 2],
92 ) -> Option<GeodesicIntersection> {
93 let seed = [
94 (first1[0] + first2[0] + second1[0] + second2[0]) / 4.0,
95 (first1[1] + first2[1] + second1[1] + second2[1]) / 4.0,
96 ];
97 self.intersection_from_seed(first1, first2, second1, second2, seed)
98 }
99
100 #[cfg(feature = "std")]
101 fn intersection_from_seed(
102 &self,
103 first1: [f64; 2],
104 first2: [f64; 2],
105 second1: [f64; 2],
106 second2: [f64; 2],
107 mut center: [f64; 2],
108 ) -> Option<GeodesicIntersection> {
109 for _ in 0..10 {
110 let projected_first1 = self.forward(center, first1)?;
111 let projected_first2 = self.forward(center, first2)?;
112 let projected_second1 = self.forward(center, second1)?;
113 let projected_second2 = self.forward(center, second2)?;
114 let projected = intersect_lines(
115 projected_first1,
116 projected_first2,
117 projected_second1,
118 projected_second2,
119 )?;
120 let next = self.reverse(center, projected)?;
121 if (next[0] - center[0]).abs() <= 1e-13 && (next[1] - center[1]).abs() <= 1e-13 {
122 center = next;
123 break;
124 }
125 center = next;
126 }
127 Some(GeodesicIntersection {
128 longitude: normalize_longitude(center[0]),
129 latitude: center[1],
130 })
131 }
132}
133
134impl Default for Gnomonic {
135 fn default() -> Self {
136 Self::WGS84
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq)]
143pub struct Sjoberg {
144 pub spheroid: Spheroid,
146}
147
148impl Sjoberg {
149 pub const WGS84: Self = Self {
151 spheroid: Spheroid::WGS84,
152 };
153
154 #[cfg(feature = "std")]
158 #[must_use]
159 pub fn intersection(
160 &self,
161 first1: [f64; 2],
162 first2: [f64; 2],
163 second1: [f64; 2],
164 second2: [f64; 2],
165 ) -> Option<GeodesicIntersection> {
166 let gnomonic = Gnomonic {
167 spheroid: self.spheroid,
168 };
169 let mean = [
170 (first1[0] + first2[0] + second1[0] + second2[0]) / 4.0,
171 (first1[1] + first2[1] + second1[1] + second2[1]) / 4.0,
172 ];
173 let seeds = [
174 mean,
175 midpoint(first1, first2),
176 midpoint(second1, second2),
177 first1,
178 first2,
179 second1,
180 second2,
181 ];
182 seeds.into_iter().find_map(|seed| {
183 gnomonic.intersection_from_seed(first1, first2, second1, second2, seed)
184 })
185 }
186}
187
188impl Default for Sjoberg {
189 fn default() -> Self {
190 Self::WGS84
191 }
192}
193
194#[cfg(feature = "std")]
195fn midpoint(first: [f64; 2], second: [f64; 2]) -> [f64; 2] {
196 [
197 first[0] + normalize_longitude(second[0] - first[0]) / 2.0,
198 f64::midpoint(first[1], second[1]),
199 ]
200}
201
202#[cfg(feature = "std")]
203fn intersect_lines(
204 first1: [f64; 2],
205 first2: [f64; 2],
206 second1: [f64; 2],
207 second2: [f64; 2],
208) -> Option<[f64; 2]> {
209 let line1 = cross3([first1[0], first1[1], 1.0], [first2[0], first2[1], 1.0]);
210 let line2 = cross3([second1[0], second1[1], 1.0], [second2[0], second2[1], 1.0]);
211 let point = cross3(line1, line2);
212 if point[2].abs() <= f64::EPSILON {
213 None
214 } else {
215 Some([point[0] / point[2], point[1] / point[2]])
216 }
217}
218
219fn cross3(first: [f64; 3], second: [f64; 3]) -> [f64; 3] {
220 [
221 first[1] * second[2] - first[2] * second[1],
222 first[2] * second[0] - first[0] * second[2],
223 first[0] * second[1] - first[1] * second[0],
224 ]
225}
226
227#[cfg(feature = "std")]
228fn normalize_longitude(longitude: f64) -> f64 {
229 let pi = core::f64::consts::PI;
230 (longitude + pi).rem_euclid(core::f64::consts::TAU) - pi
231}