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}