[SCM] PostGIS branch master updated. 3.5.0-353-g65789d316
git at osgeo.org
git at osgeo.org
Mon Jun 2 09:50:10 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 "PostGIS".
The branch, master has been updated
via 65789d316df326e332d4fd22032a885081766f8e (commit)
from 7829205c550b4235fdfe9b44775e72b121b34aa8 (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 65789d316df326e332d4fd22032a885081766f8e
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date: Mon Jun 2 09:48:46 2025 -0700
Add ST_CoverageClean, window function to repair overlaps and
gaps in polygonal coverages. Available with GEOS 3.14 and
greater.
diff --git a/NEWS b/NEWS
index dcc53e03d..a827c8da9 100644
--- a/NEWS
+++ b/NEWS
@@ -28,4 +28,6 @@ To take advantage of all SFCGAL features, SFCGAL 2.1+ is needed.
Straight Skeleton Partition) from SFCGAL 2 (Loïc Bartoletti)
- [raster] New GUC postgis.gdal_cpl_debug, enables GDAL debugging messages
and routes them into the PostgreSQL logging system. (Paul Ramsey)
- - #5841, Change interrupt handling to remove use of pqsignal to support PG 18 (Paul Ramsey)
\ No newline at end of file
+ - #5841, Change interrupt handling to remove use of pqsignal to support PG 18 (Paul Ramsey)
+ - Add ST_CoverageClean to edge match and gap remove polygonal
+ coverages (Paul Ramsey) from GEOS 3.14 (Martin Davis)
diff --git a/doc/reference_coverage.xml b/doc/reference_coverage.xml
index d315c6e50..cab673ff1 100644
--- a/doc/reference_coverage.xml
+++ b/doc/reference_coverage.xml
@@ -109,6 +109,7 @@ SELECT true = ALL (
<para>
<xref linkend="ST_IsValid"/>,
<xref linkend="ST_CoverageUnion"/>,
+ <xref linkend="ST_CoverageClean"/>,
<xref linkend="ST_CoverageSimplify"/>
</para>
</refsection>
@@ -212,7 +213,9 @@ SELECT id, ST_AsText(ST_CoverageSimplify(geom, 30) OVER ())
<refsection>
<title>See Also</title>
<para>
- <xref linkend="ST_CoverageInvalidEdges"/>
+ <xref linkend="ST_CoverageInvalidEdges"/>,
+ <xref linkend="ST_CoverageUnion"/>,
+ <xref linkend="ST_CoverageClean"/>
</para>
</refsection>
@@ -301,9 +304,90 @@ MULTIPOLYGON (((10 150, 80 190, 110 150, 140 80, 120 10, 10 10, 10 150), (50 60,
<title>See Also</title>
<para>
<xref linkend="ST_CoverageInvalidEdges"/>,
+ <xref linkend="ST_CoverageSimplify"/>,
+ <xref linkend="ST_CoverageClean"/>,
<xref linkend="ST_Union"/>
</para>
</refsection>
</refentry>
+
+
+
+
+ <refentry xml:id="ST_CoverageClean">
+ <refnamediv>
+ <refname>ST_CoverageClean</refname>
+
+ <refpurpose>Computes a clean (edge matched, non-overlapping, gap-cleared) polygonal coverage, given a non-clean input.</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <funcsynopsis>
+ <funcprototype>
+ <funcdef>geometry <function>ST_CoverageClean</function></funcdef>
+ <paramdef><type>geometry winset</type>
+ <parameter>geom</parameter></paramdef>
+ <paramdef choice="opt"><type>float8 </type>
+ <parameter>snappingDistance = -1</parameter></paramdef>
+ <paramdef choice="opt"><type>float8 </type>
+ <parameter>gapMaximumWidth = 0</parameter></paramdef>
+ <paramdef choice="opt"><type>text </type>
+ <parameter>overlapMergeStrategy = 'MERGE_LONGEST_BORDER'</parameter></paramdef>
+ </funcprototype>
+ </funcsynopsis>
+ </refsynopsisdiv>
+
+ <refsection>
+ <title>Description</title>
+
+ <para>A window function which alters the edges of a polygonal coverage to ensure that none of the polygons overlap, that small gaps are snapped away, and that all shared edges are exactly identical. The result is a clean coverage that will pass validation tests like <xref linkend="ST_CoverageInvalidEdges"/></para>
+ <para>The <parameter>snappingDistance</parameter> controls the node snapping step, when nearby vertices are snapped together. The default setting (-1) applies an automatic snapping distance based on an analysis of the input. Set to 0.0 to turn off all snapping.</para>
+ <para>The <parameter>gapMaximumWidth</parameter> controls the cleaning of gaps between polygons. Gaps smaller than this tolerance will be closed.</para>
+ <para>The <parameter>overlapMergeStrategy</parameter> controls the algorithm used to determine which neighboring polygons to merge overlapping areas into.</para>
+ <para><code>MERGE_LONGEST_BORDER</code> chooses polygon with longest common border</para>
+ <para><code>MERGE_MAX_AREA</code> chooses polygon with maximum area</para>
+ <para><code>MERGE_MIN_AREA</code> chooses polygon with minimum area</para>
+ <para><code>MERGE_MIN_INDEX</code> chooses polygon with smallest input index</para>
+
+ <para role="availability" conformance="3.6.0">Availability: 3.6.0 - requires GEOS >= 3.14.0</para>
+ </refsection>
+
+ <refsection>
+ <title>Examples</title>
+
+ <programlisting>-- Populate demo table
+CREATE TABLE example AS SELECT * FROM (VALUES
+ (1, 'POLYGON ((10 190, 30 160, 40 110, 100 70, 120 10, 10 10, 10 190))'::geometry),
+ (2, 'POLYGON ((100 190, 10 190, 30 160, 40 110, 50 80, 74 110.5, 100 130, 140 120, 140 160, 100 190))'::geometry),
+ (3, 'POLYGON ((140 190, 190 190, 190 80, 140 80, 140 190))'::geometry),
+ (4, 'POLYGON ((180 40, 120 10, 100 70, 140 80, 190 80, 180 40))'::geometry)
+) AS v(id, geom);
+
+-- Prove it is a dirty coverage
+SELECT ST_AsText(ST_CoverageInvalidEdges(geom) OVER ())
+ FROM example;
+
+-- Clean the coverage
+CREATE TABLE example_clean AS
+ SELECT id, ST_CoverageClean(geom) OVER () AS GEOM
+ FROM example;
+
+-- Prove it is a clean coverage
+SELECT ST_AsText(ST_CoverageInvalidEdges(geom) OVER ())
+ FROM example_clean;
+ </programlisting>
+ </refsection>
+
+ <refsection>
+ <title>See Also</title>
+ <para>
+ <xref linkend="ST_CoverageInvalidEdges"/>,
+ <xref linkend="ST_Union"/>
+ <xref linkend="ST_CoverageSimplify"/>
+ </para>
+ </refsection>
+
+ </refentry>
+
</section>
diff --git a/postgis/lwgeom_window.c b/postgis/lwgeom_window.c
index 715eecbbb..d4007a537 100644
--- a/postgis/lwgeom_window.c
+++ b/postgis/lwgeom_window.c
@@ -29,6 +29,7 @@
#include "postgres.h"
#include "funcapi.h"
#include "windowapi.h"
+#include "utils/builtins.h"
/* PostGIS */
#include "liblwgeom.h"
@@ -543,7 +544,8 @@ coverage_read_partition_into_collection(
* GEOS nulls or empties. So we need to maintain a
* map (context->idx) from the window position of the
* input to the GEOS position, so we can put the
- * right result in the output stream.
+ * right result in the output stream. Things we want to
+ * skip get an index of -1.
*/
/* Skip NULL inputs and move on */
@@ -581,7 +583,7 @@ coverage_read_partition_into_collection(
/*
* Create the GEOS input collection! The new
* collection takes ownership of the input GEOSGeometry
- * objects, leaving just the ngeoms array, which
+ * objects, leaving just the geoms array, which
* will be cleaned up on function exit.
*/
geos = GEOSGeom_createCollection(
@@ -612,10 +614,38 @@ coverage_read_partition_into_collection(
* operating mode is controlled with this enumeration.
*/
enum {
- COVERAGE_SIMPLIFY = 0,
- COVERAGE_ISVALID = 1
+ COVERAGE_SIMPLIFY = 0
+ ,COVERAGE_ISVALID = 1
+#if POSTGIS_GEOS_VERSION >= 31400
+ ,COVERAGE_CLEAN = 2
+#endif
};
+static char * overlapMergeStrategies[] = {
+ /* Merge strategy that chooses polygon with longest common border */
+ "MERGE_LONGEST_BORDER",
+ /* Merge strategy that chooses polygon with maximum area */
+ "MERGE_MAX_AREA",
+ /* Merge strategy that chooses polygon with minimum area */
+ "MERGE_MIN_AREA",
+ /* Merge strategy that chooses polygon with smallest input index */
+ "MERGE_MIN_INDEX"
+};
+
+static int
+coverage_merge_strategy(const char *strategy)
+{
+ size_t stratLen = sizeof(overlapMergeStrategies) / sizeof(overlapMergeStrategies[0]);
+ for (size_t i = 0; i < stratLen; i++)
+ {
+ if (strcasecmp(strategy, overlapMergeStrategies[i]) == 0)
+ {
+ return i;
+ }
+ }
+ return -1;
+}
+
/*
* This calculation is shared by both coverage operations
* since they have the same pattern of "consume collection,
@@ -636,11 +666,9 @@ coverage_window_calculation(PG_FUNCTION_ARGS, int mode)
if (!context->isdone)
{
bool isnull;
- Datum d;
- double tolerance = 0.0;
- bool simplifyBoundary = true;
GEOSGeometry *output = NULL;
GEOSGeometry *input = NULL;
+ Datum d;
if (!fcinfo->flinfo)
elog(ERROR, "%s: Could not find upper context", __func__);
@@ -652,20 +680,6 @@ coverage_window_calculation(PG_FUNCTION_ARGS, int mode)
PG_RETURN_NULL();
}
- /* Get the tolerance argument from second position */
- d = WinGetFuncArgCurrent(winobj, 1, &isnull);
- if (!isnull)
- tolerance = DatumGetFloat8(d);
-
- /* The third position "preserve boundary" argument */
- /* is only for the simplify mode */
- if (mode == COVERAGE_SIMPLIFY)
- {
- d = WinGetFuncArgCurrent(winobj, 2, &isnull);
- if (!isnull)
- simplifyBoundary = DatumGetBool(d);
- }
-
initGEOS(lwpgnotice, lwgeom_geos_error);
input = coverage_read_partition_into_collection(winobj, context);
@@ -675,13 +689,71 @@ coverage_window_calculation(PG_FUNCTION_ARGS, int mode)
/* Run the correct GEOS function for the calling mode */
if (mode == COVERAGE_SIMPLIFY)
{
+ bool simplifyBoundary = true;
+ double tolerance = 0.0;
+
+ d = WinGetFuncArgCurrent(winobj, 1, &isnull);
+ if (!isnull) tolerance = DatumGetFloat8(d);
+
+ d = WinGetFuncArgCurrent(winobj, 2, &isnull);
+ if (!isnull) simplifyBoundary = DatumGetFloat8(d);
+
/* GEOSCoverageSimplifyVW is "preserveBoundary" so we invert simplifyBoundary */
output = GEOSCoverageSimplifyVW(input, tolerance, !simplifyBoundary);
}
else if (mode == COVERAGE_ISVALID)
{
+ double tolerance = 0.0;
+ d = WinGetFuncArgCurrent(winobj, 1, &isnull);
+ if (!isnull) tolerance = DatumGetFloat8(d);
GEOSCoverageIsValid(input, tolerance, &output);
}
+
+#if POSTGIS_GEOS_VERSION >= 31400
+
+ else if (mode == COVERAGE_CLEAN)
+ {
+ double snappingDistance = 0.0;
+ double gapMaximumWidth = 0.0;
+ text *overlapMergeStrategyText;
+ int overlapMergeStrategy;
+ GEOSCoverageCleanParams *params = NULL;
+
+ d = WinGetFuncArgCurrent(winobj, 1, &isnull);
+ if (!isnull) snappingDistance = DatumGetFloat8(d);
+
+ d = WinGetFuncArgCurrent(winobj, 2, &isnull);
+ if (!isnull) gapMaximumWidth = DatumGetFloat8(d);
+
+ d = WinGetFuncArgCurrent(winobj, 3, &isnull);
+ // if (!isnull) overlapMergeStrategy = DatumGetInt32(d);
+ if (!isnull)
+ {
+ overlapMergeStrategyText = DatumGetTextP(d);
+ overlapMergeStrategy = coverage_merge_strategy(text_to_cstring(overlapMergeStrategyText));
+ }
+ else
+ {
+ overlapMergeStrategy = 0; /* Default to MERGE_LONGEST_BORDER */
+ }
+ if (overlapMergeStrategy < 0)
+ {
+ HANDLE_GEOS_ERROR("Invalid OverlapMergeStrategy");
+ }
+
+ params = GEOSCoverageCleanParams_create();
+ GEOSCoverageCleanParams_setGapMaximumWidth(params, gapMaximumWidth);
+ GEOSCoverageCleanParams_setSnappingDistance(params, snappingDistance);
+ if (!GEOSCoverageCleanParams_setOverlapMergeStrategy(params, overlapMergeStrategy))
+ {
+ GEOSCoverageCleanParams_destroy(params);
+ HANDLE_GEOS_ERROR("Invalid OverlapMergeStrategy");
+ }
+
+ output = GEOSCoverageCleanWithParams(input, params);
+ GEOSCoverageCleanParams_destroy(params);
+ }
+#endif
else
{
elog(ERROR, "Unknown mode, never get here");
@@ -752,8 +824,8 @@ Datum ST_CoverageSimplify(PG_FUNCTION_ARGS)
#if POSTGIS_GEOS_VERSION < 31200
lwpgerror("The GEOS version this PostGIS binary "
- "was compiled against (%d) doesn't support "
- "'GEOSCoverageSimplifyVW' function (3.12.0+ required)",
+ "was compiled against (%d) not include "
+ "'GEOSCoverageSimplifyVW' function (3.12 or greater required)",
POSTGIS_GEOS_VERSION);
PG_RETURN_NULL();
@@ -772,8 +844,8 @@ Datum ST_CoverageInvalidEdges(PG_FUNCTION_ARGS)
#if POSTGIS_GEOS_VERSION < 31200
lwpgerror("The GEOS version this PostGIS binary "
- "was compiled against (%d) doesn't support "
- "'GEOSCoverageIsValid' function (3.12.0+ required)",
+ "was compiled against (%d) does not include "
+ "'GEOSCoverageIsValid' function (3.12 or greater required)",
POSTGIS_GEOS_VERSION);
PG_RETURN_NULL();
@@ -784,6 +856,25 @@ Datum ST_CoverageInvalidEdges(PG_FUNCTION_ARGS)
#endif
}
+extern Datum ST_CoverageClean(PG_FUNCTION_ARGS);
+PG_FUNCTION_INFO_V1(ST_CoverageClean);
+Datum ST_CoverageClean(PG_FUNCTION_ARGS)
+{
+#if POSTGIS_GEOS_VERSION < 31400
+
+ lwpgerror("The GEOS version this PostGIS binary "
+ "was compiled against (%d) not include "
+ "'GEOSCoverageClean' function (3.14 or greater required)",
+ POSTGIS_GEOS_VERSION);
+ PG_RETURN_NULL();
+
+#else /* POSTGIS_GEOS_VERSION >= 31400 */
+
+ return coverage_window_calculation(fcinfo, COVERAGE_CLEAN);
+
+#endif
+}
+
/**********************************************************************
* ST_CoverageUnion(geometry[])
*
diff --git a/postgis/postgis.sql.in b/postgis/postgis.sql.in
index ed39bbb0b..64dcf343e 100644
--- a/postgis/postgis.sql.in
+++ b/postgis/postgis.sql.in
@@ -4467,6 +4467,13 @@ CREATE OR REPLACE FUNCTION ST_CoverageInvalidEdges (geom geometry, tolerance flo
LANGUAGE 'c' IMMUTABLE STRICT WINDOW PARALLEL SAFE
_COST_HIGH;
+-- Availability: 3.6.0
+CREATE OR REPLACE FUNCTION ST_CoverageClean (geom geometry, snappingDistance float8 default -1.0, gapMaximumWidth float8 default 0.0, overlapMergeStrategy text default 'MERGE_LONGEST_BORDER')
+ RETURNS geometry
+ AS 'MODULE_PATHNAME', 'ST_CoverageClean'
+ LANGUAGE 'c' IMMUTABLE STRICT WINDOW PARALLEL SAFE
+ _COST_HIGH;
+
--------------------------------------------------------------------------------
-- Availability: 2.3.0
-----------------------------------------------------------------------
Summary of changes:
NEWS | 4 +-
doc/reference_coverage.xml | 86 ++++++++++++++++++++++++++-
postgis/lwgeom_window.c | 141 +++++++++++++++++++++++++++++++++++++--------
postgis/postgis.sql.in | 7 +++
4 files changed, 211 insertions(+), 27 deletions(-)
hooks/post-receive
--
PostGIS
More information about the postgis-tickets
mailing list