From fd935b238f096b9982a20ad832379c363128a399 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Thu, 31 Oct 2024 16:00:29 -0700 Subject: [PATCH] InterpolatePoint: handle some reasonable edge cases, document current behavior in tests. --- geo/CHANGES.md | 1 + .../line_measures/interpolate_point.rs | 156 ++++++++++++++++++ .../line_measures/metric_spaces/geodesic.rs | 10 ++ .../line_measures/metric_spaces/haversine.rs | 6 + 4 files changed, 173 insertions(+) diff --git a/geo/CHANGES.md b/geo/CHANGES.md index ad1f414b0..b9961991c 100644 --- a/geo/CHANGES.md +++ b/geo/CHANGES.md @@ -2,6 +2,7 @@ ## Unreleased +* Improve handling of InterploatePoint with collapsed Line ## 0.29.0 - 2024.10.30 diff --git a/geo/src/algorithm/line_measures/interpolate_point.rs b/geo/src/algorithm/line_measures/interpolate_point.rs index d53bc226e..9f653b966 100644 --- a/geo/src/algorithm/line_measures/interpolate_point.rs +++ b/geo/src/algorithm/line_measures/interpolate_point.rs @@ -32,3 +32,159 @@ pub trait InterpolatePoint { include_ends: bool, ) -> impl Iterator>; } + +#[cfg(test)] +mod tests { + use crate::{Euclidean, Geodesic, Haversine, InterpolatePoint, Point, Rhumb}; + + #[test] + fn point_at_ratio_between_line_ends() { + let start = Point::new(0.0, 0.0); + let end = Point::new(1.0, 1.0); + + let ratio = 0.0; + assert_eq!(Haversine::point_at_ratio_between(start, end, ratio), start); + assert_eq!(Euclidean::point_at_ratio_between(start, end, ratio), start); + assert_eq!(Geodesic::point_at_ratio_between(start, end, ratio), start); + assert_eq!(Rhumb::point_at_ratio_between(start, end, ratio), start); + + let ratio = 1.0; + assert_eq!(Haversine::point_at_ratio_between(start, end, ratio), end); + assert_eq!(Euclidean::point_at_ratio_between(start, end, ratio), end); + assert_eq!(Geodesic::point_at_ratio_between(start, end, ratio), end); + assert_eq!(Rhumb::point_at_ratio_between(start, end, ratio), end); + } + + mod degenerate { + use super::*; + + #[test] + fn point_at_ratio_between_collapsed_line() { + let start = Point::new(1.0, 1.0); + + let ratio = 0.0; + assert_eq!( + Haversine::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!( + Euclidean::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!(Geodesic::point_at_ratio_between(start, start, ratio), start); + assert_eq!(Rhumb::point_at_ratio_between(start, start, ratio), start); + + let ratio = 0.5; + assert_eq!( + Haversine::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!( + Euclidean::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!(Geodesic::point_at_ratio_between(start, start, ratio), start); + assert_eq!(Rhumb::point_at_ratio_between(start, start, ratio), start); + + let ratio = 1.0; + assert_eq!( + Haversine::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!( + Euclidean::point_at_ratio_between(start, start, ratio), + start + ); + assert_eq!(Geodesic::point_at_ratio_between(start, start, ratio), start); + assert_eq!(Rhumb::point_at_ratio_between(start, start, ratio), start); + } + + #[test] + fn point_at_distance_between_collapsed_line() { + // This method just documents existing behavior. I don't think our current behavior + // is especially useful, but we might consider handling it uniformly one day. + let start: Point = Point::new(1.0, 1.0); + + let distance = 0.0; + assert_eq!( + Haversine::point_at_distance_between(start, start, distance), + start + ); + + let euclidean_result = Euclidean::point_at_distance_between(start, start, distance); + assert!(euclidean_result.x().is_nan()); + assert!(euclidean_result.y().is_nan()); + assert_eq!( + Geodesic::point_at_distance_between(start, start, distance), + start + ); + assert_eq!( + Rhumb::point_at_distance_between(start, start, distance), + start + ); + + let distance = 100000.0; + let due_north = Point::new(1.0, 1.9); + let due_south = Point::new(1.0, 0.1); + assert_relative_eq!( + Haversine::point_at_distance_between(start, start, distance), + due_north, + epsilon = 1.0e-1 + ); + let euclidean_result = Euclidean::point_at_distance_between(start, start, distance); + assert!(euclidean_result.x().is_nan()); + assert!(euclidean_result.y().is_nan()); + assert_relative_eq!( + Geodesic::point_at_distance_between(start, start, distance), + due_south, + epsilon = 1.0e-1 + ); + assert_relative_eq!( + Rhumb::point_at_distance_between(start, start, distance), + due_north, + epsilon = 1.0e-1 + ); + } + + #[test] + fn points_along_collapsed_line() { + let start = Point::new(1.0, 1.0); + + let max_distance = 1.0; + + let include_ends = true; + let points: Vec<_> = + Haversine::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![start, start]); + + let points: Vec<_> = + Euclidean::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![start, start]); + + let points: Vec<_> = + Geodesic::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![start, start]); + + let points: Vec<_> = + Rhumb::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![start, start]); + + let include_ends = false; + let points: Vec<_> = + Haversine::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![]); + + let points: Vec<_> = + Euclidean::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![]); + + let points: Vec<_> = + Geodesic::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![]); + + let points: Vec<_> = + Rhumb::points_along_line(start, start, max_distance, include_ends).collect(); + assert_eq!(points, vec![]); + } + } +} diff --git a/geo/src/algorithm/line_measures/metric_spaces/geodesic.rs b/geo/src/algorithm/line_measures/metric_spaces/geodesic.rs index 6e9ccb874..89335ca87 100644 --- a/geo/src/algorithm/line_measures/metric_spaces/geodesic.rs +++ b/geo/src/algorithm/line_measures/metric_spaces/geodesic.rs @@ -168,6 +168,9 @@ impl InterpolatePoint for Geodesic { end: Point, meters_from_start: f64, ) -> Point { + if meters_from_start == 0.0 { + return start; + } let bearing = Self::bearing(start, end); Self::destination(start, bearing, meters_from_start) } @@ -205,6 +208,13 @@ impl InterpolatePoint for Geodesic { end: Point, ratio_from_start: f64, ) -> Point { + if start == end || ratio_from_start == 0.0 { + return start; + } + if ratio_from_start == 1.0 { + return end; + } + let g = geographiclib_rs::Geodesic::wgs84(); let (total_distance, azi1, _azi2, _a12) = g.inverse(start.y(), start.x(), end.y(), end.x()); let distance = total_distance * ratio_from_start; diff --git a/geo/src/algorithm/line_measures/metric_spaces/haversine.rs b/geo/src/algorithm/line_measures/metric_spaces/haversine.rs index 971e7504e..ac4b02479 100644 --- a/geo/src/algorithm/line_measures/metric_spaces/haversine.rs +++ b/geo/src/algorithm/line_measures/metric_spaces/haversine.rs @@ -204,6 +204,12 @@ impl InterpolatePoint for Haversine { /// /// [great circle]: https://en.wikipedia.org/wiki/Great_circle fn point_at_ratio_between(start: Point, end: Point, ratio_from_start: F) -> Point { + if start == end || ratio_from_start == F::zero() { + return start; + } + if ratio_from_start == F::one() { + return end; + } let calculation = HaversineIntermediateFillCalculation::new(start, end); calculation.point_at_ratio(ratio_from_start) }