Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
54843: Implement ST_ClosestPoint r=otan a=ArjunM98

Implemented `ST_ClosestPoint` on arguments {geometry, geometry} which adopts PostGIS behaviour.

Release note (sql change): Implemented the geometry builtin `ST_ClosestPoint`.

Resolves #48896 

54868: tree: correct sizes for geospatial datums r=sumeerbhola a=otan

These were previously inaccurate, which may result in OOMs.

Release note: None

54871: builtins: add memory accounting for ST_MakeLine/ST_Union aggregates r=sumeerbhola a=otan

Release note: None

Co-authored-by: ArjunM98 <[email protected]>
Co-authored-by: Oliver Tan <[email protected]>
  • Loading branch information
3 people committed Sep 29, 2020
4 parents eaad551 + e9cd18a + 24faab4 + 8d518ef commit 991893b
Show file tree
Hide file tree
Showing 9 changed files with 374 additions and 159 deletions.
2 changes: 2 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,8 @@ from the given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_clipbybox2d"></a><code>st_clipbybox2d(geometry: geometry, box2d: box2d) &rarr; geometry</code></td><td><span class="funcdesc"><p>Clips the geometry to conform to the bounding box specified by box2d.</p>
</span></td></tr>
<tr><td><a name="st_closestpoint"></a><code>st_closestpoint(geometry_a: geometry, geometry_b: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the 2-dimensional point on geometry_a that is closest to geometry_b. This is the first point of the shortest line.</p>
</span></td></tr>
<tr><td><a name="st_collectionextract"></a><code>st_collectionextract(geometry: geometry, type: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Given a collection, returns a multitype consisting only of elements of the specified type. If there are no elements of the given type, an EMPTY geometry is returned. Types are specified as 1=POINT, 2=LINESTRING, 3=POLYGON - other types are not supported.</p>
</span></td></tr>
<tr><td><a name="st_collectionhomogenize"></a><code>st_collectionhomogenize(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the “simplest” representation of a collection’s contents. Collections of a single type will be returned as an appopriate multitype, or a singleton if it only contains a single geometry.</p>
Expand Down
19 changes: 19 additions & 0 deletions pkg/geo/geomfn/distance.go
Original file line number Diff line number Diff line change
Expand Up @@ -775,3 +775,22 @@ func HausdorffDistanceDensify(a, b geo.Geometry, densifyFrac float64) (*float64,
}
return &distance, nil
}

// ClosestPoint returns the first point located on geometry A on the shortest line between the geometries.
func ClosestPoint(a, b geo.Geometry) (geo.Geometry, error) {
shortestLine, err := ShortestLineString(a, b)
if err != nil {
return geo.Geometry{}, err
}
shortestLineT, err := shortestLine.AsGeomT()
if err != nil {
return geo.Geometry{}, err
}
closestPoint, err := geo.MakeGeometryFromPointCoords(
shortestLineT.(*geom.LineString).Coord(0).X(),
shortestLineT.(*geom.LineString).Coord(0).Y())
if err != nil {
return geo.Geometry{}, err
}
return closestPoint, nil
}
112 changes: 112 additions & 0 deletions pkg/geo/geomfn/distance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/cockroachdb/cockroach/pkg/geo/geos"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
)

var distanceTestCases = []struct {
Expand Down Expand Up @@ -889,3 +890,114 @@ func TestHausdorffDistanceDensify(t *testing.T) {
requireMismatchingSRIDError(t, err)
})
}

func TestClosestPoint(t *testing.T) {

testCases := []struct {
name string
geomA string
geomB string
expected string
}{
{"Closest point between a POINT and LINESTRING",
"POINT(100 100)",
"LINESTRING(20 80, 98 190, 110 180, 50 75 )",
"POINT(100 100)",
},
{"Closest point between a LINESTRING and POINT",
"LINESTRING(20 80, 98 190, 110 180, 50 75 )",
"POINT(100 100)",
"POINT(73.0769230769231 115.384615384615)",
},
{"Closest point between 2 POLYGONS",
"POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))",
"POLYGON((15 50, 2 4, 5 6, 12 10, 15 50))",
"POINT(20 40)",
},
{"Closest point between overlapping POLYGONS",
"POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))",
"POLYGON((175 150, 20 40, 50 60, 125 100, 175 150))",
"POINT(175 150)",
},
{"Closest point between partially-overlapping POLYGONS",
"POLYGON((10 10, 14 14, 20 14, 20 10, 10 10))",
"POLYGON((12 12, 16 12, 16 8, 12 8, 12 12))",
"POINT(12 12)",
},
{"Closest point between MULTILINESTRING and POLYGON",
"MULTILINESTRING((0 0, 1 1, 2 2),(3 3, 4 4, 5 5))",
"POLYGON((10 10, 11 11, 14 11, 14 10, 10 10))",
"POINT(5 5)",
},
{"Closest point between MULTILINESTRING and MULTIPOINT",
"MULTILINESTRING((0 0, 1 1, 2 2),(3 3, 4 4, 5 5))",
"MULTIPOINT((2 1),(10 10))",
"POINT(1.5 1.5)",
},
{"Closest point between MULTIPOLYGON and MULTIPOINT",
"MULTIPOLYGON(((0 0,4 0,4 4,0 4,0 0),(1 1,2 1,2 2,1 2,1 1)), ((-1 -1,-1 -2,-2 -2,-2 -1,-1 -1)))",
"MULTIPOINT((20 10),(10 10))",
"POINT(4 4)",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
gA, err := geo.ParseGeometry(tc.geomA)
require.NoError(t, err)
gB, err := geo.ParseGeometry(tc.geomB)
require.NoError(t, err)

ret, err := ClosestPoint(gA, gB)
require.NoError(t, err)
retAsGeomT, err := ret.AsGeomT()
require.NoError(t, err)

expected, err := geo.ParseGeometry(tc.expected)
require.NoError(t, err)
expectedAsGeomT, err := expected.AsGeomT()
require.NoError(t, err)

require.InEpsilon(t, expectedAsGeomT.(*geom.Point).X(), retAsGeomT.(*geom.Point).X(), 2e-10)
require.InEpsilon(t, expectedAsGeomT.(*geom.Point).Y(), retAsGeomT.(*geom.Point).Y(), 2e-10)
})
}

testCasesEmpty := []struct {
name string
geomA string
geomB string
}{
{"Closest point when both geometries are empty",
"LINESTRING EMPTY",
"LINESTRING EMPTY",
},
{"Closest point when first geometry is empty",
"LINESTRING EMPTY",
"POINT(100 100)",
},
{"Closest point when second geometry is empty",
"POINT(100 100)",
"LINESTRING EMPTY",
},
}

t.Run("errors for EMPTY geometries", func(t *testing.T) {
for _, tc := range testCasesEmpty {
t.Run(tc.name, func(t *testing.T) {
a, err := geo.ParseGeometry(tc.geomA)
require.NoError(t, err)
b, err := geo.ParseGeometry(tc.geomB)
require.NoError(t, err)
_, err = ClosestPoint(a, b)
require.Error(t, err)
require.True(t, geo.IsEmptyGeometryError(err))
})
}
})

t.Run("errors if SRIDs mismatch", func(t *testing.T) {
_, err := ClosestPoint(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
requireMismatchingSRIDError(t, err)
})
}
14 changes: 13 additions & 1 deletion pkg/geo/geopb/geopb.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,25 @@

package geopb

import "fmt"
import (
"fmt"
"unsafe"
)

// EWKBHex returns the EWKB-hex version of this data type
func (b *SpatialObject) EWKBHex() string {
return fmt.Sprintf("%X", b.EWKB)
}

// MemSize returns the size of the spatial object in memory.
func (b *SpatialObject) MemSize() uintptr {
var bboxSize uintptr
if bbox := b.BoundingBox; bbox != nil {
bboxSize = unsafe.Sizeof(*bbox)
}
return unsafe.Sizeof(*b) + bboxSize + uintptr(len(b.EWKB))
}

// MultiType returns the corresponding multi-type for a shape type, or unset
// if there is no multi-type.
func (s ShapeType) MultiType() ShapeType {
Expand Down
Loading

0 comments on commit 991893b

Please sign in to comment.