diff --git a/geos/entrypoints.go b/geos/entrypoints.go index d66ee1a0..2dce44bd 100644 --- a/geos/entrypoints.go +++ b/geos/entrypoints.go @@ -15,6 +15,16 @@ package geos GEOSGeometry *GEOSMakeValid_r(GEOSContextHandle_t handle, const GEOSGeometry* g) { return NULL; } #endif +#define COVERAGE_UNION_MIN_VERSION "3.8.0" +#define COVERAGE_UNION_MISSING ( \ + GEOS_VERSION_MAJOR < 3 || \ + (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 8) \ +) +#if COVERAGE_UNION_MISSING +// This stub implementation always fails: +GEOSGeometry *GEOSCoverageUnion_r(GEOSContextHandle_t handle, const GEOSGeometry* g) { return NULL; } +#endif + */ import "C" @@ -337,3 +347,29 @@ func MakeValid(g geom.Geometry, opts ...geom.ConstructorOption) (geom.Geometry, }) return result, wrap(err, "executing GEOSMakeValid_r") } + +// 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: +// +// 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. +func CoverageUnion(g geom.Geometry, opts ...geom.ConstructorOption) (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 { + return C.GEOSCoverageUnion_r(ctx, g) + }) + return result, wrap(err, "executing GEOSCoverageUnion_r") +} diff --git a/geos/entrypoints_test.go b/geos/entrypoints_test.go index 9f1c590a..bc1c7418 100644 --- a/geos/entrypoints_test.go +++ b/geos/entrypoints_test.go @@ -829,3 +829,80 @@ func TestMakeValid(t *testing.T) { }) } } + +func TestCoverageUnion(t *testing.T) { + for i, tc := range []struct { + input string + output string + wantErr bool + }{ + { + // Noded correctly (shared edge). + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 0,0 0)), + POLYGON((1 1,0 1,1 0,1 1)) + )`, + output: `POLYGON((0 0,0 1,1 1,1 0,0 0))`, + }, + { + // Noded correctly (shared vertex but no shared edge). + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 1,1 0,0 0)), + POLYGON((1 1,1 2,2 2,2 1,1 1)) + )`, + output: `MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((1 1,1 2,2 2,2 1,1 1)))`, + }, + { + // Noded correctly (completely disjoint). + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 1,1 0,0 0)), + POLYGON((2 2,2 3,3 3,3 2,2 2)) + )`, + output: `MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)),((2 2,2 3,3 3,3 2,2 2)))`, + }, + { + // Input constraint violated: inputs overlap. + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 0,0 0)), + POLYGON((0 0,0 1,1 1,1 0,0 0)) + )`, + wantErr: true, + }, + { + // Input constraint violated: inputs overlap and not noded correctly. + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 0,0 0)), + POLYGON((0 0,0 1,1 1,0 0)) + )`, + wantErr: true, + }, + { + // Input constraint violated: not noded correctly. + input: `GEOMETRYCOLLECTION( + POLYGON((0 0,0 1,1 1,1 0,0 0)), + POLYGON((0 1,2 1,2 2,0 2,0 1)) + )`, + wantErr: true, + }, + { + // Input constraint violation: not everything is a polygon. + input: `GEOMETRYCOLLECTION(POINT(1 2),POLYGON((0 0,0 1,1 0,0 0)))`, + wantErr: true, + }, + } { + t.Run(strconv.Itoa(i), func(t *testing.T) { + in := geomFromWKT(t, tc.input) + gotGeom, err := CoverageUnion(in) + if _, ok := err.(unsupportedGEOSVersionError); ok { + t.Skip(err) + } + if tc.wantErr { + expectErr(t, err) + } else { + expectNoErr(t, err) + wantGeom := geomFromWKT(t, tc.output) + expectGeomEq(t, gotGeom, wantGeom, geom.IgnoreOrder) + } + }) + } +}