diff --git a/NEWS.md b/NEWS.md index 5cdf51ad1f..42484dfc85 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,7 +3,7 @@ xxxx-xx-xx - New things: - C++14 is now required. - - Polygonal coverage operations: CoverageValidator, CoveragePolygonValidator, + - Polygonal coverages: CoverageValidator, CoveragePolygonValidator, CoverageGapFinder, CoverageUnion (JTS-900, Martin Davis & Paul Ramsey) - Support reading and writing M values through WKB and WKT readers/writers (GH-721, Dan Baston) @@ -19,6 +19,8 @@ xxxx-xx-xx - CAPI: GEOSConcaveHullByLength (GH-849, Martin Davis) - CAPI: GEOSGeomGetM (GH-864, Mike Taves) - Voronoi: Add option to create diagram in order consistent with inputs (GH-781, Dan Baston) + - Polygonal coverages: CoverageSimplifier (JTS-911, Martin Davis) + - CAPI: GEOSCoverageIsValid, GEOSCoverageSimplifyVW (GH-867, Paul Ramsey) - Fixes/Improvements: - WKTReader: Fix parsing of Z and M flags in WKTReader (#676 and GH-669, Dan Baston) diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp index f67db11c21..5757a6f0b5 100644 --- a/capi/geos_c.cpp +++ b/capi/geos_c.cpp @@ -1804,4 +1804,20 @@ extern "C" { cx, cy); } + int + GEOSCoverageIsValid( + const Geometry* input, + double gapWidth, + Geometry** invalidEdges) + { + return GEOSCoverageIsValid_r(handle, input, gapWidth, invalidEdges); + } + + Geometry* + GEOSCoverageSimplifyVW(const Geometry* input, double tolerance, int preserveBoundary) + { + return GEOSCoverageSimplifyVW_r(handle, input, tolerance, preserveBoundary); + } + + } /* extern "C" */ diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index bc583020e3..faac0e8a14 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -757,6 +757,24 @@ extern void GEOS_DLL GEOSGeom_destroy_r( GEOSContextHandle_t handle, GEOSGeometry* g); +/* ========= Coverages ========= */ + +/** \see GEOSCoverageIsValid */ +int +GEOSCoverageIsValid_r( + GEOSContextHandle_t extHandle, + const GEOSGeometry* input, + double gapWidth, + GEOSGeometry** output); + +/** \see GEOSCoverageSimplify */ +extern GEOSGeometry GEOS_DLL * +GEOSCoverageSimplifyVW_r( + GEOSContextHandle_t extHandle, + const GEOSGeometry* input, + double tolerance, + int preserveBoundary); + /* ========= Topology Operations ========= */ /** \see GEOSEnvelope */ @@ -3719,6 +3737,74 @@ extern GEOSGeometry GEOS_DLL *GEOSOffsetCurve(const GEOSGeometry* g, ///@} + +/* ====================================================================== */ +/** @name Coverages +* Functions to work with coverages represented by lists of polygons +* that exactly share edge geometry. +*/ +///@{ + +/** +* Analyze a coverage (represented as a collection of polygonal geometry +* with exactly matching edge geometry) to find places where the +* assumption of exactly matching edges is not met. +* +* \param input The polygonal coverage to access, +* stored in a geometry collection. All members must be POLYGON +* or MULTIPOLYGON. +* \param gapWidth The maximum width of gaps to detect. +* \param invalidEdges When there are invalidities in the coverage, +* this pointer +* will be set with a geometry collection of the same length as +* the input, with a MULTILINESTRING of the error edges for each +* invalid polygon, or an EMPTY where the polygon is a valid +* participant in the coverage. Pass NULL if you do not want +* the invalid edges returned. +* \return A value of 1 for a valid coverage, 0 for invalid and 2 for +* an exception or error. Invalidity includes polygons that overlap, +* that have gaps smaller than the gapWidth, or non-polygonal +* entries in the input collection. +* +* \since 3.12 +*/ +extern int GEOSCoverageIsValid( + const GEOSGeometry* input, + double gapWidth, + GEOSGeometry** invalidEdges); + +/** +* Operates on a coverage (represented as a list of polygonal geometry +* with exactly matching edge geometry) to apply a Visvalingam–Whyatt +* simplification to the edges, reducing complexity in proportion with +* the provided tolerance, while retaining a valid coverage (no edges +* will cross or touch after the simplification). +* Geometries never disappear, but they may be simplified down to just +* a triangle. Also, some invalid geoms (such as Polygons which have too +* few non-repeated points) will be returned unchanged. +* If the input dataset is not a valid coverage due to overlaps, +* it will still be simplified, but invalid topology such as crossing +* edges will still be invalid. +* +* \param input The polygonal coverage to access, +* stored in a geometry collection. All members must be POLYGON +* or MULTIPOLYGON. +* \param tolerance A tolerance parameter in linear units. +* \param preserveBoundary Use 1 to preserve the outside edges +* of the coverage without simplification, +* 0 to allow them to be simplified. +* \return A collection containing the simplified geometries, or null +* on error. +* +* \since 3.12 +*/ +extern GEOSGeometry GEOS_DLL * GEOSCoverageSimplifyVW( + const GEOSGeometry* input, + double tolerance, + int preserveBoundary); + +///@} + /* ========== Construction Operations ========== */ /** @name Geometric Constructions * Functions for computing geometric constructions. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index facefb9e3c..fdf987f2f1 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -4063,4 +4065,83 @@ extern "C" { }); } + int + GEOSCoverageIsValid_r(GEOSContextHandle_t extHandle, + const Geometry* input, + double gapWidth, + Geometry** invalidEdges) + { + using geos::coverage::CoverageValidator; + + return execute(extHandle, 2, [&]() { + const GeometryCollection* col = dynamic_cast(input); + if (!col) + throw geos::util::IllegalArgumentException("input is not a collection"); + + // Initialize to nullptr + if (invalidEdges) *invalidEdges = nullptr; + + std::vector coverage; + for (const auto& g : *col) { + coverage.push_back(g.get()); + } + + CoverageValidator cov(coverage); + cov.setGapWidth(gapWidth); + std::vector> invalid = cov.validate(); + bool hasInvalid = CoverageValidator::hasInvalidResult(invalid); + + if (invalidEdges && hasInvalid) { + const GeometryFactory* gf = input->getFactory(); + for (auto& g : invalid) { + // Replace nullptr with 'MULTILINESTRING EMPTY' + if (g == nullptr) { + auto empty = gf->createEmpty(GEOS_MULTILINESTRING); + g.reset(empty.release()); + } + } + auto r = gf->createGeometryCollection(std::move(invalid)); + *invalidEdges = r.release(); + } + + return hasInvalid ? 0 : 1; + }); + } + + Geometry* + GEOSCoverageSimplifyVW_r(GEOSContextHandle_t extHandle, + const Geometry* input, + double tolerance, + int preserveBoundary) + { + using geos::coverage::CoverageSimplifier; + + return execute(extHandle, [&]() -> Geometry* { + const GeometryCollection* col = dynamic_cast(input); + if (!col) + return nullptr; + + std::vector coverage; + for (const auto& g : *col) { + coverage.push_back(g.get()); + } + CoverageSimplifier cov(coverage); + std::vector> simple; + if (preserveBoundary == 1) { + simple = cov.simplifyInner(tolerance); + } + else if (preserveBoundary == 0) { + simple = cov.simplify(tolerance); + } + else return nullptr; + + const GeometryFactory* gf = input->getFactory(); + std::unique_ptr r = gf->createGeometryCollection(std::move(simple)); + return r.release(); + }); + } + + + + } /* extern "C" */ diff --git a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp new file mode 100644 index 0000000000..8a11cd07ab --- /dev/null +++ b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp @@ -0,0 +1,67 @@ +// +// Test Suite for C-API GEOSCoverageUnion + +#include +// geos +#include +// std +#include +#include +#include +#include + +#include "capi_test_utils.h" + +namespace tut { +// +// Test Group +// + +// Common data used in test cases. +struct test_capicoveragesimplify_data : public capitest::utility { + + test_capicoveragesimplify_data() { + } + + ~test_capicoveragesimplify_data() { + } + +}; + + +typedef test_group group; +typedef group::object object; + +group test_capicoveragesimplify_group("capi::GEOSCoverageSimplify"); + +// +// Test Cases +// + + +template<> +template<> void object::test<1> +() +{ + const char* inputWKT = "GEOMETRYCOLLECTION(POLYGON ((100 100, 200 200, 300 100, 200 101, 100 100)), POLYGON ((150 0, 100 100, 200 101, 300 100, 250 0, 150 0)))"; + + input_ = fromWKT(inputWKT); + result_ = GEOSCoverageSimplifyVW(input_, 10.0, 0); + + ensure( result_ != nullptr ); + ensure( GEOSGeomTypeId(result_) == GEOS_GEOMETRYCOLLECTION ); + + const char* expectedWKT = "GEOMETRYCOLLECTION(POLYGON ((100 100, 200 200, 300 100, 100 100)), POLYGON ((150 0, 100 100, 300 100, 250 0, 150 0)))"; + + expected_ = fromWKT(expectedWKT); + + // std::cout << toWKT(result_) << std::endl; + // std::cout << toWKT(expected_) << std::endl; + + ensure_geometry_equals(result_, expected_, 0.1); +} + + + +} // namespace tut +