Skip to main content

oxigdal_algorithms/vector/
length.rs

1//! Length calculation for linear geometries
2//!
3//! This module provides functions for computing lengths of linestrings using
4//! different methods appropriate for planar and geodetic coordinates.
5//!
6//! # Length Methods
7//!
8//! - **Planar**: Fast length calculation assuming Cartesian coordinates (projected data)
9//! - **Geodetic (Haversine)**: Great-circle length on sphere (good approximation for lat/lon)
10//! - **Geodetic (Vincenty)**: Precise ellipsoidal length on WGS84 (most accurate for lat/lon)
11//!
12//! # Examples
13//!
14//! ```
15//! # use oxigdal_algorithms::error::Result;
16//! use oxigdal_algorithms::vector::{Coordinate, LineString, length_linestring, LengthMethod};
17//!
18//! # fn main() -> Result<()> {
19//! let coords = vec![
20//!     Coordinate::new_2d(0.0, 0.0),
21//!     Coordinate::new_2d(3.0, 0.0),
22//!     Coordinate::new_2d(3.0, 4.0),
23//! ];
24//! let linestring = LineString::new(coords)?;
25//! let len = length_linestring(&linestring, LengthMethod::Planar)?;
26//! // Planar length = 3.0 + 4.0 = 7.0 units
27//! # Ok(())
28//! # }
29//! ```
30
31use 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
40/// WGS84 ellipsoid semi-major axis (meters)
41const WGS84_A: f64 = 6_378_137.0;
42
43/// WGS84 ellipsoid semi-minor axis (meters)
44const WGS84_B: f64 = 6_356_752.314_245;
45
46/// WGS84 ellipsoid flattening
47const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
48
49/// Maximum iterations for Vincenty's formula
50const VINCENTY_MAX_ITER: usize = 200;
51
52/// Convergence threshold for Vincenty's formula
53const VINCENTY_THRESHOLD: f64 = 1e-12;
54
55/// Length calculation method
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum LengthMethod {
58    /// Planar length using Cartesian coordinates (fast, for projected data)
59    Planar,
60    /// Geodetic length using Haversine formula (spherical Earth approximation)
61    Haversine,
62    /// Geodetic length using Vincenty's formula (accurate ellipsoidal distance)
63    Vincenty,
64}
65
66/// Computes the length of a geometry
67///
68/// # Arguments
69///
70/// * `geometry` - Input geometry
71/// * `method` - Length calculation method
72///
73/// # Returns
74///
75/// Length in appropriate units (meters for geodetic, coordinate units for planar)
76///
77/// # Errors
78///
79/// Returns error if geometry is not linear or invalid
80pub 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
91/// Computes the length of a linestring
92///
93/// # Arguments
94///
95/// * `linestring` - Input linestring
96/// * `method` - Length calculation method
97///
98/// # Returns
99///
100/// Total length of all segments
101///
102/// # Errors
103///
104/// Returns error if linestring is invalid or has fewer than 2 points
105pub 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
120/// Computes the length of a multilinestring
121///
122/// # Arguments
123///
124/// * `multilinestring` - Input multilinestring
125/// * `method` - Length calculation method
126///
127/// # Returns
128///
129/// Total length of all linestrings
130///
131/// # Errors
132///
133/// Returns error if any linestring is invalid
134pub 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
150/// Computes 3D length of a linestring (Euclidean distance including Z coordinate)
151///
152/// # Arguments
153///
154/// * `linestring` - Input linestring with Z coordinates
155///
156/// # Returns
157///
158/// Total 3D length
159///
160/// # Errors
161///
162/// Returns error if linestring is invalid or has fewer than 2 points
163pub 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
193/// Computes planar length of a linestring (fast, Cartesian)
194fn 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
211/// Computes geodetic length using Haversine formula
212fn 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
226/// Computes geodetic length using Vincenty's formula
227fn 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
241/// Computes Haversine distance between two coordinates (assumes lat/lon in degrees)
242fn 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    // Validate latitude range
249    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    // Use mean Earth radius
264    let radius = (WGS84_A + WGS84_B) / 2.0;
265    Ok(radius * c)
266}
267
268/// Computes Vincenty distance between two coordinates (assumes lat/lon in degrees)
269///
270/// Implementation of Vincenty's inverse formula for WGS84 ellipsoid.
271fn 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    // Validate latitude range
278    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            // Coincident points
311            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                // Length = 3.0 (first segment) + 4.0 (second segment) = 7.0
412                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                // Length = 3.0 + 4.0 + 5.0 = 12.0
428                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        // LineString::new should fail with a single point
439        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                // Total = 7.0 + 10.0 = 17.0
473                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        // Line from equator at 0° to 1° longitude
492        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                // 1 degree at equator is approximately 111.32 km
502                assert!(len > 110_000.0);
503                assert!(len < 112_000.0);
504            }
505        }
506    }
507
508    #[test]
509    fn test_length_vincenty() {
510        // Line from equator at 0° to 1° longitude
511        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                // 1 degree at equator is approximately 111.32 km
521                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            // Should be positive and reasonable
579            assert!(dist > 0.0);
580            assert!(dist < 200_000.0); // Less than 200 km
581        }
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            // Should be positive and reasonable
607            assert!(dist > 0.0);
608            assert!(dist < 200_000.0); // Less than 200 km
609        }
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); // Invalid latitude
628        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        // Square ring
640        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                // Perimeter = 4 * 10 = 40
656                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                // For 2D coordinates, planar and 3D should be equal
674                assert_relative_eq!(p, td, epsilon = 1e-10);
675            }
676        }
677    }
678}