From 3b78529bea3aac07dca2b35874896380ee0221b1 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 01/15] started work on line-split --- geo/src/algorithm/line_split.rs | 124 ++++++++++++++++++++++++++++++++ geo/src/algorithm/mod.rs | 4 ++ 2 files changed, 128 insertions(+) create mode 100644 geo/src/algorithm/line_split.rs diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split.rs new file mode 100644 index 000000000..ae646962e --- /dev/null +++ b/geo/src/algorithm/line_split.rs @@ -0,0 +1,124 @@ +use geo_types::CoordNum; + +use crate::coords_iter::CoordsIter; +use crate::{CoordFloat, EuclideanLength, Line, LineString, Point}; +use std::ops::AddAssign; + +/// +/// +/// +pub trait LineSplit { + type Output; + + fn line_split(&self, fraction: Scalar) -> (Option, Option); + fn line_split_twice( + &self, + start_fraction: Scalar, + end_fraction: Scalar, + ) -> ( + Option, + Option, + Option, + ); +} + +impl LineSplit for Line +where + Scalar: CoordFloat, +{ + type Output = Line; + + fn line_split(&self, fraction: Scalar) -> (Option, Option) { + let fraction = fraction.max(Scalar::zero()).min(Scalar::one()); + if fraction == Scalar::zero() { + (None, Some(self.clone())) + } else if fraction == Scalar::one() { + (Some(self.clone()), None) + } else { + let new_midpoint = self.start + self.delta() * fraction; + ( + Some(Line::new(self.start, new_midpoint)), + Some(Line::new(new_midpoint, self.end)), + ) + } + } + + fn line_split_twice( + &self, + start_fraction: Scalar, + end_fraction: Scalar, + ) -> ( + Option, + Option, + Option, + ) { + // forgive the user for passing in the wrong order + // because it simplifies the interface of the output type + let (start_fraction, end_fraction) = if start_fraction > end_fraction { + (end_fraction, start_fraction) + } else { + (start_fraction, end_fraction) + }; + // TODO: check for nan + let second_fraction = (end_fraction - start_fraction) / (Scalar::one() - start_fraction); + match self.line_split(start_fraction) { + (Some(first_line), Some(second_line)) => { + match second_line.line_split(second_fraction) { + (Some(second_line), Some(third_line)) => { + (Some(first_line), Some(second_line), Some(third_line)) + } + (Some(second_line), None) => (Some(first_line), Some(second_line), None), + (None, Some(third_line)) => (Some(first_line), None, Some(third_line)), + (None, None) => (Some(first_line), None, None), + } + } + (None, Some(second_line)) => match second_line.line_split(second_fraction) { + (Some(second_line), Some(third_line)) => { + (None, Some(second_line), Some(third_line)) + } + (Some(second_line), None) => (None, Some(second_line), None), // Never + (None, Some(third_line)) => (None, None, Some(third_line)), + (None, None) => (None, None, None), + }, + (Some(first_line), None) => (Some(first_line), None, None), + (None, None) => (None, None, None), + } + } +} + +impl LineSplit for LineString +where + Scalar: CoordFloat, +{ + type Output = LineString; + + fn line_split(&self, fraction: Scalar) -> (Option, Option) { + todo!() + } + + fn line_split_twice( + &self, + start_fraction: Scalar, + end_fraction: Scalar, + ) -> ( + Option, + Option, + Option, + ) { + todo!() + } +} + +#[cfg(test)] +mod test { + + use super::*; + use crate::{coord, point}; + use crate::{ClosestPoint, LineLocatePoint}; + use num_traits::Float; + + #[test] + fn test_linestring_slice() { + todo!(); + } +} diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index bc2555431..c4d73d3c2 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -176,6 +176,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; + /// Iterate over the lines in a geometry. pub mod lines_iter; pub use lines_iter::LinesIter; From 95bb9901a034876210ff6bdcce515e1ef073b957 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 02/15] progress --- geo/src/algorithm/line_split.rs | 391 +++++++++++++++++++++++++------- 1 file changed, 311 insertions(+), 80 deletions(-) diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split.rs index ae646962e..5f3209204 100644 --- a/geo/src/algorithm/line_split.rs +++ b/geo/src/algorithm/line_split.rs @@ -1,57 +1,80 @@ -use geo_types::CoordNum; +use crate::{CoordFloat, EuclideanLength, Line, LineString}; -use crate::coords_iter::CoordsIter; -use crate::{CoordFloat, EuclideanLength, Line, LineString, Point}; -use std::ops::AddAssign; -/// -/// -/// -pub trait LineSplit { - type Output; - fn line_split(&self, fraction: Scalar) -> (Option, Option); - fn line_split_twice( - &self, - start_fraction: Scalar, - end_fraction: Scalar, - ) -> ( - Option, - Option, - Option, - ); +#[rustfmt::skip] +#[derive(PartialEq, Debug)] +pub enum LineSplitResult { + First (Result ), + Second ( Result), + FirstSecond (Result, Result), + InvalidLineString, } -impl LineSplit for Line -where - Scalar: CoordFloat, +#[rustfmt::skip] +#[derive(PartialEq, Debug)] +pub enum LineSplitTwiceResult { + First (Result ), + Second ( Result ), + Third ( Result), + FirstSecond (Result, Result ), + SecondThird ( Result, Result), + FirstThird (Result, Result), + FirstSecondThird (Result, Result, Result), + InvalidLineString +} + +#[derive(PartialEq, Debug)] +struct LineStringMeasurements{ + length_total:Scalar, + length_segments:Vec, +} +/// Simultaneously measure the total length of a line and the length of each segment +/// Returns `None` when +/// +/// - The `LineString` has less than two coords +/// - The resulting total_length is not finite +// TODO: consider re-implementing as a trait? +fn measure_line_string(line_string:&LineString) -> Option> where + Scalar:CoordFloat, + Line: EuclideanLength { - type Output = Line; - - fn line_split(&self, fraction: Scalar) -> (Option, Option) { - let fraction = fraction.max(Scalar::zero()).min(Scalar::one()); - if fraction == Scalar::zero() { - (None, Some(self.clone())) - } else if fraction == Scalar::one() { - (Some(self.clone()), None) - } else { - let new_midpoint = self.start + self.delta() * fraction; - ( - Some(Line::new(self.start, new_midpoint)), - Some(Line::new(new_midpoint, self.end)), - ) + 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==Scalar::zero() || !result.length_total.is_finite() { + None + }else{ + Some(result) } +} +/// +/// +/// +pub trait LineSplit where Self:Sized, Scalar: CoordFloat { + + fn line_split(&self, fraction: Scalar) -> LineSplitResult; + + // TODO: I only want to skip formatting the match block, but because attributes on expressions + // are experimental we are forced to put it on the function to avoid an error message. + #[rustfmt::skip] fn line_split_twice( &self, start_fraction: Scalar, end_fraction: Scalar, - ) -> ( - Option, - Option, - Option, - ) { + ) -> LineSplitTwiceResult { + // import enum variants + use LineSplitTwiceResult::*; + // forgive the user for passing in the wrong order // because it simplifies the interface of the output type let (start_fraction, end_fraction) = if start_fraction > end_fraction { @@ -61,64 +84,272 @@ where }; // TODO: check for nan let second_fraction = (end_fraction - start_fraction) / (Scalar::one() - start_fraction); + match self.line_split(start_fraction) { - (Some(first_line), Some(second_line)) => { - match second_line.line_split(second_fraction) { - (Some(second_line), Some(third_line)) => { - (Some(first_line), Some(second_line), Some(third_line)) - } - (Some(second_line), None) => (Some(first_line), Some(second_line), None), - (None, Some(third_line)) => (Some(first_line), None, Some(third_line)), - (None, None) => (Some(first_line), None, None), - } - } - (None, Some(second_line)) => match second_line.line_split(second_fraction) { - (Some(second_line), Some(third_line)) => { - (None, Some(second_line), Some(third_line)) - } - (Some(second_line), None) => (None, Some(second_line), None), // Never - (None, Some(third_line)) => (None, None, Some(third_line)), - (None, None) => (None, None, None), + LineSplitResult::FirstSecond(line1, line2) => match line2.line_split(second_fraction) { + LineSplitResult::FirstSecond(line2, line3) => FirstSecondThird(line1, line2, line3), + LineSplitResult::First (line2 ) => FirstSecond (line1, line2 ), + LineSplitResult::Second ( line3) => FirstThird (line1, line3), + LineSplitResult::InvalidLineString => InvalidLineString, }, - (Some(first_line), None) => (Some(first_line), None, None), - (None, None) => (None, None, None), + LineSplitResult::First (line1) => First(line1), + LineSplitResult::Second(line2) => match line2.line_split(second_fraction) { + LineSplitResult::FirstSecond(line2, line3) => SecondThird ( line2, line3), + LineSplitResult::First (line2 ) => Second ( line2 ), + LineSplitResult::Second ( line3) => Third ( line3), + LineSplitResult::InvalidLineString => InvalidLineString, + }, + LineSplitResult::InvalidLineString => InvalidLineString, + } + } +} + +impl LineSplit for Line where Scalar: CoordFloat { + fn line_split(&self, fraction: Scalar) -> LineSplitResult { + if fraction <= Scalar::zero() { + LineSplitResult::Second(self.clone()) + } else if fraction >= Scalar::one() { + LineSplitResult::First(self.clone()) + } else { + let new_midpoint = self.start + self.delta() * fraction; + LineSplitResult::FirstSecond( + Line::new(self.start, new_midpoint), + Line::new(new_midpoint, self.end), + ) } } } impl LineSplit for LineString where - Scalar: CoordFloat, + Scalar: CoordFloat + std::iter::Sum, { - type Output = LineString; + fn line_split(&self, fraction: Scalar) -> LineSplitResult { + // import enum variants + use LineSplitResult::*; + match (fraction.is_finite(), fraction <= Scalar::zero(), fraction >= Scalar::one()){ + (false, _, _) => InvalidLineString, + (true, true, false) => First(self.clone()), + (true, false, true) => Second(self.clone()), + (true, _, _) => { + // find the total length, and at the same time the length of each segment + // TODO: consider the possibility of a `LineStringMeasured` datatype in the future + // as this will be a common requirement in several algorithms, and would be a big + // performance boost when repeatedly slicing portions from the same LineStrings + // I think I saw a PreparedGeometry PR? maybe that will cover this? + let LineStringMeasurements{length_total, length_segments} = match measure_line_string(&self) { + Some(x) =>x, + None=> return InvalidLineString + }; + if ! Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { + // TODO: Does this cover a linestring with zero or one points? + return InvalidLineString + } - fn line_split(&self, fraction: Scalar) -> (Option, Option) { - todo!() - } + // 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(); + // TODO: unwrap used; but should be safe since we check the length above + let mut coords_first_part = vec![*self.0.first().unwrap()]; + let mut coords_second_part = Vec::new(); + + // Convert window slices to tuples because destructuring slices of unknown length is not + // possible + // TODO: the itertools crate has a pairwise function which returns tuples + let pairs = self.0.as_slice().windows(2).map(|item| (item[0], item[1])); - fn line_split_twice( - &self, - start_fraction: Scalar, - end_fraction: Scalar, - ) -> ( - Option, - Option, - Option, - ) { - todo!() + for ((a, b), &length_segment) in pairs.zip(length_segments.iter()) { + let length_accumulated_before_segment = length_accumulated; + length_accumulated = length_accumulated + length_segment; + let length_accumulated_after_segment = length_accumulated; + if length_accumulated_after_segment < length_fraction { + coords_first_part.push(b); + } else if length_accumulated_before_segment > length_fraction { + coords_second_part.push(b); + } else { + // TODO: check for divide by zero + let fraction_to_split_segment = (length_fraction - length_accumulated_before_segment) / length_segment; + match Line::new(a, b).line_split(fraction_to_split_segment) { + FirstSecond(line1, _line2) => { + coords_first_part.push(line1.end); + coords_second_part.push(line1.end); + coords_second_part.push(b); + }, + First (_line1 ) => { + coords_first_part.push(b); + coords_second_part.push(b); + }, + Second ( _line2) => { + coords_second_part.push(a); + coords_second_part.push(b); + }, + InvalidLineString => return InvalidLineString + } + } + } + FirstSecond(coords_first_part.into(), coords_second_part.into()) + } + } } } #[cfg(test)] mod test { + use geo_types::{line_string, coord}; + use super::*; - use crate::{coord, point}; - use crate::{ClosestPoint, LineLocatePoint}; - use num_traits::Float; + + + #[test] + fn test_measure_line_string() { + 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 test_linestring_slice() { - todo!(); + fn test_measure_line_string_malformed_zero() { + let line_string:LineString = line_string![]; + assert!(measure_line_string(&line_string).is_none()); } + + #[test] + fn test_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 test_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 test_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 test_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()); + } + + #[test] + fn test_measure_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, 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, 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, LineSplitResult::FirstSecond( + Line::new( + line.start, + split_point, + ), + Line::new( + split_point, + line.end, + ) + )); + } + + #[test] + fn test_measure_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, 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, LineSplitResult::First(line)); + } + #[test] + fn test_measure_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, 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, LineSplitResult::Second(line)); + } + } + + From 520191c2e9f6e93b6175a9438fba183d0be778f6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 03/15] added into_tuple for result types --- geo/src/algorithm/line_split.rs | 308 ++++++++++++++++++++++++++------ 1 file changed, 250 insertions(+), 58 deletions(-) diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split.rs index 5f3209204..a24ee8acf 100644 --- a/geo/src/algorithm/line_split.rs +++ b/geo/src/algorithm/line_split.rs @@ -1,27 +1,150 @@ -use crate::{CoordFloat, EuclideanLength, Line, LineString}; +//! + +use crate::{CoordFloat, EuclideanLength, Line, LineString}; #[rustfmt::skip] #[derive(PartialEq, Debug)] -pub enum LineSplitResult { - First (Result ), - Second ( Result), - FirstSecond (Result, Result), - InvalidLineString, +pub enum LineSplitResult { + First (T ), + Second ( T), + FirstSecond (T, T), +} + + +#[rustfmt::skip] +impl LineSplitResult{ + pub fn first(&self) -> Option<&T> { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + pub fn into_first(self) -> Option { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + pub fn second(&self) -> Option<&T> { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + pub fn into_second(self) -> Option { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + + 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)), + } + } } #[rustfmt::skip] #[derive(PartialEq, Debug)] -pub enum LineSplitTwiceResult { - First (Result ), - Second ( Result ), - Third ( Result), - FirstSecond (Result, Result ), - SecondThird ( Result, Result), - FirstThird (Result, Result), - FirstSecondThird (Result, Result, Result), - InvalidLineString +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 { + 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), + } + } + 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), + } + } + 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), + } + } + 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), + } + } + pub fn third(&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), + } + } + 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), + } + } + 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)), + } + } } #[derive(PartialEq, Debug)] @@ -62,8 +185,28 @@ fn measure_line_string(line_string:&LineString) -> Option where Self:Sized, Scalar: CoordFloat { - fn line_split(&self, fraction: Scalar) -> LineSplitResult; - + /// Note on choice of return type: + /// + /// You may wonder why this does not return `Option<(Option, Option)>`? + /// It is because then the return type causes uncertainty; The user may expect to possibly + /// receive `Some((None, None))` which is never possible, this would lead to clutter in match + /// statements. + /// + /// To make it easier to 'just get the first' or 'just get the second' you can use + /// `LineSplitResult::first()` and `LineSplitResult::second()` which return `Option` + /// + /// + fn line_split(&self, fraction: Scalar) -> Option>; + + /// Note on choice of return type: + /// + /// You may wonder why this does not return `Option<(Option,Option,Option)>`? + /// It is because then the return type causes uncertainty; The user may expect to possibly + /// receive `Some((None, None, None))` which is never possible. + /// The user would have a hard time writing an exhaustive match statement. + /// + /// To make it easier to 'just get the second' the `LineSplitResult` has a function called `first()->Option` + /// // TODO: I only want to skip formatting the match block, but because attributes on expressions // are experimental we are forced to put it on the function to avoid an error message. #[rustfmt::skip] @@ -71,7 +214,7 @@ pub trait LineSplit where Self:Sized, Scalar: CoordFloat { &self, start_fraction: Scalar, end_fraction: Scalar, - ) -> LineSplitTwiceResult { + ) -> Option> { // import enum variants use LineSplitTwiceResult::*; @@ -86,36 +229,39 @@ pub trait LineSplit where Self:Sized, Scalar: CoordFloat { let second_fraction = (end_fraction - start_fraction) / (Scalar::one() - start_fraction); match self.line_split(start_fraction) { - LineSplitResult::FirstSecond(line1, line2) => match line2.line_split(second_fraction) { - LineSplitResult::FirstSecond(line2, line3) => FirstSecondThird(line1, line2, line3), - LineSplitResult::First (line2 ) => FirstSecond (line1, line2 ), - LineSplitResult::Second ( line3) => FirstThird (line1, line3), - LineSplitResult::InvalidLineString => InvalidLineString, + 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, }, - LineSplitResult::First (line1) => First(line1), - LineSplitResult::Second(line2) => match line2.line_split(second_fraction) { - LineSplitResult::FirstSecond(line2, line3) => SecondThird ( line2, line3), - LineSplitResult::First (line2 ) => Second ( line2 ), - LineSplitResult::Second ( line3) => Third ( line3), - LineSplitResult::InvalidLineString => InvalidLineString, + 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, }, - LineSplitResult::InvalidLineString => InvalidLineString, + None => None, } } } impl LineSplit for Line where Scalar: CoordFloat { - fn line_split(&self, fraction: Scalar) -> LineSplitResult { + fn line_split(&self, fraction: Scalar) -> Option> { + if fraction.is_nan() { + return None + } if fraction <= Scalar::zero() { - LineSplitResult::Second(self.clone()) + Some(LineSplitResult::Second(self.clone())) } else if fraction >= Scalar::one() { - LineSplitResult::First(self.clone()) + Some(LineSplitResult::First(self.clone())) } else { let new_midpoint = self.start + self.delta() * fraction; - LineSplitResult::FirstSecond( + Some(LineSplitResult::FirstSecond( Line::new(self.start, new_midpoint), Line::new(new_midpoint, self.end), - ) + )) } } } @@ -124,14 +270,16 @@ impl LineSplit for LineString where Scalar: CoordFloat + std::iter::Sum, { - fn line_split(&self, fraction: Scalar) -> LineSplitResult { + fn line_split(&self, fraction: Scalar) -> Option> { // import enum variants use LineSplitResult::*; - match (fraction.is_finite(), fraction <= Scalar::zero(), fraction >= Scalar::one()){ - (false, _, _) => InvalidLineString, - (true, true, false) => First(self.clone()), - (true, false, true) => Second(self.clone()), - (true, _, _) => { + 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())), + _ => { // find the total length, and at the same time the length of each segment // TODO: consider the possibility of a `LineStringMeasured` datatype in the future // as this will be a common requirement in several algorithms, and would be a big @@ -139,11 +287,11 @@ where // I think I saw a PreparedGeometry PR? maybe that will cover this? let LineStringMeasurements{length_total, length_segments} = match measure_line_string(&self) { Some(x) =>x, - None=> return InvalidLineString + None=> return None }; if ! Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { // TODO: Does this cover a linestring with zero or one points? - return InvalidLineString + return None } // Find the length of the first part of the line string before the split; @@ -171,24 +319,24 @@ where // TODO: check for divide by zero let fraction_to_split_segment = (length_fraction - length_accumulated_before_segment) / length_segment; match Line::new(a, b).line_split(fraction_to_split_segment) { - FirstSecond(line1, _line2) => { + Some(FirstSecond(line1, _line2)) => { coords_first_part.push(line1.end); coords_second_part.push(line1.end); coords_second_part.push(b); }, - First (_line1 ) => { + Some(First (_line1 )) => { coords_first_part.push(b); coords_second_part.push(b); }, - Second ( _line2) => { + Some(Second ( _line2)) => { coords_second_part.push(a); coords_second_part.push(b); }, - InvalidLineString => return InvalidLineString + None => return None } } } - FirstSecond(coords_first_part.into(), coords_second_part.into()) + Some(FirstSecond(coords_first_part.into(), coords_second_part.into())) } } } @@ -266,7 +414,7 @@ mod test { coord!{x:10.0_f32, y:0.0_f32}, ); let result = line.line_split(0.6); - assert_eq!(result, LineSplitResult::FirstSecond( + 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}, @@ -275,7 +423,7 @@ mod test { 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( @@ -283,7 +431,7 @@ mod test { coord!{x:0.0_f32, y:10.0_f32}, ); let result = line.line_split(0.3); - assert_eq!(result, LineSplitResult::FirstSecond( + 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}, @@ -292,7 +440,7 @@ mod test { 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( @@ -301,7 +449,7 @@ mod test { ); let split_point = line.start + line.delta() * 0.7; let result = line.line_split(0.7); - assert_eq!(result, LineSplitResult::FirstSecond( + assert_eq!(result, Some(LineSplitResult::FirstSecond( Line::new( line.start, split_point, @@ -310,7 +458,7 @@ mod test { split_point, line.end, ) - )); + ))); } #[test] @@ -321,7 +469,7 @@ mod test { coord!{x:10.0_f32, y:0.0_f32}, ); let result = line.line_split(1.0); - assert_eq!(result, LineSplitResult::First(line)); + assert_eq!(result, Some(LineSplitResult::First(line))); // Test numbers larger than one let line = Line::new( @@ -329,7 +477,7 @@ mod test { coord!{x:10.0_f32, y:0.0_f32}, ); let result = line.line_split(2.0); - assert_eq!(result, LineSplitResult::First(line)); + assert_eq!(result, Some(LineSplitResult::First(line))); } #[test] fn test_measure_line_split_second() { @@ -339,7 +487,7 @@ mod test { coord!{x:10.0_f32, y:0.0_f32}, ); let result = line.line_split(0.0); - assert_eq!(result, LineSplitResult::Second(line)); + assert_eq!(result, Some(LineSplitResult::Second(line))); // Test negative numbers let line = Line::new( @@ -347,8 +495,52 @@ mod test { coord!{x:10.0_f32, y:0.0_f32}, ); let result = line.line_split(-2.0); - assert_eq!(result, LineSplitResult::Second(line)); + assert_eq!(result, Some(LineSplitResult::Second(line))); + } + + + #[test] + fn test_measure_linestring_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 test_measure_linestring_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 test_measure_linestring_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)) + ); + } + } From 7a7211ae7e7b721d9284974c4e15fdab2757f4e5 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 04/15] tests passing --- geo/src/algorithm/line_split.rs | 179 +++++++++++++++++++++++++++++--- 1 file changed, 166 insertions(+), 13 deletions(-) diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split.rs index a24ee8acf..1e6fd4722 100644 --- a/geo/src/algorithm/line_split.rs +++ b/geo/src/algorithm/line_split.rs @@ -289,6 +289,9 @@ where Some(x) =>x, None=> return None }; + println!("length_total: {:?}", length_total); + println!("length_segments: {:?}", length_segments); + if ! Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { // TODO: Does this cover a linestring with zero or one points? return None @@ -296,6 +299,7 @@ where // Find the length of the first part of the line string before the split; let length_fraction = fraction * length_total; + println!("length_fraction: {:?}", length_fraction); // Set up some variables to track state in the for-loop let mut length_accumulated = Scalar::zero(); // TODO: unwrap used; but should be safe since we check the length above @@ -311,28 +315,37 @@ where let length_accumulated_before_segment = length_accumulated; length_accumulated = length_accumulated + length_segment; let length_accumulated_after_segment = length_accumulated; - if length_accumulated_after_segment < length_fraction { - coords_first_part.push(b); - } else if length_accumulated_before_segment > length_fraction { + if length_fraction < length_accumulated_before_segment { + println!("---{length_accumulated_before_segment:?}-----{length_accumulated_after_segment:?}--X"); coords_second_part.push(b); + }else if length_fraction >= length_accumulated_after_segment { + println!("X--{length_accumulated_before_segment:?}-----{length_accumulated_after_segment:?}---"); + coords_first_part.push(b); } else { // TODO: check for divide by zero - let fraction_to_split_segment = (length_fraction - length_accumulated_before_segment) / length_segment; + let fraction_to_split_segment = ( + length_fraction + - length_accumulated_before_segment + ) / length_segment; + println!("---{length_accumulated_before_segment:?}-{fraction_to_split_segment:?}-{length_accumulated_after_segment:?}---"); match Line::new(a, b).line_split(fraction_to_split_segment) { Some(FirstSecond(line1, _line2)) => { - coords_first_part.push(line1.end); + println!("AAA"); + 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); + println!("BBB"); + coords_first_part .push(b); coords_second_part.push(b); }, Some(Second ( _line2)) => { + println!("CCC"); coords_second_part.push(a); coords_second_part.push(b); }, - None => return None + None => return None // probably never? } } } @@ -349,6 +362,9 @@ mod test { use super::*; + // ============================================================================================= + // measure_line_string(LineString) + // ============================================================================================= #[test] fn test_measure_line_string() { @@ -406,8 +422,12 @@ mod test { assert!(measure_line_string(&line_string).is_none()); } + // ============================================================================================= + // Line::line_split() + // ============================================================================================= + #[test] - fn test_measure_line_split_first_second() { + fn test_line_split_first_second() { // simple x-axis aligned check let line = Line::new( coord!{x: 0.0_f32, y:0.0_f32}, @@ -462,7 +482,7 @@ mod test { } #[test] - fn test_measure_line_split_first() { + fn test_line_split_first() { // test one let line = Line::new( coord!{x: 0.0_f32, y:0.0_f32}, @@ -480,7 +500,7 @@ mod test { assert_eq!(result, Some(LineSplitResult::First(line))); } #[test] - fn test_measure_line_split_second() { + fn test_line_split_second() { // test zero let line = Line::new( coord!{x: 0.0_f32, y:0.0_f32}, @@ -498,9 +518,12 @@ mod test { assert_eq!(result, Some(LineSplitResult::Second(line))); } + // ============================================================================================= + // LineString::line_split() + // ============================================================================================= #[test] - fn test_measure_linestring_split() { + fn test_linestring_split() { let line_string:LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), @@ -517,8 +540,62 @@ mod test { )) ); } + #[test] - fn test_measure_linestring_split_first() { + fn test_linestring_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 test_linestring_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 test_linestring_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 test_linestring_split_first() { let line_string:LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), @@ -530,7 +607,7 @@ mod test { } #[test] - fn test_measure_linestring_split_second() { + fn test_linestring_split_second() { let line_string:LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), @@ -541,7 +618,83 @@ mod test { ); } + // ============================================================================================= + // 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)); + } + + // ============================================================================================= + // LineString::line_split_twice() + // ============================================================================================= + #[test] + fn test_line_string_split_twice(){ + // I haven't done a formal proof 🤓, + // but 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 to a spot check for the most common use 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), + ], + ) + ); + } } From c9c43aa926e3081e45637f1d0fa1dd7c4f4fccc3 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 05/15] remove print statements used for debugging --- geo/src/algorithm/line_split.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split.rs index 1e6fd4722..8c47f5897 100644 --- a/geo/src/algorithm/line_split.rs +++ b/geo/src/algorithm/line_split.rs @@ -289,8 +289,6 @@ where Some(x) =>x, None=> return None }; - println!("length_total: {:?}", length_total); - println!("length_segments: {:?}", length_segments); if ! Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { // TODO: Does this cover a linestring with zero or one points? @@ -299,7 +297,6 @@ where // Find the length of the first part of the line string before the split; let length_fraction = fraction * length_total; - println!("length_fraction: {:?}", length_fraction); // Set up some variables to track state in the for-loop let mut length_accumulated = Scalar::zero(); // TODO: unwrap used; but should be safe since we check the length above @@ -316,10 +313,8 @@ where length_accumulated = length_accumulated + length_segment; let length_accumulated_after_segment = length_accumulated; if length_fraction < length_accumulated_before_segment { - println!("---{length_accumulated_before_segment:?}-----{length_accumulated_after_segment:?}--X"); coords_second_part.push(b); }else if length_fraction >= length_accumulated_after_segment { - println!("X--{length_accumulated_before_segment:?}-----{length_accumulated_after_segment:?}---"); coords_first_part.push(b); } else { // TODO: check for divide by zero @@ -327,21 +322,17 @@ where length_fraction - length_accumulated_before_segment ) / length_segment; - println!("---{length_accumulated_before_segment:?}-{fraction_to_split_segment:?}-{length_accumulated_after_segment:?}---"); match Line::new(a, b).line_split(fraction_to_split_segment) { Some(FirstSecond(line1, _line2)) => { - println!("AAA"); coords_first_part .push(line1.end); coords_second_part.push(line1.end); coords_second_part.push(b); }, Some(First (_line1 )) => { - println!("BBB"); coords_first_part .push(b); coords_second_part.push(b); }, Some(Second ( _line2)) => { - println!("CCC"); coords_second_part.push(a); coords_second_part.push(b); }, From fb0d9def766f61549680ba0e196ecc822e0faaa0 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 06/15] move to folder module --- geo/src/algorithm/{line_split.rs => line_split/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename geo/src/algorithm/{line_split.rs => line_split/mod.rs} (100%) diff --git a/geo/src/algorithm/line_split.rs b/geo/src/algorithm/line_split/mod.rs similarity index 100% rename from geo/src/algorithm/line_split.rs rename to geo/src/algorithm/line_split/mod.rs From e7602caf4e3dab7bcf6a673b9c55f783f733b1ee Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:29 +0800 Subject: [PATCH 07/15] split into modules --- .../algorithm/line_split/line_split_result.rs | 55 ++ .../algorithm/line_split/line_split_trait.rs | 68 ++ .../line_split_trait_impl_for_line.rs | 167 +++++ .../line_split_trait_impl_for_linestring.rs | 237 ++++++ .../line_split/line_split_twice_result.rs | 103 +++ .../line_split/measure_line_string.rs | 115 +++ geo/src/algorithm/line_split/mod.rs | 698 +----------------- 7 files changed, 755 insertions(+), 688 deletions(-) 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 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..e47427318 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_result.rs @@ -0,0 +1,55 @@ +#[rustfmt::skip] +#[derive(PartialEq, Debug)] +pub enum LineSplitResult { + First (T ), + Second ( T), + FirstSecond (T, T), +} + +#[rustfmt::skip] +impl LineSplitResult{ + pub fn first(&self) -> Option<&T> { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + pub fn into_first(self) -> Option { + match self { + Self::First (x ) => Some(x), + Self::Second ( _) => None, + Self::FirstSecond(x, _) => Some(x), + } + } + pub fn second(&self) -> Option<&T> { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + pub fn into_second(self) -> Option { + match self { + Self::First (_ ) => None, + Self::Second ( x) => Some(x), + Self::FirstSecond(_, x) => Some(x), + } + } + + 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)), + } + } + + 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..63cbf1259 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -0,0 +1,68 @@ +use geo_types::CoordFloat; + +use super::{LineSplitResult, LineSplitTwiceResult}; + + +pub trait LineSplit where Self:Sized, Scalar: CoordFloat { + + /// Note on choice of return type: + /// + /// You may wonder why this does not return `Option<(Option, Option)>`? + /// It is because then the return type causes uncertainty; The user may expect to possibly + /// receive `Some((None, None))` which is never possible, this would lead to clutter in match + /// statements. + /// + /// To make it easier to 'just get the first' or 'just get the second' you can use + /// `LineSplitResult::first()` and `LineSplitResult::second()` which return `Option` + /// + /// + fn line_split(&self, fraction: Scalar) -> Option>; + + /// Note on choice of return type: + /// + /// You may wonder why this does not return `Option<(Option,Option,Option)>`? + /// It is because then the return type causes uncertainty; The user may expect to possibly + /// receive `Some((None, None, None))` which is never possible. + /// The user would have a hard time writing an exhaustive match statement. + /// + /// To make it easier to 'just get the second' the `LineSplitResult` has a function called `first()->Option` + /// + // TODO: I only want to skip formatting the match block, but because attributes on expressions + // are experimental we are forced to put it on the function to avoid an error message. + #[rustfmt::skip] + fn line_split_twice( + &self, + start_fraction: Scalar, + end_fraction: Scalar, + ) -> Option> { + // import enum variants + use LineSplitTwiceResult::*; + + // forgive the user for passing in the wrong order + // because it simplifies the interface of the output type + let (start_fraction, end_fraction) = if start_fraction > end_fraction { + (end_fraction, start_fraction) + } else { + (start_fraction, end_fraction) + }; + // TODO: check for nan + 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..5a9e7be86 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait_impl_for_line.rs @@ -0,0 +1,167 @@ +use geo_types::{Line, CoordFloat}; + +use super::{ + LineSplitResult, + LineSplit, +}; + +impl LineSplit for Line where Scalar: CoordFloat { + fn line_split(&self, fraction: Scalar) -> Option> { + if fraction.is_nan() { + return None + } + if fraction <= Scalar::zero() { + Some(LineSplitResult::Second(self.clone())) + } else if fraction >= Scalar::one() { + Some(LineSplitResult::First(self.clone())) + } else { + let new_midpoint = self.start + self.delta() * fraction; + Some(LineSplitResult::FirstSecond( + Line::new(self.start, new_midpoint), + Line::new(new_midpoint, self.end), + )) + } + } +} + +#[cfg(test)] +mod test{ + use geo_types::coord; + use super::super::LineSplitTwiceResult; + use super::*; + + // ============================================================================================= + // 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)); + } + +} \ No newline at end of file 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..48b283e76 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_trait_impl_for_linestring.rs @@ -0,0 +1,237 @@ +use geo_types::{CoordFloat, Line, LineString}; + +use super::{measure_line_string, LineSplit, LineSplitResult, LineStringMeasurements}; + +impl LineSplit for LineString +where + Scalar: CoordFloat + std::iter::Sum, +{ + 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())), + _ => { + // find the total length, and at the same time the length of each segment + // TODO: consider the possibility of a `LineStringMeasured` datatype in the future + // as this will be a common requirement in several algorithms, and would be a big + // performance boost when repeatedly slicing portions from the same LineStrings + // I think I saw a PreparedGeometry PR? maybe that will cover this? + let LineStringMeasurements { + length_total, + length_segments, + } = match measure_line_string(&self) { + Some(x) => x, + None => return None, + }; + + if !Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { + // TODO: Does this cover a linestring with zero or one points? + 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(); + // TODO: unwrap used; but should be safe since we check the length above + let mut coords_first_part = vec![*self.0.first().unwrap()]; + let mut coords_second_part = Vec::new(); + + // Convert window slices to tuples because destructuring slices of unknown length is not + // possible + // TODO: the itertools crate has a pairwise function which returns tuples + let pairs = self.0.as_slice().windows(2).map(|item| (item[0], item[1])); + + for ((a, b), &length_segment) in pairs.zip(length_segments.iter()) { + 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 { + // TODO: check for divide by zero + 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::{line_string, coord}; + + 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 split_twice_typical(){ + // 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), + ], + ) + ); + } +} \ No newline at end of file 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..e42495cc4 --- /dev/null +++ b/geo/src/algorithm/line_split/line_split_twice_result.rs @@ -0,0 +1,103 @@ +#[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 { + 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), + } + } + 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), + } + } + 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), + } + } + 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), + } + } + 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), + } + } + 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), + } + } + 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)), + } + } + 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)), + } + } +} \ No newline at end of file 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..43dd5aa7f --- /dev/null +++ b/geo/src/algorithm/line_split/measure_line_string.rs @@ -0,0 +1,115 @@ +use crate::CoordFloat; +use crate::EuclideanLength; +use crate::Line; +use crate::LineString; + +#[derive(PartialEq, Debug)] +pub struct LineStringMeasurements { + pub length_total: Scalar, + pub length_segments: Vec, +} + +/// Simultaneously measure the total length of a line and the length of each segment +/// Returns `None` when +/// +/// - The `LineString` has less than two coords +/// - The resulting total_length is not finite +// TODO: consider re-implementing as a trait? +pub fn measure_line_string( + line_string: &LineString, +) -> Option> +where + Scalar: CoordFloat, + Line: EuclideanLength, +{ + 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 == Scalar::zero() || !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 index 8c47f5897..edfe83556 100644 --- a/geo/src/algorithm/line_split/mod.rs +++ b/geo/src/algorithm/line_split/mod.rs @@ -1,691 +1,13 @@ -//! +mod line_split_trait; +pub use line_split_trait::LineSplit; +mod line_split_trait_impl_for_line; +mod line_split_trait_impl_for_linestring; +mod line_split_result; +pub use line_split_result::LineSplitResult; -use crate::{CoordFloat, EuclideanLength, Line, LineString}; - - -#[rustfmt::skip] -#[derive(PartialEq, Debug)] -pub enum LineSplitResult { - First (T ), - Second ( T), - FirstSecond (T, T), -} - - -#[rustfmt::skip] -impl LineSplitResult{ - pub fn first(&self) -> Option<&T> { - match self { - Self::First (x ) => Some(x), - Self::Second ( _) => None, - Self::FirstSecond(x, _) => Some(x), - } - } - pub fn into_first(self) -> Option { - match self { - Self::First (x ) => Some(x), - Self::Second ( _) => None, - Self::FirstSecond(x, _) => Some(x), - } - } - pub fn second(&self) -> Option<&T> { - match self { - Self::First (_ ) => None, - Self::Second ( x) => Some(x), - Self::FirstSecond(_, x) => Some(x), - } - } - pub fn into_second(self) -> Option { - match self { - Self::First (_ ) => None, - Self::Second ( x) => Some(x), - Self::FirstSecond(_, x) => Some(x), - } - } - - 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)), - } - } -} - -#[rustfmt::skip] -#[derive(PartialEq, Debug)] -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 { - 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), - } - } - 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), - } - } - 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), - } - } - 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), - } - } - pub fn third(&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), - } - } - 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), - } - } - 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)), - } - } -} - -#[derive(PartialEq, Debug)] -struct LineStringMeasurements{ - length_total:Scalar, - length_segments:Vec, -} -/// Simultaneously measure the total length of a line and the length of each segment -/// Returns `None` when -/// -/// - The `LineString` has less than two coords -/// - The resulting total_length is not finite -// TODO: consider re-implementing as a trait? -fn measure_line_string(line_string:&LineString) -> Option> where - Scalar:CoordFloat, - Line: EuclideanLength -{ - 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==Scalar::zero() || !result.length_total.is_finite() { - None - }else{ - Some(result) - } -} - -/// -/// -/// -pub trait LineSplit where Self:Sized, Scalar: CoordFloat { - - /// Note on choice of return type: - /// - /// You may wonder why this does not return `Option<(Option, Option)>`? - /// It is because then the return type causes uncertainty; The user may expect to possibly - /// receive `Some((None, None))` which is never possible, this would lead to clutter in match - /// statements. - /// - /// To make it easier to 'just get the first' or 'just get the second' you can use - /// `LineSplitResult::first()` and `LineSplitResult::second()` which return `Option` - /// - /// - fn line_split(&self, fraction: Scalar) -> Option>; - - /// Note on choice of return type: - /// - /// You may wonder why this does not return `Option<(Option,Option,Option)>`? - /// It is because then the return type causes uncertainty; The user may expect to possibly - /// receive `Some((None, None, None))` which is never possible. - /// The user would have a hard time writing an exhaustive match statement. - /// - /// To make it easier to 'just get the second' the `LineSplitResult` has a function called `first()->Option` - /// - // TODO: I only want to skip formatting the match block, but because attributes on expressions - // are experimental we are forced to put it on the function to avoid an error message. - #[rustfmt::skip] - fn line_split_twice( - &self, - start_fraction: Scalar, - end_fraction: Scalar, - ) -> Option> { - // import enum variants - use LineSplitTwiceResult::*; - - // forgive the user for passing in the wrong order - // because it simplifies the interface of the output type - let (start_fraction, end_fraction) = if start_fraction > end_fraction { - (end_fraction, start_fraction) - } else { - (start_fraction, end_fraction) - }; - // TODO: check for nan - 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, - } - } -} - -impl LineSplit for Line where Scalar: CoordFloat { - fn line_split(&self, fraction: Scalar) -> Option> { - if fraction.is_nan() { - return None - } - if fraction <= Scalar::zero() { - Some(LineSplitResult::Second(self.clone())) - } else if fraction >= Scalar::one() { - Some(LineSplitResult::First(self.clone())) - } else { - let new_midpoint = self.start + self.delta() * fraction; - Some(LineSplitResult::FirstSecond( - Line::new(self.start, new_midpoint), - Line::new(new_midpoint, self.end), - )) - } - } -} - -impl LineSplit for LineString -where - Scalar: CoordFloat + std::iter::Sum, -{ - 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())), - _ => { - // find the total length, and at the same time the length of each segment - // TODO: consider the possibility of a `LineStringMeasured` datatype in the future - // as this will be a common requirement in several algorithms, and would be a big - // performance boost when repeatedly slicing portions from the same LineStrings - // I think I saw a PreparedGeometry PR? maybe that will cover this? - let LineStringMeasurements{length_total, length_segments} = match measure_line_string(&self) { - Some(x) =>x, - None=> return None - }; - - if ! Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { - // TODO: Does this cover a linestring with zero or one points? - 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(); - // TODO: unwrap used; but should be safe since we check the length above - let mut coords_first_part = vec![*self.0.first().unwrap()]; - let mut coords_second_part = Vec::new(); - - // Convert window slices to tuples because destructuring slices of unknown length is not - // possible - // TODO: the itertools crate has a pairwise function which returns tuples - let pairs = self.0.as_slice().windows(2).map(|item| (item[0], item[1])); - - for ((a, b), &length_segment) in pairs.zip(length_segments.iter()) { - 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 { - // TODO: check for divide by zero - 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::{line_string, coord}; - - use super::*; - - // ============================================================================================= - // measure_line_string(LineString) - // ============================================================================================= - - #[test] - fn test_measure_line_string() { - 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 test_measure_line_string_malformed_zero() { - let line_string:LineString = line_string![]; - assert!(measure_line_string(&line_string).is_none()); - } - - #[test] - fn test_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 test_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 test_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 test_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()); - } - - // ============================================================================================= - // 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))); - } - - // ============================================================================================= - // LineString::line_split() - // ============================================================================================= - - #[test] - fn test_linestring_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 test_linestring_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 test_linestring_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 test_linestring_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 test_linestring_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 test_linestring_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)) - ); - } - - // ============================================================================================= - // 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)); - } - - // ============================================================================================= - // LineString::line_split_twice() - // ============================================================================================= - #[test] - fn test_line_string_split_twice(){ - // I haven't done a formal proof 🤓, - // but 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 to a spot check for the most common use 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), - ], - ) - ); - } -} - +mod line_split_twice_result; +pub use line_split_twice_result::LineSplitTwiceResult; +mod measure_line_string; +pub use measure_line_string::{measure_line_string, LineStringMeasurements}; From 5d78a4346a24e24a16ddf7a675da32571b637d64 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:30 +0800 Subject: [PATCH 08/15] cargo fmt + clippy --- .../algorithm/line_split/line_split_trait.rs | 18 +-- .../line_split_trait_impl_for_line.rs | 127 +++++++++--------- .../line_split_trait_impl_for_linestring.rs | 32 +++-- .../line_split/line_split_twice_result.rs | 2 +- 4 files changed, 92 insertions(+), 87 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index 63cbf1259..0b87e605a 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -2,20 +2,22 @@ use geo_types::CoordFloat; use super::{LineSplitResult, LineSplitTwiceResult}; - -pub trait LineSplit where Self:Sized, Scalar: CoordFloat { - +pub trait LineSplit +where + Self: Sized, + Scalar: CoordFloat, +{ /// Note on choice of return type: - /// + /// /// You may wonder why this does not return `Option<(Option, Option)>`? /// It is because then the return type causes uncertainty; The user may expect to possibly /// receive `Some((None, None))` which is never possible, this would lead to clutter in match /// statements. - /// + /// /// To make it easier to 'just get the first' or 'just get the second' you can use /// `LineSplitResult::first()` and `LineSplitResult::second()` which return `Option` - /// - /// + /// + /// fn line_split(&self, fraction: Scalar) -> Option>; /// Note on choice of return type: @@ -57,7 +59,7 @@ pub trait LineSplit where Self:Sized, Scalar: CoordFloat { }, 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::FirstSecond(line2, line3)) => Some(SecondThird ( line2, line3)), Some(LineSplitResult::First (line2 )) => Some(Second ( line2 )), Some(LineSplitResult::Second ( line3)) => Some(Third ( line3)), 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 index 5a9e7be86..49b37d6ce 100644 --- 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 @@ -1,19 +1,19 @@ -use geo_types::{Line, CoordFloat}; +use geo_types::{CoordFloat, Line}; -use super::{ - LineSplitResult, - LineSplit, -}; +use super::{LineSplit, LineSplitResult}; -impl LineSplit for Line where Scalar: CoordFloat { +impl LineSplit for Line +where + Scalar: CoordFloat, +{ fn line_split(&self, fraction: Scalar) -> Option> { if fraction.is_nan() { - return None + return None; } if fraction <= Scalar::zero() { - Some(LineSplitResult::Second(self.clone())) + Some(LineSplitResult::Second(*self)) } else if fraction >= Scalar::one() { - Some(LineSplitResult::First(self.clone())) + Some(LineSplitResult::First(*self)) } else { let new_midpoint = self.start + self.delta() * fraction; Some(LineSplitResult::FirstSecond( @@ -25,10 +25,10 @@ impl LineSplit for Line where Scalar: CoordFloat { } #[cfg(test)] -mod test{ - use geo_types::coord; +mod test { use super::super::LineSplitTwiceResult; use super::*; + use geo_types::coord; // ============================================================================================= // Line::line_split() @@ -38,71 +38,71 @@ mod 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}, + 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}, - ) - ))); + 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}, + 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}, - ) - ))); + 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}, + 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, - ) - ))); + 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}, + 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}, + 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))); @@ -111,26 +111,25 @@ mod 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}, + 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}, + 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( @@ -154,8 +153,15 @@ mod test{ } #[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)); + 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)); @@ -163,5 +169,4 @@ mod test{ 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)); } - -} \ No newline at end of file +} 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 index 48b283e76..73581d064 100644 --- 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 @@ -24,7 +24,7 @@ where let LineStringMeasurements { length_total, length_segments, - } = match measure_line_string(&self) { + } = match measure_line_string(self) { Some(x) => x, None => return None, }; @@ -88,7 +88,7 @@ where #[cfg(test)] mod test { - use geo_types::{line_string, coord}; + use geo_types::{coord, line_string}; use super::super::LineSplitTwiceResult; @@ -99,7 +99,7 @@ mod test { #[test] fn split() { - let line_string:LineString = line_string![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), (x:1.0, y:1.0), @@ -110,15 +110,15 @@ mod test { 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]]) + 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![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), (x:1.0, y:1.0), @@ -129,7 +129,7 @@ mod test { 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![line_string.0[0], line_string.0[1], slice_point]), LineString::new(vec![slice_point, line_string.0[3], line_string.0[4]]) )) ); @@ -137,7 +137,7 @@ mod test { #[test] fn split_half_way_through_last_segment() { - let line_string:LineString = line_string![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), (x:1.0, y:1.0), @@ -154,7 +154,7 @@ mod test { #[test] fn split_half_way_through_first_segment() { - let line_string:LineString = line_string![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), (x:1.0, y:1.0), @@ -171,7 +171,7 @@ mod test { #[test] fn split_first() { - let line_string:LineString = line_string![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), ]; @@ -183,7 +183,7 @@ mod test { #[test] fn split_second() { - let line_string:LineString = line_string![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), ]; @@ -193,13 +193,11 @@ mod test { ); } - - // ============================================================================================= // LineString::line_split_twice() // ============================================================================================= #[test] - fn split_twice_typical(){ + fn split_twice_typical() { // I think if we exhaustively check // - `Line::line_split_twice()` and // - `LineString::line_split()` @@ -207,7 +205,7 @@ mod test { // 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![ + let line_string: LineString = line_string![ (x:0.0, y:0.0), (x:1.0, y:0.0), (x:1.0, y:1.0), @@ -234,4 +232,4 @@ mod test { ) ); } -} \ No newline at end of file +} diff --git a/geo/src/algorithm/line_split/line_split_twice_result.rs b/geo/src/algorithm/line_split/line_split_twice_result.rs index e42495cc4..0738a81a0 100644 --- a/geo/src/algorithm/line_split/line_split_twice_result.rs +++ b/geo/src/algorithm/line_split/line_split_twice_result.rs @@ -100,4 +100,4 @@ impl LineSplitTwiceResult { Self::FirstSecondThird(a, b, c) => (Some(a), Some(b), Some(c)), } } -} \ No newline at end of file +} From 6a2176f2177718732fad022b6517909c6c12dae4 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:30 +0800 Subject: [PATCH 09/15] make measure_line_string private to mod --- geo/src/algorithm/line_split/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geo/src/algorithm/line_split/mod.rs b/geo/src/algorithm/line_split/mod.rs index edfe83556..db150f926 100644 --- a/geo/src/algorithm/line_split/mod.rs +++ b/geo/src/algorithm/line_split/mod.rs @@ -10,4 +10,4 @@ mod line_split_twice_result; pub use line_split_twice_result::LineSplitTwiceResult; mod measure_line_string; -pub use measure_line_string::{measure_line_string, LineStringMeasurements}; +use measure_line_string::{measure_line_string, LineStringMeasurements}; From e74e4dddb78781381f056d6617b0b0f0506310a6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:30 +0800 Subject: [PATCH 10/15] improve docs --- geo/src/algorithm/line_split/line_split_trait.rs | 6 ++++++ .../line_split/line_split_trait_impl_for_linestring.rs | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index 0b87e605a..0828032a4 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -7,6 +7,12 @@ where Self: Sized, Scalar: CoordFloat, { + /// Split a line or linestring at some fraction of its length. + /// + /// Returns `None` when + /// - The provided fraction is nan (infinite values are allowed and saturate to 0.0 or 1.0) + /// - The `Line` or `LineString` include nan or infinite values + /// /// Note on choice of return type: /// /// You may wonder why this does not return `Option<(Option, Option)>`? 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 index 73581d064..8f3f356f9 100644 --- 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 @@ -16,7 +16,7 @@ where (false, true) => Some(First(self.clone())), (true, false) => Some(Second(self.clone())), _ => { - // find the total length, and at the same time the length of each segment + // Find the total length and the lengths of each segment at the same time; // TODO: consider the possibility of a `LineStringMeasured` datatype in the future // as this will be a common requirement in several algorithms, and would be a big // performance boost when repeatedly slicing portions from the same LineStrings @@ -29,8 +29,8 @@ where None => return None, }; + // Reject line strings with zero length, nan values, or infinite values; if !Scalar::is_finite(length_total) || Scalar::is_zero(&length_total) { - // TODO: Does this cover a linestring with zero or one points? return None; } From abe8b62d9729969c0e90bae3cbc7eb238a8befb6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:30 +0800 Subject: [PATCH 11/15] fail if result is not finite --- .../line_split/line_split_trait_impl_for_line.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 index 49b37d6ce..683e883a0 100644 --- 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 @@ -1,5 +1,5 @@ use geo_types::{CoordFloat, Line}; - +use crate::Vector2DOps; use super::{LineSplit, LineSplitResult}; impl LineSplit for Line @@ -16,10 +16,14 @@ where Some(LineSplitResult::First(*self)) } else { let new_midpoint = self.start + self.delta() * fraction; - Some(LineSplitResult::FirstSecond( - Line::new(self.start, new_midpoint), - Line::new(new_midpoint, self.end), - )) + if new_midpoint.is_finite() { + Some(LineSplitResult::FirstSecond( + Line::new(self.start, new_midpoint), + Line::new(new_midpoint, self.end), + )) + } else { + None + } } } } From 096e6f4e6dbb6a401db22c3d900959c11c2061f4 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Wed, 9 Aug 2023 23:22:31 +0800 Subject: [PATCH 12/15] add line_split_many --- .../algorithm/line_split/line_split_trait.rs | 58 ++++++++++++++++++- .../line_split_trait_impl_for_line.rs | 14 +++++ 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index 0828032a4..a75189497 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -8,11 +8,11 @@ where Scalar: CoordFloat, { /// Split a line or linestring at some fraction of its length. - /// + /// /// Returns `None` when /// - The provided fraction is nan (infinite values are allowed and saturate to 0.0 or 1.0) /// - The `Line` or `LineString` include nan or infinite values - /// + /// /// Note on choice of return type: /// /// You may wonder why this does not return `Option<(Option, Option)>`? @@ -26,6 +26,60 @@ where /// fn line_split(&self, fraction: Scalar) -> Option>; + /// This default implementation is inefficient because it uses repeated application of + /// the line_split function. Implementing types should override this with a more efficient + /// algorithm if possible. + fn line_split_many(&self, mut 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(); + + if fractions.last().unwrap() != Scalar::one() { + fractions.push(Scalar::one()); + } else{ + return None; + } + let fractions = fractions; // remove mutability + let output = Vec::new(); + let mut remaining_self = self.clone(); + // TODO handel case where fractions is len 1 or 0 + for fraction in fractions.windows(2) { + if let &[a, b] = fraction { + let fraction_interval = b - a; + let fraction_to_end = Scalar::one() - a; + let next_fraction = fraction_interval / fraction_to_end; + remaining_self = match remaining_self.line_split(next_fraction) { + Some(LineSplitResult::FirstSecond(line1, line2)) => { + output.push(Some(line1)); + line2 + }, + Some(LineSplitResult::First(line1))=>{ + output.push(Some(line1)); + break + }, + Some(LineSplitResult::Second(_))=>{ + output.push(None); + break + }, + None=>break + } + } + } + output + } + } + + } + /// Note on choice of return type: /// /// You may wonder why this does not return `Option<(Option,Option,Option)>`? 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 index 683e883a0..f9f2eb0a4 100644 --- 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 @@ -173,4 +173,18 @@ mod test { 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_f32, y:0.0_f32}, + coord!{x:10.0_f32, y:0.0_f32}, + ); + let result = line.line_split_many(vec![0.1, 0.2, 0.5]).unwrap(); + println!(result); + } } From 9e2c5d6efecd4d09c3b6b02e432935dedc872ab1 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Thu, 10 Aug 2023 00:06:28 +0800 Subject: [PATCH 13/15] add some tests for line_split_many --- .../algorithm/line_split/line_split_trait.rs | 61 ++++++++++--------- .../line_split_trait_impl_for_line.rs | 61 ++++++++++++++++--- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index a75189497..6873877fc 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -29,7 +29,10 @@ where /// This default implementation is inefficient because it uses repeated application of /// the line_split function. Implementing types should override this with a more efficient /// algorithm if possible. - fn line_split_many(&self, mut fractions: &Vec) -> Option> where Self: Clone { + fn line_split_many(&self, fractions: &Vec) -> Option>> + where + Self: Clone, + { match fractions.len() { 0 => None, 1 => self.line_split(fractions[0]).map(|item| { @@ -37,47 +40,47 @@ where vec![a, b] }), _ => { - let mut fractions:Vec = fractions + let mut fractions: Vec = fractions .iter() .map(|item| item.min(Scalar::one()).max(Scalar::zero())) .collect(); - fractions.sort_unstable(); - - if fractions.last().unwrap() != Scalar::one() { - fractions.push(Scalar::one()); - } else{ - return None; - } + 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 output = Vec::new(); - let mut remaining_self = self.clone(); - // TODO handel case where fractions is len 1 or 0 + let mut output: Vec> = Vec::new(); + let mut remaining_self = Some(self.clone()); for fraction in fractions.windows(2) { if let &[a, b] = fraction { let fraction_interval = b - a; let fraction_to_end = Scalar::one() - a; - let next_fraction = fraction_interval / fraction_to_end; - remaining_self = match remaining_self.line_split(next_fraction) { - Some(LineSplitResult::FirstSecond(line1, line2)) => { - output.push(Some(line1)); - line2 - }, - Some(LineSplitResult::First(line1))=>{ - output.push(Some(line1)); - break - }, - Some(LineSplitResult::Second(_))=>{ - output.push(None); - break - }, - None=>break + 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(_)) => { + output.push(None); + None + } + None => return None, + } + } else { + output.push(None); + None } } } - output + Some(output) } } - } /// Note on choice of return type: 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 index f9f2eb0a4..396099035 100644 --- 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 @@ -1,6 +1,6 @@ -use geo_types::{CoordFloat, Line}; -use crate::Vector2DOps; use super::{LineSplit, LineSplitResult}; +use crate::Vector2DOps; +use geo_types::{CoordFloat, Line}; impl LineSplit for Line where @@ -179,12 +179,59 @@ mod test { // ============================================================================================= #[test] - fn test_line_split_many(){ + fn test_line_split_many() { let line = Line::new( - coord!{x: 0.0_f32, y:0.0_f32}, - coord!{x:10.0_f32, y:0.0_f32}, + coord! {x: 0.0_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + 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() { + 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_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 + ]) ); - let result = line.line_split_many(vec![0.1, 0.2, 0.5]).unwrap(); - println!(result); } } From 92ace587f8f692846556648b0d0c01d44e427edd Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Thu, 10 Aug 2023 22:50:46 +0800 Subject: [PATCH 14/15] added docs and tests for line_split_many --- .../algorithm/line_split/line_split_result.rs | 34 +++- .../algorithm/line_split/line_split_trait.rs | 179 ++++++++++++------ .../line_split_trait_impl_for_line.rs | 140 +++++++++++++- .../line_split_trait_impl_for_linestring.rs | 60 +++++- .../line_split/line_split_twice_result.rs | 41 +++- .../line_split/measure_line_string.rs | 1 - geo/src/algorithm/line_split/mod.rs | 9 + geo/src/algorithm/mod.rs | 2 +- 8 files changed, 400 insertions(+), 66 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_result.rs b/geo/src/algorithm/line_split/line_split_result.rs index e47427318..8bfcfafc3 100644 --- a/geo/src/algorithm/line_split/line_split_result.rs +++ b/geo/src/algorithm/line_split/line_split_result.rs @@ -1,3 +1,19 @@ +/// 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). +/// +/// ``` +/// if let Some(second) = my_line.line_split_twice(0.2, 0.5).into_second() { +/// // got the part between the two splits +/// } +/// ``` #[rustfmt::skip] #[derive(PartialEq, Debug)] pub enum LineSplitResult { @@ -8,6 +24,7 @@ pub enum LineSplitResult { #[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), @@ -15,6 +32,7 @@ impl LineSplitResult{ 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), @@ -22,6 +40,7 @@ impl LineSplitResult{ 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, @@ -29,6 +48,7 @@ impl LineSplitResult{ 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, @@ -36,7 +56,12 @@ impl LineSplitResult{ 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 ), @@ -44,7 +69,12 @@ impl LineSplitResult{ 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 ), diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index 6873877fc..e47701725 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -2,33 +2,67 @@ 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 or linestring at some fraction of its length. + /// Split a [Line](crate::Line) or [LineString](crate::LineString) at some `fraction` of its length. /// - /// Returns `None` when - /// - The provided fraction is nan (infinite values are allowed and saturate to 0.0 or 1.0) - /// - The `Line` or `LineString` include nan or infinite values + /// `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. /// - /// Note on choice of return type: + /// Returns `None` when + /// - The provided `fraction` is NAN + /// - The the object being sliced includes NAN or infinite coordinates /// - /// You may wonder why this does not return `Option<(Option, Option)>`? - /// It is because then the return type causes uncertainty; The user may expect to possibly - /// receive `Some((None, None))` which is never possible, this would lead to clutter in match - /// statements. + /// Otherwise returns [`Some(LineSplitResult)`](crate::algorithm::LineSplitResult) /// - /// To make it easier to 'just get the first' or 'just get the second' you can use - /// `LineSplitResult::first()` and `LineSplitResult::second()` which return `Option` + /// example /// + /// ``` + /// use geo::{Line, coord}; + /// use geo::algorithm::{LineSplit, LineSplitResult}; + /// 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}, + /// ) + /// )) + /// ); /// + /// match result { + /// Some(LineSplitResult::First(line1))=>todo!(), + /// Some(LineSplitResult::Second(line2))=>todo!(), + /// Some(LineSplitResult::FirstSecond(line1, line2))=>todo!(), + /// None=>todo!(), + /// } + /// ``` fn line_split(&self, fraction: Scalar) -> Option>; - /// This default implementation is inefficient because it uses repeated application of - /// the line_split function. Implementing types should override this with a more efficient - /// algorithm if possible. + /// + /// + /// 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, @@ -52,66 +86,105 @@ where let mut output: Vec> = Vec::new(); let mut remaining_self = Some(self.clone()); for fraction in fractions.windows(2) { - if let &[a, b] = fraction { - 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(_)) => { - output.push(None); - None - } - None => return None, + // 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) } - } else { - output.push(None); - None + None => return None, } + } else { + output.push(None); + None } } + Some(output) } } } - /// Note on choice of return type: + /// 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 to + /// executing the splits. + /// + /// 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) /// - /// You may wonder why this does not return `Option<(Option,Option,Option)>`? - /// It is because then the return type causes uncertainty; The user may expect to possibly - /// receive `Some((None, None, None))` which is never possible. - /// The user would have a hard time writing an exhaustive match statement. + /// A [LineSplitTwiceResult]: LineSplitTwiceResult can contain between one and + /// three [Line](crate::Line) or [LineString](crate::LineString) objects. Please see the docs + /// for that type as it provides various helper methods to get the desired part(s) of the + /// output. /// - /// To make it easier to 'just get the second' the `LineSplitResult` has a function called `first()->Option` + /// 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}; + /// use geo::algorithm::{LineSplit, EuclideanLength}; + /// // get the road section between chainage_from and chaingage_to + /// // (gets the second of the three result parts) + /// let my_road_line_string:LineString = todo!(); + /// let chainage_from = 20.0; + /// let chainage_to = 150.0; + /// let my_road_len = my_road_line_string.euclidean_length(); + /// let my_road_section:Option = my_road_line_string + /// .split_twice(chainage_from / road_len, chaingage_to / road_len) + /// .into_second(); + /// ``` /// - // TODO: I only want to skip formatting the match block, but because attributes on expressions - // are experimental we are forced to put it on the function to avoid an error message. #[rustfmt::skip] fn line_split_twice( &self, - start_fraction: Scalar, - end_fraction: Scalar, + 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()); - // forgive the user for passing in the wrong order - // because it simplifies the interface of the output type - let (start_fraction, end_fraction) = if start_fraction > end_fraction { - (end_fraction, start_fraction) + // swap interval if incorrectly ordered + let (start_fraction, end_fraction) = if fraction_start > fraction_end { + (fraction_end, fraction_start) } else { - (start_fraction, end_fraction) + (fraction_start, fraction_end) }; - // TODO: check for nan - let second_fraction = (end_fraction - start_fraction) / (Scalar::one() - start_fraction); + + // 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) { 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 index 396099035..096933bf0 100644 --- 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 @@ -6,6 +6,42 @@ 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_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}, + /// ) + /// )) + /// ); + /// ``` fn line_split(&self, fraction: Scalar) -> Option> { if fraction.is_nan() { return None; @@ -209,7 +245,7 @@ mod test { } #[test] - fn test_line_split_many_edge() { + fn test_line_split_many_edge_right() { let line = Line::new( coord! {x: 0.0_f32, y:0.0_f32}, coord! {x:10.0_f32, y:0.0_f32}, @@ -234,4 +270,106 @@ mod test { ]) ); } + + #[test] + fn test_line_split_many_double_edge_right() { + 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_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_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + 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_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + 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_f32, y:0.0_f32}, + coord! {x:10.0_f32, y:0.0_f32}, + ); + 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 index 8f3f356f9..173c0b747 100644 --- 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 @@ -42,12 +42,14 @@ where let mut coords_first_part = vec![*self.0.first().unwrap()]; let mut coords_second_part = Vec::new(); - // Convert window slices to tuples because destructuring slices of unknown length is not - // possible - // TODO: the itertools crate has a pairwise function which returns tuples - let pairs = self.0.as_slice().windows(2).map(|item| (item[0], item[1])); - - for ((a, b), &length_segment) in pairs.zip(length_segments.iter()) { + 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; @@ -232,4 +234,50 @@ mod test { ) ); } + + // ============================================================================================= + // LineString::line_split_many() + // ============================================================================================= + #[test] + fn test_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 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_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), + ]), + ] + ); + } } diff --git a/geo/src/algorithm/line_split/line_split_twice_result.rs b/geo/src/algorithm/line_split/line_split_twice_result.rs index 0738a81a0..b37a3d614 100644 --- a/geo/src/algorithm/line_split/line_split_twice_result.rs +++ b/geo/src/algorithm/line_split/line_split_twice_result.rs @@ -1,3 +1,31 @@ +/// 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). +/// +/// ``` +/// if let Some(second) = my_line.line_split_twice(...).into_second() { +/// // got the part between the two splits +/// } +/// ``` +/// +/// To get more than one part, consider using consider using +/// [.into_tuple()](LineSplitTwiceResult#method.into_tuple): +/// +/// ``` +/// match my_line.line_split_twice(...).into_tuple() { +/// (Some(first), Some(second), _) => todo!(), +/// _ => None +/// } +/// ``` #[derive(PartialEq, Debug)] #[rustfmt::skip] pub enum LineSplitTwiceResult { @@ -12,6 +40,8 @@ pub enum LineSplitTwiceResult { #[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), @@ -23,6 +53,7 @@ impl LineSplitTwiceResult { 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), @@ -34,6 +65,7 @@ impl LineSplitTwiceResult { 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, @@ -45,6 +77,7 @@ impl LineSplitTwiceResult { 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, @@ -56,6 +89,7 @@ impl LineSplitTwiceResult { 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, @@ -67,6 +101,7 @@ impl LineSplitTwiceResult { 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, @@ -78,7 +113,8 @@ impl LineSplitTwiceResult { Self::FirstSecondThird(_, _, x) => Some(x), } } - pub fn into_tuple(self) -> (Option, Option, Option) { + /// 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 ), @@ -89,7 +125,8 @@ impl LineSplitTwiceResult { Self::FirstSecondThird(a, b, c) => (Some(a), Some(b), Some(c)), } } - pub fn as_tuple(&self) -> (Option<&T>, Option<&T>, Option<&T>) { + /// 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 ), diff --git a/geo/src/algorithm/line_split/measure_line_string.rs b/geo/src/algorithm/line_split/measure_line_string.rs index 43dd5aa7f..a306271ae 100644 --- a/geo/src/algorithm/line_split/measure_line_string.rs +++ b/geo/src/algorithm/line_split/measure_line_string.rs @@ -14,7 +14,6 @@ pub struct LineStringMeasurements { /// /// - The `LineString` has less than two coords /// - The resulting total_length is not finite -// TODO: consider re-implementing as a trait? pub fn measure_line_string( line_string: &LineString, ) -> Option> diff --git a/geo/src/algorithm/line_split/mod.rs b/geo/src/algorithm/line_split/mod.rs index db150f926..c43194111 100644 --- a/geo/src/algorithm/line_split/mod.rs +++ b/geo/src/algorithm/line_split/mod.rs @@ -1,13 +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 c4d73d3c2..3360d3488 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -178,7 +178,7 @@ 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; +pub use line_split::{LineSplit, LineSplitResult, LineSplitTwiceResult}; /// Iterate over the lines in a geometry. pub mod lines_iter; From e709ebecf118a22099d17cd002947fc66d221131 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 15 Aug 2023 23:47:58 +0800 Subject: [PATCH 15/15] improve docs, imprecision causing failed tests :( --- .../algorithm/line_split/line_split_result.rs | 11 +- .../algorithm/line_split/line_split_trait.rs | 68 +++++---- .../line_split_trait_impl_for_line.rs | 40 +++--- .../line_split_trait_impl_for_linestring.rs | 132 +++++++++++++++--- .../line_split/line_split_twice_result.rs | 7 +- .../line_split/measure_line_string.rs | 20 ++- 6 files changed, 199 insertions(+), 79 deletions(-) diff --git a/geo/src/algorithm/line_split/line_split_result.rs b/geo/src/algorithm/line_split/line_split_result.rs index 8bfcfafc3..716e7a5c0 100644 --- a/geo/src/algorithm/line_split/line_split_result.rs +++ b/geo/src/algorithm/line_split/line_split_result.rs @@ -10,8 +10,15 @@ /// [.into_second()](LineSplitResult#method.into_second). /// /// ``` -/// if let Some(second) = my_line.line_split_twice(0.2, 0.5).into_second() { -/// // got the part between the two splits +/// # 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] diff --git a/geo/src/algorithm/line_split/line_split_trait.rs b/geo/src/algorithm/line_split/line_split_trait.rs index e47701725..decbf7bcd 100644 --- a/geo/src/algorithm/line_split/line_split_trait.rs +++ b/geo/src/algorithm/line_split/line_split_trait.rs @@ -25,29 +25,29 @@ where /// use geo::{Line, coord}; /// use geo::algorithm::{LineSplit, LineSplitResult}; /// let line = Line::new( - /// coord! {x: 0.0_f32, y:0.0_f32}, - /// coord! {x:10.0_f32, y:0.0_f32}, + /// 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_f32, y:0.0_f32}, - /// coord! {x: 6.0_f32, y:0.0_f32}, + /// coord! {x: 0.0, y:0.0}, + /// coord! {x: 6.0, y:0.0}, /// ), /// Line::new( - /// coord! {x: 6.0_f32, y:0.0_f32}, - /// coord! {x:10.0_f32, y:0.0_f32}, + /// coord! {x: 6.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, /// ) /// )) /// ); /// /// match result { - /// Some(LineSplitResult::First(line1))=>todo!(), - /// Some(LineSplitResult::Second(line2))=>todo!(), - /// Some(LineSplitResult::FirstSecond(line1, line2))=>todo!(), - /// None=>todo!(), + /// Some(LineSplitResult::First(line1))=>{}, + /// Some(LineSplitResult::Second(line2))=>{}, + /// Some(LineSplitResult::FirstSecond(line1, line2))=>{}, + /// None=>{}, /// } /// ``` fn line_split(&self, fraction: Scalar) -> Option>; @@ -128,34 +128,48 @@ where /// 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 to - /// executing the splits. + /// If `fraction_start > fraction_end`, then the values will be swapped prior splitting. /// - /// Returns `None` when + /// 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) + /// 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). /// - /// A [LineSplitTwiceResult]: LineSplitTwiceResult can contain between one and - /// three [Line](crate::Line) or [LineString](crate::LineString) objects. Please see the docs - /// for that type as it provides various helper methods to get the desired part(s) of the - /// output. + /// 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}; + /// use geo::{LineString, line_string}; /// use geo::algorithm::{LineSplit, EuclideanLength}; - /// // get the road section between chainage_from and chaingage_to - /// // (gets the second of the three result parts) - /// let my_road_line_string:LineString = todo!(); - /// let chainage_from = 20.0; - /// let chainage_to = 150.0; + /// 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 my_road_section:Option = my_road_line_string - /// .split_twice(chainage_from / road_len, chaingage_to / road_len) - /// .into_second(); + /// 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] 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 index 096933bf0..024982de7 100644 --- 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 @@ -12,11 +12,11 @@ where /// 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 + /// Returns [None] when /// - The provided `fraction` is NAN /// - The the object being sliced includes NAN or infinite coordinates /// - /// Otherwise Returns a [Some(LineSplitResult)](LineSplitResult) + /// Otherwise Returns a [`Some(LineSplitResult)`](LineSplitResult) /// /// example /// @@ -24,20 +24,20 @@ where /// use geo::{Line, coord}; /// use geo::algorithm::{LineSplit, LineSplitResult}; /// let line = Line::new( - /// coord! {x: 0.0_f32, y:0.0_f32}, - /// coord! {x:10.0_f32, y:0.0_f32}, + /// 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_f32, y:0.0_f32}, - /// coord! {x: 6.0_f32, y:0.0_f32}, + /// coord! {x: 0.0, y:0.0}, + /// coord! {x: 6.0, y:0.0}, /// ), /// Line::new( - /// coord! {x: 6.0_f32, y:0.0_f32}, - /// coord! {x:10.0_f32, y:0.0_f32}, + /// coord! {x: 6.0, y:0.0}, + /// coord! {x:10.0, y:0.0}, /// ) /// )) /// ); @@ -217,8 +217,8 @@ mod test { #[test] fn test_line_split_many() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( @@ -247,8 +247,8 @@ mod test { #[test] fn test_line_split_many_edge_right() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( @@ -274,8 +274,8 @@ mod test { #[test] fn test_line_split_many_double_edge_right() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( @@ -298,8 +298,8 @@ mod test { #[test] fn test_line_split_many_edge_left() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( @@ -325,8 +325,8 @@ mod test { #[test] fn test_line_split_many_double_edge_left() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( @@ -349,8 +349,8 @@ mod test { #[test] fn test_line_split_many_same_value() { let line = Line::new( - coord! {x: 0.0_f32, y:0.0_f32}, - coord! {x:10.0_f32, y:0.0_f32}, + 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!( 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 index 173c0b747..4cfaa5289 100644 --- 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 @@ -6,6 +6,44 @@ 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::*; @@ -16,18 +54,12 @@ where (false, true) => Some(First(self.clone())), (true, false) => Some(Second(self.clone())), _ => { - // Find the total length and the lengths of each segment at the same time; - // TODO: consider the possibility of a `LineStringMeasured` datatype in the future - // as this will be a common requirement in several algorithms, and would be a big - // performance boost when repeatedly slicing portions from the same LineStrings - // I think I saw a PreparedGeometry PR? maybe that will cover this? + + // measure linestring (rejects linestrings with less than 2 points) let LineStringMeasurements { length_total, length_segments, - } = match measure_line_string(self) { - Some(x) => x, - None => return None, - }; + } = 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) { @@ -38,8 +70,7 @@ where let length_fraction = fraction * length_total; // Set up some variables to track state in the for-loop let mut length_accumulated = Scalar::zero(); - // TODO: unwrap used; but should be safe since we check the length above - let mut coords_first_part = vec![*self.0.first().unwrap()]; + let mut coords_first_part = vec![*self.0.first()?]; let mut coords_second_part = Vec::new(); for (fractions, &length_segment) in @@ -58,7 +89,9 @@ where } else if length_fraction >= length_accumulated_after_segment { coords_first_part.push(b); } else { - // TODO: check for divide by zero + // 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) { @@ -199,7 +232,7 @@ mod test { // LineString::line_split_twice() // ============================================================================================= #[test] - fn split_twice_typical() { + fn line_split_twice_typical_1() { // I think if we exhaustively check // - `Line::line_split_twice()` and // - `LineString::line_split()` @@ -235,17 +268,44 @@ mod test { ); } + #[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 test_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 spot check for a typical case + fn line_split_many_typical_1() { + let line_string: LineString = line_string![ (x:0.0, y:0.0), @@ -280,4 +340,36 @@ mod test { ] ); } + #[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 index b37a3d614..2e5af8778 100644 --- a/geo/src/algorithm/line_split/line_split_twice_result.rs +++ b/geo/src/algorithm/line_split/line_split_twice_result.rs @@ -12,16 +12,15 @@ /// [.into_third()](LineSplitTwiceResult#method.into_third). /// /// ``` -/// if let Some(second) = my_line.line_split_twice(...).into_second() { -/// // got the part between the two splits -/// } +/// // 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(...).into_tuple() { +/// match my_line.line_split_twice(0.2, 0.5).unwrap().into_tuple() { /// (Some(first), Some(second), _) => todo!(), /// _ => None /// } diff --git a/geo/src/algorithm/line_split/measure_line_string.rs b/geo/src/algorithm/line_split/measure_line_string.rs index a306271ae..5d17ed871 100644 --- a/geo/src/algorithm/line_split/measure_line_string.rs +++ b/geo/src/algorithm/line_split/measure_line_string.rs @@ -3,17 +3,22 @@ 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, } -/// Simultaneously measure the total length of a line and the length of each segment -/// Returns `None` when -/// -/// - The `LineString` has less than two coords -/// - The resulting total_length is not finite +/// 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> @@ -21,6 +26,9 @@ where Scalar: CoordFloat, Line: EuclideanLength, { + if line_string.0.len() < 2 { + return None; + } let result = line_string.lines().fold( LineStringMeasurements { length_total: Scalar::zero(), @@ -39,7 +47,7 @@ where } }, ); - if result.length_total == Scalar::zero() || !result.length_total.is_finite() { + if !result.length_total.is_finite() { None } else { Some(result)