[geos-commits] [SCM] GEOS branch main updated. 4118b3bb4d47169a11c1228933ba3e8510e588c4

git at osgeo.org git at osgeo.org
Thu Nov 9 00:25:58 PST 2023


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  4118b3bb4d47169a11c1228933ba3e8510e588c4 (commit)
       via  4e40afd8f16773c85a8124b72665754721189837 (commit)
       via  e4a6e1af65f7d60fa63d7666f6cb000e365bb50f (commit)
       via  02f8fb1da4c94024f8a9193d90ced021be6fcb95 (commit)
       via  c1927788e6e23c37c9f3d87dbdba5f92644a2a96 (commit)
       via  1fc2612972070d56576a788cd6511d91c62bb0e0 (commit)
       via  bb2db233eb2d51ba1f44cfaaf5dbd39f0cb1af87 (commit)
       via  f89a64b8037f05d0838b5d0ec518ffe57d782637 (commit)
       via  13c25a877704327db9f4f0dfd9c41d527dc8f46d (commit)
       via  a7a12f54371ddefee4e468ef89677ba9305719b1 (commit)
       via  d7a5a51cb762675f3d068a37d02a31142fd6f822 (commit)
       via  e068d35c47f785e5141c7d6a8f23a4d43846fff8 (commit)
       via  c7ff046189466411ebcd43e81a39b2a78db517ca (commit)
       via  7d8455ce42677411c3d4ee950928506eb65745e1 (commit)
       via  0dc1901601fd226036e1b898a7ae026f2067a71a (commit)
       via  28d70a2e4582edcd3625659f70e31d1853423875 (commit)
      from  d51982c6da5b7adb63ca0933ae7b53828cc8d72e (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 4118b3bb4d47169a11c1228933ba3e8510e588c4
Author: Regina Obe <lr at pcorp.us>
Date:   Wed Nov 8 21:41:34 2023 -0500

    Minor revision to steps in HOWTO_RELEASE

diff --git a/HOWTO_RELEASE b/HOWTO_RELEASE
index 4df8b2c79..36fb123e4 100644
--- a/HOWTO_RELEASE
+++ b/HOWTO_RELEASE
@@ -21,35 +21,29 @@
    $ ctest --output-on-failure .
    $ cmake --build . --target distcheck
 
-4. Update the NEWS file (extract most important things from the
-   repository log). Make sure to update the release date.
+4. Update the NEWS file (extract most important things from the repository log).
+Make sure to update the release date.
 
 5. Commit the changes in the NEWS file.
 
-6. Build the distribution package.
+6. Tag the release:
 
-   $ BRANCH_NAME=main
-   $ git clone --depth 1 --branch $BRANCH_NAME \
-         https://github.com/libgeos/geos.git geos-$BRANCH_NAME
-   $ cd geos-$BRANCH_NAME
-   $ mkdir _build && cd _build
-   $ cmake ..
-   $ cmake --build . --target dist
+   $ git tag MAJOR.MINOR.PATCH
+   $ git push origin MAJOR.MINOR.PATCH
 
-7. Verify that you can un-pack and build the tarball.
+7. At this point the github/release.yml action should run, wait for it to finish.
 
-   $ tar xvfz geos-VERSION.tar.bz2
-   $ cd geos-VERSION
+8. Verify that you can use the released tarball, replacing MAJOR.MINOR.PATCH with the tagged version variables
+
+   $ VERSION=MAJOR.MINOR.PATCH
+   $ wget https://github.com/libgeos/geos/releases/download/$VERSION/geos-$VERSION.tar.bz2
+   $ tar xvfz geos-$VERSION.tar.bz2
+   $ cd geos-$VERSION
    $ mkdir _build && cd _build && cmake ..
    $ cmake --build .
    $ ctest --output-on-failure .
 
-8. Copy the tarball to upload.osgeo.org:/osgeo/download/geos
-
-9. Tag the release:
-
-   $ git tag MAJOR.MINOR.PATCH
-   $ git push origin MAJOR.MINOR.PATCH
+9. Copy the tar.bz2 file to upload.osgeo.org:/osgeo/download/geos
 
 10. Create and push a release branch if this was a PATCH=0 release,
     update the versions for next release in Version.txt (both in
@@ -62,5 +56,5 @@
 
 12. Close current GitHub milestone and add a new one for next release.
 
-12. Announce on geos-devel
+13. Announce on geos-devel
 

commit 4e40afd8f16773c85a8124b72665754721189837
Author: Mike Taves <mwtoews at gmail.com>
Date:   Thu Nov 9 09:37:10 2023 +1300

    Rename for consistency -> "Angle::SinCos" -> "Angle::sinCos"

diff --git a/NEWS.md b/NEWS.md
index a77bd1d32..c58399ad5 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,7 +2,7 @@
 20xx-xx-xx
 
 - New things:
-  - Add Angle::SinCos to avoid small errors, e.g. with buffer operations (GH-978, Mike Taves)
+  - Add Angle::sinCos to avoid small errors, e.g. with buffer operations (GH-978, Mike Taves)
 
 - Breaking Changes
 
diff --git a/include/geos/algorithm/Angle.h b/include/geos/algorithm/Angle.h
index 2c1a5edab..f4f2f604e 100644
--- a/include/geos/algorithm/Angle.h
+++ b/include/geos/algorithm/Angle.h
@@ -228,7 +228,7 @@ public:
     /// @param rSin the result of sin(ang)
     /// @param rCos the result of cos(ang)
     ///
-    static inline void SinCos(const double ang, double& rSin, double& rCos) {
+    static inline void sinCos(const double ang, double& rSin, double& rCos) {
         // calculate both; may be optimized with FSINCOS instruction
         rSin = std::sin(ang);
         rCos = std::cos(ang);
diff --git a/src/operation/buffer/OffsetSegmentGenerator.cpp b/src/operation/buffer/OffsetSegmentGenerator.cpp
index db7061b27..41e22a91c 100644
--- a/src/operation/buffer/OffsetSegmentGenerator.cpp
+++ b/src/operation/buffer/OffsetSegmentGenerator.cpp
@@ -222,7 +222,7 @@ OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1
         // segment endpoints
         Coordinate squareCapSideOffset;
         double sinangle, cosangle;
-        Angle::SinCos(angle, sinangle, cosangle);
+        Angle::sinCos(angle, sinangle, cosangle);
         squareCapSideOffset.x = fabs(distance) * cosangle;
         squareCapSideOffset.y = fabs(distance) * sinangle;
 
@@ -283,7 +283,7 @@ OffsetSegmentGenerator::addDirectedFillet(const Coordinate& p, double startAngle
     double sinangle, cosangle;
     Coordinate pt;
     for (int i = 0; i < nSegs; i++) {
-        Angle::SinCos(startAngle + directionFactor * i * angleInc, sinangle, cosangle);
+        Angle::sinCos(startAngle + directionFactor * i * angleInc, sinangle, cosangle);
         pt.x = p.x + radius * cosangle;
         pt.y = p.y + radius * sinangle;
         segList.addPt(pt);
@@ -583,7 +583,7 @@ Coordinate
 OffsetSegmentGenerator::project(const Coordinate& pt, double d, double dir)
 {
     double sindir, cosdir;
-    Angle::SinCos(dir, sindir, cosdir);
+    Angle::sinCos(dir, sindir, cosdir);
     double x = pt.x + d * cosdir;
     double y = pt.y + d * sindir;
     return Coordinate(x, y);
diff --git a/src/util/GeometricShapeFactory.cpp b/src/util/GeometricShapeFactory.cpp
index f4d714650..4a63e0ac3 100644
--- a/src/util/GeometricShapeFactory.cpp
+++ b/src/util/GeometricShapeFactory.cpp
@@ -138,7 +138,7 @@ GeometricShapeFactory::createCircle()
     uint32_t iPt = 0;
     double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        Angle::SinCos(i * Angle::PI_TIMES_2 / nPts, sinang, cosang);
+        Angle::sinCos(i * Angle::PI_TIMES_2 / nPts, sinang, cosang);
         double x = xRadius * cosang + centreX;
         double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
@@ -170,7 +170,7 @@ GeometricShapeFactory::createArc(double startAng, double angExtent)
     uint32_t iPt = 0;
     double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        Angle::SinCos(startAng + i * angInc, sinang, cosang);
+        Angle::sinCos(startAng + i * angInc, sinang, cosang);
         double x = xRadius * cosang + centreX;
         double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
@@ -201,7 +201,7 @@ GeometricShapeFactory::createArcPolygon(double startAng, double angExtent)
     (*pts)[iPt++] = coord(centreX, centreY);
     double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        Angle::SinCos(startAng + i * angInc, sinang, cosang);
+        Angle::sinCos(startAng + i * angInc, sinang, cosang);
         double x = xRadius * cosang + centreX;
         double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
diff --git a/tests/unit/algorithm/AngleTest.cpp b/tests/unit/algorithm/AngleTest.cpp
index 372b8ebeb..02eae99e8 100644
--- a/tests/unit/algorithm/AngleTest.cpp
+++ b/tests/unit/algorithm/AngleTest.cpp
@@ -174,7 +174,7 @@ void object::test<6>
     for (int angdeg = -720; angdeg <= 720; angdeg++) {
         const double ang = Angle::toRadians(angdeg);
 
-        Angle::SinCos(ang, rSin, rCos);
+        Angle::sinCos(ang, rSin, rCos);
 
         double cSin = std::sin(ang);
         double cCos = std::cos(ang);
@@ -192,7 +192,7 @@ void object::test<6>
     // use radian increments that don't snap to exact degrees or zero
     for (double angrad = -6.3; angrad < 6.3; angrad += 0.013) {
 
-        Angle::SinCos(angrad, rSin, rCos);
+        Angle::sinCos(angrad, rSin, rCos);
 
         ensure_equals(std::to_string(angrad), rSin, std::sin(angrad));
         ensure_equals(std::to_string(angrad), rCos, std::cos(angrad));

commit e4a6e1af65f7d60fa63d7666f6cb000e365bb50f
Author: Mike Taves <mwtoews at gmail.com>
Date:   Wed Nov 8 10:05:13 2023 +1300

    Add Angle::SinCos to avoid small errors, e.g. with buffer operations (#978)

diff --git a/NEWS.md b/NEWS.md
index 08752b6b1..a77bd1d32 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -2,6 +2,7 @@
 20xx-xx-xx
 
 - New things:
+  - Add Angle::SinCos to avoid small errors, e.g. with buffer operations (GH-978, Mike Taves)
 
 - Breaking Changes
 
diff --git a/include/geos/algorithm/Angle.h b/include/geos/algorithm/Angle.h
index ddcdedb7b..2c1a5edab 100644
--- a/include/geos/algorithm/Angle.h
+++ b/include/geos/algorithm/Angle.h
@@ -216,6 +216,27 @@ public:
     ///         (in range [0, Pi] )
     ///
     static double diff(double ang1, double ang2);
+
+    /// \brief
+    /// Computes both sin and cos of an angle.
+    ///
+    /// The angle does not need to be normalized. Unlike std::sin
+    /// and std::cos, this method will clip near-zero values to zero
+    /// for (e.g.) sin(pi) and cos(pi/2).
+    ///
+    /// @param ang the input angle (in radians)
+    /// @param rSin the result of sin(ang)
+    /// @param rCos the result of cos(ang)
+    ///
+    static inline void SinCos(const double ang, double& rSin, double& rCos) {
+        // calculate both; may be optimized with FSINCOS instruction
+        rSin = std::sin(ang);
+        rCos = std::cos(ang);
+        // clip near zero values
+        if (std::fabs(rSin) < 5e-16) rSin = 0.0;
+        if (std::fabs(rCos) < 5e-16) rCos = 0.0;
+    }
+
 };
 
 
diff --git a/src/algorithm/Angle.cpp b/src/algorithm/Angle.cpp
index 3b0aebf41..7069607f5 100644
--- a/src/algorithm/Angle.cpp
+++ b/src/algorithm/Angle.cpp
@@ -195,7 +195,7 @@ Angle::diff(double ang1, double ang2)
     }
 
     if(delAngle > MATH_PI) {
-        delAngle = (2 * MATH_PI) - delAngle;
+        delAngle = PI_TIMES_2 - delAngle;
     }
 
     return delAngle;
diff --git a/src/operation/buffer/BufferParameters.cpp b/src/operation/buffer/BufferParameters.cpp
index 8c97a51c3..76c6c13e9 100644
--- a/src/operation/buffer/BufferParameters.cpp
+++ b/src/operation/buffer/BufferParameters.cpp
@@ -19,6 +19,7 @@
 #include <cstdlib> // for std::abs()
 #include <cmath> // for cos
 
+#include <geos/algorithm/Angle.h>
 #include <geos/constants.h>
 #include <geos/operation/buffer/BufferParameters.h>
 
@@ -95,7 +96,7 @@ BufferParameters::setQuadrantSegments(int quadSegs)
 double
 BufferParameters::bufferDistanceError(int quadSegs)
 {
-    double alpha = MATH_PI / 2.0 / quadSegs;
+    double alpha = algorithm::Angle::PI_OVER_2 / quadSegs;
     return 1 - cos(alpha / 2.0);
 }
 
diff --git a/src/operation/buffer/OffsetSegmentGenerator.cpp b/src/operation/buffer/OffsetSegmentGenerator.cpp
index 729f6c7ac..db7061b27 100644
--- a/src/operation/buffer/OffsetSegmentGenerator.cpp
+++ b/src/operation/buffer/OffsetSegmentGenerator.cpp
@@ -79,11 +79,11 @@ OffsetSegmentGenerator::OffsetSegmentGenerator(
 {
     // compute intersections in full precision, to provide accuracy
     // the points are rounded as they are inserted into the curve line
-    filletAngleQuantum = MATH_PI / 2.0 / bufParams.getQuadrantSegments();
+    filletAngleQuantum = Angle::PI_OVER_2 / bufParams.getQuadrantSegments();
 
     int quadSegs = bufParams.getQuadrantSegments();
     if (quadSegs < 1) quadSegs = 1;
-    filletAngleQuantum = MATH_PI / 2.0 / quadSegs;
+    filletAngleQuantum = Angle::PI_OVER_2 / quadSegs;
 
     /*
      * Non-round joins cause issues with short closing segments,
@@ -208,7 +208,7 @@ OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1
     case BufferParameters::CAP_ROUND:
         // add offset seg points with a fillet between them
         segList.addPt(offsetL.p1);
-        addDirectedFillet(p1, angle + MATH_PI / 2.0, angle - MATH_PI / 2.0,
+        addDirectedFillet(p1, angle + Angle::PI_OVER_2, angle - Angle::PI_OVER_2,
                   Orientation::CLOCKWISE, distance);
         segList.addPt(offsetR.p1);
         break;
@@ -221,8 +221,10 @@ OffsetSegmentGenerator::addLineEndCap(const Coordinate& p0, const Coordinate& p1
         // add a square defined by extensions of the offset
         // segment endpoints
         Coordinate squareCapSideOffset;
-        squareCapSideOffset.x = fabs(distance) * cos(angle);
-        squareCapSideOffset.y = fabs(distance) * sin(angle);
+        double sinangle, cosangle;
+        Angle::SinCos(angle, sinangle, cosangle);
+        squareCapSideOffset.x = fabs(distance) * cosangle;
+        squareCapSideOffset.y = fabs(distance) * sinangle;
 
         Coordinate squareCapLOffset(
             offsetL.p1.x + squareCapSideOffset.x,
@@ -250,12 +252,12 @@ OffsetSegmentGenerator::addDirectedFillet(const Coordinate& p, const Coordinate&
 
     if(direction == Orientation::CLOCKWISE) {
         if(startAngle <= endAngle) {
-            startAngle += 2.0 * MATH_PI;
+            startAngle += Angle::PI_TIMES_2;
         }
     }
     else {    // direction==COUNTERCLOCKWISE
         if(startAngle >= endAngle) {
-            startAngle -= 2.0 * MATH_PI;
+            startAngle -= Angle::PI_TIMES_2;
         }
     }
 
@@ -277,13 +279,13 @@ OffsetSegmentGenerator::addDirectedFillet(const Coordinate& p, double startAngle
     // no segments because angle is less than increment-nothing to do!
     if(nSegs < 1) return;
 
-    // double initAngle, currAngleInc;
     double angleInc = totalAngle / nSegs;
+    double sinangle, cosangle;
     Coordinate pt;
     for (int i = 0; i < nSegs; i++) {
-        double angle = startAngle + directionFactor * i * angleInc;
-        pt.x = p.x + radius * cos(angle);
-        pt.y = p.y + radius * sin(angle);
+        Angle::SinCos(startAngle + directionFactor * i * angleInc, sinangle, cosangle);
+        pt.x = p.x + radius * cosangle;
+        pt.y = p.y + radius * sinangle;
         segList.addPt(pt);
     }
 }
@@ -295,7 +297,7 @@ OffsetSegmentGenerator::createCircle(const Coordinate& p, double p_distance)
     // add start point
     Coordinate pt(p.x + p_distance, p.y);
     segList.addPt(pt);
-    addDirectedFillet(p, 0.0, 2.0 * MATH_PI, -1, p_distance);
+    addDirectedFillet(p, 0.0, Angle::PI_TIMES_2, -1, p_distance);
     segList.closeRing();
 }
 
@@ -531,7 +533,7 @@ OffsetSegmentGenerator::addLimitedMitreJoin(
     Coordinate bevelMidPt = project(cornerPt, p_mitreLimitDistance, dirBisectorOut);
 
     // slope angle of bevel segment
-    double dirBevel = Angle::normalize(dirBisectorOut + MATH_PI/2.0);
+    double dirBevel = Angle::normalize(dirBisectorOut + Angle::PI_OVER_2);
 
     // compute the candidate bevel segment by projecting both sides of the midpoint
     Coordinate bevel0 = project(bevelMidPt, p_distance, dirBevel);
@@ -580,8 +582,10 @@ OffsetSegmentGenerator::extend(const LineSegment& seg, double dist)
 Coordinate
 OffsetSegmentGenerator::project(const Coordinate& pt, double d, double dir)
 {
-    double x = pt.x + d * std::cos(dir);
-    double y = pt.y + d * std::sin(dir);
+    double sindir, cosdir;
+    Angle::SinCos(dir, sindir, cosdir);
+    double x = pt.x + d * cosdir;
+    double y = pt.y + d * sindir;
     return Coordinate(x, y);
 }
 
diff --git a/src/util/GeometricShapeFactory.cpp b/src/util/GeometricShapeFactory.cpp
index bfe485460..f4d714650 100644
--- a/src/util/GeometricShapeFactory.cpp
+++ b/src/util/GeometricShapeFactory.cpp
@@ -17,6 +17,7 @@
  *
  **********************************************************************/
 
+#include <geos/algorithm/Angle.h>
 #include <geos/constants.h>
 #include <geos/util/GeometricShapeFactory.h>
 #include <geos/geom/Coordinate.h>
@@ -31,6 +32,7 @@
 #include <memory>
 
 
+using namespace geos::algorithm;
 using namespace geos::geom;
 
 namespace geos {
@@ -134,10 +136,11 @@ GeometricShapeFactory::createCircle()
 
     auto pts = detail::make_unique<CoordinateSequence>(nPts + 1);
     uint32_t iPt = 0;
+    double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        double ang = i * (2 * 3.14159265358979 / nPts);
-        double x = xRadius * cos(ang) + centreX;
-        double y = yRadius * sin(ang) + centreY;
+        Angle::SinCos(i * Angle::PI_TIMES_2 / nPts, sinang, cosang);
+        double x = xRadius * cosang + centreX;
+        double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
     }
     (*pts)[iPt++] = (*pts)[0];
@@ -158,17 +161,18 @@ GeometricShapeFactory::createArc(double startAng, double angExtent)
     env.reset();
 
     double angSize = angExtent;
-    if(angSize <= 0.0 || angSize > 2 * MATH_PI) {
-        angSize = 2 * MATH_PI;
+    if(angSize <= 0.0 || angSize > Angle::PI_TIMES_2) {
+        angSize = Angle::PI_TIMES_2;
     }
     double angInc = angSize / (nPts - 1);
 
     auto pts = detail::make_unique<CoordinateSequence>(nPts);
     uint32_t iPt = 0;
+    double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        double ang = startAng + i * angInc;
-        double x = xRadius * cos(ang) + centreX;
-        double y = yRadius * sin(ang) + centreY;
+        Angle::SinCos(startAng + i * angInc, sinang, cosang);
+        double x = xRadius * cosang + centreX;
+        double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
     }
     auto line = geomFact->createLineString(std::move(pts));
@@ -187,18 +191,19 @@ GeometricShapeFactory::createArcPolygon(double startAng, double angExtent)
     env.reset();
 
     double angSize = angExtent;
-    if(angSize <= 0.0 || angSize > 2 * MATH_PI) {
-        angSize = 2 * MATH_PI;
+    if(angSize <= 0.0 || angSize > Angle::PI_TIMES_2) {
+        angSize = Angle::PI_TIMES_2;
     }
     double angInc = angSize / (nPts - 1);
 
     auto pts = detail::make_unique<CoordinateSequence>(nPts + 2);
     uint32_t iPt = 0;
     (*pts)[iPt++] = coord(centreX, centreY);
+    double sinang, cosang;
     for(uint32_t i = 0; i < nPts; i++) {
-        double ang = startAng + i * angInc;
-        double x = xRadius * cos(ang) + centreX;
-        double y = yRadius * sin(ang) + centreY;
+        Angle::SinCos(startAng + i * angInc, sinang, cosang);
+        double x = xRadius * cosang + centreX;
+        double y = yRadius * sinang + centreY;
         (*pts)[iPt++] = coord(x, y);
     }
     (*pts)[iPt++] = coord(centreX, centreY);
diff --git a/tests/unit/algorithm/AngleTest.cpp b/tests/unit/algorithm/AngleTest.cpp
index 86a942b6c..372b8ebeb 100644
--- a/tests/unit/algorithm/AngleTest.cpp
+++ b/tests/unit/algorithm/AngleTest.cpp
@@ -162,6 +162,45 @@ void object::test<5>
         TOL);
 }
 
+// testSinCos()
+template<>
+template<>
+void object::test<6>
+()
+{
+    double rSin, rCos;
+
+    // -720 to 720 degrees with 1 degree increments
+    for (int angdeg = -720; angdeg <= 720; angdeg++) {
+        const double ang = Angle::toRadians(angdeg);
+
+        Angle::SinCos(ang, rSin, rCos);
+
+        double cSin = std::sin(ang);
+        double cCos = std::cos(ang);
+        if ( (angdeg % 90) == 0 ) {
+            // not always the same for multiples of 90 degrees
+            ensure(std::to_string(angdeg), std::fabs(rSin - cSin) < 1e-15);
+            ensure(std::to_string(angdeg), std::fabs(rCos - cCos) < 1e-15);
+        } else {
+            ensure_equals(std::to_string(angdeg), rSin, cSin);
+            ensure_equals(std::to_string(angdeg), rCos, cCos);
+        }
+
+    }
+
+    // use radian increments that don't snap to exact degrees or zero
+    for (double angrad = -6.3; angrad < 6.3; angrad += 0.013) {
+
+        Angle::SinCos(angrad, rSin, rCos);
+
+        ensure_equals(std::to_string(angrad), rSin, std::sin(angrad));
+        ensure_equals(std::to_string(angrad), rCos, std::cos(angrad));
+
+    }
+
+}
+
 
 } // namespace tut
 
diff --git a/tests/unit/capi/GEOSBufferTest.cpp b/tests/unit/capi/GEOSBufferTest.cpp
index 0c7cab962..f47a2237f 100644
--- a/tests/unit/capi/GEOSBufferTest.cpp
+++ b/tests/unit/capi/GEOSBufferTest.cpp
@@ -258,7 +258,7 @@ void object::test<9>
     ensure_area(area_, 150.0, 0.001);
 
     ensure_equals(std::string(wkt_), std::string(
-                      "POLYGON ((10.0000000000000000 15.0000000000000000, 15.0000000000000000 15.0000000000000000, 15.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000009, 0.0000000000000000 15.0000000000000000, 10.0000000000000000 15.0000000000000000))"
+                      "POLYGON ((10.0000000000000000 15.0000000000000000, 15.0000000000000000 15.0000000000000000, 15.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000000, 0.0000000000000000 15.0000000000000000, 10.0000000000000000 15.0000000000000000))"
                   ));
 }
 
@@ -311,7 +311,7 @@ void object::test<11>
     ensure_area(area_, 250.0, 0.001);
 
     ensure_equals(std::string(wkt_), std::string(
-                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000009, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
+                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000000, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
                   ));
 }
 
@@ -339,7 +339,7 @@ void object::test<12>
     ensure_area(area_, 237.5, 0.001);
 
     ensure_equals(std::string(wkt_), std::string(
-                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 10.0000000000000000, 10.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000009, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
+                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 10.0000000000000000, 10.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000000, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
                   ));
 }
 
@@ -368,7 +368,7 @@ void object::test<13>
     ensure_area(area_, 237.5, 0.001);
 
     ensure_equals(std::string(wkt_), std::string(
-                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 10.0000000000000000, 10.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000009, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
+                      "POLYGON ((5.0000000000000000 15.0000000000000000, 5.0000000000000000 20.0000000000000000, 5.0000000000000000 25.0000000000000000, 15.0000000000000000 25.0000000000000000, 15.0000000000000000 10.0000000000000000, 10.0000000000000000 5.0000000000000000, 5.0000000000000000 5.0000000000000000, 0.0000000000000000 5.0000000000000000, 0.0000000000000000 15.0000000000000000, 5.0000000000000000 15.0000000000000000))"
                   ));
 }
 
diff --git a/tests/unit/operation/buffer/BufferOpTest.cpp b/tests/unit/operation/buffer/BufferOpTest.cpp
index 4d4c693d9..a324bfb2a 100644
--- a/tests/unit/operation/buffer/BufferOpTest.cpp
+++ b/tests/unit/operation/buffer/BufferOpTest.cpp
@@ -95,7 +95,24 @@ void object::test<2>
     ensure_not(gBuffer->isEmpty());
     ensure(gBuffer->isValid());
     ensure_equals(gBuffer->getGeometryTypeId(), geos::geom::GEOS_POLYGON);
-    ensure(gBuffer->getNumPoints() > std::size_t(32));
+    ensure_equals(gBuffer->getNumPoints(), 33u);
+    auto coords = gBuffer->getCoordinates();
+    ensure_equals(coords->getSize(), 33u);
+
+    // Check four sides to check they are exactly on unit circle
+    auto coord = coords->getAt(0);
+    ensure_equals(coord.x, 1.0);
+    ensure_equals(coord.y, 0.0);
+    coord = coords->getAt(8);
+    ensure_equals(coord.x, 0.0);
+    ensure_equals(coord.y, -1.0);
+    coord = coords->getAt(16);
+    ensure_equals(coord.x, -1.0);
+    ensure_equals(coord.y, 0.0);
+    coord = coords->getAt(24);
+    ensure_equals(coord.x, 0.0);
+    ensure_equals(coord.y, 1.0);
+
 }
 
 template<>

commit 02f8fb1da4c94024f8a9193d90ced021be6fcb95
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Tue Nov 7 11:06:24 2023 -0800

    Clean up simplifier unit test code

diff --git a/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp b/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp
index e2b4ab415..d8d631cd9 100644
--- a/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp
+++ b/tests/unit/simplify/DouglasPeuckerSimplifierTest.cpp
@@ -35,6 +35,24 @@ struct test_dpsimp_data {
         :
         wktreader()
     {}
+
+    void
+    checkDP(const std::string& wkt, double tolerance, const std::string& wkt_expected)
+    {
+        GeomPtr g(wktreader.read(wkt));
+        GeomPtr simplified = DouglasPeuckerSimplifier::simplify(g.get(), tolerance);
+
+        ensure("Simplified geometry is invalid!", simplified->isValid());
+    
+        GeomPtr exp(wktreader.read(wkt_expected));
+        ensure_equals_geometry(exp.get(), simplified.get());
+    }
+
+    void
+    checkDPNoChange(const std::string& wkt, double tolerance)
+    {
+        checkDP(wkt, tolerance, wkt);
+    }
 };
 
 typedef test_group<test_dpsimp_data> group;
@@ -46,262 +64,118 @@ group test_dpsimp_group("geos::simplify::DouglasPeuckerSimplifier");
 // Test Cases
 //
 
-// 1 - PolygonNoReduction
+// 1 - testPolygonWithFlatVertices
 template<>
 template<>
-void object::test<1>
-()
+void object::test<1>()
 {
-    std::string wkt("POLYGON((20 220, 40 220, 60 220, 80 220, 100 220, \
-					120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))");
-
-    GeomPtr g(wktreader.read(wkt));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    // topology is unchanged
-    ensure(simplified->equals(g.get()));
+    checkDP("POLYGON ((20 220, 40 220, 60 220, 80 220, 100 220, 120 220, 140 220, 140 180, 100 180, 60 180, 20 180, 20 220))", 
+        10.0, 
+        "POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))");
 }
 
 // 2 - PolygonReductionWithSplit
 template<>
 template<>
-void object::test<2>
-()
+void object::test<2>()
 {
-    std::string wkt_in("POLYGON ((40 240, 160 241, 280 240, 280 160, \
-					160 240, 40 140, 40 240))");
-
-    std::string wkt_ex("MULTIPOLYGON (((40.0 240.0, 160.0 240.0, 40.0 140.0, 40.0 240.0)), \
-					((160.0 240.0, 280.0 240.0, 280.0 160.0, 160.0 240.0)))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    // TODO: This test blows because if instability of geos.index.strtree::yComparator() predicate
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-
+    checkDP("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))", 
+        1, 
+        "MULTIPOLYGON (((40 240, 160 240, 40 140, 40 240)), ((160 240, 280 240, 280 160, 160 240)))");
 }
 
 // 3 - PolygonReduction
 template<>
 template<>
-void object::test<3>
-()
+void object::test<3>()
 {
-    std::string wkt_in("POLYGON ((120 120, 121 121, 122 122, 220 120, \
-					180 199, 160 200, 140 199, 120 120))");
-
-    std::string wkt_ex("POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-
+    checkDP("POLYGON ((120 120, 121 121, 122 122, 220 120, 180 199, 160 200, 140 199, 120 120))",
+        10, 
+        "POLYGON ((120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
 }
 
 // 4 - PolygonWithTouchingHole
 template<>
 template<>
-void object::test<4>
-()
+void object::test<4>()
 {
-    std::string wkt_in("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), \
-					(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
-
-    std::string wkt_ex("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), \
-					(120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-
+    checkDP("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))",
+        10,
+        "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
 }
 
 // 5 - FlattishPolygon
 template<>
 template<>
-void object::test<5>
-()
+void object::test<5>()
 {
-    std::string wkt_in("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))");
-    std::string wkt_ex("POLYGON EMPTY");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-    //ensure_equals( *simplified, *expected );
-
+    checkDP("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1,  60 1, 50 1, 40 1, 0 0))",
+        10,
+        "POLYGON EMPTY");
 }
 
 // 6 - TinySquare
 template<>
 template<>
-void object::test<6>
-()
+void object::test<6>()
 {
-    std::string wkt_in("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))");
-    std::string wkt_ex("POLYGON EMPTY");
-
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-
+    checkDP("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))",
+        10,
+        "POLYGON EMPTY");
 }
 
 // TinyHole
 template<>
 template<>
-void object::test<7>
-()
+void object::test<7>()
 {
-    std::string wkt_in("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))");
-    std::string wkt_ex("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 30.0);
-
-    ensure(simplified->isValid());
-    ensure(simplified->equalsExact(expected.get()));
+    checkDP("POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10), (160 190, 180 190, 180 170, 160 190))",
+        30,
+        "POLYGON ((10 10, 10 310, 370 310, 370 10, 10 10))");
 }
 
 // 7 - TinyLineString
 template<>
 template<>
-void object::test<8>
-()
+void object::test<8>()
 {
-    std::string wkt_in("LINESTRING (0 5, 1 5, 2 5, 5 5)");
-    std::string wkt_ex("LINESTRING (0 5, 5 5)");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
-
+    checkDP("LINESTRING (0 5, 1 5, 2 5, 5 5)",
+        10,
+        "LINESTRING (0 5, 5 5)");
 }
 
 // 8 - MultiPoint
 template<>
 template<>
-void object::test<9>
-()
+void object::test<9>()
 {
-    std::string wkt_in("MULTIPOINT((80 200), (240 200), (240 60), (80 60), (80 200), (140 199), (120 120))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    // MultiPoint is *not* simplified
-    ensure(simplified->equalsExact(g.get()));
+    checkDPNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)",
+        10);
 }
 
 // 9 - MultiLineString
 template<>
 template<>
-void object::test<10>
-()
+void object::test<10>()
 {
-    std::string wkt_in("MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), \
-					(0 0, 50 1, 60 1, 100 0) )");
-
-    std::string wkt_ex("MULTILINESTRING( (0 0, 100 0), (0 0, 100 0) )");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
+    checkDP("MULTILINESTRING((0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )",
+        10,
+        "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))");
 }
 
 // 10 - GeometryCollection
 template<>
 template<>
-void object::test<11>
-()
+void object::test<11>()
 {
-    std::string wkt_in("GEOMETRYCOLLECTION ( \
-					MULTIPOINT ((80 200), (240 200), (240 60), (80 60), (80 200), (140 199), (120 120)), \
-					POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), \
-					LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120) )");
-
-    std::string wkt_ex("MULTILINESTRING( (0 0, 100 0), (0 0, 100 0) )");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 10.0);
-
-    ensure(simplified->isValid());
-
-    // Non simplification occurs
-    ensure(simplified->equalsExact(g.get()));
+    checkDPNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))",
+      10.0);
 }
 
 // 11 - A kind of reversed simplification
 template<>
 template<>
-void object::test<12>
-()
+void object::test<12>()
 {
     using namespace geos::geom;
 
@@ -373,23 +247,13 @@ void object::test<12>
 // 13 - Polygon with inner ring whose extent is less than the simplify distance (#741)
 template<>
 template<>
-void object::test<13>
-()
+void object::test<13>()
 {
-    std::string wkt_in("POLYGON ((0 0,0 1,1 1,0 0),(0.1 0.1,0.2 0.1,0.2 0.2,0.1 0.1))");
-
-    std::string wkt_ex("POLYGON ((0 0,0 1,1 1,0 0))");
-
-    GeomPtr g(wktreader.read(wkt_in));
-
-    GeomPtr expected(wktreader.read(wkt_ex));
-
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(
-                             g.get(), 0.5);
-
-    ensure(simplified->isValid());
-
-    ensure(simplified->equalsExact(expected.get()));
+    checkDP(
+        "POLYGON ((0 0,0 1,1 1,0 0),(0.1 0.1,0.2 0.1,0.2 0.2,0.1 0.1))",
+        0.5,
+        "POLYGON ((0 0,0 1,1 1,0 0))"
+        );
 }
 
 /**
@@ -400,16 +264,13 @@ void object::test<13>
 */
 template<>
 template<>
-void object::test<14>
-()
+void object::test<14>()
 {
-    std::string wkt_in("POLYGON ((21.32686 47.78723, 21.32386 47.79023, 21.32186 47.80223, 21.31486 47.81023, 21.32786 47.81123, 21.33986 47.80223, 21.33886 47.81123, 21.32686 47.82023, 21.32586 47.82723, 21.32786 47.82323, 21.33886 47.82623, 21.34186 47.82123, 21.36386 47.82223, 21.40686 47.81723, 21.32686 47.78723))");
-    std::string wkt_ex("POLYGON ((21.32686 47.78723, 21.31486 47.81023, 21.32786 47.81123, 21.33986 47.80223, 21.328068201892744 47.823286782334385, 21.33886 47.82623, 21.34186 47.82123, 21.40686 47.81723, 21.32686 47.78723))");
-    GeomPtr g(wktreader.read(wkt_in));
-    GeomPtr expected(wktreader.read(wkt_ex));
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(g.get(), 0.0036);
-    ensure(simplified->isValid());
-    ensure_equals_geometry(simplified.get(), expected.get());
+    checkDP(
+        "POLYGON ((21.32686 47.78723, 21.32386 47.79023, 21.32186 47.80223, 21.31486 47.81023, 21.32786 47.81123, 21.33986 47.80223, 21.33886 47.81123, 21.32686 47.82023, 21.32586 47.82723, 21.32786 47.82323, 21.33886 47.82623, 21.34186 47.82123, 21.36386 47.82223, 21.40686 47.81723, 21.32686 47.78723))", 
+        0.0036,
+        "POLYGON ((21.32686 47.78723, 21.31486 47.81023, 21.32786 47.81123, 21.33986 47.80223, 21.328068201892744 47.823286782334385, 21.33886 47.82623, 21.34186 47.82123, 21.40686 47.81723, 21.32686 47.78723))"
+        );
 }
 
   /**
@@ -420,40 +281,57 @@ void object::test<14>
    */
 template<>
 template<>
-void object::test<15>
-()
+void object::test<15>()
 {
-    std::string wkt_in("MULTIPOLYGON (((-76.02716827 36.55671692, -75.99866486 36.55665207, -75.91191864 36.54253006, -75.92480469 36.47397614, -75.97727966 36.4780159, -75.97628784 36.51792526, -76.02716827 36.55671692)), ((-75.90198517 36.55619812, -75.8781662 36.55587387, -75.77315521 36.22925568, -75.78317261 36.22519302, -75.90198517 36.55619812)))");
-    std::string wkt_ex("POLYGON ((-76.02716827 36.55671692, -75.91191864 36.54253006, -75.92480469 36.47397614, -76.02716827 36.55671692))");
-    GeomPtr g(wktreader.read(wkt_in));
-    GeomPtr expected(wktreader.read(wkt_ex));
-    GeomPtr simplified = DouglasPeuckerSimplifier::simplify(g.get(), 0.05);
-    ensure(simplified->isValid());
-    ensure_equals_geometry(simplified.get(), expected.get());
+    checkDP(
+        "MULTIPOLYGON (((-76.02716827 36.55671692, -75.99866486 36.55665207, -75.91191864 36.54253006, -75.92480469 36.47397614, -75.97727966 36.4780159, -75.97628784 36.51792526, -76.02716827 36.55671692)), ((-75.90198517 36.55619812, -75.8781662 36.55587387, -75.77315521 36.22925568, -75.78317261 36.22519302, -75.90198517 36.55619812)))", 
+        0.05,
+        "POLYGON ((-76.02716827 36.55671692, -75.91191864 36.54253006, -75.92480469 36.47397614, -76.02716827 36.55671692))"
+        );
 }
 
 // Test that start point of a polygon can be removed
 template<>
 template<>
-void object::test<16>
-()
+void object::test<16>()
 {
-     auto g = wktreader.read("POLYGON ((1 0, 2 0, 2 2, 0 2, 0 0, 1 0))");
-     auto simplified = DouglasPeuckerSimplifier::simplify(g.get(), 0);
-     auto expected = wktreader.read("POLYGON (( 0 0, 2 0, 2 2, 0 2, 0 0))");
-     ensure_equals_geometry(simplified.get(), expected.get());
+    checkDP("POLYGON ((1 0, 2 0, 2 2, 0 2, 0 0, 1 0))", 
+        0,
+        "POLYGON (( 0 0, 2 0, 2 2, 0 2, 0 0))");
 }
 
 // Test that start point of a closed LineString is not changed
 template<>
 template<>
-void object::test<17>
-()
+void object::test<17>()
 {
-     auto g = wktreader.read("LINESTRING (1 0, 2 0, 2 2, 0 2, 0 0, 1 0)");
-     auto simplified = DouglasPeuckerSimplifier::simplify(g.get(), 0);
-     ensure_equals_geometry(simplified.get(), g.get());
+    checkDPNoChange("LINESTRING (1 0, 2 0, 2 2, 0 2, 0 0, 1 0)", 
+        0);
 }
 
+// testPolygonRemoveFlatEndpoint
+// see https://trac.osgeo.org/geos/ticket/1064
+template<>
+template<>
+void object::test<18>()
+{
+    checkDP(
+      "POLYGON ((42 42, 0 42, 0 100, 42 100, 100 42, 42 42))",
+        1,
+        "POLYGON ((100 42, 0 42, 0 100, 42 100, 100 42))"
+        );
+}
+
+// testPolygonEndpointCollapse
+template<>
+template<>
+void object::test<19>()
+{
+    checkDP(
+        "POLYGON ((5 2, 9 1, 1 1, 5 2))",
+        1,
+        "POLYGON EMPTY"
+        );
+}
 
 } // namespace tut
diff --git a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
index c21b2ce42..73190e7a9 100644
--- a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
+++ b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
@@ -46,6 +46,12 @@ struct test_tpsimp_data {
         GeomPtr exp(wktreader.read(wkt_expected));
         ensure_equals_geometry(exp.get(), simplified.get());
     }
+
+    void
+    checkTPSNoChange(const std::string& wkt, double tolerance)
+    {
+        checkTPS(wkt, tolerance, wkt);
+    }
 };
 
 typedef test_group<test_tpsimp_data> group;
@@ -63,28 +69,15 @@ template<>
 void object::test<1>
 ()
 {
-    std::string wkt("POLYGON EMPTY");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("POLYGON(EMPTY)", 1);
 }
 
 // Point
 template<>
 template<>
-void object::test<2>
-()
+void object::test<2> ()
 {
-    std::string wkt("POINT (10 10)");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("POINT (10 10)", 1);
 }
 
 #if 0 // Fails with JTS too !
@@ -95,308 +88,166 @@ template<>
 void object::test<3>
 ()
 {
-    std::string wkt(
-        "MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.730096
 4 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13.7214
 718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.04395
 58,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.7408331 51.
 0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.7473063 51
 .0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563383 5
 1.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,13.749
 2723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.737321 51.
 0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,13.739
 0933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))"
-    );
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 0.0057);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-
+    checkTPS("MULTIPOLYGON(((13.73095 51.024734,13.7309323 51.0247668,13.7306959 51.0247959,13.7292724 51.0249742,13.7280216 51.0251252,13.7266598 51.0252998,13.7259617 51.0254072,13.7258854 51.0254201,13.7253253 51.0255144,13.725276 51.025492,13.724538 51.025631,13.7230288 51.0259021,13.7223529 51.0260273,13.7223299 51.0260863,13.7222292 51.026391,13.7220002 51.0273366,13.7217875 51.0282094,13.721746 51.028243,13.7217693 51.0282803,13.7215512 51.0291967,13.721513 51.029222,13.7215203 51.0292567,13.7212713 51.0295967,13.7222258 51.0299532,13.722234 51.03,13.7222931 51.0299823,13.7232514 51.0303187,13.7242514 51.0306715,13.724263 51.030714,13.7243024 51.0306951,13.7249934 51.0309315,13.7265097 51.0314552,13.7266116 51.0313952,13.7267988 51.0313334,13.7269952 51.0313243,13.72703 51.0314107,13.7271637 51.0313254,13.7272524 51.0313839,13.72739 51.031449,13.7276768 51.0313074,13.7283793 51.0309944,13.7296654 51.0304157,13.7297572 51.0303637,13.729845 51.0303139,13.7299557 51.0301763,13.7
 300964 51.0300176,13.730252 51.0298919,13.7304615 51.0297932,13.730668 51.0297363,13.730743 51.029783,13.7307859 51.0298398,13.7307094 51.0301388,13.730624 51.030263,13.7306955 51.0303267,13.7301182 51.0325594,13.7300528 51.0325663,13.7301114 51.0327342,13.7301645 51.0329094,13.7300035 51.0327693,13.7299669 51.0327351,13.7299445 51.0327211,13.7298934 51.032814,13.7298539 51.0328585,13.7297737 51.0328321,13.7288526 51.0325639,13.7288201 51.0324367,13.7284426 51.0324383,13.7276461 51.032179,13.7274569 51.0321976,13.7272787 51.0322421,13.7271265 51.0322903,13.7267034 51.0322495,13.7265364 51.0322161,13.7259018 51.0324269,13.7258649 51.03242,13.725733 51.0326646,13.7251933 51.0328876,13.7247918 51.0331374,13.7244439 51.0331106,13.7242967 51.0334273,13.7239131 51.0337529,13.7237035 51.0338511,13.7235429 51.033967,13.7233375 51.0339148,13.7232064 51.0339347,13.7231786 51.0339863,13.7228848 51.0340776,13.7224481 51.0341888,13.7220471 51.0342483,13.7217493 51.0343198,13.721552 51.0343861,13
 .7214718 51.0344095,13.7215108 51.034534,13.7205032 51.0349932,13.7197657 51.0352983,13.7195764 51.0352291,13.7195934 51.0352797,13.7182451 51.0359157,13.7181108 51.0359003,13.7181657 51.0359571,13.717622 51.0361956,13.7159749 51.0369683,13.7159057 51.0369284,13.7158604 51.0370288,13.7157161 51.0370124,13.7157523 51.0370733,13.7153708 51.0372801,13.7150274 51.0374899,13.7144074 51.0379192,13.7138287 51.0383899,13.7137514 51.0383857,13.7137492 51.0384566,13.7134249 51.0387269,13.7130179 51.0390385,13.7125791 51.0393343,13.7120736 51.039611,13.7115839 51.0398558,13.7112945 51.0399894,13.7114637 51.0402313,13.7123153 51.041449,13.7126333 51.0417033,13.713371 51.0421453,13.7138861 51.0424061,13.7142518 51.0425683,13.7164587 51.0435668,13.7167995 51.0437957,13.7170883 51.0439897,13.7190694 51.0451663,13.7196131 51.0458277,13.7197562 51.0461521,13.7198262 51.0464192,13.7198377 51.0467389,13.7205681 51.0455573,13.7210009 51.0450379,13.7214987 51.0445401,13.7220306 51.0442859,13.7227215 51.
 0439558,13.7237962 51.0434514,13.723979 51.0435278,13.7241448 51.0435041,13.7241052 51.0436042,13.7247987 51.0438896,13.7250186 51.0439093,13.7250579 51.0440386,13.7257225 51.0443545,13.7259312 51.0443456,13.725955 51.0443813,13.7260235 51.0443873,13.7260682 51.0445303,13.7282191 51.0455848,13.7290532 51.045927,13.7292643 51.0458591,13.7292228 51.0459969,13.729706 51.0461854,13.7303185 51.046393,13.7309107 51.0465601,13.731546 51.0466841,13.7321939 51.0467752,13.7332896 51.0468999,13.7333733 51.0469094,13.7334778 51.0468127,13.7335706 51.0469078,13.733651 51.0470684,13.7338458 51.0471508,13.7346109 51.0472333,13.7346367 51.0471474,13.7346922 51.0470697,13.7346666 51.0470056,13.7346564 51.0468714,13.7345552 51.0467095,13.7336001 51.0465496,13.733427 51.046454,13.7335317 51.0464255,13.7347225 51.0465948,13.7348421 51.0466562,13.7349123 51.0466203,13.736811 51.0468537,13.7382043 51.0469796,13.7383487 51.0469803,13.7394909 51.0469005,13.7400899 51.0467949,13.7405051 51.0464739,13.740833
 1 51.0462204,13.7412027 51.0463256,13.741053 51.0466451,13.7407291 51.0469007,13.7405095 51.0469726,13.7400888 51.0470337,13.7393051 51.0471049,13.7393014 51.0472015,13.7393088 51.0473019,13.7395556 51.0473056,13.7404944 51.0472245,13.740932 51.0470192,13.7414421 51.0465652,13.7414893 51.0465576,13.7416494 51.0464916,13.7416003 51.0466074,13.7416246 51.04663,13.741668 51.0466443,13.7417272 51.0467159,13.7417503 51.0466716,13.7423587 51.0468732,13.7426958 51.0470246,13.7429143 51.0471813,13.74318 51.04726,13.7430363 51.0472995,13.7433021 51.047588,13.7434678 51.0475916,13.7433805 51.0477019,13.7436362 51.0479981,13.7446308 51.0491622,13.7447961 51.0491827,13.744722 51.0492509,13.7448536 51.0494078,13.745056 51.0494766,13.7450313 51.0496901,13.7453573 51.0500052,13.7465317 51.0512807,13.7466999 51.0513722,13.746638 51.0514149,13.7468683 51.0516781,13.7470071 51.051777,13.7469985 51.0518746,13.7470732 51.0519866,13.7471316 51.0520528,13.7472989 51.0523089,13.7472368 51.0523858,13.74730
 63 51.0524932,13.7473468 51.0527412,13.7473392 51.0531614,13.7472987 51.0533157,13.7473919 51.0534224,13.7472684 51.0534549,13.7472134 51.0536926,13.7472913 51.0537784,13.7473216 51.053725,13.7474649 51.0537575,13.7474492 51.053833,13.7475625 51.0537839,13.7497379 51.0544435,13.7515333 51.0551019,13.7527693 51.0555438,13.7549766 51.0564993,13.7550622 51.0565364,13.755105 51.0566612,13.7552745 51.0566237,13.7558661 51.0560648,13.7559318 51.0560101,13.755908 51.055897,13.7559252 51.0558292,13.7559566 51.0557055,13.7564494 51.0551377,13.7564124 51.0550457,13.7573213 51.0539813,13.7575007 51.0539933,13.757856 51.0540047,13.7580394 51.054028,13.7580896 51.053984,13.7580949 51.0539463,13.7579963 51.0538534,13.7581294 51.0537147,13.7582346 51.0535957,13.758354 51.053433,13.758363 51.053392,13.7583656 51.0533457,13.758359 51.0532095,13.7583338 51.0530937,13.7582902 51.0529647,13.7580365 51.0522637,13.7577683 51.051463,13.7573182 51.0501993,13.7571595 51.0497164,13.7567579 51.0490095,13.7563
 383 51.0482979,13.7557757 51.0473383,13.7557095 51.0472522,13.7555771 51.0471199,13.7554448 51.0470471,13.7548596 51.0462612,13.7547097 51.046054,13.7549127 51.0460086,13.7548633 51.0459174,13.7548127 51.0458413,13.7547176 51.0457237,13.7538293 51.0449222,13.7530218 51.0441346,13.7526711 51.0437838,13.752446 51.0435522,13.7522297 51.0433547,13.751704 51.042833,13.7513058 51.0424448,13.7505766 51.0417281,13.7499967 51.0411283,13.7497695 51.0408943,13.7493849 51.0405205,13.7486222 51.0397896,13.7478209 51.0390261,13.7477474 51.0389532,13.7477041 51.0389189,13.7476277 51.0388729,13.7475781 51.0388513,13.7472699 51.038726,13.747131 51.0386506,13.7469329 51.0385052,13.7468562 51.0384284,13.7466683 51.0383483,13.7467998 51.038236,13.7473841 51.0380129,13.747838 51.0378277,13.7481801 51.0376558,13.7489728 51.0370285,13.7491313 51.0368016,13.7492665 51.0363477,13.7493166 51.0359389,13.7492966 51.0358087,13.7493888 51.0356942,13.7492867 51.0357016,13.7492855 51.0354359,13.7492829 51.034867,1
 3.7492723 51.0348311,13.7492455 51.0347398,13.7493034 51.0346612,13.7491987 51.0346142,13.748866 51.034723,13.748791 51.034201,13.748335 51.034159,13.748294 51.034034,13.748205 51.033764,13.7488691 51.0333037,13.748962 51.033245,13.7486777 51.0332252,13.7483008 51.032683,13.7484397 51.0324582,13.7469913 51.0327817,13.7466998 51.0326205,13.7459997 51.0314852,13.7460996 51.0313569,13.745967 51.0314864,13.7449355 51.0317377,13.7447301 51.0316513,13.7446705 51.0318463,13.7420262 51.0323659,13.7419131 51.0322884,13.7418636 51.0322552,13.7416501 51.0321425,13.7415567 51.0317708,13.7414972 51.0314666,13.741484 51.0311492,13.741923 51.031003,13.7418649 51.030884,13.74209 51.0304134,13.7422077 51.0300143,13.7421975 51.0299222,13.742286 51.029835,13.7421463 51.0297533,13.7420951 51.0296254,13.7415933 51.0288452,13.7414906 51.0286855,13.7414437 51.0286127,13.7413482 51.0284642,13.7410545 51.0280777,13.7407158 51.0277229,13.7401513 51.0273842,13.7392803 51.0270293,13.7382744 51.0267844,13.73732
 1 51.0267454,13.7365929 51.0267541,13.736556 51.026812,13.7364715 51.026754,13.7357088 51.0268017,13.7353967 51.02678,13.73534 51.02685,13.7352667 51.0267757,13.734907 51.0267324,13.734824 51.02679,13.7347684 51.0267064,13.7342093 51.0266674,13.73409 51.026725,13.7340359 51.0266283,13.7335072 51.0265633,13.733407 51.02663,13.7333208 51.0265373,13.7317087 51.0263813,13.7317173 51.0263119,13.73167 51.026241,13.7317563 51.0261602,13.7318473 51.0258395,13.7318647 51.0254971,13.73183 51.0253281,13.7317736 51.0252414,13.731663 51.025181,13.7316826 51.0251114,13.7310803 51.0247604,13.73095 51.024734)),((13.7368533 51.0470386,13.7368426 51.0471226,13.7368067 51.0472669,13.7368255 51.0473828,13.7369099 51.0474154,13.7376695 51.0474677,13.7382756 51.0474245,13.738513 51.0474297,13.7386105 51.0474065,13.738705 51.0473737,13.7385856 51.0473757,13.7385618 51.0473751,13.7385263 51.0473743,13.7384706 51.0473744,13.7383071 51.0473734,13.7383822 51.0473564,13.7390821 51.047287,13.7390933 51.047209,1
 3.7390933 51.0471421,13.7368533 51.0470386)),((13.7367293 51.0470057,13.7346615 51.0466892,13.7347551 51.0468411,13.7347754 51.0470359,13.7347106 51.0471899,13.7356421 51.0472919,13.7366963 51.0474074,13.736705 51.047249,13.7367293 51.0470057)))",
+        0.0057,
+        "MULTIPOLYGON (((13.73095 51.024734, 13.7123153 51.041449, 13.7552745 51.0566237, 13.7484397 51.0324582, 13.73095 51.024734)), ((13.7390933 51.0471421, 13.7369099 51.0474154, 13.7390933 51.047209, 13.7390933 51.0471421)), ((13.7367293 51.0470057, 13.7346615 51.0466892, 13.7347106 51.0471899, 13.7367293 51.0470057)))");
 }
 #endif // fails with JTS too
 
 // PolygonWithSpike
 template<>
 template<>
-void object::test<4>
-()
+void object::test<4>()
 {
-    std::string wkt("POLYGON ((3312459.605 6646878.353, \
-      3312460.524 6646875.969, 3312459.427 6646878.421, \
-      3312460.014 6646886.391, 3312465.889 6646887.398, \
-      3312470.827 6646884.839, 3312475.4 6646878.027, \
-      3312477.289 6646871.694, 3312472.748 6646869.547, \
-      3312468.253 6646874.01, 3312463.52 6646875.779, \
-      3312459.605 6646878.353))");
-
-    std::string wkt_expected("POLYGON (( \
-      3312459.605 6646878.353, \
-      3312460.524 6646875.969, \
-      3312459.427 6646878.421, \
-      3312460.014 6646886.391, \
-      3312465.889 6646887.398, \
-      3312470.827 6646884.839, \
-      3312477.289 6646871.694, \
-      3312472.748 6646869.547, \
-      3312459.605 6646878.353))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 2.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-
-    GeomPtr g_expected(wktreader.read(wkt_expected));
-    ensure(g_expected->equalsExact(simplified.get()));
-
+    checkTPS("POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312475.4 6646878.027, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312468.253 6646874.01, 3312463.52 6646875.779, 3312459.605 6646878.353))",
+        2,
+        "POLYGON ((3312459.605 6646878.353, 3312460.524 6646875.969, 3312459.427 6646878.421, 3312460.014 6646886.391, 3312465.889 6646887.398, 3312470.827 6646884.839, 3312477.289 6646871.694, 3312472.748 6646869.547, 3312459.605 6646878.353))");
 }
 
 // PolygonNoReduction
 template<>
 template<>
-void object::test<5>
-()
+void object::test<5>()
 {
-    std::string wkt("POLYGON((20 220, 40 220, 60 220, 80 220, \
-                    100 220, 120 220, 140 220, 140 180, 100 180, \
-                    60 180, 20 180, 20 220))");
-    GeomPtr g(wktreader.read(wkt));
-
-    std::string wkt_exp("POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))");
-    GeomPtr exp(wktreader.read(wkt_exp));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure("Simplified and original geometry inequal", simplified->equals(g.get()));
-    ensure_equals_geometry(exp.get(), simplified.get());
+    checkTPSNoChange("POLYGON ((20 220, 140 220, 140 180, 20 180, 20 220))",
+        10);
 }
 
 // PolygonNoReductionWithConflicts
 template<>
 template<>
-void object::test<6>
-()
+void object::test<6>()
 {
-    std::string wkt("POLYGON ((40 240, 160 241, 280 240, 280 160, \
-                        160 240, 40 140, 40 240))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure("Topology has been changed by simplification!", simplified->equals(g.get()));
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("POLYGON ((40 240, 160 241, 280 240, 280 160, 160 240, 40 140, 40 240))",
+        10);
 }
 
 // PolygonWithTouchingHole
 template<>
 template<>
-void object::test<7>
-()
+void object::test<7>()
 {
-    std::string wkt("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), \
-                    (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
-
-    std::string wkt_expected("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), \
-                    (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
-
-    GeomPtr g_expected(wktreader.read(wkt_expected));
-
-    ensure(g_expected->equalsExact(simplified.get()));
+    checkTPS("POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))",
+        10,
+        "POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200), (120 120, 220 120, 180 199, 160 200, 140 199, 120 120))");
 }
 
 // FlattishPolygon
 template<>
 template<>
-void object::test<8>
-()
+void object::test<8>()
 {
-    std::string wkt("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1, 60 1, 50 1, 40 1, 0 0))");
-    GeomPtr g(wktreader.read(wkt));
-
-    std::string wkt_exp("POLYGON ((0 0, 50 0, 100 0, 70 1, 0 0))");
-    GeomPtr exp(wktreader.read(wkt_exp));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(exp.get(), simplified.get());
+    checkTPS("POLYGON ((0 0, 50 0, 53 0, 55 0, 100 0, 70 1,  60 1, 50 1, 40 1, 0 0))",
+        10,
+        "POLYGON ((0 0, 50 0, 100 0, 70 1, 0 0))");
 }
 
 // PolygonWithFlattishHole
 template<>
 template<>
-void object::test<9>
-()
+void object::test<9>()
 {
-    std::string wkt("POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), \
-                    (140 40, 90 95, 40 160, 95 100, 140 40))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr g_expected(wktreader.read(wkt));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
-    ensure(g_expected->equalsExact(simplified.get()));
+    checkTPS("POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))",
+        20,
+        "POLYGON ((0 0, 0 200, 200 200, 200 0, 0 0), (140 40, 90 95, 40 160, 95 100, 140 40))");
 }
 
 // TinySquare
 template<>
 template<>
-void object::test<10>
-()
+void object::test<10>()
 {
-    std::string wkt("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))");
-    GeomPtr g(wktreader.read(wkt));
-
-    std::string wkt_exp("POLYGON ((0 0, 5 5, 5 0, 0 0))");
-    GeomPtr exp(wktreader.read(wkt_exp));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(exp.get(), simplified.get());
+    checkTPS("POLYGON ((0 5, 5 5, 5 0, 0 0, 0 1, 0 5))",
+        10,
+        "POLYGON ((0 0, 5 5, 5 0, 0 0))");
 }
 
 // TinyLineString
 template<>
 template<>
-void object::test<11>
-()
+void object::test<11>()
 {
-    std::string wkt("LINESTRING (0 5, 1 5, 2 5, 5 5)");
-    GeomPtr g(wktreader.read(wkt));
-
-    std::string wkt_exp("LINESTRING (0 5, 5 5)");
-    GeomPtr exp(wktreader.read(wkt_exp));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(exp.get(), simplified.get());
+    checkTPS("LINESTRING (0 5, 1 5, 2 5, 5 5)",
+        10,
+        "LINESTRING (0 5, 5 5)");
 }
 
 // TinyClosedLineString
 template<>
 template<>
-void object::test<12>
-()
+void object::test<12>()
 {
-    std::string wkt("LINESTRING (0 0, 5 0, 5 5, 0 0)");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("LINESTRING (0 0, 5 0, 5 5, 0 0)",
+        10);
 }
 
 // MultiPoint
 template<>
 template<>
-void object::test<13>
-()
+void object::test<13>()
 {
-    std::string wkt("MULTIPOINT((80 200), (240 200), (240 60), \
-                    (80 60), (80 200), (140 199), (120 120))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("MULTIPOINT(80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120)",
+        10);
 }
 
 // MultiLineString
 template<>
 template<>
-void object::test<14>
-()
+void object::test<14>()
 {
-    std::string wkt("MULTILINESTRING((0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0))");
-    GeomPtr g(wktreader.read(wkt));
+    checkTPS("MULTILINESTRING( (0 0, 50 0, 70 0, 80 0, 100 0), (0 0, 50 1, 60 1, 100 0) )",
+    10,
+    "MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))");
 
-    //TODO: investigate why TPS does not prevent lines from collapsing (JTS has same behaviour)
+    //TODO: investigate why TPS does not prevent lines from collapsing 
+    // JTS has correct behaviour - result should be:
     //std::string wkt_exp("MULTILINESTRING ((0 0, 100 0), (0 0, 50 1, 100 0))");
-    std::string wkt_exp("MULTILINESTRING ((0 0, 100 0), (0 0, 100 0))");
-    GeomPtr exp(wktreader.read(wkt_exp));
-
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-    // std::cout << "expected " << *exp << std::endl;
-    // std::cout << "result " << *simplified << std::endl;
-    ensure("Simplified geometry is invalid!", simplified->isValid());
- /* Temporarily disable this component of the test only for MSVC
-    See https://trac.osgeo.org/geos/ticket/1081 */
-#ifndef _MSC_VER
-    ensure_equals_geometry(exp.get(), simplified.get());
-#endif
 }
 
 // GeometryCollection
 template<>
 template<>
-void object::test<15>
-()
+void object::test<15>()
 {
-    std::string wkt("GEOMETRYCOLLECTION ( \
-                    MULTIPOINT ((80 200), (240 200), (240 60), (80 60), (80 200), (140 199), (120 120)), \
-                    POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), \
-                    LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), 10.0);
-
-    ensure("Simplified geometry is invalid!", simplified->isValid());
-    ensure_equals_geometry(g.get(), simplified.get());
+    checkTPSNoChange("GEOMETRYCOLLECTION (MULTIPOINT (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120), POLYGON ((80 200, 240 200, 240 60, 80 60, 80 200)), LINESTRING (80 200, 240 200, 240 60, 80 60, 80 200, 140 199, 120 120))",
+      10);
 }
 
 // GeometryCollection with empty elements
 // See http://trac.osgeo.org/geos/ticket/519
 template<>
 template<>
-void object::test<16>
-()
+void object::test<16>()
 {
-    std::string wkt("GEOMETRYCOLLECTION ( \
-                    LINESTRING (0 0, 10 0), POLYGON EMPTY)");
-
-    GeomPtr g(wktreader.read(wkt));
-    GeomPtr simp = TopologyPreservingSimplifier::simplify(g.get(), 1);
-
-    ensure("Simplified geometry is invalid!", simp->isValid());
-    ensure_equals(wktwriter.write(simp.get()),
-                  "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))");
+    checkTPS("GEOMETRYCOLLECTION ( LINESTRING (0 0, 10 0), POLYGON EMPTY)",
+        1,
+        "GEOMETRYCOLLECTION (LINESTRING (0 0, 10 0))");
 }
 
 // Test that start point of a closed LineString is not changed
 template<>
 template<>
-void object::test<17>
-()
+void object::test<17>()
 {
-     auto g = wktreader.read("LINESTRING (1 0, 2 0, 2 2, 0 2, 0 0, 1 0)");
-     auto simplified = TopologyPreservingSimplifier::simplify(g.get(), 0);
-     ensure_equals_geometry(simplified.get(), g.get());
+    checkTPSNoChange("LINESTRING (1 0, 2 0, 2 2, 0 2, 0 0, 1 0)",
+      0);
 }
 
-// Test that removing starting point of ring does not break topology
+// testPolygonKeepFlatEndpointWithTouch
+// Test that flat ring endpoint is not removed if it touches another ring
 template<>
 template<>
-void object::test<18>
-()
+void object::test<18>()
 {
-    auto g = wktreader.read("POLYGON ((0 0, 5 2.05, 10 0, 10 10, 0 10, 0 0),  (5 2.1, 6 2, 6 4, 4 4, 4 2, 5 2.1))");
-    auto simplified = TopologyPreservingSimplifier::simplify(g.get(), 0.1);
-
-    ensure_equals_geometry(simplified.get(), g.get());
+    checkTPSNoChange("POLYGON ((0 0, 5 2.05, 10 0, 10 10, 0 10, 0 0),  (5 2.1, 6 2, 6 4, 4 4, 4 2, 5 2.1))",
+        0.1);
 }
 
 // testPolygonKeepEndpointWithCross
 // Test that endpoint is not simplified if it breaks topology
 template<>
 template<>
-void object::test<19>
-()
+void object::test<19>()
 {
     checkTPS(
       "POLYGON ((50 52, 60 50, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 40 50, 50 52))",

commit c1927788e6e23c37c9f3d87dbdba5f92644a2a96
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Mon Nov 6 17:18:52 2023 -0800

    Update NEWS

diff --git a/NEWS.md b/NEWS.md
index 77a2b7fc7..08752b6b1 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -18,7 +18,7 @@
   - Fix error in CoordinateSequence::add when disallowing repeated points (GH-963, Dan Baston)
   - Fix WKTWriter::writeTrimmedNumber for big and small values (GH-973, Mike Taves)
   - Fix InteriorPointPoint to handle empty elements (GH-977, Martin Davis)
-
+  - Fix TopologyPreservingSimplifier endpoint handling to avoid self-intersections (GH-986, Martin Davis)
 
 ## Changes in 3.12.0
 2023-06-27

commit 1fc2612972070d56576a788cd6511d91c62bb0e0
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Mon Nov 6 17:17:44 2023 -0800

    Fix TopologyPreservingSimplifier endpoint handling to avoid self-intersections (#986)

diff --git a/include/geos/simplify/TaggedLineStringSimplifier.h b/include/geos/simplify/TaggedLineStringSimplifier.h
index 835f6e870..5ed98e766 100644
--- a/include/geos/simplify/TaggedLineStringSimplifier.h
+++ b/include/geos/simplify/TaggedLineStringSimplifier.h
@@ -113,12 +113,12 @@ private:
         double& maxDistance);
 
     bool hasBadIntersection(const TaggedLineString* parentLine,
-                            const std::pair<std::size_t, std::size_t>& sectionIndex,
+                            const size_t excludeStart, const size_t excludeEnd,
                             const geom::LineSegment& candidateSeg);
 
     bool hasBadInputIntersection(const TaggedLineString* parentLine,
-                                 const std::pair<std::size_t, std::size_t>& sectionIndex,
-                                 const geom::LineSegment& candidateSeg);
+                                const size_t excludeStart, const size_t excludeEnd,
+                                const geom::LineSegment& candidateSeg);
 
     bool hasBadOutputIntersection(const geom::LineSegment& candidateSeg);
 
@@ -129,16 +129,20 @@ private:
         std::size_t start, std::size_t end);
 
     /** \brief
-     * Tests whether a segment is in a section of a TaggedLineString
+     * Tests whether a segment is in a section of a TaggedLineString.
+     * Sections may wrap around the endpoint of the line, 
+     * to support ring endpoint simplification.
+     * This is indicated by excludedStart > excludedEnd
      *
      * @param line line to be checked for the presence of `seg`
-     * @param sectionIndex start and end indices of the section to check
+     * @param excludeStart  the index of the first segment in the excluded section  
+     * @param excludeEnd the index of the last segment in the excluded section
      * @param seg segment to look for in `line`
-     * @return
+     * @return true if the test segment intersects some segment in the line not in the excluded section
      */
     static bool isInLineSection(
         const TaggedLineString* line,
-        const std::pair<std::size_t, std::size_t>& sectionIndex,
+        const size_t excludeStart, const size_t excludeEnd,
         const TaggedLineSegment* seg);
 
     /** \brief
diff --git a/src/simplify/TaggedLineStringSimplifier.cpp b/src/simplify/TaggedLineStringSimplifier.cpp
index c266816fb..185218f4b 100644
--- a/src/simplify/TaggedLineStringSimplifier.cpp
+++ b/src/simplify/TaggedLineStringSimplifier.cpp
@@ -38,7 +38,6 @@
 #endif
 
 using namespace geos::geom;
-using std::pair;
 using std::unique_ptr;
 using std::vector;
 
@@ -149,7 +148,7 @@ TaggedLineStringSimplifier::simplifySection(std::size_t i,
     // test if flattened section would cause intersection
     LineSegment candidateSeg(linePts->getAt(i), linePts->getAt(j));
 
-    if(hasBadIntersection(line, std::make_pair(i, j), candidateSeg)) {
+    if(hasBadIntersection(line, i, j, candidateSeg)) {
         isValidToSimplify = false;
     }
 
@@ -182,8 +181,8 @@ TaggedLineStringSimplifier::simplifyRingEndpoint()
 
         LineSegment candidateSeg(lastSeg->p0, firstSeg->p1);
         if (candidateSeg.distance(firstSeg->p0) <= distanceTolerance &&
-                !hasBadIntersection(line, std::make_pair(0, line->getSegments().size()), candidateSeg)) {
-            auto newSeg = detail::make_unique<TaggedLineSegment>(candidateSeg.p0, candidateSeg.p1);
+                ! hasBadIntersection(line, line->getSegments().size() - 2, 0, candidateSeg)) {
+            //auto newSeg = detail::make_unique<TaggedLineSegment>(candidateSeg.p0, candidateSeg.p1);
             line->removeRingEndpoint();
         }
     }
@@ -207,14 +206,14 @@ TaggedLineStringSimplifier::flatten(std::size_t start, std::size_t end)
 bool
 TaggedLineStringSimplifier::hasBadIntersection(
     const TaggedLineString* parentLine,
-    const pair<size_t, size_t>& sectionIndex,
+    const size_t excludeStart, const size_t excludeEnd,
     const LineSegment& candidateSeg)
 {
     if(hasBadOutputIntersection(candidateSeg)) {
         return true;
     }
 
-    if(hasBadInputIntersection(parentLine, sectionIndex, candidateSeg)) {
+    if(hasBadInputIntersection(parentLine, excludeStart, excludeEnd, candidateSeg)) {
         return true;
     }
 
@@ -252,7 +251,7 @@ TaggedLineStringSimplifier::hasInteriorIntersection(
 bool
 TaggedLineStringSimplifier::hasBadInputIntersection(
     const TaggedLineString* parentLine,
-    const pair<std::size_t, std::size_t>& sectionIndex,
+    const size_t excludeStart, const size_t excludeEnd,
     const LineSegment& candidateSeg)
 {
     const auto& foundSegs = inputIndex->query(&candidateSeg);
@@ -260,7 +259,7 @@ TaggedLineStringSimplifier::hasBadInputIntersection(
     for(const LineSegment* ls : *foundSegs) {
         const TaggedLineSegment* foundSeg = static_cast<const TaggedLineSegment*>(ls);
 
-        if(!isInLineSection(parentLine, sectionIndex, foundSeg) && hasInteriorIntersection(*foundSeg, candidateSeg)) {
+        if(!isInLineSection(parentLine, excludeStart, excludeEnd, foundSeg) && hasInteriorIntersection(*foundSeg, candidateSeg)) {
             return true;
         }
     }
@@ -272,7 +271,7 @@ TaggedLineStringSimplifier::hasBadInputIntersection(
 bool
 TaggedLineStringSimplifier::isInLineSection(
     const TaggedLineString* line,
-    const pair<size_t, size_t>& sectionIndex,
+    const size_t excludeStart, const size_t excludeEnd,
     const TaggedLineSegment* seg)
 {
     // not in this line
@@ -281,10 +280,16 @@ TaggedLineStringSimplifier::isInLineSection(
     }
 
     std::size_t segIndex = seg->getIndex();
-    if(segIndex >= sectionIndex.first && segIndex < sectionIndex.second) {
+    if (excludeStart <= excludeEnd) {
+      //-- section is contiguous
+      if (segIndex >= excludeStart && segIndex < excludeEnd)
         return true;
     }
-
+    else {
+      //-- section wraps around the end of a ring
+      if (segIndex >= excludeStart || segIndex <= excludeEnd)
+      return true;
+    }
     return false;
 }
 
diff --git a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
index b36ad12a3..c21b2ce42 100644
--- a/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
+++ b/tests/unit/simplify/TopologyPreservingSimplifierTest.cpp
@@ -34,6 +34,18 @@ struct test_tpsimp_data {
         , wktwriter()
     {
     }
+
+    void
+    checkTPS(const std::string& wkt, double tolerance, const std::string& wkt_expected)
+    {
+        GeomPtr g(wktreader.read(wkt));
+        GeomPtr simplified = TopologyPreservingSimplifier::simplify(g.get(), tolerance);
+
+        ensure("Simplified geometry is invalid!", simplified->isValid());
+    
+        GeomPtr exp(wktreader.read(wkt_expected));
+        ensure_equals_geometry(exp.get(), simplified.get());
+    }
 };
 
 typedef test_group<test_tpsimp_data> group;
@@ -379,4 +391,18 @@ void object::test<18>
     ensure_equals_geometry(simplified.get(), g.get());
 }
 
+// testPolygonKeepEndpointWithCross
+// Test that endpoint is not simplified if it breaks topology
+template<>
+template<>
+void object::test<19>
+()
+{
+    checkTPS(
+      "POLYGON ((50 52, 60 50, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 40 50, 50 52))",
+        10,
+        "POLYGON ((50 52, 90 60, 90 10, 10 10, 10 90, 60 90, 50 55, 40 80, 20 60, 50 52))"
+        );
+}
+
 } // namespace tut

commit bb2db233eb2d51ba1f44cfaaf5dbd39f0cb1af87
Author: Mike Taves <mwtoews at gmail.com>
Date:   Mon Nov 6 15:21:09 2023 +1300

    CI: workaround for https://github.com/actions/runner-images/issues/8659

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b452533d7..57fb4d0ce 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -195,6 +195,15 @@ jobs:
     runs-on: ${{ matrix.ci.os }}
     steps:
 
+    # Work around https://github.com/actions/runner-images/issues/8659
+    - name: 'Remove GCC 13 from runner image (workaround)'
+      if: matrix.ci.os == 'ubuntu-22.04' && matrix.ci.cxx_compiler == 'clang++-14'
+      shell: bash
+      run: |
+        sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
+        sudo apt-get update
+        sudo apt-get install -y --allow-downgrades libc6=2.35-0ubuntu3.4 libc6-dev=2.35-0ubuntu3.4 libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
+
     - name: 'Install'
       run: |
         set -e

commit f89a64b8037f05d0838b5d0ec518ffe57d782637
Author: Mike Taves <mwtoews at gmail.com>
Date:   Mon Nov 6 13:57:13 2023 +1300

    Allow geosop to accept  isWKTLiteral with (e.g.) "point empty"

diff --git a/util/geosop/GeosOp.cpp b/util/geosop/GeosOp.cpp
index dda2c2e9e..4e50cdc7d 100644
--- a/util/geosop/GeosOp.cpp
+++ b/util/geosop/GeosOp.cpp
@@ -214,7 +214,12 @@ std::vector<std::unique_ptr<Geometry>> collect( std::vector<std::unique_ptr<Geom
 
 bool isWKTLiteral(std::string s) {
     // check for empty geoms (which do not have parens)
-    if (endsWith(s, " EMPTY")) return true;
+    const size_t slen = s.size();
+    if (slen < 6) return false;
+    auto lastWord = s.substr(slen - 6, slen);
+    for (char& c : lastWord)
+        c = static_cast<char>(toupper(static_cast<unsigned char>(c)));
+    if ( lastWord.compare(" EMPTY") == 0 ) return true;
 
     // assume if string contains a ( it is WKT
     auto numLParen = std::count(s.begin(), s.end(), '(');

commit 13c25a877704327db9f4f0dfd9c41d527dc8f46d
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Thu Nov 2 14:45:36 2023 -0700

    Skip over testing empty distances for mixed collections. Closes #979

diff --git a/src/operation/distance/DistanceOp.cpp b/src/operation/distance/DistanceOp.cpp
index 2f3cb6663..952c68d96 100644
--- a/src/operation/distance/DistanceOp.cpp
+++ b/src/operation/distance/DistanceOp.cpp
@@ -358,6 +358,10 @@ DistanceOp::computeMinDistanceLines(
 {
     for(const LineString* line0 : lines0) {
         for(const LineString* line1 : lines1) {
+
+            if (line0->isEmpty() || line1->isEmpty())
+                continue;
+
             computeMinDistance(line0, line1, locGeom);
             if(minDistance <= terminateDistance) {
                 return;
@@ -374,13 +378,11 @@ DistanceOp::computeMinDistancePoints(
     std::array<std::unique_ptr<GeometryLocation>, 2> & locGeom)
 {
     for(const Point* pt0 : points0) {
-        if (pt0->isEmpty()) {
-            continue;
-        }
         for(const Point* pt1 : points1) {
-            if (pt1->isEmpty()) {
+
+            if (pt1->isEmpty() || pt0->isEmpty())
                 continue;
-            }
+
             double dist = pt0->getCoordinate()->distance(*(pt1->getCoordinate()));
 
 #if GEOS_DEBUG
@@ -414,6 +416,10 @@ DistanceOp::computeMinDistanceLinesPoints(
 {
     for(const LineString* line : lines) {
         for(const Point* pt : points) {
+
+            if (line->isEmpty() || pt->isEmpty())
+                continue;
+
             computeMinDistance(line, pt, locGeom);
             if(minDistance <= terminateDistance) {
                 return;
diff --git a/tests/unit/operation/distance/DistanceOpTest.cpp b/tests/unit/operation/distance/DistanceOpTest.cpp
index 9b862e377..9df481a39 100644
--- a/tests/unit/operation/distance/DistanceOpTest.cpp
+++ b/tests/unit/operation/distance/DistanceOpTest.cpp
@@ -565,6 +565,11 @@ void object::test<21>()
     ensure_equals(g1->distance(g2.get()), 1.9996999774966246);
 }
 
+//
+// Variations on a theme: testing EMPTY and collections with EMPTY
+//
+
+// Ignoring empty component
 template<>
 template<>
 void object::test<22>()
@@ -576,6 +581,69 @@ void object::test<22>()
     ensure_equals(g2->distance(g1.get()), 1);
 }
 
+// Empty is same as empty so zero...?
+template<>
+template<>
+void object::test<23>()
+{
+    auto g1 = wktreader.read("POINT EMPTY");
+    auto g2 = wktreader.read("LINESTRING EMPTY");
+
+    ensure(g1 != nullptr && g2 != nullptr);
+    ensure_equals(g1->distance(g2.get()), 0);
+    ensure_equals(g2->distance(g1.get()), 0);
+}
+
+template<>
+template<>
+void object::test<24>()
+{
+    auto g1 = wktreader.read("GEOMETRYCOLLECTION(POINT EMPTY, LINESTRING EMPTY)");
+    auto g2 = wktreader.read("LINESTRING EMPTY");
+
+    ensure(g1 != nullptr && g2 != nullptr);
+    ensure_equals(g1->distance(g2.get()), 0);
+    ensure_equals(g2->distance(g1.get()), 0);
+}
+
+// But ignore empty if there's a real distance?
+template<>
+template<>
+void object::test<25>()
+{
+    auto g1 = wktreader.read("GEOMETRYCOLLECTION(LINESTRING EMPTY, POINT(2 1))");
+    auto g2 = wktreader.read("POINT(1 1)");
+
+    ensure(g1 != nullptr && g2 != nullptr);
+    ensure_equals(g1->distance(g2.get()), 1);
+    ensure_equals(g2->distance(g1.get()), 1);
+}
+
+template<>
+template<>
+void object::test<26>()
+{
+    auto g1 = wktreader.read("GEOMETRYCOLLECTION(POINT(-2 0), POINT EMPTY)");
+    auto g2 = wktreader.read("GEOMETRYCOLLECTION(POINT(1 0),LINESTRING(0 0,1 0))");
+
+    ensure(g1 != nullptr && g2 != nullptr);
+    ensure_equals(g1->distance(g2.get()), 2);
+    ensure_equals(g2->distance(g1.get()), 2);
+}
+
+template<>
+template<>
+void object::test<27>()
+{
+    auto g1 = wktreader.read("GEOMETRYCOLLECTION(POINT EMPTY)");
+    auto g2 = wktreader.read("GEOMETRYCOLLECTION(POINT(1 0))");
+
+    ensure(g1 != nullptr && g2 != nullptr);
+    ensure_equals(g1->distance(g2.get()), 0);
+    ensure_equals(g2->distance(g1.get()), 0);
+}
+
+
 // TODO: finish the tests by adding:
 // 	LINESTRING - *all*
 // 	MULTILINESTRING - *all*
diff --git a/tests/xmltester/tests/general/TestDistance.xml b/tests/xmltester/tests/general/TestDistance.xml
index 4ce6d0fed..3835c919d 100644
--- a/tests/xmltester/tests/general/TestDistance.xml
+++ b/tests/xmltester/tests/general/TestDistance.xml
@@ -6,6 +6,7 @@
   <a>    POINT(10 10)  </a>
   <b>    POINT EMPTY  </b>
 <test><op name="distance" arg1="A" arg2="B">    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    0.0   </op></test>
 </case>
 
 <case>
@@ -13,6 +14,31 @@
   <a>    POINT(10 10)  </a>
   <b>    POINT (10 0)  </b>
 <test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>PP - point to multipoint</desc>
+  <a>    POINT(10 10)  </a>
+  <b>    MULTIPOINT ((10 0), (30 30))  </b>
+<test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>PP - point to multipoint with empty element</desc>
+  <a>    POINT(10 10)  </a>
+  <b>    MULTIPOINT ((10 0), EMPTY)  </b>
+<test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>LL - line to empty line</desc>
+  <a>    LINESTRING (0 0, 0 10)  </a>
+  <b>    LINESTRING EMPTY  </b>
+<test><op name="distance" arg1="A" arg2="B">    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    0.0   </op></test>
 </case>
 
 <case>
@@ -20,6 +46,31 @@
   <a>    LINESTRING (0 0, 0 10)  </a>
   <b>    LINESTRING (10 0, 10 10)  </b>
 <test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>LL - line to multiline</desc>
+  <a>    LINESTRING (0 0, 0 10)  </a>
+  <b>    MULTILINESTRING ((10 0, 10 10), (50 50, 60 60))  </b>
+<test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>LL - line to multiline with empty element</desc>
+  <a>    LINESTRING (0 0, 0 10)  </a>
+  <b>    MULTILINESTRING ((10 0, 10 10), EMPTY)  </b>
+<test><op name="distance" arg1="A" arg2="B">    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A">    10.0   </op></test>
+</case>
+
+<case>
+  <desc>PA - point to empty polygon</desc>
+  <a>    POINT (240 160)  </a>
+  <b>    POLYGON EMPTY  </b>
+<test><op name="distance" arg1="A" arg2="B" >    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    0.0   </op></test>
 </case>
 
 <case>
@@ -27,6 +78,7 @@
   <a>    POINT (240 160)  </a>
   <b>    POLYGON ((100 260, 340 180, 100 60, 180 160, 100 260))  </b>
 <test><op name="distance" arg1="A" arg2="B" >    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    0.0   </op></test>
 </case>
 
 <case>
@@ -34,6 +86,7 @@
   <a>    LINESTRING (40 300, 280 220, 60 160, 140 60)  </a>
   <b>    LINESTRING (140 360, 260 280, 240 120, 120 160)  </b>
 <test><op name="distance" arg1="A" arg2="B" >    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    0.0   </op></test>
 </case>
 
 <case>
@@ -41,6 +94,7 @@
   <a>    POLYGON ((60 260, 260 180, 100 60, 60 160, 60 260))  </a>
   <b>    POLYGON ((220 280, 120 160, 300 60, 360 220, 220 280))  </b>
 <test><op name="distance" arg1="A" arg2="B" >    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    0.0   </op></test>
 </case>
 
 <case>
@@ -48,6 +102,7 @@
   <a>    POLYGON ((100 320, 60 120, 240 180, 200 260, 100 320))  </a>
   <b>    POLYGON ((420 320, 280 260, 400 100, 420 320))  </b>
 <test><op name="distance" arg1="A" arg2="B" >   71.55417527999327  </op></test>
+<test><op name="distance" arg1="B" arg2="A" >   71.55417527999327  </op></test>
 </case>
 
 <case>
@@ -55,13 +110,23 @@
   <a>    MULTIPOLYGON (((40 240, 160 320, 40 380, 40 240)),   ((100 240, 240 60, 40 40, 100 240)))  </a>
   <b>    MULTIPOLYGON (((220 280, 120 160, 300 60, 360 220, 220 280)),   ((240 380, 280 300, 420 340, 240 380)))  </b>
 <test><op name="distance" arg1="A" arg2="B" >    0.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    0.0   </op></test>
 </case>
 
 <case>
-  <desc>mAmA - multipolygon with empty component</desc>
+  <desc>mAmA - multipolygon with empty element</desc>
   <a> MULTIPOLYGON (EMPTY, ((98 200, 200 200, 200 99, 98 99, 98 200))) </a>
   <b> POLYGON ((300 200, 400 200, 400 100, 300 100, 300 200)) </b>
 <test><op name="distance" arg1="A" arg2="B" >    100.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    100.0   </op></test>
+</case>
+
+<case>
+  <desc>GCGC - geometry collections with mixed dimensions</desc>
+  <a> GEOMETRYCOLLECTION (LINESTRING (10 10, 50 10), POINT (90 10)) </a>
+  <b> GEOMETRYCOLLECTION (POLYGON ((90 20, 60 20, 60 50, 90 50, 90 20)), LINESTRING (10 50, 30 70)) </b>
+<test><op name="distance" arg1="A" arg2="B" >    10.0   </op></test>
+<test><op name="distance" arg1="B" arg2="A" >    10.0   </op></test>
 </case>
 
 </run>

commit a7a12f54371ddefee4e468ef89677ba9305719b1
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Tue Oct 31 14:30:57 2023 -0700

    Add test of prepared geometry against mixed collections

diff --git a/tests/unit/geom/prep/PreparedGeometryTest.cpp b/tests/unit/geom/prep/PreparedGeometryTest.cpp
index efc5c4b82..ffdd90faf 100644
--- a/tests/unit/geom/prep/PreparedGeometryTest.cpp
+++ b/tests/unit/geom/prep/PreparedGeometryTest.cpp
@@ -68,4 +68,21 @@ void object::test<1>
     ensure( pg1->covers(g2.get()));
 }
 
+
+// See https://trac.osgeo.org/postgis/ticket/5601
+template<>
+template<>
+void object::test<2>
+()
+{
+    g1 = reader.read( "LINESTRING(-1 0,0 0)" );
+    g2 = reader.read( "GEOMETRYCOLLECTION(MULTIPOINT(-1 0),LINESTRING(0 -1,1 0))" );
+
+    pg1 = prep::PreparedGeometryFactory::prepare(g1.get());
+
+    ensure(  g1->intersects(g2.get()) );
+    ensure( pg1->intersects(g2.get()) );
+}
+
+
 } // namespace tut

commit d7a5a51cb762675f3d068a37d02a31142fd6f822
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Sun Oct 29 14:40:00 2023 -0700

    Add GEOSOrientPolygons version number doc

diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index e108784b8..d5dd9c425 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -2949,6 +2949,7 @@ extern int GEOS_DLL GEOSNormalize(GEOSGeometry* g);
 * \param exteriorCW if 1, exterior rings will be clockwise and interior rings
 *                         will be counter-clockwise
 * \return 0 on success or -1 on exception
+* \since 3.12
 */
 extern int GEOS_DLL GEOSOrientPolygons(GEOSGeometry* g,
                                        int exteriorCW);

commit e068d35c47f785e5141c7d6a8f23a4d43846fff8
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Sun Oct 29 14:27:26 2023 -0700

    Fix GEOSGeom_releaseCollection version doc

diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index faef97494..e108784b8 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -2484,7 +2484,7 @@ extern GEOSGeometry GEOS_DLL *GEOSGeom_createCollection(
 *       with GEOSFree() and all the elements with GEOSGeom_destroy().
 *       If called with an empty collection, null will be returned
 *       and ngeoms set to zero.
-* \since 3.4
+* \since 3.12
 */
 extern GEOSGeometry GEOS_DLL ** GEOSGeom_releaseCollection(
     GEOSGeometry * collection,

commit c7ff046189466411ebcd43e81a39b2a78db517ca
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Sun Oct 29 08:28:03 2023 -0700

    Update NEWS

diff --git a/NEWS.md b/NEWS.md
index 930a81d94..77a2b7fc7 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -17,6 +17,7 @@
   - Improve scale handling for PrecisionModel (GH-956, Martin Davis)
   - Fix error in CoordinateSequence::add when disallowing repeated points (GH-963, Dan Baston)
   - Fix WKTWriter::writeTrimmedNumber for big and small values (GH-973, Mike Taves)
+  - Fix InteriorPointPoint to handle empty elements (GH-977, Martin Davis)
 
 
 ## Changes in 3.12.0

commit 7d8455ce42677411c3d4ee950928506eb65745e1
Author: Martin Davis <mtnclimb at gmail.com>
Date:   Sun Oct 29 08:26:44 2023 -0700

    Fix InteriorPointPoint to handle empty elements (#977)

diff --git a/src/algorithm/InteriorPointPoint.cpp b/src/algorithm/InteriorPointPoint.cpp
index 6fb48f397..9070aa670 100644
--- a/src/algorithm/InteriorPointPoint.cpp
+++ b/src/algorithm/InteriorPointPoint.cpp
@@ -47,6 +47,9 @@ InteriorPointPoint::InteriorPointPoint(const Geometry* g)
 void
 InteriorPointPoint::add(const Geometry* geom)
 {
+    if (geom->isEmpty())
+        return;
+
     const Point* po = dynamic_cast<const Point*>(geom);
     if (po) {
         add(po->getCoordinate());
diff --git a/tests/xmltester/tests/general/TestInteriorPoint.xml b/tests/xmltester/tests/general/TestInteriorPoint.xml
index 0d765caca..23b4a8705 100644
--- a/tests/xmltester/tests/general/TestInteriorPoint.xml
+++ b/tests/xmltester/tests/general/TestInteriorPoint.xml
@@ -14,12 +14,19 @@
 </case>
 
 <case>
-  <desc>P - single point</desc>
+  <desc>P - multipoint</desc>
   <a>    MULTIPOINT ((60 300), (200 200), (240 240), (200 300), (40 140), (80 240), (140 240), (100 160), (140 200), (60 200))
 	</a>
 <test><op name="getInteriorPoint" arg1="A" >    POINT (140 240)   </op></test>
 </case>
 
+<case>
+  <desc>P - multipoint with EMPTY</desc>
+  <a>    MULTIPOINT((0 0), EMPTY)
+	</a>
+<test><op name="getInteriorPoint" arg1="A" >    POINT (0 0)   </op></test>
+</case>
+
 <case>
   <desc>L - linestring with single segment</desc>
   <a>    LINESTRING (0 0, 7 14)

commit 0dc1901601fd226036e1b898a7ae026f2067a71a
Author: Mike Taves <mwtoews at gmail.com>
Date:   Tue Oct 24 11:04:32 2023 +1300

    Refactor WKTWriter::writeTrimmedNumber for big and small values (#973)

diff --git a/NEWS.md b/NEWS.md
index a1502586a..930a81d94 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -16,6 +16,7 @@
   - Fix PreparedLineStringDistance for lines within envelope and polygons (GH-959, Martin Davis)
   - Improve scale handling for PrecisionModel (GH-956, Martin Davis)
   - Fix error in CoordinateSequence::add when disallowing repeated points (GH-963, Dan Baston)
+  - Fix WKTWriter::writeTrimmedNumber for big and small values (GH-973, Mike Taves)
 
 
 ## Changes in 3.12.0
diff --git a/capi/geos_c.h.in b/capi/geos_c.h.in
index 37502912c..faef97494 100644
--- a/capi/geos_c.h.in
+++ b/capi/geos_c.h.in
@@ -1898,11 +1898,13 @@ extern void GEOS_DLL GEOSWKTWriter_setOld3D_r(
     GEOSWKTWriter *writer,
     int useOld3D);
 
-/** Print the shortest representation of a double using fixed notation.
- * Only works for numbers smaller than 1e17 (absolute value).
+/** Print the shortest representation of a double. Non-zero absolute values
+ * that are <1e-4 and >=1e+17 are formatted using scientific notation, and
+ * other values are formatted with positional notation with precision used for
+ * the max digits after decimal point.
  * \param d The number to format.
- * \param precision The desired precision. (max digits after decimal point)
- * \param result The buffer to write the result to.
+ * \param precision The desired precision.
+ * \param result The buffer to write the result to, with a suggested size 28.
  * \return the length of the written string.
  */
 extern int GEOS_DLL GEOS_printDouble(
@@ -5436,7 +5438,10 @@ extern char GEOS_DLL *GEOSWKTWriter_write(
 /**
 * Sets the number trimming option on a \ref GEOSWKTWriter.
 * With trim set to 1, the writer will strip trailing 0's from
-* the output coordinates. With 0, all coordinates will be
+* the output coordinates. With 1 (trimming enabled), big and small
+* absolute coordinates will use scientific notation, otherwise
+* positional notation is used; see \ref GEOS_printDouble for details.
+* With 0 (trimming disabled), all coordinates will be
 * padded with 0's out to the rounding precision.
 * Default since GEOS 3.12 is with trim set to 1 for 'on'.
 * \param writer A \ref GEOSWKTWriter.
diff --git a/src/io/WKTWriter.cpp b/src/io/WKTWriter.cpp
index 1471d669d..e4f62573f 100644
--- a/src/io/WKTWriter.cpp
+++ b/src/io/WKTWriter.cpp
@@ -407,7 +407,21 @@ WKTWriter::appendSequenceText(const CoordinateSequence& seq,
 int 
 WKTWriter::writeTrimmedNumber(double d, uint32_t precision, char* buf)
 {
-    return geos_d2sfixed_buffered_n(d, precision, buf);
+    const auto da = std::fabs(d);
+    if ( !std::isfinite(d) || (da == 0.0) )
+        // non-finite or exactly zero
+        return geos_d2sfixed_buffered_n(d, precision, buf);
+    else if ( (da >= 1e+17) || (da < 1e-4) )
+        // very large or small numbers, use scientific notation
+        return geos_d2sexp_buffered_n(d, precision, buf);
+    else {
+        // most real-world coordinates, use positional notation
+        if ( (precision < 4) && (da < 1.0) ) {
+            // adjust precision to avoid rounding to zero
+            precision = static_cast<std::uint32_t>(-floor(log10(da)));
+        }
+        return geos_d2sfixed_buffered_n(d, precision, buf);
+    }
 }
 
 std::string
@@ -417,7 +431,7 @@ WKTWriter::writeNumber(double d, bool trim, uint32_t precision) {
     * the ryu library.
     */
     if (trim) {
-        char buf[128];
+        char buf[28];
         int len = writeTrimmedNumber(d, precision, buf);
         buf[len] = '\0';
         std::string s(buf);
diff --git a/tests/unit/capi/GEOS_printDoubleTest.cpp b/tests/unit/capi/GEOS_printDoubleTest.cpp
new file mode 100644
index 000000000..69cd61562
--- /dev/null
+++ b/tests/unit/capi/GEOS_printDoubleTest.cpp
@@ -0,0 +1,84 @@
+#include <tut/tut.hpp>
+// geos
+#include <geos_c.h>
+
+#include "capi_test_utils.h"
+
+namespace tut {
+//
+// Test Group
+//
+
+struct test_geos_printdouble_data : public capitest::utility {};
+
+typedef test_group<test_geos_printdouble_data> group;
+typedef group::object object;
+
+group test_geos_printdouble("capi::GEOS_printDouble");
+
+template<>
+template<>
+void object::test<1>()
+{
+    struct TESTCASE {
+        TESTCASE(unsigned int p_p, double p_d, std::string p_expected)
+            : p(p_p), d(p_d), expected(p_expected) {}
+        unsigned int p;
+        double d;
+        std::string expected;
+    };
+    std::vector<TESTCASE> testcase_l{
+        TESTCASE(1, 0.0, "0"),
+        TESTCASE(1, std::nan("0"), "NaN"),
+        TESTCASE(1, std::numeric_limits<double>::infinity(), "Infinity"),
+        TESTCASE(1, -std::numeric_limits<double>::infinity(), "-Infinity"),
+        TESTCASE(16, 1.0, "1"),
+        TESTCASE(16, 1.2e+234, "1.2e+234"),
+        TESTCASE(2, -1.2e+234, "-1.2e+234"),
+        TESTCASE(16, 1.2e-234, "1.2e-234"),
+        TESTCASE(2, -1.2e-234, "-1.2e-234"),
+        TESTCASE(2, 1.1e-5, "1.1e-5"),
+        TESTCASE(0, 1e-4, "0.0001"),
+        TESTCASE(1, 1e-4, "0.0001"),
+        TESTCASE(2, 1e-4, "0.0001"),
+        TESTCASE(3, 1e-4, "0.0001"),
+        TESTCASE(4, 1e-4, "0.0001"),
+        TESTCASE(5, 1e-4, "0.0001"),
+        TESTCASE(0, 5.6e-4, "0.0006"),
+        TESTCASE(1, 5.6e-4, "0.0006"),
+        TESTCASE(2, 5.6e-4, "0.0006"),
+        TESTCASE(3, 5.6e-4, "0.0006"),
+        TESTCASE(4, 5.6e-4, "0.0006"),
+        TESTCASE(5, 5.6e-4, "0.00056"),
+        TESTCASE(0, 1.2345678901234e+15, "1234567890123400"),
+        TESTCASE(1, 1.2345678901234e+15, "1234567890123400"),
+        TESTCASE(0, 1.2345678901234e+16, "12345678901234000"),
+        TESTCASE(1, 1.2345678901234e+16, "12345678901234000"),
+        TESTCASE(0, 1.2345678901234e+17, "1e+17"),
+        TESTCASE(1, 1.2345678901234e+17, "1.2e+17"),
+        TESTCASE(2, 1.2345678901234e+17, "1.23e+17"),
+        TESTCASE(3, 1.2345678901234e+17, "1.235e+17"),
+        TESTCASE(4, 1.2345678901234e+17, "1.2346e+17"),
+        TESTCASE(5, 1.2345678901234e+17, "1.23457e+17"),
+        TESTCASE(6, 1.2345678901234e+17, "1.234568e+17"),
+        TESTCASE(7, 1.2345678901234e+17, "1.2345679e+17"),
+        TESTCASE(8, 1.2345678901234e+17, "1.23456789e+17"),
+        TESTCASE(9, 1.2345678901234e+17, "1.23456789e+17"),
+        TESTCASE(10, 1.2345678901234e+17, "1.2345678901e+17"),
+        TESTCASE(11, 1.2345678901234e+17, "1.23456789012e+17"),
+        TESTCASE(12, 1.2345678901234e+17, "1.234567890123e+17"),
+        TESTCASE(13, 1.2345678901234e+17, "1.2345678901234e+17"),
+        TESTCASE(14, 1.2345678901234e+17, "1.2345678901234e+17"),
+    };
+    for (const auto& testcase : testcase_l) {
+        char buf[28];
+        const auto len = GEOS_printDouble(testcase.d, testcase.p, buf);
+        buf[len] = '\0';
+        const auto res_str = std::string(buf);
+        ensure_equals(res_str, testcase.expected);
+        ensure_equals(len, static_cast<int>(testcase.expected.size()));
+    }
+
+}
+
+} // namespace tut
diff --git a/tests/unit/io/WKTWriterTest.cpp b/tests/unit/io/WKTWriterTest.cpp
index 19809825f..7dbb7379d 100644
--- a/tests/unit/io/WKTWriterTest.cpp
+++ b/tests/unit/io/WKTWriterTest.cpp
@@ -464,4 +464,115 @@ void object::test<15>
 
 }
 
+// Test big, small, and non-finite values
+// https://github.com/libgeos/geos/issues/970
+template<>
+template<>
+void object::test<16>
+()
+{
+    PrecisionModel pmf(PrecisionModel::FLOATING);
+    GeometryFactory::Ptr gff(GeometryFactory::create(&pmf));
+    WKTReader wktreaderf(gff.get());
+
+    // Big values
+    auto big = wktreaderf.read("POINT (-1.234e+15 1.234e+16 1.234e+17 -1.234e+18)");
+
+    // Check precision from 0 to 5
+    wktwriter.setRoundingPrecision(0);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1e+17 -1e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 123400000000000000 -1234000000000000000)");
+
+    wktwriter.setRoundingPrecision(1);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1.2e+17 -1.2e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000.0 12340000000000000.0 123400000000000000.0 -1234000000000000000.0)");
+
+    wktwriter.setRoundingPrecision(2);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1.23e+17 -1.23e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000.00 12340000000000000.00 123400000000000000.00 -1234000000000000000.00)");
+
+    wktwriter.setRoundingPrecision(3);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1.234e+17 -1.234e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000.000 12340000000000000.000 123400000000000000.000 -1234000000000000000.000)");
+
+    wktwriter.setRoundingPrecision(4);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1.234e+17 -1.234e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000.0000 12340000000000000.0000 123400000000000000.0000 -1234000000000000000.0000)");
+
+    wktwriter.setRoundingPrecision(5);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000 12340000000000000 1.234e+17 -1.234e+18)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*big), "POINT ZM (-1234000000000000.00000 12340000000000000.00000 123400000000000000.00000 -1234000000000000000.00000)");
+
+    // Small values
+    auto small = wktreaderf.read("POINT (-1.234e-3 2.234e-4 1.234e-5 -1.234e-6)");
+
+    // Check precision from 0 to 5
+    wktwriter.setRoundingPrecision(0);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.001 0.0002 1e-5 -1e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0 0 0 -0)");
+
+    wktwriter.setRoundingPrecision(1);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.001 0.0002 1.2e-5 -1.2e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.0 0.0 0.0 -0.0)");
+
+    wktwriter.setRoundingPrecision(2);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.001 0.0002 1.23e-5 -1.23e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.00 0.00 0.00 -0.00)");
+
+    wktwriter.setRoundingPrecision(3);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.001 0.0002 1.234e-5 -1.234e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.001 0.000 0.000 -0.000)");
+
+    wktwriter.setRoundingPrecision(4);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.0012 0.0002 1.234e-5 -1.234e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.0012 0.0002 0.0000 -0.0000)");
+
+    wktwriter.setRoundingPrecision(5);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.00123 0.00022 1.234e-5 -1.234e-6)");
+    wktwriter.setTrim(false);
+    ensure_equals(wktwriter.write(*small), "POINT ZM (-0.00123 0.00022 0.00001 -0.00000)");
+
+    // Extremely small and big
+    auto extreme = wktreaderf.read("POINT (-1.2e-208 9.1e-191 3.8e+221 4.9e+154)");
+    wktwriter.setRoundingPrecision(5);
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*extreme), "POINT ZM (-1.2e-208 9.1e-191 3.8e+221 4.9e+154)");
+    // Skip non-trim, as this may vary between compilers
+    // wktwriter.setTrim(false);
+    // ensure_equals(wktwriter.write(*extreme), "POINT ZM (-0.00000 0.00000 ...)");
+
+    // Non-finite values
+    auto nonfinite = wktreaderf.read("POINT(-inf inf nan)");
+
+    wktwriter.setTrim(true);
+    ensure_equals(wktwriter.write(*nonfinite), "POINT Z (-Infinity Infinity NaN)");
+    // Skip non-trim, as this may vary between compilers
+    // wktwriter.setTrim(false);
+    // ensure_equals(wktwriter.write(*nonfinite), "POINT Z (-inf inf nan)");
+
+}
+
 } // namespace tut

commit 28d70a2e4582edcd3625659f70e31d1853423875
Author: Dan Baston <dbaston at gmail.com>
Date:   Tue Oct 3 12:21:03 2023 -0400

    CI: Add gcc 13 build (#969)

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bce5791d0..b452533d7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -105,6 +105,15 @@ jobs:
           cmake: 3.22.*
           os: ubuntu-22.04
 
+        - cxx_compiler: g++-13
+          c_compiler: gcc-13
+          build_type: Release
+          cxxstd: 17
+          arch: 64
+          packages: 'g++-13-multilib gcc-13-multilib'
+          cmake: 3.22.*
+          os: ubuntu-22.04
+
           # clang 6 and lower are not supported
           # in ubuntu 20.04 and higher
 

-----------------------------------------------------------------------

Summary of changes:
 .github/workflows/ci.yml                           |  18 ++
 HOWTO_RELEASE                                      |  34 +-
 NEWS.md                                            |   5 +-
 capi/geos_c.h.in                                   |  18 +-
 include/geos/algorithm/Angle.h                     |  21 ++
 include/geos/simplify/TaggedLineStringSimplifier.h |  18 +-
 src/algorithm/Angle.cpp                            |   2 +-
 src/algorithm/InteriorPointPoint.cpp               |   3 +
 src/io/WKTWriter.cpp                               |  18 +-
 src/operation/buffer/BufferParameters.cpp          |   3 +-
 src/operation/buffer/OffsetSegmentGenerator.cpp    |  34 +-
 src/operation/distance/DistanceOp.cpp              |  16 +-
 src/simplify/TaggedLineStringSimplifier.cpp        |  27 +-
 src/util/GeometricShapeFactory.cpp                 |  31 +-
 tests/unit/algorithm/AngleTest.cpp                 |  39 +++
 tests/unit/capi/GEOSBufferTest.cpp                 |   8 +-
 tests/unit/capi/GEOS_printDoubleTest.cpp           |  84 +++++
 tests/unit/geom/prep/PreparedGeometryTest.cpp      |  17 +
 tests/unit/io/WKTWriterTest.cpp                    | 111 +++++++
 tests/unit/operation/buffer/BufferOpTest.cpp       |  19 +-
 tests/unit/operation/distance/DistanceOpTest.cpp   |  68 ++++
 .../unit/simplify/DouglasPeuckerSimplifierTest.cpp | 344 +++++++--------------
 .../simplify/TopologyPreservingSimplifierTest.cpp  | 309 ++++++------------
 tests/xmltester/tests/general/TestDistance.xml     |  67 +++-
 .../xmltester/tests/general/TestInteriorPoint.xml  |   9 +-
 util/geosop/GeosOp.cpp                             |   7 +-
 26 files changed, 791 insertions(+), 539 deletions(-)
 create mode 100644 tests/unit/capi/GEOS_printDoubleTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list