Commit a51c5e3d authored by craig[bot]'s avatar craig[bot]

Merge #49799

49799: geo/geodist: speed up dwithin/distance operations with bbox r=sumeerbhola a=otan

We can optimize distance checks by saying if their bounding boxes do not
intersect then we only need to check the exteriors. This shaved 2-3s off
a 7s query available in the PostGIS tutorial.

Existing test cases exercise this functionality well.

Release note: None
Co-authored-by: default avatarOliver Tan <[email protected]>
parents a18b32f9 3d85fedf
......@@ -108,6 +108,9 @@ type DistanceCalculator interface {
// ClosestPointToEdge returns the closest point to the infinite line denoted by
// the edge, and a bool on whether this point lies on the edge segment.
ClosestPointToEdge(edge Edge, point Point) (Point, bool)
// BoundingBoxIntersects returns whether the bounding boxes of the shapes in
// question intersect.
BoundingBoxIntersects() bool
}
// ShapeDistance returns the distance between two given shapes.
......@@ -203,8 +206,10 @@ func onPointToPolygon(c DistanceCalculator, a Point, b Polygon) bool {
// - The point P is contained in the polygon. One can again prove the same property.
// So we only need to compare with the exterior ring.
// MinDistance: If the exterior ring does not contain the point, we just need to calculate the distance to
// the exterior ring.
if c.DistanceUpdater().IsMaxDistance() || !c.PointInLinearRing(a, b.LinearRing(0)) {
// the exterior ring.
// BoundingBoxIntersects: if the bounding box of the shape being calculated does not intersect,
// then we only need to compare the outer loop.
if c.DistanceUpdater().IsMaxDistance() || !c.BoundingBoxIntersects() || !c.PointInLinearRing(a, b.LinearRing(0)) {
return onPointToEdgesExceptFirstEdgeStart(c, a, b.LinearRing(0))
}
// At this point it may be inside a hole.
......@@ -226,47 +231,57 @@ func onPointToPolygon(c DistanceCalculator, a Point, b Polygon) bool {
func onShapeEdgesToShapeEdges(c DistanceCalculator, a shapeWithEdges, b shapeWithEdges) bool {
for aEdgeIdx := 0; aEdgeIdx < a.NumEdges(); aEdgeIdx++ {
aEdge := a.Edge(aEdgeIdx)
crosser := c.NewEdgeCrosser(aEdge, b.Edge(0).V0)
var crosser EdgeCrosser
// MaxDistance: the max distance between 2 edges is the maximum of the distance across
// pairs of vertices chosen from each edge.
// It does not matter whether the edges cross, so we skip this check.
// BoundingBoxIntersects: if the bounding box of the two shapes do not intersect,
// then we don't need to check whether edges intersect either.
if !c.DistanceUpdater().IsMaxDistance() && c.BoundingBoxIntersects() {
crosser = c.NewEdgeCrosser(aEdge, b.Edge(0).V0)
}
for bEdgeIdx := 0; bEdgeIdx < b.NumEdges(); bEdgeIdx++ {
bEdge := b.Edge(bEdgeIdx)
// Max distance between 2 edges is the maximum of the distance across pairs of vertices chosen from each edge.
// It does not matter whether the edges cross, so we skip this check.
if !c.DistanceUpdater().IsMaxDistance() {
if crosser != nil {
// If the edges cross, the distance is 0.
if crosser.ChainCrossing(bEdge.V1) {
return c.DistanceUpdater().OnIntersects()
}
}
// Compare each vertex against the edge of the other.
for _, toCheck := range []struct {
vertex Point
edge Edge
}{
{aEdge.V0, bEdge},
{aEdge.V1, bEdge},
{bEdge.V0, aEdge},
{bEdge.V1, aEdge},
} {
// Check the vertex against the ends of the edges.
if c.DistanceUpdater().Update(toCheck.vertex, toCheck.edge.V0) ||
c.DistanceUpdater().Update(toCheck.vertex, toCheck.edge.V1) {
// Check the vertex against the ends of the edges.
if c.DistanceUpdater().Update(aEdge.V0, bEdge.V0) ||
c.DistanceUpdater().Update(aEdge.V0, bEdge.V1) ||
c.DistanceUpdater().Update(aEdge.V1, bEdge.V0) ||
c.DistanceUpdater().Update(aEdge.V1, bEdge.V1) {
return true
}
// Only project vertexes to edges if we are looking at the edges.
if !c.DistanceUpdater().IsMaxDistance() {
if projectVertexToEdge(c, aEdge.V0, bEdge) ||
projectVertexToEdge(c, aEdge.V1, bEdge) ||
projectVertexToEdge(c, bEdge.V0, aEdge) ||
projectVertexToEdge(c, bEdge.V1, aEdge) {
return true
}
if !c.DistanceUpdater().IsMaxDistance() {
// Also check the projection of the vertex onto the edge.
if closestPoint, ok := c.ClosestPointToEdge(toCheck.edge, toCheck.vertex); ok {
if c.DistanceUpdater().Update(toCheck.vertex, closestPoint) {
return true
}
}
}
}
}
}
return false
}
// projectVertexToEdge attempts to project the point onto the given edge.
// Returns true if the calling function should early exit.
func projectVertexToEdge(c DistanceCalculator, vertex Point, edge Edge) bool {
// Also check the projection of the vertex onto the edge.
if closestPoint, ok := c.ClosestPointToEdge(edge, vertex); ok {
if c.DistanceUpdater().Update(vertex, closestPoint) {
return true
}
}
return false
}
// onLineStringToPolygon updates the distance between a polyline and a polygon.
// Returns true if the calling function should early exit.
func onLineStringToPolygon(c DistanceCalculator, a LineString, b Polygon) bool {
......@@ -280,7 +295,11 @@ func onLineStringToPolygon(c DistanceCalculator, a LineString, b Polygon) bool {
// MaxDistance: the furthest distance from a LineString to a Polygon is always against the
// exterior ring. This follows the reasoning under "onPointToPolygon", but we must now
// check each point in the LineString.
if c.DistanceUpdater().IsMaxDistance() || !c.PointInLinearRing(a.Vertex(0), b.LinearRing(0)) {
// BoundingBoxIntersects: if the bounding box of the two shapes do not intersect,
// then the distance is always from the LineString to the exterior ring.
if c.DistanceUpdater().IsMaxDistance() ||
!c.BoundingBoxIntersects() ||
!c.PointInLinearRing(a.Vertex(0), b.LinearRing(0)) {
return onShapeEdgesToShapeEdges(c, a, b.LinearRing(0))
}
......@@ -335,12 +354,15 @@ func onPolygonToPolygon(c DistanceCalculator, a Polygon, b Polygon) bool {
// As such, we only need to compare the exterior rings if we detect this.
//
// MaxDistance:
// The furthest distance between two polygons is always against the exterior rings of each other.
// This closely follows the reasoning pointed out in "onPointToPolygon". Holes are always located
// inside the exterior ring of a polygon, so the exterior ring will always contain a point
// with a larger max distance.
// The furthest distance between two polygons is always against the exterior rings of each other.
// This closely follows the reasoning pointed out in "onPointToPolygon". Holes are always located
// inside the exterior ring of a polygon, so the exterior ring will always contain a point
// with a larger max distance.
// BoundingBoxIntersects: if the bounding box of the two shapes do not intersect,
// then the distance is always between the two exterior rings.
if c.DistanceUpdater().IsMaxDistance() ||
(!c.PointInLinearRing(bFirstPoint, a.LinearRing(0)) && !c.PointInLinearRing(aFirstPoint, b.LinearRing(0))) {
!c.BoundingBoxIntersects() ||
!c.PointInLinearRing(bFirstPoint, a.LinearRing(0)) && !c.PointInLinearRing(aFirstPoint, b.LinearRing(0)) {
return onShapeEdgesToShapeEdges(c, a.LinearRing(0), b.LinearRing(0))
}
......
......@@ -176,8 +176,6 @@ func (c *s2GeodistEdgeCrosser) ChainCrossing(p geodist.Point) bool {
// the spheroid are different than the two closest points on the sphere.
// See distance_test.go for examples of the "truer" distance values.
// Since we aim to be compatible with PostGIS, we adopt the same approach.
//
// TODO(otan): accelerate checks with bounding boxes.
func distanceGeographyRegions(
spheroid geographiclib.Spheroid,
useSphereOrSpheroid UseSphereOrSpheroid,
......@@ -305,6 +303,13 @@ func (c *geographyDistanceCalculator) DistanceUpdater() geodist.DistanceUpdater
return c.updater
}
// BoundingBoxIntersects implements geodist.DistanceCalculator.
func (c *geographyDistanceCalculator) BoundingBoxIntersects() bool {
// Return true, as it does the safer thing beneath.
// TODO(otan): update bounding box intersects.
return true
}
// NewEdgeCrosser implements geodist.DistanceCalculator.
func (c *geographyDistanceCalculator) NewEdgeCrosser(
edge geodist.Edge, startPoint geodist.Point,
......
......@@ -84,7 +84,7 @@ func maxDistanceInternal(
a *geo.Geometry, b *geo.Geometry, stopAfterGT float64, emptyBehavior geo.EmptyBehavior,
) (float64, error) {
u := newGeomMaxDistanceUpdater(stopAfterGT)
c := &geomDistanceCalculator{updater: u}
c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.BoundingBoxIntersects(b)}
return distanceInternal(a, b, c, emptyBehavior)
}
......@@ -94,7 +94,7 @@ func minDistanceInternal(
a *geo.Geometry, b *geo.Geometry, stopAfterLE float64, emptyBehavior geo.EmptyBehavior,
) (float64, error) {
u := newGeomMinDistanceUpdater(stopAfterLE)
c := &geomDistanceCalculator{updater: u}
c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.BoundingBoxIntersects(b)}
return distanceInternal(a, b, c, emptyBehavior)
}
......@@ -381,7 +381,8 @@ func (u *geomMaxDistanceUpdater) IsMaxDistance() bool {
// geomDistanceCalculator implements geodist.DistanceCalculator
type geomDistanceCalculator struct {
updater geodist.DistanceUpdater
updater geodist.DistanceUpdater
boundingBoxIntersects bool
}
var _ geodist.DistanceCalculator = (*geomDistanceCalculator)(nil)
......@@ -391,6 +392,11 @@ func (c *geomDistanceCalculator) DistanceUpdater() geodist.DistanceUpdater {
return c.updater
}
// BoundingBoxIntersects implements geodist.DistanceCalculator.
func (c *geomDistanceCalculator) BoundingBoxIntersects() bool {
return c.boundingBoxIntersects
}
// NewEdgeCrosser implements geodist.DistanceCalculator.
func (c *geomDistanceCalculator) NewEdgeCrosser(
edge geodist.Edge, startPoint geodist.Point,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment