From dace23d84182da083f38b2541d3c15ec6772ecd2 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:24 +0800 Subject: [PATCH 01/27] created OffsetSignedCartesian trait --- geo/src/algorithm/mod.rs | 3 + geo/src/algorithm/offset_signed_cartesian.rs | 92 ++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 geo/src/algorithm/offset_signed_cartesian.rs diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index bba8a95d2..4632e28de 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -176,6 +176,9 @@ pub use lines_iter::LinesIter; pub mod map_coords; pub use map_coords::{MapCoords, MapCoordsInPlace}; +/// Apply a simple signed offset azzuming cartesian +pub mod offset_signed_cartesian; + /// Orient a `Polygon`'s exterior and interior rings. pub mod orient; pub use orient::Orient; diff --git a/geo/src/algorithm/offset_signed_cartesian.rs b/geo/src/algorithm/offset_signed_cartesian.rs new file mode 100644 index 000000000..aa6da633f --- /dev/null +++ b/geo/src/algorithm/offset_signed_cartesian.rs @@ -0,0 +1,92 @@ +use geo_types::Coord; + +use crate::{ + CoordFloat, CoordNum, Geometry, GeometryCollection, Line, LineString, MultiLineString, + MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle, kernels::Kernel, +}; + +/// Signed offset of Geometry assuming cartesian coordinate system. +/// This is a cheap offset algorithim that is suitable for flat coordinate systems (or if your lat/lon data is near the equator) +/// +/// My Priority for implementing the trait is as follows: +/// - Line +/// - LineString +/// - MultiLineString +/// - ... maybe some other easy ones like triangle, polygon +/// +/// The following are a list of known limitations, +/// some may be removed during development, +/// others are very hard to fix. +/// +/// - No checking for zero length input. +/// Invalid results may be caused by division by zero. +/// - Only local cropping where the output is self-intersecting. +/// Non-adjacent line segments in the output may be self-intersecting. +/// - There is no mitre-limit; A LineString which doubles back on itself will produce an elbow at infinity +pub trait OffsetSignedCartesian +where + T: CoordNum, +{ + fn offset(&self, distance:T) -> Self; +} + +impl OffsetSignedCartesian for Line where T: CoordFloat { + fn offset(&self, distance:T) -> Self{ + let delta = self.delta(); + let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); + let delta = Coord{ + x:delta.y/len, + y:-delta.x/len + }; + Line::new( + self.start+delta*distance, + self.end+delta*distance + ) + + } +} + +// impl Offset for LineString where T: CoordFloat { +// fn offset(&self, distance:T) -> Self{ +// let offset_segments = self.lines().map(|item|item.offset(distance)); +// let raw_offset_ls:Vec>; +// todo!("Working form here") +// } +// } + + +#[cfg(test)] +mod test { + use crate::algorithm::offset_signed_cartesian::OffsetSignedCartesian; + use crate::{line_string, Line, Coord}; + #[test] + fn offset_line_test(){ + let line = Line::new( + Coord{x:1f64, y:1f64}, + Coord{x:1f64, y:2f64}, + ); + let actual_result = line.offset(1.0); + assert_eq!( + actual_result, + Line::new( + Coord{x:2f64, y:1f64}, + Coord{x:2f64, y:2f64}, + ) + ); + } + #[test] + fn offset_line_test_negative(){ + let line = Line::new( + Coord{x:1f64, y:1f64}, + Coord{x:1f64, y:2f64} + ); + let actual_result = line.offset(-1.0); + assert_eq!( + actual_result, + Line::new( + Coord{x:0f64, y:1f64}, + Coord{x:0f64, y:2f64}, + ) + ); + } +} \ No newline at end of file From b3e4a97b7599fae2ac58b28f7d80ff90ee1a90b6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:24 +0800 Subject: [PATCH 02/27] added private fn line_intersection_with_parameter --- geo/src/algorithm/offset_signed_cartesian.rs | 256 ++++++++++++++++--- 1 file changed, 214 insertions(+), 42 deletions(-) diff --git a/geo/src/algorithm/offset_signed_cartesian.rs b/geo/src/algorithm/offset_signed_cartesian.rs index aa6da633f..0e884e866 100644 --- a/geo/src/algorithm/offset_signed_cartesian.rs +++ b/geo/src/algorithm/offset_signed_cartesian.rs @@ -1,10 +1,159 @@ + + use geo_types::Coord; use crate::{ - CoordFloat, CoordNum, Geometry, GeometryCollection, Line, LineString, MultiLineString, - MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle, kernels::Kernel, + Orientation, kernels::RobustKernel, CoordFloat, CoordNum, Geometry, GeometryCollection, Kernel, Line, + LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle }; +/// 2D Dot Product +/// TODO: Not sure where to put this +/// it looks like there is some stuff implemented on Point; but that feels misplaced to me? +/// I would have implemented it on Coord +/// but for my first pull request I trying to keep the damage to one file + +fn dot(a:Coord, b: Coord) -> T where T: CoordNum { + a.x * b.x + a.y * b.y +} + +/// 2D "Cross Product" +/// +/// If we pretend the `z` ordinate is zero we can still use the 3D cross product on 2D vectors and various useful properties still hold. +/// It is useful to simplify line segment intersection +/// Note: `cross_prod` is already defined on Point... but that it seems to be some other operation on 3 points +/// +/// TODO: make some tests to confirm the negative is in the right place +fn cross(a:Coord, b:Coord) -> T where T:CoordNum { + a.x*b.y - a.y*b.x +} + +/// Compute the magnitude of a Coord as if it was a vector +fn magnitude(a:Coord) -> T where T:CoordFloat{ + (a.x*a.x+a.y*a.y).sqrt() +} + +/// Return a new coord in the same direction as the old coord but with a magnitude of 1 unit +/// no protection from divide by zero +fn normalize(a:Coord) -> Coord where T:CoordFloat{ + a.clone()/magnitude(a) +} + +/// Return a new coord in the same direction as the old coord but with the specified magnitude +/// No protection from divide by zero +fn rescale(a:Coord, new_magnitude: T) -> Coord where T:CoordFloat{ + normalize(a) * new_magnitude +} + +/// computes the intersection between two line segments; a to b, and c to d +/// +/// The intersection of segments can be expressed as a parametric equation +/// where t1 and t2 are unknown scalars +/// +/// ```text +/// a + ab·t1 = c + cd·t2 +/// ``` +/// +/// > note: a real intersection can only happen when `0<=t1<=1 and 0<=t2<=1` +/// +/// This can be rearranged as follows: +/// +/// ```text +/// ab·t1 - cd·t2 = c - a +/// ``` +/// +/// by collecting the scalars t1 and -t2 into the column vector T, +/// and by collecting the vectors ab and cd into matrix M: +/// we get the matrix form: +/// +/// ```text +/// [ab_x cd_x][ t1] = [ac_x] +/// [ab_y cd_y][-t2] [ac_y] +/// ``` +/// +/// or +/// +/// ```text +/// M·T=ac +/// ``` +/// +/// the determinant of the matrix M is the inverse of the cross product of ab and cd. +/// +/// ```text +/// 1/(ab×cd) +/// ``` +/// +/// Therefore if ab×cd=0 the determinant is undefined and the matrix cannot be inverted +/// This means the lines are +/// a) parallel and +/// b) possibly collinear +/// +/// pre-multiplying both sides by the inverted 2x2 matrix we get: +/// +/// ```text +/// [ t1] = 1/(ab×cd)·[ cd_y -cd_x][ac_x] +/// [-t2] [-ab_y ab_x][ac_y] +/// ``` +/// +/// or +/// +/// ```text +/// T = M⁻¹·ac +/// ``` +/// +/// multiplied out +/// +/// ```text +/// [ t1] = 1/(ab_x·cd_y - ab_y·cd_x)·[ cd_y·ac_x - cd_x·ac_y] +/// [-t2] [-ab_y·ac_x + ab_x·ac_y] +/// ``` +/// +/// since it is neater to write cross products, observe that the above is equivalent to: +/// +/// ```text +/// [ t1] = [ ac×cd / ab×cd ] +/// [-t2] = [ ab×ac / ab×cd ] +/// ``` +struct LineItersectionWithParameterResult where T : CoordNum{ + t_ab:T, + t_cd:T, + point:Coord, +} + +fn line_intersection_with_parameter(line_ab:Line, line_cd:Line) -> LineItersectionWithParameterResult where T: CoordFloat{ + // TODO: we already have line intersection trait + // but i require the parameters for both lines + // which can be obtained at the same time + // This is a much dirtier algorithm; + let a = line_ab.start; + let b = line_ab.end; + let c = line_cd.start; + let d = line_cd.end; + + let ab = b - a; + let cd = d - c; + let ac = c - a; + + let ab_cross_cd = cross(ab, cd); + + if ab_cross_cd == num_traits::zero() { + // TODO: We can't tolerate this situation as it will cause a divide by zero in the next step + // Even values close to zero are a problem, but I don't know how to deal with that problem jut yet + todo!("") + } + + + let t_ab = cross(ac,cd) / ab_cross_cd; + let t_cd = cross(ac,cd) / ab_cross_cd; + let point = a + rescale(ab, t_ab); + LineItersectionWithParameterResult{ + t_ab, + t_cd, + point + } + +} + /// Signed offset of Geometry assuming cartesian coordinate system. /// This is a cheap offset algorithim that is suitable for flat coordinate systems (or if your lat/lon data is near the equator) /// @@ -12,14 +161,15 @@ use crate::{ /// - Line /// - LineString /// - MultiLineString -/// - ... maybe some other easy ones like triangle, polygon -/// +/// - ... maybe some closed shapes easy ones like triangle, polygon +/// /// The following are a list of known limitations, /// some may be removed during development, /// others are very hard to fix. /// /// - No checking for zero length input. /// Invalid results may be caused by division by zero. +/// - No check is implemented to prevent execution if the specified offset distance is zero /// - Only local cropping where the output is self-intersecting. /// Non-adjacent line segments in the output may be self-intersecting. /// - There is no mitre-limit; A LineString which doubles back on itself will produce an elbow at infinity @@ -27,66 +177,88 @@ pub trait OffsetSignedCartesian where T: CoordNum, { - fn offset(&self, distance:T) -> Self; + fn offset(&self, distance: T) -> Self; } -impl OffsetSignedCartesian for Line where T: CoordFloat { - fn offset(&self, distance:T) -> Self{ +impl OffsetSignedCartesian for Line +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self { let delta = self.delta(); let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); - let delta = Coord{ - x:delta.y/len, - y:-delta.x/len + let delta = Coord { + x: delta.y / len, + y: -delta.x / len, }; - Line::new( - self.start+delta*distance, - self.end+delta*distance - ) - + Line::new(self.start + delta * distance, self.end + delta * distance) } } -// impl Offset for LineString where T: CoordFloat { -// fn offset(&self, distance:T) -> Self{ -// let offset_segments = self.lines().map(|item|item.offset(distance)); -// let raw_offset_ls:Vec>; -// todo!("Working form here") -// } -// } +impl OffsetSignedCartesian for LineString +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self { + // if self.0.len() < 2 { + // // TODO: Fail on invalid input + // return self.clone(); + // } + // let offset_segments: Vec> = + // self.lines().map(|item| item.offset(distance)).collect(); + // if offset_segments.len() == 1 { + // return offset_segments[0].into(); + // } + // let x = offset_segments[0]; + // // TODO: we make a bet that the output has the same number of verticies as the input plus 1 + // // It is a safe bet for inputs with oblique bends and long segments; we could try `self.0.len() + 5` or something and + // // do some benchmarks on real examples to see if it is worth guessing a bit larger. + // let raw_offset_ls: Vec> = Vec::with_capacity(self.0.len()); + // raw_offset_ls.push(offset_segments[0].start); + // // the `pairwise` iterator is not a thing in rust because it makes the borrow checker sad :(, + // // the itertools crate has a similar tuple_windows function, + // // it does a clone of every element which is probably fine + // // for now we dont want to add that dependancy so we sacrifice + // // the readability of iterators and go for an old style for loop + // for i in 0..offset_segments.len() - 1usize { + // let ab = offset_segments[i]; + // let cd = offset_segments[i + 1usize]; + // // TODO: Do I need to use the RobustKernel for this? The simple kernel has no impl + // // (it has a comment which says SimpleKernel says it is for integer types? + // // I could add an impl to SimpleKernel for float types that does a faster check?) + // // We really want to prevent "very close to parallel" segments as this will cause + // // nonsense for our line segment intersection algorithim + // if RobustKernel::orient2d(ab.start, ab.end, cd.d) == Orientation::Collinear { + // raw_offset_ls.push(ab.end); + // continue; + // } + + // } + todo!("Not done yet. I need to make a commit here to keep my progress") + } +} #[cfg(test)] mod test { use crate::algorithm::offset_signed_cartesian::OffsetSignedCartesian; - use crate::{line_string, Line, Coord}; + use crate::{line_string, Coord, Line}; #[test] - fn offset_line_test(){ - let line = Line::new( - Coord{x:1f64, y:1f64}, - Coord{x:1f64, y:2f64}, - ); + fn offset_line_test() { + let line = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); let actual_result = line.offset(1.0); assert_eq!( actual_result, - Line::new( - Coord{x:2f64, y:1f64}, - Coord{x:2f64, y:2f64}, - ) + Line::new(Coord { x: 2f64, y: 1f64 }, Coord { x: 2f64, y: 2f64 },) ); } #[test] - fn offset_line_test_negative(){ - let line = Line::new( - Coord{x:1f64, y:1f64}, - Coord{x:1f64, y:2f64} - ); + fn offset_line_test_negative() { + let line = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); let actual_result = line.offset(-1.0); assert_eq!( actual_result, - Line::new( - Coord{x:0f64, y:1f64}, - Coord{x:0f64, y:2f64}, - ) + Line::new(Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 },) ); } -} \ No newline at end of file +} From 9d1a2f4fef52fa4df51d98affae19b20dced7a9c Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:24 +0800 Subject: [PATCH 03/27] started drafting offset RFC --- rfcs/2022-11-11-offset.md | 145 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 rfcs/2022-11-11-offset.md diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md new file mode 100644 index 000000000..f54e56d4c --- /dev/null +++ b/rfcs/2022-11-11-offset.md @@ -0,0 +1,145 @@ +- Feature Name: `offset` +- Start Date: 2022-11-11 +- [Feature PR] + + +This is a cheap offset algorithim that is suitable for flat coordinate systems +(or if your lat/lon data is near the equator and there is no need for +correctness) + +## Trait Implementations + +My Priority for implementing the trait is as follows: + +- Line +- LineString +- MultiLineString +- ... maybe some closed shapes like triangle, rect, and the most diffucult is + polygon and multi polygon. Possibly this algorithim is not suitable for + polygon, and other operations like th the minkowski sum is more appropriate in + those cases? + +## Limitations + +Some may be removed during development, others are very hard to fix. + +- [ ] No checking for zero length input. Invalid results may be caused by + division by zero. +- [ ] No check is implemented to prevent execution if the specified offset + distance is zero +- [ ] No Mitre-limit is implemented; A LineString which doubles back on itself + will produce an elbow at infinity +- [ ] Only local cropping where the output is self-intersecting. Non-adjacent + line segments in the output may be self-intersecting. + +## References + +Loosely follows the algorithim described by +[Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005](https://hal.inria.fr/inria-00518005/document) +This was the first google result for 'line offset algorithm' + + +## Algorithim + +### Definitions (For the psudocode in this readme only) + +Type definitions +```python +from typing import List, Tuple +from nicks_line_tools.Vector2 import Vector2 + +# define type aliases for convenience: +LineString = List[Vector2] +MultiLineString = List[LineString] +LineSegment = Tuple[Vector2, Vector2] +LineSegmentList = List[Tuple[Vector2, Vector2]] +Parameter = float + +# declare type of variables used: +input_linestring: LineString +offset_ls: LineString +offset_segments: LineSegmentList +raw_offset_ls: LineString +``` + +Function Type Definitions (pseudocode) +```python +intersect = (tool: LineString, target: LineString) -> (point_of_intersection: Optional[Vector2], distance_along_target: List[Parameter]) +project = (tool: Vector2, target: LineString) -> (nearest_point_on_target_to_tool: Vector2, distance_along_target: Parameter) +interpolate = (distance_along_target: Parameter, target: LineString) -> (point_on_target: Vector2) +``` + + + + +### Algorithm 1 - Pre-Treatment +1. Pretreatment steps from the paper are not implemented... these mostly deal with arcs and malformed input geometry +1. No check is performed to prevent execution when `d==0` + + +### Algorithm 0.1 - Segment Offset + +1. Create an empty `LineSegmentList` called `offset_segments` +1. For each `LineSegment` of `input_linestring` + 1. Take each segment `(a,b)` of `input_linestring` and compute the vector from the start to the end of the segment
+ `ab = b - a` + 1. rotate this vector by -90 degrees to obtain the 'left normal'
+ `ab_left = Vector2(-ab.y, ab.x)` + 1. normalise `ab_left` by dividing each component by the magnitude of `ab_left`. + 1. multiply the vector by the scalar `d` to obtain the `segment_offset_vector` + 1. add the `segment_offset_vector` to `a` and `b` to get `offset_a` and `offset_b` + 1. append `(offset_a, offset_b)` to `offset_segments` + + +### Algorithm 1 - Line Extension +4. Create an empty `LineString` called `raw_offset_ls` +1. Append `offset_segments[0][0]` to `raw_offset_ls` +1. For each pair of consecutive segments `(a,b),(c,d)` in `offset_segments` + 1. If `(a,b)` is co-linear with `(c,d)` then append `b` to raw_offset_ls, and go to the next pair of segments. + 1. Otherwise, find the intersection point `p` of the infinite lines that are collinear with `(a,b)` and `(c,d)`;
+ Pay attention to the location of `p` relative to each of the segments: + 1. if `p` is within the segment it is a *True Intersection Point* or **TIP** + 1. If `p` is outside the segment it is called a *False Intersection Point* or **FIP**.
+ **FIP**s are further classified as follows: + - **Positive FIP** or **PFIP** if `p` is after the end of a segment, or + - **Negative FIP** or **NFIP** if `p` is before the start of a segment. + 1. If `p` is a **TIP** for both `(a,b)` and `(c,d)` append `p` to `raw_offset_ls` + 1. If `p` is a **FIP** for both `(a,b)` and `(c,d)` + 1. If `p` is a **PFIP** for `(a,b)` then append `p`to `raw_offset_ls` + 1. Otherwise, append `b` then `c` to `raw_offset_ls` + 1. Otherwise, append `b` then `c` to `raw_offset_ls` +1. Remove zero length segments in `raw_offset_ls` + +### Algorithm 4.1 - Dual Clipping: +8. Find `raw_offset_ls_twin` by repeating Algorithms 0.1 and 1 but offset the `input_linestring` in the opposite direction (`-d`) +1. Find `intersection_points` between + 1. `raw_offset_ls` and `raw_offset_ls` + 1. `raw_offset_ls` and `raw_offset_ls_twin` + +1. Find `split_offset_mls` by splitting `raw_offset_ls` at each point in `intersection_points` +1. If `intersection_points` was empty, then add `raw_offset_ls` to `split_offset_mls` and skip to Algorithm 4.2. +1. Delete each `LineString` in `split_offset_mls` if it intersects the `input_linestring`
+ unless the intersection is with the first or last `LineSegment` of `input_linestring` + 1. If we find such an intersection point that *is* on the first or last `LineSegment` of `input_linestring`
+ then add the intersection point to a list called `cut_targets` + +### Algorithm 4.1.2 - Cookie Cutter: +13. For each point `p` in `cut_targets` + 1. construct a circle of diameter `d` with its center at `p` + 1. delete all parts of any linestring in `split_offset_mls` which falls within this circle +1. Empty the `cut_targets` list + +### Algorithm 4.1.3 - Proximity Clipping +17. For each linestring `item_ls` in `split_offset_mls` + 1. For each segment `(a,b)` in `item_ls` + 1. For each segment `(u,v)` of `input_linestring` + - Find `a_proj` and `b_proj` by projecting `a` and `b` onto segment `(u,v)` + - Adjust the projected points such that `a_proj` and `b_proj` lie **at** or **between** `u` and `v` + - Find `a_dist` by computing `magnitude(a_proj - a)` + - Find `b_dist` by computing `magnitude(b_proj - b)` + - if either `a_dist` or `b_dist` is less than the absolute value of `d` then + - if `a_dist > b_dist`add `a_proj` to `cut_targets` + - Otherwise, add `b_proj` to `cut_targets` +1. Repeat Algorithm 4.1.2 +1. Remove zero length segments and empty linestrings etc **(TODO: not yet implemented)** +1. Join remaining linestrings that are touching to form new linestring(s) **(TODO: not yet implemented)** \ No newline at end of file From 62f1690b6a006c6380bd45c813dc3c1d5900b61b Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:25 +0800 Subject: [PATCH 04/27] add to docs and tests --- geo/src/algorithm/offset_signed_cartesian.rs | 413 +++++++++++++------ 1 file changed, 284 insertions(+), 129 deletions(-) diff --git a/geo/src/algorithm/offset_signed_cartesian.rs b/geo/src/algorithm/offset_signed_cartesian.rs index 0e884e866..edd4b0997 100644 --- a/geo/src/algorithm/offset_signed_cartesian.rs +++ b/geo/src/algorithm/offset_signed_cartesian.rs @@ -1,167 +1,231 @@ - +/// # Offset - Signed Cartesian +/// +/// ## Utility Functions +/// +/// This module starts by defining a heap of private utility functions +/// [dot_product], [cross_product], [magnitude], [normalize], [rescale] +/// +/// It looks like some of these are already implemented on stuff implemented +/// on the Point struct; but that feels misplaced to me? +/// +/// Looks like they might eventually belong in the Kernel trait?? +/// +/// For my first pull request I'll just implement them functional style and keep +/// the damage to one module ;) use geo_types::Coord; +use crate::{kernels::RobustKernel, CoordFloat, CoordNum, Kernel, Line, LineString, Orientation}; -use crate::{ - Orientation, kernels::RobustKernel, CoordFloat, CoordNum, Geometry, GeometryCollection, Kernel, Line, - LineString, MultiLineString, MultiPoint, MultiPolygon, Point, Polygon, Rect, Triangle -}; /// 2D Dot Product -/// TODO: Not sure where to put this -/// it looks like there is some stuff implemented on Point; but that feels misplaced to me? -/// I would have implemented it on Coord -/// but for my first pull request I trying to keep the damage to one file - -fn dot(a:Coord, b: Coord) -> T where T: CoordNum { - a.x * b.x + a.y * b.y +fn dot_product(left: Coord, right: Coord) -> T +where + T: CoordNum, +{ + left.x * right.x + left.y * right.y } /// 2D "Cross Product" /// -/// If we pretend the `z` ordinate is zero we can still use the 3D cross product on 2D vectors and various useful properties still hold. -/// It is useful to simplify line segment intersection -/// Note: `cross_prod` is already defined on Point... but that it seems to be some other operation on 3 points +/// If we pretend the `z` ordinate is zero we can still use the 3D cross product +/// on 2D vectors and various useful properties still hold (e.g. it is still the +/// area of the parallelogram formed by the two input vectors) +/// +/// From basis vectors `i`,`j`,`k` and the axioms on wikipedia +/// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); +/// +/// ```text +/// i×j = k +/// j×k = i +/// k×i = j /// -/// TODO: make some tests to confirm the negative is in the right place -fn cross(a:Coord, b:Coord) -> T where T:CoordNum { - a.x*b.y - a.y*b.x +/// j×i = -k +/// k×j = -i +/// i×k = -j +/// +/// i×i = j×j = k×k = 0 +/// ``` +/// +/// We can define the 2D cross product as the magnitude of the 3D cross product +/// as follows +/// +/// ```text +/// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| +/// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| +/// = |a_x·b_x·( 0 ) + a_x·b_y·( k ) + a_y·b_x·(-k ) + a_y·b_y·( 0 )| +/// = | (a_x·b_y - a_y·b_x)·k | +/// = a_x·b_y - a_y·b_x +/// ``` +/// +/// Note: `cross_prod` is already defined on Point... but that it seems to be +/// some other operation on 3 points +fn cross_product(left: Coord, right: Coord) -> T +where + T: CoordNum, +{ + left.x * right.y - left.y * right.x } /// Compute the magnitude of a Coord as if it was a vector -fn magnitude(a:Coord) -> T where T:CoordFloat{ - (a.x*a.x+a.y*a.y).sqrt() +fn magnitude(a: Coord) -> T +where + T: CoordFloat, +{ + (a.x * a.x + a.y * a.y).sqrt() } -/// Return a new coord in the same direction as the old coord but with a magnitude of 1 unit +/// Return a new coord in the same direction as the old coord but +/// with a magnitude of 1 unit /// no protection from divide by zero -fn normalize(a:Coord) -> Coord where T:CoordFloat{ - a.clone()/magnitude(a) +fn normalize(a: Coord) -> Coord +where + T: CoordFloat, +{ + a.clone() / magnitude(a) } -/// Return a new coord in the same direction as the old coord but with the specified magnitude +/// Return a new coord in the same direction as the old coord but with the +/// specified magnitude /// No protection from divide by zero -fn rescale(a:Coord, new_magnitude: T) -> Coord where T:CoordFloat{ +fn rescale(a: Coord, new_magnitude: T) -> Coord +where + T: CoordFloat, +{ normalize(a) * new_magnitude } -/// computes the intersection between two line segments; a to b, and c to d +/// Struct to contain the result for [line_intersection_with_parameter] +struct LineIntersectionWithParameterResult +where + T: CoordNum, +{ + t_ab: T, + t_cd: T, + intersection: Coord, +} + +/// Computes the intersection between two line segments; +/// a to b (`ab`), and c to d (`cd`) /// +/// We already have LineIntersection trait BUT we need a function that also +/// returns the parameters for both lines described below. The LineIntersection +/// trait uses some fancy unrolled code it seems unlikely it could be adapted +/// for this purpose. +/// +/// Returns the intersection point **and** parameters `t_ab` and `t_cd` +/// described below +/// /// The intersection of segments can be expressed as a parametric equation -/// where t1 and t2 are unknown scalars -/// +/// where `t_ab` and `t_cd` are unknown scalars : +/// /// ```text -/// a + ab·t1 = c + cd·t2 +/// a + ab · t_ab = c + cd · t_cd /// ``` -/// -/// > note: a real intersection can only happen when `0<=t1<=1 and 0<=t2<=1` +/// +/// > note: a real intersection can only happen when `0 <= t_ab <= 1` and +/// > `0 <= t_cd <= 1` but this function will find intersections anyway +/// > which may lay outside of the line segments /// /// This can be rearranged as follows: -/// +/// /// ```text -/// ab·t1 - cd·t2 = c - a +/// ab · t_ab - cd · t_cd = c - a /// ``` /// -/// by collecting the scalars t1 and -t2 into the column vector T, -/// and by collecting the vectors ab and cd into matrix M: +/// Collecting the scalars `t_ab` and `-t_cd` into the column vector `T`, +/// and by collecting the vectors `ab` and `cd` into matrix `M`: /// we get the matrix form: /// /// ```text -/// [ab_x cd_x][ t1] = [ac_x] -/// [ab_y cd_y][-t2] [ac_y] +/// [ab_x cd_x][ t_ab] = [ac_x] +/// [ab_y cd_y][-t_cd] [ac_y] /// ``` -/// +/// /// or -/// +/// /// ```text /// M·T=ac /// ``` -/// -/// the determinant of the matrix M is the inverse of the cross product of ab and cd. -/// +/// +/// The determinant of the matrix `M` is the reciprocal of the cross product +/// of `ab` and `cd`. +/// /// ```text /// 1/(ab×cd) /// ``` -/// -/// Therefore if ab×cd=0 the determinant is undefined and the matrix cannot be inverted -/// This means the lines are -/// a) parallel and -/// b) possibly collinear /// -/// pre-multiplying both sides by the inverted 2x2 matrix we get: -/// +/// Therefore if `ab×cd = 0` the determinant is undefined and the matrix cannot +/// be inverted this means the lines are either +/// a) parallel or +/// b) collinear +/// +/// Pre-multiplying both sides by the inverted 2x2 matrix we get: +/// /// ```text -/// [ t1] = 1/(ab×cd)·[ cd_y -cd_x][ac_x] -/// [-t2] [-ab_y ab_x][ac_y] +/// [ t_ab] = 1/(ab×cd) · [ cd_y -cd_x][ac_x] +/// [-t_cd] [-ab_y ab_x][ac_y] /// ``` -/// +/// /// or -/// +/// /// ```text /// T = M⁻¹·ac /// ``` -/// -/// multiplied out -/// +/// +/// Expands to: +/// /// ```text -/// [ t1] = 1/(ab_x·cd_y - ab_y·cd_x)·[ cd_y·ac_x - cd_x·ac_y] -/// [-t2] [-ab_y·ac_x + ab_x·ac_y] +/// [ t_ab] = 1/(ab_x·cd_y - ab_y·cd_x)·[ cd_y·ac_x - cd_x·ac_y] +/// [-t_cd] [-ab_y·ac_x + ab_x·ac_y] /// ``` -/// -/// since it is neater to write cross products, observe that the above is equivalent to: -/// +/// +/// Since it is tidier to write cross products, observe that the above is +/// equivalent to: +/// /// ```text -/// [ t1] = [ ac×cd / ab×cd ] -/// [-t2] = [ ab×ac / ab×cd ] +/// [ t_ab] = [ ac×cd / ab×cd ] +/// [-t_cd] = [ ab×ac / ab×cd ] /// ``` -struct LineItersectionWithParameterResult where T : CoordNum{ - t_ab:T, - t_cd:T, - point:Coord, -} -fn line_intersection_with_parameter(line_ab:Line, line_cd:Line) -> LineItersectionWithParameterResult where T: CoordFloat{ - // TODO: we already have line intersection trait - // but i require the parameters for both lines - // which can be obtained at the same time - // This is a much dirtier algorithm; - let a = line_ab.start; - let b = line_ab.end; - let c = line_cd.start; - let d = line_cd.end; +fn line_intersection_with_parameter( + a:Coord, + b:Coord, + c:Coord, + d:Coord, +) -> LineIntersectionWithParameterResult +where + T: CoordFloat, +{ let ab = b - a; let cd = d - c; let ac = c - a; - let ab_cross_cd = cross(ab, cd); + let ab_cross_cd = cross_product(ab, cd); if ab_cross_cd == num_traits::zero() { - // TODO: We can't tolerate this situation as it will cause a divide by zero in the next step - // Even values close to zero are a problem, but I don't know how to deal with that problem jut yet + // TODO: We can't tolerate this situation as it will cause a divide by + // zero in the next step. Even values close to zero are a problem, + // but I don't know how to deal with that problem jut yet todo!("") } - - let t_ab = cross(ac,cd) / ab_cross_cd; - let t_cd = cross(ac,cd) / ab_cross_cd; - let point = a + rescale(ab, t_ab); - LineItersectionWithParameterResult{ - t_ab, - t_cd, - point - } - + let t_ab = cross_product(ac, cd) / ab_cross_cd; + let t_cd = cross_product(ac, cd) / ab_cross_cd; + let intersection = a + rescale(ab, t_ab); + LineIntersectionWithParameterResult { t_ab, t_cd, intersection } } /// Signed offset of Geometry assuming cartesian coordinate system. -/// This is a cheap offset algorithim that is suitable for flat coordinate systems (or if your lat/lon data is near the equator) +/// +/// This is a cheap offset algorithm that is suitable for flat coordinate systems +/// (or if your lat/lon data is near the equator) /// /// My Priority for implementing the trait is as follows: /// - Line /// - LineString /// - MultiLineString -/// - ... maybe some closed shapes easy ones like triangle, polygon +/// - ... maybe some closed shapes like triangle, polygon? /// /// The following are a list of known limitations, /// some may be removed during development, @@ -169,10 +233,12 @@ fn line_intersection_with_parameter(line_ab:Line, line_cd:Line) -> Line /// /// - No checking for zero length input. /// Invalid results may be caused by division by zero. -/// - No check is implemented to prevent execution if the specified offset distance is zero +/// - No check is implemented to prevent execution if the specified offset +/// distance is zero. /// - Only local cropping where the output is self-intersecting. /// Non-adjacent line segments in the output may be self-intersecting. -/// - There is no mitre-limit; A LineString which doubles back on itself will produce an elbow at infinity +/// - There is no mitre-limit; A LineString which +/// doubles back on itself will produce an elbow at infinity pub trait OffsetSignedCartesian where T: CoordNum, @@ -200,49 +266,138 @@ where T: CoordFloat, { fn offset(&self, distance: T) -> Self { - // if self.0.len() < 2 { - // // TODO: Fail on invalid input - // return self.clone(); - // } - // let offset_segments: Vec> = - // self.lines().map(|item| item.offset(distance)).collect(); - // if offset_segments.len() == 1 { - // return offset_segments[0].into(); - // } - // let x = offset_segments[0]; - // // TODO: we make a bet that the output has the same number of verticies as the input plus 1 - // // It is a safe bet for inputs with oblique bends and long segments; we could try `self.0.len() + 5` or something and - // // do some benchmarks on real examples to see if it is worth guessing a bit larger. - // let raw_offset_ls: Vec> = Vec::with_capacity(self.0.len()); - // raw_offset_ls.push(offset_segments[0].start); - // // the `pairwise` iterator is not a thing in rust because it makes the borrow checker sad :(, - // // the itertools crate has a similar tuple_windows function, - // // it does a clone of every element which is probably fine - // // for now we dont want to add that dependancy so we sacrifice - // // the readability of iterators and go for an old style for loop - // for i in 0..offset_segments.len() - 1usize { - // let ab = offset_segments[i]; - // let cd = offset_segments[i + 1usize]; - // // TODO: Do I need to use the RobustKernel for this? The simple kernel has no impl - // // (it has a comment which says SimpleKernel says it is for integer types? - // // I could add an impl to SimpleKernel for float types that does a faster check?) - // // We really want to prevent "very close to parallel" segments as this will cause - // // nonsense for our line segment intersection algorithim - // if RobustKernel::orient2d(ab.start, ab.end, cd.d) == Orientation::Collinear { - // raw_offset_ls.push(ab.end); - // continue; - // } + if self.0.len() < 2 { + // TODO: Fail on invalid input + return self.clone(); + } + let offset_segments: Vec> = + self.lines().map(|item| item.offset(distance)).collect(); + if offset_segments.len() == 1 { + return offset_segments[0].into(); + } + let x = offset_segments[0]; + // Guess that the output has the same number of vertices as the input. + // It is a safe bet for inputs with oblique bends and long segments; + let mut raw_offset_ls: Vec> = Vec::with_capacity(self.0.len()); + raw_offset_ls.push(offset_segments[0].start.clone()); + // safe, non-copy `pairwise` iterator is not a thing in rust because + // the borrow checker is triggered by too much fun. + // The itertools crate has a `tuple_windows` function + // (it does a clone of every element which is probably fine?) + // Extra dependencies are naff so we sacrifice the readability of + // iterators and go for an old style for loop; + for i in 0..offset_segments.len() - 1usize { + let line_ab = offset_segments[i]; + let line_cd = offset_segments[i + 1usize]; + + let a = line_ab.start; + let b = line_ab.end; + let c = line_cd.start; + let d = line_cd.end; + + let ab = b-a; + let cd = d-c; + + // check for colinear case + // This is a flakey check with potentially unsafe type cast :/ + if ::from(cross_product(ab, cd)).unwrap() < 0.0000001f64 { + raw_offset_ls.push(b); + continue; + } + // TODO: Do we need the full overhead of RobustKernel for this? + // The simple kernel impl seems to be blank? + // if RobustKernel::orient2d(ab.start, ab.end, cd.end) == Orientation::Collinear { + // raw_offset_ls.push(ab.end); + // continue; + // } + let LineIntersectionWithParameterResult{t_ab, t_cd, intersection} = line_intersection_with_parameter(a, b, c, d); - // } - todo!("Not done yet. I need to make a commit here to keep my progress") + let TIP_ab = num_traits::zero::() <= t_ab && t_ab <= num_traits::one(); + let FIP_ab = ! TIP_ab; + let PFIP_ab = FIP_ab && t_ab > num_traits::zero(); + + + let TIP_cd = num_traits::zero::() <= t_cd && t_cd <= num_traits::one(); + let FIP_cd = ! TIP_cd; + + + if TIP_ab && TIP_cd { + // Case 2a + // TODO: test for mitre limit + raw_offset_ls.push(intersection); + } else if FIP_ab && FIP_cd{ + // Case 2b. + if PFIP_ab { + // TODO: test for mitre limit + raw_offset_ls.push(intersection); + } else { + raw_offset_ls.push(b); + raw_offset_ls.push(c); + } + } else { + // Case 2c. (either ab or cd + raw_offset_ls.push(b); + raw_offset_ls.push(c); + } + raw_offset_ls.push(d) + } + todo!("Not done yet.") } } #[cfg(test)] mod test { - use crate::algorithm::offset_signed_cartesian::OffsetSignedCartesian; + // crate dependencies use crate::{line_string, Coord, Line}; + + // private imports + use super::{ + cross_product, + OffsetSignedCartesian + }; + + #[test] + fn test_cross_product(){ + let a = Coord{x:0f64, y:0f64}; + let b = Coord{x:0f64, y:1f64}; + let c = Coord{x:1f64, y:0f64}; + + let ab = b - a; + let ac = c - a; + + + // expect the area of the parallelogram + assert_eq!( + cross_product(ac, ab), + 1f64 + ); + // expect swapping will result in negative + assert_eq!( + cross_product(ab, ac), + -1f64 + ); + + // Add skew + let a = Coord{x:0f64, y:0f64}; + let b = Coord{x:0f64, y:1f64}; + let c = Coord{x:1f64, y:1f64}; + + let ab = b - a; + let ac = c - a; + + // expect the area of the parallelogram + assert_eq!( + cross_product(ac, ab), + 1f64 + ); + // expect swapping will result in negative + assert_eq!( + cross_product(ab, ac), + -1f64 + ); + } + #[test] fn offset_line_test() { let line = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); From b3cb56da185ef2e2da14cdfce4039fa0035a287a Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:25 +0800 Subject: [PATCH 05/27] switch to pairwise iterator --- geo/src/algorithm/offset_signed_cartesian.rs | 223 +++++++++---------- 1 file changed, 103 insertions(+), 120 deletions(-) diff --git a/geo/src/algorithm/offset_signed_cartesian.rs b/geo/src/algorithm/offset_signed_cartesian.rs index edd4b0997..00a76657c 100644 --- a/geo/src/algorithm/offset_signed_cartesian.rs +++ b/geo/src/algorithm/offset_signed_cartesian.rs @@ -1,21 +1,19 @@ +use crate::{kernels::RobustKernel, CoordFloat, CoordNum, Kernel, Line, LineString, Orientation}; /// # Offset - Signed Cartesian -/// +/// /// ## Utility Functions -/// -/// This module starts by defining a heap of private utility functions +/// +/// This module starts by defining a heap of private utility functions /// [dot_product], [cross_product], [magnitude], [normalize], [rescale] /// -/// It looks like some of these are already implemented on stuff implemented +/// It looks like some of these are already implemented on stuff implemented /// on the Point struct; but that feels misplaced to me? -/// +/// /// Looks like they might eventually belong in the Kernel trait?? -/// +/// /// For my first pull request I'll just implement them functional style and keep /// the damage to one module ;) - use geo_types::Coord; -use crate::{kernels::RobustKernel, CoordFloat, CoordNum, Kernel, Line, LineString, Orientation}; - /// 2D Dot Product fn dot_product(left: Coord, right: Coord) -> T @@ -26,29 +24,29 @@ where } /// 2D "Cross Product" -/// +/// /// If we pretend the `z` ordinate is zero we can still use the 3D cross product /// on 2D vectors and various useful properties still hold (e.g. it is still the /// area of the parallelogram formed by the two input vectors) -/// +/// /// From basis vectors `i`,`j`,`k` and the axioms on wikipedia /// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); -/// +/// /// ```text /// i×j = k /// j×k = i /// k×i = j -/// +/// /// j×i = -k /// k×j = -i /// i×k = -j -/// +/// /// i×i = j×j = k×k = 0 /// ``` -/// +/// /// We can define the 2D cross product as the magnitude of the 3D cross product /// as follows -/// +/// /// ```text /// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| /// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| @@ -56,7 +54,7 @@ where /// = | (a_x·b_y - a_y·b_x)·k | /// = a_x·b_y - a_y·b_x /// ``` -/// +/// /// Note: `cross_prod` is already defined on Point... but that it seems to be /// some other operation on 3 points fn cross_product(left: Coord, right: Coord) -> T @@ -66,7 +64,7 @@ where left.x * right.y - left.y * right.x } -/// Compute the magnitude of a Coord as if it was a vector +/// Compute the magnitude of a Coord fn magnitude(a: Coord) -> T where T: CoordFloat, @@ -106,10 +104,10 @@ where /// Computes the intersection between two line segments; /// a to b (`ab`), and c to d (`cd`) -/// +/// /// We already have LineIntersection trait BUT we need a function that also /// returns the parameters for both lines described below. The LineIntersection -/// trait uses some fancy unrolled code it seems unlikely it could be adapted +/// trait uses some fancy unrolled code it seems unlikely it could be adapted /// for this purpose. /// /// Returns the intersection point **and** parameters `t_ab` and `t_cd` @@ -188,18 +186,17 @@ where /// ``` fn line_intersection_with_parameter( - a:Coord, - b:Coord, - c:Coord, - d:Coord, + a: &Coord, + b: &Coord, + c: &Coord, + d: &Coord, ) -> LineIntersectionWithParameterResult where T: CoordFloat, { - - let ab = b - a; - let cd = d - c; - let ac = c - a; + let ab = *b - *a; + let cd = *d - *c; + let ac = *c - *a; let ab_cross_cd = cross_product(ab, cd); @@ -212,12 +209,16 @@ where let t_ab = cross_product(ac, cd) / ab_cross_cd; let t_cd = cross_product(ac, cd) / ab_cross_cd; - let intersection = a + rescale(ab, t_ab); - LineIntersectionWithParameterResult { t_ab, t_cd, intersection } + let intersection = *a + rescale(ab, t_ab); + LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + } } /// Signed offset of Geometry assuming cartesian coordinate system. -/// +/// /// This is a cheap offset algorithm that is suitable for flat coordinate systems /// (or if your lat/lon data is near the equator) /// @@ -261,6 +262,10 @@ where } } +fn pairwise(iterable: &[T]) -> std::iter::Zip, std::slice::Iter> { + iterable.iter().zip(iterable[1..].iter()) +} + impl OffsetSignedCartesian for LineString where T: CoordFloat, @@ -276,73 +281,67 @@ where return offset_segments[0].into(); } let x = offset_segments[0]; - // Guess that the output has the same number of vertices as the input. - // It is a safe bet for inputs with oblique bends and long segments; - let mut raw_offset_ls: Vec> = Vec::with_capacity(self.0.len()); - raw_offset_ls.push(offset_segments[0].start.clone()); - // safe, non-copy `pairwise` iterator is not a thing in rust because - // the borrow checker is triggered by too much fun. - // The itertools crate has a `tuple_windows` function - // (it does a clone of every element which is probably fine?) - // Extra dependencies are naff so we sacrifice the readability of - // iterators and go for an old style for loop; - for i in 0..offset_segments.len() - 1usize { - let line_ab = offset_segments[i]; - let line_cd = offset_segments[i + 1usize]; + + + std::iter::once(offset_segments[0].start) + .chain( + pairwise(&offset_segments[..]) + .flat_map(|(Line { start:a, end:b }, Line{start:c, end:d})| { - let a = line_ab.start; - let b = line_ab.end; - let c = line_cd.start; - let d = line_cd.end; + let ab = *b - *a; + let cd = *d - *c; - let ab = b-a; - let cd = d-c; + let mut raw_offset_ls:Vec> = Vec::new(); + // check for colinear case; this is a flakey check with a + // possible panic type cast :/ + // TODO: Could use RobustKernel for this? The simple kernel impl seems to be blank? + // I don't need the accuracy, need speed :) + // Alternative implementation: + // if RobustKernel::orient2d(a, b, c) == Orientation::Collinear { + // raw_offset_ls.push(b); + // }else {... + if ::from(cross_product(ab, cd)).unwrap() < 0.0000001f64 { + raw_offset_ls.push(*b); + } else { - // check for colinear case - // This is a flakey check with potentially unsafe type cast :/ - if ::from(cross_product(ab, cd)).unwrap() < 0.0000001f64 { - raw_offset_ls.push(b); - continue; - } - // TODO: Do we need the full overhead of RobustKernel for this? - // The simple kernel impl seems to be blank? - // if RobustKernel::orient2d(ab.start, ab.end, cd.end) == Orientation::Collinear { - // raw_offset_ls.push(ab.end); - // continue; - // } + let LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + } = line_intersection_with_parameter(a, b, c, d); - let LineIntersectionWithParameterResult{t_ab, t_cd, intersection} = line_intersection_with_parameter(a, b, c, d); + let tip_ab = num_traits::zero::() <= t_ab && t_ab <= num_traits::one(); + let fip_ab = !tip_ab; + let pfip_ab = fip_ab && t_ab > num_traits::zero(); - let TIP_ab = num_traits::zero::() <= t_ab && t_ab <= num_traits::one(); - let FIP_ab = ! TIP_ab; - let PFIP_ab = FIP_ab && t_ab > num_traits::zero(); - - - let TIP_cd = num_traits::zero::() <= t_cd && t_cd <= num_traits::one(); - let FIP_cd = ! TIP_cd; - - - if TIP_ab && TIP_cd { - // Case 2a - // TODO: test for mitre limit - raw_offset_ls.push(intersection); - } else if FIP_ab && FIP_cd{ - // Case 2b. - if PFIP_ab { - // TODO: test for mitre limit - raw_offset_ls.push(intersection); - } else { - raw_offset_ls.push(b); - raw_offset_ls.push(c); + let tip_cd = num_traits::zero::() <= t_cd && t_cd <= num_traits::one(); + let fip_cd = !tip_cd; + + if tip_ab && tip_cd { + // Case 2a + // TODO: test for mitre limit + raw_offset_ls.push(intersection); + } else if fip_ab && fip_cd { + // Case 2b. + if pfip_ab { + // TODO: test for mitre limit + raw_offset_ls.push(intersection); + } else { + raw_offset_ls.push(*b); + raw_offset_ls.push(*c); + } + } else { + // Case 2c. + // TODO: this is a repetition of the branch above. + // we can probably tidy up the branches to avoid repeated code + raw_offset_ls.push(*b); + raw_offset_ls.push(*c); + } + raw_offset_ls.push(*d); } - } else { - // Case 2c. (either ab or cd - raw_offset_ls.push(b); - raw_offset_ls.push(c); - } - raw_offset_ls.push(d) - } - todo!("Not done yet.") + raw_offset_ls + }) + ).collect() } } @@ -352,50 +351,34 @@ mod test { use crate::{line_string, Coord, Line}; // private imports - use super::{ - cross_product, - OffsetSignedCartesian - }; + use super::{cross_product, OffsetSignedCartesian}; #[test] - fn test_cross_product(){ - let a = Coord{x:0f64, y:0f64}; - let b = Coord{x:0f64, y:1f64}; - let c = Coord{x:1f64, y:0f64}; + fn test_cross_product() { + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + let c = Coord { x: 1f64, y: 0f64 }; let ab = b - a; let ac = c - a; - // expect the area of the parallelogram - assert_eq!( - cross_product(ac, ab), - 1f64 - ); + assert_eq!(cross_product(ac, ab), 1f64); // expect swapping will result in negative - assert_eq!( - cross_product(ab, ac), - -1f64 - ); + assert_eq!(cross_product(ab, ac), -1f64); // Add skew - let a = Coord{x:0f64, y:0f64}; - let b = Coord{x:0f64, y:1f64}; - let c = Coord{x:1f64, y:1f64}; + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + let c = Coord { x: 1f64, y: 1f64 }; let ab = b - a; let ac = c - a; // expect the area of the parallelogram - assert_eq!( - cross_product(ac, ab), - 1f64 - ); + assert_eq!(cross_product(ac, ab), 1f64); // expect swapping will result in negative - assert_eq!( - cross_product(ab, ac), - -1f64 - ); + assert_eq!(cross_product(ab, ac), -1f64); } #[test] From e6b4f6fddc00ea9d134ffadf02f6ce03dcfd4718 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:25 +0800 Subject: [PATCH 06/27] basic offset algorithm working --- geo/src/algorithm/mod.rs | 3 +- geo/src/algorithm/offset/cross_product.rs | 82 ++++ geo/src/algorithm/offset/line_intersection.rs | 150 +++++++ geo/src/algorithm/offset/mod.rs | 7 + geo/src/algorithm/offset/offset_trait.rs | 172 ++++++++ geo/src/algorithm/offset_signed_cartesian.rs | 402 ------------------ 6 files changed, 413 insertions(+), 403 deletions(-) create mode 100644 geo/src/algorithm/offset/cross_product.rs create mode 100644 geo/src/algorithm/offset/line_intersection.rs create mode 100644 geo/src/algorithm/offset/mod.rs create mode 100644 geo/src/algorithm/offset/offset_trait.rs delete mode 100644 geo/src/algorithm/offset_signed_cartesian.rs diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index 4632e28de..db20108d6 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -177,7 +177,8 @@ pub mod map_coords; pub use map_coords::{MapCoords, MapCoordsInPlace}; /// Apply a simple signed offset azzuming cartesian -pub mod offset_signed_cartesian; +pub mod offset; +pub use offset::Offset; /// Orient a `Polygon`'s exterior and interior rings. pub mod orient; diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs new file mode 100644 index 000000000..7c4547302 --- /dev/null +++ b/geo/src/algorithm/offset/cross_product.rs @@ -0,0 +1,82 @@ +use crate::{ + CoordFloat, +}; +use geo_types::Coord; + +/// 2D "Cross Product" +/// +/// > Note: `cross_prod` is already defined on Point... but that it seems to be +/// > some other operation on 3 points +/// +/// If we pretend the `z` ordinate is zero we can still use the 3D cross product +/// on 2D vectors and various useful properties still hold (e.g. it is still the +/// area of the parallelogram formed by the two input vectors) +/// +/// From basis vectors `i`,`j`,`k` and the axioms on wikipedia +/// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); +/// +/// ```text +/// i×j = k +/// j×k = i +/// k×i = j +/// +/// j×i = -k +/// k×j = -i +/// i×k = -j +/// +/// i×i = j×j = k×k = 0 +/// ``` +/// +/// We can define the 2D cross product as the magnitude of the 3D cross product +/// as follows +/// +/// ```text +/// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| +/// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| +/// = |a_x·b_x·( 0 ) + a_x·b_y·( k ) + a_y·b_x·(-k ) + a_y·b_y·( 0 )| +/// = | (a_x·b_y - a_y·b_x)·k | +/// = a_x·b_y - a_y·b_x +/// ``` +pub(super) fn cross_product(left: Coord, right: Coord) -> T +where + T: CoordFloat, +{ + left.x * right.y - left.y * right.x +} + +#[cfg(test)] +mod test { + // crate dependencies + use crate::{Coord}; + + // private imports + use super::{cross_product}; + + #[test] + fn test_cross_product() { + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + let c = Coord { x: 1f64, y: 0f64 }; + + let ab = b - a; + let ac = c - a; + + // expect the area of the parallelogram + assert_eq!(cross_product(ac, ab), 1f64); + // expect swapping will result in negative + assert_eq!(cross_product(ab, ac), -1f64); + + // Add skew + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + let c = Coord { x: 1f64, y: 1f64 }; + + let ab = b - a; + let ac = c - a; + + // expect the area of the parallelogram + assert_eq!(cross_product(ac, ab), 1f64); + // expect swapping will result in negative + assert_eq!(cross_product(ab, ac), -1f64); + } +} \ No newline at end of file diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs new file mode 100644 index 000000000..d8a7eacea --- /dev/null +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -0,0 +1,150 @@ +use crate::{ + CoordFloat, + CoordNum, +}; +use super::cross_product; +use geo_types::Coord; + +/// Struct to contain the result for [line_intersection_with_parameter] +pub (super) struct LineIntersectionWithParameterResult +where + T: CoordNum, +{ + pub t_ab: T, + pub t_cd: T, + pub intersection: Coord, +} + +/// Computes the intersection between two line segments; +/// a to b (`ab`), and c to d (`cd`) +/// +/// We already have LineIntersection trait BUT we need a function that also +/// returns the parameters for both lines described below. The LineIntersection +/// trait uses some fancy unrolled code it seems unlikely it could be adapted +/// for this purpose. +/// +/// Returns the intersection point **and** parameters `t_ab` and `t_cd` +/// described below +/// +/// The intersection of segments can be expressed as a parametric equation +/// where `t_ab` and `t_cd` are unknown scalars : +/// +/// ```text +/// a + ab · t_ab = c + cd · t_cd +/// ``` +/// +/// > note: a real intersection can only happen when `0 <= t_ab <= 1` and +/// > `0 <= t_cd <= 1` but this function will find intersections anyway +/// > which may lay outside of the line segments +/// +/// This can be rearranged as follows: +/// +/// ```text +/// ab · t_ab - cd · t_cd = c - a +/// ``` +/// +/// Collecting the scalars `t_ab` and `-t_cd` into the column vector `T`, +/// and by collecting the vectors `ab` and `cd` into matrix `M`: +/// we get the matrix form: +/// +/// ```text +/// [ab_x cd_x][ t_ab] = [ac_x] +/// [ab_y cd_y][-t_cd] [ac_y] +/// ``` +/// +/// or +/// +/// ```text +/// M·T=ac +/// ``` +/// +/// The determinant of the matrix `M` is the reciprocal of the cross product +/// of `ab` and `cd`. +/// +/// ```text +/// 1/(ab×cd) +/// ``` +/// +/// Therefore if `ab×cd = 0` the determinant is undefined and the matrix cannot +/// be inverted this means the lines are either +/// a) parallel or +/// b) collinear +/// +/// Pre-multiplying both sides by the inverted 2x2 matrix we get: +/// +/// ```text +/// [ t_ab] = 1/(ab×cd) · [ cd_y -cd_x][ac_x] +/// [-t_cd] [-ab_y ab_x][ac_y] +/// ``` +/// +/// or +/// +/// ```text +/// T = M⁻¹·ac +/// ``` +/// +/// Expands to: +/// +/// ```text +/// [ t_ab] = 1/(ab_x·cd_y - ab_y·cd_x)·[ cd_y·ac_x - cd_x·ac_y] +/// [-t_cd] [-ab_y·ac_x + ab_x·ac_y] +/// ``` +/// +/// Since it is tidier to write cross products, observe that the above is +/// equivalent to: +/// +/// ```text +/// [t_ab] = [ ac×cd / ab×cd ] +/// [t_cd] = [ - ab×ac / ab×cd ] +/// ``` + +pub (super) fn line_intersection_with_parameter( + a: &Coord, + b: &Coord, + c: &Coord, + d: &Coord, +) -> LineIntersectionWithParameterResult +where + T: CoordFloat, +{ + let ab = *b - *a; + let cd = *d - *c; + let ac = *c - *a; + + let ab_cross_cd = cross_product(ab, cd); + + if ab_cross_cd == num_traits::zero() { + // TODO: We can't tolerate this situation as it will cause a divide by + // zero in the next step. Even values close to zero are a problem, + // but I don't know how to deal with that problem jut yet + + // TODO: this is prevented anyway by the only use of this function. + todo!("") + } + + let t_ab = cross_product(ac, cd) / ab_cross_cd; + let t_cd = - cross_product(ab, ac) / ab_cross_cd; + let intersection = *a + ab * t_ab; + LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + } +} + +#[cfg(test)] +mod test { + use crate::{Coord}; + use super::*; + #[test] + fn test_intersection(){ + let a = Coord{x:0f64, y:0f64}; + let b = Coord{x:2f64, y:2f64}; + let c = Coord{x:0f64, y:1f64}; + let d = Coord{x:1f64, y:0f64}; + let LineIntersectionWithParameterResult{t_ab, t_cd, intersection} = line_intersection_with_parameter(&a, &b, &c, &d); + assert_eq!(t_ab, 0.25f64); + assert_eq!(t_cd, 0.5f64); + assert_eq!(intersection, Coord{x:0.5f64, y:0.5f64}); + } +} \ No newline at end of file diff --git a/geo/src/algorithm/offset/mod.rs b/geo/src/algorithm/offset/mod.rs new file mode 100644 index 000000000..c59a91e11 --- /dev/null +++ b/geo/src/algorithm/offset/mod.rs @@ -0,0 +1,7 @@ +mod cross_product; +mod line_intersection; +mod offset_trait; + +use cross_product::cross_product; +use line_intersection::{line_intersection_with_parameter, LineIntersectionWithParameterResult}; +pub use offset_trait::Offset; \ No newline at end of file diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs new file mode 100644 index 000000000..d6caaf30a --- /dev/null +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -0,0 +1,172 @@ + +use crate::{ + //kernels::RobustKernel, + //Orientation + CoordFloat, + CoordNum, + Line, + LineString, +}; +use geo_types::Coord; +use super::{cross_product,line_intersection_with_parameter,LineIntersectionWithParameterResult}; + +/// # Offset Trait +/// +/// Signed offset of Geometry assuming cartesian coordinate system. +/// +/// This is a cheap offset algorithm that is suitable for flat coordinate systems +/// (or if your lat/lon data is near the equator) +/// +/// My Priority for implementing the trait is as follows: +/// - Line +/// - LineString +/// - MultiLineString +/// - ... maybe some closed shapes like triangle, polygon? +/// +/// The following are a list of known limitations, +/// some may be removed during development, +/// others are very hard to fix. +/// +/// - No checking for zero length input. +/// Invalid results may be caused by division by zero. +/// - No check is implemented to prevent execution if the specified offset +/// distance is zero. +/// - Only local cropping where the output is self-intersecting. +/// Non-adjacent line segments in the output may be self-intersecting. +/// - There is no mitre-limit; A LineString which +/// doubles back on itself will produce an elbow at infinity +pub trait Offset +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self; +} + +impl Offset for Line +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self { + let delta = self.delta(); + let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); + let delta = Coord { + x: delta.y / len, + y: -delta.x / len, + }; + Line::new(self.start + delta * distance, self.end + delta * distance) + } +} + +fn pairwise(iterable: &[T]) -> std::iter::Zip, std::slice::Iter> { + iterable.iter().zip(iterable[1..].iter()) +} + +impl Offset for LineString +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self { + if self.0.len() < 2 { + // TODO: Fail on invalid input + return self.clone(); + } + + let offset_segments: Vec> = + self.lines().map(|item| item.offset(distance)).collect(); + + if offset_segments.len() == 1 { + return offset_segments[0].into(); + } + let first_point = offset_segments.first().unwrap().start; + let last_point = offset_segments.last().unwrap().end; + + let mut result = Vec::with_capacity(self.0.len()); + result.push(first_point); + result.extend(pairwise(&offset_segments[..]).flat_map( + |(Line { start: a, end: b }, Line { start: c, end: d })| { + let ab = *b - *a; + let cd = *d - *c; + let ab_cross_cd = cross_product(ab, cd); + // check for colinear case; this is a flakey check with a + // possible panic type cast :/ + // TODO: Could use RobustKernel for this? The simple kernel impl seems to be blank? + // I don't need the accuracy, need speed :) + // if RobustKernel::orient2d(a, b, c) == Orientation::Collinear { + if ::from(ab_cross_cd).unwrap().abs() < num_traits::cast(0.0000001f64).unwrap() { + vec![*b] + } else { + // TODO: if we can inline this function we only need to calculate ab_cross_cd once + let LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + } = line_intersection_with_parameter(a, b, c, d); + + let zero = num_traits::zero::(); + let one = num_traits::one::(); + + let tip_ab = zero <= t_ab && t_ab <= one; + let fip_ab = !tip_ab; + let pfip_ab = fip_ab && t_ab > zero; + + let tip_cd = zero <= t_cd && t_cd <= one; + let fip_cd = !tip_cd; + + if tip_ab && tip_cd { + // TODO: test for mitre limit + vec![intersection] + } else if fip_ab && fip_cd && pfip_ab { + // TODO: test for mitre limit + vec![intersection] + } else { + vec![*b, *c] + } + } + }, + )); + result.push(last_point); + result.into() + } +} + +#[cfg(test)] +mod test { + // crate dependencies + use crate::{line_string, Coord, Line}; + + // private imports + use super::{Offset}; + + #[test] + fn offset_line_test() { + let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); + let actual_result = input.offset(1.0); + assert_eq!( + actual_result, + Line::new(Coord { x: 2f64, y: 1f64 }, Coord { x: 2f64, y: 2f64 },) + ); + } + #[test] + fn offset_line_test_negative() { + let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); + let output_actual = input.offset(-1.0); + let output_expected = Line::new(Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 }); + assert_eq!(output_actual, output_expected); + } + + #[test] + fn offset_linestring_basic() { + let input = line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: 2f64 }, + Coord { x: 2f64, y: 2f64 }, + ]; + let output_expected = line_string![ + Coord { x: 1f64, y: 0f64 }, + Coord { x: 1f64, y: 1f64 }, + Coord { x: 2f64, y: 1f64 }, + ]; + let output_actual = input.offset(1f64); + assert_eq!(output_actual, output_expected); + } +} diff --git a/geo/src/algorithm/offset_signed_cartesian.rs b/geo/src/algorithm/offset_signed_cartesian.rs deleted file mode 100644 index 00a76657c..000000000 --- a/geo/src/algorithm/offset_signed_cartesian.rs +++ /dev/null @@ -1,402 +0,0 @@ -use crate::{kernels::RobustKernel, CoordFloat, CoordNum, Kernel, Line, LineString, Orientation}; -/// # Offset - Signed Cartesian -/// -/// ## Utility Functions -/// -/// This module starts by defining a heap of private utility functions -/// [dot_product], [cross_product], [magnitude], [normalize], [rescale] -/// -/// It looks like some of these are already implemented on stuff implemented -/// on the Point struct; but that feels misplaced to me? -/// -/// Looks like they might eventually belong in the Kernel trait?? -/// -/// For my first pull request I'll just implement them functional style and keep -/// the damage to one module ;) -use geo_types::Coord; - -/// 2D Dot Product -fn dot_product(left: Coord, right: Coord) -> T -where - T: CoordNum, -{ - left.x * right.x + left.y * right.y -} - -/// 2D "Cross Product" -/// -/// If we pretend the `z` ordinate is zero we can still use the 3D cross product -/// on 2D vectors and various useful properties still hold (e.g. it is still the -/// area of the parallelogram formed by the two input vectors) -/// -/// From basis vectors `i`,`j`,`k` and the axioms on wikipedia -/// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); -/// -/// ```text -/// i×j = k -/// j×k = i -/// k×i = j -/// -/// j×i = -k -/// k×j = -i -/// i×k = -j -/// -/// i×i = j×j = k×k = 0 -/// ``` -/// -/// We can define the 2D cross product as the magnitude of the 3D cross product -/// as follows -/// -/// ```text -/// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| -/// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| -/// = |a_x·b_x·( 0 ) + a_x·b_y·( k ) + a_y·b_x·(-k ) + a_y·b_y·( 0 )| -/// = | (a_x·b_y - a_y·b_x)·k | -/// = a_x·b_y - a_y·b_x -/// ``` -/// -/// Note: `cross_prod` is already defined on Point... but that it seems to be -/// some other operation on 3 points -fn cross_product(left: Coord, right: Coord) -> T -where - T: CoordNum, -{ - left.x * right.y - left.y * right.x -} - -/// Compute the magnitude of a Coord -fn magnitude(a: Coord) -> T -where - T: CoordFloat, -{ - (a.x * a.x + a.y * a.y).sqrt() -} - -/// Return a new coord in the same direction as the old coord but -/// with a magnitude of 1 unit -/// no protection from divide by zero -fn normalize(a: Coord) -> Coord -where - T: CoordFloat, -{ - a.clone() / magnitude(a) -} - -/// Return a new coord in the same direction as the old coord but with the -/// specified magnitude -/// No protection from divide by zero -fn rescale(a: Coord, new_magnitude: T) -> Coord -where - T: CoordFloat, -{ - normalize(a) * new_magnitude -} - -/// Struct to contain the result for [line_intersection_with_parameter] -struct LineIntersectionWithParameterResult -where - T: CoordNum, -{ - t_ab: T, - t_cd: T, - intersection: Coord, -} - -/// Computes the intersection between two line segments; -/// a to b (`ab`), and c to d (`cd`) -/// -/// We already have LineIntersection trait BUT we need a function that also -/// returns the parameters for both lines described below. The LineIntersection -/// trait uses some fancy unrolled code it seems unlikely it could be adapted -/// for this purpose. -/// -/// Returns the intersection point **and** parameters `t_ab` and `t_cd` -/// described below -/// -/// The intersection of segments can be expressed as a parametric equation -/// where `t_ab` and `t_cd` are unknown scalars : -/// -/// ```text -/// a + ab · t_ab = c + cd · t_cd -/// ``` -/// -/// > note: a real intersection can only happen when `0 <= t_ab <= 1` and -/// > `0 <= t_cd <= 1` but this function will find intersections anyway -/// > which may lay outside of the line segments -/// -/// This can be rearranged as follows: -/// -/// ```text -/// ab · t_ab - cd · t_cd = c - a -/// ``` -/// -/// Collecting the scalars `t_ab` and `-t_cd` into the column vector `T`, -/// and by collecting the vectors `ab` and `cd` into matrix `M`: -/// we get the matrix form: -/// -/// ```text -/// [ab_x cd_x][ t_ab] = [ac_x] -/// [ab_y cd_y][-t_cd] [ac_y] -/// ``` -/// -/// or -/// -/// ```text -/// M·T=ac -/// ``` -/// -/// The determinant of the matrix `M` is the reciprocal of the cross product -/// of `ab` and `cd`. -/// -/// ```text -/// 1/(ab×cd) -/// ``` -/// -/// Therefore if `ab×cd = 0` the determinant is undefined and the matrix cannot -/// be inverted this means the lines are either -/// a) parallel or -/// b) collinear -/// -/// Pre-multiplying both sides by the inverted 2x2 matrix we get: -/// -/// ```text -/// [ t_ab] = 1/(ab×cd) · [ cd_y -cd_x][ac_x] -/// [-t_cd] [-ab_y ab_x][ac_y] -/// ``` -/// -/// or -/// -/// ```text -/// T = M⁻¹·ac -/// ``` -/// -/// Expands to: -/// -/// ```text -/// [ t_ab] = 1/(ab_x·cd_y - ab_y·cd_x)·[ cd_y·ac_x - cd_x·ac_y] -/// [-t_cd] [-ab_y·ac_x + ab_x·ac_y] -/// ``` -/// -/// Since it is tidier to write cross products, observe that the above is -/// equivalent to: -/// -/// ```text -/// [ t_ab] = [ ac×cd / ab×cd ] -/// [-t_cd] = [ ab×ac / ab×cd ] -/// ``` - -fn line_intersection_with_parameter( - a: &Coord, - b: &Coord, - c: &Coord, - d: &Coord, -) -> LineIntersectionWithParameterResult -where - T: CoordFloat, -{ - let ab = *b - *a; - let cd = *d - *c; - let ac = *c - *a; - - let ab_cross_cd = cross_product(ab, cd); - - if ab_cross_cd == num_traits::zero() { - // TODO: We can't tolerate this situation as it will cause a divide by - // zero in the next step. Even values close to zero are a problem, - // but I don't know how to deal with that problem jut yet - todo!("") - } - - let t_ab = cross_product(ac, cd) / ab_cross_cd; - let t_cd = cross_product(ac, cd) / ab_cross_cd; - let intersection = *a + rescale(ab, t_ab); - LineIntersectionWithParameterResult { - t_ab, - t_cd, - intersection, - } -} - -/// Signed offset of Geometry assuming cartesian coordinate system. -/// -/// This is a cheap offset algorithm that is suitable for flat coordinate systems -/// (or if your lat/lon data is near the equator) -/// -/// My Priority for implementing the trait is as follows: -/// - Line -/// - LineString -/// - MultiLineString -/// - ... maybe some closed shapes like triangle, polygon? -/// -/// The following are a list of known limitations, -/// some may be removed during development, -/// others are very hard to fix. -/// -/// - No checking for zero length input. -/// Invalid results may be caused by division by zero. -/// - No check is implemented to prevent execution if the specified offset -/// distance is zero. -/// - Only local cropping where the output is self-intersecting. -/// Non-adjacent line segments in the output may be self-intersecting. -/// - There is no mitre-limit; A LineString which -/// doubles back on itself will produce an elbow at infinity -pub trait OffsetSignedCartesian -where - T: CoordNum, -{ - fn offset(&self, distance: T) -> Self; -} - -impl OffsetSignedCartesian for Line -where - T: CoordFloat, -{ - fn offset(&self, distance: T) -> Self { - let delta = self.delta(); - let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); - let delta = Coord { - x: delta.y / len, - y: -delta.x / len, - }; - Line::new(self.start + delta * distance, self.end + delta * distance) - } -} - -fn pairwise(iterable: &[T]) -> std::iter::Zip, std::slice::Iter> { - iterable.iter().zip(iterable[1..].iter()) -} - -impl OffsetSignedCartesian for LineString -where - T: CoordFloat, -{ - fn offset(&self, distance: T) -> Self { - if self.0.len() < 2 { - // TODO: Fail on invalid input - return self.clone(); - } - let offset_segments: Vec> = - self.lines().map(|item| item.offset(distance)).collect(); - if offset_segments.len() == 1 { - return offset_segments[0].into(); - } - let x = offset_segments[0]; - - - std::iter::once(offset_segments[0].start) - .chain( - pairwise(&offset_segments[..]) - .flat_map(|(Line { start:a, end:b }, Line{start:c, end:d})| { - - let ab = *b - *a; - let cd = *d - *c; - - let mut raw_offset_ls:Vec> = Vec::new(); - // check for colinear case; this is a flakey check with a - // possible panic type cast :/ - // TODO: Could use RobustKernel for this? The simple kernel impl seems to be blank? - // I don't need the accuracy, need speed :) - // Alternative implementation: - // if RobustKernel::orient2d(a, b, c) == Orientation::Collinear { - // raw_offset_ls.push(b); - // }else {... - if ::from(cross_product(ab, cd)).unwrap() < 0.0000001f64 { - raw_offset_ls.push(*b); - } else { - - let LineIntersectionWithParameterResult { - t_ab, - t_cd, - intersection, - } = line_intersection_with_parameter(a, b, c, d); - - let tip_ab = num_traits::zero::() <= t_ab && t_ab <= num_traits::one(); - let fip_ab = !tip_ab; - let pfip_ab = fip_ab && t_ab > num_traits::zero(); - - let tip_cd = num_traits::zero::() <= t_cd && t_cd <= num_traits::one(); - let fip_cd = !tip_cd; - - if tip_ab && tip_cd { - // Case 2a - // TODO: test for mitre limit - raw_offset_ls.push(intersection); - } else if fip_ab && fip_cd { - // Case 2b. - if pfip_ab { - // TODO: test for mitre limit - raw_offset_ls.push(intersection); - } else { - raw_offset_ls.push(*b); - raw_offset_ls.push(*c); - } - } else { - // Case 2c. - // TODO: this is a repetition of the branch above. - // we can probably tidy up the branches to avoid repeated code - raw_offset_ls.push(*b); - raw_offset_ls.push(*c); - } - raw_offset_ls.push(*d); - } - raw_offset_ls - }) - ).collect() - } -} - -#[cfg(test)] -mod test { - // crate dependencies - use crate::{line_string, Coord, Line}; - - // private imports - use super::{cross_product, OffsetSignedCartesian}; - - #[test] - fn test_cross_product() { - let a = Coord { x: 0f64, y: 0f64 }; - let b = Coord { x: 0f64, y: 1f64 }; - let c = Coord { x: 1f64, y: 0f64 }; - - let ab = b - a; - let ac = c - a; - - // expect the area of the parallelogram - assert_eq!(cross_product(ac, ab), 1f64); - // expect swapping will result in negative - assert_eq!(cross_product(ab, ac), -1f64); - - // Add skew - let a = Coord { x: 0f64, y: 0f64 }; - let b = Coord { x: 0f64, y: 1f64 }; - let c = Coord { x: 1f64, y: 1f64 }; - - let ab = b - a; - let ac = c - a; - - // expect the area of the parallelogram - assert_eq!(cross_product(ac, ab), 1f64); - // expect swapping will result in negative - assert_eq!(cross_product(ab, ac), -1f64); - } - - #[test] - fn offset_line_test() { - let line = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let actual_result = line.offset(1.0); - assert_eq!( - actual_result, - Line::new(Coord { x: 2f64, y: 1f64 }, Coord { x: 2f64, y: 2f64 },) - ); - } - #[test] - fn offset_line_test_negative() { - let line = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let actual_result = line.offset(-1.0); - assert_eq!( - actual_result, - Line::new(Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 },) - ); - } -} From 076caf183d6754aa9f852794109503e9069eccbf Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:43:25 +0800 Subject: [PATCH 07/27] improve offset RFC --- rfcs/2022-11-11-offset.md | 55 ++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md index f54e56d4c..31f0e809c 100644 --- a/rfcs/2022-11-11-offset.md +++ b/rfcs/2022-11-11-offset.md @@ -1,46 +1,51 @@ +# Offset + - Feature Name: `offset` - Start Date: 2022-11-11 - [Feature PR] +Proposal to add a cheap and simple offset algorithm that is suitable for flat +coordinate systems. + +## Offset Trait -This is a cheap offset algorithim that is suitable for flat coordinate systems -(or if your lat/lon data is near the equator and there is no need for -correctness) +Create a Trait called `Offset` in the `algorithms` module. ## Trait Implementations -My Priority for implementing the trait is as follows: +Priority for implementing the trait is as follows: -- Line -- LineString -- MultiLineString -- ... maybe some closed shapes like triangle, rect, and the most diffucult is - polygon and multi polygon. Possibly this algorithim is not suitable for - polygon, and other operations like th the minkowski sum is more appropriate in - those cases? +- [X] `Line` +- [X] `LineString` +- [ ] `MultiLineString` +- [ ] `Triangle` +- [ ] `Rectangle` +- [ ] And if some of the limitations below are addressed + `Polygon`, `MultiPolygon` ## Limitations -Some may be removed during development, others are very hard to fix. +Some may be removed during development, others are very hard to fix, +and potentially a better algorithm is needed: -- [ ] No checking for zero length input. Invalid results may be caused by - division by zero. +- [ ] Currently does not make proper use of SimpleKernel / RobustKernel - [ ] No check is implemented to prevent execution if the specified offset distance is zero - [ ] No Mitre-limit is implemented; A LineString which doubles back on itself will produce an elbow at infinity - [ ] Only local cropping where the output is self-intersecting. Non-adjacent line segments in the output may be self-intersecting. +- [ ] Does not handle closed shapes + -## References +## Algorithm + +### References Loosely follows the algorithim described by [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005](https://hal.inria.fr/inria-00518005/document) This was the first google result for 'line offset algorithm' - -## Algorithim - ### Definitions (For the psudocode in this readme only) Type definitions @@ -69,12 +74,8 @@ project = (tool: Vector2, target: LineString) -> (nearest_point_on_target_to interpolate = (distance_along_target: Parameter, target: LineString) -> (point_on_target: Vector2) ``` - - - ### Algorithm 1 - Pre-Treatment 1. Pretreatment steps from the paper are not implemented... these mostly deal with arcs and malformed input geometry -1. No check is performed to prevent execution when `d==0` ### Algorithm 0.1 - Segment Offset @@ -110,7 +111,7 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o 1. Otherwise, append `b` then `c` to `raw_offset_ls` 1. Remove zero length segments in `raw_offset_ls` -### Algorithm 4.1 - Dual Clipping: +### Algorithm 4.1 - Dual Clipping - **(TODO: not yet implemented)** 8. Find `raw_offset_ls_twin` by repeating Algorithms 0.1 and 1 but offset the `input_linestring` in the opposite direction (`-d`) 1. Find `intersection_points` between 1. `raw_offset_ls` and `raw_offset_ls` @@ -123,13 +124,13 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o 1. If we find such an intersection point that *is* on the first or last `LineSegment` of `input_linestring`
then add the intersection point to a list called `cut_targets` -### Algorithm 4.1.2 - Cookie Cutter: +### Algorithm 4.1.2 - Cookie Cutter - **(TODO: not yet implemented)** 13. For each point `p` in `cut_targets` 1. construct a circle of diameter `d` with its center at `p` 1. delete all parts of any linestring in `split_offset_mls` which falls within this circle 1. Empty the `cut_targets` list -### Algorithm 4.1.3 - Proximity Clipping +### Algorithm 4.1.3 - Proximity Clipping **(TODO: not yet implemented)** 17. For each linestring `item_ls` in `split_offset_mls` 1. For each segment `(a,b)` in `item_ls` 1. For each segment `(u,v)` of `input_linestring` @@ -141,5 +142,5 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o - if `a_dist > b_dist`add `a_proj` to `cut_targets` - Otherwise, add `b_proj` to `cut_targets` 1. Repeat Algorithm 4.1.2 -1. Remove zero length segments and empty linestrings etc **(TODO: not yet implemented)** -1. Join remaining linestrings that are touching to form new linestring(s) **(TODO: not yet implemented)** \ No newline at end of file +1. Remove zero length segments and empty linestrings etc +1. Join remaining linestrings that are touching to form new linestring(s) \ No newline at end of file From 9a1c701659a9332a683ad3055f6b62a44e66af34 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 08/27] fix formatting and impl offset for MultiLineString --- geo/src/algorithm/mod.rs | 2 +- geo/src/algorithm/offset/cross_product.rs | 21 +-- geo/src/algorithm/offset/line_intersection.rs | 46 +++--- geo/src/algorithm/offset/mod.rs | 2 +- geo/src/algorithm/offset/offset_trait.rs | 138 ++++++++++++++---- 5 files changed, 156 insertions(+), 53 deletions(-) diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index db20108d6..cd001566f 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -176,7 +176,7 @@ pub use lines_iter::LinesIter; pub mod map_coords; pub use map_coords::{MapCoords, MapCoordsInPlace}; -/// Apply a simple signed offset azzuming cartesian +/// Apply a simple signed offset pub mod offset; pub use offset::Offset; diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs index 7c4547302..6b9f2ef9b 100644 --- a/geo/src/algorithm/offset/cross_product.rs +++ b/geo/src/algorithm/offset/cross_product.rs @@ -1,16 +1,19 @@ -use crate::{ - CoordFloat, -}; +use crate::CoordFloat; use geo_types::Coord; /// 2D "Cross Product" -/// -/// > Note: `cross_prod` is already defined on Point... but that it seems to be +/// +/// > Note: `cross_prod` is already defined on `Point`... but that it seems to be /// > some other operation on 3 points /// +/// > Note: Elsewhere in this project the cross product seems to be done inline +/// > and is referred to as 'determinant' since it is the same as the +/// > determinant of a 2x2 matrix. +/// /// If we pretend the `z` ordinate is zero we can still use the 3D cross product /// on 2D vectors and various useful properties still hold (e.g. it is still the -/// area of the parallelogram formed by the two input vectors) +/// signed area of the parallelogram formed by the two input vectors, with the +/// sign being dependant on the order and properties of the inputs) /// /// From basis vectors `i`,`j`,`k` and the axioms on wikipedia /// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); @@ -47,10 +50,10 @@ where #[cfg(test)] mod test { // crate dependencies - use crate::{Coord}; + use crate::Coord; // private imports - use super::{cross_product}; + use super::cross_product; #[test] fn test_cross_product() { @@ -79,4 +82,4 @@ mod test { // expect swapping will result in negative assert_eq!(cross_product(ab, ac), -1f64); } -} \ No newline at end of file +} diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index d8a7eacea..0d2faf653 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -1,12 +1,9 @@ -use crate::{ - CoordFloat, - CoordNum, -}; use super::cross_product; +use crate::{CoordFloat, CoordNum}; use geo_types::Coord; /// Struct to contain the result for [line_intersection_with_parameter] -pub (super) struct LineIntersectionWithParameterResult +pub(super) struct LineIntersectionWithParameterResult where T: CoordNum, { @@ -18,6 +15,11 @@ where /// Computes the intersection between two line segments; /// a to b (`ab`), and c to d (`cd`) /// +/// > note: looks like there is already `cartesian_intersect` as a private +/// > method in simplifyvw.rs. It is nice because it uses the orient2d method +/// > of the Kernel, however it only gives a true/false answer and does not +/// > return the intersection point or parameters needed. +/// /// We already have LineIntersection trait BUT we need a function that also /// returns the parameters for both lines described below. The LineIntersection /// trait uses some fancy unrolled code it seems unlikely it could be adapted @@ -98,7 +100,7 @@ where /// [t_cd] = [ - ab×ac / ab×cd ] /// ``` -pub (super) fn line_intersection_with_parameter( +pub(super) fn line_intersection_with_parameter( a: &Coord, b: &Coord, c: &Coord, @@ -122,8 +124,8 @@ where todo!("") } - let t_ab = cross_product(ac, cd) / ab_cross_cd; - let t_cd = - cross_product(ab, ac) / ab_cross_cd; + let t_ab = cross_product(ac, cd) / ab_cross_cd; + let t_cd = -cross_product(ab, ac) / ab_cross_cd; let intersection = *a + ab * t_ab; LineIntersectionWithParameterResult { t_ab, @@ -134,17 +136,27 @@ where #[cfg(test)] mod test { - use crate::{Coord}; use super::*; + use crate::Coord; #[test] - fn test_intersection(){ - let a = Coord{x:0f64, y:0f64}; - let b = Coord{x:2f64, y:2f64}; - let c = Coord{x:0f64, y:1f64}; - let d = Coord{x:1f64, y:0f64}; - let LineIntersectionWithParameterResult{t_ab, t_cd, intersection} = line_intersection_with_parameter(&a, &b, &c, &d); + fn test_intersection() { + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 2f64, y: 2f64 }; + let c = Coord { x: 0f64, y: 1f64 }; + let d = Coord { x: 1f64, y: 0f64 }; + let LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + } = line_intersection_with_parameter(&a, &b, &c, &d); assert_eq!(t_ab, 0.25f64); assert_eq!(t_cd, 0.5f64); - assert_eq!(intersection, Coord{x:0.5f64, y:0.5f64}); + assert_eq!( + intersection, + Coord { + x: 0.5f64, + y: 0.5f64 + } + ); } -} \ No newline at end of file +} diff --git a/geo/src/algorithm/offset/mod.rs b/geo/src/algorithm/offset/mod.rs index c59a91e11..c9b7e5f26 100644 --- a/geo/src/algorithm/offset/mod.rs +++ b/geo/src/algorithm/offset/mod.rs @@ -4,4 +4,4 @@ mod offset_trait; use cross_product::cross_product; use line_intersection::{line_intersection_with_parameter, LineIntersectionWithParameterResult}; -pub use offset_trait::Offset; \ No newline at end of file +pub use offset_trait::Offset; diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index d6caaf30a..2af2c9faa 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,26 +1,25 @@ - +use super::{cross_product, line_intersection_with_parameter, LineIntersectionWithParameterResult}; use crate::{ - //kernels::RobustKernel, - //Orientation CoordFloat, - CoordNum, + // Kernel, + // Orientation, Line, LineString, + MultiLineString, }; use geo_types::Coord; -use super::{cross_product,line_intersection_with_parameter,LineIntersectionWithParameterResult}; /// # Offset Trait -/// +/// /// Signed offset of Geometry assuming cartesian coordinate system. /// /// This is a cheap offset algorithm that is suitable for flat coordinate systems /// (or if your lat/lon data is near the equator) /// /// My Priority for implementing the trait is as follows: -/// - Line -/// - LineString -/// - MultiLineString +/// - [X] Line +/// - [X] LineString +/// - [X] MultiLineString /// - ... maybe some closed shapes like triangle, polygon? /// /// The following are a list of known limitations, @@ -39,6 +38,27 @@ pub trait Offset where T: CoordFloat, { + /// Offset the edges of the geometry by `distance`, where `distance` may be + /// negative. + /// + /// Negative `distance` values will offset the edges of the geometry to the + /// left, when facing the direction of increasing coordinate index. + /// + /// ``` + /// #use crate::{line_string, Coord}; + /// let input = line_string![ + /// Coord { x: 0f64, y: 0f64 }, + /// Coord { x: 0f64, y: 2f64 }, + /// Coord { x: 2f64, y: 2f64 }, + /// ]; + /// let output_expected = line_string![ + /// Coord { x: 1f64, y: 0f64 }, + /// Coord { x: 1f64, y: 1f64 }, + /// Coord { x: 2f64, y: 1f64 }, + /// ]; + /// let output_actual = input.offset(1f64); + /// assert_eq!(output_actual, output_expected); + /// ``` fn offset(&self, distance: T) -> Self; } @@ -57,6 +77,14 @@ where } } +/// Iterate over a slice in overlapping pairs +/// +/// ```ignore +/// let items = vec![1, 2, 3, 4, 5]; +/// let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); +/// let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; +/// assert_eq!(actual_result, expected_result); +/// ``` fn pairwise(iterable: &[T]) -> std::iter::Zip, std::slice::Iter> { iterable.iter().zip(iterable[1..].iter()) } @@ -67,7 +95,7 @@ where { fn offset(&self, distance: T) -> Self { if self.0.len() < 2 { - // TODO: Fail on invalid input + // TODO: How should it fail on invalid input? return self.clone(); } @@ -87,15 +115,25 @@ where let ab = *b - *a; let cd = *d - *c; let ab_cross_cd = cross_product(ab, cd); - // check for colinear case; this is a flakey check with a - // possible panic type cast :/ - // TODO: Could use RobustKernel for this? The simple kernel impl seems to be blank? - // I don't need the accuracy, need speed :) - // if RobustKernel::orient2d(a, b, c) == Orientation::Collinear { - if ::from(ab_cross_cd).unwrap().abs() < num_traits::cast(0.0000001f64).unwrap() { + // TODO: I'm still confused about how to use Kernel / RobustKernel; + // the following did not work. I need to read more code + // from the rest of this repo to understand. + // if Kernel::orient2d(*a, *b, *d) == Orientation::Collinear { + // note that it is sufficient to check that only one of + // c or d are colinear with ab because of how they are + // related by the original line string. + // TODO: The following line + // - Does not use the Kernel + // - uses an arbitrary threshold value which needs more thought + if ::from(ab_cross_cd) + .unwrap() + .abs() + < num_traits::cast(0.0000001f64).unwrap() + { vec![*b] } else { - // TODO: if we can inline this function we only need to calculate ab_cross_cd once + // TODO: if we can inline this function we only need to + // calculate `ab_cross_cd` once let LineIntersectionWithParameterResult { t_ab, t_cd, @@ -103,10 +141,10 @@ where } = line_intersection_with_parameter(a, b, c, d); let zero = num_traits::zero::(); - let one = num_traits::one::(); + let one = num_traits::one::(); - let tip_ab = zero <= t_ab && t_ab <= one; - let fip_ab = !tip_ab; + let tip_ab = zero <= t_ab && t_ab <= one; + let fip_ab = !tip_ab; let pfip_ab = fip_ab && t_ab > zero; let tip_cd = zero <= t_cd && t_cd <= one; @@ -125,20 +163,40 @@ where }, )); result.push(last_point); + // TODO: there are more steps to this algorithm which are not yet + // implemented. See rfcs\2022-11-11-offset.md result.into() } } +impl Offset for MultiLineString +where + T: CoordFloat, +{ + fn offset(&self, distance: T) -> Self { + self.iter().map(|item| item.offset(distance)).collect() + } +} + #[cfg(test)] mod test { + // crate dependencies - use crate::{line_string, Coord, Line}; + use crate::{line_string, Coord, Line, MultiLineString, Offset}; // private imports - use super::{Offset}; + use super::pairwise; #[test] - fn offset_line_test() { + fn test_pairwise() { + let items = vec![1, 2, 3, 4, 5]; + let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); + let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; + assert_eq!(actual_result, expected_result); + } + + #[test] + fn test_offset_line() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); let actual_result = input.offset(1.0); assert_eq!( @@ -147,7 +205,7 @@ mod test { ); } #[test] - fn offset_line_test_negative() { + fn test_offset_line_negative() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); let output_actual = input.offset(-1.0); let output_expected = Line::new(Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 }); @@ -155,7 +213,7 @@ mod test { } #[test] - fn offset_linestring_basic() { + fn test_offset_line_string() { let input = line_string![ Coord { x: 0f64, y: 0f64 }, Coord { x: 0f64, y: 2f64 }, @@ -169,4 +227,34 @@ mod test { let output_actual = input.offset(1f64); assert_eq!(output_actual, output_expected); } + + #[test] + fn test_offset_multi_line_string() { + let input = MultiLineString::new(vec![ + line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: 2f64 }, + Coord { x: 2f64, y: 2f64 }, + ], + line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: -2f64 }, + Coord { x: -2f64, y: -2f64 }, + ], + ]); + let output_expected = MultiLineString::new(vec![ + line_string![ + Coord { x: 1f64, y: 0f64 }, + Coord { x: 1f64, y: 1f64 }, + Coord { x: 2f64, y: 1f64 }, + ], + line_string![ + Coord { x: -1f64, y: 0f64 }, + Coord { x: -1f64, y: -1f64 }, + Coord { x: -2f64, y: -1f64 }, + ], + ]); + let output_actual = input.offset(1f64); + assert_eq!(output_actual, output_expected); + } } From 37510a6bbd53092071bc05fbac0f95eb7cff63a6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 09/27] Refactor and prepare for closed shapes --- geo/src/algorithm/offset/cross_product.rs | 14 +-- geo/src/algorithm/offset/line_intersection.rs | 105 +++++++++++------ geo/src/algorithm/offset/mod.rs | 6 +- geo/src/algorithm/offset/offset_trait.rs | 108 +++++++----------- geo/src/algorithm/offset/slice_itertools.rs | 45 ++++++++ 5 files changed, 168 insertions(+), 110 deletions(-) create mode 100644 geo/src/algorithm/offset/slice_itertools.rs diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs index 6b9f2ef9b..36d3f2d6b 100644 --- a/geo/src/algorithm/offset/cross_product.rs +++ b/geo/src/algorithm/offset/cross_product.rs @@ -1,7 +1,7 @@ use crate::CoordFloat; use geo_types::Coord; -/// 2D "Cross Product" +/// The signed magnitude of the 3D "Cross Product" assuming z ordinates are zero /// /// > Note: `cross_prod` is already defined on `Point`... but that it seems to be /// > some other operation on 3 points @@ -40,7 +40,7 @@ use geo_types::Coord; /// = | (a_x·b_y - a_y·b_x)·k | /// = a_x·b_y - a_y·b_x /// ``` -pub(super) fn cross_product(left: Coord, right: Coord) -> T +pub(super) fn cross_product_2d(left: Coord, right: Coord) -> T where T: CoordFloat, { @@ -53,7 +53,7 @@ mod test { use crate::Coord; // private imports - use super::cross_product; + use super::cross_product_2d; #[test] fn test_cross_product() { @@ -65,9 +65,9 @@ mod test { let ac = c - a; // expect the area of the parallelogram - assert_eq!(cross_product(ac, ab), 1f64); + assert_eq!(cross_product_2d(ac, ab), 1f64); // expect swapping will result in negative - assert_eq!(cross_product(ab, ac), -1f64); + assert_eq!(cross_product_2d(ab, ac), -1f64); // Add skew let a = Coord { x: 0f64, y: 0f64 }; @@ -78,8 +78,8 @@ mod test { let ac = c - a; // expect the area of the parallelogram - assert_eq!(cross_product(ac, ab), 1f64); + assert_eq!(cross_product_2d(ac, ab), 1f64); // expect swapping will result in negative - assert_eq!(cross_product(ab, ac), -1f64); + assert_eq!(cross_product_2d(ab, ac), -1f64); } } diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index 0d2faf653..ead1ec0fa 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -1,4 +1,4 @@ -use super::cross_product; +use super::cross_product::cross_product_2d; use crate::{CoordFloat, CoordNum}; use geo_types::Coord; @@ -60,15 +60,15 @@ where /// M·T=ac /// ``` /// -/// The determinant of the matrix `M` is the reciprocal of the cross product -/// of `ab` and `cd`. +/// Inverting the matrix `M` involves taking the reciprocal of the determinant +/// (the determinant is same as the of the [cross_product()] of `ab` and `cd`) /// /// ```text /// 1/(ab×cd) /// ``` /// /// Therefore if `ab×cd = 0` the determinant is undefined and the matrix cannot -/// be inverted this means the lines are either +/// be inverted. The lines are either /// a) parallel or /// b) collinear /// @@ -105,7 +105,7 @@ pub(super) fn line_intersection_with_parameter( b: &Coord, c: &Coord, d: &Coord, -) -> LineIntersectionWithParameterResult +) -> Option> where T: CoordFloat, { @@ -113,24 +113,33 @@ where let cd = *d - *c; let ac = *c - *a; - let ab_cross_cd = cross_product(ab, cd); - - if ab_cross_cd == num_traits::zero() { - // TODO: We can't tolerate this situation as it will cause a divide by - // zero in the next step. Even values close to zero are a problem, - // but I don't know how to deal with that problem jut yet - - // TODO: this is prevented anyway by the only use of this function. - todo!("") - } - - let t_ab = cross_product(ac, cd) / ab_cross_cd; - let t_cd = -cross_product(ab, ac) / ab_cross_cd; - let intersection = *a + ab * t_ab; - LineIntersectionWithParameterResult { - t_ab, - t_cd, - intersection, + // TODO: I'm still confused about how to use Kernel / RobustKernel; + // the following did not work. I need to read more code + // from the rest of this repo to understand. + // if Kernel::orient2d(*a, *b, *d) == Orientation::Collinear { + // note that it is sufficient to check that only one of + // c or d are colinear with ab because of how they are + // related by the original line string. + // TODO: The following line + // - Does not use the Kernel + // - uses an arbitrary threshold value which needs more thought + let ab_cross_cd = cross_product_2d(ab, cd); + if ::from(ab_cross_cd) + .unwrap() + .abs() + < num_traits::cast(0.0000001f64).unwrap() + { + // Segments are parallel or colinear + None + } else { + let t_ab = cross_product_2d(ac, cd) / ab_cross_cd; + let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; + let intersection = *a + ab * t_ab; + Some(LineIntersectionWithParameterResult { + t_ab, + t_cd, + intersection, + }) } } @@ -144,19 +153,49 @@ mod test { let b = Coord { x: 2f64, y: 2f64 }; let c = Coord { x: 0f64, y: 1f64 }; let d = Coord { x: 1f64, y: 0f64 }; - let LineIntersectionWithParameterResult { + if let Some(LineIntersectionWithParameterResult { t_ab, t_cd, intersection, - } = line_intersection_with_parameter(&a, &b, &c, &d); - assert_eq!(t_ab, 0.25f64); - assert_eq!(t_cd, 0.5f64); - assert_eq!( + }) = line_intersection_with_parameter(&a, &b, &c, &d) + { + assert_eq!(t_ab, 0.25f64); + assert_eq!(t_cd, 0.5f64); + assert_eq!( + intersection, + Coord { + x: 0.5f64, + y: 0.5f64 + } + ); + } else { + assert!(false) + } + } + + #[test] + fn test_intersection_colinear() { + let a = Coord { x: 3f64, y: 4f64 }; + let b = Coord { x: 6f64, y: 8f64 }; + let c = Coord { x: 7f64, y: 7f64 }; + let d = Coord { x: 10f64, y: 9f64 }; + if let Some(LineIntersectionWithParameterResult { + t_ab, + t_cd, intersection, - Coord { - x: 0.5f64, - y: 0.5f64 - } - ); + }) = line_intersection_with_parameter(&a, &b, &c, &d) + { + assert_eq!(t_ab, 0.25f64); + assert_eq!(t_cd, 0.5f64); + assert_eq!( + intersection, + Coord { + x: 0.5f64, + y: 0.5f64 + } + ); + } else { + assert!(false) + } } } diff --git a/geo/src/algorithm/offset/mod.rs b/geo/src/algorithm/offset/mod.rs index c9b7e5f26..1dd3a44d1 100644 --- a/geo/src/algorithm/offset/mod.rs +++ b/geo/src/algorithm/offset/mod.rs @@ -1,7 +1,7 @@ + mod cross_product; +mod slice_itertools; mod line_intersection; -mod offset_trait; -use cross_product::cross_product; -use line_intersection::{line_intersection_with_parameter, LineIntersectionWithParameterResult}; +mod offset_trait; pub use offset_trait::Offset; diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index 2af2c9faa..1603596d7 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,4 +1,6 @@ -use super::{cross_product, line_intersection_with_parameter, LineIntersectionWithParameterResult}; +use super::line_intersection::{line_intersection_with_parameter, LineIntersectionWithParameterResult}; +use super::slice_itertools::pairwise; + use crate::{ CoordFloat, // Kernel, @@ -6,6 +8,7 @@ use crate::{ Line, LineString, MultiLineString, + Polygon }; use geo_types::Coord; @@ -77,17 +80,6 @@ where } } -/// Iterate over a slice in overlapping pairs -/// -/// ```ignore -/// let items = vec![1, 2, 3, 4, 5]; -/// let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); -/// let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; -/// assert_eq!(actual_result, expected_result); -/// ``` -fn pairwise(iterable: &[T]) -> std::iter::Zip, std::slice::Iter> { - iterable.iter().zip(iterable[1..].iter()) -} impl Offset for LineString where @@ -112,52 +104,32 @@ where result.push(first_point); result.extend(pairwise(&offset_segments[..]).flat_map( |(Line { start: a, end: b }, Line { start: c, end: d })| { - let ab = *b - *a; - let cd = *d - *c; - let ab_cross_cd = cross_product(ab, cd); - // TODO: I'm still confused about how to use Kernel / RobustKernel; - // the following did not work. I need to read more code - // from the rest of this repo to understand. - // if Kernel::orient2d(*a, *b, *d) == Orientation::Collinear { - // note that it is sufficient to check that only one of - // c or d are colinear with ab because of how they are - // related by the original line string. - // TODO: The following line - // - Does not use the Kernel - // - uses an arbitrary threshold value which needs more thought - if ::from(ab_cross_cd) - .unwrap() - .abs() - < num_traits::cast(0.0000001f64).unwrap() - { - vec![*b] - } else { - // TODO: if we can inline this function we only need to - // calculate `ab_cross_cd` once - let LineIntersectionWithParameterResult { + match line_intersection_with_parameter(a, b, c, d) { + None => vec![*b], // colinear + Some(LineIntersectionWithParameterResult { t_ab, t_cd, intersection, - } = line_intersection_with_parameter(a, b, c, d); - - let zero = num_traits::zero::(); - let one = num_traits::one::(); - - let tip_ab = zero <= t_ab && t_ab <= one; - let fip_ab = !tip_ab; - let pfip_ab = fip_ab && t_ab > zero; - - let tip_cd = zero <= t_cd && t_cd <= one; - let fip_cd = !tip_cd; - - if tip_ab && tip_cd { - // TODO: test for mitre limit - vec![intersection] - } else if fip_ab && fip_cd && pfip_ab { - // TODO: test for mitre limit - vec![intersection] - } else { - vec![*b, *c] + }) => { + let zero = num_traits::zero::(); + let one = num_traits::one::(); + + let tip_ab = zero <= t_ab && t_ab <= one; + let fip_ab = !tip_ab; + let pfip_ab = fip_ab && t_ab > zero; + + let tip_cd = zero <= t_cd && t_cd <= one; + let fip_cd = !tip_cd; + + if tip_ab && tip_cd { + // TODO: test for mitre limit + vec![intersection] + } else if fip_ab && fip_cd && pfip_ab { + // TODO: test for mitre limit + vec![intersection] + } else { + vec![*b, *c] + } } } }, @@ -178,23 +150,25 @@ where } } + +// impl Offset for Polygon +// where +// T: CoordFloat, +// { +// fn offset(&self, distance: T) -> Self { +// // TODO: not finished yet... need to do interiors +// // self.interiors() +// // TODO: is the winding order configurable? +// self.exterior(); +// todo!("Not finished") +// } +// } + #[cfg(test)] mod test { - // crate dependencies use crate::{line_string, Coord, Line, MultiLineString, Offset}; - // private imports - use super::pairwise; - - #[test] - fn test_pairwise() { - let items = vec![1, 2, 3, 4, 5]; - let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); - let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; - assert_eq!(actual_result, expected_result); - } - #[test] fn test_offset_line() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); diff --git a/geo/src/algorithm/offset/slice_itertools.rs b/geo/src/algorithm/offset/slice_itertools.rs new file mode 100644 index 000000000..79c77d8b8 --- /dev/null +++ b/geo/src/algorithm/offset/slice_itertools.rs @@ -0,0 +1,45 @@ +/// Iterate over a slice in overlapping pairs +/// +/// ```ignore +/// let items = vec![1, 2, 3, 4, 5]; +/// let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); +/// let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; +/// ``` +pub(super) fn pairwise(slice: &[T]) -> std::iter::Zip, std::slice::Iter> { + slice.iter().zip(slice[1..].iter()) +} + +/// Iterate over a slice and repeat the first item at the end +/// +/// ```ignore +/// let items = vec![1, 2, 3, 4, 5]; +/// let actual_result: Vec = wrap_one(&items[..]).cloned().collect(); +/// let expected_result = vec![1, 2, 3, 4, 5, 1]; +/// ``` +pub(super) fn wrap_one( + slice: &[T], +) -> std::iter::Chain, std::slice::Iter> { + slice.iter().chain(slice[..1].iter()) + //.chain::<&T>(std::iter::once(slice[0])) +} + +#[cfg(test)] +mod test { + use super::{pairwise, wrap_one}; + + #[test] + fn test_pairwise() { + let items = vec![1, 2, 3, 4, 5]; + let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); + let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; + assert_eq!(actual_result, expected_result); + } + + #[test] + fn test_wrap() { + let items = vec![1, 2, 3, 4, 5]; + let actual_result: Vec = wrap_one(&items[..]).cloned().collect(); + let expected_result = vec![1, 2, 3, 4, 5, 1]; + assert_eq!(actual_result, expected_result); + } +} From b77606c2e52d35cd37c4173e0c827b7eba383afd Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 10/27] LineSegmentIntersectionType as enum for clarity --- geo/src/algorithm/offset/line_intersection.rs | 75 +++++++++-- geo/src/algorithm/offset/offset_trait.rs | 127 ++++++++++++++---- rfcs/2022-11-11-offset.md | 10 +- 3 files changed, 168 insertions(+), 44 deletions(-) diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index ead1ec0fa..2489e30db 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -2,13 +2,46 @@ use super::cross_product::cross_product_2d; use crate::{CoordFloat, CoordNum}; use geo_types::Coord; +// No nested enums :( Goes into the enum below +#[derive(PartialEq, Eq, Debug)] +pub(super) enum FalseIntersectionPointType{ + /// The intersection point is 'false' or 'virtual': it lies on the infinite + /// ray defined by the line segment, but before the start of the line segment. + /// + /// Abbreviated to `NFIP` in original paper (Negative) + BeforeStart, + /// The intersection point is 'false' or 'virtual': it lies on the infinite + /// ray defined by the line segment, but after the end of the line segment. + /// + /// Abbreviated to `PFIP` in original paper (Positive) + AfterEnd +} + +/// Used to encode the relationship between a segment (e.g. between [Coord] `a` and `b`) +/// and an intersection point ([Coord] `p`) +#[derive(PartialEq, Eq, Debug)] +pub(super) enum LineSegmentIntersectionType { + /// The intersection point lies between the start and end of the line segment. + /// + /// Abbreviated to `TIP` in original paper + TrueIntersectionPoint, + /// The intersection point is 'false' or 'virtual': it lies on the infinite + /// ray defined by the line segment, but not between the start and end points + /// + /// Abbreviated to `FIP` in original paper + FalseIntersectionPoint(FalseIntersectionPointType) +} + +use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; +use FalseIntersectionPointType::{BeforeStart, AfterEnd}; + /// Struct to contain the result for [line_intersection_with_parameter] pub(super) struct LineIntersectionWithParameterResult where T: CoordNum, { - pub t_ab: T, - pub t_cd: T, + pub ab: LineSegmentIntersectionType, + pub cd: LineSegmentIntersectionType, pub intersection: Coord, } @@ -135,14 +168,32 @@ where let t_ab = cross_product_2d(ac, cd) / ab_cross_cd; let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; let intersection = *a + ab * t_ab; + + let zero = num_traits::zero::(); + let one = num_traits::one::(); + Some(LineIntersectionWithParameterResult { - t_ab, - t_cd, + ab: if zero <= t_ab && t_ab <= one { + TrueIntersectionPoint + }else if t_ab < zero { + FalseIntersectionPoint(BeforeStart) + }else{ + FalseIntersectionPoint(AfterEnd) + }, + cd: if zero <= t_cd && t_cd <= one { + TrueIntersectionPoint + }else if t_cd < zero { + FalseIntersectionPoint(BeforeStart) + }else{ + FalseIntersectionPoint(AfterEnd) + }, intersection, }) } } +// TODO: add more relationship tests; + #[cfg(test)] mod test { use super::*; @@ -154,13 +205,13 @@ mod test { let c = Coord { x: 0f64, y: 1f64 }; let d = Coord { x: 1f64, y: 0f64 }; if let Some(LineIntersectionWithParameterResult { - t_ab, - t_cd, + ab: t_ab, + cd: t_cd, intersection, }) = line_intersection_with_parameter(&a, &b, &c, &d) { - assert_eq!(t_ab, 0.25f64); - assert_eq!(t_cd, 0.5f64); + assert_eq!(t_ab, TrueIntersectionPoint); + assert_eq!(t_cd, TrueIntersectionPoint); assert_eq!( intersection, Coord { @@ -180,13 +231,13 @@ mod test { let c = Coord { x: 7f64, y: 7f64 }; let d = Coord { x: 10f64, y: 9f64 }; if let Some(LineIntersectionWithParameterResult { - t_ab, - t_cd, + ab: t_ab, + cd: t_cd, intersection, }) = line_intersection_with_parameter(&a, &b, &c, &d) { - assert_eq!(t_ab, 0.25f64); - assert_eq!(t_cd, 0.5f64); + assert_eq!(t_ab, TrueIntersectionPoint); + assert_eq!(t_cd, TrueIntersectionPoint); assert_eq!( intersection, Coord { diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index 1603596d7..4b9fcbf86 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,16 +1,22 @@ -use super::line_intersection::{line_intersection_with_parameter, LineIntersectionWithParameterResult}; +use super::line_intersection::FalseIntersectionPointType::AfterEnd; +use super::line_intersection::LineSegmentIntersectionType::{ + FalseIntersectionPoint, TrueIntersectionPoint, +}; +use super::line_intersection::{ + line_intersection_with_parameter, LineIntersectionWithParameterResult, +}; use super::slice_itertools::pairwise; use crate::{ + Coord, CoordFloat, // Kernel, // Orientation, Line, LineString, MultiLineString, - Polygon + // Polygon, }; -use geo_types::Coord; /// # Offset Trait /// @@ -80,7 +86,6 @@ where } } - impl Offset for LineString where T: CoordFloat, @@ -105,32 +110,25 @@ where result.extend(pairwise(&offset_segments[..]).flat_map( |(Line { start: a, end: b }, Line { start: c, end: d })| { match line_intersection_with_parameter(a, b, c, d) { - None => vec![*b], // colinear + None => { + // TODO: this is the colinear case; + // we are potentially creating a redundant point in the + // output here. Colinear segments should maybe get + // removed before or after this algorithm + vec![*b] + }, Some(LineIntersectionWithParameterResult { - t_ab, - t_cd, + ab, + cd, intersection, - }) => { - let zero = num_traits::zero::(); - let one = num_traits::one::(); - - let tip_ab = zero <= t_ab && t_ab <= one; - let fip_ab = !tip_ab; - let pfip_ab = fip_ab && t_ab > zero; - - let tip_cd = zero <= t_cd && t_cd <= one; - let fip_cd = !tip_cd; - - if tip_ab && tip_cd { - // TODO: test for mitre limit - vec![intersection] - } else if fip_ab && fip_cd && pfip_ab { - // TODO: test for mitre limit + }) => match (ab, cd) { + (TrueIntersectionPoint, TrueIntersectionPoint) => vec![intersection], + (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { + // TODO: Mitre limit logic goes here vec![intersection] - } else { - vec![*b, *c] } - } + _ => vec![*b, *c], + }, } }, )); @@ -150,7 +148,6 @@ where } } - // impl Offset for Polygon // where // T: CoordFloat, @@ -167,7 +164,9 @@ where #[cfg(test)] mod test { - use crate::{line_string, Coord, Line, MultiLineString, Offset}; + use crate::{line_string, Coord, Line, LineString, MultiLineString, Offset}; + + use super::super::slice_itertools::pairwise; #[test] fn test_offset_line() { @@ -231,4 +230,76 @@ mod test { let output_actual = input.offset(1f64); assert_eq!(output_actual, output_expected); } + + /// Function to draw test output to geogebra.org for inspection + /// + /// Paste the output into the javascript console on geogebra.org to + /// visualize the result + /// + /// The following snippet will extract existing (points and vectors) from geogebra: + /// + /// ```javascript + /// console.log([ + /// "line_string![", + /// ...ggbApplet.getAllObjectNames().filter(item=>item==item.toUpperCase()).map(name=>` Coord{x:${ggbApplet.getXcoord(name)}f64, y:${ggbApplet.getYcoord(name)}f64},`), + /// "]", + /// ].join("\n")) + /// ``` + /// + fn print_geogebra_draw_commands(input: &LineString, prefix: &str, r: u8, g: u8, b: u8) { + let prefix_upper = prefix.to_uppercase(); + let prefix_lower = prefix.to_lowercase(); + input + .coords() + .enumerate() + .for_each(|(index, Coord { x, y })| { + println!(r#"ggbApplet.evalCommand("{prefix_upper}_{{{index}}} = ({x:?},{y:?})")"#) + }); + let x: Vec<_> = input.coords().enumerate().collect(); + pairwise(&x[..]).for_each(|((a, _), (b, _))|{ + println!(r#"ggbApplet.evalCommand("{prefix_lower}_{{{a},{b}}} = Vector({prefix_upper}_{a},{prefix_upper}_{b})")"#); + () + }); + let (dim_r, dim_g, dim_b) = (r / 2, g / 2, b / 2); + println!( + r#"ggbApplet.getAllObjectNames().filter(item=>item.startsWith("{prefix_upper}_")).forEach(item=>ggbApplet.setColor(item,{r},{g},{b}))"# + ); + println!( + r#"ggbApplet.getAllObjectNames().filter(item=>item.startsWith("{prefix_lower}_")).forEach(item=>ggbApplet.setColor(item,{dim_r},{dim_g},{dim_b}))"# + ); + } + + #[test] + fn test_offset_line_string_all_branch() { + // attempts to hit all branches of the line extension / cropping test + let input = line_string![ + Coord { x: 3f64, y: 2f64 }, + Coord { + x: 2.740821628422733f64, + y: 2.2582363315313816f64 + }, + Coord { + x: 5.279039119779313f64, + y: 2.516847170273373f64 + }, + Coord { x: 5f64, y: 2f64 }, + Coord { + x: 3.2388869474813826f64, + y: 4.489952088082639f64 + }, + Coord { x: 3f64, y: 4f64 }, + Coord { x: 4f64, y: 4f64 }, + Coord { x: 5.5f64, y: 4f64 }, + Coord { + x: 5.240726402928647f64, + y: 4.250497607765981f64 + }, + ]; + print_geogebra_draw_commands(&input, "I", 90, 90, 90); + print_geogebra_draw_commands(&input.offset(-0.1f64), "L", 0, 200, 0); + print_geogebra_draw_commands(&input.offset(0.1f64), "R", 200, 0, 0); + + // TODO: test always fails + assert!(false); + } } diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md index 31f0e809c..4c302467f 100644 --- a/rfcs/2022-11-11-offset.md +++ b/rfcs/2022-11-11-offset.md @@ -17,11 +17,13 @@ Priority for implementing the trait is as follows: - [X] `Line` - [X] `LineString` -- [ ] `MultiLineString` +- [X] `MultiLineString` - [ ] `Triangle` - [ ] `Rectangle` -- [ ] And if some of the limitations below are addressed - `Polygon`, `MultiPolygon` +- [ ] If some of the limitations discussed below can be addressed + - [ ] `Polygon` + - [ ] `MultiPolygon` + - [ ] `Geometry` & `GeometryCollection` ## Limitations @@ -35,7 +37,7 @@ and potentially a better algorithm is needed: will produce an elbow at infinity - [ ] Only local cropping where the output is self-intersecting. Non-adjacent line segments in the output may be self-intersecting. -- [ ] Does not handle closed shapes +- [ ] Does not handle closed shapes yet ## Algorithm From 5a218e244b185d270ab4e6d0ec1db764ed4a1b8d Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 11/27] improved testing --- geo/src/algorithm/offset/line_intersection.rs | 172 +++++++++++++----- geo/src/algorithm/offset/offset_trait.rs | 4 +- 2 files changed, 128 insertions(+), 48 deletions(-) diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index 2489e30db..d48f9b31e 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -1,20 +1,19 @@ use super::cross_product::cross_product_2d; -use crate::{CoordFloat, CoordNum}; -use geo_types::Coord; +use crate::{Coord, CoordFloat, CoordNum}; // No nested enums :( Goes into the enum below -#[derive(PartialEq, Eq, Debug)] -pub(super) enum FalseIntersectionPointType{ +#[derive(PartialEq, Eq, Debug)] +pub(super) enum FalseIntersectionPointType { /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but before the start of the line segment. - /// + /// /// Abbreviated to `NFIP` in original paper (Negative) BeforeStart, /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but after the end of the line segment. - /// + /// /// Abbreviated to `PFIP` in original paper (Positive) - AfterEnd + AfterEnd, } /// Used to encode the relationship between a segment (e.g. between [Coord] `a` and `b`) @@ -22,18 +21,18 @@ pub(super) enum FalseIntersectionPointType{ #[derive(PartialEq, Eq, Debug)] pub(super) enum LineSegmentIntersectionType { /// The intersection point lies between the start and end of the line segment. - /// + /// /// Abbreviated to `TIP` in original paper TrueIntersectionPoint, /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but not between the start and end points - /// + /// /// Abbreviated to `FIP` in original paper - FalseIntersectionPoint(FalseIntersectionPointType) + FalseIntersectionPoint(FalseIntersectionPointType), } +use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; -use FalseIntersectionPointType::{BeforeStart, AfterEnd}; /// Struct to contain the result for [line_intersection_with_parameter] pub(super) struct LineIntersectionWithParameterResult @@ -133,12 +132,12 @@ where /// [t_cd] = [ - ab×ac / ab×cd ] /// ``` -pub(super) fn line_intersection_with_parameter( +fn line_segment_intersection_with_parameters( a: &Coord, b: &Coord, c: &Coord, d: &Coord, -) -> Option> +) -> Option<(T, T, Coord)> where T: CoordFloat, { @@ -169,49 +168,66 @@ where let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; let intersection = *a + ab * t_ab; + Some((t_ab, t_cd, intersection)) + } +} + +pub(super) fn line_segment_intersection_with_relationships( + a: &Coord, + b: &Coord, + c: &Coord, + d: &Coord, +) -> Option> +where + T: CoordFloat, +{ + line_segment_intersection_with_parameters(a, b, c, d).map(|(t_ab, t_cd, intersection)| { let zero = num_traits::zero::(); let one = num_traits::one::(); - - Some(LineIntersectionWithParameterResult { + LineIntersectionWithParameterResult { ab: if zero <= t_ab && t_ab <= one { TrueIntersectionPoint - }else if t_ab < zero { + } else if t_ab < zero { FalseIntersectionPoint(BeforeStart) - }else{ + } else { FalseIntersectionPoint(AfterEnd) }, cd: if zero <= t_cd && t_cd <= one { TrueIntersectionPoint - }else if t_cd < zero { + } else if t_cd < zero { FalseIntersectionPoint(BeforeStart) - }else{ + } else { FalseIntersectionPoint(AfterEnd) }, intersection, - }) - } + } + }) } // TODO: add more relationship tests; #[cfg(test)] mod test { - use super::*; + use super::{ + line_segment_intersection_with_parameters, line_segment_intersection_with_relationships, + FalseIntersectionPointType, LineIntersectionWithParameterResult, + LineSegmentIntersectionType, + }; use crate::Coord; + use FalseIntersectionPointType::{AfterEnd, BeforeStart}; + use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; + #[test] - fn test_intersection() { + fn test_line_segment_intersection_with_parameters() { let a = Coord { x: 0f64, y: 0f64 }; let b = Coord { x: 2f64, y: 2f64 }; let c = Coord { x: 0f64, y: 1f64 }; let d = Coord { x: 1f64, y: 0f64 }; - if let Some(LineIntersectionWithParameterResult { - ab: t_ab, - cd: t_cd, - intersection, - }) = line_intersection_with_parameter(&a, &b, &c, &d) + if let Some((t_ab, t_cd, intersection)) = + line_segment_intersection_with_parameters(&a, &b, &c, &d) { - assert_eq!(t_ab, TrueIntersectionPoint); - assert_eq!(t_cd, TrueIntersectionPoint); + assert_eq!(t_ab, 0.25f64); + assert_eq!(t_cd, 0.50f64); assert_eq!( intersection, Coord { @@ -225,28 +241,92 @@ mod test { } #[test] - fn test_intersection_colinear() { + fn test_line_segment_intersection_with_parameters_parallel() { let a = Coord { x: 3f64, y: 4f64 }; let b = Coord { x: 6f64, y: 8f64 }; - let c = Coord { x: 7f64, y: 7f64 }; - let d = Coord { x: 10f64, y: 9f64 }; + let c = Coord { x: 9f64, y: 9f64 }; + let d = Coord { x: 12f64, y: 13f64 }; + assert_eq!( + line_segment_intersection_with_parameters(&a, &b, &c, &d), + None + ) + } + #[test] + fn test_line_segment_intersection_with_parameters_colinear() { + let a = Coord { x: 1f64, y: 2f64 }; + let b = Coord { x: 2f64, y: 4f64 }; + let c = Coord { x: 3f64, y: 6f64 }; + let d = Coord { x: 5f64, y: 10f64 }; + assert_eq!( + line_segment_intersection_with_parameters(&a, &b, &c, &d), + None + ) + } + + #[test] + fn test_line_segment_intersection_with_relationships() { + let a = Coord { x: 1f64, y: 2f64 }; + let b = Coord { x: 2f64, y: 3f64 }; + let c = Coord { x: 0f64, y: 2f64 }; + let d = Coord { x: -2f64, y: 6f64 }; if let Some(LineIntersectionWithParameterResult { - ab: t_ab, - cd: t_cd, + ab, + cd, intersection, - }) = line_intersection_with_parameter(&a, &b, &c, &d) + }) = line_segment_intersection_with_relationships(&a, &b, &c, &d) { - assert_eq!(t_ab, TrueIntersectionPoint); - assert_eq!(t_cd, TrueIntersectionPoint); - assert_eq!( - intersection, - Coord { - x: 0.5f64, - y: 0.5f64 - } - ); + assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); + assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); + println!("{intersection:?}"); + let diff = intersection + - Coord { + x: 1.0 / 3.0f64, + y: 4.0 / 3.0f64, + }; + println!("{diff:?}"); + assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); } else { - assert!(false) + assert!(false); + } + + if let Some(LineIntersectionWithParameterResult { + ab, + cd, + intersection, + }) = line_segment_intersection_with_relationships(&b, &a, &c, &d) + { + assert_eq!(ab, FalseIntersectionPoint(AfterEnd)); + assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); + println!("{intersection:?}"); + let diff = intersection + - Coord { + x: 1.0 / 3.0f64, + y: 4.0 / 3.0f64, + }; + println!("{diff:?}"); + assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + } else { + assert!(false); + } + + if let Some(LineIntersectionWithParameterResult { + ab, + cd, + intersection, + }) = line_segment_intersection_with_relationships(&a, &b, &d, &c) + { + assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); + assert_eq!(cd, FalseIntersectionPoint(AfterEnd)); + println!("{intersection:?}"); + let diff = intersection + - Coord { + x: 1.0 / 3.0f64, + y: 4.0 / 3.0f64, + }; + println!("{diff:?}"); + assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + } else { + assert!(false); } } } diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index 4b9fcbf86..8843f7b96 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -3,7 +3,7 @@ use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; use super::line_intersection::{ - line_intersection_with_parameter, LineIntersectionWithParameterResult, + line_segment_intersection_with_relationships, LineIntersectionWithParameterResult, }; use super::slice_itertools::pairwise; @@ -109,7 +109,7 @@ where result.push(first_point); result.extend(pairwise(&offset_segments[..]).flat_map( |(Line { start: a, end: b }, Line { start: c, end: d })| { - match line_intersection_with_parameter(a, b, c, d) { + match line_segment_intersection_with_relationships(a, b, c, d) { None => { // TODO: this is the colinear case; // we are potentially creating a redundant point in the From 70b5d6d25b54cf9e115b2917f8e4afe327ee206b Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 12/27] improving tests for Offset trait --- geo/src/algorithm/offset/cross_product.rs | 2 +- geo/src/algorithm/offset/line_intersection.rs | 35 ++++++------------- geo/src/algorithm/offset/offset_trait.rs | 15 ++++++-- 3 files changed, 23 insertions(+), 29 deletions(-) diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs index 36d3f2d6b..40b2cae9f 100644 --- a/geo/src/algorithm/offset/cross_product.rs +++ b/geo/src/algorithm/offset/cross_product.rs @@ -69,7 +69,7 @@ mod test { // expect swapping will result in negative assert_eq!(cross_product_2d(ab, ac), -1f64); - // Add skew + // Add skew; results should be the same let a = Coord { x: 0f64, y: 0f64 }; let b = Coord { x: 0f64, y: 1f64 }; let c = Coord { x: 1f64, y: 1f64 }; diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index d48f9b31e..e91d06d6f 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -213,7 +213,7 @@ mod test { FalseIntersectionPointType, LineIntersectionWithParameterResult, LineSegmentIntersectionType, }; - use crate::Coord; + use crate::{Coord, coord}; use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; @@ -269,6 +269,12 @@ mod test { let b = Coord { x: 2f64, y: 3f64 }; let c = Coord { x: 0f64, y: 2f64 }; let d = Coord { x: -2f64, y: 6f64 }; + + fn check_intersection(intersection:Coord){ + let diff = intersection - Coord { x: 1f64 / 3f64, y: 4f64 / 3f64 }; + assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + } + if let Some(LineIntersectionWithParameterResult { ab, cd, @@ -277,14 +283,7 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); - println!("{intersection:?}"); - let diff = intersection - - Coord { - x: 1.0 / 3.0f64, - y: 4.0 / 3.0f64, - }; - println!("{diff:?}"); - assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + check_intersection(intersection); } else { assert!(false); } @@ -297,14 +296,7 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(AfterEnd)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); - println!("{intersection:?}"); - let diff = intersection - - Coord { - x: 1.0 / 3.0f64, - y: 4.0 / 3.0f64, - }; - println!("{diff:?}"); - assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + check_intersection(intersection); } else { assert!(false); } @@ -317,14 +309,7 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(AfterEnd)); - println!("{intersection:?}"); - let diff = intersection - - Coord { - x: 1.0 / 3.0f64, - y: 4.0 / 3.0f64, - }; - println!("{diff:?}"); - assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); + check_intersection(intersection); } else { assert!(false); } diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index 8843f7b96..670f6b423 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -115,6 +115,7 @@ where // we are potentially creating a redundant point in the // output here. Colinear segments should maybe get // removed before or after this algorithm + //println!("CASE 0 - colinear"); vec![*b] }, Some(LineIntersectionWithParameterResult { @@ -122,12 +123,20 @@ where cd, intersection, }) => match (ab, cd) { - (TrueIntersectionPoint, TrueIntersectionPoint) => vec![intersection], + (TrueIntersectionPoint, TrueIntersectionPoint) => { + //println!("CASE 1 - extend"); + vec![intersection] + }, (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { // TODO: Mitre limit logic goes here + //println!("CASE 2 - extend"); vec![intersection] } - _ => vec![*b, *c], + _ => { + println!("CASE 3 - bridge"); + vec![*b, *c] + }, + }, } }, @@ -282,7 +291,7 @@ mod test { x: 5.279039119779313f64, y: 2.516847170273373f64 }, - Coord { x: 5f64, y: 2f64 }, + Coord { x: 5.20f64, y: 2.36f64 }, Coord { x: 3.2388869474813826f64, y: 4.489952088082639f64 From 33a5ca139c2d211486e84a991ef1b729c29b829f Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:42 +0800 Subject: [PATCH 13/27] Offset trait testing improvements --- geo/src/algorithm/offset/line_intersection.rs | 4 +++ geo/src/algorithm/offset/offset_trait.rs | 33 ++++++++++++++++--- rfcs/2022-11-11-offset.md | 6 ++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index e91d06d6f..9ffe66453 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -8,6 +8,8 @@ pub(super) enum FalseIntersectionPointType { /// ray defined by the line segment, but before the start of the line segment. /// /// Abbreviated to `NFIP` in original paper (Negative) + /// (also referred to as `FFIP` in Figure 6, but i think this is an + /// error?) BeforeStart, /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but after the end of the line segment. @@ -172,6 +174,8 @@ where } } +/// Return the intersection point as well as the relationship between the point +/// and each of the input line segments. See [LineSegmentIntersectionType] pub(super) fn line_segment_intersection_with_relationships( a: &Coord, b: &Coord, diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index 670f6b423..e11431dc0 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,4 +1,7 @@ -use super::line_intersection::FalseIntersectionPointType::AfterEnd; +use super::line_intersection::FalseIntersectionPointType::{ + BeforeStart, + AfterEnd, +}; use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; @@ -51,7 +54,9 @@ where /// negative. /// /// Negative `distance` values will offset the edges of the geometry to the - /// left, when facing the direction of increasing coordinate index. + /// left, when facing the direction of increasing coordinate index. For a + /// polygon with clockwise winding order, a positive 'offset' corresponds with + /// an 'inset'. /// /// ``` /// #use crate::{line_string, Coord}; @@ -86,6 +91,13 @@ where } } + +/// # Offset for LineString +/// ## Algorithm +/// Loosely follows the algorithm described by +/// [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005] +/// (https://hal.inria.fr/inria-00518005/document) +/// This was the first google result for 'line offset algorithm' impl Offset for LineString where T: CoordFloat, @@ -102,6 +114,7 @@ where if offset_segments.len() == 1 { return offset_segments[0].into(); } + // First and last will always work: let first_point = offset_segments.first().unwrap().start; let last_point = offset_segments.last().unwrap().end; @@ -127,13 +140,23 @@ where //println!("CASE 1 - extend"); vec![intersection] }, - (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { + + (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { + //println!("CASE 1 - extend"); + vec![intersection] + }, + (FalseIntersectionPoint(_), TrueIntersectionPoint) => { + //println!("CASE 1 - extend"); + vec![intersection] + }, + (FalseIntersectionPoint(BeforeStart), FalseIntersectionPoint(_)) => { // TODO: Mitre limit logic goes here //println!("CASE 2 - extend"); vec![intersection] - } + }, _ => { - println!("CASE 3 - bridge"); + //println!("CASE 3 - bridge"); + //vec![intersection] vec![*b, *c] }, diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md index 4c302467f..3696237d3 100644 --- a/rfcs/2022-11-11-offset.md +++ b/rfcs/2022-11-11-offset.md @@ -23,7 +23,7 @@ Priority for implementing the trait is as follows: - [ ] If some of the limitations discussed below can be addressed - [ ] `Polygon` - [ ] `MultiPolygon` - - [ ] `Geometry` & `GeometryCollection` + - [ ] `Geometry` & `GeometryCollection ## Limitations @@ -44,11 +44,11 @@ and potentially a better algorithm is needed: ### References -Loosely follows the algorithim described by +Loosely follows the algorithm described by [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005](https://hal.inria.fr/inria-00518005/document) This was the first google result for 'line offset algorithm' -### Definitions (For the psudocode in this readme only) +### Definitions (For the psudo-code in this readme only) Type definitions ```python From a7203385281f07393a769e90dfcdf77bff9cf6a4 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:46:43 +0800 Subject: [PATCH 14/27] corrections to Offset trait --- geo/src/algorithm/offset/offset_trait.rs | 3 +-- geo/src/algorithm/offset/slice_itertools.rs | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index e11431dc0..b4997a9be 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -140,7 +140,6 @@ where //println!("CASE 1 - extend"); vec![intersection] }, - (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { //println!("CASE 1 - extend"); vec![intersection] @@ -149,7 +148,7 @@ where //println!("CASE 1 - extend"); vec![intersection] }, - (FalseIntersectionPoint(BeforeStart), FalseIntersectionPoint(_)) => { + (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { // TODO: Mitre limit logic goes here //println!("CASE 2 - extend"); vec![intersection] diff --git a/geo/src/algorithm/offset/slice_itertools.rs b/geo/src/algorithm/offset/slice_itertools.rs index 79c77d8b8..b1e410619 100644 --- a/geo/src/algorithm/offset/slice_itertools.rs +++ b/geo/src/algorithm/offset/slice_itertools.rs @@ -2,7 +2,8 @@ /// /// ```ignore /// let items = vec![1, 2, 3, 4, 5]; -/// let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); +/// let actual_result: Vec<(i32, i32)> = +/// pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); /// let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; /// ``` pub(super) fn pairwise(slice: &[T]) -> std::iter::Zip, std::slice::Iter> { From 68829742f91a304231b0393fa8874f73beab553e Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:39 +0800 Subject: [PATCH 15/27] change result to option type, misc improvements --- geo/src/algorithm/offset/cross_product.rs | 5 +- geo/src/algorithm/offset/line_intersection.rs | 50 ++-- geo/src/algorithm/offset/offset_trait.rs | 255 +++++++++--------- geo/src/algorithm/offset/slice_itertools.rs | 64 ++++- 4 files changed, 213 insertions(+), 161 deletions(-) diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs index 40b2cae9f..6eaa1e1d9 100644 --- a/geo/src/algorithm/offset/cross_product.rs +++ b/geo/src/algorithm/offset/cross_product.rs @@ -3,12 +3,15 @@ use geo_types::Coord; /// The signed magnitude of the 3D "Cross Product" assuming z ordinates are zero /// -/// > Note: `cross_prod` is already defined on `Point`... but that it seems to be +/// > Note: [geo_types::Point::cross_prod] is already defined on [geo_types::Point]... but that it seems to be /// > some other operation on 3 points /// /// > Note: Elsewhere in this project the cross product seems to be done inline /// > and is referred to as 'determinant' since it is the same as the /// > determinant of a 2x2 matrix. +/// +/// > Note: The [geo_types::Line] struct also has a [geo_types::Line::determinant()] function +/// > which has the same /// /// If we pretend the `z` ordinate is zero we can still use the 3D cross product /// on 2D vectors and various useful properties still hold (e.g. it is still the diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index 9ffe66453..096f6a2bf 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -1,5 +1,12 @@ use super::cross_product::cross_product_2d; -use crate::{Coord, CoordFloat, CoordNum}; +use crate::{ + Coord, + CoordFloat, + CoordNum, + algorithm::kernels::Kernel, + algorithm::kernels::RobustKernel, + Orientation +}; // No nested enums :( Goes into the enum below #[derive(PartialEq, Eq, Debug)] @@ -36,8 +43,8 @@ pub(super) enum LineSegmentIntersectionType { use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; -/// Struct to contain the result for [line_intersection_with_parameter] -pub(super) struct LineIntersectionWithParameterResult +/// Struct to contain the result for [line_segment_intersection_with_relationships] +pub(super) struct LineIntersectionResultWithRelationships where T: CoordNum, { @@ -157,15 +164,19 @@ where // TODO: The following line // - Does not use the Kernel // - uses an arbitrary threshold value which needs more thought + + match RobustKernel::orient2d(*a, *b, *d) { + Orientation::Collinear => (), + _ => () + } + let ab_cross_cd = cross_product_2d(ab, cd); - if ::from(ab_cross_cd) - .unwrap() - .abs() - < num_traits::cast(0.0000001f64).unwrap() - { - // Segments are parallel or colinear + if T::is_zero(&ab_cross_cd) { + // Segments are exactly parallel or colinear None } else { + // Division my zero is prevented, but testing is needed to see what + // happens for near-parallel sections of line. let t_ab = cross_product_2d(ac, cd) / ab_cross_cd; let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; let intersection = *a + ab * t_ab; @@ -174,24 +185,25 @@ where } } -/// Return the intersection point as well as the relationship between the point +/// This is a simple wrapper for [line_segment_intersection_with_parameters]; +/// Returns the intersection point as well as the relationship between the point /// and each of the input line segments. See [LineSegmentIntersectionType] pub(super) fn line_segment_intersection_with_relationships( a: &Coord, b: &Coord, c: &Coord, d: &Coord, -) -> Option> +) -> Option> where T: CoordFloat, { line_segment_intersection_with_parameters(a, b, c, d).map(|(t_ab, t_cd, intersection)| { let zero = num_traits::zero::(); let one = num_traits::one::(); - LineIntersectionWithParameterResult { - ab: if zero <= t_ab && t_ab <= one { + LineIntersectionResultWithRelationships { + ab: if T::zero() <= t_ab && t_ab <= T::one() { TrueIntersectionPoint - } else if t_ab < zero { + } else if t_ab < T::zero() { FalseIntersectionPoint(BeforeStart) } else { FalseIntersectionPoint(AfterEnd) @@ -208,13 +220,11 @@ where }) } -// TODO: add more relationship tests; - #[cfg(test)] mod test { use super::{ line_segment_intersection_with_parameters, line_segment_intersection_with_relationships, - FalseIntersectionPointType, LineIntersectionWithParameterResult, + FalseIntersectionPointType, LineIntersectionResultWithRelationships, LineSegmentIntersectionType, }; use crate::{Coord, coord}; @@ -279,7 +289,7 @@ mod test { assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); } - if let Some(LineIntersectionWithParameterResult { + if let Some(LineIntersectionResultWithRelationships { ab, cd, intersection, @@ -292,7 +302,7 @@ mod test { assert!(false); } - if let Some(LineIntersectionWithParameterResult { + if let Some(LineIntersectionResultWithRelationships { ab, cd, intersection, @@ -305,7 +315,7 @@ mod test { assert!(false); } - if let Some(LineIntersectionWithParameterResult { + if let Some(LineIntersectionResultWithRelationships { ab, cd, intersection, diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index b4997a9be..da2ef70a9 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,12 +1,9 @@ -use super::line_intersection::FalseIntersectionPointType::{ - BeforeStart, - AfterEnd, -}; +use super::line_intersection::FalseIntersectionPointType::{AfterEnd, BeforeStart}; use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; use super::line_intersection::{ - line_segment_intersection_with_relationships, LineIntersectionWithParameterResult, + line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, }; use super::slice_itertools::pairwise; @@ -23,16 +20,19 @@ use crate::{ /// # Offset Trait /// -/// Signed offset of Geometry assuming cartesian coordinate system. +/// The offset trait is implemented for geometries where the edges of the +/// geometry can be offset perpendicular to the direction of the edges by some +/// positive or negative distance. For example, an offset [Line] will become a +/// [Line], and an offset [LineString] will become a [LineString]. +/// [geo_types::Point] cannot be offset as it has no directionality. +/// +/// This the [Offset::offset()] function is a more primitive operation than a +/// 'buffer()' operation. A buffer or inset/outset operation will normally +/// produce an enclosed shape; For example a [geo_types::Point] would become a +/// circular [geo_types::Polygon], a [geo_types::Line] would be come a capsule +/// shaped [geo_types::Polygon]. /// -/// This is a cheap offset algorithm that is suitable for flat coordinate systems -/// (or if your lat/lon data is near the equator) /// -/// My Priority for implementing the trait is as follows: -/// - [X] Line -/// - [X] LineString -/// - [X] MultiLineString -/// - ... maybe some closed shapes like triangle, polygon? /// /// The following are a list of known limitations, /// some may be removed during development, @@ -49,14 +49,34 @@ use crate::{ pub trait Offset where T: CoordFloat, + Self: Sized, { - /// Offset the edges of the geometry by `distance`, where `distance` may be - /// negative. + /// Offset the edges of the geometry by `distance`. + /// + /// + /// `distance` may also be negative. Negative `distance` values will offset + /// the edges of the geometry to the left, when facing the direction of + /// increasing coordinate index. + /// + /// For a polygon with clockwise winding order, where the y axis is + /// northward positive, a positive 'distance' corresponds with an 'inset'. + /// This direction has been flagged as counter-intuitive in the PR comments. + /// I will double check this later once I get the polygon offset working and + /// possibly change the choice of direction. + /// + /// If you are using 'screen coordinates' where the y axis is often downward + /// positive then the offset direction described above will be reversed. /// - /// Negative `distance` values will offset the edges of the geometry to the - /// left, when facing the direction of increasing coordinate index. For a - /// polygon with clockwise winding order, a positive 'offset' corresponds with - /// an 'inset'. + /// # TODO + /// + /// - [ ] PR comment suggested name change to + /// `OffsetCurve::offset_curve(...)` ? + /// - [ ] check if any other part of Geo sets the northward-positive + /// assumption. + /// - [ ] Consider a `Result` type with a message explaining the reason for + /// failure? + /// + /// # Examples /// /// ``` /// #use crate::{line_string, Coord}; @@ -70,51 +90,80 @@ where /// Coord { x: 1f64, y: 1f64 }, /// Coord { x: 2f64, y: 1f64 }, /// ]; - /// let output_actual = input.offset(1f64); + /// let output_actual = input.offset(1f64).unwrap(); /// assert_eq!(output_actual, output_expected); /// ``` - fn offset(&self, distance: T) -> Self; + fn offset(&self, distance: T) -> Option; } impl Offset for Line where T: CoordFloat, { - fn offset(&self, distance: T) -> Self { + fn offset(&self, distance: T) -> Option { let delta = self.delta(); let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); - let delta = Coord { - x: delta.y / len, - y: -delta.x / len, - }; - Line::new(self.start + delta * distance, self.end + delta * distance) + if T::is_zero(&len) { + // Cannot offset a zero length Line + None + } else { + // TODO: Is it worth adding a branch to check if the `distance` + // argument is 0 to prevent further computation? The branch + // might hurt performance more than this tiny bit of math? + let delta_norm = delta / len; + // Rotate 90 degrees clockwise (right normal) + // Note that the "rotation direction" depends on the direction of + // the y coordinate: Geographic systems normally have the y axis + // northward positive (like a conventional axes in math). But screen + // coordinates are sometimes downward positive in which case this is + // the left_normal and everything gets reversed. + let delta_norm_right = Coord { + x: delta_norm.y, + y: -delta_norm.x, + }; + Some(Line::new( + self.start + delta_norm_right * distance, + self.end + delta_norm_right * distance, + )) + } } } - -/// # Offset for LineString -/// ## Algorithm -/// Loosely follows the algorithm described by -/// [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005] -/// (https://hal.inria.fr/inria-00518005/document) -/// This was the first google result for 'line offset algorithm' impl Offset for LineString where T: CoordFloat, { - fn offset(&self, distance: T) -> Self { + fn offset(&self, distance: T) -> Option { + // Loosely follows the algorithm described by + // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005] + // (https://hal.inria.fr/inria-00518005/document) + + // TODO: is `self.into_inner()` rather than `self.0` preferred? The + // contents of the tuple struct are public. if self.0.len() < 2 { - // TODO: How should it fail on invalid input? - return self.clone(); + // The docs say "operations and predicates are undefined on invalid + // LineStrings." and a LineString is valid "if it is either empty or + // contains 2 or more coordinates" + + return None; + } + + if T::is_zero(&distance) { + // Prevent unnecessary work when offset distance is zero + return Some(self.clone()); } let offset_segments: Vec> = - self.lines().map(|item| item.offset(distance)).collect(); + match self.lines().map(|item| item.offset(distance)).collect() { + Some(a) => a, + _ => return None, // bail out if any line segment fails + }; if offset_segments.len() == 1 { - return offset_segments[0].into(); + return Some(offset_segments[0].into()); } - // First and last will always work: + // First and last will always work, checked length above: + // TODO: try to eliminate unwrap anyway? let first_point = offset_segments.first().unwrap().start; let last_point = offset_segments.last().unwrap().end; @@ -125,13 +174,13 @@ where match line_segment_intersection_with_relationships(a, b, c, d) { None => { // TODO: this is the colinear case; - // we are potentially creating a redundant point in the + // we are creating a redundant point in the // output here. Colinear segments should maybe get // removed before or after this algorithm //println!("CASE 0 - colinear"); vec![*b] - }, - Some(LineIntersectionWithParameterResult { + } + Some(LineIntersectionResultWithRelationships { ab, cd, intersection, @@ -139,26 +188,25 @@ where (TrueIntersectionPoint, TrueIntersectionPoint) => { //println!("CASE 1 - extend"); vec![intersection] - }, + } (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { //println!("CASE 1 - extend"); vec![intersection] - }, + } (FalseIntersectionPoint(_), TrueIntersectionPoint) => { //println!("CASE 1 - extend"); vec![intersection] - }, + } (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { // TODO: Mitre limit logic goes here //println!("CASE 2 - extend"); vec![intersection] - }, + } _ => { //println!("CASE 3 - bridge"); //vec![intersection] vec![*b, *c] - }, - + } }, } }, @@ -166,7 +214,7 @@ where result.push(last_point); // TODO: there are more steps to this algorithm which are not yet // implemented. See rfcs\2022-11-11-offset.md - result.into() + Some(result.into()) } } @@ -174,7 +222,7 @@ impl Offset for MultiLineString where T: CoordFloat, { - fn offset(&self, distance: T) -> Self { + fn offset(&self, distance: T) -> Option { self.iter().map(|item| item.offset(distance)).collect() } } @@ -202,17 +250,21 @@ mod test { #[test] fn test_offset_line() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let actual_result = input.offset(1.0); - assert_eq!( - actual_result, - Line::new(Coord { x: 2f64, y: 1f64 }, Coord { x: 2f64, y: 2f64 },) - ); + let output_actual = input.offset(1.0); + let output_expected = Some(Line::new( + Coord { x: 2f64, y: 1f64 }, + Coord { x: 2f64, y: 2f64 }, + )); + assert_eq!(output_actual, output_expected); } #[test] fn test_offset_line_negative() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); let output_actual = input.offset(-1.0); - let output_expected = Line::new(Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 }); + let output_expected = Some(Line::new( + Coord { x: 0f64, y: 1f64 }, + Coord { x: 0f64, y: 2f64 }, + )); assert_eq!(output_actual, output_expected); } @@ -223,12 +275,20 @@ mod test { Coord { x: 0f64, y: 2f64 }, Coord { x: 2f64, y: 2f64 }, ]; - let output_expected = line_string![ + let output_actual = input.offset(1f64); + let output_expected = Some(line_string![ Coord { x: 1f64, y: 0f64 }, Coord { x: 1f64, y: 1f64 }, Coord { x: 2f64, y: 1f64 }, - ]; + ]); + assert_eq!(output_actual, output_expected); + } + + #[test] + fn test_offset_line_string_invalid() { + let input = line_string![Coord { x: 0f64, y: 0f64 },]; let output_actual = input.offset(1f64); + let output_expected = None; assert_eq!(output_actual, output_expected); } @@ -246,7 +306,8 @@ mod test { Coord { x: -2f64, y: -2f64 }, ], ]); - let output_expected = MultiLineString::new(vec![ + let output_actual = input.offset(1f64); + let output_expected = Some(MultiLineString::new(vec![ line_string![ Coord { x: 1f64, y: 0f64 }, Coord { x: 1f64, y: 1f64 }, @@ -257,80 +318,8 @@ mod test { Coord { x: -1f64, y: -1f64 }, Coord { x: -2f64, y: -1f64 }, ], - ]); - let output_actual = input.offset(1f64); + ])); assert_eq!(output_actual, output_expected); } - /// Function to draw test output to geogebra.org for inspection - /// - /// Paste the output into the javascript console on geogebra.org to - /// visualize the result - /// - /// The following snippet will extract existing (points and vectors) from geogebra: - /// - /// ```javascript - /// console.log([ - /// "line_string![", - /// ...ggbApplet.getAllObjectNames().filter(item=>item==item.toUpperCase()).map(name=>` Coord{x:${ggbApplet.getXcoord(name)}f64, y:${ggbApplet.getYcoord(name)}f64},`), - /// "]", - /// ].join("\n")) - /// ``` - /// - fn print_geogebra_draw_commands(input: &LineString, prefix: &str, r: u8, g: u8, b: u8) { - let prefix_upper = prefix.to_uppercase(); - let prefix_lower = prefix.to_lowercase(); - input - .coords() - .enumerate() - .for_each(|(index, Coord { x, y })| { - println!(r#"ggbApplet.evalCommand("{prefix_upper}_{{{index}}} = ({x:?},{y:?})")"#) - }); - let x: Vec<_> = input.coords().enumerate().collect(); - pairwise(&x[..]).for_each(|((a, _), (b, _))|{ - println!(r#"ggbApplet.evalCommand("{prefix_lower}_{{{a},{b}}} = Vector({prefix_upper}_{a},{prefix_upper}_{b})")"#); - () - }); - let (dim_r, dim_g, dim_b) = (r / 2, g / 2, b / 2); - println!( - r#"ggbApplet.getAllObjectNames().filter(item=>item.startsWith("{prefix_upper}_")).forEach(item=>ggbApplet.setColor(item,{r},{g},{b}))"# - ); - println!( - r#"ggbApplet.getAllObjectNames().filter(item=>item.startsWith("{prefix_lower}_")).forEach(item=>ggbApplet.setColor(item,{dim_r},{dim_g},{dim_b}))"# - ); - } - - #[test] - fn test_offset_line_string_all_branch() { - // attempts to hit all branches of the line extension / cropping test - let input = line_string![ - Coord { x: 3f64, y: 2f64 }, - Coord { - x: 2.740821628422733f64, - y: 2.2582363315313816f64 - }, - Coord { - x: 5.279039119779313f64, - y: 2.516847170273373f64 - }, - Coord { x: 5.20f64, y: 2.36f64 }, - Coord { - x: 3.2388869474813826f64, - y: 4.489952088082639f64 - }, - Coord { x: 3f64, y: 4f64 }, - Coord { x: 4f64, y: 4f64 }, - Coord { x: 5.5f64, y: 4f64 }, - Coord { - x: 5.240726402928647f64, - y: 4.250497607765981f64 - }, - ]; - print_geogebra_draw_commands(&input, "I", 90, 90, 90); - print_geogebra_draw_commands(&input.offset(-0.1f64), "L", 0, 200, 0); - print_geogebra_draw_commands(&input.offset(0.1f64), "R", 200, 0, 0); - - // TODO: test always fails - assert!(false); - } } diff --git a/geo/src/algorithm/offset/slice_itertools.rs b/geo/src/algorithm/offset/slice_itertools.rs index b1e410619..73c11247b 100644 --- a/geo/src/algorithm/offset/slice_itertools.rs +++ b/geo/src/algorithm/offset/slice_itertools.rs @@ -1,17 +1,51 @@ /// Iterate over a slice in overlapping pairs /// +/// # Examples +/// /// ```ignore -/// let items = vec![1, 2, 3, 4, 5]; -/// let actual_result: Vec<(i32, i32)> = -/// pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); -/// let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; +/// # use crate::offset::slice_itertools::pairwise; +/// let input = vec![1, 2, 3, 4, 5]; +/// let output_actual: Vec<(i32, i32)> = +/// pairwise(&input[..]).map(|(a, b)| (*a, *b)).collect(); +/// let output_expected = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; +/// assert_eq!( +/// output_actual, +/// output_expected +/// ) /// ``` -pub(super) fn pairwise(slice: &[T]) -> std::iter::Zip, std::slice::Iter> { - slice.iter().zip(slice[1..].iter()) +/// +/// # Note +/// +/// We already have [std::slice::windows()] but the problem is it returns a +/// slice, not a tuple; and therefore it is not easy to unpack the result since +/// a slice cannot be used as an irrefutable pattern. For example, the `.map()` +/// in the following snippet creates a compiler error something like `Refutable +/// pattern in function argument; options &[_] and &[_,_,..] are not covered.` +/// +/// ```ignore +/// let some_vector:Vec = vec![1,2,3]; +/// let some_slice:&[i64] = &some_vector[..]; +/// let some_result:Vec = some_slice +/// .windows(2) +/// .map(|&[a, b]| a + b) +/// .collect(); +/// ``` +/// +pub(super) fn pairwise( + slice: &[T], +) -> std::iter::Zip, std::slice::Iter> { + if slice.len() == 0 { + // The following nonsense is needed because slice[1..] would panic + // and because std::iter::empty returns a new type which is super annoying + // fingers crossed the compiler will optimize this out anyway + [].iter().zip([].iter()) + } else { + slice.iter().zip(slice[1..].iter()) + } } /// Iterate over a slice and repeat the first item at the end -/// +/// /// ```ignore /// let items = vec![1, 2, 3, 4, 5]; /// let actual_result: Vec = wrap_one(&items[..]).cloned().collect(); @@ -36,6 +70,22 @@ mod test { assert_eq!(actual_result, expected_result); } + #[test] + fn test_pairwise_one_element() { + let items = vec![1]; + let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); + let expected_result = vec![]; + assert_eq!(actual_result, expected_result); + } + + #[test] + fn test_pairwise_zero_elements() { + let items = vec![]; + let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); + let expected_result = vec![]; + assert_eq!(actual_result, expected_result); + } + #[test] fn test_wrap() { let items = vec![1, 2, 3, 4, 5]; From ea00ad2b14b98ab8bd1c3de6177e6da48b8c73e6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:39 +0800 Subject: [PATCH 16/27] fix logic error --- geo/src/algorithm/offset/offset_trait.rs | 89 ++++++++++++++---------- 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index da2ef70a9..c6226f4d8 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -101,30 +101,32 @@ where T: CoordFloat, { fn offset(&self, distance: T) -> Option { - let delta = self.delta(); - let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); - if T::is_zero(&len) { - // Cannot offset a zero length Line - None + if distance == T::zero() { + // prevent unnecessary work + Some(self.clone()) } else { - // TODO: Is it worth adding a branch to check if the `distance` - // argument is 0 to prevent further computation? The branch - // might hurt performance more than this tiny bit of math? - let delta_norm = delta / len; - // Rotate 90 degrees clockwise (right normal) - // Note that the "rotation direction" depends on the direction of - // the y coordinate: Geographic systems normally have the y axis - // northward positive (like a conventional axes in math). But screen - // coordinates are sometimes downward positive in which case this is - // the left_normal and everything gets reversed. - let delta_norm_right = Coord { - x: delta_norm.y, - y: -delta_norm.x, - }; - Some(Line::new( - self.start + delta_norm_right * distance, - self.end + delta_norm_right * distance, - )) + let delta = self.delta(); + let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); + if T::is_zero(&len) { + // Cannot offset a zero length Line + None + } else { + let delta_norm = delta / len; + // Rotate 90 degrees clockwise (right normal) + // Note that the "rotation direction" depends on the direction of + // the y coordinate: Geographic systems normally have the y axis + // northward positive (like a conventional axes in math). But screen + // coordinates are sometimes downward positive in which case this is + // the left_normal and everything gets reversed. + let delta_norm_right = Coord { + x: delta_norm.y, + y: -delta_norm.x, + }; + Some(Line::new( + self.start + delta_norm_right * distance, + self.end + delta_norm_right * distance, + )) + } } } } @@ -153,6 +155,11 @@ where return Some(self.clone()); } + // TODO: I feel like offset_segments should be lazily computed as part + // of the main iterator below if possible; + // - so we don't need to keep all this in memory at once + // - and so that if we have to bail out later we didn't do all this + // work for nothing let offset_segments: Vec> = match self.lines().map(|item| item.offset(distance)).collect() { Some(a) => a, @@ -186,25 +193,38 @@ where intersection, }) => match (ab, cd) { (TrueIntersectionPoint, TrueIntersectionPoint) => { - //println!("CASE 1 - extend"); - vec![intersection] - } - (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { - //println!("CASE 1 - extend"); - vec![intersection] - } - (FalseIntersectionPoint(_), TrueIntersectionPoint) => { - //println!("CASE 1 - extend"); + // Inside elbow + // No mitre limit needed vec![intersection] } (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { + // Outside elbow // TODO: Mitre limit logic goes here - //println!("CASE 2 - extend"); + // Need to calculate how far out the corner is + // projected relative to the offset `distance` + // + // Pseudocode: + // + // let mitre_limit_config = distance*2; // 200% requested offset distance + // if magnitude(intersection - original_bc) > mitre_limit_config { + // ... + // } vec![intersection] } + + // Not needed I think? + // (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { + // //println!("CASE 1 - extend"); + // vec![*b, *c] + // } + // (FalseIntersectionPoint(_), TrueIntersectionPoint) => { + // //println!("CASE 1 - extend"); + // vec![*b, *c] + // } _ => { + //Inside pinched elbow (forearm curled back through + // bicep 🙃) //println!("CASE 3 - bridge"); - //vec![intersection] vec![*b, *c] } }, @@ -321,5 +341,4 @@ mod test { ])); assert_eq!(output_actual, output_expected); } - } From 1647131720e7676a651f47226e10ac20803fb241 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:39 +0800 Subject: [PATCH 17/27] Added mitre limit; needs refactoring --- geo/src/algorithm/offset/cross_product.rs | 13 +- geo/src/algorithm/offset/line_intersection.rs | 32 ++-- geo/src/algorithm/offset/offset_trait.rs | 141 ++++++++++++------ 3 files changed, 118 insertions(+), 68 deletions(-) diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs index 6eaa1e1d9..861f09325 100644 --- a/geo/src/algorithm/offset/cross_product.rs +++ b/geo/src/algorithm/offset/cross_product.rs @@ -3,15 +3,18 @@ use geo_types::Coord; /// The signed magnitude of the 3D "Cross Product" assuming z ordinates are zero /// -/// > Note: [geo_types::Point::cross_prod] is already defined on [geo_types::Point]... but that it seems to be -/// > some other operation on 3 points +/// > Note: [geo_types::Point::cross_prod()] is already defined on +/// > [geo_types::Point]... but that it seems to be some other operation +/// > on 3 points?? /// /// > Note: Elsewhere in this project the cross product seems to be done inline /// > and is referred to as 'determinant' since it is the same as the /// > determinant of a 2x2 matrix. -/// -/// > Note: The [geo_types::Line] struct also has a [geo_types::Line::determinant()] function -/// > which has the same +/// +/// > Note: The [geo_types::Line] struct also has a +/// > [geo_types::Line::determinant()] function which is the same as +/// > `cross_product_2d(line.start, line.end)` +/// /// /// If we pretend the `z` ordinate is zero we can still use the 3D cross product /// on 2D vectors and various useful properties still hold (e.g. it is still the diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset/line_intersection.rs index 096f6a2bf..b77b84794 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset/line_intersection.rs @@ -3,9 +3,9 @@ use crate::{ Coord, CoordFloat, CoordNum, - algorithm::kernels::Kernel, - algorithm::kernels::RobustKernel, - Orientation + // algorithm::kernels::Kernel, + // algorithm::kernels::RobustKernel, + // Orientation }; // No nested enums :( Goes into the enum below @@ -42,6 +42,7 @@ pub(super) enum LineSegmentIntersectionType { use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; +use geo_types::{Line, line_string}; /// Struct to contain the result for [line_segment_intersection_with_relationships] pub(super) struct LineIntersectionResultWithRelationships @@ -157,18 +158,9 @@ where // TODO: I'm still confused about how to use Kernel / RobustKernel; // the following did not work. I need to read more code // from the rest of this repo to understand. - // if Kernel::orient2d(*a, *b, *d) == Orientation::Collinear { - // note that it is sufficient to check that only one of - // c or d are colinear with ab because of how they are - // related by the original line string. - // TODO: The following line - // - Does not use the Kernel - // - uses an arbitrary threshold value which needs more thought + - match RobustKernel::orient2d(*a, *b, *d) { - Orientation::Collinear => (), - _ => () - } + let ab_cross_cd = cross_product_2d(ab, cd); if T::is_zero(&ab_cross_cd) { @@ -183,6 +175,18 @@ where Some((t_ab, t_cd, intersection)) } + + // OR + + // match RobustKernel::orient2d(*a, *b, *d) { + // Orientation::Collinear => None, + // _ => { + // let t_ab = cross_product_2d(ac, cd) / ab_cross_cd; + // let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; + // let intersection = *a + ab * t_ab; + // Some((t_ab, t_cd, intersection)) + // } + // } } /// This is a simple wrapper for [line_segment_intersection_with_parameters]; diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset/offset_trait.rs index c6226f4d8..d02406752 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset/offset_trait.rs @@ -1,4 +1,4 @@ -use super::line_intersection::FalseIntersectionPointType::{AfterEnd, BeforeStart}; +use super::line_intersection::FalseIntersectionPointType::AfterEnd; use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; @@ -7,16 +7,10 @@ use super::line_intersection::{ }; use super::slice_itertools::pairwise; -use crate::{ - Coord, - CoordFloat, - // Kernel, - // Orientation, - Line, - LineString, - MultiLineString, - // Polygon, -}; +use crate::offset::cross_product::cross_product_2d; +// TODO: Should I be using the re-exported types or, +// using these from the geo_types crate? +use crate::{Coord, CoordFloat, CoordNum, Line, LineString, MultiLineString}; /// # Offset Trait /// @@ -115,9 +109,9 @@ where // Rotate 90 degrees clockwise (right normal) // Note that the "rotation direction" depends on the direction of // the y coordinate: Geographic systems normally have the y axis - // northward positive (like a conventional axes in math). But screen - // coordinates are sometimes downward positive in which case this is - // the left_normal and everything gets reversed. + // northward-positive (like a conventional axes in math). But screen + // coordinates are sometimes downward-positive, in which case this is + // the left_normal and everything gets reversed: let delta_norm_right = Coord { x: delta_norm.y, y: -delta_norm.x, @@ -131,27 +125,57 @@ where } } +// TODO: Trying to get a custom iterator working to replace the pairwise +// function +// +// struct OffsetSegmentsIterator<'a, T> where T:CoordNum{ +// line_string:& 'a LineString, +// distance:T, +// last_offset_segment:Option> +// } +// +// impl<'a, T> Iterator for OffsetSegmentsIterator<'a, T> where T:CoordNum { +// type Item = u32; +// +// fn next(&mut self) -> Option { +// Some(5) +// } +// } +// +// fn line_string_offset_segments<'a, T>(line_string:&'a LineString, distance:T) -> OffsetSegmentsIterator<'a, T> where T:CoordNum { +// OffsetSegmentsIterator { line_string, distance, last_offset_segment: None } +// } + impl Offset for LineString where T: CoordFloat, { fn offset(&self, distance: T) -> Option { // Loosely follows the algorithm described by - // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005] + // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset + // algorithm for polyline curves. Computers in Industry, Elsevier, 2007, + // 15p. inria-00518005] // (https://hal.inria.fr/inria-00518005/document) + // Handle trivial cases; + // Note: Docs say LineString is valid "if it is either empty or contains + // two or more coordinates" // TODO: is `self.into_inner()` rather than `self.0` preferred? The // contents of the tuple struct are public. - if self.0.len() < 2 { - // The docs say "operations and predicates are undefined on invalid - // LineStrings." and a LineString is valid "if it is either empty or - // contains 2 or more coordinates" - - return None; + match self.0.len() { + 0 => return Some(self.clone()), + 1 => return None, + 2 => { + return match Line::new(self.0[0], self.0[1]).offset(distance) { + Some(line) => Some(line.into()), + None => None, + } + } + _ => (), } + // Prevent unnecessary work: if T::is_zero(&distance) { - // Prevent unnecessary work when offset distance is zero return Some(self.clone()); } @@ -160,10 +184,12 @@ where // - so we don't need to keep all this in memory at once // - and so that if we have to bail out later we didn't do all this // work for nothing + // However I haven't been able to get a nice lazy pairwise + // iterator working.. I suspect it requires unsafe code :/ let offset_segments: Vec> = match self.lines().map(|item| item.offset(distance)).collect() { Some(a) => a, - _ => return None, // bail out if any line segment fails + _ => return None, // bail out if any segment fails }; if offset_segments.len() == 1 { @@ -181,9 +207,10 @@ where match line_segment_intersection_with_relationships(a, b, c, d) { None => { // TODO: this is the colinear case; - // we are creating a redundant point in the - // output here. Colinear segments should maybe get - // removed before or after this algorithm + // In some cases this creates a redundant point in the + // output. Colinear segments should maybe get + // merged before or after this algorithm. Not easy + // to fix here. //println!("CASE 0 - colinear"); vec![*b] } @@ -199,28 +226,39 @@ where } (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { // Outside elbow - // TODO: Mitre limit logic goes here - // Need to calculate how far out the corner is - // projected relative to the offset `distance` + // Check for Mitre Limit + // TODO: Mitre limit code below is awful; + // - Some values calculated here were + // previously calculated in + // [line_segment_intersection_with_parameters()] + // - Various optimizations are possible; + // Check against magnitude squared + // - Magnitude function to be moved somewhere + // else // - // Pseudocode: - // - // let mitre_limit_config = distance*2; // 200% requested offset distance - // if magnitude(intersection - original_bc) > mitre_limit_config { - // ... - // } - vec![intersection] + fn magnitude(coord: Coord) -> T + where + T: CoordFloat, + { + (coord.x * coord.x + coord.y * coord.y).sqrt() + } + let mitre_limit_factor = T::from(2.0).unwrap(); + let mitre_limit_distance = distance.abs() * mitre_limit_factor; + let elbow_length = magnitude(intersection - *b); + if elbow_length > mitre_limit_distance { + // Mitre Limited / Truncated Corner + let ab: Coord = *b - *a; + let cd: Coord = *d - *c; + vec![ + *b + ab / magnitude(ab) * mitre_limit_distance, + *c - cd / magnitude(cd) * mitre_limit_distance, + ] + } else { + // Sharp Corner + vec![intersection] + } } - // Not needed I think? - // (TrueIntersectionPoint, FalseIntersectionPoint(_)) => { - // //println!("CASE 1 - extend"); - // vec![*b, *c] - // } - // (FalseIntersectionPoint(_), TrueIntersectionPoint) => { - // //println!("CASE 1 - extend"); - // vec![*b, *c] - // } _ => { //Inside pinched elbow (forearm curled back through // bicep 🙃) @@ -254,7 +292,7 @@ where // fn offset(&self, distance: T) -> Self { // // TODO: not finished yet... need to do interiors // // self.interiors() -// // TODO: is the winding order configurable? +// // TODO: is winding order guaranteed? // self.exterior(); // todo!("Not finished") // } @@ -263,9 +301,14 @@ where #[cfg(test)] mod test { - use crate::{line_string, Coord, Line, LineString, MultiLineString, Offset}; - - use super::super::slice_itertools::pairwise; + use crate::{ + line_string, + Coord, + Line, + //LineString, + MultiLineString, + Offset, + }; #[test] fn test_offset_line() { From f3f9092373aae741941d49d54d99b16f591d7cfa Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:39 +0800 Subject: [PATCH 18/27] renamed offset_curve, flip offset direction --- geo/src/algorithm/mod.rs | 7 +- geo/src/algorithm/offset/cross_product.rs | 91 ------ geo/src/algorithm/offset/mod.rs | 7 - .../line_intersection.rs | 55 ++-- geo/src/algorithm/offset_curve/mod.rs | 9 + .../offset_curve_trait.rs} | 183 ++++-------- .../algorithm/offset_curve/offset_line_raw.rs | 35 +++ .../offset_curve/offset_segments_iterator.rs | 145 ++++++++++ .../slice_itertools.rs | 2 +- .../offset_curve/vector_extensions.rs | 273 ++++++++++++++++++ rfcs/2022-11-11-offset.md | 78 +++-- 11 files changed, 605 insertions(+), 280 deletions(-) delete mode 100644 geo/src/algorithm/offset/cross_product.rs delete mode 100644 geo/src/algorithm/offset/mod.rs rename geo/src/algorithm/{offset => offset_curve}/line_intersection.rs (85%) create mode 100644 geo/src/algorithm/offset_curve/mod.rs rename geo/src/algorithm/{offset/offset_trait.rs => offset_curve/offset_curve_trait.rs} (61%) create mode 100644 geo/src/algorithm/offset_curve/offset_line_raw.rs create mode 100644 geo/src/algorithm/offset_curve/offset_segments_iterator.rs rename geo/src/algorithm/{offset => offset_curve}/slice_itertools.rs (98%) create mode 100644 geo/src/algorithm/offset_curve/vector_extensions.rs diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index cd001566f..9436beae4 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -176,9 +176,10 @@ pub use lines_iter::LinesIter; pub mod map_coords; pub use map_coords::{MapCoords, MapCoordsInPlace}; -/// Apply a simple signed offset -pub mod offset; -pub use offset::Offset; +/// Offset the edges of a geometry perpendicular to the edge direction, either +/// to the left or to the right depending on the sign of the specified distance. +pub mod offset_curve; +pub use offset_curve::OffsetCurve; /// Orient a `Polygon`'s exterior and interior rings. pub mod orient; diff --git a/geo/src/algorithm/offset/cross_product.rs b/geo/src/algorithm/offset/cross_product.rs deleted file mode 100644 index 861f09325..000000000 --- a/geo/src/algorithm/offset/cross_product.rs +++ /dev/null @@ -1,91 +0,0 @@ -use crate::CoordFloat; -use geo_types::Coord; - -/// The signed magnitude of the 3D "Cross Product" assuming z ordinates are zero -/// -/// > Note: [geo_types::Point::cross_prod()] is already defined on -/// > [geo_types::Point]... but that it seems to be some other operation -/// > on 3 points?? -/// -/// > Note: Elsewhere in this project the cross product seems to be done inline -/// > and is referred to as 'determinant' since it is the same as the -/// > determinant of a 2x2 matrix. -/// -/// > Note: The [geo_types::Line] struct also has a -/// > [geo_types::Line::determinant()] function which is the same as -/// > `cross_product_2d(line.start, line.end)` -/// -/// -/// If we pretend the `z` ordinate is zero we can still use the 3D cross product -/// on 2D vectors and various useful properties still hold (e.g. it is still the -/// signed area of the parallelogram formed by the two input vectors, with the -/// sign being dependant on the order and properties of the inputs) -/// -/// From basis vectors `i`,`j`,`k` and the axioms on wikipedia -/// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); -/// -/// ```text -/// i×j = k -/// j×k = i -/// k×i = j -/// -/// j×i = -k -/// k×j = -i -/// i×k = -j -/// -/// i×i = j×j = k×k = 0 -/// ``` -/// -/// We can define the 2D cross product as the magnitude of the 3D cross product -/// as follows -/// -/// ```text -/// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| -/// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| -/// = |a_x·b_x·( 0 ) + a_x·b_y·( k ) + a_y·b_x·(-k ) + a_y·b_y·( 0 )| -/// = | (a_x·b_y - a_y·b_x)·k | -/// = a_x·b_y - a_y·b_x -/// ``` -pub(super) fn cross_product_2d(left: Coord, right: Coord) -> T -where - T: CoordFloat, -{ - left.x * right.y - left.y * right.x -} - -#[cfg(test)] -mod test { - // crate dependencies - use crate::Coord; - - // private imports - use super::cross_product_2d; - - #[test] - fn test_cross_product() { - let a = Coord { x: 0f64, y: 0f64 }; - let b = Coord { x: 0f64, y: 1f64 }; - let c = Coord { x: 1f64, y: 0f64 }; - - let ab = b - a; - let ac = c - a; - - // expect the area of the parallelogram - assert_eq!(cross_product_2d(ac, ab), 1f64); - // expect swapping will result in negative - assert_eq!(cross_product_2d(ab, ac), -1f64); - - // Add skew; results should be the same - let a = Coord { x: 0f64, y: 0f64 }; - let b = Coord { x: 0f64, y: 1f64 }; - let c = Coord { x: 1f64, y: 1f64 }; - - let ab = b - a; - let ac = c - a; - - // expect the area of the parallelogram - assert_eq!(cross_product_2d(ac, ab), 1f64); - // expect swapping will result in negative - assert_eq!(cross_product_2d(ab, ac), -1f64); - } -} diff --git a/geo/src/algorithm/offset/mod.rs b/geo/src/algorithm/offset/mod.rs deleted file mode 100644 index 1dd3a44d1..000000000 --- a/geo/src/algorithm/offset/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ - -mod cross_product; -mod slice_itertools; -mod line_intersection; - -mod offset_trait; -pub use offset_trait::Offset; diff --git a/geo/src/algorithm/offset/line_intersection.rs b/geo/src/algorithm/offset_curve/line_intersection.rs similarity index 85% rename from geo/src/algorithm/offset/line_intersection.rs rename to geo/src/algorithm/offset_curve/line_intersection.rs index b77b84794..47a018e76 100644 --- a/geo/src/algorithm/offset/line_intersection.rs +++ b/geo/src/algorithm/offset_curve/line_intersection.rs @@ -1,4 +1,4 @@ -use super::cross_product::cross_product_2d; +use super::vector_extensions::VectorExtensions; use crate::{ Coord, CoordFloat, @@ -42,7 +42,6 @@ pub(super) enum LineSegmentIntersectionType { use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; -use geo_types::{Line, line_string}; /// Struct to contain the result for [line_segment_intersection_with_relationships] pub(super) struct LineIntersectionResultWithRelationships @@ -155,28 +154,22 @@ where let cd = *d - *c; let ac = *c - *a; - // TODO: I'm still confused about how to use Kernel / RobustKernel; - // the following did not work. I need to read more code - // from the rest of this repo to understand. - - - - - let ab_cross_cd = cross_product_2d(ab, cd); + let ab_cross_cd = ab.cross_product_2d(cd); if T::is_zero(&ab_cross_cd) { // Segments are exactly parallel or colinear None } else { // Division my zero is prevented, but testing is needed to see what // happens for near-parallel sections of line. - let t_ab = cross_product_2d(ac, cd) / ab_cross_cd; - let t_cd = -cross_product_2d(ab, ac) / ab_cross_cd; + let t_ab = ac.cross_product_2d(cd) / ab_cross_cd; + let t_cd = -ab.cross_product_2d(ac) / ab_cross_cd; let intersection = *a + ab * t_ab; Some((t_ab, t_cd, intersection)) } - // OR + // TODO: + // The above could be replaced with the following. // match RobustKernel::orient2d(*a, *b, *d) { // Orientation::Collinear => None, @@ -231,7 +224,7 @@ mod test { FalseIntersectionPointType, LineIntersectionResultWithRelationships, LineSegmentIntersectionType, }; - use crate::{Coord, coord}; + use crate::{Coord}; use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; @@ -288,10 +281,7 @@ mod test { let c = Coord { x: 0f64, y: 2f64 }; let d = Coord { x: -2f64, y: 6f64 }; - fn check_intersection(intersection:Coord){ - let diff = intersection - Coord { x: 1f64 / 3f64, y: 4f64 / 3f64 }; - assert!(diff.x * diff.x + diff.y * diff.y < 0.00000000001f64); - } + let expected_intersection_point = Coord { x: 1f64 / 3f64, y: 4f64 / 3f64 }; if let Some(LineIntersectionResultWithRelationships { ab, @@ -301,7 +291,7 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); - check_intersection(intersection); + assert_relative_eq!(intersection, expected_intersection_point); } else { assert!(false); } @@ -314,7 +304,7 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(AfterEnd)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); - check_intersection(intersection); + assert_relative_eq!(intersection, expected_intersection_point); } else { assert!(false); } @@ -327,7 +317,30 @@ mod test { { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(AfterEnd)); - check_intersection(intersection); + assert_relative_eq!(intersection, expected_intersection_point); + } else { + assert!(false); + } + } + + #[test] + fn test_line_segment_intersection_with_relationships_true() { + let a = Coord { x: 0f64, y: 1f64 }; + let b = Coord { x: 2f64, y: 3f64 }; + let c = Coord { x: 0f64, y: 2f64 }; + let d = Coord { x: -2f64, y: 6f64 }; + + let expected_intersection_point = Coord { x: 1f64 / 3f64, y: 4f64 / 3f64 }; + + if let Some(LineIntersectionResultWithRelationships { + ab, + cd, + intersection, + }) = line_segment_intersection_with_relationships(&a, &b, &c, &d) + { + assert_eq!(ab, TrueIntersectionPoint); + assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); + assert_relative_eq!(intersection, expected_intersection_point); } else { assert!(false); } diff --git a/geo/src/algorithm/offset_curve/mod.rs b/geo/src/algorithm/offset_curve/mod.rs new file mode 100644 index 000000000..3e4e67e32 --- /dev/null +++ b/geo/src/algorithm/offset_curve/mod.rs @@ -0,0 +1,9 @@ + +mod vector_extensions; +mod slice_itertools; +mod line_intersection; +mod offset_segments_iterator; +mod offset_line_raw; + +mod offset_curve_trait; +pub use offset_curve_trait::OffsetCurve; diff --git a/geo/src/algorithm/offset/offset_trait.rs b/geo/src/algorithm/offset_curve/offset_curve_trait.rs similarity index 61% rename from geo/src/algorithm/offset/offset_trait.rs rename to geo/src/algorithm/offset_curve/offset_curve_trait.rs index d02406752..0211d9410 100644 --- a/geo/src/algorithm/offset/offset_trait.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait.rs @@ -2,73 +2,47 @@ use super::line_intersection::FalseIntersectionPointType::AfterEnd; use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; + use super::line_intersection::{ line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, }; -use super::slice_itertools::pairwise; -use crate::offset::cross_product::cross_product_2d; -// TODO: Should I be using the re-exported types or, -// using these from the geo_types crate? -use crate::{Coord, CoordFloat, CoordNum, Line, LineString, MultiLineString}; +use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; +use super::{slice_itertools::pairwise}; + +// TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` +use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; /// # Offset Trait /// -/// The offset trait is implemented for geometries where the edges of the +/// The OffsetCurve trait is implemented for geometries where the edges of the /// geometry can be offset perpendicular to the direction of the edges by some /// positive or negative distance. For example, an offset [Line] will become a /// [Line], and an offset [LineString] will become a [LineString]. -/// [geo_types::Point] cannot be offset as it has no directionality. -/// -/// This the [Offset::offset()] function is a more primitive operation than a -/// 'buffer()' operation. A buffer or inset/outset operation will normally -/// produce an enclosed shape; For example a [geo_types::Point] would become a -/// circular [geo_types::Polygon], a [geo_types::Line] would be come a capsule -/// shaped [geo_types::Polygon]. -/// +/// Geometry with no length ([geo_types::Point]) cannot be offset as it has no +/// directionality. /// -/// -/// The following are a list of known limitations, -/// some may be removed during development, -/// others are very hard to fix. -/// -/// - No checking for zero length input. -/// Invalid results may be caused by division by zero. -/// - No check is implemented to prevent execution if the specified offset -/// distance is zero. -/// - Only local cropping where the output is self-intersecting. -/// Non-adjacent line segments in the output may be self-intersecting. -/// - There is no mitre-limit; A LineString which -/// doubles back on itself will produce an elbow at infinity -pub trait Offset +/// The [OffsetCurve::offset()] function is different to a `buffer` operation. +/// A buffer (or inset / outset operation) would normally produce an enclosed +/// shape; For example a [geo_types::Point] would become a circular +/// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped +/// [geo_types::Polygon]. + +pub trait OffsetCurve where T: CoordFloat, Self: Sized, { /// Offset the edges of the geometry by `distance`. /// + /// In a coordinate system where positive is up and to the right; + /// when facing the direction of increasing coordinate index: + /// + /// - Positive `distance` will offset the edges of a geometry to the left + /// - Negative `distance` will offset the edges of a geometry to the right /// - /// `distance` may also be negative. Negative `distance` values will offset - /// the edges of the geometry to the left, when facing the direction of - /// increasing coordinate index. - /// - /// For a polygon with clockwise winding order, where the y axis is - /// northward positive, a positive 'distance' corresponds with an 'inset'. - /// This direction has been flagged as counter-intuitive in the PR comments. - /// I will double check this later once I get the polygon offset working and - /// possibly change the choice of direction. - /// - /// If you are using 'screen coordinates' where the y axis is often downward - /// positive then the offset direction described above will be reversed. - /// - /// # TODO - /// - /// - [ ] PR comment suggested name change to - /// `OffsetCurve::offset_curve(...)` ? - /// - [ ] check if any other part of Geo sets the northward-positive - /// assumption. - /// - [ ] Consider a `Result` type with a message explaining the reason for - /// failure? + /// If you are using 'screen coordinates' where the y axis is often flipped + /// then the offset direction described above will be reversed. /// /// # Examples /// @@ -84,73 +58,39 @@ where /// Coord { x: 1f64, y: 1f64 }, /// Coord { x: 2f64, y: 1f64 }, /// ]; - /// let output_actual = input.offset(1f64).unwrap(); + /// let output_actual = input.offset_curve(-1f64).unwrap(); /// assert_eq!(output_actual, output_expected); /// ``` - fn offset(&self, distance: T) -> Option; + fn offset_curve(&self, distance: T) -> Option; } -impl Offset for Line +impl OffsetCurve for Line where T: CoordFloat, { - fn offset(&self, distance: T) -> Option { + fn offset_curve(&self, distance: T) -> Option { if distance == T::zero() { // prevent unnecessary work Some(self.clone()) } else { - let delta = self.delta(); - let len = (delta.x * delta.x + delta.y * delta.y).sqrt(); - if T::is_zero(&len) { - // Cannot offset a zero length Line - None - } else { - let delta_norm = delta / len; - // Rotate 90 degrees clockwise (right normal) - // Note that the "rotation direction" depends on the direction of - // the y coordinate: Geographic systems normally have the y axis - // northward-positive (like a conventional axes in math). But screen - // coordinates are sometimes downward-positive, in which case this is - // the left_normal and everything gets reversed: - let delta_norm_right = Coord { - x: delta_norm.y, - y: -delta_norm.x, - }; - Some(Line::new( - self.start + delta_norm_right * distance, - self.end + delta_norm_right * distance, - )) + let Line { start: a, end: b } = *self; + match offset_line_raw(a,b,distance){ + Some(OffsetLineRawResult{ + a_offset, + b_offset, + .. + })=>Some(Line::new(a_offset,b_offset)), + _=>None } } } } -// TODO: Trying to get a custom iterator working to replace the pairwise -// function -// -// struct OffsetSegmentsIterator<'a, T> where T:CoordNum{ -// line_string:& 'a LineString, -// distance:T, -// last_offset_segment:Option> -// } -// -// impl<'a, T> Iterator for OffsetSegmentsIterator<'a, T> where T:CoordNum { -// type Item = u32; -// -// fn next(&mut self) -> Option { -// Some(5) -// } -// } -// -// fn line_string_offset_segments<'a, T>(line_string:&'a LineString, distance:T) -> OffsetSegmentsIterator<'a, T> where T:CoordNum { -// OffsetSegmentsIterator { line_string, distance, last_offset_segment: None } -// } - -impl Offset for LineString +impl OffsetCurve for LineString where T: CoordFloat, { - fn offset(&self, distance: T) -> Option { + fn offset_curve(&self, distance: T) -> Option { // Loosely follows the algorithm described by // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset // algorithm for polyline curves. Computers in Industry, Elsevier, 2007, @@ -160,13 +100,15 @@ where // Handle trivial cases; // Note: Docs say LineString is valid "if it is either empty or contains // two or more coordinates" + // TODO: is `self.into_inner()` rather than `self.0` preferred? The // contents of the tuple struct are public. + // Issue #816 seems to suggest that `self.0` is to be deprecated match self.0.len() { 0 => return Some(self.clone()), 1 => return None, 2 => { - return match Line::new(self.0[0], self.0[1]).offset(distance) { + return match Line::new(self.0[0], self.0[1]).offset_curve(distance) { Some(line) => Some(line.into()), None => None, } @@ -182,12 +124,12 @@ where // TODO: I feel like offset_segments should be lazily computed as part // of the main iterator below if possible; // - so we don't need to keep all this in memory at once - // - and so that if we have to bail out later we didn't do all this - // work for nothing + // - and so that if we have to bail out later we didn't do all + // this work for nothing // However I haven't been able to get a nice lazy pairwise // iterator working.. I suspect it requires unsafe code :/ let offset_segments: Vec> = - match self.lines().map(|item| item.offset(distance)).collect() { + match self.lines().map(|item| item.offset_curve(distance)).collect() { Some(a) => a, _ => return None, // bail out if any segment fails }; @@ -204,10 +146,10 @@ where result.push(first_point); result.extend(pairwise(&offset_segments[..]).flat_map( |(Line { start: a, end: b }, Line { start: c, end: d })| { - match line_segment_intersection_with_relationships(a, b, c, d) { + match line_segment_intersection_with_relationships(&a, &b, &c, &d) { None => { // TODO: this is the colinear case; - // In some cases this creates a redundant point in the + // (In some cases?) this creates a redundant point in the // output. Colinear segments should maybe get // merged before or after this algorithm. Not easy // to fix here. @@ -260,8 +202,8 @@ where } _ => { - //Inside pinched elbow (forearm curled back through - // bicep 🙃) + // Inside pinched elbow + // (ie forearm curled back through bicep 🙃) //println!("CASE 3 - bridge"); vec![*b, *c] } @@ -276,28 +218,15 @@ where } } -impl Offset for MultiLineString +impl OffsetCurve for MultiLineString where T: CoordFloat, { - fn offset(&self, distance: T) -> Option { - self.iter().map(|item| item.offset(distance)).collect() + fn offset_curve(&self, distance: T) -> Option { + self.iter().map(|item| item.offset_curve(distance)).collect() } } -// impl Offset for Polygon -// where -// T: CoordFloat, -// { -// fn offset(&self, distance: T) -> Self { -// // TODO: not finished yet... need to do interiors -// // self.interiors() -// // TODO: is winding order guaranteed? -// self.exterior(); -// todo!("Not finished") -// } -// } - #[cfg(test)] mod test { @@ -307,13 +236,13 @@ mod test { Line, //LineString, MultiLineString, - Offset, + OffsetCurve, }; #[test] fn test_offset_line() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let output_actual = input.offset(1.0); + let output_actual = input.offset_curve(-1.0); let output_expected = Some(Line::new( Coord { x: 2f64, y: 1f64 }, Coord { x: 2f64, y: 2f64 }, @@ -323,7 +252,7 @@ mod test { #[test] fn test_offset_line_negative() { let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let output_actual = input.offset(-1.0); + let output_actual = input.offset_curve(1.0); let output_expected = Some(Line::new( Coord { x: 0f64, y: 1f64 }, Coord { x: 0f64, y: 2f64 }, @@ -338,7 +267,7 @@ mod test { Coord { x: 0f64, y: 2f64 }, Coord { x: 2f64, y: 2f64 }, ]; - let output_actual = input.offset(1f64); + let output_actual = input.offset_curve(-1f64); let output_expected = Some(line_string![ Coord { x: 1f64, y: 0f64 }, Coord { x: 1f64, y: 1f64 }, @@ -350,7 +279,7 @@ mod test { #[test] fn test_offset_line_string_invalid() { let input = line_string![Coord { x: 0f64, y: 0f64 },]; - let output_actual = input.offset(1f64); + let output_actual = input.offset_curve(-1f64); let output_expected = None; assert_eq!(output_actual, output_expected); } @@ -369,7 +298,7 @@ mod test { Coord { x: -2f64, y: -2f64 }, ], ]); - let output_actual = input.offset(1f64); + let output_actual = input.offset_curve(-1f64); let output_expected = Some(MultiLineString::new(vec![ line_string![ Coord { x: 1f64, y: 0f64 }, diff --git a/geo/src/algorithm/offset_curve/offset_line_raw.rs b/geo/src/algorithm/offset_curve/offset_line_raw.rs new file mode 100644 index 000000000..8e62ac910 --- /dev/null +++ b/geo/src/algorithm/offset_curve/offset_line_raw.rs @@ -0,0 +1,35 @@ +use crate::{Coord, CoordFloat}; +use super::vector_extensions::VectorExtensions; + +pub(super) struct OffsetLineRawResult where T:CoordFloat { + pub a_offset:Coord, + pub b_offset:Coord, + pub ab_len:T, +} + + +/// +/// TODO: Document properly +/// +/// +pub(super) fn offset_line_raw( + a: Coord, + b: Coord, + distance: T, +) -> Option> +where + T: CoordFloat, +{ + let ab = b - a; + let ab_len = ab.magnitude(); + if ab_len == T::zero() { + return None; + } + let ab_offset = ab.left() / ab_len * distance; + + Some(OffsetLineRawResult { + a_offset: a + ab_offset, + b_offset: b + ab_offset, + ab_len, + }) +} diff --git a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs new file mode 100644 index 000000000..e2a6d9970 --- /dev/null +++ b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs @@ -0,0 +1,145 @@ +/// I am trying to get a custom iterator working to replace the +/// [super::slice_itertools::pairwise()] function. +/// +/// It is turning out to be very complicated :( +/// +/// My requirements are +/// +/// - Facilitate iterating over `Line`s in a LineString in a pairwise fashion +/// - Offset the `Line` inside the iterator +/// - Avoid repeatedly calculating length for each line +/// - Make iterator lazier (don't keep all offset `Line`s in memory) +/// - Iterator should provide +/// - the offset points +/// - the intersection point ([LineIntersectionResultWithRelationships]) +/// - the pre-calculated length of offset line segments (for miter limit calculation) +/// + +use crate::{Coord, CoordFloat, CoordNum, LineString}; + +use super::line_intersection::{ + line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, +}; +use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; + + +pub(super) struct OffsetSegmentsIterator<'a, T> +where + T: CoordFloat, +{ + line_string: &'a LineString, + distance: T, + last_offset_segment: Option>, + next_offset_segment: OffsetLineRawResult, + index: usize, +} + +impl<'a, T> OffsetSegmentsIterator<'a, T> +where + T: CoordFloat, +{ + fn try_new(line_string: &'a LineString, distance: T) -> Option> + where + T: CoordNum, + { + if line_string.0.len() < 3 { + None + } else { + let a = line_string.0[0]; + let b = line_string.0[1]; + match offset_line_raw(a, b, distance) { + Some(offset_result) => Some(OffsetSegmentsIterator { + line_string, + distance, + last_offset_segment: None, + next_offset_segment: offset_result, + index: 0, + }), + _ => None, + } + } + } +} + +/// +/// ```text +/// a +/// m \ +/// \ \ +/// \ b---------c +/// n +/// +/// i o---------p +/// ``` +pub(super) struct OffsetSegmentsIteratorItem +where + T: CoordNum, +{ + a: Coord, + b: Coord, + c: Coord, + + m: Coord, + n: Coord, + o: Coord, + p: Coord, + + mn_len:T, + op_len:T, + + i: LineIntersectionResultWithRelationships, +} + +impl<'a, T> Iterator for OffsetSegmentsIterator<'a, T> +where + T: CoordFloat, +{ + type Item = OffsetSegmentsIteratorItem; + + fn next(&mut self) -> Option { + if self.index + 3 > self.line_string.0.len() { + // TODO: cant tell the difference between terminating early and + // completing the iterator all the way. + // I think type Item = Option<...> is needed? + return None; + } else { + let a = self.line_string[self.index]; + let b = self.line_string[self.index + 1]; + let c = self.line_string[self.index + 2]; + + self.index += 1; + + let Some(OffsetLineRawResult { + a_offset: m, + b_offset: n, + ab_len: mn_len, + }) = self.last_offset_segment else { + return None + }; + + let Some(OffsetLineRawResult { + a_offset: o, + b_offset: p, + ab_len: op_len, + }) = offset_line_raw(b, c, self.distance) else { + return None + }; + + match line_segment_intersection_with_relationships(&m, &n, &o, &p) { + Some(i)=>Some(OffsetSegmentsIteratorItem { + a, + b, + c, + m, + n, + o, + p, + mn_len, + op_len, + i, + }), + _=>None + } + } + } +} diff --git a/geo/src/algorithm/offset/slice_itertools.rs b/geo/src/algorithm/offset_curve/slice_itertools.rs similarity index 98% rename from geo/src/algorithm/offset/slice_itertools.rs rename to geo/src/algorithm/offset_curve/slice_itertools.rs index 73c11247b..58db7136a 100644 --- a/geo/src/algorithm/offset/slice_itertools.rs +++ b/geo/src/algorithm/offset_curve/slice_itertools.rs @@ -27,7 +27,7 @@ /// let some_slice:&[i64] = &some_vector[..]; /// let some_result:Vec = some_slice /// .windows(2) -/// .map(|&[a, b]| a + b) +/// .map(|&[a, b]| a + b) // <-- error /// .collect(); /// ``` /// diff --git a/geo/src/algorithm/offset_curve/vector_extensions.rs b/geo/src/algorithm/offset_curve/vector_extensions.rs new file mode 100644 index 000000000..809bf3a72 --- /dev/null +++ b/geo/src/algorithm/offset_curve/vector_extensions.rs @@ -0,0 +1,273 @@ +use crate::{Coord, CoordFloat}; + +/// Extends the `Coord` struct with more vector operations; +/// +/// - [VectorExtensions::cross_product_2d], +/// - [VectorExtensions::magnitude], +/// - [VectorExtensions::magnitude_squared], +/// - [VectorExtensions::dot_product], +/// - [VectorExtensions::left], +/// - [VectorExtensions::right], +/// +/// > Note: I implemented these functions here because I am trying to localize +/// > my changes to the [crate::algorithm::offset] module for the time being. +/// +/// > I think I remember seeing some open issues and pull requests that will +/// > hopefully make this trait unnecessary. +/// +/// TODO: make a list +/// +/// > Also there is the [crate::algorithm::Kernel] trait which has some default +/// > implementations very similar to this trait. This is definitely a +/// > re-invented wheel, +/// +/// > Probably better to try to contribute to the existing structure of the code +/// > though rather than suggest disruptive changes.... buuuuut I'm still +/// > feeling this way of implementing [VectorExtensions] on the [Coord] struct +/// > is not entirely indefensible...? Perhaps there could be a +/// > `VectorExtensionsRobust`? +/// > Just thinking aloud. + +pub(super) trait VectorExtensions +where + Self: Copy, +{ + type NumericType; + /// The signed magnitude of the 3D "Cross Product" assuming z ordinates are + /// zero + /// + /// > Note: [geo_types::Point::cross_prod()] is already defined on + /// > [geo_types::Point]... but that it seems to be some other + /// > operation on 3 points?? + /// + /// > Note: Elsewhere in this project the cross product seems to be done + /// > inline and is referred to as 'determinant' since it is the same + /// > as the determinant of a 2x2 matrix. + /// + /// > Note: The [geo_types::Line] struct also has a + /// > [geo_types::Line::determinant()] function which is the same as + /// > `cross_product_2d(line.start, line.end)` + /// + /// + /// If we pretend the `z` ordinate is zero we can still use the 3D cross + /// product on 2D vectors and various useful properties still hold: + /// + /// - the magnitude is the signed area of the parallelogram formed by the + /// two input vectors; + /// - the sign depends on the order of the operands and their clockwise / + /// anti-clockwise orientation with respect to the origin (is b to the + /// left or right of the line between the origin and a) + /// - if the two input points are colinear with the origin, the magnitude is + /// zero + /// + /// From basis vectors `i`,`j`,`k` and the axioms on wikipedia + /// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); + /// + /// ```text + /// i×j = k + /// j×k = i + /// k×i = j + /// + /// j×i = -k + /// k×j = -i + /// i×k = -j + /// + /// i×i = j×j = k×k = 0 + /// ``` + /// + /// We can define the 2D cross product as the magnitude of the 3D cross product + /// as follows + /// + /// ```text + /// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| + /// = |a_x·b_x·(i×i) + a_x·b_y·(i×j) + a_y·b_x·(j×i) + a_y·b_y·(j×j)| + /// = |a_x·b_x·( 0 ) + a_x·b_y·( k ) + a_y·b_x·(-k ) + a_y·b_y·( 0 )| + /// = | (a_x·b_y - a_y·b_x)·k | + /// = a_x·b_y - a_y·b_x + /// ``` + fn cross_product_2d(self, other: Rhs) -> Self::NumericType; + + /// The inner product of the coordinate components + /// + /// ```ignore + /// self.x*other.x + self.y*other.y + /// ``` + fn dot_product(self, other: Rhs) -> Self::NumericType; + + /// The euclidean distance between this coordinate and the origin + /// + /// ``` + /// (self.x*self.x + self.y*self.y).sqrt() + /// ``` + fn magnitude(self) -> Self::NumericType; + + /// The squared distance between this coordinate and the origin. + /// (Avoids the square root calculation when it is not needed) + /// + /// ``` + /// self.x*self.x + self.y*self.y + /// ``` + fn magnitude_squared(self) -> Self::NumericType; + + /// Rotate this coordinate around the origin in the xy plane 90 degrees + /// anti-clockwise (Consistent with [crate::algorithm::rotate::Rotate]). + fn left(self) -> Self; + + /// Rotate this coordinate around the origin in the xy plane 90 degrees + /// clockwise (Consistent with [crate::algorithm::rotate::Rotate]). + fn right(self) -> Self; +} + +impl VectorExtensions for Coord +where + T: CoordFloat, +{ + type NumericType = T; + + fn cross_product_2d(self, right: Coord) -> Self::NumericType { + self.x * right.y - self.y * right.x + } + + fn dot_product(self, other: Self) -> Self::NumericType { + self.x * other.x + self.y * other.y + } + + fn magnitude(self) -> Self::NumericType { + (self.x * self.x + self.y * self.y).sqrt() + } + + fn magnitude_squared(self) -> Self::NumericType { + self.x * self.x + self.y * self.y + } + + fn left(self) -> Self { + Self { + x: -self.y, + y: self.x, + } + } + + fn right(self) -> Self { + Self { + x: self.y, + y: -self.x, + } + } +} + +#[cfg(test)] +mod test { + // crate dependencies + use crate::Coord; + + // private imports + use super::VectorExtensions; + + #[test] + fn test_cross_product() { + // perpendicular unit length + let a = Coord { x: 1f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + + // expect the area of parallelogram + assert_eq!(a.cross_product_2d(b), 1f64); + // expect swapping will result in negative + assert_eq!(b.cross_product_2d(a), -1f64); + + // Add skew; expect results should be the same + let a = Coord { x: 1f64, y: 0f64 }; + let b = Coord { x: 1f64, y: 1f64 }; + + // expect the area of parallelogram + assert_eq!(a.cross_product_2d(b), 1f64); + // expect swapping will result in negative + assert_eq!(b.cross_product_2d(a), -1f64); + + // Make Colinear; expect zero + let a = Coord { x: 2f64, y: 2f64 }; + let b = Coord { x: 1f64, y: 1f64 }; + assert_eq!(a.cross_product_2d(b), 0f64); + } + + #[test] + fn test_dot_product() { + // perpendicular unit length + let a = Coord { x: 1f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + // expect zero for perpendicular + assert_eq!(a.dot_product(b), 0f64); + + // Parallel, same direction + let a = Coord { x: 1f64, y: 0f64 }; + let b = Coord { x: 2f64, y: 0f64 }; + // expect + product of magnitudes + assert_eq!(a.dot_product(b), 2f64); + // expect swapping will have same result + assert_eq!(b.dot_product(a), 2f64); + + // Parallel, opposite direction + let a = Coord { x: 3f64, y: 4f64 }; + let b = Coord { x: -3f64, y: -4f64 }; + // expect - product of magnitudes + assert_eq!(a.dot_product(b), -25f64); + // expect swapping will have same result + assert_eq!(b.dot_product(a), -25f64); + } + + #[test] + fn test_magnitude() { + let a = Coord { x: 1f64, y: 0f64 }; + assert_eq!(a.magnitude(), 1f64); + + let a = Coord { x: 0f64, y: 0f64 }; + assert_eq!(a.magnitude(), 0f64); + + let a = Coord { x: -3f64, y: 4f64 }; + assert_eq!(a.magnitude(), 5f64); + } + + #[test] + fn test_magnitude_squared() { + let a = Coord { x: 1f64, y: 0f64 }; + assert_eq!(a.magnitude_squared(), 1f64); + + let a = Coord { x: 0f64, y: 0f64 }; + assert_eq!(a.magnitude_squared(), 0f64); + + let a = Coord { x: -3f64, y: 4f64 }; + assert_eq!(a.magnitude_squared(), 25f64); + } + + #[test] + fn test_left_right() { + let a = Coord { x: 1f64, y: 0f64 }; + let a_left = Coord { x: 0f64, y: 1f64 }; + let a_right = Coord { x: 0f64, y: -1f64 }; + + assert_eq!(a.left(), a_left); + assert_eq!(a.right(), a_right); + } + + #[test] + fn test_left_right_match_rotate() { + use crate::algorithm::rotate::Rotate; + use crate::Point; + // the documentation for the Rotate trait says: 'Positive angles are + // counter-clockwise, and negative angles are clockwise rotations' + // left is anti-clockwise and right is clockwise: check that the results + // match: + + let a: Point = Coord { x: 1f64, y: 0f64 }.into(); + + assert_relative_eq!( + a.0.right(), + a.rotate_around_point(-90.0, Coord { x: 0.0, y: 0.0 }.into()) + .0 + ); + assert_relative_eq!( + a.0.left(), + a.rotate_around_point(90.0, Coord { x: 0.0, y: 0.0 }.into()) + .0 + ); + } +} diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md index 3696237d3..8122fffb9 100644 --- a/rfcs/2022-11-11-offset.md +++ b/rfcs/2022-11-11-offset.md @@ -1,44 +1,62 @@ -# Offset +# Offset -- Feature Name: `offset` + +- Feature Name: `offset_curve` - Start Date: 2022-11-11 - [Feature PR] -Proposal to add a cheap and simple offset algorithm that is suitable for flat -coordinate systems. +Proposal to add an Offset operation which allows linear geometry to be offset perpendicular to its direction based on the paper outlined below. + +Note that the proposed `offset_curve` is different to a `buffer` operation. ## Offset Trait -Create a Trait called `Offset` in the `algorithms` module. +Create a Trait called `OffsetCurve` in the `algorithms` module. ## Trait Implementations -Priority for implementing the trait is as follows: - -- [X] `Line` -- [X] `LineString` -- [X] `MultiLineString` -- [ ] `Triangle` -- [ ] `Rectangle` -- [ ] If some of the limitations discussed below can be addressed - - [ ] `Polygon` - - [ ] `MultiPolygon` - - [ ] `Geometry` & `GeometryCollection +- [X] `Line` +- [X] `LineString` +- [X] `MultiLineString` +- [ ] `Triangle` +- [ ] `Rectangle` +- [ ] `Polygon` +- [ ] `MultiPolygon` +- [ ] ~~`Point`~~
+ - [ ] ~~`MultiPoint`~~ + - [ ] ~~`Geometry`~~ + - [ ] ~~`GeometryCollection`~~ + +> Note: +> Offset for `Point` Can't be defined without confusion. +> The user may expect the result to be one of the following: +> +> 1. Return the input unchanged, +> 1. a translation (but in what direction?) +> 1. a circle (like a buffer, but as previously explained, +> the `offset` operation is different to the `buffer` operation). +> 1. an empty `LineString` (wierd, yes, but this is actually what GEOS does 🤔) + + +For coordinate types + +- [X] `where T:CoordFloat` +- [ ] `where T:CoordNum` (??? seems tricky) -## Limitations - -Some may be removed during development, others are very hard to fix, -and potentially a better algorithm is needed: - -- [ ] Currently does not make proper use of SimpleKernel / RobustKernel -- [ ] No check is implemented to prevent execution if the specified offset - distance is zero -- [ ] No Mitre-limit is implemented; A LineString which doubles back on itself - will produce an elbow at infinity -- [ ] Only local cropping where the output is self-intersecting. Non-adjacent - line segments in the output may be self-intersecting. -- [ ] Does not handle closed shapes yet - +## To Do / Limitations + +Some may be removed during development + +- [X] Change name from `Offset` to `OffsetCurve` to match terminology in `GEOS` + and `jts` +- [ ] Make proper use of SimpleKernel / RobustKernel +- [X] Prevent execution when the specified offset distance is zero +- [X] Mitre-limit is implemented so that a LineString which doubles-back on + itself will not produce an elbow at infinity + - [ ] Make Mitre Limit configurable? +- [ ] Option to clip output such that non-adjacent line segments in the output + are not self-intersecting. +- [ ] Handle closed shapes ## Algorithm From 171f8fea8823dfd5699b51e4da24a55b9cd80a68 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:39 +0800 Subject: [PATCH 19/27] added trait LineStringOffsetSegmentPairs --- .../offset_curve/line_intersection.rs | 5 +- .../offset_curve/offset_curve_trait.rs | 35 ++-- .../algorithm/offset_curve/offset_line_raw.rs | 26 ++- .../offset_curve/offset_segments_iterator.rs | 188 +++++++++++++----- .../offset_curve/vector_extensions.rs | 80 ++++---- rfcs/2022-11-11-offset.md | 58 ++++-- 6 files changed, 257 insertions(+), 135 deletions(-) diff --git a/geo/src/algorithm/offset_curve/line_intersection.rs b/geo/src/algorithm/offset_curve/line_intersection.rs index 47a018e76..f45fade5e 100644 --- a/geo/src/algorithm/offset_curve/line_intersection.rs +++ b/geo/src/algorithm/offset_curve/line_intersection.rs @@ -9,7 +9,7 @@ use crate::{ }; // No nested enums :( Goes into the enum below -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub(super) enum FalseIntersectionPointType { /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but before the start of the line segment. @@ -27,7 +27,7 @@ pub(super) enum FalseIntersectionPointType { /// Used to encode the relationship between a segment (e.g. between [Coord] `a` and `b`) /// and an intersection point ([Coord] `p`) -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Eq, Debug, Clone)] pub(super) enum LineSegmentIntersectionType { /// The intersection point lies between the start and end of the line segment. /// @@ -44,6 +44,7 @@ use FalseIntersectionPointType::{AfterEnd, BeforeStart}; use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; /// Struct to contain the result for [line_segment_intersection_with_relationships] +#[derive(Clone)] pub(super) struct LineIntersectionResultWithRelationships where T: CoordNum, diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait.rs b/geo/src/algorithm/offset_curve/offset_curve_trait.rs index 0211d9410..dae3036d9 100644 --- a/geo/src/algorithm/offset_curve/offset_curve_trait.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait.rs @@ -8,7 +8,7 @@ use super::line_intersection::{ }; use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; -use super::{slice_itertools::pairwise}; +use super::slice_itertools::pairwise; // TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; @@ -23,7 +23,7 @@ use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; /// directionality. /// /// The [OffsetCurve::offset()] function is different to a `buffer` operation. -/// A buffer (or inset / outset operation) would normally produce an enclosed +/// A buffer (or inset / outset operation) would normally produce an enclosed /// shape; For example a [geo_types::Point] would become a circular /// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped /// [geo_types::Polygon]. @@ -37,7 +37,7 @@ where /// /// In a coordinate system where positive is up and to the right; /// when facing the direction of increasing coordinate index: - /// + /// /// - Positive `distance` will offset the edges of a geometry to the left /// - Negative `distance` will offset the edges of a geometry to the right /// @@ -74,13 +74,11 @@ where Some(self.clone()) } else { let Line { start: a, end: b } = *self; - match offset_line_raw(a,b,distance){ - Some(OffsetLineRawResult{ - a_offset, - b_offset, - .. - })=>Some(Line::new(a_offset,b_offset)), - _=>None + match offset_line_raw(a, b, distance) { + Some(OffsetLineRawResult { + a_offset, b_offset, .. + }) => Some(Line::new(a_offset, b_offset)), + _ => None, } } } @@ -128,11 +126,14 @@ where // this work for nothing // However I haven't been able to get a nice lazy pairwise // iterator working.. I suspect it requires unsafe code :/ - let offset_segments: Vec> = - match self.lines().map(|item| item.offset_curve(distance)).collect() { - Some(a) => a, - _ => return None, // bail out if any segment fails - }; + let offset_segments: Vec> = match self + .lines() + .map(|item| item.offset_curve(distance)) + .collect() + { + Some(a) => a, + _ => return None, // bail out if any segment fails + }; if offset_segments.len() == 1 { return Some(offset_segments[0].into()); @@ -223,7 +224,9 @@ where T: CoordFloat, { fn offset_curve(&self, distance: T) -> Option { - self.iter().map(|item| item.offset_curve(distance)).collect() + self.iter() + .map(|item| item.offset_curve(distance)) + .collect() } } diff --git a/geo/src/algorithm/offset_curve/offset_line_raw.rs b/geo/src/algorithm/offset_curve/offset_line_raw.rs index 8e62ac910..431fd0c9e 100644 --- a/geo/src/algorithm/offset_curve/offset_line_raw.rs +++ b/geo/src/algorithm/offset_curve/offset_line_raw.rs @@ -1,6 +1,9 @@ use crate::{Coord, CoordFloat}; use super::vector_extensions::VectorExtensions; + +/// The result of the [offset_line_raw()] function +#[derive(Clone)] pub(super) struct OffsetLineRawResult where T:CoordFloat { pub a_offset:Coord, pub b_offset:Coord, @@ -8,8 +11,27 @@ pub(super) struct OffsetLineRawResult where T:CoordFloat { } -/// -/// TODO: Document properly +/// Offset a line defined by [Coord]s `a` and `b` by `distance`. +/// +/// In a coordinate system where positive is up and to the right; +/// Positive `distance` will offset the line to the left (when standing +/// at `a` and facing `b`) +/// +/// This could be implemented on [geo_types::Line]... +/// +/// There are 2 reasons +/// +/// 1. I am trying to localize my changes to the offset_curve module for now. +/// 2. I am trying to do is avoid repeated calculation of segment length. +/// This function has a special return type which also yields the length. +/// +/// TODO: In future it may be preferable to create new types called +/// `LineMeasured` and `LineStringMeasured` which store pre-computed length. +/// +/// - Confirm if significant performance benefit to using a bigger structs to +/// avoid recomputing the line segment length? +/// - I think there certainly might be in future parts of the algorithm which +/// need the length repeatedly) /// /// pub(super) fn offset_line_raw( diff --git a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs index e2a6d9970..6b3d19f64 100644 --- a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs +++ b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs @@ -1,8 +1,8 @@ -/// I am trying to get a custom iterator working to replace the +/// I am trying to get a custom iterator working to replace the /// [super::slice_itertools::pairwise()] function. -/// +/// /// It is turning out to be very complicated :( -/// +/// /// My requirements are /// /// - Facilitate iterating over `Line`s in a LineString in a pairwise fashion @@ -12,9 +12,11 @@ /// - Iterator should provide /// - the offset points /// - the intersection point ([LineIntersectionResultWithRelationships]) -/// - the pre-calculated length of offset line segments (for miter limit calculation) +/// - the pre-calculated length of offset line segments (for miter limit +/// calculation) +/// - support wrapping over to the first segment at the end to simplify +/// closed shapes /// - use crate::{Coord, CoordFloat, CoordNum, LineString}; use super::line_intersection::{ @@ -22,6 +24,18 @@ use super::line_intersection::{ }; use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; +/// Bring this into scope to imbue [LineString] with +/// [LineStringOffsetSegmentPairIterable::iter_offset_segment_pairs()] +pub(super) trait LineStringOffsetSegmentPairs +where + T: CoordFloat, +{ + /// Loop over the segments of a [LineString] in a pairwise fashion, + /// offsetting and intersecting them as we go + /// Returns an Option<[OffsetSegmentsIterator]> since if the LineString has + /// less than 3 vertices this operation is impossible. + fn iter_offset_segment_pairs(&self, distance: T) -> OffsetSegmentsIterator; +} pub(super) struct OffsetSegmentsIterator<'a, T> where @@ -29,48 +43,55 @@ where { line_string: &'a LineString, distance: T, - last_offset_segment: Option>, - next_offset_segment: OffsetLineRawResult, + previous_offset_segment: Option>, index: usize, } -impl<'a, T> OffsetSegmentsIterator<'a, T> +impl LineStringOffsetSegmentPairs for LineString where T: CoordFloat, { - fn try_new(line_string: &'a LineString, distance: T) -> Option> + fn iter_offset_segment_pairs(&self, distance: T) -> OffsetSegmentsIterator where T: CoordNum, { - if line_string.0.len() < 3 { - None + if self.0.len() < 3 { + // return an iterator that will return None as first result + OffsetSegmentsIterator { + line_string: self, + distance, + previous_offset_segment: None, + index: 0, + } } else { - let a = line_string.0[0]; - let b = line_string.0[1]; - match offset_line_raw(a, b, distance) { - Some(offset_result) => Some(OffsetSegmentsIterator { - line_string, - distance, - last_offset_segment: None, - next_offset_segment: offset_result, - index: 0, - }), - _ => None, + // TODO: Length check above prevents panic; use + // unsafe get_unchecked for performance? + let a = self.0[0]; + let b = self.0[1]; + OffsetSegmentsIterator { + line_string: self, + distance, + previous_offset_segment: offset_line_raw(a, b, distance), + index: 0, } } } } +/// +/// The following diagram illustrates the meaning of the struct members. +/// The `LineString` `abc` is offset to form the `Line`s `mn` and `op`. /// /// ```text -/// a -/// m \ -/// \ \ -/// \ b---------c -/// n +/// a +/// m \ +/// \ \ +/// \ b---------c +/// n /// -/// i o---------p +/// i o---------p /// ``` +#[derive(Clone)] pub(super) struct OffsetSegmentsIteratorItem where T: CoordNum, @@ -84,9 +105,12 @@ where o: Coord, p: Coord, - mn_len:T, - op_len:T, + /// Distance between `a` and `b` (same as distance between `m` and `n`) + ab_len: T, + /// Distance between `b` and `c` (same as distance between `o` and `p`) + bc_len: T, + /// Intersection [Coord] between segments `mn` and `op` i: LineIntersectionResultWithRelationships, } @@ -94,52 +118,108 @@ impl<'a, T> Iterator for OffsetSegmentsIterator<'a, T> where T: CoordFloat, { - type Item = OffsetSegmentsIteratorItem; + /// The result item is optional since each step of the iteration may fail. + type Item = Option>; + /// The nested Option type here is confusing. The outer Option indicates if + /// iteration is finished. The inner Option indicates if the result of each + /// iteration is valid. The user could, but should not, continue iterating + /// if `Some(None)` is returned. fn next(&mut self) -> Option { if self.index + 3 > self.line_string.0.len() { - // TODO: cant tell the difference between terminating early and - // completing the iterator all the way. - // I think type Item = Option<...> is needed? + // Iteration is complete return None; } else { + // TODO: Length check above prevents panic; use + // unsafe get_unchecked for performance? let a = self.line_string[self.index]; let b = self.line_string[self.index + 1]; let c = self.line_string[self.index + 2]; self.index += 1; - let Some(OffsetLineRawResult { - a_offset: m, - b_offset: n, - ab_len: mn_len, - }) = self.last_offset_segment else { + // Fetch previous offset segment + let Some(OffsetLineRawResult{ + a_offset:m, + b_offset:n, + ab_len, + }) = self.previous_offset_segment else { return None }; - let Some(OffsetLineRawResult { - a_offset: o, - b_offset: p, - ab_len: op_len, - }) = offset_line_raw(b, c, self.distance) else { - return None + // Compute next offset segment + self.previous_offset_segment = offset_line_raw(b, c, self.distance); + let Some(OffsetLineRawResult{ + a_offset:o, + b_offset:p, + ab_len:bc_len, + }) = self.previous_offset_segment else { + return Some(None); }; - match line_segment_intersection_with_relationships(&m, &n, &o, &p) { - Some(i)=>Some(OffsetSegmentsIteratorItem { + Some( + match line_segment_intersection_with_relationships(&m, &n, &o, &p) { + Some(i) => Some(OffsetSegmentsIteratorItem { + a, + b, + c, + m, + n, + o, + p, + ab_len, + bc_len, + i, + }), + _ => None, + }, + ) + } + } +} + +#[cfg(test)] +mod test { + use super::{LineStringOffsetSegmentPairs, OffsetSegmentsIteratorItem}; + use crate::{ + line_string, offset_curve::line_intersection::LineIntersectionResultWithRelationships, + Coord, + }; + + #[test] + fn test_iterator() { + let input = line_string![ + Coord { x: 1f64, y: 0f64 }, + Coord { x: 1f64, y: 1f64 }, + Coord { x: 2f64, y: 1f64 }, + ]; + + let result: Option> = input + .iter_offset_segment_pairs(1f64) + .map(|item| match item { + Some(OffsetSegmentsIteratorItem { a, b, c, + m, n, o, p, - mn_len, - op_len, - i, - }), - _=>None - } - } + + ab_len, + bc_len, + + i: + LineIntersectionResultWithRelationships { + ab, + cd, + intersection, + }, + }) => Some(()), + _ => None, + }) + .collect(); + assert!(result.unwrap().len()==1); } } diff --git a/geo/src/algorithm/offset_curve/vector_extensions.rs b/geo/src/algorithm/offset_curve/vector_extensions.rs index 809bf3a72..600641558 100644 --- a/geo/src/algorithm/offset_curve/vector_extensions.rs +++ b/geo/src/algorithm/offset_curve/vector_extensions.rs @@ -9,57 +9,55 @@ use crate::{Coord, CoordFloat}; /// - [VectorExtensions::left], /// - [VectorExtensions::right], /// -/// > Note: I implemented these functions here because I am trying to localize -/// > my changes to the [crate::algorithm::offset] module for the time being. +/// TODO: I implemented these functions here because I am trying to localize +/// my changes to the [crate::algorithm::offset_curve] module at the moment. /// -/// > I think I remember seeing some open issues and pull requests that will -/// > hopefully make this trait unnecessary. -/// -/// TODO: make a list -/// -/// > Also there is the [crate::algorithm::Kernel] trait which has some default -/// > implementations very similar to this trait. This is definitely a -/// > re-invented wheel, -/// -/// > Probably better to try to contribute to the existing structure of the code -/// > though rather than suggest disruptive changes.... buuuuut I'm still -/// > feeling this way of implementing [VectorExtensions] on the [Coord] struct -/// > is not entirely indefensible...? Perhaps there could be a -/// > `VectorExtensionsRobust`? -/// > Just thinking aloud. +/// The [crate::algorithm::Kernel] trait has some functions which overlap with +/// this trait. I have realized I am re-inventing the wheel here. pub(super) trait VectorExtensions where Self: Copy, { type NumericType; - /// The signed magnitude of the 3D "Cross Product" assuming z ordinates are - /// zero - /// - /// > Note: [geo_types::Point::cross_prod()] is already defined on - /// > [geo_types::Point]... but that it seems to be some other - /// > operation on 3 points?? + /// The 2D cross product is the signed magnitude of the 3D "Cross Product" + /// assuming z ordinates are zero. + /// + /// ## Other names for this function: + /// + /// - In exterior algebra, it is called the wedge product. + /// - If the inputs are packed into a 2x2 matrix, this is the determinant. /// - /// > Note: Elsewhere in this project the cross product seems to be done - /// > inline and is referred to as 'determinant' since it is the same - /// > as the determinant of a 2x2 matrix. + /// ## Other appearances in this library + /// + /// 1. [geo_types::Point::cross_prod()] is already defined on + /// [geo_types::Point]... but that it seems to be some other + /// operation on 3 points?? /// - /// > Note: The [geo_types::Line] struct also has a - /// > [geo_types::Line::determinant()] function which is the same as - /// > `cross_product_2d(line.start, line.end)` + /// 2. Note: The [geo_types::Line] struct also has a + /// [geo_types::Line::determinant()] function which is the same as + /// `cross_product_2d(line.start, line.end)` + /// + /// 3. The [crate::algorithm::Kernel::orient2d()] trait default + /// implementation uses cross product to compute orientation. It returns + /// an enum, not the numeric value which is needed for line segment + /// intersection. /// /// - /// If we pretend the `z` ordinate is zero we can still use the 3D cross - /// product on 2D vectors and various useful properties still hold: + /// ## Properties /// - /// - the magnitude is the signed area of the parallelogram formed by the - /// two input vectors; - /// - the sign depends on the order of the operands and their clockwise / - /// anti-clockwise orientation with respect to the origin (is b to the - /// left or right of the line between the origin and a) - /// - if the two input points are colinear with the origin, the magnitude is - /// zero + /// - The absolute value of the cross product is the area of the + /// parallelogram formed by the operands + /// - The sign of the output is reversed if the operands are reversed + /// - The sign can be used to check if the operands are clockwise / + /// anti-clockwise orientation with respect to the origin; + /// or phrased differently "is b to the left or right of the line between + /// the origin and a"? + /// - If the operands are colinear with the origin, the magnitude is zero /// + /// + /// ## Derivation + /// /// From basis vectors `i`,`j`,`k` and the axioms on wikipedia /// [Cross product](https://en.wikipedia.org/wiki/Cross_product#Computing); /// @@ -75,8 +73,8 @@ where /// i×i = j×j = k×k = 0 /// ``` /// - /// We can define the 2D cross product as the magnitude of the 3D cross product - /// as follows + /// We can define the 2D cross product as the magnitude of the 3D cross + /// product as follows /// /// ```text /// |a × b| = |(a_x·i + a_y·j + 0·k) × (b_x·i + b_y·j + 0·k)| @@ -109,10 +107,12 @@ where /// ``` fn magnitude_squared(self) -> Self::NumericType; + /// In a coordinate system where positive is up and to the right; /// Rotate this coordinate around the origin in the xy plane 90 degrees /// anti-clockwise (Consistent with [crate::algorithm::rotate::Rotate]). fn left(self) -> Self; + /// In a coordinate system where positive is up and to the right; /// Rotate this coordinate around the origin in the xy plane 90 degrees /// clockwise (Consistent with [crate::algorithm::rotate::Rotate]). fn right(self) -> Self; diff --git a/rfcs/2022-11-11-offset.md b/rfcs/2022-11-11-offset.md index 8122fffb9..3d8345926 100644 --- a/rfcs/2022-11-11-offset.md +++ b/rfcs/2022-11-11-offset.md @@ -1,19 +1,30 @@ -# Offset - - -- Feature Name: `offset_curve` -- Start Date: 2022-11-11 -- [Feature PR] - -Proposal to add an Offset operation which allows linear geometry to be offset perpendicular to its direction based on the paper outlined below. +# Offset Curve + +- [1. Introduction](#1-introduction) +- [2. OffsetCurve Trait](#2-offsetcurve-trait) +- [3. Trait Implementations](#3-trait-implementations) +- [4. To Do / Limitations](#4-to-do--limitations) +- [5. Algorithm](#5-algorithm) + - [5.1. References](#51-references) + - [5.2. Definitions (For the psudo-code in this readme only)](#52-definitions-for-the-psudo-code-in-this-readme-only) + - [5.3. Algorithm Part 0 - Pre-Treatment](#53-algorithm-part-0---pre-treatment) + - [5.4. Algorithm Part 0.1 - Segment Offset](#54-algorithm-part-01---segment-offset) + - [5.5. Algorithm Part 1 - Line Extension](#55-algorithm-part-1---line-extension) + - [5.6. Algorithm Part 4.1 - Dual Clipping - **(TODO: not yet implemented)**](#56-algorithm-part-41---dual-clipping---todo-not-yet-implemented) + - [5.7. Algorithm Part 4.1.2 - Cookie Cutter - **(TODO: not yet implemented)**](#57-algorithm-part-412---cookie-cutter---todo-not-yet-implemented) + - [5.8. Algorithm Part 4.1.3 - Proximity Clipping **(TODO: not yet implemented)**](#58-algorithm-part-413---proximity-clipping-todo-not-yet-implemented) + +## 1. Introduction + +Proposal to add an Offset Curve operation which allows linear geometry to be offset perpendicular to its direction based on the paper outlined below. Note that the proposed `offset_curve` is different to a `buffer` operation. -## Offset Trait +## 2. OffsetCurve Trait Create a Trait called `OffsetCurve` in the `algorithms` module. -## Trait Implementations +## 3. Trait Implementations - [X] `Line` - [X] `LineString` @@ -43,7 +54,7 @@ For coordinate types - [X] `where T:CoordFloat` - [ ] `where T:CoordNum` (??? seems tricky) -## To Do / Limitations +## 4. To Do / Limitations Some may be removed during development @@ -58,15 +69,14 @@ Some may be removed during development are not self-intersecting. - [ ] Handle closed shapes -## Algorithm +## 5. Algorithm -### References +### 5.1. References Loosely follows the algorithm described by [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset algorithm for polyline curves. Computers in Industry, Elsevier, 2007, 15p. inria-00518005](https://hal.inria.fr/inria-00518005/document) -This was the first google result for 'line offset algorithm' -### Definitions (For the psudo-code in this readme only) +### 5.2. Definitions (For the psudo-code in this readme only) Type definitions ```python @@ -88,17 +98,19 @@ raw_offset_ls: LineString ``` Function Type Definitions (pseudocode) + ```python intersect = (tool: LineString, target: LineString) -> (point_of_intersection: Optional[Vector2], distance_along_target: List[Parameter]) project = (tool: Vector2, target: LineString) -> (nearest_point_on_target_to_tool: Vector2, distance_along_target: Parameter) interpolate = (distance_along_target: Parameter, target: LineString) -> (point_on_target: Vector2) ``` -### Algorithm 1 - Pre-Treatment +### 5.3. Algorithm Part 0 - Pre-Treatment + 1. Pretreatment steps from the paper are not implemented... these mostly deal with arcs and malformed input geometry -### Algorithm 0.1 - Segment Offset +### 5.4. Algorithm Part 0.1 - Segment Offset 1. Create an empty `LineSegmentList` called `offset_segments` 1. For each `LineSegment` of `input_linestring` @@ -112,7 +124,8 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o 1. append `(offset_a, offset_b)` to `offset_segments` -### Algorithm 1 - Line Extension +### 5.5. Algorithm Part 1 - Line Extension + 4. Create an empty `LineString` called `raw_offset_ls` 1. Append `offset_segments[0][0]` to `raw_offset_ls` 1. For each pair of consecutive segments `(a,b),(c,d)` in `offset_segments` @@ -131,7 +144,8 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o 1. Otherwise, append `b` then `c` to `raw_offset_ls` 1. Remove zero length segments in `raw_offset_ls` -### Algorithm 4.1 - Dual Clipping - **(TODO: not yet implemented)** +### 5.6. Algorithm Part 4.1 - Dual Clipping - **(TODO: not yet implemented)** + 8. Find `raw_offset_ls_twin` by repeating Algorithms 0.1 and 1 but offset the `input_linestring` in the opposite direction (`-d`) 1. Find `intersection_points` between 1. `raw_offset_ls` and `raw_offset_ls` @@ -144,13 +158,15 @@ interpolate = (distance_along_target: Parameter, target: LineString) -> (point_o 1. If we find such an intersection point that *is* on the first or last `LineSegment` of `input_linestring`
then add the intersection point to a list called `cut_targets` -### Algorithm 4.1.2 - Cookie Cutter - **(TODO: not yet implemented)** +### 5.7. Algorithm Part 4.1.2 - Cookie Cutter - **(TODO: not yet implemented)** + 13. For each point `p` in `cut_targets` 1. construct a circle of diameter `d` with its center at `p` 1. delete all parts of any linestring in `split_offset_mls` which falls within this circle 1. Empty the `cut_targets` list -### Algorithm 4.1.3 - Proximity Clipping **(TODO: not yet implemented)** +### 5.8. Algorithm Part 4.1.3 - Proximity Clipping **(TODO: not yet implemented)** + 17. For each linestring `item_ls` in `split_offset_mls` 1. For each segment `(a,b)` in `item_ls` 1. For each segment `(u,v)` of `input_linestring` From 233f45ad96c2de551d05a485fb6a769e85e681a6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 20/27] transition to iterator approach --- geo/src/algorithm/mod.rs | 2 +- .../offset_curve/line_intersection.rs | 19 +- geo/src/algorithm/offset_curve/mod.rs | 3 + .../offset_curve/offset_curve_trait.rs | 75 +++++ .../offset_curve/offset_curve_trait_old.rs | 315 ++++++++++++++++++ .../offset_curve/offset_segments_iterator.rs | 80 ++--- .../algorithm/offset_curve/slice_itertools.rs | 19 +- 7 files changed, 457 insertions(+), 56 deletions(-) create mode 100644 geo/src/algorithm/offset_curve/offset_curve_trait_old.rs diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index 9436beae4..6db49456b 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -179,7 +179,7 @@ pub use map_coords::{MapCoords, MapCoordsInPlace}; /// Offset the edges of a geometry perpendicular to the edge direction, either /// to the left or to the right depending on the sign of the specified distance. pub mod offset_curve; -pub use offset_curve::OffsetCurve; +pub use offset_curve::OffsetCurveOld; /// Orient a `Polygon`'s exterior and interior rings. pub mod orient; diff --git a/geo/src/algorithm/offset_curve/line_intersection.rs b/geo/src/algorithm/offset_curve/line_intersection.rs index f45fade5e..62852e2e1 100644 --- a/geo/src/algorithm/offset_curve/line_intersection.rs +++ b/geo/src/algorithm/offset_curve/line_intersection.rs @@ -40,8 +40,7 @@ pub(super) enum LineSegmentIntersectionType { FalseIntersectionPoint(FalseIntersectionPointType), } -use FalseIntersectionPointType::{AfterEnd, BeforeStart}; -use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; + /// Struct to contain the result for [line_segment_intersection_with_relationships] #[derive(Clone)] @@ -58,9 +57,9 @@ where /// a to b (`ab`), and c to d (`cd`) /// /// > note: looks like there is already `cartesian_intersect` as a private -/// > method in simplifyvw.rs. It is nice because it uses the orient2d method -/// > of the Kernel, however it only gives a true/false answer and does not -/// > return the intersection point or parameters needed. +/// > method in simplifyvw.rs. It uses the orient2d method of [Kernel], +/// > however it only gives a true/false answer and does not return the +/// > intersection point or parameters needed. /// /// We already have LineIntersection trait BUT we need a function that also /// returns the parameters for both lines described below. The LineIntersection @@ -160,7 +159,7 @@ where // Segments are exactly parallel or colinear None } else { - // Division my zero is prevented, but testing is needed to see what + // Division by zero is prevented, but testing is needed to see what // happens for near-parallel sections of line. let t_ab = ac.cross_product_2d(cd) / ab_cross_cd; let t_cd = -ab.cross_product_2d(ac) / ab_cross_cd; @@ -196,8 +195,8 @@ where T: CoordFloat, { line_segment_intersection_with_parameters(a, b, c, d).map(|(t_ab, t_cd, intersection)| { - let zero = num_traits::zero::(); - let one = num_traits::one::(); + use FalseIntersectionPointType::{AfterEnd, BeforeStart}; + use LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}; LineIntersectionResultWithRelationships { ab: if T::zero() <= t_ab && t_ab <= T::one() { TrueIntersectionPoint @@ -206,9 +205,9 @@ where } else { FalseIntersectionPoint(AfterEnd) }, - cd: if zero <= t_cd && t_cd <= one { + cd: if T::zero() <= t_cd && t_cd <= T::one() { TrueIntersectionPoint - } else if t_cd < zero { + } else if t_cd < T::zero() { FalseIntersectionPoint(BeforeStart) } else { FalseIntersectionPoint(AfterEnd) diff --git a/geo/src/algorithm/offset_curve/mod.rs b/geo/src/algorithm/offset_curve/mod.rs index 3e4e67e32..96a448467 100644 --- a/geo/src/algorithm/offset_curve/mod.rs +++ b/geo/src/algorithm/offset_curve/mod.rs @@ -5,5 +5,8 @@ mod line_intersection; mod offset_segments_iterator; mod offset_line_raw; +mod offset_curve_trait_old; +pub use offset_curve_trait_old::OffsetCurveOld; + mod offset_curve_trait; pub use offset_curve_trait::OffsetCurve; diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait.rs b/geo/src/algorithm/offset_curve/offset_curve_trait.rs index dae3036d9..a3ceaf9dc 100644 --- a/geo/src/algorithm/offset_curve/offset_curve_trait.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait.rs @@ -3,11 +3,14 @@ use super::line_intersection::LineSegmentIntersectionType::{ FalseIntersectionPoint, TrueIntersectionPoint, }; +use super::vector_extensions::VectorExtensions; + use super::line_intersection::{ line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, }; use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; +use super::offset_segments_iterator::{LineStringOffsetSegmentPairs, OffsetSegmentsIteratorItem}; use super::slice_itertools::pairwise; // TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` @@ -119,6 +122,78 @@ where return Some(self.clone()); } + // TODO: Consider adding parameters for miter limit method and factor + let mitre_limit_factor = T::from(2.0).unwrap(); + let mitre_limit_distance = distance.abs() * mitre_limit_factor; + let mitre_limit_distance_squared = mitre_limit_distance * mitre_limit_distance; + + // TODO: Unforeseen problem: I want to use flat_map here, + // but I also want to do the fancy collect() trick; + // we can collect Option> from Iter> + let offset_points: Option>>> = self + .iter_offset_segment_pairs(distance) + .map(|item| match item { + Some(OffsetSegmentsIteratorItem { + a, + b, + c, + m, + n, + o, + p, + ab_len, + bc_len, + i: + Some(LineIntersectionResultWithRelationships { + ab, + cd, + intersection, + }), + }) => match (ab, cd) { + (TrueIntersectionPoint, TrueIntersectionPoint) => { + // Inside elbow + // No mitre limit needed + Some(vec![intersection]) + } + (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { + // Outside elbow + // Check for Mitre Limit + let elbow_length_squared = (intersection - n).magnitude_squared(); + if elbow_length_squared > mitre_limit_distance_squared { + // Mitre Limited / Truncated Corner + let mn: Coord = n - m; + let op: Coord = p - o; + Some(vec![ + n + mn / ab_len * mitre_limit_distance, + o - op / bc_len * mitre_limit_distance, + ]) + } else { + // Sharp Corner + Some(vec![intersection]) + } + } + _ => { + // Inside pinched elbow + // (ie forearm curled back through bicep 🙃) + //println!("CASE 3 - bridge"); + Some(vec![n, o]) + } + }, + Some(OffsetSegmentsIteratorItem { i: None, n, .. }) => { + // Collinear + Some(vec![n]) + } + _ => { + // One of the segments could not be offset + None + } + }) + .collect(); + + if let Some(item) = offset_points { + let res: _ = item.iter().flat_map(|item| item).collect(); + } + // TODO: I feel like offset_segments should be lazily computed as part // of the main iterator below if possible; // - so we don't need to keep all this in memory at once diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs b/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs new file mode 100644 index 000000000..80cc74c44 --- /dev/null +++ b/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs @@ -0,0 +1,315 @@ +use super::line_intersection::FalseIntersectionPointType::AfterEnd; +use super::line_intersection::LineSegmentIntersectionType::{ + FalseIntersectionPoint, TrueIntersectionPoint, +}; + +use super::vector_extensions::VectorExtensions; + +use super::line_intersection::{ + line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, +}; + +use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; +use super::slice_itertools::pairwise; + +// TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` +use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; + +/// # Offset Trait +/// +/// The OffsetCurve trait is implemented for geometries where the edges of the +/// geometry can be offset perpendicular to the direction of the edges by some +/// positive or negative distance. For example, an offset [Line] will become a +/// [Line], and an offset [LineString] will become a [LineString]. +/// Geometry with no length ([geo_types::Point]) cannot be offset as it has no +/// directionality. +/// +/// The [OffsetCurve::offset()] function is different to a `buffer` operation. +/// A buffer (or inset / outset operation) would normally produce an enclosed +/// shape; For example a [geo_types::Point] would become a circular +/// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped +/// [geo_types::Polygon]. + +pub trait OffsetCurveOld +where + T: CoordFloat, + Self: Sized, +{ + /// Offset the edges of the geometry by `distance`. + /// + /// In a coordinate system where positive is up and to the right; + /// when facing the direction of increasing coordinate index: + /// + /// - Positive `distance` will offset the edges of a geometry to the left + /// - Negative `distance` will offset the edges of a geometry to the right + /// + /// If you are using 'screen coordinates' where the y axis is often flipped + /// then the offset direction described above will be reversed. + /// + /// # Examples + /// + /// ``` + /// #use crate::{line_string, Coord}; + /// let input = line_string![ + /// Coord { x: 0f64, y: 0f64 }, + /// Coord { x: 0f64, y: 2f64 }, + /// Coord { x: 2f64, y: 2f64 }, + /// ]; + /// let output_expected = line_string![ + /// Coord { x: 1f64, y: 0f64 }, + /// Coord { x: 1f64, y: 1f64 }, + /// Coord { x: 2f64, y: 1f64 }, + /// ]; + /// let output_actual = input.offset_curve(-1f64).unwrap(); + /// assert_eq!(output_actual, output_expected); + /// ``` + fn offset_curve_old(&self, distance: T) -> Option; +} + +impl OffsetCurveOld for Line +where + T: CoordFloat, +{ + fn offset_curve_old(&self, distance: T) -> Option { + if distance == T::zero() { + // prevent unnecessary work + Some(self.clone()) + } else { + let Line { start: a, end: b } = *self; + match offset_line_raw(a, b, distance) { + Some(OffsetLineRawResult { + a_offset, b_offset, .. + }) => Some(Line::new(a_offset, b_offset)), + _ => None, + } + } + } +} + +impl OffsetCurveOld for LineString +where + T: CoordFloat, +{ + fn offset_curve_old(&self, distance: T) -> Option { + // Loosely follows the algorithm described by + // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset + // algorithm for polyline curves. Computers in Industry, Elsevier, 2007, + // 15p. inria-00518005] + // (https://hal.inria.fr/inria-00518005/document) + + // Handle trivial cases; + // Note: Docs say LineString is valid "if it is either empty or contains + // two or more coordinates" + + // TODO: is `self.into_inner()` rather than `self.0` preferred? The + // contents of the tuple struct are public. + // Issue #816 seems to suggest that `self.0` is to be deprecated + match self.0.len() { + 0 => return Some(self.clone()), + 1 => return None, + 2 => { + return match Line::new(self.0[0], self.0[1]).offset_curve_old(distance) { + Some(line) => Some(line.into()), + None => None, + } + } + _ => (), + } + + // Prevent unnecessary work: + if T::is_zero(&distance) { + return Some(self.clone()); + } + + // TODO: I feel like offset_segments should be lazily computed as part + // of the main iterator below if possible; + // - so we don't need to keep all this in memory at once + // - and so that if we have to bail out later we didn't do all + // this work for nothing + // However I haven't been able to get a nice lazy pairwise + // iterator working.. I suspect it requires unsafe code :/ + let offset_segments: Vec> = match self + .lines() + .map(|item| item.offset_curve_old(distance)) + .collect() + { + Some(a) => a, + _ => return None, // bail out if any segment fails + }; + + if offset_segments.len() == 1 { + return Some(offset_segments[0].into()); + } + // First and last will always work, checked length above: + // TODO: try to eliminate unwrap anyway? + let first_point = offset_segments.first().unwrap().start; + let last_point = offset_segments.last().unwrap().end; + + let mut result = Vec::with_capacity(self.0.len()); + result.push(first_point); + result.extend(pairwise(&offset_segments[..]).flat_map( + |(Line { start: a, end: b }, Line { start: c, end: d })| { + match line_segment_intersection_with_relationships(&a, &b, &c, &d) { + None => { + // TODO: this is the colinear case; + // (In some cases?) this creates a redundant point in the + // output. Colinear segments should maybe get + // merged before or after this algorithm. Not easy + // to fix here. + //println!("CASE 0 - colinear"); + vec![*b] + } + Some(LineIntersectionResultWithRelationships { + ab, + cd, + intersection, + }) => match (ab, cd) { + (TrueIntersectionPoint, TrueIntersectionPoint) => { + // Inside elbow + // No mitre limit needed + vec![intersection] + } + (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { + // Outside elbow + // Check for Mitre Limit + // TODO: Mitre limit code below is awful; + // - Some values calculated here were + // previously calculated in + // [line_segment_intersection_with_parameters()] + // - Various optimizations are possible; + // Check against magnitude squared + // - Magnitude function to be moved somewhere + // else + // + let mitre_limit_factor = T::from(2.0).unwrap(); + let mitre_limit_distance = distance.abs() * mitre_limit_factor; + let elbow_length = (intersection - *b).magnitude(); + if elbow_length > mitre_limit_distance { + // Mitre Limited / Truncated Corner + let ab: Coord = *b - *a; + let cd: Coord = *d - *c; + vec![ + *b + ab / ab.magnitude() * mitre_limit_distance, + *c - cd / cd.magnitude() * mitre_limit_distance, + ] + } else { + // Sharp Corner + vec![intersection] + } + } + + _ => { + // Inside pinched elbow + // (ie forearm curled back through bicep 🙃) + //println!("CASE 3 - bridge"); + vec![*b, *c] + } + }, + } + }, + )); + result.push(last_point); + // TODO: there are more steps to this algorithm which are not yet + // implemented. See rfcs\2022-11-11-offset.md + Some(result.into()) + } +} + +impl OffsetCurveOld for MultiLineString +where + T: CoordFloat, +{ + fn offset_curve_old(&self, distance: T) -> Option { + self.iter() + .map(|item| item.offset_curve_old(distance)) + .collect() + } +} + +#[cfg(test)] +mod test { + + use crate::{ + line_string, + Coord, + Line, + //LineString, + MultiLineString, + OffsetCurveOld, + }; + + #[test] + fn test_offset_line() { + let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); + let output_actual = input.offset_curve_old(-1.0); + let output_expected = Some(Line::new( + Coord { x: 2f64, y: 1f64 }, + Coord { x: 2f64, y: 2f64 }, + )); + assert_eq!(output_actual, output_expected); + } + #[test] + fn test_offset_line_negative() { + let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); + let output_actual = input.offset_curve_old(1.0); + let output_expected = Some(Line::new( + Coord { x: 0f64, y: 1f64 }, + Coord { x: 0f64, y: 2f64 }, + )); + assert_eq!(output_actual, output_expected); + } + + #[test] + fn test_offset_line_string() { + let input = line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: 2f64 }, + Coord { x: 2f64, y: 2f64 }, + ]; + let output_actual = input.offset_curve_old(-1f64); + let output_expected = Some(line_string![ + Coord { x: 1f64, y: 0f64 }, + Coord { x: 1f64, y: 1f64 }, + Coord { x: 2f64, y: 1f64 }, + ]); + assert_eq!(output_actual, output_expected); + } + + #[test] + fn test_offset_line_string_invalid() { + let input = line_string![Coord { x: 0f64, y: 0f64 },]; + let output_actual = input.offset_curve_old(-1f64); + let output_expected = None; + assert_eq!(output_actual, output_expected); + } + + #[test] + fn test_offset_multi_line_string() { + let input = MultiLineString::new(vec![ + line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: 2f64 }, + Coord { x: 2f64, y: 2f64 }, + ], + line_string![ + Coord { x: 0f64, y: 0f64 }, + Coord { x: 0f64, y: -2f64 }, + Coord { x: -2f64, y: -2f64 }, + ], + ]); + let output_actual = input.offset_curve_old(-1f64); + let output_expected = Some(MultiLineString::new(vec![ + line_string![ + Coord { x: 1f64, y: 0f64 }, + Coord { x: 1f64, y: 1f64 }, + Coord { x: 2f64, y: 1f64 }, + ], + line_string![ + Coord { x: -1f64, y: 0f64 }, + Coord { x: -1f64, y: -1f64 }, + Coord { x: -2f64, y: -1f64 }, + ], + ])); + assert_eq!(output_actual, output_expected); + } +} diff --git a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs index 6b3d19f64..f675e62b4 100644 --- a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs +++ b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs @@ -31,9 +31,9 @@ where T: CoordFloat, { /// Loop over the segments of a [LineString] in a pairwise fashion, - /// offsetting and intersecting them as we go - /// Returns an Option<[OffsetSegmentsIterator]> since if the LineString has - /// less than 3 vertices this operation is impossible. + /// offsetting and intersecting them as we go. + /// + /// Returns an [OffsetSegmentsIterator] fn iter_offset_segment_pairs(&self, distance: T) -> OffsetSegmentsIterator; } @@ -56,7 +56,8 @@ where T: CoordNum, { if self.0.len() < 3 { - // return an iterator that will return None as first result + // LineString is not long enough, therefore return an iterator that + // will return None as first result OffsetSegmentsIterator { line_string: self, distance, @@ -81,6 +82,7 @@ where /// /// The following diagram illustrates the meaning of the struct members. /// The `LineString` `abc` is offset to form the `Line`s `mn` and `op`. +/// `i` is the intersection point. /// /// ```text /// a @@ -96,35 +98,40 @@ pub(super) struct OffsetSegmentsIteratorItem where T: CoordNum, { - a: Coord, - b: Coord, - c: Coord, + pub a: Coord, + pub b: Coord, + pub c: Coord, - m: Coord, - n: Coord, - o: Coord, - p: Coord, + pub m: Coord, + pub n: Coord, + pub o: Coord, + pub p: Coord, /// Distance between `a` and `b` (same as distance between `m` and `n`) - ab_len: T, + pub ab_len: T, /// Distance between `b` and `c` (same as distance between `o` and `p`) - bc_len: T, + pub bc_len: T, /// Intersection [Coord] between segments `mn` and `op` - i: LineIntersectionResultWithRelationships, + pub i: Option>, } impl<'a, T> Iterator for OffsetSegmentsIterator<'a, T> where T: CoordFloat, { - /// The result item is optional since each step of the iteration may fail. + /// Option since each step of the iteration may fail. type Item = Option>; - /// The nested Option type here is confusing. The outer Option indicates if - /// iteration is finished. The inner Option indicates if the result of each - /// iteration is valid. The user could, but should not, continue iterating - /// if `Some(None)` is returned. + /// Return type is confusing; `Option>>` + /// + /// The outer Option is required by the Iterator trait, and indicates if + /// iteration is finished, (When this iterator is used via `.map()` or + /// similar the user does not see the outer Option.) + /// The inner Option indicates if the result of each iteration is valid. + /// Returning None will halt iteration, returning Some(None) will not, + /// but the user should stop iterating. + /// fn next(&mut self) -> Option { if self.index + 3 > self.line_string.0.len() { // Iteration is complete @@ -157,23 +164,20 @@ where return Some(None); }; - Some( - match line_segment_intersection_with_relationships(&m, &n, &o, &p) { - Some(i) => Some(OffsetSegmentsIteratorItem { - a, - b, - c, - m, - n, - o, - p, - ab_len, - bc_len, - i, - }), - _ => None, - }, - ) + Some(Some( + OffsetSegmentsIteratorItem { + a, + b, + c, + m, // TODO < replace mnop and ab_len and bc_len with two optional OffsetLineRawResult and remove the Option form Self::Item + n,# + o, + p, + ab_len, + bc_len, + i:line_segment_intersection_with_relationships(&m, &n, &o, &p), + } + )) } } } @@ -211,11 +215,11 @@ mod test { bc_len, i: - LineIntersectionResultWithRelationships { + Some(LineIntersectionResultWithRelationships { ab, cd, intersection, - }, + }), }) => Some(()), _ => None, }) diff --git a/geo/src/algorithm/offset_curve/slice_itertools.rs b/geo/src/algorithm/offset_curve/slice_itertools.rs index 58db7136a..2ff7718e0 100644 --- a/geo/src/algorithm/offset_curve/slice_itertools.rs +++ b/geo/src/algorithm/offset_curve/slice_itertools.rs @@ -34,13 +34,18 @@ pub(super) fn pairwise( slice: &[T], ) -> std::iter::Zip, std::slice::Iter> { - if slice.len() == 0 { - // The following nonsense is needed because slice[1..] would panic - // and because std::iter::empty returns a new type which is super annoying - // fingers crossed the compiler will optimize this out anyway - [].iter().zip([].iter()) - } else { - slice.iter().zip(slice[1..].iter()) + match slice.split_first() { + None=>{ + // The following nonsense is needed because std::iter::empty returns + // a new and therefore incompatible type which is unexpected and + // super annoying. I think empty iterators need special treatment by + // the rust compiler in future to fix this. Stack Overflow + // recommends a boxed iterator trait object to avoid this problem, + // but then there are unnecessary heap allocations? Not sure if the + // current method uses the heap anyway? + [].iter().zip([].iter()) + } + Some((_, rest)) => slice.iter().zip(rest.iter()) } } From 1758443e0527e7e7b3eb80581af856f690b6166e Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 21/27] offset_curve uses iterator, working but more complex now :/ --- geo/src/algorithm/mod.rs | 2 +- .../offset_curve/line_intersection.rs | 44 ++-- .../algorithm/offset_curve/line_measured.rs | 30 +++ geo/src/algorithm/offset_curve/mod.rs | 4 +- .../offset_curve/offset_curve_trait.rs | 215 ++++++------------ .../offset_curve/offset_curve_trait_old.rs | 16 +- .../algorithm/offset_curve/offset_line_raw.rs | 59 +++-- .../offset_curve/offset_segments_iterator.rs | 141 ++++++------ 8 files changed, 248 insertions(+), 263 deletions(-) create mode 100644 geo/src/algorithm/offset_curve/line_measured.rs diff --git a/geo/src/algorithm/mod.rs b/geo/src/algorithm/mod.rs index 6db49456b..9436beae4 100644 --- a/geo/src/algorithm/mod.rs +++ b/geo/src/algorithm/mod.rs @@ -179,7 +179,7 @@ pub use map_coords::{MapCoords, MapCoordsInPlace}; /// Offset the edges of a geometry perpendicular to the edge direction, either /// to the left or to the right depending on the sign of the specified distance. pub mod offset_curve; -pub use offset_curve::OffsetCurveOld; +pub use offset_curve::OffsetCurve; /// Orient a `Polygon`'s exterior and interior rings. pub mod orient; diff --git a/geo/src/algorithm/offset_curve/line_intersection.rs b/geo/src/algorithm/offset_curve/line_intersection.rs index 62852e2e1..1e75cb568 100644 --- a/geo/src/algorithm/offset_curve/line_intersection.rs +++ b/geo/src/algorithm/offset_curve/line_intersection.rs @@ -9,7 +9,7 @@ use crate::{ }; // No nested enums :( Goes into the enum below -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub(super) enum FalseIntersectionPointType { /// The intersection point is 'false' or 'virtual': it lies on the infinite /// ray defined by the line segment, but before the start of the line segment. @@ -27,7 +27,7 @@ pub(super) enum FalseIntersectionPointType { /// Used to encode the relationship between a segment (e.g. between [Coord] `a` and `b`) /// and an intersection point ([Coord] `p`) -#[derive(PartialEq, Eq, Debug, Clone)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub(super) enum LineSegmentIntersectionType { /// The intersection point lies between the start and end of the line segment. /// @@ -43,7 +43,7 @@ pub(super) enum LineSegmentIntersectionType { /// Struct to contain the result for [line_segment_intersection_with_relationships] -#[derive(Clone)] +#[derive(Clone, Debug)] pub(super) struct LineIntersectionResultWithRelationships where T: CoordNum, @@ -142,17 +142,17 @@ where /// ``` fn line_segment_intersection_with_parameters( - a: &Coord, - b: &Coord, - c: &Coord, - d: &Coord, + a: Coord, + b: Coord, + c: Coord, + d: Coord, ) -> Option<(T, T, Coord)> where T: CoordFloat, { - let ab = *b - *a; - let cd = *d - *c; - let ac = *c - *a; + let ab = b - a; + let cd = d - c; + let ac = c - a; let ab_cross_cd = ab.cross_product_2d(cd); if T::is_zero(&ab_cross_cd) { @@ -163,7 +163,7 @@ where // happens for near-parallel sections of line. let t_ab = ac.cross_product_2d(cd) / ab_cross_cd; let t_cd = -ab.cross_product_2d(ac) / ab_cross_cd; - let intersection = *a + ab * t_ab; + let intersection = a + ab * t_ab; Some((t_ab, t_cd, intersection)) } @@ -186,10 +186,10 @@ where /// Returns the intersection point as well as the relationship between the point /// and each of the input line segments. See [LineSegmentIntersectionType] pub(super) fn line_segment_intersection_with_relationships( - a: &Coord, - b: &Coord, - c: &Coord, - d: &Coord, + a: Coord, + b: Coord, + c: Coord, + d: Coord, ) -> Option> where T: CoordFloat, @@ -235,7 +235,7 @@ mod test { let c = Coord { x: 0f64, y: 1f64 }; let d = Coord { x: 1f64, y: 0f64 }; if let Some((t_ab, t_cd, intersection)) = - line_segment_intersection_with_parameters(&a, &b, &c, &d) + line_segment_intersection_with_parameters(a, b, c, d) { assert_eq!(t_ab, 0.25f64); assert_eq!(t_cd, 0.50f64); @@ -258,7 +258,7 @@ mod test { let c = Coord { x: 9f64, y: 9f64 }; let d = Coord { x: 12f64, y: 13f64 }; assert_eq!( - line_segment_intersection_with_parameters(&a, &b, &c, &d), + line_segment_intersection_with_parameters(a, b, c, d), None ) } @@ -269,7 +269,7 @@ mod test { let c = Coord { x: 3f64, y: 6f64 }; let d = Coord { x: 5f64, y: 10f64 }; assert_eq!( - line_segment_intersection_with_parameters(&a, &b, &c, &d), + line_segment_intersection_with_parameters(a, b, c, d), None ) } @@ -287,7 +287,7 @@ mod test { ab, cd, intersection, - }) = line_segment_intersection_with_relationships(&a, &b, &c, &d) + }) = line_segment_intersection_with_relationships(a, b, c, d) { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); @@ -300,7 +300,7 @@ mod test { ab, cd, intersection, - }) = line_segment_intersection_with_relationships(&b, &a, &c, &d) + }) = line_segment_intersection_with_relationships(b, a, c, d) { assert_eq!(ab, FalseIntersectionPoint(AfterEnd)); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); @@ -313,7 +313,7 @@ mod test { ab, cd, intersection, - }) = line_segment_intersection_with_relationships(&a, &b, &d, &c) + }) = line_segment_intersection_with_relationships(a, b, d, c) { assert_eq!(ab, FalseIntersectionPoint(BeforeStart)); assert_eq!(cd, FalseIntersectionPoint(AfterEnd)); @@ -336,7 +336,7 @@ mod test { ab, cd, intersection, - }) = line_segment_intersection_with_relationships(&a, &b, &c, &d) + }) = line_segment_intersection_with_relationships(a, b, c, d) { assert_eq!(ab, TrueIntersectionPoint); assert_eq!(cd, FalseIntersectionPoint(BeforeStart)); diff --git a/geo/src/algorithm/offset_curve/line_measured.rs b/geo/src/algorithm/offset_curve/line_measured.rs new file mode 100644 index 000000000..c5d95c265 --- /dev/null +++ b/geo/src/algorithm/offset_curve/line_measured.rs @@ -0,0 +1,30 @@ + +use crate::{Line, CoordNum}; + +// Note: Previously I had a struct called "OffsetLineRaw" which turned out to +// be a line and it's length. This new struct [LineMeasured] is basically the +// same, but +// - has wider use cases +// - has a name that communicate's it's content better + +/// A struct containing a [Line] and it's precalculated length. +/// +/// TODO: I always Assume that it is faster to store calculated lengths than +/// simply recalculate them every time they are needed. This is likely the case +/// if the length is recalculated many times for the same Line. But in practice +/// there may be no benefit if it is only used once or twice in the same +/// function, where the compiler might have made the same optimization on our +/// behalf +/// +/// TODO: I would like to mark this type as immutable so that it can only be +/// instantiated using the full struct constructor and never mutated. Apparently +/// that isn't possible in rust without making all members private. This is sad +/// because we give up nice destructuring syntax if we make members private. For +/// the time being, I am leaving members public. I think ultimately this type +/// might go away in refactoring and integrating with other parts of the geo +/// crate. +#[derive(Clone, PartialEq, Eq, Debug)] +pub(super)struct LineMeasured where T:CoordNum { + pub line:Line, + pub length:T +} \ No newline at end of file diff --git a/geo/src/algorithm/offset_curve/mod.rs b/geo/src/algorithm/offset_curve/mod.rs index 96a448467..eacce8a57 100644 --- a/geo/src/algorithm/offset_curve/mod.rs +++ b/geo/src/algorithm/offset_curve/mod.rs @@ -4,9 +4,11 @@ mod slice_itertools; mod line_intersection; mod offset_segments_iterator; mod offset_line_raw; +mod line_measured; +// Kept temporarily during transition to new approach mod offset_curve_trait_old; -pub use offset_curve_trait_old::OffsetCurveOld; +use offset_curve_trait_old::OffsetCurveOld; mod offset_curve_trait; pub use offset_curve_trait::OffsetCurve; diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait.rs b/geo/src/algorithm/offset_curve/offset_curve_trait.rs index a3ceaf9dc..cd4deb8a6 100644 --- a/geo/src/algorithm/offset_curve/offset_curve_trait.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait.rs @@ -1,20 +1,19 @@ -use super::line_intersection::FalseIntersectionPointType::AfterEnd; -use super::line_intersection::LineSegmentIntersectionType::{ - FalseIntersectionPoint, TrueIntersectionPoint, +// TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` +use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; + +use super::line_intersection::{ + FalseIntersectionPointType::AfterEnd, + LineIntersectionResultWithRelationships, + LineSegmentIntersectionType::{FalseIntersectionPoint, TrueIntersectionPoint}, }; +use super::line_measured::LineMeasured; + use super::vector_extensions::VectorExtensions; -use super::line_intersection::{ - line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, -}; +use super::offset_line_raw::offset_line_raw; -use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; use super::offset_segments_iterator::{LineStringOffsetSegmentPairs, OffsetSegmentsIteratorItem}; -use super::slice_itertools::pairwise; - -// TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` -use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; /// # Offset Trait /// @@ -78,9 +77,7 @@ where } else { let Line { start: a, end: b } = *self; match offset_line_raw(a, b, distance) { - Some(OffsetLineRawResult { - a_offset, b_offset, .. - }) => Some(Line::new(a_offset, b_offset)), + Some(LineMeasured { line, .. }) => Some(line), _ => None, } } @@ -127,33 +124,42 @@ where let mitre_limit_distance = distance.abs() * mitre_limit_factor; let mitre_limit_distance_squared = mitre_limit_distance * mitre_limit_distance; - // TODO: Unforeseen problem: I want to use flat_map here, - // but I also want to do the fancy collect() trick; - // we can collect Option> from Iter> - let offset_points: Option>>> = self - .iter_offset_segment_pairs(distance) - .map(|item| match item { - Some(OffsetSegmentsIteratorItem { - a, - b, - c, - m, - n, - o, - p, - ab_len, - bc_len, + let mut offset_points = Vec::with_capacity(self.0.len()); + + for item in self.iter_offset_segment_pairs(distance) { + println!("{item:?}"); + if let OffsetSegmentsIteratorItem { + ab_offset: Some(LineMeasured { line, .. }), + first: true, + .. + } = item + { + offset_points.push(line.start) + }; + match item { + OffsetSegmentsIteratorItem { + ab_offset: + Some(LineMeasured { + line: Line { start: m, end: n }, + length: ab_len, + }), + bc_offset: + Some(LineMeasured { + line: Line { start: o, end: p }, + length: bc_len, + }), i: Some(LineIntersectionResultWithRelationships { ab, cd, intersection, }), - }) => match (ab, cd) { + .. + } => match (ab, cd) { (TrueIntersectionPoint, TrueIntersectionPoint) => { // Inside elbow // No mitre limit needed - Some(vec![intersection]) + offset_points.push(intersection) } (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { // Outside elbow @@ -163,134 +169,55 @@ where // Mitre Limited / Truncated Corner let mn: Coord = n - m; let op: Coord = p - o; - Some(vec![ - n + mn / ab_len * mitre_limit_distance, - o - op / bc_len * mitre_limit_distance, - ]) + offset_points.push(n + mn / ab_len * mitre_limit_distance); + offset_points.push(o - op / bc_len * mitre_limit_distance); } else { // Sharp Corner - Some(vec![intersection]) + offset_points.push(intersection) } } _ => { // Inside pinched elbow // (ie forearm curled back through bicep 🙃) //println!("CASE 3 - bridge"); - Some(vec![n, o]) + offset_points.push(n); + offset_points.push(o); } }, - Some(OffsetSegmentsIteratorItem { i: None, n, .. }) => { + OffsetSegmentsIteratorItem { + ab_offset: + Some(LineMeasured { + line: Line { end: n, .. }, + .. + }), + i: None, + .. + } => { // Collinear - Some(vec![n]) + // TODO: this is not an elegant way to handle colinear + // input: in some (all?) cases this produces a redundant + // colinear point in the output. It might be easier to + // eliminate this redundant point in a pre-processing step + // rather than try do it here. + offset_points.push(n) } _ => { - // One of the segments could not be offset - None + // Several ways to end up here... probably one of the + // segments could not be offset + return None; } - }) - .collect(); - - if let Some(item) = offset_points { - let res: _ = item.iter().flat_map(|item| item).collect(); - } - - // TODO: I feel like offset_segments should be lazily computed as part - // of the main iterator below if possible; - // - so we don't need to keep all this in memory at once - // - and so that if we have to bail out later we didn't do all - // this work for nothing - // However I haven't been able to get a nice lazy pairwise - // iterator working.. I suspect it requires unsafe code :/ - let offset_segments: Vec> = match self - .lines() - .map(|item| item.offset_curve(distance)) - .collect() - { - Some(a) => a, - _ => return None, // bail out if any segment fails - }; - - if offset_segments.len() == 1 { - return Some(offset_segments[0].into()); + } + if let OffsetSegmentsIteratorItem { + bc_offset: Some(LineMeasured { line, .. }), + last: true, + .. + } = item + { + offset_points.push(line.end) + }; } - // First and last will always work, checked length above: - // TODO: try to eliminate unwrap anyway? - let first_point = offset_segments.first().unwrap().start; - let last_point = offset_segments.last().unwrap().end; - let mut result = Vec::with_capacity(self.0.len()); - result.push(first_point); - result.extend(pairwise(&offset_segments[..]).flat_map( - |(Line { start: a, end: b }, Line { start: c, end: d })| { - match line_segment_intersection_with_relationships(&a, &b, &c, &d) { - None => { - // TODO: this is the colinear case; - // (In some cases?) this creates a redundant point in the - // output. Colinear segments should maybe get - // merged before or after this algorithm. Not easy - // to fix here. - //println!("CASE 0 - colinear"); - vec![*b] - } - Some(LineIntersectionResultWithRelationships { - ab, - cd, - intersection, - }) => match (ab, cd) { - (TrueIntersectionPoint, TrueIntersectionPoint) => { - // Inside elbow - // No mitre limit needed - vec![intersection] - } - (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { - // Outside elbow - // Check for Mitre Limit - // TODO: Mitre limit code below is awful; - // - Some values calculated here were - // previously calculated in - // [line_segment_intersection_with_parameters()] - // - Various optimizations are possible; - // Check against magnitude squared - // - Magnitude function to be moved somewhere - // else - // - fn magnitude(coord: Coord) -> T - where - T: CoordFloat, - { - (coord.x * coord.x + coord.y * coord.y).sqrt() - } - let mitre_limit_factor = T::from(2.0).unwrap(); - let mitre_limit_distance = distance.abs() * mitre_limit_factor; - let elbow_length = magnitude(intersection - *b); - if elbow_length > mitre_limit_distance { - // Mitre Limited / Truncated Corner - let ab: Coord = *b - *a; - let cd: Coord = *d - *c; - vec![ - *b + ab / magnitude(ab) * mitre_limit_distance, - *c - cd / magnitude(cd) * mitre_limit_distance, - ] - } else { - // Sharp Corner - vec![intersection] - } - } - - _ => { - // Inside pinched elbow - // (ie forearm curled back through bicep 🙃) - //println!("CASE 3 - bridge"); - vec![*b, *c] - } - }, - } - }, - )); - result.push(last_point); - // TODO: there are more steps to this algorithm which are not yet - // implemented. See rfcs\2022-11-11-offset.md - Some(result.into()) + Some(offset_points.into()) } } diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs b/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs index 80cc74c44..b522d3c3a 100644 --- a/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs @@ -9,7 +9,8 @@ use super::line_intersection::{ line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, }; -use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; +use super::offset_line_raw::{offset_line_raw}; +use super::line_measured::LineMeasured; use super::slice_itertools::pairwise; // TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` @@ -30,6 +31,7 @@ use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; /// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped /// [geo_types::Polygon]. +#[deprecated] pub trait OffsetCurveOld where T: CoordFloat, @@ -77,9 +79,9 @@ where } else { let Line { start: a, end: b } = *self; match offset_line_raw(a, b, distance) { - Some(OffsetLineRawResult { - a_offset, b_offset, .. - }) => Some(Line::new(a_offset, b_offset)), + Some(LineMeasured { + line, .. + }) => Some(line), _ => None, } } @@ -149,7 +151,7 @@ where result.push(first_point); result.extend(pairwise(&offset_segments[..]).flat_map( |(Line { start: a, end: b }, Line { start: c, end: d })| { - match line_segment_intersection_with_relationships(&a, &b, &c, &d) { + match line_segment_intersection_with_relationships(*a, *b, *c, *d) { None => { // TODO: this is the colinear case; // (In some cases?) this creates a redundant point in the @@ -235,7 +237,9 @@ mod test { Line, //LineString, MultiLineString, - OffsetCurveOld, + }; + use super::{ + OffsetCurveOld }; #[test] diff --git a/geo/src/algorithm/offset_curve/offset_line_raw.rs b/geo/src/algorithm/offset_curve/offset_line_raw.rs index 431fd0c9e..c693dc710 100644 --- a/geo/src/algorithm/offset_curve/offset_line_raw.rs +++ b/geo/src/algorithm/offset_curve/offset_line_raw.rs @@ -1,14 +1,14 @@ -use crate::{Coord, CoordFloat}; -use super::vector_extensions::VectorExtensions; +use crate::{Coord, CoordFloat, Line}; +use super::{vector_extensions::VectorExtensions, line_measured::LineMeasured}; /// The result of the [offset_line_raw()] function -#[derive(Clone)] -pub(super) struct OffsetLineRawResult where T:CoordFloat { - pub a_offset:Coord, - pub b_offset:Coord, - pub ab_len:T, -} +// #[derive(Clone)] +// pub(super) struct OffsetLineRawResult where T:CoordFloat { +// pub a_offset:Coord, +// pub b_offset:Coord, +// pub ab_len:T, +// } /// Offset a line defined by [Coord]s `a` and `b` by `distance`. @@ -38,20 +38,47 @@ pub(super) fn offset_line_raw( a: Coord, b: Coord, distance: T, -) -> Option> +) -> Option> where T: CoordFloat, { let ab = b - a; - let ab_len = ab.magnitude(); - if ab_len == T::zero() { + let length = ab.magnitude(); + if length == T::zero() { return None; } - let ab_offset = ab.left() / ab_len * distance; + let ab_offset = ab.left() / length * distance; - Some(OffsetLineRawResult { - a_offset: a + ab_offset, - b_offset: b + ab_offset, - ab_len, + Some(LineMeasured { + line: Line{ + start:a + ab_offset, + end: b + ab_offset + }, + length, }) } + + +// TODO: test + +#[cfg(test)] +mod test { + + use crate::{ + Coord, + Line, + offset_curve::{offset_line_raw::offset_line_raw, line_measured::LineMeasured}, + }; + + #[test] + fn test_offset_line_raw() { + let a = Coord { x: 0f64, y: 0f64 }; + let b = Coord { x: 0f64, y: 1f64 }; + let output_actual = offset_line_raw(a, b, 1f64); + let output_expected = Some(LineMeasured{ + line:Line { start: Coord { x: 1f64, y: 0f64 }, end: Coord { x: 1f64, y: 1f64 } }, + length:1f64, + }); + assert_eq!(output_actual, output_expected); + } +} \ No newline at end of file diff --git a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs index f675e62b4..c0a711faa 100644 --- a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs +++ b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs @@ -19,10 +19,13 @@ /// use crate::{Coord, CoordFloat, CoordNum, LineString}; -use super::line_intersection::{ - line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, +use super::{ + line_intersection::{ + line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, + }, + line_measured::LineMeasured, + offset_line_raw::offset_line_raw, }; -use super::offset_line_raw::{offset_line_raw, OffsetLineRawResult}; /// Bring this into scope to imbue [LineString] with /// [LineStringOffsetSegmentPairIterable::iter_offset_segment_pairs()] @@ -32,7 +35,7 @@ where { /// Loop over the segments of a [LineString] in a pairwise fashion, /// offsetting and intersecting them as we go. - /// + /// /// Returns an [OffsetSegmentsIterator] fn iter_offset_segment_pairs(&self, distance: T) -> OffsetSegmentsIterator; } @@ -43,7 +46,7 @@ where { line_string: &'a LineString, distance: T, - previous_offset_segment: Option>, + previous_offset_segment: Option>, index: usize, } @@ -81,36 +84,38 @@ where /// /// The following diagram illustrates the meaning of the struct members. -/// The `LineString` `abc` is offset to form the `Line`s `mn` and `op`. -/// `i` is the intersection point. +/// +/// - `LineString` `a---b---c` is offset to form +/// - [LineMeasured] `ab_offset` (`a'---b'`) and +/// - [LineMeasured] `bc_offset` (`b'---c'`) +/// - [LineIntersectionResultWithRelationships] `i` is the intersection point. /// /// ```text /// a -/// m \ +/// a' \ /// \ \ /// \ b---------c -/// n +/// b' /// -/// i o---------p +/// i b'--------c' /// ``` -#[derive(Clone)] +#[derive(Clone, Debug)] pub(super) struct OffsetSegmentsIteratorItem where T: CoordNum, { + /// This is true for the first result + pub first: bool, + + // this is true for the last result + pub last: bool, + pub a: Coord, pub b: Coord, pub c: Coord, - pub m: Coord, - pub n: Coord, - pub o: Coord, - pub p: Coord, - - /// Distance between `a` and `b` (same as distance between `m` and `n`) - pub ab_len: T, - /// Distance between `b` and `c` (same as distance between `o` and `p`) - pub bc_len: T, + pub ab_offset: Option>, + pub bc_offset: Option>, /// Intersection [Coord] between segments `mn` and `op` pub i: Option>, @@ -121,17 +126,19 @@ where T: CoordFloat, { /// Option since each step of the iteration may fail. - type Item = Option>; + type Item = OffsetSegmentsIteratorItem; /// Return type is confusing; `Option>>` - /// + /// + /// TODO: Revise + /// /// The outer Option is required by the Iterator trait, and indicates if /// iteration is finished, (When this iterator is used via `.map()` or /// similar the user does not see the outer Option.) /// The inner Option indicates if the result of each iteration is valid. /// Returning None will halt iteration, returning Some(None) will not, /// but the user should stop iterating. - /// + /// fn next(&mut self) -> Option { if self.index + 3 > self.line_string.0.len() { // Iteration is complete @@ -146,38 +153,31 @@ where self.index += 1; // Fetch previous offset segment - let Some(OffsetLineRawResult{ - a_offset:m, - b_offset:n, - ab_len, - }) = self.previous_offset_segment else { - return None - }; + let ab_offset = self.previous_offset_segment.clone(); // Compute next offset segment self.previous_offset_segment = offset_line_raw(b, c, self.distance); - let Some(OffsetLineRawResult{ - a_offset:o, - b_offset:p, - ab_len:bc_len, - }) = self.previous_offset_segment else { - return Some(None); - }; - - Some(Some( - OffsetSegmentsIteratorItem { - a, - b, - c, - m, // TODO < replace mnop and ab_len and bc_len with two optional OffsetLineRawResult and remove the Option form Self::Item - n,# - o, - p, - ab_len, - bc_len, - i:line_segment_intersection_with_relationships(&m, &n, &o, &p), - } - )) + + Some(OffsetSegmentsIteratorItem { + first: self.index == 1, + last: self.index + 3 > self.line_string.0.len(), + a, + b, + c, + i: match (&ab_offset, &self.previous_offset_segment) { + (Some(ab_offset), Some(bc_offset)) => { + line_segment_intersection_with_relationships( + ab_offset.line.start, + ab_offset.line.end, + bc_offset.line.start, + bc_offset.line.end, + ) + } + _ => None, + }, + ab_offset, + bc_offset: self.previous_offset_segment.clone(), + }) } } } @@ -186,7 +186,10 @@ where mod test { use super::{LineStringOffsetSegmentPairs, OffsetSegmentsIteratorItem}; use crate::{ - line_string, offset_curve::line_intersection::LineIntersectionResultWithRelationships, + line_string, + offset_curve::{ + line_intersection::LineIntersectionResultWithRelationships, line_measured::LineMeasured, + }, Coord, }; @@ -198,32 +201,24 @@ mod test { Coord { x: 2f64, y: 1f64 }, ]; + // TODO: this test is a bit useless after recent changes let result: Option> = input .iter_offset_segment_pairs(1f64) .map(|item| match item { - Some(OffsetSegmentsIteratorItem { - a, - b, - c, - - m, - n, - o, - p, - - ab_len, - bc_len, - - i: - Some(LineIntersectionResultWithRelationships { - ab, - cd, - intersection, + OffsetSegmentsIteratorItem { + ab_offset: Some(LineMeasured { .. }), + bc_offset: + Some(LineMeasured { + line: bc_offset, + length: bc_len, }), - }) => Some(()), + + i: Some(LineIntersectionResultWithRelationships { .. }), + .. + } => Some(()), _ => None, }) .collect(); - assert!(result.unwrap().len()==1); + assert!(result.unwrap().len() == 1); } } From 850c50d5cb697876ee3023994cfc17c72e25b6b9 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 22/27] remove old offset_curve approach --- geo/src/algorithm/offset_curve/mod.rs | 5 - .../offset_curve/offset_curve_trait_old.rs | 319 ------------------ .../algorithm/offset_curve/slice_itertools.rs | 101 ------ 3 files changed, 425 deletions(-) delete mode 100644 geo/src/algorithm/offset_curve/offset_curve_trait_old.rs delete mode 100644 geo/src/algorithm/offset_curve/slice_itertools.rs diff --git a/geo/src/algorithm/offset_curve/mod.rs b/geo/src/algorithm/offset_curve/mod.rs index eacce8a57..b5662e925 100644 --- a/geo/src/algorithm/offset_curve/mod.rs +++ b/geo/src/algorithm/offset_curve/mod.rs @@ -1,14 +1,9 @@ mod vector_extensions; -mod slice_itertools; mod line_intersection; mod offset_segments_iterator; mod offset_line_raw; mod line_measured; -// Kept temporarily during transition to new approach -mod offset_curve_trait_old; -use offset_curve_trait_old::OffsetCurveOld; - mod offset_curve_trait; pub use offset_curve_trait::OffsetCurve; diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs b/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs deleted file mode 100644 index b522d3c3a..000000000 --- a/geo/src/algorithm/offset_curve/offset_curve_trait_old.rs +++ /dev/null @@ -1,319 +0,0 @@ -use super::line_intersection::FalseIntersectionPointType::AfterEnd; -use super::line_intersection::LineSegmentIntersectionType::{ - FalseIntersectionPoint, TrueIntersectionPoint, -}; - -use super::vector_extensions::VectorExtensions; - -use super::line_intersection::{ - line_segment_intersection_with_relationships, LineIntersectionResultWithRelationships, -}; - -use super::offset_line_raw::{offset_line_raw}; -use super::line_measured::LineMeasured; -use super::slice_itertools::pairwise; - -// TODO: Should I be doing `use crate ::{...}` or `use geo_types::{...}` -use crate::{Coord, CoordFloat, Line, LineString, MultiLineString}; - -/// # Offset Trait -/// -/// The OffsetCurve trait is implemented for geometries where the edges of the -/// geometry can be offset perpendicular to the direction of the edges by some -/// positive or negative distance. For example, an offset [Line] will become a -/// [Line], and an offset [LineString] will become a [LineString]. -/// Geometry with no length ([geo_types::Point]) cannot be offset as it has no -/// directionality. -/// -/// The [OffsetCurve::offset()] function is different to a `buffer` operation. -/// A buffer (or inset / outset operation) would normally produce an enclosed -/// shape; For example a [geo_types::Point] would become a circular -/// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped -/// [geo_types::Polygon]. - -#[deprecated] -pub trait OffsetCurveOld -where - T: CoordFloat, - Self: Sized, -{ - /// Offset the edges of the geometry by `distance`. - /// - /// In a coordinate system where positive is up and to the right; - /// when facing the direction of increasing coordinate index: - /// - /// - Positive `distance` will offset the edges of a geometry to the left - /// - Negative `distance` will offset the edges of a geometry to the right - /// - /// If you are using 'screen coordinates' where the y axis is often flipped - /// then the offset direction described above will be reversed. - /// - /// # Examples - /// - /// ``` - /// #use crate::{line_string, Coord}; - /// let input = line_string![ - /// Coord { x: 0f64, y: 0f64 }, - /// Coord { x: 0f64, y: 2f64 }, - /// Coord { x: 2f64, y: 2f64 }, - /// ]; - /// let output_expected = line_string![ - /// Coord { x: 1f64, y: 0f64 }, - /// Coord { x: 1f64, y: 1f64 }, - /// Coord { x: 2f64, y: 1f64 }, - /// ]; - /// let output_actual = input.offset_curve(-1f64).unwrap(); - /// assert_eq!(output_actual, output_expected); - /// ``` - fn offset_curve_old(&self, distance: T) -> Option; -} - -impl OffsetCurveOld for Line -where - T: CoordFloat, -{ - fn offset_curve_old(&self, distance: T) -> Option { - if distance == T::zero() { - // prevent unnecessary work - Some(self.clone()) - } else { - let Line { start: a, end: b } = *self; - match offset_line_raw(a, b, distance) { - Some(LineMeasured { - line, .. - }) => Some(line), - _ => None, - } - } - } -} - -impl OffsetCurveOld for LineString -where - T: CoordFloat, -{ - fn offset_curve_old(&self, distance: T) -> Option { - // Loosely follows the algorithm described by - // [Xu-Zheng Liu, Jun-Hai Yong, Guo-Qin Zheng, Jia-Guang Sun. An offset - // algorithm for polyline curves. Computers in Industry, Elsevier, 2007, - // 15p. inria-00518005] - // (https://hal.inria.fr/inria-00518005/document) - - // Handle trivial cases; - // Note: Docs say LineString is valid "if it is either empty or contains - // two or more coordinates" - - // TODO: is `self.into_inner()` rather than `self.0` preferred? The - // contents of the tuple struct are public. - // Issue #816 seems to suggest that `self.0` is to be deprecated - match self.0.len() { - 0 => return Some(self.clone()), - 1 => return None, - 2 => { - return match Line::new(self.0[0], self.0[1]).offset_curve_old(distance) { - Some(line) => Some(line.into()), - None => None, - } - } - _ => (), - } - - // Prevent unnecessary work: - if T::is_zero(&distance) { - return Some(self.clone()); - } - - // TODO: I feel like offset_segments should be lazily computed as part - // of the main iterator below if possible; - // - so we don't need to keep all this in memory at once - // - and so that if we have to bail out later we didn't do all - // this work for nothing - // However I haven't been able to get a nice lazy pairwise - // iterator working.. I suspect it requires unsafe code :/ - let offset_segments: Vec> = match self - .lines() - .map(|item| item.offset_curve_old(distance)) - .collect() - { - Some(a) => a, - _ => return None, // bail out if any segment fails - }; - - if offset_segments.len() == 1 { - return Some(offset_segments[0].into()); - } - // First and last will always work, checked length above: - // TODO: try to eliminate unwrap anyway? - let first_point = offset_segments.first().unwrap().start; - let last_point = offset_segments.last().unwrap().end; - - let mut result = Vec::with_capacity(self.0.len()); - result.push(first_point); - result.extend(pairwise(&offset_segments[..]).flat_map( - |(Line { start: a, end: b }, Line { start: c, end: d })| { - match line_segment_intersection_with_relationships(*a, *b, *c, *d) { - None => { - // TODO: this is the colinear case; - // (In some cases?) this creates a redundant point in the - // output. Colinear segments should maybe get - // merged before or after this algorithm. Not easy - // to fix here. - //println!("CASE 0 - colinear"); - vec![*b] - } - Some(LineIntersectionResultWithRelationships { - ab, - cd, - intersection, - }) => match (ab, cd) { - (TrueIntersectionPoint, TrueIntersectionPoint) => { - // Inside elbow - // No mitre limit needed - vec![intersection] - } - (FalseIntersectionPoint(AfterEnd), FalseIntersectionPoint(_)) => { - // Outside elbow - // Check for Mitre Limit - // TODO: Mitre limit code below is awful; - // - Some values calculated here were - // previously calculated in - // [line_segment_intersection_with_parameters()] - // - Various optimizations are possible; - // Check against magnitude squared - // - Magnitude function to be moved somewhere - // else - // - let mitre_limit_factor = T::from(2.0).unwrap(); - let mitre_limit_distance = distance.abs() * mitre_limit_factor; - let elbow_length = (intersection - *b).magnitude(); - if elbow_length > mitre_limit_distance { - // Mitre Limited / Truncated Corner - let ab: Coord = *b - *a; - let cd: Coord = *d - *c; - vec![ - *b + ab / ab.magnitude() * mitre_limit_distance, - *c - cd / cd.magnitude() * mitre_limit_distance, - ] - } else { - // Sharp Corner - vec![intersection] - } - } - - _ => { - // Inside pinched elbow - // (ie forearm curled back through bicep 🙃) - //println!("CASE 3 - bridge"); - vec![*b, *c] - } - }, - } - }, - )); - result.push(last_point); - // TODO: there are more steps to this algorithm which are not yet - // implemented. See rfcs\2022-11-11-offset.md - Some(result.into()) - } -} - -impl OffsetCurveOld for MultiLineString -where - T: CoordFloat, -{ - fn offset_curve_old(&self, distance: T) -> Option { - self.iter() - .map(|item| item.offset_curve_old(distance)) - .collect() - } -} - -#[cfg(test)] -mod test { - - use crate::{ - line_string, - Coord, - Line, - //LineString, - MultiLineString, - }; - use super::{ - OffsetCurveOld - }; - - #[test] - fn test_offset_line() { - let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let output_actual = input.offset_curve_old(-1.0); - let output_expected = Some(Line::new( - Coord { x: 2f64, y: 1f64 }, - Coord { x: 2f64, y: 2f64 }, - )); - assert_eq!(output_actual, output_expected); - } - #[test] - fn test_offset_line_negative() { - let input = Line::new(Coord { x: 1f64, y: 1f64 }, Coord { x: 1f64, y: 2f64 }); - let output_actual = input.offset_curve_old(1.0); - let output_expected = Some(Line::new( - Coord { x: 0f64, y: 1f64 }, - Coord { x: 0f64, y: 2f64 }, - )); - assert_eq!(output_actual, output_expected); - } - - #[test] - fn test_offset_line_string() { - let input = line_string![ - Coord { x: 0f64, y: 0f64 }, - Coord { x: 0f64, y: 2f64 }, - Coord { x: 2f64, y: 2f64 }, - ]; - let output_actual = input.offset_curve_old(-1f64); - let output_expected = Some(line_string![ - Coord { x: 1f64, y: 0f64 }, - Coord { x: 1f64, y: 1f64 }, - Coord { x: 2f64, y: 1f64 }, - ]); - assert_eq!(output_actual, output_expected); - } - - #[test] - fn test_offset_line_string_invalid() { - let input = line_string![Coord { x: 0f64, y: 0f64 },]; - let output_actual = input.offset_curve_old(-1f64); - let output_expected = None; - assert_eq!(output_actual, output_expected); - } - - #[test] - fn test_offset_multi_line_string() { - let input = MultiLineString::new(vec![ - line_string![ - Coord { x: 0f64, y: 0f64 }, - Coord { x: 0f64, y: 2f64 }, - Coord { x: 2f64, y: 2f64 }, - ], - line_string![ - Coord { x: 0f64, y: 0f64 }, - Coord { x: 0f64, y: -2f64 }, - Coord { x: -2f64, y: -2f64 }, - ], - ]); - let output_actual = input.offset_curve_old(-1f64); - let output_expected = Some(MultiLineString::new(vec![ - line_string![ - Coord { x: 1f64, y: 0f64 }, - Coord { x: 1f64, y: 1f64 }, - Coord { x: 2f64, y: 1f64 }, - ], - line_string![ - Coord { x: -1f64, y: 0f64 }, - Coord { x: -1f64, y: -1f64 }, - Coord { x: -2f64, y: -1f64 }, - ], - ])); - assert_eq!(output_actual, output_expected); - } -} diff --git a/geo/src/algorithm/offset_curve/slice_itertools.rs b/geo/src/algorithm/offset_curve/slice_itertools.rs deleted file mode 100644 index 2ff7718e0..000000000 --- a/geo/src/algorithm/offset_curve/slice_itertools.rs +++ /dev/null @@ -1,101 +0,0 @@ -/// Iterate over a slice in overlapping pairs -/// -/// # Examples -/// -/// ```ignore -/// # use crate::offset::slice_itertools::pairwise; -/// let input = vec![1, 2, 3, 4, 5]; -/// let output_actual: Vec<(i32, i32)> = -/// pairwise(&input[..]).map(|(a, b)| (*a, *b)).collect(); -/// let output_expected = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; -/// assert_eq!( -/// output_actual, -/// output_expected -/// ) -/// ``` -/// -/// # Note -/// -/// We already have [std::slice::windows()] but the problem is it returns a -/// slice, not a tuple; and therefore it is not easy to unpack the result since -/// a slice cannot be used as an irrefutable pattern. For example, the `.map()` -/// in the following snippet creates a compiler error something like `Refutable -/// pattern in function argument; options &[_] and &[_,_,..] are not covered.` -/// -/// ```ignore -/// let some_vector:Vec = vec![1,2,3]; -/// let some_slice:&[i64] = &some_vector[..]; -/// let some_result:Vec = some_slice -/// .windows(2) -/// .map(|&[a, b]| a + b) // <-- error -/// .collect(); -/// ``` -/// -pub(super) fn pairwise( - slice: &[T], -) -> std::iter::Zip, std::slice::Iter> { - match slice.split_first() { - None=>{ - // The following nonsense is needed because std::iter::empty returns - // a new and therefore incompatible type which is unexpected and - // super annoying. I think empty iterators need special treatment by - // the rust compiler in future to fix this. Stack Overflow - // recommends a boxed iterator trait object to avoid this problem, - // but then there are unnecessary heap allocations? Not sure if the - // current method uses the heap anyway? - [].iter().zip([].iter()) - } - Some((_, rest)) => slice.iter().zip(rest.iter()) - } -} - -/// Iterate over a slice and repeat the first item at the end -/// -/// ```ignore -/// let items = vec![1, 2, 3, 4, 5]; -/// let actual_result: Vec = wrap_one(&items[..]).cloned().collect(); -/// let expected_result = vec![1, 2, 3, 4, 5, 1]; -/// ``` -pub(super) fn wrap_one( - slice: &[T], -) -> std::iter::Chain, std::slice::Iter> { - slice.iter().chain(slice[..1].iter()) - //.chain::<&T>(std::iter::once(slice[0])) -} - -#[cfg(test)] -mod test { - use super::{pairwise, wrap_one}; - - #[test] - fn test_pairwise() { - let items = vec![1, 2, 3, 4, 5]; - let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); - let expected_result = vec![(1, 2), (2, 3), (3, 4), (4, 5)]; - assert_eq!(actual_result, expected_result); - } - - #[test] - fn test_pairwise_one_element() { - let items = vec![1]; - let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); - let expected_result = vec![]; - assert_eq!(actual_result, expected_result); - } - - #[test] - fn test_pairwise_zero_elements() { - let items = vec![]; - let actual_result: Vec<(i32, i32)> = pairwise(&items[..]).map(|(a, b)| (*a, *b)).collect(); - let expected_result = vec![]; - assert_eq!(actual_result, expected_result); - } - - #[test] - fn test_wrap() { - let items = vec![1, 2, 3, 4, 5]; - let actual_result: Vec = wrap_one(&items[..]).cloned().collect(); - let expected_result = vec![1, 2, 3, 4, 5, 1]; - assert_eq!(actual_result, expected_result); - } -} From ddc2f1de73963cb4214fa30ea4820e7d9ed9a4c6 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 23/27] improve documentation and testing --- .../offset_curve/vector_extensions.rs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/geo/src/algorithm/offset_curve/vector_extensions.rs b/geo/src/algorithm/offset_curve/vector_extensions.rs index 600641558..0ea2d1631 100644 --- a/geo/src/algorithm/offset_curve/vector_extensions.rs +++ b/geo/src/algorithm/offset_curve/vector_extensions.rs @@ -1,6 +1,6 @@ use crate::{Coord, CoordFloat}; -/// Extends the `Coord` struct with more vector operations; +/// Extends the `Coord` struct with some common vector operations; /// /// - [VectorExtensions::cross_product_2d], /// - [VectorExtensions::magnitude], @@ -200,7 +200,7 @@ mod test { // Parallel, same direction let a = Coord { x: 1f64, y: 0f64 }; let b = Coord { x: 2f64, y: 0f64 }; - // expect + product of magnitudes + // expect +ive product of magnitudes assert_eq!(a.dot_product(b), 2f64); // expect swapping will have same result assert_eq!(b.dot_product(a), 2f64); @@ -208,7 +208,7 @@ mod test { // Parallel, opposite direction let a = Coord { x: 3f64, y: 4f64 }; let b = Coord { x: -3f64, y: -4f64 }; - // expect - product of magnitudes + // expect -ive product of magnitudes assert_eq!(a.dot_product(b), -25f64); // expect swapping will have same result assert_eq!(b.dot_product(a), -25f64); @@ -246,28 +246,39 @@ mod test { assert_eq!(a.left(), a_left); assert_eq!(a.right(), a_right); + assert_eq!(a.left(), -a.right()); } #[test] fn test_left_right_match_rotate() { use crate::algorithm::rotate::Rotate; use crate::Point; + // The aim of this test is to confirm that wording in documentation is + // consistent. + + // when the user is in a coordinate system where the y axis is flipped + // (eg screen coordinates in a HTML canvas), then rotation directions + // will be different to those described in the documentation. + // the documentation for the Rotate trait says: 'Positive angles are // counter-clockwise, and negative angles are clockwise rotations' - // left is anti-clockwise and right is clockwise: check that the results - // match: + + let counter_clockwise_rotation_degrees = 90.0; + let clockwise_rotation_degrees = -counter_clockwise_rotation_degrees; - let a: Point = Coord { x: 1f64, y: 0f64 }.into(); + let a: Point = Coord { x: 1.0, y: 0.0 }.into(); + let origin:Point = Coord::::zero().into(); + // left is anti-clockwise assert_relative_eq!( - a.0.right(), - a.rotate_around_point(-90.0, Coord { x: 0.0, y: 0.0 }.into()) - .0 + Point::from(a.0.left()), + a.rotate_around_point(counter_clockwise_rotation_degrees, origin), ); + // right is clockwise assert_relative_eq!( - a.0.left(), - a.rotate_around_point(90.0, Coord { x: 0.0, y: 0.0 }.into()) - .0 + Point::from(a.0.right()), + a.rotate_around_point(clockwise_rotation_degrees, origin), ); + } } From a32bd992b4b5065f4ffae5d0807bd60036c82e04 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 24/27] improve docs for vector_extensions --- geo/src/algorithm/offset_curve/vector_extensions.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/geo/src/algorithm/offset_curve/vector_extensions.rs b/geo/src/algorithm/offset_curve/vector_extensions.rs index 0ea2d1631..f50b9bbcd 100644 --- a/geo/src/algorithm/offset_curve/vector_extensions.rs +++ b/geo/src/algorithm/offset_curve/vector_extensions.rs @@ -51,8 +51,8 @@ where /// - The sign of the output is reversed if the operands are reversed /// - The sign can be used to check if the operands are clockwise / /// anti-clockwise orientation with respect to the origin; - /// or phrased differently "is b to the left or right of the line between - /// the origin and a"? + /// or phrased differently: + /// "is b to the left of the line between the origin and a"? /// - If the operands are colinear with the origin, the magnitude is zero /// /// @@ -260,7 +260,7 @@ mod test { // (eg screen coordinates in a HTML canvas), then rotation directions // will be different to those described in the documentation. - // the documentation for the Rotate trait says: 'Positive angles are + // The documentation for the Rotate trait says: 'Positive angles are // counter-clockwise, and negative angles are clockwise rotations' let counter_clockwise_rotation_degrees = 90.0; From 754f2e9b99a770e8e1584613a0fcc3b7c66c7f8e Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:40 +0800 Subject: [PATCH 25/27] improve docs for offset curve trait --- .../offset_curve/offset_curve_trait.rs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/geo/src/algorithm/offset_curve/offset_curve_trait.rs b/geo/src/algorithm/offset_curve/offset_curve_trait.rs index cd4deb8a6..d7e87a188 100644 --- a/geo/src/algorithm/offset_curve/offset_curve_trait.rs +++ b/geo/src/algorithm/offset_curve/offset_curve_trait.rs @@ -15,8 +15,7 @@ use super::offset_line_raw::offset_line_raw; use super::offset_segments_iterator::{LineStringOffsetSegmentPairs, OffsetSegmentsIteratorItem}; -/// # Offset Trait -/// + /// The OffsetCurve trait is implemented for geometries where the edges of the /// geometry can be offset perpendicular to the direction of the edges by some /// positive or negative distance. For example, an offset [Line] will become a @@ -24,11 +23,11 @@ use super::offset_segments_iterator::{LineStringOffsetSegmentPairs, OffsetSegmen /// Geometry with no length ([geo_types::Point]) cannot be offset as it has no /// directionality. /// -/// The [OffsetCurve::offset()] function is different to a `buffer` operation. -/// A buffer (or inset / outset operation) would normally produce an enclosed -/// shape; For example a [geo_types::Point] would become a circular -/// [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped -/// [geo_types::Polygon]. +/// > NOTE: The [OffsetCurve::offset_curve()] function is different to a `buffer` operation. +/// > A buffer (or inset / outset operation) would normally produce an enclosed +/// > shape; For example a [geo_types::Point] would become a circular +/// > [geo_types::Polygon], a [geo_types::Line] would become a capsule shaped +/// > [geo_types::Polygon]. pub trait OffsetCurve where @@ -72,8 +71,12 @@ where { fn offset_curve(&self, distance: T) -> Option { if distance == T::zero() { - // prevent unnecessary work + // prevent unnecessary work; Some(self.clone()) + // TODO: for typical use cases the offset would rarely be zero; + // This check may add unnecessary branching when there are a lot of + // Lines. It makes more sense to do this performance check for + // LineStrings... } else { let Line { start: a, end: b } = *self; match offset_line_raw(a, b, distance) { @@ -119,7 +122,7 @@ where return Some(self.clone()); } - // TODO: Consider adding parameters for miter limit method and factor + // TODO: Parameterize miter limit, and miter limit distance / factor. let mitre_limit_factor = T::from(2.0).unwrap(); let mitre_limit_distance = distance.abs() * mitre_limit_factor; let mitre_limit_distance_squared = mitre_limit_distance * mitre_limit_distance; From 63409d3fa6af2c077639bde679b8a57373bf0da1 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:41 +0800 Subject: [PATCH 26/27] improve docs for offset_segments_iterator --- .../offset_curve/offset_segments_iterator.rs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs index c0a711faa..704a3cb5d 100644 --- a/geo/src/algorithm/offset_curve/offset_segments_iterator.rs +++ b/geo/src/algorithm/offset_curve/offset_segments_iterator.rs @@ -1,7 +1,3 @@ -/// I am trying to get a custom iterator working to replace the -/// [super::slice_itertools::pairwise()] function. -/// -/// It is turning out to be very complicated :( /// /// My requirements are /// @@ -28,7 +24,7 @@ use super::{ }; /// Bring this into scope to imbue [LineString] with -/// [LineStringOffsetSegmentPairIterable::iter_offset_segment_pairs()] +/// [iter_offset_segment_pairs()] pub(super) trait LineStringOffsetSegmentPairs where T: CoordFloat, @@ -82,10 +78,8 @@ where } } -/// -/// The following diagram illustrates the meaning of the struct members. -/// -/// - `LineString` `a---b---c` is offset to form + +/// - [LineString] `a---b---c` is offset to form /// - [LineMeasured] `ab_offset` (`a'---b'`) and /// - [LineMeasured] `bc_offset` (`b'---c'`) /// - [LineIntersectionResultWithRelationships] `i` is the intersection point. @@ -110,6 +104,7 @@ where // this is true for the last result pub last: bool, + // TODO: seems a,b,c are unused... pub a: Coord, pub b: Coord, pub c: Coord, @@ -207,12 +202,7 @@ mod test { .map(|item| match item { OffsetSegmentsIteratorItem { ab_offset: Some(LineMeasured { .. }), - bc_offset: - Some(LineMeasured { - line: bc_offset, - length: bc_len, - }), - + bc_offset: Some(LineMeasured { .. }), i: Some(LineIntersectionResultWithRelationships { .. }), .. } => Some(()), From d95420f1f8d6b0d2dd1e6d803fe3d3e2a2b3dd13 Mon Sep 17 00:00:00 2001 From: thehappycheese Date: Tue, 20 Jun 2023 23:47:41 +0800 Subject: [PATCH 27/27] improve docs for line_intersection enum --- .../offset_curve/line_intersection.rs | 66 ++++++++++++------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/geo/src/algorithm/offset_curve/line_intersection.rs b/geo/src/algorithm/offset_curve/line_intersection.rs index 1e75cb568..eaaebdaba 100644 --- a/geo/src/algorithm/offset_curve/line_intersection.rs +++ b/geo/src/algorithm/offset_curve/line_intersection.rs @@ -1,3 +1,21 @@ +/// Definitions used in documentation for this module; +/// +/// - **line**: +/// - The straight path on a plane that +/// - extends infinitely in both directions. +/// - defined by two distinct points `a` and `b` and +/// - has the direction of the vector from `a` to `b` +/// +/// - **segment**: +/// - A finite portion of a `line` which +/// - lies between the points `a` and `b` +/// - has the direction of the vector from `a` to `b` +/// +/// - **ray**: +/// - A segment which extends infinitely in the forward direction +/// +/// - `Line`: the type [crate::Line] which is actually a **segment**. + use super::vector_extensions::VectorExtensions; use crate::{ Coord, @@ -8,39 +26,42 @@ use crate::{ // Orientation }; -// No nested enums :( Goes into the enum below +/// Used to encode the relationship between a **segment** and an intersection +/// point. See documentation for [LineIntersectionResultWithRelationships] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(super) enum LineSegmentIntersectionType { + /// The intersection point lies between the start and end of the **segment** + /// + /// Abbreviated to `TIP` in original paper + TrueIntersectionPoint, + /// The intersection point is 'false' or 'virtual': it lies on the same + /// **line** as the **segment**, but not between the start and end points of + /// the **segment**. + /// + /// Abbreviated to `FIP` in original paper + + // Note: Rust does not permit nested enum declaration, so + // FalseIntersectionPointType has to be declared below. + FalseIntersectionPoint(FalseIntersectionPointType), +} + +/// These are the variants of [LineSegmentIntersectionType::FalseIntersectionPoint] #[derive(PartialEq, Eq, Debug, Clone, Copy)] pub(super) enum FalseIntersectionPointType { - /// The intersection point is 'false' or 'virtual': it lies on the infinite - /// ray defined by the line segment, but before the start of the line segment. + /// The intersection point is 'false' or 'virtual': it lies on the same + /// **line** as the **segment**, and before the start of the **segment**. /// /// Abbreviated to `NFIP` in original paper (Negative) /// (also referred to as `FFIP` in Figure 6, but i think this is an /// error?) BeforeStart, - /// The intersection point is 'false' or 'virtual': it lies on the infinite - /// ray defined by the line segment, but after the end of the line segment. + /// The intersection point is 'false' or 'virtual': it lies on the same + /// **line** as the **segment**, and after the end of the **segment**. /// /// Abbreviated to `PFIP` in original paper (Positive) AfterEnd, } -/// Used to encode the relationship between a segment (e.g. between [Coord] `a` and `b`) -/// and an intersection point ([Coord] `p`) -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub(super) enum LineSegmentIntersectionType { - /// The intersection point lies between the start and end of the line segment. - /// - /// Abbreviated to `TIP` in original paper - TrueIntersectionPoint, - /// The intersection point is 'false' or 'virtual': it lies on the infinite - /// ray defined by the line segment, but not between the start and end points - /// - /// Abbreviated to `FIP` in original paper - FalseIntersectionPoint(FalseIntersectionPointType), -} - - /// Struct to contain the result for [line_segment_intersection_with_relationships] #[derive(Clone, Debug)] @@ -169,7 +190,8 @@ where } // TODO: - // The above could be replaced with the following. + // The above could be replaced with the following, but at the cost of + // repeating some computation. // match RobustKernel::orient2d(*a, *b, *d) { // Orientation::Collinear => None,