rapidgeo_simplify/xt.rs
1//! Cross-track distance calculation implementations.
2//!
3//! This module provides different methods for calculating the perpendicular
4//! distance from a point to a line segment, which is the core operation
5//! in the Douglas-Peucker algorithm.
6//!
7//! The choice of distance calculation method affects both accuracy and performance:
8//!
9//! - [`XtGreatCircle`]: Most accurate for geographic data, works globally
10//! - [`XtEnu`]: Good balance of accuracy and performance for regional data
11//! - [`XtEuclid`]: Fastest, suitable for projected or non-geographic data
12
13use rapidgeo_distance::{geodesic, LngLat};
14
15/// Trait for calculating perpendicular distance from a point to a line segment.
16///
17/// This abstraction allows the Douglas-Peucker algorithm to work with different
18/// distance calculation methods without changing the core algorithm.
19pub trait PerpDistance {
20 /// Calculate the perpendicular distance from point `p` to line segment `a`-`b`.
21 ///
22 /// # Arguments
23 ///
24 /// * `a` - Start point of the line segment
25 /// * `b` - End point of the line segment
26 /// * `p` - Point to measure distance from
27 ///
28 /// # Returns
29 ///
30 /// Perpendicular distance in the implementation's units (usually meters)
31 fn d_perp_m(&self, a: LngLat, b: LngLat, p: LngLat) -> f64;
32}
33
34/// Great circle distance calculation using spherical geometry.
35///
36/// Uses the [great circle method](https://en.wikipedia.org/wiki/Great_circle)
37/// to calculate the shortest distance between a point and a line segment on
38/// the Earth's surface. This is the most accurate method for geographic data
39/// but requires more computation.
40///
41/// # Examples
42///
43/// ```rust
44/// use rapidgeo_distance::LngLat;
45/// use rapidgeo_simplify::xt::{PerpDistance, XtGreatCircle};
46///
47/// let backend = XtGreatCircle;
48/// let distance = backend.d_perp_m(
49/// LngLat::new_deg(-122.0, 37.0), // San Francisco area
50/// LngLat::new_deg(-121.0, 37.0), // Point east
51/// LngLat::new_deg(-121.5, 37.1), // Point slightly north of line
52/// );
53///
54/// // Distance should be roughly 11km (0.1 degree latitude difference)
55/// assert!((distance - 11100.0).abs() < 1000.0);
56/// ```
57pub struct XtGreatCircle;
58
59impl PerpDistance for XtGreatCircle {
60 fn d_perp_m(&self, a: LngLat, b: LngLat, p: LngLat) -> f64 {
61 geodesic::great_circle_point_to_seg(p, (a, b))
62 }
63}
64
65/// East-North-Up (ENU) planar projection distance calculation.
66///
67/// Projects coordinates to a local [East-North-Up coordinate system](https://en.wikipedia.org/wiki/Local_tangent_plane_coordinates)
68/// around the specified origin point, then calculates Euclidean distance.
69/// This provides better performance than great circle calculations while
70/// maintaining reasonable accuracy for regional datasets.
71///
72/// # Examples
73///
74/// ```rust
75/// use rapidgeo_distance::LngLat;
76/// use rapidgeo_simplify::xt::{PerpDistance, XtEnu};
77///
78/// let origin = LngLat::new_deg(-121.5, 37.0); // Midpoint
79/// let backend = XtEnu { origin };
80///
81/// let distance = backend.d_perp_m(
82/// LngLat::new_deg(-122.0, 37.0),
83/// LngLat::new_deg(-121.0, 37.0),
84/// LngLat::new_deg(-121.5, 37.1), // 0.1 degree north
85/// );
86///
87/// // Should be close to great circle result for this regional example
88/// assert!(distance > 10000.0 && distance < 12000.0);
89/// ```
90pub struct XtEnu {
91 /// Origin point for the ENU projection. Usually set to the midpoint
92 /// of the polyline being simplified for optimal accuracy.
93 pub origin: LngLat,
94}
95
96impl PerpDistance for XtEnu {
97 fn d_perp_m(&self, a: LngLat, b: LngLat, p: LngLat) -> f64 {
98 geodesic::point_to_segment_enu_m(p, (a, b))
99 }
100}
101
102/// Raw Euclidean distance calculation between coordinates.
103///
104/// Treats longitude and latitude values as Cartesian coordinates and
105/// calculates standard Euclidean distance. This is the fastest method
106/// but should only be used for:
107///
108/// - Non-geographic coordinate systems (screen coordinates, etc.)
109/// - Already-projected data where coordinates represent planar distances
110/// - Data where geographic accuracy is not important
111///
112/// # Examples
113///
114/// ```rust
115/// use rapidgeo_distance::LngLat;
116/// use rapidgeo_simplify::xt::{PerpDistance, XtEuclid};
117///
118/// let backend = XtEuclid;
119///
120/// // Using as screen coordinates (not geographic)
121/// let distance = backend.d_perp_m(
122/// LngLat::new_deg(0.0, 0.0), // Point A
123/// LngLat::new_deg(10.0, 0.0), // Point B
124/// LngLat::new_deg(5.0, 3.0), // Point P (3 units above midpoint)
125/// );
126///
127/// assert!((distance - 3.0).abs() < 0.001); // Should be exactly 3.0
128/// ```
129pub struct XtEuclid;
130
131impl PerpDistance for XtEuclid {
132 /// Calculate Euclidean distance from point to line segment.
133 ///
134 /// Uses the standard [point-to-line distance formula](https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_two_points)
135 /// with projection clamping to handle line segments (not infinite lines).
136 ///
137 /// # Algorithm
138 ///
139 /// 1. If start and end points are identical, return direct distance
140 /// 2. Project point P onto the line AB to find the closest point
141 /// 3. Clamp projection parameter to \[0,1\] to stay within segment bounds
142 /// 4. Calculate distance from P to the clamped projection point
143 fn d_perp_m(&self, a: LngLat, b: LngLat, p: LngLat) -> f64 {
144 let (ax, ay) = (a.lng_deg, a.lat_deg);
145 let (bx, by) = (b.lng_deg, b.lat_deg);
146 let (px, py) = (p.lng_deg, p.lat_deg);
147
148 // Handle degenerate case where A and B are the same point
149 if ax == bx && ay == by {
150 let dx = px - ax;
151 let dy = py - ay;
152 return (dx * dx + dy * dy).sqrt();
153 }
154
155 // Vector from A to B
156 let dx = bx - ax;
157 let dy = by - ay;
158
159 // Project P onto line AB: t = (AP · AB) / |AB|²
160 let t = ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy);
161 // Clamp to segment: t ∈ [0,1]
162 let t = t.clamp(0.0, 1.0);
163
164 // Find closest point on segment
165 let proj_x = ax + t * dx;
166 let proj_y = ay + t * dy;
167
168 // Distance from P to closest point
169 let dpx = px - proj_x;
170 let dpy = py - proj_y;
171
172 (dpx * dpx + dpy * dpy).sqrt()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
181 fn test_euclid_perpendicular_distance() {
182 let backend = XtEuclid;
183 let a = LngLat::new_deg(0.0, 0.0);
184 let b = LngLat::new_deg(10.0, 0.0);
185 let p = LngLat::new_deg(5.0, 3.0);
186
187 let dist = backend.d_perp_m(a, b, p);
188 assert!((dist - 3.0).abs() < 0.001);
189 }
190
191 #[test]
192 fn test_euclid_point_on_line() {
193 let backend = XtEuclid;
194 let a = LngLat::new_deg(0.0, 0.0);
195 let b = LngLat::new_deg(10.0, 0.0);
196 let p = LngLat::new_deg(5.0, 0.0);
197
198 let dist = backend.d_perp_m(a, b, p);
199 assert!(dist.abs() < 0.001);
200 }
201
202 #[test]
203 fn test_euclid_point_at_endpoint() {
204 let backend = XtEuclid;
205 let a = LngLat::new_deg(0.0, 0.0);
206 let b = LngLat::new_deg(10.0, 0.0);
207
208 let dist_a = backend.d_perp_m(a, b, a);
209 let dist_b = backend.d_perp_m(a, b, b);
210
211 assert!(dist_a.abs() < 0.001);
212 assert!(dist_b.abs() < 0.001);
213 }
214
215 #[test]
216 fn test_euclid_degenerate_segment() {
217 let backend = XtEuclid;
218 let a = LngLat::new_deg(5.0, 5.0);
219 let b = LngLat::new_deg(5.0, 5.0);
220 let p = LngLat::new_deg(8.0, 9.0);
221
222 let dist = backend.d_perp_m(a, b, p);
223 let expected = (3.0_f64 * 3.0 + 4.0 * 4.0).sqrt();
224 assert!((dist - expected).abs() < 0.001);
225 }
226
227 #[test]
228 fn test_euclid_projection_beyond_segment() {
229 let backend = XtEuclid;
230 let a = LngLat::new_deg(0.0, 0.0);
231 let b = LngLat::new_deg(5.0, 0.0);
232 let p = LngLat::new_deg(10.0, 3.0);
233
234 let dist = backend.d_perp_m(a, b, p);
235 let expected = (5.0_f64 * 5.0 + 3.0 * 3.0).sqrt();
236 assert!((dist - expected).abs() < 0.001);
237 }
238
239 #[test]
240 fn test_great_circle_basic() {
241 let backend = XtGreatCircle;
242 let a = LngLat::new_deg(-122.0, 37.0);
243 let b = LngLat::new_deg(-121.0, 37.0);
244 let p = LngLat::new_deg(-121.5, 37.1);
245
246 let dist = backend.d_perp_m(a, b, p);
247 assert!(dist > 10000.0 && dist < 12000.0);
248 }
249
250 #[test]
251 fn test_great_circle_point_on_line() {
252 let backend = XtGreatCircle;
253 let a = LngLat::new_deg(-122.0, 37.0);
254 let b = LngLat::new_deg(-121.0, 37.0);
255 let p = LngLat::new_deg(-121.5, 37.0);
256
257 let dist = backend.d_perp_m(a, b, p);
258 assert!(dist < 1000.0);
259 }
260
261 #[test]
262 fn test_enu_basic() {
263 let origin = LngLat::new_deg(-121.5, 37.0);
264 let backend = XtEnu { origin };
265
266 let a = LngLat::new_deg(-122.0, 37.0);
267 let b = LngLat::new_deg(-121.0, 37.0);
268 let p = LngLat::new_deg(-121.5, 37.1);
269
270 let dist = backend.d_perp_m(a, b, p);
271 assert!(dist > 10000.0 && dist < 12000.0);
272 }
273}