Skip to content

Commit

Permalink
geo/geomfn: implement ST_MaxDistance / ST_DFullyWithin
Browse files Browse the repository at this point in the history
MaxDistance/DFullyWithin is a specialisation of distance, where it takes
the maximum of distances found instead and disregards intersections and
closest points. The `stopAfterLE` condition becomes `stopAfterGT`.

Release note (sql change): Implemented the ST_MaxDistance and
ST_DFullyWithin function for geometries.
  • Loading branch information
otan committed May 15, 2020
1 parent 02c168c commit b51da62
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 148 deletions.
4 changes: 4 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,8 @@ has no relationship with the commit order of concurrent transactions.</p>
<p>This function utilizes the GEOS module.</p>
<p>This function will automatically use any available index.</p>
</span></td></tr>
<tr><td><a name="st_dfullywithin"></a><code>st_dfullywithin(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 all of geometry_a is in distance units of geometry_b. In other words, the max distance between geometry_a and geometry_b is less than distance units.</p>
</span></td></tr>
<tr><td><a name="st_distance"></a><code>st_distance(geography_a: geography, geography_b: geography) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Returns the distance in meters between geography_a and 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>This function utilizes the GeographicLib library for spheroid calculations.</p>
<p>This function will automatically use any available index.</p>
Expand Down Expand Up @@ -848,6 +850,8 @@ has no relationship with the commit order of concurrent transactions.</p>
</span></td></tr>
<tr><td><a name="st_linestringfromwkb"></a><code>st_linestringfromwkb(wkb: <a href="bytes.html">bytes</a>, srid: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the Geometry from a WKB representation with an SRID. If the shape underneath is not LineString, NULL is returned.</p>
</span></td></tr>
<tr><td><a name="st_maxdistance"></a><code>st_maxdistance(geometry_a: geometry, geometry_b: geometry) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Returns the maximum distance between the given geometries. Note if the geometries are the same, it will return the maximum distance between the geometry’s vertexes.</p>
</span></td></tr>
<tr><td><a name="st_mlinefromtext"></a><code>st_mlinefromtext(str: <a href="string.html">string</a>, srid: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the Geometry from a WKT or EWKT representation with an SRID. If the shape underneath is not MultiLineString, NULL is returned. If the SRID is present in both the EWKT and the argument, the argument value is used.</p>
</span></td></tr>
<tr><td><a name="st_mlinefromtext"></a><code>st_mlinefromtext(val: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the Geometry from a WKT or EWKT representation. If the shape underneath is not MultiLineString, NULL is returned.</p>
Expand Down
43 changes: 28 additions & 15 deletions pkg/geo/geodist/geodist.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ type DistanceUpdater interface {
OnIntersects() bool
// Distance returns the distance to return so far.
Distance() float64
// IsMaxDistance returns whether the updater is looking for maximum distance.
IsMaxDistance() bool
}

// Crosser is a provided hook that has a series of functions that can help
Expand Down Expand Up @@ -153,11 +155,13 @@ func onPointToEdgeEnds(c DistanceCalculator, a Point, b shapeWithEdges) bool {
if c.DistanceUpdater().Update(a, edge.V1) {
return true
}
// Also project the point to the infinite line of the edge, and compare if the closestPoint
// lies on the edge.
if closestPoint, ok := c.ClosestPointToEdge(edge, a); ok {
if c.DistanceUpdater().Update(a, closestPoint) {
return true
if !c.DistanceUpdater().IsMaxDistance() {
// Also project the point to the infinite line of the edge, and compare if the closestPoint
// lies on the edge.
if closestPoint, ok := c.ClosestPointToEdge(edge, a); ok {
if c.DistanceUpdater().Update(a, closestPoint) {
return true
}
}
}
}
Expand All @@ -179,7 +183,8 @@ func onPointToLineString(c DistanceCalculator, a Point, b LineString) bool {
func onPointToPolygon(c DistanceCalculator, a Point, b Polygon) bool {
// If the exterior ring does not cover the outer ring, we just need to calculate the distance
// to the outer ring.
if !c.PointInLinearRing(a, b.LinearRing(0)) {
// If we are calculating max distance, we only want the distance to the exterior.
if c.DistanceUpdater().IsMaxDistance() || !c.PointInLinearRing(a, b.LinearRing(0)) {
return onPointToEdgeEnds(c, a, b.LinearRing(0))
}
// At this point it may be inside a hole.
Expand All @@ -204,9 +209,11 @@ func onShapeEdgesToShapeEdges(c DistanceCalculator, a shapeWithEdges, b shapeWit
crosser := c.NewCrosser(aEdge, b.Edge(0).V0)
for bEdgeIdx := 0; bEdgeIdx < b.NumEdges(); bEdgeIdx++ {
bEdge := b.Edge(bEdgeIdx)
// If the edges cross, the distance is 0.
if crosser.ChainCrossing(bEdge.V1) {
return c.DistanceUpdater().OnIntersects()
if !c.DistanceUpdater().IsMaxDistance() {
// 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.
Expand All @@ -224,10 +231,12 @@ func onShapeEdgesToShapeEdges(c DistanceCalculator, a shapeWithEdges, b shapeWit
c.DistanceUpdater().Update(toCheck.vertex, toCheck.edge.V1) {
return true
}
// 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
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
}
}
}
}
Expand All @@ -246,7 +255,8 @@ func onLineStringToPolygon(c DistanceCalculator, a LineString, b Polygon) bool {
// In both these cases, we can defer to the edge to edge comparison between the line
// and the exterior ring.
// We use the first point of the linestring for this check.
if !c.PointInLinearRing(a.Vertex(0), b.LinearRing(0)) {
// If we are looking for max distance, we only need to check against the outer ring anyway.
if c.DistanceUpdater().IsMaxDistance() || !c.PointInLinearRing(a.Vertex(0), b.LinearRing(0)) {
return onShapeEdgesToShapeEdges(c, a, b.LinearRing(0))
}

Expand Down Expand Up @@ -299,7 +309,10 @@ func onPolygonToPolygon(c DistanceCalculator, a Polygon, b Polygon) bool {
// that is outside the exterior ring of B.
//
// As such, we only need to compare the exterior rings if we detect this.
if !c.PointInLinearRing(bFirstPoint, a.LinearRing(0)) && !c.PointInLinearRing(aFirstPoint, b.LinearRing(0)) {
//
// If we are only looking at the max distance, we only want to compare exteriors.
if c.DistanceUpdater().IsMaxDistance() ||
(!c.PointInLinearRing(bFirstPoint, a.LinearRing(0)) && !c.PointInLinearRing(aFirstPoint, b.LinearRing(0))) {
return onShapeEdgesToShapeEdges(c, a.LinearRing(0), b.LinearRing(0))
}

Expand Down
5 changes: 5 additions & 0 deletions pkg/geo/geogfn/distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,11 @@ func (u *spheroidMinDistanceUpdater) OnIntersects() bool {
return true
}

// IsMaxDistance implements the geodist.DistanceUpdater interface.
func (u *spheroidMinDistanceUpdater) IsMaxDistance() bool {
return false
}

// spheroidDistanceCalculator implements geodist.DistanceCalculator
type spheroidDistanceCalculator struct {
updater *spheroidMinDistanceUpdater
Expand Down
84 changes: 83 additions & 1 deletion pkg/geo/geomfn/distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ func MinDistance(a *geo.Geometry, b *geo.Geometry) (float64, error) {
return minDistanceInternal(a, b, 0)
}

// MaxDistance returns the maximum distance between geometries A and B.
func MaxDistance(a *geo.Geometry, b *geo.Geometry) (float64, error) {
if a.SRID() != b.SRID() {
return 0, geo.NewMismatchingSRIDsError(a, b)
}
return maxDistanceInternal(a, b, math.MaxFloat64)
}

// 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 a.SRID() != b.SRID() {
Expand All @@ -43,6 +51,29 @@ func DWithin(a *geo.Geometry, b *geo.Geometry, d float64) (bool, error) {
return dist <= d, nil
}

// DFullyWithin determines if any part of geometry A is fully within D units of
// geometry B.
func DFullyWithin(a *geo.Geometry, b *geo.Geometry, d float64) (bool, error) {
if a.SRID() != b.SRID() {
return false, geo.NewMismatchingSRIDsError(a, b)
}
if d < 0 {
return false, errors.Newf("dwithin distance cannot be less than zero")
}
dist, err := maxDistanceInternal(a, b, d)
if err != nil {
return false, err
}
return dist <= d, nil
}

// maxDistanceInternal finds the maximum distance between two geometries.
func maxDistanceInternal(a *geo.Geometry, b *geo.Geometry, stopAfterGT float64) (float64, error) {
u := newGeomMaxDistanceUpdater(stopAfterGT)
c := &geomDistanceCalculator{updater: u}
return distanceInternal(a, b, c)
}

// 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) (float64, error) {
Expand Down Expand Up @@ -273,9 +304,60 @@ func (u *geomMinDistanceUpdater) OnIntersects() bool {
return true
}

// IsMaxDistance implements the geodist.DistanceUpdater interface.
func (u *geomMinDistanceUpdater) IsMaxDistance() bool {
return false
}

// geomMaxDistanceUpdater finds the maximum distance using geom calculations.
// Methods will return early if it finds a distance > stopAfterGT.
type geomMaxDistanceUpdater struct {
currentValue float64
stopAfterGT float64
}

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

// newGeomMaxDistanceUpdater returns a new geomMaxDistanceUpdater with the
// correct arguments set up.
func newGeomMaxDistanceUpdater(stopAfterGT float64) *geomMaxDistanceUpdater {
return &geomMaxDistanceUpdater{
currentValue: 0,
stopAfterGT: stopAfterGT,
}
}

// Distance implements the DistanceUpdater interface.
func (u *geomMaxDistanceUpdater) Distance() float64 {
return u.currentValue
}

// Update implements the geodist.DistanceUpdater interface.
func (u *geomMaxDistanceUpdater) Update(aInterface geodist.Point, bInterface geodist.Point) bool {
a := aInterface.(*geomGeodistPoint).Coord
b := bInterface.(*geomGeodistPoint).Coord

dist := coordNorm(coordSub(a, b))
if dist > u.currentValue {
u.currentValue = dist
return dist > u.stopAfterGT
}
return false
}

// OnIntersects implements the geodist.DistanceUpdater interface.
func (u *geomMaxDistanceUpdater) OnIntersects() bool {
return false
}

// IsMaxDistance implements the geodist.DistanceUpdater interface.
func (u *geomMaxDistanceUpdater) IsMaxDistance() bool {
return true
}

// geomDistanceCalculator implements geodist.DistanceCalculator
type geomDistanceCalculator struct {
updater *geomMinDistanceUpdater
updater geodist.DistanceUpdater
}

var _ geodist.DistanceCalculator = (*geomDistanceCalculator)(nil)
Expand Down
Loading

0 comments on commit b51da62

Please sign in to comment.