Skip to content

Commit

Permalink
Don't validate results from GEOS
Browse files Browse the repository at this point in the history
Recent versions of GEOS use OverlayNG (NG stands for New Generation) for
set operations between geometries (Intersection, Union, etc.). With the
introduction of OverlayNG, invalid geometries are no longer frequently
produced by GEOS. They are either non-existent, or very rare.

Before this change, the `simplefeatures` GEOS wrapper validates results
from GEOS.

This feels like a misalignment:

- Validation isn't free, so validating GEOS results seems like the wrong
  default.

- The GEOS wrapper is doing more than strictly wrapping the GEOS library
  (it is also validating the result). This blurs the lines between what
  simplefeatures is doing and what GEOS is doing.

This change removes that validation. Users can of course still validate
manually by calling the `Validate` method on the result.
  • Loading branch information
peterstace committed Sep 15, 2023
1 parent 211b84c commit a962780
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 55 deletions.
19 changes: 1 addition & 18 deletions geos/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,31 +33,14 @@ func regularPolygon(center geom.XY, radius float64, sides int) geom.Polygon {
return poly
}

func BenchmarkIntersectionWithoutValidation(b *testing.B) {
for _, sz := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("n=%d", sz), func(b *testing.B) {
inputA := regularPolygon(geom.XY{X: 0, Y: 0}, 1.0, sz).AsGeometry()
inputB := regularPolygon(geom.XY{X: 1, Y: 0}, 1.0, sz).AsGeometry()
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, err := Intersection(inputA, inputB, geom.DisableAllValidations)
if err != nil {
b.Fatal(err)
}
}
})
}
}

func BenchmarkNoOp(b *testing.B) {
for _, sz := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("n=%d", sz), func(b *testing.B) {
input := regularPolygon(geom.XY{X: 0, Y: 0}, 1.0, sz).AsGeometry()
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, err := noop(input, geom.DisableAllValidations)
_, err := noop(input)
if err != nil {
b.Fatal(err)
}
Expand Down
4 changes: 4 additions & 0 deletions geos/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
// Its purpose is to provide functionality that has been implemented in GEOS,
// but is not yet available natively in the simplefeatures library.
//
// Results from functions in this package are returned from GEOS unvalidated
// and as-is. Users may call the Validate method on results if they wish to
// check result validity.
//
// The operations in this package ignore Z and M values if they are present.
//
// To use this package, you will need to install the GEOS library.
Expand Down
61 changes: 35 additions & 26 deletions geos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ func Overlaps(a, b geom.Geometry) (bool, error) {
// Union returns a geometry that is the union of the input geometries.
// Formally, the returned geometry will contain a particular point X if and
// only if X is present in either geometry (or both).
func Union(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
g, err := binaryOpG(a, b, opts, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func Union(a, b geom.Geometry) (geom.Geometry, error) {
g, err := binaryOpG(a, b, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSUnion_r(ctx, a, b)
})
return g, wrap(err, "executing GEOSUnion_r")
Expand All @@ -189,8 +191,10 @@ func Union(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, e
// Intersection returns a geometry that is the intersection of the input
// geometries. Formally, the returned geometry will contain a particular point
// X if and only if X is present in both geometries.
func Intersection(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
g, err := binaryOpG(a, b, opts, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func Intersection(a, b geom.Geometry) (geom.Geometry, error) {
g, err := binaryOpG(a, b, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSIntersection_r(ctx, a, b)
})
return g, wrap(err, "executing GEOSIntersection_r")
Expand All @@ -204,7 +208,6 @@ type bufferOptionSet struct {
endCapStyle int
joinStyle int
mitreLimit float64
ctorOpts []geom.ConstructorOption
}

func newBufferOptionSet(opts []BufferOption) bufferOptionSet {
Expand Down Expand Up @@ -278,19 +281,13 @@ func BufferJoinStyleBevel() BufferOption {
}
}

// BufferConstructorOption sets constructor option that are used when
// reconstructing the buffered geometry that is returned from the GEOS lib.
func BufferConstructorOption(opts ...geom.ConstructorOption) BufferOption {
return func(bos *bufferOptionSet) {
bos.ctorOpts = append(bos.ctorOpts, opts...)
}
}

// Buffer returns a geometry that contains all points within the given radius
// of the input geometry.
//
// The validity of the result is not checked.
func Buffer(g geom.Geometry, radius float64, opts ...BufferOption) (geom.Geometry, error) {
optSet := newBufferOptionSet(opts)
result, err := unaryOpG(g, optSet.ctorOpts, func(ctx C.GEOSContextHandle_t, gh *C.GEOSGeometry) *C.GEOSGeometry {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, gh *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSBufferWithStyle_r(
ctx, gh, C.double(radius),
C.int(optSet.quadSegments),
Expand All @@ -306,26 +303,32 @@ func Buffer(g geom.Geometry, radius float64, opts ...BufferOption) (geom.Geometr
// Douglas-Peucker algorithm. Topological invariants may not be maintained,
// e.g. polygons can collapse into linestrings, and holes in polygons may be
// lost.
func Simplify(g geom.Geometry, tolerance float64, opts ...geom.ConstructorOption) (geom.Geometry, error) {
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, gh *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func Simplify(g geom.Geometry, tolerance float64) (geom.Geometry, error) {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, gh *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSSimplify_r(ctx, gh, C.double(tolerance))
})
return result, wrap(err, "executing GEOSSimplify_r")
}

// Difference returns the geometry that represents the parts of input geometry
// A that are not part of input geometry B.
func Difference(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
result, err := binaryOpG(a, b, opts, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func Difference(a, b geom.Geometry) (geom.Geometry, error) {
result, err := binaryOpG(a, b, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSDifference_r(ctx, a, b)
})
return result, wrap(err, "executing GEOSDifference_r")
}

// SymmetricDifference returns the geometry that represents the parts of the
// input geometries that are not part of the other input geometry.
func SymmetricDifference(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
result, err := binaryOpG(a, b, opts, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func SymmetricDifference(a, b geom.Geometry) (geom.Geometry, error) {
result, err := binaryOpG(a, b, func(ctx C.GEOSContextHandle_t, a, b *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSSymDifference_r(ctx, a, b)
})
return result, wrap(err, "executing GEOSSymDifference_r")
Expand All @@ -336,13 +339,15 @@ func SymmetricDifference(a, b geom.Geometry, opts ...geom.ConstructorOption) (ge
// geometry that is valid and similar (but not the same as) the original
// invalid geometry. If the input geometry is valid, then it is returned
// unaltered.
func MakeValid(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
//
// The validity of the result is not checked.
func MakeValid(g geom.Geometry) (geom.Geometry, error) {
if C.MAKE_VALID_MISSING != 0 {
return geom.Geometry{}, unsupportedGEOSVersionError{
C.MAKE_VALID_MIN_VERSION, "MakeValid",
}
}
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSMakeValid_r(ctx, g)
})
return result, wrap(err, "executing GEOSMakeValid_r")
Expand All @@ -362,13 +367,15 @@ func MakeValid(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry,
// and return an error, but this is not guaranteed, and an invalid result may
// be returned without an error. It is the responsibility of the caller to
// ensure that the constraints are met before using this function.
func CoverageUnion(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
//
// The validity of the result is not checked.
func CoverageUnion(g geom.Geometry) (geom.Geometry, error) {
if C.COVERAGE_UNION_MISSING != 0 {
return geom.Geometry{}, unsupportedGEOSVersionError{
C.COVERAGE_UNION_MIN_VERSION, "CoverageUnion",
}
}
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSCoverageUnion_r(ctx, g)
})
return result, wrap(err, "executing GEOSCoverageUnion_r")
Expand All @@ -377,8 +384,10 @@ func CoverageUnion(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geomet
// UnaryUnion is a single argument version of Union. It is most useful when
// supplied with a GeometryCollection, resulting in the union of the
// GeometryCollection's child geometries.
func UnaryUnion(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
//
// The validity of the result is not checked.
func UnaryUnion(g geom.Geometry) (geom.Geometry, error) {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.GEOSUnaryUnion_r(ctx, g)
})
return result, wrap(err, "executing GEOSUnaryUnion_r")
Expand Down
2 changes: 1 addition & 1 deletion geos/entrypoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ type BinaryOperationTestCase struct {
Out string
}

func RunBinaryOperationTest(t *testing.T, fn func(a, b geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error), cases []BinaryOperationTestCase) {
func RunBinaryOperationTest(t *testing.T, fn func(a, b geom.Geometry) (geom.Geometry, error), cases []BinaryOperationTestCase) {
for i, c := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
run := func(rev bool) func(t *testing.T) {
Expand Down
6 changes: 2 additions & 4 deletions geos/generic_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ func binaryOpE(

func binaryOpG(
g1, g2 geom.Geometry,
opts []geom.ConstructorOption,
op func(C.GEOSContextHandle_t, *C.GEOSGeometry, *C.GEOSGeometry) *C.GEOSGeometry,
) (geom.Geometry, error) {
var result geom.Geometry
Expand All @@ -54,7 +53,7 @@ func binaryOpG(
}
defer C.GEOSGeom_destroy(resultGH)
var err error
result, err = h.decode(resultGH, opts)
result, err = h.decode(resultGH)
return wrap(err, "decoding result")
})
return result, err
Expand Down Expand Up @@ -95,7 +94,6 @@ func unaryOpE(g geom.Geometry, op func(*handle, *C.GEOSGeometry) error) error {

func unaryOpG(
g geom.Geometry,
opts []geom.ConstructorOption,
op func(C.GEOSContextHandle_t, *C.GEOSGeometry) *C.GEOSGeometry,
) (geom.Geometry, error) {
var result geom.Geometry
Expand All @@ -111,7 +109,7 @@ func unaryOpG(
defer C.GEOSGeom_destroy(resultGH)
}
var err error
result, err = h.decode(resultGH, opts)
result, err = h.decode(resultGH)
return wrap(err, "decoding result")
})
return result, err
Expand Down
10 changes: 5 additions & 5 deletions geos/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ import (
// noop returns the geometry unaltered, via conversion to and from GEOS. This
// function is only for benchmarking purposes, hence it is not exported or used
// outside of benchmark tests.
func noop(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, error) {
result, err := unaryOpG(g, opts, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
func noop(g geom.Geometry) (geom.Geometry, error) {
result, err := unaryOpG(g, func(ctx C.GEOSContextHandle_t, g *C.GEOSGeometry) *C.GEOSGeometry {
return C.noop(ctx, g)
})
return result, wrap(err, "executing noop")
Expand Down Expand Up @@ -158,7 +158,7 @@ func (h *handle) boolErr(c C.char) (bool, error) {
}
}

func (h *handle) decode(gh *C.GEOSGeometry, opts []geom.ConstructorOption) (geom.Geometry, error) {
func (h *handle) decode(gh *C.GEOSGeometry) (geom.Geometry, error) {
var (
isWKT C.char
size C.size_t
Expand All @@ -171,10 +171,10 @@ func (h *handle) decode(gh *C.GEOSGeometry, opts []geom.ConstructorOption) (geom

if isWKT != 0 {
wkt := C.GoStringN(serialised, C.int(size))
g, err := geom.UnmarshalWKT(wkt, opts...)
g, err := geom.UnmarshalWKT(wkt, geom.DisableAllValidations)
return g, wrap(err, "failed to unmarshal GEOS WKT result")
}
wkb := C.GoBytes(unsafe.Pointer(serialised), C.int(size))
g, err := geom.UnmarshalWKB(wkb, opts...)
g, err := geom.UnmarshalWKB(wkb, geom.DisableAllValidations)
return g, wrap(err, "failed to unmarshal GEOS WKB result")
}
2 changes: 1 addition & 1 deletion internal/perf/set_op_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func BenchmarkSetOperation(b *testing.B) {
}

func adaptGEOSSetOp(
setOp func(_, _ geom.Geometry, _ ...geom.ConstructorOption) (geom.Geometry, error),
setOp func(_, _ geom.Geometry) (geom.Geometry, error),
) func(_, _ geom.Geometry) (geom.Geometry, error) {
return func(g1, g2 geom.Geometry) (geom.Geometry, error) {
return setOp(g1, g2)
Expand Down

0 comments on commit a962780

Please sign in to comment.