[geos-commits] [SCM] GEOS branch main updated. 14fd2d70311aae9646e5d2e5364921822074de2b
git at osgeo.org
git at osgeo.org
Sun Aug 9 05:33:51 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 14fd2d70311aae9646e5d2e5364921822074de2b (commit)
via 59a36b35cfe4c51019ba0f673473e1c1155b5d8e (commit)
from 5b62cd06ceeee053f2c7a4bf96138fc1c98675bc (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 14fd2d70311aae9646e5d2e5364921822074de2b
Author: Daniel Baston <dbaston at gmail.com>
Date: Wed Aug 5 14:17:14 2026 -0400
GeometryNoder: Replace OrientedCoordinateArray with arc-specific logic
Resolves https://github.com/libgeos/geos/issues/1497
diff --git a/include/geos/noding/DirectionIndependentArcString.h b/include/geos/noding/DirectionIndependentArcString.h
new file mode 100644
index 000000000..b7b0d8f10
--- /dev/null
+++ b/include/geos/noding/DirectionIndependentArcString.h
@@ -0,0 +1,52 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 ISciences, LLC
+ *
+ * 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 <memory>
+#include <geos/export.h>
+
+
+namespace geos::noding {
+class ArcString;
+}
+
+namespace geos::noding {
+
+/// The DirectionIndependentArcString is a thin wrapper over an ArcString that provides specialized
+/// equality-testing and hashing functions. DirectionIndependentArcStrings are considered equal
+/// if they cover the same path, regardless of direction. The control points of arcs are not required
+/// to be identical, but must describe the same arc.
+class GEOS_DLL DirectionIndependentArcString {
+
+public:
+
+ explicit DirectionIndependentArcString(const ArcString& as) : m_as(as) {}
+
+ const ArcString& getArcString() const
+ {
+ return m_as;
+ }
+
+ bool operator==(const DirectionIndependentArcString& other) const;
+
+private:
+ const ArcString& m_as;
+};
+}
+
+template<>
+struct GEOS_DLL std::hash<geos::noding::DirectionIndependentArcString> {
+ std::size_t operator()(const geos::noding::DirectionIndependentArcString& as) const noexcept;
+};
diff --git a/src/noding/DirectionIndependentArcString.cpp b/src/noding/DirectionIndependentArcString.cpp
new file mode 100644
index 000000000..0628384b2
--- /dev/null
+++ b/src/noding/DirectionIndependentArcString.cpp
@@ -0,0 +1,93 @@
+/**********************************************************************
+*
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2026 ISciences, LLC
+ *
+ * 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/noding/DirectionIndependentArcString.h>
+#include <geos/noding/ArcString.h>
+
+#include <cstddef>
+
+namespace geos::noding {
+static bool
+arcStringsSame(const ArcString& as1, const ArcString& as2, bool reverse)
+{
+ using geom::CircularArc;
+
+ const auto nArcs = as1.getSize();
+
+ if (as2.getSize() != nArcs) {
+ return false;
+ }
+
+ for (std::size_t i = 0; i < nArcs; i++) {
+ const CircularArc& arc1 = as1.getArc(i);
+ const CircularArc& arc2 = as2.getArc(reverse ? nArcs - i - 1: i);
+
+ if (reverse) {
+ if (arc1.p0() != arc2.p2() || arc1.p2() != arc2.p0()) {
+ return false;
+ }
+
+ const bool arc1Linear = arc1.getOrientation() == algorithm::Orientation::COLLINEAR;
+ const bool arc2Linear = arc2.getOrientation() == algorithm::Orientation::COLLINEAR;
+
+ if (arc1Linear != arc2Linear) {
+ return false;
+ }
+
+ if (!arc1Linear && arc1.getOrientation() == arc2.getOrientation()) {
+ return false;
+ }
+ }
+ else {
+ if (arc1.p0() != arc2.p0() || arc1.p2() != arc2.p2()) {
+ return false;
+ }
+ if (arc1.getOrientation() != arc2.getOrientation()) {
+ return false;
+ }
+ }
+
+ if (!arc1.isLinear() && arc1.getCenter() != arc2.getCenter()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool
+DirectionIndependentArcString::operator==(const DirectionIndependentArcString& other) const
+{
+ return arcStringsSame(m_as, other.m_as, false) || arcStringsSame(m_as, other.m_as, true);
+}
+}
+
+std::size_t
+std::hash<geos::noding::DirectionIndependentArcString>::operator()(const geos::noding::DirectionIndependentArcString&
+ as) const noexcept
+{
+ const auto* c1 = &as.getArcString().getArc(0).p0();
+ const auto* c2 = &as.getArcString().getArc(as.getArcString().getSize() - 1).p2();
+
+ if (c2->compareTo(*c1) < 0) {
+ std::swap(c1, c2);
+ }
+
+ size_t h = std::hash<double> {}(c1->x);
+ h ^= std::hash<double> {}(c1->y) << 1;
+ h ^= std::hash<double> {}(c2->x) << 1;
+ h ^= std::hash<double> {}(c2->y) << 1;
+
+ return h;
+}
diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp
index 5296e729c..5de3b56b8 100644
--- a/src/noding/GeometryNoder.cpp
+++ b/src/noding/GeometryNoder.cpp
@@ -29,6 +29,7 @@
#include <geos/noding/ArcIntersectionAdder.h>
#include <geos/noding/GeometryNoder.h>
+#include <geos/noding/DirectionIndependentArcString.h>
#include <geos/noding/IteratedNoder.h>
#include <geos/noding/MCIndexNoder.h>
#include <geos/noding/NodableArcString.h>
@@ -41,6 +42,8 @@
#include <geos/util/Assert.h>
#include <memory> // for unique_ptr
+#include <set>
+#include <unordered_set>
using geos::geom::CoordinateXY;
using geos::geom::SimpleCurve;
@@ -317,22 +320,28 @@ GeometryNoder::toGeometry(std::vector<std::unique_ptr<PathString>>& nodedEdges)
const geom::GeometryFactory* geomFact = argGeom1->getFactory();
std::set< OrientedCoordinateArray > ocas;
+ std::unordered_set<DirectionIndependentArcString> arcStrings;
std::vector<PathString*> pathsToKeep;
-
-
for(auto& path : nodedEdges) {
if (!isInResult(*path)) {
continue;
}
- const auto& coords = path->getCoordinates();
- OrientedCoordinateArray oca1(*coords);
+ if (const auto* as = dynamic_cast<const ArcString*>(path.get())) {
+ // Check if an equivalent arc is known
+ if (arcStrings.emplace(*as).second) {
+ pathsToKeep.push_back(path.get());
+ }
+ } else {
+ const auto& coords = path->getCoordinates();
+ OrientedCoordinateArray oca1(*coords);
- // Check if an equivalent edge is known
- if(ocas.insert(oca1).second) {
- pathsToKeep.push_back(path.get());
+ // Check if an equivalent edge is known
+ if(ocas.insert(oca1).second) {
+ pathsToKeep.push_back(path.get());
+ }
}
}
diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp
index 63d893a55..10166adbc 100644
--- a/tests/unit/capi/GEOSNodeTest.cpp
+++ b/tests/unit/capi/GEOSNodeTest.cpp
@@ -284,8 +284,6 @@ void object::test<12>()
expected_ = fromWKT("MULTICURVE ("
"CIRCULARSTRING (-5 0, -4.7434164902525691 1.5811388300841900, -4 3),"
"CIRCULARSTRING (-4 3, 0 5, 4 3),"
- "CIRCULARSTRING (-4 3, 0 5, 4 3)," // FIXME this component is not removed by OrientedCoordinateArray because
- // its control point differs slightly from the previous one
"CIRCULARSTRING (4 3, 4.7434164902525691 1.5811388300841898, 5 0))");
ensure_equals_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
@@ -398,5 +396,23 @@ void object::test<18>()
}
+template<>
+template<>
+void object::test<19>()
+{
+ set_test_name("two collinear CircularStrings");
+
+ input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 2), CIRCULARSTRING (2 2, 1 1, 0 0))");
+
+ result_ = GEOSNode(input_);
+ ensure(result_);
+
+ ensure_equals(GEOSGetNumGeometries(result_), 1);
+ expected_ = fromWKT("MULTICURVE(CIRCULARSTRING (0 0, 1 1, 2 2))");
+
+ ensure_equals_exact_geometry_xyzm(reinterpret_cast<Geometry*>(result_),
+ reinterpret_cast<Geometry*>(expected_), 1e-4);
+}
+
} // namespace tut
diff --git a/tests/unit/noding/DirectionIndependentArcStringTest.cpp b/tests/unit/noding/DirectionIndependentArcStringTest.cpp
new file mode 100644
index 000000000..55c71053f
--- /dev/null
+++ b/tests/unit/noding/DirectionIndependentArcStringTest.cpp
@@ -0,0 +1,108 @@
+#include <tut/tut.hpp>
+#include <unordered_set>
+
+#include <geos/noding/ArcString.h>
+#include <geos/noding/DirectionIndependentArcString.h>
+#include <geos/geom/CircularString.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/io/WKTReader.h>
+
+using namespace geos::geom;
+using namespace geos::noding;
+
+namespace tut {
+
+struct test_directionindependentarcstring_data {
+ geos::io::WKTReader reader_;
+
+ static bool isSame(const ArcString& as0, const ArcString& as1) {
+ std::unordered_set<DirectionIndependentArcString> map;
+ map.emplace(as0);
+
+ return !map.emplace(as1).second;
+ }
+
+ static bool isSame(const CircularString& cs0, const CircularString& cs1) {
+ ArcString as0(cs0.getArcs(), cs0.getSharedCoordinates(), nullptr);
+ ArcString as1(cs1.getArcs(), cs1.getSharedCoordinates(), nullptr);
+
+ return isSame(as0, as1);
+ }
+
+ bool isSame(const std::string& wkt0, const std::string& wkt1) {
+ auto cs0 = reader_.read<CircularString>(wkt0);
+ auto cs1 = reader_.read<CircularString>(wkt1);
+
+ return isSame(*cs0, *cs1);
+ }
+};
+
+using group = test_group<test_directionindependentarcstring_data>;
+using object = group::object;
+
+group test_directionindependentarcstring_data("geos::noding::DirectionIndependentArcString");
+
+template<>
+template<>
+void object::test<1>()
+{
+ set_test_name("equivalent: single-section arcs with different control points and opposite directions");
+
+ ensure(isSame("CIRCULARSTRING (-5 0, -4 3, 5 0)", "CIRCULARSTRING (5 0, 4 3, -5 0)"));
+}
+
+template<>
+template<>
+void object::test<2>()
+{
+ set_test_name("equivalent: multi-section arcs with different control points and opposite directions");
+
+ ensure(isSame("CIRCULARSTRING (-5 0, -4 3, 5 0, 6 1, 7 0)", "CIRCULARSTRING (7 0, 6 1, 5 0, 4 3, -5 0)"));
+}
+
+template<>
+template<>
+void object::test<3>()
+{
+ set_test_name("different: single-section arcs with same endpoints, covering different halves of circle");
+
+ ensure(!isSame("CIRCULARSTRING (-5 0, -4 3, 5 0)", "CIRCULARSTRING (-5 0, -4 -3, 5 0)"));
+}
+
+template<>
+template<>
+void object::test<4>()
+{
+ set_test_name("equivalent: two degenerate (linear) arcs having different control points and opposite directions");
+
+ ensure(isSame("CIRCULARSTRING (0 0, 4 4, 10 10)", "CIRCULARSTRING (10 10, 8 8, 0 0)"));
+}
+
+template<>
+template<>
+void object::test<5>()
+{
+ set_test_name("different: arc and degenerate arc having same endpoints");
+
+ ensure(!isSame("CIRCULARSTRING (-1 0, 0 1, 1 0)", "CIRCULARSTRING (-1 0, 0 0, 1 0)"));
+}
+
+template<>
+template<>
+void object::test<6>()
+{
+ set_test_name("different: two arcs having same endpoints and different control points");
+
+ ensure(!isSame("CIRCULARSTRING (-1 0, 0 1, 1 0)", "CIRCULARSTRING (-1 0, 0 0.9, 1 0)"));
+}
+
+template<>
+template<>
+void object::test<7>()
+{
+ set_test_name("different: two arcs sharing same path that later diverge");
+
+ ensure(!isSame("CIRCULARSTRING (-1 0, 0 1, 1 0, 2 -1, 3 0)", "CIRCULARSTRING (-1 0, 0 1, 1 0, 2 -1, 3 0.1)"));
+}
+
+}
diff --git a/tests/unit/operation/split/GeometrySplitterTest.cpp b/tests/unit/operation/split/GeometrySplitterTest.cpp
index 95c730693..d4700ec47 100644
--- a/tests/unit/operation/split/GeometrySplitterTest.cpp
+++ b/tests/unit/operation/split/GeometrySplitterTest.cpp
@@ -906,11 +906,10 @@ void object::test<75>()
{
set_test_name("GH-1497");
- auto geom = reader_.read("CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 0 5, 5 5), CIRCULARSTRING(5 5, 7 1, 5 0)))");
- auto edge = reader_.read("CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 5 5), CIRCULARSTRING(5 5, 7 4, 5 0)))");
-
- // This case is not currently handled correctly by the GeometrySplitter. Throw an error instead of returning an incorrect result.
- ensure_THROW(GeometrySplitter::split(*geom, *edge), geos::util::GEOSException);
+ testSplit(
+ "CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 0 5, 5 5), CIRCULARSTRING(5 5, 7 1, 5 0)))",
+ "CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 5 5), CIRCULARSTRING(5 5, 7 4, 5 0)))",
+ "GEOMETRYCOLLECTION (CURVEPOLYGON (COMPOUNDCURVE ((5 0, 0 0, 5 5), CIRCULARSTRING (5 5, 7 1, 5 0))), POLYGON ((0 0, 0 5, 5 5, 0 0)))");
}
-}
\ No newline at end of file
+}
commit 59a36b35cfe4c51019ba0f673473e1c1155b5d8e
Author: Daniel Baston <dbaston at gmail.com>
Date: Wed Aug 5 13:32:31 2026 -0400
GeometrySplitter: add check for result area
References https://github.com/libgeos/geos/issues/1497
diff --git a/src/operation/split/GeometrySplitter.cpp b/src/operation/split/GeometrySplitter.cpp
index 10e766ec5..b669b14ea 100644
--- a/src/operation/split/GeometrySplitter.cpp
+++ b/src/operation/split/GeometrySplitter.cpp
@@ -373,7 +373,16 @@ GeometrySplitter::splitPolygonalWithEdge(const Geometry &geom, const Geometry &e
return geom.getFactory()->createEmptyGeometry(geom::GEOS_GEOMETRYCOLLECTION, geom.hasZ(), geom.hasM());
}
- return geom.getFactory()->createGeometryCollection(std::move(keep));
+ auto result = geom.getFactory()->createGeometryCollection(std::move(keep));
+
+ const double inputArea = geom.getArea();
+ const double resultArea = result->getArea();
+
+ if (inputArea > 0 && std::abs(inputArea - resultArea) / inputArea > 1e-6) {
+ throw util::GEOSException("Splitting polygonal geometry failed to preserve area");
+ }
+
+ return result;
}
}
diff --git a/tests/unit/operation/split/GeometrySplitterTest.cpp b/tests/unit/operation/split/GeometrySplitterTest.cpp
index 7019ea3fe..95c730693 100644
--- a/tests/unit/operation/split/GeometrySplitterTest.cpp
+++ b/tests/unit/operation/split/GeometrySplitterTest.cpp
@@ -900,5 +900,17 @@ void object::test<74>()
);
}
+template<>
+template<>
+void object::test<75>()
+{
+ set_test_name("GH-1497");
+ auto geom = reader_.read("CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 0 5, 5 5), CIRCULARSTRING(5 5, 7 1, 5 0)))");
+ auto edge = reader_.read("CURVEPOLYGON (COMPOUNDCURVE((5 0, 0 0, 5 5), CIRCULARSTRING(5 5, 7 4, 5 0)))");
+
+ // This case is not currently handled correctly by the GeometrySplitter. Throw an error instead of returning an incorrect result.
+ ensure_THROW(GeometrySplitter::split(*geom, *edge), geos::util::GEOSException);
}
+
+}
\ No newline at end of file
-----------------------------------------------------------------------
Summary of changes:
.../geos/noding/DirectionIndependentArcString.h | 52 ++++++++++
src/noding/DirectionIndependentArcString.cpp | 93 ++++++++++++++++++
src/noding/GeometryNoder.cpp | 23 +++--
src/operation/split/GeometrySplitter.cpp | 11 ++-
tests/unit/capi/GEOSNodeTest.cpp | 20 +++-
.../noding/DirectionIndependentArcStringTest.cpp | 108 +++++++++++++++++++++
.../unit/operation/split/GeometrySplitterTest.cpp | 11 +++
7 files changed, 308 insertions(+), 10 deletions(-)
create mode 100644 include/geos/noding/DirectionIndependentArcString.h
create mode 100644 src/noding/DirectionIndependentArcString.cpp
create mode 100644 tests/unit/noding/DirectionIndependentArcStringTest.cpp
hooks/post-receive
--
GEOS
More information about the geos-commits
mailing list