Skip to content

Commit

Permalink
sql: add internal builtin for exclusive version of ST_DWithin
Browse files Browse the repository at this point in the history
This commit adds an internal geo_builtin function called
`ST_DWithinExclusive`. It is equivalent to `ST_Distance(a,b) < x`,
but is able to exit early.

Release note: None
  • Loading branch information
DrewKimball committed Aug 19, 2020
1 parent ebd5c73 commit 2088d55
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 116 deletions.
14 changes: 14 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,20 @@ has no relationship with the commit order of concurrent transactions.</p>
<tr><td><a name="_st_dwithin"></a><code>_st_dwithin(geometry_a: geometry, geometry_b: geometry, distance: <a href="float.html">float</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if any of geometry_a is within distance units of geometry_b.</p>
<p>This function variant does not utilize any geospatial index.</p>
</span></td></tr>
<tr><td><a name="_st_dwithinexclusive"></a><code>_st_dwithinexclusive(geography_a: geography, geography_b: geography, distance: <a href="float.html">float</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if any of geography_a is within distance meters of geography_b. Uses a spheroid to perform the operation.&quot;\n\nWhen operating on a spheroid, this function will use the sphere to calculate the closest two points using S2. The spheroid distance between these two points is calculated using GeographicLib. This follows observed PostGIS behavior.</p>
<p>The calculations performed are have a precision of 1cm.</p>
<p>This function utilizes the GeographicLib library for spheroid calculations.</p>
</span></td></tr>
<tr><td><a name="_st_dwithinexclusive"></a><code>_st_dwithinexclusive(geography_a: geography, geography_b: geography, distance: <a href="float.html">float</a>, use_spheroid: <a href="bool.html">bool</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if any of geography_a is within distance meters of geography_b.&quot;\n\nWhen operating on a spheroid, this function will use the sphere to calculate the closest two points using S2. The spheroid distance between these two points is calculated using GeographicLib. This follows observed PostGIS behavior.</p>
<p>The calculations performed are have a precision of 1cm.</p>
<p>This function utilizes the S2 library for spherical calculations.</p>
<p>This function utilizes the GeographicLib library for spheroid calculations.</p>
</span></td></tr>
<tr><td><a name="_st_dwithinexclusive"></a><code>_st_dwithinexclusive(geometry_a: geometry, geometry_b: geometry, distance: <a href="float.html">float</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if any of geometry_a is within distance units of geometry_b.</p>
</span></td></tr>
<tr><td><a name="_st_dwithinexclusive"></a><code>_st_dwithinexclusive(geometry_a_str: <a href="string.html">string</a>, geometry_b_str: <a href="string.html">string</a>, distance: <a href="float.html">float</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if any of geometry_a is within distance units of geometry_b.</p>
<p>This variant will cast all geometry_str arguments into Geometry types.</p>
</span></td></tr>
<tr><td><a name="_st_equals"></a><code>_st_equals(geometry_a: geometry, geometry_b: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if geometry_a is spatially equal to geometry_b, i.e. ST_Within(geometry_a, geometry_b) = ST_Within(geometry_b, geometry_a) = true.</p>
<p>This function utilizes the GEOS module.</p>
<p>This function variant does not utilize any geospatial index.</p>
Expand Down
36 changes: 26 additions & 10 deletions pkg/geo/geogfn/distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ func Distance(
aRegions,
bRegions,
a.BoundingRect().Intersects(b.BoundingRect()),
0,
0, /* stopAfter */
false, /* exclusive */
)
}

Expand Down Expand Up @@ -176,8 +177,10 @@ func (c *s2GeodistEdgeCrosser) ChainCrossing(p geodist.Point) (bool, geodist.Poi
}

// distanceGeographyRegions calculates the distance between two sets of regions.
// It will quit if it finds a distance that is less than stopAfterLE.
// It is not guaranteed to find the absolute minimum distance if stopAfterLE > 0.
// If exclusive is false, it will quit if it finds a distance that is less than
// or equal to stopAfter. Otherwise, it will quit if a distance less than
// stopAfter is found. It is not guaranteed to find the absolute minimum
// distance if stopAfter > 0.
//
// !!! SURPRISING BEHAVIOR WARNING FOR SPHEROIDS !!!
// PostGIS evaluates the distance between spheroid regions by computing the min of
Expand All @@ -197,7 +200,8 @@ func distanceGeographyRegions(
aRegions []s2.Region,
bRegions []s2.Region,
boundingBoxIntersects bool,
stopAfterLE float64,
stopAfter float64,
exclusive bool,
) (float64, error) {
minDistance := math.MaxFloat64
for _, aRegion := range aRegions {
Expand All @@ -206,7 +210,12 @@ func distanceGeographyRegions(
return 0, err
}
for _, bRegion := range bRegions {
minDistanceUpdater := newGeographyMinDistanceUpdater(spheroid, useSphereOrSpheroid, stopAfterLE)
minDistanceUpdater := newGeographyMinDistanceUpdater(
spheroid,
useSphereOrSpheroid,
stopAfter,
exclusive,
)
bGeodist, err := regionToGeodistShape(bRegion)
if err != nil {
return 0, err
Expand Down Expand Up @@ -238,15 +247,19 @@ type geographyMinDistanceUpdater struct {
useSphereOrSpheroid UseSphereOrSpheroid
minEdge s2.Edge
minD s1.ChordAngle
stopAfterLE s1.ChordAngle
stopAfter s1.ChordAngle
exclusive bool
}

var _ geodist.DistanceUpdater = (*geographyMinDistanceUpdater)(nil)

// newGeographyMinDistanceUpdater returns a new geographyMinDistanceUpdater with the
// correct arguments set up.
func newGeographyMinDistanceUpdater(
spheroid *geographiclib.Spheroid, useSphereOrSpheroid UseSphereOrSpheroid, stopAfterLE float64,
spheroid *geographiclib.Spheroid,
useSphereOrSpheroid UseSphereOrSpheroid,
stopAfter float64,
exclusive bool,
) *geographyMinDistanceUpdater {
multiplier := 1.0
if useSphereOrSpheroid == UseSpheroid {
Expand All @@ -255,12 +268,13 @@ func newGeographyMinDistanceUpdater(
// buffer for spheroid distances being slightly off.
multiplier -= SpheroidErrorFraction
}
stopAfterLEChordAngle := s1.ChordAngleFromAngle(s1.Angle(stopAfterLE * multiplier / spheroid.SphereRadius))
stopAfterChordAngle := s1.ChordAngleFromAngle(s1.Angle(stopAfter * multiplier / spheroid.SphereRadius))
return &geographyMinDistanceUpdater{
spheroid: spheroid,
minD: math.MaxFloat64,
useSphereOrSpheroid: useSphereOrSpheroid,
stopAfterLE: stopAfterLEChordAngle,
stopAfter: stopAfterChordAngle,
exclusive: exclusive,
}
}

Expand Down Expand Up @@ -288,7 +302,9 @@ func (u *geographyMinDistanceUpdater) Update(aPoint geodist.Point, bPoint geodis
// If we have a threshold, determine if we can stop early.
// If the sphere distance is within range of the stopAfter, we can
// definitively say we've reach the close enough point.
if u.minD <= u.stopAfterLE {
if !u.exclusive && u.minD <= u.stopAfter {
return true
} else if u.exclusive && u.minD < u.stopAfter {
return true
}
}
Expand Down
16 changes: 13 additions & 3 deletions pkg/geo/geogfn/dwithin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ import (
"github.com/golang/geo/s1"
)

// DWithin returns whether a is within distance d of b, i.e. Distance(a, b) <= d.
// If A or B contains empty Geography objects, this will return false.
// DWithin returns whether a is within distance d of b. If A or B contains empty
// Geography objects, this will return false. If exclusive is false, DWithin is
// equivalent to Distance(a, b) <= d. Otherwise, DWithin is instead equivalent
// to Distance(a, b) < d.
func DWithin(
a *geo.Geography, b *geo.Geography, distance float64, useSphereOrSpheroid UseSphereOrSpheroid,
a *geo.Geography,
b *geo.Geography,
distance float64,
useSphereOrSpheroid UseSphereOrSpheroid,
exclusive bool,
) (bool, error) {
if a.SRID() != b.SRID() {
return false, geo.NewMismatchingSRIDsError(a, b)
Expand Down Expand Up @@ -61,9 +67,13 @@ func DWithin(
bRegions,
a.BoundingRect().Intersects(b.BoundingRect()),
distance,
exclusive,
)
if err != nil {
return false, err
}
if exclusive {
return maybeClosestDistance < distance, nil
}
return maybeClosestDistance <= distance, nil
}
49 changes: 40 additions & 9 deletions pkg/geo/geogfn/dwithin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,27 @@ func TestDWithin(t *testing.T) {
}
for _, val := range []float64{zeroValue, 1, 10, 10000} {
t.Run(fmt.Sprintf("dwithin:%f", val), func(t *testing.T) {
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid)
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid)
dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)
})
t.Run(fmt.Sprintf("dwithinexclusive:%f", val), func(t *testing.T) {
exclusiveExpected := true
if val == subTC.expected {
exclusiveExpected = false
}
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.Equal(t, dwithin, exclusiveExpected)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.Equal(t, dwithin, exclusiveExpected)
})
}
} else {
for _, val := range []float64{
Expand All @@ -65,11 +78,20 @@ func TestDWithin(t *testing.T) {
subTC.expected * 2,
} {
t.Run(fmt.Sprintf("dwithin:%f", val), func(t *testing.T) {
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid)
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)
})
t.Run(fmt.Sprintf("dwithinexclusive:%f", val), func(t *testing.T) {
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid)
dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.True(t, dwithin)
})
Expand All @@ -82,11 +104,20 @@ func TestDWithin(t *testing.T) {
subTC.expected / 2,
} {
t.Run(fmt.Sprintf("dwithin:%f", val), func(t *testing.T) {
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid)
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.False(t, dwithin)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.False(t, dwithin)
})
t.Run(fmt.Sprintf("dwithinexclusive:%f", val), func(t *testing.T) {
dwithin, err := DWithin(a, b, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.False(t, dwithin)

dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid)
dwithin, err = DWithin(b, a, val, subTC.useSphereOrSpheroid, true /* exclusive */)
require.NoError(t, err)
require.False(t, dwithin)
})
Expand All @@ -98,12 +129,12 @@ func TestDWithin(t *testing.T) {
}

t.Run("errors if SRIDs mismatch", func(t *testing.T) {
_, err := DWithin(mismatchingSRIDGeographyA, mismatchingSRIDGeographyB, 0, UseSpheroid)
_, err := DWithin(mismatchingSRIDGeographyA, mismatchingSRIDGeographyB, 0, UseSpheroid, false /* exclusive */)
requireMismatchingSRIDError(t, err)
})

t.Run("errors if distance < 0", func(t *testing.T) {
_, err := DWithin(geo.MustParseGeography("POINT(1.0 2.0)"), geo.MustParseGeography("POINT(3.0 4.0)"), -0.01, UseSpheroid)
_, err := DWithin(geo.MustParseGeography("POINT(1.0 2.0)"), geo.MustParseGeography("POINT(3.0 4.0)"), -0.01, UseSpheroid, false /* exclusive */)
require.Error(t, err)
})

Expand All @@ -125,7 +156,7 @@ func TestDWithin(t *testing.T) {
require.NoError(t, err)
b, err := geo.ParseGeography(tc.b)
require.NoError(t, err)
dwithin, err := DWithin(a, b, 0, useSphereOrSpheroid)
dwithin, err := DWithin(a, b, 0, useSphereOrSpheroid, false /* exclusive */)
require.NoError(t, err)
require.False(t, dwithin)
})
Expand Down
38 changes: 27 additions & 11 deletions pkg/geo/geomfn/distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func MinDistance(a *geo.Geometry, b *geo.Geometry) (float64, error) {
if a.SRID() != b.SRID() {
return 0, geo.NewMismatchingSRIDsError(a, b)
}
return minDistanceInternal(a, b, 0, geo.EmptyBehaviorOmit)
return minDistanceInternal(a, b, 0, geo.EmptyBehaviorOmit, false /* exclusive */)
}

// MaxDistance returns the maximum distance across every pair of points comprising
Expand All @@ -52,7 +52,9 @@ func MaxDistance(a *geo.Geometry, b *geo.Geometry) (float64, error) {
}

// DWithin determines if any part of geometry A is within D units of geometry B.
func DWithin(a *geo.Geometry, b *geo.Geometry, d float64) (bool, error) {
// If exclusive is false, DWithin is equivalent to Distance(a, b) <= d.
// Otherwise, DWithin is equivalent to Distance(a, b) < d.
func DWithin(a *geo.Geometry, b *geo.Geometry, d float64, exclusive bool) (bool, error) {
if a.SRID() != b.SRID() {
return false, geo.NewMismatchingSRIDsError(a, b)
}
Expand All @@ -62,14 +64,17 @@ func DWithin(a *geo.Geometry, b *geo.Geometry, d float64) (bool, error) {
if !a.CartesianBoundingBox().Buffer(d, d).Intersects(b.CartesianBoundingBox()) {
return false, nil
}
dist, err := minDistanceInternal(a, b, d, geo.EmptyBehaviorError)
dist, err := minDistanceInternal(a, b, d, geo.EmptyBehaviorError, exclusive)
if err != nil {
// In case of any empty geometries return false.
if geo.IsEmptyGeometryError(err) {
return false, nil
}
return false, err
}
if exclusive {
return dist < d, nil
}
return dist <= d, nil
}

Expand Down Expand Up @@ -112,7 +117,7 @@ func ShortestLineString(a *geo.Geometry, b *geo.Geometry) (*geo.Geometry, error)
if a.SRID() != b.SRID() {
return nil, geo.NewMismatchingSRIDsError(a, b)
}
u := newGeomMinDistanceUpdater(0)
u := newGeomMinDistanceUpdater(0 /*stopAfter */, false /* exclusive */)
return distanceLineStringInternal(a, b, u, geo.EmptyBehaviorOmit)
}

Expand Down Expand Up @@ -158,9 +163,13 @@ func maxDistanceInternal(
// minDistanceInternal finds the minimum distance between two geometries.
// This implementation is done in-house, as compared to using GEOS.
func minDistanceInternal(
a *geo.Geometry, b *geo.Geometry, stopAfterLE float64, emptyBehavior geo.EmptyBehavior,
a *geo.Geometry,
b *geo.Geometry,
stopAfter float64,
emptyBehavior geo.EmptyBehavior,
exclusive bool,
) (float64, error) {
u := newGeomMinDistanceUpdater(stopAfterLE)
u := newGeomMinDistanceUpdater(stopAfter, exclusive)
c := &geomDistanceCalculator{updater: u, boundingBoxIntersects: a.CartesianBoundingBox().Intersects(b.CartesianBoundingBox())}
return distanceInternal(a, b, c, emptyBehavior)
}
Expand Down Expand Up @@ -376,10 +385,13 @@ func (c *geomGeodistEdgeCrosser) ChainCrossing(p geodist.Point) (bool, geodist.P

// geomMinDistanceUpdater finds the minimum distance using geom calculations.
// And preserve the line's endpoints as geom.Coord which corresponds to minimum
// distance. Methods will return early if it finds a minimum distance <= stopAfterLE.
// distance. If exclusive is false, methods will return early if it finds a
// minimum distance <= stopAfter. Otherwise, methods will return early if it
// finds a minimum distance < stopAfter.
type geomMinDistanceUpdater struct {
currentValue float64
stopAfterLE float64
stopAfter float64
exclusive bool
// coordA represents the first vertex of the edge that holds the maximum distance.
coordA geom.Coord
// coordB represents the second vertex of the edge that holds the maximum distance.
Expand All @@ -392,10 +404,11 @@ var _ geodist.DistanceUpdater = (*geomMinDistanceUpdater)(nil)

// newGeomMinDistanceUpdater returns a new geomMinDistanceUpdater with the
// correct arguments set up.
func newGeomMinDistanceUpdater(stopAfterLE float64) *geomMinDistanceUpdater {
func newGeomMinDistanceUpdater(stopAfter float64, exclusive bool) *geomMinDistanceUpdater {
return &geomMinDistanceUpdater{
currentValue: math.MaxFloat64,
stopAfterLE: stopAfterLE,
stopAfter: stopAfter,
exclusive: exclusive,
coordA: nil,
coordB: nil,
geometricalObjOrder: geometricalObjectsNotFlipped,
Expand All @@ -422,7 +435,10 @@ func (u *geomMinDistanceUpdater) Update(aPoint geodist.Point, bPoint geodist.Poi
u.coordA = a
u.coordB = b
}
return dist <= u.stopAfterLE
if u.exclusive {
return dist < u.stopAfter
}
return dist <= u.stopAfter
}
return false
}
Expand Down
Loading

0 comments on commit 2088d55

Please sign in to comment.