From dadc2b4ae06cc8832eca524566f039b2434ba6b3 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Thu, 12 Mar 2026 20:35:15 +0300 Subject: [PATCH] feat(planar): add polygon/line containment, intersection funcs and tests Signed-off-by: Rodney Osodo --- planar/contains.go | 192 +++++++++++++++++++++++++++++++++ planar/contains_test.go | 228 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 420 insertions(+) diff --git a/planar/contains.go b/planar/contains.go index fbfc455..dbcb52c 100644 --- a/planar/contains.go +++ b/planar/contains.go @@ -32,6 +32,22 @@ func RingContains(r orb.Ring, point orb.Point) bool { return c } +func boundContainsBound(outer, inner orb.Bound) bool { + return outer.Contains(inner.Min) && outer.Contains(inner.Max) +} + +func segmentIntersects(a, b, c, d orb.Point) bool { + denom := (d[1]-c[1])*(b[0]-a[0]) - (d[0]-c[0])*(b[1]-a[1]) + if denom == 0 { + return false + } + + ua := ((d[0]-c[0])*(a[1]-c[1]) - (d[1]-c[1])*(a[0]-c[0])) / denom + ub := ((b[0]-a[0])*(a[1]-c[1]) - (b[1]-a[1])*(a[0]-c[0])) / denom + + return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1 +} + // PolygonContains checks if the point is within the polygon. // Points on the boundary are considered in. func PolygonContains(p orb.Polygon, point orb.Point) bool { @@ -60,6 +76,182 @@ func MultiPolygonContains(mp orb.MultiPolygon, point orb.Point) bool { return false } +// RingContainsRing checks if the inner ring is completely within the outer ring. +// Points on the boundary are considered within. +func RingContainsRing(outer, inner orb.Ring) bool { + if !boundContainsBound(outer.Bound(), inner.Bound()) { + return false + } + + for _, p := range inner { + if !RingContains(outer, p) { + return false + } + } + + return true +} + +// PolygonContainsPolygon checks if the inner polygon is completely within the outer polygon. +// This includes checking the outer rings and ensuring the inner polygon +// is not in any holes of the outer polygon. +// Points on the boundary are considered within. +func PolygonContainsPolygon(outer, inner orb.Polygon) bool { + if !boundContainsBound(outer.Bound(), inner.Bound()) { + return false + } + + if !RingContainsRing(outer[0], inner[0]) { + return false + } + + if len(outer) > 1 { + center := inner[0].Bound().Center() + for i := 1; i < len(outer); i++ { + if RingContains(outer[i], center) { + return false + } + } + } + + return true +} + +// PolygonContainsLineString checks if the linestring is completely within the polygon. +// Points on the boundary are considered within. +func PolygonContainsLineString(p orb.Polygon, ls orb.LineString) bool { + if !boundContainsBound(p.Bound(), ls.Bound()) { + return false + } + + for _, pt := range ls { + if !PolygonContains(p, pt) { + return false + } + } + + return true +} + +// MultiPolygonContainsPolygon checks if the polygon is completely within the multi-polygon. +func MultiPolygonContainsPolygon(mp orb.MultiPolygon, poly orb.Polygon) bool { + for _, p := range mp { + if PolygonContainsPolygon(p, poly) { + return true + } + } + + return false +} + +// RingIntersectsRing checks if two rings intersect. +// Returns true if they touch or overlap. +func RingIntersectsRing(r1, r2 orb.Ring) bool { + if !r1.Bound().Intersects(r2.Bound()) { + return false + } + + for i := 0; i < len(r1)-1; i++ { + for j := 0; j < len(r2)-1; j++ { + if segmentIntersects(r1[i], r1[i+1], r2[j], r2[j+1]) { + return true + } + } + } + + if RingContains(r1, r2[0]) || RingContains(r2, r1[0]) { + return true + } + + return false +} + +// PolygonIntersectsPolygon checks if two polygons intersect. +// Returns true if they touch or overlap. +func PolygonIntersectsPolygon(p1, p2 orb.Polygon) bool { + if !p1.Bound().Intersects(p2.Bound()) { + return false + } + + for i := 0; i < len(p1); i++ { + for j := 0; j < len(p2); j++ { + if RingIntersectsRing(p1[i], p2[j]) { + return true + } + } + } + + return false +} + +// LineStringIntersectsPolygon checks if a linestring intersects a polygon. +// Returns true if any part of the linestring touches or is within the polygon. +func LineStringIntersectsPolygon(ls orb.LineString, p orb.Polygon) bool { + if !ls.Bound().Intersects(p.Bound()) { + return false + } + + for i := 0; i < len(ls)-1; i++ { + for _, ring := range p { + for j := 0; j < len(ring)-1; j++ { + if segmentIntersects(ls[i], ls[i+1], ring[j], ring[j+1]) { + return true + } + } + } + } + + for _, pt := range ls { + if PolygonContains(p, pt) { + return true + } + } + + return false +} + +// RingCovers checks if the ring covers the point. +// Points on the boundary are considered covered. +func RingCovers(r orb.Ring, point orb.Point) bool { + return RingContains(r, point) +} + +// PolygonCovers checks if the polygon covers the point. +// Points on the boundary are considered covered. +func PolygonCovers(p orb.Polygon, point orb.Point) bool { + return PolygonContains(p, point) +} + +// PolygonCoversLineString checks if the polygon covers the linestring. +// Points on the boundary are considered covered. +func PolygonCoversLineString(p orb.Polygon, ls orb.LineString) bool { + return PolygonContainsLineString(p, ls) +} + +// PolygonCoversPolygon checks if the outer polygon covers the inner polygon. +// Points on the boundary are considered covered. +func PolygonCoversPolygon(outer, inner orb.Polygon) bool { + return PolygonContainsPolygon(outer, inner) +} + +// RingWithin returns true if the ring is completely within the other ring. +// Points on the boundary are considered within. +func RingWithin(inner, outer orb.Ring) bool { + return RingContainsRing(outer, inner) +} + +// PolygonWithin returns true if the polygon is completely within the other polygon. +// Points on the boundary are considered within. +func PolygonWithin(inner, outer orb.Polygon) bool { + return PolygonContainsPolygon(outer, inner) +} + +// LineStringWithinPolygon returns true if the linestring is completely within the polygon. +// Points on the boundary are considered within. +func LineStringWithinPolygon(ls orb.LineString, p orb.Polygon) bool { + return PolygonContainsLineString(p, ls) +} + // Original implementation: http://rosettacode.org/wiki/Ray-casting_algorithm#Go func rayIntersect(p, s, e orb.Point) (intersects, on bool) { if s[0] > e[0] { diff --git a/planar/contains_test.go b/planar/contains_test.go index be85081..63b8a8f 100644 --- a/planar/contains_test.go +++ b/planar/contains_test.go @@ -164,6 +164,234 @@ func TestMultiPolygonContains(t *testing.T) { } } +func TestRingContainsRing(t *testing.T) { + outer := orb.Ring{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}} + inner := orb.Ring{{2, 2}, {8, 2}, {8, 8}, {2, 8}, {2, 2}} + + if !RingContainsRing(outer, inner) { + t.Errorf("inner ring should be within outer ring") + } + + larger := orb.Ring{{-1, -1}, {11, -1}, {11, 11}, {-1, 11}, {-1, -1}} + if RingContainsRing(outer, larger) { + t.Errorf("larger ring should not be within smaller ring") + } + + partial := orb.Ring{{5, 5}, {15, 5}, {15, 15}, {5, 15}, {5, 5}} + if RingContainsRing(outer, partial) { + t.Errorf("partial ring should not be within outer ring") + } +} + +func TestPolygonContainsPolygon(t *testing.T) { + outer := orb.Polygon{ + {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}, + } + inner := orb.Polygon{ + {{2, 2}, {8, 2}, {8, 8}, {2, 8}, {2, 2}}, + } + + if !PolygonContainsPolygon(outer, inner) { + t.Errorf("inner polygon should be within outer polygon") + } + + outerWithHole := orb.Polygon{ + {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}, + {{3, 3}, {7, 3}, {7, 7}, {3, 7}, {3, 3}}, + } + if PolygonContainsPolygon(outerWithHole, inner) { + t.Errorf("inner polygon should not be within hole") + } + + outerSmaller := orb.Polygon{ + {{0, 0}, {5, 0}, {5, 5}, {0, 5}, {0, 0}}, + } + if PolygonContainsPolygon(outerSmaller, inner) { + t.Errorf("inner polygon should not be within smaller outer") + } +} + +func TestPolygonContainsLineString(t *testing.T) { + poly := orb.Polygon{ + {{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}, + } + + ls := orb.LineString{{2, 2}, {4, 4}, {6, 6}} + if !PolygonContainsLineString(poly, ls) { + t.Errorf("linestring should be within polygon") + } + + lsOutside := orb.LineString{{2, 2}, {12, 12}} + if PolygonContainsLineString(poly, lsOutside) { + t.Errorf("linestring outside should not be within polygon") + } + + lsOnBoundary := orb.LineString{{0, 0}, {5, 0}} + if !PolygonContainsLineString(poly, lsOnBoundary) { + t.Errorf("linestring on boundary should be within polygon") + } +} + +func TestMultiPolygonContainsPolygon(t *testing.T) { + mp := orb.MultiPolygon{ + {{{0, 0}, {5, 0}, {5, 5}, {0, 5}, {0, 0}}}, + {{{10, 0}, {15, 0}, {15, 5}, {10, 5}, {10, 0}}}, + } + + poly := orb.Polygon{{{2, 2}, {3, 2}, {3, 3}, {2, 3}, {2, 2}}} + if !MultiPolygonContainsPolygon(mp, poly) { + t.Errorf("polygon should be within multi-polygon") + } + + polyOutside := orb.Polygon{{{7, 7}, {8, 7}, {8, 8}, {7, 8}, {7, 7}}} + if MultiPolygonContainsPolygon(mp, polyOutside) { + t.Errorf("polygon outside should not be within multi-polygon") + } +} + +func TestRingIntersectsRing(t *testing.T) { + r1 := orb.Ring{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}} + r2 := orb.Ring{{5, 5}, {15, 5}, {15, 15}, {5, 15}, {5, 5}} + + if !RingIntersectsRing(r1, r2) { + t.Errorf("rings should intersect") + } + + r3 := orb.Ring{{20, 20}, {30, 20}, {30, 30}, {20, 30}, {20, 20}} + if RingIntersectsRing(r1, r3) { + t.Errorf("non-overlapping rings should not intersect") + } +} + +func TestPolygonIntersectsPolygon(t *testing.T) { + p1 := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + p2 := orb.Polygon{{{5, 5}, {15, 5}, {15, 15}, {5, 15}, {5, 5}}} + + if !PolygonIntersectsPolygon(p1, p2) { + t.Errorf("polygons should intersect") + } + + p3 := orb.Polygon{{{20, 20}, {30, 20}, {30, 30}, {20, 30}, {20, 30}}} + if PolygonIntersectsPolygon(p1, p3) { + t.Errorf("non-overlapping polygons should not intersect") + } +} + +func TestLineStringIntersectsPolygon(t *testing.T) { + p := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + + ls := orb.LineString{{-5, 5}, {5, 5}} + if !LineStringIntersectsPolygon(ls, p) { + t.Errorf("linestring crossing polygon should intersect") + } + + lsInside := orb.LineString{{2, 2}, {4, 4}, {6, 6}} + if !LineStringIntersectsPolygon(lsInside, p) { + t.Errorf("linestring inside polygon should intersect") + } + + lsOutside := orb.LineString{{20, 20}, {30, 30}} + if LineStringIntersectsPolygon(lsOutside, p) { + t.Errorf("linestring outside should not intersect") + } +} + +func TestRingCovers(t *testing.T) { + r := orb.Ring{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}} + + if !RingCovers(r, orb.Point{5, 5}) { + t.Errorf("point inside should be covered") + } + + if !RingCovers(r, orb.Point{0, 0}) { + t.Errorf("point on boundary should be covered") + } + + if RingCovers(r, orb.Point{15, 15}) { + t.Errorf("point outside should not be covered") + } +} + +func TestPolygonCovers(t *testing.T) { + p := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + + if !PolygonCovers(p, orb.Point{5, 5}) { + t.Errorf("point inside should be covered") + } + + if PolygonCovers(p, orb.Point{15, 15}) { + t.Errorf("point outside should not be covered") + } +} + +func TestPolygonCoversLineString(t *testing.T) { + p := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + + ls := orb.LineString{{2, 2}, {4, 4}} + if !PolygonCoversLineString(p, ls) { + t.Errorf("linestring inside should be covered") + } + + lsOutside := orb.LineString{{2, 2}, {12, 12}} + if PolygonCoversLineString(p, lsOutside) { + t.Errorf("linestring outside should not be covered") + } +} + +func TestPolygonCoversPolygon(t *testing.T) { + outer := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + inner := orb.Polygon{{{2, 2}, {8, 2}, {8, 8}, {2, 8}, {2, 2}}} + + if !PolygonCoversPolygon(outer, inner) { + t.Errorf("inner polygon should be covered by outer") + } + + larger := orb.Polygon{{{0, 0}, {15, 0}, {15, 15}, {0, 15}, {0, 0}}} + if PolygonCoversPolygon(outer, larger) { + t.Errorf("larger polygon should not be covered by smaller") + } +} + +func TestRingWithin(t *testing.T) { + outer := orb.Ring{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}} + inner := orb.Ring{{2, 2}, {8, 2}, {8, 8}, {2, 8}, {2, 8}} + + if !RingWithin(inner, outer) { + t.Errorf("inner should be within outer") + } + + if RingWithin(outer, inner) { + t.Errorf("outer should not be within inner") + } +} + +func TestPolygonWithin(t *testing.T) { + outer := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + inner := orb.Polygon{{{2, 2}, {8, 2}, {8, 8}, {2, 8}, {2, 2}}} + + if !PolygonWithin(inner, outer) { + t.Errorf("inner should be within outer") + } + + if PolygonWithin(outer, inner) { + t.Errorf("outer should not be within inner") + } +} + +func TestLineStringWithinPolygon(t *testing.T) { + p := orb.Polygon{{{0, 0}, {10, 0}, {10, 10}, {0, 10}, {0, 0}}} + + ls := orb.LineString{{2, 2}, {4, 4}, {6, 6}} + if !LineStringWithinPolygon(ls, p) { + t.Errorf("linestring should be within polygon") + } + + lsOutside := orb.LineString{{-1, 5}, {5, 5}} + if LineStringWithinPolygon(lsOutside, p) { + t.Errorf("linestring outside should not be within polygon") + } +} + func interpolate(a, b orb.Point, percent float64) orb.Point { return orb.Point{ a[0] + percent*(b[0]-a[0]),