Skip to content

Commit

Permalink
geomfn: implement validity operators
Browse files Browse the repository at this point in the history
Unfortunately we cannot implement ST_IsValidDetail because it returns a
composite type, which we do not yet support.

Release note (sql change): Implements ST_IsValid, ST_IsValidReason and
ST_MakeValid operators for geometry types.
  • Loading branch information
otan committed Jul 17, 2020
1 parent 5eb3908 commit 45c6cb4
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 2 deletions.
19 changes: 19 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,22 @@ given Geometry.</p>
<p>This function utilizes the GEOS module.</p>
<p>This function variant will attempt to utilize any available geospatial index.</p>
</span></td></tr>
<tr><td><a name="st_isvalid"></a><code>st_isvalid(geometry: geometry) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns whether the geometry is valid as defined by the OGC spec.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_isvalid"></a><code>st_isvalid(geometry: geometry, flags: <a href="int.html">int</a>) &rarr; <a href="bool.html">bool</a></code></td><td><span class="funcdesc"><p>Returns whether the geometry is valid.</p>
<p>For flags=0, validity is defined by the OGC spec.</p>
<p>For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_isvalidreason"></a><code>st_isvalidreason(geometry: geometry) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Returns a string containing the reason the geometry is invalid along with the point of interest, or “Valid Geometry” if it is valid. Validity is defined by the OGC spec.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_isvalidreason"></a><code>st_isvalidreason(geometry: geometry, flags: <a href="int.html">int</a>) &rarr; <a href="string.html">string</a></code></td><td><span class="funcdesc"><p>Returns the reason the geometry is invalid or “Valid Geometry” if it is valid.</p>
<p>For flags=0, validity is defined by the OGC spec.</p>
<p>For flags=1, validity considers self-intersecting rings forming holes as valid as per ESRI. This is not valid under OGC and CRDB spatial operations may not operate correctly.</p>
<p>This function utilizes the GEOS module.</p>
</span></td></tr>
<tr><td><a name="st_length"></a><code>st_length(geography: geography) &rarr; <a href="float.html">float</a></code></td><td><span class="funcdesc"><p>Returns the length of the given geography in meters. Uses a spheroid to perform the operation.</p>
<p>This function utilizes the GeographicLib library for spheroid calculations.</p>
</span></td></tr>
Expand Down Expand Up @@ -1144,6 +1160,9 @@ given Geometry.</p>
</span></td></tr>
<tr><td><a name="st_makepolygon"></a><code>st_makepolygon(outer: geometry, interior: anyelement[]) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a new Polygon with the given outer LineString and interior (hole) LineString(s).</p>
</span></td></tr>
<tr><td><a name="st_makevalid"></a><code>st_makevalid(geometry: geometry) &rarr; geometry</code></td><td><span class="funcdesc"><p>Returns a valid form of the given geometry according to the OGC spec.</p>
<p>This function utilizes the GEOS module.</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 across every pair of points comprising 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>
Expand Down
74 changes: 74 additions & 0 deletions pkg/geo/geomfn/validity_check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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"
)

// ValidDetail contains information about the validity of a geometry.
type ValidDetail struct {
IsValid bool
// Reason is only populated if IsValid = false.
Reason string
// InvalidLocation is only populated if IsValid = false.
InvalidLocation *geo.Geometry
}

// IsValid returns whether the given Geometry is valid.
func IsValid(g *geo.Geometry) (bool, error) {
isValid, err := geos.IsValid(g.EWKB())
if err != nil {
return false, err
}
return isValid, nil
}

// IsValidReason returns the reasoning for whether the Geometry is valid or invalid.
func IsValidReason(g *geo.Geometry) (string, error) {
reason, err := geos.IsValidReason(g.EWKB())
if err != nil {
return "", err
}
return reason, nil
}

// IsValidDetail returns information about the validity of a Geometry.
// It takes in a flag parameter which behaves the same as the GEOS module, where 1
// means that self-intersecting rings forming holes are considered valid.
func IsValidDetail(g *geo.Geometry, flags int) (ValidDetail, error) {
isValid, reason, locEWKB, err := geos.IsValidDetail(g.EWKB(), flags)
if err != nil {
return ValidDetail{}, err
}
var loc *geo.Geometry
if len(locEWKB) > 0 {
loc, err = geo.ParseGeometryFromEWKB(locEWKB)
if err != nil {
return ValidDetail{}, err
}
}
return ValidDetail{
IsValid: isValid,
Reason: reason,
InvalidLocation: loc,
}, nil
}

// MakeValid returns a valid form of the given Geometry.
func MakeValid(g *geo.Geometry) (*geo.Geometry, error) {
validEWKB, err := geos.MakeValid(g.EWKB())
if err != nil {
return nil, err
}
return geo.ParseGeometryFromEWKB(validEWKB)
}
148 changes: 148 additions & 0 deletions pkg/geo/geomfn/validity_check_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// 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"
)

func TestIsValid(t *testing.T) {
testCases := []struct {
wkt string
expected bool
}{
{"POINT(1.0 1.0)", true},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", true},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", true},

{"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", false},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := IsValid(g)
require.NoError(t, err)
require.Equal(t, tc.expected, ret)
})
}
}

func TestIsValidReason(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "Valid Geometry"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "Valid Geometry"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "Valid Geometry"},

{"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))", "Self-intersection[1.5 1.5]"},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := IsValidReason(g)
require.NoError(t, err)
require.Equal(t, tc.expected, ret)
})
}
}

func TestIsValidDetail(t *testing.T) {
testCases := []struct {
wkt string
flags int
expected ValidDetail
}{
{"POINT(1.0 1.0)", 0, ValidDetail{IsValid: true}},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", 0, ValidDetail{IsValid: true}},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", 0, ValidDetail{IsValid: true}},
{"POINT(1.0 1.0)", 1, ValidDetail{IsValid: true}},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", 1, ValidDetail{IsValid: true}},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", 1, ValidDetail{IsValid: true}},
{
"POLYGON ((14 20, 8 45, 20 35, 14 20, 16 30, 12 30, 14 20))",
1,
ValidDetail{IsValid: true},
},

{
"POLYGON ((14 20, 8 45, 20 35, 14 20, 16 30, 12 30, 14 20))",
0,
ValidDetail{
IsValid: false,
Reason: "Ring Self-intersection",
InvalidLocation: geo.MustParseGeometry("POINT(14 20)"),
},
},
{
"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))",
0,
ValidDetail{
IsValid: false,
Reason: "Self-intersection",
InvalidLocation: geo.MustParseGeometry("POINT(1.5 1.5)"),
},
},
}

for _, tc := range testCases {
t.Run(fmt.Sprintf("%s(%d)", tc.wkt, tc.flags), func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)
ret, err := IsValidDetail(g, tc.flags)
require.NoError(t, err)
require.Equal(t, tc.expected, ret)
})
}
}

func TestMakeValid(t *testing.T) {
testCases := []struct {
wkt string
expected string
}{
{"POINT(1.0 1.0)", "POINT(1.0 1.0)"},
{"LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)", "LINESTRING(1.0 1.0, 2.0 2.0, 3.0 3.0)"},
{"POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))", "POLYGON((0.0 0.0, 1.0 0.0, 1.0 1.0, 0.0 0.0))"},

{
"POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))",
"GEOMETRYCOLLECTION(POLYGON((1 1,1.5 1.5,1.5 -1.5,1 1)),LINESTRING(1.5 1.5,2 2))",
},
{
"SRID=4326;POLYGON((1.0 1.0, 2.0 2.0, 1.5 1.5, 1.5 -1.5, 1.0 1.0))",
"SRID=4326;GEOMETRYCOLLECTION(POLYGON((1 1,1.5 1.5,1.5 -1.5,1 1)),LINESTRING(1.5 1.5,2 2))",
},
}

for _, tc := range testCases {
t.Run(tc.wkt, func(t *testing.T) {
g, err := geo.ParseGeometry(tc.wkt)
require.NoError(t, err)

ret, err := MakeValid(g)
require.NoError(t, err)

expected, err := geo.ParseGeometry(tc.expected)
require.NoError(t, err)
require.Equal(t, expected, ret)
})
}
}
111 changes: 111 additions & 0 deletions pkg/geo/geos/geos.cc
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ typedef int (*CR_GEOS_BufferParams_setSingleSided_r)(CR_GEOS_Handle, CR_GEOS_Buf
typedef CR_GEOS_Geometry (*CR_GEOS_BufferWithParams_r)(CR_GEOS_Handle, CR_GEOS_Geometry,
CR_GEOS_BufferParams, double width);

typedef char (*CR_GEOS_isValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry);
typedef char* (*CR_GEOS_isValidReason_r)(CR_GEOS_Handle, CR_GEOS_Geometry);
typedef char (*CR_GEOS_isValidDetail_r)(CR_GEOS_Handle, CR_GEOS_Geometry, int flags, char** reason,
CR_GEOS_Geometry* loc);
typedef CR_GEOS_Geometry (*CR_GEOS_MakeValid_r)(CR_GEOS_Handle, CR_GEOS_Geometry);

typedef int (*CR_GEOS_Area_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*);
typedef int (*CR_GEOS_Length_r)(CR_GEOS_Handle, CR_GEOS_Geometry, double*);

Expand Down Expand Up @@ -158,6 +164,11 @@ struct CR_GEOS {
CR_GEOS_WKBReader_destroy_r GEOSWKBReader_destroy_r;
CR_GEOS_WKBReader_read_r GEOSWKBReader_read_r;

CR_GEOS_isValid_r GEOSisValid_r;
CR_GEOS_isValidReason_r GEOSisValidReason_r;
CR_GEOS_isValidDetail_r GEOSisValidDetail_r;
CR_GEOS_MakeValid_r GEOSMakeValid_r;

CR_GEOS_Area_r GEOSArea_r;
CR_GEOS_Length_r GEOSLength_r;

Expand Down Expand Up @@ -229,6 +240,10 @@ struct CR_GEOS {
INIT(GEOSGeomTypeId_r);
INIT(GEOSSetSRID_r);
INIT(GEOSGetSRID_r);
INIT(GEOSisValid_r);
INIT(GEOSisValidReason_r);
INIT(GEOSisValidDetail_r);
INIT(GEOSMakeValid_r);
INIT(GEOSArea_r);
INIT(GEOSLength_r);
INIT(GEOSGetCentroid_r);
Expand Down Expand Up @@ -513,6 +528,102 @@ CR_GEOS_Status CR_GEOS_Length(CR_GEOS* lib, CR_GEOS_Slice a, double* ret) {
return CR_GEOS_UnaryOperator(lib, lib->GEOSLength_r, a, ret);
}

//
// Validity checking.
//

CR_GEOS_Status CR_GEOS_IsValid(CR_GEOS* lib, CR_GEOS_Slice g, char* ret) {
std::string error;
auto handle = initHandleWithErrorBuffer(lib, &error);
auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g);
*ret = 0;
if (geom != nullptr) {
auto r = lib->GEOSisValid_r(handle, geom);
if (r == 2) {
if (error.length() == 0) {
error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE);
}
} else {
*ret = r;
}
lib->GEOSGeom_destroy_r(handle, geom);
}
lib->GEOS_finish_r(handle);
return toGEOSString(error.data(), error.length());
}

CR_GEOS_Status CR_GEOS_IsValidReason(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* ret) {
std::string error;
auto handle = initHandleWithErrorBuffer(lib, &error);
auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g);
*ret = {.data = NULL, .len = 0};
if (geom != nullptr) {
auto r = lib->GEOSisValidReason_r(handle, geom);
if (r != NULL) {
*ret = toGEOSString(r, strlen(r));
lib->GEOSFree_r(handle, r);
}
lib->GEOSGeom_destroy_r(handle, geom);
}
lib->GEOS_finish_r(handle);
return toGEOSString(error.data(), error.length());
}

CR_GEOS_Status CR_GEOS_IsValidDetail(CR_GEOS* lib, CR_GEOS_Slice g, int flags, char* retIsValid,
CR_GEOS_String* retReason, CR_GEOS_String* retLocationEWKB) {
std::string error;
auto handle = initHandleWithErrorBuffer(lib, &error);
auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g);
*retReason = {.data = NULL, .len = 0};
*retLocationEWKB = {.data = NULL, .len = 0};
if (geom != nullptr) {
char* reason = NULL;
CR_GEOS_Geometry loc = NULL;
auto r = lib->GEOSisValidDetail_r(handle, geom, flags, &reason, &loc);

if (r == 2) {
if (error.length() == 0) {
error.assign(CR_GEOS_NO_ERROR_DEFINED_MESSAGE);
}
} else {
*retIsValid = r;
}

if (reason != NULL) {
*retReason = toGEOSString(reason, strlen(reason));
lib->GEOSFree_r(handle, reason);
}

if (loc != NULL) {
auto srid = lib->GEOSGetSRID_r(handle, geom);
CR_GEOS_writeGeomToEWKB(lib, handle, loc, retLocationEWKB, srid);
lib->GEOSGeom_destroy_r(handle, loc);
}

lib->GEOSGeom_destroy_r(handle, geom);
}
lib->GEOS_finish_r(handle);
return toGEOSString(error.data(), error.length());
}

CR_GEOS_Status CR_GEOS_MakeValid(CR_GEOS* lib, CR_GEOS_Slice g, CR_GEOS_String* validEWKB) {
std::string error;
auto handle = initHandleWithErrorBuffer(lib, &error);
auto geom = CR_GEOS_GeometryFromSlice(lib, handle, g);
*validEWKB = {.data = NULL, .len = 0};
if (geom != nullptr) {
auto validGeom = lib->GEOSMakeValid_r(handle, geom);
if (validGeom != nullptr) {
auto srid = lib->GEOSGetSRID_r(handle, geom);
CR_GEOS_writeGeomToEWKB(lib, handle, validGeom, validEWKB, srid);
lib->GEOSGeom_destroy_r(handle, validGeom);
}
lib->GEOSGeom_destroy_r(handle, geom);
}
lib->GEOS_finish_r(handle);
return toGEOSString(error.data(), error.length());
}

//
// Topology operators.
//
Expand Down
Loading

0 comments on commit 45c6cb4

Please sign in to comment.