Skip to content

Conversation

@cookiedan42
Copy link
Contributor

@cookiedan42 cookiedan42 commented Oct 4, 2025

  • I agree to follow the project's code of conduct.
  • I added an entry to CHANGES.md if knowledge of this change could be valuable to users.

Context

I run lots of Polygon ~in~ Polygon checks. The tighter boundary intersection constraint of ContainsProperly allows 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

  1. Added the is_contains_properly method to IntersectionMatrix using [T**FF*FF*]
  2. Added base ContainsProperly trait, mostly using the Relate method
  3. Added custom ContainsProperly implementations for combinations of Polygon and MultiPolygon
  4. Other pairings to come in future PR

Documentation

Public interface for ContainsProperly

Tests

ContainsProperly also isn't in the JTS *Relate*.xml files, but I used #1428 as a bit of a baseline test set

  • standard tests for poly-poly cases
  • standard tests for poly-multipoly
  • standard tests for multipoly-poly
  • standard tests for multipoly-multipoly
  • test for degenerate cases
    • degenerate self will always return false because it results in intersection with boundary
    • degenerate other will be treated as "zero-area polygon"
  • tests for empty polygons, empty multipolygons, empty element in multipolygon

References:

JTS
PostGIS

@cookiedan42
Copy link
Contributor Author

cookiedan42 commented Oct 4, 2025

Concept behind Implementation for Polygon

TLDR: ContainsProperly lets us fast fail any scenario where the RHS element intersects Self's boundary, this is a very useful precondition we can use for faster polygon in polygon checks

  1. In a valid polygon, holes can only touch each other at points and rings cannot self intersect

  2. There are no intersections between rings (enforced by boundary_intersects)
    2.1. if intersection --> return false
    2.2 therefore, with pairwise relation between rings, all rings must be either concentric or disjoint with any other ring

  3. For each hole in Self which is inside RHS's exterior, if there is no hole in RHS which ContainsProperly this self_hole, then there must exist some point of RHS's interior which lies outside of Self's interior
    3.1. Hence, for ContainsProperly to hold, every hole in Self must either be inside some hole in RHS or outside of RHS

Expanding to MultiPolygon

  1. Polygons in a valid MultiPolygon can only touch at a point, therefore there is no case where the a Polygon requires two components of a MultiPolygon to ContainsProperly
    1.1 if it touches two components of the MultiPolygon, then there it must touch the boundary of these two components and hence cannot be ContainsProperlyed by the multipolygon
  2. Hence, MultiPolygon operation can be handled by appropriate any and all iteration

Fast checking operation

  1. for two rings which do not overlap a and b (ie they are either concentric or disjoint)
  2. if any point of a is Inside ring b, then all of a must be inside b
    2.1 Symmetrical for b and a
  3. if neither of (2) are true then they must be disjoint

Benchmark

Complex input

Complex Polygon (Louisiana)

complex polygon contains_properly polygon (Trait)
                        time:   [262.51 µs 264.08 µs 265.80 µs]

complex polygon contains_properly polygon (Relate)
                        time:   [309.64 µs 311.57 µs 313.85 µs]

complex polygon contains_properly polygon (prepared Relate)
                        time:   [5.5367 µs 5.5578 µs 5.5778 µs]

Simple input

~ 20x of prepared Relate
~ 50x of unprepared Relate

Polygon x Polygon

contains_properly poly poly (Trait)
                        time:   [117.11 ns 119.50 ns 122.02 ns]

relate prepared poly poly
                        time:   [2.7458 µs 2.7528 µs 2.7601 µs]

contains poly poly (Trait)
                        time:   [6.2445 µs 6.2662 µs 6.2899 µs]

relate poly poly        time:   [6.2607 µs 6.2823 µs 6.3044 µs]

MultiPolygon x MultiPolygon

contains_properly multipoly multipoly (Trait)
                        time:   [85.860 ns 86.333 ns 86.816 ns]

relate prepared multipoly multipoly
                        time:   [2.7557 µs 2.7657 µs 2.7756 µs]

contains multipoly multipoly (Trait)
                        time:   [5.3473 µs 5.4202 µs 5.4994 µs]

relate multipoly multipoly
                        time:   [5.2378 µs 5.2668 µs 5.2967 µs]

However, it has a +700% runtime vs Relate on the louisiana contains east_baton_rouge benchmark, so this approach is not universally the better choice

@cookiedan42 cookiedan42 force-pushed the contains-properly-polygons branch from f6714a6 to ab76a60 Compare October 4, 2025 10:40
@cookiedan42 cookiedan42 force-pushed the contains-properly-polygons branch 3 times, most recently from e1a4c6d to be07f89 Compare October 9, 2025 17:07
@cookiedan42 cookiedan42 marked this pull request as ready for review October 9, 2025 17:19
@cookiedan42 cookiedan42 mentioned this pull request Oct 10, 2025
2 tasks
@cookiedan42 cookiedan42 force-pushed the contains-properly-polygons branch 2 times, most recently from 48b9c91 to 9d0ade9 Compare October 14, 2025 05:17
- better perf on large data
@cookiedan42 cookiedan42 force-pushed the contains-properly-polygons branch from 9d0ade9 to e0c7303 Compare October 14, 2025 08:06
@cookiedan42
Copy link
Contributor Author

Louisiana bench improved to 264.08 µs
Faster than base Relates for this case too

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.

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`.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// 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() {
Copy link
Member

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))
Copy link
Member

@michaelkirk michaelkirk Oct 11, 2025

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;
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
// 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 {
Copy link
Member

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
Copy link
Member

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();
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 again for writing your tests with WKT! It's so helpful to be able to pop these into JTSTestBuilder.

Image

let disjoint: Polygon<f64> =
wkt! {POLYGON((150 140,150 150,140 150,140 140,150 140))}.convert();

let empty_poly = Polygon::empty();
Copy link
Member

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() {
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 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**
Copy link
Member

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?

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.

2 participants