From git at osgeo.org Wed Feb 4 12:19:52 2026 From: git at osgeo.org (git at osgeo.org) Date: Wed, 4 Feb 2026 12:19:52 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 8ef887079e3d8f18fef4bb7f7e9ab52866b437fa Message-ID: <20260204201952.789121AEF20@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 8ef887079e3d8f18fef4bb7f7e9ab52866b437fa (commit) via b2041c1034533ddb5f4e3cdccee96309c815ffdc (commit) via bf8e9ba34a6e9081d4d8a7a4cf2f468989143667 (commit) from c7ca3dd29bf379c43ae6f97fbc1bdfa10dcf630e (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 8ef887079e3d8f18fef4bb7f7e9ab52866b437fa Author: Paul Ramsey Date: Wed Feb 4 11:55:51 2026 -0800 Add news entry for bf8e9ba diff --git a/NEWS.md b/NEWS.md index 43926615a..187b4781c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,10 @@ - Breaking Changes: - BufferOp returns POLYGON EMPTY when fed Inf/Nan coords (GH-1332) +- Fixes/Improvements: + - Buffer of Linestring includes spurious hole (GH-1217, Moritz Kirmse) + + ## Changes in 3.14.0 2025-08-21 commit b2041c1034533ddb5f4e3cdccee96309c815ffdc Author: Paul Ramsey Date: Wed Feb 4 11:53:53 2026 -0800 Remove duplicate tests diff --git a/tests/unit/operation/buffer/BufferOpTest.cpp b/tests/unit/operation/buffer/BufferOpTest.cpp index 8adc77438..8971402ed 100644 --- a/tests/unit/operation/buffer/BufferOpTest.cpp +++ b/tests/unit/operation/buffer/BufferOpTest.cpp @@ -717,38 +717,4 @@ void object::test<34> () -// testInvalidCoordPoint -template<> -template<> -void object::test<31> () -{ - // works for Inf ordinates as well - auto geom = wktreader.read("POINT (NaN NaN)"); - checkBufferPolygonEmpty(*geom, 1, true); -} - -// testInvalidCoordsLine -template<> -template<> -void object::test<32> () -{ - // works for Inf ordinates as well - auto geom = wktreader.read("LINESTRING (NaN NaN, NaN NaN)"); - checkBufferPolygonEmpty(*geom, 1, true); -} - -// testInvalidCoordShell -template<> -template<> -void object::test<33> () -{ - // using Inf ordinates creates a valid ring with equal endpoints - // this would be simpler if JTS WKT supported Inf - auto geom = wktreader.read("POLYGON ((Inf Inf, Inf Inf, Inf Inf, Inf Inf, Inf Inf))"); - checkBufferPolygonEmpty(*geom, 1, true); -} - - - - } // namespace tut commit bf8e9ba34a6e9081d4d8a7a4cf2f468989143667 Author: Paul Ramsey Date: Wed Feb 4 11:31:37 2026 -0800 Buffer of Linestring results in polygon with spurious hole. Author: Moritz Kirmse Closes GH-1217 diff --git a/include/geos/operation/buffer/OffsetSegmentGenerator.h b/include/geos/operation/buffer/OffsetSegmentGenerator.h index 5408f8787..05711d959 100644 --- a/include/geos/operation/buffer/OffsetSegmentGenerator.h +++ b/include/geos/operation/buffer/OffsetSegmentGenerator.h @@ -144,7 +144,8 @@ public: /// Add an end cap around point p1, terminating a line segment /// coming from p0 void addLineEndCap(const geom::Coordinate& p0, - const geom::Coordinate& p1); + const geom::Coordinate& p1, + const geom::Coordinate& p2); void addSegments(const geom::CoordinateSequence& pts, bool isForward) diff --git a/src/operation/buffer/OffsetCurveBuilder.cpp b/src/operation/buffer/OffsetCurveBuilder.cpp index ed2ae6c22..c57e66f2e 100644 --- a/src/operation/buffer/OffsetCurveBuilder.cpp +++ b/src/operation/buffer/OffsetCurveBuilder.cpp @@ -377,8 +377,6 @@ OffsetCurveBuilder::computeLineBufferCurve(const CoordinateSequence& inputPts, segGen.addNextSegment(simp1[i], true); } segGen.addLastSegment(); - // add line cap for end of line - segGen.addLineEndCap(simp1[n1 - 1], simp1[n1]); //---------- compute points for right side of line // Simplify the appropriate side of the line before generating @@ -387,13 +385,17 @@ OffsetCurveBuilder::computeLineBufferCurve(const CoordinateSequence& inputPts, const CoordinateSequence& simp2 = *simp2_; auto n2 = simp2.size() - 1; + + // add line cap for return of line + segGen.addLineEndCap(simp1[n1 - 1], simp1[n1], simp2[n2 - 1]); + segGen.initSideSegments(simp2[n2], simp2[n2 - 1], Position::LEFT); for(std::size_t i = n2 - 1; i > 0; --i) { segGen.addNextSegment(simp2[i - 1], true); } segGen.addLastSegment(); // add line cap for start of line - segGen.addLineEndCap(simp2[1], simp2[0]); + segGen.addLineEndCap(simp2[1], simp2[0], simp1[1]); segGen.closeRing(); } diff --git a/src/operation/buffer/OffsetSegmentGenerator.cpp b/src/operation/buffer/OffsetSegmentGenerator.cpp index 6c7c2c3b0..435d4770a 100644 --- a/src/operation/buffer/OffsetSegmentGenerator.cpp +++ b/src/operation/buffer/OffsetSegmentGenerator.cpp @@ -187,25 +187,21 @@ OffsetSegmentGenerator::computeOffsetSegment(const LineSegment& seg, int p_side, /*public*/ void -OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1) +OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1, const Coordinate& p2) { - LineSegment seg(p0, p1); + LineSegment segL(p0, p1); + LineSegment segR(p2, p1); LineSegment offsetL; - computeOffsetSegment(seg, Position::LEFT, distance, offsetL); + computeOffsetSegment(segL, Position::LEFT, distance, offsetL); LineSegment offsetR; - computeOffsetSegment(seg, Position::RIGHT, distance, offsetR); - - double dx = p1.x - p0.x; - double dy = p1.y - p0.y; - double angle = atan2(dy, dx); + computeOffsetSegment(segR, Position::RIGHT, distance, offsetR); switch(bufParams.getEndCapStyle()) { case BufferParameters::CAP_ROUND: // add offset seg points with a fillet between them segList.addPt(offsetL.p1); - addDirectedFillet(p1, angle + Angle::PI_OVER_2, angle - Angle::PI_OVER_2, - Orientation::CLOCKWISE, distance); + addDirectedFillet(p1, offsetL.p1, offsetR.p1, Orientation::CLOCKWISE, distance); segList.addPt(offsetR.p1); break; case BufferParameters::CAP_FLAT: @@ -216,7 +212,12 @@ OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1 case BufferParameters::CAP_SQUARE: // add a square defined by extensions of the offset // segment endpoints + Coordinate squareCapSideOffset; + // take average in case angles of left and right sides differ + double dx = p1.x - p0.x/2 - p2.x/2; + double dy = p1.y - p0.y/2 - p2.y/2; + double angle = atan2(dy, dx); double sinangle, cosangle; Angle::sinCosSnap(angle, sinangle, cosangle); squareCapSideOffset.x = fabs(distance) * cosangle; diff --git a/tests/unit/operation/buffer/BufferOpTest.cpp b/tests/unit/operation/buffer/BufferOpTest.cpp index 3e5bc5d92..8adc77438 100644 --- a/tests/unit/operation/buffer/BufferOpTest.cpp +++ b/tests/unit/operation/buffer/BufferOpTest.cpp @@ -629,10 +629,25 @@ void object::test<28> "MULTIPOLYGON (((24 95.239, 24 96, 24 99, 24.816 99, 24 95.239)), ((3 90, 3 93, 3 96, 3 99, 21 99, 21 96, 21 93, 21 90, 3 90)))"); } +// testEndCap +// test end cap for a line where left and right side are simplified differently, so there is an angle at the end +// See https://github.com/libgeos/geos/issues/1217 template<> template<> void object::test<29> () +{ + std::string wkt("LINESTRING (0.7 4.7, 146.3 137.1, 146.3 137, 146.6 136.7)"); + std::unique_ptr result13 = buffer(wkt, 11); + checkValidPolygon(*result13); + checkNumHoles(*result13, 0); +} + + +template<> +template<> +void object::test<30> +() { set_test_name("GH-1321"); // Ensure positive buffer of LineString produces a single-part Polygon @@ -648,7 +663,7 @@ void object::test<29> template<> template<> -void object::test<30> +void object::test<31> () { set_test_name("sf-2552"); @@ -669,6 +684,39 @@ void object::test<30> } +// testInvalidCoordPoint +template<> +template<> +void object::test<32> () +{ + // works for Inf ordinates as well + auto geom = wktreader.read("POINT (NaN NaN)"); + checkBufferPolygonEmpty(*geom, 1, true); +} + +// testInvalidCoordsLine +template<> +template<> +void object::test<33> () +{ + // works for Inf ordinates as well + auto geom = wktreader.read("LINESTRING (NaN NaN, NaN NaN)"); + checkBufferPolygonEmpty(*geom, 1, true); +} + +// testInvalidCoordShell +template<> +template<> +void object::test<34> () +{ + // using Inf ordinates creates a valid ring with equal endpoints + // this would be simpler if JTS WKT supported Inf + auto geom = wktreader.read("POLYGON ((Inf Inf, Inf Inf, Inf Inf, Inf Inf, Inf Inf))"); + checkBufferPolygonEmpty(*geom, 1, true); +} + + + // testInvalidCoordPoint template<> template<> ----------------------------------------------------------------------- Summary of changes: NEWS.md | 4 ++++ .../geos/operation/buffer/OffsetSegmentGenerator.h | 3 ++- src/operation/buffer/OffsetCurveBuilder.cpp | 8 +++++--- src/operation/buffer/OffsetSegmentGenerator.cpp | 21 ++++++++++--------- tests/unit/operation/buffer/BufferOpTest.cpp | 24 +++++++++++++++++----- 5 files changed, 41 insertions(+), 19 deletions(-) hooks/post-receive -- GEOS From git at osgeo.org Fri Feb 6 12:52:59 2026 From: git at osgeo.org (git at osgeo.org) Date: Fri, 6 Feb 2026 12:52:59 -0800 (PST) Subject: [geos-commits] [SCM] GEOS branch main updated. 9bc6ca8716552f8d35e90cd22be433123c06b175 Message-ID: <20260206205300.07257110F2B@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 9bc6ca8716552f8d35e90cd22be433123c06b175 (commit) from 8ef887079e3d8f18fef4bb7f7e9ab52866b437fa (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 9bc6ca8716552f8d35e90cd22be433123c06b175 Author: Paul Ramsey Date: Fri Feb 6 11:47:36 2026 -0800 Fix: Preserve dimension and type for empty geometries during snap/simplify Closes https://github.com/libgeos/geos/issues/1356 When performing operations like Snap on empty geometries (e.g., POINT EMPTY, LINESTRING EMPTY, POLYGON EMPTY), the GEOS library was incorrectly modifying their coordinate dimension (e.g., adding a Z dimension) and sometimes changing their type (e.g., POLYGON EMPTY becoming a LinearRing or GeometryCollection). This fix modifies GeometryTransformer to short-circuit the transformation process if the input geometry is already empty. Instead of transforming it, it now returns a direct clone of the empty input geometry. This ensures that the original type and coordinate dimension (e.g., 2D) are preserved, aligning with the expected behavior for empty geometries. diff --git a/src/geom/util/GeometryTransformer.cpp b/src/geom/util/GeometryTransformer.cpp index acec018bf..afa01ce32 100644 --- a/src/geom/util/GeometryTransformer.cpp +++ b/src/geom/util/GeometryTransformer.cpp @@ -79,6 +79,11 @@ GeometryTransformer::transform(const Geometry* nInputGeom) inputGeom = nInputGeom; factory = inputGeom->getFactory(); + // If the input geometry is empty, return a clone of it to preserve its type and dimension + if (inputGeom->isEmpty()) { + return inputGeom->clone(); + } + if(const Point* p = dynamic_cast(inputGeom)) { return transformPoint(p, nullptr); } diff --git a/tests/unit/operation/overlay/snap/GeometrySnapperTest.cpp b/tests/unit/operation/overlay/snap/GeometrySnapperTest.cpp index ef82353e3..9e5245436 100644 --- a/tests/unit/operation/overlay/snap/GeometrySnapperTest.cpp +++ b/tests/unit/operation/overlay/snap/GeometrySnapperTest.cpp @@ -76,5 +76,56 @@ void object::test<3> () ensure_equals_geometry(expected.get(), result.get()); } +// Test snapping of empty geometries -- POINT EMPTY +template<> +template<> +void object::test<4> () +{ + std::string wkt = "POINT EMPTY"; + auto src = reader.read(wkt); + GeometrySnapper snapper(*src); + auto snap = reader.read(wkt); // Snap against itself + auto result = snapper.snapTo(*snap, 1); + auto expected = reader.read(wkt); + + ensure_equals_geometry(expected.get(), result.get()); + ensure_equals(result->getCoordinateDimension(), 2); + ensure_equals(result->getGeometryType(), "Point"); +} + +// Test snapping of empty geometries -- LINESTRING EMPTY +template<> +template<> +void object::test<5> () +{ + std::string wkt = "LINESTRING EMPTY"; + auto src = reader.read(wkt); + GeometrySnapper snapper(*src); + auto snap = reader.read(wkt); // Snap against itself + auto result = snapper.snapTo(*snap, 1); + auto expected = reader.read(wkt); + + ensure_equals_geometry(expected.get(), result.get()); + ensure_equals(result->getCoordinateDimension(), 2); + ensure_equals(result->getGeometryType(), "LineString"); +} + +// Test snapping of empty geometries -- POLYGON EMPTY +template<> +template<> +void object::test<6> () +{ + std::string wkt = "POLYGON EMPTY"; + auto src = reader.read(wkt); + GeometrySnapper snapper(*src); + auto snap = reader.read(wkt); // Snap against itself + auto result = snapper.snapTo(*snap, 1); + auto expected = reader.read(wkt); + + ensure_equals_geometry(expected.get(), result.get()); + ensure_equals(result->getCoordinateDimension(), 2); + ensure_equals(result->getGeometryType(), "Polygon"); +} + } // namespace tut ----------------------------------------------------------------------- Summary of changes: src/geom/util/GeometryTransformer.cpp | 5 +++ .../operation/overlay/snap/GeometrySnapperTest.cpp | 51 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) hooks/post-receive -- GEOS