[geos-commits] [SCM] GEOS branch main updated. 5ed4cd8dce266c07f962f42fa7b8fc57288067bb

git at osgeo.org git at osgeo.org
Fri May 29 09:27:30 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  5ed4cd8dce266c07f962f42fa7b8fc57288067bb (commit)
      from  d56ef91b6b0345e021325b5ca8c0f00ad659758f (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 5ed4cd8dce266c07f962f42fa7b8fc57288067bb
Author: Daniel Baston <dbaston at gmail.com>
Date:   Fri May 29 12:27:10 2026 -0400

    GeometrySplitter: Preserve CompoundCurves (#1440)

diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h
index 90fed94de..151bbb8ae 100644
--- a/include/geos/noding/GeometryNoder.h
+++ b/include/geos/noding/GeometryNoder.h
@@ -57,23 +57,28 @@ public:
 
     void setOnlyFirstGeomEdges(bool onlyFirstGeomEdges);
 
+    void setPreserveCompoundCurves(bool preserve);
+
     // Declare type as noncopyable
     GeometryNoder(GeometryNoder const&) = delete;
     GeometryNoder& operator=(GeometryNoder const&) = delete;
 
 private:
 
+    bool isInResult(const PathString& ps) const;
+
     const geom::Geometry* argGeom1;
     const geom::Geometry* argGeom2;
     const bool argGeomHasCurves;
+    bool argGeomHasCompoundCurves;
     bool onlyFirstGeomEdges;
+    bool preserveCompoundCurves;
 
     std::unique_ptr<Noder> noder;
     std::unique_ptr<algorithm::CircularArcIntersector> m_cai;
     std::unique_ptr<ArcIntersectionAdder> m_aia;
 
-    static void extractPathStrings(const geom::Geometry& g,
-                                   std::vector<std::unique_ptr<PathString>>& to);
+    void extractPathStrings(const geom::Geometry& g, std::vector<std::unique_ptr<PathString>>& to);
 
     Noder& getNoder();
 
@@ -83,4 +88,3 @@ private:
 
 } // namespace geos.noding
 } // namespace geos
-
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 83fea2fa7..5296e729c 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -17,11 +17,7 @@
  **********************************************************************/
 
 #include <geos/algorithm/CircularArcIntersector.h>
-#include <geos/noding/GeometryNoder.h>
-#include <geos/noding/SegmentString.h>
-#include <geos/noding/NodedSegmentString.h>
-#include <geos/noding/OrientedCoordinateArray.h>
-#include <geos/noding/Noder.h>
+
 #include <geos/geom/Geometry.h>
 #include <geos/geom/PrecisionModel.h>
 #include <geos/geom/CoordinateSequence.h>
@@ -32,18 +28,23 @@
 #include <geos/geom/LineString.h>
 
 #include <geos/noding/ArcIntersectionAdder.h>
+#include <geos/noding/GeometryNoder.h>
 #include <geos/noding/IteratedNoder.h>
+#include <geos/noding/MCIndexNoder.h>
 #include <geos/noding/NodableArcString.h>
+#include <geos/noding/NodedSegmentString.h>
+#include <geos/noding/Noder.h>
+#include <geos/noding/OrientedCoordinateArray.h>
+#include <geos/noding/SegmentString.h>
 #include <geos/noding/SimpleNoder.h>
 
-#include <geos/algorithm/LineIntersector.h>
-#include <geos/noding/IntersectionAdder.h>
-#include <geos/noding/MCIndexNoder.h>
-
-#include <geos/noding/snapround/MCIndexSnapRounder.h>
+#include <geos/util/Assert.h>
 
 #include <memory> // for unique_ptr
-#include <iostream>
+
+using geos::geom::CoordinateXY;
+using geos::geom::SimpleCurve;
+using geos::geom::Geometry;
 
 
 namespace geos {
@@ -58,16 +59,19 @@ class PathStringExtractor: public geom::GeometryComponentFilter {
 public:
     PathStringExtractor(std::vector<std::unique_ptr<PathString>> & to,
                            bool constructZ,
-                           bool constructM,
-                           const void* context)
+                           bool constructM)
         : _to(to)
         , _constructZ(constructZ)
         , _constructM(constructM)
-        , _context(context)
+        , _foundCompoundCurve(false)
     {}
 
+    bool foundCompoundCurve() const {
+        return _foundCompoundCurve;
+    }
+
     void
-    filter_ro(const geom::Geometry* g) override
+    filter_ro(const Geometry* g) override
     {
         if (g->isEmpty()) {
             return;
@@ -75,15 +79,16 @@ public:
 
         if(const auto* ls = dynamic_cast<const geom::LineString*>(g)) {
             auto coord = ls->getSharedCoordinates();
-            auto ss = std::make_unique<NodedSegmentString>(coord, _constructZ, _constructM, _context);
+            auto ss = std::make_unique<NodedSegmentString>(coord, _constructZ, _constructM, ls);
             _to.push_back(std::move(ss));
         } else if (const auto* cs = dynamic_cast<const geom::CircularString*>(g)) {
             const auto& coords = cs->getSharedCoordinates();
             auto arcs = cs->getArcs();
 
-            auto as = std::make_unique<NodableArcString>(std::move(arcs), coords, _constructZ, _constructM, _context);
+            auto as = std::make_unique<NodableArcString>(std::move(arcs), coords, _constructZ, _constructM, cs);
             _to.push_back(std::move(as));
         } else if (const auto* cc = dynamic_cast<const geom::CompoundCurve*>(g)) {
+            _foundCompoundCurve = true;
             for (std::size_t i = 0; i < cc->getNumCurves(); i++) {
                 filter_ro(cc->getCurveN(i));
             }
@@ -93,12 +98,171 @@ private:
     std::vector<std::unique_ptr<PathString>>& _to;
     bool _constructZ;
     bool _constructM;
-    const void* _context;
+    bool _foundCompoundCurve;
 
     PathStringExtractor(PathStringExtractor const&); /*= delete*/
     PathStringExtractor& operator=(PathStringExtractor const&); /*= delete*/
 };
 
+/** Class to construct a geometry from a set of PathStrings, given the
+ *  geometry from which they originated. This allows CompoundCurves to
+ *  be reconstructed. Unlike CurveBuilder, PathStrings will not be
+ *  joined into SimpleCurves.
+ */
+class CurveRebuilder {
+
+public:
+    explicit CurveRebuilder(const Geometry& g) : m_srcGeom(g) {}
+
+    // Register a PathString with the rebuilder
+    // The context of the PathString will be set to nullptr during computation.
+    void add(PathString* path)
+    {
+        const auto& coords = path->getCoordinates();
+
+        m_pathForStartPoint[coords->front<CoordinateXY>()].push_back(path);
+        m_pathForContext[path->getData()].push_back(path);
+    }
+
+    // Prevent reconstruction of CompoundCurve across the starting
+    // or ending nodes of a specified PathString.
+    void disableNodes(const PathString* path)
+    {
+        const CoordinateXY& startPoint = path->getCoordinates()->front<CoordinateXY>();
+        const CoordinateXY& endPoint = path->getCoordinates()->back<CoordinateXY>();
+
+        doNotContinueFromNode(startPoint);
+        doNotContinueFromNode(endPoint);
+    }
+
+    std::unique_ptr<Geometry> getGeometry()
+    {
+        collectGeoms(m_srcGeom);
+
+        return m_srcGeom.getFactory()->buildGeometry(std::move(m_resultGeoms));
+    }
+
+private:
+    void doNotContinueFromNode(const CoordinateXY& p) {
+        m_pathForStartPoint.erase(p);
+    }
+
+    void collectGeoms(const Geometry& g)
+    {
+        if (dynamic_cast<const geom::GeometryCollection*>(&g)) {
+            for (std::size_t i = 0; i < g.getNumGeometries(); i++) {
+                collectGeoms(*g.getGeometryN(i));
+            }
+            return;
+        }
+
+        const auto geomType = g.getGeometryTypeId();
+
+        if (geomType == geom::GEOS_LINESTRING) {
+            auto it = m_pathForContext.find(&g);
+            util::Assert::isTrue(it != m_pathForContext.end());
+
+            for (auto& path : it->second) {
+                m_resultGeoms.push_back(getFactory().createLineString(path->getCoordinates()));
+            }
+        } else if (geomType == geom::GEOS_CIRCULARSTRING) {
+            auto it = m_pathForContext.find(&g);
+            util::Assert::isTrue(it != m_pathForContext.end());
+
+            for (auto& path : it->second) {
+                m_resultGeoms.push_back(getFactory().createCircularString(path->getCoordinates()));
+            }
+        } else if (geomType == geom::GEOS_COMPOUNDCURVE) {
+            collectCompoundCurveGeoms(*detail::down_cast<const geom::CompoundCurve *>(&g));
+        } else {
+            throw util::UnsupportedOperationException("Unsupported geometry type: " + g.getGeometryType());
+        }
+    }
+
+    void collectCompoundCurveGeoms(const geom::CompoundCurve& cc)
+    {
+        for (std::size_t i = 0; i < cc.getNumCurves(); i++) {
+            std::vector<std::unique_ptr<SimpleCurve>> curves;
+
+            const SimpleCurve* sc = cc.getCurveN(i);
+            const CoordinateXY& originalEndPoint = sc->getCoordinatesRO()->back<CoordinateXY>();
+
+            auto it = m_pathForContext.find(sc);
+            util::Assert::isTrue(it != m_pathForContext.end());
+
+            const auto& pathsForCurve = it->second;
+
+            for (auto& path : pathsForCurve) {
+                if (path->getData() == nullptr) {
+                    continue;
+                }
+                path->setData(nullptr);
+
+                const CoordinateXY& endPoint = path->getCoordinates()->back<CoordinateXY>();
+                std::unique_ptr<SimpleCurve> geom = getPathGeometry(*path);
+
+                if (endPoint == originalEndPoint) {
+                    curves.push_back(std::move(geom));
+                } else {
+                    m_resultGeoms.push_back(std::move(geom));
+                }
+            }
+
+            if (!curves.empty()) {
+                auto it2 = m_pathForStartPoint.find(originalEndPoint);
+                while (it2 != m_pathForStartPoint.end()) {
+                    const auto& next = it2->second;
+                    it2 = m_pathForStartPoint.end();
+
+                    if (next.size() == 1) {
+                        PathString* path = next[0];
+
+                        auto nextIndex = i + curves.size();
+                        if (nextIndex < cc.getNumCurves() && path->getData() == cc.getCurveN(nextIndex)) {
+                            curves.push_back(getPathGeometry(*path));
+                            path->setData(nullptr);
+
+                            const CoordinateXY& endPoint = path->getCoordinates()->back<CoordinateXY>();
+                            it2 = m_pathForStartPoint.find(endPoint);
+                        }
+                    }
+                }
+
+                if (curves.size() == 1) {
+                    m_resultGeoms.push_back(std::move(curves[0]));
+                } else {
+                    m_resultGeoms.push_back(getFactory().createCompoundCurve(std::move(curves)));
+                }
+            }
+        }
+    }
+
+    std::unique_ptr<SimpleCurve>
+    getPathGeometry(const PathString& path) const
+    {
+        const bool isLinear = dynamic_cast<const SegmentString*>(&path);
+
+        if (isLinear) {
+            return getFactory().createLineString(path.getCoordinates());
+        }
+
+        return getFactory().createCircularString(path.getCoordinates());
+    }
+
+    const geom::GeometryFactory&
+    getFactory() const
+    {
+        return *m_srcGeom.getFactory();
+    }
+
+    const Geometry& m_srcGeom;
+
+    std::map<CoordinateXY, std::vector<PathString*>> m_pathForStartPoint;
+    std::map<const void*, std::vector<PathString*>> m_pathForContext;
+
+    std::vector<std::unique_ptr<Geometry>> m_resultGeoms;
+};
+
 }
 
 
@@ -123,7 +287,9 @@ GeometryNoder::GeometryNoder(const geom::Geometry& g)
     argGeom1(&g),
     argGeom2(nullptr),
     argGeomHasCurves(g.hasCurvedComponents()),
-    onlyFirstGeomEdges(false)
+    argGeomHasCompoundCurves(false),
+    onlyFirstGeomEdges(false),
+    preserveCompoundCurves(false)
 {}
 
 GeometryNoder::GeometryNoder(const geom::Geometry& g1, const geom::Geometry& g2)
@@ -131,11 +297,19 @@ GeometryNoder::GeometryNoder(const geom::Geometry& g1, const geom::Geometry& g2)
     argGeom1(&g1),
     argGeom2(&g2),
     argGeomHasCurves(g1.hasCurvedComponents() || g2.hasCurvedComponents()),
-    onlyFirstGeomEdges(false)
+    argGeomHasCompoundCurves(false),
+    onlyFirstGeomEdges(false),
+    preserveCompoundCurves(false)
 {}
 
 GeometryNoder::~GeometryNoder() = default;
 
+bool
+GeometryNoder::isInResult(const PathString& ps) const
+{
+    return !onlyFirstGeomEdges || ps.getData() != nullptr;
+}
+
 /* private */
 std::unique_ptr<geom::Geometry>
 GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges) const
@@ -144,32 +318,60 @@ GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges)
 
     std::set< OrientedCoordinateArray > ocas;
 
-    // Create a geometry out of the noded substrings.
-    std::vector<std::unique_ptr<geom::Geometry>> lines;
-    lines.reserve(nodedEdges.size());
+    std::vector<PathString*> pathsToKeep;
 
-    bool resultArcs = false;
-    for(auto& path :  nodedEdges) {
-        if (onlyFirstGeomEdges && path->getData() != argGeom1) {
+
+
+    for(auto& path : nodedEdges) {
+        if (!isInResult(*path)) {
             continue;
         }
 
         const auto& coords = path->getCoordinates();
-
-        bool isLinear = dynamic_cast<SegmentString*>(path.get());
-
-        // TODO: Make OrientedCoordinateArray not require strict equality of arc control points
-
         OrientedCoordinateArray oca1(*coords);
+
         // Check if an equivalent edge is known
         if(ocas.insert(oca1).second) {
-            if (isLinear) {
-                lines.push_back(geomFact->createLineString(coords));
-            } else {
-                resultArcs = true;
-                lines.push_back(geomFact->createCircularString(coords));
+            pathsToKeep.push_back(path.get());
+        }
+    }
+
+    if (preserveCompoundCurves && argGeomHasCompoundCurves) {
+        util::Assert::isTrue(onlyFirstGeomEdges);
+
+        CurveRebuilder rebuilder(*argGeom1);
+
+        for (auto& path : pathsToKeep) {
+            rebuilder.add(path);
+        }
+
+        // Any edge that begins at a point where a geomB edge starts/ends is not
+        // eligible for merging.
+        for (auto& path : nodedEdges) {
+            if (!isInResult(*path)) {
+                rebuilder.disableNodes(path.get());
             }
         }
+
+        return rebuilder.getGeometry();
+    }
+
+    // Create a geometry out of the noded substrings.
+    std::vector<std::unique_ptr<geom::Geometry>> lines;
+    lines.reserve(nodedEdges.size());
+    bool resultArcs = false;
+
+    for (const auto& path : pathsToKeep) {
+        const auto& coords = path->getCoordinates();
+
+        const bool isLinear = dynamic_cast<const SegmentString*>(path);
+
+        if (isLinear) {
+            lines.push_back(geomFact->createLineString(coords));
+        } else {
+            resultArcs = true;
+            lines.push_back(geomFact->createCircularString(coords));
+        }
     }
 
     if (resultArcs) {
@@ -190,7 +392,12 @@ GeometryNoder::getNoded()
 
     extractPathStrings(*argGeom1, lineList);
     if (argGeom2 != nullptr) {
-        extractPathStrings(*argGeom2, lineList);
+        std::vector<std::unique_ptr<PathString>> lineList2;
+        extractPathStrings(*argGeom2, lineList2);
+        for (auto& path : lineList2) {
+            path->setData(nullptr); // prevent from appearing in result
+            lineList.push_back(std::move(path));
+        }
     }
 
     Noder& p_noder = getNoder();
@@ -207,8 +414,9 @@ void
 GeometryNoder::extractPathStrings(const geom::Geometry& g,
                                   std::vector<std::unique_ptr<PathString>>& to)
 {
-    PathStringExtractor ex(to, g.hasZ(), g.hasM(), &g);
+    PathStringExtractor ex(to, g.hasZ(), g.hasM());
     g.apply_ro(&ex);
+    argGeomHasCompoundCurves |= ex.foundCompoundCurve();
 }
 
 /* private */
@@ -236,5 +444,11 @@ GeometryNoder::setOnlyFirstGeomEdges(bool p_onlyFirstGeomEdges)
     onlyFirstGeomEdges = p_onlyFirstGeomEdges;
 }
 
+void
+GeometryNoder::setPreserveCompoundCurves(bool preserve)
+{
+    preserveCompoundCurves = preserve;
+}
+
 } // namespace geos.noding
 } // namespace geos
diff --git a/src/operation/split/GeometrySplitter.cpp b/src/operation/split/GeometrySplitter.cpp
index a795d9f4f..e78fffef9 100644
--- a/src/operation/split/GeometrySplitter.cpp
+++ b/src/operation/split/GeometrySplitter.cpp
@@ -287,6 +287,7 @@ GeometrySplitter::splitLinealWithEdge(const Geometry &geom, const Geometry &edge
 
     GeometryNoder noder(geom, edge);
     noder.setOnlyFirstGeomEdges(true);
+    noder.setPreserveCompoundCurves(true);
 
     auto nodedMLS = noder.getNoded();
 
diff --git a/tests/unit/operation/split/GeometrySplitterTest.cpp b/tests/unit/operation/split/GeometrySplitterTest.cpp
index 988644c10..526144e03 100644
--- a/tests/unit/operation/split/GeometrySplitterTest.cpp
+++ b/tests/unit/operation/split/GeometrySplitterTest.cpp
@@ -811,4 +811,41 @@ void object::test<66>()
               "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))");
 }
 
+template<>
+template<>
+void object::test<67>()
+{
+    set_test_name("split CompoundCurve with LineString at multiple points");
+    // https://github.com/libgeos/geos/issues/1438
+
+    testSplit(
+        "COMPOUNDCURVE ((0.36603773584905674 -0.84150943396226396, 1.04276729559748427 -0.84905660377358472),CIRCULARSTRING (1.04276729559748427 -0.84905660377358472, 1.12075471698113205 -0.62515723270440238, 0.94465408805031448 -0.38113207547169803),(0.94465408805031448 -0.38113207547169803, 0.39371069182389951 -0.71320754716981116))",
+        "LINESTRING (0.61079395915591483 0.40485712994934753, 0.69909271296411513 -1.02999761943391199)",
+     "GEOMETRYCOLLECTION (LINESTRING (0.36603773584905674 -0.841509433962264, 0.6877142073931135 -0.8450969039051345), COMPOUNDCURVE ((0.6877142073931135 -0.8450969039051345, 1.0427672955974843 -0.8490566037735847), CIRCULARSTRING (1.0427672955974843 -0.8490566037735847, 1.120754716981132 -0.6251572327044024, 0.9446540880503145 -0.38113207547169803), (0.9446540880503145 -0.38113207547169803, 0.6693731566093508 -0.5470548286689911)), LINESTRING (0.6693731566093508 -0.5470548286689911, 0.3937106918238995 -0.7132075471698112))");
+}
+
+template<>
+template<>
+void object::test<68>()
+{
+    set_test_name("split CompoundCurve with LineString at existing vertices");
+
+    testSplit(
+        "COMPOUNDCURVE ((-10 0, -5 0), CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, 10 0))",
+        "LINESTRING (-5 0, 0 -5, 5 0)",
+        "GEOMETRYCOLLECTION (LINESTRING (-10 0, -5 0), CIRCULARSTRING (-5 0, 0 5, 5 0), LINESTRING (5 0, 10 0))");
+}
+
+template<>
+template<>
+void object::test<69>()
+{
+    set_test_name("split MultiCurve containing CompoundCurve");
+
+    testSplit(
+        "MULTICURVE ((-10 0, -5 0), COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, 10 0)), CIRCULARSTRING (10 0, 15 -5, 20 0))",
+        "MULTILINESTRING ((-8 -10, -8 10), (8 -10, 8 10))",
+        "GEOMETRYCOLLECTION (LINESTRING (-10 0, -8 0), LINESTRING (-8 0, -5 0), COMPOUNDCURVE (CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, 8 0)), LINESTRING (8 0, 10 0), CIRCULARSTRING (10 0, 15 -5, 20 0))");
+}
+
 }

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

Summary of changes:
 include/geos/noding/GeometryNoder.h                |  10 +-
 src/noding/GeometryNoder.cpp                       | 290 ++++++++++++++++++---
 src/operation/split/GeometrySplitter.cpp           |   1 +
 .../unit/operation/split/GeometrySplitterTest.cpp  |  37 +++
 4 files changed, 297 insertions(+), 41 deletions(-)


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list