-
Couldn't load subscription status.
- Fork 226
Add ContainsProperly relate predicate + custom polygon operation #1427
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
base: main
Are you sure you want to change the base?
Conversation
Concept behind Implementation for PolygonTLDR:
Expanding to MultiPolygon
Fast checking operation
BenchmarkComplex inputComplex Polygon (Louisiana) Simple input~ 20x of prepared Relate Polygon x Polygon MultiPolygon x MultiPolygon However, it has a +700% runtime vs |
f6714a6 to
ab76a60
Compare
e1a4c6d to
be07f89
Compare
48b9c91 to
9d0ade9
Compare
- better perf on large data
9d0ade9 to
e0c7303
Compare
|
Louisiana bench improved to 264.08 µs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the slow review - I was out for a couple weeks.
In general, I'm a little leery of introducing complex machinery that is redundant with our existing code, but you've done an excellent job of commenting and testing edge cases, and the performance wins (at least in some case) are very real.
| @@ -0,0 +1,279 @@ | |||
| /// Checks if `rhs` is completely contained within `self`. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// Checks if `rhs` is completely contained within `self`. | |
| /// Checks if `rhs` is completely contained within the interior of `self`. |
"contained" and "within" already have meanings slightly at odds with this, so let's be painfully clear.
| T: GeoNum, | ||
| { | ||
| fn contains_properly(&self, rhs: &Point<T>) -> bool { | ||
| if self.is_empty() || rhs.is_empty() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
geo points can't be empty.
| return false; | ||
| } | ||
|
|
||
| rhs.coords_iter().all(|p| self.contains_properly(&p)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably not a big deal, but you'll be rechecking if polygon.is_empty every time through this loop.
edit to be clear: I think it's fine to leave it. It's a cheap check.
| @@ -0,0 +1 @@ | |||
| // use super::impl_contains_properly_from_relate; | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see a couple spots in the PR where you've commented out the Coordinate functionality. Can you clarify what your intention is for having this commented out code?
| if boundary_intersects::<T, Polygon<T>, Polygon<T>>(self, rhs) { | ||
| return false; | ||
| } | ||
| // established that pairwise relation betwwen any two rings is either concentric or disjoint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // established that pairwise relation betwwen any two rings is either concentric or disjoint | |
| // established that pairwise relation between any two rings is either concentric or disjoint |
| return false; | ||
| } | ||
|
|
||
| // let Some(lhs_ext_coord) = self_poly.exterior().0.first() else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the idea with this commented out code?
|
|
||
| // hole outside of RHS does not affect intersection | ||
| if coord_pos_relative_to_ring(*self_hole_first_coord, rhs_poly.exterior()) | ||
| != CoordPos::Inside |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly for my own understanding:
Since we've verified the boundaries are disjoint, we know CoordPos::OnBoundary is impossible. So this is equally written:
if coord_pos_relative_to_ring(*self_hole_first_coord, rhs_poly.exterior()) == CoordPos::Outside {
}
| let donut_inside: Polygon<f64> = | ||
| wkt! {POLYGON((80 10,80 80,10 80,10 10,80 10),(70 20,70 70,20 70,20 20,70 20))} | ||
| .convert(); | ||
| let fully_inside: Polygon<f64> = wkt! {POLYGON((20 10,20 20,10 20,10 10,20 10))}.convert(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| let disjoint: Polygon<f64> = | ||
| wkt! {POLYGON((150 140,150 150,140 150,140 140,150 140))}.convert(); | ||
|
|
||
| let empty_poly = Polygon::empty(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yay, thanks for using the new empty constructors. ❤️
| // degenerate polygon should always return false | ||
| // because it would be intersecting the boundary of the degenerate polygon | ||
| #[test] | ||
| fn test_degenerate_self() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for these edge case tests!
| /// - If Geometry `b` has any interaction with the boundary of Geometry `a`, then the result is `false`. | ||
| /// - Geometry`a` will never contains_properly itself. | ||
| /// - Matches `[T**FF*FF*]` | ||
| /// - This predicate is **transitive** but not **reflexive** |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please cross reference the two implementations in their corresponding docs.
Also, it'd be good to mention something about how performance characteristics might change between them.
If you have an intuition for when it's preferable to use ContainsProperly vs Relate, please include that in the docs too. As you mentioned, the baton rouge test is a lot slower when ContainsProperly. Is it that generally Relate works better for larger inputs?

CHANGES.mdif knowledge of this change could be valuable to users.Context
I run lots of Polygon ~in~ Polygon checks. The tighter boundary intersection constraint of
ContainsProperlyallows for faster algorithms since other geometry which intersects self boundary in any way can immediately be rejected. The implementations here are a generalized version of a more data-informed version I wrote for a project.Parts Implementated
is_contains_properlymethod toIntersectionMatrixusing[T**FF*FF*]ContainsProperlytrait, mostly using theRelatemethodContainsProperlyimplementations for combinations ofPolygonandMultiPolygonDocumentation
Public interface for
ContainsProperlyTests
ContainsProperly also isn't in the JTS
*Relate*.xmlfiles, but I used #1428 as a bit of a baseline test setReferences:
JTS
PostGIS