Skip to content

Commit

Permalink
Merge pull request #626 from peterstace/coverage_is_valid_wrapper
Browse files Browse the repository at this point in the history
Wrap the GEOS `CoverageIsValid` function
  • Loading branch information
peterstace authored Jun 17, 2024
2 parents 4c85f2d + d67135d commit 1212171
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 25 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
- Adds a wrapper to the `geos` package for the `GEOSTopologyPreserveSimplify_r`
function (exposed as `TopologyPreserveSimplify`).

- Adds a wrapper to the `geos` package for the `GEOSCoverageSimplifyVW_r`
function (exposed as `CoverageSimplifyVW`).
- Adds wrappers to the `geos` package for the `GEOSCoverageSimplify_r` and
`GEOSCoverageSimplifyVW_r` functions (exposed as `CoverageIsValid` and
`CoverageSimplifyVW`).

## v0.50.0

Expand Down
52 changes: 30 additions & 22 deletions geos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,42 +268,50 @@ func MakeValid(g geom.Geometry) (geom.Geometry, error) {
}

// The CoverageUnion function is used to union polygonal inputs that form a
// coverage, which are typically provided as a GeometryCollection. This method
// is much faster than other unioning methods, but there are some constraints
// that must be met by the inputs to form a valid polygonal coverage. These
// constraints are:
// coverage, provided as a GeometryCollection of Polygons and/or MultiPolygons.
// This method is much faster than other unioning methods, but it relies on the
// input being a valid coverage (see the CoverageIsValid function for details).
//
// 1. all input geometries must be polygonal,
// 2. the interiors of the inputs must not intersect, and
// 3. the common boundaries of adjacent polygons have the same set of vertices in both polygons.
//
// It should be noted that while CoverageUnion may detect constraint violations
// 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.
// CoverageUnion may detect that the input is not a coverage and return an
// error, but this is not guaranteed (causing an invalid result to be returned
// without an error). It is the responsibility of the caller to ensure that the
// is valid before using this function.
//
// The validity of the result is not checked.
func CoverageUnion(g geom.Geometry) (geom.Geometry, error) {
return rawgeos.CoverageUnion(g)
}

// CoverageSimplifyVW simplifies inputs that form a polygon coverage, provided
// as a GeometryCollection of Polygons and/or MultiPolygons. It uses the
// Visvalingam–Whyatt algorithm and relies on the fact that the input is a
// valid polygon coverage, i.e.:
//
// 1. all input geometries must be polygonal,
// 2. the interiors of the inputs must not intersect, and
// 3. the common boundaries of adjacent polygons have the same set of vertices in both polygons.
// CoverageSimplifyVW simplifies a polygon coverage, provided as a
// GeometryCollection of Polygons and/or MultiPolygons. It uses the
// Visvalingam–Whyatt algorithm and relies on the coverage being valid (see the
// CoverageIsValid function for details).
//
// It may not check that these constraints are met, so it's possible that an
// incorrect result is returned without an error if they are not.
// It may not check that the input forms a valid coverage, so it's possible
// that an incorrect result is returned without an error.
//
// The validity of the result is not checked.
func CoverageSimplifyVW(g geom.Geometry, tolerance float64, preserveBoundary bool) (geom.Geometry, error) {
return rawgeos.CoverageSimplifyVW(g, tolerance, preserveBoundary)
}

// CoverageIsValid checks if a coverage (provided as a GeometryCollection) is
// valid. Coverage validity is indicated by the boolean return value. A valid
// coverage must have the following properties:
//
// 1. all input geometries are of type Polygon or MultiPolygon,
//
// 2. the interiors of the inputs do not intersect, and
//
// 3. the common boundaries of adjacent Polygons or MultiPolygons have the
// same set of vertices.
//
// If the coverage is not valid, then the returned geometry shows the invalid
// edges.
func CoverageIsValid(g geom.Geometry, gapWidth float64) (bool, geom.Geometry, error) {
return rawgeos.CoverageIsValid(g, gapWidth)
}

// 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.
Expand Down
62 changes: 62 additions & 0 deletions geos/entrypoints_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ func skipIfUnsupported(tb testing.TB, err error) {
}
}

func expectBoolEq(t *testing.T, got, want bool) {
t.Helper()
if got != want {
t.Errorf("got: %v want: %v", got, want)
}
}

// These tests aren't exhaustive, because we are leveraging GEOS. The
// testing is just enough to make use confident that we're invoking GEOS
// correctly.
Expand Down Expand Up @@ -954,3 +961,58 @@ func TestCoverageSimplifyVW(t *testing.T) {
want := geomFromWKTFile(t, "testdata/coverage_simplify_output.wkt")
expectGeomEq(t, got, want)
}

func TestCoverageIsValid(t *testing.T) {
for _, tc := range []struct {
name string
input string
wantValid bool
wantBadEdges string
}{
{
name: "adjoining trianges",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 0,0 0)),POLYGON((1 0,1 1,0 1,1 0)))",
wantValid: true,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING EMPTY, LINESTRING EMPTY)",
},
{
name: "adjoining trianges with matching center point",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,0.5 0.5,1 0,0 0)),POLYGON((1 0,1 1,0 1,0.5 0.5,1 0)))",
wantValid: true,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING EMPTY, LINESTRING EMPTY)",
},
{
name: "adjoining trianges with mismatching center point in first geometry",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,0.5 0.5,1 0,0 0)),POLYGON((1 0,1 1,0 1,1 0)))",
wantValid: false,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING(0 1,0.5 0.5,1 0), LINESTRING(0 1,1 0))",
},
{
name: "adjoining trianges with mismatching center point in second geometry",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 0,0 0)),POLYGON((1 0,1 1,0 1,0.5 0.5,1 0)))",
wantValid: false,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING(0 1,1 0), LINESTRING(0 1,0.5 0.5,1 0))",
},
{
name: "overlapping trianges with non-common crossing points",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,1 0,0 0)),POLYGON((0 0,1 0,1 1,0 0)))",
wantValid: false,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING(0 1,1 0,0 0),LINESTRING(1 1,0 0,1 0))",
},
{
name: "overlapping trianges with common crossing points",
input: "GEOMETRYCOLLECTION(POLYGON((0 0,0 1,0.5 0.5,1 0,0 0)),POLYGON((0 0,1 0,1 1,0.5 0.5,0 0)))",
wantValid: false,
wantBadEdges: "GEOMETRYCOLLECTION(LINESTRING(0.5 0.5,1 0,0 0),LINESTRING(0.5 0.5,0 0,1 0))",
},
} {
t.Run(tc.name, func(t *testing.T) {
inputG := geomFromWKT(t, tc.input)
valid, badEdges, err := geos.CoverageIsValid(inputG, 0)
skipIfUnsupported(t, err)
expectNoErr(t, err)
expectBoolEq(t, valid, tc.wantValid)
expectGeomEq(t, badEdges, geomFromWKT(t, tc.wantBadEdges))
})
}
}
24 changes: 23 additions & 1 deletion internal/rawgeos/entrypoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,17 @@ GEOSGeometry *GEOSCoverageUnion_r(GEOSContextHandle_t handle, const GEOSGeometry
)
#if COVERAGE_SIMPLIFY_VW_MISSING
// This stub implementation always fails:
GEOSGeometry *GEOSCoverageSimplifyVW_r(GEOSContextHandle_t handle, const GEOSGeometry* g, double tolerance, int preserveTopology) { return NULL; }
GEOSGeometry *GEOSCoverageSimplifyVW_r(GEOSContextHandle_t handle, const GEOSGeometry* g, double tolerance, int preserveBoundary) { return NULL; }
#endif
#define COVERAGE_IS_VALID_MIN_VERSION "3.12.0"
#define COVERAGE_IS_VALID_MISSING ( \
GEOS_VERSION_MAJOR < 3 || \
(GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 12) \
)
#if COVERAGE_IS_VALID_MISSING
// This stub implementation always fails:
int GEOSCoverageIsValid_r(GEOSContextHandle_t handle, const GEOSGeometry* g, double gapWidth, GEOSGeometry** invalidEdges) { return 2; }
#endif
#define CONCAVE_HULL_MIN_VERSION "3.11.0"
Expand Down Expand Up @@ -289,6 +299,18 @@ func CoverageSimplifyVW(g geom.Geometry, tolerance float64, preserveBoundary boo
return result, wrap(err, "executing GEOSCoverageSimplifyVW_r")
}

func CoverageIsValid(g geom.Geometry, gapWidth float64) (bool, geom.Geometry, error) {
if C.COVERAGE_IS_VALID_MISSING != 0 {
return false, geom.Geometry{}, UnsupportedGEOSVersionError{
C.COVERAGE_IS_VALID_MIN_VERSION, "CoverageIsValid",
}
}
ok, edges, err := unaryOpBG(g, func(h C.GEOSContextHandle_t, gh *C.GEOSGeometry, invalidEdges **C.GEOSGeometry) C.int {
return C.GEOSCoverageIsValid_r(h, gh, C.double(gapWidth), invalidEdges)
})
return ok, edges, wrap(err, "executing GEOSCoverageIsValid_r")
}

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)
Expand Down
23 changes: 23 additions & 0 deletions internal/rawgeos/generic_operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,26 @@ func unaryOpI(
})
return int(result), err
}

func unaryOpBG(
g geom.Geometry,
op func(C.GEOSContextHandle_t, *C.GEOSGeometry, **C.GEOSGeometry) C.int,
) (bool, geom.Geometry, error) {
var resultB bool
var resultG geom.Geometry
err := unaryOpE(g, func(h *handle, gh *C.GEOSGeometry) error {
var resultGH *C.GEOSGeometry
var err error
resultB, err = h.boolErr(C.char(op(h.context, gh, &resultGH)))
if err != nil {
return err
}
if resultGH == nil {
return nil
}
defer C.GEOSGeom_destroy(resultGH)
resultG, err = h.decode(resultGH)
return wrap(err, "decoding result")
})
return resultB, resultG, err
}

0 comments on commit 1212171

Please sign in to comment.