From 853686639ded8c894f963eb4676e0782bd4445f3 Mon Sep 17 00:00:00 2001 From: Dustin Carlino Date: Fri, 12 Jan 2024 12:22:25 +0000 Subject: [PATCH] Add in #1050 linesplit too --- .../algorithm/line_split/line_split_result.rs | 92 +++++ .../algorithm/line_split/line_split_trait.rs | 220 ++++++++++ .../line_split_trait_impl_for_line.rs | 375 ++++++++++++++++++ .../line_split_trait_impl_for_linestring.rs | 375 ++++++++++++++++++ .../line_split/line_split_twice_result.rs | 139 +++++++ .../line_split/measure_line_string.rs | 122 ++++++ geo/src/algorithm/line_split/mod.rs | 22 + geo/src/algorithm/mod.rs | 4 + 8 files changed, 1349 insertions(+) create mode 100644 geo/src/algorithm/line_split/line_split_result.rs create mode 100644 geo/src/algorithm/line_split/line_split_trait.rs create mode 100644 geo/src/algorithm/line_split/line_split_trait_impl_for_line.rs create mode 100644 geo/src/algorithm/line_split/line_split_trait_impl_for_linestring.rs create mode 100644 geo/src/algorithm/line_split/line_split_twice_result.rs create mode 100644 geo/src/algorithm/line_split/measure_line_string.rs create mode 100644 geo/src/algorithm/line_split/mod.rs diff --git a/geo/src/algorithm/line_split/line_split_result.rs b/geo/src/algorithm/line_split/line_split_result.rs new file mode 100644 index 000000000..716e7a5c0 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_result.rs @@ -0,0 +1,92 @@ +/// The result of splitting a line using +/// [LineSplit::line_split()](crate::algorithm::LineSplit::line_split) method. +/// It can contain between one and two [Lines](crate::Line) / [LineStrings](crate::LineString). +/// +/// Note that it may not be desireable to use a `match` statement directly on this type if you only +/// ever want one part of the split. For this please see the helper functions; +/// [.first()](LineSplitResult#method.first), +/// [.second()](LineSplitResult#method.second), +/// [.into_first()](LineSplitResult#method.into_first), and +/// [.into_second()](LineSplitResult#method.into_second). +/// +/// ``` +/// # use geo::{LineString, coord}; +/// # use geo::LineSplit; +/// +/// # let my_line = LineString::from(vec![coord! {x: 0., y: 0.},coord! {x: 1., y: 0.},]); +/// if let Some(result) = my_line.line_split_twice(0.2, 0.5) { +/// if let Some(second) = result.into_second() { +/// // got the 'second' part of the split line +/// // between the points 20% and 50% along its length +/// } +/// } +/// ``` +#[rustfmt::skip] +#[derive(PartialEq, Debug)] +pub enum LineSplitResult { + First (T ), + Second ( T), + FirstSecond (T, T), +} + +#[rustfmt::skip] +impl LineSplitResult{ + /// Return only the first of two split line parts, if it exists. + pub fn first(&self) -> Option<&T> { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + /// Return only the first of two split line parts, if it exists, consuming the result. + pub fn into_first(self) -> Option { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + /// Return only the second of two split line parts, if it exists. + pub fn second(&self) -> Option<&T> { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + /// Return only the second of two split line parts, if it exists, consuming the result. + pub fn into_second(self) -> Option { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + /// Return all two parts of the split line, if they exist. + /// + /// Instead of using this, consider using a match statement directly on the + /// [LineSplitResult] type; the reason is that some combinations of this type + /// (eg `(None, None)`) can never exist, but the compiler will still complain about missing arms + /// in your match statement. + pub fn as_tuple(&self) -> (Option<&T>, Option<&T>) { + match self { + Self::First (a ) => (Some(a), None ), + Self::Second ( b) => (None , Some(b)), + Self::FirstSecond(a, b) => (Some(a), Some(b)), + } + } + /// Return all two parts of the split line, if they exist, consuming the result. + /// + /// Instead of using this, consider using a match statement directly on the + /// [LineSplitResult] type; the reason is that some combinations of this type + /// (eg `(None, None)`) can never exist, but the compiler will still complain about missing arms + /// in your match statement. + pub fn into_tuple(self) -> (Option, Option) { + match self { + Self::First (a ) => (Some(a), None ), + Self::Second ( b) => (None , Some(b)), + Self::FirstSecond(a, b) => (Some(a), Some(b)), + } + } +} diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs new file mode 100644 index 000000000..decbf7bcd --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -0,0 +1,220 @@ +use geo_types::CoordFloat; + +use super::{LineSplitResult, LineSplitTwiceResult}; + +/// Defines functions to split a [Line](crate::Line) or [LineString](crate::LineString) +pub trait LineSplit +where + Self: Sized, + Scalar: CoordFloat, +{ + /// Split a [Line](crate::Line) or [LineString](crate::LineString) at some `fraction` of its length. + /// + /// `fraction` is any real number. Only values between 0.0 and 1.0 will split the line. + /// Values outside of this range (including infinite values) will be clamped to 0.0 or 1.0. + /// + /// Returns `None` when + /// - The provided `fraction` is NAN + /// - The the object being sliced includes NAN or infinite coordinates + /// + /// Otherwise returns [`Some(LineSplitResult)`](crate::algorithm::LineSplitResult) + /// + /// example + /// + /// ``` + /// use geo::{Line, coord}; + /// use geo::algorithm::{LineSplit, LineSplitResult}; + /// let line = Line::new( + /// coord! {x: 0.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, + /// ); + /// let result = line.line_split(0.6); + /// assert_eq!( + /// result, + /// Some(LineSplitResult::FirstSecond( + /// Line::new( + /// coord! {x: 0.0, y:0.0}, + /// coord! {x: 6.0, y:0.0}, + /// ), + /// Line::new( + /// coord! {x: 6.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, + /// ) + /// )) + /// ); + /// + /// match result { + /// Some(LineSplitResult::First(line1))=>{}, + /// Some(LineSplitResult::Second(line2))=>{}, + /// Some(LineSplitResult::FirstSecond(line1, line2))=>{}, + /// None=>{}, + /// } + /// ``` + fn line_split(&self, fraction: Scalar) -> Option>; + + /// + /// + /// example + /// + /// ``` + /// + /// ``` + /// > Note: Currently the default implementation of this function provided by the trait is + /// > inefficient because it uses repeated application of the + /// > [.line_split()](LineSplit::line_split) function. In future, types implementing this trait + /// > should override this with a more efficient algorithm if possible. + fn line_split_many(&self, fractions: &Vec) -> Option>> + where + Self: Clone, + { + match fractions.len() { + 0 => None, + 1 => self.line_split(fractions[0]).map(|item| { + let (a, b) = item.into_tuple(); + vec![a, b] + }), + _ => { + let mut fractions: Vec = fractions + .iter() + .map(|item| item.min(Scalar::one()).max(Scalar::zero())) + .collect(); + fractions + .sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + fractions.insert(0, Scalar::zero()); + fractions.push(Scalar::one()); + let fractions = fractions; // remove mutability + let mut output: Vec> = Vec::new(); + let mut remaining_self = Some(self.clone()); + for fraction in fractions.windows(2) { + // cannot be irrefutably unwrapped in for loop *sad crab noises*: + let (a, b) = match fraction { + &[a, b] => (a, b), + _ => return None, + }; + let fraction_interval = b - a; + let fraction_to_end = Scalar::one() - a; + let next_fraction = fraction_interval / fraction_to_end; + remaining_self = if let Some(remaining_self) = remaining_self { + match remaining_self.line_split(next_fraction) { + Some(LineSplitResult::FirstSecond(line1, line2)) => { + output.push(Some(line1)); + Some(line2) + } + Some(LineSplitResult::First(line1)) => { + output.push(Some(line1)); + None + } + Some(LineSplitResult::Second(line2)) => { + output.push(None); + Some(line2) + } + None => return None, + } + } else { + output.push(None); + None + } + } + + Some(output) + } + } + } + + /// Split a [Line](crate::Line) or [LineString](crate::LineString) + /// at `fraction_start` and at `fraction_end`. + /// + /// `fraction_start`/`fraction_end` are any real numbers. Only values between 0.0 and 1.0 will + /// split the line. Values outside of this range (including infinite values) will be clamped to + /// 0.0 or 1.0. + /// + /// If `fraction_start > fraction_end`, then the values will be swapped prior splitting. + /// + /// Returns [None] when + /// - Either`fraction_start` or `fraction_end` are NAN + /// - The the object being sliced includes NAN or infinite coordinates + /// + /// Otherwise Returns a [`Some(LineSplitTwiceResult)`](LineSplitTwiceResult) + /// + /// A [`LineSplitTwiceResult`](LineSplitTwiceResult) can contain between one and three + /// line parts where `T` is either [Line](crate::Line) or [LineString](crate::LineString). + /// + /// Note that [LineSplitTwiceResult] provides various helper methods to get the desired part(s) + /// of the output. + /// + /// The following example shows how to always obtain the "middle" part between the two splits + /// using the [`.into_second()`](LineSplitTwiceResult#method.into_second) method: + /// + /// ``` + /// use geo::{LineString, line_string}; + /// use geo::algorithm::{LineSplit, EuclideanLength}; + /// use approx::assert_relative_eq; + /// let my_road_line_string:LineString = line_string![ + /// (x: 0.0,y: 0.0), + /// (x:10.0,y: 0.0), + /// (x:10.0,y:10.0), + /// ]; + /// let my_road_len = my_road_line_string.euclidean_length(); + /// let fraction_from = 5.0 / my_road_len; + /// let fraction_to = 12.0 / my_road_len; + /// // Extract the road section between `fraction_from` and `fraction_to` using `.into_second()` + /// let my_road_section = match my_road_line_string.line_split_twice(fraction_from, fraction_to) { + /// Some(result) => match result.into_second() { // get the second part of the result + /// Some(linestring)=>Some(linestring), + /// _=>None + /// }, + /// _=>None + /// }; + /// assert_relative_eq!(my_road_section.unwrap(), line_string![ + /// (x: 5.0,y: 0.0), + /// (x:10.0,y: 0.0), + /// (x:10.0,y: 2.0), + /// ]); + /// ``` + /// + #[rustfmt::skip] + fn line_split_twice( + &self, + fraction_start: Scalar, + fraction_end: Scalar, + ) -> Option> { + // import enum variants + use LineSplitTwiceResult::*; + // reject nan fractions + if fraction_start.is_nan() || fraction_end.is_nan() { + return None; + } + // clamp + let fraction_start = fraction_start.min(Scalar::one()).max(Scalar::zero()); + let fraction_end = fraction_end.min(Scalar::one()).max(Scalar::zero()); + + // swap interval if incorrectly ordered + let (start_fraction, end_fraction) = if fraction_start > fraction_end { + (fraction_end, fraction_start) + } else { + (fraction_start, fraction_end) + }; + + // find the fraction to split the second portion of the line + let second_fraction = + (end_fraction - start_fraction) + / (Scalar::one() - start_fraction); + + match self.line_split(start_fraction) { + Some(LineSplitResult::FirstSecond(line1, line2)) => match line2.line_split(second_fraction) { + Some(LineSplitResult::FirstSecond(line2, line3)) => Some(FirstSecondThird(line1, line2, line3)), + Some(LineSplitResult::First (line2 )) => Some(FirstSecond (line1, line2 )), + Some(LineSplitResult::Second ( line3)) => Some(FirstThird (line1, line3)), + None => None, + }, + Some(LineSplitResult::First (line1)) => Some(First(line1)), + Some(LineSplitResult::Second(line2)) => match line2.line_split(second_fraction) { + Some(LineSplitResult::FirstSecond(line2, line3)) => Some(SecondThird ( line2, line3)), + Some(LineSplitResult::First (line2 )) => Some(Second ( line2 )), + Some(LineSplitResult::Second ( line3)) => Some(Third ( line3)), + None => None, + }, + None => None, + } + } +} diff --git a/geo/src/algorithm/line_split/line_split_trait_impl_for_line.rs b/geo/src/algorithm/line_split/line_split_trait_impl_for_line.rs new file mode 100644 index 000000000..024982de7 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait_impl_for_line.rs @@ -0,0 +1,375 @@ +use super::{LineSplit, LineSplitResult}; +use crate::Vector2DOps; +use geo_types::{CoordFloat, Line}; + +impl LineSplit for Line +where + Scalar: CoordFloat, +{ + /// Split a [Line] or at some `fraction` of its length. + /// + /// The `fraction` argument is any real number. + /// Only values between 0.0 and 1.0 will split the line. + /// Values outside of this range (including infinite values) will be clamped to 0.0 or 1.0. + /// + /// Returns [None] when + /// - The provided `fraction` is NAN + /// - The the object being sliced includes NAN or infinite coordinates + /// + /// Otherwise Returns a [`Some(LineSplitResult)`](LineSplitResult) + /// + /// example + /// + /// ``` + /// use geo::{Line, coord}; + /// use geo::algorithm::{LineSplit, LineSplitResult}; + /// let line = Line::new( + /// coord! {x: 0.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, + /// ); + /// let result = line.line_split(0.6); + /// assert_eq!( + /// result, + /// Some(LineSplitResult::FirstSecond( + /// Line::new( + /// coord! {x: 0.0, y:0.0}, + /// coord! {x: 6.0, y:0.0}, + /// ), + /// Line::new( + /// coord! {x: 6.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, + /// ) + /// )) + /// ); + /// ``` + fn line_split(&self, fraction: Scalar) -> Option> { + if fraction.is_nan() { + return None; + } + if fraction <= Scalar::zero() { + Some(LineSplitResult::Second(*self)) + } else if fraction >= Scalar::one() { + Some(LineSplitResult::First(*self)) + } else { + let new_midpoint = self.start + self.delta() * fraction; + if new_midpoint.is_finite() { + Some(LineSplitResult::FirstSecond( + Line::new(self.start, new_midpoint), + Line::new(new_midpoint, self.end), + )) + } else { + None + } + } + } +} + +#[cfg(test)] +mod test { + use super::super::LineSplitTwiceResult; + use super::*; + use geo_types::coord; + + // ============================================================================================= + // Line::line_split() + // ============================================================================================= + + #[test] + fn test_line_split_first_second() { + // simple x-axis aligned check + let line = Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split(0.6); + assert_eq!( + result, + Some(LineSplitResult::FirstSecond( + Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x: 6.0_f32, y:0.0_f32}, + ), + Line::new( + coord! {x: 6.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ) + )) + ); + + // simple y-axis aligned check + let line = Line::new( + coord! {x:0.0_f32, y: 0.0_f32}, + coord! {x:0.0_f32, y:10.0_f32}, + ); + let result = line.line_split(0.3); + assert_eq!( + result, + Some(LineSplitResult::FirstSecond( + Line::new(coord! {x:0.0_f32, y:0.0_f32}, coord! {x:0.0_f32, y:3.0_f32},), + Line::new( + coord! {x:0.0_f32, y:3.0_f32}, + coord! {x:0.0_f32, y:10.0_f32}, + ) + )) + ); + + // non_trivial check + let line = Line::new( + coord! {x: 1.0_f32, y: 1.0_f32}, + coord! {x:10.0_f32, y:-10.0_f32}, + ); + let split_point = line.start + line.delta() * 0.7; + let result = line.line_split(0.7); + assert_eq!( + result, + Some(LineSplitResult::FirstSecond( + Line::new(line.start, split_point,), + Line::new(split_point, line.end,) + )) + ); + } + + #[test] + fn test_line_split_first() { + // test one + let line = Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split(1.0); + assert_eq!(result, Some(LineSplitResult::First(line))); + + // Test numbers larger than one + let line = Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split(2.0); + assert_eq!(result, Some(LineSplitResult::First(line))); + } + #[test] + fn test_line_split_second() { + // test zero + let line = Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split(0.0); + assert_eq!(result, Some(LineSplitResult::Second(line))); + + // Test negative numbers + let line = Line::new( + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split(-2.0); + assert_eq!(result, Some(LineSplitResult::Second(line))); + } + + // ============================================================================================= + // Line::line_split_twice() + // ============================================================================================= + + macro_rules! test_line_split_twice_helper{ + ($a:expr, $b:expr, $enum_variant:ident, $(($x1:expr, $x2:expr)),*)=>{{ + let line = Line::new( + coord!{x: 0.0_f32, y:0.0_f32}, + coord!{x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split_twice($a, $b).unwrap(); + // println!("{result:?}"); + assert_eq!( + result, + LineSplitTwiceResult::$enum_variant( + $( + Line::new( + coord!{x: $x1, y:0.0_f32}, + coord!{x: $x2, y:0.0_f32}, + ), + )* + ) + ); + }} + } + + #[test] + fn test_line_split_twice() { + test_line_split_twice_helper!( + 0.6, + 0.8, + FirstSecondThird, + (0.0, 6.0), + (6.0, 8.0), + (8.0, 10.0) + ); + test_line_split_twice_helper!(0.6, 1.0, FirstSecond, (0.0, 6.0), (6.0, 10.0)); + test_line_split_twice_helper!(0.6, 0.6, FirstThird, (0.0, 6.0), (6.0, 10.0)); + test_line_split_twice_helper!(0.0, 0.6, SecondThird, (0.0, 6.0), (6.0, 10.0)); + test_line_split_twice_helper!(1.0, 1.0, First, (0.0, 10.0)); + test_line_split_twice_helper!(0.0, 1.0, Second, (0.0, 10.0)); + test_line_split_twice_helper!(0.0, 0.0, Third, (0.0, 10.0)); + } + + // ============================================================================================= + // Line::line_split_many() + // ============================================================================================= + + #[test] + fn test_line_split_many() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![0.1, 0.2, 0.5]); + assert_eq!( + result, + Some(vec![ + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 1.0, y: 0.0 }, + coord! { x: 2.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 2.0, y: 0.0 }, + coord! { x: 5.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 5.0, y: 0.0 }, + coord! { x: 10.0, y: 0.0 }, + )) + ]) + ); + } + + #[test] + fn test_line_split_many_edge_right() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![0.1, 0.2, 2.0]); + assert_eq!( + result, + Some(vec![ + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 1.0, y: 0.0 }, + coord! { x: 2.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 2.0, y: 0.0 }, + coord! { x:10.0, y: 0.0 }, + )), + None + ]) + ); + } + + #[test] + fn test_line_split_many_double_edge_right() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![0.1, 1.2, 2.0]); + assert_eq!( + result, + Some(vec![ + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 1.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 1.0, y: 0.0 }, + coord! { x:10.0, y: 0.0 }, + )), + None, + None + ]) + ); + } + + #[test] + fn test_line_split_many_edge_left() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![-1.0, 0.2, 0.5]); + assert_eq!( + result, + Some(vec![ + None, + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 2.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 2.0, y: 0.0 }, + coord! { x: 5.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 5.0, y: 0.0 }, + coord! { x: 10.0, y: 0.0 }, + )) + ]) + ); + } + + #[test] + fn test_line_split_many_double_edge_left() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![-1.0, -0.5, 0.5]); + assert_eq!( + result, + Some(vec![ + None, + None, + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 5.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 5.0, y: 0.0 }, + coord! { x: 10.0, y: 0.0 }, + )) + ]) + ); + } + + #[test] + fn test_line_split_many_same_value() { + let line = Line::new( + coord! {x: 0.0, y:0.0}, + coord! {x:10.0, y:0.0}, + ); + let result = line.line_split_many(&vec![0.2, 0.2, 0.5]); + assert_eq!( + result, + Some(vec![ + Some(Line::new( + coord! { x: 0.0, y: 0.0 }, + coord! { x: 2.0, y: 0.0 }, + )), + None, + Some(Line::new( + coord! { x: 2.0, y: 0.0 }, + coord! { x: 5.0, y: 0.0 }, + )), + Some(Line::new( + coord! { x: 5.0, y: 0.0 }, + coord! { x: 10.0, y: 0.0 }, + )) + ]) + ); + } +} diff --git a/geo/src/algorithm/line_split/line_split_trait_impl_for_linestring.rs b/geo/src/algorithm/line_split/line_split_trait_impl_for_linestring.rs new file mode 100644 index 000000000..4cfaa5289 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait_impl_for_linestring.rs @@ -0,0 +1,375 @@ +use geo_types::{CoordFloat, Line, LineString}; + +use super::{measure_line_string, LineSplit, LineSplitResult, LineStringMeasurements}; + +impl LineSplit for LineString +where + Scalar: CoordFloat + std::iter::Sum, +{ + /// Split a [LineString] or at some `fraction` of its length. + /// + /// The `fraction` argument is any real number. + /// Only values between 0.0 and 1.0 will split the line. + /// Values outside of this range (including infinite values) will be clamped to 0.0 or 1.0. + /// + /// Returns [None] when + /// - The provided `fraction` is NAN + /// - The the object being sliced includes NAN or infinite coordinates + /// + /// Otherwise Returns a [`Some(LineSplitResult)`](LineSplitResult) + /// + /// example + /// + /// ``` + /// use geo::{LineString, line_string}; + /// use geo::algorithm::{LineSplit, LineSplitResult}; + /// let line = line_string![ + /// (x: 0.0, y: 0.0), + /// (x:10.0, y: 0.0), + /// (x:10.0, y:10.0), + /// ]; + /// let result = line.line_split(0.6); + /// assert_eq!( + /// result, + /// Some(LineSplitResult::FirstSecond( + /// line_string![ + /// (x: 0.0, y: 0.0), + /// (x:10.0, y: 0.0), + /// (x:10.0, y: 2.0), + /// ], + /// line_string![ + /// (x:10.0, y: 2.0), + /// (x:10.0, y:10.0), + /// ] + /// )) + /// ); + /// ``` + fn line_split(&self, fraction: Scalar) -> Option> { + // import enum variants + use LineSplitResult::*; + if fraction.is_nan() { + return None; + } + match (fraction <= Scalar::zero(), fraction >= Scalar::one()) { + (false, true) => Some(First(self.clone())), + (true, false) => Some(Second(self.clone())), + _ => { + + // measure linestring (rejects linestrings with less than 2 points) + let LineStringMeasurements { + length_total, + length_segments, + } = measure_line_string(self)?; + + // Reject line strings with zero length, nan values, or infinite values; + if !Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { + return None; + } + + // Find the length of the first part of the line string before the split; + let length_fraction = fraction * length_total; + // Set up some variables to track state in the for-loop + let mut length_accumulated = Scalar::zero(); + let mut coords_first_part = vec![*self.0.first()?]; + let mut coords_second_part = Vec::new(); + + for (fractions, &length_segment) in + self.0.as_slice().windows(2).zip(length_segments.iter()) + { + // cannot be irrefutably unwrapped in for loop *sad crab noises*: + let (a, b) = match fractions { + &[a, b] => (a, b), + _ => return None, + }; + let length_accumulated_before_segment = length_accumulated; + length_accumulated = length_accumulated + length_segment; + let length_accumulated_after_segment = length_accumulated; + if length_fraction < length_accumulated_before_segment { + coords_second_part.push(b); + } else if length_fraction >= length_accumulated_after_segment { + coords_first_part.push(b); + } else { + // Note: division by zero when calculating fraction_to_split_segment should + // not be possible; length_segment must be > 0 after the above two + // branches of the if statement. + let fraction_to_split_segment = + (length_fraction - length_accumulated_before_segment) / length_segment; + match Line::new(a, b).line_split(fraction_to_split_segment) { + Some(FirstSecond(line1, _line2)) => { + coords_first_part.push(line1.end); + coords_second_part.push(line1.end); + coords_second_part.push(b); + } + Some(First(_line1)) => { + coords_first_part.push(b); + coords_second_part.push(b); + } + Some(Second(_line2)) => { + coords_second_part.push(a); + coords_second_part.push(b); + } + None => return None, // probably never? + } + } + } + Some(FirstSecond( + coords_first_part.into(), + coords_second_part.into(), + )) + } + } + } +} + +#[cfg(test)] +mod test { + use geo_types::{coord, line_string}; + + use super::super::LineSplitTwiceResult; + + use super::*; + // ============================================================================================= + // LineString::line_split() + // ============================================================================================= + + #[test] + fn split() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + ]; + let mid_line = line_string.lines().nth(1).unwrap(); + let slice_point = mid_line.start + mid_line.delta() * 0.5; + assert_eq!( + line_string.line_split(0.5), + Some(LineSplitResult::FirstSecond( + LineString::new(vec![line_string.0[0], line_string.0[1], slice_point]), + LineString::new(vec![slice_point, line_string.0[2], line_string.0[3]]) + )) + ); + } + + #[test] + fn split_on_point() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + (x:2.0, y:2.0), + ]; + let slice_point = coord! {x:1.0, y:1.0}; + assert_eq!( + line_string.line_split(0.5), + Some(LineSplitResult::FirstSecond( + LineString::new(vec![line_string.0[0], line_string.0[1], slice_point]), + LineString::new(vec![slice_point, line_string.0[3], line_string.0[4]]) + )) + ); + } + + #[test] + fn split_half_way_through_last_segment() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + ]; + let slice_point = coord! {x:1.0, y:0.5}; + assert_eq!( + line_string.line_split(0.75), + Some(LineSplitResult::FirstSecond( + LineString::new(vec![line_string.0[0], line_string.0[1], slice_point]), + LineString::new(vec![slice_point, line_string.0[2]]) + )) + ); + } + + #[test] + fn split_half_way_through_first_segment() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + ]; + let slice_point = coord! {x:0.5, y:0.0}; + assert_eq!( + line_string.line_split(0.25), + Some(LineSplitResult::FirstSecond( + LineString::new(vec![line_string.0[0], slice_point]), + LineString::new(vec![slice_point, line_string.0[1], line_string.0[2]]) + )) + ); + } + + #[test] + fn split_first() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + ]; + assert_eq!( + line_string.line_split(1.0), + Some(LineSplitResult::First(line_string)) + ); + } + + #[test] + fn split_second() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + ]; + assert_eq!( + line_string.line_split(0.0), + Some(LineSplitResult::Second(line_string)) + ); + } + + // ============================================================================================= + // LineString::line_split_twice() + // ============================================================================================= + #[test] + fn line_split_twice_typical_1() { + // I think if we exhaustively check + // - `Line::line_split_twice()` and + // - `LineString::line_split()` + // then because the implementation for `line_split_twice` is shared + // we don't need an exhaustive check for `LineString::line_split_twice()` + // So I will just do a spot check for a typical case + + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + (x:2.0, y:2.0), + ]; + let result = line_string.line_split_twice(0.25, 0.5).unwrap(); + assert_eq!( + result, + LineSplitTwiceResult::FirstSecondThird( + line_string![ + (x: 0.0, y:0.0_f32), + (x: 1.0, y:0.0_f32), + ], + line_string![ + (x: 1.0, y:0.0_f32), + (x: 1.0, y:1.0_f32), + ], + line_string![ + (x: 1.0, y:1.0_f32), + (x: 2.0, y:1.0_f32), + (x: 2.0, y:2.0_f32), + ], + ) + ); + } + + #[test] + fn line_split_twice_typical_2() { + use crate::EuclideanLength; + let my_road_line_string:LineString = line_string![ + (x: 0.0,y: 0.0), + (x:10.0,y: 0.0), + (x:10.0,y:10.0), + ]; + let my_road_len = my_road_line_string.euclidean_length(); + let fraction_from = 5.0 / my_road_len; + let fraction_to = 12.0 / my_road_len; + // Extract the road section between `fraction_from` and `fraction_to` using `.into_second()` + let my_road_section = match my_road_line_string.line_split_twice(fraction_from, fraction_to) { + Some(result) => match result.into_second() { // get the second part of the result + Some(linestring)=>Some(linestring), + _=>None + }, + _=>None + }; + assert_relative_eq!(my_road_section.unwrap(), line_string![ + (x: 5.0,y: 0.0), + (x:10.0,y: 0.0), + (x:10.0,y: 2.0), + ]); + } + + // ============================================================================================= + // LineString::line_split_many() + // I think if we exhaustively check + // - `Line::line_split_many()` and + // - `LineString::line_split()` + // then because the implementation for `line_split_many` is shared + // we don't need an exhaustive check for `LineString::line_split_many()` + // So I will just do a few spot checks for some typical cases + // ============================================================================================= + #[test] + fn line_split_many_typical_1() { + + + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + (x:2.0, y:2.0), + ]; + let result = line_string + .line_split_many(&vec![0.25, 0.5, 0.625]) + .unwrap(); + assert_eq!( + result, + vec![ + Some(line_string![ + (x: 0.0, y:0.0_f32), + (x: 1.0, y:0.0_f32), + ]), + Some(line_string![ + (x: 1.0, y:0.0_f32), + (x: 1.0, y:1.0_f32), + ]), + Some(line_string![ + (x: 1.0, y:1.0_f32), + (x: 1.5, y:1.0_f32), + ]), + Some(line_string![ + (x: 1.5, y:1.0_f32), + (x: 2.0, y:1.0_f32), + (x: 2.0, y:2.0_f32), + ]), + ] + ); + } + #[test] + fn line_split_many_typical_2() { + use crate::EuclideanLength; + let line_string:LineString = line_string![ + (x: 0.0,y: 0.0), + (x:10.0,y: 0.0), + (x:10.0,y:10.0), + ]; + let my_road_len = line_string.euclidean_length(); + let fraction_from:f32 = 5.0 / my_road_len; + let fraction_to:f32 = 12.0 / my_road_len; + // Extract the road section between `fraction_from` and `fraction_to` using `.into_second()` + let result = line_string.line_split_many(&vec![fraction_from, fraction_to]); + assert_eq!( + result.unwrap(), + vec![ + Some(line_string![ + (x: 0.0, y: 0.0_f32), + (x: 5.0, y: 0.0_f32), + ]), + Some(line_string![ + (x: 5.0, y: 0.0_f32), + (x:10.0, y: 0.0_f32), + (x:10.0, y: 2.0_f32), + ]), + Some(line_string![ + (x:10.0, y: 2.0_f32), + (x:10.0, y:10.0_f32), + ]), + ] + ); + } +} diff --git a/geo/src/algorithm/line_split/line_split_twice_result.rs b/geo/src/algorithm/line_split/line_split_twice_result.rs new file mode 100644 index 000000000..2e5af8778 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_twice_result.rs @@ -0,0 +1,139 @@ +/// The result of splitting a line twice using +/// [LineSplit::line_split_twice()](crate::algorithm::LineSplit::line_split_twice) method. +/// It can contain between one and three [Line](crate::Line)s / [LineString](crate::LineString)s. +/// +/// Note that it may not be desireable to use a `match` statement directly on this type if you only +/// ever want one part of the split. For this please see the helper functions; +/// [.first()](LineSplitTwiceResult#method.first), +/// [.second()](LineSplitTwiceResult#method.second), +/// [.third()](LineSplitTwiceResult#method.third), +/// [.into_first()](LineSplitTwiceResult#method.into_first), +/// [.into_second()](LineSplitTwiceResult#method.into_second), and +/// [.into_third()](LineSplitTwiceResult#method.into_third). +/// +/// ``` +/// // get the second part between splits; +/// let mid_part = my_line.line_split_twice(0.2, 0.5).unwrap().into_second().unwrap(); +/// ``` +/// +/// To get more than one part, consider using consider using +/// [.into_tuple()](LineSplitTwiceResult#method.into_tuple): +/// +/// ``` +/// match my_line.line_split_twice(0.2, 0.5).unwrap().into_tuple() { +/// (Some(first), Some(second), _) => todo!(), +/// _ => None +/// } +/// ``` +#[derive(PartialEq, Debug)] +#[rustfmt::skip] +pub enum LineSplitTwiceResult { + First (T ), + Second ( T ), + Third ( T), + FirstSecond (T, T ), + SecondThird ( T, T), + FirstThird (T, T), + FirstSecondThird (T, T, T), +} + +#[rustfmt::skip] +impl LineSplitTwiceResult { + + /// Return only the first of three split line parts, if it exists. + pub fn first(&self) -> Option<&T> { + match self { + Self::First (x ) => Some(x), + Self::Second ( _ ) => None, + Self::Third ( _) => None, + Self::FirstSecond (x, _ ) => Some(x), + Self::SecondThird ( _, _) => None, + Self::FirstThird (x, _) => Some(x), + Self::FirstSecondThird(x, _, _) => Some(x), + } + } + /// Return only the first of three split line parts, if it exists, consuming the result + pub fn into_first(self) -> Option { + match self { + Self::First (x ) => Some(x), + Self::Second ( _ ) => None, + Self::Third ( _) => None, + Self::FirstSecond (x, _ ) => Some(x), + Self::SecondThird ( _, _) => None, + Self::FirstThird (x, _) => Some(x), + Self::FirstSecondThird(x, _, _) => Some(x), + } + } + /// Return only the second of three split line parts, if it exists + pub fn second(&self) -> Option<&T> { + match self { + Self::First (_ ) => None, + Self::Second ( x ) => Some(x), + Self::Third ( _) => None, + Self::FirstSecond (_, x ) => Some(x), + Self::SecondThird ( x, _) => Some(x), + Self::FirstThird (_, _) => None, + Self::FirstSecondThird(_, x, _) => Some(x), + } + } + /// Return only the second of three split line parts, if it exists, consuming the result + pub fn into_second(self) -> Option { + match self { + Self::First (_ ) => None, + Self::Second ( x ) => Some(x), + Self::Third ( _) => None, + Self::FirstSecond (_, x ) => Some(x), + Self::SecondThird ( x, _) => Some(x), + Self::FirstThird (_, _) => None, + Self::FirstSecondThird(_, x, _) => Some(x), + } + } + /// Return only the third of three split line parts, if it exists + pub fn third(&self) -> Option<&T> { + match self { + Self::First (_ ) => None, + Self::Second ( _ ) => None, + Self::Third ( x) => Some(x), + Self::FirstSecond (_, _ ) => None, + Self::SecondThird ( _, x) => Some(x), + Self::FirstThird (_, x) => Some(x), + Self::FirstSecondThird(_, _, x) => Some(x), + } + } + /// Return only the third of three split line parts, if it exists, consuming the result + pub fn into_third(self) -> Option { + match self { + Self::First (_ ) => None, + Self::Second ( _ ) => None, + Self::Third ( x) => Some(x), + Self::FirstSecond (_, _ ) => None, + Self::SecondThird ( _, x) => Some(x), + Self::FirstThird (_, x) => Some(x), + Self::FirstSecondThird(_, _, x) => Some(x), + } + } + /// Return all three parts of the split line, if they exist + pub fn as_tuple(&self) -> (Option<&T>, Option<&T>, Option<&T>) { + match self { + Self::First (a ) => (Some(a), None , None ), + Self::Second ( b ) => (None , Some(b), None ), + Self::Third ( c) => (None , None , Some(c)), + Self::FirstSecond (a, b ) => (Some(a), Some(b), None ), + Self::SecondThird ( b, c) => (None , Some(b), Some(c)), + Self::FirstThird (a, c) => (Some(a), None , Some(c)), + Self::FirstSecondThird(a, b, c) => (Some(a), Some(b), Some(c)), + } + } + /// Return all three parts of the split line, if they exist, consuming the result + pub fn into_tuple(self) -> (Option, Option, Option) { + match self { + Self::First (a ) => (Some(a), None , None ), + Self::Second ( b ) => (None , Some(b), None ), + Self::Third ( c) => (None , None , Some(c)), + Self::FirstSecond (a, b ) => (Some(a), Some(b), None ), + Self::SecondThird ( b, c) => (None , Some(b), Some(c)), + Self::FirstThird (a, c) => (Some(a), None , Some(c)), + Self::FirstSecondThird(a, b, c) => (Some(a), Some(b), Some(c)), + } + } +} diff --git a/geo/src/algorithm/line_split/measure_line_string.rs b/geo/src/algorithm/line_split/measure_line_string.rs new file mode 100644 index 000000000..5d17ed871 --- /dev/null +++ b/geo/src/algorithm/line_split/measure_line_string.rs @@ -0,0 +1,122 @@ +use crate::CoordFloat; +use crate::EuclideanLength; +use crate::Line; +use crate::LineString; + +/// The result of the [measure_line_string] function +#[derive(PartialEq, Debug)] +pub struct LineStringMeasurements { + /// Total length of the [LineString] + pub length_total: Scalar, + /// The length of each segment ([Line]) in the [LineString] + pub length_segments: Vec, +} + +/// Measure a [LineString] and return [`Option`](LineStringMeasurements); +/// The result contains both the `total_length` and the `length_segments` (the length +/// of each segment or [Line]) +/// +/// Returns [None] +/// - if the [LineString] has less than two [coords](crate::Coord) +/// - if total_length is not finite +pub fn measure_line_string( + line_string: &LineString, +) -> Option> +where + Scalar: CoordFloat, + Line: EuclideanLength, +{ + if line_string.0.len() < 2 { + return None; + } + let result = line_string.lines().fold( + LineStringMeasurements { + length_total: Scalar::zero(), + length_segments: Vec::new(), + }, + |LineStringMeasurements { + length_total, + mut length_segments, + }, + current| { + let segment_length = current.euclidean_length(); + length_segments.push(segment_length); + LineStringMeasurements { + length_total: length_total + segment_length, + length_segments, + } + }, + ); + if !result.length_total.is_finite() { + None + } else { + Some(result) + } +} + +#[cfg(test)] +mod test { + + use geo_types::{line_string, LineString}; + + use super::{measure_line_string, LineStringMeasurements}; + + #[test] + fn measure_line_string_typical() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:0.0), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + ]; + let LineStringMeasurements { + length_total, + length_segments, + } = measure_line_string(&line_string).unwrap(); + assert_eq!(length_total, 3.0); + assert_eq!(length_segments, vec![1.0_f32, 1.0_f32, 1.0_f32]); + } + + #[test] + fn measure_line_string_malformed_zero() { + let line_string: LineString = line_string![]; + assert!(measure_line_string(&line_string).is_none()); + } + + #[test] + fn measure_line_string_malformed_one() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + ]; + assert!(measure_line_string(&line_string).is_none()); + } + + #[test] + fn measure_line_string_malformed_nan() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:f32::NAN), + ]; + assert!(measure_line_string(&line_string).is_none()); + } + + #[test] + fn measure_line_string_malformed_nan2() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:f32::NAN), + (x:1.0, y:1.0), + (x:2.0, y:1.0), + ]; + assert!(measure_line_string(&line_string).is_none()); + } + + #[test] + fn measure_line_string_malformed_inf() { + let line_string: LineString = line_string![ + (x:0.0, y:0.0), + (x:1.0, y:f32::INFINITY), + ]; + assert!(measure_line_string(&line_string).is_none()); + } +} diff --git a/geo/src/algorithm/line_split/mod.rs b/geo/src/algorithm/line_split/mod.rs new file mode 100644 index 000000000..c43194111 --- /dev/null +++ b/geo/src/algorithm/line_split/mod.rs @@ -0,0 +1,22 @@ +/// LineSplit Trait +mod line_split_trait; +pub use line_split_trait::LineSplit; + +/// Implementations for LineSplit Trait for Line +mod line_split_trait_impl_for_line; + +/// Implementations for LineSplit Trait for LineString +mod line_split_trait_impl_for_linestring; + +/// Result types for LineSplit::line_split +mod line_split_result; +pub use line_split_result::LineSplitResult; + +/// Result types for LineSplit::line_split_twice +mod line_split_twice_result; +pub use line_split_twice_result::LineSplitTwiceResult; + +/// Helper function to measure the total length +/// of a LineString at the same time as the length of each segment +mod measure_line_string; +use measure_line_string::{measure_line_string, LineStringMeasurements}; diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index ca11aed94..f1f54d77f 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -184,6 +184,10 @@ pub use line_intersection::LineIntersection; pub mod line_locate_point; pub use line_locate_point::LineLocatePoint; +/// Split a `Line` or `LineString` at a given fraction of its length. +pub mod line_split; +pub use line_split::{LineSplit, LineSplitResult, LineSplitTwiceResult}; + /// Iterate over the lines in a geometry. pub mod lines_iter; pub use lines_iter::LinesIter;