oxigdal_algorithms/vector/
length.rs1use crate::error::{AlgorithmError, Result};
32use oxigdal_core::vector::{Coordinate, Geometry, LineString, MultiLineString};
33
34#[cfg(feature = "std")]
35use std::f64::consts::PI;
36
37#[cfg(not(feature = "std"))]
38use core::f64::consts::PI;
39
40const WGS84_A: f64 = 6_378_137.0;
42
43const WGS84_B: f64 = 6_356_752.314_245;
45
46const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
48
49const VINCENTY_MAX_ITER: usize = 200;
51
52const VINCENTY_THRESHOLD: f64 = 1e-12;
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum LengthMethod {
58 Planar,
60 Haversine,
62 Vincenty,
64}
65
66pub fn length(geometry: &Geometry, method: LengthMethod) -> Result<f64> {
81 match geometry {
82 Geometry::LineString(ls) => length_linestring(ls, method),
83 Geometry::MultiLineString(mls) => length_multilinestring(mls, method),
84 _ => Err(AlgorithmError::GeometryError {
85 message: "length calculation requires LineString or MultiLineString geometry"
86 .to_string(),
87 }),
88 }
89}
90
91pub fn length_linestring(linestring: &LineString, method: LengthMethod) -> Result<f64> {
106 if linestring.coords.len() < 2 {
107 return Err(AlgorithmError::InsufficientData {
108 operation: "length_linestring",
109 message: "linestring must have at least 2 coordinates".to_string(),
110 });
111 }
112
113 match method {
114 LengthMethod::Planar => Ok(length_linestring_planar(linestring)),
115 LengthMethod::Haversine => length_linestring_haversine(linestring),
116 LengthMethod::Vincenty => length_linestring_vincenty(linestring),
117 }
118}
119
120pub fn length_multilinestring(
135 multilinestring: &MultiLineString,
136 method: LengthMethod,
137) -> Result<f64> {
138 if multilinestring.line_strings.is_empty() {
139 return Ok(0.0);
140 }
141
142 let mut total = 0.0;
143 for linestring in &multilinestring.line_strings {
144 total += length_linestring(linestring, method)?;
145 }
146
147 Ok(total)
148}
149
150pub fn length_linestring_3d(linestring: &LineString) -> Result<f64> {
164 if linestring.coords.len() < 2 {
165 return Err(AlgorithmError::InsufficientData {
166 operation: "length_linestring_3d",
167 message: "linestring must have at least 2 coordinates".to_string(),
168 });
169 }
170
171 let mut total_length = 0.0;
172
173 for i in 0..(linestring.coords.len() - 1) {
174 let p1 = &linestring.coords[i];
175 let p2 = &linestring.coords[i + 1];
176
177 let dx = p2.x - p1.x;
178 let dy = p2.y - p1.y;
179
180 let segment_length = if let (Some(z1), Some(z2)) = (p1.z, p2.z) {
181 let dz = z2 - z1;
182 (dx * dx + dy * dy + dz * dz).sqrt()
183 } else {
184 (dx * dx + dy * dy).sqrt()
185 };
186
187 total_length += segment_length;
188 }
189
190 Ok(total_length)
191}
192
193fn length_linestring_planar(linestring: &LineString) -> f64 {
195 let mut total_length = 0.0;
196
197 for i in 0..(linestring.coords.len() - 1) {
198 let p1 = &linestring.coords[i];
199 let p2 = &linestring.coords[i + 1];
200
201 let dx = p2.x - p1.x;
202 let dy = p2.y - p1.y;
203 let segment_length = (dx * dx + dy * dy).sqrt();
204
205 total_length += segment_length;
206 }
207
208 total_length
209}
210
211fn length_linestring_haversine(linestring: &LineString) -> Result<f64> {
213 let mut total_length = 0.0;
214
215 for i in 0..(linestring.coords.len() - 1) {
216 let p1 = &linestring.coords[i];
217 let p2 = &linestring.coords[i + 1];
218
219 let segment_length = haversine_distance(p1, p2)?;
220 total_length += segment_length;
221 }
222
223 Ok(total_length)
224}
225
226fn length_linestring_vincenty(linestring: &LineString) -> Result<f64> {
228 let mut total_length = 0.0;
229
230 for i in 0..(linestring.coords.len() - 1) {
231 let p1 = &linestring.coords[i];
232 let p2 = &linestring.coords[i + 1];
233
234 let segment_length = vincenty_distance(p1, p2)?;
235 total_length += segment_length;
236 }
237
238 Ok(total_length)
239}
240
241fn haversine_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
243 let lat1 = c1.y * PI / 180.0;
244 let lon1 = c1.x * PI / 180.0;
245 let lat2 = c2.y * PI / 180.0;
246 let lon2 = c2.x * PI / 180.0;
247
248 if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
250 return Err(AlgorithmError::InvalidParameter {
251 parameter: "latitude",
252 message: "latitude must be between -90 and 90 degrees".to_string(),
253 });
254 }
255
256 let dlat = lat2 - lat1;
257 let dlon = lon2 - lon1;
258
259 let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
260
261 let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
262
263 let radius = (WGS84_A + WGS84_B) / 2.0;
265 Ok(radius * c)
266}
267
268fn vincenty_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
272 let lat1 = c1.y * PI / 180.0;
273 let lon1 = c1.x * PI / 180.0;
274 let lat2 = c2.y * PI / 180.0;
275 let lon2 = c2.x * PI / 180.0;
276
277 if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
279 return Err(AlgorithmError::InvalidParameter {
280 parameter: "latitude",
281 message: "latitude must be between -90 and 90 degrees".to_string(),
282 });
283 }
284
285 let l = lon2 - lon1;
286
287 let u1 = ((1.0 - WGS84_F) * lat1.tan()).atan();
288 let u2 = ((1.0 - WGS84_F) * lat2.tan()).atan();
289
290 let sin_u1 = u1.sin();
291 let cos_u1 = u1.cos();
292 let sin_u2 = u2.sin();
293 let cos_u2 = u2.cos();
294
295 let mut lambda = l;
296 let mut lambda_prev;
297 let mut iter_count = 0;
298
299 let (sin_sigma, cos_sigma, sigma, cos_sq_alpha, cos_2sigma_m);
300
301 loop {
302 let sin_lambda = lambda.sin();
303 let cos_lambda = lambda.cos();
304
305 let sin_sigma_temp = ((cos_u2 * sin_lambda).powi(2)
306 + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
307 .sqrt();
308
309 if sin_sigma_temp.abs() < f64::EPSILON {
310 return Ok(0.0);
312 }
313
314 let cos_sigma_temp = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
315 let sigma_temp = sin_sigma_temp.atan2(cos_sigma_temp);
316
317 let sin_alpha_temp = cos_u1 * cos_u2 * sin_lambda / sin_sigma_temp;
318 let cos_sq_alpha_temp = 1.0 - sin_alpha_temp * sin_alpha_temp;
319
320 let cos_2sigma_m_temp = if cos_sq_alpha_temp.abs() < f64::EPSILON {
321 0.0
322 } else {
323 cos_sigma_temp - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha_temp
324 };
325
326 let c =
327 WGS84_F / 16.0 * cos_sq_alpha_temp * (4.0 + WGS84_F * (4.0 - 3.0 * cos_sq_alpha_temp));
328
329 lambda_prev = lambda;
330 lambda = l
331 + (1.0 - c)
332 * WGS84_F
333 * sin_alpha_temp
334 * (sigma_temp
335 + c * sin_sigma_temp
336 * (cos_2sigma_m_temp
337 + c * cos_sigma_temp
338 * (-1.0 + 2.0 * cos_2sigma_m_temp * cos_2sigma_m_temp)));
339
340 iter_count += 1;
341 if (lambda - lambda_prev).abs() < VINCENTY_THRESHOLD || iter_count >= VINCENTY_MAX_ITER {
342 sin_sigma = sin_sigma_temp;
343 cos_sigma = cos_sigma_temp;
344 sigma = sigma_temp;
345 cos_sq_alpha = cos_sq_alpha_temp;
346 cos_2sigma_m = cos_2sigma_m_temp;
347 break;
348 }
349 }
350
351 if iter_count >= VINCENTY_MAX_ITER {
352 return Err(AlgorithmError::NumericalError {
353 operation: "vincenty_distance",
354 message: "failed to converge".to_string(),
355 });
356 }
357
358 let u_sq = cos_sq_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
359 let a = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
360 let b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
361
362 let delta_sigma = b
363 * sin_sigma
364 * (cos_2sigma_m
365 + b / 4.0
366 * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
367 - b / 6.0
368 * cos_2sigma_m
369 * (-3.0 + 4.0 * sin_sigma * sin_sigma)
370 * (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
371
372 let distance = WGS84_B * a * (sigma - delta_sigma);
373
374 Ok(distance)
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use approx::assert_relative_eq;
381
382 fn create_linestring_2d() -> Result<LineString> {
383 let coords = vec![
384 Coordinate::new_2d(0.0, 0.0),
385 Coordinate::new_2d(3.0, 0.0),
386 Coordinate::new_2d(3.0, 4.0),
387 ];
388 LineString::new(coords).map_err(AlgorithmError::Core)
389 }
390
391 fn create_linestring_3d() -> Result<LineString> {
392 let coords = vec![
393 Coordinate::new_3d(0.0, 0.0, 0.0),
394 Coordinate::new_3d(3.0, 0.0, 0.0),
395 Coordinate::new_3d(3.0, 4.0, 0.0),
396 Coordinate::new_3d(3.0, 4.0, 5.0),
397 ];
398 LineString::new(coords).map_err(AlgorithmError::Core)
399 }
400
401 #[test]
402 fn test_length_linestring_planar() {
403 let ls = create_linestring_2d();
404 assert!(ls.is_ok());
405
406 if let Ok(linestring) = ls {
407 let result = length_linestring(&linestring, LengthMethod::Planar);
408 assert!(result.is_ok());
409
410 if let Ok(len) = result {
411 assert_relative_eq!(len, 7.0, epsilon = 1e-10);
413 }
414 }
415 }
416
417 #[test]
418 fn test_length_linestring_3d() {
419 let ls = create_linestring_3d();
420 assert!(ls.is_ok());
421
422 if let Ok(linestring) = ls {
423 let result = length_linestring_3d(&linestring);
424 assert!(result.is_ok());
425
426 if let Ok(len) = result {
427 assert_relative_eq!(len, 12.0, epsilon = 1e-10);
429 }
430 }
431 }
432
433 #[test]
434 fn test_length_linestring_single_point() {
435 let coords = vec![Coordinate::new_2d(0.0, 0.0)];
436 let ls = LineString::new(coords);
437
438 assert!(ls.is_err());
440 }
441
442 #[test]
443 fn test_length_linestring_two_points() {
444 let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(5.0, 0.0)];
445 let ls = LineString::new(coords);
446 assert!(ls.is_ok());
447
448 if let Ok(linestring) = ls {
449 let result = length_linestring(&linestring, LengthMethod::Planar);
450 assert!(result.is_ok());
451
452 if let Ok(len) = result {
453 assert_relative_eq!(len, 5.0, epsilon = 1e-10);
454 }
455 }
456 }
457
458 #[test]
459 fn test_length_multilinestring() {
460 let ls1 = create_linestring_2d();
461 let coords2 = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
462 let ls2 = LineString::new(coords2);
463
464 assert!(ls1.is_ok() && ls2.is_ok());
465
466 if let (Ok(l1), Ok(l2)) = (ls1, ls2) {
467 let mls = MultiLineString::new(vec![l1, l2]);
468 let result = length_multilinestring(&mls, LengthMethod::Planar);
469 assert!(result.is_ok());
470
471 if let Ok(len) = result {
472 assert_relative_eq!(len, 17.0, epsilon = 1e-10);
474 }
475 }
476 }
477
478 #[test]
479 fn test_length_multilinestring_empty() {
480 let mls = MultiLineString::empty();
481 let result = length_multilinestring(&mls, LengthMethod::Planar);
482 assert!(result.is_ok());
483
484 if let Ok(len) = result {
485 assert_eq!(len, 0.0);
486 }
487 }
488
489 #[test]
490 fn test_length_haversine() {
491 let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 0.0)];
493 let ls = LineString::new(coords);
494 assert!(ls.is_ok());
495
496 if let Ok(linestring) = ls {
497 let result = length_linestring(&linestring, LengthMethod::Haversine);
498 assert!(result.is_ok());
499
500 if let Ok(len) = result {
501 assert!(len > 110_000.0);
503 assert!(len < 112_000.0);
504 }
505 }
506 }
507
508 #[test]
509 fn test_length_vincenty() {
510 let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 0.0)];
512 let ls = LineString::new(coords);
513 assert!(ls.is_ok());
514
515 if let Ok(linestring) = ls {
516 let result = length_linestring(&linestring, LengthMethod::Vincenty);
517 assert!(result.is_ok());
518
519 if let Ok(len) = result {
520 assert!(len > 110_000.0);
522 assert!(len < 112_000.0);
523 }
524 }
525 }
526
527 #[test]
528 fn test_length_geometry_linestring() {
529 let ls = create_linestring_2d();
530 assert!(ls.is_ok());
531
532 if let Ok(linestring) = ls {
533 let geom = Geometry::LineString(linestring);
534 let result = length(&geom, LengthMethod::Planar);
535 assert!(result.is_ok());
536
537 if let Ok(len) = result {
538 assert_relative_eq!(len, 7.0, epsilon = 1e-10);
539 }
540 }
541 }
542
543 #[test]
544 fn test_length_geometry_multilinestring() {
545 let ls = create_linestring_2d();
546 assert!(ls.is_ok());
547
548 if let Ok(linestring) = ls {
549 let mls = MultiLineString::new(vec![linestring]);
550 let geom = Geometry::MultiLineString(mls);
551 let result = length(&geom, LengthMethod::Planar);
552 assert!(result.is_ok());
553
554 if let Ok(len) = result {
555 assert_relative_eq!(len, 7.0, epsilon = 1e-10);
556 }
557 }
558 }
559
560 #[test]
561 fn test_length_invalid_geometry() {
562 use oxigdal_core::vector::Point;
563
564 let point = Geometry::Point(Point::new(0.0, 0.0));
565 let result = length(&point, LengthMethod::Planar);
566 assert!(result.is_err());
567 }
568
569 #[test]
570 fn test_haversine_distance_basic() {
571 let c1 = Coordinate::new_2d(0.0, 0.0);
572 let c2 = Coordinate::new_2d(1.0, 1.0);
573
574 let result = haversine_distance(&c1, &c2);
575 assert!(result.is_ok());
576
577 if let Ok(dist) = result {
578 assert!(dist > 0.0);
580 assert!(dist < 200_000.0); }
582 }
583
584 #[test]
585 fn test_haversine_distance_same_point() {
586 let c1 = Coordinate::new_2d(0.0, 0.0);
587 let c2 = Coordinate::new_2d(0.0, 0.0);
588
589 let result = haversine_distance(&c1, &c2);
590 assert!(result.is_ok());
591
592 if let Ok(dist) = result {
593 assert!(dist.abs() < 1e-10);
594 }
595 }
596
597 #[test]
598 fn test_vincenty_distance_basic() {
599 let c1 = Coordinate::new_2d(0.0, 0.0);
600 let c2 = Coordinate::new_2d(1.0, 1.0);
601
602 let result = vincenty_distance(&c1, &c2);
603 assert!(result.is_ok());
604
605 if let Ok(dist) = result {
606 assert!(dist > 0.0);
608 assert!(dist < 200_000.0); }
610 }
611
612 #[test]
613 fn test_vincenty_distance_same_point() {
614 let c1 = Coordinate::new_2d(0.0, 0.0);
615 let c2 = Coordinate::new_2d(0.0, 0.0);
616
617 let result = vincenty_distance(&c1, &c2);
618 assert!(result.is_ok());
619
620 if let Ok(dist) = result {
621 assert!(dist.abs() < 1e-10);
622 }
623 }
624
625 #[test]
626 fn test_invalid_latitude() {
627 let c1 = Coordinate::new_2d(0.0, 95.0); let c2 = Coordinate::new_2d(0.0, 0.0);
629
630 let result = haversine_distance(&c1, &c2);
631 assert!(result.is_err());
632
633 let result2 = vincenty_distance(&c1, &c2);
634 assert!(result2.is_err());
635 }
636
637 #[test]
638 fn test_length_linestring_closed_ring() {
639 let coords = vec![
641 Coordinate::new_2d(0.0, 0.0),
642 Coordinate::new_2d(10.0, 0.0),
643 Coordinate::new_2d(10.0, 10.0),
644 Coordinate::new_2d(0.0, 10.0),
645 Coordinate::new_2d(0.0, 0.0),
646 ];
647 let ls = LineString::new(coords);
648 assert!(ls.is_ok());
649
650 if let Ok(linestring) = ls {
651 let result = length_linestring(&linestring, LengthMethod::Planar);
652 assert!(result.is_ok());
653
654 if let Ok(len) = result {
655 assert_relative_eq!(len, 40.0, epsilon = 1e-10);
657 }
658 }
659 }
660
661 #[test]
662 fn test_length_planar_equals_3d_for_2d() {
663 let ls = create_linestring_2d();
664 assert!(ls.is_ok());
665
666 if let Ok(linestring) = ls {
667 let planar = length_linestring(&linestring, LengthMethod::Planar);
668 let three_d = length_linestring_3d(&linestring);
669
670 assert!(planar.is_ok() && three_d.is_ok());
671
672 if let (Ok(p), Ok(td)) = (planar, three_d) {
673 assert_relative_eq!(p, td, epsilon = 1e-10);
675 }
676 }
677 }
678}