From a3ba07382326c7f44e04f5f947d83f48eb3fd37d Mon Sep 17 00:00:00 2001 From: Andy Yang Date: Mon, 29 Mar 2021 20:38:31 -0400 Subject: [PATCH] geo/geomfn: refactor logic for point in polygon optimization Release note: None --- pkg/geo/geomfn/BUILD.bazel | 1 + pkg/geo/geomfn/binary_predicates.go | 10 +- pkg/geo/geomfn/distance.go | 121 +--------- pkg/geo/geomfn/point_polygon_optimization.go | 222 +++++++++++++++++++ 4 files changed, 229 insertions(+), 125 deletions(-) create mode 100644 pkg/geo/geomfn/point_polygon_optimization.go diff --git a/pkg/geo/geomfn/BUILD.bazel b/pkg/geo/geomfn/BUILD.bazel index 8d832a92a342..7cbd74d6c95d 100644 --- a/pkg/geo/geomfn/BUILD.bazel +++ b/pkg/geo/geomfn/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "make_geometry.go", "node.go", "orientation.go", + "point_polygon_optimization.go", "remove_repeated_points.go", "reverse.go", "segmentize.go", diff --git a/pkg/geo/geomfn/binary_predicates.go b/pkg/geo/geomfn/binary_predicates.go index 44ba23a79d4c..098a18b50a68 100644 --- a/pkg/geo/geomfn/binary_predicates.go +++ b/pkg/geo/geomfn/binary_predicates.go @@ -36,7 +36,7 @@ func Covers(a geo.Geometry, b geo.Geometry) (bool, error) { case PolygonAndPoint: // Computing whether a polygon covers a point is equivalent // to computing whether the point is covered by the polygon. - return PointKindRelatesToPolygonKind(pointKind, polygonKind, PointPolygonCoveredBy) + return PointKindCoveredByPolygonKind(pointKind, polygonKind) } return geos.Covers(a.EWKB(), b.EWKB()) @@ -58,7 +58,7 @@ func CoveredBy(a geo.Geometry, b geo.Geometry) (bool, error) { // A polygon cannot be covered by a point. return false, nil case PointAndPolygon: - return PointKindRelatesToPolygonKind(pointKind, polygonKind, PointPolygonCoveredBy) + return PointKindCoveredByPolygonKind(pointKind, polygonKind) } return geos.CoveredBy(a.EWKB(), b.EWKB()) @@ -82,7 +82,7 @@ func Contains(a geo.Geometry, b geo.Geometry) (bool, error) { case PolygonAndPoint: // Computing whether a polygon contains a point is equivalent // to computing whether the point is contained within the polygon. - return PointKindRelatesToPolygonKind(pointKind, polygonKind, PointPolygonWithin) + return PointKindWithinPolygonKind(pointKind, polygonKind) } return geos.Contains(a.EWKB(), b.EWKB()) @@ -148,7 +148,7 @@ func Intersects(a geo.Geometry, b geo.Geometry) (bool, error) { pointPolygonPair, pointKind, polygonKind := PointKindAndPolygonKind(a, b) switch pointPolygonPair { case PointAndPolygon, PolygonAndPoint: - return PointKindRelatesToPolygonKind(pointKind, polygonKind, PointPolygonIntersects) + return PointKindIntersectsPolygonKind(pointKind, polygonKind) } return geos.Intersects(a.EWKB(), b.EWKB()) @@ -335,7 +335,7 @@ func Within(a geo.Geometry, b geo.Geometry) (bool, error) { // A polygon cannot be contained within a point. return false, nil case PointAndPolygon: - return PointKindRelatesToPolygonKind(pointKind, polygonKind, PointPolygonWithin) + return PointKindWithinPolygonKind(pointKind, polygonKind) } return geos.Within(a.EWKB(), b.EWKB()) diff --git a/pkg/geo/geomfn/distance.go b/pkg/geo/geomfn/distance.go index 555dcae2ea34..3e064cb91ec1 100644 --- a/pkg/geo/geomfn/distance.go +++ b/pkg/geo/geomfn/distance.go @@ -635,7 +635,7 @@ func findPointSideOfLinearRing(point geodist.Point, linearRing geodist.LinearRin // See also: https://en.wikipedia.org/wiki/Nonzero-rule windingNumber := 0 p := point.GeomPoint - for edgeIdx := 0; edgeIdx < linearRing.NumEdges(); edgeIdx++ { + for edgeIdx, numEdges := 0, linearRing.NumEdges(); edgeIdx < numEdges; edgeIdx++ { e := linearRing.Edge(edgeIdx) eV0 := e.V0.GeomPoint eV1 := e.V1.GeomPoint @@ -836,125 +836,6 @@ func verifyDensifyFrac(f float64) error { return nil } -// PointPolygonRelationType defines a relationship type between -// a (multi)point and a (multi)polygon. -type PointPolygonRelationType int - -const ( - // PointPolygonIntersects is the relationship where a (multi)point - // intersects a (multi)polygon. - PointPolygonIntersects PointPolygonRelationType = iota + 1 - // PointPolygonCoveredBy is the relationship where a (multi)point - // is covered by a (multi)polygon. - PointPolygonCoveredBy - // PointPolygonWithin is the relationship where a (multi)point - // is contained within a (multi)polygon. - PointPolygonWithin -) - -// PointKindRelatesToPolygonKind returns whether a (multi)point and -// a (multi)polygon have the given relationship. -func PointKindRelatesToPolygonKind( - pointKind geo.Geometry, polygonKind geo.Geometry, relationType PointPolygonRelationType, -) (bool, error) { - pointKindBaseT, err := pointKind.AsGeomT() - if err != nil { - return false, err - } - polygonKindBaseT, err := polygonKind.AsGeomT() - if err != nil { - return false, err - } - pointKindIterator := geo.NewGeomTIterator(pointKindBaseT, geo.EmptyBehaviorOmit) - polygonKindIterator := geo.NewGeomTIterator(polygonKindBaseT, geo.EmptyBehaviorOmit) - - // TODO(ayang): Think about how to refactor these nested for loops - // Check whether each point intersects with at least one polygon. - // - For Intersects, at least one point must intersect with at least one polygon. - // - For CoveredBy, every point must intersect with at least one polygon. - // - For Within, every point must intersect with at least one polygon - // and at least one point must be inside at least one polygon. - intersectsOnce := false - insideOnce := false -pointOuterLoop: - for { - point, hasPoint, err := pointKindIterator.Next() - if err != nil { - return false, err - } - if !hasPoint { - break - } - // Reset the polygon iterator on each iteration of the point iterator. - polygonKindIterator.Reset() - curIntersects := false - for { - polygon, hasPolygon, err := polygonKindIterator.Next() - if err != nil { - return false, err - } - if !hasPolygon { - break - } - pointSide, err := findPointSideOfPolygon(point, polygon) - if err != nil { - return false, err - } - switch pointSide { - case insideLinearRing: - insideOnce = true - switch relationType { - case PointPolygonWithin: - continue pointOuterLoop - } - fallthrough - case onLinearRing: - intersectsOnce = true - curIntersects = true - switch relationType { - case PointPolygonIntersects: - // A single intersection is sufficient. - return true, nil - case PointPolygonCoveredBy: - // If the current point intersects, check the next point. - continue pointOuterLoop - case PointPolygonWithin: - // We can only skip to the next point if we have already seen a point - // that is inside the (multi)polygon. - if insideOnce { - continue pointOuterLoop - } - default: - return false, errors.Newf("unknown PointPolygonRelationType") - } - case outsideLinearRing: - default: - return false, errors.Newf("findPointSideOfPolygon returned unknown linearRingSide %d", pointSide) - } - } - // Case where a point in the (multi)point does not intersect - // a polygon in the (multi)polygon. - switch relationType { - case PointPolygonCoveredBy: - // Each point in a (multi)point must intersect a polygon in the - // (multi)point to be covered by it. - return false, nil - case PointPolygonWithin: - if !curIntersects { - return false, nil - } - } - } - switch relationType { - case PointPolygonCoveredBy: - return intersectsOnce, nil - case PointPolygonWithin: - return insideOnce, nil - default: - return false, nil - } -} - // findPointSideOfPolygon returns whether a point intersects with a polygon. func findPointSideOfPolygon(point geom.T, polygon geom.T) (linearRingSide, error) { // Convert point from a geom.T to a *geodist.Point. diff --git a/pkg/geo/geomfn/point_polygon_optimization.go b/pkg/geo/geomfn/point_polygon_optimization.go new file mode 100644 index 000000000000..eaffa2b27cf5 --- /dev/null +++ b/pkg/geo/geomfn/point_polygon_optimization.go @@ -0,0 +1,222 @@ +// Copyright 2021 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package geomfn + +import ( + "github.com/cockroachdb/cockroach/pkg/geo" + "github.com/cockroachdb/errors" +) + +// PointPolygonControlFlowType signals what control flow to follow. +type PointPolygonControlFlowType int + +const ( + // PPCFCheckNextPolygon signals that the current point should be checked + // against the next polygon. + PPCFCheckNextPolygon PointPolygonControlFlowType = iota + // PPCFSkipToNextPoint signals that the rest of the checking for the current + // point can be skipped. + PPCFSkipToNextPoint + // PPCFReturnTrue signals that the function should exit early and return true. + PPCFReturnTrue +) + +// PointInPolygonEventListener is an interface implemented for each +// binary predicate making use of the point in polygon optimization +// to specify the behavior in pointKindRelatesToPolygonKind. +type PointInPolygonEventListener interface { + // OnPointIntersectsPolygon returns whether the function should exit and + // return true, skip to the next point, or check the current point against + // the next polygon in the case where a point intersects with a polygon. + // The strictlyInside param signifies whether the point is strictly inside + // or on the boundary of the polygon. + OnPointIntersectsPolygon(strictlyInside bool) PointPolygonControlFlowType + // OnPointDoesNotIntersect returns whether the function should early exit and + // return false in the case where a point does not intersect any polygon. + ExitIfPointDoesNotIntersect() bool + // AfterPointPolygonLoops returns the bool to return after the point-polygon + // loops have finished. + AfterPointPolygonLoops() bool +} + +// For Intersects, at least one point must intersect with at least one polygon. +type intersectsPIPEventListener struct{} + +func (el *intersectsPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // A single intersection is sufficient. + return PPCFReturnTrue +} + +func (el *intersectsPIPEventListener) ExitIfPointDoesNotIntersect() bool { + return false +} + +func (el *intersectsPIPEventListener) AfterPointPolygonLoops() bool { + return false +} + +var _ PointInPolygonEventListener = (*intersectsPIPEventListener)(nil) + +func newIntersectsPIPEventListener() *intersectsPIPEventListener { + return &intersectsPIPEventListener{} +} + +// For CoveredBy, every point must intersect with at least one polygon. +type coveredByPIPEventListener struct { + intersectsOnce bool +} + +func (el *coveredByPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // If the current point intersects, check the next point. + el.intersectsOnce = true + return PPCFSkipToNextPoint +} + +func (el *coveredByPIPEventListener) ExitIfPointDoesNotIntersect() bool { + // Each point in a (multi)point must intersect a polygon in the + // (multi)point to be covered by it. + return true +} + +func (el *coveredByPIPEventListener) AfterPointPolygonLoops() bool { + return el.intersectsOnce +} + +var _ PointInPolygonEventListener = (*coveredByPIPEventListener)(nil) + +func newCoveredByPIPEventListener() *coveredByPIPEventListener { + return &coveredByPIPEventListener{intersectsOnce: false} +} + +// For Within, every point must intersect with at least one polygon. +type withinPIPEventListener struct { + insideOnce bool +} + +func (el *withinPIPEventListener) OnPointIntersectsPolygon( + strictlyInside bool, +) PointPolygonControlFlowType { + // We can only skip to the next point if we have already seen a point + // that is inside the (multi)polygon. + if el.insideOnce { + return PPCFSkipToNextPoint + } + if strictlyInside { + el.insideOnce = true + return PPCFSkipToNextPoint + } + return PPCFCheckNextPolygon +} + +func (el *withinPIPEventListener) ExitIfPointDoesNotIntersect() bool { + // Each point in a (multi)point must intersect a polygon in the + // (multi)polygon to be contained within it. + return true +} + +func (el *withinPIPEventListener) AfterPointPolygonLoops() bool { + return el.insideOnce +} + +var _ PointInPolygonEventListener = (*withinPIPEventListener)(nil) + +func newWithinPIPEventListener() *withinPIPEventListener { + return &withinPIPEventListener{insideOnce: false} +} + +// PointKindIntersectsPolygonKind returns whether a (multi)point +// and a (multi)polygon intersect. +func PointKindIntersectsPolygonKind( + pointKind geo.Geometry, polygonKind geo.Geometry, +) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newIntersectsPIPEventListener()) +} + +// PointKindCoveredByPolygonKind returns whether a (multi)point +// is covered by a (multi)polygon. +func PointKindCoveredByPolygonKind(pointKind geo.Geometry, polygonKind geo.Geometry) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newCoveredByPIPEventListener()) +} + +// PointKindWithinPolygonKind returns whether a (multi)point +// is contained within a (multi)polygon. +func PointKindWithinPolygonKind(pointKind geo.Geometry, polygonKind geo.Geometry) (bool, error) { + return pointKindRelatesToPolygonKind(pointKind, polygonKind, newWithinPIPEventListener()) +} + +// pointKindRelatesToPolygonKind returns whether a (multi)point +// and a (multi)polygon have the given relationship. +func pointKindRelatesToPolygonKind( + pointKind geo.Geometry, polygonKind geo.Geometry, eventListener PointInPolygonEventListener, +) (bool, error) { + pointKindBaseT, err := pointKind.AsGeomT() + if err != nil { + return false, err + } + polygonKindBaseT, err := polygonKind.AsGeomT() + if err != nil { + return false, err + } + pointKindIterator := geo.NewGeomTIterator(pointKindBaseT, geo.EmptyBehaviorOmit) + polygonKindIterator := geo.NewGeomTIterator(polygonKindBaseT, geo.EmptyBehaviorOmit) + + // Check whether each point intersects with at least one polygon. + // The behavior for each predicate is dictated by eventListener. +pointOuterLoop: + for { + point, hasPoint, err := pointKindIterator.Next() + if err != nil { + return false, err + } + if !hasPoint { + break + } + // Reset the polygon iterator on each iteration of the point iterator. + polygonKindIterator.Reset() + curIntersects := false + for { + polygon, hasPolygon, err := polygonKindIterator.Next() + if err != nil { + return false, err + } + if !hasPolygon { + break + } + pointSide, err := findPointSideOfPolygon(point, polygon) + if err != nil { + return false, err + } + switch pointSide { + case insideLinearRing, onLinearRing: + curIntersects = true + strictlyInside := pointSide == insideLinearRing + switch eventListener.OnPointIntersectsPolygon(strictlyInside) { + case PPCFCheckNextPolygon: + case PPCFSkipToNextPoint: + continue pointOuterLoop + case PPCFReturnTrue: + return true, nil + } + case outsideLinearRing: + default: + return false, errors.Newf("findPointSideOfPolygon returned unknown linearRingSide %d", pointSide) + } + } + if !curIntersects && eventListener.ExitIfPointDoesNotIntersect() { + return false, nil + } + } + return eventListener.AfterPointPolygonLoops(), nil +}