Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Robust Intersect and Contains #511

Merged
merged 14 commits into from
Sep 20, 2020
Merged

Conversation

rmanoka
Copy link
Contributor

@rmanoka rmanoka commented Sep 13, 2020

Use robust predicates in Intersects and Contains traits

  • use robust preds (doesn't need Float anymore)
  • define interior, boundary, and exterior of geometry types (similar to geos/jts)
  • define Intersect and Contains using DE-9IM model
  • fix contains logic in a few corner cases

Future work:

The polygon contains line / polygon logic is not yet fully correct. I've added a few TODO comments, and an ignored test for cases where our predicate doesn't match the DE-9IM definition. Unfortunately, it doesn't seem to be a simple change to fix these corner cases. For instance, geos seems to use a concept called GeometryGraph for these cases which I'm yet to understand.

+ restructure: `intersects/mod.rs` and `contains/mod.rs`
+ robust intersection
+ macro for symmetric intersection implementations
+ robust contains
Fix some tests
@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 13, 2020

A couple of clarifications needed:

  1. These two tests seem inconsistent with definition of Contains which says it does not include the boundary.

    fn triangle_contains_point_on_edge() {

    fn triangle_contains_point_on_vertex() {

  2. Implementation of Rect: Contains<Point> also seems to allow the point to be on the edges. Again, not consistent with definition, and implementation on Polygon.

Also checked against shapely/geos which seems to concur with our definition (so points on edges / vertices of the polygon are not considered to be "contained"). Shall we go with the definition (so fix Rect and Triangle)?

@michaelkirk @urschrei

@michaelkirk
Copy link
Member

michaelkirk commented Sep 15, 2020

Shall we go with the definition (so fix Rect and Triangle)?

Yes. Apparently I wrote that broken triangle implementation 😞: b3944ca

For my own benefit, I've just now referred to http://portal.opengeospatial.org/files/?artifact_id=25355 (thanks @urschrei 😉)

Contains
a.Contains(b) ⇔ b.Within(a)

So it's defined in terms of within, which itself is defined as:

Within
The Within relationship is defined as
a.Within(b) ⇔ (a∩b=a) ∧ (I(a)∩E(b)=∅)
Expressed in terms of the DE-9IM:

a.Within(b) ⇔ [ I(a)∩I(b)≠∅ ∧ I(a)∩E(b)=∅ ∧ B(a)∩E(b)=∅ ]
    ⇔ a.Relate(b, “T*F**F***”) 

And from the jts impl:

contains
...
[T*****FF*]

So for a.contains(b), boundary contact is not sufficient; The interior of b must reside (wholly) within the interior of a.

In the future, I'll be careful to be rigorous about the cases represented in DE-9IM.

@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 16, 2020

Yeah, DE-9IM based rigorous definitions of our geometry types, and traits definitely seems useful. I believe jts, geos (hence postgis), shapely all use the same definition of Intersects, and Contains so makes sense for us to with the same for interop with the community. Will add some doc to the types trying to formalize the definitions.

Aside: why didn't they just name it denim? Possible trademark issues? I'm so tempted to use it as it is so much simpler to type (and fashionable)

+ define interior, boundary for geometry types
+ define `Intersects` and `Contains` as per DE-9IM
@rmanoka rmanoka force-pushed the fix/robust-intersects branch from c84a5ab to 98b3058 Compare September 17, 2020 05:05
@rmanoka rmanoka marked this pull request as ready for review September 17, 2020 05:12
Copy link
Member

@michaelkirk michaelkirk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have yet to review the Contains portion, but I ran out of time for now.

I have reviewed the Intersecs and other changes, and they look great! Just some optional feedback for clarity.

I'll finish reviewing up hopefully yet tonight, or if not, tomorrow.

geo-types/src/line_string.rs Show resolved Hide resolved
geo/src/algorithm/intersects/mod.rs Outdated Show resolved Hide resolved
geo/src/algorithm/intersects/mod.rs Show resolved Hide resolved
geo/src/algorithm/intersects/mod.rs Outdated Show resolved Hide resolved
geo/src/utils.rs Outdated Show resolved Hide resolved
geo/src/algorithm/intersects/polygon.rs Outdated Show resolved Hide resolved
geo/src/algorithm/intersects/polygon.rs Outdated Show resolved Hide resolved
geo-types/src/line.rs Outdated Show resolved Hide resolved
geo-types/src/line_string.rs Show resolved Hide resolved
geo/src/algorithm/contains/mod.rs Show resolved Hide resolved
rmanoka and others added 4 commits September 18, 2020 10:52
Co-authored-by: Corey Farwell <[email protected]>
+ remove unnecessary PhantomData in Kernel defn.
+ cargo fmt
@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 19, 2020

Thanks for the comments @frewsxcv @michaelkirk ; I have addressed them, also added a few lines in CHANGES.md to highlight the recent robust pred. changes. Will wait a couple more days to see there are any minor fixes we spot, and merge this PR.

closes #415

@@ -1,5 +1,10 @@
# Changes

## Master (not released)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thank you for doing this! Tracking as we go will make the eventual release easier.

{
fn contains(&self, coord: &Coordinate<T>) -> bool {
if self.start == self.end {
&self.start == coord
Copy link
Member

@michaelkirk michaelkirk Sep 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a surprising consequence.

Given a coordinate: (0,0)
And a line Line((0,0), (0,1))

The line does not contain the coordinate, since the coordinate is only on the boundary of the line, not in the line's interior.

This continues to be true as we shrink the line — the line Line((0,0), (0,0.5)) doesn't contain the coordinate.

...until the line is shortened all the way to degeneracy - Line((0,0), (0,0)) now contains the coordinate.

Is that right? 🤯

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is confusing, but that is the precise expected behavior. I cross verified with shapely (geos), and it does the same.

}

// TODO: ensure DE-9IM compliance: esp., when
// line.start and line.end is on the boundaries
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right as your TODO alludes, it seems like the current logic would fail for a case like:

image

Where the line touches, but does not pass, the interior.
Or if the line touches, but does not pass, the exterior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Also see the test line_in_polygon_edgecases in contains/mod.rs. We might be able to use the winding order trait and a few additions to contains to solve these cases, but wanted to attempt this in a different PR.

}
}

// TODO: also check interiors
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this one also looks wrong. I wouldn't want to include new broken implementations, but I believe you are just moving these, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, these implementations were just moved into this file. We should fix the two traits for de9im compliance in another pr

impl<G, T> Contains<G> for MultiPolygon<T>
where
T: CoordinateType,
Polygon<T>: Contains<G>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we're newly accepting any type which implements the Contains trait, whereas before MultiPolygon only supported Coordinate and Point, is that right?

Does this existing implementation work for types with more dimensionality than a point?

e.g. If the two squares are part of a single multipolygon, even though the black line is contained by neither square individually, is it contained by the multipolygon?

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, MultiPolygon should not contain overlapping constituent polys:

As per the OGC SFS specification, the Polygons in a MultiPolygon may not overlap, and may only touch at single points. This allows the topological point-set semantics to be well-defined.

With the above defn., I think we can lift any Polygon: Contains to work on MultiPolygon

{
fn contains(&self, bounding_rect: &Rect<T>) -> bool {
// All points of LineString must be in the polygon ?
self.min().x <= bounding_rect.min().x
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So a degenerate Rect which is either a Line or a Point will be contained if it's on the boundary - I don't think that's correct, is it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! The previous implementation was the same with strict checking, which would also miss some edge cases. Want to address these in a different PR (needs a few extra impls, utils). Let me add a todo comment here so we get back to it.

fn contains(&self, coord: &Coordinate<T>) -> bool {
let ls = LineString(vec![self.0, self.1, self.2, self.0]);
use utils::*;
coord_pos_relative_to_ring(*coord, &ls) == CoordPos::Inside
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing this!

@michaelkirk
Copy link
Member

michaelkirk commented Sep 20, 2020 via email

@michaelkirk
Copy link
Member

michaelkirk commented Sep 20, 2020 via email

@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 20, 2020

Ah, I didn’t realize polygons in a multi-polygon couldn’t overlap. Thank you for explaining.

Actually, you're right. The polygons can touch at a single point which introduces some edge cases. For eg. two rectangles touching at a single point, and a line passing through that point. It won't be in any of the individual polygons but is contained in the multipolygon.

Will leave a comment, and we can get to this when we ensure correctness of these two traits.

+ update CHANGES.md
+ add TODO comments for DE-9IM compliance
@rmanoka rmanoka force-pushed the fix/robust-intersects branch from 719131e to b93fd10 Compare September 20, 2020 08:00
@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 20, 2020

bors r=michaelkirk

@rmanoka
Copy link
Contributor Author

rmanoka commented Sep 20, 2020

bors r=michaelkirk

@bors
Copy link
Contributor

bors bot commented Sep 20, 2020

Build succeeded:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants