[geos-commits] [SCM] GEOS branch 3.15 updated. 5240ae533bd5c64206557e461add548d14add7f0
git at osgeo.org
git at osgeo.org
Sat Oct 17 08:15:16 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, 3.15 has been updated
via 5240ae533bd5c64206557e461add548d14add7f0 (commit)
via 98941d5f894b057e0d74f70ac4025ca4d2f7fd4f (commit)
via 45965bad7f22b2bcc8970a769546847c45875c53 (commit)
via 7b582b60ec20440f463de2296fb97aefb19b2daf (commit)
via aee7f91d6528689fd99577ba790e52d54fc007db (commit)
from e2a32cc0c3ad9c1015279a18b28a730d39414b38 (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 5240ae533bd5c64206557e461add548d14add7f0
Author: Daniel Baston <dbaston at gmail.com>
Date: Sat Oct 17 11:14:32 2026 -0400
Update NEWS
diff --git a/NEWS.md b/NEWS.md
index d80676c80..d049da6fb 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -7,6 +7,9 @@
- GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston)
- Avoid raising `FE_INVALID` from Envelope (GH-1555, Mike Taves)
- Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston)
+ - CircularArcIntersector: Use Orient2D to check for arc control points on line (GH-1546, Dan Baston)
+ - CircularArcIntersector: Avoid incorrectly classifying intersections as cocircular (GH-1549, Dan Baston)
+ - GEOSProject: Throw IllegalArgumentException on invalid input (GH-1547, Mohd Quamar Tyagi)
## Changes in 3.15.0
2026-09-01
commit 98941d5f894b057e0d74f70ac4025ca4d2f7fd4f
Author: Daniel Baston <dbaston at gmail.com>
Date: Tue Oct 13 15:07:01 2026 -0400
GEOSProject_r: throw IllegalArgumentException and handle empty points (#1547)
Fixes #1534
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index eaccf1739..57a46ed93 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -4766,8 +4766,8 @@ extern "C" {
{
return execute(extHandle, -1.0, [&]() {
const Point* point = dynamic_cast<const Point*>(p);
- if(!point) {
- throw std::runtime_error("third argument of GEOSProject_r must be Point");
+ if(!point || point->isEmpty()) {
+ throw IllegalArgumentException("third argument of GEOSProject_r must be non-empty Point");
}
const geos::geom::Coordinate inputPt(*p->getCoordinate());
const auto inputLine = convertToLineIfNeeded(extHandle, g);
diff --git a/tests/unit/capi/GEOSProjectTest.cpp b/tests/unit/capi/GEOSProjectTest.cpp
index a8f550809..6c3ac7d4c 100644
--- a/tests/unit/capi/GEOSProjectTest.cpp
+++ b/tests/unit/capi/GEOSProjectTest.cpp
@@ -143,4 +143,40 @@ void object::test<6>
ensure_equals("GEOSProjectNormalized result does not match", dist_norm, 0.5, 1e-3);
}
+// Empty point as second argument returns -1.0 instead of crashing
+// https://github.com/libgeos/geos/issues/1534
+template<>
+template<>
+void object::test<7>
+()
+{
+ geom1_ = GEOSGeomFromWKT("LINESTRING (0 0, 0 2)");
+ geom2_ = GEOSGeomFromWKT("POINT EMPTY");
+
+ ensure_equals(GEOSProject(geom1_, geom2_), -1.0);
+ ensure_equals(GEOSProjectNormalized(geom1_, geom2_), -1.0);
+}
+
+// Invalid second argument reports IllegalArgumentException
+// https://github.com/libgeos/geos/issues/1534
+template<>
+template<>
+void object::test<8>
+()
+{
+ useContext();
+
+ geom1_ = fromWKT("LINESTRING (0 0, 0 2)");
+ geom2_ = fromWKT("LINESTRING (0 0, 0 2)");
+
+ std::string errorMsg;
+ GEOSContext_setErrorMessageHandler_r(ctxt_, [](const char* message, void* userdata) {
+ static_cast<std::string*>(userdata)->append(message);
+ }, &errorMsg);
+
+ ensure_equals(GEOSProject_r(ctxt_, geom1_, geom2_), -1.0);
+ ensure("error message contains IllegalArgumentException",
+ errorMsg.find("IllegalArgumentException") != std::string::npos);
+}
+
} // namespace tut
commit 45965bad7f22b2bcc8970a769546847c45875c53
Author: Daniel Baston <dbaston at gmail.com>
Date: Tue Oct 13 15:05:15 2026 -0400
CircularArcIntersector: Avoid incorrectly classifying intersection as cocircular
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index f08bafa54..d503fb384 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -229,16 +229,16 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a
return;
}
- // a: the distance from c1 to the "radical line", which connects the two intersection points
- // Expression rewritten by Herbie, https://herbie.uwplse.org/demo/
- // const double a = (d*d + r1*r1 - r2*r2) / (2*d);
- const double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5);
-
// TODO because the circle center calculation is inexact we need some kind of tolerance here.
// Take a PrecisionModel like LineIntersector?
- if (a == 0 || (d == 0 && r1 == r2)) {
+ if (d == 0 && r1 == r2) {
computeCocircularIntersection(arc1, arc2);
} else {
+ // a: the distance from c1 to the "radical line", which connects the two intersection points
+ // Expression rewritten by Herbie, https://herbie.uwplse.org/demo/
+ // const double a = (d*d + r1*r1 - r2*r2) / (2*d);
+ const double a = std::fma(r1-r2, (r1 + r2) / (d+d), d*0.5);
+
// Compute interior intersection points.
const double dx = c2.x-c1.x;
const double dy = c2.y-c1.y;
diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
index 6e5805b08..63695fb9b 100644
--- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp
+++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
@@ -1811,6 +1811,18 @@ void object::test<90>()
XY{208, 22});
}
+template<>
+template<>
+void object::test<91>()
+{
+ set_test_name("two arcs with interior intersection at control point");
+
+ checkIntersection(XY{-5, 0}, XY{0, 5}, XY{5, 0},
+ XY{-7, 0}, XY{-3, 4}, XY{1, 0},
+ CircularArcIntersector::ONE_POINT_INTERSECTION,
+ XY{-3, 4});
+}
+
// TODO: check Z values of arc result centerpoints
// TODO: add tests for seg/seg
commit 7b582b60ec20440f463de2296fb97aefb19b2daf
Author: Daniel Baston <dbaston at gmail.com>
Date: Tue Oct 13 15:03:57 2026 -0400
CircularArcIntersector: use Orient2D to check for arc control points on line
Resolves https://github.com/libgeos/geos/issues/1539
diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp
index 79ab344b9..f08bafa54 100644
--- a/src/algorithm/CircularArcIntersector.cpp
+++ b/src/algorithm/CircularArcIntersector.cpp
@@ -142,21 +142,21 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSeque
return;
}
- // Check for exact endpoint-endpoint or endpoint-control point intersections
- // If found, replace the computed intersection points with an exact endpoint
+ // Check if an endpoint or control point of the arc is exactly on the line.
+ // If found, replace the computed intersection point with the exact control point.
const CoordinateXY& ap0 = arc.p0<CoordinateXY>();
const CoordinateXY& ap1 = arc.p1<CoordinateXY>();
const CoordinateXY& ap2 = arc.p2<CoordinateXY>();
const CoordinateXY& bp0 = seq.getAt<CoordinateXY>(segPos0);
const CoordinateXY& bp1 = seq.getAt<CoordinateXY>(segPos1);
- if (ap0 == bp0 || ap0 == bp1) {
+ if (Orientation::index(bp0, bp1, ap0) == Orientation::COLLINEAR) {
closestPoint(isect0, isect1, nPointsIntersectingLine, ap0) = ap0;
}
- if (ap1 == bp0 || ap1 == bp1) {
+ if (Orientation::index(bp0, bp1, ap1) == Orientation::COLLINEAR) {
closestPoint(isect0, isect1, nPointsIntersectingLine, ap1) = ap1;
}
- if (ap2 == bp0 || ap2 == bp1) {
+ if (Orientation::index(bp0, bp1, ap2) == Orientation::COLLINEAR) {
closestPoint(isect0, isect1, nPointsIntersectingLine, ap2) = ap2;
}
diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
index 5d0123c17..6e5805b08 100644
--- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp
+++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp
@@ -29,9 +29,19 @@ struct test_circulararcintersector_data {
using XYZM = CoordinateXYZM;
static constexpr double NaN = geos::DoubleNotANumber;
+ double eps = 1e-8;
+ bool toleranceIsDistance = true;
using ArcOrPoint = std::variant<XY, XYZ, XYM, XYZM, CircularArc>;
+ void setTolerance(double value) {
+ eps = value;
+ }
+
+ void setToleranceIsDistance(bool value) {
+ toleranceIsDistance = value;
+ }
+
static std::string to_string(CircularArcIntersector::intersection_type t)
{
switch (t) {
@@ -96,11 +106,11 @@ struct test_circulararcintersector_data {
}
template<typename C1, typename C2>
- static void checkIntersection(C1 p0, C1 p1, C1 p2,
- C2 q0, C2 q1, C2 q2,
- CircularArcIntersector::intersection_type result,
- const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
- const ArcOrPoint& i1 = CoordinateXYZM::getNull())
+ void checkIntersection(C1 p0, C1 p1, C1 p2,
+ C2 q0, C2 q1, C2 q2,
+ CircularArcIntersector::intersection_type result,
+ const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+ const ArcOrPoint& i1 = CoordinateXYZM::getNull())
{
CoordinateSequence cs1(3, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
cs1.setAt(p0, 0);
@@ -119,11 +129,11 @@ struct test_circulararcintersector_data {
}
template<typename C1, typename C2>
- static void checkIntersectionArcSeg(C1 p0, C1 p1, C1 p2,
- C2 q0, C2 q1,
- CircularArcIntersector::intersection_type result,
- const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
- const ArcOrPoint& i1 = CoordinateXYZM::getNull())
+ void checkIntersectionArcSeg(C1 p0, C1 p1, C1 p2,
+ C2 q0, C2 q1,
+ CircularArcIntersector::intersection_type result,
+ const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+ const ArcOrPoint& i1 = CoordinateXYZM::getNull())
{
CoordinateSequence cs1(3, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
cs1.setAt(p0, 0);
@@ -140,11 +150,11 @@ struct test_circulararcintersector_data {
}
template<typename C1, typename C2>
- static void checkIntersectionSegArc(C1 p0, C1 p1,
- C2 q0, C2 q1, C2 q2,
- CircularArcIntersector::intersection_type result,
- const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
- const ArcOrPoint& i1 = CoordinateXYZM::getNull())
+ void checkIntersectionSegArc(C1 p0, C1 p1,
+ C2 q0, C2 q1, C2 q2,
+ CircularArcIntersector::intersection_type result,
+ const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+ const ArcOrPoint& i1 = CoordinateXYZM::getNull())
{
CoordinateSequence seg(2, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
seg.setAt(p0, 0);
@@ -161,11 +171,11 @@ struct test_circulararcintersector_data {
}
template<typename C1, typename C2>
- static void checkIntersectionSegSeg(C1 p0, C1 p1,
- C2 q0, C2 q1,
- CircularArcIntersector::intersection_type result,
- const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
- const ArcOrPoint& i1 = CoordinateXYZM::getNull())
+ void checkIntersectionSegSeg(C1 p0, C1 p1,
+ C2 q0, C2 q1,
+ CircularArcIntersector::intersection_type result,
+ const ArcOrPoint& i0 = CoordinateXYZM::getNull(),
+ const ArcOrPoint& i1 = CoordinateXYZM::getNull())
{
CoordinateSequence seg0(2, C1::template has<Ordinate::Z>(), C1::template has<Ordinate::M>());
seg0.setAt(p0, 0);
@@ -179,7 +189,7 @@ struct test_circulararcintersector_data {
}
- static bool pointWithinTolerance(const CoordinateXYZM& actual, const CoordinateXYZM& expected, double tol)
+ static bool pointWithinTolerance(const CoordinateXYZM& actual, const CoordinateXYZM& expected, double eps, bool isDistance)
{
if (std::isnan(actual.z) != std::isnan(expected.z)) {
return false;
@@ -189,23 +199,23 @@ struct test_circulararcintersector_data {
return false;
}
- if (!std::isnan(expected.z) && std::abs(actual.z - expected.z) > tol * std::abs(expected.z)) {
+ if (!std::isnan(expected.z) && std::abs(actual.z - expected.z) > eps * std::abs(expected.z)) {
return false;
}
- if (!std::isnan(expected.m) && std::abs(actual.m - expected.m) > tol * std::abs(expected.m)) {
+ if (!std::isnan(expected.m) && std::abs(actual.m - expected.m) > eps * std::abs(expected.m)) {
return false;
}
- if (actual.distance(expected) < tol) {
+ if (isDistance && actual.distance(expected) < eps) {
return true;
}
- if (std::abs(actual.x - expected.x) > tol * std::abs(expected.x)) {
+ if (std::abs(actual.x - expected.x) > eps * std::abs(expected.x)) {
return false;
}
- if (std::abs(actual.y - expected.y) > tol * std::abs(expected.y)) {
+ if (std::abs(actual.y - expected.y) > eps * std::abs(expected.y)) {
return false;
}
@@ -213,7 +223,7 @@ struct test_circulararcintersector_data {
}
template<typename T1, typename T2>
- static void checkIntersection(const T1& a0,
+ void checkIntersection(const T1& a0,
const T2& a1,
CircularArcIntersector::intersection_type result,
const ArcOrPoint& p0 = CoordinateXYZM::getNull(),
@@ -292,11 +302,9 @@ struct test_circulararcintersector_data {
equal = false;
}
- constexpr double eps = 1e-8;
-
if (equal) {
for (std::size_t i = 0; i < actualPoints.size(); i++) {
- if (!pointWithinTolerance(actualPoints[i], expectedPoints[i], eps)) {
+ if (!pointWithinTolerance(actualPoints[i], expectedPoints[i], eps, toleranceIsDistance)) {
equal = false;
}
}
@@ -309,7 +317,7 @@ struct test_circulararcintersector_data {
equal = false;
}
- if (!pointWithinTolerance(XYZM(actualArcs[i].getCenter()), XYZM(expectedArcs[i].getCenter()), eps)) {
+ if (!pointWithinTolerance(XYZM(actualArcs[i].getCenter()), XYZM(expectedArcs[i].getCenter()), eps, toleranceIsDistance)) {
equal = false;
}
@@ -317,7 +325,7 @@ struct test_circulararcintersector_data {
actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual0);
expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected0);
- if (!pointWithinTolerance(actual0, expected0, eps)) {
+ if (!pointWithinTolerance(actual0, expected0, eps, toleranceIsDistance)) {
equal = false;
}
@@ -325,7 +333,7 @@ struct test_circulararcintersector_data {
actualArcs[i].getCoordinateSequence()->getAt(actualArcs[i].getCoordinatePosition(), actual2);
expectedArcs[i].getCoordinateSequence()->getAt(expectedArcs[i].getCoordinatePosition(), expected2);
- if (!pointWithinTolerance(actual2, expected2, eps)) {
+ if (!pointWithinTolerance(actual2, expected2, eps, toleranceIsDistance)) {
equal = false;
}
}
@@ -1753,6 +1761,56 @@ void object::test<87>()
ensure_equals(arc.p2(), arcOut.p2());
}
+template<>
+template<>
+void object::test<88>()
+{
+ // From https://github.com/libgeos/geos/issues/1539
+ set_test_name("arc endpoint intersects interior of horizontal line");
+ setTolerance(0);
+ setToleranceIsDistance(false);
+
+ // clockwise arc
+ checkIntersectionArcSeg(XY{1, 10}, XY{3, 7}, XY{1, 5},
+ XY{0, 10}, XY{10, 10},
+ CircularArcIntersector::ONE_POINT_INTERSECTION,
+ XY{1, 10});
+
+ // counter-clockwise arc
+ checkIntersectionArcSeg(XY{1, 5}, XY{3, 7}, XY{1, 10},
+ XY{0, 10}, XY{10, 10},
+ CircularArcIntersector::ONE_POINT_INTERSECTION,
+ XY{1, 10});
+}
+
+template<>
+template<>
+void object::test<89>()
+{
+ set_test_name("arc endpoint intersects interior of vertical line");
+ setTolerance(0);
+ setToleranceIsDistance(false);
+
+ checkIntersectionArcSeg(XY{-7, 3}, XY{8, 11}, XY{1043, 6},
+ XY{-7, 0}, XY{-7, 5},
+ CircularArcIntersector::ONE_POINT_INTERSECTION,
+ XY{-7, 3});
+}
+
+template<>
+template<>
+void object::test<90>()
+{
+ set_test_name("arc control point intersects interior of diagonal line");
+ setTolerance(0);
+ setToleranceIsDistance(false);
+
+ checkIntersectionArcSeg(XY{0 , 0}, XY{208, 22}, XY{140355444245, 3882200024},
+ XY{104, 11}, XY{2080, 220},
+ CircularArcIntersector::ONE_POINT_INTERSECTION,
+ XY{208, 22});
+}
+
// TODO: check Z values of arc result centerpoints
// TODO: add tests for seg/seg
diff --git a/tests/unit/operation/split/GeometrySplitterTest.cpp b/tests/unit/operation/split/GeometrySplitterTest.cpp
index 478c053ec..5e5dc4d74 100644
--- a/tests/unit/operation/split/GeometrySplitterTest.cpp
+++ b/tests/unit/operation/split/GeometrySplitterTest.cpp
@@ -923,4 +923,15 @@ void object::test<77>()
"GEOMETRYCOLLECTION (CURVEPOLYGON (COMPOUNDCURVE ((5 0, 3 0, 3 5, 5 5), CIRCULARSTRING (5 5, 7 1, 5 0))), POLYGON ((3 0, 0 0, 0 5, 3 5, 3 0)))");
}
+template<>
+template<>
+void object::test<78>()
+{
+ set_test_name("split Polygon with CircularString (#1539)");
+
+ testSplit("POLYGON ((0 5, 0 10, 10 10, 10 5, 0 5))",
+ "CIRCULARSTRING (1 10, 3 7, 1 5)",
+ "GEOMETRYCOLLECTION (CURVEPOLYGON (COMPOUNDCURVE ((0 5, 0 10, 1 10), CIRCULARSTRING (1 10, 3 7, 1 5), (1 5, 0 5))), CURVEPOLYGON (COMPOUNDCURVE ((1 10, 10 10, 10 5, 1 5), CIRCULARSTRING (1 5, 3 7, 1 10))))");
+}
+
}
diff --git a/tests/xmltester/tests/general/TestOverlayCurvesLA.xml b/tests/xmltester/tests/general/TestOverlayCurvesLA.xml
index 1cef3bf89..1c6c60340 100644
--- a/tests/xmltester/tests/general/TestOverlayCurvesLA.xml
+++ b/tests/xmltester/tests/general/TestOverlayCurvesLA.xml
@@ -117,7 +117,7 @@
<test>
<op name="union" arg1="A" arg2="B">
- GEOMETRYCOLLECTION (CURVEPOLYGON (CIRCULARSTRING (60 80, 100 120, 120 180, 140 120, 180 80, 120 90, 60 80)), CIRCULARSTRING (60 220, 120 270, 180 220, 156.0555127546399 190.91673086804016, 120 180, 83.94448724536011 190.91673086804016, 60 220), POLYGON ((100 240, 140 240, 120 220, 100 240)), LINESTRING (60 40, 60 80, 60 80.00000000000001, 60 220), LINESTRING (60 220, 60 300))
+ GEOMETRYCOLLECTION (CURVEPOLYGON (CIRCULARSTRING (60 80, 100 120, 120 180, 140 120, 180 80, 120 90, 60 80)), CIRCULARSTRING (60 220, 120 270, 180 220, 156.0555127546399 190.91673086804016, 120 180, 83.94448724536011 190.91673086804016, 60 220), POLYGON ((100 240, 140 240, 120 220, 100 240)), LINESTRING (60 40, 60 80, 60 220), LINESTRING (60 220, 60 300))
</op>
</test>
@@ -129,7 +129,7 @@
<test>
<op name="symdifference" arg1="A" arg2="B">
- GEOMETRYCOLLECTION (CURVEPOLYGON (CIRCULARSTRING (60 80, 100 120, 120 180, 140 120, 180 80, 120 90, 60 80)), CIRCULARSTRING (60 220, 120 270, 180 220, 156.0555127546399 190.91673086804016, 120 180, 83.94448724536011 190.91673086804016, 60 220), POLYGON ((100 240, 140 240, 120 220, 100 240)), LINESTRING (60 40, 60 80, 60 80.00000000000001, 60 220), LINESTRING (60 220, 60 300))
+ GEOMETRYCOLLECTION (CURVEPOLYGON (CIRCULARSTRING (60 80, 100 120, 120 180, 140 120, 180 80, 120 90, 60 80)), CIRCULARSTRING (60 220, 120 270, 180 220, 156.0555127546399 190.91673086804016, 120 180, 83.94448724536011 190.91673086804016, 60 220), POLYGON ((100 240, 140 240, 120 220, 100 240)), LINESTRING (60 40, 60 80, 60 220), LINESTRING (60 220, 60 300))
</op>
</test>
commit aee7f91d6528689fd99577ba790e52d54fc007db
Author: Daniel Baston <dbaston at gmail.com>
Date: Tue Oct 13 15:01:47 2026 -0400
CurveBuilder: Copy Z/M values at shared endpoints of mixed-dimension sequences
References https://github.com/libgeos/geos/issues/1541
diff --git a/NEWS.md b/NEWS.md
index 579fe1ed3..d80676c80 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -6,6 +6,7 @@
- Make GeometrySplitter able to split MultiSurface inputs (GH-1518, Dan Baston)
- GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston)
- Avoid raising `FE_INVALID` from Envelope (GH-1555, Mike Taves)
+ - Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston)
## Changes in 3.15.0
2026-09-01
diff --git a/include/geos/algorithm/LineToCurveConverter.h b/include/geos/algorithm/LineToCurveConverter.h
index b14b3f98a..c0efb0c06 100644
--- a/include/geos/algorithm/LineToCurveConverter.h
+++ b/include/geos/algorithm/LineToCurveConverter.h
@@ -53,17 +53,6 @@ private:
void addLineCoords(const geom::CoordinateSequence& points, std::size_t from, std::size_t to);
- /*
-
- void finishArc();
-
- void finishLine();
-
- std::shared_ptr<geom::CoordinateSequence> lineCoords;
- std::shared_ptr<geom::CoordinateSequence> arcCoords;
- std::vector<std::unique_ptr<geom::SimpleCurve>> curves;
- const geom::GeometryFactory& factory;
- */
geom::util::CurveBuilder curveBuilder;
/// Declared as non-copyable
diff --git a/include/geos/geom/util/CurveBuilder.h b/include/geos/geom/util/CurveBuilder.h
index 27fccd2fa..ab5913bf8 100644
--- a/include/geos/geom/util/CurveBuilder.h
+++ b/include/geos/geom/util/CurveBuilder.h
@@ -40,6 +40,9 @@ public:
// Add all coordinates in the provided sequence
void add(const CoordinateSequence& seq, bool isCurved);
+ // Add all coordinates in the provided sequence (optionally reversed)
+ void add(const CoordinateSequence& seq, bool isCurved, bool isForward);
+
// Add coordinates between the specified indices (inclusive)
void add(const CoordinateSequence& seq, std::size_t from, std::size_t to, bool isCurved);
@@ -62,28 +65,29 @@ public:
}
bool hasActiveSequence() const {
- return m_pts != nullptr;
+ return !m_pts.empty();
}
bool isCurved() const {
- return m_isCurved;
+ return hasActiveSequence() && m_pts.back().second;
}
void setOutputLinearRing(bool outputLinearRing) {
m_outputLinearRing = outputLinearRing;
}
-private:
- void finishCurve();
- void finishLine();
+ void matchRingZM();
- std::vector<std::unique_ptr<SimpleCurve>> m_curves;
- std::unique_ptr<CoordinateSequence> m_pts{nullptr};
+ // Declare type as noncopyable
+ CurveBuilder(const CurveBuilder& other) = delete;
+ CurveBuilder& operator=(const CurveBuilder& rhs) = delete;
+
+private:
+ std::vector<std::pair<std::unique_ptr<CoordinateSequence>, bool>> m_pts;
const GeometryFactory& m_gfact;
const bool m_hasZ;
const bool m_hasM;
bool m_outputLinearRing{true};
- bool m_isCurved{false};
};
}
\ No newline at end of file
diff --git a/include/geos/operation/polygonize/EdgeRing.h b/include/geos/operation/polygonize/EdgeRing.h
index 5f8b7df5b..281b200d9 100644
--- a/include/geos/operation/polygonize/EdgeRing.h
+++ b/include/geos/operation/polygonize/EdgeRing.h
@@ -82,10 +82,6 @@ private:
return *getRingInternal()->getEnvelopeInternal();
}
- static void addEdge(const geom::CoordinateSequence* coords,
- bool isForward,
- geom::CoordinateSequence* coordList);
-
algorithm::locate::PointOnGeometryLocator* getLocator() const;
bool contains(const EdgeRing& otherRing) const;
diff --git a/src/geom/util/CurveBuilder.cpp b/src/geom/util/CurveBuilder.cpp
index 28859a7cc..2996a81f4 100644
--- a/src/geom/util/CurveBuilder.cpp
+++ b/src/geom/util/CurveBuilder.cpp
@@ -20,6 +20,9 @@
#include <geos/geom/CoordinateSequence.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/LineString.h>
+#include <geos/util/GEOSException.h>
+
+#include <cmath>
namespace geos::geom::util {
@@ -47,108 +50,186 @@ CurveBuilder::add(const Curve& geom)
void
CurveBuilder::add(const CoordinateSequence& coords, bool isCurved)
{
- getSeq(isCurved).add(coords, false);
+ add(coords, isCurved, true);
}
void
-CurveBuilder::add(const CoordinateSequence& coords, std::size_t from, std::size_t to, bool isCurved)
+CurveBuilder::add(const CoordinateSequence& coords, bool isCurved, bool isForward)
{
- getSeq(isCurved).add(coords, from, to, false);
+ if (coords.isEmpty()) {
+ return;
+ }
+
+ if (isForward) {
+ add(coords, 0, coords.size() - 1, isCurved);
+ } else {
+ add(coords, coords.size() - 1, 0, isCurved);
+ }
+}
+
+void
+CurveBuilder::add(const CoordinateSequence& src, std::size_t from, std::size_t to, bool isCurved)
+{
+ CoordinateSequence& dst = getSeq(isCurved);
+ const std::size_t insertionPos = dst.size();
+
+ if (from > to) {
+ for(std::size_t i = from + 1; i > to; --i) {
+ src.applyAt(i-1, [&dst](const auto& coord) {
+ dst.add(coord, false);
+ });
+ }
+ } else {
+ dst.add(src, from, to, false);
+ }
+
+ // When adding a sequence whose initial point is the same as the current final point, we may need to
+ // copy Z/M values from the initial point to the final point, or vice-versa.
+
+ if (!m_hasZ && !m_hasM) {
+ return;
+ }
+
+ if (insertionPos == 0 && m_pts.size() == 1) {
+ return;
+ }
+
+ CoordinateSequence& prev = insertionPos == 0 ? *m_pts[m_pts.size() - 2].first : dst;
+ std::size_t prevPos = insertionPos == 0 ? prev.size() - 1 : insertionPos - 1;
+ std::size_t currPos = insertionPos == 0 ? 0 : insertionPos - 1;
+
+ const bool firstPointEqualsLast = prev.getAt<CoordinateXY>(prevPos).equals2D(src.getAt<CoordinateXY>(from));
+
+ if (!firstPointEqualsLast) {
+ return;
+ }
+
+ if (m_hasZ) {
+ const double currZ = src.getZ(from);
+ const double prevZ = prev.getZ(prevPos);
+
+ if (std::isnan(prevZ) && !std::isnan(currZ)) {
+ prev.setZ(prevPos, currZ);
+ }
+ if (std::isnan(currZ) && !std::isnan(prevZ)) {
+ dst.setZ(currPos, prevZ);
+ }
+ }
+
+ if (m_hasM) {
+ const double currM = src.getM(from);
+ const double prevM = prev.getM(prevPos);
+
+ if (std::isnan(prevM) && !std::isnan(currM)) {
+ prev.setM(prevPos, currM);
+ }
+ if (std::isnan(currM) && !std::isnan(prevM)) {
+ dst.setM(currPos, prevM);
+ }
+ }
+}
+
+void
+CurveBuilder::matchRingZM()
+{
+ if (m_pts.empty()) {
+ return;
+ }
+
+ CoordinateSequence& firstSeq = *m_pts.front().first;
+ CoordinateSequence& lastSeq = *m_pts.back().first;
+
+ if (!firstSeq.front<CoordinateXY>().equals2D(lastSeq.back<CoordinateXY>())) {
+ throw geos::util::GEOSException("CurveBuilder::matchRingZM called on non-ring");
+ }
+
+ if (m_hasZ) {
+ const double firstZ = firstSeq.getZ(0);
+ const double lastZ = lastSeq.getZ(lastSeq.size() - 1);
+
+ if (std::isnan(firstZ) && !std::isnan(lastZ)) {
+ firstSeq.setZ(0, lastZ);
+ }
+ if (std::isnan(lastZ) && !std::isnan(firstZ)) {
+ lastSeq.setZ(lastSeq.size() - 1, firstZ);
+ }
+ }
+
+ if (m_hasM) {
+ const double firstM = firstSeq.getM(0);
+ const double lastM = lastSeq.getM(lastSeq.size() - 1);
+
+ if (std::isnan(firstM) && !std::isnan(lastM)) {
+ firstSeq.setM(0, lastM);
+ }
+ if (std::isnan(lastM) && !std::isnan(firstM)) {
+ lastSeq.setM(lastSeq.size() - 1, firstM);
+ }
+ }
}
void
CurveBuilder::closeRing()
{
- if (m_curves.empty() && (m_pts == nullptr || m_pts->isEmpty())) {
+ if (m_pts.empty()) {
return;
}
CoordinateXYZM first;
- if (!m_curves.empty()) {
- m_curves.front()->getCoordinatesRO()->getAt(0, first);
- } else {
- m_pts->getAt(0, first);
- }
+ m_pts.front().first->getAt(0, first);
CoordinateXYZM last;
- if (m_pts && !m_pts->isEmpty()) {
- m_pts->getAt(m_pts->size() - 1, last);
- } else {
- const auto* seq = m_curves.back()->getCoordinatesRO();
- seq->getAt(seq->size() - 1, last);
- }
+ m_pts.back().first->getAt(m_pts.back().first->size() - 1, last);
if (first.equals2D(last)) {
+ matchRingZM();
return;
}
- if (m_pts && m_isCurved) {
- finishCurve();
+ if (isCurved()) {
getSeq(false).add(last);
}
-
getSeq(false).add(first);
}
-void
-CurveBuilder::finishCurve()
-{
- m_curves.push_back(m_gfact.createCircularString(std::move(m_pts)));
- m_pts = nullptr;
-}
-
-void
-CurveBuilder::finishLine()
-{
- m_curves.push_back(m_gfact.createLineString(std::move(m_pts)));
- m_pts = nullptr;
-}
-
std::unique_ptr<Curve>
CurveBuilder::getGeometry()
{
- if (m_pts) {
- if (m_isCurved) {
- finishCurve();
- } else {
+ if (m_pts.empty()) {
+ return m_gfact.createLineString(std::make_unique<CoordinateSequence>(0, m_hasZ, m_hasM));
+ }
- if (m_outputLinearRing && m_curves.empty() && m_pts->isRing()) {
- m_curves.push_back(m_gfact.createLinearRing(std::move(m_pts)));
- } else {
- finishLine();
- }
+ if (m_pts.size() == 1) {
+ auto& [pts, isCurve] = m_pts.front();
+ if (isCurve) {
+ return m_gfact.createCircularString(std::move(pts));
+ }
+ if (m_outputLinearRing && pts->isRing()) {
+ return m_gfact.createLinearRing(std::move(pts));
+ }
+ return m_gfact.createLineString(std::move(pts));
+ }
+
+ std::vector<std::unique_ptr<SimpleCurve>> curves;
+ for (auto& [pts, isCurve] : m_pts) {
+ if (isCurve) {
+ curves.push_back(m_gfact.createCircularString(std::move(pts)));
+ } else {
+ curves.push_back(m_gfact.createLineString(std::move(pts)));
}
}
- if (m_curves.empty()) {
- auto seq = std::make_unique<CoordinateSequence>(0, m_hasZ, m_hasM);
- return m_gfact.createLineString(std::move(seq));
- }
-
- if (m_curves.size() == 1) {
- return std::move(m_curves[0]);
- }
-
- return m_gfact.createCompoundCurve(std::move(m_curves));
+ return m_gfact.createCompoundCurve(std::move(curves));
}
CoordinateSequence&
CurveBuilder::getSeq(bool isCurved)
{
- if (m_pts) {
- if (m_isCurved && !isCurved) {
- finishCurve();
- } else if (isCurved && !m_isCurved) {
- finishLine();
- }
+ if (m_pts.empty() || m_pts.back().second != isCurved) {
+ m_pts.emplace_back(std::make_unique<CoordinateSequence>(0, m_hasZ, m_hasM), isCurved);
}
- if (!m_pts) {
- m_pts = std::make_unique<CoordinateSequence>(0, m_hasZ, m_hasM);
- m_isCurved = isCurved;
- }
-
- return *m_pts;
+ return *m_pts.back().first;
}
}
diff --git a/src/operation/linemerge/EdgeString.cpp b/src/operation/linemerge/EdgeString.cpp
index 54fbbae5c..3b826c8dc 100644
--- a/src/operation/linemerge/EdgeString.cpp
+++ b/src/operation/linemerge/EdgeString.cpp
@@ -64,26 +64,9 @@ addCoordinates(geom::util::CurveBuilder& curveBuilder, const SimpleCurve* curve,
}
const CoordinateSequence& srcCoords = *curve->getCoordinatesRO();
-
- // Patch Z value in last coordinate, if needed
- if (curveBuilder.hasActiveSequence() && curve->hasZ()) {
- CoordinateSequence& dstCoords = curveBuilder.getSeq(curveBuilder.isCurved());
- if (std::isnan(dstCoords.getZ(dstCoords.getSize() - 1))) {
- dstCoords.setZ(dstCoords.getSize() - 1, forward ? srcCoords.getZ(0) : srcCoords.getZ(srcCoords.getSize() - 1));
- }
- }
-
- // Patch M value in last coordinate, if needed
- if (curveBuilder.hasActiveSequence() && curve->hasM()) {
- CoordinateSequence& dstCoords = curveBuilder.getSeq(curveBuilder.isCurved());
- if (std::isnan(dstCoords.getM(dstCoords.getSize() - 1))) {
- dstCoords.setM(dstCoords.getSize() - 1, forward ? srcCoords.getM(0) : srcCoords.getM(srcCoords.getSize() - 1));
- }
- }
-
const bool isCurved = curve->getGeometryTypeId() == GEOS_CIRCULARSTRING;
- CoordinateSequence& dstCoords = curveBuilder.getSeq(isCurved);
- dstCoords.add(srcCoords, false, forward);
+
+ curveBuilder.add(srcCoords, isCurved, forward);
}
std::unique_ptr<Curve>
diff --git a/src/operation/polygonize/EdgeRing.cpp b/src/operation/polygonize/EdgeRing.cpp
index 8917c1bf7..2ce1bd724 100644
--- a/src/operation/polygonize/EdgeRing.cpp
+++ b/src/operation/polygonize/EdgeRing.cpp
@@ -305,13 +305,12 @@ EdgeRing::getRingInternal() const
for(const auto& de : deList) {
const auto edge = detail::down_cast<PolygonizeEdge*>(de->getEdge());
const bool isCurved = edge->getLine()->getGeometryTypeId() == GEOS_CIRCULARSTRING;
+ const bool isForward = de->getEdgeDirection();
- CoordinateSequence& ringPts = builder.getSeq(isCurved);
-
- addEdge(edge->getLine()->getCoordinatesRO(),
- de->getEdgeDirection(), &ringPts);
+ builder.add(*edge->getLine()->getCoordinatesRO(), isCurved, isForward);
}
+ builder.matchRingZM();
ring = builder.getGeometry();
}
@@ -326,36 +325,6 @@ EdgeRing::getRingOwnership()
return std::move(ring);
}
-/*private*/
-void
-EdgeRing::addEdge(const CoordinateSequence* srcCoords, bool isForward,
- CoordinateSequence* dstCoords)
-{
- const std::size_t npts = srcCoords->getSize();
-
- if (!dstCoords->isEmpty() && !srcCoords->isEmpty()) {
- // Patch Z value in last coordinate, if needed
- if (srcCoords->hasZ() && std::isnan(dstCoords->getZ(dstCoords->size() - 1))) {
- dstCoords->setZ(dstCoords->size() - 1, isForward ? srcCoords->getZ(0) : srcCoords->getZ(srcCoords->getSize() - 1));
- }
- // Patch M value in last coordinate, if needed
- if (srcCoords->hasM() && std::isnan(dstCoords->getM(dstCoords->size() - 1))) {
- dstCoords->setM(dstCoords->size() - 1, isForward ? srcCoords->getM(0) : srcCoords->getM(srcCoords->getSize() - 1));
- }
- }
-
- if(isForward) {
- dstCoords->add(*srcCoords, 0, npts - 1, false);
- }
- else {
- for(std::size_t i = npts; i > 0; --i) {
- srcCoords->applyAt(i-1, [&dstCoords](const auto& coord) {
- dstCoords->add(coord, false);
- });
- }
- }
-}
-
EdgeRing*
EdgeRing::getOuterHole() const {
// Only shells can have outer holes
diff --git a/tests/unit/geom/util/CurveBuilderTest.cpp b/tests/unit/geom/util/CurveBuilderTest.cpp
index d7e801746..05a44fe48 100644
--- a/tests/unit/geom/util/CurveBuilderTest.cpp
+++ b/tests/unit/geom/util/CurveBuilderTest.cpp
@@ -174,4 +174,86 @@ void object::test<8>()
ensure_equals_geometry(static_cast<const Geometry*>(result.get()), expected.get());
}
+template<>
+template<>
+void object::test<9>()
+{
+ set_test_name("Z/M matched when combining mixed-dimension sequences");
+
+ CurveBuilder builder(*factory_, true, true);
+ builder.add(*reader_.read<LineString>("LINESTRING (0 0, 1 1)"));
+ // Patch Z Linear <- Linear
+ builder.add(*reader_.read<LineString>("LINESTRING Z (1 1 5, 2 2 4)"));
+ // Patch Z Linear -> Linear
+ builder.add(*reader_.read<LineString>("LINESTRING (2 2, 5 0)"));
+ // Patch M Linear <- Curve
+ builder.add(*reader_.read<CircularString>("CIRCULARSTRING M (5 0 8, 6 1 7, 7 0 8)"));
+ // Patch M Curve -> Curve
+ builder.add(*reader_.read<CircularString>("CIRCULARSTRING (7 0, 8 -1, 9 0)"));
+ // Patch Z Curve <- Linear
+ builder.add(*reader_.read<LineString>("LINESTRING (9 0 11, 10 20 12)"));
+
+ auto result = builder.getGeometry();
+
+ auto expected = reader_.read("COMPOUNDCURVE ZM ((0 0 NaN NaN, 1 1 5 NaN, 2 2 4 NaN, 5 0 NaN 8), CIRCULARSTRING ZM (5 0 NaN 8, 6 1 NaN 7, 7 0 NaN 8, 8 -1 NaN NaN, 9 0 11 NaN), (9 0 11 NaN, 10 20 12 NaN))");
+
+ ensure_equals_geometry_xyzm(static_cast<const Geometry*>(result.get()), expected.get());
+}
+
+template<>
+template<>
+void object::test<10>()
+{
+ set_test_name("Z/M matched when calling closeRing on open CircularString");
+
+ CurveBuilder builder(*factory_, true, true);
+ builder.add(*reader_.read<CircularString>("CIRCULARSTRING ZM (0 0 100 200, 1 1 200 400, 2 0 300 600)"));
+ builder.closeRing();
+
+ auto result = builder.getGeometry();
+
+ auto expected = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING(0 0 100 200, 1 1 200 400, 2 0 300 600), (2 0 300 600, 0 0 100 200))");
+
+ ensure_equals_geometry_xyzm(static_cast<const Geometry*>(result.get()), expected.get());
+}
+
+template<>
+template<>
+void object::test<11>()
+{
+ set_test_name("Z/M matched when calling closeRing on closed CircularString ZM -> LineString");
+
+ CurveBuilder builder(*factory_, true, true);
+ builder.add(*reader_.read<CircularString>("CIRCULARSTRING ZM (0 0 100 200, 1 1 200 400, 2 0 300 600)"));
+ builder.add(*reader_.read<LineString>("LINESTRING (2 0, 0 0)"));
+
+ builder.closeRing();
+
+ auto result = builder.getGeometry();
+
+ auto expected = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING(0 0 100 200, 1 1 200 400, 2 0 300 600), (2 0 300 600, 0 0 100 200))");
+
+ ensure_equals_geometry_xyzm(static_cast<const Geometry*>(result.get()), expected.get());
+}
+
+template<>
+template<>
+void object::test<12>()
+{
+ set_test_name("Z/M matched when calling closeRing on closed CircularString -> LineString ZM");
+
+ CurveBuilder builder(*factory_, true, true);
+ builder.add(*reader_.read<CircularString>("CIRCULARSTRING (0 0, 1 1, 2 0)"));
+ builder.add(*reader_.read<LineString>("LINESTRING ZM (2 0 300 600, 0 0 100 200)"));
+
+ builder.closeRing();
+
+ auto result = builder.getGeometry();
+
+ auto expected = reader_.read("COMPOUNDCURVE ZM (CIRCULARSTRING(0 0 100 200, 1 1 NaN NaN, 2 0 300 600), (2 0 300 600, 0 0 100 200))");
+
+ ensure_equals_geometry_xyzm(static_cast<const Geometry*>(result.get()), expected.get());
+}
+
+
} // namespace tut
diff --git a/tests/unit/operation/polygonize/PolygonizeTest.cpp b/tests/unit/operation/polygonize/PolygonizeTest.cpp
index 502fc2c84..9c6002a62 100644
--- a/tests/unit/operation/polygonize/PolygonizeTest.cpp
+++ b/tests/unit/operation/polygonize/PolygonizeTest.cpp
@@ -62,8 +62,8 @@ struct test_polygonizetest_data {
void
printAll(std::ostream& os, T& cnt)
{
- for(typename T::iterator i = cnt.begin(), e = cnt.end(); i != e; ++i) {
- os << **i << std::endl;
+ for(auto& geom : cnt) {
+ os << geom->toString() << std::endl;
}
}
@@ -71,9 +71,8 @@ struct test_polygonizetest_data {
bool
contains(T& cnt, const Geom* g)
{
- for(typename T::iterator i = cnt.begin(), e = cnt.end(); i != e; ++i) {
- const auto& element = *i;
- if(element->equalsExact(g)) {
+ for(const auto& element : cnt) {
+ if(element->equalsIdentical(g)) {
return true;
}
}
@@ -92,9 +91,9 @@ struct test_polygonizetest_data {
<< ob.size() << endl;
return false;
}
- for(typename T::iterator i = ex.begin(), e = ex.end(); i != e; ++i) {
- if(! contains(ob, i->get())) {
- cout << "Expected " << wktwriter.write(i->get())
+ for(const auto& expected : ex) {
+ if(! contains(ob, expected.get())) {
+ cout << "Expected " << wktwriter.write(expected.get())
<< " not found" << endl;
return false;
}
@@ -395,7 +394,7 @@ void object::test<11>()
},
{
"POLYGON ZM ((0 0 0 0, 0 10 1 2, 10 10 2 3, 10 0 3 4, 0 0 0 0))",
- "POLYGON ZM ((10 0 3 4, 10 10 2 3, 20 0 NaN 9, 10 0 3 4))"
+ "POLYGON ZM ((10 0 3 4, 10 10 2 3, 20 0 NaN 9, 10 0 3 8))"
},
false,
POLYGONS);
@@ -481,5 +480,23 @@ void object::test<15>()
doTest(input, expected, false, POLYGONS);
}
+template<>
+template<>
+void object::test<16>()
+{
+ set_test_name("mixed-dimension curved and linear inputs");
+
+ std::vector<std::string> input{
+ "LINESTRING Z (-5 0 10, 5 0 20)",
+ "CIRCULARSTRING (-5 0, 0 5, 5 0)"
+ };
+
+ std::vector<std::string> expected{
+ "CURVEPOLYGON Z (COMPOUNDCURVE((-5 0 10, 5 0 20), CIRCULARSTRING (5 0 20, 0 5 NaN, -5 0 10)))"
+ };
+
+ doTest(input, expected, false, POLYGONS);
+}
+
} // namespace tut
-----------------------------------------------------------------------
Summary of changes:
NEWS.md | 4 +
capi/geos_ts_c.cpp | 4 +-
include/geos/algorithm/LineToCurveConverter.h | 11 --
include/geos/geom/util/CurveBuilder.h | 20 ++-
include/geos/operation/polygonize/EdgeRing.h | 4 -
src/algorithm/CircularArcIntersector.cpp | 22 +--
src/geom/util/CurveBuilder.cpp | 195 +++++++++++++++------
src/operation/linemerge/EdgeString.cpp | 21 +--
src/operation/polygonize/EdgeRing.cpp | 37 +---
.../unit/algorithm/CircularArcIntersectorTest.cpp | 136 ++++++++++----
tests/unit/capi/GEOSProjectTest.cpp | 36 ++++
tests/unit/geom/util/CurveBuilderTest.cpp | 82 +++++++++
tests/unit/operation/polygonize/PolygonizeTest.cpp | 35 +++-
.../unit/operation/split/GeometrySplitterTest.cpp | 11 ++
.../tests/general/TestOverlayCurvesLA.xml | 4 +-
15 files changed, 432 insertions(+), 190 deletions(-)
hooks/post-receive
--
GEOS
More information about the geos-commits
mailing list