[geos-commits] [SCM] GEOS branch main updated. c66d7231d700b075d4644322de4e0b8e0612493d
git at osgeo.org
git at osgeo.org
Wed May 28 13:16:59 PDT 2025
This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "GEOS".
The branch, main has been updated
via c66d7231d700b075d4644322de4e0b8e0612493d (commit)
via 1f03fb038207e884f470c9d30ebbcb33defe73b6 (commit)
via 19da203022b4527e9f385813aefb9ed94ac1cdfe (commit)
via 79d642dff43f95e57aea810598901d2eb51f4e4f (commit)
from 605ecc1f0184f63b320fc66e153da59544db05b0 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit c66d7231d700b075d4644322de4e0b8e0612493d
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Wed May 28 13:16:29 2025 -0700
CoverageCleaner news
diff --git a/NEWS.md b/NEWS.md
index cf107c291..2df858695 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -4,6 +4,7 @@
- New things:
- Add clustering functions to C API (GH-1154, Dan Baston)
- Ported LineDissolver (Paul Ramsey)
+ - Ported CoverageCleaner (Paul Ramsey)
- Breaking Changes:
- Stricter WKT parsing (GH-1241, @freemine)
commit 1f03fb038207e884f470c9d30ebbcb33defe73b6
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Wed May 28 13:15:08 2025 -0700
Port CoverageCleaner and associated classes and tests.
https://github.com/locationtech/jts/pull/1126/files
diff --git a/include/geos/coverage/CleanCoverage.h b/include/geos/coverage/CleanCoverage.h
new file mode 100644
index 000000000..49aee1886
--- /dev/null
+++ b/include/geos/coverage/CleanCoverage.h
@@ -0,0 +1,245 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2025 Martin Davis
+ * Copyright (C) 2025 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/geom/Envelope.h>
+#include <geos/constants.h>
+#include <geos/index/quadtree/Quadtree.h>
+
+#include <vector>
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+ class Geometry;
+ class GeometryFactory;
+ class LineString;
+ class LinearRing;
+ class Polygon;
+}
+namespace operation {
+namespace relateng {
+ class RelateNG;
+}
+}
+namespace index {
+namespace quadtree {
+}
+}
+}
+
+
+namespace geos { // geos.
+namespace coverage { // geos.coverage
+
+class CleanCoverage {
+
+ using Envelope = geos::geom::Envelope;
+ using Geometry = geos::geom::Geometry;
+ using GeometryFactory = geos::geom::GeometryFactory;
+ using LineString = geos::geom::LineString;
+ using LinearRing = geos::geom::LinearRing;
+ using Polygon = geos::geom::Polygon;
+ using RelateNG = geos::operation::relateng::RelateNG;
+ using Quadtree = geos::index::quadtree::Quadtree;
+
+
+public:
+
+ // Classes
+
+ class CleanArea {
+
+ private:
+
+ // Members
+ std::vector<const Polygon*> polys;
+ Envelope env;
+
+ public:
+
+ // Methods
+ void add(const Polygon* poly);
+ const Envelope* getEnvelope();
+ double getBorderLength(const Polygon* adjPoly);
+ double getArea();
+ bool isAdjacent(RelateNG& rel);
+ std::unique_ptr<Geometry> getUnion();
+
+ }; // CleanArea
+
+
+ class MergeStrategy {
+
+ public:
+
+ virtual ~MergeStrategy() = default;
+
+ virtual std::size_t getTarget() const = 0;
+
+ virtual void checkMergeTarget(
+ std::size_t areaIndex,
+ CleanArea* cleanArea,
+ const Polygon* poly) = 0;
+
+ }; // MergeStrategy
+
+
+ class BorderMergeStrategy : public MergeStrategy {
+
+ private:
+
+ std::size_t m_targetIndex = INDEX_UNKNOWN;
+ double m_targetBorderLen;
+
+ public:
+
+ BorderMergeStrategy() {};
+
+ std::size_t getTarget() const override {
+ return m_targetIndex;
+ };
+
+ void checkMergeTarget(std::size_t areaIndex, CleanArea* area, const Polygon* poly) override {
+ double borderLen = area == nullptr ? 0.0 : area->getBorderLength(poly);
+ if (m_targetIndex == INDEX_UNKNOWN || borderLen > m_targetBorderLen) {
+ m_targetIndex = areaIndex;
+ m_targetBorderLen = borderLen;
+ }
+ };
+
+ }; // BorderStrategy
+
+
+ class AreaMergeStrategy : public MergeStrategy {
+
+ private:
+
+ std::size_t m_targetIndex = INDEX_UNKNOWN;
+ double m_targetArea;
+ bool m_isMax;
+
+ public:
+
+ AreaMergeStrategy(bool isMax) : m_isMax(isMax) {};
+
+ std::size_t getTarget() const override {
+ return m_targetIndex;
+ }
+
+ void checkMergeTarget(std::size_t areaIndex, CleanArea* area, const Polygon* poly) override {
+ (void)poly;
+ double areaVal = area == nullptr ? 0.0 : area->getArea();
+ bool isBetter = m_isMax
+ ? areaVal > m_targetArea
+ : areaVal < m_targetArea;
+ if (m_targetIndex == INDEX_UNKNOWN || isBetter) {
+ m_targetIndex = areaIndex;
+ m_targetArea = areaVal;
+ }
+ }
+
+ }; // AreaMergeStrategy
+
+
+ class IndexMergeStrategy : public MergeStrategy {
+
+ private:
+
+ std::size_t m_targetIndex = INDEX_UNKNOWN;
+ bool m_isMax;
+
+ public:
+
+ IndexMergeStrategy(bool isMax) : m_isMax(isMax) {};
+
+ std::size_t getTarget() const override {
+ return m_targetIndex;
+ }
+
+ void checkMergeTarget(std::size_t areaIndex, CleanArea* area, const Polygon* poly) override {
+ (void)area;
+ (void)poly;
+ bool isBetter = m_isMax
+ ? areaIndex > m_targetIndex
+ : areaIndex < m_targetIndex;
+ if (isBetter) {
+ m_targetIndex = areaIndex;
+ }
+ }
+ }; // MergeStrategy
+
+
+private:
+
+ // Members
+
+ /**
+ * The areas in the clean coverage.
+ * Entries may be null, if no resultant corresponded to the input area.
+ */
+ std::vector<std::unique_ptr<CleanArea>> cov;
+ //-- used for finding areas to merge gaps
+ std::unique_ptr<Quadtree> covIndex = nullptr;
+
+ void mergeGap(const Polygon* gap);
+
+ CleanArea* findMaxBorderLength(const Polygon* poly, std::vector<CleanArea*>& areas);
+
+ std::vector<CleanArea*> findAdjacentAreas(const Geometry* poly);
+
+ void createIndex();
+
+
+public:
+
+ // Methods
+
+ CleanCoverage(std::size_t size);
+
+ void add(std::size_t i, const Polygon* poly);
+
+ void mergeOverlap(const Polygon* overlap,
+ MergeStrategy& mergeStrategy,
+ std::vector<std::size_t>& parentIndexes);
+
+ static std::size_t findMergeTarget(const Polygon* poly,
+ MergeStrategy& strategy,
+ std::vector<std::size_t>& parentIndexes,
+ std::vector<std::unique_ptr<CleanArea>>& cov);
+
+ void mergeGaps(std::vector<const Polygon*>& gaps);
+
+ std::vector<std::unique_ptr<Geometry>> toCoverage(const GeometryFactory* geomFactory);
+
+ /**
+ * Disable copy construction and assignment. Apparently needed to make this
+ * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+ */
+ CleanCoverage(const CleanCoverage&) = delete;
+ CleanCoverage& operator=(const CleanCoverage&) = delete;
+
+
+};
+
+} // namespace geos.coverage
+} // namespace geos
+
+
+
+
+
diff --git a/include/geos/coverage/CoverageCleaner.h b/include/geos/coverage/CoverageCleaner.h
new file mode 100644
index 000000000..8452a4925
--- /dev/null
+++ b/include/geos/coverage/CoverageCleaner.h
@@ -0,0 +1,349 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2025 Martin Davis
+ * Copyright (C) 2025 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/index/strtree/TemplateSTRtree.h>
+#include <geos/coverage/CleanCoverage.h>
+#include <geos/geom/Envelope.h>
+#include <geos/constants.h>
+#include <geos/export.h>
+
+#include <vector>
+#include <memory>
+#include <map>
+
+
+// Forward declarations
+namespace geos {
+namespace geom {
+ class Coordinate;
+ class CoordinateSequence;
+ class Geometry;
+ class GeometryFactory;
+ class LineString;
+ class LinearRing;
+ class Polygon;
+}
+namespace index {
+}
+namespace noding {
+ class SegmentString;
+}
+namespace coverage {
+ class CleanCoverage;
+}
+}
+
+namespace geos { // geos.
+namespace coverage { // geos.coverage
+
+
+/**
+ * Cleans the linework of a set of polygonal geometries to form a valid polygonal coverage.
+ * The input is an array of valid Polygon or MultiPolygon geometries
+ * which may contain topological errors such as overlaps and gaps.
+ * Empty or non-polygonal inputs are removed.
+ * Linework is snapped together to eliminate small discrepancies.
+ * Overlaps are merged with an adjacent polygon, according to a given merge strategy.
+ * Gaps narrower than a given width are filled and merged with an adjacent polygon.
+ * The output is an array of polygonal geometries forming a valid polygonal coverage.
+ *
+ * ** Snapping **
+ *
+ * Snapping to nearby vertices and line segment snapping
+ * is used to improve noding robustness
+ * and eliminate small errors in an efficient way,
+ * By default this uses a very small snapping distance
+ * based on the extent of the input data.
+ * The snapping distance may be specified explicitly.
+ * This can reduce the number of overlaps and gaps that need to be merged,
+ * and reduce the risk of spikes formed by merging gaps.
+ * However, a large snapping distance may introduce undesirable
+ * data alteration.
+ *
+ * ** Overlap Merging **
+ *
+ * Overlaps are merged with an adjacent polygon chosen according to a specified merge strategy.
+ * The supported strategies are:
+ *
+ * * **Longest Border**: (default) merge with the polygon with longest shared border (#MERGE_LONGEST_BORDER.)
+ * * **Maximum/Minimum Area**: merge with the polygon with largest or smallest area (#MERGE_MAX_AREA, #MERGE_MIN_AREA.)
+ * * **Minimum Index**: merge with the polygon with the lowest index in the input array (#MERGE_MIN_INDEX.)
+ *
+ * This allows sorting the input according to some criteria to provide a priority
+ * for merging gaps.
+ *
+ * ** Gap Merging **
+ *
+ * Gaps which are wider than a given distance are merged with an adjacent polygon.
+ * Polygon width is determined as twice the radius of the MaximumInscribedCircle
+ * of the gap polygon.
+ * Gaps are merged with the adjacent polygon with longest shared border.
+ * Empty holes in input polygons are treated as gaps, and may be filled in.
+ * Gaps which are not fully enclosed ("inlets") are not removed.
+ *
+ * Cleaning can be run on a valid coverage to remove gaps.
+ *
+ *
+ * The clean result is an array of polygonal geometries
+ * which match one-to-one with the input array.
+ * A result item may be <tt>null</tt> if:
+ *
+ * * the input item is non-polygonal or empty
+ * * the input item is so small it is snapped to collapse
+ * * the input item is covered by another input item
+ * (which may be a larger or a duplicate (nearly or exactly) geometry)
+ *
+ * The result is a valid coverage according to CoverageValidator#isValid();
+ *
+ * ** Known Issues **
+ *
+ * * Long narrow gaps adjacent to multiple polygons may form spikes when merged with a single polygon.
+ *
+ * ** Future Enhancements **
+ *
+ * * Provide an area-based tolerance for gap merging
+ * * Prevent long narrow gaps from forming spikes by partitioning them before merging.
+ * * Allow merging narrow parts of a gap while leaving wider portions.
+ * * Support a priority value for each input polygon to control overlap and gap merging
+ * (this could also allow blocking polygons from being merge targets)
+ *
+ * @see CoverageValidator
+ * @author Martin Davis
+ *
+ */
+class GEOS_DLL CoverageCleaner {
+
+ using Coordinate = geos::geom::Coordinate;
+ using CoordinateSequence = geos::geom::CoordinateSequence;
+ using Geometry = geos::geom::Geometry;
+ using GeometryFactory = geos::geom::GeometryFactory;
+ using Point = geos::geom::Point;
+ using Polygon = geos::geom::Polygon;
+ using LineString = geos::geom::LineString;
+ using LinearRing = geos::geom::LinearRing;
+ using Envelope = geos::geom::Envelope;
+ using SegmentString = geos::noding::SegmentString;
+
+public:
+
+ /** Merge strategy that chooses polygon with longest common border */
+ static constexpr int MERGE_LONGEST_BORDER = 0;
+ /** Merge strategy that chooses polygon with maximum area */
+ static constexpr int MERGE_MAX_AREA = 1;
+ /** Merge strategy that chooses polygon with minimum area */
+ static constexpr int MERGE_MIN_AREA = 2;
+ /** Merge strategy that chooses polygon with smallest input index */
+ static constexpr int MERGE_MIN_INDEX = 3;
+
+private:
+
+ std::vector<const Geometry*> coverage;
+ const GeometryFactory* geomFactory;
+ double snappingDistance;
+
+ double gapMaximumWidth = 0.0;
+ int overlapMergeStrategy = MERGE_LONGEST_BORDER;
+ std::unique_ptr<index::strtree::TemplateSTRtree<std::size_t>> covIndex;
+ std::vector<std::unique_ptr<Polygon>> resultants;
+ std::unique_ptr<CleanCoverage> cleanCov;
+ std::map<std::size_t, std::vector<std::size_t>> overlapParentMap;
+ std::vector<const Polygon*> overlaps;
+ std::vector<const Polygon*> gaps;
+ std::vector<const Polygon*> mergableGaps;
+
+ static constexpr double DEFAULT_SNAPPING_FACTOR = 1.0e8;
+
+
+ static double computeDefaultSnappingDistance(
+ std::vector<const Geometry*>& geoms);
+
+ static Envelope extent(std::vector<const Geometry*>& geoms);
+
+ void mergeOverlaps(
+ std::map<std::size_t, std::vector<std::size_t>>& overlapMap);
+
+ std::unique_ptr<CleanCoverage::MergeStrategy> mergeStrategy(
+ int mergeStrategyId);
+
+ void computeResultants(double tolerance);
+
+ void createCoverageIndex();
+
+ void classifyResult(std::vector<std::unique_ptr<Polygon>>& rs);
+
+ void classifyResultant(std::size_t resultIndex, const Polygon* resPoly);
+
+ static bool covers(const Geometry* poly, const Point* intPt);
+
+ std::vector<const Polygon*> findMergableGaps(
+ std::vector<const Polygon*> gaps);
+
+ bool isMergableGap(const Polygon* gap);
+
+ static std::vector<std::unique_ptr<geom::Polygon>> polygonize(
+ const Geometry* cleanEdges);
+
+ static bool isPolygonal(const Geometry* geom);
+
+ static std::vector<const Polygon*> toPolygonArray(
+ const Geometry* geom);
+
+
+public:
+
+ /**
+ * Create a new cleaner instance for a set of polygonal geometries.
+ *
+ * @param coverage an array of polygonal geometries to clean
+ */
+ CoverageCleaner(std::vector<const Geometry*>& coverage);
+
+ /**
+ * Cleans a set of polygonal geometries to form a valid coverage,
+ * allowing all cleaning parameters to be specified.
+ *
+ * @param coverage an array of polygonal geometries to clean
+ * @param snapDistance the distance tolerance for snapping
+ * @param mergeStrategy the strategy to use for merging overlaps
+ * @param maxWidth the maximum width of gaps to merge
+ * @return the clean coverage
+ */
+ static std::vector<std::unique_ptr<Geometry>> clean(
+ std::vector<const Geometry*>& coverage,
+ double snapDistance,
+ int mergeStrategy,
+ double maxWidth);
+
+ /**
+ * Cleans a set of polygonal geometries to form a valid coverage,
+ * using the default overlap merge strategy {@link #MERGE_LONGEST_BORDER}.
+ *
+ * @param coverage an array of polygonal geometries to clean
+ * @param snapDistance the distance tolerance for snapping
+ * @param maxWidth the maximum width of gaps to merge
+ * @return the clean coverage
+ */
+ static std::vector<std::unique_ptr<Geometry>> clean(
+ std::vector<const Geometry*>& coverage,
+ double snapDistance,
+ double maxWidth);
+
+ /**
+ * Cleans a set of polygonal geometries to form a valid coverage,
+ * using the default snapping distance tolerance.
+ *
+ * @param coverage an array of polygonal geometries to clean
+ * @param mergeStrategy the strategy to use for merging overlaps
+ * @param maxWidth the maximum width of gaps to merge
+ * @return the clean coverage
+ */
+ static std::vector<std::unique_ptr<Geometry>> cleanOverlapGap(
+ std::vector<const Geometry*>& coverage,
+ int mergeStrategy,
+ double maxWidth);
+
+ /**
+ * Cleans a set of polygonal geometries to form a valid coverage,
+ * with default snapping tolerance and overlap merging,
+ * and merging gaps which are narrower than a specified width.
+ *
+ * @param coverage an array of polygonal geometries to clean
+ * @param maxWidth the maximum width of gaps to merge
+ * @return the clean coverage
+ */
+ static std::vector<std::unique_ptr<Geometry>> cleanGapWidth(
+ std::vector<const Geometry*>& coverage,
+ double maxWidth);
+
+ /**
+ * Sets the snapping distance tolerance.
+ * The default is to use a small fraction of the input extent diameter.
+ * A distance of zero prevents snapping from being used.
+ *
+ * @param snapDistance the snapping distance tolerance
+ */
+ void setSnappingDistance(double snapDistance);
+
+ /**
+ * Sets the overlap merge strategy to use.
+ * The default is {@link #MERGE_LONGEST_BORDER}.
+ *
+ * @param mergeStrategy the merge strategy code
+ */
+ void setOverlapMergeStrategy(int mergeStrategy);
+
+ /**
+ * Sets the maximum width of the gaps that will be filled and merged.
+ * The width of a gap is twice the radius of the Maximum Inscribed Circle in the gap polygon,
+ * A width of zero prevents gaps from being merged.
+ *
+ * @param maxWidth the maximum gap width to merge
+ */
+ void setGapMaximumWidth(double maxWidth);
+
+ /**
+ * Cleans the coverage.
+ */
+ void clean();
+
+ /**
+ * Gets the cleaned coverage.
+ *
+ * @return the clean coverage
+ */
+ std::vector<std::unique_ptr<Geometry>> getResult();
+
+ /**
+ * Gets polygons representing the overlaps in the input,
+ * which have been merged.
+ *
+ * @return a list of overlap polygons
+ */
+ std::vector<const Polygon*> getOverlaps();
+
+ /**
+ * Gets polygons representing the gaps in the input
+ * which have been merged.
+ *
+ * @return a list of gap polygons
+ */
+ std::vector<const Polygon*> getMergedGaps();
+
+ std::unique_ptr<Geometry> toGeometry(
+ std::vector<SegmentString*>& segStrings,
+ const GeometryFactory* geomFact);
+
+ std::unique_ptr<Geometry> node(
+ std::vector<const Geometry*>& coverage,
+ double snapDistance);
+
+ /**
+ * Disable copy construction and assignment. Apparently needed to make this
+ * class compile under MSVC. (See https://stackoverflow.com/q/29565299)
+ */
+ CoverageCleaner(const CoverageCleaner&) = delete;
+ CoverageCleaner& operator=(const CoverageCleaner&) = delete;
+
+};
+
+} // namespace geos.coverage
+} // namespace geos
+
+
+
+
+
diff --git a/include/geos/coverage/CoverageValidator.h b/include/geos/coverage/CoverageValidator.h
index 2185d23cb..f285ee6e7 100644
--- a/include/geos/coverage/CoverageValidator.h
+++ b/include/geos/coverage/CoverageValidator.h
@@ -115,6 +115,18 @@ public:
static bool isValid(
std::vector<const Geometry*>& coverage);
+ /**
+ * Tests whether a polygonal coverage is valid
+ * and contains no gaps narrower than a specified width.
+ *
+ * @param coverage an array of polygons forming a coverage
+ * @param gapWidth the maximum width of invalid gaps
+ * @return true if the coverage is valid with no narrow gaps
+ */
+ static bool isValid(
+ std::vector<const Geometry*>& coverage,
+ double gapWidth);
+
/**
* Tests if some element of an array of geometries is a coverage invalidity
* indicator.
diff --git a/src/coverage/CleanCoverage.cpp b/src/coverage/CleanCoverage.cpp
new file mode 100644
index 000000000..2b616c5e0
--- /dev/null
+++ b/src/coverage/CleanCoverage.cpp
@@ -0,0 +1,285 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2025 Martin Davis
+ * Copyright (C) 2025 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/coverage/CleanCoverage.h>
+
+#include <geos/coverage/CoverageUnion.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/Polygon.h>
+#include <geos/index/quadtree/Quadtree.h>
+#include <geos/operation/overlayng/OverlayNG.h>
+#include <geos/operation/overlayng/OverlayNGRobust.h>
+#include <geos/operation/relateng/IntersectionMatrixPattern.h>
+#include <geos/operation/relateng/RelateNG.h>
+
+#include <algorithm>
+
+using geos::geom::Envelope;
+using geos::geom::Geometry;
+using geos::geom::GeometryFactory;
+using geos::geom::Polygon;
+using geos::index::quadtree::Quadtree;
+using geos::operation::overlayng::OverlayNG;
+using geos::operation::overlayng::OverlayNGRobust;
+using geos::operation::relateng::IntersectionMatrixPattern;
+using geos::operation::relateng::RelateNG;
+
+
+namespace geos { // geos
+namespace coverage { // geos.coverage
+
+using CleanArea = geos::coverage::CleanCoverage::CleanArea;
+
+/* public */
+CleanCoverage::CleanCoverage(std::size_t size)
+{
+ cov.resize(size);
+}
+
+
+/* public */
+void
+CleanCoverage::add(std::size_t i, const Polygon* poly)
+{
+ if (cov[i] == nullptr) {
+ cov[i] = std::make_unique<CleanArea>();
+ }
+ cov[i]->add(poly);
+}
+
+
+/* public */
+void
+CleanCoverage::mergeOverlap(const Polygon* overlap,
+ MergeStrategy& mergeStrategy,
+ std::vector<std::size_t>& parentIndexes)
+{
+ std::size_t mergeTarget = findMergeTarget(overlap, mergeStrategy, parentIndexes, cov);
+ add(mergeTarget, overlap);
+}
+
+
+/* public static */
+std::size_t
+CleanCoverage::findMergeTarget(const Polygon* poly,
+ MergeStrategy& strategy,
+ std::vector<std::size_t>& parentIndexes,
+ std::vector<std::unique_ptr<CleanArea>>& cov)
+{
+ //-- sort parent indexes ascending, so that overlaps merge to first parent by default
+ std::vector<size_t> indexesAsc;
+ std::copy(parentIndexes.begin(), parentIndexes.end(), back_inserter(indexesAsc));
+ std::sort(indexesAsc.begin(), indexesAsc.end());
+
+ for (std::size_t index : indexesAsc) {
+ strategy.checkMergeTarget(index, cov[index].get(), poly);
+ }
+ return strategy.getTarget();
+}
+
+
+/* public */
+void
+CleanCoverage::mergeGaps(std::vector<const Polygon*>& gaps)
+{
+ createIndex();
+ for (const Polygon* gap : gaps) {
+ mergeGap(gap);
+ }
+}
+
+
+/* private */
+void
+CleanCoverage::mergeGap(const Polygon* gap)
+{
+ std::vector<CleanArea*> adjacents = findAdjacentAreas(gap);
+
+ /**
+ * No adjacent means this is likely an artifact
+ * of an invalid input polygon.
+ * Discard polygon.
+ */
+ if (adjacents.empty())
+ return;
+
+ CleanArea* mergeTarget = findMaxBorderLength(gap, adjacents);
+ covIndex->remove(mergeTarget->getEnvelope(), mergeTarget);
+ mergeTarget->add(gap);
+ covIndex->insert(mergeTarget->getEnvelope(), mergeTarget);
+}
+
+
+/* private */
+CleanArea*
+CleanCoverage::findMaxBorderLength(const Polygon* poly,
+ std::vector<CleanArea*>& areas)
+{
+ double maxLen = 0;
+ CleanArea* maxLenArea = nullptr;
+ for (CleanArea* a : areas) {
+ double len = a->getBorderLength(poly);
+ if (maxLenArea == nullptr || len > maxLen) {
+ maxLen = len;
+ maxLenArea = a;
+ }
+ }
+ return maxLenArea;
+}
+
+
+/* private */
+std::vector<CleanArea*>
+CleanCoverage::findAdjacentAreas(const Geometry* poly)
+{
+ std::vector<CleanArea*> adjacents;
+ auto rel = RelateNG::prepare(poly);
+ const Envelope* queryEnv = poly->getEnvelopeInternal();
+
+ std::vector<void*> candidateAdjIndex;
+ covIndex->query(queryEnv, candidateAdjIndex);
+
+ for (void* ptr : candidateAdjIndex) {
+ CleanArea* area = static_cast<CleanArea*>(ptr);
+ if (area != nullptr && area->isAdjacent(*rel)) {
+ adjacents.push_back(area);
+ }
+ }
+ return adjacents;
+}
+
+
+/* private */
+void
+CleanCoverage::createIndex()
+{
+ covIndex = std::make_unique<Quadtree>();
+ for (std::size_t i = 0; i < cov.size(); i++) {
+ //-- null areas are never merged to
+ if (cov[i] != nullptr) {
+ covIndex->insert(cov[i]->getEnvelope(), static_cast<void*>(cov[i].get()));
+ }
+ }
+}
+
+
+/* public */
+std::vector<std::unique_ptr<Geometry>>
+CleanCoverage::toCoverage(const GeometryFactory* geomFactory)
+{
+ std::vector<std::unique_ptr<Geometry>> cleanCov;
+ cleanCov.resize(cov.size());
+ for (std::size_t i = 0; i < cov.size(); i++) {
+ std::unique_ptr<Geometry> merged;
+ if (cov[i] == nullptr) {
+ cleanCov[i] = geomFactory->createEmpty(2);
+ }
+ else {
+ cleanCov[i] = cov[i]->getUnion();
+ }
+ }
+ return cleanCov;
+}
+
+
+///// CleanCoverage::CleanArea ////////////////////////////////////////
+
+
+/* public */
+void
+CleanCoverage::CleanArea::add(const Polygon* poly)
+{
+ polys.push_back(poly);
+}
+
+
+/* public */
+const Envelope*
+CleanCoverage::CleanArea::getEnvelope()
+{
+ env.init();
+ for (const Polygon* poly : polys) {
+ env.expandToInclude(poly->getEnvelopeInternal());
+ }
+ return &env;
+}
+
+
+/* public */
+double
+CleanCoverage::CleanArea::getBorderLength(const Polygon* adjPoly)
+{
+ //TODO: find optimal way of computing border len given a coverage
+ double len = 0.0;
+ for (const Polygon* poly : polys) {
+ //TODO: find longest connected border len
+ auto border = OverlayNGRobust::Overlay(
+ static_cast<const Geometry*>(poly),
+ static_cast<const Geometry*>(adjPoly),
+ OverlayNG::INTERSECTION);
+ double borderLen = border->getLength();
+ len += borderLen;
+ }
+ return len;
+}
+
+
+/* public */
+double
+CleanCoverage::CleanArea::getArea()
+{
+ //TODO: cache area?
+ double area = 0.0;
+ for (const Polygon* poly : polys) {
+ area += poly->getArea();
+ }
+ return area;
+}
+
+
+/* public */
+bool
+CleanCoverage::CleanArea::isAdjacent(RelateNG& rel)
+{
+ for (const Polygon* poly : polys) {
+ //TODO: is there a faster way to check adjacency in coverage?
+ auto geom = static_cast<const Geometry*>(poly);
+ bool isAdjacent = rel.evaluate(geom, IntersectionMatrixPattern::ADJACENT);
+ if (isAdjacent)
+ return true;
+ }
+ return false;
+}
+
+
+/* public */
+std::unique_ptr<Geometry>
+CleanCoverage::CleanArea::getUnion()
+{
+ std::vector<const Geometry*> geoms;
+ for (const Polygon* poly : polys) {
+ geoms.push_back(static_cast<const Geometry*>(poly));
+ }
+ return CoverageUnion::Union(geoms);
+}
+
+
+
+} // namespace geos.coverage
+} // namespace geos
+
+
diff --git a/src/coverage/CoverageCleaner.cpp b/src/coverage/CoverageCleaner.cpp
new file mode 100644
index 000000000..80c044eb9
--- /dev/null
+++ b/src/coverage/CoverageCleaner.cpp
@@ -0,0 +1,463 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (c) 2025 Martin Davis
+ * Copyright (C) 2025 Paul Ramsey <pramsey at cleverelephant.ca>
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/coverage/CoverageCleaner.h>
+
+#include <geos/algorithm/construct/MaximumInscribedCircle.h>
+#include <geos/algorithm/locate/SimplePointInAreaLocator.h>
+#include <geos/dissolve/LineDissolver.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/MultiPolygon.h>
+#include <geos/geom/Point.h>
+#include <geos/geom/Polygon.h>
+#include <geos/noding/Noder.h>
+#include <geos/noding/SegmentStringUtil.h>
+#include <geos/noding/snap/SnappingNoder.h>
+#include <geos/operation/polygonize/Polygonizer.h>
+#include <geos/operation/relateng/RelateNG.h>
+#include <geos/util/IllegalArgumentException.h>
+
+
+using geos::algorithm::construct::MaximumInscribedCircle;
+using geos::algorithm::locate::SimplePointInAreaLocator;
+using geos::dissolve::LineDissolver;
+using geos::geom::Envelope;
+using geos::geom::Geometry;
+using geos::geom::GeometryFactory;
+using geos::geom::MultiPolygon;
+using geos::geom::Point;
+using geos::geom::Polygon;
+using geos::noding::Noder;
+using geos::noding::SegmentStringUtil;
+using geos::noding::snap::SnappingNoder;
+using geos::operation::polygonize::Polygonizer;
+using geos::operation::relateng::RelateNG;
+
+
+namespace geos { // geos
+namespace coverage { // geos.coverage
+
+
+/* public static */
+std::vector<std::unique_ptr<Geometry>>
+CoverageCleaner::clean(std::vector<const Geometry*>& p_coverage,
+ double p_snappingDistance,
+ int p_overlapMergeStrategy,
+ double p_maxGapWidth)
+{
+ CoverageCleaner cc(p_coverage);
+ cc.setSnappingDistance(p_snappingDistance);
+ cc.setGapMaximumWidth(p_maxGapWidth);
+ cc.setOverlapMergeStrategy(p_overlapMergeStrategy);
+ cc.clean();
+ return cc.getResult();
+}
+
+
+/* public static */
+std::vector<std::unique_ptr<Geometry>>
+CoverageCleaner::clean(std::vector<const Geometry*>& p_coverage,
+ double p_snappingDistance,
+ double p_maxGapWidth)
+{
+ CoverageCleaner cc(p_coverage);
+ cc.setSnappingDistance(p_snappingDistance);
+ cc.setGapMaximumWidth(p_maxGapWidth);
+ cc.clean();
+ return cc.getResult();
+}
+
+
+/* public static */
+std::vector<std::unique_ptr<Geometry>>
+CoverageCleaner::cleanOverlapGap(std::vector<const Geometry*>& p_coverage,
+ int p_overlapMergeStrategy,
+ double p_maxGapWidth)
+{
+ return clean(p_coverage, -1, p_overlapMergeStrategy, p_maxGapWidth);
+}
+
+
+/* public static */
+std::vector<std::unique_ptr<Geometry>>
+CoverageCleaner::cleanGapWidth(std::vector<const Geometry*>& p_coverage,
+ double p_maxGapWidth)
+{
+ return clean(p_coverage, -1, p_maxGapWidth);
+}
+
+
+/* public */
+CoverageCleaner::CoverageCleaner(std::vector<const Geometry*>& p_coverage)
+ : coverage(p_coverage)
+ , geomFactory(p_coverage.empty() ? nullptr : coverage[0]->getFactory())
+ , snappingDistance(computeDefaultSnappingDistance(p_coverage))
+{}
+
+
+/* public */
+void
+CoverageCleaner::setSnappingDistance(double p_snappingDistance)
+{
+ //-- use default distance if invalid argument
+ if (p_snappingDistance < 0)
+ return;
+ snappingDistance = p_snappingDistance;
+}
+
+
+/* public */
+void
+CoverageCleaner::setOverlapMergeStrategy(int mergeStrategy)
+{
+ if (mergeStrategy < MERGE_LONGEST_BORDER ||
+ mergeStrategy > MERGE_MIN_INDEX)
+ throw util::IllegalArgumentException("Invalid merge strategy code");
+
+ overlapMergeStrategy = mergeStrategy;
+}
+
+
+/* public */
+void
+CoverageCleaner::setGapMaximumWidth(double maxWidth)
+{
+ if (maxWidth < 0)
+ return;
+ gapMaximumWidth = maxWidth;
+}
+
+
+/* public */
+void
+CoverageCleaner::clean()
+{
+ computeResultants(snappingDistance);
+ //System.out.format("Overlaps: %d Gaps: %d\n", overlaps.size(), mergableGaps.size());
+
+ //Stopwatch sw = new Stopwatch();
+ mergeOverlaps(overlapParentMap);
+ //System.out.println("Merge Overlaps: " + sw.getTimeString());
+ //sw.reset();
+ cleanCov->mergeGaps(mergableGaps);
+ //System.out.println("Merge Gaps: " + sw.getTimeString());
+}
+
+
+/* public */
+std::vector<std::unique_ptr<Geometry>>
+CoverageCleaner::getResult()
+{
+ return cleanCov->toCoverage(geomFactory);
+}
+
+
+/* public */
+std::vector<const Polygon*>
+CoverageCleaner::getOverlaps()
+{
+ return overlaps;
+}
+
+
+/* public */
+std::vector<const Polygon*>
+CoverageCleaner::getMergedGaps()
+{
+ return mergableGaps;
+}
+
+//-------------------------------------------------
+
+/* private static */
+double
+CoverageCleaner::computeDefaultSnappingDistance(std::vector<const Geometry*>& geoms)
+{
+ double diameter = extent(geoms).getDiameter();
+ return diameter / DEFAULT_SNAPPING_FACTOR;
+}
+
+
+/* private static */
+Envelope
+CoverageCleaner::extent(std::vector<const Geometry*>& geoms)
+{
+ Envelope env;
+ for (const Geometry* geom : geoms) {
+ env.expandToInclude(geom->getEnvelopeInternal());
+ }
+ return env;
+}
+
+
+/* private */
+void
+CoverageCleaner::mergeOverlaps(
+ std::map<std::size_t, std::vector<std::size_t>>& overlapParentMap_p)
+{
+ for (const auto& [resIndex, _] : overlapParentMap_p) {
+ auto ms = mergeStrategy(overlapMergeStrategy);
+ cleanCov->mergeOverlap(
+ resultants[resIndex].get(),
+ *ms,
+ overlapParentMap_p[resIndex]);
+ }
+}
+
+
+/* private */
+std::unique_ptr<CleanCoverage::MergeStrategy>
+CoverageCleaner::mergeStrategy(int mergeStrategyId)
+{
+ switch (mergeStrategyId) {
+ case MERGE_LONGEST_BORDER:
+ return std::make_unique<CleanCoverage::BorderMergeStrategy>();
+ case MERGE_MAX_AREA:
+ return std::make_unique<CleanCoverage::AreaMergeStrategy>(true);
+ case MERGE_MIN_AREA:
+ return std::make_unique<CleanCoverage::AreaMergeStrategy>(false);
+ case MERGE_MIN_INDEX:
+ return std::make_unique<CleanCoverage::IndexMergeStrategy>(false);
+ }
+ throw util::IllegalArgumentException("CoverageCleaner::mergeStrategy - Unknown merge strategy");
+}
+
+
+/* private */
+void
+CoverageCleaner::computeResultants(double tolerance)
+{
+ //System.out.println("Coverage Cleaner ===> polygons: " + coverage.length);
+ //System.out.format("Snapping distance: %f\n", snappingDistance);
+ //Stopwatch sw = new Stopwatch();
+ //sw.start();
+
+ std::unique_ptr<Geometry> nodedEdges = node(coverage, tolerance);
+ //System.out.println("Noding: " + sw.getTimeString());
+
+ //sw.reset();
+ std::unique_ptr<Geometry> cleanEdges = LineDissolver::dissolve(nodedEdges.get());
+ //System.out.println("Dissolve: " + sw.getTimeString());
+
+ //sw.reset();
+ resultants = polygonize(cleanEdges.get());
+ //System.out.println("Polygonize: " + sw.getTimeString());
+
+ cleanCov = std::make_unique<CleanCoverage>(coverage.size());
+
+ //sw.reset();
+ createCoverageIndex();
+ classifyResult(resultants);
+ //System.out.println("Classify: " + sw.getTimeString());
+
+ mergableGaps = findMergableGaps(gaps);
+ }
+
+
+/* private */
+void
+CoverageCleaner::createCoverageIndex()
+{
+ covIndex = std::make_unique<index::strtree::TemplateSTRtree<std::size_t>>();
+ for (std::size_t i = 0; i < coverage.size(); i++) {
+ covIndex->insert(*(coverage[i]->getEnvelopeInternal()), i);
+ }
+}
+
+
+/* private */
+void
+CoverageCleaner::classifyResult(std::vector<std::unique_ptr<Polygon>>& rs)
+{
+ for (std::size_t i = 0; i < rs.size(); i++) {
+ classifyResultant(i, rs[i].get());
+ }
+}
+
+
+/* private */
+void
+CoverageCleaner::classifyResultant(std::size_t resultIndex, const Polygon* resPoly)
+{
+ std::unique_ptr<Point> intPt = resPoly->getInteriorPoint();
+ std::size_t parentIndex = INDEX_UNKNOWN;
+ std::vector<std::size_t> overlapIndexes;
+
+ std::vector<std::size_t> candidateParentIndex;
+ covIndex->query(*(intPt->getEnvelopeInternal()), candidateParentIndex);
+
+ for (std::size_t i : candidateParentIndex) {
+ const Geometry* parent = coverage[i];
+ if (covers(parent, intPt.get())) {
+ //-- found first parent
+ if (parentIndex == INDEX_UNKNOWN) {
+ parentIndex = i;
+ }
+ else {
+ //-- more than one parent - record them all
+ overlapIndexes.push_back(parentIndex);
+ overlapIndexes.push_back(i);
+ }
+ }
+ }
+ /**
+ * Classify resultant based on # of parents:
+ * 0 - gap
+ * 1 - single polygon face
+ * >1 - overlap
+ */
+ if (parentIndex == INDEX_UNKNOWN) {
+ gaps.push_back(resPoly);
+ }
+ else if (!overlapIndexes.empty()) {
+ overlapParentMap[resultIndex] = overlapIndexes;
+ overlaps.push_back(resPoly);
+ }
+ else {
+ cleanCov->add(parentIndex, resPoly);
+ }
+}
+
+
+/* private static */
+bool
+CoverageCleaner::covers(const Geometry* poly, const Point* intPt)
+{
+ return SimplePointInAreaLocator::isContained(
+ *(intPt->getCoordinate()),
+ poly);
+}
+
+
+/* private */
+std::vector<const Polygon*>
+CoverageCleaner::findMergableGaps(std::vector<const Polygon*> p_gaps)
+{
+ std::vector<const Polygon*> filtered;
+
+ std::copy_if(p_gaps.begin(), p_gaps.end(),
+ std::back_inserter(filtered),
+ [this](const Polygon* gap) { return isMergableGap(gap); }
+ );
+
+ return filtered;
+
+ // return gaps.stream().filter(gap -> isMergableGap(gap)).collect(Collectors.toList());
+}
+
+
+/* private */
+bool
+CoverageCleaner::isMergableGap(const Polygon* gap)
+{
+ if (gapMaximumWidth <= 0) {
+ return false;
+ }
+ return MaximumInscribedCircle::isRadiusWithin(gap, gapMaximumWidth / 2.0);
+}
+
+
+/* private static */
+std::vector<std::unique_ptr<geom::Polygon>>
+CoverageCleaner::polygonize(const Geometry* cleanEdges)
+{
+ Polygonizer polygonizer;
+ polygonizer.add(cleanEdges);
+ return polygonizer.getPolygons();
+}
+
+
+/* public static */
+std::unique_ptr<Geometry>
+CoverageCleaner::toGeometry(
+ std::vector<SegmentString*>& segStrings,
+ const GeometryFactory* geomFact)
+{
+ std::vector<std::unique_ptr<LineString>> lines;
+ for (SegmentString* ss : segStrings) {
+ auto cs = ss->getCoordinates()->clone();
+ std::unique_ptr<LineString> line = geomFact->createLineString(std::move(cs));
+ lines.emplace_back(line.release());
+ }
+ if (lines.size() == 1) return lines[0]->clone();
+ return geomFact->createMultiLineString(std::move(lines));
+}
+
+
+/* public static */
+std::unique_ptr<Geometry>
+CoverageCleaner::node(std::vector<const Geometry*>& p_coverage, double p_snapDistance)
+{
+ std::vector<const SegmentString*> csegs;
+
+ for (const Geometry* geom : p_coverage) {
+ //-- skip non-polygonal and empty elements
+ if (! isPolygonal(geom))
+ continue;
+ if (geom->isEmpty())
+ continue;
+ SegmentStringUtil::extractSegmentStrings(geom, csegs);
+ }
+
+ std::vector<SegmentString*> segs;
+ for (auto* css : csegs) {
+ segs.push_back(const_cast<SegmentString*>(css));
+ }
+
+ SnappingNoder noder(p_snapDistance);
+ noder.computeNodes(&segs);
+ std::unique_ptr<std::vector<SegmentString*>> nodedSegStrings(noder.getNodedSubstrings());
+ for (auto* ss : segs) {
+ delete ss;
+ }
+
+ auto result = toGeometry(*nodedSegStrings, geomFactory);
+ for (SegmentString* ss : *nodedSegStrings) {
+ delete ss;
+ }
+
+ return result;
+}
+
+/* private static */
+bool
+CoverageCleaner::isPolygonal(const Geometry* geom)
+{
+ return geom->getGeometryTypeId() == geom::GEOS_POLYGON ||
+ geom->getGeometryTypeId() == geom::GEOS_MULTIPOLYGON;
+}
+
+
+/* private static */
+std::vector<const Polygon*>
+CoverageCleaner::toPolygonArray(const Geometry* geom)
+{
+ std::size_t sz = geom->getNumGeometries();
+ std::vector<const Polygon*> geoms;
+ geoms.resize(sz);
+ for (std::size_t i = 0; i < sz; i++) {
+ const Geometry* subgeom = geom->getGeometryN(i);
+ geoms.push_back(static_cast<const Polygon*>(subgeom));
+ }
+ return geoms;
+}
+
+
+} // namespace geos.coverage
+} // namespace geos
+
+
diff --git a/src/coverage/CoverageValidator.cpp b/src/coverage/CoverageValidator.cpp
index 644558334..19a933ef6 100644
--- a/src/coverage/CoverageValidator.cpp
+++ b/src/coverage/CoverageValidator.cpp
@@ -36,6 +36,15 @@ CoverageValidator::isValid(std::vector<const Geometry*>& coverage)
return ! hasInvalidResult(v.validate());
}
+/* public static */
+bool
+CoverageValidator::isValid(std::vector<const Geometry*>& coverage, double p_gapWidth)
+{
+ CoverageValidator v(coverage);
+ v.setGapWidth(p_gapWidth);
+ return ! hasInvalidResult(v.validate());
+}
+
/* public static */
bool
CoverageValidator::hasInvalidResult(const std::vector<std::unique_ptr<Geometry>>& validateResult)
diff --git a/tests/unit/coverage/CoverageCleanerTest.cpp b/tests/unit/coverage/CoverageCleanerTest.cpp
new file mode 100644
index 000000000..5eb141be1
--- /dev/null
+++ b/tests/unit/coverage/CoverageCleanerTest.cpp
@@ -0,0 +1,425 @@
+//
+// Test Suite for geos::coverage::CoverageGapFinderTest class.
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/coverage/CoverageCleaner.h>
+#include <geos/coverage/CoverageValidator.h>
+
+using geos::coverage::CoverageCleaner;
+using geos::coverage::CoverageValidator;
+
+namespace tut {
+//
+// Test Group
+//
+
+// Common data used by all tests
+struct test_coveragecleaner_data {
+
+ WKTReader r;
+ WKTWriter w;
+
+ void
+ printResult(
+ const std::unique_ptr<Geometry>& expected,
+ const std::unique_ptr<Geometry>& actual)
+ {
+ std::cout << std::endl;
+ std::cout << "--expect--" << std::endl;
+ std::cout << w.write(expected.get()) << std::endl;
+ std::cout << "--actual--" << std::endl;
+ std::cout << w.write(actual.get()) << std::endl;
+ }
+
+ void
+ printResult(
+ const std::vector<std::unique_ptr<Geometry>>& expected,
+ const std::vector<std::unique_ptr<Geometry>>& actual)
+ {
+ std::cout << std::endl;
+ std::cout << "--expect--" << std::endl;
+ for (auto& e : expected) {
+ std::cout << w.write(e.get()) << std::endl;
+ }
+ std::cout << "--actual--" << std::endl;
+ for (auto& a : actual) {
+ std::cout << w.write(a.get()) << std::endl;
+ }
+ std::cout << std::endl;
+ }
+
+
+ std::vector<const Geometry*>
+ toArray(const std::unique_ptr<Geometry>& geom)
+ {
+ std::vector<const Geometry*> geoms;
+ for (std::size_t i = 0; i < geom->getNumGeometries(); i++) {
+ geoms.push_back(geom->getGeometryN(i));
+ }
+ return geoms;
+ }
+
+ std::vector<const Geometry*>
+ toArray(const std::vector<std::unique_ptr<Geometry>>& cov)
+ {
+ std::vector<const Geometry*> geoms;
+ for (const auto& g : cov) {
+ geoms.push_back(g.get());
+ }
+ return geoms;
+ }
+
+ void
+ checkEqual(std::vector<const Geometry*>& expected, std::vector<const Geometry*>& actual)
+ {
+ ensure_equals("checkEqual sizes", actual.size(), expected.size());
+ for (std::size_t i = 0; i < actual.size(); i++) {
+ ensure_equals_geometry(actual[i], expected[i]);
+ }
+ }
+
+ void
+ checkEqual(
+ std::vector<std::unique_ptr<Geometry>>& expected,
+ std::vector<std::unique_ptr<Geometry>>& actual)
+ {
+ auto actualArr = toArray(actual);
+ auto expectedArr = toArray(expected);
+ checkEqual(expectedArr, actualArr);
+ }
+
+ void
+ checkClean(const std::string& wkt, const std::string& wktExpected)
+ {
+ std::unique_ptr<Geometry> geom = r.read(wkt);
+ std::vector<const Geometry*> cov = toArray(geom);
+ std::vector<std::unique_ptr<Geometry>> actual = CoverageCleaner::cleanGapWidth(cov, 0);
+ std::unique_ptr<Geometry> expected = r.read(wktExpected);
+ auto expectedArr = toArray(expected);
+ auto actualArr = toArray(actual);
+ checkEqual(expectedArr, actualArr);
+ }
+
+ void
+ checkCleanGapWidth(const std::string& wkt, double gapWidth, const std::string& wktExpected)
+ {
+ std::unique_ptr<Geometry> geom = r.read(wkt);
+ std::vector<const Geometry*> cov = toArray(geom);
+ std::vector<std::unique_ptr<Geometry>> actual = CoverageCleaner::cleanGapWidth(cov, gapWidth);
+ std::unique_ptr<Geometry> expected = r.read(wktExpected);
+ auto expectedArr = toArray(expected);
+ auto actualArr = toArray(actual);
+ checkEqual(expectedArr, actualArr);
+ }
+
+ void
+ checkCleanOverlapMerge(const std::string& wkt, int mergeStrategy, const std::string& wktExpected)
+ {
+ std::unique_ptr<Geometry> geom = r.read(wkt);
+ std::vector<const Geometry*> cov = toArray(geom);
+ std::vector<std::unique_ptr<Geometry>> actual = CoverageCleaner::cleanOverlapGap(cov, mergeStrategy, 0);
+ std::unique_ptr<Geometry> expected = r.read(wktExpected);
+ auto expectedArr = toArray(expected);
+ auto actualArr = toArray(actual);
+ checkEqual(expectedArr, actualArr);
+ }
+
+ void
+ checkCleanSnapInt(
+ std::vector<const Geometry*> cov,
+ double snapDist,
+ std::vector<const Geometry*> expected)
+ {
+ std::vector<std::unique_ptr<Geometry>> actualPtr = CoverageCleaner::clean(cov, snapDist, 0);
+ std::vector<const Geometry*> actual = toArray(actualPtr);
+ checkValidCoverage(actual, snapDist);
+ checkEqual(expected, actual);
+ }
+
+ void
+ checkCleanSnapInt(
+ std::vector<const Geometry*> cov,
+ double snapDist)
+ {
+ std::vector<std::unique_ptr<Geometry>> covClean = CoverageCleaner::clean(cov, snapDist, 0);
+ checkValidCoverage(toArray(covClean), snapDist);
+ }
+
+ void
+ checkCleanSnap(const std::vector<std::string>& covStrs, double snapDist)
+ {
+ std::vector<std::unique_ptr<Geometry>> cov = readArray(covStrs);
+ std::vector<const Geometry*> covArr = toArray(cov);
+ checkCleanSnapInt(covArr, snapDist);
+ }
+
+ void
+ checkCleanSnap(
+ const std::vector<std::string>& covStrs,
+ double snapDist,
+ const std::vector<std::string>& expStrs)
+ {
+ std::vector<std::unique_ptr<Geometry>> cov = readArray(covStrs);
+ std::vector<const Geometry*> covArr = toArray(cov);
+ std::vector<std::unique_ptr<Geometry>> exp = readArray(expStrs);
+ std::vector<const Geometry*> expArr = toArray(exp);
+ checkCleanSnapInt(covArr, snapDist, expArr);
+ }
+
+ void
+ checkValidCoverage(std::vector<const Geometry*> coverage, double tolerance)
+ {
+ for (const Geometry* geom : coverage) {
+ ensure("checkValidCoverage geom->isValid()", geom->isValid());
+ }
+ bool isValid = CoverageValidator::isValid(coverage, tolerance);
+ ensure("checkValidCoverage CoverageValidator", isValid);
+ }
+
+ std::vector<std::unique_ptr<Geometry>>
+ readArray(const std::vector<std::string>& wkts)
+ {
+ std::vector<std::unique_ptr<Geometry>> geometries;
+ for (const std::string& wkt : wkts) {
+ auto geom = r.read(wkt);
+ if (geom != nullptr) {
+ geometries.push_back(std::move(geom));
+ }
+ }
+ return geometries;
+ }
+
+};
+
+
+
+typedef test_group<test_coveragecleaner_data> group;
+typedef group::object object;
+
+group test_coveragecleaner_data("geos::coverage::CoverageCleaner");
+
+
+
+// testCoverageWithEmpty
+template<>
+template<>
+void object::test<1> ()
+{
+ checkClean(
+ "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 4, 1 4, 1 9)), POLYGON EMPTY, POLYGON ((2 1, 2 5, 8 5, 8 1, 2 1)))",
+ "GEOMETRYCOLLECTION (POLYGON ((1 4, 1 9, 9 9, 9 4, 8 4, 2 4, 1 4)), POLYGON EMPTY, POLYGON ((8 1, 2 1, 2 4, 8 4, 8 1)))");
+}
+
+
+// testSingleNearMatch
+template<>
+template<>
+void object::test<2>()
+{
+ checkCleanSnap(
+ {
+ "POLYGON ((1 9, 9 9, 9 4.99, 1 5, 1 9))",
+ "POLYGON ((1 1, 1 5, 9 5, 9 1, 1 1))"
+ },
+ 0.1);
+}
+
+// testManyNearMatches
+template<>
+template<>
+void object::test<3>()
+{
+ checkCleanSnap(
+ {
+ "POLYGON ((1 9, 9 9, 9 5, 8 5, 7 5, 4 5.5, 3 5, 2 5, 1 5, 1 9))",
+ "POLYGON ((1 1, 1 4.99, 2 5.01, 3.01 4.989, 5 3, 6.99 4.99, 7.98 4.98, 9 5, 9 1, 1 1))"
+ },
+ 0.1);
+}
+
+// testPolygonSnappedPreserved
+// Tests that if interior point lies in a spike that is snapped away, polygon is still in result
+template<>
+template<>
+void object::test<4>()
+{
+ checkCleanSnap(
+ {"POLYGON ((90 0, 10 0, 89.99 30, 90 100, 90 0))"},
+ 0.1,
+ {"POLYGON ((90 0, 10 0, 89.99 30, 90 0))"}
+ );
+}
+
+// testPolygonsSnappedPreserved
+// Tests that if interior point lies in a spike that is snapped away, polygon is still in result
+template<>
+template<>
+void object::test<5>()
+{
+ checkCleanSnap(
+ {
+ "POLYGON ((0 0, 0 2, 5 2, 5 8, 5.01 0, 0 0))",
+ "POLYGON ((0 8, 5 8, 5 2, 0 2, 0 8))"
+ },
+ 0.02,
+ {
+ "POLYGON ((0 0, 0 2, 5 2, 5.01 0, 0 0))",
+ "POLYGON ((0 8, 5 8, 5 2, 0 2, 0 8))"
+ });
+}
+
+// testMergeGapToLongestBorder
+template<>
+template<>
+void object::test<6>()
+{
+ checkCleanGapWidth(
+ "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 5, 1 5, 1 9)), POLYGON ((5 1, 5 5, 1 5, 5 1)), POLYGON ((5 1, 5.1 5, 9 5, 5 1)))",
+ 1,
+ "GEOMETRYCOLLECTION (POLYGON ((5.1 5, 5 5, 1 5, 1 9, 9 9, 9 5, 5.1 5)), POLYGON ((5 1, 1 5, 5 5, 5 1)), POLYGON ((5 1, 5 5, 5.1 5, 9 5, 5 1)))"
+ );
+}
+
+std::string covWithGaps = "GEOMETRYCOLLECTION (POLYGON ((1 3, 9 3, 9 1, 1 1, 1 3)), POLYGON ((1 3, 1 9, 4 9, 4 3, 3 4, 1 3)), POLYGON ((4 9, 7 9, 7 3, 6 5, 5 5, 4 3, 4 9)), POLYGON ((7 9, 9 9, 9 3, 8 3.1, 7 3, 7 9)))";
+
+// testMergeGapWidth_0
+template<>
+template<>
+void object::test<7>()
+{
+ checkCleanGapWidth(covWithGaps,
+ 0,
+ "GEOMETRYCOLLECTION (POLYGON ((9 3, 9 1, 1 1, 1 3, 4 3, 7 3, 9 3)), POLYGON ((1 9, 4 9, 4 3, 3 4, 1 3, 1 9)), POLYGON ((6 5, 5 5, 4 3, 4 9, 7 9, 7 3, 6 5)), POLYGON ((7 9, 9 9, 9 3, 8 3.1, 7 3, 7 9)))"
+ );
+}
+
+// testMergeGapWidth_1
+template<>
+template<>
+void object::test<8>()
+{
+ checkCleanGapWidth(covWithGaps,
+ 1,
+ "GEOMETRYCOLLECTION (POLYGON ((7 3, 9 3, 9 1, 1 1, 1 3, 4 3, 7 3)), POLYGON ((1 9, 4 9, 4 3, 1 3, 1 9)), POLYGON ((7 3, 6 5, 5 5, 4 3, 4 9, 7 9, 7 3)), POLYGON ((7 9, 9 9, 9 3, 7 3, 7 9)))"
+ );
+}
+
+// testMergeGapWidth_2
+template<>
+template<>
+void object::test<9>()
+{
+ checkCleanGapWidth(covWithGaps,
+ 2,
+ "GEOMETRYCOLLECTION (POLYGON ((9 3, 9 1, 1 1, 1 3, 4 3, 7 3, 9 3)), POLYGON ((1 9, 4 9, 4 3, 1 3, 1 9)), POLYGON ((7 3, 4 3, 4 9, 7 9, 7 3)), POLYGON ((9 9, 9 3, 7 3, 7 9, 9 9)))"
+ );
+}
+
+std::string covWithOverlap = "GEOMETRYCOLLECTION (POLYGON ((1 3, 5 3, 4 1, 1 1, 1 3)), POLYGON ((1 3, 1 9, 4 9, 4 3, 3 1.9, 1 3)))";
+
+// testMergeOverlapMinArea
+template<>
+template<>
+void object::test<10>()
+{
+ checkCleanOverlapMerge(covWithOverlap,
+ CoverageCleaner::MERGE_MIN_AREA,
+ "GEOMETRYCOLLECTION (POLYGON ((5 3, 4 1, 1 1, 1 3, 4 3, 5 3)), POLYGON ((1 9, 4 9, 4 3, 1 3, 1 9)))"
+ );
+}
+
+// testMergeOverlapMaxArea
+template<>
+template<>
+void object::test<11>()
+{
+ checkCleanOverlapMerge(covWithOverlap,
+ CoverageCleaner::MERGE_MAX_AREA,
+ "GEOMETRYCOLLECTION (POLYGON ((1 1, 1 3, 3 1.9, 4 3, 5 3, 4 1, 1 1)), POLYGON ((1 3, 1 9, 4 9, 4 3, 3 1.9, 1 3)))"
+ );
+}
+
+// testMergeOverlapMinId
+template<>
+template<>
+void object::test<12>()
+{
+ checkCleanOverlapMerge(covWithOverlap,
+ CoverageCleaner::MERGE_MIN_INDEX,
+ "GEOMETRYCOLLECTION (POLYGON ((5 3, 4 1, 1 1, 1 3, 4 3, 5 3)), POLYGON ((1 9, 4 9, 4 3, 1 3, 1 9)))"
+ );
+}
+
+// testMergeOverlap2
+template<>
+template<>
+void object::test<13>()
+{
+ checkCleanSnap(
+ {
+ "POLYGON ((5 9, 9 9, 9 1, 5 1, 5 9))",
+ "POLYGON ((1 5, 5 5, 5 2, 1 2, 1 5))",
+ "POLYGON ((2 7, 5 7, 5 4, 2 4, 2 7))"
+ },
+ 0.1,
+ {
+ "POLYGON ((5 1, 5 2, 5 4, 5 5, 5 7, 5 9, 9 9, 9 1, 5 1))",
+ "POLYGON ((5 2, 1 2, 1 5, 2 5, 5 5, 5 4, 5 2))",
+ "POLYGON ((5 5, 2 5, 2 7, 5 7, 5 5))"
+ });
+}
+
+// testMergeOverlap
+template<>
+template<>
+void object::test<14>()
+{
+ checkCleanOverlapMerge(
+ "GEOMETRYCOLLECTION (POLYGON ((5 9, 9 9, 9 1, 5 1, 5 9)), POLYGON ((1 5, 5 5, 5 2, 1 2, 1 5)), POLYGON ((2 7, 5 7, 5 4, 2 4, 2 7)))",
+ CoverageCleaner::MERGE_LONGEST_BORDER,
+ "GEOMETRYCOLLECTION (POLYGON ((5 7, 5 9, 9 9, 9 1, 5 1, 5 2, 5 4, 5 5, 5 7)), POLYGON ((5 2, 1 2, 1 5, 2 5, 5 5, 5 4, 5 2)), POLYGON ((2 5, 2 7, 5 7, 5 5, 2 5)))"
+ );
+}
+
+//-------------------------------------------
+
+//-- a duplicate coverage element is assigned to the lowest result index
+// testDuplicateItems
+template<>
+template<>
+void object::test<15>()
+{
+ checkClean(
+ "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 1, 1 1, 1 9)), POLYGON ((1 9, 9 1, 1 1, 1 9)))",
+ "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 1, 1 1, 1 9)), POLYGON EMPTY)"
+ );
+}
+
+// testCoveredItem
+template<>
+template<>
+void object::test<16>()
+{
+ checkClean(
+ "GEOMETRYCOLLECTION (POLYGON ((1 9, 9 9, 9 4, 1 4, 1 9)), POLYGON ((2 5, 2 8, 8 8, 8 5, 2 5)))",
+ "GEOMETRYCOLLECTION (POLYGON ((9 9, 9 4, 1 4, 1 9, 9 9)), POLYGON EMPTY)"
+ );
+}
+
+// testCoveredItemMultiPolygon
+template<>
+template<>
+void object::test<17>()
+{
+ checkClean(
+ "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 1, 1 5, 5 5, 5 1, 1 1)), ((6 5, 6 1, 9 1, 6 5))), POLYGON ((6 1, 6 5, 9 1, 6 1)))",
+ "GEOMETRYCOLLECTION (MULTIPOLYGON (((1 5, 5 5, 5 1, 1 1, 1 5)), ((6 5, 9 1, 6 1, 6 5))), POLYGON EMPTY)"
+ );
+}
+
+
+
+} // namespace tut
commit 19da203022b4527e9f385813aefb9ed94ac1cdfe
Merge: 79d642dff 605ecc1f0
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Wed May 28 13:14:36 2025 -0700
Merge branch 'main' of github.com:libgeos/geos
commit 79d642dff43f95e57aea810598901d2eb51f4e4f
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Wed May 28 13:14:31 2025 -0700
LineDissolver news
diff --git a/NEWS.md b/NEWS.md
index 07ad61eec..3a1b33e5a 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -3,6 +3,7 @@
- New things:
- Add clustering functions to C API (GH-1154, Dan Baston)
+ - Ported LineDissolver (Paul Ramsey)
- Breaking Changes:
- Stricter WKT parsing (GH-1241, @freemine)
-----------------------------------------------------------------------
Summary of changes:
NEWS.md | 2 +
include/geos/coverage/CleanCoverage.h | 245 +++++++++++++++
include/geos/coverage/CoverageCleaner.h | 349 +++++++++++++++++++++
include/geos/coverage/CoverageValidator.h | 12 +
src/coverage/CleanCoverage.cpp | 285 +++++++++++++++++
src/coverage/CoverageCleaner.cpp | 463 ++++++++++++++++++++++++++++
src/coverage/CoverageValidator.cpp | 9 +
tests/unit/coverage/CoverageCleanerTest.cpp | 425 +++++++++++++++++++++++++
8 files changed, 1790 insertions(+)
create mode 100644 include/geos/coverage/CleanCoverage.h
create mode 100644 include/geos/coverage/CoverageCleaner.h
create mode 100644 src/coverage/CleanCoverage.cpp
create mode 100644 src/coverage/CoverageCleaner.cpp
create mode 100644 tests/unit/coverage/CoverageCleanerTest.cpp
hooks/post-receive
--
GEOS
More information about the geos-commits
mailing list