Skip to main content

oxigdal_algorithms/vector/
distance.rs

1//! Distance calculation between geometric features
2//!
3//! This module provides functions for computing distances between geometries
4//! using different metrics appropriate for planar and geodetic coordinates.
5//!
6//! # Distance Methods
7//!
8//! - **Euclidean**: Simple Cartesian distance (fast, for projected data)
9//! - **Haversine**: Great-circle distance on sphere (good approximation for lat/lon)
10//! - **Vincenty**: Precise ellipsoidal distance on WGS84 (most accurate for lat/lon)
11//!
12//! # Examples
13//!
14//! ```
15//! # use oxigdal_algorithms::error::Result;
16//! use oxigdal_algorithms::vector::{Point, distance_point_to_point, DistanceMethod};
17//!
18//! # fn main() -> Result<()> {
19//! let p1 = Point::new(0.0, 0.0);
20//! let p2 = Point::new(3.0, 4.0);
21//! let dist = distance_point_to_point(&p1, &p2, DistanceMethod::Euclidean)?;
22//! // Distance = 5.0
23//! # Ok(())
24//! # }
25//! ```
26
27use crate::error::{AlgorithmError, Result};
28use oxigdal_core::vector::{Coordinate, LineString, Point, Polygon};
29
30#[cfg(feature = "std")]
31use std::f64::consts::PI;
32
33#[cfg(not(feature = "std"))]
34use core::f64::consts::PI;
35
36/// WGS84 ellipsoid semi-major axis (meters)
37const WGS84_A: f64 = 6_378_137.0;
38
39/// WGS84 ellipsoid semi-minor axis (meters)
40const WGS84_B: f64 = 6_356_752.314_245;
41
42/// WGS84 ellipsoid flattening
43const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
44
45/// Maximum iterations for Vincenty's formula
46const VINCENTY_MAX_ITER: usize = 200;
47
48/// Convergence threshold for Vincenty's formula
49const VINCENTY_THRESHOLD: f64 = 1e-12;
50
51/// Distance calculation method
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DistanceMethod {
54    /// Euclidean (Cartesian) distance
55    Euclidean,
56    /// Haversine formula (spherical Earth approximation)
57    Haversine,
58    /// Vincenty's formula (accurate ellipsoidal distance)
59    Vincenty,
60}
61
62/// Computes distance between two points
63///
64/// # Arguments
65///
66/// * `p1` - First point
67/// * `p2` - Second point
68/// * `method` - Distance calculation method
69///
70/// # Returns
71///
72/// Distance in appropriate units (meters for geodetic methods, coordinate units for Euclidean)
73///
74/// # Errors
75///
76/// Returns error if coordinates are invalid or computation fails
77pub fn distance_point_to_point(p1: &Point, p2: &Point, method: DistanceMethod) -> Result<f64> {
78    match method {
79        DistanceMethod::Euclidean => Ok(euclidean_distance(&p1.coord, &p2.coord)),
80        DistanceMethod::Haversine => haversine_distance(&p1.coord, &p2.coord),
81        DistanceMethod::Vincenty => vincenty_distance(&p1.coord, &p2.coord),
82    }
83}
84
85/// Computes minimum distance from a point to a linestring
86///
87/// # Arguments
88///
89/// * `point` - Input point
90/// * `linestring` - Input linestring
91/// * `method` - Distance calculation method
92///
93/// # Returns
94///
95/// Minimum distance from point to any point on the linestring
96///
97/// # Errors
98///
99/// Returns error if linestring is empty or computation fails
100pub fn distance_point_to_linestring(
101    point: &Point,
102    linestring: &LineString,
103    method: DistanceMethod,
104) -> Result<f64> {
105    if linestring.coords.is_empty() {
106        return Err(AlgorithmError::EmptyInput {
107            operation: "distance_point_to_linestring",
108        });
109    }
110
111    if linestring.coords.len() == 1 {
112        return distance_point_to_point(point, &Point::from_coord(linestring.coords[0]), method);
113    }
114
115    let mut min_dist = f64::INFINITY;
116
117    for i in 0..linestring.coords.len() - 1 {
118        let seg_start = &linestring.coords[i];
119        let seg_end = &linestring.coords[i + 1];
120
121        let dist = distance_point_to_segment(&point.coord, seg_start, seg_end, method)?;
122        min_dist = min_dist.min(dist);
123    }
124
125    Ok(min_dist)
126}
127
128/// Computes minimum distance from a point to a polygon boundary
129///
130/// # Arguments
131///
132/// * `point` - Input point
133/// * `polygon` - Input polygon
134/// * `method` - Distance calculation method
135///
136/// # Returns
137///
138/// Minimum distance from point to polygon boundary (0 if point is inside)
139///
140/// # Errors
141///
142/// Returns error if polygon is invalid or computation fails
143pub fn distance_point_to_polygon(
144    point: &Point,
145    polygon: &Polygon,
146    method: DistanceMethod,
147) -> Result<f64> {
148    if polygon.exterior.coords.len() < 3 {
149        return Err(AlgorithmError::InsufficientData {
150            operation: "distance_point_to_polygon",
151            message: "polygon must have at least 3 coordinates".to_string(),
152        });
153    }
154
155    // Check if point is inside polygon (using ray casting)
156    if point_in_polygon_boundary(&point.coord, polygon) {
157        return Ok(0.0);
158    }
159
160    // Compute distance to exterior ring
161    let mut min_dist = distance_point_to_linestring(point, &polygon.exterior, method)?;
162
163    // Check distance to holes
164    for hole in &polygon.interiors {
165        let hole_dist = distance_point_to_linestring(point, hole, method)?;
166        min_dist = min_dist.min(hole_dist);
167    }
168
169    Ok(min_dist)
170}
171
172/// Computes Euclidean distance between two coordinates
173fn euclidean_distance(c1: &Coordinate, c2: &Coordinate) -> f64 {
174    let dx = c2.x - c1.x;
175    let dy = c2.y - c1.y;
176
177    if let (Some(z1), Some(z2)) = (c1.z, c2.z) {
178        let dz = z2 - z1;
179        (dx * dx + dy * dy + dz * dz).sqrt()
180    } else {
181        (dx * dx + dy * dy).sqrt()
182    }
183}
184
185/// Computes Haversine distance between two coordinates (assumes lat/lon in degrees)
186fn haversine_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
187    let lat1 = c1.y * PI / 180.0;
188    let lon1 = c1.x * PI / 180.0;
189    let lat2 = c2.y * PI / 180.0;
190    let lon2 = c2.x * PI / 180.0;
191
192    // Validate latitude range
193    if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
194        return Err(AlgorithmError::InvalidParameter {
195            parameter: "latitude",
196            message: "latitude must be between -90 and 90 degrees".to_string(),
197        });
198    }
199
200    let dlat = lat2 - lat1;
201    let dlon = lon2 - lon1;
202
203    let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
204
205    let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
206
207    // Use mean Earth radius
208    let radius = (WGS84_A + WGS84_B) / 2.0;
209    Ok(radius * c)
210}
211
212/// Computes Vincenty distance between two coordinates (assumes lat/lon in degrees)
213///
214/// Implementation of Vincenty's inverse formula for WGS84 ellipsoid.
215fn vincenty_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
216    let lat1 = c1.y * PI / 180.0;
217    let lon1 = c1.x * PI / 180.0;
218    let lat2 = c2.y * PI / 180.0;
219    let lon2 = c2.x * PI / 180.0;
220
221    // Validate latitude range
222    if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
223        return Err(AlgorithmError::InvalidParameter {
224            parameter: "latitude",
225            message: "latitude must be between -90 and 90 degrees".to_string(),
226        });
227    }
228
229    let l = lon2 - lon1;
230
231    let u1 = ((1.0 - WGS84_F) * lat1.tan()).atan();
232    let u2 = ((1.0 - WGS84_F) * lat2.tan()).atan();
233
234    let sin_u1 = u1.sin();
235    let cos_u1 = u1.cos();
236    let sin_u2 = u2.sin();
237    let cos_u2 = u2.cos();
238
239    let mut lambda = l;
240    let mut lambda_prev;
241    let mut iter_count = 0;
242
243    let (sin_sigma, cos_sigma, sigma, cos_sq_alpha, cos_2sigma_m);
244
245    loop {
246        let sin_lambda = lambda.sin();
247        let cos_lambda = lambda.cos();
248
249        let sin_sigma_temp = ((cos_u2 * sin_lambda).powi(2)
250            + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
251        .sqrt();
252
253        if sin_sigma_temp.abs() < f64::EPSILON {
254            // Coincident points
255            return Ok(0.0);
256        }
257
258        let cos_sigma_temp = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
259        let sigma_temp = sin_sigma_temp.atan2(cos_sigma_temp);
260
261        let sin_alpha_temp = cos_u1 * cos_u2 * sin_lambda / sin_sigma_temp;
262        let cos_sq_alpha_temp = 1.0 - sin_alpha_temp * sin_alpha_temp;
263
264        let cos_2sigma_m_temp = if cos_sq_alpha_temp.abs() < f64::EPSILON {
265            0.0
266        } else {
267            cos_sigma_temp - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha_temp
268        };
269
270        let c =
271            WGS84_F / 16.0 * cos_sq_alpha_temp * (4.0 + WGS84_F * (4.0 - 3.0 * cos_sq_alpha_temp));
272
273        lambda_prev = lambda;
274        lambda = l
275            + (1.0 - c)
276                * WGS84_F
277                * sin_alpha_temp
278                * (sigma_temp
279                    + c * sin_sigma_temp
280                        * (cos_2sigma_m_temp
281                            + c * cos_sigma_temp
282                                * (-1.0 + 2.0 * cos_2sigma_m_temp * cos_2sigma_m_temp)));
283
284        iter_count += 1;
285        if (lambda - lambda_prev).abs() < VINCENTY_THRESHOLD || iter_count >= VINCENTY_MAX_ITER {
286            sin_sigma = sin_sigma_temp;
287            cos_sigma = cos_sigma_temp;
288            sigma = sigma_temp;
289            cos_sq_alpha = cos_sq_alpha_temp;
290            cos_2sigma_m = cos_2sigma_m_temp;
291            break;
292        }
293    }
294
295    if iter_count >= VINCENTY_MAX_ITER {
296        return Err(AlgorithmError::NumericalError {
297            operation: "vincenty_distance",
298            message: "failed to converge".to_string(),
299        });
300    }
301
302    let u_sq = cos_sq_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
303    let a = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
304    let b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
305
306    let delta_sigma = b
307        * sin_sigma
308        * (cos_2sigma_m
309            + b / 4.0
310                * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
311                    - b / 6.0
312                        * cos_2sigma_m
313                        * (-3.0 + 4.0 * sin_sigma * sin_sigma)
314                        * (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
315
316    let distance = WGS84_B * a * (sigma - delta_sigma);
317
318    Ok(distance)
319}
320
321/// Computes distance from a point to a line segment
322fn distance_point_to_segment(
323    point: &Coordinate,
324    seg_start: &Coordinate,
325    seg_end: &Coordinate,
326    method: DistanceMethod,
327) -> Result<f64> {
328    match method {
329        DistanceMethod::Euclidean => Ok(distance_point_to_segment_euclidean(
330            point, seg_start, seg_end,
331        )),
332        DistanceMethod::Haversine | DistanceMethod::Vincenty => {
333            // For geodetic methods, approximate by sampling the segment
334            distance_point_to_segment_geodetic(point, seg_start, seg_end, method)
335        }
336    }
337}
338
339/// Euclidean distance from point to segment
340fn distance_point_to_segment_euclidean(
341    point: &Coordinate,
342    seg_start: &Coordinate,
343    seg_end: &Coordinate,
344) -> f64 {
345    let dx = seg_end.x - seg_start.x;
346    let dy = seg_end.y - seg_start.y;
347
348    let len_sq = dx * dx + dy * dy;
349
350    if len_sq < f64::EPSILON {
351        // Segment is actually a point
352        return euclidean_distance(point, seg_start);
353    }
354
355    // Compute projection parameter t
356    let t = ((point.x - seg_start.x) * dx + (point.y - seg_start.y) * dy) / len_sq;
357
358    let t_clamped = t.clamp(0.0, 1.0);
359
360    // Compute closest point on segment
361    let closest_x = seg_start.x + t_clamped * dx;
362    let closest_y = seg_start.y + t_clamped * dy;
363
364    let closest = Coordinate::new_2d(closest_x, closest_y);
365
366    euclidean_distance(point, &closest)
367}
368
369/// Geodetic distance from point to segment (approximated by sampling)
370fn distance_point_to_segment_geodetic(
371    point: &Coordinate,
372    seg_start: &Coordinate,
373    seg_end: &Coordinate,
374    method: DistanceMethod,
375) -> Result<f64> {
376    // Sample the segment at regular intervals
377    const NUM_SAMPLES: usize = 10;
378
379    let mut min_dist = f64::INFINITY;
380
381    for i in 0..=NUM_SAMPLES {
382        let t = i as f64 / NUM_SAMPLES as f64;
383
384        let sample_x = seg_start.x + t * (seg_end.x - seg_start.x);
385        let sample_y = seg_start.y + t * (seg_end.y - seg_start.y);
386        let sample = Coordinate::new_2d(sample_x, sample_y);
387
388        let dist = match method {
389            DistanceMethod::Haversine => haversine_distance(point, &sample)?,
390            DistanceMethod::Vincenty => vincenty_distance(point, &sample)?,
391            _ => euclidean_distance(point, &sample),
392        };
393
394        min_dist = min_dist.min(dist);
395    }
396
397    Ok(min_dist)
398}
399
400/// Simple point-in-polygon test using ray casting
401fn point_in_polygon_boundary(point: &Coordinate, polygon: &Polygon) -> bool {
402    ray_casting_test(point, &polygon.exterior.coords)
403}
404
405/// Ray casting algorithm for point-in-polygon test
406fn ray_casting_test(point: &Coordinate, ring: &[Coordinate]) -> bool {
407    let mut inside = false;
408    let n = ring.len();
409
410    let mut j = n - 1;
411    for i in 0..n {
412        let xi = ring[i].x;
413        let yi = ring[i].y;
414        let xj = ring[j].x;
415        let yj = ring[j].y;
416
417        let intersect = ((yi > point.y) != (yj > point.y))
418            && (point.x < (xj - xi) * (point.y - yi) / (yj - yi) + xi);
419
420        if intersect {
421            inside = !inside;
422        }
423
424        j = i;
425    }
426
427    inside
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_distance_point_to_point_euclidean() {
436        let p1 = Point::new(0.0, 0.0);
437        let p2 = Point::new(3.0, 4.0);
438
439        let result = distance_point_to_point(&p1, &p2, DistanceMethod::Euclidean);
440        assert!(result.is_ok());
441
442        if let Ok(dist) = result {
443            assert!((dist - 5.0).abs() < 1e-10);
444        }
445    }
446
447    #[test]
448    fn test_distance_point_to_point_3d() {
449        let p1 = Point::new_3d(0.0, 0.0, 0.0);
450        let p2 = Point::new_3d(1.0, 1.0, 1.0);
451
452        let result = distance_point_to_point(&p1, &p2, DistanceMethod::Euclidean);
453        assert!(result.is_ok());
454
455        if let Ok(dist) = result {
456            assert!((dist - 3.0_f64.sqrt()).abs() < 1e-10);
457        }
458    }
459
460    #[test]
461    fn test_distance_point_to_point_haversine() {
462        // Distance from New York to London (approximately)
463        let nyc = Point::new(-74.0, 40.7); // NYC: 74°W, 40.7°N
464        let lon = Point::new(-0.1, 51.5); // London: 0.1°W, 51.5°N
465
466        let result = distance_point_to_point(&nyc, &lon, DistanceMethod::Haversine);
467        assert!(result.is_ok());
468
469        if let Ok(dist) = result {
470            // Approximate distance is about 5,570 km
471            assert!(dist > 5_000_000.0);
472            assert!(dist < 6_000_000.0);
473        }
474    }
475
476    #[test]
477    fn test_distance_point_to_linestring() {
478        let point = Point::new(1.0, 1.0);
479
480        let coords = vec![
481            Coordinate::new_2d(0.0, 0.0),
482            Coordinate::new_2d(2.0, 0.0),
483            Coordinate::new_2d(2.0, 2.0),
484        ];
485        let linestring = LineString::new(coords);
486        assert!(linestring.is_ok());
487
488        if let Ok(ls) = linestring {
489            let result = distance_point_to_linestring(&point, &ls, DistanceMethod::Euclidean);
490            assert!(result.is_ok());
491
492            if let Ok(dist) = result {
493                // Point (1,1) is 1 unit from segment (0,0)-(2,0)
494                assert!((dist - 1.0).abs() < 1e-10);
495            }
496        }
497    }
498
499    #[test]
500    fn test_distance_point_to_polygon() {
501        // Square polygon
502        let coords = vec![
503            Coordinate::new_2d(0.0, 0.0),
504            Coordinate::new_2d(4.0, 0.0),
505            Coordinate::new_2d(4.0, 4.0),
506            Coordinate::new_2d(0.0, 4.0),
507            Coordinate::new_2d(0.0, 0.0),
508        ];
509        let exterior = LineString::new(coords);
510        assert!(exterior.is_ok());
511
512        if let Ok(ext) = exterior {
513            let polygon = Polygon::new(ext, vec![]);
514            assert!(polygon.is_ok());
515
516            if let Ok(poly) = polygon {
517                // Point inside polygon
518                let inside = Point::new(2.0, 2.0);
519                let result1 = distance_point_to_polygon(&inside, &poly, DistanceMethod::Euclidean);
520                assert!(result1.is_ok());
521
522                if let Ok(dist) = result1 {
523                    assert_eq!(dist, 0.0);
524                }
525
526                // Point outside polygon
527                let outside = Point::new(5.0, 5.0);
528                let result2 = distance_point_to_polygon(&outside, &poly, DistanceMethod::Euclidean);
529                assert!(result2.is_ok());
530
531                if let Ok(dist) = result2 {
532                    // Distance from (5,5) to corner (4,4)
533                    assert!((dist - 2.0_f64.sqrt()).abs() < 1e-10);
534                }
535            }
536        }
537    }
538
539    #[test]
540    fn test_euclidean_distance() {
541        let c1 = Coordinate::new_2d(0.0, 0.0);
542        let c2 = Coordinate::new_2d(3.0, 4.0);
543
544        let dist = euclidean_distance(&c1, &c2);
545        assert!((dist - 5.0).abs() < 1e-10);
546    }
547
548    #[test]
549    fn test_haversine_same_point() {
550        let c1 = Coordinate::new_2d(0.0, 0.0);
551        let c2 = Coordinate::new_2d(0.0, 0.0);
552
553        let result = haversine_distance(&c1, &c2);
554        assert!(result.is_ok());
555
556        if let Ok(dist) = result {
557            assert!(dist.abs() < 1e-10);
558        }
559    }
560
561    #[test]
562    fn test_vincenty_same_point() {
563        let c1 = Coordinate::new_2d(0.0, 0.0);
564        let c2 = Coordinate::new_2d(0.0, 0.0);
565
566        let result = vincenty_distance(&c1, &c2);
567        assert!(result.is_ok());
568
569        if let Ok(dist) = result {
570            assert!(dist.abs() < 1e-10);
571        }
572    }
573
574    #[test]
575    fn test_distance_point_to_segment_euclidean() {
576        let point = Coordinate::new_2d(1.0, 1.0);
577        let seg_start = Coordinate::new_2d(0.0, 0.0);
578        let seg_end = Coordinate::new_2d(2.0, 0.0);
579
580        let dist = distance_point_to_segment_euclidean(&point, &seg_start, &seg_end);
581        assert!((dist - 1.0).abs() < 1e-10);
582    }
583
584    #[test]
585    fn test_ray_casting() {
586        let ring = vec![
587            Coordinate::new_2d(0.0, 0.0),
588            Coordinate::new_2d(4.0, 0.0),
589            Coordinate::new_2d(4.0, 4.0),
590            Coordinate::new_2d(0.0, 4.0),
591            Coordinate::new_2d(0.0, 0.0),
592        ];
593
594        // Point inside
595        let inside = Coordinate::new_2d(2.0, 2.0);
596        assert!(ray_casting_test(&inside, &ring));
597
598        // Point outside
599        let outside = Coordinate::new_2d(5.0, 5.0);
600        assert!(!ray_casting_test(&outside, &ring));
601    }
602
603    #[test]
604    fn test_invalid_latitude() {
605        let c1 = Coordinate::new_2d(0.0, 95.0); // Invalid latitude
606        let c2 = Coordinate::new_2d(0.0, 0.0);
607
608        let result = haversine_distance(&c1, &c2);
609        assert!(result.is_err());
610    }
611}