[geos-commits] [SCM] GEOS branch main updated. e151d238c16578b5474024526632382ecd10ecf2

git at osgeo.org git at osgeo.org
Tue Jul 6 11:16:26 PDT 2021


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  e151d238c16578b5474024526632382ecd10ecf2 (commit)
      from  8626422ff0a09703bf4207bb3c6c4ad0768e7a3a (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 e151d238c16578b5474024526632382ecd10ecf2
Author: Paul Ramsey <pramsey at cleverelephant.ca>
Date:   Tue Jul 6 11:14:48 2021 -0700

    IsValid improvements,
    Port https://github.com/locationtech/jts/pull/743, Improve IsValidOp design and performance
    Port https://github.com/locationtech/jts/pull/755, Add IndexedNestedPolygonTester to IsValidOp

diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml
index 3c43a97..7cfddbc 100644
--- a/.azure-pipelines.yml
+++ b/.azure-pipelines.yml
@@ -47,59 +47,44 @@ stages:
           CXXSTD: 11
           CXX: g++-5
           PACKAGES: g++-5
-        GCC 4.9:
-          CXXSTD: 11
-          CXX: g++-4.9
-          PACKAGES: g++-4.9
+        # GCC 4.9:
+        #   CXXSTD: 11
+        #   CXX: g++-4.9
+        #   PACKAGES: g++-4.9
         GCC 4.8:
           CXXSTD: 11
           CXX: g++-4.8
           PACKAGES: g++-4.8
-        Clang 8:
+        Clang 10:
           CXXSTD: 11, 14, 17, 20
+          CXX: clang++-10
+          PACKAGES: clang-10
+          LLVM_REPO: llvm-toolchain-bionic-10
+        Clang 9:
+          CXXSTD: 11, 14, 17, 20
+          CXX: clang++-9
+          PACKAGES: clang-9
+          LLVM_REPO: llvm-toolchain-bionic-9
+        Clang 8:
+          CXXSTD: 11, 14, 17
           CXX: clang++-8
           PACKAGES: clang-8
           LLVM_REPO: llvm-toolchain-bionic-8
         Clang 7:
-          CXXSTD: 14, 17, 20
+          CXXSTD: 11, 14
           CXX: clang++-7
           PACKAGES: clang-7
           LLVM_REPO: llvm-toolchain-bionic-7
         Clang 6:
-          CXXSTD: 14, 17, 20
+          CXXSTD: 11, 14
           CXX: clang++-6.0
           PACKAGES: clang-6.0
           LLVM_REPO: llvm-toolchain-bionic-6.0
         Clang 5:
-          CXXSTD: 11, 14, 17
+          CXXSTD: 11, 14
           PACKAGES: clang-5.0
           CXX: clang++-5.0
           LLVM_REPO: llvm-toolchain-bionic-5.0
-        Clang 4:
-          CXXSTD: 11, 14
-          CXX: clang++-4.0
-          PACKAGES: clang-4.0
-          LLVM_REPO: llvm-toolchain-bionic-4.0
-        Clang 3.9:
-          CXXSTD: 11, 14
-          CXX: clang++-3.9
-          PACKAGES: clang-3.9
-        Clang 3.8:
-          CXX: clang++-3.8
-          CXXSTD: 11, 14
-          PACKAGES: clang-3.8
-        Clang 3.7:
-          CXXSTD: 11
-          CXX: clang++-3.7
-          PACKAGES: clang-3.7
-        Clang 3.6:
-          CXXSTD: 11
-          CXX: clang++-3.6
-          PACKAGES: clang-3.6
-        Clang 3.5:
-          CXXSTD: 11
-          CXX: clang++-3.5
-          PACKAGES: clang-3.5
     steps:
     - script: |
         set -e
@@ -107,7 +92,7 @@ stages:
         sudo -E apt-add-repository -y "ppa:ubuntu-toolchain-r/test"
         if test -n "${LLVM_REPO}" ; then
           wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
-          sudo -E apt-add-repository "deb http://apt.llvm.org/xenial/ ${LLVM_REPO} main"
+          sudo -E apt-add-repository "deb http://apt.llvm.org/bionic/ ${LLVM_REPO} main"
         fi
         sudo -E apt-get update
         sudo -E apt-get -yq --no-install-suggests --no-install-recommends install cmake ${PACKAGES}
@@ -174,10 +159,10 @@ stages:
       vmImage: $(VM_IMAGE)
     steps:
     - powershell: |
-        Write-Host "Installing CMake 3.14.4"
-        Invoke-WebRequest https://cmake.org/files/v3.14/cmake-3.14.4-win64-x64.zip -OutFile C:\cmake-3.14.4-win64-x64.zip
-        Expand-Archive C:\cmake-3.14.4-win64-x64.zip -DestinationPath C:\
-        Rename-Item -Path C:\cmake-3.14.4-win64-x64 -NewName C:\cmake
+        Write-Host "Installing CMake 3.14.7"
+        Invoke-WebRequest https://cmake.org/files/v3.14/cmake-3.14.7-win64-x64.zip -OutFile C:\cmake-3.14.7-win64-x64.zip
+        Expand-Archive C:\cmake-3.14.7-win64-x64.zip -DestinationPath C:\
+        Rename-Item -Path C:\cmake-3.14.7-win64-x64 -NewName C:\cmake
         Write-Host "##vso[task.prependpath]C:\cmake\bin"
       displayName: 'Install'
     - script: |
@@ -261,7 +246,7 @@ stages:
 
   - job: Code_quality_checks
     pool:
-      vmImage: 'ubuntu-18.04'
+      vmImage: 'ubuntu-latest'
     steps:
     - script: |
         set -e
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 494b8d6..fbedddb 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -157,7 +157,7 @@ jobs:
        ctest --output-on-failure .
 
   windows-msvc-19:
-    name: 'Windows (Visual Studio 2019, Release, windows-latest)'
+    name: 'Windows (Visual Studio 2019, Release, windows-2019)'
     runs-on: windows-2019
     steps:
     - name: 'Check Out'
@@ -177,7 +177,7 @@ jobs:
        ctest --output-on-failure -C Release
 
   windows-msvc-17:
-    name: 'Windows (Visual Studio 2017, Release, windows-latest)'
+    name: 'Windows (Visual Studio 2017, Release, windows-2016)'
     runs-on: windows-2016
     steps:
     - name: 'Check Out'
diff --git a/capi/geos_ts_c.cpp b/capi/geos_ts_c.cpp
index 14ce229..cd362e1 100644
--- a/capi/geos_ts_c.cpp
+++ b/capi/geos_ts_c.cpp
@@ -168,6 +168,7 @@ using geos::operation::geounion::CascadedPolygonUnion;
 using geos::operation::overlayng::OverlayNG;
 using geos::operation::overlayng::UnaryUnionNG;
 using geos::operation::overlayng::OverlayNGRobust;
+using geos::operation::valid::TopologyValidationError;
 
 using geos::precision::GeometryPrecisionReducer;
 
@@ -691,10 +692,9 @@ extern "C" {
             GEOSContextHandleInternal_t* handle = reinterpret_cast<GEOSContextHandleInternal_t*>(extHandle);
 
             using geos::operation::valid::IsValidOp;
-            using geos::operation::valid::TopologyValidationError;
 
             IsValidOp ivo(g1);
-            TopologyValidationError* err = ivo.getValidationError();
+            const TopologyValidationError* err = ivo.getValidationError();
 
             if(err) {
                 handle->NOTICE_MESSAGE("%s", err->toString().c_str());
@@ -711,13 +711,12 @@ extern "C" {
     {
         return execute(extHandle, [&]() {
             using geos::operation::valid::IsValidOp;
-            using geos::operation::valid::TopologyValidationError;
 
             char* result = nullptr;
             char const* const validstr = "Valid Geometry";
 
             IsValidOp ivo(g1);
-            TopologyValidationError* err = ivo.getValidationError();
+            const TopologyValidationError* err = ivo.getValidationError();
 
             if(err) {
                 std::ostringstream ss;
@@ -741,14 +740,13 @@ extern "C" {
                         int flags, char** reason, Geometry** location)
     {
         using geos::operation::valid::IsValidOp;
-        using geos::operation::valid::TopologyValidationError;
 
         return execute(extHandle, 2, [&]() {
             IsValidOp ivo(g);
             if(flags & GEOSVALID_ALLOW_SELFTOUCHING_RING_FORMING_HOLE) {
                 ivo.setSelfTouchingRingFormingHoleValid(true);
             }
-            TopologyValidationError* err = ivo.getValidationError();
+            const TopologyValidationError* err = ivo.getValidationError();
             if(err != nullptr) {
                 if(location) {
                     *location = g->getFactory()->createPoint(err->getCoordinate());
diff --git a/include/geos/algorithm/BoundaryNodeRule.h b/include/geos/algorithm/BoundaryNodeRule.h
index 6391388..e12a14e 100644
--- a/include/geos/algorithm/BoundaryNodeRule.h
+++ b/include/geos/algorithm/BoundaryNodeRule.h
@@ -45,7 +45,7 @@ namespace algorithm { // geos::algorithm
  * @version 1.7
  *
  * @see operation::relate::RelateOp
- * @see operation::IsSimpleOp
+ * @see operation::valid::IsSimpleOp
  * @see algorithm::PointLocator
  */
 class GEOS_DLL BoundaryNodeRule {
diff --git a/include/geos/noding/BasicSegmentString.h b/include/geos/noding/BasicSegmentString.h
index 39a371e..9045c82 100644
--- a/include/geos/noding/BasicSegmentString.h
+++ b/include/geos/noding/BasicSegmentString.h
@@ -92,6 +92,10 @@ private:
 
     geom::CoordinateSequence* pts;
 
+    // Declare type as noncopyable
+    BasicSegmentString(const BasicSegmentString& other) = delete;
+    BasicSegmentString& operator=(const BasicSegmentString& rhs) = delete;
+
 };
 
 } // namespace geos.noding
diff --git a/include/geos/operation/valid/IndexedNestedHoleTester.h b/include/geos/operation/valid/IndexedNestedHoleTester.h
new file mode 100644
index 0000000..9139c97
--- /dev/null
+++ b/include/geos/operation/valid/IndexedNestedHoleTester.h
@@ -0,0 +1,83 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/index/strtree/TemplateSTRtree.h>
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+class Polygon;
+class LinearRing;
+}
+}
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Polygon;
+using geos::geom::LinearRing;
+using geos::geom::Coordinate;
+
+
+class GEOS_DLL IndexedNestedHoleTester {
+
+private:
+
+    const Polygon* polygon;
+    index::strtree::TemplateSTRtree<const LinearRing*> index;
+    Coordinate nestedPt;
+
+    void loadIndex();
+
+
+public:
+
+    IndexedNestedHoleTester(const Polygon* p_polygon)
+        : polygon(p_polygon)
+    {
+        loadIndex();
+    }
+
+    /**
+    * Gets a point on a nested hole, if one exists.
+    *
+    * @return a point on a nested hole, or null if none are nested
+    */
+    const Coordinate& getNestedPoint() { return nestedPt; }
+
+    /**
+    * Tests if any hole is nested (contained) within another hole.
+    * This is invalid.
+    * The nested point will be set to reflect this.
+    * @return true if some hole is nested
+    */
+    bool isNested();
+
+};
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/IndexedNestedPolygonTester.h b/include/geos/operation/valid/IndexedNestedPolygonTester.h
new file mode 100644
index 0000000..f4bdb56
--- /dev/null
+++ b/include/geos/operation/valid/IndexedNestedPolygonTester.h
@@ -0,0 +1,111 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+#include <geos/index/strtree/TemplateSTRtree.h>
+#include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
+
+#include <memory>
+#include <map>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+class Polygon;
+class LinearRing;
+}
+}
+
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Polygon;
+using geos::geom::MultiPolygon;
+using geos::geom::LinearRing;
+using geos::geom::Coordinate;
+using algorithm::locate::IndexedPointInAreaLocator;
+using index::strtree::TemplateSTRtree;
+
+class GEOS_DLL IndexedNestedPolygonTester {
+
+private:
+
+    const MultiPolygon* multiPoly;
+    TemplateSTRtree<const Polygon*> index;
+    // std::vector<IndexedPointInAreaLocator> locators;
+    std::map<const Polygon*, IndexedPointInAreaLocator> locators;
+    Coordinate nestedPt;
+
+    void loadIndex();
+
+    IndexedPointInAreaLocator& getLocator(const Polygon* poly);
+
+    bool findNestedPoint(const LinearRing* shell,
+        const Polygon* possibleOuterPoly,
+        IndexedPointInAreaLocator& locator,
+        Coordinate& coordNested);
+
+    /**
+    * Finds a point of a shell segment which lies inside a polygon, if any.
+    * The shell is assume to touch the polyon only at shell vertices,
+    * and does not cross the polygon.
+    *
+    * @param the shell to test
+    * @param the polygon to test against
+    * @param coordNested return parametr for found coordinate
+    * @return an interior segment point, or null if the shell is nested correctly
+    */
+    static bool findSegmentInPolygon(
+        const LinearRing* shell,
+        const Polygon* poly,
+        Coordinate& coordNested);
+
+    // Declare type as noncopyable
+    IndexedNestedPolygonTester(const IndexedNestedPolygonTester& other) = delete;
+    IndexedNestedPolygonTester& operator=(const IndexedNestedPolygonTester& rhs) = delete;
+
+public:
+
+    IndexedNestedPolygonTester(const MultiPolygon* p_multiPoly);
+
+    /**
+    * Gets a point on a nested polygon, if one exists.
+    *
+    * @return a point on a nested polygon, or null if none are nested
+    */
+    const Coordinate& getNestedPoint() const { return nestedPt; }
+
+    /**
+    * Tests if any polygon is nested (contained) within another polygon.
+    * This is invalid.
+    * The nested point will be set to reflect this.
+    * @return true if some polygon is nested
+    */
+    bool isNested();
+
+
+};
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/IndexedNestedRingTester.h b/include/geos/operation/valid/IndexedNestedRingTester.h
deleted file mode 100644
index 7d5e557..0000000
--- a/include/geos/operation/valid/IndexedNestedRingTester.h
+++ /dev/null
@@ -1,109 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2009 Sandro Santilli <strk at kbt.io>
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/IndexedNestedRingTester.java r399 (JTS-1.12)
- *
- **********************************************************************/
-
-#ifndef GEOS_OP_VALID_OFFSETCURVEVERTEXLIST_H
-#define GEOS_OP_VALID_OFFSETCURVEVERTEXLIST_H
-
-#include <cstddef>
-#include <vector> // for composition
-
-#include <geos/index/strtree/TemplateSTRtree.h>
-#include <memory>
-
-// Forward declarations
-namespace geos {
-namespace geom {
-//class Envelope;
-class Coordinate;
-class LinearRing;
-}
-namespace index {
-class SpatialIndex;
-}
-namespace geomgraph {
-class GeometryGraph;
-}
-}
-
-namespace geos {
-namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-/** \brief
- * Tests whether any of a set of [LinearRings](@ref geom::LinearRing) are
- * nested inside another ring in the set, using a spatial
- * index to speed up the comparisons.
- *
- */
-class IndexedNestedRingTester {
-public:
-    // @param newGraph : ownership retained by caller
-    IndexedNestedRingTester(geomgraph::GeometryGraph* newGraph, std::size_t initialCapacity)
-        :
-        graph(newGraph),
-        index(nullptr),
-        nestedPt(nullptr)
-    {
-        rings.reserve(initialCapacity);
-    }
-
-    ~IndexedNestedRingTester();
-
-    /*
-     * Be aware that the returned Coordinate (if != NULL)
-     * will point to storage owned by one of the LinearRing
-     * previously added. If you destroy them, this
-     * will point to an invalid memory address.
-     */
-    const geom::Coordinate*
-    getNestedPoint() const
-    {
-        return nestedPt;
-    }
-
-    /// @param ring : ownership retained by caller
-    void
-    add(const geom::LinearRing* ring)
-    {
-        rings.push_back(ring);
-    }
-
-    bool isNonNested();
-
-private:
-
-    /// Externally owned
-    geomgraph::GeometryGraph* graph;
-
-    /// Ownership of this vector elements are externally owned
-    std::vector<const geom::LinearRing*> rings;
-
-    // Owned by us
-    std::unique_ptr<geos::index::strtree::TemplateSTRtree<const geom::LinearRing*>> index;
-
-    // Externally owned, if not null
-    const geom::Coordinate* nestedPt;
-
-    void buildIndex();
-};
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
-
-#endif // GEOS_OP_VALID_OFFSETCURVEVERTEXLIST_H
diff --git a/include/geos/operation/valid/IndexedNestedShellTester.h b/include/geos/operation/valid/IndexedNestedShellTester.h
deleted file mode 100644
index 3c9580a..0000000
--- a/include/geos/operation/valid/IndexedNestedShellTester.h
+++ /dev/null
@@ -1,108 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- * Copyright (C) 2005 Refractions Research Inc.
- * Copyright (C) 2010 Safe Software Inc.
- * Copyright (C) 2010 Sandro Santilli <strk at kbt.io>
- * Copyright (C) 2019 Daniel Baston <dbaston at gmail.com>
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************/
-
-#ifndef GEOS_OP_VALID_INDEXEDNESTEDSHELLTESTER_H
-#define GEOS_OP_VALID_INDEXEDNESTEDSHELLTESTER_H
-
-#include <geos/geom/Polygon.h>
-#include <geos/index/SpatialIndex.h>
-
-#include <cstddef>
-#include <memory>
-
-// Forward declarations
-namespace geos {
-namespace algorithm {
-namespace locate {
-    class IndexedPointInAreaLocator;
-}
-}
-namespace geom {
-    class Polygon;
-}
-namespace geomgraph {
-    class GeometryGraph;
-}
-namespace operation {
-namespace valid {
-    class PolygonIndexedLocators;
-}
-}
-}
-
-namespace geos {
-namespace operation {
-namespace valid {
-
-class IndexedNestedShellTester {
-
-public:
-    IndexedNestedShellTester(const geomgraph::GeometryGraph& g, std::size_t initialCapacity);
-
-    void add(const geom::Polygon& p) {
-        polys.push_back(&p);
-    }
-
-    const geom::Coordinate* getNestedPoint();
-
-    bool isNonNested();
-
-private:
-    void compute();
-
-    /**
-     * Check if a shell is incorrectly nested within a polygon.
-     * This is the case if the shell is inside the polygon shell,
-     * but not inside a polygon hole.
-     * (If the shell is inside a polygon hole, the nesting is valid.)
-     *
-     * The algorithm used relies on the fact that the rings must be
-     * properly contained.
-     * E.g. they cannot partially overlap (this has been previously
-     * checked by <code>checkRelateConsistency</code>
-     */
-    void checkShellNotNested(const geom::LinearRing* shell, PolygonIndexedLocators & locs);
-
-    /**
-     * This routine checks to see if a shell is properly contained
-     * in a hole.
-     * It assumes that the edges of the shell and hole do not
-     * properly intersect.
-     *
-     * @return <code>null</code> if the shell is properly contained, or
-     *   a Coordinate which is not inside the hole if it is not
-     */
-    const geom::Coordinate* checkShellInsideHole(const geom::LinearRing* shell,
-            algorithm::locate::IndexedPointInAreaLocator & holeLoc);
-
-    /// Externally owned
-    const geomgraph::GeometryGraph& graph;
-
-    std::vector<const geom::Polygon*> polys;
-
-    // Externally owned, if not null
-    const geom::Coordinate* nestedPt;
-
-    bool processed;
-};
-
-}
-}
-}
-
-#endif //GEOS_INDEXEDNESTEDSHELLTESTER_H
diff --git a/include/geos/operation/IsSimpleOp.h b/include/geos/operation/valid/IsSimpleOp.h
similarity index 98%
rename from include/geos/operation/IsSimpleOp.h
rename to include/geos/operation/valid/IsSimpleOp.h
index f6d9d31..1ae8e8f 100644
--- a/include/geos/operation/IsSimpleOp.h
+++ b/include/geos/operation/valid/IsSimpleOp.h
@@ -43,8 +43,9 @@ class GeometryCollection;
 }
 
 
-namespace geos {
+namespace geos {      // geos
 namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
 
 
 class GEOS_DLL IsSimpleOp {
@@ -248,5 +249,6 @@ public:
 }; // IsSimpleOp
 
 
+} // namespace geos.operation.valid
 } // namespace geos.operation
 } // namespace geos
diff --git a/include/geos/operation/valid/IsValidOp.h b/include/geos/operation/valid/IsValidOp.h
index f0b988f..9f56b70 100644
--- a/include/geos/operation/valid/IsValidOp.h
+++ b/include/geos/operation/valid/IsValidOp.h
@@ -3,250 +3,306 @@
  * GEOS - Geometry Engine Open Source
  * http://geos.osgeo.org
  *
- * Copyright (C) 2010 Sandro Santilli <strk at kbt.io>
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
  *
  * This is free software; you can redistribute and/or modify it under
  * the terms of the GNU Lesser General Public Licence as published
  * by the Free Software Foundation.
  * See the COPYING file for more information.
  *
- **********************************************************************
- *
- * Last port: operation/valid/IsValidOp.java r335 (JTS-1.12)
- *
  **********************************************************************/
 
-#ifndef GEOS_OP_ISVALIDOP_H
-#define GEOS_OP_ISVALIDOP_H
+#pragma once
 
 #include <geos/export.h>
 
-#include <geos/operation/valid/TopologyValidationError.h> // for inlined destructor
+#include <geos/operation/valid/PolygonTopologyAnalyzer.h>
+#include <geos/operation/valid/TopologyValidationError.h>
+
 
 // Forward declarations
 namespace geos {
-namespace util {
-class TopologyValidationError;
-}
 namespace geom {
-class CoordinateSequence;
-class GeometryFactory;
+class Coordinate;
 class Geometry;
 class Point;
-class LinearRing;
+class MultiPoint;
 class LineString;
+class LinearRing;
 class Polygon;
-class GeometryCollection;
 class MultiPolygon;
-class MultiLineString;
+class GeometryCollection;
+}
+namespace algorithm {
+namespace locate {
+class IndexedPointInAreaLocator;
 }
-namespace geomgraph {
-class DirectedEdge;
-class EdgeIntersectionList;
-class EdgeIntersection;
-class PlanarGraph;
-class GeometryGraph;
 }
 }
 
-namespace geos {
-namespace operation { // geos::operation
-namespace valid { // geos::operation::valid
 
-/** \brief
- * Implements the algorithsm required to compute the <code>isValid()</code>
- * method for [Geometrys](@ref geom::Geometry).
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+/**
+ * Implements the algorithms required to compute the <code>isValid()</code> method
+ * for Geometry.
+ * See the documentation for the various geometry types for a specification of validity.
+ *
+ * @version 1.7
  */
 class GEOS_DLL IsValidOp {
-    friend class Unload;
+
 private:
-    /// the base Geometry to be validated
-    const geom::Geometry* parentGeometry;
 
-    bool isChecked;
+    static constexpr int MIN_SIZE_LINESTRING = 2;
+    static constexpr int MIN_SIZE_RING = 4;
 
-    // CHECKME: should this really be a pointer ?
-    TopologyValidationError* validErr;
+    /**
+    * The geometry being validated
+    */
+    const geom::Geometry* inputGeometry;
+    /**
+    * If the following condition is true GEOS will validate
+    * inverted shells and exverted holes (the ESRI SDE model)
+    */
+    bool isInvertedRingValid = false;
+    std::unique_ptr<TopologyValidationError> validErr;
 
-    // This is the version using 'isChecked' flag
-    void checkValid();
+    bool hasInvalidError()
+    {
+        return validErr != nullptr;
+    }
 
-    void checkValid(const geom::Geometry* g);
-    void checkValid(const geom::Point* g);
-    void checkValid(const geom::LinearRing* g);
-    void checkValid(const geom::LineString* g);
-    void checkValid(const geom::Polygon* g);
-    void checkValid(const geom::MultiPolygon* g);
-    void checkValid(const geom::GeometryCollection* gc);
-    void checkConsistentArea(geomgraph::GeometryGraph* graph);
 
+    void logInvalid(int code, const geom::Coordinate* pt);
+
+    bool isValidGeometry(const geom::Geometry* g);
 
     /**
-     * Check that there is no ring which self-intersects
-     * (except of course at its endpoints).
-     * This is required by OGC topology rules (but not by other models
-     * such as ESRI SDE, which allow inverted shells and exverted holes).
-     *
-     * @param graph the topology graph of the geometry
+     * Tests validity of a Point.
      */
-    void checkNoSelfIntersectingRings(geomgraph::GeometryGraph* graph);
+    bool isValid(const geom::Point* g);
 
     /**
-     * check that a ring does not self-intersect, except at its endpoints.
-     * Algorithm is to count the number of times each node along edge
-     * occurs.
-     * If any occur more than once, that must be a self-intersection.
+     * Tests validity of a MultiPoint.
      */
-    void checkNoSelfIntersectingRing(
-        geomgraph::EdgeIntersectionList& eiList);
+    bool isValid(const geom::MultiPoint* g);
 
-    bool isStartNode(const geomgraph::EdgeIntersection& ei);
+    /**
+     * Tests validity of a LineString.
+     * Almost anything goes for linestrings!
+     */
+    bool isValid(const geom::LineString* g);
 
-    void checkTooFewPoints(geomgraph::GeometryGraph* graph);
+    /**
+     * Tests validity of a LinearRing.
+     */
+    bool isValid(const geom::LinearRing* g);
 
     /**
-     * Test that each hole is inside the polygon shell.
-     * This routine assumes that the holes have previously been tested
-     * to ensure that all vertices lie on the shell or inside it.
-     * A simple test of a single point in the hole can be used,
-     * provide the point is chosen such that it does not lie on the
-     * boundary of the shell.
-     *
-     * @param p the polygon to be tested for hole inclusion
-     * @param graph a geomgraph::GeometryGraph incorporating the polygon
+     * Tests the validity of a polygon.
+     * Sets the validErr flag.
      */
-    void checkHolesInShell(const geom::Polygon* p,
-                           geomgraph::GeometryGraph* graph);
+    bool isValid(const geom::Polygon* g);
 
     /**
-     * Tests that no hole is nested inside another hole.
-     * This routine assumes that the holes are disjoint.
-     * To ensure this, holes have previously been tested
-     * to ensure that:
-     *
-     *  - they do not partially overlap
-     *    (checked by <code>checkRelateConsistency</code>)
-     *  - they are not identical
-     *    (checked by <code>checkRelateConsistency</code>)
+     * Tests validity of a MultiPolygon.
      *
+     * @param g
+     * @return
      */
-    void checkHolesNotNested(const geom::Polygon* p,
-                             geomgraph::GeometryGraph* graph);
+    bool isValid(const geom::MultiPolygon* g);
 
     /**
-     * Tests that no element polygon is wholly in the interior of another
-     * element polygon.
-     *
-     * Preconditions:
+     * Tests validity of a GeometryCollection.
      *
-     * - shells do not partially overlap
-     * - shells do not touch along an edge
-     * - no duplicate rings exist
-     *
-     * This routine relies on the fact that while polygon shells
-     * may touch at one or more vertices, they cannot touch at
-     * ALL vertices.
+     * @param gc
+     * @return
      */
-    void checkShellsNotNested(const geom::MultiPolygon* mp,
-                              geomgraph::GeometryGraph* graph);
+    bool isValid(const geom::GeometryCollection* gc);
 
+    void checkCoordinateInvalid(const geom::CoordinateSequence* coords);
+    void checkCoordinateInvalid(const geom::Polygon* poly);
+    void checkRingNotClosed(const geom::LinearRing* ring);
+    void checkRingsNotClosed(const geom::Polygon* poly);
+    void checkRingsTooFewPoints(const geom::Polygon* poly);
+    void checkRingTooFewPoints(const geom::LinearRing* ring);
 
-    void checkConnectedInteriors(geomgraph::GeometryGraph& graph);
+    /**
+     * Check the number of non-repeated points is at least a given size.
+     *
+     * @param line
+     * @param minSize
+     */
+    void checkTooFewPoints(const geom::LineString* line, std::size_t minSize);
 
-    void checkInvalidCoordinates(const geom::CoordinateSequence* cs);
+    /**
+     * Test if the number of non-repeated points in a line
+     * is at least a given minimum size.
+     *
+     * @param line the line to test
+     * @param minSize the minimum line size
+     * @return true if the line has the required number of non-repeated points
+     */
+    bool isNonRepeatedSizeAtLeast(const geom::LineString* line, std::size_t minSize);
 
-    void checkInvalidCoordinates(const geom::Polygon* poly);
+    void checkAreaIntersections(PolygonTopologyAnalyzer& areaAnalyzer);
 
-    void checkClosedRings(const geom::Polygon* poly);
+    /**
+     * Check whether a ring self-intersects (except at its endpoints).
+     *
+     * @param ring the linear ring to check
+     */
+    void checkSelfIntersectingRing(const geom::LinearRing* ring);
 
-    void checkClosedRing(const geom::LinearRing* ring);
 
-    bool isSelfTouchingRingFormingHoleValid;
+    /**
+     * Tests that each hole is inside the polygon shell.
+     * This routine assumes that the holes have previously been tested
+     * to ensure that all vertices lie on the shell or on the same side of it
+     * (i.e. that the hole rings do not cross the shell ring).
+     * Given this, a simple point-in-polygon test of a single point in the hole can be used,
+     * provided the point is chosen such that it does not lie on the shell.
+     *
+     * @param poly the polygon to be tested for hole inclusion
+     */
+    void checkHolesOutsideShell(const geom::Polygon* poly);
 
-public:
-    /** \brief
-     * Find a point from the list of testCoords
-     * that is NOT a node in the edge for the list of searchCoords
+    /**
+     * Checks if a polygon hole lies inside its shell
+     * and if not returns a point indicating this.
+     * The hole is known to be wholly inside or outside the shell,
+     * so it suffices to find a single point which is interior or exterior,
+     * or check the edge topology at a point on the boundary of the shell.
      *
-     * @return the point found, or NULL if none found
+     * @param hole the hole to test
+     * @param shell the polygon shell to test against
+     * @return a hole point outside the shell, or null if it is inside
      */
-    static const geom::Coordinate* findPtNotNode(
-        const geom::CoordinateSequence* testCoords,
-        const geom::LinearRing* searchRing,
-        const geomgraph::GeometryGraph* graph);
+    const Coordinate * findHoleOutsideShellPoint(
+        const geom::LinearRing* hole,
+        const geom::LinearRing* shell);
 
-    /** \brief
-     * Checks whether a coordinate is valid for processing.
-     * Coordinates are valid iff their x and y coordinates are in the
-     * range of the floating point representation.
+    /**
+     * Checks if any polygon hole is nested inside another.
+     * Assumes that holes do not cross (overlap),
+     * This is checked earlier.
      *
-     * @param coord the coordinate to validate
-     * @return `true` if the coordinate is valid
+     * @param poly the polygon with holes to test
      */
-    static bool isValid(const geom::Coordinate& coord);
+    void checkHolesNested(const geom::Polygon* poly);
 
-    /** \brief
-     * Tests whether a geom::Geometry is valid.
+    /**
+     * Checks that no element polygon is in the interior of another element polygon.
      *
-     * @param geom the Geometry to test
-     * @return `true` if the geometry is valid
+     * Preconditions:
+     *
+     *  * shells do not partially overlap
+     *  * shells do not touch along an edge
+     *  * no duplicate rings exist
+     *
+     * These have been confirmed by the PolygonTopologyAnalyzer.
      */
-    static bool isValid(const geom::Geometry& geom);
+    void checkShellsNested(const geom::MultiPolygon* mp);
 
-    IsValidOp(const geom::Geometry* geom)
-        :
-        parentGeometry(geom),
-        isChecked(false),
-        validErr(nullptr),
-        isSelfTouchingRingFormingHoleValid(false)
-    {}
+    void checkInteriorDisconnected(PolygonTopologyAnalyzer& areaAnalyzer);
 
-    /// TODO: validErr can't be a pointer!
-    virtual
-    ~IsValidOp()
-    {
-        delete validErr;
-    }
 
-    bool isValid();
+public:
 
-    TopologyValidationError* getValidationError();
+    /**
+     * Creates a new validator for a geometry.
+     *
+     * @param p_inputGeometry the geometry to validate
+     */
+    IsValidOp(const geom::Geometry* p_inputGeometry)
+        : inputGeometry(p_inputGeometry)
+        , validErr(nullptr)
+        {};
 
-    /** \brief
-     * Sets whether polygons using **Self-Touching Rings** to form
+    /**
+     * Sets whether polygons using <b>Self-Touching Rings</b> to form
      * holes are reported as valid.
-     *
      * If this flag is set, the following Self-Touching conditions
      * are treated as being valid:
      *
-     * - the shell ring self-touches to create a hole touching the shell
-     * - a hole ring self-touches to create two holes touching at a point
+     *  * the shell ring self-touches to create a hole touching the shell
+     *  * a hole ring self-touches to create two holes touching at a point
      *
      * The default (following the OGC SFS standard)
-     * is that this condition is **not** valid (`false`).
+     * is that this condition is not valid (false).
+     *
+     * Self-Touching Rings which disconnect the
+     * the polygon interior are still considered to be invalid
+     * (these are <b>invalid</b> under the SFS, and many other
+     * spatial models as well).
+     * This includes:
      *
-     * This does not affect whether Self-Touching Rings
-     * disconnecting the polygon interior are considered valid
-     * (these are considered to be **invalid** under the SFS,
-     * and many other spatial models as well).
-     * This includes "bow-tie" shells, which self-touch at a single point
-     * causing the interior to be disconnected, and "C-shaped" holes which
-     * self-touch at a single point causing an island to be formed.
+     *  * exverted ("bow-tie") shells which self-touch at a single point
+     *  * inverted shells with the inversion touching the shell at another point
+     *  * exverted holes with exversion touching the hole at another point
+     *  * inverted ("C-shaped") holes which self-touch at a single point causing an island to be formed
+     *  * inverted shells or exverted holes which form part of a chain of touching rings
+     *    (which disconnect the interior)
      *
      * @param p_isValid states whether geometry with this condition is valid
      */
-    void
-    setSelfTouchingRingFormingHoleValid(bool p_isValid)
+    void setSelfTouchingRingFormingHoleValid(bool p_isValid)
+    {
+        isInvertedRingValid = p_isValid;
+    };
+
+    /**
+     * Tests whether a Geometry is valid.
+     * @param geom the Geometry to test
+     * @return true if the geometry is valid
+     */
+    static bool isValid(const geom::Geometry* geom)
+    {
+        IsValidOp ivo(geom);
+        return ivo.isValid();
+    };
+
+    static bool isValid(const geom::Coordinate& coord)
     {
-        isSelfTouchingRingFormingHoleValid = p_isValid;
+        return isValid(&coord);
     }
 
+    /**
+     * Tests the validity of the input geometry.
+     *
+     * @return true if the geometry is valid
+     */
+    bool isValid();
+
+    /**
+     * Checks whether a coordinate is valid for processing.
+     * Coordinates are valid if their x and y ordinates are in the
+     * range of the floating point representation.
+     *
+     * @param coord the coordinate to validate
+     * @return <code>true</code> if the coordinate is valid
+     */
+    static bool isValid(const geom::Coordinate* coord);
+
+    /**
+     * Computes the validity of the geometry,
+     * and if not valid returns the validation error for the geometry,
+     * or null if the geometry is valid.
+     *
+     * @return the validation error, if the geometry is invalid
+     * or null if the geometry is valid
+     */
+    const TopologyValidationError* getValidationError();
+
+
 };
 
+
 } // namespace geos.operation.valid
 } // namespace geos.operation
 } // namespace geos
 
-#endif // GEOS_OP_ISVALIDOP_H
diff --git a/include/geos/operation/valid/PolygonIntersectionAnalyzer.h b/include/geos/operation/valid/PolygonIntersectionAnalyzer.h
new file mode 100644
index 0000000..5566602
--- /dev/null
+++ b/include/geos/operation/valid/PolygonIntersectionAnalyzer.h
@@ -0,0 +1,127 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/noding/SegmentIntersector.h>
+#include <geos/algorithm/LineIntersector.h>
+#include <geos/operation/valid/TopologyValidationError.h>
+
+
+#include <geos/export.h>
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+}
+namespace noding {
+class SegmentString;
+}
+}
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+using geos::noding::SegmentString;
+
+class GEOS_DLL PolygonIntersectionAnalyzer : public noding::SegmentIntersector {
+
+private:
+
+    algorithm::LineIntersector li;
+    bool m_hasDoubleTouch = false;
+    bool isInvertedRingValid = false;
+    int invalidCode = TopologyValidationError::oNoInvalidIntersection;
+    Coordinate invalidLocation;
+    Coordinate doubleTouchLocation;
+
+    int findInvalidIntersection(
+        const SegmentString* ss0, std::size_t segIndex0,
+        const SegmentString* ss1, std::size_t segIndex1);
+
+    bool addDoubleTouch(
+        const SegmentString* ss0, const SegmentString* ss1,
+        const Coordinate& intPt);
+
+    void addSelfTouch(
+        const SegmentString* ss, const Coordinate& intPt,
+        const Coordinate* e00, const Coordinate* e01,
+        const Coordinate* e10, const Coordinate* e11);
+
+    const Coordinate& prevCoordinateInRing(
+        const SegmentString* ringSS, std::size_t segIndex) const;
+
+    bool isAdjacentInRing(const SegmentString* ringSS,
+        std::size_t segIndex0, std::size_t segIndex1) const;
+
+
+public:
+
+    /**
+    * Creates a new finder, allowing for the mode where inverted rings are valid.
+    *
+    * @param isInvertedRingValid true if inverted rings are valid.
+    */
+    PolygonIntersectionAnalyzer(bool p_isInvertedRingValid)
+        : isInvertedRingValid(p_isInvertedRingValid)
+        , invalidLocation(Coordinate::getNull())
+        , doubleTouchLocation(Coordinate::getNull())
+        {}
+
+    void processIntersections(
+        SegmentString* ss0, std::size_t segIndex0,
+        SegmentString* ss1, std::size_t segIndex1) override;
+
+    bool isDone() const override {
+        return isInvalid() || m_hasDoubleTouch;
+    };
+
+    bool isInvalid() const
+    {
+        return invalidCode >= 0;
+    };
+
+    int getInvalidCode() const
+    {
+        return invalidCode;
+    };
+
+    const Coordinate& getInvalidLocation() const
+    {
+        return invalidLocation;
+    };
+
+    bool hasDoubleTouch() const
+    {
+        return m_hasDoubleTouch;
+    };
+
+    const Coordinate& getDoubleTouchLocation() const
+    {
+        return doubleTouchLocation;
+    };
+
+};
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/PolygonNode.h b/include/geos/operation/valid/PolygonNode.h
new file mode 100644
index 0000000..a689caf
--- /dev/null
+++ b/include/geos/operation/valid/PolygonNode.h
@@ -0,0 +1,113 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+}
+}
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+
+class GEOS_DLL PolygonNode {
+
+private:
+
+    /**
+    * Tests if an edge p is between edges e0 and e1,
+    * where the edges all originate at a common origin.
+    * The "inside" of e0 and e1 is the arc which does not include the origin.
+    * The edges are assumed to be distinct (non-collinear).
+    *
+    * @param origin the origin
+    * @param p the destination point of edge p
+    * @param e0 the destination point of edge e0
+    * @param e1 the destination point of edge e1
+    * @return true if p is between e0 and e1
+    */
+    static bool
+    isBetween(const Coordinate* origin, const Coordinate* p,
+        const Coordinate* e0, const Coordinate* e1);
+
+    /**
+    * Tests if the angle with the origin of a vector P is greater than that of the
+    * vector Q.
+    *
+    * @param origin the origin of the vectors
+    * @param p the endpoint of the vector P
+    * @param q the endpoint of the vector Q
+    * @return true if vector P has angle greater than Q
+    */
+    static bool
+    isAngleGreater(const Coordinate* origin,
+        const Coordinate* p, const Coordinate* q);
+
+    static int
+    quadrant(const Coordinate* origin, const Coordinate* p);
+
+
+public:
+
+    /**
+    * Check if the edges at a node between two rings (or one ring) cross.
+    * The node is topologically valid if the ring edges do not cross.
+    * This function assumes that the edges are not collinear.
+    *
+    * @param nodePt the node location
+    * @param a0 the previous edge endpoint in a ring
+    * @param a1 the next edge endpoint in a ring
+    * @param b0 the previous edge endpoint in the other ring
+    * @param b1 the next edge endpoint in the other ring
+    * @return true if the edges cross at the node
+    */
+    static bool isCrossing(const Coordinate* nodePt,
+        const Coordinate* a0, const Coordinate* a1,
+        const Coordinate* b0, const Coordinate* b1);
+
+    /**
+    * Tests whether an edge node-b lies in the interior or exterior
+    * of a corner of a ring given by a0-node-a1.
+    * The ring interior is assumed to be on the right of the corner (a CW ring).
+    * The edge must not be collinear with the corner segments.
+    *
+    * @param nodePt the node location
+    * @param a0 the first vertex of the corner
+    * @param a1 the second vertex of the corner
+    * @param b the destination vertex of the edge
+    * @return true if the edge is interior to the ring corner
+    */
+    static bool isInteriorSegment(const Coordinate* nodePt,
+        const Coordinate* a0, const Coordinate* a1,
+        const Coordinate* b);
+
+};
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/PolygonRing.h b/include/geos/operation/valid/PolygonRing.h
new file mode 100644
index 0000000..8670f07
--- /dev/null
+++ b/include/geos/operation/valid/PolygonRing.h
@@ -0,0 +1,232 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/operation/valid/PolygonRingTouch.h>
+#include <geos/operation/valid/PolygonRingSelfNode.h>
+
+#include <geos/export.h>
+
+
+#include <memory>
+#include <map>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+class LinearRing;
+}
+}
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+using geos::geom::LinearRing;
+
+
+class GEOS_DLL PolygonRing {
+
+private:
+
+    int id = -1;
+    PolygonRing* shell = nullptr;
+    const LinearRing* ring = nullptr;
+
+    /**
+    * The root of the touch graph tree containing this ring.
+    * Serves as the id for the graph partition induced by the touch relation.
+    */
+    PolygonRing* touchSetRoot = nullptr;
+
+    /**
+    * The set of PolygonRingTouch links
+    * for this ring.
+    * The set of all touches in the rings of a polygon
+    * forms the polygon touch graph.
+    * This supports detecting touch cycles, which
+    * reveal the condition of a disconnected interior.
+    *
+    * Only a single touch is recorded between any two rings,
+    * since more than one touch between two rings
+    * indicates interior disconnection as well.
+    */
+    std::map<int, PolygonRingTouch> touches;
+
+    /**
+    * The set of self-nodes in this ring.
+    * This supports checking valid ring self-touch topology.
+    */
+    std::vector<PolygonRingSelfNode> selfNodes;
+
+    /* METHODS */
+
+    /**
+    * Tests if this ring touches a given ring at
+    * the single point specified.
+    *
+    * @param ring the other PolygonRing
+    * @param pt the touch point
+    * @return true if the rings touch only at the given point
+    */
+    bool isOnlyTouch(const PolygonRing* polyRing, const Coordinate& pt) const;
+
+    /**
+    * Detects whether the subgraph of holes linked by touch to this ring
+    * contains a hole cycle.
+    * If no cycles are detected, the set of touching rings is a tree.
+    * The set is marked using this ring as the root.
+    *
+    * @return a vertex in a hole cycle, or null if no cycle found
+    */
+    const Coordinate* findHoleCycleLocation();
+
+    void init(PolygonRing* root, std::stack<PolygonRingTouch*>& touchStack);
+
+    /**
+    * Scans for a hole cycle starting at a given touch.
+    *
+    * @param currentTouch the touch to investigate
+    * @param root the root of the touch subgraph
+    * @param touchStack the stack of touches to scan
+    * @return a vertex in a hole cycle if found, or null
+    */
+    const Coordinate* scanForHoleCycle(PolygonRingTouch* currentTouch,
+        PolygonRing* root,
+        std::stack<PolygonRingTouch*>& touchStack);
+
+
+    bool isInTouchSet() const
+    {
+        return touchSetRoot != nullptr;
+    };
+
+    void setTouchSetRoot(PolygonRing* polyRing)
+    {
+        touchSetRoot = polyRing;
+    };
+
+    PolygonRing* getTouchSetRoot() const
+    {
+        return touchSetRoot;
+    };
+
+    bool hasTouches() const
+    {
+        return ! touches.empty();
+    };
+
+    std::vector<PolygonRingTouch*> getTouches() const;
+
+    void addTouch(PolygonRing* polyRing, const Coordinate& pt);
+
+
+public:
+
+    /**
+    * Creates a ring for a polygon hole.
+    * @param p_ring the ring geometry
+    * @param p_index the index of the hole
+    * @param p_shell the parent polygon shell
+    */
+    PolygonRing(const LinearRing* p_ring, int p_index, PolygonRing* p_shell)
+        : id(p_index)
+        , shell(p_shell)
+        , ring(p_ring)
+        {};
+
+    /**
+    * Creates a ring for a polygon shell.
+    * @param p_ring
+    */
+    PolygonRing(const LinearRing* p_ring)
+        : PolygonRing(p_ring, -1, this)
+        {};
+
+    /**
+    * Tests if a polygon ring represents a shell.
+    *
+    * @param polyRing the ring to test (may be null)
+    * @return true if the ring represents a shell
+    */
+    static bool isShell(const PolygonRing* polyRing);
+
+    /**
+    * Records a touch location between two rings,
+    * and checks if the rings already touch in a different location.
+    *
+    * @param ring0 a polygon ring
+    * @param ring1 a polygon ring
+    * @param pt the location where they touch
+    * @return true if the polygons already touch
+    */
+    static bool addTouch(PolygonRing* ring0, PolygonRing* ring1, const Coordinate& pt);
+
+    /**
+    * Finds a location (if any) where a chain of holes forms a cycle
+    * in the ring touch graph.
+    * The shell may form part of the chain as well.
+    * This indicates that a set of holes disconnects the interior of a polygon.
+    *
+    * @param polyRings the list of rings to check
+    * @return a vertex contained in a ring cycle, or null if none is found
+    */
+    static const Coordinate* findHoleCycleLocation(std::vector<PolygonRing*> polyRings);
+
+    /**
+    * Finds a location of an interior self-touch in a list of rings,
+    * if one exists.
+    * This indicates that a self-touch disconnects the interior of a polygon,
+    * which is invalid.
+    *
+    * @param polyRings the list of rings to check
+    * @return the location of an interior self-touch node, or null if there are none
+    */
+    static const Coordinate* findInteriorSelfNode(std::vector<PolygonRing*> polyRings);
+
+    bool isSamePolygon(const PolygonRing* polyRing) const
+    {
+        return shell == polyRing->shell;
+    };
+
+    bool isShell() const
+    {
+        return shell == this;
+    };
+
+    void addSelfTouch(const Coordinate& origin,
+        const Coordinate* e00, const Coordinate* e01,
+        const Coordinate* e10, const Coordinate* e11);
+
+    /**
+    * Finds the location of an invalid interior self-touch in this ring,
+    * if one exists.
+    *
+    * @return the location of an interior self-touch node, or null if there are none
+    */
+    const Coordinate* findInteriorSelfNode();
+
+
+};
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/PolygonRingSelfNode.h b/include/geos/operation/valid/PolygonRingSelfNode.h
new file mode 100644
index 0000000..c811b2f
--- /dev/null
+++ b/include/geos/operation/valid/PolygonRingSelfNode.h
@@ -0,0 +1,89 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+}
+}
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+
+class GEOS_DLL PolygonRingSelfNode {
+
+private:
+
+    Coordinate nodePt;
+    const Coordinate* e00;
+    const Coordinate* e01;
+    const Coordinate* e10;
+    const Coordinate* e11;
+
+
+public:
+
+    PolygonRingSelfNode(
+        const Coordinate& p_nodePt,
+        const Coordinate* p_e00,
+        const Coordinate* p_e01,
+        const Coordinate* p_e10,
+        const Coordinate* p_e11)
+        : nodePt(p_nodePt)
+        , e00(p_e00)
+        , e01(p_e01)
+        , e10(p_e10)
+        , e11(p_e11)
+        {}
+
+    /**
+    * The node point.
+    *
+    * @return
+    */
+    const Coordinate* getCoordinate() const {
+        return &nodePt;
+    }
+
+    /**
+    * Tests if a self-touch has the segments of each half of the touch
+    * lying in the exterior of a polygon.
+    * This is a valid self-touch.
+    * It applies to both shells and holes.
+    * Only one of the four possible cases needs to be tested,
+    * since the situation has full symmetry.
+    *
+    * @param isInteriorOnRight whether the interior is to the right of the parent ring
+    * @return true if the self-touch is in the exterior
+    */
+    bool isExterior(bool isInteriorOnRight) const;
+
+};
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/PolygonRingTouch.h b/include/geos/operation/valid/PolygonRingTouch.h
new file mode 100644
index 0000000..9b36516
--- /dev/null
+++ b/include/geos/operation/valid/PolygonRingTouch.h
@@ -0,0 +1,67 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Coordinate;
+}
+namespace operation {
+namespace valid {
+class PolygonRing;
+}
+}
+}
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+
+class GEOS_DLL PolygonRingTouch {
+
+private:
+
+    PolygonRing* ring;
+    Coordinate touchPt;
+
+
+public:
+
+    PolygonRingTouch(PolygonRing* p_ring, const Coordinate& p_pt)
+        : ring(p_ring)
+        , touchPt(p_pt)
+        {};
+
+    const Coordinate* getCoordinate() const;
+
+    PolygonRing* getRing() const;
+
+    bool isAtLocation(const Coordinate& pt) const;
+
+};
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/PolygonTopologyAnalyzer.h b/include/geos/operation/valid/PolygonTopologyAnalyzer.h
new file mode 100644
index 0000000..d8365f7
--- /dev/null
+++ b/include/geos/operation/valid/PolygonTopologyAnalyzer.h
@@ -0,0 +1,198 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#pragma once
+
+#include <geos/export.h>
+
+#include <geos/operation/valid/PolygonIntersectionAnalyzer.h>
+#include <geos/operation/valid/PolygonRing.h>
+#include <geos/noding/BasicSegmentString.h>
+
+#include <memory>
+
+// Forward declarations
+namespace geos {
+namespace geom {
+class Geometry;
+class Coordinate;
+}
+}
+
+namespace geos {      // geos.
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using geos::geom::Coordinate;
+using geos::geom::CoordinateSequence;
+using geos::geom::Geometry;
+using geos::geom::LinearRing;
+
+class GEOS_DLL PolygonTopologyAnalyzer {
+
+private:
+
+    // const Geometry* inputGeom;
+    bool isInvertedRingValid = false;
+    PolygonIntersectionAnalyzer segInt;
+    std::vector<PolygonRing*> polyRings;
+    geom::Coordinate disconnectionPt;
+
+
+    // holding area for PolygonRings and SegmentStrings so we
+    // can pass around pointers with abandon
+    std::deque<PolygonRing> polyRingStore;
+    std::deque<noding::BasicSegmentString> segStringStore;
+    // when building SegmentStrings we sometimes want
+    // to use deduped CoordinateSequences so we will
+    // keep the deduped ones here so they get cleaned
+    // up when processing is complete
+    std::vector<std::unique_ptr<CoordinateSequence>> coordSeqStore;
+
+    PolygonRing* createPolygonRing(const LinearRing* p_ring);
+    PolygonRing* createPolygonRing(const LinearRing* p_ring, int p_index, PolygonRing* p_shell);
+
+    /**
+     * Computes the index of the segment which intersects a given point.
+     * @param ringPts the ring points
+     * @param pt the intersection point
+     * @return the intersection segment index, or -1 if no intersection is found
+     */
+    static std::size_t intersectingSegIndex(const CoordinateSequence* ringPts, const Coordinate* pt);
+
+    static std::size_t ringIndexPrev(const CoordinateSequence* ringPts, std::size_t index);
+
+    std::vector<SegmentString*> createSegmentStrings(const Geometry* geom, bool isInvertedRingValid);
+
+    std::vector<PolygonRing*> getPolygonRings(const std::vector<SegmentString*>& segStrings);
+
+    SegmentString* createSegString(const LinearRing* ring, const PolygonRing* polyRing);
+
+    // Declare type as noncopyable
+    PolygonTopologyAnalyzer(const PolygonTopologyAnalyzer& other) = delete;
+    PolygonTopologyAnalyzer& operator=(const PolygonTopologyAnalyzer& rhs) = delete;
+
+public:
+
+    /* public */
+    PolygonTopologyAnalyzer(const Geometry* geom, bool p_isInvertedRingValid);
+
+    /**
+     * Finds a self-intersection (if any) in a LinearRing.
+     *
+     * @param ring the ring to analyze
+     * @return a self-intersection point if one exists, or null
+     */
+    static Coordinate findSelfIntersection(const LinearRing* ring);
+
+    /**
+     * Tests whether a segment p0-p1 is inside or outside a ring.
+     *
+     * Preconditions:
+     *
+     *  * The segment does not cross the ring
+     *  * One or both of the segment endpoints may lie on the ring
+     *  * The ring is valid
+     *
+     * @param p0 a segment vertex
+     * @param p1 a segment vertex
+     * @param ring the ring to test
+     * @return true if the segment lies inside the ring
+     */
+    static bool
+    isSegmentInRing(const Coordinate* p0, const Coordinate* p1,
+        const LinearRing* ring);
+
+    /**
+     * Tests whether a touching segment is interior to a ring.
+     *
+     * Preconditions:
+     *
+     *  * The segment does not cross the ring
+     *  * The segment vertex p0 lies on the ring
+     *  * The ring is valid
+     *
+     * This works for both shells and holes, but the caller must know
+     * the ring role.
+     *
+     * @param p0 the first vertex of the segment
+     * @param p1 the second vertex of the segment
+     * @param ringPts the points of the ring
+     * @return true if the segment is inside the ring.
+     */
+    static bool isIncidentSegmentInRing(const Coordinate* p0, const Coordinate* p1,
+        const CoordinateSequence* ringPts);
+
+
+    bool hasInvalidIntersection() {
+        return segInt.isInvalid();
+    }
+
+    int getInvalidCode() {
+        return segInt.getInvalidCode();
+    }
+
+    const Coordinate& getInvalidLocation() {
+        return segInt.getInvalidLocation();
+    }
+
+    /**
+    * Tests whether the interior of the polygonal geometry is
+    * disconnected.
+    * If true, the disconnection location is available from
+    * getDisconnectionLocation().
+    *
+    * @return true if the interior is disconnected
+    */
+    bool isInteriorDisconnected();
+
+    const Coordinate& getDisconnectionLocation() const
+    {
+        return disconnectionPt;
+    };
+
+
+    /**
+    * Tests whether any polygon with holes has a disconnected interior
+    * by virtue of the holes (and possibly shell) forming a hole cycle.
+    *
+    * This is a global check, which relies on determining
+    * the touching graph of all holes in a polygon.
+    *
+    * If inverted rings disconnect the interior
+    * via a self-touch, this is checked by the PolygonIntersectionAnalyzer.
+    * If inverted rings are part of a hole cycle
+    * this is detected here as well.
+    */
+    void checkInteriorDisconnectedByHoleCycle();
+
+    /**
+    * Tests if an area interior is disconnected by a self-touching ring.
+    * This must be evaluated after other self-intersections have been analyzed
+    * and determined to not exist, since the logic relies on
+    * the rings not self-crossing (winding).
+    * <p>
+    * If self-touching rings are not allowed,
+    * then the self-touch will previously trigger a self-intersection error.
+    */
+    void checkInteriorDisconnectedBySelfTouch();
+
+};
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
+
diff --git a/include/geos/operation/valid/QuadtreeNestedRingTester.h b/include/geos/operation/valid/QuadtreeNestedRingTester.h
deleted file mode 100644
index a5600b3..0000000
--- a/include/geos/operation/valid/QuadtreeNestedRingTester.h
+++ /dev/null
@@ -1,103 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/QuadtreeNestedRingTester.java rev. 1.12 (JTS-1.10)
- *
- **********************************************************************/
-
-#ifndef GEOS_OP_QUADTREENESTEDRINGTESTER_H
-#define GEOS_OP_QUADTREENESTEDRINGTESTER_H
-
-#include <geos/export.h>
-
-#include <geos/geom/Envelope.h> // for composition
-
-#include <vector>
-
-#ifdef _MSC_VER
-#pragma warning(push)
-#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
-#endif
-
-// Forward declarations
-namespace geos {
-namespace geom {
-class LinearRing;
-class Coordinate;
-}
-namespace index {
-namespace quadtree {
-class Quadtree;
-}
-}
-namespace geomgraph {
-class GeometryGraph;
-}
-}
-
-namespace geos {
-namespace operation { // geos::operation
-namespace valid { // geos::operation::valid
-
-/** \brief
- * Tests whether any of a set of [LinearRings](@ref geom::LinearRing) are nested
- * inside another ring in the set, using a [Quadtree](@ref index::quadtree::Quadtree)
- * index to speed up the comparisons.
- *
- */
-class GEOS_DLL QuadtreeNestedRingTester {
-public:
-
-    /// Caller retains ownership of GeometryGraph
-    QuadtreeNestedRingTester(geomgraph::GeometryGraph* newGraph);
-
-    ~QuadtreeNestedRingTester();
-
-    /*
-     * Be aware that the returned Coordinate (if != NULL)
-     * will point to storage owned by one of the LinearRing
-     * previously added. If you destroy them, this
-     * will point to an invalid memory address.
-     */
-    geom::Coordinate* getNestedPoint();
-
-    void add(const geom::LinearRing* ring);
-
-    bool isNonNested();
-
-private:
-
-    geomgraph::GeometryGraph* graph;  // used to find non-node vertices
-
-    std::vector<const geom::LinearRing*> rings;
-
-    geom::Envelope totalEnv;
-
-    index::quadtree::Quadtree* qt;
-
-    geom::Coordinate* nestedPt;
-
-    void buildQuadtree();
-};
-
-} // namespace geos::operation::valid
-} // namespace geos::operation
-} // namespace geos
-
-#ifdef _MSC_VER
-#pragma warning(pop)
-#endif
-
-#endif // GEOS_OP_QUADTREENESTEDRINGTESTER_H
diff --git a/include/geos/operation/valid/SimpleNestedRingTester.h b/include/geos/operation/valid/SimpleNestedRingTester.h
deleted file mode 100644
index 963c92b..0000000
--- a/include/geos/operation/valid/SimpleNestedRingTester.h
+++ /dev/null
@@ -1,100 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/SimpleNestedRingTester.java rev. 1.14 (JTS-1.10)
- *
- **********************************************************************/
-
-#ifndef GEOS_OP_SIMPLENESTEDRINGTESTER_H
-#define GEOS_OP_SIMPLENESTEDRINGTESTER_H
-
-#include <geos/export.h>
-
-#include <cstddef>
-#include <vector>
-
-#ifdef _MSC_VER
-#pragma warning(push)
-#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
-#endif
-
-// Forward declarations
-namespace geos {
-namespace geom {
-class Coordinate;
-class LinearRing;
-}
-namespace geomgraph {
-class GeometryGraph;
-}
-}
-
-namespace geos {
-namespace operation { // geos::operation
-namespace valid { // geos::operation::valid
-
-/** \brief
- * Tests whether any of a set of [LinearRings](@ref geom::LinearRing) are
- * nested inside another ring in the set, using a simple O(n^2)
- * comparison.
- *
- */
-class GEOS_DLL SimpleNestedRingTester {
-private:
-    geomgraph::GeometryGraph* graph;  // used to find non-node vertices
-    std::vector<geom::LinearRing*> rings;
-    geom::Coordinate* nestedPt;
-public:
-    SimpleNestedRingTester(geomgraph::GeometryGraph* newGraph)
-        :
-        graph(newGraph),
-        rings(),
-        nestedPt(nullptr)
-    {}
-
-    ~SimpleNestedRingTester()
-    {
-    }
-
-    void
-    add(geom::LinearRing* ring)
-    {
-        rings.push_back(ring);
-    }
-
-    /*
-     * Be aware that the returned Coordinate (if != NULL)
-     * will point to storage owned by one of the LinearRing
-     * previously added. If you destroy them, this
-     * will point to an invalid memory address.
-     */
-    geom::Coordinate*
-    getNestedPoint()
-    {
-        return nestedPt;
-    }
-
-    bool isNonNested();
-};
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
-
-#ifdef _MSC_VER
-#pragma warning(pop)
-#endif
-
-#endif // GEOS_OP_SIMPLENESTEDRINGTESTER_H
diff --git a/include/geos/operation/valid/SweeplineNestedRingTester.h b/include/geos/operation/valid/SweeplineNestedRingTester.h
deleted file mode 100644
index b540dd7..0000000
--- a/include/geos/operation/valid/SweeplineNestedRingTester.h
+++ /dev/null
@@ -1,129 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/SweeplineNestedRingTester.java rev. 1.12 (JTS-1.10)
- *
- **********************************************************************/
-
-#ifndef GEOS_OP_SWEEPLINENESTEDRINGTESTER_H
-#define GEOS_OP_SWEEPLINENESTEDRINGTESTER_H
-
-#include <geos/export.h>
-#include <geos/geom/Envelope.h> // for inline
-//#include <geos/indexSweepline.h> // for inline and inheritance
-#include <geos/index/sweepline/SweepLineOverlapAction.h> // for inheritance
-#include <geos/index/sweepline/SweepLineIndex.h> // for inlines
-
-#include <vector>
-
-#ifdef _MSC_VER
-#pragma warning(push)
-#pragma warning(disable: 4251) // warning C4251: needs to have dll-interface to be used by clients of class
-#endif
-
-// Forward declarations
-namespace geos {
-namespace geom {
-class LinearRing;
-class Envelope;
-class Coordinate;
-}
-namespace index {
-namespace sweepline {
-class SweepLineIndex;
-}
-}
-namespace geomgraph {
-class GeometryGraph;
-}
-}
-
-namespace geos {
-namespace operation { // geos::operation
-namespace valid { // geos::operation::valid
-
-/** \brief
- * Tests whether any of a set of [LinearRings](@ref geom::LinearRing) are
- * nested inside another ring in the set, using an
- * [SweepLineIndex](@ref index::sweepline::SweepLineIndex) to speed up
- * the comparisons.
- */
-class GEOS_DLL SweeplineNestedRingTester {
-
-private:
-    geomgraph::GeometryGraph* graph;  // used to find non-node vertices
-    std::vector<geom::LinearRing*> rings;
-    index::sweepline::SweepLineIndex* sweepLine;
-    geom::Coordinate* nestedPt;
-    void buildIndex();
-
-    SweeplineNestedRingTester(const SweeplineNestedRingTester&) = delete;
-    SweeplineNestedRingTester& operator=(const SweeplineNestedRingTester&) = delete;
-
-public:
-
-    SweeplineNestedRingTester(geomgraph::GeometryGraph* newGraph)
-        :
-        graph(newGraph),
-        rings(),
-        sweepLine(new index::sweepline::SweepLineIndex()),
-        nestedPt(nullptr)
-    {}
-
-    ~SweeplineNestedRingTester()
-    {
-        delete sweepLine;
-    }
-
-    /*
-     * Be aware that the returned Coordinate (if != NULL)
-     * will point to storage owned by one of the LinearRing
-     * previously added. If you destroy them, this
-     * will point to an invalid memory address.
-     */
-    geom::Coordinate*
-    getNestedPoint()
-    {
-        return nestedPt;
-    }
-
-    void
-    add(geom::LinearRing* ring)
-    {
-        rings.push_back(ring);
-    }
-
-    bool isNonNested();
-    bool isInside(geom::LinearRing* innerRing, geom::LinearRing* searchRing);
-    class OverlapAction: public index::sweepline::SweepLineOverlapAction {
-    public:
-        bool isNonNested;
-        OverlapAction(SweeplineNestedRingTester* p);
-        void overlap(index::sweepline::SweepLineInterval* s0,
-                     index::sweepline::SweepLineInterval* s1) override;
-    private:
-        SweeplineNestedRingTester* parent;
-    };
-};
-
-} // namespace geos::operation::valid
-} // namespace geos::operation
-} // namespace geos
-
-#ifdef _MSC_VER
-#pragma warning(pop)
-#endif
-
-#endif // GEOS_OP_SWEEPLINENESTEDRINGTESTER_H
diff --git a/include/geos/operation/valid/TopologyValidationError.h b/include/geos/operation/valid/TopologyValidationError.h
index cca3721..e201ad7 100644
--- a/include/geos/operation/valid/TopologyValidationError.h
+++ b/include/geos/operation/valid/TopologyValidationError.h
@@ -52,21 +52,22 @@ public:
         eDuplicatedRings,
         eTooFewPoints,
         eInvalidCoordinate,
-        eRingNotClosed
+        eRingNotClosed,
+        oNoInvalidIntersection = -1
     };
 
     TopologyValidationError(int newErrorType, const geom::Coordinate& newPt);
     TopologyValidationError(int newErrorType);
-    geom::Coordinate& getCoordinate();
-    std::string getMessage();
-    int getErrorType();
-    std::string toString();
+    const geom::Coordinate& getCoordinate() const;
+    std::string getMessage() const;
+    int getErrorType() const;
+    std::string toString() const;
 
 private:
     // Used const char* to reduce dynamic allocations
     static const char* errMsg[];
     int errorType;
-    geom::Coordinate pt;
+    const geom::Coordinate pt;
 };
 
 
diff --git a/src/geom/Geometry.cpp b/src/geom/Geometry.cpp
index c0d0ef0..49c05f3 100644
--- a/src/geom/Geometry.cpp
+++ b/src/geom/Geometry.cpp
@@ -50,7 +50,7 @@
 #include <geos/operation/overlay/snap/SnapIfNeededOverlayOp.h>
 #include <geos/operation/buffer/BufferOp.h>
 #include <geos/operation/distance/DistanceOp.h>
-#include <geos/operation/IsSimpleOp.h>
+#include <geos/operation/valid/IsSimpleOp.h>
 #include <geos/operation/overlayng/OverlayNGRobust.h>
 #include <geos/io/WKBWriter.h>
 #include <geos/io/WKTWriter.h>
@@ -905,7 +905,7 @@ Geometry::apply_rw(GeometryComponentFilter* filter)
 bool
 Geometry::isSimple() const
 {
-    operation::IsSimpleOp op(*this);
+    operation::valid::IsSimpleOp op(*this);
     return op.isSimple();
 }
 
diff --git a/src/geom/HeuristicOverlay.cpp b/src/geom/HeuristicOverlay.cpp
index 361e90f..8a20600 100644
--- a/src/geom/HeuristicOverlay.cpp
+++ b/src/geom/HeuristicOverlay.cpp
@@ -51,7 +51,7 @@
 #include <geos/operation/overlayng/OverlayNGRobust.h>
 
 #include <geos/simplify/TopologyPreservingSimplifier.h>
-#include <geos/operation/IsSimpleOp.h>
+#include <geos/operation/valid/IsSimpleOp.h>
 #include <geos/operation/valid/IsValidOp.h>
 #include <geos/operation/valid/TopologyValidationError.h>
 #include <geos/util/TopologyException.h>
@@ -187,7 +187,7 @@ check_valid(const Geometry& g, const std::string& label, bool doThrow = false, b
 {
     if(g.isLineal()) {
         if(! validOnly) {
-            operation::IsSimpleOp sop(g, algorithm::BoundaryNodeRule::getBoundaryEndPoint());
+            operation::valid::IsSimpleOp sop(g, algorithm::BoundaryNodeRule::getBoundaryEndPoint());
             if(! sop.isSimple()) {
                 if(doThrow) {
                     throw geos::util::TopologyException(
@@ -201,7 +201,7 @@ check_valid(const Geometry& g, const std::string& label, bool doThrow = false, b
         operation::valid::IsValidOp ivo(&g);
         if(! ivo.isValid()) {
             using operation::valid::TopologyValidationError;
-            TopologyValidationError* err = ivo.getValidationError();
+            const TopologyValidationError* err = ivo.getValidationError();
 #if GEOS_DEBUG_HEURISTICOVERLAY
             std::cerr << label << " is INVALID: "
                       << err->toString()
@@ -258,7 +258,7 @@ fix_self_intersections(std::unique_ptr<Geometry> g, const std::string& label)
     // Not all invalidities can be fixed by this code
 
     using operation::valid::TopologyValidationError;
-    TopologyValidationError* err = ivo.getValidationError();
+    const TopologyValidationError* err = ivo.getValidationError();
     switch(err->getErrorType()) {
     case TopologyValidationError::eRingSelfIntersection:
     case TopologyValidationError::eTooFewPoints: // collapsed lines
@@ -634,6 +634,7 @@ HeuristicOverlay(const Geometry* g0, const Geometry* g1, int opCode)
                 std::cerr << "Reduced with scale (" << scale << "): "
                           << ex.what() << std::endl;
 #endif
+                (void)ex; // quiet compiler warning about unused variable
                 if(scale == 1) {
                     throw;
                 }
diff --git a/src/operation/buffer/BufferBuilder.cpp b/src/operation/buffer/BufferBuilder.cpp
index d5f359a..9da2749 100644
--- a/src/operation/buffer/BufferBuilder.cpp
+++ b/src/operation/buffer/BufferBuilder.cpp
@@ -158,16 +158,14 @@ BufferBuilder::bufferLineSingleSided(const Geometry* g, double distance,
     BufferParameters modParams = bufParams;
     modParams.setEndCapStyle(BufferParameters::CAP_FLAT);
     modParams.setSingleSided(false); // ignore parameter for areal-only geometries
-    std::unique_ptr<Geometry> buf;
+
 
     // This is a (temp?) hack to workaround the fact that
     // BufferBuilder BufferParameters are immutable after
     // construction, while we want to force the end cap
     // style to FLAT for single-sided buffering
-    {
-        BufferBuilder tmp(modParams);
-        buf.reset(tmp.buffer(l, distance));
-    }
+    BufferBuilder tmpBB(modParams);
+    std::unique_ptr<Geometry> buf(tmpBB.buffer(l, distance));
 
     // Create MultiLineStrings from this polygon.
     std::unique_ptr<Geometry> bufLineString(buf->getBoundary());
diff --git a/src/operation/polygonize/EdgeRing.cpp b/src/operation/polygonize/EdgeRing.cpp
index 3d99c54..ec882f9 100644
--- a/src/operation/polygonize/EdgeRing.cpp
+++ b/src/operation/polygonize/EdgeRing.cpp
@@ -31,7 +31,6 @@
 #include <geos/algorithm/Orientation.h>
 #include <geos/util/IllegalArgumentException.h>
 #include <geos/util.h> // TODO: drop this, includes too much
-#include <geos/index/strtree/STRtree.h>
 #include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
 #include <geos/geom/Location.h>
 
diff --git a/src/operation/polygonize/Polygonizer.cpp b/src/operation/polygonize/Polygonizer.cpp
index d7d1501..3f44dad 100644
--- a/src/operation/polygonize/Polygonizer.cpp
+++ b/src/operation/polygonize/Polygonizer.cpp
@@ -27,7 +27,6 @@
 #include <geos/geom/Polygon.h>
 #include <geos/geom/CoordinateArraySequence.h>
 #include <geos/util/Interrupt.h>
-#include <geos/index/strtree/STRtree.h>
 // std
 #include <vector>
 
diff --git a/src/operation/union/CascadedPolygonUnion.cpp b/src/operation/union/CascadedPolygonUnion.cpp
index ac959be..92d7bba 100644
--- a/src/operation/union/CascadedPolygonUnion.cpp
+++ b/src/operation/union/CascadedPolygonUnion.cpp
@@ -39,7 +39,7 @@
 #include <sstream>
 
 #include <geos/operation/valid/IsValidOp.h>
-#include <geos/operation/IsSimpleOp.h>
+#include <geos/operation/valid/IsSimpleOp.h>
 #include <geos/algorithm/BoundaryNodeRule.h>
 #include <geos/util/TopologyException.h>
 #include <string>
@@ -58,7 +58,7 @@ check_valid(const geos::geom::Geometry& g, const std::string& label, bool doThro
 
     if(g.isLineal()) {
         if(! validOnly) {
-            operation::IsSimpleOp sop(g, algorithm::BoundaryNodeRule::getBoundaryEndPoint());
+            operation::valid::IsSimpleOp sop(g, algorithm::BoundaryNodeRule::getBoundaryEndPoint());
             if(! sop.isSimple()) {
                 if(doThrow) {
                     throw geos::util::TopologyException(
diff --git a/src/operation/valid/IndexedNestedHoleTester.cpp b/src/operation/valid/IndexedNestedHoleTester.cpp
new file mode 100644
index 0000000..9df6c6e
--- /dev/null
+++ b/src/operation/valid/IndexedNestedHoleTester.cpp
@@ -0,0 +1,82 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/PointLocation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Polygon.h>
+#include <geos/index/strtree/TemplateSTRtree.h>
+#include <geos/operation/valid/IndexedNestedHoleTester.h>
+#include <geos/operation/valid/PolygonTopologyAnalyzer.h>
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+/* private */
+void
+IndexedNestedHoleTester::loadIndex()
+{
+    for (std::size_t i = 0; i < polygon->getNumInteriorRing(); i++) {
+        const LinearRing* hole = static_cast<const LinearRing*>(polygon->getInteriorRingN(i));
+        const Envelope* env = hole->getEnvelopeInternal();
+        index.insert(*env, hole);
+    }
+}
+
+/* public */
+bool
+IndexedNestedHoleTester::isNested()
+{
+    for (std::size_t i = 0; i < polygon->getNumInteriorRing(); i++) {
+        const LinearRing* hole = static_cast<const LinearRing*>(polygon->getInteriorRingN(i));
+
+        std::vector<const LinearRing*> results;
+        index.query(*(hole->getEnvelopeInternal()), results);
+
+        for (const LinearRing* testHole: results) {
+            if (hole == testHole)
+                continue;
+
+            /**
+             * Hole is not fully covered by test hole, so cannot be nested
+             */
+            if (! testHole->getEnvelopeInternal()->covers(hole->getEnvelopeInternal()))
+                continue;
+
+            /**
+             * Checks nesting via a point-in-polygon test,
+             * or if the point lies on the boundary via
+             * the topology of the incident edges.
+             */
+            const Coordinate& holePt0 = hole->getCoordinateN(0);
+            const Coordinate& holePt1 = hole->getCoordinateN(1);
+            if (PolygonTopologyAnalyzer::isSegmentInRing(&holePt0, &holePt1, testHole)) {
+                nestedPt = holePt0;
+                return true;
+            }
+        }
+    }
+    return false;
+}
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/IndexedNestedPolygonTester.cpp b/src/operation/valid/IndexedNestedPolygonTester.cpp
new file mode 100644
index 0000000..18f5c47
--- /dev/null
+++ b/src/operation/valid/IndexedNestedPolygonTester.cpp
@@ -0,0 +1,198 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Envelope.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/MultiPolygon.h>
+#include <geos/geom/Polygon.h>
+#include <geos/geom/Location.h>
+#include <geos/index/SpatialIndex.h>
+#include <geos/index/strtree/STRtree.h>
+#include <geos/operation/valid/PolygonTopologyAnalyzer.h>
+#include <geos/operation/valid/IndexedNestedPolygonTester.h>
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+
+/* public */
+IndexedNestedPolygonTester::IndexedNestedPolygonTester(const MultiPolygon* p_multiPoly)
+    : multiPoly(p_multiPoly)
+    , nestedPt(Coordinate::getNull())
+{
+    loadIndex();
+}
+
+
+/* private */
+void
+IndexedNestedPolygonTester::loadIndex()
+{
+    for (std::size_t i = 0; i < multiPoly->getNumGeometries(); i++) {
+        const Polygon* poly = multiPoly->getGeometryN(i);
+        const Envelope* env = poly->getEnvelopeInternal();
+        index.insert(*env, poly);
+    }
+}
+
+
+/* private */
+IndexedPointInAreaLocator&
+IndexedNestedPolygonTester::getLocator(const Polygon* poly)
+{
+    auto search = locators.find(poly);
+
+    // Entry not found
+    if (search == locators.end())
+    {
+        // uses pair's piecewise constructor to emplace into
+        // std::map<const Polygon*, IndexedPointInAreaLocator> locators;
+        locators.emplace(std::piecewise_construct,
+            std::forward_as_tuple(poly),
+            std::forward_as_tuple(*poly));
+        auto search2 = locators.find(poly);
+        return search2->second;
+    }
+
+    IndexedPointInAreaLocator& locator = search->second;
+    return locator;
+}
+
+
+/* public */
+bool
+IndexedNestedPolygonTester::isNested()
+{
+    for (std::size_t i = 0; i < multiPoly->getNumGeometries(); i++) {
+        const Polygon* poly = multiPoly->getGeometryN(i);
+        const LinearRing* shell = poly->getExteriorRing();
+
+        std::vector<const Polygon*> results;
+        index.query(*(poly->getEnvelopeInternal()), results);
+
+        for (const Polygon* possibleOuterPoly: results) {
+
+            if (poly == possibleOuterPoly)
+                continue;
+            /**
+             * If polygon is not fully covered by candidate polygon it cannot be nested
+             */
+            if (! possibleOuterPoly->getEnvelopeInternal()->covers(poly->getEnvelopeInternal()))
+                continue;
+
+            bool gotNestedPt = findNestedPoint(shell, possibleOuterPoly, getLocator(possibleOuterPoly), nestedPt);
+            if (gotNestedPt)
+                return true;
+        }
+    }
+    return false;
+}
+
+
+/* private */
+bool
+IndexedNestedPolygonTester::findNestedPoint(
+    const LinearRing* shell,
+    const Polygon* possibleOuterPoly,
+    IndexedPointInAreaLocator& locator,
+    Coordinate& coordNested)
+{
+    /**
+     * Try checking two points, since checking point location is fast.
+     */
+    const Coordinate& shellPt0 = shell->getCoordinateN(0);
+    Location loc0 = locator.locate(&shellPt0);
+    if (loc0 == Location::EXTERIOR) return false;
+    if (loc0 == Location::INTERIOR) {
+        coordNested = shellPt0;
+        return true;
+    }
+
+    const Coordinate& shellPt1 = shell->getCoordinateN(0);
+    Location loc1 = locator.locate(&shellPt1);
+    if (loc1 == Location::EXTERIOR) return false;
+    if (loc1 == Location::INTERIOR) {
+        coordNested = shellPt1;
+        return true;
+    }
+
+    /**
+     * The shell points both lie on the boundary of
+     * the polygon.
+     * Nesting can be checked via the topology of the incident edges.
+     */
+    return findSegmentInPolygon(shell, possibleOuterPoly, coordNested);
+}
+
+
+/**
+* Finds a point of a shell segment which lies inside a polygon, if any.
+* The shell is assume to touch the polyon only at shell vertices,
+* and does not cross the polygon.
+*
+* @param the shell to test
+* @param the polygon to test against
+* @return an interior segment point, or null if the shell is nested correctly
+*/
+/* private static */
+bool
+IndexedNestedPolygonTester::findSegmentInPolygon(
+    const LinearRing* shell,
+    const Polygon* poly,
+    Coordinate& coordNested)
+{
+    const LinearRing* polyShell = poly->getExteriorRing();
+    if (polyShell->isEmpty())
+        return false;
+
+    const Coordinate& shell0 = shell->getCoordinateN(0);
+    const Coordinate& shell1 = shell->getCoordinateN(1);
+
+    if (! PolygonTopologyAnalyzer::isSegmentInRing(&shell0, &shell1, polyShell))
+        return false;
+
+    /**
+     * Check if the shell is inside a hole (if there are any).
+     * If so this is valid.
+     */
+    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+        const LinearRing* hole = poly->getInteriorRingN(i);
+        if (hole->getEnvelopeInternal()->covers(shell->getEnvelopeInternal())
+            && PolygonTopologyAnalyzer::isSegmentInRing(&shell0, &shell1, hole))
+        {
+            return false;
+        }
+    }
+
+    /**
+     * The shell is contained in the polygon, but is not contained in a hole.
+     * This is invalid.
+     */
+    coordNested = shell0;
+    return true;
+}
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/IndexedNestedRingTester.cpp b/src/operation/valid/IndexedNestedRingTester.cpp
deleted file mode 100644
index 390c24d..0000000
--- a/src/operation/valid/IndexedNestedRingTester.cpp
+++ /dev/null
@@ -1,106 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2009 Sandro Santilli <strk at kbt.io>
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/IndexedNestedRingTester.java r399 (JTS-1.12)
- *
- **********************************************************************/
-
-#include <geos/geom/LinearRing.h> // for use
-#include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
-#include <geos/operation/valid/IsValidOp.h> // for use (findPtNotNode)
-#include <geos/index/strtree/STRtree.h> // for use
-#include <geos/geom/Location.h>
-#include <geos/operation/valid/IndexedNestedRingTester.h>
-
-// Forward declarations
-namespace geos {
-namespace geom {
-class CoordinateSequence;
-class Envelope;
-}
-}
-
-namespace geos {
-namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-bool
-IndexedNestedRingTester::isNonNested()
-{
-    buildIndex();
-
-    std::vector<const geom::LinearRing*> results;
-    for(const auto& outerRing : rings) {
-        results.clear();
-
-        geos::algorithm::locate::IndexedPointInAreaLocator locator(*outerRing);
-
-        index->query(*outerRing->getEnvelopeInternal(), results);
-        for(const auto& possibleInnerRing : results) {
-            const geom::CoordinateSequence* possibleInnerRingPts = possibleInnerRing->getCoordinatesRO();
-
-            if(outerRing == possibleInnerRing) {
-                continue;
-            }
-
-            if(!outerRing->getEnvelopeInternal()->covers(possibleInnerRing->getEnvelopeInternal())) {
-                continue;
-            }
-
-            const geom::Coordinate* innerRingPt =
-                IsValidOp::findPtNotNode(possibleInnerRingPts,
-                                         outerRing,
-                                         graph);
-
-            /*
-             * If no non-node pts can be found, this means
-             * that the possibleInnerRing touches ALL of the outerRing vertices.
-             * This indicates an invalid polygon, since either
-             * the two holes create a disconnected interior,
-             * or they touch in an infinite number of points
-             * (i.e. along a line segment).
-             * Both of these cases are caught by other tests,
-             * so it is safe to simply skip this situation here.
-             */
-            if(! innerRingPt) {
-                continue;
-            }
-
-            bool isInside = locator.locate(innerRingPt) != geom::Location::EXTERIOR;
-
-            if(isInside) {
-                nestedPt = innerRingPt;
-                return false;
-            }
-
-        }
-    }
-
-    return true;
-}
-
-IndexedNestedRingTester::~IndexedNestedRingTester() = default;
-
-void
-IndexedNestedRingTester::buildIndex()
-{
-    index.reset(new index::strtree::TemplateSTRtree<const geom::LinearRing*>(10, rings.size()));
-    for(const auto& ring : rings) {
-        index->insert(ring);
-    }
-}
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
diff --git a/src/operation/valid/IndexedNestedShellTester.cpp b/src/operation/valid/IndexedNestedShellTester.cpp
deleted file mode 100644
index ea0cbf0..0000000
--- a/src/operation/valid/IndexedNestedShellTester.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2019 Daniel Baston <dbaston at gmail.com>
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************/
-
-#include <geos/algorithm/PointLocation.h>
-#include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
-#include <geos/geom/Polygon.h>
-#include <geos/index/strtree/TemplateSTRtree.h>
-#include <geos/operation/valid/IndexedNestedRingTester.h>
-#include <geos/operation/valid/IndexedNestedShellTester.h>
-#include <geos/operation/valid/IsValidOp.h>
-
-#include <deque>
-
-namespace geos {
-namespace operation {
-namespace valid {
-
-class PolygonIndexedLocators {
-
-public:
-    using Locator = algorithm::locate::IndexedPointInAreaLocator;
-
-    PolygonIndexedLocators(const geom::Polygon & p) :
-        poly(p),
-        shellLoc(*poly.getExteriorRing())
-    {
-        auto n = poly.getNumInteriorRing();
-        for (std::size_t i = 0; i < n; i++) {
-            ringLoc.emplace_back(*poly.getInteriorRingN(i));
-        }
-    }
-
-    Locator& getShellLocator() {
-        return shellLoc;
-    }
-
-    Locator& getHoleLocator(std::size_t holeNum) {
-        return ringLoc[holeNum];
-    }
-
-    const geom::Polygon* getPolygon() const {
-        return &poly;
-    }
-
-    const geom::LinearRing* getInteriorRingN(std::size_t n) const {
-        return poly.getInteriorRingN(n);
-    }
-
-private:
-    const geom::Polygon& poly;
-    Locator shellLoc;
-    std::deque<Locator> ringLoc;
-};
-
-IndexedNestedShellTester::IndexedNestedShellTester(const geos::geomgraph::GeometryGraph &g, std::size_t initialCapacity) :
-    graph(g),
-    nestedPt(nullptr),
-    processed(false)
-{
-    polys.reserve(initialCapacity);
-}
-
-bool
-IndexedNestedShellTester::isNonNested() {
-    return getNestedPoint() == nullptr;
-}
-
-const geom::Coordinate*
-IndexedNestedShellTester::getNestedPoint() {
-    compute();
-
-    return nestedPt;
-}
-
-void
-IndexedNestedShellTester::compute() {
-    if (processed) {
-        return;
-    }
-
-    processed = true;
-
-    index::strtree::TemplateSTRtree<const geom::LinearRing*> tree;
-    for (const auto& p : polys) {
-        tree.insert(p->getExteriorRing());
-    }
-
-    std::vector<const geom::LinearRing*> hits;
-    for (const auto& outerPoly : polys) {
-        hits.clear();
-
-        PolygonIndexedLocators locs(*outerPoly);
-        const geom::LinearRing* outerShell = outerPoly->getExteriorRing();
-
-        tree.query(*outerShell->getEnvelopeInternal(), hits);
-
-        for (const auto& potentialInnerShell : hits) {
-            if (potentialInnerShell == outerShell) {
-                continue;
-            }
-
-            // check if p1 can possibly by inside p2
-            if (!outerShell->getEnvelopeInternal()->covers(potentialInnerShell->getEnvelopeInternal())) {
-                continue;
-            }
-
-            checkShellNotNested(potentialInnerShell, locs);
-
-            if (nestedPt != nullptr) {
-                return;
-            }
-        }
-
-    }
-}
-
-/*private*/
-void
-IndexedNestedShellTester::checkShellNotNested(const geom::LinearRing* shell, PolygonIndexedLocators & locs)
-{
-    const geom::CoordinateSequence* shellPts = shell->getCoordinatesRO();
-
-    // test if shell is inside polygon shell
-    const geom::LinearRing* polyShell = locs.getPolygon()->getExteriorRing();
-    const geom::Coordinate* shellPt = IsValidOp::findPtNotNode(shellPts, polyShell, &graph);
-
-    // if no non-node point is found, we can assume that the shell
-    // is outside the polygon ==> Valid
-    if(shellPt == nullptr) {
-        return;
-    }
-
-    // check if shell is outside the test polygon shell ==> Valid
-    bool insidePolyShell = locs.getShellLocator().locate(shellPt) != geom::Location::EXTERIOR;
-    if(!insidePolyShell) {
-        return;
-    }
-
-    // if no holes then the shell is inside the test polygon ==> INVALID
-    auto nholes = locs.getPolygon()->getNumInteriorRing();
-    if (nholes == 0) {
-        nestedPt = shellPt;
-        return;
-    }
-
-    // Check if the shell is inside one of the holes.
-    // This is the case if one of the calls to checkShellInsideHole
-    // returns a null coordinate.
-    const geom::Coordinate* badNestedPt = nullptr;
-    for (std::size_t i = 0; i < nholes; i++) {
-        const geom::LinearRing* hole = locs.getPolygon()->getInteriorRingN(i);
-
-        if (hole->getEnvelopeInternal()->covers(shell->getEnvelopeInternal())) {
-            badNestedPt = checkShellInsideHole(shell, locs.getHoleLocator(i));
-            // if no bad point found then shell is inside hole ==> Valid
-            if(badNestedPt == nullptr) {
-                return;
-            }
-
-        }
-    }
-    // The shell pt is inside polygon but not inside any hole ==> INVALID
-    nestedPt = shellPt;
-}
-
-
-const geom::Coordinate*
-IndexedNestedShellTester::checkShellInsideHole(const geom::LinearRing* shell,
-        algorithm::locate::IndexedPointInAreaLocator & holeLoc) {
-
-    const geom::CoordinateSequence* shellPts = shell->getCoordinatesRO();
-    const geom::LinearRing* hole = static_cast<const geom::LinearRing*>(&holeLoc.getGeometry());
-    const geom::CoordinateSequence* holePts = hole->getCoordinatesRO();
-
-    const geom::Coordinate* shellPtNotOnHole = IsValidOp::findPtNotNode(shellPts, hole, &graph);
-
-    if (shellPtNotOnHole) {
-        // Found a point not on the hole boundary. Is it outside the hole?
-        if (holeLoc.locate(shellPtNotOnHole) == geom::Location::EXTERIOR) {
-            return shellPtNotOnHole;
-        }
-    }
-
-    const geom::Coordinate* holePt = IsValidOp::findPtNotNode(holePts, shell, &graph);
-    // if point is on hole but not on shell, check that the hole is outside the shell
-
-    if (holePt != nullptr) {
-        if (algorithm::PointLocation::isInRing(*holePt, shellPts)) {
-            return holePt;
-        }
-
-        return nullptr;
-    }
-
-    // should never reach here: points in hole and shell appear to be equal
-    throw util::GEOSException("Hole and shell appear to be equal in IndexedNestedShellTester");
-}
-
-}
-}
-}
diff --git a/src/operation/IsSimpleOp.cpp b/src/operation/valid/IsSimpleOp.cpp
similarity index 91%
rename from src/operation/IsSimpleOp.cpp
rename to src/operation/valid/IsSimpleOp.cpp
index 68c13a4..3659638 100644
--- a/src/operation/IsSimpleOp.cpp
+++ b/src/operation/valid/IsSimpleOp.cpp
@@ -15,7 +15,7 @@
  *
  **********************************************************************/
 
-#include <geos/operation/IsSimpleOp.h>
+#include <geos/operation/valid/IsSimpleOp.h>
 
 #include <geos/algorithm/BoundaryNodeRule.h>
 #include <geos/algorithm/LineIntersector.h>
@@ -43,6 +43,7 @@ using namespace geos::geom::util;
 
 namespace geos {
 namespace operation {
+namespace valid {
 
 
 /* public static */
@@ -113,16 +114,25 @@ bool
 IsSimpleOp::computeSimple(const Geometry& geom)
 {
     if (geom.isEmpty()) return true;
-    if (geom.getGeometryTypeId() == GEOS_POINT) return true;
-    if (geom.getGeometryTypeId() == GEOS_MULTIPOINT) return isSimpleMultiPoint(dynamic_cast<const MultiPoint&>(geom));
-    if (geom.getGeometryTypeId() == GEOS_LINESTRING) return isSimpleLinearGeometry(geom);
-    if (geom.getGeometryTypeId() == GEOS_MULTILINESTRING) return isSimpleLinearGeometry(geom);
-    if (geom.getGeometryTypeId() == GEOS_LINEARRING) return isSimplePolygonal(geom);
-    if (geom.getGeometryTypeId() == GEOS_POLYGON) return isSimplePolygonal(geom);
-    if (geom.getGeometryTypeId() == GEOS_MULTIPOLYGON) return isSimplePolygonal(geom);
-    if (geom.getGeometryTypeId() == GEOS_GEOMETRYCOLLECTION) return isSimpleGeometryCollection(geom);
-    // all other geometry types are simple by definition
-    return true;
+    switch(geom.getGeometryTypeId()) {
+        case GEOS_MULTIPOINT:
+            return isSimpleMultiPoint(dynamic_cast<const MultiPoint&>(geom));
+        case GEOS_LINESTRING:
+            return isSimpleLinearGeometry(geom);
+        case GEOS_MULTILINESTRING:
+            return isSimpleLinearGeometry(geom);
+        case GEOS_LINEARRING:
+            return isSimplePolygonal(geom);
+        case GEOS_POLYGON:
+            return isSimplePolygonal(geom);
+        case GEOS_MULTIPOLYGON:
+            return isSimplePolygonal(geom);
+        case GEOS_GEOMETRYCOLLECTION:
+            return isSimpleGeometryCollection(geom);
+        // all other geometry types are simple by definition
+        default:
+            return true;
+    }
 }
 
 /* private */
@@ -355,5 +365,6 @@ IsSimpleOp::NonSimpleIntersectionFinder::isDone() const
 // --------------------------------------------------------------------------------
 
 
+} // geos.operation.valid
 } // geos.operation
 } // geos
diff --git a/src/operation/valid/IsValidOp.cpp b/src/operation/valid/IsValidOp.cpp
index 5c89f34..df23de5 100644
--- a/src/operation/valid/IsValidOp.cpp
+++ b/src/operation/valid/IsValidOp.cpp
@@ -1,599 +1,481 @@
 /**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2010 Safe Software Inc.
- * Copyright (C) 2010 Sandro Santilli <strk at kbt.io>
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- * Copyright (C) 2005 Refractions Research Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/IsValidOp.java r335 (JTS-1.12)
- *
- **********************************************************************/
-
-#include <geos/export.h>
-#include <geos/constants.h>
-#include <geos/algorithm/LineIntersector.h>
-#include <geos/algorithm/PointLocation.h>
+*
+* GEOS - Geometry Engine Open Source
+* http://geos.osgeo.org
+*
+* Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+* Copyright (C) 2021 Martin Davis
+*
+* This is free software; you can redistribute and/or modify it under
+* the terms of the GNU Lesser General Public Licence as published
+* by the Free Software Foundation.
+* See the COPYING file for more information.
+*
+**********************************************************************/
+
 #include <geos/algorithm/locate/IndexedPointInAreaLocator.h>
-#include <geos/geom/CoordinateSequence.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Geometry.h>
 #include <geos/geom/GeometryCollection.h>
 #include <geos/geom/LineString.h>
 #include <geos/geom/LinearRing.h>
 #include <geos/geom/Location.h>
+#include <geos/geom/MultiPoint.h>
 #include <geos/geom/MultiPolygon.h>
 #include <geos/geom/Point.h>
 #include <geos/geom/Polygon.h>
-#include <geos/geomgraph/GeometryGraph.h>
-#include <geos/geomgraph/Edge.h>
-#include <geos/geomgraph/index/SegmentIntersector.h>
-#include <geos/index/chain/MonotoneChainSelectAction.h>
-#include <geos/operation/valid/ConnectedInteriorTester.h>
-#include <geos/operation/valid/ConsistentAreaTester.h>
 #include <geos/operation/valid/IsValidOp.h>
-#include <geos/operation/valid/IndexedNestedRingTester.h>
-#include <geos/operation/valid/IndexedNestedShellTester.h>
+#include <geos/operation/valid/IndexedNestedHoleTester.h>
+#include <geos/operation/valid/IndexedNestedPolygonTester.h>
 #include <geos/util/UnsupportedOperationException.h>
 
-
-#include <cassert>
 #include <cmath>
-#include <typeinfo>
-#include <set>
-
 
-using namespace geos::algorithm;
-using namespace geos::geomgraph;
 using namespace geos::geom;
+using geos::algorithm::locate::IndexedPointInAreaLocator;
 
-namespace geos {
+namespace geos {      // geos
 namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-/**
- * Find a point from the list of testCoords
- * that is NOT a node in the edge for the list of searchCoords
- *
- * @return the point found, or <code>null</code> if none found
- */
-const Coordinate*
-IsValidOp::findPtNotNode(const CoordinateSequence* testCoords,
-                         const LinearRing* searchRing, const GeometryGraph* graph)
-{
-    // find edge corresponding to searchRing.
-    Edge* searchEdge = graph->findEdge(searchRing);
-    // find a point in the testCoords which is not a node of the searchRing
-    EdgeIntersectionList& eiList = searchEdge->getEdgeIntersectionList();
-    // somewhat inefficient - is there a better way? (Use a node map, for instance?)
-    auto npts = testCoords->getSize();
-    for(unsigned int i = 0; i < npts; ++i) {
-        const Coordinate& pt = testCoords->getAt(i);
-        if(!eiList.isIntersection(pt)) {
-            return &pt;
-        }
-    }
-    return nullptr;
-}
-
+namespace valid {     // geos.operation.valid
 
+/* public */
 bool
 IsValidOp::isValid()
 {
-    checkValid();
-    return validErr == nullptr;
+    return isValidGeometry(inputGeometry);
 }
 
-/* static public */
+
+/* public static */
 bool
-IsValidOp::isValid(const Coordinate& coord)
+IsValidOp::isValid(const Coordinate* coord)
 {
-    if(! std::isfinite(coord.x)) {
-        return false;
+    if (std::isfinite(coord->x) && std::isfinite(coord->y)) {
+        return true;
     }
-    if(! std::isfinite(coord.y)) {
+    else {
         return false;
     }
-    return true;
 }
 
-/* static public */
-bool
-IsValidOp::isValid(const Geometry& g)
-{
-    IsValidOp op(&g);
-    return op.isValid();
-}
 
-TopologyValidationError*
+/* public */
+const TopologyValidationError *
 IsValidOp::getValidationError()
 {
-    checkValid();
-    return validErr;
+    isValidGeometry(inputGeometry);
+    return validErr.get();
 }
 
+
+/* private */
 void
-IsValidOp::checkValid()
+IsValidOp::logInvalid(int code, const Coordinate* pt)
 {
-    if(isChecked) {
-        return;
-    }
-    checkValid(parentGeometry);
-    isChecked = true;
+    validErr.reset(new TopologyValidationError(code, *pt));
 }
 
-void
-IsValidOp::checkValid(const Geometry* g)
-{
-    assert(validErr == nullptr);
 
-    if(nullptr == g) {
-        return;
-    }
+/* private */
+bool
+IsValidOp::isValidGeometry(const Geometry* g)
+{
+    validErr.reset(nullptr);
+
+    // empty geometries are always valid
+    if (g->isEmpty()) return true;
+    switch (g->getGeometryTypeId()) {
+        case GEOS_POINT:
+            return isValid(static_cast<const Point*>(g));
+        case GEOS_MULTIPOINT:
+            return isValid(static_cast<const MultiPoint*>(g));
+        case GEOS_LINEARRING:
+            return isValid(static_cast<const LinearRing*>(g));
+        case GEOS_LINESTRING:
+            return isValid(static_cast<const LineString*>(g));
+        case GEOS_POLYGON:
+            return isValid(static_cast<const Polygon*>(g));
+        case GEOS_MULTIPOLYGON:
+            return isValid(static_cast<const MultiPolygon*>(g));
+        case GEOS_MULTILINESTRING:
+            return isValid(static_cast<const GeometryCollection*>(g));
+        case GEOS_GEOMETRYCOLLECTION:
+            return isValid(static_cast<const GeometryCollection*>(g));
+    }
+
+    // geometry type not known
+    throw util::UnsupportedOperationException(g->getGeometryType());
+}
 
-    // empty geometries are always valid!
-    if(g->isEmpty()) {
-        return;
-    }
 
-    if(const Point* x1 = dynamic_cast<const Point*>(g)) {
-        checkValid(x1);
-    }
-    // LineString also handles LinearRings, so we check LinearRing first
-    else if(const LinearRing* x2 = dynamic_cast<const LinearRing*>(g)) {
-        checkValid(x2);
-    }
-    else if(const LineString* x3 = dynamic_cast<const LineString*>(g)) {
-        checkValid(x3);
-    }
-    else if(const Polygon* x4 = dynamic_cast<const Polygon*>(g)) {
-        checkValid(x4);
-    }
-    else if(const MultiPolygon* x5 = dynamic_cast<const MultiPolygon*>(g)) {
-        checkValid(x5);
-    }
-    else if(const GeometryCollection* x6 =
-                dynamic_cast<const GeometryCollection*>(g)) {
-        checkValid(x6);
-    }
-    else {
-        throw util::UnsupportedOperationException();
-    }
+/* private */
+bool
+IsValidOp::isValid(const Point* g)
+{
+    checkCoordinateInvalid(g->getCoordinatesRO());
+    if (hasInvalidError()) return false;
+    return true;
 }
 
-/*
- * Checks validity of a Point.
- */
-void
-IsValidOp::checkValid(const Point* g)
+
+/* private */
+bool
+IsValidOp::isValid(const MultiPoint* g)
 {
-    checkInvalidCoordinates(g->getCoordinatesRO());
+    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
+        const Point* p = g->getGeometryN(i);
+        if (p->isEmpty()) continue;
+        if (!isValid(p->getCoordinate())) {
+            logInvalid(TopologyValidationError::eInvalidCoordinate,
+                       p->getCoordinate());
+            return false;;
+        }
+    }
+    return true;
 }
 
-/*
- * Checks validity of a LineString.  Almost anything goes for linestrings!
- */
-void
-IsValidOp::checkValid(const LineString* g)
+
+/* private */
+bool
+IsValidOp::isValid(const LineString* g)
 {
-    checkInvalidCoordinates(g->getCoordinatesRO());
-    if(validErr != nullptr) {
-        return;
-    }
+    checkCoordinateInvalid(g->getCoordinatesRO());
+    if (hasInvalidError()) return false;
 
-    GeometryGraph graph(0, g);
-    checkTooFewPoints(&graph);
+    checkTooFewPoints(g, MIN_SIZE_LINESTRING);
+    if (hasInvalidError()) return false;
+
+    return true;
 }
 
-/**
- * Checks validity of a LinearRing.
- */
-void
-IsValidOp::checkValid(const LinearRing* g)
+
+/* private */
+bool
+IsValidOp::isValid(const LinearRing* g)
 {
-    checkInvalidCoordinates(g->getCoordinatesRO());
-    if(validErr != nullptr) {
-        return;
-    }
+    checkCoordinateInvalid(g->getCoordinatesRO());
+    if (hasInvalidError()) return false;
 
-    checkClosedRing(g);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkRingNotClosed(g);
+    if (hasInvalidError()) return false;
 
-    GeometryGraph graph(0, g);
-    checkTooFewPoints(&graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkRingTooFewPoints(g);
+    if (hasInvalidError()) return false;
+
+    checkSelfIntersectingRing(g);
+    if (hasInvalidError()) return false;
 
-    LineIntersector li;
-    graph.computeSelfNodes(&li, true, true);
-    checkNoSelfIntersectingRings(&graph);
+    return true;
 }
 
-/**
- * Checks the validity of a polygon.
- * Sets the validErr flag.
- */
-void
-IsValidOp::checkValid(const Polygon* g)
+
+/* private */
+bool
+IsValidOp::isValid(const Polygon* g)
 {
-    checkInvalidCoordinates(g);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkCoordinateInvalid(g);
+    if (hasInvalidError()) return false;
 
-    checkClosedRings(g);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkRingsNotClosed(g);
+    if (hasInvalidError()) return false;
 
-    GeometryGraph graph(0, g);
+    checkRingsTooFewPoints(g);
+    if (hasInvalidError()) return false;
 
-    checkTooFewPoints(&graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    PolygonTopologyAnalyzer areaAnalyzer(g, isInvertedRingValid);
 
-    checkConsistentArea(&graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkAreaIntersections(areaAnalyzer);
+    if (hasInvalidError()) return false;
 
-    if(!isSelfTouchingRingFormingHoleValid) {
-        checkNoSelfIntersectingRings(&graph);
-        if(validErr != nullptr) {
-            return;
-        }
-    }
+    checkHolesOutsideShell(g);
+    if (hasInvalidError()) return false;
 
-    checkHolesInShell(g, &graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkHolesNested(g);
+    if (hasInvalidError()) return false;
 
-    checkHolesNotNested(g, &graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkInteriorDisconnected(areaAnalyzer);
+    if (hasInvalidError()) return false;
 
-    checkConnectedInteriors(graph);
+    return true;
 }
 
-void
-IsValidOp::checkValid(const MultiPolygon* g)
-{
-    auto ngeoms = g->getNumGeometries();
-    std::vector<const Polygon*>polys(ngeoms);
 
-    for(std::size_t i = 0; i < ngeoms; ++i) {
+/* private */
+bool
+IsValidOp::isValid(const MultiPolygon* g)
+{
+    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
         const Polygon* p = g->getGeometryN(i);
+        checkCoordinateInvalid(p);
+        if (hasInvalidError()) return false;
 
-        checkInvalidCoordinates(p);
-        if(validErr != nullptr) {
-            return;
-        }
+        checkRingsNotClosed(p);
+        if (hasInvalidError()) return false;
 
-        checkClosedRings(p);
-        if(validErr != nullptr) {
-            return;
-        }
-
-        polys[i] = p;
+        checkRingsTooFewPoints(p);
+        if (hasInvalidError()) return false;
     }
 
-    GeometryGraph graph(0, g);
+    PolygonTopologyAnalyzer areaAnalyzer(g, isInvertedRingValid);
 
-    checkTooFewPoints(&graph);
-    if(validErr != nullptr) {
-        return;
-    }
+    checkAreaIntersections(areaAnalyzer);
+    if (hasInvalidError()) return false;
 
-    checkConsistentArea(&graph);
-    if(validErr != nullptr) {
-        return;
+    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
+        const Polygon* p = g->getGeometryN(i);
+        checkHolesOutsideShell(p);
+        if (hasInvalidError()) return false;
     }
 
-    if(!isSelfTouchingRingFormingHoleValid) {
-        checkNoSelfIntersectingRings(&graph);
-        if(validErr != nullptr) {
-            return;
-        }
+    for (std::size_t i = 0; i < g->getNumGeometries(); i++) {
+        const Polygon* p = g->getGeometryN(i);
+        checkHolesNested(p);
+        if (hasInvalidError()) return false;
     }
 
-    for(unsigned int i = 0; i < ngeoms; ++i) {
-        const Polygon* p = polys[i];
-        checkHolesInShell(p, &graph);
-        if(validErr != nullptr) {
-            return;
-        }
-    }
+    checkShellsNested(g);
+    if (hasInvalidError()) return false;
 
-    for(unsigned int i = 0; i < ngeoms; ++i) {
-        const Polygon* p = polys[i];
-        checkHolesNotNested(p, &graph);
-        if(validErr != nullptr) {
-            return;
-        }
-    }
+    checkInteriorDisconnected(areaAnalyzer);
+    if (hasInvalidError()) return false;
+
+    return true;
+}
 
-    if (ngeoms > 1) {
-        checkShellsNotNested(g, &graph);
-    }
-    if(validErr != nullptr) {
-        return;
-    }
 
-    checkConnectedInteriors(graph);
+/* private */
+bool
+IsValidOp::isValid(const GeometryCollection* gc)
+{
+    for (std::size_t i = 0; i < gc->getNumGeometries(); i++) {
+        if (! isValidGeometry(gc->getGeometryN(i)))
+            return false;
+    }
+    return true;
 }
 
+
+/* private */
 void
-IsValidOp::checkValid(const GeometryCollection* gc)
+IsValidOp::checkCoordinateInvalid(const CoordinateSequence* coords)
 {
-    for(std::size_t i = 0, ngeoms = gc->getNumGeometries(); i < ngeoms; ++i) {
-        const Geometry* g = gc->getGeometryN(i);
-        checkValid(g);
-        if(validErr != nullptr) {
+    for (std::size_t i = 0; i < coords->size(); i++) {
+        if (! isValid(coords->getAt(i))) {
+            logInvalid(TopologyValidationError::eInvalidCoordinate,
+                       &coords->getAt(i));
             return;
         }
     }
 }
 
+
+/* private */
 void
-IsValidOp::checkTooFewPoints(GeometryGraph* graph)
+IsValidOp::checkCoordinateInvalid(const Polygon* poly)
 {
-    if(graph->hasTooFewPoints()) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eTooFewPoints,
-            graph->getInvalidPoint());
-        return;
+    checkCoordinateInvalid(poly->getExteriorRing()->getCoordinatesRO());
+    if (hasInvalidError()) return;
+    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+        checkCoordinateInvalid(poly->getInteriorRingN(i)->getCoordinatesRO());
+        if (hasInvalidError()) return;
     }
 }
 
-/**
- * Checks that the arrangement of edges in a polygonal geometry graph
- * forms a consistent area.
- *
- * @param graph
- *
- * @see ConsistentAreaTester
- */
+
+/* private */
 void
-IsValidOp::checkConsistentArea(GeometryGraph* graph)
+IsValidOp::checkRingNotClosed(const LinearRing* ring)
 {
-    ConsistentAreaTester cat(graph);
-    bool isValidArea = cat.isNodeConsistentArea();
-
-    if(!isValidArea) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eSelfIntersection,
-            cat.getInvalidPoint());
+    if (ring->isEmpty()) return;
+    if (! ring->isClosed()) {
+        Coordinate pt = ring->getNumPoints() >= 1
+                        ? ring->getCoordinateN(0)
+                        : Coordinate();
+        logInvalid(TopologyValidationError::eRingNotClosed, &pt);
         return;
     }
-
-    if(cat.hasDuplicateRings()) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eDuplicatedRings,
-            cat.getInvalidPoint());
-    }
 }
 
 
-/*private*/
+/* private */
 void
-IsValidOp::checkNoSelfIntersectingRings(GeometryGraph* graph)
+IsValidOp::checkRingsNotClosed(const Polygon* poly)
 {
-    std::vector<Edge*>* edges = graph->getEdges();
-    for(unsigned int i = 0; i < edges->size(); ++i) {
-        Edge* e = (*edges)[i];
-        checkNoSelfIntersectingRing(e->getEdgeIntersectionList());
-        if(validErr != nullptr) {
-            return;
-        }
+    checkRingNotClosed(poly->getExteriorRing());
+    if (hasInvalidError()) return;
+    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+        checkRingNotClosed(poly->getInteriorRingN(i));
+        if (hasInvalidError()) return;
     }
 }
 
-/*private*/
-bool
-IsValidOp::isStartNode(const EdgeIntersection& ei) {
-    return ei.getSegmentIndex() == 0 && ei.getDistance() == 0.0;
-}
 
-/*private*/
+/* private */
 void
-IsValidOp::checkNoSelfIntersectingRing(EdgeIntersectionList& eiList)
+IsValidOp::checkRingsTooFewPoints(const Polygon* poly)
 {
-    std::set<const Coordinate*, CoordinateLessThen>nodeSet;
-    for(const EdgeIntersection& ei : eiList) {
-        if(isStartNode(ei)) {
-            continue;
-        }
-        if(nodeSet.find(&ei.coord) != nodeSet.end()) {
-            validErr = new TopologyValidationError(
-                TopologyValidationError::eRingSelfIntersection,
-                ei.coord);
-            return;
-        }
-        else {
-            nodeSet.insert(&ei.coord);
-        }
+    checkRingTooFewPoints(poly->getExteriorRing());
+    if (hasInvalidError()) return;
+    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+        checkRingTooFewPoints(poly->getInteriorRingN(i));
+        if (hasInvalidError()) return;
     }
 }
 
-/*private*/
+
+/* private */
 void
-IsValidOp::checkHolesInShell(const Polygon* p, GeometryGraph* graph)
+IsValidOp::checkRingTooFewPoints(const LinearRing* ring)
 {
-    auto nholes = p->getNumInteriorRing();
-    if (nholes == 0) {
-        return;
-    }
+    if (ring->isEmpty()) return;
+    checkTooFewPoints(ring, MIN_SIZE_RING);
+}
 
-    const LinearRing* shell = p->getExteriorRing();
 
-    bool isShellEmpty = shell->isEmpty();
+/* private */
+void
+IsValidOp::checkTooFewPoints(const LineString* line, std::size_t minSize)
+{
+    if (! isNonRepeatedSizeAtLeast(line, minSize) ) {
+        Coordinate pt = line->getNumPoints() >= 1
+                        ? line->getCoordinateN(0)
+                        : Coordinate();
+        logInvalid(TopologyValidationError::eTooFewPoints, &pt);
+    }
+}
 
-    locate::IndexedPointInAreaLocator ipial(*shell);
 
-    for(std::size_t i = 0; i < nholes; ++i) {
-        const LinearRing* hole = p->getInteriorRingN(i);
+/* private */
+bool
+IsValidOp::isNonRepeatedSizeAtLeast(const LineString* line, std::size_t minSize)
+{
+    std::size_t numPts = 0;
+    const Coordinate* prevPt = nullptr;
+    for (std::size_t i = 0; i < line->getNumPoints(); i++) {
+        if (numPts >= minSize) return true;
+        const Coordinate& pt = line->getCoordinateN(i);
+        if (prevPt == nullptr || ! pt.equals2D(*prevPt))
+            numPts++;
+        prevPt = &pt;
+    }
+    return numPts >= minSize;
+}
 
-        if (hole->isEmpty()) continue;
 
-        const Coordinate* holePt = findPtNotNode(hole->getCoordinatesRO(), shell, graph);
-        /*
-         * If no non-node hole vertex can be found, the hole must
-         * split the polygon into disconnected interiors.
-         * This will be caught by a subsequent check.
-         */
-        if (holePt == nullptr) return;
-        bool outside = isShellEmpty || (Location::EXTERIOR == ipial.locate(holePt));
-        if (outside) {
-            validErr = new TopologyValidationError(
-                TopologyValidationError::eHoleOutsideShell, *holePt);
-            return;
-        }
+/* private */
+void
+IsValidOp::checkAreaIntersections(PolygonTopologyAnalyzer& areaAnalyzer)
+{
+    if (areaAnalyzer.hasInvalidIntersection()) {
+        logInvalid(areaAnalyzer.getInvalidCode(),
+                   &areaAnalyzer.getInvalidLocation());
     }
-
 }
 
-/*private*/
+
+/* private */
 void
-IsValidOp::checkHolesNotNested(const Polygon* p, GeometryGraph* graph)
+IsValidOp::checkSelfIntersectingRing(const LinearRing* ring)
 {
-    //SimpleNestedRingTester nestedTester(graph);
-    //SweeplineNestedRingTester nestedTester(graph);
-    //QuadtreeNestedRingTester nestedTester(graph);
-    auto nholes = p->getNumInteriorRing();
-
-    IndexedNestedRingTester nestedTester(graph, nholes);
-    for (std::size_t i = 0; i < nholes; ++i) {
-        const LinearRing* innerHole = p->getInteriorRingN(i);
-
-        //empty holes always pass
-        if (innerHole->isEmpty()) {
-            continue;
-        }
-
-        nestedTester.add(innerHole);
-    }
-
-    bool isNonNested = nestedTester.isNonNested();
-    if (!isNonNested) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eNestedHoles,
-            *(nestedTester.getNestedPoint()));
+    Coordinate intPt = PolygonTopologyAnalyzer::findSelfIntersection(ring);
+    if (! intPt.isNull()) {
+        logInvalid(TopologyValidationError::eRingSelfIntersection,
+            &intPt);
     }
 }
 
-/*private*/
+
+/* private */
 void
-IsValidOp::checkShellsNotNested(const MultiPolygon* mp, GeometryGraph* graph)
+IsValidOp::checkHolesOutsideShell(const Polygon* poly)
 {
-    auto ngeoms = mp->getNumGeometries();
+    // skip test if no holes are present
+    if (poly->getNumInteriorRing() <= 0) return;
 
-    IndexedNestedShellTester tester(*graph, ngeoms);
+    const LinearRing* shell = poly->getExteriorRing();
+    bool isShellEmpty = shell->isEmpty();
 
-    for (std::size_t i = 0; i < ngeoms; ++i) {
-        tester.add(*mp->getGeometryN(i));
-    }
+    for (std::size_t i = 0; i < poly->getNumInteriorRing(); i++) {
+        const LinearRing* hole = poly->getInteriorRingN(i);
+        if (hole->isEmpty()) continue;
 
-    if (!tester.isNonNested()) {
-        validErr = new TopologyValidationError(TopologyValidationError::eNestedShells,
-                *tester.getNestedPoint());
+        const Coordinate* invalidPt = nullptr;
+        if (isShellEmpty) {
+            invalidPt = hole->getCoordinate();
+        }
+        else {
+            invalidPt = findHoleOutsideShellPoint(hole, shell);
+        }
+        if (invalidPt != nullptr) {
+            logInvalid(
+                TopologyValidationError::eHoleOutsideShell,
+                invalidPt);
+            return;
+        }
     }
-
 }
 
 
-/*private*/
-void
-IsValidOp::checkConnectedInteriors(GeometryGraph& graph)
+/* private */
+const Coordinate *
+IsValidOp::findHoleOutsideShellPoint(const LinearRing* hole, const LinearRing* shell)
 {
-    ConnectedInteriorTester cit(graph);
-    if(!cit.isInteriorsConnected()) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eDisconnectedInterior,
-            cit.getCoordinate());
-    }
+
+    const Coordinate& holePt0 = hole->getCoordinateN(0);
+    const Coordinate& holePt1 = hole->getCoordinateN(1);
+    /**
+     * If hole envelope is not covered by shell, it must be outside
+     */
+    if (! shell->getEnvelopeInternal()->covers(hole->getEnvelopeInternal()))
+        return &holePt0;
+
+    if (PolygonTopologyAnalyzer::isSegmentInRing(&holePt0, &holePt1, shell))
+        return nullptr;
+    return &holePt0;
 }
 
 
-/*private*/
+/* private */
 void
-IsValidOp::checkInvalidCoordinates(const CoordinateSequence* cs)
+IsValidOp::checkHolesNested(const Polygon* poly)
 {
-    auto size = cs->size();
-    for(std::size_t i = 0; i < size; ++i) {
-        if(! isValid(cs->getAt(i))) {
-            validErr = new TopologyValidationError(
-                TopologyValidationError::eInvalidCoordinate,
-                cs->getAt(i));
-            return;
+    // skip test if no holes are present
+    if (poly->getNumInteriorRing() <= 0) return;
 
-        }
+    IndexedNestedHoleTester nestedTester(poly);
+    if (nestedTester.isNested()) {
+        logInvalid(TopologyValidationError::eNestedHoles,
+                   &nestedTester.getNestedPoint());
     }
 }
 
-/*private*/
-void
-IsValidOp::checkInvalidCoordinates(const Polygon* poly)
-{
-    checkInvalidCoordinates(poly->getExteriorRing()->getCoordinatesRO());
-    if(validErr != nullptr) {
-        return;
-    }
-
-    auto nholes = poly->getNumInteriorRing();
-    for(std::size_t i = 0; i < nholes; ++i) {
-        checkInvalidCoordinates(
-            poly->getInteriorRingN(i)->getCoordinatesRO()
-        );
-        if(validErr != nullptr) {
-            return;
-        }
-    }
-}
 
-/*private*/
+/* private */
 void
-IsValidOp::checkClosedRings(const Polygon* poly)
+IsValidOp::checkShellsNested(const MultiPolygon* mp)
 {
-    const LinearRing* lr = poly->getExteriorRing();
-    checkClosedRing(lr);
-    if(validErr) {
+    // skip test if only one shell present
+    if (mp->getNumGeometries() <= 1)
         return;
-    }
 
-    auto nholes = poly->getNumInteriorRing();
-    for(std::size_t i = 0; i < nholes; ++i) {
-        lr = (const LinearRing*)poly->getInteriorRingN(i);
-        checkClosedRing(lr);
-        if(validErr) {
-            return;
-        }
+    IndexedNestedPolygonTester nestedTester(mp);
+    if (nestedTester.isNested()) {
+        logInvalid(TopologyValidationError::eNestedShells,
+                   &nestedTester.getNestedPoint());
     }
 }
 
-/*private*/
+
+/* private */
 void
-IsValidOp::checkClosedRing(const LinearRing* ring)
+IsValidOp::checkInteriorDisconnected(PolygonTopologyAnalyzer& analyzer)
 {
-    if(! ring->isClosed() && ! ring->isEmpty()) {
-        validErr = new TopologyValidationError(
-            TopologyValidationError::eRingNotClosed,
-            ring->getCoordinateN(0));
-    }
+    if (analyzer.isInteriorDisconnected())
+        logInvalid(TopologyValidationError::eDisconnectedInterior,
+                   &analyzer.getDisconnectionLocation());
 }
 
+
 } // namespace geos.operation.valid
 } // namespace geos.operation
 } // namespace geos
diff --git a/src/operation/valid/PolygonIntersectionAnalyzer.cpp b/src/operation/valid/PolygonIntersectionAnalyzer.cpp
new file mode 100644
index 0000000..fbdf35d
--- /dev/null
+++ b/src/operation/valid/PolygonIntersectionAnalyzer.cpp
@@ -0,0 +1,226 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/geom/Coordinate.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/operation/valid/PolygonIntersectionAnalyzer.h>
+#include <geos/operation/valid/PolygonNode.h>
+#include <geos/operation/valid/PolygonRing.h>
+#include <geos/operation/valid/TopologyValidationError.h>
+#include <geos/util/IllegalStateException.h>
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+
+/* public */
+void
+PolygonIntersectionAnalyzer::processIntersections(
+    SegmentString* ss0, std::size_t segIndex0,
+    SegmentString* ss1, std::size_t segIndex1)
+{
+    // don't test a segment with itself
+    bool isSameSegString = ss0 == ss1;
+    bool isSameSegment = isSameSegString && segIndex0 == segIndex1;
+    if (isSameSegment) return;
+
+    int code = findInvalidIntersection(ss0, segIndex0, ss1, segIndex1);
+    /**
+     * Ensure that invalidCode is only set once,
+     * since the short-circuiting in {@link SegmentIntersector} is not guaranteed
+     * to happen immediately.
+     */
+    if (code != TopologyValidationError::oNoInvalidIntersection) {
+        invalidCode = code;
+        invalidLocation = li.getIntersection(0);
+    }
+}
+
+/* private */
+int
+PolygonIntersectionAnalyzer::findInvalidIntersection(
+    const SegmentString* ss0, std::size_t segIndex0,
+    const SegmentString* ss1, std::size_t segIndex1)
+{
+    const Coordinate& p00 = ss0->getCoordinate(segIndex0);
+    const Coordinate& p01 = ss0->getCoordinate(segIndex0 + 1);
+    const Coordinate& p10 = ss1->getCoordinate(segIndex1);
+    const Coordinate& p11 = ss1->getCoordinate(segIndex1 + 1);
+
+    li.computeIntersection(p00, p01, p10, p11);
+
+    if (! li.hasIntersection()) {
+        return TopologyValidationError::oNoInvalidIntersection;
+    }
+
+    /**
+     * Check for an intersection in the interior of both segments.
+     * Collinear intersections by definition contain an interior intersection.
+     * They occur in either a zero-width spike or gore,
+     * or adjacent rings.
+     */
+    if (li.isProper() || li.getIntersectionNum() >= 2) {
+        return TopologyValidationError::eSelfIntersection;
+    }
+
+    /**
+     * Now know there is exactly one intersection,
+     * at a vertex of at least one segment.
+     */
+    Coordinate intPt = li.getIntersection(0);
+
+    /**
+     * If segments are adjacent the intersection must be their common endpoint.
+     * (since they are not collinear).
+     * This is valid.
+     */
+    bool isSameSegString = ss0 == ss1;
+    bool isAdjacentSegments = isSameSegString && isAdjacentInRing(ss0, segIndex0, segIndex1);
+    // Assert: intersection is an endpoint of both segs
+    if (isAdjacentSegments) return TopologyValidationError::oNoInvalidIntersection;
+
+    /**
+     * Under OGC semantics, rings cannot self-intersect.
+     * So the intersection is invalid.
+     */
+    if (isSameSegString && ! isInvertedRingValid) {
+        return TopologyValidationError::eSelfIntersection;
+    }
+
+    /**
+     * Optimization: don't analyze intPts at the endpoint of a segment.
+     * This is because they are also start points, so don't need to be
+     * evaluated twice.
+     * This simplifies following logic, by removing the segment endpoint case.
+     */
+    if (intPt.equals2D(p01) || intPt.equals2D(p11))
+        return TopologyValidationError::oNoInvalidIntersection;
+
+    /**
+     * Check topology of a vertex intersection.
+     * The ring(s) must not cross.
+     */
+    const Coordinate* e00 = &p00;
+    const Coordinate* e01 = &p01;
+    if (intPt.equals2D(p00)) {
+        e00 = &(prevCoordinateInRing(ss0, segIndex0));
+        e01 = &p01;
+    }
+    const Coordinate* e10 = &p10;
+    const Coordinate* e11 = &p11;
+    if (intPt.equals2D(p10)) {
+        e10 = &(prevCoordinateInRing(ss1, segIndex1));
+        e11 = &p11;
+    }
+    bool hasCrossing = PolygonNode::isCrossing(&intPt, e00, e01, e10, e11);
+    if (hasCrossing) {
+        return TopologyValidationError::eSelfIntersection;
+    }
+
+    /**
+     * If allowing inverted rings, record a self-touch to support later checking
+     * that it does not disconnect the interior.
+     */
+    if (isSameSegString && isInvertedRingValid) {
+        addSelfTouch(ss0, intPt, e00, e01, e10, e11);
+    }
+
+    /**
+     * If the rings are in the same polygon
+     * then record the touch to support connected interior checking.
+     *
+     * Also check for an invalid double-touch situation,
+     * if the rings are different.
+     */
+    bool isDoubleTouch = addDoubleTouch(ss0, ss1, intPt);
+    if (isDoubleTouch && ! isSameSegString) {
+        m_hasDoubleTouch = true;
+        doubleTouchLocation = intPt;
+        // TODO: for poly-hole or hole-hole touch, check if it has bad topology.  If so return invalid code
+    }
+
+    return TopologyValidationError::oNoInvalidIntersection;
+}
+
+
+/* private */
+bool
+PolygonIntersectionAnalyzer::addDoubleTouch(
+    const SegmentString* ss0, const SegmentString* ss1,
+    const Coordinate& intPt)
+{
+    return PolygonRing::addTouch(
+        const_cast<PolygonRing*>(static_cast<const PolygonRing*>(ss0->getData())),
+        const_cast<PolygonRing*>(static_cast<const PolygonRing*>(ss1->getData())),
+        intPt);
+}
+
+/* private */
+void
+PolygonIntersectionAnalyzer::addSelfTouch(
+    const SegmentString* ss, const Coordinate& intPt,
+    const Coordinate* e00, const Coordinate* e01,
+    const Coordinate* e10, const Coordinate* e11)
+{
+    const PolygonRing* constPolyRing = static_cast<const PolygonRing*>(ss->getData());
+    PolygonRing* polyRing = const_cast<PolygonRing*>(constPolyRing);
+    if (polyRing == nullptr) {
+        throw util::IllegalStateException("SegmentString missing PolygonRing data when checking self-touches");
+    }
+    polyRing->addSelfTouch(intPt, e00, e01, e10, e11);
+}
+
+/* private */
+const Coordinate&
+PolygonIntersectionAnalyzer::prevCoordinateInRing(
+    const SegmentString* ringSS, std::size_t segIndex) const
+{
+    std::size_t prevIndex;
+    if (segIndex == 0) {
+        prevIndex = ringSS->size() - 2;
+    }
+    else {
+        prevIndex = segIndex - 1;
+    }
+    return ringSS->getCoordinate(prevIndex);
+}
+
+/* private */
+bool
+PolygonIntersectionAnalyzer::isAdjacentInRing(const SegmentString* ringSS,
+    std::size_t segIndex0, std::size_t segIndex1) const
+{
+    std::size_t delta = segIndex0 > segIndex1
+                        ? segIndex0 - segIndex1
+                        : segIndex1 - segIndex0 ;
+
+    if (delta <= 1) return true;
+    /**
+     * A string with N vertices has maximum segment index of N-2.
+     * If the delta is at least N-2, the segments must be
+     * at the start and end of the string and thus adjacent.
+     */
+    if (delta >= ringSS->size() - 2) return true;
+    return false;
+}
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/PolygonNode.cpp b/src/operation/valid/PolygonNode.cpp
new file mode 100644
index 0000000..73821a1
--- /dev/null
+++ b/src/operation/valid/PolygonNode.cpp
@@ -0,0 +1,114 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Quadrant.h>
+#include <geos/operation/valid/PolygonNode.h>
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+
+/* public static */
+bool
+PolygonNode::isCrossing(const Coordinate* nodePt, const Coordinate* a0, const Coordinate* a1, const Coordinate* b0, const Coordinate* b1)
+{
+    const Coordinate* aLo = a0;
+    const Coordinate* aHi = a1;
+    if (isAngleGreater(nodePt, aLo, aHi)) {
+        aLo = a1;
+        aHi = a0;
+    }
+    /**
+     * Find positions of b0 and b1.
+     * If they are the same they do not cross the other edge
+     */
+    bool isBetween0 = isBetween(nodePt, b0, aLo, aHi);
+    bool isBetween1 = isBetween(nodePt, b1, aLo, aHi);
+
+    return isBetween0 != isBetween1;
+}
+
+
+/* public static */
+bool
+PolygonNode::isInteriorSegment(const Coordinate* nodePt, const Coordinate* a0, const Coordinate* a1, const Coordinate* b)
+{
+    const Coordinate* aLo = a0;
+    const Coordinate* aHi = a1;
+    bool bIsInteriorBetween = true;
+    if (isAngleGreater(nodePt, aLo, aHi)) {
+        aLo = a1;
+        aHi = a0;
+        bIsInteriorBetween = false;
+    }
+    bool bIsBetween = isBetween(nodePt, b, aLo, aHi);
+    bool bIsInterior = (bIsBetween && bIsInteriorBetween)
+        || (! bIsBetween && ! bIsInteriorBetween);
+    return bIsInterior;
+}
+
+
+/* private static */
+bool
+PolygonNode::isBetween(const Coordinate* origin, const Coordinate* p, const Coordinate* e0, const Coordinate* e1)
+{
+    bool isGreater0 = isAngleGreater(origin, p, e0);
+    if (! isGreater0) return false;
+    bool isGreater1 = isAngleGreater(origin, p, e1);
+    return ! isGreater1;
+}
+
+
+/* private static */
+bool
+PolygonNode::isAngleGreater(const Coordinate* origin, const Coordinate* p, const Coordinate* q)
+{
+    int quadrantP = quadrant(origin, p);
+    int quadrantQ = quadrant(origin, q);
+
+    /**
+     * If the vectors are in different quadrants,
+     * that determines the ordering
+     */
+    if (quadrantP > quadrantQ) return true;
+    if (quadrantP < quadrantQ) return false;
+
+    //--- vectors are in the same quadrant
+    // Check relative orientation of vectors
+    // P > Q if it is CCW of Q
+    int orient = algorithm::Orientation::index(*origin, *q, *p);
+    return orient == algorithm::Orientation::COUNTERCLOCKWISE;
+}
+
+
+/* private static */
+int
+PolygonNode::quadrant(const Coordinate* origin, const Coordinate* p)
+{
+    double dx = p->x - origin->x;
+    double dy = p->y - origin->y;
+    return Quadrant::quadrant(dx, dy);
+}
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/PolygonRing.cpp b/src/operation/valid/PolygonRing.cpp
new file mode 100644
index 0000000..a586e60
--- /dev/null
+++ b/src/operation/valid/PolygonRing.cpp
@@ -0,0 +1,255 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/Orientation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/operation/valid/PolygonRing.h>
+
+#include <stack>
+
+using namespace geos::geom;
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+
+/* public static */
+bool
+PolygonRing::isShell(const PolygonRing* polyRing)
+{
+    if (polyRing == nullptr) return true;
+    return polyRing->isShell();
+}
+
+
+/* public static */
+bool
+PolygonRing::addTouch(PolygonRing* ring0, PolygonRing* ring1, const Coordinate& pt)
+{
+    //--- skip if either polygon does not have holes
+    if (ring0 == nullptr || ring1 == nullptr)
+      return false;
+
+    //--- only record touches within a polygon
+    if (! ring0->isSamePolygon(ring1)) return false;
+
+    if (! ring0->isOnlyTouch(ring1, pt)) return true;
+    if (! ring1->isOnlyTouch(ring0, pt)) return true;
+
+    ring0->addTouch(ring1, pt);
+    ring1->addTouch(ring0, pt);
+    return false;
+}
+
+
+/* public static */
+const Coordinate*
+PolygonRing::findHoleCycleLocation(std::vector<PolygonRing*> polyRings)
+{
+    for (PolygonRing* polyRing : polyRings) {
+        if (! polyRing->isInTouchSet()) {
+            const Coordinate* holeCycleLoc = polyRing->findHoleCycleLocation();
+            if (holeCycleLoc != nullptr) return holeCycleLoc;
+        }
+    }
+    return nullptr;
+}
+
+
+/* public static */
+const Coordinate*
+PolygonRing::findInteriorSelfNode(std::vector<PolygonRing*> polyRings)
+{
+    for (PolygonRing* polyRing : polyRings) {
+        const Coordinate* interiorSelfNode = polyRing->findInteriorSelfNode();
+        if (interiorSelfNode != nullptr) {
+            return interiorSelfNode;
+        }
+    }
+    return nullptr;
+}
+
+
+/* private */
+std::vector<PolygonRingTouch*>
+PolygonRing::getTouches() const
+{
+    std::vector<PolygonRingTouch*> touchesVect;
+    for (auto& mapEntry: touches) {
+        PolygonRingTouch* touch = const_cast<PolygonRingTouch*>(&mapEntry.second);
+        touchesVect.push_back(touch);
+    }
+    return touchesVect;
+}
+
+
+/* private */
+void
+PolygonRing::addTouch(PolygonRing* polyRing, const Coordinate& pt)
+{
+    std::size_t nTouches = touches.count(polyRing->id);
+    if (nTouches == 0) {
+        // uses pair's piecewise constructor to emplace into
+        // std::map<int, PolygonRingTouch> touches;
+        touches.emplace(std::piecewise_construct,
+              std::forward_as_tuple(polyRing->id),
+              std::forward_as_tuple(polyRing, pt));
+    };
+}
+
+
+/* public */
+void
+PolygonRing::addSelfTouch(const Coordinate& origin,
+    const Coordinate* e00, const Coordinate* e01,
+    const Coordinate* e10, const Coordinate* e11)
+{
+    selfNodes.emplace_back(origin, e00, e01, e10, e11);
+}
+
+
+/* private */
+bool
+PolygonRing::isOnlyTouch(const PolygonRing* polyRing, const Coordinate& pt) const
+{
+    //--- no touches for this ring
+    if (touches.empty()) return true;
+    //--- no touches for other ring
+
+    // std::map.find() returns std::pair
+    auto search = touches.find(polyRing->id);
+    // Not found
+    if (search == touches.end()) return true;
+    // Found
+    const PolygonRingTouch& touch = search->second;
+    //--- the rings touch - check if point is the same
+    return touch.isAtLocation(pt);
+}
+
+
+/* private */
+const Coordinate*
+PolygonRing::findHoleCycleLocation()
+{
+    //--- the touch set including this ring is already processed
+    if (isInTouchSet()) return nullptr;
+
+    //--- scan the touch set tree rooted at this ring
+    // Assert: this.touchSetRoot is null
+    PolygonRing* root = this;
+    root->setTouchSetRoot(root);
+
+    if (! hasTouches())
+        return nullptr;
+
+    std::stack<PolygonRingTouch*> touchStack;
+    init(root, touchStack);
+
+    while (! touchStack.empty()) {
+        PolygonRingTouch* touch = touchStack.top();
+        touchStack.pop();
+        const Coordinate* holeCyclePt = scanForHoleCycle(touch, root, touchStack);
+        if (holeCyclePt != nullptr) {
+            return holeCyclePt;
+        }
+    }
+    return nullptr;
+}
+
+
+/* private static */
+void
+PolygonRing::init(PolygonRing* root, std::stack<PolygonRingTouch*>& touchStack)
+{
+    for (PolygonRingTouch* touch : root->getTouches()) {
+        touch->getRing()->setTouchSetRoot(root);
+        touchStack.push(touch);
+    }
+}
+
+
+/* private */
+const Coordinate*
+PolygonRing::scanForHoleCycle(PolygonRingTouch* currentTouch,
+    PolygonRing* root,
+    std::stack<PolygonRingTouch*>& touchStack)
+{
+    PolygonRing* polyRing = currentTouch->getRing();
+    const Coordinate* currentPt = currentTouch->getCoordinate();
+
+    /**
+     * Scan the touched rings
+     * Either they form a hole cycle, or they are added to the touch set
+     * and pushed on the stack for scanning
+     */
+    for (PolygonRingTouch* touch : polyRing->getTouches()) {
+        /**
+        * Don't check touches at the entry point
+        * to avoid trivial cycles.
+        * They will already be processed or on the stack
+        * from the previous ring (which touched
+        * all the rings at that point as well)
+        */
+        if (currentPt->equals2D(*touch->getCoordinate()))
+            continue;
+
+        /**
+        * Test if the touched ring has already been
+        * reached via a different touch path.
+        * This is indicated by it already being marked as
+        * part of the touch set.
+        * This indicates a hole cycle has been found.
+        */
+        PolygonRing* touchRing = touch->getRing();
+        if (touchRing->getTouchSetRoot() == root)
+            return touch->getCoordinate();
+
+        touchRing->setTouchSetRoot(root);
+
+        touchStack.push(touch);
+    }
+    return nullptr;
+}
+
+
+/* public */
+const Coordinate*
+PolygonRing::findInteriorSelfNode()
+{
+    if (selfNodes.empty()) return nullptr;
+
+    /**
+     * Determine if the ring interior is on the Right.
+     * This is the case if the ring is a shell and is CW,
+     * or is a hole and is CCW.
+     */
+    bool isCCW = algorithm::Orientation::isCCW(ring->getCoordinatesRO());
+    bool isInteriorOnRight = isShell() ^ isCCW;
+
+    for (const PolygonRingSelfNode& selfNode : selfNodes) {
+        if (!selfNode.isExterior(isInteriorOnRight)) {
+            return selfNode.getCoordinate();
+        }
+    }
+    return nullptr;
+}
+
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/PolygonRingSelfNode.cpp b/src/operation/valid/PolygonRingSelfNode.cpp
new file mode 100644
index 0000000..0c6e0e0
--- /dev/null
+++ b/src/operation/valid/PolygonRingSelfNode.cpp
@@ -0,0 +1,45 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/geom/Coordinate.h>
+#include <geos/operation/valid/PolygonNode.h>
+#include <geos/operation/valid/PolygonRingSelfNode.h>
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+
+/* public */
+bool
+PolygonRingSelfNode::isExterior(bool isInteriorOnRight) const
+{
+    (void)e11; // unused variable
+    /**
+     * Note that either corner and either of the other edges could be used to test.
+     * The situation is fully symmetrical.
+     */
+    bool bIsInteriorSeg = PolygonNode::isInteriorSegment(&nodePt, e00, e01, e10);
+    bool bIsExterior = isInteriorOnRight ? ! bIsInteriorSeg : bIsInteriorSeg;
+    return bIsExterior;
+}
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/PolygonRingTouch.cpp b/src/operation/valid/PolygonRingTouch.cpp
new file mode 100644
index 0000000..7b8b752
--- /dev/null
+++ b/src/operation/valid/PolygonRingTouch.cpp
@@ -0,0 +1,51 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/geom/Coordinate.h>
+#include <geos/operation/valid/PolygonRingTouch.h>
+#include <geos/operation/valid/PolygonRing.h>
+
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+using namespace geos::geom;
+
+/* public */
+const Coordinate*
+PolygonRingTouch::getCoordinate() const
+{
+    return &touchPt;
+}
+
+/* public */
+PolygonRing*
+PolygonRingTouch::getRing() const
+{
+    return ring;
+}
+
+/* public */
+bool
+PolygonRingTouch::isAtLocation(const Coordinate& pt) const
+{
+    return touchPt.equals2D(pt);
+}
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/PolygonTopologyAnalyzer.cpp b/src/operation/valid/PolygonTopologyAnalyzer.cpp
new file mode 100644
index 0000000..829128c
--- /dev/null
+++ b/src/operation/valid/PolygonTopologyAnalyzer.cpp
@@ -0,0 +1,302 @@
+/**********************************************************************
+ *
+ * GEOS - Geometry Engine Open Source
+ * http://geos.osgeo.org
+ *
+ * Copyright (C) 2021 Paul Ramsey <pramsey at cleverelephant.ca>
+ * Copyright (C) 2021 Martin Davis
+ *
+ * This is free software; you can redistribute and/or modify it under
+ * the terms of the GNU Lesser General Public Licence as published
+ * by the Free Software Foundation.
+ * See the COPYING file for more information.
+ *
+ **********************************************************************/
+
+#include <geos/algorithm/LineIntersector.h>
+#include <geos/algorithm/Orientation.h>
+#include <geos/algorithm/PointLocation.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/LinearRing.h>
+#include <geos/geom/Location.h>
+#include <geos/geom/Polygon.h>
+#include <geos/noding/BasicSegmentString.h>
+#include <geos/noding/MCIndexNoder.h>
+#include <geos/noding/SegmentString.h>
+#include <geos/operation/valid/PolygonIntersectionAnalyzer.h>
+#include <geos/operation/valid/PolygonNode.h>
+#include <geos/operation/valid/PolygonRing.h>
+#include <geos/operation/valid/PolygonTopologyAnalyzer.h>
+#include <geos/operation/valid/RepeatedPointRemover.h>
+#include <geos/util/IllegalArgumentException.h>
+
+using namespace geos::geom;
+using geos::noding::SegmentString;
+
+namespace geos {      // geos
+namespace operation { // geos.operation
+namespace valid {     // geos.operation.valid
+
+
+/* public */
+PolygonTopologyAnalyzer::PolygonTopologyAnalyzer(const Geometry* geom, bool p_isInvertedRingValid)
+    : isInvertedRingValid(p_isInvertedRingValid)
+    , segInt(p_isInvertedRingValid)
+    , disconnectionPt(Coordinate::getNull())
+{
+    if (geom->isEmpty()){
+        return;
+    }
+    // Code copied in from analyze()
+    std::vector<SegmentString*> segStrings = createSegmentStrings(geom, p_isInvertedRingValid);
+    polyRings = getPolygonRings(segStrings);
+    // Code copied in from analyzeIntersections()
+    noding::MCIndexNoder noder;
+    noder.setSegmentIntersector(&segInt);
+    noder.computeNodes(&segStrings);
+    if (segInt.hasDoubleTouch()) {
+        disconnectionPt = segInt.getDoubleTouchLocation();
+    }
+}
+
+
+/* public static */
+Coordinate
+PolygonTopologyAnalyzer::findSelfIntersection(const LinearRing* ring)
+{
+    PolygonTopologyAnalyzer ata(ring, false);
+    if (ata.hasInvalidIntersection())
+        return ata.getInvalidLocation();
+    return Coordinate::getNull();
+}
+
+
+/* public static */
+bool
+PolygonTopologyAnalyzer::isSegmentInRing(const Coordinate* p0, const Coordinate* p1,
+    const LinearRing* ring)
+{
+    const CoordinateSequence* ringPts = ring->getCoordinatesRO();
+    Location loc = algorithm::PointLocation::locateInRing(*p0, *ringPts);
+    if (loc == Location::EXTERIOR) return false;
+    if (loc == Location::INTERIOR) return true;
+
+    /**
+    * The segment point is on the boundary of the ring.
+    * Use the topology at the node to check if the segment
+    * is inside or outside the ring.
+    */
+    return isIncidentSegmentInRing(p0, p1, ringPts);
+}
+
+
+/* public static */
+bool
+PolygonTopologyAnalyzer::isIncidentSegmentInRing(
+    const Coordinate* p0, const Coordinate* p1,
+    const CoordinateSequence* ringPts)
+{
+    std::size_t index = intersectingSegIndex(ringPts, p0);
+    const Coordinate* rPrev = &(ringPts->getAt(index));
+    const Coordinate* rNext = &(ringPts->getAt(index + 1));
+    if (p0->equals2D(ringPts->getAt(index))) {
+        rPrev = &(ringPts->getAt(ringIndexPrev(ringPts, index)));
+    }
+    /**
+    * If ring orientation is not normalized, flip the corner orientation
+    */
+    bool isInteriorOnRight = ! algorithm::Orientation::isCCW(ringPts);
+    if (! isInteriorOnRight) {
+        const Coordinate* temp = rPrev;
+        rPrev = rNext;
+        rNext = temp;
+    }
+    return PolygonNode::isInteriorSegment(p0, rPrev, rNext, p1);
+}
+
+
+/* private static */
+std::size_t
+PolygonTopologyAnalyzer::intersectingSegIndex(const CoordinateSequence* ringPts,
+    const Coordinate* pt)
+{
+    algorithm::LineIntersector li;
+    for (std::size_t i = 0; i < ringPts->size() - 1; i++) {
+      li.computeIntersection(*pt, ringPts->getAt(i), ringPts->getAt(i+1));
+      if (li.hasIntersection()) {
+        //-- check if pt is the start point of the next segment
+        if (pt->equals2D(ringPts->getAt(i + 1))) {
+          return i + 1;
+        }
+        return i;
+      }
+    }
+    throw util::IllegalArgumentException("Segment vertex does not intersect ring");
+}
+
+
+/* private static */
+std::size_t
+PolygonTopologyAnalyzer::ringIndexPrev(const CoordinateSequence* ringPts, std::size_t index)
+{
+    if (index == 0)
+        return ringPts->size() - 2;
+    else
+        return index - 1;
+}
+
+
+/* public */
+bool
+PolygonTopologyAnalyzer::isInteriorDisconnected()
+{
+    /**
+     * May already be set by a double-touching hole
+     */
+    if (!disconnectionPt.isNull()) {
+        return true;
+    }
+    if (isInvertedRingValid) {
+        checkInteriorDisconnectedBySelfTouch();
+        if (!disconnectionPt.isNull()) {
+            return true;
+        }
+    }
+    checkInteriorDisconnectedByHoleCycle();
+    if (!disconnectionPt.isNull()) {
+        return true;
+    }
+    return false;
+}
+
+
+/* public */
+void
+PolygonTopologyAnalyzer::checkInteriorDisconnectedBySelfTouch()
+{
+    if (! polyRings.empty()) {
+        const Coordinate* dPt = PolygonRing::findInteriorSelfNode(polyRings);
+        if (dPt)
+            disconnectionPt = *dPt;
+    }
+}
+
+
+/* public */
+void
+PolygonTopologyAnalyzer::checkInteriorDisconnectedByHoleCycle()
+{
+    /**
+    * PolyRings will be null for empty, no hole or LinearRing inputs
+    */
+    if (! polyRings.empty()) {
+        const Coordinate* dPt = PolygonRing::findHoleCycleLocation(polyRings);
+        if (dPt)
+            disconnectionPt = *dPt;
+    }
+}
+
+
+/* private static */
+std::vector<SegmentString*>
+PolygonTopologyAnalyzer::createSegmentStrings(const Geometry* geom, bool bIsInvertedRingValid)
+{
+    std::vector<SegmentString*> segStrings;
+    int typeId = geom->getGeometryTypeId();
+    if (typeId == GEOS_LINEARRING) {
+        const LinearRing* ring = static_cast<const LinearRing*>(geom);
+        segStrings.push_back(createSegString(ring, nullptr));
+        return segStrings;
+    }
+    if (! (typeId == GEOS_POLYGON || typeId == GEOS_MULTIPOLYGON)) {
+        throw util::IllegalArgumentException("Cannot process non-polygonal input");
+    }
+    for (std::size_t i = 0; i < geom->getNumGeometries(); i++) {
+        const Polygon* poly = static_cast<const Polygon*>(geom->getGeometryN(i));
+        if (poly->isEmpty()) continue;
+        bool hasHoles = poly->getNumInteriorRing() > 0;
+
+        //--- polygons with no holes do not need connected interior analysis
+        PolygonRing* shellRing = nullptr;
+        if (hasHoles || bIsInvertedRingValid) {
+            shellRing = createPolygonRing(poly->getExteriorRing());
+        }
+        segStrings.push_back(createSegString(poly->getExteriorRing(), shellRing));
+
+        for (std::size_t j = 0 ; j < poly->getNumInteriorRing(); j++) {
+            const LinearRing* hole = poly->getInteriorRingN(j);
+            if (hole->isEmpty()) continue;
+            PolygonRing* holeRing = createPolygonRing(hole, static_cast<int>(j), shellRing);
+            segStrings.push_back(createSegString(hole, holeRing));
+        }
+    }
+    return segStrings;
+}
+
+
+/* private */
+PolygonRing*
+PolygonTopologyAnalyzer::createPolygonRing(const LinearRing* p_ring)
+{
+    polyRingStore.emplace_back(p_ring);
+    return &(polyRingStore.back());
+}
+
+
+/* private */
+PolygonRing*
+PolygonTopologyAnalyzer::createPolygonRing(const LinearRing* p_ring, int p_index, PolygonRing* p_shell)
+{
+    polyRingStore.emplace_back(p_ring, p_index, p_shell);
+    return &(polyRingStore.back());
+}
+
+
+/* private static */
+std::vector<PolygonRing*>
+PolygonTopologyAnalyzer::getPolygonRings(const std::vector<SegmentString*>& segStrings)
+{
+    std::vector<PolygonRing*> polygonRings;
+    for (SegmentString* ss : segStrings) {
+
+        PolygonRing* polyRing = const_cast<PolygonRing*>(static_cast<const PolygonRing*>(ss->getData()));
+        if (polyRing != nullptr) {
+            polygonRings.push_back(polyRing);
+        }
+    }
+    return polygonRings;
+}
+
+
+/* private static */
+SegmentString*
+PolygonTopologyAnalyzer::createSegString(const LinearRing* ring, const PolygonRing* polyRing)
+{
+    // Let the input LinearRing retain ownership of the
+    // CoordinateSequence, and pass it directly into the BasicSegmentString
+    // constructor.
+    CoordinateSequence* pts = const_cast<CoordinateSequence*>(ring->getCoordinatesRO());
+
+    // Repeated points must be removed for accurate intersection detection
+    // So, in this case we create a de-duped copy of the CoordinateSequence
+    // and manage the lifecycle locally. This we pass on to the SegmentString
+    if (pts->hasRepeatedPoints()) {
+        std::unique_ptr<CoordinateSequence> newPts = RepeatedPointRemover::removeRepeatedPoints(pts);
+        pts = newPts.get();
+        coordSeqStore.emplace_back(newPts.release());
+    }
+
+    // Allocate the BasicSegmentString in the store and return a
+    // pointer into the store. This way we don't have to track the
+    // individual SegmentStrings, they just go away when the
+    // PolygonTopologyAnalyzer deallocates.
+    segStringStore.emplace_back(pts, polyRing);
+    SegmentString* ss = static_cast<SegmentString*>(&(segStringStore.back()));
+    return ss;
+}
+
+
+} // namespace geos.operation.valid
+} // namespace geos.operation
+} // namespace geos
diff --git a/src/operation/valid/QuadtreeNestedRingTester.cpp b/src/operation/valid/QuadtreeNestedRingTester.cpp
deleted file mode 100644
index 138652f..0000000
--- a/src/operation/valid/QuadtreeNestedRingTester.cpp
+++ /dev/null
@@ -1,131 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/QuadtreeNestedRingTester.java rev. 1.12 (JTS-1.10)
- *
- **********************************************************************/
-
-
-#include <geos/operation/valid/QuadtreeNestedRingTester.h>
-#include <geos/operation/valid/IsValidOp.h>
-#include <geos/algorithm/PointLocation.h>
-#include <geos/geom/Envelope.h>
-#include <geos/geom/LinearRing.h>
-#include <geos/index/quadtree/Quadtree.h>
-
-#include <vector>
-#include <cassert>
-
-
-using namespace geos::geomgraph;
-using namespace geos::geom;
-using namespace geos::algorithm;
-using namespace geos::index::quadtree;
-
-namespace geos {
-namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-QuadtreeNestedRingTester::QuadtreeNestedRingTester(GeometryGraph* newGraph):
-    graph(newGraph),
-    rings(),
-    totalEnv(),
-    qt(nullptr),
-    nestedPt(nullptr)
-{
-}
-
-QuadtreeNestedRingTester::~QuadtreeNestedRingTester()
-{
-    delete qt;
-}
-
-Coordinate*
-QuadtreeNestedRingTester::getNestedPoint()
-{
-    return nestedPt;
-}
-
-void
-QuadtreeNestedRingTester::add(const LinearRing* ring)
-{
-    rings.push_back(ring);
-    const Envelope* envi = ring->getEnvelopeInternal();
-    totalEnv.expandToInclude(envi);
-}
-
-bool
-QuadtreeNestedRingTester::isNonNested()
-{
-    buildQuadtree();
-    for(std::size_t i = 0, ni = rings.size(); i < ni; ++i) {
-        const LinearRing* innerRing = rings[i];
-        const CoordinateSequence* innerRingPts = innerRing->getCoordinatesRO();
-        const Envelope* envi = innerRing->getEnvelopeInternal();
-
-        std::vector<void*> results;
-        qt->query(envi, results);
-        for(std::size_t j = 0, nj = results.size(); j < nj; ++j) {
-            LinearRing* searchRing = (LinearRing*)results[j];
-            const CoordinateSequence* searchRingPts = searchRing->getCoordinatesRO();
-
-            if(innerRing == searchRing) {
-                continue;
-            }
-
-            const Envelope* e1 = innerRing->getEnvelopeInternal();
-            const Envelope* e2 = searchRing->getEnvelopeInternal();
-            if(!e1->intersects(e2)) {
-                continue;
-            }
-
-            const Coordinate* innerRingPt = IsValidOp::findPtNotNode(innerRingPts,
-                                            searchRing, graph);
-
-            // Unable to find a ring point not a node of the search ring
-            assert(innerRingPt != nullptr);
-
-            bool isInside = PointLocation::isInRing(*innerRingPt, searchRingPts);
-            if(isInside) {
-                /*
-                 * innerRingPt is const just because the input
-                 * CoordinateSequence is const. If the input
-                 * Polygon survives lifetime of this object
-                 * we are safe.
-                 */
-                nestedPt = const_cast<Coordinate*>(innerRingPt);
-                return false;
-            }
-        }
-    }
-    return true;
-}
-
-void
-QuadtreeNestedRingTester::buildQuadtree()
-{
-    qt = new Quadtree();
-    for(std::size_t i = 0, n = rings.size(); i < n; ++i) {
-        const LinearRing* ring = rings[i];
-        const Envelope* env = ring->getEnvelopeInternal();
-
-        qt->insert(env, (void*)ring);
-    }
-}
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
-
diff --git a/src/operation/valid/SimpleNestedRingTester.cpp b/src/operation/valid/SimpleNestedRingTester.cpp
deleted file mode 100644
index 8abe461..0000000
--- a/src/operation/valid/SimpleNestedRingTester.cpp
+++ /dev/null
@@ -1,73 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- * Copyright (C) 2005 Refractions Research Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/SimpleNestedRingTester.java rev. 1.14 (JTS-1.10)
- *
- **********************************************************************/
-
-#include <geos/operation/valid/SimpleNestedRingTester.h>
-#include <geos/operation/valid/IsValidOp.h>
-#include <geos/algorithm/PointLocation.h>
-#include <geos/geom/LinearRing.h>
-#include <geos/geom/Envelope.h>
-
-#include <cassert>
-
-using namespace geos::algorithm;
-using namespace geos::geom;
-
-namespace geos {
-namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-bool
-SimpleNestedRingTester::isNonNested()
-{
-    for(std::size_t i = 0, ni = rings.size(); i < ni; i++) {
-        LinearRing* innerRing = rings[i];
-        const CoordinateSequence* innerRingPts = innerRing->getCoordinatesRO();
-        for(std::size_t j = 0, nj = rings.size(); j < nj; j++) {
-            LinearRing* searchRing = rings[j];
-            const CoordinateSequence* searchRingPts = searchRing->getCoordinatesRO();
-            if(innerRing == searchRing) {
-                continue;
-            }
-            if(!innerRing->getEnvelopeInternal()->intersects(searchRing->getEnvelopeInternal())) {
-                continue;
-            }
-            const Coordinate* innerRingPt = IsValidOp::findPtNotNode(innerRingPts, searchRing, graph);
-            // Unable to find a ring point not a node of the search ring
-            assert(innerRingPt != nullptr);
-
-            bool isInside = PointLocation::isInRing(*innerRingPt, searchRingPts);
-            if(isInside) {
-                /*
-                 * innerRingPt is const just because the input
-                 * CoordinateSequence is const. If the input
-                 * Polygon survives lifetime of this object
-                 * we are safe.
-                 */
-                nestedPt = const_cast<Coordinate*>(innerRingPt);
-                return false;
-            }
-        }
-    }
-    return true;
-}
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
-
diff --git a/src/operation/valid/SweeplineNestedRingTester.cpp b/src/operation/valid/SweeplineNestedRingTester.cpp
deleted file mode 100644
index 7c68ae8..0000000
--- a/src/operation/valid/SweeplineNestedRingTester.cpp
+++ /dev/null
@@ -1,109 +0,0 @@
-/**********************************************************************
- *
- * GEOS - Geometry Engine Open Source
- * http://geos.osgeo.org
- *
- * Copyright (C) 2005-2006 Refractions Research Inc.
- * Copyright (C) 2001-2002 Vivid Solutions Inc.
- *
- * This is free software; you can redistribute and/or modify it under
- * the terms of the GNU Lesser General Public Licence as published
- * by the Free Software Foundation.
- * See the COPYING file for more information.
- *
- **********************************************************************
- *
- * Last port: operation/valid/SweeplineNestedRingTester.java rev. 1.12 (JTS-1.10)
- *
- **********************************************************************/
-
-#include <geos/operation/valid/SweeplineNestedRingTester.h>
-#include <geos/operation/valid/IsValidOp.h>
-#include <geos/index/sweepline/SweepLineInterval.h>
-#include <geos/index/sweepline/SweepLineIndex.h>
-#include <geos/algorithm/PointLocation.h>
-#include <geos/geom/LinearRing.h>
-
-#include <cassert>
-
-using namespace geos::algorithm;
-using namespace geos::index::sweepline;
-using namespace geos::geom;
-
-namespace geos {
-namespace operation { // geos.operation
-namespace valid { // geos.operation.valid
-
-SweeplineNestedRingTester::OverlapAction::OverlapAction(SweeplineNestedRingTester* p)
-{
-    isNonNested = true;
-    parent = p;
-}
-
-void
-SweeplineNestedRingTester::OverlapAction::overlap(SweepLineInterval* s0, SweepLineInterval* s1)
-{
-    LinearRing* innerRing = (LinearRing*) s0->getItem();
-    LinearRing* searchRing = (LinearRing*) s1->getItem();
-    if(innerRing == searchRing) {
-        return;
-    }
-    if(parent->isInside(innerRing, searchRing)) {
-        isNonNested = false;
-    }
-}
-
-
-bool
-SweeplineNestedRingTester::isNonNested()
-{
-    buildIndex();
-    OverlapAction* action = new OverlapAction(this);
-    sweepLine->computeOverlaps(action);
-    return action->isNonNested;
-}
-
-void
-SweeplineNestedRingTester::buildIndex()
-{
-    sweepLine = new SweepLineIndex();
-    for(std::size_t i = 0, n = rings.size(); i < n; i++) {
-        LinearRing* ring = rings[i];
-        const Envelope* env = ring->getEnvelopeInternal();
-        SweepLineInterval* sweepInt = new SweepLineInterval(env->getMinX(), env->getMaxX(), ring);
-        sweepLine->add(sweepInt);
-    }
-}
-
-bool
-SweeplineNestedRingTester::isInside(LinearRing* innerRing, LinearRing* searchRing)
-{
-    const CoordinateSequence* innerRingPts = innerRing->getCoordinatesRO();
-    const CoordinateSequence* searchRingPts = searchRing->getCoordinatesRO();
-
-    if(!innerRing->getEnvelopeInternal()->intersects(searchRing->getEnvelopeInternal())) {
-        return false;
-    }
-    const Coordinate* innerRingPt = IsValidOp::findPtNotNode(innerRingPts, searchRing, graph);
-
-    // Unable to find a ring point not a node of the search ring
-    assert(innerRingPt != nullptr);
-
-    bool p_isInside = PointLocation::isInRing(*innerRingPt, searchRingPts);
-    if(p_isInside) {
-        /*
-         * innerRingPt is const just because the input
-         * CoordinateSequence is const. If the input
-         * Polygon survives lifetime of this object
-         * we are safe.
-         */
-        nestedPt = const_cast<Coordinate*>(innerRingPt);
-        return true;
-    }
-    return false;
-}
-
-} // namespace geos.operation.valid
-} // namespace geos.operation
-} // namespace geos
-
diff --git a/src/operation/valid/TopologyValidationError.cpp b/src/operation/valid/TopologyValidationError.cpp
index 43f4da9..9d80c50 100644
--- a/src/operation/valid/TopologyValidationError.cpp
+++ b/src/operation/valid/TopologyValidationError.cpp
@@ -60,25 +60,28 @@ TopologyValidationError::TopologyValidationError(int newErrorType)
 }
 
 int
-TopologyValidationError::getErrorType()
+TopologyValidationError::getErrorType() const
 {
     return errorType;
 }
 
-Coordinate&
-TopologyValidationError::getCoordinate()
+const Coordinate&
+TopologyValidationError::getCoordinate() const
 {
     return pt;
 }
 
 std::string
-TopologyValidationError::getMessage()
+TopologyValidationError::getMessage() const
 {
-    return std::string(errMsg[errorType]);
+    if (errorType >= 0)
+        return std::string(errMsg[errorType]);
+    else
+        return std::string("");
 }
 
 std::string
-TopologyValidationError::toString()
+TopologyValidationError::toString() const
 {
     return getMessage().append(" at or near point ").append(pt.toString());
 }
diff --git a/src/precision/CommonBitsOp.cpp b/src/precision/CommonBitsOp.cpp
index febe01e..8a111b7 100644
--- a/src/precision/CommonBitsOp.cpp
+++ b/src/precision/CommonBitsOp.cpp
@@ -67,8 +67,8 @@ CommonBitsOp::intersection(
     const Geometry* geom0,
     const Geometry* geom1)
 {
-    std::unique_ptr<Geometry> rgeom0;
-    std::unique_ptr<Geometry> rgeom1;
+    std::unique_ptr<Geometry> rgeom0 = geom0->clone();
+    std::unique_ptr<Geometry> rgeom1 = geom1->clone();
     removeCommonBits(geom0, geom1, rgeom0, rgeom1);
     return computeResultPrecision(rgeom0->intersection(rgeom1.get()));
 }
@@ -79,8 +79,8 @@ CommonBitsOp::Union(
     const Geometry* geom0,
     const Geometry* geom1)
 {
-    std::unique_ptr<Geometry> rgeom0;
-    std::unique_ptr<Geometry> rgeom1;
+    std::unique_ptr<Geometry> rgeom0 = geom0->clone();
+    std::unique_ptr<Geometry> rgeom1 = geom1->clone();
     removeCommonBits(geom0, geom1, rgeom0, rgeom1);
     return computeResultPrecision(rgeom0->Union(rgeom1.get()));
 }
@@ -91,8 +91,8 @@ CommonBitsOp::difference(
     const Geometry* geom0,
     const Geometry* geom1)
 {
-    std::unique_ptr<Geometry> rgeom0;
-    std::unique_ptr<Geometry> rgeom1;
+    std::unique_ptr<Geometry> rgeom0 = geom0->clone();
+    std::unique_ptr<Geometry> rgeom1 = geom1->clone();
     removeCommonBits(geom0, geom1, rgeom0, rgeom1);
     return computeResultPrecision(rgeom0->difference(rgeom1.get()));
 }
@@ -103,8 +103,8 @@ CommonBitsOp::symDifference(
     const Geometry* geom0,
     const Geometry* geom1)
 {
-    std::unique_ptr<Geometry> rgeom0;
-    std::unique_ptr<Geometry> rgeom1;
+    std::unique_ptr<Geometry> rgeom0 = geom0->clone();
+    std::unique_ptr<Geometry> rgeom1 = geom1->clone();
     removeCommonBits(geom0, geom1, rgeom0, rgeom1);
     return computeResultPrecision(rgeom0->symDifference(rgeom1.get()));
 }
@@ -165,10 +165,7 @@ CommonBitsOp::removeCommonBits(
     std::cerr << "CommonBitsRemover bits: " << commonCoord.x << ", " << commonCoord.y << std::endl;
 #endif
 
-    rgeom0 = geom0->clone();
     cbr->removeCommonBits(rgeom0.get());
-
-    rgeom1 = geom1->clone();
     cbr->removeCommonBits(rgeom1.get());
 }
 
diff --git a/tests/unit/capi/GEOSisValidDetailTest.cpp b/tests/unit/capi/GEOSisValidDetailTest.cpp
index 20092ba..b342b17 100644
--- a/tests/unit/capi/GEOSisValidDetailTest.cpp
+++ b/tests/unit/capi/GEOSisValidDetailTest.cpp
@@ -116,7 +116,7 @@ void object::test<4>
     geom1_ = GEOSGeomFromWKT("POLYGON((0 1, -10 10, 10 10, 0 1, 4 6, -4 6, 0 1))");
     int r = GEOSisValidDetail(geom1_, 0, &reason_, &loc_);
     ensure_equals(r, 0); // invalid
-    ensure_equals(std::string(reason_), std::string("Ring Self-intersection"));
+    ensure_equals(std::string(reason_), std::string("Self-intersection"));
     ensure_equals(toWKT(loc_), "POINT (0 1)");
 }
 
diff --git a/tests/unit/operation/IsSimpleOpTest.cpp b/tests/unit/operation/IsSimpleOpTest.cpp
deleted file mode 100644
index 4fecfbf..0000000
--- a/tests/unit/operation/IsSimpleOpTest.cpp
+++ /dev/null
@@ -1,188 +0,0 @@
-//
-// Test Suite for geos::operation::IsSimpleOp class
-// Ported from JTS junit/operation/IsSimpleTest.java
-
-#include <tut/tut.hpp>
-#include <utility.h>
-
-// geos
-#include <geos/operation/IsSimpleOp.h>
-#include <geos/geom/Coordinate.h>
-#include <geos/geom/Dimension.h>
-#include <geos/geom/Geometry.h>
-#include <geos/geom/GeometryFactory.h>
-#include <geos/geom/PrecisionModel.h>
-#include <geos/io/WKTReader.h>
-#include <geos/io/WKBReader.h>
-#include <geos/util/GEOSException.h>
-
-// std
-#include <string>
-#include <sstream>
-#include <memory>
-
-using namespace geos::geom;
-using namespace geos::operation;
-
-namespace tut {
-//
-// Test Group
-//
-
-struct test_issimpleop_data {
-    typedef geos::geom::GeometryFactory GeometryFactory;
-    geos::geom::PrecisionModel pm_;
-    GeometryFactory::Ptr factory_;
-    geos::io::WKTReader reader_;
-    double tolerance_;
-
-    test_issimpleop_data()
-        : pm_(1)
-        , factory_(GeometryFactory::create(&pm_, 0))
-        , reader_(factory_.get())
-        , tolerance_(0.00005)
-    {}
-};
-
-typedef test_group<test_issimpleop_data> group;
-typedef group::object object;
-
-group test_issimpleop_group("geos::operation::IsSimpleOp");
-
-//
-// Test Cases
-//
-
-// 1 - Test cross
-template<>
-template<>
-void object::test<1>
-()
-{
-    const std::string wkt("MULTILINESTRING ((20 120, 120 20), (20 20, 120 120))");
-    const Geometry::Ptr geom(reader_.read(wkt));
-
-    // TODO - mloskot: What about support of new features of BoundaryNodeRule, in JTS
-
-    IsSimpleOp op(*geom);
-    bool simple = op.isSimple();
-
-    ensure(false == simple);
-
-    // TODO - mloskot:
-    // There are missing features not (re)implemented in IsSimpleOp, in GEOS.
-    // So, all tests in this suite have been simplified in comparison to original JTS tests.
-    //
-    //Coordinate loc(70, 70);
-    //Coordinate nonSimpleLoc = op.getNonSimpleLocation();
-    //loc.distance(nonSimpleLoc) < TOLERANCE
-}
-
-// 2 - Test MultiLineString with ring touching at the end point
-template<>
-template<>
-void object::test<2>
-()
-{
-    const std::string wkt("MULTILINESTRING ((100 100, 20 20, 200 20, 100 100), (100 200, 100 100))");
-    const Geometry::Ptr geom(reader_.read(wkt));
-
-    IsSimpleOp op(*geom);
-    bool simple = op.isSimple();
-
-    ensure(false == simple);
-}
-
-// 3 - Test simple LineString
-template<>
-template<>
-void object::test<3>
-()
-{
-    const std::string wkt("LINESTRING (100 100, 20 20, 200 20, 100 100)");
-    const Geometry::Ptr geom(reader_.read(wkt));
-
-    IsSimpleOp op(*geom);
-    bool simple = op.isSimple();
-
-    ensure(true == simple);
-}
-
-
-template<>
-template<>
-void object::test<4>
-()
-{
-    // Adapted from https://trac.osgeo.org/geos/ticket/858
-    constexpr char kData[] =
-            "00000000020000000e0000000000000000"
-            "0000000000000000240424242424242424"
-            "24242424280000000000ffffffffffff3b"
-            "ffffffffffffffffffffffff4000010800"
-            "0000030000003b01980000000000000000"
-            "0000000000000000000000000000002900"
-            "000000000100000000490001f34e537437"
-            "6c6f63616c653500000000000000000000"
-            "2800000000000000000000000000000000"
-            "fb0000000000010700000000003a000000"
-            "f100000000000000000000f60000000000"
-            "0000000000000000000000000000000000"
-            "0000000000000000200000000000000000"
-            "0000000000000000000000000000000000";
-
-    geos::io::WKBReader reader;
-    std::istringstream s(kData);
-
-    auto g = reader.readHEX(s);
-
-    try {
-        g->isSimple();
-    } catch (geos::util::GEOSException &) {
-        // no memory leaks or invalid reads on exception
-    }
-}
-
- // public void testLinesAll() {
- //    checkIsSimpleAll("MULTILINESTRING ((10 20, 90 20), (10 30, 90 30), (50 40, 50 10))",
- //        BoundaryNodeRule.MOD2_BOUNDARY_RULE,
- //        "");
- //  }
-
- // private void checkIsSimpleAll(String wkt, BoundaryNodeRule bnRule,
- //      String wktExpectedPts)
- //  {
- //    Geometry g = read(wkt);
- //    IsSimpleOp op = new IsSimpleOp(g, bnRule);
- //    op.setFindAllLocations(true);
- //    op.isSimple();
- //    List<Coordinate> nonSimpleCoords = op.getNonSimpleLocations();
- //    Geometry nsPts = g.getFactory().createMultiPointFromCoords(CoordinateArrays.toCoordinateArray(nonSimpleCoords));
-
- //    Geometry expectedPts = read(wktExpectedPts);
- //    checkEqual(expectedPts, nsPts);
- //  }
-
-template<>
-template<>
-void object::test<5>
-()
-{
-    const std::string wkt("MULTILINESTRING ((10 20, 90 20), (10 30, 90 30), (50 40, 50 10))");
-    const Geometry::Ptr geom(reader_.read(wkt));
-
-    const std::string wktNonSimple("MULTIPOINT((50 20), (50 30))");
-    const Geometry::Ptr geomExpectedNonSimple(reader_.read(wktNonSimple));
-
-    IsSimpleOp op(*geom);
-    op.setFindAllLocations(true);
-    bool simple = op.isSimple();
-    const std::vector<Coordinate>& nonSimpleCoords = op.getNonSimpleLocations();
-    Geometry::Ptr nsPts(geom->getFactory()->createMultiPoint(nonSimpleCoords));
-
-    ensure(false == simple);
-    ensure_equals_geometry(nsPts.get(), geomExpectedNonSimple.get());
-}
-
-
-} // namespace tut
diff --git a/tests/unit/operation/valid/IsSimpleOpTest.cpp b/tests/unit/operation/valid/IsSimpleOpTest.cpp
new file mode 100644
index 0000000..31afac1
--- /dev/null
+++ b/tests/unit/operation/valid/IsSimpleOpTest.cpp
@@ -0,0 +1,208 @@
+//
+// Test Suite for geos::operation::valid::IsSimpleOp class
+
+
+#include <tut/tut.hpp>
+#include <utility.h>
+
+// geos
+#include <geos/operation/valid/IsSimpleOp.h>
+#include <geos/geom/Coordinate.h>
+#include <geos/geom/Dimension.h>
+#include <geos/geom/Geometry.h>
+#include <geos/geom/GeometryFactory.h>
+#include <geos/geom/PrecisionModel.h>
+#include <geos/io/WKTReader.h>
+#include <geos/io/WKBReader.h>
+#include <geos/util/GEOSException.h>
+
+// std
+#include <string>
+#include <sstream>
+#include <memory>
+
+using geos::operation::valid::IsSimpleOp;
+using geos::geom::Coordinate;
+using geos::geom::Geometry;
+using geos::geom::GeometryFactory;
+using geos::algorithm::BoundaryNodeRule;
+
+namespace tut {
+//
+// Test Group
+//
+
+struct test_issimpleop_data {
+
+    geos::geom::PrecisionModel pm_;
+    GeometryFactory::Ptr factory_;
+    geos::io::WKTReader reader_;
+    double tolerance_;
+
+    test_issimpleop_data()
+        : pm_(1)
+        , factory_(GeometryFactory::create(&pm_, 0))
+        , reader_(factory_.get())
+        , tolerance_(0.00005)
+    {}
+
+    void checkIsSimple(
+        std::string& wkt,
+        const BoundaryNodeRule& bnRule,
+        bool expectedResult)
+    {
+        checkIsSimple(wkt, bnRule, expectedResult, Coordinate::getNull());
+    }
+
+    void checkIsSimple(
+        std::string& wkt,
+        const BoundaryNodeRule& bnRule,
+        bool expectedResult,
+        Coordinate expectedLocation)
+    {
+        auto g = reader_.read(wkt);
+        IsSimpleOp op(*g, bnRule);
+        bool isSimple = op.isSimple();
+        Coordinate nonSimpleLoc = op.getNonSimpleLocation();
+        // if geom is not simple, should have a valid location
+        ensure("unexpected result", expectedResult == isSimple);
+        ensure("not simple implies a non-simple location", isSimple || ! nonSimpleLoc.isNull());
+        if (!isSimple && !nonSimpleLoc.isNull() && !expectedLocation.isNull()) {
+            ensure(expectedLocation.distance(nonSimpleLoc) < tolerance_);
+        }
+    }
+
+    void checkIsSimpleAll(
+        std::string& wkt,
+        const BoundaryNodeRule& bnRule,
+        std::string& wktExpectedPts)
+    {
+        auto g = reader_.read(wkt);
+        IsSimpleOp op(*g, bnRule);
+        op.setFindAllLocations(true);
+        op.isSimple();
+
+        auto nonSimpleCoords = op.getNonSimpleLocations();
+        std::unique_ptr<Geometry> nsPts(g->getFactory()->createMultiPoint(nonSimpleCoords));
+        auto expectedPts = reader_.read(wktExpectedPts);
+        ensure_equals_geometry(expectedPts.get(), nsPts.get());
+    }
+
+
+};
+
+typedef test_group<test_issimpleop_data> group;
+typedef group::object object;
+
+group test_issimpleop_group("geos::operation::valid::IsSimpleOp");
+
+//
+// Test Cases
+//
+
+
+template<>
+template<>
+void object::test<1>
+()
+{
+    // Adapted from https://trac.osgeo.org/geos/ticket/858
+    constexpr char kData[] =
+            "00000000020000000e0000000000000000"
+            "0000000000000000240424242424242424"
+            "24242424280000000000ffffffffffff3b"
+            "ffffffffffffffffffffffff4000010800"
+            "0000030000003b01980000000000000000"
+            "0000000000000000000000000000002900"
+            "000000000100000000490001f34e537437"
+            "6c6f63616c653500000000000000000000"
+            "2800000000000000000000000000000000"
+            "fb0000000000010700000000003a000000"
+            "f100000000000000000000f60000000000"
+            "0000000000000000000000000000000000"
+            "0000000000000000200000000000000000"
+            "0000000000000000000000000000000000";
+
+    geos::io::WKBReader reader;
+    std::istringstream s(kData);
+
+    auto g = reader.readHEX(s);
+
+    try {
+        g->isSimple();
+    } catch (geos::util::GEOSException &) {
+        // no memory leaks or invalid reads on exception
+    }
+}
+
+
+// test2TouchAtEndpoint
+template<>
+template<>
+void object::test<2> ()
+{
+    std::string a("MULTILINESTRING((0 1, 1 1, 2 1), (0 0, 1 0, 2 1))");
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryRuleMod2(), true, Coordinate(2, 1));
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryEndPoint(), true, Coordinate(2, 1));
+}
+
+// test3TouchAtEndpoint
+template<>
+template<>
+void object::test<3> ()
+{
+    // rings are simple under all rules
+    std::string a("MULTILINESTRING ((0 1, 1 1, 2 1),   (0 0, 1 0, 2 1),  (0 2, 1 2, 2 1))");
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryRuleMod2(), true, Coordinate(2, 1));
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryEndPoint(), true, Coordinate(2, 1));
+}
+
+// testCross
+template<>
+template<>
+void object::test<4> ()
+{
+    std::string a("MULTILINESTRING ((20 120, 120 20), (20 20, 120 120))");
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryRuleMod2(), false, Coordinate(70, 70));
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryEndPoint(), false, Coordinate(70, 70));
+}
+
+// testMultiLineStringWithRingTouchAtEndpoint
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string a("MULTILINESTRING ((100 100, 20 20, 200 20, 100 100), (100 200, 100 100))");
+    // under Mod-2, the ring has no boundary, so the line intersects the interior ==> not simple
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryRuleMod2(), false, Coordinate(100, 100));
+    // under Endpoint, the ring has a boundary point, so the line does NOT intersect the interior ==> simple
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryEndPoint(), true);
+}
+
+// testRing
+template<>
+template<>
+void object::test<6> ()
+{
+    // rings are simple under all rules
+    std::string a("LINESTRING (100 100, 20 20, 200 20, 100 100)");
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryRuleMod2(), true);
+    checkIsSimple(a, BoundaryNodeRule::getBoundaryEndPoint(), true);
+}
+
+
+// testLinesAll
+template<>
+template<>
+void object::test<7> ()
+{
+    // rings are simple under all rules
+    std::string a("MULTILINESTRING ((10 20, 90 20), (10 30, 90 30), (50 40, 50 10))");
+    std::string b("MULTIPOINT((50 20), (50 30))");
+    checkIsSimpleAll(a, BoundaryNodeRule::getBoundaryRuleMod2(), b);
+}
+
+
+
+
+} // namespace tut
diff --git a/tests/unit/operation/valid/IsValidOpTest.cpp b/tests/unit/operation/valid/IsValidOpTest.cpp
index 75d5b9b..30ae4d5 100644
--- a/tests/unit/operation/valid/IsValidOpTest.cpp
+++ b/tests/unit/operation/valid/IsValidOpTest.cpp
@@ -5,7 +5,6 @@
 #include <tut/tut.hpp>
 // geos
 #include <geos/constants.h> // for std::isnan
-#include <geos/operation/valid/IsValidOp.h>
 #include <geos/geom/Coordinate.h>
 #include <geos/geom/CoordinateArraySequence.h>
 #include <geos/geom/Dimension.h>
@@ -14,6 +13,7 @@
 #include <geos/geom/GeometryFactory.h>
 #include <geos/geom/PrecisionModel.h>
 #include <geos/io/WKTReader.h>
+#include <geos/operation/valid/IsValidOp.h>
 #include <geos/operation/valid/TopologyValidationError.h>
 // std
 #include <cmath>
@@ -39,6 +39,30 @@ struct test_isvalidop_data {
     test_isvalidop_data()
         : pm_(1), factory_(GeometryFactory::create(&pm_, 0))
     {}
+
+    void checkValid(const char* wkt)
+    {
+        std::string wktstr(wkt);
+        auto g = wktreader.read(wktstr);
+        ensure(g->isValid());
+    }
+
+    void checkInvalid(const char* wkt)
+    {
+        std::string wktstr(wkt);
+        auto g = wktreader.read(wktstr);
+        ensure(!g->isValid());
+    }
+
+    void checkInvalid(int errExpected, const char* wkt)
+    {
+        std::string wktstr(wkt);
+        auto geom = wktreader.read(wktstr);
+        IsValidOp validOp(geom.get());
+        int err = validOp.getValidationError()->getErrorType();
+        ensure_equals("error codes do not match", err, errExpected);
+    }
+
 };
 
 typedef test_group<test_isvalidop_data> group;
@@ -53,8 +77,7 @@ group test_isvalidop_group("geos::operation::valid::IsValidOp");
 // 1 - testInvalidCoordinate
 template<>
 template<>
-void object::test<1>
-()
+void object::test<1> ()
 {
     CoordinateArraySequence* cs = new CoordinateArraySequence();
     cs->add(Coordinate(0.0, 0.0));
@@ -65,7 +88,7 @@ void object::test<1>
     IsValidOp isValidOp(line.get());
     bool valid = isValidOp.isValid();
 
-    TopologyValidationError* err = isValidOp.getValidationError();
+    const TopologyValidationError* err = isValidOp.getValidationError();
     ensure(nullptr != err);
     const Coordinate& errCoord = err->getCoordinate();
 
@@ -78,8 +101,7 @@ void object::test<1>
 
 template<>
 template<>
-void object::test<2>
-()
+void object::test<2> ()
 {
     std::string wkt0("POLYGON((25495445.625 6671632.625,25495445.625 6671711.375,25495555.375 6671711.375,25495555.375 6671632.625,25495445.625 6671632.625),(25495368.0441 6671726.9312,25495368.3959388 6671726.93601515,25495368.7478 6671726.9333,25495368.0441 6671726.9312))");
     GeomPtr g0(wktreader.read(wkt0));
@@ -87,7 +109,7 @@ void object::test<2>
     IsValidOp isValidOp(g0.get());
     bool valid = isValidOp.isValid();
 
-    TopologyValidationError* err = isValidOp.getValidationError();
+    const TopologyValidationError* err = isValidOp.getValidationError();
     ensure(nullptr != err);
     const Coordinate& errCoord = err->getCoordinate();
 
@@ -103,8 +125,7 @@ void object::test<2>
 
 template<>
 template<>
-void object::test<3>
-()
+void object::test<3> ()
 {
     // https://trac.osgeo.org/geos/ticket/588
 
@@ -120,8 +141,7 @@ void object::test<3>
 
 template<>
 template<>
-void object::test<4>
-()
+void object::test<4> ()
 {
     // https://github.com/locationtech/jts/pull/737
 
@@ -131,4 +151,164 @@ void object::test<4>
     ensure(! g->isValid());
 }
 
+template<>
+template<>
+void object::test<5> ()
+{
+    std::string wkt("MULTIPOLYGON(((0 0, 10 0, 10 10, 0 10, 0 0),(2 2, 2 6, 6 4, 2 2)),((60 60, 60 50, 70 40, 60 60)))");
+    auto g = wktreader.read(wkt);
+    ensure(g->isValid());
+}
+
+template<>
+template<>
+void object::test<6> ()
+{
+    std::string wkt("POLYGON((40 320,340 320,340 20,40 20,40 320),(100 120,40 20,180 100,100 120),(200 200,180 100,240 160,200 200),(260 260,240 160,300 200,260 260),(300 300,300 200,340 260,300 300))");
+    auto g = wktreader.read(wkt);
+    ensure(!g->isValid());
+}
+
+// testValidSimplePolygon
+template<>
+template<>
+void object::test<7> ()
+{
+    checkValid(
+        "POLYGON ((10 89, 90 89, 90 10, 10 10, 10 89))");
+}
+
+// testInvalidSimplePolygonRingSelfIntersection
+template<>
+template<>
+void object::test<8> ()
+{
+    checkInvalid(
+        TopologyValidationError::eSelfIntersection,
+        "POLYGON ((10 90, 90 10, 90 90, 10 10, 10 90))");
+}
+
+// testSimplePolygonHole
+template<>
+template<>
+void object::test<9> ()
+{
+    checkValid(
+        "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (60 20, 20 70, 90 90, 60 20))");
+}
+
+// testPolygonTouchingHoleAtVertex
+template<>
+template<>
+void object::test<10> ()
+{
+    checkValid(
+        "POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260), (140 180, 40 260, 140 240, 140 180))");
+}
+
+// testInvalidPolygonHoleProperIntersection
+template<>
+template<>
+void object::test<11> ()
+{
+    checkInvalid(
+        TopologyValidationError::eSelfIntersection,
+        "POLYGON ((10 90, 50 50, 10 10, 10 90), (20 50, 60 70, 60 30, 20 50))");
+}
+
+// testInvalidPolygonDisconnectedInterior
+template<>
+template<>
+void object::test<12> ()
+{
+    checkInvalid(
+        TopologyValidationError::eDisconnectedInterior,
+        "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 30 80, 20 20, 20 80), (80 30, 20 20, 80 20, 80 30), (80 80, 30 80, 80 30, 80 80))");
+}
+
+// testValidMultiPolygonTouchAtVertices
+template<>
+template<>
+void object::test<13> ()
+{
+    checkValid(
+        "MULTIPOLYGON (((10 10, 10 90, 90 90, 90 10, 80 80, 50 20, 20 80, 10 10)), ((90 10, 10 10, 50 20, 90 10)))");
+}
+
+// testValidMultiPolygonTouchAtVerticesSegments
+template<>
+template<>
+void object::test<14> ()
+{
+    checkValid(
+        "MULTIPOLYGON (((60 40, 90 10, 90 90, 10 90, 10 10, 40 40, 60 40)), ((50 40, 20 20, 80 20, 50 40)))");
+}
+
+// testInvalidMultiPolygonNestedAllTouchAtVertices
+template<>
+template<>
+void object::test<15> ()
+{
+    checkInvalid(
+        TopologyValidationError::eNestedShells,
+        "MULTIPOLYGON (((10 10, 20 30, 10 90, 90 90, 80 30, 90 10, 50 20, 10 10)), ((80 30, 20 30, 50 20, 80 30)))");
+}
+
+// testValidMultiPolygonHoleTouchVertices
+template<>
+template<>
+void object::test<16> ()
+{
+    checkValid(
+        "MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), (220 340, 80 320, 60 200, 140 100, 340 60, 300 240, 220 340)), ((60 200, 340 60, 220 340, 60 200)))");
+}
+
+// testPolygonMultipleHolesTouchAtSamePoint
+template<>
+template<>
+void object::test<17> ()
+{
+    checkValid(
+        "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (40 80, 60 80, 50 50, 40 80), (20 60, 20 40, 50 50, 20 60), (40 20, 60 20, 50 50, 40 20))");
+}
+
+// testPolygonHoleOutsideShellAllTouch
+template<>
+template<>
+void object::test<18> ()
+{
+    checkInvalid(TopologyValidationError::eHoleOutsideShell,
+        "POLYGON ((10 10, 30 10, 30 50, 70 50, 70 10, 90 10, 90 90, 10 90, 10 10), (50 50, 30 10, 70 10, 50 50))");
+}
+
+// testPolygonHoleOutsideShellDoubleTouch
+template<>
+template<>
+void object::test<19> ()
+{
+    checkInvalid(TopologyValidationError::eHoleOutsideShell,
+        "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 80 80, 80 20, 20 20, 20 80), (90 70, 150 50, 90 20, 110 40, 90 70))");
+}
+
+// testPolygonNestedHolesAllTouch
+template<>
+template<>
+void object::test<20> ()
+{
+    checkInvalid(TopologyValidationError::eNestedHoles,
+        "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 80 80, 80 20, 20 20, 20 80), (50 80, 80 50, 50 20, 20 50, 50 80))");
+}
+
+// testInvalidMultiPolygonHoleOverlapCrossing
+template<>
+template<>
+void object::test<21> ()
+{
+    checkInvalid(
+        TopologyValidationError::eSelfIntersection,
+        "MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), (220 340, 180 240, 60 200, 140 100, 340 60, 300 240, 220 340)), ((60 200, 340 60, 220 340, 60 200)))");
+}
+
+
+
 } // namespace tut
diff --git a/tests/unit/operation/valid/ValidSelfTouchingRingFormingHoleTest.cpp b/tests/unit/operation/valid/ValidSelfTouchingRingFormingHoleTest.cpp
index 5918bb2..f7d6ed8 100644
--- a/tests/unit/operation/valid/ValidSelfTouchingRingFormingHoleTest.cpp
+++ b/tests/unit/operation/valid/ValidSelfTouchingRingFormingHoleTest.cpp
@@ -121,7 +121,10 @@ void object::test<2>
 ()
 {
     string wkt =
-        "POLYGON ((0 0, 0 340, 320 340, 320 0, 120 0, 0 0),   (120 0, 180 100, 60 100, 120 0),   (80 300, 80 180, 200 180, 200 240, 200 300, 80 300),  (200 240, 280 200, 280 280, 200 240))";
+        "POLYGON ((0 0, 0 340, 320 340, 320 0, 120 0, 0 0), "
+        "(120 0, 180 100, 60 100, 120 0), "
+        "(80 300, 80 180, 200 180, 200 240, 200 300, 80 300), "
+        "(200 240, 280 200, 280 280, 200 240))";
     checkIsValidSTR(wkt, true);
     checkIsValidDefault(wkt, true);
 }
@@ -188,14 +191,44 @@ void object::test<6>
 /// 7 - testShellCrossAndSTR
 template<>
 template<>
-void object::test<7>
-()
+void object::test<7> ()
 {
     string wkt = "POLYGON ((20 20, 120 20, 120 220, 180 220, 140 160, 200 160, 180 220, 240 220, 240 120, 20 120,  20 20))";
     checkIsValidSTR(wkt, false);
     checkIsValidDefault(wkt, false);
 }
 
+/// Basic one-ring self-touch polygon
+template<>
+template<>
+void object::test<8> ()
+{
+    string wkt = "POLYGON ((100 0, 100 100, 200 100, 200 0, 150 0, 170 40, 130 40, 150 0, 100 0))";
+    checkIsValidSTR(wkt, true);
+    checkIsValidDefault(wkt, false);
+}
+
+
+/// testExvertedHoleStarTouchHoleCycle
+template<>
+template<>
+void object::test<9> ()
+{
+    string wkt = "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 50 30, 80 80, 80 30, 20 30, 20 80), (40 70, 50 70, 50 30, 40 70), (40 20, 60 20, 50 30, 40 20), (40 80, 20 80, 40 70, 40 80))";
+    checkIsValidSTR(wkt, false);
+}
+
+
+/// testExvertedHoleStarTouchHoleCycle
+template<>
+template<>
+void object::test<10> ()
+{
+    string wkt = "POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 50 30, 80 80, 80 30, 20 30, 20 80), (40 70, 50 70, 50 30, 40 70), (40 20, 60 20, 50 30, 40 20))";
+    checkIsValidSTR(wkt, true);
+    checkIsValidDefault(wkt, false);
+}
+
 
 
 } // namespace tut
diff --git a/tests/xmltester/XMLTester.cpp b/tests/xmltester/XMLTester.cpp
index 9dbd584..01f22f7 100644
--- a/tests/xmltester/XMLTester.cpp
+++ b/tests/xmltester/XMLTester.cpp
@@ -93,6 +93,7 @@ using std::runtime_error;
 using geos::operation::overlayng::OverlayNG;
 using geos::operation::overlayng::UnaryUnionNG;
 using geos::operation::overlayng::OverlayNGRobust;
+using operation::valid::TopologyValidationError;
 
 namespace {
 
@@ -571,7 +572,7 @@ XMLTester::testValid(const geom::Geometry* g, const std::string& label)
     operation::valid::IsValidOp ivo(g);
     bool valid = ivo.isValid();
     if(! valid) {
-        operation::valid::TopologyValidationError* err = ivo.getValidationError();
+        const TopologyValidationError* err = ivo.getValidationError();
         std::cerr << *curr_file << ":"
                   << " case" << caseCount << ":"
                   << " test" << testCount << ": "
diff --git a/tests/xmltester/tests/general/TestValid.xml b/tests/xmltester/tests/general/TestValid.xml
index d618818..15924a2 100644
--- a/tests/xmltester/tests/general/TestValid.xml
+++ b/tests/xmltester/tests/general/TestValid.xml
@@ -2,806 +2,629 @@
    <precisionModel scale="1.0" offsetx="0.0" offsety="0.0"/>
 
    <case>
-      <desc>L - linear-ring bowtie</desc>
-      <a>LINEARRING(0 0, 100 100, 100 0, 0 100, 0 0)</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <desc>P - point (valid)</desc>
+      <a>
+    POINT(10 10)
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>L - linestring bowtie</desc>
-      <a>LINESTRING(0 0, 100 100, 100 0, 0 100, 0 0)</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
+      <desc>P - empty point (valid)</desc>
+      <a>
+    POINT EMPTY
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>P - point</desc>
+      <desc>mP - no repeated points (valid)</desc>
       <a>
-    POINT(10 10)
+    MULTIPOINT((10 10), (20 20), (30 30))
   </a>
-      <test>         <op name="isValid" arg1="A">    true  </op>      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>P - empty point</desc>
+      <desc>mP - repeated points (valid)</desc>
       <a>
-    POINT EMPTY
+    MULTIPOINT((10 10), (20 20), (30 30), (10 10))
   </a>
-      <test>         <op name="isValid" arg1="A">    true  </op>      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>P - point with invalid X ordinate</desc>
+      <desc>L - empty (valid)</desc>
       <a>
-    POINT(NaN 10)
+LINESTRING EMPTY
   </a>
-      <test>         <op name="isValid" arg1="A">    false  </op>      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>P - point with invalid Y ordinate</desc>
+      <desc>L - no repeated points (valid)</desc>
       <a>
-    POINT(10 NaN)
+LINESTRING (40 180, 120 120, 140 200, 200 140, 240 200)
   </a>
-      <test>         <op name="isValid" arg1="A">    false  </op>      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>mP - no repeated points</desc>
+      <desc>L - repeated points (valid)</desc>
       <a>
-    MULTIPOINT((10 10), (20 20), (30 30))
+LINESTRING (40 180, 120 120, 140 200, 140 200, 200 140, 240 200)
   </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+
+   <case>
+      <desc>L - linestring bowtie (valid)</desc>
+      <a>LINESTRING(0 0, 100 100, 100 0, 0 100, 0 0)</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+
+   <case>
+      <desc>mL - MultiLinestring with empty component (valid)</desc>
+      <a>MULTILINESTRING((1 1, 0 0), EMPTY)</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>LR - linear-ring (valid)</desc>
+      <a>LINEARRING (100 200, 200 200, 200 100, 100 100, 100 200)</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - polygon with repeated point (valid) </desc>
+      <a>POLYGON ((107 246, 107 246, 250 285, 294 137, 151 90, 15 125, 157 174, 107 246))</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - with hole (valid)</desc>
+      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 40 20, 40 40, 20 40))</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - shell has repeated points (valid)</desc>
+      <a>POLYGON ((0 60, 0 0, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 40 20, 40 40, 20 40))</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+
+   <case>
+      <desc>A - shell touches hole (valid)</desc>
+      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 60 20, 20 40))</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+
+   <case>
+      <desc>A - non-empty shell and empty hole (valid)</desc>
+      <a>POLYGON ((60 280, 260 180, 60 80, 60 280), EMPTY)</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>mP - repeated points</desc>
+      <desc>A - empty shell and holes (valid)</desc>
+      <a>POLYGON (EMPTY, EMPTY, EMPTY)</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - hole with repeated points (valid) </desc>
       <a>
-    MULTIPOINT((10 10), (20 20), (30 30), (10 10))
+POLYGON ((40 260, 40 60, 120 60, 180 160, 240 60, 300 60, 300 260, 40 260), 
+  (70 230, 80 230, 80 220, 80 220, 70 230))  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - hole touches hole (valid) </desc>
+      <a>POLYGON ((0 120, 0 0, 140 0, 140 120, 0 120), (100 100, 100 20, 120 20, 120 100, 100 100), (20 100, 20 40, 100 40, 20 100))</a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - hole touches shell at non-vertex (valid)</desc>
+      <a>
+POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260), 
+  (140 180, 40 180, 140 240, 140 180))
   </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>mP - invalid point</desc>
+      <desc>A - hole touches shell at vertex (valid)</desc>
       <a>
-    MULTIPOINT((10 10), (20 20), (30 30), (10 NaN))
-      </a>
-      <test>
-         <op name="isValid" arg1="A">    false  </op>
-      </test>
+POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260), 
+  (140 180, 40 260, 140 240, 140 180))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - holes do not overlap, first point is identical (valid) </desc>
+      <a>
+POLYGON ((20 320, 240 320, 240 40, 20 40, 20 320), 
+  (140 180, 60 120, 60 240, 140 180), 
+  (140 180, 200 120, 200 240, 140 180))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - holes touch at one point (valid) </desc>
+      <a>
+POLYGON ((190 190, 360 20, 20 20, 190 190), 
+  (90 50, 150 110, 190 50, 90 50), 
+  (190 50, 230 110, 290 50, 190 50))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - multiple holes touch at one point (valid) </desc>
+      <a>
+POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (40 80, 60 80, 50 50, 40 80), (20 60, 20 40, 50 50, 20 60), (40 20, 60 20, 50 50, 40 20), (80 60, 80 40, 50 50, 80 60))
+    </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>mA - shell inside hole, no touch (valid) </desc>
+      <a>
+MULTIPOLYGON (((60 320, 60 80, 300 80, 60 320), 
+  (80 280, 80 100, 260 100, 80 280)), 
+  ((120 160, 140 160, 140 140, 120 160)))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>L - empty</desc>
+      <desc>mA - shell inside hole, all shell vertices touch (valid) </desc>
       <a>
-LINESTRING EMPTY
+MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), 
+  (220 340, 180 240, 60 200, 180 160, 340 60, 240 220, 220 340)), 
+  ((180 240, 180 160, 240 220, 180 240)))
   </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>mA - shells touch, disconnected exterior (valid) </desc>
+      <a>
+MULTIPOLYGON (((100 20, 180 20, 180 100, 100 100, 100 20)), 
+  ((20 100, 100 100, 100 180, 20 180, 20 100)), 
+  ((100 180, 180 180, 180 260, 100 260, 100 180)), 
+  ((180 100, 260 100, 260 180, 180 180, 180 100)))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>L - no repeated points</desc>
+      <desc>mA - shells touch at one point (valid) </desc>
       <a>
-LINESTRING (40 180, 120 120, 140 200, 200 140, 240 200)
+MULTIPOLYGON (((110 110, 70 200, 150 200, 110 110)), 
+  ((110 110, 150 20, 70 20, 110 110)))
   </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+   
+   <case>
+      <desc>mA - shell touches other shell at all vertices (valid) </desc>
+      <a>
+MULTIPOLYGON (((180 60, 240 160, 300 60, 180 60)), 
+  ((80 80, 180 60, 160 140, 240 160, 360 140, 300 60, 420 100, 320 280, 120 260, 80 80)))
+  </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
    <case>
-      <desc>L - invalid ordinate</desc>
+      <desc>mA - shell is inside hole (valid) </desc>
       <a>
-LINESTRING (40 180, 120 120, 140 200, 200 140, NaN 200)
+MULTIPOLYGON (((0 0, 0 8, 8 8, 8 0, 0 0), 
+  (3 3, 7 3, 7 7, 3 7, 3 3), 
+  (1 1, 2 1, 2 2, 1 2, 1 1)), 
+  ((4 4, 4 6, 6 6, 6 4, 4 4)))
   </a>
-      <test>
-         <op name="isValid" arg1="A">    false  </op>
-      </test>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
+   </case>
+
+   <case>
+      <desc>mA - non-empty and empty polygon (valid) </desc>
+      <a> MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)), EMPTY)  
+      </a>
+      <test> <op name="isValid" arg1="A"> true </op>  </test>
    </case>
 
+<!--  ================================================  -->
+   
    <case>
-      <desc>L - repeated points</desc>
+      <desc>P - invalid NaN X ordinate</desc>
       <a>
-LINESTRING (40 180, 120 120, 140 200, 140 200, 200 140, 240 200)
+    POINT(NaN 10)
   </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>L - linestring with two identical points </desc>
-      <a>LINESTRING(0 0, 0 0)</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <desc>P - invalid NaN Y ordinate</desc>
+      <a>
+    POINT(10 NaN)
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mL - MultiLinestring with two identical points in first component</desc>
-      <a>MULTILINESTRING((1 1, 0 0), (0 0, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <desc>mP - NaN ordinate</desc>
+      <a>
+    MULTIPOINT((10 10), (20 20), (30 30), (10 NaN))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+   
+   <case>
+      <desc>L - NaN ordinate</desc>
+      <a>
+LINESTRING (40 180, 120 120, 140 200, 200 140, NaN 200)
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+
+
+   <case>
+      <desc>L - linestring with two identical points (too few distinct points) </desc>
+      <a>LINESTRING(0 0, 0 0)</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
+   
 
    <case>
-      <desc>mL - MultiLinestring with two identical points in second component</desc>
-      <a>MULTILINESTRING((1 1, 0 0), (0 0, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <desc>mL - MultiLinestring with two identical points in first component (too few distinct points)</desc>
+      <a>MULTILINESTRING((0 0, 0 0), (1 1, 0 0))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mL - MultiLinestring with empty component</desc>
-      <a>MULTILINESTRING((1 1, 0 0), EMPTY)</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+      <desc>mL - MultiLinestring with two identical points in second component (too few distinct points)</desc>
+      <a>MULTILINESTRING((1 1, 0 0), (0 0, 0 0))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
-  <case>
+   <case>
       <desc>mL - MultiLinestring with invalid point</desc>
       <a>MULTILINESTRING((1 1, 2 NaN, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - zero-area polygon </desc>
+      <desc>LR - linear-ring bowtie (self-intersection)</desc>
+      <a>LINEARRING(0 0, 100 100, 100 0, 0 100, 0 0)</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - zero-area polygon (too few distinct points) </desc>
       <a>POLYGON ((0 0, 0 0, 0 0, 0 0, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - polygon with too few points </desc>
+      <desc>A - zero-area polygon with multiple points (self-intersection) </desc>
       <a>POLYGON ((0 0, 10 0, 20 0, 0 0, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - polygon with invalid point </desc>
-      <a>POLYGON ((0 0, 10 NaN, 20 0, 0 10, 0 0))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <desc>A - polygon with too few points (too few distinct points) </desc>
+      <a>POLYGON ((0 0, 10 10, 0 0))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - polygon with repeated point </desc>
-      <a>POLYGON ((107 246, 107 246, 250 285, 294 137, 151 90, 15 125, 157 174, 107 246))</a>
-      <test>
-         <op name="isValid" arg1="A"> true </op>
-      </test>
+      <desc>A - polygon with invalid point </desc>
+      <a>POLYGON ((0 0, 10 NaN, 20 0, 0 10, 0 0))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - polygon with degenerate hole ring (A-B-A) </desc>
-      <a>POLYGON ((0 0, 0 240, 260 240, 260 0, 0 0),
+      <desc>A - polygon with degenerate hole ring A-B-C-B--A (self-intersection) </desc>
+      <a>POLYGON ((0 0, 0 240, 260 240, 260 0, 0 0), 
   (220 200, 40 200, 40 20, 40 200, 220 200, 220 200))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mA - multipolygon with component with too few points </desc>
+      <desc>mA - multipolygon with component with too few points (too few distinct points) </desc>
       <a>MULTIPOLYGON ( ((100 20, 180 20, 180 100, 100 100, 100 20)),
 ((20 100, 100 100, 100 180, 20 180, 20 100)),
 ((100 180, 180 180, 180 260, 100 260, 100 180)),
 ((180 100, 180 180, 180 180, 180 100)))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+
+   <case>
+      <desc>A - shell self-touches at vertex (self-intersection) </desc>
+      <a>
+POLYGON ((340 320, 340 200, 200 280, 200 80, 340 200, 340 20, 60 20, 60 340, 340 320))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+
+   <case>
+      <desc>A - shell self-touches at non-vertex (self-intersection) </desc>
+      <a>
+POLYGON ((300 320, 300 220, 260 260, 180 220, 360 220, 360 140, 120 140, 120 320, 300 320)) </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - polygon self-intersects at non-vertex</desc>
+      <desc>A - shell self-crosses at non-vertex (self-intersection)</desc>
       <a>POLYGON ((0 40, 0 0, 40 40, 40 0, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - polygon self-intersects at vertex</desc>
+      <desc>A - shell self-crosses at vertex (self-intersection)</desc>
       <a>MULTIPOLYGON ( ((0 40, 20 20, 40 0, 40 40, 20 20, 0 0, 0 40)) ) </a>
-      <test>
-         <op name="isValid" arg1="A"> false </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - polygon self-intersects at vertex/non-vertex</desc>
+      <desc>A - shell self-crosses at vertex/non-vertex (self-intersection)</desc>
       <a>POLYGON ((0 40, 20 20, 40 0, 40 40, 0 0, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - hole self-intersects at non-vertex</desc>
+      <desc>A - hole self-crosses at non-vertex (self-intersection)</desc>
       <a>POLYGON ((-10 50, 50 50, 50 -10, -10 -10, -10 50), (0 40, 0 0, 40 40, 40 0, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - polygon self-intersects at vertex</desc>
+      <desc>A - hole self-crosses at vertex (self-intersection)</desc>
       <a>POLYGON ((-10 50, 50 50, 50 -10, -10 -10, -10 50), (0 40, 20 20, 40 0, 40 40, 20 20, 0 0, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - polygon self-intersects at vertex/non-vertex</desc>
+      <desc>A - hole self-crosses at vertex/non-vertex (self-intersection)</desc>
       <a>POLYGON ((-10 50, 50 50, 50 -10, -10 -10, -10 50), (0 40, 20 20, 40 0, 40 40, 0 0, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - Valid doughnut</desc>
-      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 40 20, 40 40, 20 40))</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - shell has repeated points</desc>
-      <a>POLYGON ((0 60, 0 0, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 40 20, 40 40, 20 40))</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - shell touches hole without crossing it (valid)</desc>
-      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (20 40, 20 20, 60 20, 20 40))</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - shell touches hole without crossing it, but does so twice (invalid)</desc>
-      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (0 40, 20 20, 60 20, 0 40))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - hole touches hole without crossing it (valid)</desc>
-      <a>POLYGON ((0 120, 0 0, 140 0, 140 120, 0 120), (100 100, 100 20, 120 20, 120 100, 100 100), (20 100, 20 40, 100 40, 20 100))</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - holel touches hole without crossing it, but does so twice (invalid)</desc>
-      <a>POLYGON ((0 120, 0 0, 140 0, 140 120, 0 120),
-		(100 100, 100 20, 120 20, 120 100, 100 100),
-		(20 100, 20 40, 100 40, 80 60, 100 80, 20 100))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - hole touches hole without crossing it, but does so at an infinite number of points (invalid)</desc>
-      <a>POLYGON ((0 120, 0 0, 140 0, 140 120, 0 120),
-		(100 100, 100 20, 120 20, 120 100, 100 100),
+      <desc>A - hole adjacent to hole (self-intersection)</desc>
+      <a>POLYGON ((0 120, 0 0, 140 0, 140 120, 0 120), 
+		(100 100, 100 20, 120 20, 120 100, 100 100), 
 		(20 100, 20 40, 100 40, 100 80, 20 100))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - spike (invalid)</desc>
+      <desc>A - spike (self-intersection))</desc>
       <a>POLYGON ((0 60, 0 0, 60 0, 60 20, 100 20, 60 20, 60 60, 0 60))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
    <case>
-      <desc>A - puncture (invalid)</desc>
+      <desc>A - gore (self-intersection)</desc>
       <a>POLYGON ((0 60, 0 0, 60 0, 60 20, 20 20, 60 20, 60 60, 0 60))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
+   
    <case>
-      <desc>A - hole within a hole (invalid)</desc>
-      <a>POLYGON ((0 140, 0 0, 180 0, 180 140, 0 140), (20 20, 160 20, 160 120, 20 120, 20 20), (40 100, 40 40, 140 40, 140 100, 40 100))</a>
-      <test>
-         <op name="isValid" arg1="A">false</op>
-      </test>
-   </case>
-   <case>
-      <desc>A - non-empty shell and empty hole (valid)</desc>
-      <a>POLYGON ((60 280, 260 180, 60 80, 60 280), EMPTY)</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - empty shell and holes (valid)</desc>
-      <a>POLYGON (EMPTY, EMPTY, EMPTY)</a>
-      <test>
-         <op name="isValid" arg1="A">true</op>
-      </test>
+      <desc>A - hole crossing shell at non-vertex (self-intersection)</desc>
+      <a>POLYGON ((60 280, 260 180, 60 80, 60 280), (140 80, 120 180, 200 180, 140 80))  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole overlapping shell at non-vertex</desc>
-      <a>
-POLYGON ((60 280, 260 180, 60 80, 60 280),
-  (140 80, 120 180, 200 180, 140 80))
-  </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - shell self-overlaps </desc>
+      <desc>A - shell self-overlaps (self-intersection) </desc>
       <a>
 POLYGON ((60 340, 60 100, 340 100, 340 280, 340 200, 340 340, 60 340))
   </a>
-      <test>
-         <op name="isValid" arg1="A">    false  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole with repeated points</desc>
-      <a>
-POLYGON ((40 260, 40 60, 120 60, 180 160, 240 60, 300 60, 300 260, 40 260),
-  (70 230, 80 230, 80 220, 80 220, 70 230))  </a>
-      <test>
-         <op name="isValid" arg1="A">
-    true
-  </op>
-      </test>
-   </case>
-   <case>
-      <desc>A - hole outside but adjacent to shell</desc>
+      <desc>A - hole outside, adjacent (self-intersection)</desc>
       <a>
-POLYGON ((40 260, 40 60, 120 60, 180 160, 240 60, 300 60, 300 260, 40 260),
+POLYGON ((40 260, 40 60, 120 60, 180 160, 240 60, 300 60, 300 260, 40 260), 
   (180 160, 240 60, 120 60, 180 160))  </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - hole touches shell at two points</desc>
-      <a>
-POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260),
-  (140 180, 40 180, 140 260, 140 180))
-  </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - hole touches shell at one non-vertex point</desc>
-      <a>
-POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260),
-  (140 180, 40 180, 140 240, 140 180))
-  </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - hole touches shell at one vertex point</desc>
-      <a>
-POLYGON ((240 260, 40 260, 40 80, 240 80, 240 260),
-  (140 180, 40 260, 140 240, 140 180))
-  </a>
-      <test>
-         <op name="isValid" arg1="A">    true  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole outside shell</desc>
+      <desc>A - hole outside, disjoint (hole outside shell)</desc>
       <a>
-POLYGON ((20 180, 20 20, 140 20, 140 180, 20 180),
-  (160 120, 180 100, 160 80, 160 120))
+POLYGON ((20 180, 20 20, 140 20, 140 180, 20 180), (160 120, 180 100, 160 80, 160 120))  
 </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole identical to shell</desc>
+      <desc>A - hole outside, all vertices touch (hole outside shell)</desc>
       <a>
-POLYGON ((20 180, 20 20, 140 20, 140 180, 20 180),
-  (20 180, 20 20, 140 20, 140 180, 20 180))
+POLYGON ((10 10, 30 10, 30 50, 70 50, 70 10, 90 10, 90 90, 10 90, 10 10), (50 50, 30 10, 70 10, 50 50))
 </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole identical to shell</desc>
+      <desc>A - hole identical to shell (self-intersection) </desc>
       <a>
-POLYGON ((20 180, 20 20, 140 20, 140 180, 20 180),
-  (20 180, 20 20, 140 20, 140 180, 20 180))
+POLYGON ((20 180, 20 20, 140 20, 140 180, 20 180), 
+  (20 180, 20 20, 140 20, 140 180, 20 180))  
 </a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole self-intersects </desc>
+      <desc>A - hole self-touch at vertex-segment (self-intersection) </desc>
       <a>
-POLYGON ((380 340, 40 340, 40 20, 380 20, 380 340),
+POLYGON ((380 340, 40 340, 40 20, 380 20, 380 340), 
   (120 300, 300 280, 320 200, 160 140, 200 80, 320 120, 320 200, 360 60, 120 40, 120 300))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - holes overlap, first point is identical </desc>
+      <desc>A - hole inside hole, disjoint (nested holes)</desc>
+      <a>POLYGON ((0 140, 0 0, 180 0, 180 140, 0 140), (20 20, 160 20, 160 120, 20 120, 20 20), (40 100, 40 40, 140 40, 140 100, 40 100))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
+   </case>
+   
+   <case>
+      <desc>A - hole inside hole, first point is identical (nested holes) </desc>
       <a>
-POLYGON ((20 320, 260 320, 260 20, 20 20, 20 320),
-  (140 280, 80 100, 200 100, 140 280),
+POLYGON ((20 320, 260 320, 260 20, 20 20, 20 320), 
+  (140 280, 80 100, 200 100, 140 280), 
   (140 280, 40 80, 240 80, 140 280))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - holes do not overlap, first point is identical </desc>
+      <desc>A - hole inside hole, all vertices touch (nested holes) </desc>
       <a>
-POLYGON ((20 320, 240 320, 240 40, 20 40, 20 320),
-  (140 180, 60 120, 60 240, 140 180),
-  (140 180, 200 120, 200 240, 140 180))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+POLYGON ((10 90, 90 90, 90 10, 10 10, 10 90), (20 80, 80 80, 80 20, 20 20, 20 80), (50 80, 20 50, 50 20, 80 50, 50 80))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - shell self-touches at vertex </desc>
+      <desc>A - duplicate holes (self-intersection) </desc>
       <a>
-POLYGON ((340 320, 340 200, 200 280, 200 80, 340 200, 340 20, 60 20, 60 340, 340 320))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+POLYGON ((100 200, 200 200, 200 100, 100 100, 100 200), (120 180, 180 180, 180 120, 120 120, 120 180), (120 180, 180 180, 180 120, 120 120, 120 180))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - shell self-touches at non-vertex </desc>
-      <a>
-POLYGON ((300 320, 300 220, 260 260, 180 220, 360 220, 360 140, 120 140, 120 320, 300 320))	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <desc>A - hole touches shell twice (interior disconnected)</desc>
+      <a>POLYGON ((0 60, 0 0, 60 0, 60 60, 0 60), (0 40, 20 20, 60 20, 0 40))</a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
-
+   
     <case>
-      <desc>A - chain of holes surrounds an island inside the polygon </desc>
+      <desc>A - chain of holes surrounds area (interior disconnected) </desc>
       <a>
-POLYGON ((40 300, 40 20, 280 20, 280 300, 40 300),
-  (120 240, 80 180, 160 220, 120 240),
-  (220 240, 160 220, 220 160, 220 240),
-  (160 100, 80 180, 100 80, 160 100),
+POLYGON ((40 300, 40 20, 280 20, 280 300, 40 300), 
+  (120 240, 80 180, 160 220, 120 240), 
+  (220 240, 160 220, 220 160, 220 240), 
+  (160 100, 80 180, 100 80, 160 100), 
   (160 100, 220 160, 240 100, 160 100))	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - chain of holes splits polygon in two (touching at vertices) </desc>
+      <desc>A - chain of holes splits polygon, touches at vertices (interior disconnected) </desc>
       <a>
-POLYGON ((40 320, 340 320, 340 20, 40 20, 40 320),
-  (100 120, 40 20, 180 100, 100 120),
-  (200 200, 180 100, 240 160, 200 200),
-  (260 260, 240 160, 300 200, 260 260),
-  (300 300, 300 200, 340 320, 300 300))
+POLYGON ((40 320, 340 320, 340 20, 40 20, 40 320), 
+  (100 120, 40 20, 180 100, 100 120), 
+  (200 200, 180 100, 240 160, 200 200), 
+  (260 260, 240 160, 300 200, 260 260), 
+  (300 300, 300 200, 340 320, 300 300))	
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - chain of holes splits polygon in two (touching at non-vertex) </desc>
+      <desc>A - chain of holes splits polygon, touches at non-vertex (interior disconnected) </desc>
       <a>
-POLYGON ((40 320, 340 320, 340 20, 40 20, 40 320),
-  (100 120, 40 20, 180 100, 100 120),
-  (200 200, 180 100, 240 160, 200 200),
-  (260 260, 240 160, 300 200, 260 260),
+POLYGON ((40 320, 340 320, 340 20, 40 20, 40 320), 
+  (100 120, 40 20, 180 100, 100 120), 
+  (200 200, 180 100, 240 160, 200 200), 
+  (260 260, 240 160, 300 200, 260 260), 
   (300 300, 300 200, 340 260, 300 300))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - holes touch in one point </desc>
-      <a>
-POLYGON ((190 190, 360 20, 20 20, 190 190),
-  (90 50, 150 110, 190 50, 90 50),
-  (190 50, 230 110, 290 50, 190 50))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - holes touch in one point </desc>
-      <a>
-POLYGON ((190 190, 360 20, 20 20, 190 190),
-  (90 50, 150 110, 190 50, 90 50),
-  (190 50, 230 110, 290 50, 190 50))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - one holes touches another at all vertices </desc>
+      <desc>A - hole touches hole at all vertices (interior disconnected) </desc>
       <a>
 POLYGON( (0 0, 0 5, 6 5, 6 0, 0 0), (2 1, 4 1, 3 2, 2 1), (2 1, 1 4, 5 4, 4 1, 4 3, 3 2, 2 3, 2 1) )
 </a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - one holes touches another at several vertices </desc>
+      <desc>A - hole touches hole at two vertices (interior disconnected) </desc>
       <a>
-POLYGON ((0 0, 0 5, 6 5, 6 0, 0 0),
-  (2.5 1, 3.5 1, 3.5 2, 2.5 2, 2.5 1),
+POLYGON ((0 0, 0 5, 6 5, 6 0, 0 0), 
+  (2.5 1, 3.5 1, 3.5 2, 2.5 2, 2.5 1), 
   (2.5 1.5, 1 4, 5 4, 3.5 1.5, 4 3, 3 2, 2 3, 2.5 1.5))
   </a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>A - hole disconnects interiors </desc>
+      <desc>A - hole touches shell at all vertices (interior disconnected) </desc>
       <a>
-POLYGON ((0 0, 10 10, 10 0, 0 0),
+POLYGON ((0 0, 10 10, 10 0, 0 0), 
   (5 5, 5 0, 10 5, 5 5))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>A - touching holes do NOT disconnect (isCCW bug) </desc>
-      <a>
-POLYGON ((60 40, 60 240, 460 240, 460 40, 60 40),
-  (260 200, 340 60, 400 120, 260 200),
-  (260 200, 120 100, 200 60, 260 200))
-  	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
-
-
-
+  
   <case>
-      <desc>mA - adjacent shells (shared vertices) </desc>
+      <desc>mA - shells adjacent, same vertices (self-intersection) </desc>
       <a>
-MULTIPOLYGON (((40 120, 140 120, 140 40, 40 40, 40 120)),
+MULTIPOLYGON (((40 120, 140 120, 140 40, 40 40, 40 120)), 
   ((140 120, 40 120, 40 200, 140 200, 140 120)))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mA - adjacent shells (different vertices) </desc>
+      <desc>mA - shells adjacent, different vertices (self-intersection) </desc>
       <a>
-MULTIPOLYGON (((40 120, 140 120, 140 40, 40 40, 40 120)),
+MULTIPOLYGON (((40 120, 140 120, 140 40, 40 40, 40 120)), 
   ((160 120, 60 120, 40 200, 140 200, 160 120)))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">
-    false
-  </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - nested overlapping shells </desc>
-      <a>
-MULTIPOLYGON (((80 260, 240 260, 240 100, 80 100, 80 260)),
-  ((120 240, 220 240, 220 140, 120 140, 120 240)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
-
-   <case>
-      <desc>mA - nested overlapping shells, one with hole </desc>
-      <a>
-MULTIPOLYGON (((110 190, 150 190, 150 150, 110 150, 110 190)), ((100 200, 200 200, 200 100, 100 100, 100 200), (160 140, 190 140, 190 110, 160 110, 160 140)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
-   </case>
-
+   
    <case>
-      <desc>mA - nested non-overlapping shells </desc>
+      <desc>mA - duplicate shells (self-intersection) </desc>
       <a>
-MULTIPOLYGON (((60 320, 60 80, 300 80, 60 320),
-  (80 280, 80 100, 260 100, 80 280)),
-  ((120 160, 140 160, 140 140, 120 160)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+MULTIPOLYGON (((60 300, 320 220, 260 60, 60 100, 60 300)), 
+  ((60 300, 320 220, 260 60, 60 100, 60 300)))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mA - nested non-overlapping shells, all vertices touch </desc>
+      <desc>mA - shell inside shell, disjoint (nested shells) </desc>
       <a>
-MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380),
-  (220 340, 180 240, 60 200, 180 160, 340 60, 240 220, 220 340)),
-  ((180 240, 180 160, 240 220, 180 240)))
+MULTIPOLYGON (((80 260, 240 260, 240 100, 80 100, 80 260)), 
+  ((120 240, 220 240, 220 140, 120 140, 120 240)))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mA - nested overlapping shells, all vertices touch </desc>
+      <desc>mA - shell inside shell, all vertices touch (nested shells)</desc>
       <a>
-MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380),
-  (220 340, 180 240, 60 200, 140 100, 340 60, 300 240, 220 340)),
-  ((60 200, 340 60, 220 340, 60 200)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
+MULTIPOLYGON (((10 10, 20 30, 10 90, 90 90, 80 30, 90 10, 50 20, 10 10)), ((80 30, 20 30, 50 20, 80 30)))
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
-
+   
    <case>
-      <desc>mA - nested non-overlapping shells, all vertices touch </desc>
+      <desc>mA - shell inside shell, hole overlapped, all vertices touch hole (self-intersection) </desc>
       <a>
-MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380),
-  (220 340, 80 320, 60 200, 140 100, 340 60, 300 240, 220 340)),
+MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), 
+  (220 340, 180 240, 60 200, 140 100, 340 60, 300 240, 220 340)), 
   ((60 200, 340 60, 220 340, 60 200)))
 	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
    <case>
-      <desc>mA - nested overlapping shells, all vertices touch </desc>
+      <desc>mA - shell inside shell, all vertices touch hole (nested shells)</desc>
       <a>
-MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380),
-  (220 340, 180 240, 60 200, 200 180, 340 60, 240 220, 220 340)),
+MULTIPOLYGON (((20 380, 420 380, 420 20, 20 20, 20 380), 
+  (220 340, 180 240, 60 200, 200 180, 340 60, 240 220, 220 340)), 
   ((60 200, 340 60, 220 340, 60 200)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - disconnected exterior </desc>
-      <a>
-MULTIPOLYGON (((100 20, 180 20, 180 100, 100 100, 100 20)),
-  ((20 100, 100 100, 100 180, 20 180, 20 100)),
-  ((100 180, 180 180, 180 260, 100 260, 100 180)),
-  ((180 100, 260 100, 260 180, 180 180, 180 100)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - shells touch in single point </desc>
-      <a>
-MULTIPOLYGON (((110 110, 70 200, 150 200, 110 110)),
-  ((110 110, 150 20, 70 20, 110 110)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - duplicate shells </desc>
-      <a>
-MULTIPOLYGON (((60 300, 320 220, 260 60, 60 100, 60 300)),
-  ((60 300, 320 220, 260 60, 60 100, 60 300)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      false      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - shells are not nested but share all vertices </desc>
-      <a>
-MULTIPOLYGON (((180 60, 240 160, 300 60, 180 60)),
-  ((80 80, 180 60, 160 140, 240 160, 360 140, 300 60, 420 100, 320 280, 120 260, 80 80)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - shell is nested inside first hole </desc>
-      <a>
-MULTIPOLYGON (((0 0, 0 8, 8 8, 8 0, 0 0),
-  (3 3, 7 3, 7 7, 3 7, 3 3),
-  (1 1, 2 1, 2 2, 1 2, 1 1)),
-  ((4 4, 4 6, 6 6, 6 4, 4 4)))
-	</a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
-   </case>
-
-   <case>
-      <desc>mA - non-empty and empty polygon </desc>
-      <a> MULTIPOLYGON (((30 10, 40 40, 20 40, 10 20, 30 10)), EMPTY)  </a>
-      <test>
-         <op name="isValid" arg1="A">      true      </op>
-      </test>
+  </a>
+      <test>  <op name="isValid" arg1="A"> false  </op>  </test>
    </case>
 
 </run>

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

Summary of changes:
 .azure-pipelines.yml                               |  63 +-
 .github/workflows/ci.yml                           |   4 +-
 capi/geos_ts_c.cpp                                 |  10 +-
 include/geos/algorithm/BoundaryNodeRule.h          |   2 +-
 include/geos/noding/BasicSegmentString.h           |   4 +
 .../geos/operation/valid/IndexedNestedHoleTester.h |  83 ++
 .../operation/valid/IndexedNestedPolygonTester.h   | 111 +++
 .../geos/operation/valid/IndexedNestedRingTester.h | 109 ---
 .../operation/valid/IndexedNestedShellTester.h     | 108 ---
 include/geos/operation/{ => valid}/IsSimpleOp.h    |   4 +-
 include/geos/operation/valid/IsValidOp.h           | 366 ++++----
 .../operation/valid/PolygonIntersectionAnalyzer.h  | 127 +++
 include/geos/operation/valid/PolygonNode.h         | 113 +++
 include/geos/operation/valid/PolygonRing.h         | 232 +++++
 include/geos/operation/valid/PolygonRingSelfNode.h |  89 ++
 .../PolygonRingTouch.h}                            |  45 +-
 .../geos/operation/valid/PolygonTopologyAnalyzer.h | 198 +++++
 .../operation/valid/QuadtreeNestedRingTester.h     | 103 ---
 .../geos/operation/valid/SimpleNestedRingTester.h  | 100 ---
 .../operation/valid/SweeplineNestedRingTester.h    | 129 ---
 .../geos/operation/valid/TopologyValidationError.h |  13 +-
 src/geom/Geometry.cpp                              |   4 +-
 src/geom/HeuristicOverlay.cpp                      |   9 +-
 src/operation/buffer/BufferBuilder.cpp             |   8 +-
 src/operation/polygonize/EdgeRing.cpp              |   1 -
 src/operation/polygonize/Polygonizer.cpp           |   1 -
 src/operation/union/CascadedPolygonUnion.cpp       |   4 +-
 src/operation/valid/IndexedNestedHoleTester.cpp    |  82 ++
 src/operation/valid/IndexedNestedPolygonTester.cpp | 198 +++++
 src/operation/valid/IndexedNestedRingTester.cpp    | 106 ---
 src/operation/valid/IndexedNestedShellTester.cpp   | 212 -----
 src/operation/{ => valid}/IsSimpleOp.cpp           |  33 +-
 src/operation/valid/IsValidOp.cpp                  | 750 +++++++---------
 .../valid/PolygonIntersectionAnalyzer.cpp          | 226 +++++
 src/operation/valid/PolygonNode.cpp                | 114 +++
 src/operation/valid/PolygonRing.cpp                | 255 ++++++
 src/operation/valid/PolygonRingSelfNode.cpp        |  45 +
 .../PolygonRingTouch.cpp}                          |  40 +-
 src/operation/valid/PolygonTopologyAnalyzer.cpp    | 302 +++++++
 src/operation/valid/QuadtreeNestedRingTester.cpp   | 131 ---
 src/operation/valid/SimpleNestedRingTester.cpp     |  73 --
 src/operation/valid/SweeplineNestedRingTester.cpp  | 109 ---
 src/operation/valid/TopologyValidationError.cpp    |  15 +-
 src/precision/CommonBitsOp.cpp                     |  19 +-
 tests/unit/capi/GEOSisValidDetailTest.cpp          |   2 +-
 tests/unit/operation/IsSimpleOpTest.cpp            | 188 -----
 tests/unit/operation/valid/IsSimpleOpTest.cpp      | 208 +++++
 tests/unit/operation/valid/IsValidOpTest.cpp       | 202 ++++-
 .../valid/ValidSelfTouchingRingFormingHoleTest.cpp |  39 +-
 tests/xmltester/XMLTester.cpp                      |   3 +-
 tests/xmltester/tests/general/TestValid.xml        | 939 +++++++++------------
 51 files changed, 3666 insertions(+), 2665 deletions(-)
 create mode 100644 include/geos/operation/valid/IndexedNestedHoleTester.h
 create mode 100644 include/geos/operation/valid/IndexedNestedPolygonTester.h
 delete mode 100644 include/geos/operation/valid/IndexedNestedRingTester.h
 delete mode 100644 include/geos/operation/valid/IndexedNestedShellTester.h
 rename include/geos/operation/{ => valid}/IsSimpleOp.h (98%)
 create mode 100644 include/geos/operation/valid/PolygonIntersectionAnalyzer.h
 create mode 100644 include/geos/operation/valid/PolygonNode.h
 create mode 100644 include/geos/operation/valid/PolygonRing.h
 create mode 100644 include/geos/operation/valid/PolygonRingSelfNode.h
 copy include/geos/operation/{overlayng/IndexedPointOnLineLocator.h => valid/PolygonRingTouch.h} (50%)
 create mode 100644 include/geos/operation/valid/PolygonTopologyAnalyzer.h
 delete mode 100644 include/geos/operation/valid/QuadtreeNestedRingTester.h
 delete mode 100644 include/geos/operation/valid/SimpleNestedRingTester.h
 delete mode 100644 include/geos/operation/valid/SweeplineNestedRingTester.h
 create mode 100644 src/operation/valid/IndexedNestedHoleTester.cpp
 create mode 100644 src/operation/valid/IndexedNestedPolygonTester.cpp
 delete mode 100644 src/operation/valid/IndexedNestedRingTester.cpp
 delete mode 100644 src/operation/valid/IndexedNestedShellTester.cpp
 rename src/operation/{ => valid}/IsSimpleOp.cpp (91%)
 create mode 100644 src/operation/valid/PolygonIntersectionAnalyzer.cpp
 create mode 100644 src/operation/valid/PolygonNode.cpp
 create mode 100644 src/operation/valid/PolygonRing.cpp
 create mode 100644 src/operation/valid/PolygonRingSelfNode.cpp
 copy src/operation/{overlayng/IndexedPointOnLineLocator.cpp => valid/PolygonRingTouch.cpp} (50%)
 create mode 100644 src/operation/valid/PolygonTopologyAnalyzer.cpp
 delete mode 100644 src/operation/valid/QuadtreeNestedRingTester.cpp
 delete mode 100644 src/operation/valid/SimpleNestedRingTester.cpp
 delete mode 100644 src/operation/valid/SweeplineNestedRingTester.cpp
 delete mode 100644 tests/unit/operation/IsSimpleOpTest.cpp
 create mode 100644 tests/unit/operation/valid/IsSimpleOpTest.cpp


hooks/post-receive
-- 
GEOS


More information about the geos-commits mailing list