From git at osgeo.org Sun Nov 1 07:48:41 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 1 Nov 2026 07:48:41 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/all-actions-c2c22ea26e created. 87cce888a8b58f6814e6bf0af6c495d30d3796e1 Message-ID: <20261101154841.6FCA91DC79A@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, dependabot/github_actions/all-actions-c2c22ea26e has been created at 87cce888a8b58f6814e6bf0af6c495d30d3796e1 (commit) - Log ----------------------------------------------------------------- commit 87cce888a8b58f6814e6bf0af6c495d30d3796e1 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun Nov 1 15:48:13 2026 +0000 Bump the all-actions group with 3 updates Bumps the all-actions group with 3 updates: [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm), [github/codeql-action](https://github.com/github/codeql-action) and [docker/login-action](https://github.com/docker/login-action). Updates `vmactions/freebsd-vm` from 1.5.7 to 1.5.9 - [Release notes](https://github.com/vmactions/freebsd-vm/releases) - [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.5.7...v1.5.9) Updates `github/codeql-action` from 4.38.2 to 4.39.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.38.2...v4.39.1) Updates `docker/login-action` from 4.6.0 to 4.7.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v4.6.0...v4.7.0) --- updated-dependencies: - dependency-name: vmactions/freebsd-vm dependency-version: 1.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action dependency-version: 4.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions - dependency-name: docker/login-action dependency-version: 4.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions ... Signed-off-by: dependabot[bot] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 96ccada6e..4148575ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,7 +556,7 @@ jobs: restore-keys: freebsd - name: Build, test, install - uses: vmactions/freebsd-vm at v1.5.7 + uses: vmactions/freebsd-vm at v1.5.9 with: release: "15.0" envs: CCACHE_COMPRESS CCACHE_COMPRESSLEVEL CCACHE_MAXSIZE diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5c5d0ec71..c19fed357 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init at v4.38.2 + uses: github/codeql-action/init at v4.39.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,6 +63,6 @@ jobs: make -j$(nproc) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze at v4.38.2 + uses: github/codeql-action/analyze at v4.39.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 79e6d1c09..73fc993c5 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -40,7 +40,7 @@ jobs: type=sha,prefix= - name: Login to GitHub Container Registry - uses: docker/login-action at v4.6.0 + uses: docker/login-action at v4.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Sun Nov 1 18:20:28 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 1 Nov 2026 18:20:28 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 1bd84740d81df4eec08f82d19e425c7c74cadbf0 Message-ID: <20261102022028.EB9921CA107@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 1bd84740d81df4eec08f82d19e425c7c74cadbf0 (commit) from 2289bad7aac673a1bffcbbdd96944fcc57405a9c (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 1bd84740d81df4eec08f82d19e425c7c74cadbf0 Author: Daniel Baston Date: Sat Oct 31 16:00:34 2026 -0400 NodableArcString: Handle degenerate arcs with repeated points Co-authored-by: Alejandro Pacheco diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 5b41a62f8..7c91634ee 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -268,7 +268,7 @@ public: m_coords = std::make_unique(0, m_constructZ, m_constructM); } - m_coords->add(seq, pos, pos + 2, false); + m_coords->add(seq, pos + !m_coords->isEmpty(), pos + 2, true); m_arcs.emplace_back(*m_coords, m_coords->getSize() - 3, center, radius, orientation); } diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index 10166adbc..97401d714 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -414,5 +414,28 @@ void object::test<19>() reinterpret_cast(expected_), 1e-4); } +template<> +template<> +void object::test<20>() +{ + set_test_name("CircularString with repeated points"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0), LINESTRING (3 0, 3 2))"); + + result_ = GEOSNode(input_); + ensure(result_); + + ensure_equals(GEOSGetNumGeometries(result_), 4); + + expected_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (0 0, 0 0, 2 0, 2.2928932188 0.7071067812, 3 1)," + "CIRCULARSTRING (3 1, 3.7071067812 0.7071067812, 4 0)," + "(3 0, 3 1)," + "(3 1, 3 2))"); + + 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 c1f1c02ce..31b8b9832 100644 --- a/tests/unit/noding/NodableArcStringTest.cpp +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -12,6 +12,7 @@ using geos::geom::CoordinateXY; using geos::geom::CoordinateXYZM; using geos::noding::ArcString; using geos::noding::NodableArcString; +using namespace std::string_literals; namespace tut { @@ -223,4 +224,72 @@ void object::test<5>() }); } -} \ No newline at end of file +template<> +template<> +void object::test<6>() +{ + set_test_name("getNoded keeps all three points of arcs with repeated points"); + + for (const char* wkt : { + "CIRCULARSTRING (0 0, 0 0, 0 0)", // 0 == 1 == 2 + "CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0)", // 0 == 1 + "CIRCULARSTRING (0 0, 1 1, 1 1, 3 1, 4 0)", // 1 == 2 + "CIRCULARSTRING (0 0, 1 1, 2 0, 2 0, 4 0)", // 2 == 3 + "CIRCULARSTRING (0 0, 1 1, 2 0, 3 1, 3 1)" // 3 == 4 + }) { + const auto g = reader_.read(wkt); + + NodableArcString nas(g->getArcs(), g->getSharedCoordinates(), false, false, nullptr); + + std::vector> noded; + nas.getNoded(noded); + + ensure_equals("noded.size(): "s + wkt, noded.size(), 1u); + ensure_equals("noded[0]->getSize(): "s + wkt, noded[0]->getSize(), g->getArcs().size()); + ensure("noded[0]->getCoordinates(): "s + wkt, noded[0]->getCoordinates()->equalsIdentical(*g->getCoordinatesRO())); + for (std::size_t i = 0; i < noded[0]->getSize(); i++) { + ensure_equals(wkt, noded[0]->getArc(i).getCoordinatePosition(), 2*i); + } + } +} + +template<> +template<> +void object::test<7>() +{ + set_test_name("getNoded keeps arc with repeated points before a split arc"); + + const auto g = reader_.read("CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0)"); + + NodableArcString nas(g->getArcs(), g->getSharedCoordinates(), false, false, nullptr); + nas.addIntersection(CoordinateXY{3, 1}, 1); + + std::vector> noded; + nas.getNoded(noded); + + ensure_equals(noded.size(), 2u); + + const auto& seq = *noded[0]->getCoordinates(); + ensure_equals(noded[0]->getSize(), 2u); + ensure_equals(seq.size(), 5u); + + const CircularArc& arc0 = noded[0]->getArc(0); + ensure_equals(arc0.getCoordinatePosition(), 0u); + ensure_equals(arc0.p0(), XY{0, 0}); + ensure_equals(arc0.p1(), XY{0, 0}); + ensure_equals(arc0.p2(), XY{2, 0}); + + const CircularArc& arc1 = noded[0]->getArc(1); + ensure_equals(arc1.getCoordinatePosition(), 2u); + ensure_equals(arc1.p0(), XY{2, 0}); + ensure_equals(arc1.p2(), XY{3, 1}); + + ensure_equals(noded[1]->getSize(), 1u); + ensure_equals(noded[1]->getCoordinates()->size(), 3u); + + const CircularArc& arc2 = noded[1]->getArc(0); + ensure_arc_equals(arc2, CircularArc::create(XY{3, 1}, XY{4, 0}, XY{3, 0}, 1, Orientation::CLOCKWISE), 1e-8); + ensure_equals(arc2.p2(), XY{4, 0}); +} + +} // namespace tut ----------------------------------------------------------------------- Summary of changes: src/noding/NodableArcString.cpp | 2 +- tests/unit/capi/GEOSNodeTest.cpp | 23 ++++++++++ tests/unit/noding/NodableArcStringTest.cpp | 71 +++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Sun Nov 1 18:23:27 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 1 Nov 2026 18:23:27 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch dependabot/github_actions/all-actions-c2c22ea26e deleted. 87cce888a8b58f6814e6bf0af6c495d30d3796e1 Message-ID: <20261102022327.BC96E1CA152@trac.osgeo.org> This is an automated email from the git hooks/post-receive script. It was generated because a ref change was pushed to the repository containing the project "GEOS". The branch, dependabot/github_actions/all-actions-c2c22ea26e has been deleted was 87cce888a8b58f6814e6bf0af6c495d30d3796e1 - Log ----------------------------------------------------------------- 87cce888a8b58f6814e6bf0af6c495d30d3796e1 Bump the all-actions group with 3 updates ----------------------------------------------------------------------- hooks/post-receive -- GEOS From git at osgeo.org Sun Nov 1 18:23:27 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 1 Nov 2026 18:23:27 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 1441ad5b48a86e8ff99a2f693fe777377d0f766f Message-ID: <20261102022327.E45051CA1AF@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 1441ad5b48a86e8ff99a2f693fe777377d0f766f (commit) from 1bd84740d81df4eec08f82d19e425c7c74cadbf0 (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 1441ad5b48a86e8ff99a2f693fe777377d0f766f Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun Nov 1 21:22:58 2026 -0500 CI: Bump the all-actions group with 3 updates (#1587) Bumps the all-actions group with 3 updates: [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm), [github/codeql-action](https://github.com/github/codeql-action) and [docker/login-action](https://github.com/docker/login-action). Updates `vmactions/freebsd-vm` from 1.5.7 to 1.5.9 - [Release notes](https://github.com/vmactions/freebsd-vm/releases) - [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.5.7...v1.5.9) Updates `github/codeql-action` from 4.38.2 to 4.39.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v4.38.2...v4.39.1) Updates `docker/login-action` from 4.6.0 to 4.7.0 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v4.6.0...v4.7.0) --- updated-dependencies: - dependency-name: vmactions/freebsd-vm dependency-version: 1.5.9 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: all-actions - dependency-name: github/codeql-action dependency-version: 4.39.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions - dependency-name: docker/login-action dependency-version: 4.7.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: all-actions ... 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 96ccada6e..4148575ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -556,7 +556,7 @@ jobs: restore-keys: freebsd - name: Build, test, install - uses: vmactions/freebsd-vm at v1.5.7 + uses: vmactions/freebsd-vm at v1.5.9 with: release: "15.0" envs: CCACHE_COMPRESS CCACHE_COMPRESSLEVEL CCACHE_MAXSIZE diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 5c5d0ec71..c19fed357 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -40,7 +40,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init at v4.38.2 + uses: github/codeql-action/init at v4.39.1 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -63,6 +63,6 @@ jobs: make -j$(nproc) - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze at v4.38.2 + uses: github/codeql-action/analyze at v4.39.1 with: category: "/language:${{matrix.language}}" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index 79e6d1c09..73fc993c5 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -40,7 +40,7 @@ jobs: type=sha,prefix= - name: Login to GitHub Container Registry - uses: docker/login-action at v4.6.0 + uses: docker/login-action at v4.7.0 with: registry: ghcr.io username: ${{ github.repository_owner }} ----------------------------------------------------------------------- Summary of changes: .github/workflows/ci.yml | 2 +- .github/workflows/codeql.yml | 4 ++-- .github/workflows/container.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Sun Nov 1 18:52:09 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 1 Nov 2026 18:52:09 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 159cc1f9dba3ee820e31123af4f90a2b6e090546 Message-ID: <20261102025209.5BA221CB8C8@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 159cc1f9dba3ee820e31123af4f90a2b6e090546 (commit) via 080bf38feb81ffae17dd6d7e48b7601e468ef1b0 (commit) via 1c29a5fa73afabaa78d4310ea48187097723611b (commit) from 1441ad5b48a86e8ff99a2f693fe777377d0f766f (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 159cc1f9dba3ee820e31123af4f90a2b6e090546 Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CircularArc: fix copy assignment of arc not starting at position 0 The copy assignment operator copied the three points of the arc into a new CoordinateSequence, but kept the position of the arc in the original sequence, so an arc assigned from one that did not start at position 0 referred to points past the end of its copy. Implement copy assignment with the copy constructor and move assignment. diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index 44414c0ad..1ae2e9e9f 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -145,28 +145,10 @@ CircularArc::CircularArc(CircularArc&& other) noexcept { CircularArc& CircularArc::operator=(const CircularArc& other) { - if (this == &other) { - return *this; + if (this != &other) { + *this = CircularArc(other); } - if (m_own_coordinates) { - delete m_seq; - } - - m_seq = new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM()); - m_pos = other.m_pos; - m_own_coordinates = true; - m_orientation = other.m_orientation; - m_orientation_known = other.m_orientation_known; - m_center = other.m_center; - m_center_known = other.m_center_known; - m_radius = other.m_radius; - m_radius_known = other.m_radius_known; - - CoordinateSequence* seq = const_cast(m_seq); - seq->reserve(3); - seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2); - return *this; } diff --git a/tests/unit/geom/CircularArcTest.cpp b/tests/unit/geom/CircularArcTest.cpp index 47078db1f..3d33a3110 100644 --- a/tests/unit/geom/CircularArcTest.cpp +++ b/tests/unit/geom/CircularArcTest.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include using geos::geom::CoordinateXY; @@ -176,4 +177,31 @@ void object::test<7>() { } +template<> +template<> +void object::test<8>() { + set_test_name("copy assignment from arc that does not start at the beginning of its CoordinateSequence"); + + CoordinateSequence seq{ + CoordinateXY(0, 0), + CoordinateXY(1, 1), + CoordinateXY(2, 0), + CoordinateXY(3, -1), + CoordinateXY(4, 0) + }; + + CircularArc second(seq, 2); + + CircularArc arc = CircularArc::create(CoordinateXY(0, 0), CoordinateXY(1, 1), CoordinateXY(2, 0)); + arc = second; + + ensure_equals(arc.p0(), CoordinateXY(2, 0)); + ensure_equals(arc.p1(), CoordinateXY(3, -1)); + ensure_equals(arc.p2(), CoordinateXY(4, 0)); + + // arc has its own copy of the points + seq.setAt(CoordinateXY(5, 5), 2); + ensure_equals(arc.p0(), CoordinateXY(2, 0)); +} + } commit 080bf38feb81ffae17dd6d7e48b7601e468ef1b0 Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CompoundCurve: update component curves in geometryChanged() CompoundCurve::geometryChangedAction() recomputed its envelope from the envelopes of its component curves, but those curves are not visited by apply_rw(GeometryComponentFilter*), so their envelopes (and the cached arcs of a CircularString) were not updated. After apply_rw() and geometryChanged(), as done by GEOSGeom_transformXY, a CompoundCurve kept the envelope of its original coordinates. Call geometryChanged() on each component curve before recomputing the envelope. diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h index d090be2af..50146b8bb 100644 --- a/include/geos/geom/CompoundCurve.h +++ b/include/geos/geom/CompoundCurve.h @@ -119,6 +119,11 @@ protected: void geometryChangedAction() override { + // The component curves are not visited by apply_rw(GeometryComponentFilter*), + // so update them before computing the envelope from theirs. + for (auto& curve : curves) { + curve->geometryChanged(); + } envelope = computeEnvelopeInternal(); } diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp index d6f5cbfde..040f3fd8d 100644 --- a/tests/unit/geom/CompoundCurveTest.cpp +++ b/tests/unit/geom/CompoundCurveTest.cpp @@ -454,4 +454,30 @@ void object::test<13>() ensure_equals_exact_geometry_xyzm(curved.get(), cc_.get(), 0); } +template<> +template<> +void object::test<14>() +{ + set_test_name("getEnvelopeInternal() after apply_rw(const CoordinateFilter*) and geometryChanged()"); + + struct TranslateFilter : public geos::geom::CoordinateFilter { + void filter_rw(CoordinateXY* c) const override + { + c->x += 10; + } + }; + + TranslateFilter filter; + cc_->apply_rw(&filter); + cc_->geometryChanged(); + + auto expected = wktreader_.read("COMPOUNDCURVE(CIRCULARSTRING(10 0, 11 1, 12 0), (12 0, 12 2))"); + + ensure_equals(*cc_->getEnvelopeInternal(), *expected->getEnvelopeInternal()); + for (std::size_t i = 0; i < cc_->getNumCurves(); i++) { + ensure_equals(*cc_->getCurveN(i)->getEnvelopeInternal(), *expected->getCurveN(i)->getEnvelopeInternal()); + } + ensure_equals("length", cc_->getLength(), expected->getLength(), 1e-12); +} + } commit 1c29a5fa73afabaa78d4310ea48187097723611b Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CircularString: clear cached arcs when coordinates change CircularString::getArcs() caches CircularArcs that refer to positions in the CircularString's CoordinateSequence and cache their center, radius and orientation. The cache was cleared when normalize() reversed an open CircularString, but not when: - normalize() of a closed CircularString replaced the CoordinateSequence to change the start point, or reversed it - apply_rw() modified the coordinates, or first replaced a shared CoordinateSequence with a copy - geometryChanged() was called after an external change In these cases the arcs described the old coordinates, or referred to a CoordinateSequence that had since been freed. For example, the length of a CircularString returned by GEOSGeom_transformXY could be the length of its input, and calling GEOSNormalize on a closed CircularString after GEOSLength left its arcs referring to freed memory. Clear the cache in each of these cases. The apply_rw() overloads are overridden because SimpleCurve::apply_rw() can replace shared coordinates before the caller has a chance to call geometryChanged(). diff --git a/NEWS.md b/NEWS.md index a6510e8d4..dc9ffb1fb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -10,6 +10,7 @@ - GeometrySplitter: Allow splitting a MultiSurface (GH-1520, Dan Baston) - GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston) - GEOSisSimpleDetail returns locations with input SRID (Sandro Santilli) + - CircularString: clear cached arcs when coordinates change (Alejandro Pacheco) - New things: - Expose code revision: GEOS_REVISION, GEOSrevision, geos-config --revision diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h index af5fb6555..fd282f180 100644 --- a/include/geos/geom/CircularString.h +++ b/include/geos/geom/CircularString.h @@ -25,11 +25,16 @@ class GEOS_DLL CircularString : public SimpleCurve { public: using SimpleCurve::SimpleCurve; + using SimpleCurve::apply_rw; friend class GeometryFactory; ~CircularString() override; + void apply_rw(const CoordinateFilter* filter) override; + + void apply_rw(CoordinateSequenceFilter& filter) override; + std::unique_ptr clone() const; const std::vector& getArcs() const; @@ -82,6 +87,7 @@ protected: void geometryChangedAction() override { envelope = computeEnvelopeInternal(false); + arcs.clear(); } CircularString* getCurvedImpl(const algorithm::LineToCurveParams&) const override { @@ -105,6 +111,9 @@ private: void normalizeClosed(); + /// Arcs describing `points`, created on demand by getArcs(). They refer to + /// positions in `points` (or, in a copy, to copies of the points of the + /// original) and must be cleared whenever `points` is replaced or modified. mutable std::vector arcs; }; diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp index 400094df8..4ec463453 100644 --- a/src/geom/CircularString.cpp +++ b/src/geom/CircularString.cpp @@ -40,6 +40,22 @@ CircularString::CircularString(const std::shared_ptr& CircularString::~CircularString() = default; +// SimpleCurve::apply_rw may replace shared coordinates with a copy, and +// callers are not required to call geometryChanged(), so clear the arcs here. +void +CircularString::apply_rw(const CoordinateFilter* filter) +{ + arcs.clear(); + SimpleCurve::apply_rw(filter); +} + +void +CircularString::apply_rw(CoordinateSequenceFilter& filter) +{ + arcs.clear(); + SimpleCurve::apply_rw(filter); +} + std::unique_ptr CircularString::clone() const { @@ -145,11 +161,13 @@ CircularString::normalizeClosed() { coords->reverse(); } points = std::move(coords); + arcs.clear(); } else if (reverse) { if (points.use_count() > 1) { points = points->clone(); } const_cast(points.get())->reverse(); + arcs.clear(); } } diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp index 3be6a7b7e..41cf5a77d 100644 --- a/tests/unit/geom/CircularStringTest.cpp +++ b/tests/unit/geom/CircularStringTest.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -50,6 +52,29 @@ struct test_circularstring_data { ensure_equals_exact_geometry_xyzm(cs.get(), expected.get(), 0); } + // Check that the arcs of a CircularString refer to its current coordinates and + // match the arcs of a new CircularString created from the same coordinates. + void checkArcs(const CircularString& cs) const { + auto expected = factory_->createCircularString(*cs.getCoordinatesRO()); + + const auto& arcs = cs.getArcs(); + const auto& expectedArcs = expected->getArcs(); + + ensure_equals("number of arcs", arcs.size(), expectedArcs.size()); + for (std::size_t i = 0; i < arcs.size(); i++) { + ensure("arc refers to coordinates of CircularString", arcs[i].getCoordinateSequence() == cs.getCoordinatesRO()); + ensure_equals("arc position", arcs[i].getCoordinatePosition(), 2*i); + ensure_equals_xy(arcs[i].p0(), expectedArcs[i].p0()); + ensure_equals_xy(arcs[i].p1(), expectedArcs[i].p1()); + ensure_equals_xy(arcs[i].p2(), expectedArcs[i].p2()); + ensure_equals("orientation", arcs[i].getOrientation(), expectedArcs[i].getOrientation()); + ensure_equals_xy(arcs[i].getCenter(), expectedArcs[i].getCenter()); + ensure_equals("radius", arcs[i].getRadius(), expectedArcs[i].getRadius()); + } + + ensure_equals("length", cs.getLength(), expected->getLength()); + } + void checkLinearizeMaxDeviation(const std::string& wkt_in, const std::string& wkt_expected, double maxDeviation, double tol=1e-12) { auto ctl = CurveToLineParams::maxDeviation(maxDeviation); checkLinearize(wkt_in, wkt_expected, ctl, tol); @@ -535,4 +560,149 @@ void object::test<27>() ensure_equals_xyzm(seq->getAt(5), XYZM{2.5, -4.33, 6 + (30.0 - 6)*(2.0/3), 8 + (40.0 - 8)*(2.0/3)}, 1e-3); } +template<> +template<> +void object::test<28>() +{ + set_test_name("getArcs() after normalize() of closed CircularString"); + + for (const char* wkt : { + // circle, start point is OK but need to reverse points + "CIRCULARSTRING (-5 0, 0 -5, 5 0, 0 5, -5 0)", + // circle, need to change start point but orientation is OK + "CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0)", + // circle, need to change start point and orientation + "CIRCULARSTRING (5 0, 0 5, -5 0, 0 -5, 5 0)", + // half-moon, start point is OK but need to make CW + "CIRCULARSTRING (0 0, -3 5, 0 10, -5 5, 0 0)", + // half-moon, need to change start point but orientation is OK + "CIRCULARSTRING (0 10, -3 5, 0 0, -5 5, 0 10)" + }) { + auto cs = wktreader_.read(wkt); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->normalize(); + + checkArcs(*cs); + } +} + +template<> +template<> +void object::test<29>() +{ + set_test_name("getArcs() after normalize() of closed CircularString with shared coordinates"); + + for (const char* wkt : { + // start point is OK but need to reverse points + "CIRCULARSTRING (-5 0, 0 -5, 5 0, 0 5, -5 0)", + // need to change start point but orientation is OK + "CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0)", + // need to change start point and orientation + "CIRCULARSTRING (5 0, 0 5, -5 0, 0 -5, 5 0)" + }) { + auto other = wktreader_.read(wkt); + auto cs = factory_->createCircularString(other->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->normalize(); + other.reset(); // destroy the coordinates used by cs before normalize() + + checkArcs(*cs); + } +} + +template<> +template<> +void object::test<30>() +{ + set_test_name("getArcs() after apply_rw(const CoordinateFilter*)"); + + struct ScaleFilter : public geos::geom::CoordinateFilter { + void filter_rw(XY* c) const override + { + c->x *= 2; + c->y *= 2; + } + }; + + ScaleFilter filter; + + // coordinates modified in place + cs_->getLength(); // create arcs, computing their center, radius and orientation + cs_->apply_rw(&filter); + checkArcs(*cs_); + + // coordinates copied before modification because they are shared + auto cs = factory_->createCircularString(cs_->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->apply_rw(&filter); + checkArcs(*cs); + cs_.reset(); // destroy the coordinates used by cs before apply_rw() + checkArcs(*cs); + + // arcs copied by clone() + auto copy = cs->clone(); + copy->apply_rw(&filter); + checkArcs(*copy); +} + +template<> +template<> +void object::test<31>() +{ + set_test_name("getArcs() after apply_rw(CoordinateSequenceFilter&)"); + + struct ScaleFilter : public geos::geom::CoordinateSequenceFilter { + explicit ScaleFilter(double p_factor) : factor(p_factor) {} + + void filter_rw(CoordinateSequence& seq, std::size_t i) override + { + auto& c = seq.getAt(i); + c.x *= factor; + c.y *= factor; + } + + bool isDone() const override + { + return false; + } + + bool isGeometryChanged() const override + { + return factor != 1; + } + + double factor; + }; + + // coordinates modified in place + ScaleFilter scale(2); + cs_->getLength(); // create arcs, computing their center, radius and orientation + cs_->apply_rw(scale); + checkArcs(*cs_); + + // coordinates unchanged, but copied before the filter is applied because they are shared + ScaleFilter identity(1); + auto cs = factory_->createCircularString(cs_->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->apply_rw(identity); + checkArcs(*cs); + cs_.reset(); // destroy the coordinates used by cs before apply_rw() + checkArcs(*cs); +} + +template<> +template<> +void object::test<32>() +{ + set_test_name("getArcs() after geometryChanged()"); + + cs_->getLength(); // create arcs, computing their center, radius and orientation + + auto& pts = *const_cast(cs_->getCoordinatesRO()); + pts.setAt(XY(1, 2), 1); + cs_->geometryChanged(); + + checkArcs(*cs_); +} + } ----------------------------------------------------------------------- Summary of changes: NEWS.md | 1 + include/geos/geom/CircularString.h | 9 ++ include/geos/geom/CompoundCurve.h | 5 + src/geom/CircularArc.cpp | 22 +---- src/geom/CircularString.cpp | 18 ++++ tests/unit/geom/CircularArcTest.cpp | 28 ++++++ tests/unit/geom/CircularStringTest.cpp | 170 +++++++++++++++++++++++++++++++++ tests/unit/geom/CompoundCurveTest.cpp | 26 +++++ 8 files changed, 259 insertions(+), 20 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Mon Nov 2 05:49:08 2026 From: git at osgeo.org (git at osgeo.org) Date: Mon, 2 Nov 2026 05:49:08 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 8bace6161f6593643e45875d7972247699357720 Message-ID: <20261102134909.24B311CABEE@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 8bace6161f6593643e45875d7972247699357720 (commit) from 159cc1f9dba3ee820e31123af4f90a2b6e090546 (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 8bace6161f6593643e45875d7972247699357720 Author: Daniel Baston Date: Sun Nov 1 21:26:50 2026 -0500 SpatialIndex.h: fix undocumented parameter diff --git a/include/geos/index/SpatialIndex.h b/include/geos/index/SpatialIndex.h index b3842543b..5e82089f0 100644 --- a/include/geos/index/SpatialIndex.h +++ b/include/geos/index/SpatialIndex.h @@ -70,9 +70,9 @@ public: * intersect the query envelope. * * @param searchEnv the envelope to query for + * @param matches a vector into which found items should be inserted */ - //virtual std::vector* query(const geom::Envelope *searchEnv)=0; - virtual void query(const geom::Envelope* searchEnv, std::vector&) = 0; + virtual void query(const geom::Envelope* searchEnv, std::vector& matches) = 0; /** \brief * Queries the index for all items whose extents intersect the given search Envelope ----------------------------------------------------------------------- Summary of changes: include/geos/index/SpatialIndex.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Tue Nov 3 04:10:45 2026 From: git at osgeo.org (git at osgeo.org) Date: Tue, 3 Nov 2026 04:10:45 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch 3.15 updated. 3ee1c785707005880df0899d9ba153093b4e8a85 Message-ID: <20261103121046.2F18F44D2B@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, 3.15 has been updated via 3ee1c785707005880df0899d9ba153093b4e8a85 (commit) via a2919fc504321a166d11714ad4c8e98629b8e374 (commit) via f05ccef210d32ce0307ed4c540ec9bbf5f299b68 (commit) via 2d3157fc739f88f87d652801ff3425da3528a5d6 (commit) via b3d656abfe09307a47d8315f1f1309c540839225 (commit) via 24e5696dcea1d5e52c75f05d9b60063351bb48c0 (commit) via 0aed98e0728e7386281cce1ead13bdaebcb676cc (commit) via 11588117abcf73fe531d3805711070c50a345032 (commit) via 9025863856f46c2c436e1ce070bead91d74b7fa1 (commit) via c87e8f2d67d1d347bb81b1d741f426486f10a20e (commit) via 771e8a83f8407a56f187433b441cc2e49a2ac4c0 (commit) via 68ef040f1cb2eec8573a2048ab343f02e31e5083 (commit) via dc0fe45d2e0fa938b025ed626df157b233546648 (commit) via 4f5281976597245f3fb39aa786b928961a131234 (commit) via a3022a63ce3d29e62b3b14861446f641376ff4d1 (commit) from 5b3950ef9058283c39e18b7927704054f1cafd7f (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 3ee1c785707005880df0899d9ba153093b4e8a85 Author: Daniel Baston Date: Tue Nov 3 06:16:38 2026 -0500 NEWS updates diff --git a/NEWS.md b/NEWS.md index e604cf255..05d6eecaf 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,16 +2,24 @@ 2026-xx-xx - Fixes/Improvements: - - Fix arc noding failure with endpoint intersections (GH-1513, Dan Baston) + - Arc noding: fix failure with endpoint intersections (GH-1513, Dan Baston) + - Arc noding: handle degenerate arcs with repeated points (GH-1584, Alejandro Pacheco and Dan Baston) - Make GeometrySplitter able to split MultiSurface inputs (GH-1518, Dan Baston) - GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston) - Skip testing FE_INVALID from unit-geom-Envelope for some Clang builds (GH-1555, Mike Taves) - Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston) - CircularArcIntersector: Use Orient2D to check for arc control points on line (GH-1546, Dan Baston) - CircularArcIntersector: Avoid incorrectly classifying intersections as cocircular (GH-1549, Dan Baston) + - CircularArcIntersector: Avoid missed intersections at shared points (GH-1559, Dan Baston) - GEOSProject: Throw IllegalArgumentException on invalid input (GH-1547, Mohd Quamar Tyagi) - - CircularString: clear cached arcs when coordinates change (Alejandro Pacheco) - + - CircularString: clear cached arcs when coordinates change (GH-1586, Alejandro Pacheco) + - CircularArc: fix copy assignment of arc not starting at position 0 (GH-1586, Alejandro Pacheco) + - CircularArcs: fix containsAngle for non-normalized angles (GH-1579, Dan Baston) + - CompoundCurve: update component curves in geometryChanged (GH-1586, Alejandro Pacheco) + - WKBWriter: preserve includeSRID flag after failed write (GH-1578, Dan Baston) + - Envelope: improve robustness of distanceSquared (GH-1576, Jeroen Bloemscheer) + - Distance: avoid erroneously large distance between arc and segment (GH-1577, Dan Baston and Jeroen Bloemscheer) + - GeometryFactory: fix return type from buildGeometry with collection inputs (GH-1580, Alejandro Pacheco) ## Changes in 3.15.0 2026-09-01 commit a2919fc504321a166d11714ad4c8e98629b8e374 Author: Daniel Baston Date: Sun Nov 1 21:26:50 2026 -0500 SpatialIndex.h: fix undocumented parameter diff --git a/include/geos/index/SpatialIndex.h b/include/geos/index/SpatialIndex.h index b3842543b..5e82089f0 100644 --- a/include/geos/index/SpatialIndex.h +++ b/include/geos/index/SpatialIndex.h @@ -70,9 +70,9 @@ public: * intersect the query envelope. * * @param searchEnv the envelope to query for + * @param matches a vector into which found items should be inserted */ - //virtual std::vector* query(const geom::Envelope *searchEnv)=0; - virtual void query(const geom::Envelope* searchEnv, std::vector&) = 0; + virtual void query(const geom::Envelope* searchEnv, std::vector& matches) = 0; /** \brief * Queries the index for all items whose extents intersect the given search Envelope commit f05ccef210d32ce0307ed4c540ec9bbf5f299b68 Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CircularArc: fix copy assignment of arc not starting at position 0 The copy assignment operator copied the three points of the arc into a new CoordinateSequence, but kept the position of the arc in the original sequence, so an arc assigned from one that did not start at position 0 referred to points past the end of its copy. Implement copy assignment with the copy constructor and move assignment. diff --git a/src/geom/CircularArc.cpp b/src/geom/CircularArc.cpp index 44414c0ad..1ae2e9e9f 100644 --- a/src/geom/CircularArc.cpp +++ b/src/geom/CircularArc.cpp @@ -145,28 +145,10 @@ CircularArc::CircularArc(CircularArc&& other) noexcept { CircularArc& CircularArc::operator=(const CircularArc& other) { - if (this == &other) { - return *this; + if (this != &other) { + *this = CircularArc(other); } - if (m_own_coordinates) { - delete m_seq; - } - - m_seq = new CoordinateSequence(0, other.getCoordinateSequence()->hasZ(), other.getCoordinateSequence()->hasM()); - m_pos = other.m_pos; - m_own_coordinates = true; - m_orientation = other.m_orientation; - m_orientation_known = other.m_orientation_known; - m_center = other.m_center; - m_center_known = other.m_center_known; - m_radius = other.m_radius; - m_radius_known = other.m_radius_known; - - CoordinateSequence* seq = const_cast(m_seq); - seq->reserve(3); - seq->add(*other.getCoordinateSequence(), other.getCoordinatePosition(), other.getCoordinatePosition() + 2); - return *this; } diff --git a/tests/unit/geom/CircularArcTest.cpp b/tests/unit/geom/CircularArcTest.cpp index 47078db1f..3d33a3110 100644 --- a/tests/unit/geom/CircularArcTest.cpp +++ b/tests/unit/geom/CircularArcTest.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include using geos::geom::CoordinateXY; @@ -176,4 +177,31 @@ void object::test<7>() { } +template<> +template<> +void object::test<8>() { + set_test_name("copy assignment from arc that does not start at the beginning of its CoordinateSequence"); + + CoordinateSequence seq{ + CoordinateXY(0, 0), + CoordinateXY(1, 1), + CoordinateXY(2, 0), + CoordinateXY(3, -1), + CoordinateXY(4, 0) + }; + + CircularArc second(seq, 2); + + CircularArc arc = CircularArc::create(CoordinateXY(0, 0), CoordinateXY(1, 1), CoordinateXY(2, 0)); + arc = second; + + ensure_equals(arc.p0(), CoordinateXY(2, 0)); + ensure_equals(arc.p1(), CoordinateXY(3, -1)); + ensure_equals(arc.p2(), CoordinateXY(4, 0)); + + // arc has its own copy of the points + seq.setAt(CoordinateXY(5, 5), 2); + ensure_equals(arc.p0(), CoordinateXY(2, 0)); +} + } commit 2d3157fc739f88f87d652801ff3425da3528a5d6 Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CompoundCurve: update component curves in geometryChanged() CompoundCurve::geometryChangedAction() recomputed its envelope from the envelopes of its component curves, but those curves are not visited by apply_rw(GeometryComponentFilter*), so their envelopes (and the cached arcs of a CircularString) were not updated. After apply_rw() and geometryChanged(), as done by GEOSGeom_transformXY, a CompoundCurve kept the envelope of its original coordinates. Call geometryChanged() on each component curve before recomputing the envelope. diff --git a/include/geos/geom/CompoundCurve.h b/include/geos/geom/CompoundCurve.h index d090be2af..50146b8bb 100644 --- a/include/geos/geom/CompoundCurve.h +++ b/include/geos/geom/CompoundCurve.h @@ -119,6 +119,11 @@ protected: void geometryChangedAction() override { + // The component curves are not visited by apply_rw(GeometryComponentFilter*), + // so update them before computing the envelope from theirs. + for (auto& curve : curves) { + curve->geometryChanged(); + } envelope = computeEnvelopeInternal(); } diff --git a/tests/unit/geom/CompoundCurveTest.cpp b/tests/unit/geom/CompoundCurveTest.cpp index d6f5cbfde..040f3fd8d 100644 --- a/tests/unit/geom/CompoundCurveTest.cpp +++ b/tests/unit/geom/CompoundCurveTest.cpp @@ -454,4 +454,30 @@ void object::test<13>() ensure_equals_exact_geometry_xyzm(curved.get(), cc_.get(), 0); } +template<> +template<> +void object::test<14>() +{ + set_test_name("getEnvelopeInternal() after apply_rw(const CoordinateFilter*) and geometryChanged()"); + + struct TranslateFilter : public geos::geom::CoordinateFilter { + void filter_rw(CoordinateXY* c) const override + { + c->x += 10; + } + }; + + TranslateFilter filter; + cc_->apply_rw(&filter); + cc_->geometryChanged(); + + auto expected = wktreader_.read("COMPOUNDCURVE(CIRCULARSTRING(10 0, 11 1, 12 0), (12 0, 12 2))"); + + ensure_equals(*cc_->getEnvelopeInternal(), *expected->getEnvelopeInternal()); + for (std::size_t i = 0; i < cc_->getNumCurves(); i++) { + ensure_equals(*cc_->getCurveN(i)->getEnvelopeInternal(), *expected->getCurveN(i)->getEnvelopeInternal()); + } + ensure_equals("length", cc_->getLength(), expected->getLength(), 1e-12); +} + } commit b3d656abfe09307a47d8315f1f1309c540839225 Author: Alejandro Pacheco Date: Thu Oct 29 07:27:45 2026 +0100 CircularString: clear cached arcs when coordinates change CircularString::getArcs() caches CircularArcs that refer to positions in the CircularString's CoordinateSequence and cache their center, radius and orientation. The cache was cleared when normalize() reversed an open CircularString, but not when: - normalize() of a closed CircularString replaced the CoordinateSequence to change the start point, or reversed it - apply_rw() modified the coordinates, or first replaced a shared CoordinateSequence with a copy - geometryChanged() was called after an external change In these cases the arcs described the old coordinates, or referred to a CoordinateSequence that had since been freed. For example, the length of a CircularString returned by GEOSGeom_transformXY could be the length of its input, and calling GEOSNormalize on a closed CircularString after GEOSLength left its arcs referring to freed memory. Clear the cache in each of these cases. The apply_rw() overloads are overridden because SimpleCurve::apply_rw() can replace shared coordinates before the caller has a chance to call geometryChanged(). diff --git a/NEWS.md b/NEWS.md index 1ba0b572e..e604cf255 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,10 +6,12 @@ - Make GeometrySplitter able to split MultiSurface inputs (GH-1518, Dan Baston) - GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston) - Skip testing FE_INVALID from unit-geom-Envelope for some Clang builds (GH-1555, Mike Taves) - - Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston) + - Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston) - CircularArcIntersector: Use Orient2D to check for arc control points on line (GH-1546, Dan Baston) - CircularArcIntersector: Avoid incorrectly classifying intersections as cocircular (GH-1549, Dan Baston) - GEOSProject: Throw IllegalArgumentException on invalid input (GH-1547, Mohd Quamar Tyagi) + - CircularString: clear cached arcs when coordinates change (Alejandro Pacheco) + ## Changes in 3.15.0 2026-09-01 diff --git a/include/geos/geom/CircularString.h b/include/geos/geom/CircularString.h index af5fb6555..fd282f180 100644 --- a/include/geos/geom/CircularString.h +++ b/include/geos/geom/CircularString.h @@ -25,11 +25,16 @@ class GEOS_DLL CircularString : public SimpleCurve { public: using SimpleCurve::SimpleCurve; + using SimpleCurve::apply_rw; friend class GeometryFactory; ~CircularString() override; + void apply_rw(const CoordinateFilter* filter) override; + + void apply_rw(CoordinateSequenceFilter& filter) override; + std::unique_ptr clone() const; const std::vector& getArcs() const; @@ -82,6 +87,7 @@ protected: void geometryChangedAction() override { envelope = computeEnvelopeInternal(false); + arcs.clear(); } CircularString* getCurvedImpl(const algorithm::LineToCurveParams&) const override { @@ -105,6 +111,9 @@ private: void normalizeClosed(); + /// Arcs describing `points`, created on demand by getArcs(). They refer to + /// positions in `points` (or, in a copy, to copies of the points of the + /// original) and must be cleared whenever `points` is replaced or modified. mutable std::vector arcs; }; diff --git a/src/geom/CircularString.cpp b/src/geom/CircularString.cpp index 400094df8..4ec463453 100644 --- a/src/geom/CircularString.cpp +++ b/src/geom/CircularString.cpp @@ -40,6 +40,22 @@ CircularString::CircularString(const std::shared_ptr& CircularString::~CircularString() = default; +// SimpleCurve::apply_rw may replace shared coordinates with a copy, and +// callers are not required to call geometryChanged(), so clear the arcs here. +void +CircularString::apply_rw(const CoordinateFilter* filter) +{ + arcs.clear(); + SimpleCurve::apply_rw(filter); +} + +void +CircularString::apply_rw(CoordinateSequenceFilter& filter) +{ + arcs.clear(); + SimpleCurve::apply_rw(filter); +} + std::unique_ptr CircularString::clone() const { @@ -145,11 +161,13 @@ CircularString::normalizeClosed() { coords->reverse(); } points = std::move(coords); + arcs.clear(); } else if (reverse) { if (points.use_count() > 1) { points = points->clone(); } const_cast(points.get())->reverse(); + arcs.clear(); } } diff --git a/tests/unit/geom/CircularStringTest.cpp b/tests/unit/geom/CircularStringTest.cpp index 3be6a7b7e..41cf5a77d 100644 --- a/tests/unit/geom/CircularStringTest.cpp +++ b/tests/unit/geom/CircularStringTest.cpp @@ -6,7 +6,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -50,6 +52,29 @@ struct test_circularstring_data { ensure_equals_exact_geometry_xyzm(cs.get(), expected.get(), 0); } + // Check that the arcs of a CircularString refer to its current coordinates and + // match the arcs of a new CircularString created from the same coordinates. + void checkArcs(const CircularString& cs) const { + auto expected = factory_->createCircularString(*cs.getCoordinatesRO()); + + const auto& arcs = cs.getArcs(); + const auto& expectedArcs = expected->getArcs(); + + ensure_equals("number of arcs", arcs.size(), expectedArcs.size()); + for (std::size_t i = 0; i < arcs.size(); i++) { + ensure("arc refers to coordinates of CircularString", arcs[i].getCoordinateSequence() == cs.getCoordinatesRO()); + ensure_equals("arc position", arcs[i].getCoordinatePosition(), 2*i); + ensure_equals_xy(arcs[i].p0(), expectedArcs[i].p0()); + ensure_equals_xy(arcs[i].p1(), expectedArcs[i].p1()); + ensure_equals_xy(arcs[i].p2(), expectedArcs[i].p2()); + ensure_equals("orientation", arcs[i].getOrientation(), expectedArcs[i].getOrientation()); + ensure_equals_xy(arcs[i].getCenter(), expectedArcs[i].getCenter()); + ensure_equals("radius", arcs[i].getRadius(), expectedArcs[i].getRadius()); + } + + ensure_equals("length", cs.getLength(), expected->getLength()); + } + void checkLinearizeMaxDeviation(const std::string& wkt_in, const std::string& wkt_expected, double maxDeviation, double tol=1e-12) { auto ctl = CurveToLineParams::maxDeviation(maxDeviation); checkLinearize(wkt_in, wkt_expected, ctl, tol); @@ -535,4 +560,149 @@ void object::test<27>() ensure_equals_xyzm(seq->getAt(5), XYZM{2.5, -4.33, 6 + (30.0 - 6)*(2.0/3), 8 + (40.0 - 8)*(2.0/3)}, 1e-3); } +template<> +template<> +void object::test<28>() +{ + set_test_name("getArcs() after normalize() of closed CircularString"); + + for (const char* wkt : { + // circle, start point is OK but need to reverse points + "CIRCULARSTRING (-5 0, 0 -5, 5 0, 0 5, -5 0)", + // circle, need to change start point but orientation is OK + "CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0)", + // circle, need to change start point and orientation + "CIRCULARSTRING (5 0, 0 5, -5 0, 0 -5, 5 0)", + // half-moon, start point is OK but need to make CW + "CIRCULARSTRING (0 0, -3 5, 0 10, -5 5, 0 0)", + // half-moon, need to change start point but orientation is OK + "CIRCULARSTRING (0 10, -3 5, 0 0, -5 5, 0 10)" + }) { + auto cs = wktreader_.read(wkt); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->normalize(); + + checkArcs(*cs); + } +} + +template<> +template<> +void object::test<29>() +{ + set_test_name("getArcs() after normalize() of closed CircularString with shared coordinates"); + + for (const char* wkt : { + // start point is OK but need to reverse points + "CIRCULARSTRING (-5 0, 0 -5, 5 0, 0 5, -5 0)", + // need to change start point but orientation is OK + "CIRCULARSTRING (5 0, 0 -5, -5 0, 0 5, 5 0)", + // need to change start point and orientation + "CIRCULARSTRING (5 0, 0 5, -5 0, 0 -5, 5 0)" + }) { + auto other = wktreader_.read(wkt); + auto cs = factory_->createCircularString(other->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->normalize(); + other.reset(); // destroy the coordinates used by cs before normalize() + + checkArcs(*cs); + } +} + +template<> +template<> +void object::test<30>() +{ + set_test_name("getArcs() after apply_rw(const CoordinateFilter*)"); + + struct ScaleFilter : public geos::geom::CoordinateFilter { + void filter_rw(XY* c) const override + { + c->x *= 2; + c->y *= 2; + } + }; + + ScaleFilter filter; + + // coordinates modified in place + cs_->getLength(); // create arcs, computing their center, radius and orientation + cs_->apply_rw(&filter); + checkArcs(*cs_); + + // coordinates copied before modification because they are shared + auto cs = factory_->createCircularString(cs_->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->apply_rw(&filter); + checkArcs(*cs); + cs_.reset(); // destroy the coordinates used by cs before apply_rw() + checkArcs(*cs); + + // arcs copied by clone() + auto copy = cs->clone(); + copy->apply_rw(&filter); + checkArcs(*copy); +} + +template<> +template<> +void object::test<31>() +{ + set_test_name("getArcs() after apply_rw(CoordinateSequenceFilter&)"); + + struct ScaleFilter : public geos::geom::CoordinateSequenceFilter { + explicit ScaleFilter(double p_factor) : factor(p_factor) {} + + void filter_rw(CoordinateSequence& seq, std::size_t i) override + { + auto& c = seq.getAt(i); + c.x *= factor; + c.y *= factor; + } + + bool isDone() const override + { + return false; + } + + bool isGeometryChanged() const override + { + return factor != 1; + } + + double factor; + }; + + // coordinates modified in place + ScaleFilter scale(2); + cs_->getLength(); // create arcs, computing their center, radius and orientation + cs_->apply_rw(scale); + checkArcs(*cs_); + + // coordinates unchanged, but copied before the filter is applied because they are shared + ScaleFilter identity(1); + auto cs = factory_->createCircularString(cs_->getSharedCoordinates()); + cs->getLength(); // create arcs, computing their center, radius and orientation + cs->apply_rw(identity); + checkArcs(*cs); + cs_.reset(); // destroy the coordinates used by cs before apply_rw() + checkArcs(*cs); +} + +template<> +template<> +void object::test<32>() +{ + set_test_name("getArcs() after geometryChanged()"); + + cs_->getLength(); // create arcs, computing their center, radius and orientation + + auto& pts = *const_cast(cs_->getCoordinatesRO()); + pts.setAt(XY(1, 2), 1); + cs_->geometryChanged(); + + checkArcs(*cs_); +} + } commit 24e5696dcea1d5e52c75f05d9b60063351bb48c0 Author: Daniel Baston Date: Sat Oct 31 16:00:34 2026 -0400 NodableArcString: Handle degenerate arcs with repeated points Co-authored-by: Alejandro Pacheco diff --git a/src/noding/NodableArcString.cpp b/src/noding/NodableArcString.cpp index 5b41a62f8..7c91634ee 100644 --- a/src/noding/NodableArcString.cpp +++ b/src/noding/NodableArcString.cpp @@ -268,7 +268,7 @@ public: m_coords = std::make_unique(0, m_constructZ, m_constructM); } - m_coords->add(seq, pos, pos + 2, false); + m_coords->add(seq, pos + !m_coords->isEmpty(), pos + 2, true); m_arcs.emplace_back(*m_coords, m_coords->getSize() - 3, center, radius, orientation); } diff --git a/tests/unit/capi/GEOSNodeTest.cpp b/tests/unit/capi/GEOSNodeTest.cpp index 10166adbc..97401d714 100644 --- a/tests/unit/capi/GEOSNodeTest.cpp +++ b/tests/unit/capi/GEOSNodeTest.cpp @@ -414,5 +414,28 @@ void object::test<19>() reinterpret_cast(expected_), 1e-4); } +template<> +template<> +void object::test<20>() +{ + set_test_name("CircularString with repeated points"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0), LINESTRING (3 0, 3 2))"); + + result_ = GEOSNode(input_); + ensure(result_); + + ensure_equals(GEOSGetNumGeometries(result_), 4); + + expected_ = fromWKT("MULTICURVE (" + "CIRCULARSTRING (0 0, 0 0, 2 0, 2.2928932188 0.7071067812, 3 1)," + "CIRCULARSTRING (3 1, 3.7071067812 0.7071067812, 4 0)," + "(3 0, 3 1)," + "(3 1, 3 2))"); + + 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 c1f1c02ce..31b8b9832 100644 --- a/tests/unit/noding/NodableArcStringTest.cpp +++ b/tests/unit/noding/NodableArcStringTest.cpp @@ -12,6 +12,7 @@ using geos::geom::CoordinateXY; using geos::geom::CoordinateXYZM; using geos::noding::ArcString; using geos::noding::NodableArcString; +using namespace std::string_literals; namespace tut { @@ -223,4 +224,72 @@ void object::test<5>() }); } -} \ No newline at end of file +template<> +template<> +void object::test<6>() +{ + set_test_name("getNoded keeps all three points of arcs with repeated points"); + + for (const char* wkt : { + "CIRCULARSTRING (0 0, 0 0, 0 0)", // 0 == 1 == 2 + "CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0)", // 0 == 1 + "CIRCULARSTRING (0 0, 1 1, 1 1, 3 1, 4 0)", // 1 == 2 + "CIRCULARSTRING (0 0, 1 1, 2 0, 2 0, 4 0)", // 2 == 3 + "CIRCULARSTRING (0 0, 1 1, 2 0, 3 1, 3 1)" // 3 == 4 + }) { + const auto g = reader_.read(wkt); + + NodableArcString nas(g->getArcs(), g->getSharedCoordinates(), false, false, nullptr); + + std::vector> noded; + nas.getNoded(noded); + + ensure_equals("noded.size(): "s + wkt, noded.size(), 1u); + ensure_equals("noded[0]->getSize(): "s + wkt, noded[0]->getSize(), g->getArcs().size()); + ensure("noded[0]->getCoordinates(): "s + wkt, noded[0]->getCoordinates()->equalsIdentical(*g->getCoordinatesRO())); + for (std::size_t i = 0; i < noded[0]->getSize(); i++) { + ensure_equals(wkt, noded[0]->getArc(i).getCoordinatePosition(), 2*i); + } + } +} + +template<> +template<> +void object::test<7>() +{ + set_test_name("getNoded keeps arc with repeated points before a split arc"); + + const auto g = reader_.read("CIRCULARSTRING (0 0, 0 0, 2 0, 3 1, 4 0)"); + + NodableArcString nas(g->getArcs(), g->getSharedCoordinates(), false, false, nullptr); + nas.addIntersection(CoordinateXY{3, 1}, 1); + + std::vector> noded; + nas.getNoded(noded); + + ensure_equals(noded.size(), 2u); + + const auto& seq = *noded[0]->getCoordinates(); + ensure_equals(noded[0]->getSize(), 2u); + ensure_equals(seq.size(), 5u); + + const CircularArc& arc0 = noded[0]->getArc(0); + ensure_equals(arc0.getCoordinatePosition(), 0u); + ensure_equals(arc0.p0(), XY{0, 0}); + ensure_equals(arc0.p1(), XY{0, 0}); + ensure_equals(arc0.p2(), XY{2, 0}); + + const CircularArc& arc1 = noded[0]->getArc(1); + ensure_equals(arc1.getCoordinatePosition(), 2u); + ensure_equals(arc1.p0(), XY{2, 0}); + ensure_equals(arc1.p2(), XY{3, 1}); + + ensure_equals(noded[1]->getSize(), 1u); + ensure_equals(noded[1]->getCoordinates()->size(), 3u); + + const CircularArc& arc2 = noded[1]->getArc(0); + ensure_arc_equals(arc2, CircularArc::create(XY{3, 1}, XY{4, 0}, XY{3, 0}, 1, Orientation::CLOCKWISE), 1e-8); + ensure_equals(arc2.p2(), XY{4, 0}); +} + +} // namespace tut commit 0aed98e0728e7386281cce1ead13bdaebcb676cc Author: Alejandro Pacheco Date: Thu Oct 29 07:16:51 2026 +0100 Check element types of MultiPoint, MultiLineString and MultiPolygon The constructors taking a vector of Geometry now throw IllegalArgumentException for an element of another type, as the MultiCurve and MultiSurface constructors already do. This also covers GEOSGeom_createCollection. buildGeometry(from, toofar) chose its result type by sort index and dimension, so it put several CircularStrings or CompoundCurves into a MultiLineString and several collections of one type into a Multi* geometry. It now calls buildGeometry(const std::vector&), whose semantics its documentation already refers to. As a result, GEOSUnaryUnion of CircularStrings alone no longer fails with "Noder does not support curved paths". diff --git a/include/geos/geom/GeometryFactory.h b/include/geos/geom/GeometryFactory.h index 73fcb5995..22d663468 100644 --- a/include/geos/geom/GeometryFactory.h +++ b/include/geos/geom/GeometryFactory.h @@ -434,54 +434,7 @@ public: std::unique_ptr buildGeometry(T from, T toofar) const { - bool isHeterogeneous = false; - std::size_t count = 0; - int geomClass = -1; - for(T i = from; i != toofar; ++i) { - ++count; - const Geometry* g = *i; - if(geomClass < 0) { - geomClass = g->getSortIndex(); - } - else if(geomClass != g->getSortIndex()) { - isHeterogeneous = true; - } - } - - // for the empty geometry, return an empty GeometryCollection - if(count == 0) { - return std::unique_ptr(createGeometryCollection()); - } - - // for the single geometry, return a clone - if(count == 1) { - return (*from)->clone(); - } - - // Now we know it is a collection - - // FIXME: - // Until we tweak all the createMulti* interfaces - // to support taking iterators we'll have to build - // a custom vector here. - std::vector> fromGeoms; - for(T i = from; i != toofar; ++i) { - fromGeoms.push_back((*i)->clone()); - } - - // for an heterogeneous ... - if(isHeterogeneous) { - return createGeometryCollection(std::move(fromGeoms)); - } - - // At this point we know the collection is not hetereogenous. - switch((*from)->getDimension()) { - case Dimension::A: return createMultiPolygon(std::move(fromGeoms)); - case Dimension::L: return createMultiLineString(std::move(fromGeoms)); - case Dimension::P: return createMultiPoint(std::move(fromGeoms)); - default: - throw geos::util::IllegalArgumentException(std::string("Invalid geometry type.")); - } + return buildGeometry(std::vector(from, toofar)); } /** \brief diff --git a/src/geom/MultiLineString.cpp b/src/geom/MultiLineString.cpp index c58615076..f4116f0b2 100644 --- a/src/geom/MultiLineString.cpp +++ b/src/geom/MultiLineString.cpp @@ -45,7 +45,14 @@ MultiLineString::MultiLineString(std::vector> && new MultiLineString::MultiLineString(std::vector> && newLines, const GeometryFactory& factory) : GeometryCollection(std::move(newLines), factory) -{} +{ + for (const auto& geom : geometries) { + const auto typeId = geom->getGeometryTypeId(); + if (typeId != GEOS_LINESTRING && typeId != GEOS_LINEARRING) { + throw util::IllegalArgumentException("All elements of MultiLineString must be a LineString"); + } + } +} MultiLineString::MultiLineString(std::vector> && newLines, const GeometryFactory& factory) diff --git a/src/geom/MultiPoint.cpp b/src/geom/MultiPoint.cpp index aa615bf1a..8513e1bf1 100644 --- a/src/geom/MultiPoint.cpp +++ b/src/geom/MultiPoint.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include @@ -40,7 +41,11 @@ MultiPoint::MultiPoint(std::vector> && newPoints, cons : GeometryCollection(std::move(newPoints), factory) { - + for (const auto& geom : geometries) { + if (geom->getGeometryTypeId() != GEOS_POINT) { + throw util::IllegalArgumentException("All elements of MultiPoint must be a Point"); + } + } } Dimension::DimensionType diff --git a/src/geom/MultiPolygon.cpp b/src/geom/MultiPolygon.cpp index ae5c253fc..8f7c82b68 100644 --- a/src/geom/MultiPolygon.cpp +++ b/src/geom/MultiPolygon.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include @@ -42,7 +43,13 @@ MultiPolygon::MultiPolygon(std::vector> && newPolys, co MultiPolygon::MultiPolygon(std::vector> && newPolys, const GeometryFactory& factory) : GeometryCollection(std::move(newPolys), factory) -{} +{ + for (const auto& geom : geometries) { + if (geom->getGeometryTypeId() != GEOS_POLYGON) { + throw util::IllegalArgumentException("All elements of MultiPolygon must be a Polygon"); + } + } +} MultiPolygon::~MultiPolygon() {} diff --git a/tests/unit/capi/GEOSGeom_createCollectionTest.cpp b/tests/unit/capi/GEOSGeom_createCollectionTest.cpp index c52bce50a..56f40ef39 100644 --- a/tests/unit/capi/GEOSGeom_createCollectionTest.cpp +++ b/tests/unit/capi/GEOSGeom_createCollectionTest.cpp @@ -202,5 +202,46 @@ void object::test<9> } -} // namespace tut +// Error on an element of another type, ownership is still transferred +template<> +template<> +void object::test<10> +() +{ + GEOSGeometry* points[2]; + points[0] = fromWKT("POINT (0 0)"); + points[1] = fromWKT("LINESTRING (0 0, 1 1)"); + geom1_ = GEOSGeom_createCollection(GEOS_MULTIPOINT, points, 2); + ensure(geom1_ == nullptr); + GEOSGeometry* lines[2]; + lines[0] = fromWKT("LINESTRING (0 0, 1 1)"); + lines[1] = fromWKT("MULTILINESTRING ((2 2, 3 3))"); + geom1_ = GEOSGeom_createCollection(GEOS_MULTILINESTRING, lines, 2); + ensure(geom1_ == nullptr); + + GEOSGeometry* polygons[2]; + polygons[0] = fromWKT("POLYGON ((0 0, 1 0, 1 1, 0 0))"); + polygons[1] = fromWKT("POINT (5 5)"); + geom1_ = GEOSGeom_createCollection(GEOS_MULTIPOLYGON, polygons, 2); + ensure(geom1_ == nullptr); +} + +// A LinearRing is accepted as an element of a MultiLineString +template<> +template<> +void object::test<11> +() +{ + GEOSGeometry* lines[2]; + lines[0] = fromWKT("LINEARRING (0 0, 1 0, 1 1, 0 0)"); + lines[1] = fromWKT("LINESTRING (2 2, 3 3)"); + geom1_ = GEOSGeom_createCollection(GEOS_MULTILINESTRING, lines, 2); + ensure(geom1_ != nullptr); + + ensure_equals(GEOSGeomTypeId(geom1_), GEOS_MULTILINESTRING); + ensure_equals(GEOSGetNumGeometries(geom1_), 2); + ensure_equals(GEOSGeomTypeId(GEOSGetGeometryN(geom1_, 0)), GEOS_LINEARRING); +} + +} // namespace tut diff --git a/tests/unit/capi/GEOSUnaryUnionTest.cpp b/tests/unit/capi/GEOSUnaryUnionTest.cpp index af484b8ef..802078ba9 100644 --- a/tests/unit/capi/GEOSUnaryUnionTest.cpp +++ b/tests/unit/capi/GEOSUnaryUnionTest.cpp @@ -333,6 +333,21 @@ void object::test<17>() ensure_geometry_equals(result_, input_, 1e-8); } +template<> +template<> +void object::test<18>() +{ + set_test_name("CircularString-only dim=1 inputs"); + + input_ = fromWKT("MULTICURVE (CIRCULARSTRING (0 0, 1 1, 2 0), CIRCULARSTRING (2 0, 3 -1, 4 0))"); + + result_ = GEOSUnaryUnion(input_); + ensure(result_); + + expected_ = fromWKT("CIRCULARSTRING (0 0, 1 1, 2 0, 3 -1, 4 0)"); + ensure_geometry_equals(result_, expected_, 1e-8); +} + } // namespace tut diff --git a/tests/unit/geom/GeometryFactoryTest.cpp b/tests/unit/geom/GeometryFactoryTest.cpp index 30a27a924..3d950d03c 100644 --- a/tests/unit/geom/GeometryFactoryTest.cpp +++ b/tests/unit/geom/GeometryFactoryTest.cpp @@ -3,6 +3,7 @@ // tut #include +#include #include // geos #include @@ -1622,4 +1623,87 @@ void object::test<44>() } } +template<> +template<> +void object::test<45>() +{ + set_test_name("createMultiPoint, createMultiLineString, createMultiPolygon reject elements of another type"); + + using geos::geom::Geometry; + using geos::util::IllegalArgumentException; + + auto elements = [this](const std::string& wkt1, const std::string& wkt2) { + std::vector> geoms; + geoms.push_back(reader_.read(wkt1)); + geoms.push_back(reader_.read(wkt2)); + return geoms; + }; + + ensure_THROW(factory_->createMultiPoint(elements("POINT (0 0)", "MULTIPOINT ((1 1))")), IllegalArgumentException); + ensure_THROW(factory_->createMultiPoint(elements("POINT (0 0)", "LINESTRING (0 0, 1 1)")), IllegalArgumentException); + + ensure_THROW(factory_->createMultiLineString(elements("LINESTRING (0 0, 1 1)", "MULTILINESTRING ((2 2, 3 3))")), IllegalArgumentException); + ensure_THROW(factory_->createMultiLineString(elements("LINESTRING (0 0, 1 1)", "CIRCULARSTRING (0 0, 1 1, 2 0)")), IllegalArgumentException); + ensure_THROW(factory_->createMultiLineString(elements("LINESTRING (0 0, 1 1)", "POINT (0 0)")), IllegalArgumentException); + + ensure_THROW(factory_->createMultiPolygon(elements("POLYGON ((0 0, 1 0, 1 1, 0 0))", "MULTIPOLYGON (((2 2, 3 2, 3 3, 2 2)))")), IllegalArgumentException); + ensure_THROW(factory_->createMultiPolygon(elements("POLYGON ((0 0, 1 0, 1 1, 0 0))", "CURVEPOLYGON ((2 2, 3 2, 3 3, 2 2))")), IllegalArgumentException); + ensure_THROW(factory_->createMultiPolygon(elements("POLYGON ((0 0, 1 0, 1 1, 0 0))", "LINESTRING (0 0, 1 1)")), IllegalArgumentException); + + // createMultiPolygon(const std::vector&) clones into the checked constructor + auto poly = reader_.read("POLYGON ((0 0, 1 0, 1 1, 0 0))"); + auto mpoly = reader_.read("MULTIPOLYGON (((2 2, 3 2, 3 3, 2 2)))"); + std::vector borrowed{ poly.get(), mpoly.get() }; + ensure_THROW(factory_->createMultiPolygon(borrowed), IllegalArgumentException); + + // A LinearRing is a LineString + auto mls = factory_->createMultiLineString(elements("LINESTRING (0 0, 1 1)", "LINEARRING (2 2, 3 2, 3 3, 2 2)")); + ensure_equals(mls->getNumGeometries(), 2u); +} + +template<> +template<> +void object::test<46>() +{ + set_test_name("buildGeometry(from, toofar) and buildGeometry(const std::vector&) give the same type"); + + using geos::geom::Geometry; + using geos::geom::GeometryTypeId; + + struct Case { + std::string wkt1; + std::string wkt2; + GeometryTypeId type; + }; + + const std::vector cases = { + { "POINT (1 1)", "POINT (2 2)", geos::geom::GEOS_MULTIPOINT }, + { "LINESTRING (0 0, 1 1)", "LINESTRING (2 2, 3 3)", geos::geom::GEOS_MULTILINESTRING }, + { "LINESTRING (0 0, 1 1)", "LINEARRING (2 2, 3 2, 3 3, 2 2)", geos::geom::GEOS_MULTILINESTRING }, + { "POLYGON ((0 0, 1 0, 1 1, 0 0))", "POLYGON ((2 2, 3 2, 3 3, 2 2))", geos::geom::GEOS_MULTIPOLYGON }, + { "CIRCULARSTRING (0 0, 1 1, 2 0)", "CIRCULARSTRING (2 0, 3 1, 4 0)", geos::geom::GEOS_MULTICURVE }, + { "COMPOUNDCURVE ((0 0, 1 1))", "COMPOUNDCURVE ((2 2, 3 3))", geos::geom::GEOS_MULTICURVE }, + { "LINESTRING (0 0, 1 1)", "CIRCULARSTRING (2 0, 3 1, 4 0)", geos::geom::GEOS_MULTICURVE }, + { "CURVEPOLYGON ((0 0, 1 0, 1 1, 0 0))", "CURVEPOLYGON ((2 2, 3 2, 3 3, 2 2))", geos::geom::GEOS_MULTISURFACE }, + { "POLYGON ((0 0, 1 0, 1 1, 0 0))", "CURVEPOLYGON ((2 2, 3 2, 3 3, 2 2))", geos::geom::GEOS_MULTISURFACE }, + { "MULTIPOINT ((0 0))", "MULTIPOINT ((2 2), (3 3))", geos::geom::GEOS_GEOMETRYCOLLECTION }, + { "MULTIPOLYGON (((0 0, 1 0, 1 1, 0 0)))", "MULTIPOLYGON (((2 2, 3 2, 3 3, 2 2)))", geos::geom::GEOS_GEOMETRYCOLLECTION }, + { "POINT (1 1)", "LINESTRING (2 2, 3 3)", geos::geom::GEOS_GEOMETRYCOLLECTION }, + }; + + for (const auto& c : cases) { + auto g1 = reader_.read(c.wkt1); + auto g2 = reader_.read(c.wkt2); + std::vector geoms{ g1.get(), g2.get() }; + + auto fromVector = factory_->buildGeometry(geoms); + ensure_equals(c.wkt2, fromVector->getGeometryTypeId(), c.type); + ensure_equals(c.wkt2, fromVector->getNumGeometries(), 2u); + + auto fromIterators = factory_->buildGeometry(geoms.begin(), geoms.end()); + ensure_equals(c.wkt2, fromIterators->getGeometryTypeId(), c.type); + ensure_equals(c.wkt2, fromIterators->getNumGeometries(), 2u); + } +} + } // namespace tut commit 11588117abcf73fe531d3805711070c50a345032 Author: Alejandro Pacheco Date: Thu Oct 29 07:16:51 2026 +0100 GeometryFactory::buildGeometry: return GeometryCollection when an element is a collection Since 4e50c4d3c, commonType compares the dimension of the elements rather than their type, so a collection among elements of one dimension gave a MultiPoint, MultiLineString or MultiPolygon holding that collection. Return a GeometryCollection for these, as the documentation of buildGeometry says and as 3.14 did. diff --git a/src/geom/GeometryFactory.cpp b/src/geom/GeometryFactory.cpp index 1454dc86a..541b90649 100644 --- a/src/geom/GeometryFactory.cpp +++ b/src/geom/GeometryFactory.cpp @@ -903,7 +903,9 @@ GeometryTypeId commonType(const T& geoms) { for (std::size_t i = 1; i < geoms.size(); i++) { curvedType |= geoms[i]->hasCurvedTypes(); - if (geoms[i]->getDimension() != dim) { + // A collection can only be an element of a GeometryCollection, + // even when its dimension matches the other elements. + if (geoms[i]->isCollection() || geoms[i]->getDimension() != dim) { return GEOS_GEOMETRYCOLLECTION; } } diff --git a/tests/unit/capi/GEOSDensifyTest.cpp b/tests/unit/capi/GEOSDensifyTest.cpp index 43e87d569..2f1145132 100644 --- a/tests/unit/capi/GEOSDensifyTest.cpp +++ b/tests/unit/capi/GEOSDensifyTest.cpp @@ -227,4 +227,20 @@ void object::test<14>() ); } +template<> +template<> +void object::test<15>() +{ + set_test_name("MultiPolygon with rings collapsed to lines gives a GeometryCollection"); + + input_ = fromWKT("MULTIPOLYGON (((0 0, 1 1, 0 0)), ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); + + result_ = GEOSDensify(input_, 100); + ensure(result_); + + // Not a MultiLineString with a MultiLineString element + ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION); + ensure_equals(toWKT(result_), "GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1, 0 0), MULTILINESTRING ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); +} + } // namespace tut diff --git a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp index 2c70f84d3..4f1f1609c 100644 --- a/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp +++ b/tests/unit/capi/GEOSGeom_setPrecisionTest.cpp @@ -339,5 +339,20 @@ void object::test<24>() ensure_geometry_equals_identical(result_, input_); } -} // namespace tut +template<> +template<> +void object::test<25>() +{ + set_test_name("MultiPolygon with rings collapsed to lines gives a GeometryCollection"); + input_ = fromWKT("MULTIPOLYGON (((0 0, 1 1, 0 0)), ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); + + result_ = GEOSGeom_setPrecision(input_, 1.0, GEOS_PREC_NO_TOPO); + ensure(result_); + + // Not a MultiLineString with a MultiLineString element + ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION); + ensure_equals(toWKT(result_), "GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1, 0 0), MULTILINESTRING ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); +} + +} // namespace tut diff --git a/tests/unit/capi/GEOSSnapTest.cpp b/tests/unit/capi/GEOSSnapTest.cpp index 7fedee2e6..f56f0d5d6 100644 --- a/tests/unit/capi/GEOSSnapTest.cpp +++ b/tests/unit/capi/GEOSSnapTest.cpp @@ -220,5 +220,20 @@ void object::test<11>() ensure("curved geometry not supported", result_ == nullptr); } -} // namespace tut +template<> +template<> +void object::test<12>() +{ + set_test_name("MultiPolygon with rings collapsed to lines gives a GeometryCollection"); + input_ = fromWKT("MULTIPOLYGON (((0 0, 1 1, 0 0)), ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); + + result_ = GEOSSnap(input_, input_, 0); + ensure(result_); + + // Not a MultiLineString with a MultiLineString element + ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION); + ensure_equals(toWKT(result_), "GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1, 0 0), MULTILINESTRING ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); +} + +} // namespace tut diff --git a/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp b/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp index a6ada22d1..11517b2e9 100644 --- a/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp +++ b/tests/unit/capi/GEOSTopologyPreserveSimplifyTest.cpp @@ -74,5 +74,20 @@ void object::test<4>() ensure_geometry_equals_identical(result_, expected_); } -} // namespace tut +template<> +template<> +void object::test<5>() +{ + set_test_name("MultiPolygon with rings collapsed to lines gives a GeometryCollection"); + input_ = fromWKT("MULTIPOLYGON (((0 0, 1 1, 0 0)), ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); + + result_ = GEOSTopologyPreserveSimplify(input_, 0); + ensure(result_); + + // Not a MultiLineString with a MultiLineString element + ensure_equals(GEOSGeomTypeId(result_), GEOS_GEOMETRYCOLLECTION); + ensure_equals(toWKT(result_), "GEOMETRYCOLLECTION (LINESTRING (0 0, 1 1, 0 0), MULTILINESTRING ((10 10, 11 11, 10 10), (0 0, 30 0, 30 30, 0 30, 0 0)))"); +} + +} // namespace tut diff --git a/tests/unit/geom/GeometryFactoryTest.cpp b/tests/unit/geom/GeometryFactoryTest.cpp index 3b10c2015..30a27a924 100644 --- a/tests/unit/geom/GeometryFactoryTest.cpp +++ b/tests/unit/geom/GeometryFactoryTest.cpp @@ -22,6 +22,8 @@ #include #include // std +#include +#include #include #include // std::size_t @@ -1575,5 +1577,49 @@ void object::test<43>() ensure_equals(g->getNumGeometries(), 2u); } } - + +template<> +template<> +void object::test<44>() +{ + set_test_name("buildGeometry returns a GeometryCollection when an element is a collection"); + + using geos::geom::Geometry; + + const std::vector> cases = { + { "POINT (1 1)", "MULTIPOINT ((2 2), (3 3))" }, + { "LINESTRING (0 0, 1 1)", "MULTILINESTRING ((2 2, 3 3), (4 4, 5 5))" }, + { "POLYGON ((0 0, 1 0, 1 1, 0 0))", "MULTIPOLYGON (((2 2, 3 2, 3 3, 2 2)))" }, + { "LINESTRING (0 0, 1 1)", "GEOMETRYCOLLECTION (LINESTRING (2 2, 3 3))" }, + { "CIRCULARSTRING (0 0, 1 1, 2 0)", "MULTICURVE ((2 2, 3 3))" }, + { "POLYGON ((0 0, 1 0, 1 1, 0 0))", "MULTISURFACE (((2 2, 3 2, 3 3, 2 2)))" }, + }; + + for (const auto& c : cases) { + auto single = reader_.read(c.first); + auto multi = reader_.read(c.second); + + for (bool multiFirst : { false, true }) { + const Geometry* a = multiFirst ? multi.get() : single.get(); + const Geometry* b = multiFirst ? single.get() : multi.get(); + + std::vector> owned; + owned.push_back(a->clone()); + owned.push_back(b->clone()); + auto fromOwned = factory_->buildGeometry(std::move(owned)); + ensure_equals(c.second, fromOwned->getGeometryTypeId(), geos::geom::GEOS_GEOMETRYCOLLECTION); + ensure_equals(c.second, fromOwned->getNumGeometries(), 2u); + + std::vector borrowed{ a, b }; + auto fromBorrowed = factory_->buildGeometry(borrowed); + ensure_equals(c.second, fromBorrowed->getGeometryTypeId(), geos::geom::GEOS_GEOMETRYCOLLECTION); + ensure_equals(c.second, fromBorrowed->getNumGeometries(), 2u); + + auto fromIterators = factory_->buildGeometry(borrowed.begin(), borrowed.end()); + ensure_equals(c.second, fromIterators->getGeometryTypeId(), geos::geom::GEOS_GEOMETRYCOLLECTION); + ensure_equals(c.second, fromIterators->getNumGeometries(), 2u); + } + } +} + } // namespace tut commit 9025863856f46c2c436e1ce070bead91d74b7fa1 Author: Daniel Baston Date: Thu Oct 29 10:28:09 2026 -0400 CircularArcs: Fix missing candidates in closestPointsArcSegment (#1577) Check for case where closest points are found on segment interior, arc endpoint, or segment/arc tangency missed by intersection test. Co-authored-by: Jeroen Bloemscheer diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp index f997f19f0..4c403f942 100644 --- a/src/algorithm/CircularArcs.cpp +++ b/src/algorithm/CircularArcs.cpp @@ -502,7 +502,7 @@ CircularArcs::closestPointsArcSegment(const CoordinateXY& c, double r, } // Calculate distance between line and circle center - const CoordinateXY d = geom::LineSegment::closestPoint(q0, q1, c); + const CoordinateXY d = LineSegment::closestPoint(q0, q1, c); const double dist_cd = d.distance(c); if (dist_cd <= r) { @@ -516,14 +516,18 @@ CircularArcs::closestPointsArcSegment(const CoordinateXY& c, double r, return {isect1, isect1}; } - // Closest point is one of the segment endpoints. - const auto e0 = closestPointArcPoint(c, r, p0, p2, isCCW, q0); - const auto e1 = closestPointArcPoint(c, r, p0, p2, isCCW, q1); - if (e0.distance(q0) < e1.distance(q1)) { - return {e0, q0}; - } + // Closest point should be a segment or arc endpoint + std::array, 5> candidates; + candidates[0] = {closestPointArcPoint(c, r, p0, p2, isCCW, q0), q0}; + candidates[1] = {closestPointArcPoint(c, r, p0, p2, isCCW, q1), q1}; + candidates[2] = {p0, LineSegment::closestPoint(q0, q1, p0)}; + candidates[3] = {p2, LineSegment::closestPoint(q0, q1, p2)}; + // But we could have also missed a tangency point + candidates[4] = {closestPointArcPoint(c, r, p0, p2, isCCW, d), d}; - return {e1, q1}; + return *std::min_element(candidates.begin(), candidates.end(), [](const auto& a, const auto& b) { + return a[0].distance(a[1]) < b[0].distance(b[1]); + }); } // Find closest point on circle @@ -541,10 +545,10 @@ CircularArcs::closestPointsArcSegment(const CoordinateXY& c, double r, const auto e1 = LineSegment::closestPoint(q0, q1, p2); if (e0.distance(p0) < e1.distance(p2)) { - return {e0, p0}; + return {p0, e0}; } - return {e1, p2}; + return {p2, e1}; } double diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp index a19ccb50d..b1de7b939 100644 --- a/tests/unit/algorithm/CircularArcsTest.cpp +++ b/tests/unit/algorithm/CircularArcsTest.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -1006,8 +1007,7 @@ set_test_name("getMidpointAngle"); template<> template<> -void object::test<71>() -{ +void object::test<71>() { set_test_name("containsAngle"); using geos::algorithm::Orientation; @@ -1043,4 +1043,90 @@ void object::test<71>() checkAngleContained(MATH_PI, MATH_PI/4, 9*MATH_PI/4, BOTH_); } +template<> +template<> +void object::test<72>() +{ + set_test_name("arc-seg distance, segment inside circle, closest to arc endpoint"); + + // The segment lies inside the circle and does not reach the arc, so the + // closest pair joins the arc endpoint at 70 degrees to the segment interior. + const double r = 10; + CircularArc arc = CircularArc::create(XY{r*std::cos(MATH_PI/3), r*std::sin(MATH_PI/3)}, + XY{r*std::cos(13*MATH_PI/36), r*std::sin(13*MATH_PI/36)}, + XY{r*std::cos(7*MATH_PI/18), r*std::sin(7*MATH_PI/18)}); + XY q0{2, 0}; + XY q1{2, 9.5}; + + ensure_equals("distance", arc.distance(q0, q1), arc.p2().x - 2, 1e-12); +} + +template<> +template<> +void object::test<73>() +{ + set_test_name("arc-seg distance, segment outside circle, closest to segment endpoint"); + + // The closest pair joins a segment endpoint to the arc interior. + CircularArc arc = CircularArc::create(XY{-3.48172, 2.91175}, XY{9.45735, -6.02278}, XY{1.44247, -0.448891}); + XY q0{4.48559, 4.09741}; + XY q1{-5.45188, -9.20177}; + + ensure_equals("distance", arc.distance(q0, q1), 0.2835665009749685, 1e-9); + + const auto pts = CircularArcs::closestPointsArcSegment(arc.getCenter(), arc.getRadius(), + arc.p0(), arc.p2(), arc.isCCW(), q0, q1); + ensure("first point is on the arc", std::abs(pts[0].distance(arc.getCenter()) - arc.getRadius()) < 1e-9); + ensure_equals("closest points distance", pts[0].distance(pts[1]), 0.2835665009749685, 1e-9); +} + +template<> +template<> +void object::test<74>() +{ + set_test_name("arc-seg distance, segment tangent to arc"); + + // The segment is tangent to the circle at (4, 3), which lies on the arc, + // but roundoff hides the intersection from circleIntersectsSegment. + CircularArc arc = CircularArc::create(XY{0, -5}, XY{4, -3}, XY{0, 5}); + XY q0{7, -1}; + XY q1{1, 7}; + + ensure_equals("distance", arc.distance(q0, q1), 0, 1e-12); + ensure_equals("distance (reversed segment)", arc.distance(q1, q0), 0, 1e-12); +} + +template<> +template<> +void object::test<75>() +{ + set_test_name("arc-seg distance, segment nearly tangent to arc"); + + // Found by differential testing against NetTopologySuite.Proofs ARC_SEGMENT_DISTANCE. + CircularArc arc = CircularArc::create(XY{-81.85405104117687, -125.94712311622965}, + XY{118.46299847713223, -81.33910481556457}, + XY{73.85498017646715, 118.97794470274454}); + XY q0{176.08930671152348, -164.72607692939968}; + XY q1{97.98804198508518, 227.31432409657776}; + + ensure_equals("distance", arc.distance(q0, q1), 0, 1e-9); + ensure_equals("distance (reversed segment)", arc.distance(q1, q0), 0, 1e-9); +} + +template<> +template<> +void object::test<76>() +{ + set_test_name("arc-seg closest points, segment outside circle, closest to arc endpoint"); + + // Closest pair joins the arc endpoint (5, 0) to the segment endpoint (10, -1). + CircularArc arc = CircularArc::create(XY{5, 0}, XY{3, 4}, XY{0, 5}); + XY q0{10, -10}; + XY q1{10, -1}; + + const auto pts = arc.closestPoints(q0, q1); + ensure_equals("first point is on the arc", pts[0], XY{5, 0}); + ensure_equals("second point is on the segment", pts[1], XY{10, -1}); +} + } commit c87e8f2d67d1d347bb81b1d741f426486f10a20e Author: Daniel Baston Date: Wed Oct 28 19:44:26 2026 -0400 CircularArcs: fix containsAngle for non-normalized angles (#1579) diff --git a/include/geos/algorithm/CircularArcs.h b/include/geos/algorithm/CircularArcs.h index a563e8f12..4b6fab60a 100644 --- a/include/geos/algorithm/CircularArcs.h +++ b/include/geos/algorithm/CircularArcs.h @@ -98,10 +98,12 @@ public: const geom::CoordinateXY& p0, const geom::CoordinateXY& p1, geom::CoordinateXY& isect0, geom::CoordinateXY& isect1); - /** Returns whether an angle is included between two other angles. + /** Returns whether an angle is included between two other angles. The angles + * do not need to be normalized. If the start and end angles are the same, + * the input is assumed to represent a full circle. * * @param t0 starting angle (radians) - * @param t2 ending engle (radians) + * @param t2 ending angle (radians) * @param isCCW whether the interval from t0 to t2 is counterclockwise * @param theta angle to test (radians) * @return true if theta is included between t0 and t2 diff --git a/src/algorithm/CircularArcs.cpp b/src/algorithm/CircularArcs.cpp index 03fc02cda..f997f19f0 100644 --- a/src/algorithm/CircularArcs.cpp +++ b/src/algorithm/CircularArcs.cpp @@ -416,10 +416,6 @@ CircularArcs::circleIntersectsSegment(const CoordinateXY& center, double r, bool CircularArcs::containsAngle(double t0, double t2, bool isCCW, double theta) { - if (theta == t0 || theta == t2) { - return true; - } - if (isCCW) { std::swap(t0, t2); } @@ -427,15 +423,9 @@ CircularArcs::containsAngle(double t0, double t2, bool isCCW, double theta) t2 -= t0; theta -= t0; - if (t2 < 0) { - t2 += 2*MATH_PI; - } - if (theta < 0) { - theta += 2*MATH_PI; - } - - return theta >= t2; + const auto norm = Angle::normalizePositive(theta); + return norm == 0 || norm >= Angle::normalizePositive(t2); } bool diff --git a/tests/unit/algorithm/CircularArcsTest.cpp b/tests/unit/algorithm/CircularArcsTest.cpp index 07eda9cab..a19ccb50d 100644 --- a/tests/unit/algorithm/CircularArcsTest.cpp +++ b/tests/unit/algorithm/CircularArcsTest.cpp @@ -20,8 +20,41 @@ namespace tut { struct test_circulararcs_data { const double eps = 1e-8; + static constexpr int CCW_ = geos::algorithm::Orientation::COUNTERCLOCKWISE; + static constexpr int CW_ = geos::algorithm::Orientation::CLOCKWISE; + static constexpr int BOTH_ = 0; + static constexpr int NONE_ = -2; + using XY = CoordinateXY; + static void checkAngleContained(double theta, double t0, double t2, int orientation) + { + std::string angleRange = std::to_string(t0) + " to " + std::to_string(t2); + + if (orientation == CCW_) { + ensure(std::to_string(theta) + " should be contained in ccw angle from " + angleRange, + CircularArcs::containsAngle(t0, t2, true, theta)); + ensure(std::to_string(theta) + " should not be contained in cw angle from " + angleRange, + !CircularArcs::containsAngle(t0, t2, false, theta)); + } + else if (orientation == CW_) { + ensure(std::to_string(theta) + " should not be contained in ccw angle from " + angleRange, + !CircularArcs::containsAngle(t0, t2, true, theta)); + ensure(std::to_string(theta) + " should be contained in cw angle from " + angleRange, + CircularArcs::containsAngle(t0, t2, false, theta)); + } else if (orientation == BOTH_) { + ensure(std::to_string(theta) + " should be contained in ccw angle from " + angleRange, + CircularArcs::containsAngle(t0, t2, true, theta)); + ensure(std::to_string(theta) + " should be contained in cw angle from " + angleRange, + CircularArcs::containsAngle(t0, t2, false, theta)); + } else { + ensure(std::to_string(theta) + " should not be contained in ccw angle from " + angleRange, + !CircularArcs::containsAngle(t0, t2, true, theta)); + ensure(std::to_string(theta) + " should not be contained in cw angle from " + angleRange, + !CircularArcs::containsAngle(t0, t2, false, theta)); + } + } + void checkEnvelope(const CoordinateXY& p0, const CoordinateXY& p1, const CoordinateXY& p2, double xmin, double ymin, double xmax, double ymax) const { @@ -970,4 +1003,44 @@ set_test_name("getMidpointAngle"); ensure_equals(CircularArcs::getMidpointAngle(0, 0, false), MATH_PI); } + +template<> +template<> +void object::test<71>() +{ + set_test_name("containsAngle"); + + using geos::algorithm::Orientation; + + checkAngleContained(MATH_PI / 2, 0, MATH_PI, CCW_); + checkAngleContained(MATH_PI / 2, 2*MATH_PI, 3*MATH_PI, CCW_); + checkAngleContained(5*MATH_PI / 2, 2*MATH_PI, 3*MATH_PI, CCW_); + checkAngleContained(-MATH_PI / 2, 0, MATH_PI, CW_); + checkAngleContained(2.5 + MATH_PI, -2.5, 0, CCW_); + + checkAngleContained(0, -MATH_PI / 2, MATH_PI / 2, CCW_); + checkAngleContained(2*MATH_PI, -MATH_PI / 2, MATH_PI / 2, CCW_); + checkAngleContained(4*MATH_PI, -MATH_PI / 2, MATH_PI / 2, CCW_); + checkAngleContained(-2*MATH_PI, -MATH_PI / 2, MATH_PI / 2, CCW_); + checkAngleContained(-4*MATH_PI, -MATH_PI / 2, MATH_PI / 2, CCW_); + + checkAngleContained(MATH_PI, 3*MATH_PI/4, -3*MATH_PI/4, CCW_); + checkAngleContained(-MATH_PI, 3*MATH_PI/4, -3*MATH_PI/4, CCW_); + checkAngleContained(0, 3*MATH_PI/4, -3*MATH_PI/4, CW_); + checkAngleContained(MATH_PI/2, 3*MATH_PI/4, -3*MATH_PI/4, CW_); + + // theta == t0 + checkAngleContained(0, 0, 0, BOTH_); + checkAngleContained(0, 0, MATH_PI, BOTH_); + checkAngleContained(2*MATH_PI, 0, MATH_PI, BOTH_); + + // t0 == t2, t0 == 0, full circle + checkAngleContained(MATH_PI, 0, 0, BOTH_); + checkAngleContained(MATH_PI, 0, 2*MATH_PI, BOTH_); + + // t0 == t2, t0 != 0, full circle + checkAngleContained(MATH_PI, MATH_PI/4, MATH_PI/4, BOTH_); + checkAngleContained(MATH_PI, MATH_PI/4, 9*MATH_PI/4, BOTH_); +} + } commit 771e8a83f8407a56f187433b441cc2e49a2ac4c0 Author: Daniel Baston Date: Wed Oct 28 11:39:25 2026 -0400 WKBWriter: preserve includeSRID setting after failed write (#1578) Resolves https://github.com/libgeos/geos/issues/1567 diff --git a/src/io/WKBWriter.cpp b/src/io/WKBWriter.cpp index 95c64b044..c71fd1031 100644 --- a/src/io/WKBWriter.cpp +++ b/src/io/WKBWriter.cpp @@ -186,9 +186,14 @@ WKBWriter::writeCompoundCurve(const CompoundCurve& g) auto orig_includeSRID = includeSRID; includeSRID = false; - for (std::size_t i = 0; i < g.getNumCurves(); i++) { - const SimpleCurve& section = *g.getCurveN(i); - writeSimpleCurve(section); + try { + for (std::size_t i = 0; i < g.getNumCurves(); i++) { + const SimpleCurve& section = *g.getCurveN(i); + writeSimpleCurve(section); + } + } catch (...) { + includeSRID = orig_includeSRID; + throw; } includeSRID = orig_includeSRID; @@ -273,12 +278,19 @@ WKBWriter::writeGeometryCollection(const GeometryCollection& g) includeSRID = false; assert(outStream); - for(std::size_t i = 0; i < ngeoms; i++) { - const Geometry* elem = g.getGeometryN(i); - assert(elem); - write(*elem, *outStream); + try { + for(std::size_t i = 0; i < ngeoms; i++) { + const Geometry* elem = g.getGeometryN(i); + assert(elem); + + write(*elem, *outStream); + } + } catch (...) { + includeSRID = orig_includeSRID; + throw; } + includeSRID = orig_includeSRID; } diff --git a/tests/unit/io/WKBWriterTest.cpp b/tests/unit/io/WKBWriterTest.cpp index 21387fe6c..71ef08ac3 100644 --- a/tests/unit/io/WKBWriterTest.cpp +++ b/tests/unit/io/WKBWriterTest.cpp @@ -3,6 +3,7 @@ // tut #include +#include // geos #include #include @@ -478,6 +479,39 @@ void object::test<19> "010A0000200E1600000200000001090000000200000001080000000500000000000000000000000000000000000000000000000000004000000000000000000000000000000040000000000000F03F00000000000000400000000000000840000000000000104000000000000008400102000000040000000000000000001040000000000000084000000000000010400000000000001440000000000000F03F000000000000104000000000000000000000000000000000010800000005000000333333333333FB3F000000000000F03F666666666666F63F9A9999999999D93F9A9999999999F93F9A9999999999D93F9A9999999999F93F000000000000E03F333333333333FB3F000000000000F03F"); } +// includeSRID is kept when writing a collection or a compound curve fails +// because the output stream throws +template<> +template<> +void object::test<20> +() { + // accepts 20 bytes, then fails + struct FixedBuf : std::streambuf { + char data[20]{}; + FixedBuf() { setp(data, data + sizeof data); } + }; + + geos::io::WKBWriter writer(2, geos::io::ByteOrderValues::ENDIAN_LITTLE, true); + auto point = wktreader.read("POINT (1 2)"); + point->setSRID(4326); + + for (const char *wkt: { + "GEOMETRYCOLLECTION (POINT (1 2), LINESTRING (0 0, 1 1))", + "COMPOUNDCURVE (CIRCULARSTRING (0 0, 1 1, 2 0), (2 0, 3 0))" + }) { + auto geom = wktreader.read(wkt); + geom->setSRID(3857); + + FixedBuf buf; + std::ostream os(&buf); + os.exceptions(std::ios::badbit | std::ios::failbit); + ensure_THROW(writer.write(*geom, os), std::ios_base::failure); + ensure("getIncludeSRID() should be set after writing " + std::string(wkt), writer.getIncludeSRID()); + std::stringstream out; + writer.writeHEX(*point, out); + ensure_equals(wkt, out.str(), "0101000020E6100000000000000000F03F0000000000000040"); + } +} } // namespace tut commit 68ef040f1cb2eec8573a2048ab343f02e31e5083 Author: Jeroen Bloemscheer Date: Mon Oct 26 21:07:40 2026 +0100 Envelope: Return zero distance between touching envelopes (#1576) distanceSquared computed each gap as a combined span minus two widths, which cancels: envelopes touching along y = -48 came out 7e-15 apart, so GEOSDistanceWithin(a, b, 0) was false for lines sharing an endpoint. Compute each gap as a single difference between facing sides. diff --git a/include/geos/geom/Envelope.h b/include/geos/geom/Envelope.h index 15d360030..654edbc2f 100644 --- a/include/geos/geom/Envelope.h +++ b/include/geos/geom/Envelope.h @@ -742,12 +742,8 @@ public: */ double distanceSquared(const Envelope& env) const { - double dx = std::max(0.0, - std::max(maxx, env.maxx) - std::min(minx, env.minx) - (maxx - minx) - - (env.maxx - env.minx)); - double dy = std::max(0.0, - std::max(maxy, env.maxy) - std::min(miny, env.miny) - (maxy - miny) - - (env.maxy - env.miny)); + double dx = std::max(0.0, std::max(minx - env.maxx, env.minx - maxx)); + double dy = std::max(0.0, std::max(miny - env.maxy, env.miny - maxy)); return dx * dx + dy * dy; }; diff --git a/tests/unit/capi/GEOSDistanceWithinTest.cpp b/tests/unit/capi/GEOSDistanceWithinTest.cpp index 0b164d199..044598918 100644 --- a/tests/unit/capi/GEOSDistanceWithinTest.cpp +++ b/tests/unit/capi/GEOSDistanceWithinTest.cpp @@ -437,4 +437,18 @@ void object::test<33>() } + +template<> +template<> +void object::test<34>() { + set_test_name("lines touching at endpoint"); + + testGEOSDistanceWithin( + "LINESTRING (0 -80, 64 -48)", + "LINESTRING (64 -48, 0 -0.1)", + 0.0, + 1 + ); +} + } // namespace tut diff --git a/tests/unit/geom/EnvelopeTest.cpp b/tests/unit/geom/EnvelopeTest.cpp index a289438bd..f4f95ff84 100644 --- a/tests/unit/geom/EnvelopeTest.cpp +++ b/tests/unit/geom/EnvelopeTest.cpp @@ -692,4 +692,18 @@ void object::test<21>() ensure(env.containsProperly({3.0000001, 8})); } + +template<> +template<> +void object::test<22>() +{ + set_test_name("distance between touching envelopes is zero"); + + Envelope a(0, 64, -80, -48); + Envelope b(0, 64, -48, -0.1); + + ensure_equals(a.distance(b), 0.0); + ensure_equals(b.distance(a), 0.0); +} + } commit dc0fe45d2e0fa938b025ed626df157b233546648 Author: Daniel Baston Date: Tue Oct 20 10:31:30 2026 -0400 CircularArcIntesector: check for common points before excluding based on distance (#1559) diff --git a/src/algorithm/CircularArcIntersector.cpp b/src/algorithm/CircularArcIntersector.cpp index d503fb384..a41265367 100644 --- a/src/algorithm/CircularArcIntersector.cpp +++ b/src/algorithm/CircularArcIntersector.cpp @@ -182,6 +182,30 @@ CircularArcIntersector::intersects(const CircularArc& arc, const CoordinateSeque } } +static std::vector +getCommonPoints(const CircularArc& arc1, const CircularArc& arc2) +{ + std::vector ret; + const CoordinateXY& ap0 = arc1.p0(); + const CoordinateXY& ap1 = arc1.p1(); + const CoordinateXY& ap2 = arc1.p2(); + const CoordinateXY& bp0 = arc2.p0(); + const CoordinateXY& bp1 = arc2.p1(); + const CoordinateXY& bp2 = arc2.p2(); + + if (ap0 == bp0 || ap0 == bp1 || ap0 == bp2) { + ret.push_back(ap0); + } + if (ap1 == bp0 || ap1 == bp1 || ap1 == bp2) { + ret.push_back(ap1); + } + if (ap2 == bp0 || ap2 == bp1 || ap2 == bp2) { + ret.push_back(ap2); + } + + return ret; +} + void CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& arc2) { @@ -217,16 +241,20 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a const auto d = c1.distance(c2); - if (d > r1 + r2) { - // Circles are disjoint - result = NO_INTERSECTION; - return; - } + auto commonPoints = getCommonPoints(arc1, arc2); - if (d < std::abs(r1-r2)) { - // One circle contained within the other; arcs cannot intersect - result = NO_INTERSECTION; - return; + if (commonPoints.empty()) { + if (d > r1 + r2) { + // Circles are disjoint + result = NO_INTERSECTION; + return; + } + + if (d < std::abs(r1-r2)) { + // One circle contained within the other; arcs cannot intersect + result = NO_INTERSECTION; + return; + } } // TODO because the circle center calculation is inexact we need some kind of tolerance here. @@ -253,22 +281,8 @@ CircularArcIntersector::intersects(const CircularArc& arc1, const CircularArc& a CoordinateXY isect0{p.x + h* dy/d, p.y - h* dx/d }; CoordinateXY isect1{p.x - h* dy/d, p.y + h* dx/d }; - // Check to see if computed intersection points are inexact versions of an endpoint intersection - const CoordinateXY& ap0 = arc1.p0(); - const CoordinateXY& ap1 = arc1.p1(); - const CoordinateXY& ap2 = arc1.p2(); - const CoordinateXY& bp0 = arc2.p0(); - const CoordinateXY& bp1 = arc2.p1(); - const CoordinateXY& bp2 = arc2.p2(); - - if (ap0 == bp0 || ap0 == bp1 || ap0 == bp2) { - closestPoint(isect0, isect1, 2, ap0) = ap0; - } - if (ap1 == bp0 || ap1 == bp1 || ap1 == bp2) { - closestPoint(isect0, isect1, 2, ap1) = ap1; - } - if (ap2 == bp0 || ap2 == bp1 || ap2 == bp2) { - closestPoint(isect0, isect1, 2, ap2) = ap2; + for (const auto& pt : commonPoints) { + closestPoint(isect0, isect1, 2 , pt) = pt; } if (arc1.containsPointOnCircle(isect0) && arc2.containsPointOnCircle(isect0)) { diff --git a/tests/unit/algorithm/CircularArcIntersectorTest.cpp b/tests/unit/algorithm/CircularArcIntersectorTest.cpp index 63695fb9b..518de55e2 100644 --- a/tests/unit/algorithm/CircularArcIntersectorTest.cpp +++ b/tests/unit/algorithm/CircularArcIntersectorTest.cpp @@ -1823,6 +1823,18 @@ void object::test<91>() XY{-3, 4}); } +template<> +template<> +void object::test<92>() +{ + set_test_name("arcs intersect at shared vertex when inexact distance calculation says they cannot"); + + checkIntersection(XY{5, 0}, XY{6.530331962423936, 32.9099081287533}, XY{39.1383458646334, 37.61324718856101}, + XY{5, 0}, XY{-13.013189892510923, 476.4218618845866}, XY{426.8482396243887, 660.3353967726679}, + CircularArcIntersector::ONE_POINT_INTERSECTION, + XY{5, 0}); +} + // TODO: check Z values of arc result centerpoints // TODO: add tests for seg/seg commit 4f5281976597245f3fb39aa786b928961a131234 Author: apocelipes Date: Thu Oct 15 08:43:29 2026 +0800 DiscreteFrechetDistance: Add missing algorithm header file (#1563) diff --git a/include/geos/algorithm/distance/DiscreteFrechetDistance.h b/include/geos/algorithm/distance/DiscreteFrechetDistance.h index 819a4b146..3a90e256c 100644 --- a/include/geos/algorithm/distance/DiscreteFrechetDistance.h +++ b/include/geos/algorithm/distance/DiscreteFrechetDistance.h @@ -20,6 +20,7 @@ #include #include +#include // for lower_bound #include #include #include commit a3022a63ce3d29e62b3b14861446f641376ff4d1 Author: apocelipes Date: Wed Oct 14 18:49:00 2026 +0800 CoordinateSequence: Add missing type_traits header file (#1562) diff --git a/include/geos/geom/CoordinateSequence.h b/include/geos/geom/CoordinateSequence.h index e52849e7f..c7d5920f2 100644 --- a/include/geos/geom/CoordinateSequence.h +++ b/include/geos/geom/CoordinateSequence.h @@ -25,6 +25,7 @@ #include #include // ostream #include // for unique_ptr typedef +#include // for add_const // Forward declarations namespace geos { ----------------------------------------------------------------------- Summary of changes: NEWS.md | 14 +- include/geos/algorithm/CircularArcs.h | 6 +- .../algorithm/distance/DiscreteFrechetDistance.h | 1 + include/geos/geom/CircularString.h | 9 ++ include/geos/geom/CompoundCurve.h | 5 + include/geos/geom/CoordinateSequence.h | 1 + include/geos/geom/Envelope.h | 8 +- include/geos/geom/GeometryFactory.h | 49 +----- include/geos/index/SpatialIndex.h | 4 +- src/algorithm/CircularArcIntersector.cpp | 64 +++++--- src/algorithm/CircularArcs.cpp | 38 ++--- src/geom/CircularArc.cpp | 22 +-- src/geom/CircularString.cpp | 18 +++ src/geom/GeometryFactory.cpp | 4 +- src/geom/MultiLineString.cpp | 9 +- src/geom/MultiPoint.cpp | 7 +- src/geom/MultiPolygon.cpp | 9 +- src/io/WKBWriter.cpp | 26 +++- src/noding/NodableArcString.cpp | 2 +- .../unit/algorithm/CircularArcIntersectorTest.cpp | 12 ++ tests/unit/algorithm/CircularArcsTest.cpp | 159 +++++++++++++++++++ tests/unit/capi/GEOSDensifyTest.cpp | 16 ++ tests/unit/capi/GEOSDistanceWithinTest.cpp | 14 ++ tests/unit/capi/GEOSGeom_createCollectionTest.cpp | 43 +++++- tests/unit/capi/GEOSGeom_setPrecisionTest.cpp | 17 ++- tests/unit/capi/GEOSNodeTest.cpp | 23 +++ tests/unit/capi/GEOSSnapTest.cpp | 17 ++- .../unit/capi/GEOSTopologyPreserveSimplifyTest.cpp | 17 ++- tests/unit/capi/GEOSUnaryUnionTest.cpp | 15 ++ tests/unit/geom/CircularArcTest.cpp | 28 ++++ tests/unit/geom/CircularStringTest.cpp | 170 +++++++++++++++++++++ tests/unit/geom/CompoundCurveTest.cpp | 26 ++++ tests/unit/geom/EnvelopeTest.cpp | 14 ++ tests/unit/geom/GeometryFactoryTest.cpp | 132 +++++++++++++++- tests/unit/io/WKBWriterTest.cpp | 34 +++++ tests/unit/noding/NodableArcStringTest.cpp | 71 ++++++++- 36 files changed, 959 insertions(+), 145 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Nov 5 06:10:37 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 5 Nov 2026 06:10:37 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. fc3eca9063096a665700651d6523f56de4dae415 Message-ID: <20261105141037.E51961A4268@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 fc3eca9063096a665700651d6523f56de4dae415 (commit) from 8bace6161f6593643e45875d7972247699357720 (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 fc3eca9063096a665700651d6523f56de4dae415 Author: Daniel Baston Date: Thu Nov 5 09:10:08 2026 -0500 GridIntersection: robustness fixes (#1589) Co-authored-by: Alejandro Pacheco diff --git a/include/geos/operation/grid/Grid.h b/include/geos/operation/grid/Grid.h index d94d80916..e56d7dd84 100644 --- a/include/geos/operation/grid/Grid.h +++ b/include/geos/operation/grid/Grid.h @@ -19,6 +19,7 @@ #include #include +#include namespace geos::operation::grid { struct infinite_extent @@ -69,6 +70,12 @@ class GEOS_DLL Grid , m_num_cols{ 2 * extent_tag::padding + (extent.getMaxX() > extent.getMinX() ? static_cast(std::round(extent.getWidth() / dx)) : 0) } { static_assert(std::is_same_v); + if (!isEmpty()) { + if (m_domain.getMinX() >= m_extent.getMinX() || m_domain.getMaxX() <= m_extent.getMaxX() || + m_domain.getMinY() >= m_extent.getMinY() || m_domain.getMaxY() <= m_extent.getMaxY()) { + throw geos::util::IllegalArgumentException("Domain of infinite grid must be larger than extent"); + } + } } diff --git a/src/operation/grid/Grid.cpp b/src/operation/grid/Grid.cpp index d4db6659b..bd63dff71 100644 --- a/src/operation/grid/Grid.cpp +++ b/src/operation/grid/Grid.cpp @@ -41,11 +41,9 @@ Grid::getCellEnvelope(size_t row, size_t col) const { double cell_xmin, cell_xmax, cell_ymin, cell_ymax; - constexpr double PADDED_CELL_SIZE = 1.0; - if (col == 0) { // Left-side padding - cell_xmin = std::min(xmin() - PADDED_CELL_SIZE, m_domain.getMinX()); + cell_xmin = m_domain.getMinX(); } else if (col == getNumCols() - 1) { // Right-side padding cell_xmin = xmax(); // because rightmost col of regular may have different width from others @@ -57,7 +55,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const switch (getNumCols() - col) { case 1: // Right-side padding - cell_xmax = std::max(xmax() + PADDED_CELL_SIZE, m_domain.getMaxX()); + cell_xmax = m_domain.getMaxX(); break; case 2: // Rightmost internal cell @@ -69,7 +67,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const if (row == 0) { // Top row padding - cell_ymax = std::max(ymax() + PADDED_CELL_SIZE, m_domain.getMaxY()); + cell_ymax = m_domain.getMaxY(); } else if (row == getNumRows() - 1) { // Bottom row padding cell_ymax = ymin(); // because bottom row of regular may have different height from others @@ -80,7 +78,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const switch (getNumRows() - row) { case 1: // Bottom row padding - cell_ymin = std::min(xmin() - PADDED_CELL_SIZE, m_domain.getMinY()); + cell_ymin = m_domain.getMinY(); break; case 2: // Bottom-most internal row diff --git a/src/operation/grid/GridIntersection.cpp b/src/operation/grid/GridIntersection.cpp index c60c3aaec..ef94ce5ad 100644 --- a/src/operation/grid/GridIntersection.cpp +++ b/src/operation/grid/GridIntersection.cpp @@ -34,6 +34,24 @@ using geos::geom::CoordinateXY; namespace geos::operation::grid { +static Envelope +getDomain(const Envelope& gridExtent, const Envelope& geomExtent) +{ + constexpr double PADDED_CELL_SIZE = 1.0; + constexpr double PADDED_CELL_RELATIVE_SIZE = 1e-12; + + Envelope padded(gridExtent); + padded.expandToInclude(geomExtent); + + const double xmag = std::max(std::abs(padded.getMinX()), std::abs(padded.getMaxX())); + const double ymag = std::max(std::abs(padded.getMinY()), std::abs(padded.getMaxY())); + + padded.expandBy(std::max(PADDED_CELL_SIZE, PADDED_CELL_RELATIVE_SIZE * xmag), + std::max(PADDED_CELL_SIZE, PADDED_CELL_RELATIVE_SIZE * ymag)); + + return padded; +} + std::shared_ptr> GridIntersection::getIntersectionFractions(const Grid& raster_grid, const Geometry& g) { @@ -111,7 +129,7 @@ GridIntersection::processingRegion(const Envelope& raster_extent, const Geometry } GridIntersection::GridIntersection(const Grid& raster_grid, const Geometry& g, const std::shared_ptr>& cov) - : m_geometry_grid{ make_infinite(raster_grid, *g.getEnvelopeInternal()) } + : m_geometry_grid{ make_infinite(raster_grid, getDomain(raster_grid.getExtent(), *g.getEnvelopeInternal())) } , m_results{ cov ? cov : std::make_shared>(raster_grid.getNumRows(), raster_grid.getNumCols()) } , m_first_geom{ true } , m_areal{ false } @@ -130,7 +148,7 @@ GridIntersection::GridIntersection(const Grid& raster_grid, cons } GridIntersection::GridIntersection(const Grid& raster_grid, const Envelope& box, const std::shared_ptr>& cov) - : m_geometry_grid{ make_infinite(raster_grid, box) } + : m_geometry_grid{ make_infinite(raster_grid, getDomain(raster_grid.getExtent(), box)) } , m_results{ cov ? cov : std::make_shared>(raster_grid.getNumRows(), raster_grid.getNumCols()) } , m_first_geom { true } , m_areal{ false } @@ -182,6 +200,10 @@ GridIntersection::processRectangularRing(const Envelope& box, bool exterior_ring auto ring_grid = getSubgrid(box, m_geometry_grid); + if (ring_grid.isEmpty()) { + return; + } + auto row_min = ring_grid.getRow(box.getMaxY()); auto row_max = ring_grid.getRow(box.getMinY()); auto col_min = ring_grid.getColumn(box.getMinX()); @@ -590,8 +612,7 @@ GridIntersection::subdividePolygon(const Grid& p_grid, const Geo const auto cropped_grid = p_grid.shrinkToFit(p_grid.getExtent().intersection(*g.getEnvelopeInternal()), false); - geom::Envelope gridExtent = *g.getEnvelopeInternal(); - gridExtent.expandBy(1); + geom::Envelope gridExtent = getDomain(cropped_grid.getExtent(), *g.getEnvelopeInternal()); const Grid cell_grid = make_infinite(cropped_grid, gridExtent); Matrix> cells(cell_grid.getNumRows(), cell_grid.getNumCols()); diff --git a/tests/unit/operation/grid/GridIntersectionTest.cpp b/tests/unit/operation/grid/GridIntersectionTest.cpp index 856bbe60d..940d54f3a 100644 --- a/tests/unit/operation/grid/GridIntersectionTest.cpp +++ b/tests/unit/operation/grid/GridIntersectionTest.cpp @@ -1013,4 +1013,83 @@ void object::test<54>() ensure_equals("check area", tot/100, g->getArea(), 1e-6); } +template<> +template<> +void object::test<55>() +{ + set_test_name("getIntersectionFractions: traversal stays in grid when a vertex is the top corner of the geometry envelope"); + + // The vertex at (-259.05369950585896 811.4225290921494) is the upper-left corner of the polygon envelope + // and lies on a cell boundary, to within rounding. + Envelope e(-300.91149168825694, -175.33811514106293, 225.41343853857734, 602.13356818015927); + + Grid ext(e, e.getWidth() / 6, e.getHeight() / 18); + + auto g = wkt_reader_.read("POLYGON ((-70.69363468506793 685.8491525449554, -133.48032295866494 748.6358408185523, -175.33811514106293 790.4936330009504, -259.05369950585896 811.4225290921494, -238.12480341465994 706.7780486361544, -259.05369950585896 664.9202564537563, -259.05369950585896 581.2046720889604, -196.26701123226195 643.9913603625573, -112.55142686746595 623.0624642713583, -70.69363468506793 685.8491525449554))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_area(*fracs, ext, *inside); +} + +template<> +template<> +void object::test<56>() +{ + set_test_name("getIntersectionFractions: traversal stays in grid when a vertex is the bottom corner of the geometry envelope"); + + // The vertex at (68.44482903503193 -90.56515438144179) is the lower-right corner of the polygon envelope + // and lies on a cell boundary, to within rounding. + Envelope e(-10.004484448704343, 152.12409675101728, 35.385086543444757, 73.717768564062396); + Grid ext(e, e.getWidth() / 31, e.getHeight() / 14); + + auto g = wkt_reader_.read("POLYGON ((173.04391368001362 60.02752498527039, 78.9047374995301 109.31240186892164, 89.36464596402827 257.1670325198754, -25.694347145451598 213.35825306774097, -93.68375216468971 161.3353274683313, -104.14366062918788 90.14606085861283, -119.83352332593513 24.432891680411146, -51.84411830669703 5.266550670102323, -20.464392913202513 -52.23247236082415, 68.44482903503193 -90.56515438144179, 173.04391368001362 -54.97052107658256, 173.04391368001362 60.02752498527039))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_area(*fracs, ext, *inside); +} + +template<> +template<> +void object::test<57>() +{ + set_test_name("subdividePolygon: traversal stays in grid when padding is lost to rounding at large coordinates"); + + // At 1e22, adding a fixed padding of 1 to the geometry envelope has no effect, + // so the vertex at (1e22 0.9) would lie on the outer edge of a padding cell. + Envelope e(-0.375, 2.1875, -0.375, 2.1875); + Grid ext(e, e.getWidth() / 64, e.getHeight() / 2); + + auto g = wkt_reader_.read("POLYGON ((0.1 0.1, 0.1 0.2, 0.95 0.95, 1e22 0.9, 0.1 0.1))"); + + auto subd = GridIntersection::subdividePolygon(ext, *g, false); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_subdivided_polygon(*inside, *subd); +} + +template<> +template<> +void object::test<58>() +{ + set_test_name("getIntersectionFractions: box sharing an edge with the grid"); + + Envelope e(0, 10, 0, 10); + Grid ext(e, 1, 1); + + for (const auto& box : { + Envelope(10, 20, 0, 10), + Envelope(0, 10, 10, 20), + Envelope(-10, 0, -10, 0) }) + { + auto fracs = GridIntersection::getIntersectionFractions(ext, box); + auto tot = std::accumulate(fracs->begin(), fracs->end(), 0.0); + + ensure_equals(tot, 0.0); + } +} + } diff --git a/tests/unit/operation/grid/GridTest.cpp b/tests/unit/operation/grid/GridTest.cpp index 0df5ea8a7..c142c4911 100644 --- a/tests/unit/operation/grid/GridTest.cpp +++ b/tests/unit/operation/grid/GridTest.cpp @@ -314,8 +314,10 @@ void object::test<17>() set_test_name("Crop infinite grid while calculating cell boundaries relative to parent"); double res = 1.0/3601; + Envelope domain = global; + domain.expandBy(1); - Grid grid{global, res, res, global}; + Grid grid{global, res, res, domain}; const auto env0 = grid.getCellEnvelope(30, 30); ----------------------------------------------------------------------- Summary of changes: include/geos/operation/grid/Grid.h | 7 ++ src/operation/grid/Grid.cpp | 10 ++- src/operation/grid/GridIntersection.cpp | 29 ++++++-- tests/unit/operation/grid/GridIntersectionTest.cpp | 79 ++++++++++++++++++++++ tests/unit/operation/grid/GridTest.cpp | 4 +- 5 files changed, 118 insertions(+), 11 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Thu Nov 5 06:11:15 2026 From: git at osgeo.org (git at osgeo.org) Date: Thu, 5 Nov 2026 06:11:15 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 2c8e04faf8235456b417979bcf23271ef34148a7 Message-ID: <20261105141115.BA0FC1A455A@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 2c8e04faf8235456b417979bcf23271ef34148a7 (commit) from fc3eca9063096a665700651d6523f56de4dae415 (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 2c8e04faf8235456b417979bcf23271ef34148a7 Author: Daniel Baston Date: Thu Nov 5 09:10:33 2026 -0500 AdjacentEdgeLocator: Avoid checking empty rings (#1590) Resolves https://github.com/libgeos/geos/issues/1585 diff --git a/src/operation/relateng/AdjacentEdgeLocator.cpp b/src/operation/relateng/AdjacentEdgeLocator.cpp index aeb5f8084..fa63f4fc9 100644 --- a/src/operation/relateng/AdjacentEdgeLocator.cpp +++ b/src/operation/relateng/AdjacentEdgeLocator.cpp @@ -133,6 +133,10 @@ AdjacentEdgeLocator::addRings(const Geometry* geom) void AdjacentEdgeLocator::addRing(const LinearRing* ring, bool requireCW) { + if (ring->isEmpty()) { + return; + } + //TODO: remove repeated points? const CoordinateSequence* pts = ring->getCoordinatesRO(); bool isFlipped = requireCW == Orientation::isCCW(pts); diff --git a/tests/unit/operation/relateng/RelateNGTest.cpp b/tests/unit/operation/relateng/RelateNGTest.cpp index ee8d3a841..8c7860577 100644 --- a/tests/unit/operation/relateng/RelateNGTest.cpp +++ b/tests/unit/operation/relateng/RelateNGTest.cpp @@ -925,5 +925,17 @@ void object::test<64> () checkRelate(b, a, "102F01FF2"); } +template<> +template<> +void object::test<65>() +{ + set_test_name("GeometryCollection with empty polygon"); -} // namespace tut + std::string a = "POINT (1 0.5)"; + std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0)), POLYGON ((2 0, 2 1, 1 1, 1 0, 2 0)), POLYGON EMPTY)"; + + checkIntersectsDisjoint(a, b, true); +} + + +} // namespace tut \ No newline at end of file ----------------------------------------------------------------------- Summary of changes: src/operation/relateng/AdjacentEdgeLocator.cpp | 4 ++++ tests/unit/operation/relateng/RelateNGTest.cpp | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Nov 6 10:09:10 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 6 Nov 2026 10:09:10 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch 3.15 updated. 5e34781c76282e949dde1498fc89a3f8f42d2e20 Message-ID: <20261106180911.6CC9840FE0@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, 3.15 has been updated via 5e34781c76282e949dde1498fc89a3f8f42d2e20 (commit) via a38377342a9c1786c425b9a33270eec64900866a (commit) via a6f1f8227fd45b5e16cb21f0f03a26b6cf61f54d (commit) from 3ee1c785707005880df0899d9ba153093b4e8a85 (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 5e34781c76282e949dde1498fc89a3f8f42d2e20 Author: Daniel Baston Date: Fri Nov 6 11:32:00 2026 -0500 NEWS updates diff --git a/NEWS.md b/NEWS.md index 05d6eecaf..e836ba19d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -6,6 +6,7 @@ - Arc noding: handle degenerate arcs with repeated points (GH-1584, Alejandro Pacheco and Dan Baston) - Make GeometrySplitter able to split MultiSurface inputs (GH-1518, Dan Baston) - GEOSGridIntersectionFractions: Set cells outside geometry bounding box to zero (GH-1532, Dan Baston) + - GEOSGridIntersectionFractions: Improve robustness (GH-1589, Dan Baston, Alejandro Pacheco) - Skip testing FE_INVALID from unit-geom-Envelope for some Clang builds (GH-1555, Mike Taves) - Polygonize / LineMerge: Copy Z/M values at shared endpoints of mixed-dimension inputs (GH-1545, Dan Baston) - CircularArcIntersector: Use Orient2D to check for arc control points on line (GH-1546, Dan Baston) @@ -20,6 +21,7 @@ - Envelope: improve robustness of distanceSquared (GH-1576, Jeroen Bloemscheer) - Distance: avoid erroneously large distance between arc and segment (GH-1577, Dan Baston and Jeroen Bloemscheer) - GeometryFactory: fix return type from buildGeometry with collection inputs (GH-1580, Alejandro Pacheco) + - RelateNG: fix crash with on GeometryCollection with empty Polygon (GH-1590, Dan Baston) ## Changes in 3.15.0 2026-09-01 commit a38377342a9c1786c425b9a33270eec64900866a Author: Daniel Baston Date: Thu Nov 5 09:10:33 2026 -0500 AdjacentEdgeLocator: Avoid checking empty rings (#1590) Resolves https://github.com/libgeos/geos/issues/1585 diff --git a/src/operation/relateng/AdjacentEdgeLocator.cpp b/src/operation/relateng/AdjacentEdgeLocator.cpp index aeb5f8084..fa63f4fc9 100644 --- a/src/operation/relateng/AdjacentEdgeLocator.cpp +++ b/src/operation/relateng/AdjacentEdgeLocator.cpp @@ -133,6 +133,10 @@ AdjacentEdgeLocator::addRings(const Geometry* geom) void AdjacentEdgeLocator::addRing(const LinearRing* ring, bool requireCW) { + if (ring->isEmpty()) { + return; + } + //TODO: remove repeated points? const CoordinateSequence* pts = ring->getCoordinatesRO(); bool isFlipped = requireCW == Orientation::isCCW(pts); diff --git a/tests/unit/operation/relateng/RelateNGTest.cpp b/tests/unit/operation/relateng/RelateNGTest.cpp index ee8d3a841..8c7860577 100644 --- a/tests/unit/operation/relateng/RelateNGTest.cpp +++ b/tests/unit/operation/relateng/RelateNGTest.cpp @@ -925,5 +925,17 @@ void object::test<64> () checkRelate(b, a, "102F01FF2"); } +template<> +template<> +void object::test<65>() +{ + set_test_name("GeometryCollection with empty polygon"); -} // namespace tut + std::string a = "POINT (1 0.5)"; + std::string b = "GEOMETRYCOLLECTION (POLYGON ((1 0, 1 1, 0 1, 0 0, 1 0)), POLYGON ((2 0, 2 1, 1 1, 1 0, 2 0)), POLYGON EMPTY)"; + + checkIntersectsDisjoint(a, b, true); +} + + +} // namespace tut \ No newline at end of file commit a6f1f8227fd45b5e16cb21f0f03a26b6cf61f54d Author: Daniel Baston Date: Thu Nov 5 09:10:08 2026 -0500 GridIntersection: robustness fixes (#1589) Co-authored-by: Alejandro Pacheco diff --git a/include/geos/operation/grid/Grid.h b/include/geos/operation/grid/Grid.h index d94d80916..e56d7dd84 100644 --- a/include/geos/operation/grid/Grid.h +++ b/include/geos/operation/grid/Grid.h @@ -19,6 +19,7 @@ #include #include +#include namespace geos::operation::grid { struct infinite_extent @@ -69,6 +70,12 @@ class GEOS_DLL Grid , m_num_cols{ 2 * extent_tag::padding + (extent.getMaxX() > extent.getMinX() ? static_cast(std::round(extent.getWidth() / dx)) : 0) } { static_assert(std::is_same_v); + if (!isEmpty()) { + if (m_domain.getMinX() >= m_extent.getMinX() || m_domain.getMaxX() <= m_extent.getMaxX() || + m_domain.getMinY() >= m_extent.getMinY() || m_domain.getMaxY() <= m_extent.getMaxY()) { + throw geos::util::IllegalArgumentException("Domain of infinite grid must be larger than extent"); + } + } } diff --git a/src/operation/grid/Grid.cpp b/src/operation/grid/Grid.cpp index d4db6659b..bd63dff71 100644 --- a/src/operation/grid/Grid.cpp +++ b/src/operation/grid/Grid.cpp @@ -41,11 +41,9 @@ Grid::getCellEnvelope(size_t row, size_t col) const { double cell_xmin, cell_xmax, cell_ymin, cell_ymax; - constexpr double PADDED_CELL_SIZE = 1.0; - if (col == 0) { // Left-side padding - cell_xmin = std::min(xmin() - PADDED_CELL_SIZE, m_domain.getMinX()); + cell_xmin = m_domain.getMinX(); } else if (col == getNumCols() - 1) { // Right-side padding cell_xmin = xmax(); // because rightmost col of regular may have different width from others @@ -57,7 +55,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const switch (getNumCols() - col) { case 1: // Right-side padding - cell_xmax = std::max(xmax() + PADDED_CELL_SIZE, m_domain.getMaxX()); + cell_xmax = m_domain.getMaxX(); break; case 2: // Rightmost internal cell @@ -69,7 +67,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const if (row == 0) { // Top row padding - cell_ymax = std::max(ymax() + PADDED_CELL_SIZE, m_domain.getMaxY()); + cell_ymax = m_domain.getMaxY(); } else if (row == getNumRows() - 1) { // Bottom row padding cell_ymax = ymin(); // because bottom row of regular may have different height from others @@ -80,7 +78,7 @@ Grid::getCellEnvelope(size_t row, size_t col) const switch (getNumRows() - row) { case 1: // Bottom row padding - cell_ymin = std::min(xmin() - PADDED_CELL_SIZE, m_domain.getMinY()); + cell_ymin = m_domain.getMinY(); break; case 2: // Bottom-most internal row diff --git a/src/operation/grid/GridIntersection.cpp b/src/operation/grid/GridIntersection.cpp index c60c3aaec..ef94ce5ad 100644 --- a/src/operation/grid/GridIntersection.cpp +++ b/src/operation/grid/GridIntersection.cpp @@ -34,6 +34,24 @@ using geos::geom::CoordinateXY; namespace geos::operation::grid { +static Envelope +getDomain(const Envelope& gridExtent, const Envelope& geomExtent) +{ + constexpr double PADDED_CELL_SIZE = 1.0; + constexpr double PADDED_CELL_RELATIVE_SIZE = 1e-12; + + Envelope padded(gridExtent); + padded.expandToInclude(geomExtent); + + const double xmag = std::max(std::abs(padded.getMinX()), std::abs(padded.getMaxX())); + const double ymag = std::max(std::abs(padded.getMinY()), std::abs(padded.getMaxY())); + + padded.expandBy(std::max(PADDED_CELL_SIZE, PADDED_CELL_RELATIVE_SIZE * xmag), + std::max(PADDED_CELL_SIZE, PADDED_CELL_RELATIVE_SIZE * ymag)); + + return padded; +} + std::shared_ptr> GridIntersection::getIntersectionFractions(const Grid& raster_grid, const Geometry& g) { @@ -111,7 +129,7 @@ GridIntersection::processingRegion(const Envelope& raster_extent, const Geometry } GridIntersection::GridIntersection(const Grid& raster_grid, const Geometry& g, const std::shared_ptr>& cov) - : m_geometry_grid{ make_infinite(raster_grid, *g.getEnvelopeInternal()) } + : m_geometry_grid{ make_infinite(raster_grid, getDomain(raster_grid.getExtent(), *g.getEnvelopeInternal())) } , m_results{ cov ? cov : std::make_shared>(raster_grid.getNumRows(), raster_grid.getNumCols()) } , m_first_geom{ true } , m_areal{ false } @@ -130,7 +148,7 @@ GridIntersection::GridIntersection(const Grid& raster_grid, cons } GridIntersection::GridIntersection(const Grid& raster_grid, const Envelope& box, const std::shared_ptr>& cov) - : m_geometry_grid{ make_infinite(raster_grid, box) } + : m_geometry_grid{ make_infinite(raster_grid, getDomain(raster_grid.getExtent(), box)) } , m_results{ cov ? cov : std::make_shared>(raster_grid.getNumRows(), raster_grid.getNumCols()) } , m_first_geom { true } , m_areal{ false } @@ -182,6 +200,10 @@ GridIntersection::processRectangularRing(const Envelope& box, bool exterior_ring auto ring_grid = getSubgrid(box, m_geometry_grid); + if (ring_grid.isEmpty()) { + return; + } + auto row_min = ring_grid.getRow(box.getMaxY()); auto row_max = ring_grid.getRow(box.getMinY()); auto col_min = ring_grid.getColumn(box.getMinX()); @@ -590,8 +612,7 @@ GridIntersection::subdividePolygon(const Grid& p_grid, const Geo const auto cropped_grid = p_grid.shrinkToFit(p_grid.getExtent().intersection(*g.getEnvelopeInternal()), false); - geom::Envelope gridExtent = *g.getEnvelopeInternal(); - gridExtent.expandBy(1); + geom::Envelope gridExtent = getDomain(cropped_grid.getExtent(), *g.getEnvelopeInternal()); const Grid cell_grid = make_infinite(cropped_grid, gridExtent); Matrix> cells(cell_grid.getNumRows(), cell_grid.getNumCols()); diff --git a/tests/unit/operation/grid/GridIntersectionTest.cpp b/tests/unit/operation/grid/GridIntersectionTest.cpp index 2c5cb8a01..940d54f3a 100644 --- a/tests/unit/operation/grid/GridIntersectionTest.cpp +++ b/tests/unit/operation/grid/GridIntersectionTest.cpp @@ -998,4 +998,98 @@ void object::test<53>() { ensure(subd->getGeometryN(1)->equals(outside.get())); } +template<> +template<> +void object::test<54>() +{ + Envelope e(0, 10, 0, 10); + Grid ext(e, 0.1, 0.1); + + auto g = wkt_reader_.read("POLYGON ((0.3 0.3, 4.7 0.3, 2.5 6.2, 0.3 0.3))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + auto tot = std::accumulate(fracs->begin(), fracs->end(), 0.0); + + ensure_equals("check area", tot/100, g->getArea(), 1e-6); +} + +template<> +template<> +void object::test<55>() +{ + set_test_name("getIntersectionFractions: traversal stays in grid when a vertex is the top corner of the geometry envelope"); + + // The vertex at (-259.05369950585896 811.4225290921494) is the upper-left corner of the polygon envelope + // and lies on a cell boundary, to within rounding. + Envelope e(-300.91149168825694, -175.33811514106293, 225.41343853857734, 602.13356818015927); + + Grid ext(e, e.getWidth() / 6, e.getHeight() / 18); + + auto g = wkt_reader_.read("POLYGON ((-70.69363468506793 685.8491525449554, -133.48032295866494 748.6358408185523, -175.33811514106293 790.4936330009504, -259.05369950585896 811.4225290921494, -238.12480341465994 706.7780486361544, -259.05369950585896 664.9202564537563, -259.05369950585896 581.2046720889604, -196.26701123226195 643.9913603625573, -112.55142686746595 623.0624642713583, -70.69363468506793 685.8491525449554))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_area(*fracs, ext, *inside); +} + +template<> +template<> +void object::test<56>() +{ + set_test_name("getIntersectionFractions: traversal stays in grid when a vertex is the bottom corner of the geometry envelope"); + + // The vertex at (68.44482903503193 -90.56515438144179) is the lower-right corner of the polygon envelope + // and lies on a cell boundary, to within rounding. + Envelope e(-10.004484448704343, 152.12409675101728, 35.385086543444757, 73.717768564062396); + Grid ext(e, e.getWidth() / 31, e.getHeight() / 14); + + auto g = wkt_reader_.read("POLYGON ((173.04391368001362 60.02752498527039, 78.9047374995301 109.31240186892164, 89.36464596402827 257.1670325198754, -25.694347145451598 213.35825306774097, -93.68375216468971 161.3353274683313, -104.14366062918788 90.14606085861283, -119.83352332593513 24.432891680411146, -51.84411830669703 5.266550670102323, -20.464392913202513 -52.23247236082415, 68.44482903503193 -90.56515438144179, 173.04391368001362 -54.97052107658256, 173.04391368001362 60.02752498527039))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_area(*fracs, ext, *inside); +} + +template<> +template<> +void object::test<57>() +{ + set_test_name("subdividePolygon: traversal stays in grid when padding is lost to rounding at large coordinates"); + + // At 1e22, adding a fixed padding of 1 to the geometry envelope has no effect, + // so the vertex at (1e22 0.9) would lie on the outer edge of a padding cell. + Envelope e(-0.375, 2.1875, -0.375, 2.1875); + Grid ext(e, e.getWidth() / 64, e.getHeight() / 2); + + auto g = wkt_reader_.read("POLYGON ((0.1 0.1, 0.1 0.2, 0.95 0.95, 1e22 0.9, 0.1 0.1))"); + + auto subd = GridIntersection::subdividePolygon(ext, *g, false); + + auto inside = g->intersection(g->getFactory()->toGeometry(&e).get()); + check_subdivided_polygon(*inside, *subd); +} + +template<> +template<> +void object::test<58>() +{ + set_test_name("getIntersectionFractions: box sharing an edge with the grid"); + + Envelope e(0, 10, 0, 10); + Grid ext(e, 1, 1); + + for (const auto& box : { + Envelope(10, 20, 0, 10), + Envelope(0, 10, 10, 20), + Envelope(-10, 0, -10, 0) }) + { + auto fracs = GridIntersection::getIntersectionFractions(ext, box); + auto tot = std::accumulate(fracs->begin(), fracs->end(), 0.0); + + ensure_equals(tot, 0.0); + } +} + } diff --git a/tests/unit/operation/grid/GridTest.cpp b/tests/unit/operation/grid/GridTest.cpp index 0df5ea8a7..c142c4911 100644 --- a/tests/unit/operation/grid/GridTest.cpp +++ b/tests/unit/operation/grid/GridTest.cpp @@ -314,8 +314,10 @@ void object::test<17>() set_test_name("Crop infinite grid while calculating cell boundaries relative to parent"); double res = 1.0/3601; + Envelope domain = global; + domain.expandBy(1); - Grid grid{global, res, res, global}; + Grid grid{global, res, res, domain}; const auto env0 = grid.getCellEnvelope(30, 30); ----------------------------------------------------------------------- Summary of changes: NEWS.md | 2 + include/geos/operation/grid/Grid.h | 7 ++ src/operation/grid/Grid.cpp | 10 +-- src/operation/grid/GridIntersection.cpp | 29 ++++++- src/operation/relateng/AdjacentEdgeLocator.cpp | 4 + tests/unit/operation/grid/GridIntersectionTest.cpp | 94 ++++++++++++++++++++++ tests/unit/operation/grid/GridTest.cpp | 4 +- tests/unit/operation/relateng/RelateNGTest.cpp | 14 +++- 8 files changed, 152 insertions(+), 12 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Sun Nov 8 09:16:36 2026 From: git at osgeo.org (git at osgeo.org) Date: Sun, 8 Nov 2026 09:16:36 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 06da4f3a7808288b3aef036aed02c866038985ad Message-ID: <20261108171637.6EDBF17753A@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 06da4f3a7808288b3aef036aed02c866038985ad (commit) from 2c8e04faf8235456b417979bcf23271ef34148a7 (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 06da4f3a7808288b3aef036aed02c866038985ad Author: Alejandro Date: Mon Nov 9 00:16:01 2026 +0700 GridIntersection: keep traversal on the grid when a segment's slope underflows (#1593) * GridIntersection: keep traversal on the grid when a segment's slope underflows crossing() picked the side through which a segment leaves a cell using the segment's slope dy/dx. For a nearly horizontal segment the slope underflows to zero (e.g. 2e-30 / 1e300), and for a nearly vertical one it overflows to infinity, so the wrong side can be chosen. In a padding cell this sends traverseCells() outside the cell matrix, where get_cell() only asserts. - crossing(): interpolate using the fraction of the segment's extent in x or y instead of its slope. The coordinate is still computed from the same endpoint, so a segment gives the same result in either orientation. - get_cell(): throw instead of asserting when the traversal leaves the grid. * Edit comments --------- Co-authored-by: Dan Baston diff --git a/src/operation/grid/Cell.cpp b/src/operation/grid/Cell.cpp index 45b652d7c..f48f728ae 100644 --- a/src/operation/grid/Cell.cpp +++ b/src/operation/grid/Cell.cpp @@ -64,7 +64,10 @@ crossing(const geom::Envelope& e, const CoordinateXY& c1, const CoordinateXY& c2 } } - const double m = std::abs((c2.y - c1.y) / (c2.x - c1.x)); + // Interpolate with the fraction of the segment's extent in x or y rather than with its slope, which + // underflows to zero or overflows to infinity when the segment is nearly horizontal or vertical. + const double dx = std::abs(c2.x - c1.x); + const double dy = std::abs(c2.y - c1.y); const bool up = c2.y > c1.y; const bool right = c2.x > c1.x; @@ -73,22 +76,22 @@ crossing(const geom::Envelope& e, const CoordinateXY& c1, const CoordinateXY& c2 if (right) { // 1st quadrant - const double y2 = c1.y + m * (e.getMaxX() - c1.x); + const double y2 = c1.y + dy * ((e.getMaxX() - c1.x) / dx); if (y2 < e.getMaxY()) { return Crossing{ Side::RIGHT, e.getMaxX(), std::clamp(y2, e.getMinY(), e.getMaxY()) }; } else { - double x2 = c1.x + (e.getMaxY() - c1.y) / m; + double x2 = c1.x + dx * ((e.getMaxY() - c1.y) / dy); return Crossing{ Side::TOP, std::clamp(x2, e.getMinX(), e.getMaxX()), e.getMaxY() }; } } else { // 2nd quadrant - const double y2 = c1.y + m * (c1.x - e.getMinX()); + const double y2 = c1.y + dy * ((c1.x - e.getMinX()) / dx); if (y2 < e.getMaxY()) { return Crossing{ Side::LEFT, e.getMinX(), std::clamp(y2, e.getMinY(), e.getMaxY()) }; } else { - double x2 = c1.x - (e.getMaxY() - c1.y) / m; + double x2 = c1.x - dx * ((e.getMaxY() - c1.y) / dy); return Crossing{ Side::TOP, std::clamp(x2, e.getMinX(), e.getMaxX()), e.getMaxY() }; } } @@ -100,22 +103,22 @@ crossing(const geom::Envelope& e, const CoordinateXY& c1, const CoordinateXY& c2 if (right) { // 4th quadrant - const double y2 = c2.y + m * (c2.x - e.getMaxX()); + const double y2 = c2.y + dy * ((c2.x - e.getMaxX()) / dx); if (y2 > e.getMinY()) { return Crossing{ Side::RIGHT, e.getMaxX(), std::clamp(y2, e.getMinY(), e.getMaxY()) }; } else { - double x2 = c2.x - (e.getMinY() - c2.y) / m; + double x2 = c2.x - dx * ((e.getMinY() - c2.y) / dy); return Crossing{ Side::BOTTOM, std::clamp(x2, e.getMinX(), e.getMaxX()), e.getMinY() }; } } else { // 3rd quadrant - const double y2 = c2.y + m * (e.getMinX() - c2.x); + const double y2 = c2.y + dy * ((e.getMinX() - c2.x) / dx); if (y2 > e.getMinY()) { return Crossing{ Side::LEFT, e.getMinX(), std::clamp(y2, e.getMinY(), e.getMaxY()) }; } else { - double x2 = c2.x + (e.getMinY() - c2.y) / m; + double x2 = c2.x + dx * ((e.getMinY() - c2.y) / dy); return Crossing{ Side::BOTTOM, std::clamp(x2, e.getMinX(), e.getMaxX()), e.getMinY() }; } } diff --git a/src/operation/grid/GridIntersection.cpp b/src/operation/grid/GridIntersection.cpp index ef94ce5ad..8440b84fd 100644 --- a/src/operation/grid/GridIntersection.cpp +++ b/src/operation/grid/GridIntersection.cpp @@ -69,8 +69,12 @@ GridIntersection::getIntersectionFractions(const Grid& raster_gr static Cell* get_cell(Matrix>& cells, const Grid& ex, size_t row, size_t col) { - assert(row < cells.getNumRows()); - assert(col < cells.getNumCols()); + // A traversal that leaves the grid through its first row or column + // wraps around to SIZE_MAX, so one comparison covers both directions. + if (row >= cells.getNumRows() || col >= cells.getNumCols()) { + throw util::GEOSException("Grid traversal left the grid."); + } + if (cells(row, col) == nullptr) { cells(row, col) = std::make_unique(ex.getCellEnvelope(row, col)); } diff --git a/tests/unit/operation/grid/GridIntersectionTest.cpp b/tests/unit/operation/grid/GridIntersectionTest.cpp index 940d54f3a..d1ce00c99 100644 --- a/tests/unit/operation/grid/GridIntersectionTest.cpp +++ b/tests/unit/operation/grid/GridIntersectionTest.cpp @@ -1092,4 +1092,66 @@ void object::test<58>() } } +template<> +template<> +void object::test<59>() +{ + set_test_name("subdividePolygon: traversal stays in grid when the slope of a segment underflows"); + + // The first edge crosses y = 0 far to the right of the grid, so it leaves the right + // padding cell through its top. Its slope (2e-30 / 1e300) underflows to zero. + Envelope e(0, 10, -5, 5); + Grid ext(e, 1, 5); + + auto g = wkt_reader_.read("POLYGON ((2 -1e-30, 1e300 1e-30, 1e300 1, 2 1, 2 -1e-30))"); + + auto subd = GridIntersection::subdividePolygon(ext, *g, false); + + // Part of the polygon inside the grid extent + // input manually because g->getFactory()->toGeometry(&e)->intersection(g.get()) gives POLYGON EMPTY + auto inside = wkt_reader_.read("POLYGON ((2 0, 10 0, 10 1, 2 1, 2 0))"); + check_subdivided_polygon(*inside, *subd); +} + +template<> +template<> +void object::test<60>() +{ + set_test_name("getIntersectionFractions: traversal stays in grid when the slope of a segment underflows"); + + // Same as the previous test, with an extra vertex so that the ring is not processed as a rectangle. + Envelope e(0, 10, -5, 5); + Grid ext(e, 1, 5); + + auto g = wkt_reader_.read("POLYGON ((2 -1e-30, 1e300 1e-30, 1e300 1, 5 1, 2 1, 2 -1e-30))"); + + auto fracs = GridIntersection::getIntersectionFractions(ext, *g); + + // Part of the polygon inside the grid extent + // input manually because g->getFactory()->toGeometry(&e)->intersection(g.get()) gives POLYGON EMPTY + auto inside = wkt_reader_.read("POLYGON ((2 0, 10 0, 10 1, 2 1, 2 0))"); + check_area(*fracs, ext, *inside); +} + +template<> +template<> +void object::test<61>() +{ + set_test_name("subdividePolygon: traversal stays in grid when the slope of a segment overflows"); + + // The first edge crosses x = 0 far below the grid, so it leaves the bottom padding + // cell through its left side. Its slope (1e300 / 2e-30) overflows to infinity. + Envelope e(-5, 5, -10, 0); + Grid ext(e, 5, 1); + + auto g = wkt_reader_.read("POLYGON ((1e-30 -2.5, -1e-30 -1e300, -1 -1e300, -1 -2.5, 1e-30 -2.5))"); + + auto subd = GridIntersection::subdividePolygon(ext, *g, false); + + // Part of the polygon inside the grid extent + // input manually because g->getFactory()->toGeometry(&e)->intersection(g.get()) gives a LINESTRING + auto inside = wkt_reader_.read("POLYGON ((-1 -10, 0 -10, 0 -2.5, -1 -2.5, -1 -10))"); + check_subdivided_polygon(*inside, *subd); +} + } ----------------------------------------------------------------------- Summary of changes: src/operation/grid/Cell.cpp | 21 ++++---- src/operation/grid/GridIntersection.cpp | 8 ++- tests/unit/operation/grid/GridIntersectionTest.cpp | 62 ++++++++++++++++++++++ 3 files changed, 80 insertions(+), 11 deletions(-) hooks/post-receive -- GEOS