[geos-commits] [SCM] GEOS branch main updated. 40b2404209e65edd044386b11c4989d8226aeecb

git at osgeo.org git at osgeo.org
Tue Jul 7 08:33:59 PDT 2026


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  40b2404209e65edd044386b11c4989d8226aeecb (commit)
      from  28726481ede802eee10a9585f9df781873242a71 (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 40b2404209e65edd044386b11c4989d8226aeecb
Author: Daniel Baston <dbaston at gmail.com>
Date:   Tue Jul 7 11:33:37 2026 -0400

    Add progress reporting mechanism (#1466)
    
    Co-authored-by: Even Rouault <even.rouault at spatialys.com>

diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp
index a421eabc0..568f1c0b4 100644
--- a/capi/geos_c.cpp
+++ b/capi/geos_c.cpp
@@ -2194,5 +2194,4 @@ extern "C" {
         return GEOSCoverageSimplifyVW_r(handle, input, tolerance, preserveBoundary);
     }
 
-
 } /* extern "C" */
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 8f706e543..cb91502fd 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -117,6 +117,21 @@ typedef void (*GEOSMessageHandler)(GEOS_PRINTF_FORMAT const char *fmt, ...)
 */
 typedef void (*GEOSMessageHandler_r)(const char *message, void *userdata);
 
+
+/**
+ * A GEOS progress callback function.
+ *
+ * Such function takes a progress ratio (0 to 1), and an optional message.
+ *
+ * \param progressRatio Progression ratio (between 0 and 1)
+ * \param message Information message (can be NULL)
+ * \param userdata the user data pointer that was passed to GEOS together with
+ * this callback.
+ */
+typedef void (GEOSProgressCallback)(double progressRatio,
+                                    const char* message, void* userdata);
+
+
 /*
 * When we're included by geos_c.cpp, these types are #defined to the
 * C++ definitions via preprocessor. We don't touch them to allow the
@@ -426,6 +441,23 @@ extern void GEOS_DLL GEOS_interruptRequest(void);
 */
 extern void GEOS_DLL GEOS_interruptCancel(void);
 
+/* ========== Progress monitoring ========== */
+
+/**
+* Register a function to be called with progress updates
+*
+* \param extHandle the context returned by \ref GEOS_init_r.
+* \param cb Callback function to invoke
+* \param userData optional data to be pe provided as argument to callback
+* \return the previously registered callback, or NULL
+* \see GEOSProgressCallback
+* \since 3.15
+*/
+extern GEOSProgressCallback GEOS_DLL *GEOSContext_setProgressCallback_r(
+       GEOSContextHandle_t extHandle,
+       GEOSProgressCallback* cb,
+       void* userData);
+
 /* ========== Initialization and Cleanup ========== */
 
 /**
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 7b06105cf..32254f9e3 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -84,6 +84,7 @@
 #include <geos/operation/cluster/GeometryIntersectsClusterFinder.h>
 #include <geos/operation/distance/DistanceOp.h>
 #include <geos/operation/distance/IndexedFacetDistance.h>
+#include <geos/operation/union/UnaryUnionOp.h>
 #include <geos/operation/grid/Grid.h>
 #include <geos/operation/grid/GridIntersection.h>
 #include <geos/operation/linemerge/LineMerger.h>
@@ -122,6 +123,7 @@
 #include <geos/util/Interrupt.h>
 #include <geos/util/UniqueCoordinateArrayFilter.h>
 #include <geos/util/Machine.h>
+#include <geos/util/Progress.h>
 #include <geos/version.h>
 
 // This should go away
@@ -255,6 +257,8 @@ typedef struct GEOSContextHandle_HS {
     GEOSMessageHandler_r errorMessageNew;
     GEOSContextInterruptCallback* interrupt_cb;
     void* interrupt_cb_data;
+    GEOSProgressCallback* progress_cb;
+    void* progress_cb_data;
     void* errorData;
     uint8_t WKBOutputDims;
     int WKBByteOrder;
@@ -262,6 +266,7 @@ typedef struct GEOSContextHandle_HS {
     std::unique_ptr<Point> point2d;
     std::optional<GEOSLineToCurveParams> lineToCurveParams;
     std::optional<GEOSCurveToLineParams> curveToLineParams;
+    geos::util::ProgressFunction progressFunction;
 
     GEOSContextHandle_HS()
         :
@@ -273,6 +278,8 @@ typedef struct GEOSContextHandle_HS {
         errorMessageNew(nullptr),
         interrupt_cb(nullptr),
         interrupt_cb_data(nullptr),
+        progress_cb(nullptr),
+        progress_cb_data(nullptr),
         errorData(nullptr),
         point2d(nullptr)
     {
@@ -284,6 +291,13 @@ typedef struct GEOSContextHandle_HS {
         setNoticeHandler(nullptr);
         setErrorHandler(nullptr);
         initialized = 1;
+
+        progressFunction = [this](double progress, const char* message)
+        {
+            if (progress_cb) {
+                (*progress_cb)(progress, message, progress_cb_data);
+            }
+        };
     }
 
     GEOSMessageHandler
@@ -339,6 +353,15 @@ typedef struct GEOSContextHandle_HS {
         return old;
     }
 
+    GEOSProgressCallback*
+    setProgressCallback(GEOSProgressCallback* cb, void* userData)
+    {
+        auto old = progress_cb;
+        progress_cb = cb;
+        progress_cb_data = userData;
+        return old;
+    }
+
     void
     NOTICE_MESSAGE(GEOS_PRINTF_FORMAT const char *fmt, ...) GEOS_PRINTF_FORMAT_ATTR(2, 3)
     {
@@ -548,17 +571,49 @@ struct InterruptManager {
 };
 
 struct NotInterruptible {
-    NotInterruptible(GEOSContextHandle_t handle) {
+    explicit NotInterruptible(GEOSContextHandle_t handle) {
         (void) handle;
     }
 };
 
+using Interruptible = InterruptManager;
+
+// RAII construct that ensures a progress callback is called at completion of an operation,
+// even if that operation does not support progress reporting.
+struct DefaultProgress {
+    explicit DefaultProgress(GEOSContextHandle_t handle)
+        : cb(handle->progress_cb)
+        , cb_data(handle->progress_cb_data)
+    {}
+
+    ~DefaultProgress() {
+        if (cb) {
+            (*cb)(1.0, "", cb_data);
+        }
+    }
+
+    GEOSProgressCallback* cb;
+    void* cb_data;
+};
+
+// Construct that avoids automatic progress reporting.
+// Used for functions that have progress reporting implemented.
+struct ReportsProgress {
+    explicit ReportsProgress(GEOSContextHandle_t handle) {
+        (void) handle;
+    }
+};
+
+// Construct for trivial functions that avoids automatic progress
+// reporting.
+using NoProgress = ReportsProgress;
+
 } // namespace anonymous
 
 // Execute a lambda, using the given context handle to process errors.
 // Return errval on error.
 // Errval should be of the type returned by f, unless f returns a bool in which case we promote to char.
-template<typename InterruptManagerType=InterruptManager, typename F>
+template<typename InterruptManagerType=InterruptManager, typename ProgressManagerType=DefaultProgress, typename F>
 inline auto execute(
         GEOSContextHandle_t extHandle,
         typename std::conditional<std::is_same<decltype(std::declval<F>()()),bool>::value,
@@ -575,6 +630,7 @@ inline auto execute(
     }
 
     InterruptManagerType ic(handle);
+    ProgressManagerType pc(handle);
 
     try {
         return f();
@@ -589,7 +645,7 @@ inline auto execute(
 
 // Execute a lambda, using the given context handle to process errors.
 // Return nullptr on error.
-template<typename InterruptManagerType=InterruptManager, typename F, typename std::enable_if<!std::is_void<decltype(std::declval<F>()())>::value, std::nullptr_t>::type = nullptr>
+template<typename InterruptManagerType=InterruptManager, typename ProgressManagerType=DefaultProgress, typename F, typename std::enable_if<!std::is_void<decltype(std::declval<F>()())>::value, std::nullptr_t>::type = nullptr>
 inline auto execute(GEOSContextHandle_t extHandle, F&& f) -> decltype(f()) {
     if (extHandle == nullptr) {
         throw std::runtime_error("context handle is uninitialized, call initGEOS");
@@ -601,6 +657,7 @@ inline auto execute(GEOSContextHandle_t extHandle, F&& f) -> decltype(f()) {
     }
 
     InterruptManagerType ic(handle);
+    ProgressManagerType pc(handle);
 
     try {
         return f();
@@ -615,7 +672,7 @@ inline auto execute(GEOSContextHandle_t extHandle, F&& f) -> decltype(f()) {
 
 // Execute a lambda, using the given context handle to process errors.
 // No return value.
-template<typename InterruptManagerType=InterruptManager, typename F, typename std::enable_if<std::is_void<decltype(std::declval<F>()())>::value, std::nullptr_t>::type = nullptr>
+template<typename InterruptManagerType=InterruptManager, typename ProgressManagerType=DefaultProgress, typename F, typename std::enable_if<std::is_void<decltype(std::declval<F>()())>::value, std::nullptr_t>::type = nullptr>
 inline void execute(GEOSContextHandle_t extHandle, F&& f) {
     GEOSContextHandleInternal_t* handle = reinterpret_cast<GEOSContextHandleInternal_t*>(extHandle);
 
@@ -653,10 +710,10 @@ Geometry* convertCurvesAndExecute(GEOSContextHandle_t extHandle, const Geometry*
     });
 }
 
-template<typename F>
+template<typename InterruptManagerType=InterruptManager, typename ProgressManagerType=DefaultProgress, typename F>
 Geometry* convertCurvesAndExecute(GEOSContextHandle_t extHandle, const Geometry* g1, F&& f)
 {
-    return execute(extHandle, [&]() {
+    return execute<InterruptManagerType, ProgressManagerType>(extHandle, [&]() {
         const auto geom1 = convertToLineIfNeeded(extHandle, g1);
 
         auto g3 = f(geom1.get());
@@ -750,6 +807,16 @@ extern "C" {
         return handle->setInterruptHandler(cb, userData);
     }
 
+    GEOSProgressCallback*
+    GEOSContext_setProgressCallback_r(GEOSContextHandle_t extHandle, GEOSProgressCallback* cb, void* userData)
+    {
+        if(0 == extHandle->initialized) {
+            return nullptr;
+        }
+
+        return extHandle->setProgressCallback(cb, userData);
+    }
+
     void GEOSContext_setCurveToLineParams_r(GEOSContextHandle_t extHandle, const GEOSCurveToLineParams* params)
     {
         if (params) {
@@ -1241,7 +1308,7 @@ extern "C" {
     int
     GEOSArea_r(GEOSContextHandle_t extHandle, const Geometry* g, double* area)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             *area = g->getArea();
             return 1;
         });
@@ -1250,7 +1317,7 @@ extern "C" {
     int
     GEOSLength_r(GEOSContextHandle_t extHandle, const Geometry* g, double* length)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             *length = g->getLength();
             return 1;
         });
@@ -2006,7 +2073,8 @@ extern "C" {
     Geometry*
     GEOSUnaryUnion_r(GEOSContextHandle_t extHandle, const Geometry* g)
     {
-        return convertCurvesAndExecute(extHandle, g, [&](const Geometry* input1) {
+        return convertCurvesAndExecute<Interruptible, ReportsProgress>(extHandle, g, [&](const Geometry* input1) {
+            return geos::operation::geounion::UnaryUnionOp::Union(*input1, &extHandle->progressFunction);
             return input1->Union();
         });
     }
@@ -2014,7 +2082,7 @@ extern "C" {
     Geometry*
     GEOSUnaryUnionPrec_r(GEOSContextHandle_t extHandle, const Geometry* g1, double gridSize)
     {
-        return execute(extHandle, [&]() {
+        return execute<Interruptible, DefaultProgress>(extHandle, [&]() {
 
             std::unique_ptr<PrecisionModel> pm;
             if(gridSize != 0) {
@@ -2276,7 +2344,7 @@ extern "C" {
     int
     GEOSGetNumInteriorRings_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
-        return execute<NotInterruptible>(extHandle, -1, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, -1, [&]() {
             const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
                 throw IllegalArgumentException("Argument is not a Surface");
@@ -2290,7 +2358,7 @@ extern "C" {
     int
     GEOSGetNumGeometries_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
-        return execute<NotInterruptible>(extHandle, -1, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, -1, [&]() {
             return static_cast<int>(g1->getNumGeometries());
         });
     }
@@ -2303,7 +2371,7 @@ extern "C" {
     const Geometry*
     GEOSGetGeometryN_r(GEOSContextHandle_t extHandle, const Geometry* g1, int n)
     {
-        return execute<NotInterruptible>(extHandle, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, [&]() {
             if(n < 0) {
                 throw IllegalArgumentException("Index must be non-negative.");
             }
@@ -2314,7 +2382,7 @@ extern "C" {
     int
     GEOSGetNumCurves_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
-        return execute<NotInterruptible>(extHandle, -1, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, -1, [&]() {
             const Curve* curve = dynamic_cast<const Curve*>(g1);
             if (curve) {
                 return static_cast<int>(curve->getNumCurves());
@@ -2327,7 +2395,7 @@ extern "C" {
     const GEOSGeometry*
     GEOSGetCurveN_r(GEOSContextHandle_t extHandle, const Geometry* g, int n)
     {
-        return execute<NotInterruptible>(extHandle, [&]() -> const Geometry* {
+        return execute<NotInterruptible, NoProgress>(extHandle, [&]() -> const Geometry* {
             if(n < 0) {
                 throw IllegalArgumentException("Index must be non-negative.");
             }
@@ -2528,7 +2596,7 @@ extern "C" {
     const Geometry*
     GEOSGetExteriorRing_r(GEOSContextHandle_t extHandle, const Geometry* g1)
     {
-        return execute<NotInterruptible>(extHandle, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, [&]() {
             const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
                 throw IllegalArgumentException("Invalid argument (must be a Surface)");
@@ -2544,7 +2612,7 @@ extern "C" {
     const Geometry*
     GEOSGetInteriorRingN_r(GEOSContextHandle_t extHandle, const Geometry* g1, int n)
     {
-        return execute<NotInterruptible>(extHandle, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, [&]() {
             const Surface* p = dynamic_cast<const Surface*>(g1);
             if(!p) {
                 throw IllegalArgumentException("Invalid argument (must be a Surface)");
@@ -3374,7 +3442,7 @@ extern "C" {
     GEOSCoordSeq_setOrdinate_r(GEOSContextHandle_t extHandle, CoordinateSequence* cs,
                                unsigned int idx, unsigned int dim, double val)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             cs->setOrdinate(idx, dim, val);
             return 1;
         });
@@ -3407,7 +3475,7 @@ extern "C" {
     int
     GEOSCoordSeq_setXY_r(GEOSContextHandle_t extHandle, CoordinateSequence* cs, unsigned int idx, double x, double y)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             cs->setAt(CoordinateXY{x, y}, idx);
             return 1;
         });
@@ -3416,7 +3484,7 @@ extern "C" {
     int
     GEOSCoordSeq_setXYZ_r(GEOSContextHandle_t extHandle, CoordinateSequence* cs, unsigned int idx, double x, double y, double z)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             cs->setAt(Coordinate{x, y, z}, idx);
             return 1;
         });
@@ -3434,7 +3502,7 @@ extern "C" {
     GEOSCoordSeq_getOrdinate_r(GEOSContextHandle_t extHandle, const CoordinateSequence* cs,
                                unsigned int idx, unsigned int dim, double* val)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             *val = cs->getOrdinate(idx, dim);
             return 1;
         });
@@ -3467,7 +3535,7 @@ extern "C" {
     int
     GEOSCoordSeq_getXY_r(GEOSContextHandle_t extHandle, const CoordinateSequence* cs, unsigned int idx, double* x, double* y)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             auto& c = cs->getAt<CoordinateXY>(idx);
             *x = c.x;
             *y = c.y;
@@ -3478,7 +3546,7 @@ extern "C" {
     int
     GEOSCoordSeq_getXYZ_r(GEOSContextHandle_t extHandle, const CoordinateSequence* cs, unsigned int idx, double* x, double* y, double* z)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             auto& c = cs->getAt(idx);
             *x = c.x;
             *y = c.y;
@@ -3490,7 +3558,7 @@ extern "C" {
     int
     GEOSCoordSeq_getSize_r(GEOSContextHandle_t extHandle, const CoordinateSequence* cs, unsigned int* size)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             const std::size_t sz = cs->getSize();
             *size = static_cast<unsigned int>(sz);
             return 1;
@@ -3500,7 +3568,7 @@ extern "C" {
     int
     GEOSCoordSeq_getDimensions_r(GEOSContextHandle_t extHandle, const CoordinateSequence* cs, unsigned int* dims)
     {
-        return execute<NotInterruptible>(extHandle, 0, [&]() {
+        return execute<NotInterruptible, NoProgress>(extHandle, 0, [&]() {
             const std::size_t dim = cs->getDimension();
             *dims = static_cast<unsigned int>(dim);
 
@@ -5100,11 +5168,11 @@ extern "C" {
     GEOSCoverageSimplifyVW_r(GEOSContextHandle_t extHandle,
         const Geometry* input,
         double tolerance,
-        int preserveBoundary)
-    {
+        int preserveBoundary) {
+
         using geos::coverage::CoverageSimplifier;
 
-        return convertCurvesAndExecute(extHandle, input, [&](const Geometry* linearized) -> std::unique_ptr<Geometry> {
+        return convertCurvesAndExecute<Interruptible, ReportsProgress>(extHandle, input, [&](const Geometry* linearized) -> std::unique_ptr<Geometry> {
             const GeometryCollection* col = dynamic_cast<const GeometryCollection*>(linearized);
             if (!col)
                 return nullptr;
@@ -5116,10 +5184,10 @@ extern "C" {
             CoverageSimplifier cov(coverage);
             std::vector<std::unique_ptr<Geometry>> simple;
             if (preserveBoundary == 1) {
-                simple = cov.simplifyInner(tolerance);
+                simple = cov.simplifyInner(tolerance, &extHandle->progressFunction);
             }
             else if (preserveBoundary == 0) {
-                simple = cov.simplify(tolerance);
+                simple = cov.simplify(tolerance, &extHandle->progressFunction);
             }
             else return nullptr;
 
diff --git a/include/geos/coverage/CoverageSimplifier.h b/include/geos/coverage/CoverageSimplifier.h
index 74df234dd..4c87e12eb 100644
--- a/include/geos/coverage/CoverageSimplifier.h
+++ b/include/geos/coverage/CoverageSimplifier.h
@@ -19,6 +19,7 @@
 #include <vector>
 #include <memory>
 #include <geos/export.h>
+#include <geos/util/Progress.h>
 
 
 namespace geos {
@@ -87,15 +88,18 @@ public:
     *
     * @param coverage a set of polygonal geometries forming a coverage
     * @param tolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified polygons
     */
     static std::vector<std::unique_ptr<Geometry>> simplify(
         std::vector<const Geometry*>& coverage,
-        double tolerance);
+        double tolerance,
+        geos::util::ProgressFunction* progressFunction = nullptr);
 
     static std::vector<std::unique_ptr<Geometry>> simplify(
         const std::vector<std::unique_ptr<Geometry>>& coverage,
-        double tolerance);
+        double tolerance,
+        geos::util::ProgressFunction* progressFunction = nullptr);
 
     /**
     * Simplifies the inner boundaries of a set of polygonal geometries forming a coverage,
@@ -104,24 +108,28 @@ public:
     *
     * @param coverage a set of polygonal geometries forming a coverage
     * @param tolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified polygons
     */
     static std::vector<std::unique_ptr<Geometry>> simplifyInner(
         std::vector<const Geometry*>& coverage,
-        double tolerance);
+        double tolerance,
+        geos::util::ProgressFunction* progressFunction);
 
     static std::vector<std::unique_ptr<Geometry>> simplifyInner(
         const std::vector<std::unique_ptr<Geometry>>& coverage,
-        double tolerance);
+        double tolerance,
+        geos::util::ProgressFunction* progressFunction);
 
     /**
     * Computes the simplified coverage, preserving the coverage topology.
     *
     * @param tolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified polygons
     */
     std::vector<std::unique_ptr<Geometry>> simplify(
-        double tolerance);
+        double tolerance, geos::util::ProgressFunction* progressFunction);
 
     /**
     * Computes the inner-boundary simplified coverage,
@@ -129,10 +137,11 @@ public:
     * and leaving outer boundary edges unchanged.
     *
     * @param tolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified polygons
     */
     std::vector<std::unique_ptr<Geometry>> simplifyInner(
-        double tolerance);
+        double tolerance, geos::util::ProgressFunction* progressFunction);
 
 
 private:
@@ -145,7 +154,8 @@ private:
     void simplifyEdges(
         std::vector<CoverageEdge*> edges,
         const MultiLineString* constraints,
-        double tolerance);
+        double tolerance,
+        geos::util::ProgressFunction* progressFunction);
 
     void setCoordinates(
         std::vector<CoverageEdge*>& edges,
diff --git a/include/geos/coverage/TPVWSimplifier.h b/include/geos/coverage/TPVWSimplifier.h
index 0b904c8a8..ec2bce993 100644
--- a/include/geos/coverage/TPVWSimplifier.h
+++ b/include/geos/coverage/TPVWSimplifier.h
@@ -21,6 +21,7 @@
 #include <geos/simplify/LinkedLine.h>
 #include <geos/coverage/Corner.h>
 #include <geos/export.h>
+#include <geos/util/Progress.h>
 
 
 namespace geos {
@@ -164,11 +165,13 @@ public:
     *
     * @param lines the lines to simplify
     * @param distanceTolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified lines
     */
     static std::unique_ptr<MultiLineString> simplify(
         const MultiLineString* lines,
-        double distanceTolerance);
+        double distanceTolerance,
+        geos::util::ProgressFunction* progressFunction);
 
     /**
     * Simplifies a set of lines, preserving the topology of the lines between
@@ -181,13 +184,15 @@ public:
     * @param freeRings flags indicating which ring edges do not have node endpoints
     * @param constraintLines the linear constraints
     * @param distanceTolerance the simplification tolerance
+    * @param progressFunction Progress function, or nullptr.
     * @return the simplified lines
     */
     static std::unique_ptr<MultiLineString> simplify(
         const MultiLineString* lines,
         std::vector<bool>& freeRings,
         const MultiLineString* constraintLines,
-        double distanceTolerance);
+        double distanceTolerance,
+        geos::util::ProgressFunction* progressFunction);
 
     // Constructor
     TPVWSimplifier(const MultiLineString* lines,
@@ -209,11 +214,12 @@ private:
 
     void setConstraints(const MultiLineString* constraints);
 
-    std::unique_ptr<MultiLineString> simplify();
+    std::unique_ptr<MultiLineString> simplify(geos::util::ProgressFunction* progressFunction);
 
     std::vector<Edge> createEdges(
         const MultiLineString* lines,
-        std::vector<bool>& freeRing);
+        std::vector<bool>& freeRing,
+        geos::util::ProgressFunction* progressFunction);
 
 
 }; // TPVWSimplifier
diff --git a/include/geos/geom/Geometry.h b/include/geos/geom/Geometry.h
index 8fb442832..5a4a4fa24 100644
--- a/include/geos/geom/Geometry.h
+++ b/include/geos/geom/Geometry.h
@@ -35,6 +35,7 @@
 #include <geos/geom/Dimension.h> // for Dimension::DimensionType
 #include <geos/geom/GeometryComponentFilter.h> // for inheritance
 #include <geos/geom/CoordinateSequence.h> // to materialize CoordinateSequence
+#include <geos/util/Progress.h>
 
 #include <algorithm>
 #include <string>
@@ -748,9 +749,15 @@ public:
      *
      * @see UnaryUnionOp
      */
-    Ptr Union() const;
+    Ptr Union(geos::util::ProgressFunction* progressFunction) const;
     // throw(IllegalArgumentException *, TopologyException *);
 
+    Ptr Union() const
+    {
+        geos::util::ProgressFunction* progressFunction = nullptr;
+        return Union(progressFunction);
+    }
+
     /**
      * \brief
      * Returns a Geometry representing the points making up this
diff --git a/include/geos/operation/union/CascadedPolygonUnion.h b/include/geos/operation/union/CascadedPolygonUnion.h
index 2b1e29267..fcc5ba9a7 100644
--- a/include/geos/operation/union/CascadedPolygonUnion.h
+++ b/include/geos/operation/union/CascadedPolygonUnion.h
@@ -25,6 +25,7 @@
 #include <geos/export.h>
 
 #include <geos/operation/union/UnionStrategy.h>
+#include <geos/util/Progress.h>
 
 // Forward declarations
 namespace geos {
@@ -140,7 +141,7 @@ public:
      *              ownership of elements *and* vector are left to caller.
      */
     static std::unique_ptr<geom::Geometry> Union(std::vector<geom::Polygon*>* polys);
-    static std::unique_ptr<geom::Geometry> Union(std::vector<geom::Polygon*>* polys, UnionStrategy* unionFun);
+    static std::unique_ptr<geom::Geometry> Union(std::vector<geom::Polygon*>* polys, UnionStrategy* unionFun, geos::util::ProgressFunction* progressFunction);
 
     /** \brief
      * Computes the union of a set of polygonal [Geometrys](@ref geom::Geometry).
@@ -149,17 +150,18 @@ public:
      * @param start start iterator
      * @param end end iterator
      * @param unionStrategy strategy to apply
+     * @param progressFunction progress function
      */
     template <class T>
     static std::unique_ptr<geom::Geometry>
-    Union(T start, T end, UnionStrategy *unionStrategy)
+    Union(T start, T end, UnionStrategy *unionStrategy, geos::util::ProgressFunction* progressFunction)
     {
         std::vector<geom::Polygon*> polys;
         for(T i = start; i != end; ++i) {
             const geom::Polygon* p = dynamic_cast<const geom::Polygon*>(*i);
             polys.push_back(const_cast<geom::Polygon*>(p));
         }
-        return Union(&polys, unionStrategy);
+        return Union(&polys, unionStrategy, progressFunction);
     }
 
     /** \brief
@@ -167,8 +169,9 @@ public:
      *
      * @param polys a collection of polygonal [Geometrys](@ref geom::Geometry).
      *              Ownership of elements *and* vector are left to caller.
+     * @param progressFunction progress function
      */
-    static std::unique_ptr<geom::Geometry> Union(const geom::MultiPolygon* polys);
+    static std::unique_ptr<geom::Geometry> Union(const geom::MultiPolygon* polys, geos::util::ProgressFunction* progressFunction);
 
     /** \brief
      * Creates a new instance to union the given collection of
@@ -192,10 +195,11 @@ public:
     /** \brief
      * Computes the union of the input geometries.
      *
+     * @param progressFunction progress function
      * @return the union of the input geometries
      * @return `null` if no input geometries were provided
      */
-    std::unique_ptr<geom::Geometry> Union();
+    std::unique_ptr<geom::Geometry> Union(geos::util::ProgressFunction* progressFunction);
 
 private:
 
@@ -211,7 +215,11 @@ private:
      * @param end the index after the end of the section
      * @return the union of the list section
      */
-    std::unique_ptr<geom::Geometry> binaryUnion(const std::vector<const geom::Geometry*> & geoms, std::size_t start, std::size_t end);
+    std::unique_ptr<geom::Geometry> binaryUnion(
+        const std::vector<const geom::Geometry*> & geoms,
+        std::size_t start,
+        std::size_t end,
+        std::function<void()>* unitProgress);
 
     /**
      * Computes the union of two geometries,
diff --git a/include/geos/operation/union/UnaryUnionOp.h b/include/geos/operation/union/UnaryUnionOp.h
index 3fed7e32b..98b6a9e70 100644
--- a/include/geos/operation/union/UnaryUnionOp.h
+++ b/include/geos/operation/union/UnaryUnionOp.h
@@ -28,6 +28,7 @@
 #include <geos/geom/Polygon.h>
 #include <geos/geom/util/GeometryExtracter.h>
 #include <geos/operation/union/CascadedPolygonUnion.h>
+#include <geos/util/Progress.h>
 
 #include <geos/util.h>
 
@@ -93,7 +94,8 @@ public:
     Union(const T& geoms)
     {
         UnaryUnionOp op(geoms);
-        return op.Union();
+        geos::util::ProgressFunction* progressFunction = nullptr;
+        return op.Union(progressFunction);
     }
 
     template <class T>
@@ -102,14 +104,15 @@ public:
           geom::GeometryFactory& geomFact)
     {
         UnaryUnionOp op(geoms, geomFact);
-        return op.Union();
+        geos::util::ProgressFunction* progressFunction = nullptr;
+        return op.Union(progressFunction);
     }
 
     static std::unique_ptr<geom::Geometry>
-    Union(const geom::Geometry& geom)
+    Union(const geom::Geometry& geom, geos::util::ProgressFunction* progressFunction)
     {
         UnaryUnionOp op(geom);
-        return op.Union();
+        return op.Union(progressFunction);
     }
 
     template <class T>
@@ -150,7 +153,7 @@ public:
      * @return an empty GEOMETRYCOLLECTION if no geometries were provided
      *         in the input
      */
-    std::unique_ptr<geom::Geometry> Union();
+    std::unique_ptr<geom::Geometry> Union(geos::util::ProgressFunction* progressFunction);
 
 private:
 
diff --git a/include/geos/util/Progress.h b/include/geos/util/Progress.h
new file mode 100644
index 000000000..c581c60ac
--- /dev/null
+++ b/include/geos/util/Progress.h
@@ -0,0 +1,84 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 Even Rouault <even.rouault at spatialys.com>
+ *
+ * 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 <functional>
+
+#include <geos/export.h>
+
+namespace geos {
+namespace util { // geos::util
+
+/** Signature of a progression function.
+ *
+ * Such function takes a progression ratio (between 0 and 1), and an optional
+ * message.
+ */
+typedef std::function<void(double, const char*)> ProgressFunction;
+
+/** Do progress function related processing for an iteration loop of iterCount iterations.
+ *
+ * This function will invoke (*progressFunction) every notificationInterval
+ *iteration.
+ *
+ * This function will return, or throw a geos::util::InterruptedException.
+ *
+ * A typical use is:
+ * \code
+ * const size_t notificationInterval = std::max<size_t>(1, iterCount / 100);
+ * for (size_t i = 0, iNotify = 0; i < iterCount; ++i) {
+ *     // do something useful
+ *     if (progressFunction) {
+ *         geos::util::ProgressFunctionIteration(*progressFunction, i, iterCount, iNotify, notificationInterval);
+ *     }
+ * }
+ * if (progressFunction) {
+ *   (*progressFunction)(1.0, nullptr);
+ * }
+ * \endcode
+ *
+ * @param progressFunction Progress function
+ * @param i Current index of loop iteration.
+ * @param iterCount Total number of loop iteration.
+ * @param iNotify Notification counter, updated by this function. Must be set by the caller (before the loop) to 0.
+ * @param notificationInterval Notification interval (e.g. iterCount / 100).
+ */
+void ProgressFunctionIteration(ProgressFunction& progressFunction, size_t i, size_t iterCount, size_t& iNotify, size_t notificationInterval);
+
+/** Create a scaled progress function.
+ *
+ * Sometimes when an operation wants to report progress, it actually
+ * invokes several subprocesses which also take a ProgressFunction,
+ * and it is desirable to map the progress of each sub operation into
+ * a portion of 0.0 to 1.0 progress of the overall process. The scaled
+ * progress function can be used for this.
+ *
+ * For each subsection a scaled progress function is created and
+ * instead of passing the overall progress func down to the sub functions,
+ * the scale progress function is passed instead.
+ *
+ * @param ratioMin the value to which 0.0 in the sub operation is mapped.
+ * @param ratioMax the value to which 1.0 is the sub operation is mapped.
+ * @param progressFunction the overall progress function.
+ *
+ * @return scaled progress function.
+ */
+ProgressFunction CreateScaledProgressFunction(double ratioMin, double ratioMax,
+                                              ProgressFunction& progressFunction);
+
+
+} // namespace geos::util
+} // namespace geos
+
diff --git a/src/coverage/CoverageSimplifier.cpp b/src/coverage/CoverageSimplifier.cpp
index 0623cedce..fd52f0e84 100644
--- a/src/coverage/CoverageSimplifier.cpp
+++ b/src/coverage/CoverageSimplifier.cpp
@@ -26,7 +26,7 @@
 using geos::geom::Geometry;
 using geos::geom::GeometryFactory;
 using geos::geom::MultiLineString;
-
+using geos::util::ProgressFunction;
 
 namespace geos {     // geos
 namespace coverage { // geos.coverage
@@ -35,23 +35,25 @@ namespace coverage { // geos.coverage
 std::vector<std::unique_ptr<Geometry>>
 CoverageSimplifier::simplify(
     std::vector<const Geometry*>& coverage,
-    double tolerance)
+    double tolerance,
+    ProgressFunction* progressFunction)
 {
     CoverageSimplifier simplifier(coverage);
-    return simplifier.simplify(tolerance);
+    return simplifier.simplify(tolerance, progressFunction);
 }
 
 /* public static */
 std::vector<std::unique_ptr<Geometry>>
 CoverageSimplifier::simplify(
     const std::vector<std::unique_ptr<Geometry>>& coverage,
-    double tolerance)
+    double tolerance,
+    ProgressFunction* progressFunction)
 {
     std::vector<const Geometry*> geoms;
     for (auto& geom : coverage) {
         geoms.push_back(geom.get());
     }
-    return simplify(geoms, tolerance);
+    return simplify(geoms, tolerance, progressFunction);
 }
 
 
@@ -59,10 +61,11 @@ CoverageSimplifier::simplify(
 std::vector<std::unique_ptr<Geometry>>
 CoverageSimplifier::simplifyInner(
     std::vector<const Geometry*>& coverage,
-    double tolerance)
+    double tolerance,
+    ProgressFunction* progressFunction)
 {
     CoverageSimplifier simplifier(coverage);
-    return simplifier.simplifyInner(tolerance);
+    return simplifier.simplifyInner(tolerance, progressFunction);
 }
 
 
@@ -70,13 +73,14 @@ CoverageSimplifier::simplifyInner(
 std::vector<std::unique_ptr<Geometry>>
 CoverageSimplifier::simplifyInner(
     const std::vector<std::unique_ptr<Geometry>>& coverage,
-    double tolerance)
+    double tolerance,
+    ProgressFunction* progressFunction)
 {
     std::vector<const Geometry*> geoms;
     for (auto& geom : coverage) {
         geoms.push_back(geom.get());
     }
-    return simplifyInner(geoms, tolerance);
+    return simplifyInner(geoms, tolerance, progressFunction);
 }
 
 
@@ -94,23 +98,25 @@ CoverageSimplifier::CoverageSimplifier(const std::vector<const Geometry*>& cover
 
 /* public */
 std::vector<std::unique_ptr<Geometry>>
-CoverageSimplifier::simplify(double tolerance)
+CoverageSimplifier::simplify(double tolerance,
+                             ProgressFunction* progressFunction)
 {
     CoverageRingEdges cov(m_input);
-    simplifyEdges(cov.getEdges(), nullptr, tolerance);
+    simplifyEdges(cov.getEdges(), nullptr, tolerance, progressFunction);
     return cov.buildCoverage();
 }
 
 /* public */
 std::vector<std::unique_ptr<Geometry>>
-CoverageSimplifier::simplifyInner(double tolerance)
+CoverageSimplifier::simplifyInner(double tolerance,
+                                  ProgressFunction* progressFunction)
 {
     CoverageRingEdges cov(m_input);
     std::vector<CoverageEdge*> innerEdges = cov.selectEdges(2);
     std::vector<CoverageEdge*> outerEdges = cov.selectEdges(1);
     std::unique_ptr<MultiLineString> constraintEdges = CoverageEdge::createLines(outerEdges, m_geomFactory);
 
-    simplifyEdges(innerEdges, constraintEdges.get(), tolerance);
+    simplifyEdges(innerEdges, constraintEdges.get(), tolerance, progressFunction);
     return cov.buildCoverage();
 }
 
@@ -119,11 +125,12 @@ void
 CoverageSimplifier::simplifyEdges(
     std::vector<CoverageEdge*> edges,
     const MultiLineString* constraints,
-    double tolerance)
+    double tolerance,
+    ProgressFunction* progressFunction)
 {
     std::unique_ptr<MultiLineString> lines = CoverageEdge::createLines(edges, m_geomFactory);
     std::vector<bool> freeRings = getFreeRings(edges);
-    std::unique_ptr<MultiLineString> linesSimp = TPVWSimplifier::simplify(lines.get(), freeRings, constraints, tolerance);
+    std::unique_ptr<MultiLineString> linesSimp = TPVWSimplifier::simplify(lines.get(), freeRings, constraints, tolerance, progressFunction);
     //Assert: mlsSimp.getNumGeometries = edges.length
 
     setCoordinates(edges, linesSimp.get());
diff --git a/src/coverage/TPVWSimplifier.cpp b/src/coverage/TPVWSimplifier.cpp
index 0bf36ac30..d31169774 100644
--- a/src/coverage/TPVWSimplifier.cpp
+++ b/src/coverage/TPVWSimplifier.cpp
@@ -47,10 +47,11 @@ typedef TPVWSimplifier::EdgeIndex EdgeIndex;
 std::unique_ptr<MultiLineString>
 TPVWSimplifier::simplify(
     const MultiLineString* lines,
-    double distanceTolerance)
+    double distanceTolerance,
+    geos::util::ProgressFunction* progressFunction)
 {
     TPVWSimplifier simp(lines, distanceTolerance);
-    std::unique_ptr<MultiLineString> result = simp.simplify();
+    std::unique_ptr<MultiLineString> result = simp.simplify(progressFunction);
     return result;
 }
 
@@ -61,12 +62,13 @@ TPVWSimplifier::simplify(
     const MultiLineString* p_lines,
     std::vector<bool>& p_freeRings,
     const MultiLineString* p_constraintLines,
-    double distanceTolerance)
+    double distanceTolerance,
+    geos::util::ProgressFunction* progressFunction)
 {
     TPVWSimplifier simp(p_lines, distanceTolerance);
     simp.setFreeRingIndices(p_freeRings);
     simp.setConstraints(p_constraintLines);
-    std::unique_ptr<MultiLineString> result = simp.simplify();
+    std::unique_ptr<MultiLineString> result = simp.simplify(progressFunction);
     return result;
 }
 
@@ -99,21 +101,60 @@ TPVWSimplifier::setFreeRingIndices(std::vector<bool>& freeRing)
 
 /* private */
 std::unique_ptr<MultiLineString>
-TPVWSimplifier::simplify()
+TPVWSimplifier::simplify(geos::util::ProgressFunction* progressFunction)
 {
     std::vector<bool> emptyList;
-    std::vector<Edge> edges = createEdges(inputLines, isFreeRing);
-    std::vector<Edge> constraintEdges = createEdges(constraintLines, emptyList);
+    geos::util::ProgressFunction subProgress;
+
+    constexpr double RATIO_FIRST_PASS = 0.4;
+
+    const double ratioInputLinesOverInputAndConstraint =
+        RATIO_FIRST_PASS *
+        static_cast<double>(inputLines ? inputLines->getNumGeometries() : 0) /
+        static_cast<double>(std::max<size_t>(1,
+            (inputLines ? inputLines->getNumGeometries() : 0) +
+            (constraintLines ? constraintLines->getNumGeometries() : 0)));
+
+    if (progressFunction)
+    {
+        subProgress = geos::util::CreateScaledProgressFunction(
+            0, ratioInputLinesOverInputAndConstraint, *progressFunction);
+    }
+    std::vector<Edge> edges = createEdges(inputLines, isFreeRing,
+        progressFunction ? &subProgress : nullptr);
+
+    if (progressFunction)
+    {
+        subProgress = geos::util::CreateScaledProgressFunction(
+            ratioInputLinesOverInputAndConstraint, RATIO_FIRST_PASS, *progressFunction);
+    }
+    std::vector<Edge> constraintEdges = createEdges(
+        constraintLines, emptyList,
+        progressFunction ? &subProgress : nullptr);
 
     EdgeIndex edgeIndex;
     edgeIndex.add(edges);
     edgeIndex.add(constraintEdges);
 
     std::vector<std::unique_ptr<LineString>> result;
-    for (auto& edge : edges) {
+    const size_t iterCount = edges.size();
+    const size_t notificationInterval = std::max<size_t>(1, iterCount / 100);
+    if (progressFunction)
+    {
+        subProgress = geos::util::CreateScaledProgressFunction(
+                        RATIO_FIRST_PASS, 1.0, *progressFunction);
+    }
+    for (size_t i = 0, iNotify = 0; i < iterCount; ++i) {
+        auto& edge = edges[i];
         std::unique_ptr<CoordinateSequence> ptsSimp = edge.simplify(edgeIndex);
         auto ls = geomFactory->createLineString(std::move(ptsSimp));
         result.emplace_back(ls.release());
+        if (progressFunction) {
+            geos::util::ProgressFunctionIteration(subProgress, i, iterCount, iNotify, notificationInterval);
+        }
+    }
+    if (progressFunction) {
+        (*progressFunction)(1.0, nullptr);
     }
     return geomFactory->createMultiLineString(std::move(result));
 }
@@ -122,17 +163,25 @@ TPVWSimplifier::simplify()
 std::vector<Edge>
 TPVWSimplifier::createEdges(
     const MultiLineString* lines,
-    std::vector<bool>& freeRing)
+    std::vector<bool>& freeRing,
+    geos::util::ProgressFunction* progressFunction)
 {
     std::vector<Edge> edges;
 
     if (lines == nullptr)
         return edges;
-
-    for (std::size_t i = 0; i < lines->getNumGeometries(); i++) {
+    const size_t iterCount = lines->getNumGeometries();
+    const size_t notificationInterval = std::max<size_t>(1, iterCount / 100);
+    for (size_t i = 0, iNotify = 0; i < iterCount; ++i) {
         const LineString* line = lines->getGeometryN(i);
         bool isFree = freeRing.empty() ? false : freeRing[i];
         edges.emplace_back(line, isFree, areaTolerance);
+        if (progressFunction) {
+            geos::util::ProgressFunctionIteration(*progressFunction, i, iterCount, iNotify, notificationInterval);
+        }
+    }
+    if (progressFunction) {
+        (*progressFunction)(1.0, nullptr);
     }
     return edges;
 }
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index a34c7bf35..b833a0499 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -624,10 +624,10 @@ Geometry::Union(const Geometry* other) const
 
 /* public */
 Geometry::Ptr
-Geometry::Union() const
+Geometry::Union(geos::util::ProgressFunction* progressFunction) const
 {
     using geos::operation::geounion::UnaryUnionOp;
-    return UnaryUnionOp::Union(*this);
+    return UnaryUnionOp::Union(*this, progressFunction);
 }
 
 std::unique_ptr<Geometry>
diff --git a/src/geom/util/GeometryFixer.cpp b/src/geom/util/GeometryFixer.cpp
index 6d73ecbc9..7523978f4 100644
--- a/src/geom/util/GeometryFixer.cpp
+++ b/src/geom/util/GeometryFixer.cpp
@@ -341,7 +341,8 @@ GeometryFixer::unionGeometry(std::vector<const Geometry*>& polys) const
     }
 
     UnaryUnionOp op(polys);
-    return op.Union();
+    geos::util::ProgressFunction* progressFunction = nullptr;
+    return op.Union(progressFunction);
     // return OverlayNGRobust::Union(polys);
 }
 
diff --git a/src/operation/overlayng/OverlayNGRobust.cpp b/src/operation/overlayng/OverlayNGRobust.cpp
index 258a7ecda..3e8da6f9d 100644
--- a/src/operation/overlayng/OverlayNGRobust.cpp
+++ b/src/operation/overlayng/OverlayNGRobust.cpp
@@ -76,7 +76,8 @@ OverlayNGRobust::Union(const Geometry* a)
     geounion::UnaryUnionOp op(*a);
     SRUnionStrategy unionSRFun;
     op.setUnionFunction(&unionSRFun);
-    return op.Union();
+    geos::util::ProgressFunction* progressFunction = nullptr;
+    return op.Union(progressFunction);
 }
 
 /*public static*/
diff --git a/src/operation/overlayng/UnaryUnionNG.cpp b/src/operation/overlayng/UnaryUnionNG.cpp
index f09b987b9..609fa93ee 100644
--- a/src/operation/overlayng/UnaryUnionNG.cpp
+++ b/src/operation/overlayng/UnaryUnionNG.cpp
@@ -37,7 +37,8 @@ UnaryUnionNG::Union(const Geometry* geom, const PrecisionModel& pm)
     NGUnionStrategy ngUnionStrat(pm);
     geounion::UnaryUnionOp op(*geom);
     op.setUnionFunction(&ngUnionStrat);
-    return op.Union();
+    geos::util::ProgressFunction* progressFunction = nullptr;
+    return op.Union(progressFunction);
 }
 
 /*public static*/
diff --git a/src/operation/polygonize/BuildArea.cpp b/src/operation/polygonize/BuildArea.cpp
index 37ccd371e..9f16ac647 100644
--- a/src/operation/polygonize/BuildArea.cpp
+++ b/src/operation/polygonize/BuildArea.cpp
@@ -270,7 +270,7 @@ std::unique_ptr<geom::Geometry> BuildArea::build(const geom::Geometry* geom) {
 
     /* Run a single overlay operation to dissolve shared edges */
     auto shp = std::unique_ptr<geom::Geometry>(
-        geos::operation::geounion::CascadedPolygonUnion::CascadedPolygonUnion::Union(tmp.get()));
+        geos::operation::geounion::CascadedPolygonUnion::CascadedPolygonUnion::Union(tmp.get(), nullptr));
     if( shp ) {
         shp->setSRID(geom->getSRID());
     }
diff --git a/src/operation/union/CascadedPolygonUnion.cpp b/src/operation/union/CascadedPolygonUnion.cpp
index 35a93b0e9..aac120c80 100644
--- a/src/operation/union/CascadedPolygonUnion.cpp
+++ b/src/operation/union/CascadedPolygonUnion.cpp
@@ -47,18 +47,19 @@ std::unique_ptr<geom::Geometry>
 CascadedPolygonUnion::Union(std::vector<geom::Polygon*>* polys)
 {
     CascadedPolygonUnion op(polys);
-    return op.Union();
+    geos::util::ProgressFunction* progressFunction = nullptr;
+    return op.Union(progressFunction);
 }
 
 std::unique_ptr<geom::Geometry>
-CascadedPolygonUnion::Union(std::vector<geom::Polygon*>* polys, UnionStrategy* unionFun)
+CascadedPolygonUnion::Union(std::vector<geom::Polygon*>* polys, UnionStrategy* unionFun, geos::util::ProgressFunction* progressFunction)
 {
     CascadedPolygonUnion op(polys, unionFun);
-    return op.Union();
+    return op.Union(progressFunction);
 }
 
 std::unique_ptr<geom::Geometry>
-CascadedPolygonUnion::Union(const geom::MultiPolygon* multipoly)
+CascadedPolygonUnion::Union(const geom::MultiPolygon* multipoly, geos::util::ProgressFunction* progressFunction)
 {
     std::vector<geom::Polygon*> polys;
 
@@ -67,11 +68,11 @@ CascadedPolygonUnion::Union(const geom::MultiPolygon* multipoly)
     }
 
     CascadedPolygonUnion op(&polys);
-    return op.Union();
+    return op.Union(progressFunction);
 }
 
 std::unique_ptr<geom::Geometry>
-CascadedPolygonUnion::Union()
+CascadedPolygonUnion::Union(geos::util::ProgressFunction* progressFunction)
 {
     if(inputPolys->empty()) {
         return nullptr;
@@ -94,28 +95,43 @@ CascadedPolygonUnion::Union()
     // TODO avoid creating this vector and run binaryUnion off the iterators directly
     std::vector<const geom::Geometry*> geoms(index.items().begin(), index.items().end());
 
-    return binaryUnion(geoms, 0, geoms.size());
+    size_t inc = 0;
+    std::function<void()> UnitProgress = [progressFunction, &inc, &geoms]()
+    {
+        ++inc;
+        (*progressFunction)(static_cast<double>(inc)/static_cast<double>(geoms.size()), "");
+    };
+
+    return binaryUnion(geoms, 0, geoms.size(), progressFunction ? &UnitProgress : nullptr);
 }
 
 
 std::unique_ptr<geom::Geometry>
 CascadedPolygonUnion::binaryUnion(const std::vector<const geom::Geometry*> & geoms,
-                                  std::size_t start, std::size_t end)
+                                  std::size_t start, std::size_t end,
+                                  std::function<void()>* unitProgress)
 {
     if(end - start == 0) {
         return nullptr;
     }
     else if(end - start == 1) {
+        if( unitProgress )
+            (*unitProgress)();
         return unionSafe(geoms[start], nullptr);
     }
     else if(end - start == 2) {
+        if( unitProgress )
+        {
+            (*unitProgress)();
+            (*unitProgress)();
+        }
         return unionSafe(geoms[start], geoms[start + 1]);
     }
     else {
         // recurse on both halves of the list
         std::size_t mid = (end + start) / 2;
-        std::unique_ptr<geom::Geometry> g0(binaryUnion(geoms, start, mid));
-        std::unique_ptr<geom::Geometry> g1(binaryUnion(geoms, mid, end));
+        std::unique_ptr<geom::Geometry> g0(binaryUnion(geoms, start, mid, unitProgress));
+        std::unique_ptr<geom::Geometry> g1(binaryUnion(geoms, mid, end, unitProgress));
         return unionSafe(std::move(g0), std::move(g1));
     }
 }
diff --git a/src/operation/union/UnaryUnionOp.cpp b/src/operation/union/UnaryUnionOp.cpp
index 09073f59a..7b6601c14 100644
--- a/src/operation/union/UnaryUnionOp.cpp
+++ b/src/operation/union/UnaryUnionOp.cpp
@@ -64,7 +64,7 @@ UnaryUnionOp::unionWithNull(std::unique_ptr<geom::Geometry> g0,
 
 /*public*/
 std::unique_ptr<geom::Geometry>
-UnaryUnionOp::Union()
+UnaryUnionOp::Union(geos::util::ProgressFunction* progressFunction)
 {
     typedef std::unique_ptr<geom::Geometry> GeomPtr;
 
@@ -95,7 +95,7 @@ UnaryUnionOp::Union()
 
     GeomPtr unionPolygons;
     if(!polygons.empty()) {
-        unionPolygons = CascadedPolygonUnion::Union(polygons.begin(), polygons.end(), unionFunction);
+        unionPolygons = CascadedPolygonUnion::Union(polygons.begin(), polygons.end(), unionFunction, progressFunction);
     }
 
     /*
@@ -122,6 +122,10 @@ UnaryUnionOp::Union()
         ret = geomFact->createGeometryCollection();
     }
 
+    if (polygons.empty() && progressFunction) {
+        (*progressFunction)(1.0, "");
+    }
+
     return ret;
 
 }
diff --git a/src/util/Progress.cpp b/src/util/Progress.cpp
new file mode 100644
index 000000000..ab4041e68
--- /dev/null
+++ b/src/util/Progress.cpp
@@ -0,0 +1,43 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2025 Even Rouault <even.rouault at spatialys.com>
+ *
+ * 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/util/Progress.h>
+
+namespace geos {
+namespace util { // geos::util
+
+void ProgressFunctionIteration(ProgressFunction& progressFunction, size_t i,
+                               size_t iterCount, size_t& iNotify,
+                               size_t notificationInterval) {
+    if (iNotify + 1 == notificationInterval) {
+        progressFunction(static_cast<double>(i + 1)/static_cast<double>(iterCount), nullptr);
+        iNotify = 0;
+    }
+    else {
+        ++iNotify;
+    }
+}
+
+ProgressFunction CreateScaledProgressFunction(double ratioMin, double ratioMax,
+                                              ProgressFunction& progressFunction)
+{
+    return [ratioMin, ratioMax, &progressFunction](double ratio, const char* msg)
+    {
+        progressFunction(ratioMin + (ratioMax - ratioMin) * ratio, msg);
+    };
+}
+
+} // namespace geos::util
+} // namespace geos
+
diff --git a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
index d22ae9b79..ea4ff4353 100644
--- a/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
+++ b/tests/unit/capi/GEOSCoverageSimplifyTest.cpp
@@ -84,10 +84,27 @@ template<> void object::test<3>
     const char* inputWKT = "GEOMETRYCOLLECTION(POLYGON(( 0 0,10 0,10.1 5,10 10,0 10,0 0)),POLYGON((10 0,20 0,20 10,10 10,10.1 5,10 0)))";
 
     input_ = fromWKT(inputWKT);
-    result_ = GEOSCoverageSimplifyVW(input_, 1.0, 0);
 
+    struct Cbk
+    {
+        double lastRatio = 0;
+
+        static void Func(double progressRatio, const char* /* msg */, void* userdata)
+        {
+            Cbk* self = static_cast<Cbk*>(userdata);
+            self->lastRatio = progressRatio;
+        }
+    };
+
+    Cbk cbk;
+    auto context = initGEOS_r(nullptr, nullptr);
+    GEOSContext_setProgressCallback_r(context, &Cbk::Func, &cbk);
+    result_ = GEOSCoverageSimplifyVW_r(context, input_, 1.0, 0);
+    finishGEOS_r(context);
+
+    ensure_equals("cbk.lastRatio == 1.0", cbk.lastRatio, 1.0);
     ensure( result_ != nullptr );
-    ensure( GEOSGeomTypeId(result_) == GEOS_GEOMETRYCOLLECTION );
+    ensure_equals( GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION );
 
     const char* expectedWKT = "GEOMETRYCOLLECTION(POLYGON((0 0,10 0,10 10,0 10,0 0)),POLYGON((10 0,20 0,20 10,10 10,10 0)))";
 
diff --git a/tests/unit/capi/GEOSDifferenceTest.cpp b/tests/unit/capi/GEOSDifferenceTest.cpp
index ee1e8c823..d38f28c1d 100644
--- a/tests/unit/capi/GEOSDifferenceTest.cpp
+++ b/tests/unit/capi/GEOSDifferenceTest.cpp
@@ -78,5 +78,31 @@ void object::test<3>()
     ensure_geometry_equals(result_, expected_, 1e-8);
 }
 
+template<>
+template<>
+void object::test<4>()
+{
+    set_test_name("progress reporting");
+    // GEOSDifference does not actually implement progress reporting, but our callback will automatically be
+    // called at 100% by the C API "DefaultProgress" handler.
+
+    useContext();
+
+    geom1_ = fromWKT("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))");
+    geom2_ = fromWKT("POLYGON ((5 5, 5 7, 7 7, 7 5, 5 5))");
+
+    std::vector<double> fracs;
+
+    auto progress = [](double f, const char*, void* userData) {
+        static_cast<std::vector<double>*>(userData)->push_back(f);
+    };
+
+    GEOSContext_setProgressCallback_r(ctxt_, progress, &fracs);
+    result_ = GEOSDifference_r(ctxt_, geom1_, geom2_);
+
+    ensure_equals(fracs.size(), 1u);
+    ensure_equals(fracs[0], 1.0);
+}
+
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSUnaryUnionTest.cpp b/tests/unit/capi/GEOSUnaryUnionTest.cpp
index dea0c8941..24f99fcb9 100644
--- a/tests/unit/capi/GEOSUnaryUnionTest.cpp
+++ b/tests/unit/capi/GEOSUnaryUnionTest.cpp
@@ -5,6 +5,7 @@
 // geos
 #include <geos_c.h>
 // std
+#include <algorithm>
 #include <cstring>
 
 #include "capi_test_utils.h"
@@ -252,6 +253,60 @@ void object::test<12>()
     ensure_equals("length does not match", result_length, input_length, 1e-5);
 }
 
+template<>
+template<>
+void object::test<13>()
+{
+    set_test_name("progress callback invoked (cascaded union)");
+    useContext();
+
+    std::vector<GEOSGeometry*> geoms;
+    for (int i = 0; i < 10; i++) {
+        GEOSGeometry* pt = GEOSGeom_createPointFromXY_r(ctxt_, static_cast<double>(i), static_cast<double>(i));
+        GEOSGeometry* circle = GEOSBuffer_r(ctxt_, pt, 3, 8);
+        geoms.push_back(circle);
+        GEOSGeom_destroy_r(ctxt_, pt);
+    }
+    input_ = GEOSGeom_createCollection_r(ctxt_, GEOS_GEOMETRYCOLLECTION, geoms.data(), static_cast<unsigned>(geoms.size()));
+
+    std::vector<double> fracs;
+
+    auto progress = [](double f, const char*, void* userData) {
+        static_cast<std::vector<double>*>(userData)->push_back(f);
+    };
+
+    GEOSContext_setProgressCallback_r(ctxt_, progress, &fracs);
+    result_ = GEOSUnaryUnion_r(ctxt_, input_);
+
+    ensure(fracs.size() > 3); // arbitrary
+
+    // progress reported in-order
+    ensure(std::is_sorted(fracs.begin(), fracs.end()));
+
+    // no duplicate progress values
+    ensure(std::adjacent_find(fracs.begin(), fracs.end()) == fracs.end());
+}
+
+template<>
+template<>
+void object::test<14>() {
+    set_test_name("progress callback invoked (input already unioned)");
+    useContext();
+
+    input_ = fromWKT("POINT (1 1)");
+
+    std::vector<double> fracs;
+
+    auto progress = [](double f, const char*, void* userData) {
+        static_cast<std::vector<double>*>(userData)->push_back(f);
+    };
+
+    GEOSContext_setProgressCallback_r(ctxt_, progress, &fracs);
+    result_ = GEOSUnaryUnion_r(ctxt_, input_);
+
+    ensure_equals(fracs.size(), 1u);
+    ensure_equals(fracs[0], 1.0);
+}
 
 } // namespace tut
 
diff --git a/tests/unit/coverage/CoverageSimplifierTest.cpp b/tests/unit/coverage/CoverageSimplifierTest.cpp
index be8b1938c..926436409 100644
--- a/tests/unit/coverage/CoverageSimplifierTest.cpp
+++ b/tests/unit/coverage/CoverageSimplifierTest.cpp
@@ -31,7 +31,7 @@ struct test_coveragesimplifier_data {
     void checkNoop(
         const std::vector<std::unique_ptr<Geometry>>& input)
     {
-        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplify(input, 0);
+        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplify(input, 0, nullptr);
 
         // std::cout << w.write(*input[0]) << std::endl;
         // std::cout << w.write(*input[1]) << std::endl;
@@ -47,8 +47,16 @@ struct test_coveragesimplifier_data {
         double tolerance,
         const std::vector<std::unique_ptr<Geometry>>& expected)
     {
-        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplify(input, tolerance);
+        double lastRatio = 0.0;
+        geos::util::ProgressFunction myProgress = [&lastRatio](double ratio, const char*)
+        {
+            ensure("ratio >= lastRatio", ratio >= lastRatio);
+            lastRatio = ratio;
+            return true;
+        };
+        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplify(input, tolerance, &myProgress);
         checkArrayEqual(expected, actual);
+        ensure("lastRatio == 1.0", lastRatio == 1.0);
     }
 
     void checkResultInner(
@@ -56,7 +64,7 @@ struct test_coveragesimplifier_data {
         double tolerance,
         const std::vector<std::unique_ptr<Geometry>>& expected)
     {
-        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplifyInner(input, tolerance);
+        std::vector<std::unique_ptr<Geometry>> actual = CoverageSimplifier::simplifyInner(input, tolerance, nullptr);
         checkArrayEqual(expected, actual);
     }
 
@@ -477,7 +485,7 @@ void object::test<30> ()
         });
     try {
         std::vector<std::unique_ptr<Geometry>> result =
-        CoverageSimplifier::simplify(input, 10);
+        CoverageSimplifier::simplify(input, 10, nullptr);
     }
     catch (geos::util::IllegalArgumentException&) {
         ensure("caught IllegalArgumentException", true);
@@ -497,7 +505,7 @@ void object::test<31> ()
         });
     try {
         std::vector<std::unique_ptr<Geometry>> result =
-        CoverageSimplifier::simplify(input, 10);
+        CoverageSimplifier::simplify(input, 10, nullptr);
     }
     catch (geos::util::IllegalArgumentException&) {
         ensure("caught IllegalArgumentException", true);
diff --git a/tests/unit/coverage/TPVWSimplifierTest.cpp b/tests/unit/coverage/TPVWSimplifierTest.cpp
index ce55c9965..e8762ce42 100644
--- a/tests/unit/coverage/TPVWSimplifierTest.cpp
+++ b/tests/unit/coverage/TPVWSimplifierTest.cpp
@@ -31,7 +31,7 @@ struct test_tpvwsimplifier_data {
     {
         std::unique_ptr<Geometry> geom = r.read(wkt);
         const MultiLineString* mls = static_cast<const MultiLineString*>(geom.get());
-        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(mls, tolerance);
+        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(mls, tolerance, nullptr);
         ensure_equals_geometry(actual.get(), geom.get());
     }
 
@@ -42,7 +42,7 @@ struct test_tpvwsimplifier_data {
         const std::string& wktExpected)
     {
         auto mls = r.read<MultiLineString>(wkt);
-        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(mls.get(), tolerance);
+        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(mls.get(), tolerance, nullptr);
         std::unique_ptr<Geometry> expected = r.read(wktExpected);
         ensure_equals_geometry(actual.get(), expected.get());
     }
@@ -76,7 +76,7 @@ struct test_tpvwsimplifier_data {
             constraints = r.read<MultiLineString>(wktConstraints);
         }
 
-        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(lines.get(), freeRings, constraints.get(), tolerance);
+        std::unique_ptr<Geometry> actual = TPVWSimplifier::simplify(lines.get(), freeRings, constraints.get(), tolerance, nullptr);
         std::unique_ptr<Geometry> expected = r.read(wktExpected);
 
         // std::cout << "-- actual" << std::endl;
diff --git a/util/geosop/GeometryOp.cpp b/util/geosop/GeometryOp.cpp
index f10e22e84..7ef4c4613 100644
--- a/util/geosop/GeometryOp.cpp
+++ b/util/geosop/GeometryOp.cpp
@@ -1009,7 +1009,7 @@ std::vector<GeometryOpCreator> opRegistry {
     [](const Geometry& geom, double d) {
         std::vector<const Geometry*> coverage = toList(geom);
         std::vector<std::unique_ptr<Geometry>> result
-            = geos::coverage::CoverageSimplifier::simplify(coverage, d);
+            = geos::coverage::CoverageSimplifier::simplify(coverage, d, nullptr);
         //-- convert list type (be nice to avoid this)
         std::vector<std::unique_ptr<const Geometry>> resultList;
         for (std::size_t i = 0; i < result.size(); i++) {

-----------------------------------------------------------------------

Summary of changes:
 capi/geos_c.cpp                                    |   1 -
 capi/geos_c.h.in                                   |  32 ++++++
 capi/geos_ts_c.cpp                                 | 128 ++++++++++++++++-----
 include/geos/coverage/CoverageSimplifier.h         |  24 ++--
 include/geos/coverage/TPVWSimplifier.h             |  14 ++-
 include/geos/geom/Geometry.h                       |   9 +-
 .../geos/operation/union/CascadedPolygonUnion.h    |  20 +++-
 include/geos/operation/union/UnaryUnionOp.h        |  13 ++-
 include/geos/util/Progress.h                       |  84 ++++++++++++++
 src/coverage/CoverageSimplifier.cpp                |  37 +++---
 src/coverage/TPVWSimplifier.cpp                    |  71 ++++++++++--
 src/geom/Geometry.cpp                              |   4 +-
 src/geom/util/GeometryFixer.cpp                    |   3 +-
 src/operation/overlayng/OverlayNGRobust.cpp        |   3 +-
 src/operation/overlayng/UnaryUnionNG.cpp           |   3 +-
 src/operation/polygonize/BuildArea.cpp             |   2 +-
 src/operation/union/CascadedPolygonUnion.cpp       |  36 ++++--
 src/operation/union/UnaryUnionOp.cpp               |   8 +-
 src/util/Progress.cpp                              |  43 +++++++
 tests/unit/capi/GEOSCoverageSimplifyTest.cpp       |  21 +++-
 tests/unit/capi/GEOSDifferenceTest.cpp             |  26 +++++
 tests/unit/capi/GEOSUnaryUnionTest.cpp             |  55 +++++++++
 tests/unit/coverage/CoverageSimplifierTest.cpp     |  18 ++-
 tests/unit/coverage/TPVWSimplifierTest.cpp         |   6 +-
 util/geosop/GeometryOp.cpp                         |   2 +-
 25 files changed, 554 insertions(+), 109 deletions(-)
 create mode 100644 include/geos/util/Progress.h
 create mode 100644 src/util/Progress.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list