From git at osgeo.org Thu Oct 1 05:34:02 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 05:34:02 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. e0ff2cd9eda39cf7a3283e6321b0c69eca5db49b Message-ID: <20261001123402.8D16218E892@trac.osgeo.org> 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 e0ff2cd9eda39cf7a3283e6321b0c69eca5db49b (commit) from 7c25167af9fd326cd957ad588296495ac188d231 (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 e0ff2cd9eda39cf7a3283e6321b0c69eca5db49b Author: Mohd Quamar Tyagi <104281681+Tyagiquamar at users.noreply.github.com> Date: Thu Oct 1 18:03:32 2026 +0530 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 179015f24..e118726e7 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -4773,8 +4773,8 @@ extern "C" { { return execute(extHandle, -1.0, [&]() { const Point* point = dynamic_cast(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(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 ----------------------------------------------------------------------- Summary of changes: capi/geos_ts_c.cpp | 4 ++-- tests/unit/capi/GEOSProjectTest.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 05:39:02 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 05:39:02 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 2e9e1b5bdeb2af6cd4b4e0161bc810e5e273235e Message-ID: <20261001123902.8A3D418E2B6@trac.osgeo.org> 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 2e9e1b5bdeb2af6cd4b4e0161bc810e5e273235e (commit) from e0ff2cd9eda39cf7a3283e6321b0c69eca5db49b (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 2e9e1b5bdeb2af6cd4b4e0161bc810e5e273235e Author: Daniel Baston Date: Thu Oct 1 08:38:43 2026 -0400 CircularArcIntersector: use Orient2D to check for arc control points on line (#1546) 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(); const CoordinateXY& ap1 = arc.p1(); const CoordinateXY& ap2 = arc.p2(); const CoordinateXY& bp0 = seq.getAt(segPos0); const CoordinateXY& bp1 = seq.getAt(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; + 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 - 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(), C1::template has()); cs1.setAt(p0, 0); @@ -119,11 +129,11 @@ struct test_circulararcintersector_data { } template - 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(), C1::template has()); cs1.setAt(p0, 0); @@ -140,11 +150,11 @@ struct test_circulararcintersector_data { } template - 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(), C1::template has()); seg.setAt(p0, 0); @@ -161,11 +171,11 @@ struct test_circulararcintersector_data { } template - 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(), C1::template has()); 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 - 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 8209abca4..97192839b 100644 --- a/tests/unit/operation/split/GeometrySplitterTest.cpp +++ b/tests/unit/operation/split/GeometrySplitterTest.cpp @@ -936,4 +936,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 @@ - 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)) @@ -129,7 +129,7 @@ - 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)) ----------------------------------------------------------------------- Summary of changes: src/algorithm/CircularArcIntersector.cpp | 10 +- .../unit/algorithm/CircularArcIntersectorTest.cpp | 124 +++++++++++++++------ .../unit/operation/split/GeometrySplitterTest.cpp | 11 ++ .../tests/general/TestOverlayCurvesLA.xml | 4 +- 4 files changed, 109 insertions(+), 40 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 06:05:33 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 06:05:33 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. cd6b251809cc60f1d05684b324eaac1635ab04d9 Message-ID: <20261001130534.1D29218FF17@trac.osgeo.org> 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 cd6b251809cc60f1d05684b324eaac1635ab04d9 (commit) from 2e9e1b5bdeb2af6cd4b4e0161bc810e5e273235e (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 cd6b251809cc60f1d05684b324eaac1635ab04d9 Author: Daniel Baston Date: Thu Oct 1 09:05:15 2026 -0400 TopologyPreservingSimplifier: Avoid stack overflow (#1519) References https://github.com/libgeos/geos/issues/1515 diff --git a/src/simplify/TaggedLineStringSimplifier.cpp b/src/simplify/TaggedLineStringSimplifier.cpp index 842710ae8..69e88f904 100644 --- a/src/simplify/TaggedLineStringSimplifier.cpp +++ b/src/simplify/TaggedLineStringSimplifier.cpp @@ -89,101 +89,112 @@ TaggedLineStringSimplifier::simplify(TaggedLineString* nLine, double distanceTol } } +struct Section { +std::size_t i; +std::size_t j; +std::size_t depth; +}; + /*private*/ void -TaggedLineStringSimplifier::simplifySection(std::size_t i, - std::size_t j, std::size_t depth, double distanceTolerance) +TaggedLineStringSimplifier::simplifySection(std::size_t init_i, + std::size_t init_j, std::size_t init_depth, double distanceTolerance) { - depth += 1; + std::stack
stack; + stack.push({init_i, init_j, init_depth + 1}); -#if GEOS_DEBUG - std::cerr << "TaggedLineStringSimplifier[" << this << "] " - << " simplifying section " << i << "-" << j - << std::endl; -#endif + while (!stack.empty()) { + const auto [i, j, depth] = stack.top(); + stack.pop(); - if((i + 1) == j) { - -#if GEOS_DEBUG - std::cerr << "single segment, no flattening" + #if GEOS_DEBUG + std::cerr << "TaggedLineStringSimplifier[" << this << "] " + << " simplifying section " << i << "-" << j << std::endl; -#endif - std::unique_ptr newSeg(new - TaggedLineSegment(*(line->getSegment(i)))); + #endif - line->addToResult(std::move(newSeg)); - // leave this segment in the input index, for efficiency - return; - } + if((i + 1) == j) { - bool isValidToSimplify = true; + #if GEOS_DEBUG + std::cerr << "single segment, no flattening" + << std::endl; + #endif + auto newSeg = std::make_unique(*(line->getSegment(i))); - /* - * Following logic ensures that there is enough points in the - * output line. - * If there is already more points than the minimum, there's - * nothing to check. - * Otherwise, if in the worst case there wouldn't be enough points, - * don't flatten this segment (which avoids the worst case scenario) - */ - if(line->getResultSize() < line->getMinimumSize()) { - std::size_t worstCaseSize = depth + 1; - if(worstCaseSize < line->getMinimumSize()) { + line->addToResult(std::move(newSeg)); + // leave this segment in the input index, for efficiency + continue; + } + + bool isValidToSimplify = true; + + /* + * Following logic ensures that there is enough points in the + * output line. + * If there is already more points than the minimum, there's + * nothing to check. + * Otherwise, if in the worst case there wouldn't be enough points, + * don't flatten this segment (which avoids the worst case scenario) + */ + if(line->getResultSize() < line->getMinimumSize()) { + std::size_t worstCaseSize = depth + 1; + if(worstCaseSize < line->getMinimumSize()) { + isValidToSimplify = false; + } + } + + double distance; + + // pass distance by ref + std::size_t furthestPtIndex = findFurthestPoint(linePts, i, j, distance); + + #if GEOS_DEBUG + std::cerr << "furthest point " << furthestPtIndex + << " at distance " << distance + << std::endl; + #endif + + if (distance < 0) { + // negative distance indicates that we could not compute distance to the + // farthest point, probably because of infinite or large-magnitude coordinates. + // avoid trying to simplify this section. + for (std::size_t k = i; k < j; k++) { + auto newSeg = std::make_unique(*(line->getSegment(k))); + line->addToResult(std::move(newSeg)); + } + + continue; + } + + // flattening must be less than distanceTolerance + if(distance > distanceTolerance) { isValidToSimplify = false; } - } - double distance; - - // pass distance by ref - std::size_t furthestPtIndex = findFurthestPoint(linePts, i, j, distance); - -#if GEOS_DEBUG - std::cerr << "furthest point " << furthestPtIndex - << " at distance " << distance - << std::endl; -#endif - - if (distance < 0) { - // negative distance indicates that we could not compute distance to the - // farthest point, probably because of infinite or large-magnitude coordinates. - // avoid trying to simplify this section. - for (std::size_t k = i; k < j; k++) { - auto newSeg = std::make_unique(*(line->getSegment(k))); - line->addToResult(std::move(newSeg)); + if (isValidToSimplify) { + // test if flattened section would cause intersection or jump + LineSegment flatSeg(linePts->getAt(i), linePts->getAt(j)); + isValidToSimplify = isTopologyValid(line, i, j, flatSeg); } - return; + if(isValidToSimplify) { + + std::unique_ptr newSeg = flatten(i, j); + + #if GEOS_DEBUG + std::cerr << "isValidToSimplify, adding seg " + << newSeg->p0 << ", " << newSeg->p1 + << " to TaggedLineSegment[" << line << "] result " + << std::endl; + #endif + + line->addToResult(std::move(newSeg)); + continue; + } + + stack.push({furthestPtIndex, j, depth+1}); + stack.push({i, furthestPtIndex, depth+1}); } - - // flattening must be less than distanceTolerance - if(distance > distanceTolerance) { - isValidToSimplify = false; - } - - if (isValidToSimplify) { - // test if flattened section would cause intersection or jump - LineSegment flatSeg(linePts->getAt(i), linePts->getAt(j)); - isValidToSimplify = isTopologyValid(line, i, j, flatSeg); - } - - if(isValidToSimplify) { - - std::unique_ptr newSeg = flatten(i, j); - -#if GEOS_DEBUG - std::cerr << "isValidToSimplify, adding seg " - << newSeg->p0 << ", " << newSeg->p1 - << " to TaggedLineSegment[" << line << "] result " - << std::endl; -#endif - - line->addToResult(std::move(newSeg)); - return; - } - - simplifySection(i, furthestPtIndex, depth, distanceTolerance); - simplifySection(furthestPtIndex, j, depth, distanceTolerance); } /*private*/ diff --git a/src/simplify/TopologyPreservingSimplifier.cpp b/src/simplify/TopologyPreservingSimplifier.cpp index b796610e0..e798a47f1 100644 --- a/src/simplify/TopologyPreservingSimplifier.cpp +++ b/src/simplify/TopologyPreservingSimplifier.cpp @@ -186,14 +186,13 @@ LineStringMapBuilderFilter::filter_ro(const Geometry* geom) auto ls = static_cast(geom); std::size_t minSize = ls->isClosed() ? 4 : 2; - TaggedLineString* taggedLine = new TaggedLineString(ls, minSize, isRing); + auto taggedLine = std::make_unique(ls, minSize, isRing); // Duplicated Geometry pointers shouldn't happen - if(! linestringMap.insert(std::make_pair(geom, taggedLine)).second) { - delete taggedLine; + if(! linestringMap.insert(std::make_pair(geom, taggedLine.get())).second) { throw util::GEOSException("Duplicated Geometry components detected"); } - tlsVector.push_back(taggedLine); + tlsVector.push_back(taggedLine.release()); } diff --git a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp index 651627b05..48a178ce5 100644 --- a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp +++ b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp @@ -473,4 +473,23 @@ void object::test<36>() } } +template<> +template<> +void object::test<37>() +{ + set_test_name("no stack overflow with large number of repeated vertices"); + + const std::size_t n = 10000; + auto seq = std::make_shared(n, false, false); + for (std::size_t i = 0; i < n; i++) { + seq->setAt(geos::geom::CoordinateXY{1, 1}, i); + } + + auto input = gf->createLineString(seq); + + auto simplified = TopologyPreservingSimplifier::simplify(input.get(), 1e-3); + + // No crash +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: src/simplify/TaggedLineStringSimplifier.cpp | 157 +++++++++++---------- src/simplify/TopologyPreservingSimplifier.cpp | 7 +- .../simplify/TopologyPreservingSimplifierTest.cpp | 19 +++ 3 files changed, 106 insertions(+), 77 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 07:11:19 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 07:11:19 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 7e7c2689257fa53ee3252c44264a776bc74c9c9f Message-ID: <20261001141120.25C811936EF@trac.osgeo.org> 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 7e7c2689257fa53ee3252c44264a776bc74c9c9f (commit) from cd6b251809cc60f1d05684b324eaac1635ab04d9 (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 7e7c2689257fa53ee3252c44264a776bc74c9c9f Author: Sandro Santilli Date: Wed Sep 30 16:45:35 2026 +0200 Add IsSimpleWithParams with params create/destroy/setters Closes GH-1543 Includes unit test for the new CAPI signatures diff --git a/NEWS.md b/NEWS.md index 10451996e..ccf7bf58a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -9,6 +9,7 @@ - New things: - Expose code revision: GEOS_REVISION, GEOSrevision, geos-config --revision (GH-1446 Sandro Santilli) + - GEOSisSimpleWithParams (GH-1543 Sandro Santilli) ## Changes in 3.15.0 2026-09-01 diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp index 568f1c0b4..b2918e466 100644 --- a/capi/geos_c.cpp +++ b/capi/geos_c.cpp @@ -69,6 +69,11 @@ typedef struct { int keepCollapsed; } GEOSMakeValidParams; +// Implementation struct for the GEOSisSimpleParams object +typedef struct { + bool findAllLocations; + int boundaryNodeRule; +} GEOSisSimpleParams; #include "geos_c.h" @@ -546,6 +551,36 @@ extern "C" { return GEOSisSimpleDetail_r(handle, g, returnAllPoints, result); } + GEOSisSimpleParams* + GEOSisSimpleParams_create(void) + { + return GEOSisSimpleParams_create_r(handle); + } + + void + GEOSisSimpleParams_destroy(GEOSisSimpleParams* parms) + { + return GEOSisSimpleParams_destroy_r(handle, parms); + } + + void + GEOSisSimpleParams_setFindAllLocations(GEOSisSimpleParams* params, int val) + { + return GEOSisSimpleParams_setFindAllLocations_r(handle, params, val); + } + + void + GEOSisSimpleParams_setBoundaryNodeRule(GEOSisSimpleParams* params, int bnr) + { + return GEOSisSimpleParams_setBoundaryNodeRule_r(handle, params, bnr); + } + + char + GEOSisSimpleWithParams(const Geometry *g, const GEOSisSimpleParams* params, Geometry** result) + { + return GEOSisSimpleWithParams_r(handle, g, params, result); + } + char GEOSisRing(const Geometry* g) { diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 19ec83cd0..ae9d7c384 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -220,7 +220,15 @@ typedef struct GEOSCurveToLineParams_t GEOSCurveToLineParams; */ typedef struct GEOSLineToCurveParams_t GEOSLineToCurveParams; -#endif +/** +* Parameter object for simplicity check. +* \see GEOSisSimpleParams_create() +* \see GEOSisSimpleParams_destroy() +*/ +typedef struct GEOSisSimpleParams_t GEOSisSimpleParams; + +#endif /* ndef GEOSGeometry */ + /** \cond */ @@ -1678,6 +1686,36 @@ extern char GEOS_DLL GEOSisSimple_r( GEOSContextHandle_t handle, const GEOSGeometry* g); +/** \see GEOSIsSimpleParams_create */ +extern GEOSisSimpleParams GEOS_DLL * +GEOSisSimpleParams_create_r( + GEOSContextHandle_t handle); + +/** \see GEOSIsSimpleParams_destroy */ +extern void GEOS_DLL +GEOSisSimpleParams_destroy_r( + GEOSContextHandle_t handle, + GEOSisSimpleParams* params); + +/** \see GEOSisSimpleParams_setFindAllLocations */ +extern void GEOS_DLL GEOSisSimpleParams_setFindAllLocations_r( + GEOSContextHandle_t handle, + GEOSisSimpleParams* params, + int findAllLocations); + +/** \see GEOSisSimpleParams_setBoundaryNodeRule */ +extern void GEOS_DLL GEOSisSimpleParams_setBoundaryNodeRule_r( + GEOSContextHandle_t handle, + GEOSisSimpleParams* params, + int bnr); + +/** \see GEOSisSimpleWithParams */ +extern char GEOS_DLL GEOSisSimpleWithParams_r( + GEOSContextHandle_t handle, + const GEOSGeometry* g, + const GEOSisSimpleParams* params, + GEOSGeometry** location); + /** \see GEOSisRing */ extern char GEOS_DLL GEOSisRing_r( GEOSContextHandle_t handle, @@ -3680,6 +3718,71 @@ extern char GEOS_DLL GEOSisSimpleDetail( int findAllLocations, GEOSGeometry** locations); +/** +* Create a GEOSisSimpleParams to hold the desired parameters +* to control the algorithm and behavior of the simplicity check process. +* \return a parameter object configured to find all intersection +* and use Mod2 boundary node rule +* \see GEOSisSimpleWithParams +* +* \since 3.16 +*/ +extern GEOSisSimpleParams GEOS_DLL *GEOSisSimpleParams_create(void); + +/** +* Destroy a GEOSisSimpleParams. +* \param params the object to destroy +* \see GEOSisSimpleWithParams +* +* \since 3.16 +*/ +extern void GEOS_DLL GEOSisSimpleParams_destroy(GEOSisSimpleParams* params); + +/** +* Set whether to return all self-intersection locations or just one +* \param params the object to modify +* \param findAllLocations Whether to return all self-intersection locations, or just one +* \see GEOSisSimpleWithParams +* +* \since 3.16 +*/ +extern void GEOS_DLL GEOSisSimpleParams_setFindAllLocations( + GEOSisSimpleParams* params, + int findAllLocations); + +/** +* Set BoundaryNodeRule to use with the simplicity check +* \param params the object to modify +* \param bnr A member of the \ref GEOSRelateBoundaryNodeRules enum +* \see GEOSisSimpleWithParams +* +* \since 3.16 +*/ +extern void GEOS_DLL GEOSisSimpleParams_setBoundaryNodeRule( + GEOSisSimpleParams* params, + int bnr); + +/** +* In one step, calculate and return whether a geometry is simple +* and one more more points at which the geometry self-intersects +* at interior points. +* Caller has the responsibility to destroy 'location' with +* GEOSGeom_destroy() +* +* @INPUT_CURVES_CONVERTED_TO_LINES@ +* +* \param g The geometry to test +* \param params various parameters for the operation +* \param locations A pointer in which the location GEOSGeometry will be placed +* \return 1 when simple, 0 when non-simple, 2 on exception +* +* \since 3.16 +*/ +extern char GEOS_DLL GEOSisSimpleWithParams( + const GEOSGeometry* g, + const GEOSisSimpleParams* params, + GEOSGeometry** locations); + /** * Check the validity of the provided geometry. * - All points are valid. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index e118726e7..1f05db1ff 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -177,6 +177,12 @@ typedef struct { int keepCollapsed; } GEOSMakeValidParams; +// Implementation struct for the GEOSisSimpleParams object +typedef struct { + bool findAllLocations; + int boundaryNodeRule; +} GEOSisSimpleParams; + #include "geos_c.h" // Intentional, to allow non-standard C elements like C99 functions to be @@ -1550,6 +1556,10 @@ extern "C" { }); } +/************************************************************************ + * IsSimpleOp { + */ + char GEOSisSimple_r(GEOSContextHandle_t extHandle, const Geometry* g1) { @@ -1583,6 +1593,92 @@ extern "C" { }); } + GEOSisSimpleParams* + GEOSisSimpleParams_create_r(GEOSContextHandle_t extHandle) + { + using geos::algorithm::BoundaryNodeRule; + + return execute(extHandle, [&]() { + GEOSisSimpleParams* p = new GEOSisSimpleParams(); + p->findAllLocations = false; + p->boundaryNodeRule = GEOSRELATE_BNR_MOD2; + return p; + }); + } + + void + GEOSisSimpleParams_destroy_r(GEOSContextHandle_t extHandle, GEOSisSimpleParams* parms) + { + (void)extHandle; + delete parms; + } + + void + GEOSisSimpleParams_setFindAllLocations_r(GEOSContextHandle_t extHandle, GEOSisSimpleParams* params, int val) + { + (void)extHandle; + params->findAllLocations = val; + } + + void + GEOSisSimpleParams_setBoundaryNodeRule_r(GEOSContextHandle_t extHandle, GEOSisSimpleParams* params, int bnr) + { + (void)extHandle; + params->boundaryNodeRule = bnr; + } + + + char + GEOSisSimpleWithParams_r(GEOSContextHandle_t extHandle, const Geometry* g1, const GEOSisSimpleParams* params, Geometry** result) + { + using geos::algorithm::BoundaryNodeRule; + using geos::operation::valid::IsSimpleOp; + + return execute(extHandle, 2, [&]() { + const auto inputGeom = convertToLineIfNeeded(extHandle, g1); + std::unique_ptr op; + + switch ( params->boundaryNodeRule ) { + case GEOSRELATE_BNR_MOD2: /* same as OGC */ + op = std::make_unique(*inputGeom, BoundaryNodeRule::getBoundaryRuleMod2()); + break; + case GEOSRELATE_BNR_ENDPOINT: + op = std::make_unique(*inputGeom, BoundaryNodeRule::getBoundaryEndPoint()); + break; + case GEOSRELATE_BNR_MULTIVALENT_ENDPOINT: + op = std::make_unique(*inputGeom, BoundaryNodeRule::getBoundaryMultivalentEndPoint()); + break; + case GEOSRELATE_BNR_MONOVALENT_ENDPOINT: + op = std::make_unique(*inputGeom, BoundaryNodeRule::getBoundaryMonovalentEndPoint()); + break; + default: + std::ostringstream ss; + ss << "Invalid boundary node rule " << params->boundaryNodeRule; + throw std::runtime_error(ss.str()); + } + + op->setFindAllLocations( params->findAllLocations ); + + *result = nullptr; + + bool simple = op->isSimple(); + if (!simple) { + auto locations = op->getNonSimpleLocations(); + if (locations.size() == 1 || ! params->findAllLocations ) { + *result = extHandle->geomFactory->createPoint(locations.front()).release(); + } else { + *result = extHandle->geomFactory->createMultiPoint(locations).release(); + } + } + + return simple; + }); + } + +/* + * IsSimpleOp } + ************************************************************************/ + char GEOSisRing_r(GEOSContextHandle_t extHandle, const Geometry* g) { diff --git a/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp new file mode 100644 index 000000000..f469fea99 --- /dev/null +++ b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp @@ -0,0 +1,83 @@ +#include +// geos +#include + +#include "capi_test_utils.h" + +namespace tut { +// +// Test Group +// + +struct test_geosissimplewithparams_data : public capitest::utility { + GEOSisSimpleParams* _params; + test_geosissimplewithparams_data() + { + _params = GEOSisSimpleParams_create(); + } + ~test_geosissimplewithparams_data() + { + GEOSisSimpleParams_destroy( _params ); + } +}; + +typedef test_group group; +typedef group::object object; + +group test_geosissimplewithparams("capi::GEOSisSimpleWithParams"); + +template<> +template<> +void object::test<1>() +{ + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); + ensure(nullptr != input_); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); + ensure_geometry_equals(result_, "POINT (0 0)"); +} + +template<> +template<> +void object::test<2>() +{ + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); + ensure(nullptr != input_); + GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_ENDPOINT); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 1); + ensure(result_ == nullptr); +} + +template<> +template<> +void object::test<3>() +{ + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); + ensure(nullptr != input_); + GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MULTIVALENT_ENDPOINT); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 1); + ensure(result_ == nullptr); +} + +template<> +template<> +void object::test<4>() +{ + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); + ensure(nullptr != input_); + GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MONOVALENT_ENDPOINT); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); + ensure_geometry_equals(result_, "POINT (0 0)"); +} + +template<> +template<> +void object::test<5>() +{ + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0,10 0),(2 -5,2 5),(4 -5, 4 5) )"); + ensure(nullptr != input_); + GEOSisSimpleParams_setFindAllLocations(_params, 1); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); + ensure_equals(GEOSGetNumGeometries(result_), 2); +} + +} // namespace tut ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + capi/geos_c.cpp | 35 +++++++++ capi/geos_c.h.in | 105 ++++++++++++++++++++++++- capi/geos_ts_c.cpp | 96 ++++++++++++++++++++++ tests/unit/capi/GEOSisSimpleWithParamsTest.cpp | 83 +++++++++++++++++++ 5 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 tests/unit/capi/GEOSisSimpleWithParamsTest.cpp hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 07:24:39 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 07:24:39 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 395d979c05ddcfd729ffe80b2b18e7873dbf8b29 Message-ID: <20261001142439.91ACD194188@trac.osgeo.org> 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 395d979c05ddcfd729ffe80b2b18e7873dbf8b29 (commit) from 7e7c2689257fa53ee3252c44264a776bc74c9c9f (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 395d979c05ddcfd729ffe80b2b18e7873dbf8b29 Author: Sandro Santilli Date: Thu Oct 1 16:24:18 2026 +0200 Use IllegalArgumentException when given invalid boundaryNodeRule diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index ae9d7c384..2635099fd 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -3721,7 +3721,7 @@ extern char GEOS_DLL GEOSisSimpleDetail( /** * Create a GEOSisSimpleParams to hold the desired parameters * to control the algorithm and behavior of the simplicity check process. -* \return a parameter object configured to find all intersection +* \return a parameter object configured to find all intersections * and use Mod2 boundary node rule * \see GEOSisSimpleWithParams * diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index 1f05db1ff..547e4570f 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1035,6 +1035,7 @@ extern "C" { using geos::operation::relate::RelateOp; using geos::geom::IntersectionMatrix; using geos::algorithm::BoundaryNodeRule; + using geos::util::IllegalArgumentException; return execute(extHandle, [&]() -> char* { std::unique_ptr im; @@ -1061,7 +1062,7 @@ extern "C" { default: std::ostringstream ss; ss << "Invalid boundary node rule " << bnr; - throw std::runtime_error(ss.str()); + throw IllegalArgumentException(ss.str()); } if(!im) { @@ -1633,6 +1634,7 @@ extern "C" { { using geos::algorithm::BoundaryNodeRule; using geos::operation::valid::IsSimpleOp; + using geos::util::IllegalArgumentException; return execute(extHandle, 2, [&]() { const auto inputGeom = convertToLineIfNeeded(extHandle, g1); @@ -1654,7 +1656,7 @@ extern "C" { default: std::ostringstream ss; ss << "Invalid boundary node rule " << params->boundaryNodeRule; - throw std::runtime_error(ss.str()); + throw IllegalArgumentException(ss.str()); } op->setFindAllLocations( params->findAllLocations ); ----------------------------------------------------------------------- Summary of changes: capi/geos_c.h.in | 2 +- capi/geos_ts_c.cpp | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 07:35:47 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 07:35:47 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. a8361685706776ba18266ac873d914d99f60ffad Message-ID: <20261001143548.2EFC11948E5@trac.osgeo.org> 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 a8361685706776ba18266ac873d914d99f60ffad (commit) from 395d979c05ddcfd729ffe80b2b18e7873dbf8b29 (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 a8361685706776ba18266ac873d914d99f60ffad Author: Sandro Santilli Date: Thu Oct 1 16:35:26 2026 +0200 Properly set SRID in GEOSisSimpleWithParams diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index 547e4570f..e35bc743e 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1671,6 +1671,7 @@ extern "C" { } else { *result = extHandle->geomFactory->createMultiPoint(locations).release(); } + (*result)->setSRID(inputGeom->getSRID()); } return simple; diff --git a/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp index f469fea99..307d53573 100644 --- a/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp +++ b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp @@ -30,6 +30,8 @@ template<> template<> void object::test<1>() { + set_test_name("MultiLineString containing two rings that touch at their endpoints, default params"); + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); ensure(nullptr != input_); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); @@ -40,6 +42,8 @@ template<> template<> void object::test<2>() { + set_test_name("MultiLineString containing two rings that touch at their endpoints, EndPoint boundary node rule"); + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); ensure(nullptr != input_); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_ENDPOINT); @@ -51,6 +55,8 @@ template<> template<> void object::test<3>() { + set_test_name("MultiLineString containing two rings that touch at their endpoints, MultiValent boundary node rule"); + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); ensure(nullptr != input_); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MULTIVALENT_ENDPOINT); @@ -62,6 +68,8 @@ template<> template<> void object::test<4>() { + set_test_name("MultiLineString containing two rings that touch at their endpoints, MonoValent boundary node rule"); + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); ensure(nullptr != input_); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MONOVALENT_ENDPOINT); @@ -73,6 +81,8 @@ template<> template<> void object::test<5>() { + set_test_name("MultiLineString with a line intersected by two other lines, finding all locations"); + input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0,10 0),(2 -5,2 5),(4 -5, 4 5) )"); ensure(nullptr != input_); GEOSisSimpleParams_setFindAllLocations(_params, 1); @@ -80,4 +90,32 @@ void object::test<5>() ensure_equals(GEOSGetNumGeometries(result_), 2); } +template<> +template<> +void object::test<6>() +{ + set_test_name("Non-simple line with a SRID"); + + input_ = GEOSGeomFromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); + GEOSSetSRID(input_, 3857); + ensure(nullptr != input_); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); + ensure_equals(GEOSGetNumGeometries(result_), 1); + ensure_equals(GEOSGetSRID(result_), 3857); +} + +template<> +template<> +void object::test<7>() +{ + set_test_name("Non-simple line with a SRID and finding all locations"); + + input_ = GEOSGeomFromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); + GEOSSetSRID(input_, 4326); + ensure(nullptr != input_); + GEOSisSimpleParams_setFindAllLocations(_params, 1); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); + ensure_equals(GEOSGetNumGeometries(result_), 3); + ensure_equals(GEOSGetSRID(result_), 4326); +} } // namespace tut ----------------------------------------------------------------------- Summary of changes: capi/geos_ts_c.cpp | 1 + tests/unit/capi/GEOSisSimpleWithParamsTest.cpp | 38 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 07:45:49 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 07:45:49 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 5885024d0f7f58085c8ca5260be58863c24410e5 Message-ID: <20261001144550.1E8F7193DD6@trac.osgeo.org> 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 5885024d0f7f58085c8ca5260be58863c24410e5 (commit) via c24beb890360ba4a3afb987f332f9a094cd856f0 (commit) from a8361685706776ba18266ac873d914d99f60ffad (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 5885024d0f7f58085c8ca5260be58863c24410e5 Author: Sandro Santilli Date: Thu Oct 1 16:45:27 2026 +0200 Allow passign null as locations to GEOSisSimpleWithParams diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 906fb311e..8121af536 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -3708,7 +3708,8 @@ extern char GEOS_DLL GEOSisSimple(const GEOSGeometry* g); * * \param g The geometry to test * \param findAllLocations Whether to return all self-intersection locations, or just one -* \param locations A pointer in which the location GEOSGeometry will be placed +* \param locations A pointer in which the location GEOSGeometry will be placed, if any. +* Pass null if you don't want locations. * \return 1 when simple, 0 when non-simple, 2 on exception * * \since 3.14 diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index e35bc743e..a21c00528 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1661,17 +1661,19 @@ extern "C" { op->setFindAllLocations( params->findAllLocations ); - *result = nullptr; bool simple = op->isSimple(); - if (!simple) { - auto locations = op->getNonSimpleLocations(); - if (locations.size() == 1 || ! params->findAllLocations ) { - *result = extHandle->geomFactory->createPoint(locations.front()).release(); - } else { - *result = extHandle->geomFactory->createMultiPoint(locations).release(); + if ( result ) { + *result = nullptr; + if (!simple) { + auto locations = op->getNonSimpleLocations(); + if (locations.size() == 1 || ! params->findAllLocations ) { + *result = extHandle->geomFactory->createPoint(locations.front()).release(); + } else { + *result = extHandle->geomFactory->createMultiPoint(locations).release(); + } + (*result)->setSRID(inputGeom->getSRID()); } - (*result)->setSRID(inputGeom->getSRID()); } return simple; diff --git a/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp index 307d53573..6bb1b718e 100644 --- a/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp +++ b/tests/unit/capi/GEOSisSimpleWithParamsTest.cpp @@ -32,8 +32,7 @@ void object::test<1>() { set_test_name("MultiLineString containing two rings that touch at their endpoints, default params"); - input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); - ensure(nullptr != input_); + input_ = fromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); ensure_geometry_equals(result_, "POINT (0 0)"); } @@ -44,8 +43,7 @@ void object::test<2>() { set_test_name("MultiLineString containing two rings that touch at their endpoints, EndPoint boundary node rule"); - input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); - ensure(nullptr != input_); + input_ = fromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_ENDPOINT); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 1); ensure(result_ == nullptr); @@ -57,8 +55,7 @@ void object::test<3>() { set_test_name("MultiLineString containing two rings that touch at their endpoints, MultiValent boundary node rule"); - input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); - ensure(nullptr != input_); + input_ = fromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MULTIVALENT_ENDPOINT); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 1); ensure(result_ == nullptr); @@ -70,8 +67,7 @@ void object::test<4>() { set_test_name("MultiLineString containing two rings that touch at their endpoints, MonoValent boundary node rule"); - input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); - ensure(nullptr != input_); + input_ = fromWKT("MULTILINESTRING( (0 0, -10 10,-10 -10, 0 0), (0 0,20 -10,20 10,0 0) )"); GEOSisSimpleParams_setBoundaryNodeRule(_params, GEOSRELATE_BNR_MONOVALENT_ENDPOINT); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); ensure_geometry_equals(result_, "POINT (0 0)"); @@ -83,8 +79,7 @@ void object::test<5>() { set_test_name("MultiLineString with a line intersected by two other lines, finding all locations"); - input_ = GEOSGeomFromWKT("MULTILINESTRING( (0 0,10 0),(2 -5,2 5),(4 -5, 4 5) )"); - ensure(nullptr != input_); + input_ = fromWKT("MULTILINESTRING( (0 0,10 0),(2 -5,2 5),(4 -5, 4 5) )"); GEOSisSimpleParams_setFindAllLocations(_params, 1); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); ensure_equals(GEOSGetNumGeometries(result_), 2); @@ -96,9 +91,8 @@ void object::test<6>() { set_test_name("Non-simple line with a SRID"); - input_ = GEOSGeomFromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); + input_ = fromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); GEOSSetSRID(input_, 3857); - ensure(nullptr != input_); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); ensure_equals(GEOSGetNumGeometries(result_), 1); ensure_equals(GEOSGetSRID(result_), 3857); @@ -110,12 +104,24 @@ void object::test<7>() { set_test_name("Non-simple line with a SRID and finding all locations"); - input_ = GEOSGeomFromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); + input_ = fromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); GEOSSetSRID(input_, 4326); - ensure(nullptr != input_); GEOSisSimpleParams_setFindAllLocations(_params, 1); ensure_equals((int)GEOSisSimpleWithParams(input_, _params, &result_), 0); ensure_equals(GEOSGetNumGeometries(result_), 3); ensure_equals(GEOSGetSRID(result_), 4326); } + +template<> +template<> +void object::test<8>() +{ + set_test_name("Passing NULL as locations for non-simple line"); + + input_ = fromWKT("LINESTRING(0 0,10 0,2 -5,2 5,6 -8)"); + GEOSSetSRID(input_, 4326); + GEOSisSimpleParams_setFindAllLocations(_params, 1); + ensure_equals((int)GEOSisSimpleWithParams(input_, _params, NULL), 0); +} + } // namespace tut commit c24beb890360ba4a3afb987f332f9a094cd856f0 Author: Sandro Santilli Date: Thu Oct 1 16:36:09 2026 +0200 Fix capitalization diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 2635099fd..906fb311e 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -1686,12 +1686,12 @@ extern char GEOS_DLL GEOSisSimple_r( GEOSContextHandle_t handle, const GEOSGeometry* g); -/** \see GEOSIsSimpleParams_create */ +/** \see GEOSisSimpleParams_create */ extern GEOSisSimpleParams GEOS_DLL * GEOSisSimpleParams_create_r( GEOSContextHandle_t handle); -/** \see GEOSIsSimpleParams_destroy */ +/** \see GEOSisSimpleParams_destroy */ extern void GEOS_DLL GEOSisSimpleParams_destroy_r( GEOSContextHandle_t handle, ----------------------------------------------------------------------- Summary of changes: capi/geos_c.h.in | 7 +++--- capi/geos_ts_c.cpp | 18 ++++++++------ tests/unit/capi/GEOSisSimpleWithParamsTest.cpp | 34 +++++++++++++++----------- 3 files changed, 34 insertions(+), 25 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 08:24:38 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 08:24:38 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 6f4340223234824350773dca530424e55a734e22 Message-ID: <20261001152438.406BE19622A@trac.osgeo.org> 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 6f4340223234824350773dca530424e55a734e22 (commit) from 5885024d0f7f58085c8ca5260be58863c24410e5 (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 6f4340223234824350773dca530424e55a734e22 Author: Sandro Santilli Date: Thu Oct 1 17:23:23 2026 +0200 Set SRID to GEOSisSimpleDetail output too, and allow null locations pointer diff --git a/NEWS.md b/NEWS.md index ccf7bf58a..6891059d3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ - Curve overlay robustness improvements (GH-1513, Dan Baston) - GeometrySplitter: Allow splitting a MultiSurface (GH-1520, Dan Baston) - GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston) + - GEOSisSimpleDetail returns locations with input SRID (Sandro Santilli) - New things: - Expose code revision: GEOS_REVISION, GEOSrevision, geos-config --revision diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index a21c00528..a69e944ea 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -1578,15 +1578,17 @@ extern "C" { geos::operation::valid::IsSimpleOp iso(inputGeom); iso.setFindAllLocations(returnAllPoints); - *result = nullptr; - bool simple = iso.isSimple(); - if (!simple) { - auto locations = iso.getNonSimpleLocations(); - if (locations.size() == 1 || !returnAllPoints) { - *result = extHandle->geomFactory->createPoint(locations.front()).release(); - } else { - *result = extHandle->geomFactory->createMultiPoint(locations).release(); + if ( result ) { + *result = nullptr; + if (!simple) { + auto locations = iso.getNonSimpleLocations(); + if (locations.size() == 1 || !returnAllPoints) { + *result = extHandle->geomFactory->createPoint(locations.front()).release(); + } else { + *result = extHandle->geomFactory->createMultiPoint(locations).release(); + } + (*result)->setSRID(inputGeom->getSRID()); } } ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + capi/geos_ts_c.cpp | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Oct 1 08:51:27 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 1 Oct 2026 08:51:27 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/all-actions-3c2ac12f02 created. 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 Message-ID: <20261001155127.E2EA0197113@trac.osgeo.org> 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, dependabot/github_actions/all-actions-3c2ac12f02 has been created at 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 (commit) - Log ----------------------------------------------------------------- commit 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Oct 1 15:51:08 2026 +0000 Bump the all-actions group with 3 updates Bumps the all-actions group with 3 updates: [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm), [github/codeql-action](https://github.com/github/codeql-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `vmactions/freebsd-vm` from 1.5.5 to 1.5.7 - [Release notes](https://github.com/vmactions/freebsd-vm/releases) - [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.5.5...v1.5.7) Updates `github/codeql-action` from 4.37.8 to 4.38.2 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.37.8...v4.38.2) Updates `docker/build-push-action` from 7.3.0 to 7.4.0 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v7.3.0...v7.4.0) --- updated-dependencies: - dependency-name: vmactions/freebsd-vm dependency-version: 1.5.7 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action dependency-version: 4.38.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions - dependency-name: docker/build-push-action dependency-version: 7.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions ... Signed-off-by: dependabot[bot] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d420e0d50..85a5079a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,7 +556,7 @@ jobs: restore-keys: freebsd - name: Build, test, install - uses: vmactions/freebsd-vm at v1.5.5 + uses: vmactions/freebsd-vm at v1.5.7 with: release: "15.0" envs: CCACHE_COMPRESS CCACHE_COMPRESSLEVEL CCACHE_MAXSIZE diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6905e122d..5c5d0ec71 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init at v4.37.8 + uses: github/codeql-action/init at v4.38.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,6 +63,6 @@ jobs: make -j$(nproc) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze at v4.37.8 + uses: github/codeql-action/analyze at v4.38.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index c1726e7d3..79e6d1c09 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -54,7 +54,7 @@ jobs: cp tools/ci/geosop/Dockerfile . - name: Build and Push - uses: docker/build-push-action at v7.3.0 + uses: docker/build-push-action at v7.4.0 with: context: . push: true ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Fri Oct 2 01:43:22 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 2 Oct 2026 01:43:22 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. cc9a3cc95d160c58a68ded58fb6b83935ca856fe Message-ID: <20261002084323.6BA841A412B@trac.osgeo.org> 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 cc9a3cc95d160c58a68ded58fb6b83935ca856fe (commit) from 6f4340223234824350773dca530424e55a734e22 (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 cc9a3cc95d160c58a68ded58fb6b83935ca856fe Author: Mohd Quamar Tyagi <104281681+Tyagiquamar at users.noreply.github.com> Date: Fri Oct 2 14:12:58 2026 +0530 geosop: report error and exit non-zero when input file cannot be opened (#1548) * geosop: report error and exit non-zero when input file cannot be opened When an input file path does not exist or cannot be opened, std::ifstream previously failed silently and returned an empty list of geometries, causing geosop to succeed with exit code 0. Check whether std::ifstream is open in readWKTFile and readWKBFile, throw an exception, caught at CLI boundary Fixes #1538 diff --git a/util/geosop/GeosOp.cpp b/util/geosop/GeosOp.cpp index 398085519..844505039 100644 --- a/util/geosop/GeosOp.cpp +++ b/util/geosop/GeosOp.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #if !defined(MISSING_FENV) #define HAVE_FENV @@ -181,8 +182,15 @@ int main(int argc, char** argv) { } } - GeosOp geosop(cmdArgs); - geosop.run(opArgs); + try { + GeosOp geosop(cmdArgs); + geosop.run(opArgs); + } + catch (const std::exception& ex) { + std::cerr << ex.what() << std::endl; + return 1; + } + return 0; } GeosOp::GeosOp(GeosOpArgs& arg) @@ -271,6 +279,9 @@ readWKTFile(std::string src, int limit, int offset) { return readWKTFile( std::cin, limit, offset ); } std::ifstream f( src ); + if (!f.is_open()) { + throw util::GEOSException("Cannot open file: " + src); + } auto geoms = readWKTFile( f, limit, offset ); f.close(); return geoms; @@ -299,6 +310,9 @@ readWKBFile(std::string src, int limit, int offset) { return readWKBFile( std::cin, limit, offset ); } std::ifstream f( src ); + if (!f.is_open()) { + throw util::GEOSException("Cannot open file: " + src); + } auto geoms = readWKBFile( f, limit, offset ); f.close(); return geoms; ----------------------------------------------------------------------- Summary of changes: util/geosop/GeosOp.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Oct 2 05:51:19 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 2 Oct 2026 05:51:19 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. e8d427e4f8d323b68a8f1f6d2c99e68522ec16d2 Message-ID: <20261002125120.9251919E00E@trac.osgeo.org> 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 e8d427e4f8d323b68a8f1f6d2c99e68522ec16d2 (commit) from cc9a3cc95d160c58a68ded58fb6b83935ca856fe (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 e8d427e4f8d323b68a8f1f6d2c99e68522ec16d2 Author: Daniel Baston Date: Fri Oct 2 08:50:38 2026 -0400 CircularArcIntersector: Avoid incorrectly classifying intersection as cocircular (#1549) 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 ----------------------------------------------------------------------- Summary of changes: src/algorithm/CircularArcIntersector.cpp | 12 ++++++------ tests/unit/algorithm/CircularArcIntersectorTest.cpp | 12 ++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Oct 2 05:53:47 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 2 Oct 2026 05:53:47 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 7312930ea53f1f3954ad3f455ef549576f69743e Message-ID: <20261002125348.C99B619DEE9@trac.osgeo.org> 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 7312930ea53f1f3954ad3f455ef549576f69743e (commit) from e8d427e4f8d323b68a8f1f6d2c99e68522ec16d2 (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 7312930ea53f1f3954ad3f455ef549576f69743e Author: Daniel Baston Date: Fri Oct 2 08:53:22 2026 -0400 CurveBuilder: Copy Z/M values at shared endpoints of mixed-dimension sequences (#1545) References https://github.com/libgeos/geos/issues/1541 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 lineCoords; - std::shared_ptr arcCoords; - std::vector> 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> m_curves; - std::unique_ptr m_pts{nullptr}; + // Declare type as noncopyable + CurveBuilder(const CurveBuilder& other) = delete; + CurveBuilder& operator=(const CurveBuilder& rhs) = delete; + +private: + std::vector, 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 #include #include +#include + +#include 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(prevPos).equals2D(src.getAt(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().equals2D(lastSeq.back())) { + 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 CurveBuilder::getGeometry() { - if (m_pts) { - if (m_isCurved) { - finishCurve(); - } else { + if (m_pts.empty()) { + return m_gfact.createLineString(std::make_unique(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> 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(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(0, m_hasZ, m_hasM), isCurved); } - if (!m_pts) { - m_pts = std::make_unique(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 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(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(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 (0 0, 1 1)")); + // Patch Z Linear <- Linear + builder.add(*reader_.read("LINESTRING Z (1 1 5, 2 2 4)")); + // Patch Z Linear -> Linear + builder.add(*reader_.read("LINESTRING (2 2, 5 0)")); + // Patch M Linear <- Curve + builder.add(*reader_.read("CIRCULARSTRING M (5 0 8, 6 1 7, 7 0 8)")); + // Patch M Curve -> Curve + builder.add(*reader_.read("CIRCULARSTRING (7 0, 8 -1, 9 0)")); + // Patch Z Curve <- Linear + builder.add(*reader_.read("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(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 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(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 ZM (0 0 100 200, 1 1 200 400, 2 0 300 600)")); + builder.add(*reader_.read("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(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 (0 0, 1 1, 2 0)")); + builder.add(*reader_.read("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(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 input{ + "LINESTRING Z (-5 0 10, 5 0 20)", + "CIRCULARSTRING (-5 0, 0 5, 5 0)" + }; + + std::vector 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: include/geos/algorithm/LineToCurveConverter.h | 11 -- include/geos/geom/util/CurveBuilder.h | 20 ++- include/geos/operation/polygonize/EdgeRing.h | 4 - src/geom/util/CurveBuilder.cpp | 195 +++++++++++++++------ src/operation/linemerge/EdgeString.cpp | 21 +-- src/operation/polygonize/EdgeRing.cpp | 37 +--- tests/unit/geom/util/CurveBuilderTest.cpp | 82 +++++++++ tests/unit/operation/polygonize/PolygonizeTest.cpp | 35 +++- 8 files changed, 263 insertions(+), 142 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Sun Oct 4 19:14:45 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 4 Oct 2026 19:14:45 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/all-actions-3c2ac12f02 deleted. 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 Message-ID: <20261005021445.907DC310F2@trac.osgeo.org> 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, dependabot/github_actions/all-actions-3c2ac12f02 has been deleted was 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 - Log ----------------------------------------------------------------- 89bb85c321dd286aa0e72f8bbc1eabe266fba2f4 Bump the all-actions group with 3 updates ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Sun Oct 4 19:14:45 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 4 Oct 2026 19:14:45 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. c6af5613f1b5da92205e3cd45057499b90bfa0b8 Message-ID: <20261005021445.BE753310F3@trac.osgeo.org> 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 c6af5613f1b5da92205e3cd45057499b90bfa0b8 (commit) from 7312930ea53f1f3954ad3f455ef549576f69743e (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 c6af5613f1b5da92205e3cd45057499b90bfa0b8 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon Oct 5 15:14:09 2026 +1300 Bump the all-actions group with 3 updates (#1550) - Updates `vmactions/freebsd-vm` from 1.5.5 to 1.5.7 - Updates `github/codeql-action` from 4.37.8 to 4.38.2 - Updates `docker/build-push-action` from 7.3.0 to 7.4.0 Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d420e0d50..85a5079a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,7 +556,7 @@ jobs: restore-keys: freebsd - name: Build, test, install - uses: vmactions/freebsd-vm at v1.5.5 + uses: vmactions/freebsd-vm at v1.5.7 with: release: "15.0" envs: CCACHE_COMPRESS CCACHE_COMPRESSLEVEL CCACHE_MAXSIZE diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6905e122d..5c5d0ec71 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init at v4.37.8 + uses: github/codeql-action/init at v4.38.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,6 +63,6 @@ jobs: make -j$(nproc) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze at v4.37.8 + uses: github/codeql-action/analyze at v4.38.2 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index c1726e7d3..79e6d1c09 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -54,7 +54,7 @@ jobs: cp tools/ci/geosop/Dockerfile . - name: Build and Push - uses: docker/build-push-action at v7.3.0 + uses: docker/build-push-action at v7.4.0 with: context: . push: true ----------------------------------------------------------------------- Summary of changes: .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 4 ++-- .github/workflows/container.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Mon Oct 5 09:35:41 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 5 Oct 2026 09:35:41 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 0d790d12b25a8e1d7310a55ec701e6bf54f73fb2 Message-ID: <20261005163544.A8DBF19E3A1@trac.osgeo.org> 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 0d790d12b25a8e1d7310a55ec701e6bf54f73fb2 (commit) from c6af5613f1b5da92205e3cd45057499b90bfa0b8 (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 0d790d12b25a8e1d7310a55ec701e6bf54f73fb2 Author: Jeroen Bloemscheer Date: Mon Oct 5 18:35:13 2026 +0200 Port JTS DirectedHausdorffDistance (#1514) Co-authored-by: Dan Baston diff --git a/NEWS.md b/NEWS.md index 6891059d3..a6510e8d4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,10 @@ ## Changes in 3.16.0 2027-xx-xx +- New things: + - Add DirectedHausdorffDistance and C API GEOSDirectedHausdorffDistance / + GEOSSymmetricHausdorffDistance (Martin Davis, Jeroen Bloemscheer, Dan Baston) + - Fixes/Improvements: - Curve overlay robustness improvements (GH-1513, Dan Baston) - GeometrySplitter: Allow splitting a MultiSurface (GH-1520, Dan Baston) diff --git a/Version.txt b/Version.txt index c1bb857ba..50e4eb502 100644 --- a/Version.txt +++ b/Version.txt @@ -15,9 +15,9 @@ GEOS_PATCH_WORD=dev # - Deleting interfaces / compatibility issues - bump CURRENT, others to zero # ( THIS MUST BE CAREFULLY AVOIDED ) # -CAPI_INTERFACE_CURRENT=23 +CAPI_INTERFACE_CURRENT=24 CAPI_INTERFACE_REVISION=0 -CAPI_INTERFACE_AGE=22 +CAPI_INTERFACE_AGE=23 # JTS Port JTS_PORT=1.20.0 diff --git a/capi/geos_c.cpp b/capi/geos_c.cpp index b2918e466..a5459ee92 100644 --- a/capi/geos_c.cpp +++ b/capi/geos_c.cpp @@ -409,6 +409,30 @@ extern "C" { return GEOSHausdorffDistanceDensifyWithPoints_r(handle, g1, g2, densifyFrac, dist, p1x, p1y, p2x, p2y); } + int + GEOSDirectedHausdorffDistance(const Geometry* g1, const Geometry* g2, double* dist) + { + return GEOSDirectedHausdorffDistance_r(handle, g1, g2, dist); + } + + int + GEOSDirectedHausdorffDistanceWithPoints(const Geometry* g1, const Geometry* g2, double* dist, double* p1x, double* p1y, double* p2x, double* p2y) + { + return GEOSDirectedHausdorffDistanceWithPoints_r(handle, g1, g2, dist, p1x, p1y, p2x, p2y); + } + + int + GEOSDirectedHausdorffDistanceWithin(const Geometry* g1, const Geometry* g2, double maxDistance) + { + return GEOSDirectedHausdorffDistanceWithin_r(handle, g1, g2, maxDistance); + } + + int + GEOSSymmetricHausdorffDistance(const Geometry* g1, const Geometry* g2, double* dist) + { + return GEOSSymmetricHausdorffDistance_r(handle, g1, g2, dist); + } + int GEOSFrechetDistance(const Geometry* g1, const Geometry* g2, double* dist) { diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 8121af536..00ec6b3ee 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -2184,6 +2184,36 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints_r( double *p1x, double *p1y, double *p2x, double *p2y); +/** \see GEOSDirectedHausdorffDistance */ +extern int GEOS_DLL GEOSDirectedHausdorffDistance_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist); + +/** \see GEOSDirectedHausdorffDistanceWithPoints */ +extern int GEOS_DLL GEOSDirectedHausdorffDistanceWithPoints_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist, + double *p1x, double *p1y, + double *p2x, double *p2y); + +/** \see GEOSDirectedHausdorffDistanceWithin */ +extern int GEOS_DLL GEOSDirectedHausdorffDistanceWithin_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double maxDistance); + +/** \see GEOSSymmetricHausdorffDistance */ +extern int GEOS_DLL GEOSSymmetricHausdorffDistance_r( + GEOSContextHandle_t handle, + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist); + /** \see GEOSFrechetDistance */ extern int GEOS_DLL GEOSFrechetDistance_r( GEOSContextHandle_t handle, @@ -4223,6 +4253,92 @@ extern int GEOS_DLL GEOSHausdorffDistanceDensifyWithPoints( double* p1x, double* p1y, double* p2x, double* p2y); +/** +* Calculate the directed Hausdorff distance from g1 to g2, +* the largest distance from any point of g1 to g2. +* Empty inputs write NaN. +* +* Approximated using a tolerance of the query envelope diameter / 1e4. +* +* @INPUT_CURVES_CONVERTED_TO_LINES@ +* +* \param[in] g1 Query geometry +* \param[in] g2 Target geometry +* \param[out] dist Pointer to be filled in with distance result +* \return 1 on success, 0 on exception. +* \since 3.16 +*/ +extern int GEOS_DLL GEOSDirectedHausdorffDistance( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist); + +/** +* Calculate the directed Hausdorff distance from g1 to g2, +* the largest distance from any point of g1 to g2, +* and the coordinates of a point pair that realizes the distance +* (a point of g1 and the nearest point of g2). +* Empty inputs write NaN. +* +* Approximated using a tolerance of the query envelope diameter / 1e4. +* +* @INPUT_CURVES_CONVERTED_TO_LINES@ +* +* \param[in] g1 Query geometry +* \param[in] g2 Target geometry +* \param[out] dist Pointer to be filled in with distance result +* \param[out] p1x X coordinate of point on g1 +* \param[out] p1y Y coordinate of point on g1 +* \param[out] p2x X coordinate of point on g2 +* \param[out] p2y Y coordinate of point on g2 +* \return 1 on success, 0 on exception. +* \since 3.16 +*/ +extern int GEOS_DLL GEOSDirectedHausdorffDistanceWithPoints( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist, + double *p1x, double *p1y, + double *p2x, double *p2y); + +/** +* Test whether every point of g1 lies within maxDistance of g2. +* Empty inputs return 0. +* Uses the same automatic approximation tolerance as GEOSDirectedHausdorffDistance. +* +* @INPUT_CURVES_CONVERTED_TO_LINES@ +* +* \param[in] g1 Query geometry +* \param[in] g2 Target geometry +* \param[in] maxDistance the distance limit +* \return 1 if fully within, 0 if not, 2 on exception. +* \since 3.16 +*/ +extern int GEOS_DLL GEOSDirectedHausdorffDistanceWithin( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double maxDistance); + +/** +* Calculate the Hausdorff distance between g1 and g2, +* the larger of the two directed Hausdorff distances. +* Empty inputs write NaN. +* +* Approximated using a tolerance of the query envelope diameter / 1e4. +* +* @INPUT_CURVES_CONVERTED_TO_LINES@ +* +* \param[in] g1 Input geometry +* \param[in] g2 Input geometry +* \param[out] dist Pointer to be filled in with distance result +* \return 1 on success, 0 on exception. +* \since 3.16 +*/ +extern int GEOS_DLL GEOSSymmetricHausdorffDistance( + const GEOSGeometry *g1, + const GEOSGeometry *g2, + double *dist); + /** * Calculate the * [Frechet distance](https://en.wikipedia.org/wiki/Fr%C3%A9chet_distance) diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index a69e944ea..56218cb3b 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -135,6 +136,7 @@ #include #include #include +#include #include #include #include @@ -232,6 +234,7 @@ using geos::io::GeoJSONWriter; using geos::algorithm::distance::DiscreteFrechetDistance; using geos::algorithm::distance::DiscreteHausdorffDistance; +using geos::algorithm::distance::DirectedHausdorffDistance; using geos::algorithm::hull::ConcaveHull; using geos::algorithm::hull::ConcaveHullOfPolygons; @@ -1287,6 +1290,66 @@ extern "C" { }); } + int + GEOSDirectedHausdorffDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist) + { + return execute(extHandle, 0, [&]() { + const auto input1 = convertToLineIfNeeded(extHandle, g1); + const auto input2 = convertToLineIfNeeded(extHandle, g2); + + *dist = DirectedHausdorffDistance::distance(*input1, *input2); + return 1; + }); + } + + int + GEOSDirectedHausdorffDistanceWithPoints_r(GEOSContextHandle_t extHandle, + const Geometry* g1, const Geometry* g2, + double* dist, double* p1x, double* p1y, + double* p2x, double* p2y) + { + return execute(extHandle, 0, [&]() { + const auto input1 = convertToLineIfNeeded(extHandle, g1); + const auto input2 = convertToLineIfNeeded(extHandle, g2); + + auto pts = DirectedHausdorffDistance::distancePoints(*input1, *input2); + if (!pts) { + *dist = std::numeric_limits::quiet_NaN(); + *p1x = *p1y = *p2x = *p2y = std::numeric_limits::quiet_NaN(); + return 1; + } + *dist = (*pts)[0].distance((*pts)[1]); + *p1x = (*pts)[0].x; + *p1y = (*pts)[0].y; + *p2x = (*pts)[1].x; + *p2y = (*pts)[1].y; + return 1; + }); + } + + int + GEOSDirectedHausdorffDistanceWithin_r(GEOSContextHandle_t extHandle, + const Geometry* g1, const Geometry* g2, double maxDistance) + { + return execute(extHandle, 2, [&]() { + const auto input1 = convertToLineIfNeeded(extHandle, g1); + const auto input2 = convertToLineIfNeeded(extHandle, g2); + return DirectedHausdorffDistance::isFullyWithinDistance(*input1, *input2, maxDistance) ? 1 : 0; + }); + } + + int + GEOSSymmetricHausdorffDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist) + { + return execute(extHandle, 0, [&]() { + const auto input1 = convertToLineIfNeeded(extHandle, g1); + const auto input2 = convertToLineIfNeeded(extHandle, g2); + + *dist = DirectedHausdorffDistance::hausdorffDistance(*input1, *input2); + return 1; + }); + } + int GEOSFrechetDistance_r(GEOSContextHandle_t extHandle, const Geometry* g1, const Geometry* g2, double* dist) { diff --git a/include/geos/algorithm/distance/DirectedHausdorffDistance.h b/include/geos/algorithm/distance/DirectedHausdorffDistance.h new file mode 100644 index 000000000..8ccbb4a22 --- /dev/null +++ b/include/geos/algorithm/distance/DirectedHausdorffDistance.h @@ -0,0 +1,300 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 Martin Davis + * Copyright (C) 2026 Jeroen Bloemscheer + * + * 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. + * + ********************************************************************** + * + * Last port: algorithm/distance/DirectedHausdorffDistance.java (aff11591) + * + **********************************************************************/ + +#pragma once + +#include +#include + +#include +#include +#include + +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class +#endif + +namespace geos { +namespace geom { +class Envelope; +class Geometry; +} +} + +namespace geos { +namespace algorithm { +namespace distance { + +/** + * Computes the directed Hausdorff distance from one geometry to another. + * The directed Hausdorff distance is the maximum distance any point + * on a query geometry A can be from a target geometry B. + * Equivalently, every point in the query geometry is within that distance + * of the target geometry. + * The class can compute a pair of points at which the distance is attained: + * [ farthest A point, nearest B point ]. + * + * The directed Hausdorff distance (DHD) is defined as: + * + * DHD(A,B) = max a ? A (max b ? B (distance(a, b) ) + * + * DHD is asymmetric: DHD(A,B) may not be equal to DHD(B,A). + * Hence it is not a distance metric. + * The Hausdorff distance is a symmetric distance metric defined as: + * + * HD(A,B) = max(DHD(A,B), DHD(B,A)) + * + * This can be computed via the + * hausdorffDistancePoints(Geometry, Geometry) function. + * + * Points, lines and polygons are supported as input. + * If the query geometry is polygonal, + * the point at maximum distance may occur in the interior of a query polygon. + * For a polygonal target geometry the point always lies on the boundary. + * + * The directed Hausdorff distance can be used to test + * whether a geometry lies fully within a given distance of another one. + * The isFullyWithinDistance(Geometry, double) function + * is provided to execute this test efficiently. + * It implements heuristic checks and short-circuiting to improve performance. + * + * The class can be used in prepared mode. + * Creating an instance on a target geometry caches indexes for that geometry. + * Then farthestPoints(Geometry) + * or isFullyWithinDistance(Geometry, double) + * can be called efficiently for multiple query geometries. + * + * If the Hausdorff distance is attained at a non-vertex of the query geometry, + * the location must be approximated. + * The algorithm uses a distance tolerance to control the approximation accuracy. + * The tolerance is automatically determined to balance between accuracy and performance. + * If more accuracy is desired some function signatures are provided + * which allow specifying a distance tolerance. + * + * This algorithm is easier to use, more accurate, + * and much faster than DiscreteHausdorffDistance. + * + * \author Martin Davis + */ +class GEOS_DLL DirectedHausdorffDistance { +public: + /// JTS returns Coordinate[]; std::array is the C++ equivalent pair. + using PointPair = std::array; + + /** + * Computes the directed Hausdorff distance + * of a query geometry A from a target one B. + * + * @param a the query geometry + * @param b the target geometry + * @return the directed Hausdorff distance, + * or NaN if an input is empty + */ + static double distance(const geom::Geometry& a, const geom::Geometry& b); + + /** + * Computes the directed Hausdorff distance + * of a query geometry A from a target one B, + * up to a given distance accuracy. + * + * @param a the query geometry + * @param b the target geometry + * @param tolerance the accuracy distance tolerance + * @return the directed Hausdorff distance, + * or NaN if an input is empty + */ + static double distance(const geom::Geometry& a, const geom::Geometry& b, + double tolerance); + + /** + * Computes a line containing a pair of points which attain the directed Hausdorff distance + * of a query geometry A from a target one B. + * + * @param a the query geometry + * @param b the target geometry + * @return a pair of points [ptA, ptB] demonstrating the distance, + * or empty if an input is empty + */ + static std::optional distancePoints( + const geom::Geometry& a, const geom::Geometry& b); + + /** + * Computes a line containing a pair of points which attain the directed Hausdorff distance + * of a query geometry A from a target one B, up to a given distance accuracy. + * + * @param a the query geometry + * @param b the target geometry + * @param tolerance the accuracy distance tolerance + * @return a pair of points [ptA, ptB] demonstrating the distance, + * or empty if an input is empty + */ + static std::optional distancePoints( + const geom::Geometry& a, const geom::Geometry& b, double tolerance); + + /** + * Computes the symmetric Hausdorff distance between two geometries. + * This is the maximum of the two directed Hausdorff distances. + * + * @param a a geometry + * @param b a geometry + * @return the Hausdorff distance, or NaN if an input is empty + */ + static double hausdorffDistance(const geom::Geometry& a, const geom::Geometry& b); + + /** + * Computes a pair of points which attain the symmetric Hausdorff distance + * between two geometries. + * This is the maximum of the two directed Hausdorff distances. + * + * @param a a geometry + * @param b a geometry + * @return a pair of points [ptA, ptB] demonstrating the Hausdorff distance, + * or empty if an input is empty + */ + static std::optional hausdorffDistancePoints( + const geom::Geometry& a, const geom::Geometry& b); + + /** + * Computes whether a query geometry lies fully within a given distance of a target geometry. + * Equivalently, detects whether any point of the query geometry is farther + * from the target than the specified distance. + * This is the case if DHD(A, B) > maxDistance. + * + * @param a the query geometry + * @param b the target geometry + * @param maxDistance the distance limit + * @return true if the query geometry lies fully within the distance of the target + */ + static bool isFullyWithinDistance( + const geom::Geometry& a, const geom::Geometry& b, double maxDistance); + + /** + * Computes whether a query geometry lies fully within a given distance of a target geometry, + * up to a given distance accuracy. + * Equivalently, detects whether any point of the query geometry is farther + * from the target than the specified distance. + * This is the case if DHD(A, B) > maxDistance. + * + * @param a the query geometry + * @param b the target geometry + * @param maxDistance the distance limit + * @param tolerance the accuracy distance tolerance + * @return true if the query geometry lies fully within the distance of the target + */ + static bool isFullyWithinDistance( + const geom::Geometry& a, const geom::Geometry& b, + double maxDistance, double tolerance); + + /** + * Create a new instance for a target geometry. + * + * @param geom the geometry to compute the distance from + */ + explicit DirectedHausdorffDistance(const geom::Geometry& geom); + + DirectedHausdorffDistance(const DirectedHausdorffDistance&) = delete; + DirectedHausdorffDistance& operator=(const DirectedHausdorffDistance&) = delete; + + ~DirectedHausdorffDistance(); + + /** + * Computes a pair of points which attain the directed Hausdorff distance + * of a query geometry A from the target B. + * If either geometry is empty the result is empty. + * + * @param geom the query geometry + * @return a pair of points [ptA, ptB] attaining the distance, + * or empty if an input is empty + */ + std::optional farthestPoints(const geom::Geometry& geom) const; + + /** + * Computes a pair of points which attain the directed Hausdorff distance + * of a query geometry A from the target B, + * up to a given distance accuracy. + * If either geometry is empty the result is empty. + * + * @param geom the query geometry + * @param tolerance the approximation distance tolerance + * @return a pair of points [ptA, ptB] attaining the distance, + * or empty if an input is empty + */ + std::optional farthestPoints(const geom::Geometry& geom, double tolerance) const; + + /** + * Tests whether a query geometry lies fully within a given distance of the target geometry. + * Equivalently, detects whether any point of the query geometry is farther + * from the target than the specified distance. + * This is the case if DHD(A, B) > maxDistance. + * + * @param geom the query geometry + * @param maxDistance the distance limit + * @return true if the query geometry lies fully within the distance of the target + */ + bool isFullyWithinDistance(const geom::Geometry& geom, double maxDistance) const; + + /** + * Tests whether a query geometry lies fully within a given distance of the target geometry, + * up to a given distance accuracy. + * Equivalently, detects whether any point of the query geometry is farther + * from the target than the specified distance. + * This is the case if DHD(A, B) > maxDistance. + * + * @param geom the query geometry + * @param maxDistance the distance limit + * @param tolerance the accuracy distance tolerance + * @return true if the query geometry lies fully within the distance of the target + */ + bool isFullyWithinDistance( + const geom::Geometry& geom, double maxDistance, double tolerance) const; + +private: + class TargetDistance; + friend class DHDSegment; + + static double pairDistance(const std::optional& pts); + static PointPair pair(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1); + static double computeTolerance(const geom::Geometry& geom); + static bool isBeyond( + const geom::Envelope& envA, const geom::Envelope& envB, double maxDistance); + static bool isValidLimit(double limit); + static bool isBeyondLimit(double maxDist, double maxDistanceLimit); + static bool isWithinLimit(double maxDist, double maxDistanceLimit); + + std::optional computeDistancePoints( + const geom::Geometry& geom, double tolerance, double maxDistanceLimit) const; + std::optional computeForPoints( + const geom::Geometry& geom, double maxDistanceLimit) const; + std::optional computeForEdges( + const geom::Geometry& geom, double tolerance, double maxDistanceLimit) const; + std::optional computeForAreaInterior( + const geom::Geometry& geom, double tolerance) const; + + const geom::Geometry& target; + std::unique_ptr targetDistance; +}; + +} // namespace distance +} // namespace algorithm +} // namespace geos + +#ifdef _MSC_VER +#pragma warning(pop) +#endif diff --git a/include/geos/operation/distance/CoordinateSequenceLocation.h b/include/geos/operation/distance/CoordinateSequenceLocation.h new file mode 100644 index 000000000..8686961ac --- /dev/null +++ b/include/geos/operation/distance/CoordinateSequenceLocation.h @@ -0,0 +1,76 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 Martin Davis + * + * 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. + * + ********************************************************************** + * + * Last port: operation/distance/CoordinateSequenceLocation.java (aff11591) + * + **********************************************************************/ + +#pragma once + +#include +#include + +#include + +namespace geos { +namespace geom { +class CoordinateSequence; +} +} + +namespace geos { +namespace operation { +namespace distance { + +/** + * A location on a FacetSequence. + * + * Location indexes are always the index of a sequence segment. + * Thus they are always less than the number of vertices + * in the sequence. The endpoint in a sequence + * has the index of the final segment in the sequence. + * If the sequence is a ring, the index of the final endpoint is + * normalized to 0. + * + * \author Martin Davis + */ +class GEOS_DLL CoordinateSequenceLocation { +public: + + CoordinateSequenceLocation(const geom::CoordinateSequence* seq, + std::size_t index, + const geom::CoordinateXY& pt); + + const geom::CoordinateXY& getCoordinate() const; + + std::size_t getIndex() const; + + bool isSameSegment(const CoordinateSequenceLocation& f) const; + + const geom::CoordinateXY& getEndPoint(int i) const; + + std::size_t normalize(std::size_t index) const; + +private: + + const geom::CoordinateSequence* seq; + std::size_t index; + geom::CoordinateXY pt; + + bool isNext(std::size_t index, std::size_t index1) const; +}; + +} // namespace distance +} // namespace operation +} // namespace geos diff --git a/include/geos/operation/distance/FacetSequence.h b/include/geos/operation/distance/FacetSequence.h index 61f9fd65b..2f09acd46 100644 --- a/include/geos/operation/distance/FacetSequence.h +++ b/include/geos/operation/distance/FacetSequence.h @@ -12,7 +12,7 @@ * ********************************************************************** * - * Last port: operation/distance/FacetSequence.java (f6187ee2 JTS-1.14) + * Last port: operation/distance/FacetSequence.java (aff11591) * **********************************************************************/ @@ -20,6 +20,9 @@ #include #include +#include + +#include namespace geos { namespace geom { @@ -61,6 +64,10 @@ private: const geom::Coordinate& q0, const geom::Coordinate &q1, std::vector *locs) const; + CoordinateSequenceLocation nearestLocationOnLine(const geom::CoordinateXY& pt) const; + + static std::size_t normalize(const geom::CoordinateSequence& pts, std::size_t index); + void computeEnvelope(); public: @@ -82,13 +89,21 @@ public: * and another sequence. * The locations are presented in the same order as the input sequences. * - * @return a pair of {@link Coordinate}s for the nearest points + * @return a pair of Coordinates for the nearest points */ std::vector nearestLocations(const FacetSequence& facetSeq) const; + /** + * Computes the location of the nearest point of this sequence + * to a query point. + * + * @param p the query point + * @return the nearest location on this sequence + */ + CoordinateSequenceLocation nearestLocation(const geom::CoordinateXY& p) const; + }; } } } - diff --git a/include/geos/operation/distance/IndexedFacetDistance.h b/include/geos/operation/distance/IndexedFacetDistance.h index 6739efbf0..46e23c034 100644 --- a/include/geos/operation/distance/IndexedFacetDistance.h +++ b/include/geos/operation/distance/IndexedFacetDistance.h @@ -12,12 +12,13 @@ * ********************************************************************** * - * Last port: operation/distance/IndexedFacetDistance.java (f6187ee2 JTS-1.14) + * Last port: operation/distance/IndexedFacetDistance.java (aff11591) * **********************************************************************/ #pragma once +#include #include namespace geos { @@ -102,6 +103,21 @@ public: /// \return the nearest points std::unique_ptr nearestPoints(const geom::Geometry* g) const; + /** + * Computes the nearest point on the target geometry + * to a point. + * + * @param p the point coordinate + * @return the nearest point on the target geometry + */ + geom::CoordinateXY nearestPoint(const geom::CoordinateXY& p) const; + + double distance(const geom::CoordinateXY& p) const; + + double distance(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1) const; + + CoordinateSequenceLocation nearestLocation(const geom::CoordinateXY& p) const; + private: struct FacetDistance { diff --git a/src/algorithm/distance/DirectedHausdorffDistance.cpp b/src/algorithm/distance/DirectedHausdorffDistance.cpp new file mode 100644 index 000000000..a5a4aa6ab --- /dev/null +++ b/src/algorithm/distance/DirectedHausdorffDistance.cpp @@ -0,0 +1,659 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 Martin Davis + * Copyright (C) 2026 Jeroen Bloemscheer + * + * 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. + * + ********************************************************************** + * + * Last port: algorithm/distance/DirectedHausdorffDistance.java (aff11591) + * + **********************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using geos::algorithm::construct::IndexedPointInPolygonsLocator; +using geos::algorithm::construct::LargestEmptyCircle; +using geos::geom::CoordinateXY; +using geos::geom::Dimension; +using geos::geom::Envelope; +using geos::geom::Geometry; +using geos::geom::GeometryComponentFilter; +using geos::geom::LineString; +using geos::geom::Location; +using geos::geom::Point; +using geos::operation::distance::IndexedFacetDistance; + +namespace geos { +namespace algorithm { +namespace distance { + +namespace { + +constexpr double EMPTY_DISTANCE = std::numeric_limits::quiet_NaN(); +constexpr double AUTO_TOLERANCE_FACTOR = 1.0e4; +constexpr double AREA_INTERIOR_TOLERANCE_FACTOR = 20; +constexpr double FULLY_WITHIN_TOLERANCE_FACTOR = 10 * AUTO_TOLERANCE_FACTOR; + +} // namespace + +class DirectedHausdorffDistance::TargetDistance { +public: + explicit TargetDistance(const Geometry& geom) + : distanceToFacets(&geom) + , isArea(geom.getDimension() >= Dimension::A) + { + if (isArea) { + ptInArea = std::make_unique(geom); + } + } + + CoordinateXY nearestFacetPoint(const CoordinateXY& p) const + { + CoordinateXY np = distanceToFacets.nearestPoint(p); + return np; + } + + CoordinateXY nearestPoint(const CoordinateXY& p) const + { + if (ptInArea) { + if (ptInArea->locate(&p) != Location::EXTERIOR) { + return p; + } + } + return distanceToFacets.nearestPoint(p); + } + + bool isInterior(const CoordinateXY& p) const + { + if (!isArea) { + return false; + } + return ptInArea->locate(&p) == Location::INTERIOR; + } + + bool isInterior(const CoordinateXY& p0, const CoordinateXY& p1) const + { + if (!isArea) { + return false; + } + double segDist = distanceToFacets.distance(p0, p1); + if (segDist == 0.0) { + return false; + } + return isInterior(p0); + } + + /// JTS isSameOrCollinear: both endpoints project onto the same target segment. + bool isSameOrCollinear(const CoordinateXY& p0, const CoordinateXY& p1) const + { + const auto f0 = distanceToFacets.nearestLocation(p0); + const auto f1 = distanceToFacets.nearestLocation(p1); + return f0.isSameSegment(f1); + } + +private: + IndexedFacetDistance distanceToFacets; + bool isArea; + std::unique_ptr ptInArea; +}; + +class DHDSegment { +public: + static DHDSegment create(const CoordinateXY& p0, const CoordinateXY& p1, + DirectedHausdorffDistance::TargetDistance& dist) + { + DHDSegment seg(p0, p1); + seg.init(dist); + return seg; + } + + static DHDSegment create(const DHDSegment& prevSeg, const CoordinateXY& p1, + DirectedHausdorffDistance::TargetDistance& dist) + { + DHDSegment seg(prevSeg.p1, p1); + seg.init(prevSeg.nearPt1, dist); + return seg; + } + + CoordinateXY getEndpoint(int index) const + { + return index == 0 ? p0 : p1; + } + + double getLength() const + { + return p0.distance(p1); + } + + double getMaxDistance() const + { + return maxDistance; + } + + double getMaxDistanceBound() const + { + return maxDistanceBound; + } + + DirectedHausdorffDistance::PointPair getMaxDistPts() const + { + double dist0 = p0.distance(nearPt0); + double dist1 = p1.distance(nearPt1); + if (dist0 > dist1) { + return DirectedHausdorffDistance::PointPair{p0, nearPt0}; + } + return DirectedHausdorffDistance::PointPair{p1, nearPt1}; + } + + std::array bisect(DirectedHausdorffDistance::TargetDistance& dist) const + { + CoordinateXY mid((p0.x + p1.x) / 2.0, (p0.y + p1.y) / 2.0); + CoordinateXY nearPtMid = dist.nearestPoint(mid); + return { + DHDSegment(p0, nearPt0, mid, nearPtMid), + DHDSegment(mid, nearPtMid, p1, nearPt1) + }; + } + + bool operator<(const DHDSegment& other) const + { + // priority_queue is a max-heap: larger bound first + return maxDistanceBound < other.maxDistanceBound; + } + + CoordinateXY p0; + CoordinateXY nearPt0; + CoordinateXY p1; + CoordinateXY nearPt1; + +private: + DHDSegment(const CoordinateXY& p0_, const CoordinateXY& p1_) + : p0(p0_) + , p1(p1_) + , maxDistanceBound(-std::numeric_limits::infinity()) + , maxDistance(0.0) + {} + + DHDSegment(const CoordinateXY& p0_, const CoordinateXY& nearPt0_, + const CoordinateXY& p1_, const CoordinateXY& nearPt1_) + : p0(p0_) + , nearPt0(nearPt0_) + , p1(p1_) + , nearPt1(nearPt1_) + , maxDistanceBound(-std::numeric_limits::infinity()) + , maxDistance(0.0) + { + computeMaxDistances(); + } + + void init(DirectedHausdorffDistance::TargetDistance& dist) + { + nearPt0 = dist.nearestPoint(p0); + nearPt1 = dist.nearestPoint(p1); + computeMaxDistances(); + } + + void init(const CoordinateXY& nearest0, DirectedHausdorffDistance::TargetDistance& dist) + { + nearPt0 = nearest0; + nearPt1 = dist.nearestPoint(p1); + computeMaxDistances(); + } + + void computeMaxDistances() + { + double dist0 = p0.distance(nearPt0); + double dist1 = p1.distance(nearPt1); + maxDistance = std::max(dist0, dist1); + maxDistanceBound = maxDistance + getLength() / 2.0; + } + + double maxDistanceBound; + double maxDistance; +}; + +/* static */ +double +DirectedHausdorffDistance::pairDistance(const std::optional& pts) +{ + if (!pts) { + return EMPTY_DISTANCE; + } + return (*pts)[0].distance((*pts)[1]); +} + +/* static */ +DirectedHausdorffDistance::PointPair +DirectedHausdorffDistance::pair(const CoordinateXY& p0, const CoordinateXY& p1) +{ + return PointPair{p0, p1}; +} + +/* static */ +double +DirectedHausdorffDistance::computeTolerance(const Geometry& geom) +{ + return geom.getEnvelopeInternal()->getDiameter() / AUTO_TOLERANCE_FACTOR; +} + +/* static */ +bool +DirectedHausdorffDistance::isBeyond( + const Envelope& envA, const Envelope& envB, double maxDistance) +{ + if (envA.isNull() || envB.isNull()) { + return false; + } + return envA.getMinX() < envB.getMinX() - maxDistance + || envA.getMinY() < envB.getMinY() - maxDistance + || envA.getMaxX() > envB.getMaxX() + maxDistance + || envA.getMaxY() > envB.getMaxY() + maxDistance; +} + +/* static */ +bool +DirectedHausdorffDistance::isValidLimit(double limit) +{ + return limit >= 0.0; +} + +/* static */ +bool +DirectedHausdorffDistance::isBeyondLimit(double maxDist, double maxDistanceLimit) +{ + return maxDistanceLimit >= 0 && maxDist > maxDistanceLimit; +} + +/* static */ +bool +DirectedHausdorffDistance::isWithinLimit(double maxDist, double maxDistanceLimit) +{ + return maxDistanceLimit >= 0 && maxDist <= maxDistanceLimit; +} + +/* static */ +double +DirectedHausdorffDistance::distance(const Geometry& a, const Geometry& b) +{ + DirectedHausdorffDistance hd(b); + return pairDistance(hd.farthestPoints(a)); +} + +/* static */ +double +DirectedHausdorffDistance::distance(const Geometry& a, const Geometry& b, double tolerance) +{ + DirectedHausdorffDistance hd(b); + return pairDistance(hd.farthestPoints(a, tolerance)); +} + +/* static */ +std::optional +DirectedHausdorffDistance::distancePoints(const Geometry& a, const Geometry& b) +{ + DirectedHausdorffDistance dhd(b); + return dhd.farthestPoints(a); +} + +/* static */ +std::optional +DirectedHausdorffDistance::distancePoints( + const Geometry& a, const Geometry& b, double tolerance) +{ + DirectedHausdorffDistance dhd(b); + return dhd.farthestPoints(a, tolerance); +} + +/* static */ +std::optional +DirectedHausdorffDistance::hausdorffDistancePoints(const Geometry& a, const Geometry& b) +{ + DirectedHausdorffDistance hdAB(b); + auto ptsAB = hdAB.farthestPoints(a); + DirectedHausdorffDistance hdBA(a); + auto ptsBA = hdBA.farthestPoints(b); + + if (!ptsAB) { + return ptsBA ? std::optional(pair((*ptsBA)[1], (*ptsBA)[0])) : std::nullopt; + } + if (!ptsBA) { + return ptsAB; + } + if (pairDistance(ptsBA) > pairDistance(ptsAB)) { + return pair((*ptsBA)[1], (*ptsBA)[0]); + } + return ptsAB; +} + +/* static */ +double +DirectedHausdorffDistance::hausdorffDistance(const Geometry& a, const Geometry& b) +{ + return pairDistance(hausdorffDistancePoints(a, b)); +} + +/* static */ +bool +DirectedHausdorffDistance::isFullyWithinDistance( + const Geometry& a, const Geometry& b, double maxDistance) +{ + DirectedHausdorffDistance hd(b); + return hd.isFullyWithinDistance(a, maxDistance); +} + +/* static */ +bool +DirectedHausdorffDistance::isFullyWithinDistance( + const Geometry& a, const Geometry& b, double maxDistance, double tolerance) +{ + DirectedHausdorffDistance hd(b); + return hd.isFullyWithinDistance(a, maxDistance, tolerance); +} + +DirectedHausdorffDistance::DirectedHausdorffDistance(const Geometry& geom) + : target(geom) + , targetDistance(std::make_unique(geom)) +{} + +DirectedHausdorffDistance::~DirectedHausdorffDistance() = default; + +bool +DirectedHausdorffDistance::isFullyWithinDistance( + const Geometry& geom, double maxDistance) const +{ + double tolerance = maxDistance / FULLY_WITHIN_TOLERANCE_FACTOR; + return isFullyWithinDistance(geom, maxDistance, tolerance); +} + +bool +DirectedHausdorffDistance::isFullyWithinDistance( + const Geometry& geom, double maxDistance, double tolerance) const +{ + if (geom.isEmpty() || target.isEmpty()) { + return false; + } + if (isBeyond(*geom.getEnvelopeInternal(), *target.getEnvelopeInternal(), maxDistance)) { + return false; + } + auto maxDistCoords = computeDistancePoints(geom, tolerance, maxDistance); + if (!maxDistCoords) { + return false; + } + return pairDistance(maxDistCoords) <= maxDistance; +} + +std::optional +DirectedHausdorffDistance::farthestPoints(const Geometry& geom) const +{ + return farthestPoints(geom, computeTolerance(geom)); +} + +std::optional +DirectedHausdorffDistance::farthestPoints(const Geometry& geom, double tolerance) const +{ + return computeDistancePoints(geom, tolerance, -1.0); +} + +std::optional +DirectedHausdorffDistance::computeDistancePoints( + const Geometry& geom, double tolerance, double maxDistanceLimit) const +{ + if (tolerance < 0.0) { + throw util::IllegalArgumentException("Tolerance must be non-negative"); + } + if (geom.isEmpty() || target.isEmpty()) { + return std::nullopt; + } + if (geom.getDimension() == Dimension::P) { + return computeForPoints(geom, maxDistanceLimit); + } + + auto maxDistPtsEdge = computeForEdges(geom, tolerance, maxDistanceLimit); + if (isBeyondLimit(pairDistance(maxDistPtsEdge), maxDistanceLimit)) { + return maxDistPtsEdge; + } + if (geom.getDimension() == Dimension::A) { + auto maxDistPtsInterior = computeForAreaInterior(geom, tolerance); + if (maxDistPtsInterior + && pairDistance(maxDistPtsInterior) > pairDistance(maxDistPtsEdge)) { + return maxDistPtsInterior; + } + } + return maxDistPtsEdge; +} + +std::optional +DirectedHausdorffDistance::computeForPoints( + const Geometry& geom, double maxDistanceLimit) const +{ + double maxDist = -1.0; + std::optional maxDistPtsAB; + + struct PointFilter : public GeometryComponentFilter { + PointFilter(TargetDistance& td, double& md, std::optional& pts, + double limit) + : targetDistance(td), maxDist(md), maxDistPtsAB(pts), maxDistanceLimit(limit), done(false) + {} + + bool isDone() override { + return done; + } + + void filter_ro(const Geometry* geomElem) override + { + if (geomElem->getGeometryTypeId() != geom::GEOS_POINT) { + return; + } + + const CoordinateXY* pA = geomElem->getCoordinate(); + CoordinateXY pB = targetDistance.nearestPoint(*pA); + double dist = pA->distance(pB); + bool interior = dist > 0 && targetDistance.isInterior(*pA); + if (interior) { + dist = 0; + pB = *pA; + } + if (dist > maxDist) { + maxDist = dist; + maxDistPtsAB = DirectedHausdorffDistance::pair(*pA, pB); + } + if (DirectedHausdorffDistance::isValidLimit(maxDistanceLimit) + && DirectedHausdorffDistance::isBeyondLimit(maxDist, maxDistanceLimit)) { + done = true; + } + } + + TargetDistance& targetDistance; + double& maxDist; + std::optional& maxDistPtsAB; + double maxDistanceLimit; + bool done; + }; + + PointFilter filter(*targetDistance, maxDist, maxDistPtsAB, maxDistanceLimit); + geom.apply_ro(&filter); + return maxDistPtsAB; +} + +std::optional +DirectedHausdorffDistance::computeForEdges( + const Geometry& geom, double tolerance, double maxDistanceLimit) const +{ + std::priority_queue segQueue; + + struct LineFilter : public GeometryComponentFilter { + LineFilter(std::priority_queue& q, TargetDistance& td) + : segQueue(q), targetDistance(td) + {} + + void filter_ro(const Geometry* g) override + { + const LineString* ls = dynamic_cast(g); + if (!ls || ls->isEmpty()) { + return; + } + const auto* pts = ls->getCoordinatesRO(); + if (!pts || pts->size() < 2) { + return; + } + DHDSegment prevSeg = DHDSegment::create(pts->getAt(0), + pts->getAt(1), + targetDistance); + consider(prevSeg); + for (std::size_t i = 1; i < pts->size() - 1; i++) { + DHDSegment seg = DHDSegment::create(prevSeg, pts->getAt(i + 1), targetDistance); + consider(seg); + prevSeg = seg; + } + } + + void addNonInterior(const DHDSegment& segment) const + { + if (segment.getMaxDistance() > 0.0) { + segQueue.push(segment); + return; + } + if (targetDistance.isInterior(segment.getEndpoint(0), segment.getEndpoint(1))) { + return; + } + segQueue.push(segment); + } + + void consider(const DHDSegment& seg) + { + if (!segMaxDist || seg.getMaxDistanceBound() > segMaxDist->getMaxDistance()) { + addNonInterior(seg); + } + if (!segMaxDist || seg.getMaxDistance() > segMaxDist->getMaxDistance()) { + segMaxDist = seg; + } + } + + std::priority_queue& segQueue; + TargetDistance& targetDistance; + std::optional segMaxDist; + }; + + LineFilter filter(segQueue, *targetDistance); + geom.apply_ro(&filter); + + std::optional segMaxDist; + while (!segQueue.empty()) { + DHDSegment segMaxBound = segQueue.top(); + segQueue.pop(); + + // Save if segment point is farther than current farthest + if (!segMaxDist || segMaxBound.getMaxDistance() > segMaxDist->getMaxDistance()) { + segMaxDist = segMaxBound; + } + + // Stop searching if remaining items in queue must all be closer + // than the current maximum distance. + if (segMaxBound.getMaxDistanceBound() <= segMaxDist->getMaxDistance()) { + break; + } + + // If maxDistanceLimit is specified, can stop searching if: + // - if segment distance bound is less than distance limit, no other segment can be farther + // - if a point of segment is farther than limit, isFullyWithin must be false + if (isValidLimit(maxDistanceLimit)) { + if (isWithinLimit(segMaxBound.getMaxDistanceBound(), maxDistanceLimit) + || isBeyondLimit(segMaxBound.getMaxDistance(), maxDistanceLimit)) { + break; + } + } + + // Check for equal or collinear segments. + // If so, don't bisect the segment further. + // This greatly improves performance when the inputs + // have identical or collinear segments + // (in particular, the case when the inputs are identical). + if (segMaxBound.getMaxDistance() == 0.0 + && targetDistance->isSameOrCollinear( + segMaxBound.getEndpoint(0), segMaxBound.getEndpoint(1))) { + continue; + } + + // If segment is longer than tolerance + // it might provide a better max distance point, + // so bisect and keep searching. + if (tolerance > 0 && segMaxBound.getLength() > tolerance) { + auto bisects = segMaxBound.bisect(*targetDistance); + filter.addNonInterior(bisects[0]); + filter.addNonInterior(bisects[1]); + } + } + + // A segment at maximum distance was found. + // Return the farthest point pair + if (segMaxDist) { + return segMaxDist->getMaxDistPts(); + } + + // No DHD segment was found. + // This must be because all were inside the target. + // In this case distance is zero. + // Return a single coordinate of the input as a representative point + const CoordinateXY *maxPt = geom.getCoordinate(); + if (!maxPt) { + return std::nullopt; + } + return pair(*maxPt, *maxPt); +} + +std::optional +DirectedHausdorffDistance::computeForAreaInterior( + const Geometry& geom, double tolerance) const +{ + if (tolerance <= 0.0) { + return std::nullopt; + } + const Geometry& polygonal = geom; + if (polygonal.getEnvelopeInternal()->disjoint(target.getEnvelopeInternal())) { + return std::nullopt; + } + + LargestEmptyCircle lec(&target, &polygonal, tolerance * AREA_INTERIOR_TOLERANCE_FACTOR); + auto centerPt = lec.getCenter(); + const CoordinateXY* ptA = centerPt->getCoordinate(); + if (!ptA) { + return std::nullopt; + } + if (targetDistance->isInterior(*ptA)) { + return std::nullopt; + } + CoordinateXY ptB = targetDistance->nearestFacetPoint(*ptA); + return pair(*ptA, ptB); +} + +} // namespace distance +} // namespace algorithm +} // namespace geos diff --git a/src/operation/distance/CoordinateSequenceLocation.cpp b/src/operation/distance/CoordinateSequenceLocation.cpp new file mode 100644 index 000000000..7e6281a02 --- /dev/null +++ b/src/operation/distance/CoordinateSequenceLocation.cpp @@ -0,0 +1,107 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 Martin Davis + * + * 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. + * + ********************************************************************** + * + * Last port: operation/distance/CoordinateSequenceLocation.java (aff11591) + * + **********************************************************************/ + +#include +#include + +using geos::geom::CoordinateXY; +using geos::geom::CoordinateSequence; + +namespace geos { +namespace operation { +namespace distance { + +CoordinateSequenceLocation::CoordinateSequenceLocation( + const CoordinateSequence* p_seq, std::size_t p_index, const CoordinateXY& p_pt) + : seq(p_seq) + , index(p_index) + , pt(p_pt) +{ + if (seq && index >= seq->size() && seq->size() > 0) { + index = seq->size() - 1; + } +} + +const CoordinateXY& +CoordinateSequenceLocation::getCoordinate() const +{ + return pt; +} + +std::size_t +CoordinateSequenceLocation::getIndex() const +{ + return index; +} + +bool +CoordinateSequenceLocation::isSameSegment(const CoordinateSequenceLocation& f) const +{ + if (seq != f.seq) { + return false; + } + if (index == f.index) { + return true; + } + //-- check for end pt same as start point of next segment + if (isNext(index, f.index)) { + const CoordinateXY& endPt = seq->getAt(index + 1); + return f.pt.equals2D(endPt); + } + if (isNext(f.index, index)) { + const CoordinateXY& endPt = f.seq->getAt(index + 1); + return pt.equals2D(endPt); + } + return false; +} + +bool +CoordinateSequenceLocation::isNext(std::size_t p_index, std::size_t index1) const +{ + if (index1 == p_index + 1) { + return true; + } + // JTS writes `index1 == 0 && isRing && index1 == size-1`, which is + // unreachable. The ring wrap is last segment (size-2) adjacent to 0. + if (seq && seq->isRing() && index1 == 0 && p_index + 1 == seq->size() - 1) { + return true; + } + return false; +} + +const CoordinateXY& +CoordinateSequenceLocation::getEndPoint(int i) const +{ + if (i == 0) { + return seq->getAt(index); + } + return seq->getAt(index + 1); +} + +std::size_t +CoordinateSequenceLocation::normalize(std::size_t p_index) const +{ + if (seq && p_index >= seq->size() - 1 && seq->isRing()) { + return 0; + } + return p_index; +} + +} // namespace distance +} // namespace operation +} // namespace geos diff --git a/src/operation/distance/FacetSequence.cpp b/src/operation/distance/FacetSequence.cpp index ebc1501dd..2b08f1b72 100644 --- a/src/operation/distance/FacetSequence.cpp +++ b/src/operation/distance/FacetSequence.cpp @@ -12,10 +12,11 @@ * ********************************************************************** * - * Last port: operation/distance/FacetSequence.java (f6187ee2 JTS-1.14) + * Last port: operation/distance/FacetSequence.java (aff11591) * **********************************************************************/ +#include #include #include #include @@ -229,3 +230,57 @@ FacetSequence::getCoordinate(std::size_t index) const return &(pts->getAt(start + index)); } +std::size_t +FacetSequence::normalize(const CoordinateSequence& pts, std::size_t index) +{ + if (index >= pts.size() - 1 && pts.isRing()) { + return 0; + } + return index; +} + +CoordinateSequenceLocation +FacetSequence::nearestLocation(const CoordinateXY& p) const +{ + if (isPoint()) { + // JTS uses index 0 / pts[0]; use start so a one-point facet + // that is a subsequence of a longer CoordinateSequence is correct. + return CoordinateSequenceLocation(pts, start, pts->getAt(start)); + } + return nearestLocationOnLine(p); +} + +CoordinateSequenceLocation +FacetSequence::nearestLocationOnLine(const CoordinateXY& pt) const +{ + double minDistance = DoubleInfinity; + std::size_t index = start; + Coordinate nearestPt; + + const Coordinate queryPt(pt); + for (std::size_t i = start; i < end - 1; i++) { + const Coordinate& q0 = pts->getAt(i); + const Coordinate& q1 = pts->getAt(i + 1); + double dist = Distance::pointToSegment(queryPt, q0, q1); + if (dist < minDistance) { + minDistance = dist; + LineSegment seg(q0, q1); + seg.closestPoint(queryPt, nearestPt); + index = i; + //-- segments are half-open, so 2nd endpoint belongs to next segment + //-- except for last segment on non-closed sequence + if (dist == 0.0 && queryPt.equals2D(q1)) { + if (index < pts->size() - 1) { + index++; + } + //-- normalize index for a ring + index = normalize(*pts, index); + } + if (minDistance <= 0.0) { + break; + } + } + } + return CoordinateSequenceLocation(pts, index, nearestPt); +} + diff --git a/src/operation/distance/IndexedFacetDistance.cpp b/src/operation/distance/IndexedFacetDistance.cpp index 6555920cd..babe15438 100644 --- a/src/operation/distance/IndexedFacetDistance.cpp +++ b/src/operation/distance/IndexedFacetDistance.cpp @@ -12,13 +12,16 @@ * ********************************************************************** * - * Last port: operation/distance/IndexedFacetDistance.java (f6187ee2 JTS-1.14) + * Last port: operation/distance/IndexedFacetDistance.java (aff11591) * **********************************************************************/ #include +#include #include #include +#include +#include using namespace geos::geom; using namespace geos::index::strtree; @@ -105,6 +108,48 @@ IndexedFacetDistance::isWithinDistance(const Geometry* g, double maxDistance) co return cachedTree->isWithinDistance(*tree2, maxDistance); } +CoordinateSequenceLocation +IndexedFacetDistance::nearestLocation(const geom::CoordinateXY& p) const +{ + // JTS wraps p in a CoordinateArraySequence; GEOS CoordinateSequence is the equivalent. + CoordinateSequence seq{CoordinateXY(p.x, p.y)}; + FacetSequence query(&seq, 0, 1); + FacetDistance itemDist; + const FacetSequence* nearest = cachedTree->nearestNeighbour( + *query.getEnvelope(), &query, itemDist); + if (!nearest) { + throw util::GEOSException("Cannot calculate IndexedFacetDistance on empty geometries."); + } + return nearest->nearestLocation(p); +} + +geom::CoordinateXY +IndexedFacetDistance::nearestPoint(const geom::CoordinateXY& p) const +{ + return nearestLocation(p).getCoordinate(); +} + +double +IndexedFacetDistance::distance(const geom::CoordinateXY& p) const +{ + return p.distance(nearestPoint(p)); +} + +double +IndexedFacetDistance::distance(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1) const +{ + CoordinateSequence seq{CoordinateXY(p0.x, p0.y), CoordinateXY(p1.x, p1.y)}; + FacetSequence query(&seq, 0, 2); + FacetDistance itemDist; + const FacetSequence* nearest = cachedTree->nearestNeighbour( + *query.getEnvelope(), &query, itemDist); + if (!nearest) { + throw util::GEOSException("Cannot calculate IndexedFacetDistance on empty geometries."); + } + auto locs = nearest->nearestLocations(query); + return locs[0].distance(locs[1]); +} + } } diff --git a/tests/unit/algorithm/distance/DirectedHausdorffDistanceTest.cpp b/tests/unit/algorithm/distance/DirectedHausdorffDistanceTest.cpp new file mode 100644 index 000000000..5d9c4ce09 --- /dev/null +++ b/tests/unit/algorithm/distance/DirectedHausdorffDistanceTest.cpp @@ -0,0 +1,630 @@ +// +// Test Suite for geos::algorithm::distance::DirectedHausdorffDistance +// Ported from JTS DirectedHausdorffDistanceTest (locationtech/jts#1182) +// plus the discrete-under-estimate witness from DiscreteHausdorffDistance. + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "utility.h" + +using geos::algorithm::distance::DirectedHausdorffDistance; +using geos::algorithm::distance::DiscreteHausdorffDistance; +using geos::geom::CoordinateXY; +using geos::geom::Geometry; +using geos::geom::GeometryFactory; +using geos::geom::PrecisionModel; + +namespace tut { + +struct test_directedhausdorffdistance_data { + test_directedhausdorffdistance_data() + : pm() + , gf(GeometryFactory::create(&pm)) + , reader(gf.get()) + {} + + static constexpr double TOLERANCE = 0.001; + + std::unique_ptr + read(const std::string& wkt) const + { + return reader.read(wkt); + } + + void + checkDistance(const std::string& wkt1, const std::string& wkt2, + double expectedDistance) const + { + auto g1 = read(wkt1); + auto g2 = read(wkt2); + + double dist = DirectedHausdorffDistance::distance(*g1, *g2); + ensure(std::fabs(dist - expectedDistance) <= TOLERANCE); + } + + void checkDistance(const std::string& wkt1, const std::string& wkt2, + double tolerance, const std::string& wktExpected) const + { + auto g1 = read(wkt1); + auto g2 = read(wkt2); + + auto pts = DirectedHausdorffDistance::distancePoints(*g1, *g2, tolerance); + ensure(pts.has_value()); + + auto seq = std::make_unique(2); + seq->setAt(pts.value()[0], 0); + seq->setAt(pts.value()[1], 1); + + auto result = g1->getFactory()->createLineString(std::move(seq)); + ensure_equals_exact_geometry(static_cast(result.get()), read(wktExpected).get(), TOLERANCE); + } + + void checkDistanceStartPtLen(const std::string wkt1, const std::string& wkt2, + const std::string& wktExpected, double resultTolerance) const + { + auto g1 = read(wkt1); + auto g2 = read(wkt2); + + auto pts = DirectedHausdorffDistance::distancePoints(*g1, *g2); + ensure(pts.has_value()); + + auto seq = std::make_unique(2); + seq->setAt(pts.value()[0], 0); + seq->setAt(pts.value()[1], 1); + + auto result = g1->getFactory()->createLineString(std::move(seq)); + auto expected = reader.read(wktExpected); + + const CoordinateXY& resultPt = result->getCoordinatesRO()->getAt(0); + const CoordinateXY& expectedPt = expected->getCoordinatesRO()->getAt(0); + + ensure_equals_xy(expectedPt, resultPt, resultTolerance); + + double distResult = result->getLength(); + double distExpected = expected->getLength(); + ensure_equals("distance is not within tolerance of expected", distExpected, distResult, resultTolerance); + } + + void + checkDistance(const std::string& wkt1, const std::string& wkt2, + double tolerance, double expectedDistance) const + { + auto g1 = read(wkt1); + auto g2 = read(wkt2); + double dist = DirectedHausdorffDistance::distance(*g1, *g2, tolerance); + ensure(std::fabs(dist - expectedDistance) <= TOLERANCE); + } + + void + checkDistance(const std::string& wkt1, const std::string& wkt2, + const std::string& wktExpected) const + { + auto g1 = read(wkt1); + auto g2 = read(wkt2); + auto pts = DirectedHausdorffDistance::distancePoints(*g1, *g2); + ensure(pts.has_value()); + + auto seq = std::make_unique(2); + seq->setAt(pts.value()[0], 0); + seq->setAt(pts.value()[1], 1); + + auto result = g1->getFactory()->createLineString(std::move(seq)); + ensure_equals_exact_geometry(static_cast(result.get()), read(wktExpected).get(), TOLERANCE); + + ensure_equals_exact_geometry(static_cast(result.get()), read(wktExpected).get(), TOLERANCE); + } + + void + checkHausdorff(const std::string& wkt1, const std::string& wkt2, + const std::string& wktExpected) const + { + std::unique_ptr g1 = read(wkt1); + std::unique_ptr g2 = read(wkt2); + + auto pts = DirectedHausdorffDistance::hausdorffDistancePoints(*g1, *g2); + ensure(pts.has_value()); + + auto seq = std::make_unique(2); + seq->setAt(pts.value()[0], 0); + seq->setAt(pts.value()[1], 1); + + auto result = g1->getFactory()->createLineString(std::move(seq)); + ensure_equals_exact_geometry(static_cast(result.get()), read(wktExpected).get(), TOLERANCE); + } + + void + checkDistanceEmpty(const std::string& a, const std::string& b) const + { + std::unique_ptr g1 = read(a); + std::unique_ptr g2 = read(b); + ensure(!DirectedHausdorffDistance::distancePoints(*g1, *g2).has_value()); + ensure(std::isnan(DirectedHausdorffDistance::distance(*g1, *g2))); + ensure(std::isnan(DirectedHausdorffDistance::hausdorffDistance(*g1, *g2))); + } + + void + checkFullyWithinDistance(const std::string& a, const std::string& b, + double distance, bool expected) const + { + std::unique_ptr g1 = read(a); + std::unique_ptr g2 = read(b); + bool result = DirectedHausdorffDistance::isFullyWithinDistance(*g1, *g2, distance); + ensure_equals(result, expected); + } + + void checkFullyWithinDistanceEmpty(const std::string& a, const std::string& b) const + { + checkFullyWithinDistance(a, b, 0, false); + checkFullyWithinDistance(b, a, 0, false); + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(b, a, 1, false); + checkFullyWithinDistance(a, b, 1000, false); + checkFullyWithinDistance(b, a, 1000, false); + } + + PrecisionModel pm; + GeometryFactory::Ptr gf; + geos::io::WKTReader reader; +}; + +typedef test_group group; +typedef group::object object; + +group test_DirectedHausdorffDistance_group( + "geos::algorithm::distance::DirectedHausdorffDistance"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("testEmpty"); + + checkDistanceEmpty("POINT EMPTY", "POINT (1 1)"); + checkDistanceEmpty("LINESTRING EMPTY", "LINESTRING (0 0, 2 1)"); + checkDistanceEmpty("POLYGON EMPTY", "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("testZeroTolerancePoint"); + + checkDistance("POINT (5 5)", "LINESTRING (5 1, 9 5)", + 0, + "LINESTRING (5 5, 7 3)"); +} + +template<> +template<> +void object::test<3>() +{ + set_test_name("testZeroToleranceLine"); + + checkDistance("LINESTRING (1 5, 5 5)", "LINESTRING (5 1, 9 5)", + 0, + "LINESTRING (1 5, 5 1)"); +} + +template<> +template<> +void object::test<4>() +{ + set_test_name("testZeroToleranceZeroLengthLineQuery"); + + checkDistance("LINESTRING (5 5, 5 5)", "LINESTRING (5 1, 9 5)", + 0, + "LINESTRING (5 5, 7 3)"); +} + +template<> +template<> +void object::test<5>() +{ + set_test_name("testZeroLengthLineQuery"); + + checkDistance("LINESTRING (5 5, 5 5)", "LINESTRING (5 1, 9 5)", + "LINESTRING (5 5, 7 3)"); +} + +template<> +template<> +void object::test<6>() +{ + set_test_name("testZeroLengthPolygonQuery"); + + checkDistance("POLYGON ((5 5, 5 5, 5 5, 5 5))", "LINESTRING (5 1, 9 5)", + "LINESTRING (5 5, 7 3)"); +} + +template<> +template<> +void object::test<7>() +{ + set_test_name("testZeroLengthLineTarget"); + + checkDistance("POINT (5 5)", "LINESTRING (5 1, 5 1)", + "LINESTRING (5 5, 5 1)"); +} + +template<> +template<> +void object::test<8>() +{ + set_test_name("testNegativeTolerancePoint"); + + auto g1 = read("POINT (5 5)"); + auto g2 = read("LINESTRING (5 1, 9 5)"); + + ensure_THROW(DirectedHausdorffDistance::distance(*g1, *g2, -1.0), geos::util::IllegalArgumentException); +} + +template<> +template<> +void object::test<9>() +{ + set_test_name("testNegativeToleranceLine"); + + auto g1 = read("LINESTRING (1 1, 5 5)"); + auto g2 = read("LINESTRING (5 1, 9 5)"); + + ensure_THROW(DirectedHausdorffDistance::distance(*g1, *g2, -1.0), geos::util::IllegalArgumentException); +} + +template<> +template<> +void object::test<10>() +{ + set_test_name("testPointPoint"); + + checkHausdorff("POINT (0 0)", "POINT (1 1)", "LINESTRING (0 0, 1 1)"); +} + +template<> +template<> +void object::test<11>() +{ + set_test_name("testPointPoints"); + + std::string a = "MULTIPOINT ((0 1), (2 3), (4 5), (6 6))"; + std::string b = "MULTIPOINT ((0.1 0), (1 0), (2 0), (3 0), (4 0), (5 0))"; + checkDistance(a, b, "LINESTRING (6 6, 5 0)"); + checkDistance(b, a, "LINESTRING (5 0, 2 3)"); + checkHausdorff(a, b, "LINESTRING (6 6, 5 0)"); +} + +template<> +template<> +void object::test<12>() +{ + set_test_name("testPointPolygonInterior"); + + checkDistance("POINT (3 4)", "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))", + 0); +} + +template<> +template<> +void object::test<13>() +{ + set_test_name("testPointsPolygon"); + + checkDistance("MULTIPOINT ((4 3), (2 8), (8 5))", "POLYGON ((6 9, 6 4, 9 1, 1 1, 6 9))", + "LINESTRING (2 8, 4.426966292134832 6.48314606741573)"); +} + +template<> +template<> +void object::test<14>() +{ + set_test_name("testLineSegments"); + + checkHausdorff("LINESTRING (0 0, 2 0)", "LINESTRING (0 0, 2 1)", + "LINESTRING (2 0, 2 1)"); +} + +template<> +template<> +void object::test<15>() +{ + set_test_name("testLineSegments2"); + + checkHausdorff("LINESTRING (0 0, 2 0)", "LINESTRING (0 1, 1 2, 2 1)", + "LINESTRING (1 0, 1 2)"); +} + +template<> +template<> +void object::test<16>() +{ + set_test_name("testLinePoints"); + + checkHausdorff("LINESTRING (0 0, 2 0)", "MULTIPOINT (0 2, 1 0, 2 1)", + "LINESTRING (0 0, 0 2)"); +} + +template<> +template<> +void object::test<17>() +{ + set_test_name("testLinesTopoEqual"); + + checkDistance("MULTILINESTRING ((10 10, 10 90, 40 30), (40 30, 60 80, 90 30, 40 10))", + "LINESTRING (10 10, 10 90, 40 30, 60 80, 90 30, 40 10)", + 0.0); +} + +template<> +template<> +void object::test<18>() +{ + set_test_name("testLinesPolygon"); + + checkHausdorff("MULTILINESTRING ((1 1, 2 7), (7 1, 9 9))", + "POLYGON ((3 7, 6 7, 6 4, 3 4, 3 7))", + "LINESTRING (9 9, 6 7)"); +} + +template<> +template<> +void object::test<19>() +{ + set_test_name("testLinesPolygon2"); + + std::string a = "MULTILINESTRING ((2 3, 2 7), (9 1, 9 8, 4 9))"; + std::string b = "POLYGON ((3 7, 6 8, 8 2, 3 4, 3 7))"; + checkDistance(a, b, "LINESTRING (9 8, 6.3 7.1)"); + checkHausdorff(a, b, "LINESTRING (2 3, 5.5 3)"); +} + +template<> +template<> +void object::test<20>() +{ + set_test_name("testPolygonLineCrossingBoundaryResult"); + + checkDistance("POLYGON ((2 8, 8 2, 2 1, 2 8))", + "LINESTRING (6 5, 4 7, 0 0, 8 4)", + "LINESTRING (2 8, 3.9384615384615387 6.892307692307693)"); +} + +template<> +template<> +void object::test<21>() +{ + set_test_name("testPolygonLineCrossingInteriorPoint"); + + checkDistanceStartPtLen("POLYGON ((2 8, 8 2, 2 1, 2 8))", + "LINESTRING (6 5, 4 7, 0 0, 9 1)", + "LINESTRING (4.555 2.989, 4.828 0.536)", 0.01); +} + +template<> +template<> +void object::test<22>() +{ + set_test_name("testPolygonPolygon"); + + std::string a = "POLYGON ((2 18, 18 18, 17 3, 2 2, 2 18))"; + std::string b = "POLYGON ((1 19, 5 12, 5 3, 14 10, 11 19, 19 19, 20 0, 1 1, 1 19))"; + checkDistance(b, a, "LINESTRING (20 0, 17 3)"); + checkDistance(a, b, "LINESTRING (6.6796875 18, 11 19)"); + checkHausdorff(a, b, "LINESTRING (6.6796875 18, 11 19)"); +} + +template<> +template<> +void object::test<23>() +{ + set_test_name("testPolygonPolygonHolesNested"); + + // B is contained in A + std::string a = "POLYGON ((1 19, 19 19, 19 1, 1 1, 1 19), (6 8, 11 14, 15 7, 6 8))"; + std::string b = "POLYGON ((2 18, 18 18, 18 2, 2 2, 2 18), (10 17, 3 7, 17 5, 10 17))"; + checkDistance(a, b, "LINESTRING (9.817138671875 12.58056640625, 7.863620425230705 13.948029178901006)"); + checkDistance(b, a, 0.0); +} + +template<> +template<> +void object::test<24>() +{ + set_test_name("testMultiPolygons"); + + std::string a = "MULTIPOLYGON (((1 1, 1 10, 5 1, 1 1)), ((4 17, 9 15, 9 6, 4 17)))"; + std::string b = "MULTIPOLYGON (((1 12, 4 13, 8 10, 1 12)), ((3 8, 7 7, 6 2, 3 8)))"; + checkDistance(a, b, "LINESTRING (1 1, 5.4 3.2)"); + checkDistanceStartPtLen(b, a, + "LINESTRING (2.669921875 12.556640625, 5.446115154109589 13.818546660958905)", + 0.01); +} + +template<> +template<> +void object::test<25>() +{ + set_test_name("testLinePolygonCrossing"); + + std::string wkt1 = "LINESTRING (2 5, 5 10, 6 4)"; + std::string wkt2 = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + checkDistance(wkt1, wkt2, "LINESTRING (5 10, 5 9)"); +} + +template<> +template<> +void object::test<26>() +{ + set_test_name("testNonVertexResult"); + + std::string wkt1 = "LINESTRING (1 1, 5 10, 9 1)"; + std::string wkt2 = "LINESTRING (0 10, 0 0, 10 0)"; + + checkHausdorff(wkt1, wkt2, "LINESTRING (6.53857421875 6.5382080078125, 6.53857421875 0)"); + checkDistance(wkt1, wkt2, "LINESTRING (6.53857421875 6.5382080078125, 6.53857421875 0)"); +} + +template<> +template<> +void object::test<27>() +{ + set_test_name("testDirectedLines"); + + std::string wkt1 = "LINESTRING (1 6, 3 5, 1 4)"; + std::string wkt2 = "LINESTRING (1 10, 9 5, 1 2)"; + checkDistance(wkt1, wkt2, "LINESTRING (1 6, 2.797752808988764 8.876404494382022)"); + checkDistance(wkt2, wkt1, "LINESTRING (9 5, 3 5)"); +} + +template<> +template<> +void object::test<28>() +{ + set_test_name("testDirectedLines2"); + + std::string wkt1 = "LINESTRING (1 6, 3 5, 1 4)"; + std::string wkt2 = "LINESTRING (1 3, 1 9, 9 5, 1 1)"; + checkDistance(wkt1, wkt2, "LINESTRING (3 5, 1 5)"); + checkDistance(wkt2, wkt1, "LINESTRING (9 5, 3 5)"); +} + +/** + * Tests that segments are detected as interior even for a large tolerance. + */ +template<> +template<> +void object::test<29>() +{ + set_test_name("testInteriorSegmentsLargeTol"); + + std::string a = "POLYGON ((4 6, 5 6, 5 5, 4 5, 4 6))"; + std::string b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + checkDistance(a, b, 2.0, 0.0); +} + +/** + * Tests that segment endpoint nearest points + * which are interior to B have distance 0 + */ +template<> +template<> +void object::test<30>() +{ + set_test_name("testInteriorSegmentsSameExterior"); + std::string a = "POLYGON ((1 9, 3 9, 4 5, 5.05 9, 9 9, 9 1, 1 1, 1 9))"; + std::string b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9))"; + checkDistance(a, b, 0.0); +} + +//----------------------------------------------------- + +template<> +template<> +void object::test<31>() +{ + set_test_name("testFullyWithinDistanceEmptyPoints"); + std::string a = "POINT EMPTY"; + std::string b = "MULTIPOINT ((1 1), (9 9))"; + checkFullyWithinDistanceEmpty(a, b); +} + +template<> +template<> +void object::test<32>() +{ + set_test_name("testFullyWithinDistanceEmptyLine"); + std::string a = "LINESTRING EMPTY"; + std::string b = "LINESTRING (9 9, 1 1)"; + checkFullyWithinDistanceEmpty(a, b); +} + +template<> +template<> +void object::test<33>() +{ + //-- shows withinDistance envelope check not triggering for disconnected A + set_test_name("testFullyWithinDistancePoints"); + std::string a = "MULTIPOINT ((1 9), (9 1))"; + std::string b = "MULTIPOINT ((1 1), (9 9))"; + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(a, b, 8.1, true); +} + +template<> +template<> +void object::test<34>() +{ + set_test_name("testFullyWithinDistanceDisconnectedLines"); + std::string a = "MULTILINESTRING ((1 9, 2 9), (8 1, 9 1))"; + std::string b = "LINESTRING (9 9, 1 1)"; + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(a, b, 6, true); + checkFullyWithinDistance(b, a, 1, false); + checkFullyWithinDistance(b, a, 7.1, true); +} + +template<> +template<> +void object::test<35>() +{ + set_test_name("testFullyWithinDistanceDisconnectedPolygons"); + std::string a = "MULTIPOLYGON (((1 9, 2 9, 2 8, 1 8, 1 9)), ((8 2, 9 2, 9 1, 8 1, 8 2)))"; + std::string b = "POLYGON ((1 2, 9 9, 2 1, 1 2))"; + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(a, b, 5.3, true); + checkFullyWithinDistance(b, a, 1, false); + checkFullyWithinDistance(b, a, 7.1, true); +} + +template<> +template<> +void object::test<36>() +{ + set_test_name("testFullyWithinDistanceLines"); + std::string a = "MULTILINESTRING ((1 1, 3 3), (7 7, 9 9))"; + std::string b = "MULTILINESTRING ((1 9, 1 5), (6 4, 8 2))"; + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(a, b, 4, false); + checkFullyWithinDistance(a, b, 6, true); +} + +template<> +template<> +void object::test<37>() +{ + set_test_name("testFullyWithinDistancePolygons"); + std::string a = "POLYGON ((1 4, 4 4, 4 1, 1 1, 1 4))"; + std::string b = "POLYGON ((10 10, 10 15, 15 15, 15 10, 10 10))"; + checkFullyWithinDistance(a, b, 5, false); + checkFullyWithinDistance(a, b, 10, false); + checkFullyWithinDistance(a, b, 20, true); +} + +template<> +template<> +void object::test<38>() +{ + set_test_name("testFullyWithinDistancePolygonsNestedWithHole"); + std::string a = "POLYGON ((2 8, 8 8, 8 2, 2 2, 2 8))"; + std::string b = "POLYGON ((1 9, 9 9, 9 1, 1 1, 1 9), (3 7, 7 7, 7 3, 3 3, 3 7))"; + checkFullyWithinDistance(a, b, 1, false); + checkFullyWithinDistance(a, b, 2, true); + checkFullyWithinDistance(a, b, 3, true); +} + + +} // namespace tut diff --git a/tests/unit/capi/GEOSDirectedHausdorffDistanceTest.cpp b/tests/unit/capi/GEOSDirectedHausdorffDistanceTest.cpp new file mode 100644 index 000000000..3832542e9 --- /dev/null +++ b/tests/unit/capi/GEOSDirectedHausdorffDistanceTest.cpp @@ -0,0 +1,96 @@ +// +// Test Suite for C-API GEOSDirectedHausdorffDistance / +// GEOSSymmetricHausdorffDistance + +#include +#include + +#include "capi_test_utils.h" + +#include + +namespace tut { + +struct test_capigeosdirectedhausdorffdistance_data : public capitest::utility { +}; + +typedef test_group group; +typedef group::object object; + +group test_capigeosdirectedhausdorffdistance_group( + "capi::GEOSDirectedHausdorffDistance"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("maximum distance point pair within segment interior"); + + geom1_ = fromWKT("LINESTRING (0 0, 100 0, 10 100, 10 100)"); + geom2_ = fromWKT("LINESTRING (0 100, 0 10, 80 10)"); + + double discrete = 0.0; + double directed12 = 0.0; + double directed21 = 0.0; + double symmetric = 0.0; + ensure_equals(GEOSHausdorffDistance(geom1_, geom2_, &discrete), 1); + ensure_equals(GEOSDirectedHausdorffDistance(geom1_, geom2_, &directed12), 1); + ensure_equals(GEOSDirectedHausdorffDistance(geom2_, geom1_, &directed21), 1); + ensure_equals(GEOSSymmetricHausdorffDistance(geom1_, geom2_, &symmetric), 1); + + ensure_equals("GEOSHausdorffDistance", discrete, 22.36, 1e-2); + ensure_equals("Directed 1->2", directed12, 47.891845703125); + ensure_equals("Directed 2->1", directed21, 44.533046060947164); + ensure_equals("Symmetric", symmetric, 47.891845703125); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("empty writes NaN, not 0"); + + geom1_ = fromWKT("LINESTRING EMPTY"); + geom2_ = fromWKT("LINESTRING (0 0, 2 1)"); + + double dist = 0.0; + ensure_equals(GEOSDirectedHausdorffDistance(geom1_, geom2_, &dist), 1); + ensure(std::isnan(dist)); +} + +template<> +template<> +void object::test<3>() +{ + set_test_name("GEOSDirectedHausdorffDistanceWithPoints"); + + geom1_ = fromWKT("POINT (0 0)"); + geom2_ = fromWKT("POINT (3 4)"); + + double dist = 0.0; + double p1x = 0, p1y = 0, p2x = 0, p2y = 0; + ensure_equals(GEOSDirectedHausdorffDistanceWithPoints( + geom1_, geom2_, &dist, &p1x, &p1y, &p2x, &p2y), 1); + ensure_distance(dist, 5.0, 1e-12); + ensure_equals(p1x, 0.0); + ensure_equals(p1y, 0.0); + ensure_equals(p2x, 3.0); + ensure_equals(p2y, 4.0); +} + +template<> +template<> +void object::test<4>() +{ + set_test_name("GEOSDirectedHausdorffDistanceWithin"); + + geom1_ = fromWKT("LINESTRING (0 0, 2 0)"); + geom2_ = fromWKT("LINESTRING (0 0, 2 1)"); + + ensure_equals(GEOSDirectedHausdorffDistanceWithin(geom1_, geom2_, 0.5), 0); + ensure_equals(GEOSDirectedHausdorffDistanceWithin(geom1_, geom2_, 1.0), 1); + ensure_equals(GEOSDirectedHausdorffDistanceWithin(geom1_, geom2_, 2.0), 1); +} + + +} // namespace tut \ No newline at end of file diff --git a/util/geosop/GeometryOp.cpp b/util/geosop/GeometryOp.cpp index 29e0dd59f..0c0bcfb34 100644 --- a/util/geosop/GeometryOp.cpp +++ b/util/geosop/GeometryOp.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -643,6 +644,22 @@ std::vector opRegistry { return new Result( geos::algorithm::distance::DiscreteHausdorffDistance::distance(geom, geomB ) ); }); }}, +{"directedHausdorffDistance", [](std::string name) { return GeometryOp::create(name, + catDist, + "compute directed (locus) Hausdorff distance from geometry A to B", + Result::typeDouble, + [](const Geometry& geom, const Geometry& geomB) { + return new Result( geos::algorithm::distance::DirectedHausdorffDistance::distance(geom, geomB ) ); + }); + }}, +{"symmetricHausdorffDistance", [](std::string name) { return GeometryOp::create(name, + catDist, + "compute symmetric (locus) Hausdorff distance between geometry A and B", + Result::typeDouble, + [](const Geometry& geom, const Geometry& geomB) { + return new Result( geos::algorithm::distance::DirectedHausdorffDistance::hausdorffDistance(geom, geomB ) ); + }); + }}, /* // MD - can't get this to work for now add("frechetDistanceLine", 2, 0, Result::typeGeometry, catDist, ----------------------------------------------------------------------- Summary of changes: NEWS.md | 4 + Version.txt | 4 +- capi/geos_c.cpp | 24 + capi/geos_c.h.in | 116 ++++ capi/geos_ts_c.cpp | 63 ++ .../algorithm/distance/DirectedHausdorffDistance.h | 300 ++++++++++ .../distance/CoordinateSequenceLocation.h | 76 +++ include/geos/operation/distance/FacetSequence.h | 21 +- .../geos/operation/distance/IndexedFacetDistance.h | 18 +- .../distance/DirectedHausdorffDistance.cpp | 659 +++++++++++++++++++++ .../distance/CoordinateSequenceLocation.cpp | 107 ++++ src/operation/distance/FacetSequence.cpp | 57 +- src/operation/distance/IndexedFacetDistance.cpp | 47 +- .../distance/DirectedHausdorffDistanceTest.cpp | 630 ++++++++++++++++++++ .../capi/GEOSDirectedHausdorffDistanceTest.cpp | 96 +++ util/geosop/GeometryOp.cpp | 17 + 16 files changed, 2231 insertions(+), 8 deletions(-) create mode 100644 include/geos/algorithm/distance/DirectedHausdorffDistance.h create mode 100644 include/geos/operation/distance/CoordinateSequenceLocation.h create mode 100644 src/algorithm/distance/DirectedHausdorffDistance.cpp create mode 100644 src/operation/distance/CoordinateSequenceLocation.cpp create mode 100644 tests/unit/algorithm/distance/DirectedHausdorffDistanceTest.cpp create mode 100644 tests/unit/capi/GEOSDirectedHausdorffDistanceTest.cpp hooks/post-receive -- GEOS