Skip to content

Commit

Permalink
Implement ST_ClosestPoint
Browse files Browse the repository at this point in the history
Implemented ST_ClosestPoint on arguments {geometry, geometry} which adopts PostGIS behaviour.

Release note (sql change): Implemented the geometry builtin ST_ClosestPoint.
  • Loading branch information
ArjunM98 committed Sep 29, 2020
1 parent 0c12d8b commit e9cd18a
Show file tree
Hide file tree
Showing 5 changed files with 301 additions and 147 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)
})
}
Loading

0 comments on commit e9cd18a

Please sign in to comment.