From git at osgeo.org Wed Apr 1 08:53:42 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 1 Apr 2026 08:53:42 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. ee89eae8faa1e8bf82c98d30ce35a3328e3d57ae Message-ID: <20260401155343.0EFAE36EBF@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 ee89eae8faa1e8bf82c98d30ce35a3328e3d57ae (commit) from 1a8eb16b2c90598d71fd6e469c79ec0ce6bba33e (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 ee89eae8faa1e8bf82c98d30ce35a3328e3d57ae Author: Daniel Baston Date: Wed Apr 1 11:53:19 2026 -0400 GeometryFactory: add createSurface method (#1416) diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h index 4b5dba01f..d58b4dfe7 100644 --- a/include/geos/geom/GeometryFactory.h +++ b/include/geos/geom/GeometryFactory.h @@ -308,6 +308,15 @@ public: std::unique_ptr createCurvePolygon(std::unique_ptr&& shell, std::vector> && holes) const; + + /// Construct a Surface, taking ownership of given arguments + /// If arguments are LinearRings, a Polygon will be returned. + /// Otherwise, a CurvePolygon will be returned. + std::unique_ptr createSurface(std::unique_ptr&& shell) const; + + std::unique_ptr createSurface(std::unique_ptr&& shell, + std::vector> && holes) const; + /// Construct an EMPTY LineString std::unique_ptr createLineString(std::size_t coordinateDimension = 2) const; std::unique_ptr createLineString(bool hasZ, bool hasM) const; diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp index 152dd39a2..93dc39f61 100644 --- a/src/geom/GeometryFactory.cpp +++ b/src/geom/GeometryFactory.cpp @@ -635,6 +635,42 @@ const return std::unique_ptr(new CurvePolygon(std::move(shell), std::move(holes), *this)); } +/* public */ +std::unique_ptr +GeometryFactory::createSurface(std::unique_ptr && shell) +const +{ + if (shell->getGeometryTypeId() == GEOS_LINEARRING) { + std::unique_ptr shellLR(detail::down_cast(shell.release())); + return createPolygon(std::move(shellLR)); + } + + return createCurvePolygon(std::move(shell)); +} + +/* public */ +std::unique_ptr +GeometryFactory::createSurface(std::unique_ptr && shell, std::vector> && holes) +const +{ + const bool returnPolygon = shell->getGeometryTypeId() == GEOS_LINEARRING && std::all_of(holes.begin(), holes.end(), [](const auto& hole) { + return hole->getGeometryTypeId() == GEOS_LINEARRING; + }); + + if (returnPolygon) { + std::unique_ptr shellLR(detail::down_cast(shell.release())); + + std::vector> holesLR(holes.size()); + for (std::size_t i = 0; i < holes.size(); i++) { + holesLR[i].reset(detail::down_cast(holes[i].release())); + } + + return createPolygon(std::move(shellLR), std::move(holesLR)); + } + + return createCurvePolygon(std::move(shell), std::move(holes)); +} + /*public*/ std::unique_ptr GeometryFactory::createLineString(std::size_t coordinateDimension) const diff --git a/tests/unit/geom/GeometryFactoryTest.cpp b/tests/unit/geom/GeometryFactoryTest.cpp index a0773b21b..7e80d6703 100644 --- a/tests/unit/geom/GeometryFactoryTest.cpp +++ b/tests/unit/geom/GeometryFactoryTest.cpp @@ -1455,5 +1455,63 @@ void object::test<41> } } +template<> +template<> +void object::test<42> +() { + set_test_name("createSurface"); + using geos::geom::GEOS_POLYGON; + using geos::geom::GEOS_CURVEPOLYGON; + using geos::geom::Curve; + + // LinearRing + { + auto shell = reader_.read("LINEARRING (0 0, 1 0, 1 1, 0 0)"); + ensure_equals(factory_->createSurface(std::move(shell))->getGeometryTypeId(), GEOS_POLYGON); + } + + // LineString + { + auto shell = reader_.read("LINESTRING (0 0, 1 0, 1 1, 0 0)"); + ensure_equals(factory_->createSurface(std::move(shell))->getGeometryTypeId(), GEOS_CURVEPOLYGON); + } + + // CompoundCurve, linear + { + auto shell = reader_.read("COMPOUNDCURVE ((0 0, 1 0, 1 1, 0 0))"); + ensure_equals(factory_->createSurface(std::move(shell))->getGeometryTypeId(), GEOS_CURVEPOLYGON); + } + + // CircularString + { + auto shell = reader_.read("CIRCULARSTRING (-1 0, 0 1, 1 0, 0 -1, -1 0)"); + ensure_equals(factory_->createSurface(std::move(shell))->getGeometryTypeId(), GEOS_CURVEPOLYGON); + } + + // CompoundCurve, curved + { + auto shell = reader_.read("COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))"); + ensure_equals(factory_->createSurface(std::move(shell))->getGeometryTypeId(), GEOS_CURVEPOLYGON); + } + + // LinearRing shell, LinearRing hole + { + auto shell = reader_.read("LINEARRING (0 0, 10 0, 10 10, 0 10, 0 0)"); + auto hole = reader_.read("LINEARRING (5 5, 7 5, 7 7, 5 7, 5 5)"); + std::vector> holes; + holes.push_back(std::move(hole)); + ensure_equals(factory_->createSurface(std::move(shell), std::move(holes))->getGeometryTypeId(), GEOS_POLYGON); + } + + // LinearRing shell, CompoundCurve hole + { + auto shell = reader_.read("LINEARRING (0 0, 10 0, 10 10, 0 10, 0 0)"); + auto hole = reader_.read("COMPOUNDCURVE ((5 5, 7 5, 7 7, 5 7, 5 5))"); + std::vector> holes; + holes.push_back(std::move(hole)); + ensure_equals(factory_->createSurface(std::move(shell), std::move(holes))->getGeometryTypeId(), GEOS_CURVEPOLYGON); + } + +} } // namespace tut ----------------------------------------------------------------------- Summary of changes: include/geos/geom/GeometryFactory.h | 9 +++++ src/geom/GeometryFactory.cpp | 36 ++++++++++++++++++++ tests/unit/geom/GeometryFactoryTest.cpp | 58 +++++++++++++++++++++++++++++++++ 3 files changed, 103 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Wed Apr 1 13:12:49 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 1 Apr 2026 13:12:49 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/codecov/codecov-action-6 created. 7ee896ca55a54cefd36b276fcd9d0656ee955190 Message-ID: <20260401201249.4F782110205@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/codecov/codecov-action-6 has been created at 7ee896ca55a54cefd36b276fcd9d0656ee955190 (commit) - Log ----------------------------------------------------------------- commit 7ee896ca55a54cefd36b276fcd9d0656ee955190 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed Apr 1 20:12:24 2026 +0000 Bump codecov/codecov-action from 5 to 6 Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5885f3b90..3fddde083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -254,7 +254,7 @@ jobs: - name: Upload Coverage to Codecov if: ${{ matrix.ci.build_type == 'Coverage' && !cancelled() }} - uses: codecov/codecov-action at v5 + uses: codecov/codecov-action at v6 with: fail_ci_if_error: false files: build/coverage.info ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Wed Apr 1 13:59:42 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 1 Apr 2026 13:59:42 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/codecov/codecov-action-6 deleted. 7ee896ca55a54cefd36b276fcd9d0656ee955190 Message-ID: <20260401205942.32310110157@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/codecov/codecov-action-6 has been deleted was 7ee896ca55a54cefd36b276fcd9d0656ee955190 - Log ----------------------------------------------------------------- 7ee896ca55a54cefd36b276fcd9d0656ee955190 Bump codecov/codecov-action from 5 to 6 ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Wed Apr 1 13:59:42 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 1 Apr 2026 13:59:42 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 4a24df60ce8b4b36ded1b06d4507a1578cb4da77 Message-ID: <20260401205942.6311C34AEA@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 4a24df60ce8b4b36ded1b06d4507a1578cb4da77 (commit) from ee89eae8faa1e8bf82c98d30ce35a3328e3d57ae (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 4a24df60ce8b4b36ded1b06d4507a1578cb4da77 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu Apr 2 09:59:11 2026 +1300 Bump codecov/codecov-action from 5 to 6 (#1421) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v5...v6) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-version: '6' dependency-type: direct:production update-type: version-update:semver-major ... 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 5885f3b90..3fddde083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -254,7 +254,7 @@ jobs: - name: Upload Coverage to Codecov if: ${{ matrix.ci.build_type == 'Coverage' && !cancelled() }} - uses: codecov/codecov-action at v5 + uses: codecov/codecov-action at v6 with: fail_ci_if_error: false files: build/coverage.info ----------------------------------------------------------------------- Summary of changes: .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Apr 2 05:36:44 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 2 Apr 2026 05:36:44 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. f92148794f989e64940eba0074c4cec9083b54c7 Message-ID: <20260402123644.9761B117270@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 f92148794f989e64940eba0074c4cec9083b54c7 (commit) via f9657108f95a58051ddd38d2f64e9a2af9d39bce (commit) from 4a24df60ce8b4b36ded1b06d4507a1578cb4da77 (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 f92148794f989e64940eba0074c4cec9083b54c7 Author: Daniel Baston Date: Tue Mar 31 09:24:47 2026 -0400 ArcIntersectionAdder: Avoid processing some trivial intersections diff --git a/include/geos/noding/ArcIntersectionAdder.h b/include/geos/noding/ArcIntersectionAdder.h index e71822df4..0d606a60f 100644 --- a/include/geos/noding/ArcIntersectionAdder.h +++ b/include/geos/noding/ArcIntersectionAdder.h @@ -33,6 +33,11 @@ public: void processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) override; private: + static bool isAdjacentSegments(std::size_t i1, std::size_t i2); + + bool isTrivialIntersection(const PathString* e0, std::size_t segIndex0, + const PathString* e1, std::size_t segIndex1) const; + algorithm::CircularArcIntersector& m_intersector; }; diff --git a/src/noding/ArcIntersectionAdder.cpp b/src/noding/ArcIntersectionAdder.cpp index 61b9adae1..0232e2ae3 100644 --- a/src/noding/ArcIntersectionAdder.cpp +++ b/src/noding/ArcIntersectionAdder.cpp @@ -35,6 +35,10 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, return; } + if (isTrivialIntersection(&e0, segIndex0, &e1, segIndex1)) { + return; + } + for (std::uint8_t i = 0; i < m_intersector.getNumPoints(); i++) { detail::down_cast(&e0)->addIntersection(m_intersector.getPoint(i), segIndex0); detail::down_cast(&e1)->addIntersection(m_intersector.getPoint(i), segIndex1); @@ -73,6 +77,33 @@ ArcIntersectionAdder::processIntersections(ArcString& e0, std::size_t segIndex0, } +bool +ArcIntersectionAdder::isAdjacentSegments(std::size_t i1, std::size_t i2) +{ + return (i1 > i2 ? i1 - i2 : i2 - i1) == 1; +} + +bool +ArcIntersectionAdder::isTrivialIntersection(const PathString *e0, std::size_t segIndex0, + const PathString *e1, std::size_t segIndex1) const +{ + if (e0 != e1) { + return false; + } + + // TODO: If two ArcStrings form a circle they may have a trivial intersection with + // two points. + if (m_intersector.getNumPoints() != 1) { + return false; + } + + if (isAdjacentSegments(segIndex0, segIndex1)) { + return true; + } + + return false; +} + void ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segIndex0, SegmentString& e1, std::size_t segIndex1) { @@ -90,11 +121,13 @@ ArcIntersectionAdder::processIntersections(SegmentString& e0, std::size_t segInd return; } - // todo collinear? + if (isTrivialIntersection(&e0, segIndex0, &e1, segIndex1)) { + return; + } + + // TODO: Handle collinear segments static_cast(e0).addIntersection(m_intersector.getPoint(0), segIndex0); - - } } \ No newline at end of file commit f9657108f95a58051ddd38d2f64e9a2af9d39bce Author: Daniel Baston Date: Wed Mar 25 08:30:24 2026 -0400 NodableArcString: Split output ArcStrings at node points diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h index b7e438f8f..505cbbba6 100644 --- a/include/geos/noding/NodableArcString.h +++ b/include/geos/noding/NodableArcString.h @@ -33,7 +33,7 @@ public: m_adds[segmentIndex].push_back(intPt); } - std::unique_ptr getNoded(); + void getNoded(std::vector>& splitArcs); private: std::map> m_adds; diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 7580b6e07..208f5885f 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -127,10 +127,14 @@ GeometryNoder::toGeometry(std::vector>& nodedEdges) for(auto& path : nodedEdges) { const auto& coords = path->getCoordinates(); + bool isLinear = dynamic_cast(path.get()); + + // TODO: Make OrientedCoordinateArray not require strict equality of arc control points + OrientedCoordinateArray oca1(*coords); // Check if an equivalent edge is known if(ocas.insert(oca1).second) { - if (dynamic_cast(path.get())) { + if (isLinear) { lines.push_back(geomFact->createLineString(coords)); } else { resultArcs = true; diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 35969cac5..bbecddf99 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -3,7 +3,7 @@ * GEOS - Geometry Engine Open Source * http://geos.osgeo.org * - * Copyright (C) 2025 ISciences, LLC + * Copyright (C) 2025-2026 ISciences, LLC * * This is free software; you can redistribute and/or modify it under * the terms of the GNU Lesser General Public Licence as published @@ -13,8 +13,10 @@ **********************************************************************/ #include +#include -#define DEBUG_NODABLE_ARC_STRING 0 +using geos::geom::CoordinateXYZM; +using geos::geom::CircularArc; namespace geos::noding { @@ -36,96 +38,165 @@ NodableArcString::NodableArcString(std::vector arcs, const st { } -std::unique_ptr -NodableArcString::getNoded() { +static +std::vector prepareArcPoints(const CircularArc& arc, std::vector splitPoints) +{ + const bool isCCW = arc.getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE; + const geom::CoordinateXY& center = arc.getCenter(); + const double paStart = geom::Quadrant::pseudoAngle(center, arc.p0()); + std::vector retained; + // Add starting point of input arc + { + CoordinateXYZM p0; + arc.getCoordinateSequence()->getAt(arc.getCoordinatePosition(), p0); + retained.push_back(p0); + } + + std::sort(splitPoints.begin(), splitPoints.end(), [¢er, paStart, isCCW](const auto& p0, const auto& p1) { + double pa0 = geom::Quadrant::pseudoAngle(center, p0); + double pa1 = geom::Quadrant::pseudoAngle(center, p1); + + if (isCCW) { + return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1); + } else { + return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1); + } + }); + + // Add ending point of input arc + { + CoordinateXYZM p2; + arc.getCoordinateSequence()->getAt(arc.getCoordinatePosition() + 2, p2); + splitPoints.push_back(p2); + } + + for (const auto& p2 : splitPoints) { + auto& p0 = retained.back(); + + if (p2.equals2D(p0)) { + if (std::isnan(p0.z) && !std::isnan(p2.z)) { + p0.z = p2.z; + } + if (std::isnan(p0.m) && !std::isnan(p2.m)) { + p0.m = p2.m; + } + continue; + } + + if (!arc.containsPointOnCircle(p2)) { + continue; + } + + const auto p1 = algorithm::CircularArcs::getMidpoint(p0, p2, center, arc.getRadius(), isCCW); + + if (p1.equals2D(p0) || p1.equals2D(p2)) { + continue; + } + + // Reject split point where sub-arc midpoint doesn't fall inside arc + if (!arc.containsPointOnCircle(p1)) { + continue; + } + + // Reject split point where computed doesn't fall between endpoints + const double t0 = algorithm::Angle::normalizePositive(arc.theta0()); + const double t1 = algorithm::Angle::normalizePositive(algorithm::CircularArcs::getAngle(p1, center)); + const double t2 = algorithm::Angle::normalizePositive(algorithm::CircularArcs::getAngle(p2, center)); + + if (algorithm::Angle::isWithinCCW(t1, t0, t2) != isCCW) { + continue; + } + + retained.push_back(p2); + } + + // Make sure that endpoint of split arc is unchanged + { + CoordinateXYZM p2; + arc.getCoordinateSequence()->getAt(arc.getCoordinatePosition() + 2, p2); + CoordinateXYZM& back = retained.back(); + + if (!back.equals2D(p2)) { + back.x = p2.x; + back.y = p2.y; + } + if (std::isnan(back.z) && !std::isnan(p2.z)) { + back.z = p2.z; + } + if (std::isnan(back.m) && !std::isnan(p2.m)) { + back.m = p2.m; + } + } + + return retained; +} + +void +NodableArcString::getNoded(std::vector>& splitArcs) { auto dstSeq = std::make_unique(0, m_constructZ, m_constructM); + std::vector arcs; - std::vector arcs; - for (size_t i = 0; i < m_arcs.size(); i++) { - if (const auto it = m_adds.find(i); it == m_adds.end()) { - // No nodes added, just copy the coordinates into the sequence. - const geom::CoordinateSequence* srcSeq = m_arcs[i].getCoordinateSequence(); - std::size_t srcPos = m_arcs[i].getCoordinatePosition(); - std::size_t dstPos = dstSeq->getSize(); - dstSeq->add(*srcSeq, srcPos, srcPos + 2, false); - arcs.emplace_back(*dstSeq, dstPos); - } else { - std::vector& splitPoints = it->second; + for (size_t arcIndex = 0; arcIndex < m_arcs.size(); arcIndex++) { + const CircularArc& toSplit = m_arcs[arcIndex]; + const geom::CoordinateXY& center = toSplit.getCenter(); + const double radius = toSplit.getRadius(); + const int orientation = toSplit.getOrientation(); - // TODO check split point actually inside arc? + bool arcIsSplit = true; + std::vector arcPoints; + const auto it = m_adds.find(arcIndex); + if (it == m_adds.end()) { + arcIsSplit = false; + } else { + arcPoints = prepareArcPoints(toSplit, it->second); - const geom::CircularArc& toSplit = m_arcs[i]; - const geom::CoordinateXY& center = toSplit.getCenter(); - const double radius = toSplit.getRadius(); - const int orientation = toSplit.getOrientation(); - const bool isCCW = orientation == algorithm::Orientation::COUNTERCLOCKWISE; - const double paStart = geom::Quadrant::pseudoAngle(center, toSplit.p0()); - - std::sort(splitPoints.begin(), splitPoints.end(), [¢er, paStart, isCCW](const auto& p0, const auto& p1) { - double pa0 = geom::Quadrant::pseudoAngle(center, p0); - double pa1 = geom::Quadrant::pseudoAngle(center, p1); - - if (isCCW) { - return pseudoAngleDiffCCW(paStart, pa0) < pseudoAngleDiffCCW(paStart, pa1); - } else { - return pseudoAngleDiffCCW(paStart, pa0) > pseudoAngleDiffCCW(paStart, pa1); - } - }); - -#if DEBUG_NODABLE_ARC_STRING - std::cout << "Splitting " << toSplit.toString() << " " << (isCCW ? "CCW" : "CW") << " paStart " << paStart << " paStop " << geom::Quadrant::pseudoAngle(center, toSplit.p2()) << std::endl; - for (const auto& splitPt : splitPoints) - { - const double pa = geom::Quadrant::pseudoAngle(center, splitPt); - std::cout << " " << splitPt.toString() << " (pa " << pa << " diff " << pseudoAngleDiffCCW(paStart, pa) << ")" << std::endl; - } -#endif - - // Add first point of split arc - std::size_t dstPos = dstSeq->getSize(); - dstSeq->add(*toSplit.getCoordinateSequence(), toSplit.getCoordinatePosition(), toSplit.getCoordinatePosition()); - geom::CoordinateXYZM p0, p2; - dstSeq->getAt(dstPos, p0); - - // Add intermediate points of split arc - for (const auto& splitPoint : splitPoints) { - if (arcs.empty()) { - if (splitPoint.equals2D(p0)) { - continue; - } - } else if (splitPoint.equals2D(arcs.back().p2())) { - continue; - } - - geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, splitPoint, center, radius, isCCW)); - midpoint.z = (p0.z + splitPoint.z) / 2; - midpoint.m = (p0.m + splitPoint.m) / 2; - - dstSeq->add(midpoint); - dstSeq->add(splitPoint); - - p0 = splitPoint; - - arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); - dstPos = dstSeq->getSize() - 1; - } - - // Add last point of split arc - toSplit.getCoordinateSequence()->getAt(toSplit.getCoordinatePosition() + 2, p2); - if (arcs.empty() || !arcs.back().p2().equals2D(p2)) { - geom::CoordinateXYZM midpoint(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); - midpoint.z = (p0.z + p2.z) / 2; - midpoint.m = (p0.m + p2.m) / 2; - - dstSeq->add(midpoint); - dstSeq->add(p2); - arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); - } + if (arcPoints.size() == 2) { + // All added nodes collapsed + arcIsSplit = false; } } - return std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr); + if (!arcIsSplit) { + // No nodes added, just copy the coordinates into the sequence. + const geom::CoordinateSequence* srcSeq = m_arcs[arcIndex].getCoordinateSequence(); + std::size_t srcPos = m_arcs[arcIndex].getCoordinatePosition(); + dstSeq->add(*srcSeq, srcPos, srcPos + 2, false); + std::size_t dstPos = dstSeq->getSize() - 3; + arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + + continue; + } + + const bool isCCW = orientation == algorithm::Orientation::COUNTERCLOCKWISE; + + for (std::size_t i = 1; i < arcPoints.size(); i++) { + const CoordinateXYZM& p0 = arcPoints[i - 1]; + const CoordinateXYZM& p2 = arcPoints[i]; + + CoordinateXYZM p1(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); + p1.z = (p0.z + p2.z) / 2; + p1.m = (p0.m + p2.m) / 2; + + if (dstSeq->isEmpty()) { + dstSeq->add(p0); + } + dstSeq->add(p1); + dstSeq->add(p2); + + const std::size_t dstPos = dstSeq->getSize() - 3; + arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); + + // Finish the ArcString, start a new one. + splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr)); + dstSeq = std::make_unique(0, m_constructZ, m_constructM); + arcs.clear(); + } } + if (!arcs.empty()) { + splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr)); + } +} + } \ No newline at end of file diff --git a/src/noding/SimpleNoder.cpp b/src/noding/SimpleNoder.cpp index 496de6070..a35c0ee83 100644 --- a/src/noding/SimpleNoder.cpp +++ b/src/noding/SimpleNoder.cpp @@ -85,8 +85,12 @@ SimpleNoder::getNodedPaths() nodedPaths.push_back(std::move(segString)); } } else { + std::vector> tmp; auto* nas = detail::down_cast(ps); - nodedPaths.push_back(nas->getNoded()); + nas->getNoded(tmp); + for (auto& arcString : tmp) { + nodedPaths.push_back(std::move(arcString)); + } } } diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index e1356e44e..8feb5df96 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -212,8 +212,12 @@ void object::test<9>() ensure(result_ != nullptr); expected_ = fromWKT("MULTICURVE (" - "CIRCULARSTRING (0 0, 0.0340741737 0.2588190451, 0.1339745962 0.5, 1 1, 1.8660254038 0.5, 1.9659258263 0.2588190451, 2 0)," - "CIRCULARSTRING (0 1, 0.0340741737 0.7411809549, 0.1339745962 0.5, 1 0, 1.8660254038 0.5, 1.9659258263 0.7411809549, 2 1))"); + "CIRCULARSTRING (0 0, 0.0340741737 0.2588190451, 0.1339745962 0.5)," + "CIRCULARSTRING (0.1339745962 0.5, 1 1, 1.8660254038 0.5)," + "CIRCULARSTRING (1.8660254038 0.5, 1.9659258263 0.2588190451, 2 0)," + "CIRCULARSTRING (0 1, 0.0340741737 0.7411809549, 0.1339745962 0.5)," + "CIRCULARSTRING (0.1339745962 0.5, 1 0, 1.8660254038 0.5)," + "CIRCULARSTRING (1.8660254038 0.5, 1.9659258263 0.7411809549, 2 1))"); ensure_geometry_equals_exact(result_, expected_, 1e-8); } @@ -222,7 +226,7 @@ template<> template<> void object::test<10>() { - set_test_name("CIRCULARSTRING ZM intersecting CIRCULARSTRING M"); + set_test_name("CIRCULARSTRING ZM intersecting CIRCULARSTRING M at endpoint and interior"); input_ = fromWKT("MULTICURVE (CIRCULARSTRING ZM (-1 0 3 4, 0 1 2 5, 1 0 4 7), CIRCULARSTRING M (-1 2 9, 0 1 13, -1 0 17))"); ensure(input_); @@ -231,12 +235,12 @@ void object::test<10>() ensure(result_ != nullptr); expected_ = fromWKT("MULTICURVE ZM (" - "CIRCULARSTRING ZM (-1 0 3 4, -0.7071067812 0.7071067812 2.5 6.5, -5.5511151231e-17 1 2 9, 0.7071067812 0.7071067812 3 8, 1 0 4 7)," - "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9, -5.5511151231e-17 1 2 9, -0.2928932188 0.2928932188 2.5 6.5, -1 0 3 4))"); + "CIRCULARSTRING ZM (-1 0 3 4, -0.7071067812 0.7071067812 2.5 6.5, -5.5511151231e-17 1 2 9)," + "CIRCULARSTRING ZM (-5.5511151231e-17 1 2 9, 0.7071067812 0.7071067812 3 8, 1 0 4 7)," + "CIRCULARSTRING ZM (-1 2 NaN 9, -0.2928932188 1.7071067812 NaN 9, -5.5511151231e-17 1 2 9)," + "CIRCULARSTRING ZM (-5.5511151231e-17 1 2 9, -0.2928932188 0.2928932188 2.5 6.5, -1 0 3 4))"); - ensure_equals(GEOSGetNumGeometries(result_), 2); - ensure_equals("Noded arc 1 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 0)), 5); - ensure_equals("Noded arc 2 should have 5 points", GEOSGeomGetNumPoints(GEOSGetGeometryN(result_, 1)), 5); + ensure_equals(GEOSGetNumGeometries(result_), 4); ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), reinterpret_cast(expected_), 1e-8); @@ -255,7 +259,8 @@ void object::test<11>() ensure(result_ != nullptr); expected_ = fromWKT("MULTICURVE ZM (" - "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8, 2.2361 4.4721 3.5 7.5, 4 3 4 7)," + "CIRCULARSTRING ZM (-5 0 3 4, -3.5355 3.5355 3 6, 0 5 3 8)," + "CIRCULARSTRING ZM (0 5 3 8, 2.2361 4.4721 3.5 7.5, 4 3 4 7)," "LINESTRING ZM (0 0 NaN 7, 0 5 3 8)," "LINESTRING ZM (0 5 3 8, 0 10 NaN 13))"); @@ -274,13 +279,17 @@ void object::test<12>() "CIRCULARSTRING (-4 3, 0 5, 4 3))"); result_ = GEOSNode(input_); + ensure(result_); expected_ = fromWKT("MULTICURVE (" - "CIRCULARSTRING (-5.0000000000000000 0.0000000000000000, -4.7434164902525691 1.5811388300841900, -4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000, 4.7434164902525691 1.5811388300841898, 5.0000000000000000 0.0000000000000000)," - "CIRCULARSTRING (-4.0000000000000000 3.0000000000000000, 0.0000000000000003 5.0000000000000000, 4.0000000000000000 3.0000000000000000))"); + "CIRCULARSTRING (-5 0, -4.7434164902525691 1.5811388300841900, -4 3)," + "CIRCULARSTRING (-4 3, 0 5, 4 3)," + "CIRCULARSTRING (-4 3, 0 5, 4 3)," // FIXME this component is not removed by OrientedCoordinateArray because + // its control point differs slightly from the previous one + "CIRCULARSTRING (4 3, 4.7434164902525691 1.5811388300841898, 5 0))"); - ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), - reinterpret_cast(expected_), 1e-4); + ensure_equals_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); } template<> @@ -349,7 +358,7 @@ void object::test<16>() result_ = GEOSNode(input_); ensure(result_ != nullptr); - expected_ = fromWKT("MULTICURVE Z (CIRCULARSTRING Z (-5 0 3, -1.5811388301 4.7434164903 4.5, 4 3 6), (0 0 NaN, 4 3 6))"); + expected_ = fromWKT("MULTICURVE Z (CIRCULARSTRING Z (-5 0 3, -4 3 5, 4 3 6), (0 0 NaN, 4 3 6))"); ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), reinterpret_cast(expected_), 1e-4); @@ -367,5 +376,27 @@ void object::test<17>() ensure(result_); } +template<> +template<> +void object::test<18>() +{ + set_test_name("Multi-section CircularStrings with single interior intersection"); + + input_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (-5 0, -4 3, 0 5, 3 4, 4 3)," + "CIRCULARSTRING (3 5, 4 4, 2 3))" + ); + + result_ = GEOSNode(input_); + ensure(result_); + + ensure_equals(GEOSGetNumGeometries(result_), 4); + + expected_ = fromWKT("MULTICURVE (CIRCULARSTRING (-5 0, -4 3, 0 5, 2.1154124423 4.5304558489, 3.8335130689 3.2100120795), CIRCULARSTRING (3.8335130689 3.2100120795, 3.918163858 3.1061216946, 4 3), CIRCULARSTRING (3 5, 3.9016950794 4.3308190803, 3.8335130689 3.2100120795), CIRCULARSTRING (3.8335130689 3.2100120795, 2.9674441042 2.6624775822, 2 3))"); + ensure_equals_exact_geometry_xyzm(reinterpret_cast(result_), + reinterpret_cast(expected_), 1e-4); + +} + } // namespace tut diff --git a/tests/unit/noding/NodableArcStringTest.cpp b/tests/unit/noding/NodableArcStringTest.cpp index 7c90e3831..4bdedad95 100644 --- a/tests/unit/noding/NodableArcStringTest.cpp +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -8,6 +8,7 @@ using geos::geom::CircularArc; using geos::geom::Ordinate; using geos::geom::CoordinateXY; using geos::geom::CoordinateXYZM; +using geos::noding::ArcString; using geos::noding::NodableArcString; namespace tut { @@ -20,7 +21,7 @@ struct test_nodablearcstring_data { using XYZM = CoordinateXYZM; static void test_add_points(const CircularArc& arc, const std::vector& coords, - const std::vector& expected, bool reversed=false) { + const std::vector>& expected, bool reversed=false) { std::vector arcs; arcs.push_back(arc); NodableArcString nas(arcs, nullptr, false, false, nullptr); @@ -29,20 +30,27 @@ struct test_nodablearcstring_data { nas.addIntersection(coord, 0); } - auto noded = nas.getNoded(); + std::vector> noded; + nas.getNoded(noded); - ensure_equals(noded->getSize(), expected.size()); + ensure_equals(noded.size(), expected.size()); - for (std::size_t i = 0; i < expected.size(); i++) { - ensure_arc_equals(noded->getArc(i), expected[i], 1e-8); + for (std::size_t i = 0; i < noded.size(); i++) { + for (std::size_t j = 0; j < noded[i]->getSize(); j++) { + ensure_arc_equals(noded[i]->getArc(j), expected[i][j], 1e-8); + } } if (!reversed) { const auto revArc = arc.reverse(); - std::vector revExpected; + std::vector> revExpected; for (const auto& x : expected) { - revExpected.push_back(x.reverse()); + revExpected.push_back(x); + std::reverse(revExpected.back().begin(), revExpected.back().end()); + for (auto& y : revExpected.back()) { + y = y.reverse(); + } } std::reverse(revExpected.begin(), revExpected.end()); @@ -74,12 +82,12 @@ void object::test<1>() coords.emplace_back(-3, 4); coords.emplace_back(-4, 3); - std::vector expected; - expected.push_back(CircularArc::create(XY{-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); + std::vector> expected; + expected.push_back({CircularArc::create(XY{-5, 0}, {-4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{-4, 3}, {-3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{-3, 4}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)}); test_add_points(in, coords, expected); } @@ -99,13 +107,13 @@ void object::test<2>() coords.emplace_back(3, 4); coords.emplace_back(5, 0); - std::vector expected; - expected.push_back(CircularArc::create(XY{0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE)); - expected.push_back(CircularArc::create(XY{3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE)); + std::vector> expected; + expected.push_back({CircularArc::create(XY{0, 5}, {3, 4}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{3, 4}, {4, 3}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{4, 3}, {5, 0}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{5, 0}, {4, -3}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{4, -3}, {3, -4}, {0, 0}, 5, Orientation::CLOCKWISE)}); + expected.push_back({CircularArc::create(XY{3, -4}, {0, -5}, {0, 0}, 5, Orientation::CLOCKWISE)}); test_add_points(in, coords, expected); } @@ -118,8 +126,8 @@ void object::test<3>() CircularArc in = CircularArc::create(CoordinateXY{-1, 0}, CoordinateXY{0, 1}, CoordinateXY{1, 0}); std::vector coords; - std::vector expected; - expected.push_back(in); + std::vector> expected; + expected.push_back({in}); test_add_points(in, coords, expected); } @@ -147,15 +155,16 @@ void object::test<4>() nas.addIntersection( intPt, 0); - auto noded = nas.getNoded(); + std::vector> noded; + nas.getNoded(noded); - ensure_equals(noded->getSize(), 2u); - const CircularArc& arc0 = noded->getArc(0); + ensure_equals(noded.size(), 2u); + const CircularArc& arc0 = noded[0]->getArc(0); ensure_arc_equals(arc0, CircularArc::create(p0, intPt, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8); ensure_equals(arc0.p1().z, (p0.z + intPt.z) / 2); ensure_equals(arc0.p1().m, (p0.m + intPt.m) / 2); - const CircularArc& arc1 = noded->getArc(1); + const CircularArc& arc1 = noded[1]->getArc(0); ensure_arc_equals(arc1, CircularArc::create(intPt, p2, arc.getCenter(), arc.getRadius(), arc.getOrientation()), 1e-8); ensure_equals(arc1.p1().z, (intPt.z + p2.z) / 2); ensure_equals(arc1.p1().m, (intPt.m + p2.m) / 2); ----------------------------------------------------------------------- Summary of changes: include/geos/noding/ArcIntersectionAdder.h | 5 + include/geos/noding/NodableArcString.h | 2 +- src/noding/ArcIntersectionAdder.cpp | 39 ++++- src/noding/GeometryNoder.cpp | 6 +- src/noding/NodableArcString.cpp | 243 +++++++++++++++++++---------- src/noding/SimpleNoder.cpp | 6 +- tests/unit/capi/GEOSNodeTest.cpp | 61 ++++++-- tests/unit/noding/NodableArcStringTest.cpp | 61 +++++--- 8 files changed, 290 insertions(+), 133 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Apr 3 17:23:35 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 3 Apr 2026 17:23:35 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 3f8560ef422af5f245941ea7fd6702ac4f9261e8 Message-ID: <20260404002335.F0245155965@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 3f8560ef422af5f245941ea7fd6702ac4f9261e8 (commit) from f92148794f989e64940eba0074c4cec9083b54c7 (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 3f8560ef422af5f245941ea7fd6702ac4f9261e8 Author: Daniel Baston Date: Fri Apr 3 20:23:08 2026 -0400 Doc: Fix GEOSSubdivideByGrid availability version diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 2c21576fc..82b6127af 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -4219,7 +4219,7 @@ extern GEOSGeometry GEOS_DLL *GEOSClipByRect( * \see GEOSGetGridIntersectionFractions * -* \since 3.14.0 +* \since 3.15.0 */ extern GEOSGeometry GEOS_DLL *GEOSSubdivideByGrid( const GEOSGeometry* g, ----------------------------------------------------------------------- Summary of changes: capi/geos_c.h.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Apr 7 07:39:24 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 7 Apr 2026 07:39:24 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 52dc9337bc485361adfa0e3e0f0c135ceb4ccbdd Message-ID: <20260407143925.A4895190178@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 52dc9337bc485361adfa0e3e0f0c135ceb4ccbdd (commit) via c0169aacd10c62a9a2d4c0b4e0e469327a2b78bf (commit) via 25ea72cd522bad00110276d2643ace910fbb3866 (commit) via 99696486a8c454550367941384225a3c9f95ce14 (commit) via dfa7c7ad43e5db1cd4ef5903ea3bf26070936faa (commit) via d1a9893c4c3a9867a2149d49a9f21c6331703bd2 (commit) from 3f8560ef422af5f245941ea7fd6702ac4f9261e8 (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 52dc9337bc485361adfa0e3e0f0c135ceb4ccbdd Author: Daniel Baston Date: Tue Apr 7 09:47:17 2026 -0400 GEOSPolygonize_full: add test with curved inputs diff --git a/tests/unit/capi/GEOSPolygonizeTest.cpp b/tests/unit/capi/GEOSPolygonizeTest.cpp index 99e666f2c..5de4a27c7 100644 --- a/tests/unit/capi/GEOSPolygonizeTest.cpp +++ b/tests/unit/capi/GEOSPolygonizeTest.cpp @@ -277,6 +277,45 @@ void object::test<9>() ensure_equals(GEOSGetSRID(result_), 4326); } +template<> +template<> +void object::test<10>() +{ + set_test_name("GEOSPolygonize_full with curved inputs"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), CIRCULARSTRING (2 0, 3 1, 4 0), LINESTRING (2 0, 0 0))"); + GEOSSetSRID(input_, 4326); + + GEOSGeometry* cuts; + GEOSGeometry* dangles; + GEOSGeometry* invalidRings; + + result_ = GEOSPolygonize_full(input_, &cuts, &dangles, &invalidRings); + ensure(result_); + + expected_ = fromWKT("GEOMETRYCOLLECTION (CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))))"); + GEOSGeometry* expected_cuts = GEOSGeomFromWKT("GEOMETRYCOLLECTION EMPTY"); + GEOSGeometry* expected_dangles = GEOSGeomFromWKT("GEOMETRYCOLLECTION(CIRCULARSTRING (2 0, 3 1, 4 0))"); + GEOSGeometry* expected_invalidRings = GEOSGeomFromWKT("GEOMETRYCOLLECTION EMPTY"); + + ensure_geometry_equals(result_, expected_); + ensure_geometry_equals(cuts, expected_cuts); + ensure_geometry_equals(dangles, expected_dangles); + ensure_geometry_equals(invalidRings, expected_invalidRings); + + ensure_equals(GEOSGetSRID(result_), 4326); + ensure_equals(GEOSGetSRID(cuts), 4326); + ensure_equals(GEOSGetSRID(dangles), 4326); + ensure_equals(GEOSGetSRID(invalidRings), 4326); + + GEOSGeom_destroy(cuts); + GEOSGeom_destroy(dangles); + GEOSGeom_destroy(invalidRings); + + GEOSGeom_destroy(expected_cuts); + GEOSGeom_destroy(expected_dangles); + GEOSGeom_destroy(expected_invalidRings); +} } // namespace tut commit c0169aacd10c62a9a2d4c0b4e0e469327a2b78bf Author: Daniel Baston Date: Tue Apr 7 09:35:20 2026 -0400 GEOSPolygonize: Propagate input SRID diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index b79283c02..fe58078a5 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -2444,13 +2444,19 @@ extern "C" { // Polygonize Polygonizer plgnzr; + int srid = 0; for(std::size_t i = 0; i < ngeoms; ++i) { plgnzr.add(g[i]); + if (!srid) { + srid = g[i]->getSRID(); + } } auto polys = plgnzr.getSurfaces(); const GeometryFactory* gf = handle->geomFactory; - return gf->createGeometryCollection(std::move(polys)).release(); + auto out = gf->createGeometryCollection(std::move(polys)); + out->setSRID(srid); + return out.release(); }); } @@ -2461,39 +2467,41 @@ extern "C" { return execute(extHandle, [&]() -> Geometry* { GEOSContextHandleInternal_t* handle = reinterpret_cast(extHandle); - Geometry* out; + std::unique_ptr out; // Polygonize Polygonizer plgnzr(true); int srid = 0; for(std::size_t i = 0; i < ngeoms; ++i) { plgnzr.add(g[i]); - srid = g[i]->getSRID(); + if (!srid) { + srid = g[i]->getSRID(); + } } auto surfaces = plgnzr.getSurfaces(); if (surfaces.empty()) { - out = handle->geomFactory->createGeometryCollection().release(); + out = handle->geomFactory->createGeometryCollection(); } else if (surfaces.size() == 1) { - return surfaces[0].release(); + out = std::move(surfaces[0]); } else { const bool isCurved = std::any_of(surfaces.begin(), surfaces.end(), [](const auto& poly) { return poly->hasCurvedComponents(); }); if (isCurved) { - return handle->geomFactory->createMultiSurface(std::move(surfaces)).release(); + out = handle->geomFactory->createMultiSurface(std::move(surfaces)); } else { std::vector> polygons(surfaces.size()); for (std::size_t i = 0; i < surfaces.size(); i++) { polygons[i].reset(geos::detail::down_cast(surfaces[i].release())); } - return handle->geomFactory->createMultiPolygon(std::move(polygons)).release(); + out = handle->geomFactory->createMultiPolygon(std::move(polygons)); } } out->setSRID(srid); - return out; + return out.release(); }); } @@ -2622,7 +2630,9 @@ extern "C" { int srid = 0; for(std::size_t i = 0; i < ngeoms; ++i) { plgnzr.add(g[i]); - srid = g[i]->getSRID(); + if (srid == 0) { + srid = g[i]->getSRID(); + } } const std::vector& lines = plgnzr.getCutEdges(); @@ -2655,6 +2665,7 @@ extern "C" { return execute(extHandle, [&]() { // Polygonize Polygonizer plgnzr; + const int srid = g->getSRID(); for(std::size_t i = 0; i < g->getNumGeometries(); ++i) { plgnzr.add(g->getGeometryN(i)); } @@ -2669,6 +2680,7 @@ extern "C" { } *cuts = gf->createGeometryCollection(std::move(linevec)).release(); + (*cuts)->setSRID(srid); } if(dangles) { @@ -2679,6 +2691,7 @@ extern "C" { } *dangles = gf->createGeometryCollection(std::move(linevec)).release(); + (*dangles)->setSRID(srid); } if(invalid) { @@ -2689,12 +2702,13 @@ extern "C" { } *invalid = gf->createGeometryCollection(std::move(linevec)).release(); + (*invalid)->setSRID(srid); } auto polys = plgnzr.getSurfaces(); - Geometry* out = gf->createGeometryCollection(std::move(polys)).release(); - out->setSRID(g->getSRID()); - return out; + auto out = gf->createGeometryCollection(std::move(polys)); + out->setSRID(srid); + return out.release(); }); } diff --git a/tests/unit/capi/GEOSPolygonizeTest.cpp b/tests/unit/capi/GEOSPolygonizeTest.cpp index eb3d07e7d..99e666f2c 100644 --- a/tests/unit/capi/GEOSPolygonizeTest.cpp +++ b/tests/unit/capi/GEOSPolygonizeTest.cpp @@ -32,18 +32,20 @@ template<> void object::test<1> () { + set_test_name("GEOSPolygonizer_getCutEdges"); + constexpr int size = 2; GEOSGeometry* geoms[size] = { nullptr }; geoms[0] = GEOSGeomFromWKT("LINESTRING(1 3, 3 3, 3 1, 1 1, 1 3)"); geoms[1] = GEOSGeomFromWKT("LINESTRING(1 3, 3 3, 3 1, 1 1, 1 3)"); + GEOSSetSRID(geoms[0], 4326); - GEOSGeometry* g = GEOSPolygonizer_getCutEdges(geoms, size); + result_ = GEOSPolygonizer_getCutEdges(geoms, size); - ensure(nullptr != g); - ensure_equals(GEOSGetNumGeometries(g), size); - - GEOSGeom_destroy(g); + ensure(result_); + ensure_equals(GEOSGetNumGeometries(result_), size); + ensure_equals(GEOSGetSRID(result_), 4326); for(auto& input : geoms) { GEOSGeom_destroy(input); @@ -83,16 +85,20 @@ template<> void object::test<3> () { + set_test_name("two nested rings"); + constexpr int size = 2; GEOSGeometry* geoms[size]; geoms[0] = GEOSGeomFromWKT("LINESTRING (100 100, 100 300, 300 300, 300 100, 100 100)"); geoms[1] = GEOSGeomFromWKT("LINESTRING (150 150, 150 250, 250 250, 250 150, 150 150)"); + GEOSSetSRID(geoms[0], 4326); // GEOSPolygonize gives us a collection of two polygons GEOSGeometry* g = GEOSPolygonize(geoms, size); ensure(nullptr != g); ensure_equals(GEOSGetNumGeometries(g), 2); ensure_equals(GEOSGeomTypeId(g), GEOS_GEOMETRYCOLLECTION); + ensure_equals(GEOSGetSRID(g), 4326); GEOSGeom_destroy(g); // GEOSPolygonize_valid gives us a single polygon with a hole @@ -101,6 +107,7 @@ void object::test<3> ensure(nullptr != g); ensure_equals(GEOSGetNumGeometries(g), 1); ensure_equals(GEOSGeomTypeId(g), GEOS_POLYGON); + ensure_equals(GEOSGetSRID(g), 4326); GEOSGeom_destroy(g); for(auto& input : geoms) { @@ -113,16 +120,19 @@ template<> void object::test<4> () { + set_test_name("GEOSPolygonize_valid producing an empty GeometryCollection"); + constexpr int size = 1; GEOSGeometry* geoms[size]; geoms[0] = GEOSGeomFromWKT("LINESTRING (0 0, 1 1)"); + GEOSSetSRID(geoms[0], 4326); - GEOSGeometry* g = GEOSPolygonize_valid(geoms, size); + result_ = GEOSPolygonize_valid(geoms, size); - ensure(nullptr != g); - ensure_equals(GEOSGetNumGeometries(g), 0); - ensure_equals(GEOSGeomTypeId(g), GEOS_GEOMETRYCOLLECTION); - GEOSGeom_destroy(g); + ensure(result_); + ensure_equals(GEOSGetNumGeometries(result_), 0); + ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION); + ensure_equals(GEOSGetSRID(result_), 4326); for(auto& input : geoms) { GEOSGeom_destroy(input); @@ -134,17 +144,20 @@ template<> void object::test<5> () { + set_test_name("GEOSPolygonize_valid producing a MultiPolygon"); + constexpr int size = 2; GEOSGeometry* geoms[size]; geoms[0] = GEOSGeomFromWKT("LINESTRING (0 0, 1 0, 1 1, 0 1, 0 0)"); geoms[1] = GEOSGeomFromWKT("LINESTRING (1 1, 2 1, 2 2, 1 2, 1 1)"); + GEOSSetSRID(geoms[0], 4326); - GEOSGeometry* g = GEOSPolygonize_valid(geoms, size); + result_ = GEOSPolygonize_valid(geoms, size); - ensure(nullptr != g); - ensure_equals(GEOSGetNumGeometries(g), 2); - ensure_equals(GEOSGeomTypeId(g), GEOS_MULTIPOLYGON); - GEOSGeom_destroy(g); + ensure(result_); + ensure_equals(GEOSGetNumGeometries(result_), 2); + ensure_equals(GEOSGeomTypeId(result_), GEOS_MULTIPOLYGON); + ensure_equals(GEOSGetSRID(result_), 4326); for(auto& input : geoms) { GEOSGeom_destroy(input); @@ -157,7 +170,10 @@ template<> void object::test<6> () { + set_test_name("GEOSPolygonize_full with MultiLineString input"); + geom1_ = GEOSGeomFromWKT("MULTILINESTRING ((0 0, 1 0, 1 1, 0 1, 0 0), (0 0, 0.5 0.5), (1 1, 2 2, 1 2, 2 1, 1 1))"); + GEOSSetSRID(geom1_, 4326); GEOSGeometry* cuts; GEOSGeometry* dangles; @@ -175,6 +191,11 @@ void object::test<6> ensure_geometry_equals(dangles, expected_dangles); ensure_geometry_equals(invalidRings, expected_invalidRings); + ensure_equals(GEOSGetSRID(result_), 4326); + ensure_equals(GEOSGetSRID(cuts), 4326); + ensure_equals(GEOSGetSRID(dangles), 4326); + ensure_equals(GEOSGetSRID(invalidRings), 4326); + GEOSGeom_destroy(cuts); GEOSGeom_destroy(dangles); GEOSGeom_destroy(invalidRings); @@ -195,6 +216,7 @@ void object::test<7> GEOSGeometry* geoms[size]; geoms[0] = GEOSGeomFromWKT("LINESTRING (0 0, 2 0)"); geoms[1] = GEOSGeomFromWKT("CIRCULARSTRING (0 0, 1 1, 2 0)"); + GEOSSetSRID(geoms[0], 4326); for (auto& geom : geoms) { ensure(geom != nullptr); @@ -205,6 +227,7 @@ void object::test<7> expected_ = fromWKT("GEOMETRYCOLLECTION( CURVEPOLYGON (COMPOUNDCURVE((0 0, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0))))"); ensure_geometry_equals(result_, expected_); + ensure_equals(GEOSGetSRID(result_), 4326); for(auto& input : geoms) { GEOSGeom_destroy(input); @@ -216,7 +239,7 @@ template<> void object::test<8> () { - set_test_name("LINESTRING ZM inputs"); + set_test_name("GEOSPolygonize with LINESTRING ZM inputs"); geom1_ = fromWKT("LINESTRING ZM (0 0 5 4, 2 0 6 5, 2 2 7 6)"); geom2_ = fromWKT("LINESTRING ZM (2 2 7 6, 0 0 5 4)"); @@ -241,6 +264,7 @@ void object::test<9>() set_test_name("GEOSPolygonize_valid with curved inputs"); input_ = fromWKT("MULTICURVE ((10 0, 0 0, 0 10, 10 10), CIRCULARSTRING (10 10, 5 5, 10 0), COMPOUNDCURVE ((10 10, 20 10, 20 0, 10 0), CIRCULARSTRING (10 0, 15 5, 10 10)))"); + GEOSSetSRID(input_, 4326); result_ = GEOSPolygonize_valid(&input_, 1); ensure(result_); @@ -250,6 +274,7 @@ void object::test<9>() "CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (10 0, 15 5, 10 10), (10 10, 20 10, 20 0, 10 0))))"); ensure_geometry_equals(result_, expected_); + ensure_equals(GEOSGetSRID(result_), 4326); } commit 25ea72cd522bad00110276d2643ace910fbb3866 Author: Daniel Baston Date: Tue Mar 31 13:37:07 2026 -0400 Polygonizer: support curved inputs diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in index 82b6127af..c941d50dd 100644 --- a/capi/geos_c.h.in +++ b/capi/geos_c.h.in @@ -5267,6 +5267,7 @@ extern GEOSGeometry GEOS_DLL *GEOSNode(const GEOSGeometry* g); * linework is extracted as the edges to be polygonized. * * Z and M values from the input linework will be preserved. +* Curved geometries are supported since GEOS 3.15. * * The edges must be correctly noded; that is, they must only meet * at their endpoints and not overlap anywhere. If your edges are not @@ -5295,7 +5296,7 @@ extern GEOSGeometry GEOS_DLL *GEOSNode(const GEOSGeometry* g); * that the input lines form a valid polygonal geometry (which may * include holes or nested polygons). * -* \param geoms Array of linear geometries to polygons. Caller retains ownership of both array container and objects. +* \param geoms Array of linear geometries to polygonize. Caller retains ownership of both array container and objects. * \param ngeoms Size of the geoms array. * \return The polygonal output geometry. * Caller is responsible for freeing with GEOSGeom_destroy(). @@ -5312,7 +5313,10 @@ extern GEOSGeometry GEOS_DLL *GEOSPolygonize( * but returns a result which is a valid polygonal geometry. * The result will not contain any edge-adjacent elements. * -* \param geoms Array of linear geometries to polygons. Caller retains ownership of both array container and objects. +* Z and M values from the input linework will be preserved. +* Curved geometries are supported since GEOS 3.15. +* +* \param geoms Array of linear geometries to polygonize. Caller retains ownership of both array container and objects. * \param ngeoms Size of the geoms array. * \return The polygonal output geometry. * Caller is responsible for freeing with GEOSGeom_destroy(). @@ -5330,7 +5334,10 @@ extern GEOSGeometry GEOS_DLL *GEOSPolygonize_valid( * "cut edges", the linear features that are connected at both ends, * do *not* participate in the final polygon. * -* \param geoms Array of linear geometries to polygons. Caller retains ownersihp of both array container and objects. +* Z and M values from the input linework will be preserved. +* Curved geometries are supported since GEOS 3.15. +* +* \param geoms Array of linear geometries to polygonize. Caller retains ownership of both array container and objects. * \param ngeoms Size of the geoms array. * \return The "cut edges" * Caller is responsible for freeing with GEOSGeom_destroy(). @@ -5346,6 +5353,9 @@ extern GEOSGeometry GEOS_DLL *GEOSPolygonizer_getCutEdges( * Perform the polygonization as GEOSPolygonize() and return the * polygonal result as well as all extra outputs. * +* Z and M values from the input linework will be preserved. +* Curved geometries are supported since GEOS 3.15. +* * \param[in] input A single geometry with all the input lines to polygonize. * \param[out] cuts Pointer to hold "cut edges", connected on both ends but not part of output. Caller must free. * \param[out] dangles Pointer to hold "dangles", connected one end but not part of output. Caller must free. diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp index d183be9ea..b79283c02 100644 --- a/capi/geos_ts_c.cpp +++ b/capi/geos_ts_c.cpp @@ -2448,7 +2448,7 @@ extern "C" { plgnzr.add(g[i]); } - auto polys = plgnzr.getPolygons(); + auto polys = plgnzr.getSurfaces(); const GeometryFactory* gf = handle->geomFactory; return gf->createGeometryCollection(std::move(polys)).release(); }); @@ -2471,13 +2471,25 @@ extern "C" { srid = g[i]->getSRID(); } - auto polys = plgnzr.getPolygons(); - if (polys.empty()) { + auto surfaces = plgnzr.getSurfaces(); + if (surfaces.empty()) { out = handle->geomFactory->createGeometryCollection().release(); - } else if (polys.size() == 1) { - return polys[0].release(); + } else if (surfaces.size() == 1) { + return surfaces[0].release(); } else { - return handle->geomFactory->createMultiPolygon(std::move(polys)).release(); + const bool isCurved = std::any_of(surfaces.begin(), surfaces.end(), [](const auto& poly) { + return poly->hasCurvedComponents(); + }); + + if (isCurved) { + return handle->geomFactory->createMultiSurface(std::move(surfaces)).release(); + } else { + std::vector> polygons(surfaces.size()); + for (std::size_t i = 0; i < surfaces.size(); i++) { + polygons[i].reset(geos::detail::down_cast(surfaces[i].release())); + } + return handle->geomFactory->createMultiPolygon(std::move(polygons)).release(); + } } out->setSRID(srid); @@ -2613,7 +2625,7 @@ extern "C" { srid = g[i]->getSRID(); } - const std::vector& lines = plgnzr.getCutEdges(); + const std::vector& lines = plgnzr.getCutEdges(); // We need a vector of Geometry pointers, not Polygon pointers. // STL vector doesn't allow transparent upcast of this @@ -2650,7 +2662,7 @@ extern "C" { const GeometryFactory* gf = g->getFactory(); if(cuts) { - const std::vector& lines = plgnzr.getCutEdges(); + const std::vector& lines = plgnzr.getCutEdges(); std::vector> linevec(lines.size()); for(std::size_t i = 0, n = lines.size(); i < n; ++i) { linevec[i] = lines[i]->clone(); @@ -2660,7 +2672,7 @@ extern "C" { } if(dangles) { - const std::vector& lines = plgnzr.getDangles(); + const std::vector& lines = plgnzr.getDangles(); std::vector> linevec(lines.size()); for(std::size_t i = 0, n = lines.size(); i < n; ++i) { linevec[i] = lines[i]->clone(); @@ -2670,7 +2682,7 @@ extern "C" { } if(invalid) { - const std::vector>& lines = plgnzr.getInvalidRingLines(); + const std::vector>& lines = plgnzr.getInvalidRingLines(); std::vector> linevec(lines.size()); for(std::size_t i = 0, n = lines.size(); i < n; ++i) { linevec[i] = lines[i]->clone(); @@ -2679,7 +2691,7 @@ extern "C" { *invalid = gf->createGeometryCollection(std::move(linevec)).release(); } - auto polys = plgnzr.getPolygons(); + auto polys = plgnzr.getSurfaces(); Geometry* out = gf->createGeometryCollection(std::move(polys)).release(); out->setSRID(g->getSRID()); return out; diff --git a/include/geos/operation/polygonize/EdgeRing.h b/include/geos/operation/polygonize/EdgeRing.h index cbfb6fdc7..5f8b7df5b 100644 --- a/include/geos/operation/polygonize/EdgeRing.h +++ b/include/geos/operation/polygonize/EdgeRing.h @@ -20,7 +20,7 @@ #pragma once #include -#include +#include #include #include #include @@ -38,6 +38,7 @@ // Forward declarations namespace geos { namespace geom { +class Curve; class LineString; class CoordinateSequence; class GeometryFactory; @@ -64,11 +65,10 @@ private: DeList deList; // cache the following data for efficiency - mutable std::unique_ptr ring; - mutable std::shared_ptr ringPts; + mutable std::unique_ptr ring; mutable std::unique_ptr ringLocator; - std::unique_ptr>> holes; + std::vector> holes; EdgeRing* shell = nullptr; bool is_hole; @@ -78,14 +78,6 @@ private: bool is_included = false; bool visitedByUpdateIncludedRecursive = false; - /** \brief - * Computes the list of coordinates which are contained in this ring. - * The coordinates are computed once only and cached. - * - * @return an array of the Coordinate in this ring - */ - const geom::CoordinateSequence* getCoordinates() const; - const geom::Envelope& getEnvelope() const { return *getRingInternal()->getEnvelopeInternal(); } @@ -94,12 +86,7 @@ private: bool isForward, geom::CoordinateSequence* coordList); - algorithm::locate::PointOnGeometryLocator* getLocator() const { - if (ringLocator == nullptr) { - ringLocator.reset(new algorithm::locate::IndexedPointInAreaLocator(*getRingInternal())); - } - return ringLocator.get(); - } + algorithm::locate::PointOnGeometryLocator* getLocator() const; bool contains(const EdgeRing& otherRing) const; bool isPointInOrOut(const EdgeRing& otherRing) const; @@ -130,7 +117,7 @@ public: * @return containing EdgeRing, if there is one * @return null if no containing EdgeRing is found */ - EdgeRing* findEdgeRingContaining(const std::vector & erList); + EdgeRing* findEdgeRingContaining(const std::vector & erList) const; /** * \brief @@ -184,7 +171,7 @@ public: /** \brief * Tests whether this ring is a hole. * - * Due to the way the edges in the polyongization graph are linked, + * Due to the way the edges in the polygonization graph are linked, * a ring is a hole if it is oriented counter-clockwise. * @return true if this ring is a hole */ @@ -292,7 +279,7 @@ public: * * @param hole the LinearRing forming the hole. */ - void addHole(geom::LinearRing* hole); + void addHole(std::unique_ptr hole); void addHole(EdgeRing* holeER); @@ -304,7 +291,7 @@ public: * * @return the Polygon formed by this ring and its holes. */ - std::unique_ptr getPolygon(); + std::unique_ptr getPolygon(); /** \brief * Tests if the LinearRing ring formed by this edge ring @@ -315,32 +302,20 @@ public: void computeValid(); /** \brief - * Gets the coordinates for this ring as a LineString. - * - * Used to return the coordinates in this ring - * as a valid geometry, when it has been detected that the ring - * is topologically invalid. - * @return a LineString containing the coordinates in this ring - */ - std::unique_ptr getLineString(); - - /** \brief - * Returns this ring as a LinearRing, or null if an Exception + * Returns this ring or null if an Exception * occurs while creating it (such as a topology problem). * - * Ownership of ring is retained by the object. - * Details of problems are written to standard output. + * Ownership of the ring is retained by the object. */ - const geom::LinearRing* getRingInternal() const; + const geom::Curve* getRingInternal() const; /** \brief - * Returns this ring as a LinearRing, or null if an Exception + * Returns this ring or null if an Exception * occurs while creating it (such as a topology problem). * - * Details of problems are written to standard output. - * Caller gets ownership of ring. + * Caller gets ownership of the ring. */ - std::unique_ptr getRingOwnership(); + std::unique_ptr getRingOwnership(); geom::Location locate(const geom::CoordinateXY& pt) const { return getLocator()->locate(&pt); diff --git a/include/geos/operation/polygonize/PolygonizeEdge.h b/include/geos/operation/polygonize/PolygonizeEdge.h index d58d9b8f5..7592cf2e5 100644 --- a/include/geos/operation/polygonize/PolygonizeEdge.h +++ b/include/geos/operation/polygonize/PolygonizeEdge.h @@ -26,7 +26,7 @@ // Forward declarations namespace geos { namespace geom { -class LineString; +class SimpleCurve; } } @@ -42,14 +42,14 @@ namespace polygonize { // geos::operation::polygonize class GEOS_DLL PolygonizeEdge: public planargraph::Edge { private: // Externally owned - const geom::LineString* line; + const geom::SimpleCurve* line; public: // Keep the given pointer (won't do anything to it) - PolygonizeEdge(const geom::LineString* newLine); + PolygonizeEdge(const geom::SimpleCurve* newLine); // Just return what it was given initially - const geom::LineString* getLine() const; + const geom::SimpleCurve* getLine() const; }; } // namespace geos::operation::polygonize diff --git a/include/geos/operation/polygonize/PolygonizeGraph.h b/include/geos/operation/polygonize/PolygonizeGraph.h index e85dbba1b..52746d242 100644 --- a/include/geos/operation/polygonize/PolygonizeGraph.h +++ b/include/geos/operation/polygonize/PolygonizeGraph.h @@ -33,10 +33,12 @@ // Forward declarations namespace geos { namespace geom { -class LineString; -class GeometryFactory; +class CircularString; class Coordinate; class CoordinateSequence; +class GeometryFactory; +class LineString; +class SimpleCurve; } namespace planargraph { class Node; @@ -94,6 +96,13 @@ public: */ void addEdge(const geom::LineString* line); + /** + * \brief + * Add a CircularString forming an edge of the polygon graph. + * @param geom the CircularString to add + */ + void addEdge(const geom::CircularString* geom); + /** * \brief * Computes the EdgeRings formed by the edges in this graph. @@ -108,12 +117,12 @@ public: * \brief * Finds and removes all cut edges from the graph. * - * @param cutLines : the list of the LineString forming the removed + * @param cutLines : the list of the geometries forming the removed * cut edges will be pushed here. * * TODO: document ownership of the returned LineStrings */ - void deleteCutEdges(std::vector& cutLines); + void deleteCutEdges(std::vector& cutLines); /** \brief * Marks all edges from the graph which are "dangles". @@ -124,10 +133,10 @@ public: * In order to handle large recursion depths efficiently, * an explicit recursion stack is used * - * @param dangleLines : the LineStrings that formed dangles will + * @param dangleLines : the geometries that formed dangles will * be push_back'ed here */ - void deleteDangles(std::vector& dangleLines); + void deleteDangles(std::vector& dangleLines); private: diff --git a/include/geos/operation/polygonize/Polygonizer.h b/include/geos/operation/polygonize/Polygonizer.h index 1c21a3a34..6d17ecbdb 100644 --- a/include/geos/operation/polygonize/Polygonizer.h +++ b/include/geos/operation/polygonize/Polygonizer.h @@ -72,7 +72,7 @@ namespace polygonize { // geos::operation::polygonize * - Invalid Ring Lines - edges which form rings which are invalid * (e.g. the component lines contain a self-intersection) * - * The Polygonizer constructor allows extracting only polygons which form a + * The Polygonizer constructor allows extracting only (Curve)Polygons which form a * valid polygonal result. * The set of extracted polygons is guaranteed to be edge-disjoint. * This is useful when it is known that the input lines form a valid @@ -84,23 +84,23 @@ private: /** * Add every linear element in a geometry into the polygonizer graph. */ - class GEOS_DLL LineStringAdder: public geom::GeometryComponentFilter { + class GEOS_DLL SimpleCurveAdder: public geom::GeometryComponentFilter { public: Polygonizer* pol; - explicit LineStringAdder(Polygonizer* p); + explicit SimpleCurveAdder(Polygonizer* p); //void filter_rw(geom::Geometry *g); void filter_ro(const geom::Geometry* g) override; }; // default factory - LineStringAdder lineStringAdder; + SimpleCurveAdder lineStringAdder; /** - * Add a linestring to the graph of polygon edges. + * Add a curve to the graph of polygon edges. * - * @param line the {@link LineString} to add + * @param line the {@link SimpleCurve} to add */ - void add(const geom::LineString* line); + void add(const geom::SimpleCurve* line); /** * Perform the polygonization, if it has not already been carried out. @@ -116,11 +116,11 @@ private: * discarding rings which correspond to outer rings and hence contain * duplicate linework. */ - std::vector> extractInvalidLines( - std::vector& invalidRings); + static std::vector > extractInvalidLines( + std::vector &invalidRings); /** - * Tests if a invalid ring should be included in + * Tests if an invalid ring should be included in * the list of reported invalid rings. * * Rings are included only if they contain @@ -134,7 +134,7 @@ private: * @param invalidRing the ring to test * @return true if the ring should be included */ - bool isIncludedInvalid(EdgeRing* invalidRing); + static bool isIncludedInvalid(const EdgeRing* invalidRing); void findShellsAndHoles(const std::vector& edgeRingList); @@ -142,7 +142,7 @@ private: static void findOuterShells(std::vector& shellList); - static std::vector> extractPolygons(std::vector & shellList, bool includeAll); + static std::vector> extractPolygons(std::vector & shellList, bool includeAll); bool extractOnlyPolygonal; bool computed; @@ -152,13 +152,13 @@ protected: std::unique_ptr graph; // initialize with empty collections, in case nothing is computed - std::vector dangles; - std::vector cutEdges; - std::vector> invalidRingLines; + std::vector dangles; + std::vector cutEdges; + std::vector> invalidRingLines; std::vector holeList; std::vector shellList; - std::vector> polyList; + std::vector> polyList; public: @@ -203,22 +203,34 @@ public: void add(const geom::Geometry* g); /** \brief - * Gets the list of polygons formed by the polygonization. + * Gets the list of Polygons formed by the polygonization. + * Any CurvePolygons formed will be omitted. This function + * is provided for backward-compatibility with callers + * expecting a return type of Polygon. * - * Ownership of vector is transferred to caller, subsequent - * calls will return NULL. + * Ownership of geometries is transferred to caller. Subsequent + * calls will return an empty vector. * @return a collection of Polygons */ std::vector> getPolygons(); + /** \brief + * Gets the list of (Curve)Polygons formed by the polygonization. + * + * Ownership of geometries is transferred to caller. Subsequent + * calls will return an empty vector. + * @return a collection of Polygons + */ + std::vector> getSurfaces(); + /** \brief * Get the list of dangling lines found during polygonization. * * @return a (possibly empty) collection of pointers to - * the input LineStrings which are dangles. + * the input LineStrings or CircularStrings which are dangles. * */ - const std::vector& getDangles(); + const std::vector& getDangles(); bool hasDangles(); @@ -226,10 +238,10 @@ public: * Get the list of cut edges found during polygonization. * * @return a (possibly empty) collection of pointers to - * the input LineStrings which are cut edges. + * the input LineStrings or CircularStrings which are cut edges. * */ - const std::vector& getCutEdges(); + const std::vector& getCutEdges(); bool hasCutEdges(); @@ -238,17 +250,17 @@ public: * polygonization. * * @return a (possibly empty) collection of pointers to - * the input LineStrings which form invalid rings + * the invalid rings * */ - const std::vector>& getInvalidRingLines(); + const std::vector>& getInvalidRingLines(); bool hasInvalidRingLines(); bool allInputsFormPolygons(); -// This seems to be needed by GCC 2.95.4 - friend class Polygonizer::LineStringAdder; +// This seems to be needed by GCC 2.95.4 + friend class Polygonizer::SimpleCurveAdder; }; } // namespace geos::operation::polygonize diff --git a/src/geom/util/CurveBuilder.cpp b/src/geom/util/CurveBuilder.cpp index 314001e72..0fd0f491b 100644 --- a/src/geom/util/CurveBuilder.cpp +++ b/src/geom/util/CurveBuilder.cpp @@ -47,7 +47,7 @@ CurveBuilder::add(const Curve& geom) void CurveBuilder::add(const CoordinateSequence& coords, bool isCurved) { - getSeq(isCurved).add(*coords, false); + getSeq(isCurved).add(coords, false); } void diff --git a/src/operation/polygonize/EdgeRing.cpp b/src/operation/polygonize/EdgeRing.cpp index 74457d3e9..091715936 100644 --- a/src/operation/polygonize/EdgeRing.cpp +++ b/src/operation/polygonize/EdgeRing.cpp @@ -23,13 +23,16 @@ #include #include #include +#include #include #include +#include #include #include +#include +#include #include #include // TODO: drop this, includes too much -#include #include #include @@ -49,7 +52,7 @@ namespace polygonize { // geos.operation.polygonize /*public*/ EdgeRing* -EdgeRing::findEdgeRingContaining(const std::vector & erList) +EdgeRing::findEdgeRingContaining(const std::vector & erList) const { EdgeRing* minContainingRing = nullptr; for (auto& edgeRing : erList) { @@ -110,8 +113,6 @@ EdgeRing::EdgeRing(const GeometryFactory* newFactory) : factory(newFactory), ring(nullptr), - ringPts(nullptr), - holes(nullptr), is_hole(false) { #ifdef DEBUG_ALLOC @@ -141,37 +142,70 @@ void EdgeRing::computeHole() { getRingInternal(); - is_hole = Orientation::isCCW(ring->getCoordinatesRO()); + + if (ring->getGeometryTypeId() == GEOS_COMPOUNDCURVE) { + auto coords = ring->getCoordinates(); + is_hole = Orientation::isCCW(coords.get()); + } else { + const SimpleCurve* sc = detail::down_cast(ring.get()); + is_hole = Orientation::isCCW(sc->getCoordinatesRO()); + } } /*public*/ void -EdgeRing::addHole(LinearRing* hole) +EdgeRing::addHole(std::unique_ptr hole) { - if(holes == nullptr) { - holes.reset(new std::vector>()); - } - holes->emplace_back(hole); + holes.push_back(std::move(hole)); } void EdgeRing::addHole(EdgeRing* holeER) { holeER->setShell(this); auto hole = holeER->getRingOwnership(); // TODO is this right method? - addHole(hole.release()); + addHole(std::move(hole)); } /*public*/ -std::unique_ptr +std::unique_ptr EdgeRing::getPolygon() { - if (holes) { - return factory->createPolygon(std::move(ring), std::move(*holes)); + if (!holes.empty()) { + return factory->createSurface(std::move(ring), std::move(holes)); } else { - return factory->createPolygon(std::move(ring)); + return factory->createSurface(std::move(ring)); } } +// Adapter class to check whether a point lies within a ring. +// Unlike IndexedPointInAreaLocator, SimplePointInAreaLocator does not treat +// closed rings as areas. +class PointInCurvedRingLocator : public algorithm::locate::PointOnGeometryLocator { + public: + explicit PointInCurvedRingLocator(const Curve& ring) : m_ring(ring) {} + + geom::Location locate(const geom::CoordinateXY* p) override { + return PointLocation::locateInRing(*p, m_ring); + } + + private: + const Curve& m_ring; +}; + +algorithm::locate::PointOnGeometryLocator* +EdgeRing::getLocator() const +{ + if (ringLocator == nullptr) { + const auto* rng = getRingInternal(); + if (rng->hasCurvedComponents()) { + ringLocator = std::make_unique(*rng); + } else { + ringLocator = std::make_unique(*rng); + } + } + return ringLocator.get(); +} + /*public*/ bool EdgeRing::isValid() const @@ -182,13 +216,19 @@ EdgeRing::isValid() const void EdgeRing::computeValid() { - getCoordinates(); - if (ringPts->size() <= 3) { + const Curve* r = getRingInternal(); + + if (r->getNumPoints() <= 3) { is_valid = false; return; } - getRingInternal(); - is_valid = ring->isValid(); + + if (r->getGeometryTypeId() == GEOS_LINEARRING) { + is_valid = getRingInternal()->isValid(); + } else { + // TODO: Change once IsValidOp supports curves + is_valid = true; + } } bool @@ -206,24 +246,51 @@ EdgeRing::contains(const EdgeRing& otherRing) const { bool EdgeRing::isPointInOrOut(const EdgeRing& otherRing) const { // in most cases only one or two points will be checked - for (const CoordinateXY& pt : otherRing.getCoordinates()->items()) { - geom::Location loc = locate(pt); - if (loc == geom::Location::INTERIOR) { - return true; + + struct PointTester : public CoordinateFilter { + + public: + explicit PointTester(const EdgeRing* p_ring) : m_er(p_ring), m_result(false) {} + + void filter_ro(const CoordinateXY* pt) override { + Location loc = m_er->locate(*pt); + + if (loc == geom::Location::INTERIOR) { + m_done = true; + m_result = true; + } else if (loc == geom::Location::EXTERIOR) { + m_done = true; + m_result = false; + } + + // pt is on BOUNDARY, so keep checking for a determining location } - if (loc == geom::Location::EXTERIOR) { - return false; + + bool isDone() const override { + return m_done; } - // pt is on BOUNDARY, so keep checking for a determining location - } - return false; + + bool getResult() const { + return m_result; + } + + private: + const EdgeRing* m_er; + bool m_done{false}; + bool m_result; + }; + + PointTester tester(this); + otherRing.getRingInternal()->apply_ro(&tester); + + return tester.getResult(); } /*private*/ -const CoordinateSequence* -EdgeRing::getCoordinates() const +const Curve* +EdgeRing::getRingInternal() const { - if(ringPts == nullptr) { + if (ring == nullptr) { bool hasZ = false; bool hasM = false; @@ -233,51 +300,26 @@ EdgeRing::getCoordinates() const hasM |= edge->getLine()->hasM(); } - ringPts = std::make_shared(0u, hasZ, hasM); + geom::util::CurveBuilder builder(*factory, hasZ, hasM); for(const auto& de : deList) { - auto edge = detail::down_cast(de->getEdge()); + const auto edge = detail::down_cast(de->getEdge()); + const bool isCurved = edge->getLine()->getGeometryTypeId() == GEOS_CIRCULARSTRING; + + CoordinateSequence& ringPts = builder.getSeq(isCurved); + addEdge(edge->getLine()->getCoordinatesRO(), - de->getEdgeDirection(), ringPts.get()); + de->getEdgeDirection(), &ringPts); } - } - return ringPts.get(); -} -/*public*/ -std::unique_ptr -EdgeRing::getLineString() -{ - getCoordinates(); - return factory->createLineString(ringPts); -} - -/*public*/ -const LinearRing* -EdgeRing::getRingInternal() const -{ - if(ring != nullptr) { - return ring.get(); + ring = builder.getGeometry(); } - getCoordinates(); - try { - ring = factory->createLinearRing(*ringPts); - } - catch(const geos::util::IllegalArgumentException& e) { -#if GEOS_DEBUG - // FIXME: print also ringPts - std::cerr << "EdgeRing::getRingInternal: " - << e.what() - << std::endl; -#endif - ::geos::ignore_unused_variable_warning(e); - } return ring.get(); } /*public*/ -std::unique_ptr +std::unique_ptr EdgeRing::getRingOwnership() { getRingInternal(); // active lazy generation diff --git a/src/operation/polygonize/PolygonizeEdge.cpp b/src/operation/polygonize/PolygonizeEdge.cpp index d2d210cc5..a1837c066 100644 --- a/src/operation/polygonize/PolygonizeEdge.cpp +++ b/src/operation/polygonize/PolygonizeEdge.cpp @@ -26,12 +26,12 @@ namespace geos { namespace operation { // geos.operation namespace polygonize { // geos.operation.polygonize -PolygonizeEdge::PolygonizeEdge(const LineString* newLine) +PolygonizeEdge::PolygonizeEdge(const SimpleCurve* newLine) { line = newLine; } -const LineString* +const SimpleCurve* PolygonizeEdge::getLine() const { return line; diff --git a/src/operation/polygonize/PolygonizeGraph.cpp b/src/operation/polygonize/PolygonizeGraph.cpp index b19de4749..a8c7d0b69 100644 --- a/src/operation/polygonize/PolygonizeGraph.cpp +++ b/src/operation/polygonize/PolygonizeGraph.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -156,6 +157,46 @@ PolygonizeGraph::addEdge(const LineString* line) newCoords.push_back(linePts.release()); } +void +PolygonizeGraph::addEdge(const CircularString* geom) +{ + if(geom->isEmpty()) { + return; + } + + const CoordinateSequence* linePts = geom->getCoordinatesRO(); + + const CoordinateXY& startPt = linePts->getAt(0); + const CoordinateXY& endPt = linePts->getAt(linePts->getSize() - 1); + Node* nStart = getNode(startPt); + Node* nEnd = getNode(endPt); + + DirectedEdge* de0; + DirectedEdge* de1; + + // Forward edge + { + CircularArc startArc(*linePts, 0); + CoordinateXY dirPt = startArc.getDirectionPoint(); + de0 = new PolygonizeDirectedEdge(nStart, nEnd, dirPt, true); + } + + // Reverse edge + { + CircularArc endArc = CircularArc(*linePts, linePts->getSize() - 3).reverse(); + CoordinateXY dirPt = endArc.getDirectionPoint(); + de1 = new PolygonizeDirectedEdge(nEnd, nStart, dirPt, false); + } + + newDirEdges.push_back(de0); + newDirEdges.push_back(de1); + + Edge* edge = new PolygonizeEdge(geom); + newEdges.push_back(edge); + edge->setDirectedEdges(de0, de1); + add(edge); +} + Node* PolygonizeGraph::getNode(const CoordinateXY& pt) { @@ -276,7 +317,7 @@ PolygonizeGraph::findLabeledEdgeRings(std::vector& dirEdges, /* public */ void -PolygonizeGraph::deleteCutEdges(std::vector& cutLines) +PolygonizeGraph::deleteCutEdges(std::vector& cutLines) { computeNextCWEdges(); @@ -427,12 +468,12 @@ PolygonizeGraph::findEdgeRing(PolygonizeDirectedEdge* startDE) /* public */ void -PolygonizeGraph::deleteDangles(std::vector& dangleLines) +PolygonizeGraph::deleteDangles(std::vector& dangleLines) { std::vector nodeStack; findNodesOfDegree(1, nodeStack); - std::set uniqueDangles; + std::set uniqueDangles; while(!nodeStack.empty()) { Node* node = nodeStack.back(); @@ -448,7 +489,7 @@ PolygonizeGraph::deleteDangles(std::vector& dangleLines) } // save the line as a dangle auto e = detail::down_cast(de->getEdge()); - const LineString* ls = e->getLine(); + const SimpleCurve* ls = e->getLine(); if(uniqueDangles.insert(ls).second) { dangleLines.push_back(ls); } diff --git a/src/operation/polygonize/Polygonizer.cpp b/src/operation/polygonize/Polygonizer.cpp index 92eefdfc0..4b8c7246b 100644 --- a/src/operation/polygonize/Polygonizer.cpp +++ b/src/operation/polygonize/Polygonizer.cpp @@ -22,8 +22,11 @@ #include #include #include +#include +#include #include #include +#include #include #include // std @@ -44,17 +47,25 @@ namespace geos { namespace operation { // geos.operation namespace polygonize { // geos.operation.polygonize -Polygonizer::LineStringAdder::LineStringAdder(Polygonizer* p): +Polygonizer::SimpleCurveAdder::SimpleCurveAdder(Polygonizer* p): pol(p) { } void -Polygonizer::LineStringAdder::filter_ro(const Geometry* g) +Polygonizer::SimpleCurveAdder::filter_ro(const Geometry* g) { - auto ls = dynamic_cast(g); - if(ls) { - pol->add(ls); + if (g->getGeometryTypeId() == GEOS_COMPOUNDCURVE) { + const auto& cc = static_cast(*g); + for (std::size_t i = 0; i < cc.getNumCurves(); i++) { + filter_ro(cc.getCurveN(i)); + } + + return; + } + + if(const auto sc = dynamic_cast(g)) { + pol->add(sc); } } @@ -71,14 +82,6 @@ Polygonizer::Polygonizer(bool onlyPolygonal): { } -/* - * Add a collection of geometries to be polygonized. - * May be called multiple times. - * Any dimension of Geometry may be added; - * the constituent linework will be extracted and used - * - * @param geomList a list of {@link Geometry}s with linework to be polygonized - */ void Polygonizer::add(std::vector* geomList) { @@ -87,14 +90,6 @@ Polygonizer::add(std::vector* geomList) } } -/* - * Add a collection of geometries to be polygonized. - * May be called multiple times. - * Any dimension of Geometry may be added; - * the constituent linework will be extracted and used - * - * @param geomList a list of {@link Geometry}s with linework to be polygonized - */ void Polygonizer::add(std::vector* geomList) { @@ -103,49 +98,55 @@ Polygonizer::add(std::vector* geomList) } } -/* - * Add a geometry to the linework to be polygonized. - * May be called multiple times. - * Any dimension of Geometry may be added; - * the constituent linework will be extracted and used - * - * @param g a Geometry with linework to be polygonized - */ void Polygonizer::add(const Geometry* g) { - util::ensureNoCurvedComponents(g); g->apply_ro(&lineStringAdder); } -/* - * Add a linestring to the graph of polygon edges. - * - * @param line the LineString to add - */ void -Polygonizer::add(const LineString* line) +Polygonizer::add(const SimpleCurve* line) { // create a new graph using the factory from the input Geometry if(graph == nullptr) { graph.reset(new PolygonizeGraph(line->getFactory())); } - graph->addEdge(line); + + if (line->getGeometryTypeId() == GEOS_CIRCULARSTRING) { + graph->addEdge(detail::down_cast(line)); + } else { + graph->addEdge(detail::down_cast(line)); + } } -/* - * Gets the list of polygons formed by the polygonization. - * @return a collection of Polygons - */ std::vector> Polygonizer::getPolygons() { polygonize(); + + std::vector> ret; + ret.reserve(polyList.size()); + for (auto& surf : polyList) { + if (surf->getGeometryTypeId() == GEOS_POLYGON) { + ret.emplace_back(detail::down_cast(surf.release())); + } + } + polyList.clear(); + + return ret; +} + +std::vector> +Polygonizer::getSurfaces() +{ + polygonize(); + return std::move(polyList); } + /* public */ -const std::vector& +const std::vector& Polygonizer::getDangles() { polygonize(); @@ -159,7 +160,7 @@ Polygonizer::hasDangles() { } /* public */ -const std::vector& +const std::vector& Polygonizer::getCutEdges() { polygonize(); @@ -174,7 +175,7 @@ Polygonizer::hasCutEdges() } /* public */ -const std::vector>& +const std::vector>& Polygonizer::getInvalidRingLines() { polygonize(); @@ -265,7 +266,7 @@ Polygonizer::findValidRings(const std::vector& edgeRingList, } } -std::vector> +std::vector> Polygonizer::extractInvalidLines(std::vector& invalidRings) { /** @@ -286,10 +287,15 @@ Polygonizer::extractInvalidLines(std::vector& invalidRings) * which is either valid or marked as not processed. * This avoids including outer rings which have linework which is duplicated. */ - std::vector> invalidLines; + std::vector> invalidLines; for (EdgeRing* er : invalidRings) { if (isIncludedInvalid(er)) { - invalidLines.push_back(er->getLineString()); + auto ringGeom = er->getRingOwnership(); + if (ringGeom->getGeometryTypeId() == GEOS_LINEARRING) { + ringGeom = ringGeom->getFactory()->createLineString(ringGeom->getCoordinates()); + } + + invalidLines.push_back(std::move(ringGeom)); } er->setProcessed(true); } @@ -298,7 +304,7 @@ Polygonizer::extractInvalidLines(std::vector& invalidRings) } bool -Polygonizer::isIncludedInvalid(EdgeRing* invalidRing) +Polygonizer::isIncludedInvalid(const EdgeRing* invalidRing) { for (const PolygonizeDirectedEdge* de: invalidRing->getEdges()) { const PolygonizeDirectedEdge* deAdj = static_cast(de->getSym()); @@ -358,10 +364,10 @@ Polygonizer::findOuterShells(std::vector & shells) } } -std::vector> +std::vector> Polygonizer::extractPolygons(std::vector & shells, bool includeAll) { - std::vector> polys; + std::vector> polys; for (EdgeRing* er : shells) { if (includeAll || er->isIncluded()) { polys.emplace_back(er->getPolygon()); diff --git a/tests/unit/capi/GEOSBuildAreaTest.cpp b/tests/unit/capi/GEOSBuildAreaTest.cpp index 503003dd6..934aec586 100644 --- a/tests/unit/capi/GEOSBuildAreaTest.cpp +++ b/tests/unit/capi/GEOSBuildAreaTest.cpp @@ -38,11 +38,14 @@ template<> template<> void object::test<2>() { + set_test_name("curved inputs"); + input_ = fromWKT("MULTICURVE( CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0) )"); - ensure(input_ != nullptr); result_ = GEOSBuildArea(input_); - ensure(result_ == nullptr); + ensure(result_); + + expected_ = fromWKT("CURVEPOLYGON (COMPOUNDCURVE(CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0)))"); } } // namespace tut diff --git a/tests/unit/capi/GEOSPolygonizeTest.cpp b/tests/unit/capi/GEOSPolygonizeTest.cpp index 145743ccc..eb3d07e7d 100644 --- a/tests/unit/capi/GEOSPolygonizeTest.cpp +++ b/tests/unit/capi/GEOSPolygonizeTest.cpp @@ -189,6 +189,8 @@ template<> void object::test<7> () { + set_test_name("GEOSPolygonize with curved inputs"); + constexpr int size = 2; GEOSGeometry* geoms[size]; geoms[0] = GEOSGeomFromWKT("LINESTRING (0 0, 2 0)"); @@ -198,9 +200,11 @@ void object::test<7> ensure(geom != nullptr); } - GEOSGeometry* g = GEOSPolygonize(geoms, size); + result_ = GEOSPolygonize(geoms, size); + ensure(result_); - ensure("curved geometries not supported", g == nullptr); + expected_ = fromWKT("GEOMETRYCOLLECTION( CURVEPOLYGON (COMPOUNDCURVE((0 0, 2 0), CIRCULARSTRING (2 0, 1 1, 0 0))))"); + ensure_geometry_equals(result_, expected_); for(auto& input : geoms) { GEOSGeom_destroy(input); @@ -230,5 +234,24 @@ void object::test<8> ensure_geometry_equals_identical(result_, expected_); } +template<> +template<> +void object::test<9>() +{ + set_test_name("GEOSPolygonize_valid with curved inputs"); + + input_ = fromWKT("MULTICURVE ((10 0, 0 0, 0 10, 10 10), CIRCULARSTRING (10 10, 5 5, 10 0), COMPOUNDCURVE ((10 10, 20 10, 20 0, 10 0), CIRCULARSTRING (10 0, 15 5, 10 10)))"); + + result_ = GEOSPolygonize_valid(&input_, 1); + ensure(result_); + + expected_ = fromWKT("MULTISURFACE (" + "CURVEPOLYGON (COMPOUNDCURVE ((0 0, 0 10, 10 10), CIRCULARSTRING (10 10, 5 5, 10 0), (10 0, 0 0)))," + "CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (10 0, 15 5, 10 10), (10 10, 20 10, 20 0, 10 0))))"); + + ensure_geometry_equals(result_, expected_); +} + + } // namespace tut diff --git a/tests/unit/geom/util/CurveBuilderTest.cpp b/tests/unit/geom/util/CurveBuilderTest.cpp index 4276bd9ce..80e140f08 100644 --- a/tests/unit/geom/util/CurveBuilderTest.cpp +++ b/tests/unit/geom/util/CurveBuilderTest.cpp @@ -73,7 +73,7 @@ void object::test<2>() auto expected = reader_.read("COMPOUNDCURVE ((-6 0, -5 0), CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, 6 0), CIRCULARSTRING (6 0, 0 6, -6 0))"); - ensure_equals_geometry(result.get(), expected.get()); + ensure_equals_geometry(static_cast(result.get()), expected.get()); } template<> @@ -89,7 +89,7 @@ void object::test<3>() auto expected = reader_.read("LINEARRING (0 0, 1 0, 1 1, 0 1, 0 0)"); - ensure_equals_geometry(result.get(), expected.get()); + ensure_equals_geometry(static_cast(result.get()), expected.get()); } template<> @@ -106,7 +106,7 @@ void object::test<4>() auto expected = reader_.read("LINESTRING (0 0, 1 1, 2 2)"); - ensure_equals_geometry(result.get(), expected.get()); + ensure_equals_geometry(static_cast(result.get()), expected.get()); } template<> diff --git a/tests/unit/operation/polygonize/PolygonizeTest.cpp b/tests/unit/operation/polygonize/PolygonizeTest.cpp index 970fd4797..99f110fea 100644 --- a/tests/unit/operation/polygonize/PolygonizeTest.cpp +++ b/tests/unit/operation/polygonize/PolygonizeTest.cpp @@ -137,7 +137,7 @@ struct test_polygonizetest_data { } switch(typ) { - case ResultType::POLYGONS: retGeoms = asGeometry(polygonizer.getPolygons()); break; + case ResultType::POLYGONS: retGeoms = asGeometry(polygonizer.getSurfaces()); break; case ResultType::CUT_EDGES: retGeoms = asGeometry(polygonizer.getCutEdges()); break; case ResultType::DANGLES: retGeoms = asGeometry(polygonizer.getDangles()); break; case ResultType::INVALID_RING_LINES: retGeoms = asGeometry(polygonizer.getInvalidRingLines()); break; @@ -401,5 +401,63 @@ void object::test<11>() POLYGONS); } +template<> +template<> +void object::test<12>() +{ + set_test_name("curved inputs"); + + std::vector input{ + "MULTICURVE ((10 0, 0 0, 0 10, 10 10), CIRCULARSTRING (10 10, 5 5, 10 0), COMPOUNDCURVE ((10 10, 20 10, 20 0, 10 0), CIRCULARSTRING (10 0, 15 5, 10 10)))" + }; + + std::vector expected{ + "CURVEPOLYGON (COMPOUNDCURVE ((0 0, 0 10, 10 10), CIRCULARSTRING (10 10, 5 5, 10 0), (10 0, 0 0)))", + "CURVEPOLYGON (CIRCULARSTRING (10 0, 5 5, 10 10, 15 5, 10 0))", + "CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (10 0, 15 5, 10 10), (10 10, 20 10, 20 0, 10 0)))" + }; + + doTest(input, expected, false, POLYGONS); + + // remove the hole + expected.erase(expected.begin() + 1); + doTest(input, expected, true, POLYGONS); +} + +template<> +template<> +void object::test<13>() +{ + set_test_name("curved dangle"); + + doTest({"LINESTRING (0 0, 1 0, 1 1, 0 0)", "CIRCULARSTRING (1 1, 2 2, 3 1)"}, + {"CIRCULARSTRING (1 1, 2 2, 3 1)"}, + false, + DANGLES); +} + +template<> +template<> +void object::test<14>() +{ + set_test_name("curved inputs requiring PIP test"); + + std::vector input{ + "COMPOUNDCURVE(CIRCULARSTRING (-4 -3, 0 5, 4 -3), (4 -3, -4 -3))", + "COMPOUNDCURVE((-4 -3, -1 0), CIRCULARSTRING (-1 0, 0 1, 1 0), (1 0, -4 -3))" + }; + + std::vector expected{ + "CURVEPOLYGON (COMPOUNDCURVE (CIRCULARSTRING (-4 -3, 0 5, 4 -3), (4 -3, -4 -3)), COMPOUNDCURVE ((-4 -3, 1 0), CIRCULARSTRING (1 0, 0 1, -1 0), (-1 0, -4 -3)))", + "CURVEPOLYGON (COMPOUNDCURVE ((-4 -3, -1 0), CIRCULARSTRING (-1 0, 0 1, 1 0), (1 0, -4 -3)))" + }; + + doTest(input, expected, false, POLYGONS); + + expected.erase(expected.begin() + 1); + + doTest(input, expected, true, POLYGONS); +} + } // namespace tut commit 99696486a8c454550367941384225a3c9f95ce14 Author: Daniel Baston Date: Tue Mar 31 16:41:23 2026 -0400 Add CurveBuilder diff --git a/include/geos/geom/util/CurveBuilder.h b/include/geos/geom/util/CurveBuilder.h new file mode 100644 index 000000000..dcd2416e3 --- /dev/null +++ b/include/geos/geom/util/CurveBuilder.h @@ -0,0 +1,65 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundations. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include + +#include +#include + +namespace geos::geom { +class CoordinateSequence; +class Curve; +class GeometryFactory; +class SimpleCurve; +} + +namespace geos::geom::util { + +/// The CurveBuilder is a utility class to assist in construction of simple or +/// compound curves, such as when combining coordinates from a list of Edges. +class GEOS_DLL CurveBuilder { +public: + CurveBuilder(const GeometryFactory& gfact, bool hasZ, bool hasM); + + // Add all coordinates in the provided geometry + void add(const Curve& geom); + + // Add all coordinates in the provided sequence + void add(const CoordinateSequence& seq, bool isCurved); + + // Close the ring, if necessary, using a linear segment + void closeRing(); + + // Get the result geometry + std::unique_ptr getGeometry(); + + // Get a reference to the CoordinateSequence to which + // coordinates are currently being added. + CoordinateSequence& getSeq(bool isCurved); + +private: + void finishCurve(); + void finishLine(); + + std::vector> m_curves; + std::unique_ptr m_pts{nullptr}; + const GeometryFactory& m_gfact; + const bool m_hasZ; + const bool m_hasM; + bool m_isCurved{false}; +}; + +} \ No newline at end of file diff --git a/src/geom/util/CurveBuilder.cpp b/src/geom/util/CurveBuilder.cpp new file mode 100644 index 000000000..314001e72 --- /dev/null +++ b/src/geom/util/CurveBuilder.cpp @@ -0,0 +1,148 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include + +namespace geos::geom::util { + +CurveBuilder::CurveBuilder(const GeometryFactory& gfact, bool hasZ, bool hasM) + : m_gfact(gfact), + m_hasZ(hasZ), + m_hasM(hasM) +{} + +void +CurveBuilder::add(const Curve& geom) +{ + if (geom.getGeometryTypeId() == GEOS_COMPOUNDCURVE) { + const CompoundCurve& cc = static_cast(geom); + for (std::size_t i = 0; i < cc.getNumCurves(); i++) { + add(*cc.getCurveN(i)); + } + } else { + const CoordinateSequence* coords = static_cast(geom).getCoordinatesRO(); + const bool isCurved = geom.getGeometryTypeId() == GEOS_CIRCULARSTRING; + add(*coords, isCurved); + } +} + +void +CurveBuilder::add(const CoordinateSequence& coords, bool isCurved) +{ + getSeq(isCurved).add(*coords, false); +} + +void +CurveBuilder::closeRing() +{ + if (m_curves.empty() && (m_pts == nullptr || m_pts->isEmpty())) { + return; + } + + CoordinateXYZM first; + if (!m_curves.empty()) { + m_curves.front()->getCoordinatesRO()->getAt(0, first); + } else { + m_pts->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); + } + + if (first.equals2D(last)) { + return; + } + + if (m_pts && m_isCurved) { + finishCurve(); + 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_curves.empty() && m_pts->isRing()) { + m_curves.push_back(m_gfact.createLinearRing(std::move(m_pts))); + } else { + finishLine(); + } + } + } + + 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)); +} + +CoordinateSequence& +CurveBuilder::getSeq(bool isCurved) +{ + if (m_pts) { + if (m_isCurved && !isCurved) { + finishCurve(); + } else if (isCurved && !m_isCurved) { + finishLine(); + } + } + + if (!m_pts) { + m_pts = std::make_unique(0, m_hasZ, m_hasM); + m_isCurved = isCurved; + } + + return *m_pts; +} + +} diff --git a/tests/unit/geom/util/CurveBuilderTest.cpp b/tests/unit/geom/util/CurveBuilderTest.cpp new file mode 100644 index 000000000..4276bd9ce --- /dev/null +++ b/tests/unit/geom/util/CurveBuilderTest.cpp @@ -0,0 +1,159 @@ +// tut +#include +#include +// geos +#include +#include +#include +#include + +#include +// std +#include + +using namespace geos::geom; +using geos::geom::util::CurveBuilder; + +namespace tut { +// +// Test Group +// + +// Common data used by tests +struct test_curvebuilder_data { + geos::io::WKTReader reader_; + + GeometryFactory::Ptr factory_; + CurveBuilder builder_; + + test_curvebuilder_data() : + factory_(GeometryFactory::create()), + builder_(*factory_, false, false) + {} + + void add(const std::string& wkt) { + auto g = reader_.read(wkt); + builder_.add(*g); + } + +}; + +typedef test_group group; +typedef group::object object; + +group test_curvebuilder_group("geos::geom::util::CurveBuilder"); + +template<> +template<> +void object::test<1>() +{ + set_test_name("empty result"); + + CurveBuilder builder(*factory_, false, true); + auto result = builder.getGeometry(); + + ensure(result->isEmpty()); + ensure_equals(result->getGeometryTypeId(), GEOS_LINESTRING); + ensure(!result->hasZ()); + ensure(result->hasM()); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("CompoundCurve result"); + + add("LINESTRING (-6 0, -5 0)"); + add("CIRCULARSTRING (-5 0, 0 5, 5 0)"); + add("LINESTRING (5 0, 6 0)"); + add("CIRCULARSTRING (6 0, 0 6, -6 0)"); + + auto result = builder_.getGeometry(); + + auto expected = reader_.read("COMPOUNDCURVE ((-6 0, -5 0), CIRCULARSTRING (-5 0, 0 5, 5 0), (5 0, 6 0), CIRCULARSTRING (6 0, 0 6, -6 0))"); + + ensure_equals_geometry(result.get(), expected.get()); +} + +template<> +template<> +void object::test<3>() +{ + set_test_name("LinearRing result"); + + add("LINESTRING (0 0, 1 0, 1 1)"); + add("LINESTRING (1 1, 0 1, 0 0)"); + + auto result = builder_.getGeometry(); + + auto expected = reader_.read("LINEARRING (0 0, 1 0, 1 1, 0 1, 0 0)"); + + ensure_equals_geometry(result.get(), expected.get()); +} + +template<> +template<> +void object::test<4>() +{ + set_test_name("empty component"); + + add("LINESTRING (0 0, 1 1)"); + add("LINESTRING EMPTY"); + add("LINESTRING (1 1, 2 2)"); + + auto result = builder_.getGeometry(); + + auto expected = reader_.read("LINESTRING (0 0, 1 1, 2 2)"); + + ensure_equals_geometry(result.get(), expected.get()); +} + +template<> +template<> +void object::test<5>() +{ + set_test_name("disjoint curves"); + + add("LINESTRING (-100 0, -50 0)"); + add("CIRCULARSTRING (0 0, 1 1, 2 0)"); + + ensure_THROW(builder_.getGeometry(), geos::util::IllegalArgumentException); +} + +template<> +template<> +void object::test<6>() +{ + set_test_name("closeRing after arc"); + + auto cs = reader_.read("CIRCULARSTRING (0 0, 1 1, 2 0)"); + builder_.add(*cs); + builder_.closeRing(); + + auto result = builder_.getGeometry(); + + auto expected = reader_.read("COMPOUNDCURVE(CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 0 0))"); + + ensure_equals_geometry(static_cast(result.get()), expected.get()); + +} + +template<> +template<> +void object::test<7>() +{ + set_test_name("closeRing after line"); + + auto cs = reader_.read("LINESTRING (0 0, 1 1, 2 0)"); + builder_.add(*cs); + builder_.closeRing(); + + auto result = builder_.getGeometry(); + + auto expected = reader_.read("LINEARRING (0 0, 1 1, 2 0, 0 0)"); + + ensure_equals_geometry(static_cast(result.get()), expected.get()); +} + +} // namespace tut commit dfa7c7ad43e5db1cd4ef5903ea3bf26070936faa Author: Daniel Baston Date: Wed Apr 1 11:52:01 2026 -0400 IndexedPointInAreaLocator: guard against curved inputs diff --git a/src/algorithm/locate/IndexedPointInAreaLocator.cpp b/src/algorithm/locate/IndexedPointInAreaLocator.cpp index ce9ac78e4..45b135d9a 100644 --- a/src/algorithm/locate/IndexedPointInAreaLocator.cpp +++ b/src/algorithm/locate/IndexedPointInAreaLocator.cpp @@ -99,6 +99,7 @@ IndexedPointInAreaLocator::buildIndex(const geom::Geometry& g) IndexedPointInAreaLocator::IndexedPointInAreaLocator(const geom::Geometry& g) : areaGeom(g) { + util::ensureNoCurvedComponents(g); } geom::Location commit d1a9893c4c3a9867a2149d49a9f21c6331703bd2 Author: Daniel Baston Date: Fri Mar 27 11:30:28 2026 -0400 CircularArcs: add getDirectionPoint diff --git a/include/geos/algorithm/CircularArcs.h b/include/geos/algorithm/CircularArcs.h index 60876b97e..e1d72ef91 100644 --- a/include/geos/algorithm/CircularArcs.h +++ b/include/geos/algorithm/CircularArcs.h @@ -32,6 +32,8 @@ public: static double getAngle(const geom::CoordinateXY& pt, const geom::CoordinateXY& center); + static geom::CoordinateXY getDirectionPoint(const geom::CoordinateXY& center, double radius, double theta, bool isCCW); + static double getMidpointAngle(double theta0, double theta2, bool isCCW); static geom::CoordinateXY getMidpoint(const geom::CoordinateXY& p0, const geom::CoordinateXY& p2, const geom::CoordinateXY& center, double radius, bool isCCW); diff --git a/include/geos/geom/CircularArc.h b/include/geos/geom/CircularArc.h index de75334fc..bb3d3d2c2 100644 --- a/include/geos/geom/CircularArc.h +++ b/include/geos/geom/CircularArc.h @@ -105,6 +105,11 @@ public: return m_pos; } + /// Computes a direction point suitable for ordering arcs with the same + /// origin point. The direction point is located at a distance R along the + /// line tangent to the arc at the origin point. + CoordinateXY getDirectionPoint() const; + Envelope getEnvelope() const; /// Return the length of the arc diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp index 55d7a433a..aee3b7b5d 100644 --- a/src/algorithm/CircularArcs.cpp +++ b/src/algorithm/CircularArcs.cpp @@ -39,6 +39,18 @@ CircularArcs::getAngle(const CoordinateXY& p, const CoordinateXY& center) return std::atan2(p.y - center.y, p.x - center.x); } +CoordinateXY +CircularArcs::getDirectionPoint(const geom::CoordinateXY& center, double radius, double theta, bool isCCW) +{ + const double dt = geos::MATH_PI / 4 * (isCCW ? 1: -1); + + // To get a point that is exactly on the tangent to this arc at p0, we would create a point + // at radius * sqrt(2). During overlay, this can produce a situation where noded edges originating + // from the same point have direction points that are in the same direction. Nudging the direction point + // slightly in the direction of the arc resolves this issue. + return createPoint(center, radius * std::sqrt(2) * (1 - 1e-6), theta + dt); +} + double CircularArcs::getMidpointAngle(double theta0, double theta2, bool isCCW) { diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index 1ec27bbe6..5ac8f2980 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -276,6 +276,11 @@ CircularArc::getArea() const { return R*R/2*(theta - std::sin(theta)); } +CoordinateXY +CircularArc::getDirectionPoint() const { + return algorithm::CircularArcs::getDirectionPoint(getCenter(), getRadius(), theta0(), getOrientation() == algorithm::Orientation::COUNTERCLOCKWISE); +} + Envelope CircularArc::getEnvelope() const { Envelope env; diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp index ccb3e4979..d1296a32f 100644 --- a/tests/unit/algorithm/CircularArcsTest.cpp +++ b/tests/unit/algorithm/CircularArcsTest.cpp @@ -3,11 +3,13 @@ #include #include #include +#include using geos::geom::CircularArc; using geos::geom::CoordinateXY; using geos::algorithm::CircularArcs; using geos::geom::Envelope; +using geos::geom::Quadrant; using geos::MATH_PI; namespace tut { @@ -305,5 +307,26 @@ void object::test<17>() { CoordinateXY{std::sqrt(2)/2, std::sqrt(2)/2}.distance(CoordinateXY{0.5, 0.5})); } +template<> +template<> +void object::test<18>() { + set_test_name("getDirectionPoint"); + + const CircularArc arc1 = CircularArc::create(XY{-4, 3}, XY{0, -5}, XY{-4, -3}); + const auto dp1 = arc1.getDirectionPoint(); + ensure_equals(Quadrant::quadrant(arc1.p0(), dp1), 0); // NE + ensure_equals("dp1.x", dp1.x, -1, 1e-5); + ensure_equals("dp1.y", dp1.y, 7, 1e-5); + ensure_equals("point is not expected distance from p0", arc1.p0().distance(dp1), arc1.getRadius(), 1e-5); + + + const auto arc2 = arc1.reverse(); + const auto dp2 = arc2.getDirectionPoint(); + ensure_equals(Quadrant::quadrant(arc2.p0(), dp2), 3); // SE + ensure_equals("dp2.x", dp2.x, -1, 1e-5); + ensure_equals("dp2.y", dp2.y, -7, 1e-5); + ensure_equals("point is not expected distance from p0", arc2.p0().distance(dp2), arc2.getRadius(), 1e-5); } + +} ----------------------------------------------------------------------- Summary of changes: capi/geos_c.h.in | 16 +- capi/geos_ts_c.cpp | 66 +++++--- include/geos/algorithm/CircularArcs.h | 2 + include/geos/geom/CircularArc.h | 5 + include/geos/geom/util/CurveBuilder.h | 65 ++++++++ include/geos/operation/polygonize/EdgeRing.h | 55 ++----- include/geos/operation/polygonize/PolygonizeEdge.h | 8 +- .../geos/operation/polygonize/PolygonizeGraph.h | 21 ++- include/geos/operation/polygonize/Polygonizer.h | 66 ++++---- src/algorithm/CircularArcs.cpp | 12 ++ src/algorithm/locate/IndexedPointInAreaLocator.cpp | 1 + src/geom/CircularArc.cpp | 5 + src/geom/util/CurveBuilder.cpp | 148 ++++++++++++++++++ src/operation/polygonize/EdgeRing.cpp | 172 +++++++++++++-------- src/operation/polygonize/PolygonizeEdge.cpp | 4 +- src/operation/polygonize/PolygonizeGraph.cpp | 49 +++++- src/operation/polygonize/Polygonizer.cpp | 106 +++++++------ tests/unit/algorithm/CircularArcsTest.cpp | 23 +++ tests/unit/capi/GEOSBuildAreaTest.cpp | 7 +- tests/unit/capi/GEOSPolygonizeTest.cpp | 123 ++++++++++++--- tests/unit/geom/util/CurveBuilderTest.cpp | 159 +++++++++++++++++++ tests/unit/operation/polygonize/PolygonizeTest.cpp | 60 ++++++- 22 files changed, 931 insertions(+), 242 deletions(-) create mode 100644 include/geos/geom/util/CurveBuilder.h create mode 100644 src/geom/util/CurveBuilder.cpp create mode 100644 tests/unit/geom/util/CurveBuilderTest.cpp hooks/post-receive -- GEOS From git at osgeo.org Tue Apr 7 07:41:07 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 7 Apr 2026 07:41:07 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 3cee790e4d6f87feb09864b1941cce6b3fb7fbaf Message-ID: <20260407144107.B4CAD18F7EF@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 3cee790e4d6f87feb09864b1941cce6b3fb7fbaf (commit) from 52dc9337bc485361adfa0e3e0f0c135ceb4ccbdd (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 3cee790e4d6f87feb09864b1941cce6b3fb7fbaf Author: Daniel Baston Date: Tue Apr 7 10:40:47 2026 -0400 OverlayEdge: Avoid unintended ring rotation (#1412) diff --git a/src/operation/overlayng/OverlayEdge.cpp b/src/operation/overlayng/OverlayEdge.cpp index 8716e1503..31992b881 100644 --- a/src/operation/overlayng/OverlayEdge.cpp +++ b/src/operation/overlayng/OverlayEdge.cpp @@ -55,7 +55,7 @@ OverlayEdge::getCoordinatesOriented() const void OverlayEdge::addCoordinates(CoordinateSequence* coords) const { - bool isFirstEdge = coords->size() > 0; + const bool isFirstEdge = coords->isEmpty(); if (direction) { std::size_t startIndex = 1; if (isFirstEdge) { diff --git a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp index 4c604a183..2c70f84d3 100644 --- a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp +++ b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp @@ -333,12 +333,10 @@ void object::test<24>() // https://github.com/libgeos/geos/issues/1365{ set_test_name("M value retained on last point"); - input_ = fromWKT("POLYGON ZM ((0 0 0 0, 0 1 1 1, 1 1 2 3, 1 0 4 5, 0 0 6 7))"); - expected_ = fromWKT("POLYGON ZM ((0 1 1 1, 1 1 2 3, 1 0 4 5, 0 0 6 7, 0 1 1 1))"); - + input_ = fromWKT("POLYGON ZM ((0 0 6 7, 0 1 1 1, 1 1 2 3, 1 0 4 5, 0 0 6 7))"); result_ = GEOSGeom_setPrecision(input_, 0.1, 0); - ensure(GEOSEqualsIdentical(result_, expected_)); + ensure_geometry_equals_identical(result_, input_); } } // namespace tut diff --git a/tests/unit/capi/GEOSMakeValidTest.cpp b/tests/unit/capi/GEOSMakeValidTest.cpp index 0cad2e552..44ec52c84 100644 --- a/tests/unit/capi/GEOSMakeValidTest.cpp +++ b/tests/unit/capi/GEOSMakeValidTest.cpp @@ -125,7 +125,8 @@ void object::test<7>() ensure(input_); result_ = GEOSMakeValid(input_); - expected_ = fromWKT("MULTIPOLYGON ZM (((0 0 1 2, 0.5 0.5 2.5 3.5, 1 0 2 3, 0 0 1 2)), ((1 1 4 5, 0.5 0.5 2.5 3.5, 0 1 3 4, 1 1 4 5)))"); + expected_ = fromWKT("MULTIPOLYGON ZM (((0.5 0.5 2.5 3.5, 1 0 2 3, 0 0 1 2, 0.5 0.5 2.5 3.5)), ((0.5 0.5 2.5 3.5, 0 1 3 4, 1 1 4 5, 0.5 0.5 2.5 3.5)))"); + ensure_geometry_equals_identical(result_, expected_); } diff --git a/tests/unit/capi/GEOSSubdivideByGridTest.cpp b/tests/unit/capi/GEOSSubdivideByGridTest.cpp index d734c5bca..fc4398fb6 100644 --- a/tests/unit/capi/GEOSSubdivideByGridTest.cpp +++ b/tests/unit/capi/GEOSSubdivideByGridTest.cpp @@ -50,7 +50,7 @@ void object::test<2>() result_ = GEOSSubdivideByGrid(input_, 1, 0, 5, 3, 4, 3, true); - ensure_geometry_equals_identical(expected_, result_); + ensure_geometry_equals(expected_, result_); } } \ No newline at end of file ----------------------------------------------------------------------- Summary of changes: src/operation/overlayng/OverlayEdge.cpp | 2 +- tests/unit/capi/GEOSGeom_setPrecisionTest.cpp | 6 ++---- tests/unit/capi/GEOSMakeValidTest.cpp | 3 ++- tests/unit/capi/GEOSSubdivideByGridTest.cpp | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Apr 7 10:04:24 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 7 Apr 2026 10:04:24 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 6e25f52988427e80142f15d54e9b618a89e680d5 Message-ID: <20260407170424.E53E5191414@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 6e25f52988427e80142f15d54e9b618a89e680d5 (commit) from 3cee790e4d6f87feb09864b1941cce6b3fb7fbaf (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 6e25f52988427e80142f15d54e9b618a89e680d5 Author: Daniel Baston Date: Thu Apr 2 10:48:57 2026 -0400 Refactor PathString context management and unify Noder API for path handling Moves context data to PathString base class, makes computePathNodes() and getNodedPaths() non-pure virtuals in Noder, and simplifies GeometryNoder to use a unified interface for handling both curved and linear paths. diff --git a/include/geos/noding/ArcNoder.h b/include/geos/noding/ArcNoder.h index e5f46f7fe..ea1635bbf 100644 --- a/include/geos/noding/ArcNoder.h +++ b/include/geos/noding/ArcNoder.h @@ -42,12 +42,12 @@ public: std::vector> getNodedSubstrings() override; - virtual void computePathNodes(const std::vector& inputPaths) = 0; + void computePathNodes(const std::vector& inputPaths) override = 0; - virtual std::vector> getNodedPaths() = 0; + std::vector> getNodedPaths() override = 0; protected: - ArcIntersector* m_intersector; + ArcIntersector* m_intersector{nullptr}; }; } \ No newline at end of file diff --git a/include/geos/noding/ArcString.h b/include/geos/noding/ArcString.h index e758b7307..86a23db39 100644 --- a/include/geos/noding/ArcString.h +++ b/include/geos/noding/ArcString.h @@ -32,10 +32,10 @@ public: explicit ArcString(std::vector arcs) : m_arcs(std::move(arcs)) { } - ArcString(std::vector arcs, const std::shared_ptr& seq, void* context) - : m_arcs(std::move(arcs)), - m_seq(seq), - m_context(context) + ArcString(std::vector arcs, const std::shared_ptr& seq, const void* p_context) + : PathString(p_context), + m_arcs(std::move(arcs)), + m_seq(seq) {} std::size_t getSize() const override { @@ -67,7 +67,6 @@ public: protected: std::vector m_arcs; std::shared_ptr m_seq; - void* m_context; }; } \ No newline at end of file diff --git a/include/geos/noding/GeometryNoder.h b/include/geos/noding/GeometryNoder.h index 0938302a9..007010c35 100644 --- a/include/geos/noding/GeometryNoder.h +++ b/include/geos/noding/GeometryNoder.h @@ -25,10 +25,14 @@ // Forward declarations namespace geos { +namespace algorithm { +class CircularArcIntersector; +} namespace geom { class Geometry; } namespace noding { +class ArcIntersectionAdder; class Noder; } } @@ -56,13 +60,15 @@ private: const geom::Geometry& argGeom; const bool argGeomHasCurves; + std::unique_ptr noder; + std::unique_ptr m_cai; + std::unique_ptr m_aia; + static void extractPathStrings(const geom::Geometry& g, std::vector>& to); Noder& getNoder(); - std::unique_ptr noder; - std::unique_ptr toGeometry(std::vector>& noded) const; }; diff --git a/include/geos/noding/NodableArcString.h b/include/geos/noding/NodableArcString.h index 505cbbba6..47fea6825 100644 --- a/include/geos/noding/NodableArcString.h +++ b/include/geos/noding/NodableArcString.h @@ -23,7 +23,7 @@ namespace geos::noding { class GEOS_DLL NodableArcString : public ArcString, public NodablePath { public: - NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, void* context); + NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, const void* context); std::unique_ptr clone() const; diff --git a/include/geos/noding/Noder.h b/include/geos/noding/Noder.h index e41a80e50..ecb9e98f6 100644 --- a/include/geos/noding/Noder.h +++ b/include/geos/noding/Noder.h @@ -22,6 +22,7 @@ // Forward declarations namespace geos { namespace noding { +class PathString; class SegmentString; } } @@ -65,6 +66,11 @@ public: */ virtual std::vector> getNodedSubstrings() = 0; + virtual void computePathNodes(const std::vector& inputPaths); + + virtual std::vector> getNodedPaths(); + + virtual ~Noder() {} diff --git a/include/geos/noding/PathString.h b/include/geos/noding/PathString.h index 0de86c23a..d8aedde6e 100644 --- a/include/geos/noding/PathString.h +++ b/include/geos/noding/PathString.h @@ -30,12 +30,36 @@ namespace geos::noding { /// of a noding process. class GEOS_DLL PathString { public: + explicit PathString(const void* p_context = nullptr) : context(p_context) {} + virtual ~PathString() = default; virtual std::size_t getSize() const = 0; virtual double getLength() const = 0; + /** \brief + * Gets the user-defined data for this segment string. + * + * @return the user-defined data + */ + const void* + getData() const + { + return context; + } + + /** \brief + * Sets the user-defined data for this segment string. + * + * @param data an Object containing user-defined data + */ + void + setData(const void* data) + { + context = data; + } + /// \brief /// Return a pointer to the CoordinateSequence associated /// with this PathString. @@ -43,6 +67,10 @@ public: std::vector static toRawPointerVector(const std::vector> & segStrings); + +private: + + const void* context; }; } diff --git a/include/geos/noding/SegmentString.h b/include/geos/noding/SegmentString.h index d13c70fda..73676dfe1 100644 --- a/include/geos/noding/SegmentString.h +++ b/include/geos/noding/SegmentString.h @@ -60,35 +60,11 @@ public: /// @param newSeq coordinates of this SegmentString /// SegmentString(const void* newContext, const std::shared_ptr& newSeq) - : - seq(newSeq), - context(newContext) + : PathString(newContext), + seq(newSeq) {} - virtual - ~SegmentString() {} - - /** \brief - * Gets the user-defined data for this segment string. - * - * @return the user-defined data - */ - const void* - getData() const - { - return context; - } - - /** \brief - * Sets the user-defined data for this segment string. - * - * @param data an Object containing user-defined data - */ - void - setData(const void* data) - { - context = data; - } + ~SegmentString() override = default; std::size_t size() const { // FIXME: Remove this method, or make consistent with getSize @@ -189,8 +165,6 @@ protected: std::shared_ptr seq; private: - const void* context; - static int safeOctant(const geom::CoordinateXY& p0, const geom::CoordinateXY& p1) { if(p0.equals2D(p1)) { diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 208f5885f..0d31dcde9 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -158,28 +158,11 @@ GeometryNoder::getNoded() return argGeom.clone(); std::vector> lineList; - std::vector> nodedEdges; - extractPathStrings(argGeom, lineList); - if (argGeomHasCurves) { - ArcNoder& p_noder = static_cast(getNoder()); - - algorithm::CircularArcIntersector cai(argGeom.getPrecisionModel()); - ArcIntersectionAdder aia(cai); - p_noder.setArcIntersector(aia); - - p_noder.computePathNodes(PathString::toRawPointerVector(lineList)); - nodedEdges = p_noder.getNodedPaths(); - } else { - Noder& p_noder = getNoder(); - p_noder.computeNodes(SegmentString::toRawPointerVector(lineList)); - auto nodedSegStrings = p_noder.getNodedSubstrings(); - nodedEdges.resize(nodedSegStrings.size()); - for (size_t i = 0; i < nodedSegStrings.size(); i++) { - nodedEdges[i] = std::move(nodedSegStrings[i]); - } - } + Noder& p_noder = getNoder(); + p_noder.computePathNodes(PathString::toRawPointerVector(lineList)); + auto nodedEdges = p_noder.getNodedPaths(); std::unique_ptr noded = toGeometry(nodedEdges); @@ -203,6 +186,10 @@ GeometryNoder::getNoder() const geom::PrecisionModel* pm = argGeom.getFactory()->getPrecisionModel(); if (argGeomHasCurves) { noder = std::make_unique(); + + m_cai = std::make_unique(argGeom.getPrecisionModel()); + m_aia = std::make_unique(*m_cai); + detail::down_cast(noder.get())->setArcIntersector(*m_aia); } else { noder = std::make_unique(pm); } diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index bbecddf99..488293ac3 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -31,8 +31,8 @@ pseudoAngleDiffCCW(double paStart, double pa) { return diff; } -NodableArcString::NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, void* context) : - ArcString(std::move(arcs), coords, context), +NodableArcString::NodableArcString(std::vector arcs, const std::shared_ptr& coords, bool constructZ, bool constructM, const void* p_context) : + ArcString(std::move(arcs), coords, p_context), m_constructZ(constructZ), m_constructM(constructM) { @@ -144,6 +144,7 @@ NodableArcString::getNoded(std::vector>& splitArcs) { const int orientation = toSplit.getOrientation(); bool arcIsSplit = true; + const bool preserveControlPoint = true; std::vector arcPoints; const auto it = m_adds.find(arcIndex); if (it == m_adds.end()) { @@ -157,7 +158,7 @@ NodableArcString::getNoded(std::vector>& splitArcs) { } } - if (!arcIsSplit) { + if (preserveControlPoint && !arcIsSplit) { // No nodes added, just copy the coordinates into the sequence. const geom::CoordinateSequence* srcSeq = m_arcs[arcIndex].getCoordinateSequence(); std::size_t srcPos = m_arcs[arcIndex].getCoordinatePosition(); @@ -174,6 +175,8 @@ NodableArcString::getNoded(std::vector>& splitArcs) { const CoordinateXYZM& p0 = arcPoints[i - 1]; const CoordinateXYZM& p2 = arcPoints[i]; + // TODO: Check if control point of original arc falls into this section, + // and use it instead of calculating a midpoint here? CoordinateXYZM p1(algorithm::CircularArcs::getMidpoint(p0, p2, center, radius, isCCW)); p1.z = (p0.z + p2.z) / 2; p1.m = (p0.m + p2.m) / 2; @@ -188,14 +191,17 @@ NodableArcString::getNoded(std::vector>& splitArcs) { arcs.emplace_back(*dstSeq, dstPos, center, radius, orientation); // Finish the ArcString, start a new one. - splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr)); - dstSeq = std::make_unique(0, m_constructZ, m_constructM); - arcs.clear(); + const bool isSplitPoint = i != arcPoints.size() - 1; + if (isSplitPoint) { + splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, getData())); + dstSeq = std::make_unique(0, m_constructZ, m_constructM); + arcs.clear(); + } } } if (!arcs.empty()) { - splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, nullptr)); + splitArcs.push_back(std::make_unique(std::move(arcs), std::move(dstSeq), m_constructZ, m_constructM, getData())); } } diff --git a/src/noding/Noder.cpp b/src/noding/Noder.cpp new file mode 100644 index 000000000..89119928e --- /dev/null +++ b/src/noding/Noder.cpp @@ -0,0 +1,53 @@ +/********************************************************************** +* + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences, LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include +#include +#include +#include + +namespace geos::noding { + +void +Noder::computePathNodes(const std::vector& inputPaths) +{ + std::vector segStrings(inputPaths.size()); + for (std::size_t i = 0; i < inputPaths.size(); i++) { + auto* path = inputPaths[i]; + + if (auto* ss = dynamic_cast(path)) { + segStrings[i] = ss; + } else { + throw util::UnsupportedOperationException("Noder does not support curved paths"); + } + } + + computeNodes(segStrings); +} + +std::vector> +Noder::getNodedPaths() +{ + auto nodedSS = getNodedSubstrings(); + std::vector> ret(nodedSS.size()); + for (std::size_t i = 0; i < nodedSS.size(); i++) { + ret[i] = std::move(nodedSS[i]); + } + return ret; +} + + + + +} \ No newline at end of file ----------------------------------------------------------------------- Summary of changes: include/geos/noding/ArcNoder.h | 6 ++-- include/geos/noding/ArcString.h | 9 +++--- include/geos/noding/GeometryNoder.h | 10 +++++-- include/geos/noding/NodableArcString.h | 2 +- include/geos/noding/Noder.h | 6 ++++ include/geos/noding/PathString.h | 28 ++++++++++++++++++ include/geos/noding/SegmentString.h | 32 ++------------------ src/noding/GeometryNoder.cpp | 27 +++++------------ src/noding/NodableArcString.cpp | 20 ++++++++----- src/noding/Noder.cpp | 53 ++++++++++++++++++++++++++++++++++ 10 files changed, 126 insertions(+), 67 deletions(-) create mode 100644 src/noding/Noder.cpp hooks/post-receive -- GEOS From git at osgeo.org Sat Apr 11 19:11:05 2026 From: git at osgeo.org (git at osgeo.org) Date: Sat, 11 Apr 2026 19:11:05 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 46033aa6758b0564a165282e0a39b5d8944a71d8 Message-ID: <20260412021105.8385E139895@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 46033aa6758b0564a165282e0a39b5d8944a71d8 (commit) from 6e25f52988427e80142f15d54e9b618a89e680d5 (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 46033aa6758b0564a165282e0a39b5d8944a71d8 Author: Mike Taves Date: Sun Apr 12 14:10:30 2026 +1200 .github/dependabot.yml: make it create a grouped PR with the various updates Credits to qgis/QGIS#65676 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8ac6b8c49..7f2f6b201 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,3 +4,9 @@ updates: directory: "/" schedule: interval: "monthly" + cooldown: + default-days: 7 + groups: + all-actions: + patterns: + - "*" ----------------------------------------------------------------------- Summary of changes: .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) hooks/post-receive -- GEOS From git at osgeo.org Mon Apr 13 00:16:31 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 13 Apr 2026 00:16:31 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 255aa2f7ba360b6eaebd6b23ed73cfe9504d5acd Message-ID: <20260413071631.9E20813AB77@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 255aa2f7ba360b6eaebd6b23ed73cfe9504d5acd (commit) from 46033aa6758b0564a165282e0a39b5d8944a71d8 (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 255aa2f7ba360b6eaebd6b23ed73cfe9504d5acd Author: Sandro Santilli Date: Mon Apr 13 09:15:33 2026 +0200 Fix ensure_equals to correcly report actual vs. expected diff --git a/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp b/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp index ec8551eaf..5dddfac2f 100644 --- a/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp +++ b/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp @@ -45,7 +45,7 @@ struct test_isccw_data { ensure("WKT must be POLYGON)", poly != nullptr); const geos::geom::CoordinateSequence* cs = poly->getExteriorRing()->getCoordinatesRO(); bool actualCCW = Orientation::isCCW(cs); - ensure_equals("CoordinateSequence isCCW", expectedCCW, actualCCW); + ensure_equals("CoordinateSequence isCCW", actualCCW, expectedCCW); } void ----------------------------------------------------------------------- Summary of changes: tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Mon Apr 13 05:22:08 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 13 Apr 2026 05:22:08 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 243b13831a5e8ddc6c3b80822b09452e22c51243 Message-ID: <20260413122209.3E425189500@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 243b13831a5e8ddc6c3b80822b09452e22c51243 (commit) from 255aa2f7ba360b6eaebd6b23ed73cfe9504d5acd (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 243b13831a5e8ddc6c3b80822b09452e22c51243 Author: Sandro Santilli Date: Mon Apr 13 14:21:27 2026 +0200 More ensure_equals corrections to report proper actual / expected diff --git a/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp b/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp index 5dddfac2f..6c0dae230 100644 --- a/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp +++ b/tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp @@ -55,7 +55,7 @@ struct test_isccw_data { ensure("WKT must be POLYGON)", poly != nullptr); const geos::geom::CoordinateSequence* cs = poly->getExteriorRing()->getCoordinatesRO(); bool actualCCWArea = Orientation::isCCWArea(cs); - ensure_equals("CoordinateSequence isCCWArea", expectedCCWArea, actualCCWArea); + ensure_equals("CoordinateSequence isCCWArea", actualCCWArea, expectedCCWArea); } void @@ -64,7 +64,7 @@ struct test_isccw_data { GeometryPtr geom(breader_.readHEX(wkt)); auto cs = geom->getCoordinates(); bool actualCCW = Orientation::isCCW(cs.get()); - ensure_equals("CoordinateSequence isCCW", expectedCCW, actualCCW); + ensure_equals("CoordinateSequence isCCW", actualCCW, expectedCCW); } }; ----------------------------------------------------------------------- Summary of changes: tests/unit/algorithm/CGAlgorithms/OrientationIsCCWTest.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Mon Apr 20 09:57:36 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 20 Apr 2026 09:57:36 -0700 (PDT) Subject: [geos-commits] [SCM] GEOS branch main updated. 32ccf683317a2f649fea96b8a930f6515823a451 Message-ID: <20260420165736.8A8DC16DB67@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 32ccf683317a2f649fea96b8a930f6515823a451 (commit) via 4df44620c288e4d892397e6acc62c21bfe54d8c0 (commit) via 2e8c9c11bba750f70283b5945ee840d3c26335e6 (commit) via 1a91de165de49f7f37d3975f162731dc1c4fc7a0 (commit) via 8aa6ee310e49089c5d9e18a0ce7bf6aff1682534 (commit) from 243b13831a5e8ddc6c3b80822b09452e22c51243 (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 32ccf683317a2f649fea96b8a930f6515823a451 Author: Daniel Baston Date: Thu Apr 9 13:16:21 2026 -0400 WKBReader: Add std::string overload for readHEX diff --git a/include/geos/io/WKBReader.h b/include/geos/io/WKBReader.h index 9d410c250..58eeb8f74 100644 --- a/include/geos/io/WKBReader.h +++ b/include/geos/io/WKBReader.h @@ -124,6 +124,16 @@ public: */ std::unique_ptr readHEX(std::istream& is); + /** + * \brief Reads a Geometry hex format. + * + * @param hex the string to read + * @return the Geometry read + * @throws IOException + * @throws ParseException + */ + std::unique_ptr readHEX(const std::string& hex); + /** * \brief Print WKB in HEX form to out stream * diff --git a/src/io/WKBReader.cpp b/src/io/WKBReader.cpp index 5410ab0c7..65ba6c4c1 100644 --- a/src/io/WKBReader.cpp +++ b/src/io/WKBReader.cpp @@ -184,6 +184,13 @@ WKBReader::readHEX(std::istream& is) return this->read(os); } +std::unique_ptr +WKBReader::readHEX(const std::string& hex) +{ + std::istringstream ss(hex); + return readHEX(ss); +} + void WKBReader::minMemSize(geom::GeometryTypeId geomType, uint64_t size) const { commit 4df44620c288e4d892397e6acc62c21bfe54d8c0 Author: Daniel Baston Date: Thu Apr 9 12:23:04 2026 -0400 Add LinealExtracter diff --git a/include/geos/geom/util/LinealExtracter.h b/include/geos/geom/util/LinealExtracter.h new file mode 100644 index 000000000..ff50a357a --- /dev/null +++ b/include/geos/geom/util/LinealExtracter.h @@ -0,0 +1,51 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#pragma once + +#include +#include +#include + +namespace geos { +namespace geom { // geos.geom +namespace util { // geos.geom.util + +/** + * \brief Extracts the lineal (LineString/LinearRing/CircularString/CompoundCurve/MultiLineString/MultiCurve) + * elements from a Geometry. + */ +class GEOS_DLL LinealExtracter { + +public: + + /** + * Pushes the lineal elements from a geometry into the provided vector. + * + * @param geom the geometry to extract from + * @param lineals the vector to add the polygonal elements to + */ + static void getLineals(const Geometry& geom, std::vector& lineals); + + static void getLineals(const Geometry* geom, std::vector& lineals); + + // Declare type as noncopyable + LinealExtracter(const LinealExtracter& other) = delete; + LinealExtracter& operator=(const LinealExtracter& rhs) = delete; +}; + +} // namespace geos.geom.util +} // namespace geos.geom +} // namespace geos + diff --git a/src/geom/util/LinealExtracter.cpp b/src/geom/util/LinealExtracter.cpp new file mode 100644 index 000000000..5f62cc85b --- /dev/null +++ b/src/geom/util/LinealExtracter.cpp @@ -0,0 +1,51 @@ +/********************************************************************** + * + * GEOS - Geometry Engine Open Source + * http://geos.osgeo.org + * + * Copyright (C) 2026 ISciences LLC + * + * This is free software; you can redistribute and/or modify it under + * the terms of the GNU Lesser General Public Licence as published + * by the Free Software Foundation. + * See the COPYING file for more information. + * + **********************************************************************/ + +#include + +#include +#include +#include + +#include + + +namespace geos { +namespace geom { // geos.geom +namespace util { // geos.geom.util + +void +LinealExtracter::getLineals(const Geometry& geom, std::vector& lineals) +{ + getLineals(&geom, lineals); +} + +void +LinealExtracter::getLineals(const Geometry* geom, std::vector& lineals) +{ + if (dynamic_cast(geom) != nullptr + || dynamic_cast(geom) != nullptr + || dynamic_cast(geom) != nullptr) { + lineals.push_back(geom); + } + else if (dynamic_cast(geom) != nullptr) { + for (std::size_t i = 0; i < geom->getNumGeometries(); i++) { + getLineals(geom->getGeometryN(i), lineals); + } + } +} + +} +} +} diff --git a/tests/unit/geom/util/LinealExtracterTest.cpp b/tests/unit/geom/util/LinealExtracterTest.cpp new file mode 100644 index 000000000..941de32af --- /dev/null +++ b/tests/unit/geom/util/LinealExtracterTest.cpp @@ -0,0 +1,70 @@ +// +// Test Suite for geos::geom::util::LinealExtracter class. + +#include + +#include +#include +#include + +#include "utility.h" + +namespace tut { + +struct test_linealextracter_data { + geos::io::WKTReader reader_; +}; + +typedef test_group group; +typedef group::object object; + +group test_linealextracter_group("geos::geom::util::LinealExtracter"); + +template<> +template<> +void object::test<1>() +{ + auto input = reader_.read( + "GEOMETRYCOLLECTION (" + "POINT (1 1)," + "LINESTRING (0 0, 1 1)," + "POLYGON ((0 0, 1 0, 1 1, 0 0))," + "CIRCULARSTRING (0 0, 1 1, 2 0)," + "COMPOUNDCURVE(CIRCULARSTRING (0 0, 5 5, 10 0), (10 0, 20 0))" + ")"); + + std::vector lineals; + geos::geom::util::LinealExtracter::getLineals(*input, lineals); + + ensure_equals(lineals.size(), 3u); + ensure_equals_geometry(lineals[0], input->getGeometryN(1)); + ensure_equals_geometry(lineals[1], input->getGeometryN(3)); + ensure_equals_geometry(lineals[2], input->getGeometryN(4)); +} + +template<> +template<> +void object::test<2>() +{ + set_test_name("nested inputs"); + + auto input = reader_.read( + "GEOMETRYCOLLECTION (" + "MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))," + "GEOMETRYCOLLECTION (" + "LINESTRING (4 4, 5 5)," + "POINT (6 6)," + "MULTILINESTRING ((7 7, 8 8))" + ")" + ")"); + + std::vector lineals; + geos::geom::util::LinealExtracter::getLineals(*input, lineals); + + ensure_equals(lineals.size(), 3u); + ensure_equals_geometry(lineals[0], input->getGeometryN(0)); + ensure_equals_geometry(lineals[1], input->getGeometryN(1)->getGeometryN(0)); + ensure_equals_geometry(lineals[2], input->getGeometryN(1)->getGeometryN(2)); +} + +} // namespace tut commit 2e8c9c11bba750f70283b5945ee840d3c26335e6 Author: Daniel Baston Date: Thu Apr 9 11:54:38 2026 -0400 DistanceOp: Add nearestLocations(), mark const methods diff --git a/include/geos/operation/distance/DistanceOp.h b/include/geos/operation/distance/DistanceOp.h index b4d6fed8b..03d3d2c92 100644 --- a/include/geos/operation/distance/DistanceOp.h +++ b/include/geos/operation/distance/DistanceOp.h @@ -104,7 +104,15 @@ public: double distance); /** - * Compute the the nearest points of two geometries. + * Report the locations of the nearest points in the input geometries. + * The locations are presented in the same order as the input geometries. + * + * @return a pair of {@link GeometryLocation}s for the nearest points + */ + const std::array& nearestLocations(); + + /** + * Compute the nearest points of two geometries. * * The points are presented in the same order as the input Geometries. * diff --git a/include/geos/operation/distance/GeometryLocation.h b/include/geos/operation/distance/GeometryLocation.h index 1fb443762..6aef5123e 100644 --- a/include/geos/operation/distance/GeometryLocation.h +++ b/include/geos/operation/distance/GeometryLocation.h @@ -94,7 +94,7 @@ public: /** * Returns the geometry component on (or in) which this location occurs. */ - const geom::Geometry* getGeometryComponent(); + const geom::Geometry* getGeometryComponent() const; /** * Returns the segment index for this location. @@ -104,20 +104,20 @@ public: * * @return the segment index for the location, or INSIDE_AREA */ - std::size_t getSegmentIndex(); + std::size_t getSegmentIndex() const; /** * Returns the geom::Coordinate of this location. */ - geom::CoordinateXY& getCoordinate(); + const geom::CoordinateXY& getCoordinate() const; /** \brief * Tests whether this location represents a point * inside an area geometry. */ - bool isInsideArea(); + bool isInsideArea() const; - std::string toString(); + std::string toString() const; }; } // namespace geos::operation::distance diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp index 2eb82eaac..fa25f5be2 100644 --- a/src/operation/distance/DistanceOp.cpp +++ b/src/operation/distance/DistanceOp.cpp @@ -68,6 +68,14 @@ DistanceOp::distance(const Geometry& g0, const Geometry& g1) return distOp.distance(); } +/*public static*/ +const std::array& +DistanceOp::nearestLocations() +{ + computeMinDistance(); + return minDistanceLocation; +} + /*public static*/ std::unique_ptr DistanceOp::nearestPoints(const Geometry* g0, const Geometry* g1) diff --git a/src/operation/distance/GeometryLocation.cpp b/src/operation/distance/GeometryLocation.cpp index 6c46b5e0e..12dafa106 100644 --- a/src/operation/distance/GeometryLocation.cpp +++ b/src/operation/distance/GeometryLocation.cpp @@ -54,7 +54,7 @@ GeometryLocation::GeometryLocation(const Geometry* newComponent, const Coordinat * Returns the geometry associated with this location. */ const Geometry* -GeometryLocation::getGeometryComponent() +GeometryLocation::getGeometryComponent() const { return component; } @@ -65,27 +65,27 @@ GeometryLocation::getGeometryComponent() * @return the segment index for the location, or INSIDE_AREA */ size_t -GeometryLocation::getSegmentIndex() +GeometryLocation::getSegmentIndex() const { return segIndex; } /** * Returns the location. */ -CoordinateXY& -GeometryLocation::getCoordinate() +const CoordinateXY& +GeometryLocation::getCoordinate() const { return pt; } bool -GeometryLocation::isInsideArea() +GeometryLocation::isInsideArea() const { return inside_area; } std::string -GeometryLocation::toString() +GeometryLocation::toString() const { geos::io::WKTWriter writer; std::ostringstream ss; commit 1a91de165de49f7f37d3975f162731dc1c4fc7a0 Author: Daniel Baston Date: Thu Apr 9 10:23:40 2026 -0400 GeometryNoder: Handle CompoundCurve inputs diff --git a/src/noding/GeometryNoder.cpp b/src/noding/GeometryNoder.cpp index 0d31dcde9..6c990cbae 100644 --- a/src/noding/GeometryNoder.cpp +++ b/src/noding/GeometryNoder.cpp @@ -25,11 +25,13 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -43,7 +45,6 @@ #include // for unique_ptr #include -#include "geos/noding/ArcIntersectionAdder.h" namespace geos { namespace noding { // geos.noding @@ -80,6 +81,10 @@ public: auto as = std::make_unique(std::move(arcs), coords, _constructZ, _constructM, nullptr); _to.push_back(std::move(as)); + } else if (const auto* cc = dynamic_cast(g)) { + for (std::size_t i = 0; i < cc->getNumCurves(); i++) { + filter_ro(cc->getCurveN(i)); + } } } private: commit 8aa6ee310e49089c5d9e18a0ce7bf6aff1682534 Author: Daniel Baston Date: Wed Apr 8 12:23:08 2026 -0400 CoordinateSequence: add setZM method diff --git a/include/geos/geom/CoordinateSequence.h b/include/geos/geom/CoordinateSequence.h index b19b0f5ec..e52849e7f 100644 --- a/include/geos/geom/CoordinateSequence.h +++ b/include/geos/geom/CoordinateSequence.h @@ -49,7 +49,7 @@ namespace geom { // geos::geom * stores 2D Coordinates and accesses using the Coordinate type. Sequences used by these parts * of the code must be created with constructors without `hasz` and `hasm` arguments. * - * If a high-dimension Coordinate coordinate is read from a low-dimension CoordinateSequence, + * If a high-dimension coordinate is read from a low-dimension CoordinateSequence, * the higher dimensions will be populated with incorrect values or a segfault may occur. * */ @@ -221,6 +221,13 @@ public: return m_hasm; } + /** + * Set the dimensions of the CoordinateSequence. + * May cause a reallocation, invalidating any references to coordinates + * within this sequence. + */ + void setZM(bool hasZ, bool hasM); + /// Returns true if contains any two consecutive points bool hasRepeatedPoints() const; diff --git a/src/geom/CoordinateSequence.cpp b/src/geom/CoordinateSequence.cpp index cd97014e3..c5d7a9c8b 100644 --- a/src/geom/CoordinateSequence.cpp +++ b/src/geom/CoordinateSequence.cpp @@ -630,6 +630,25 @@ CoordinateSequence::setPoints(const std::vector& v) } } +void +CoordinateSequence::setZM(bool p_hasZ, bool p_hasM) +{ + if (p_hasZ != hasZ() || p_hasM != hasM()) { + CoordinateSequence newSeq(0, p_hasZ, p_hasM); + newSeq.add(*this); + *this = std::move(newSeq); + } + + // Make sure we don't carry over Z values that were hiding in a + // self-declared 2D sequence. + if (!p_hasZ && (getCoordinateType() == CoordinateType::XYZ || getCoordinateType() == CoordinateType::XYZM)) { + const Coordinate& nullCoord = Coordinate::getNull(); + for (std::size_t i = 2; i < m_vect.size(); i += stride()) { + m_vect[i] = nullCoord.z; + } + } +} + void CoordinateSequence::swap(std::size_t i, std::size_t j) { diff --git a/tests/unit/geom/CoordinateSequenceTest.cpp b/tests/unit/geom/CoordinateSequenceTest.cpp index 89c35ba8d..7fae91aec 100644 --- a/tests/unit/geom/CoordinateSequenceTest.cpp +++ b/tests/unit/geom/CoordinateSequenceTest.cpp @@ -1642,4 +1642,37 @@ void object::test<62> ensure_equals_xyzm(seq.getAt(1), p2); ensure_equals_xyzm(seq.getAt(2), p1); } + +template<> +template<> +void object::test<63>() +{ + set_test_name("setZM: add ZM to XY sequence"); + + CoordinateSequence seq(0, false, false); + seq.add(CoordinateXY{1, 2}); + seq.add(CoordinateXY{3, 4}); + seq.setZM(true, true); + + ensure_equals_xyzm(seq.getAt(0), CoordinateXYZM(1, 2, DoubleNotANumber, DoubleNotANumber)); + ensure_equals_xyzm(seq.getAt(1), CoordinateXYZM(3, 4, DoubleNotANumber, DoubleNotANumber)); +} + +template<> +template<> +void object::test<64>() +{ + set_test_name("setZM: remove Z from XY sequence with hidden Z value"); + + CoordinateSequence seq(0, false, false); + seq.add(CoordinateXY{1, 2}); + seq.add(Coordinate{3, 4, 5}); + + seq.setZM(false, false); + + ensure_equals_xyz(seq.getAt(0), Coordinate(1, 2, DoubleNotANumber)); + ensure_equals_xyz(seq.getAt(1), Coordinate(3, 4, DoubleNotANumber)); +} + + } // namespace tut ----------------------------------------------------------------------- Summary of changes: include/geos/geom/CoordinateSequence.h | 9 ++- .../{PolygonalExtracter.h => LinealExtracter.h} | 23 ++++--- include/geos/io/WKBReader.h | 10 ++++ include/geos/operation/distance/DistanceOp.h | 10 +++- include/geos/operation/distance/GeometryLocation.h | 10 ++-- src/geom/CoordinateSequence.cpp | 19 ++++++ ...{PolygonalExtracter.cpp => LinealExtracter.cpp} | 28 +++++---- src/io/WKBReader.cpp | 7 +++ src/noding/GeometryNoder.cpp | 7 ++- src/operation/distance/DistanceOp.cpp | 8 +++ src/operation/distance/GeometryLocation.cpp | 12 ++-- tests/unit/geom/CoordinateSequenceTest.cpp | 33 ++++++++++ tests/unit/geom/util/LinealExtracterTest.cpp | 70 ++++++++++++++++++++++ 13 files changed, 206 insertions(+), 40 deletions(-) copy include/geos/geom/util/{PolygonalExtracter.h => LinealExtracter.h} (53%) copy src/geom/util/{PolygonalExtracter.cpp => LinealExtracter.cpp} (52%) create mode 100644 tests/unit/geom/util/LinealExtracterTest.cpp hooks/post-receive -- GEOS