use crate::error::{AlgorithmError, Result};
use oxigdal_core::vector::{Coordinate, Geometry, LineString, MultiLineString};
#[cfg(feature = "std")]
use std::f64::consts::PI;
#[cfg(not(feature = "std"))]
use core::f64::consts::PI;
const WGS84_A: f64 = 6_378_137.0;
const WGS84_B: f64 = 6_356_752.314_245;
const WGS84_F: f64 = (WGS84_A - WGS84_B) / WGS84_A;
const VINCENTY_MAX_ITER: usize = 200;
const VINCENTY_THRESHOLD: f64 = 1e-12;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LengthMethod {
Planar,
Haversine,
Vincenty,
}
pub fn length(geometry: &Geometry, method: LengthMethod) -> Result<f64> {
match geometry {
Geometry::LineString(ls) => length_linestring(ls, method),
Geometry::MultiLineString(mls) => length_multilinestring(mls, method),
_ => Err(AlgorithmError::GeometryError {
message: "length calculation requires LineString or MultiLineString geometry"
.to_string(),
}),
}
}
pub fn length_linestring(linestring: &LineString, method: LengthMethod) -> Result<f64> {
if linestring.coords.len() < 2 {
return Err(AlgorithmError::InsufficientData {
operation: "length_linestring",
message: "linestring must have at least 2 coordinates".to_string(),
});
}
match method {
LengthMethod::Planar => Ok(length_linestring_planar(linestring)),
LengthMethod::Haversine => length_linestring_haversine(linestring),
LengthMethod::Vincenty => length_linestring_vincenty(linestring),
}
}
pub fn length_multilinestring(
multilinestring: &MultiLineString,
method: LengthMethod,
) -> Result<f64> {
if multilinestring.line_strings.is_empty() {
return Ok(0.0);
}
let mut total = 0.0;
for linestring in &multilinestring.line_strings {
total += length_linestring(linestring, method)?;
}
Ok(total)
}
pub fn length_linestring_3d(linestring: &LineString) -> Result<f64> {
if linestring.coords.len() < 2 {
return Err(AlgorithmError::InsufficientData {
operation: "length_linestring_3d",
message: "linestring must have at least 2 coordinates".to_string(),
});
}
let mut total_length = 0.0;
for i in 0..(linestring.coords.len() - 1) {
let p1 = &linestring.coords[i];
let p2 = &linestring.coords[i + 1];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let segment_length = if let (Some(z1), Some(z2)) = (p1.z, p2.z) {
let dz = z2 - z1;
(dx * dx + dy * dy + dz * dz).sqrt()
} else {
(dx * dx + dy * dy).sqrt()
};
total_length += segment_length;
}
Ok(total_length)
}
fn length_linestring_planar(linestring: &LineString) -> f64 {
let mut total_length = 0.0;
for i in 0..(linestring.coords.len() - 1) {
let p1 = &linestring.coords[i];
let p2 = &linestring.coords[i + 1];
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let segment_length = (dx * dx + dy * dy).sqrt();
total_length += segment_length;
}
total_length
}
fn length_linestring_haversine(linestring: &LineString) -> Result<f64> {
let mut total_length = 0.0;
for i in 0..(linestring.coords.len() - 1) {
let p1 = &linestring.coords[i];
let p2 = &linestring.coords[i + 1];
let segment_length = haversine_distance(p1, p2)?;
total_length += segment_length;
}
Ok(total_length)
}
fn length_linestring_vincenty(linestring: &LineString) -> Result<f64> {
let mut total_length = 0.0;
for i in 0..(linestring.coords.len() - 1) {
let p1 = &linestring.coords[i];
let p2 = &linestring.coords[i + 1];
let segment_length = vincenty_distance(p1, p2)?;
total_length += segment_length;
}
Ok(total_length)
}
fn haversine_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
let lat1 = c1.y * PI / 180.0;
let lon1 = c1.x * PI / 180.0;
let lat2 = c2.y * PI / 180.0;
let lon2 = c2.x * PI / 180.0;
if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "latitude",
message: "latitude must be between -90 and 90 degrees".to_string(),
});
}
let dlat = lat2 - lat1;
let dlon = lon2 - lon1;
let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
let radius = (WGS84_A + WGS84_B) / 2.0;
Ok(radius * c)
}
fn vincenty_distance(c1: &Coordinate, c2: &Coordinate) -> Result<f64> {
let lat1 = c1.y * PI / 180.0;
let lon1 = c1.x * PI / 180.0;
let lat2 = c2.y * PI / 180.0;
let lon2 = c2.x * PI / 180.0;
if lat1.abs() > PI / 2.0 || lat2.abs() > PI / 2.0 {
return Err(AlgorithmError::InvalidParameter {
parameter: "latitude",
message: "latitude must be between -90 and 90 degrees".to_string(),
});
}
let l = lon2 - lon1;
let u1 = ((1.0 - WGS84_F) * lat1.tan()).atan();
let u2 = ((1.0 - WGS84_F) * lat2.tan()).atan();
let sin_u1 = u1.sin();
let cos_u1 = u1.cos();
let sin_u2 = u2.sin();
let cos_u2 = u2.cos();
let mut lambda = l;
let mut lambda_prev;
let mut iter_count = 0;
let (sin_sigma, cos_sigma, sigma, cos_sq_alpha, cos_2sigma_m);
loop {
let sin_lambda = lambda.sin();
let cos_lambda = lambda.cos();
let sin_sigma_temp = ((cos_u2 * sin_lambda).powi(2)
+ (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
.sqrt();
if sin_sigma_temp.abs() < f64::EPSILON {
return Ok(0.0);
}
let cos_sigma_temp = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
let sigma_temp = sin_sigma_temp.atan2(cos_sigma_temp);
let sin_alpha_temp = cos_u1 * cos_u2 * sin_lambda / sin_sigma_temp;
let cos_sq_alpha_temp = 1.0 - sin_alpha_temp * sin_alpha_temp;
let cos_2sigma_m_temp = if cos_sq_alpha_temp.abs() < f64::EPSILON {
0.0
} else {
cos_sigma_temp - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha_temp
};
let c =
WGS84_F / 16.0 * cos_sq_alpha_temp * (4.0 + WGS84_F * (4.0 - 3.0 * cos_sq_alpha_temp));
lambda_prev = lambda;
lambda = l
+ (1.0 - c)
* WGS84_F
* sin_alpha_temp
* (sigma_temp
+ c * sin_sigma_temp
* (cos_2sigma_m_temp
+ c * cos_sigma_temp
* (-1.0 + 2.0 * cos_2sigma_m_temp * cos_2sigma_m_temp)));
iter_count += 1;
if (lambda - lambda_prev).abs() < VINCENTY_THRESHOLD || iter_count >= VINCENTY_MAX_ITER {
sin_sigma = sin_sigma_temp;
cos_sigma = cos_sigma_temp;
sigma = sigma_temp;
cos_sq_alpha = cos_sq_alpha_temp;
cos_2sigma_m = cos_2sigma_m_temp;
break;
}
}
if iter_count >= VINCENTY_MAX_ITER {
return Err(AlgorithmError::NumericalError {
operation: "vincenty_distance",
message: "failed to converge".to_string(),
});
}
let u_sq = cos_sq_alpha * (WGS84_A * WGS84_A - WGS84_B * WGS84_B) / (WGS84_B * WGS84_B);
let a = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
let b = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
let delta_sigma = b
* sin_sigma
* (cos_2sigma_m
+ b / 4.0
* (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m * cos_2sigma_m)
- b / 6.0
* cos_2sigma_m
* (-3.0 + 4.0 * sin_sigma * sin_sigma)
* (-3.0 + 4.0 * cos_2sigma_m * cos_2sigma_m)));
let distance = WGS84_B * a * (sigma - delta_sigma);
Ok(distance)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
fn create_linestring_2d() -> Result<LineString> {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(3.0, 0.0),
Coordinate::new_2d(3.0, 4.0),
];
LineString::new(coords).map_err(AlgorithmError::Core)
}
fn create_linestring_3d() -> Result<LineString> {
let coords = vec![
Coordinate::new_3d(0.0, 0.0, 0.0),
Coordinate::new_3d(3.0, 0.0, 0.0),
Coordinate::new_3d(3.0, 4.0, 0.0),
Coordinate::new_3d(3.0, 4.0, 5.0),
];
LineString::new(coords).map_err(AlgorithmError::Core)
}
#[test]
fn test_length_linestring_planar() {
let ls = create_linestring_2d();
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring(&linestring, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 7.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_linestring_3d() {
let ls = create_linestring_3d();
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring_3d(&linestring);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 12.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_linestring_single_point() {
let coords = vec![Coordinate::new_2d(0.0, 0.0)];
let ls = LineString::new(coords);
assert!(ls.is_err());
}
#[test]
fn test_length_linestring_two_points() {
let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(5.0, 0.0)];
let ls = LineString::new(coords);
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring(&linestring, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 5.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_multilinestring() {
let ls1 = create_linestring_2d();
let coords2 = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(10.0, 0.0)];
let ls2 = LineString::new(coords2);
assert!(ls1.is_ok() && ls2.is_ok());
if let (Ok(l1), Ok(l2)) = (ls1, ls2) {
let mls = MultiLineString::new(vec![l1, l2]);
let result = length_multilinestring(&mls, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 17.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_multilinestring_empty() {
let mls = MultiLineString::empty();
let result = length_multilinestring(&mls, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_eq!(len, 0.0);
}
}
#[test]
fn test_length_haversine() {
let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 0.0)];
let ls = LineString::new(coords);
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring(&linestring, LengthMethod::Haversine);
assert!(result.is_ok());
if let Ok(len) = result {
assert!(len > 110_000.0);
assert!(len < 112_000.0);
}
}
}
#[test]
fn test_length_vincenty() {
let coords = vec![Coordinate::new_2d(0.0, 0.0), Coordinate::new_2d(1.0, 0.0)];
let ls = LineString::new(coords);
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring(&linestring, LengthMethod::Vincenty);
assert!(result.is_ok());
if let Ok(len) = result {
assert!(len > 110_000.0);
assert!(len < 112_000.0);
}
}
}
#[test]
fn test_length_geometry_linestring() {
let ls = create_linestring_2d();
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let geom = Geometry::LineString(linestring);
let result = length(&geom, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 7.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_geometry_multilinestring() {
let ls = create_linestring_2d();
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let mls = MultiLineString::new(vec![linestring]);
let geom = Geometry::MultiLineString(mls);
let result = length(&geom, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 7.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_invalid_geometry() {
use oxigdal_core::vector::Point;
let point = Geometry::Point(Point::new(0.0, 0.0));
let result = length(&point, LengthMethod::Planar);
assert!(result.is_err());
}
#[test]
fn test_haversine_distance_basic() {
let c1 = Coordinate::new_2d(0.0, 0.0);
let c2 = Coordinate::new_2d(1.0, 1.0);
let result = haversine_distance(&c1, &c2);
assert!(result.is_ok());
if let Ok(dist) = result {
assert!(dist > 0.0);
assert!(dist < 200_000.0); }
}
#[test]
fn test_haversine_distance_same_point() {
let c1 = Coordinate::new_2d(0.0, 0.0);
let c2 = Coordinate::new_2d(0.0, 0.0);
let result = haversine_distance(&c1, &c2);
assert!(result.is_ok());
if let Ok(dist) = result {
assert!(dist.abs() < 1e-10);
}
}
#[test]
fn test_vincenty_distance_basic() {
let c1 = Coordinate::new_2d(0.0, 0.0);
let c2 = Coordinate::new_2d(1.0, 1.0);
let result = vincenty_distance(&c1, &c2);
assert!(result.is_ok());
if let Ok(dist) = result {
assert!(dist > 0.0);
assert!(dist < 200_000.0); }
}
#[test]
fn test_vincenty_distance_same_point() {
let c1 = Coordinate::new_2d(0.0, 0.0);
let c2 = Coordinate::new_2d(0.0, 0.0);
let result = vincenty_distance(&c1, &c2);
assert!(result.is_ok());
if let Ok(dist) = result {
assert!(dist.abs() < 1e-10);
}
}
#[test]
fn test_invalid_latitude() {
let c1 = Coordinate::new_2d(0.0, 95.0); let c2 = Coordinate::new_2d(0.0, 0.0);
let result = haversine_distance(&c1, &c2);
assert!(result.is_err());
let result2 = vincenty_distance(&c1, &c2);
assert!(result2.is_err());
}
#[test]
fn test_length_linestring_closed_ring() {
let coords = vec![
Coordinate::new_2d(0.0, 0.0),
Coordinate::new_2d(10.0, 0.0),
Coordinate::new_2d(10.0, 10.0),
Coordinate::new_2d(0.0, 10.0),
Coordinate::new_2d(0.0, 0.0),
];
let ls = LineString::new(coords);
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let result = length_linestring(&linestring, LengthMethod::Planar);
assert!(result.is_ok());
if let Ok(len) = result {
assert_relative_eq!(len, 40.0, epsilon = 1e-10);
}
}
}
#[test]
fn test_length_planar_equals_3d_for_2d() {
let ls = create_linestring_2d();
assert!(ls.is_ok());
if let Ok(linestring) = ls {
let planar = length_linestring(&linestring, LengthMethod::Planar);
let three_d = length_linestring_3d(&linestring);
assert!(planar.is_ok() && three_d.is_ok());
if let (Ok(p), Ok(td)) = (planar, three_d) {
assert_relative_eq!(p, td, epsilon = 1e-10);
}
}
}
}