Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

geo/geomfn: implement Intersection, PointOnSurface, Union #49833

Merged
merged 1 commit into from
Jun 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,9 @@ given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_interiorringn"></a><code>st_interiorringn(geometry: geometry, n: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the n-th (1-indexed) interior ring of a Polygon as a LineString. Returns NULL if the shape is not a Polygon, or the ring does not exist.</p>
</span></td></tr>
<tr><td><a name="st_intersection"></a><code>st_intersection(geometry_a: geometry, geometry_b: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the point intersections of the given geometries.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_intersects"></a><code>st_intersects(geography_a: geography, geography_b: geography) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns true if geography_a shares any portion of space with geography_b.</p>
<p>The calculations performed are have a precision of 1cm.</p>
<p>This function utilizes the S2 library for spherical calculations.</p>
Expand Down Expand Up @@ -1088,6 +1091,9 @@ given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_pointn"></a><code>st_pointn(geometry: geometry, n: <a href="int.html">int</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the n-th Point of a LineString (1-indexed). Returns NULL if out of bounds or not a LineString.</p>
</span></td></tr>
<tr><td><a name="st_pointonsurface"></a><code>st_pointonsurface(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a point that intersects with the given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_polyfromtext"></a><code>st_polyfromtext(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 Polygon, 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_polyfromtext"></a><code>st_polyfromtext(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 Polygon, NULL is returned.</p>
Expand Down Expand Up @@ -1160,6 +1166,9 @@ given Geometry.</p>
<tr><td><a name="st_transform"></a><code>st_transform(geometry: geometry, to_proj_text: <a href="string.html">string</a>) &rarr; geometry</code></td><td><span class="funcdesc"><p>Transforms a geometry into the coordinate reference system referenced by the projection text by projecting its coordinates.</p>
<p>This function utilizes the PROJ library for coordinate projections.</p>
</span></td></tr>
<tr><td><a name="st_union"></a><code>st_union(geometry_a: geometry, geometry_b: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns the union of the given geometries as a single Geometry object.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_within"></a><code>st_within(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 completely inside geometry_b.</p>
<p>This function utilizes the GEOS module.</p>
<p>This function will automatically use any available index.</p>
Expand Down
6 changes: 6 additions & 0 deletions pkg/geo/geo.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ func (g *Geometry) BoundingBoxIntersects(o *Geometry) bool {
return g.SpatialObject.BoundingBox.Intersects(o.SpatialObject.BoundingBox)
}

// Layout returns the geom layout of the given geometry.
func (g *Geometry) Layout() geom.Layout {
// We are currently always 2D.
return geom.XY
}

//
// Geography
//
Expand Down
65 changes: 65 additions & 0 deletions pkg/geo/geomfn/topology_operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package geomfn

import (
"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/cockroachdb/cockroach/pkg/geo/geos"
"github.com/twpayne/go-geom"
)

// Centroid returns the Centroid of a given Geometry.
func Centroid(g *geo.Geometry) (*geo.Geometry, error) {
if g.Empty() {
return geo.NewGeometryFromGeom(geom.NewPointEmpty(g.Layout()))
}
centroidEWKB, err := geos.Centroid(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(centroidEWKB)
}

// PointOnSurface returns the PointOnSurface of a given Geometry.
func PointOnSurface(g *geo.Geometry) (*geo.Geometry, error) {
if g.Empty() {
return geo.NewGeometryFromGeom(geom.NewPointEmpty(g.Layout()))
}
pointOnSurfaceEWKB, err := geos.PointOnSurface(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(pointOnSurfaceEWKB)
}

// Intersection returns the geometries of intersection between A and B.
func Intersection(a *geo.Geometry, b *geo.Geometry) (*geo.Geometry, error) {
if a.SRID() != b.SRID() {
return nil, geo.NewMismatchingSRIDsError(a, b)
}
retEWKB, err := geos.Intersection(a.EWKB(), b.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(retEWKB)
}

// Union returns the geometries of intersection between A and B.
func Union(a *geo.Geometry, b *geo.Geometry) (*geo.Geometry, error) {
if a.SRID() != b.SRID() {
return nil, geo.NewMismatchingSRIDsError(a, b)
}
retEWKB, err := geos.Union(a.EWKB(), b.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(retEWKB)
}
149 changes: 149 additions & 0 deletions pkg/geo/geomfn/topology_operations_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright 2020 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.

package geomfn

import (
"fmt"
"testing"

"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
)

func TestCentroid(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT (1.0 1.0)"},
{"SRID=4326;POINT(1.0 1.0)", "SRID=4326;POINT (1.0 1.0)"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "POINT (2.0 2.0)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POINT (0.666666666666667 0.333333333333333)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))", "POINT (0.671717171717172 0.335353535353535)"},
{"MULTIPOINT((1.0 1.0), (2.0 2.0))", "POINT (1.5 1.5)"},
{"MULTILINESTRING((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))", "POINT (3.17541743733684 3.04481549985497)"},
{"MULTIPOLYGON(((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))", "POINT (2.17671691792295 1.84187604690117)"},
{"GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))", "POINT (35 38.3333333333333)"},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := Centroid(g)
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)

// Ensure points are close in terms of precision.
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)
require.Equal(t, expected.SRID(), ret.SRID())
})
}
}

func TestPointOnSurface(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT (1.0 1.0)"},
{"SRID=4326;POINT(1.0 1.0)", "SRID=4326;POINT (1.0 1.0)"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "POINT (2.0 2.0)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POINT(0.75 0.5)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))", "POINT(0.8 0.6)"},
{"MULTIPOINT((1.0 1.0), (2.0 2.0))", "POINT (1 1)"},
{"MULTILINESTRING((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))", "POINT (2 2)"},
{"MULTIPOLYGON(((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))", "POINT(3.75 3.5)"},
{"GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))", "POINT(39.5833333333333 35)"},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := PointOnSurface(g)
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)

// Ensure points are close in terms of precision.
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)
require.Equal(t, expected.SRID(), ret.SRID())
})
}
}

func TestIntersection(t *testing.T) {
testCases := []struct {
a *geo.Geometry
b *geo.Geometry
expected *geo.Geometry
}{
{rightRect, rightRect, geo.MustParseGeometry("POLYGON ((1 0, 0 0, 0 1, 1 1, 1 0))")},
{rightRect, rightRectPoint, rightRectPoint},
{rightRectPoint, rightRectPoint, rightRectPoint},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("tc:%d", i), func(t *testing.T) {
g, err := Intersection(tc.a, tc.b)
require.NoError(t, err)
require.Equal(t, tc.expected, g)
})
}

t.Run("errors if SRIDs mismatch", func(t *testing.T) {
_, err := Intersection(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
requireMismatchingSRIDError(t, err)
})
}

func TestUnion(t *testing.T) {
testCases := []struct {
a *geo.Geometry
b *geo.Geometry
expected *geo.Geometry
}{
{rightRect, rightRect, geo.MustParseGeometry("POLYGON ((1 0, 0 0, 0 1, 1 1, 1 0))")},
{rightRect, rightRectPoint, geo.MustParseGeometry("POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))")},
{rightRectPoint, rightRectPoint, rightRectPoint},
{leftRect, rightRect, geo.MustParseGeometry("POLYGON ((0 0, -1 0, -1 1, 0 1, 1 1, 1 0, 0 0))")},
}

for i, tc := range testCases {
t.Run(fmt.Sprintf("tc:%d", i), func(t *testing.T) {
g, err := Union(tc.a, tc.b)
require.NoError(t, err)
require.Equal(t, tc.expected, g)
})
}

t.Run("errors if SRIDs mismatch", func(t *testing.T) {
_, err := Union(mismatchingSRIDGeometryA, mismatchingSRIDGeometryB)
requireMismatchingSRIDError(t, err)
})
}
19 changes: 0 additions & 19 deletions pkg/geo/geomfn/unary_operators.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,6 @@ import (
"github.com/twpayne/go-geom/encoding/ewkb"
)

// Centroid returns the Centroid of a given Geometry.
func Centroid(g *geo.Geometry) (*geo.Geometry, error) {
// Empty geometries do not react well in GEOS, so we have to
// convert and check beforehand.
// Remove after #49209 is resolved.
t, err := g.AsGeomT()
if err != nil {
return nil, err
}
if t.Empty() {
return geo.NewGeometryFromGeom(geom.NewPointEmpty(geom.XY))
}
centroidEWKB, err := geos.Centroid(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(centroidEWKB)
}

// Length returns the length of a given Geometry.
// Note only (MULTI)LINESTRING objects have a length.
// (MULTI)POLYGON objects should use Perimeter.
Expand Down
40 changes: 0 additions & 40 deletions pkg/geo/geomfn/unary_operators_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,8 @@ import (

"github.com/cockroachdb/cockroach/pkg/geo"
"github.com/stretchr/testify/require"
"github.com/twpayne/go-geom"
)

func TestCentroid(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT (1.0 1.0)"},
{"SRID=4326;POINT(1.0 1.0)", "SRID=4326;POINT (1.0 1.0)"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "POINT (2.0 2.0)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POINT (0.666666666666667 0.333333333333333)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1))", "POINT (0.671717171717172 0.335353535353535)"},
{"MULTIPOINT((1.0 1.0), (2.0 2.0))", "POINT (1.5 1.5)"},
{"MULTILINESTRING((1.0 1.0, 2.0 2.0, 3.0 3.0), (6.0 6.0, 7.0 6.0))", "POINT (3.17541743733684 3.04481549985497)"},
{"MULTIPOLYGON(((3.0 3.0, 4.0 3.0, 4.0 4.0, 3.0 3.0)), ((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0), (0.1 0.1, 0.2 0.1, 0.2 0.2, 0.1 0.1)))", "POINT (2.17671691792295 1.84187604690117)"},
{"GEOMETRYCOLLECTION (POINT (40 10),LINESTRING (10 10, 20 20, 10 40),POLYGON ((40 40, 20 45, 45 30, 40 40)))", "POINT (35 38.3333333333333)"},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := Centroid(g)
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)

// Ensure points are close in terms of precision.
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)
require.Equal(t, expected.SRID(), ret.SRID())
})
}
}

func TestLength(t *testing.T) {
testCases := []struct {
wkt string
Expand Down
Loading