oxigdal_algorithms/vector/
distance.rs1use 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
36const WGS84_A: f64 = 6_378_137.0;
38
39const WGS84_B: f64 = 6_356_752.314_245;
41
42const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
44
45const VINCENTY_MAX_ITER: usize = 200;
47
48const VINCENTY_THRESHOLD: f64 = 1e-12;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum DistanceMethod {
54 Euclidean,
56 Haversine,
58 Vincenty,
60}
61
62pub 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
85pub 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
128pub 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 if point_in_polygon_boundary(&point.coord, polygon) {
157 return Ok(0.0);
158 }
159
160 let mut min_dist = distance_point_to_linestring(point, &polygon.exterior, method)?;
162
163 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
172fn 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
185fn 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 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 let radius = (WGS84_A + WGS84_B) / 2.0;
209 Ok(radius * c)
210}
211
212fn 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 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 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
321fn 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 distance_point_to_segment_geodetic(point, seg_start, seg_end, method)
335 }
336 }
337}
338
339fn 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 return euclidean_distance(point, seg_start);
353 }
354
355 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 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
369fn distance_point_to_segment_geodetic(
371 point: &Coordinate,
372 seg_start: &Coordinate,
373 seg_end: &Coordinate,
374 method: DistanceMethod,
375) -> Result<f64> {
376 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
400fn point_in_polygon_boundary(point: &Coordinate, polygon: &Polygon) -> bool {
402 ray_casting_test(point, &polygon.exterior.coords)
403}
404
405fn 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 let nyc = Point::new(-74.0, 40.7); let lon = Point::new(-0.1, 51.5); let result = distance_point_to_point(&nyc, &lon, DistanceMethod::Haversine);
467 assert!(result.is_ok());
468
469 if let Ok(dist) = result {
470 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 assert!((dist - 1.0).abs() < 1e-10);
495 }
496 }
497 }
498
499 #[test]
500 fn test_distance_point_to_polygon() {
501 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 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 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 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 let inside = Coordinate::new_2d(2.0, 2.0);
596 assert!(ray_casting_test(&inside, &ring));
597
598 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); let c2 = Coordinate::new_2d(0.0, 0.0);
607
608 let result = haversine_distance(&c1, &c2);
609 assert!(result.is_err());
610 }
611}