/* * The JTS Topology Suite is a collection of Java classes that * implement the fundamental operations required to validate a given * geo-spatial data set to a known topological specification. * * Copyright (C) 2001 Vivid Solutions * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * For more information, contact: * * Vivid Solutions * Suite #1A * 2328 Government Street * Victoria BC V8T 5G5 * Canada * * (250)385-6040 * www.vividsolutions.com */ package com.revolsys.geometry.operation.valid; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.TreeSet; import com.revolsys.geometry.algorithm.LineIntersector; import com.revolsys.geometry.algorithm.MCPointInRing; import com.revolsys.geometry.algorithm.PointInRing; import com.revolsys.geometry.algorithm.RobustLineIntersector; import com.revolsys.geometry.geomgraph.Edge; import com.revolsys.geometry.geomgraph.EdgeIntersection; import com.revolsys.geometry.geomgraph.EdgeIntersectionList; import com.revolsys.geometry.geomgraph.GeometryGraph; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.Lineal; import com.revolsys.geometry.model.LinearRing; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.Polygon; import com.revolsys.geometry.model.Polygonal; import com.revolsys.geometry.model.Punctual; import com.revolsys.geometry.model.impl.PointDoubleXY; import com.revolsys.geometry.model.vertex.Vertex; import com.revolsys.geometry.util.Assert; import com.revolsys.util.Strings; /** * Implements the algorithms required to compute the <code>isValid()</code> method * for {@link Geometry}s. * See the documentation for the various geometry types for a specification of validity. * * @version 1.7 */ public class IsValidOp { /** * 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 */ public static Point findPtNotNode(final LineString testLine, final LinearRing searchRing, final GeometryGraph graph) { // find edge corresponding to searchRing. final Edge searchEdge = graph.findEdge(searchRing); // find a point in the testCoords which is not a node of the searchRing final EdgeIntersectionList eiList = searchEdge.getEdgeIntersectionList(); // somewhat inefficient - is there a better way? (Use a node map, for // instance?) final int vertexCount = testLine.getVertexCount(); for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) { final double x = testLine.getX(vertexIndex); final double y = testLine.getY(vertexIndex); if (!eiList.isIntersection(x, y)) { return new PointDoubleXY(x, y); } } return null; } /** * Tests whether a {@link Geometry} is valid. * @param geom the Geometry to test * @return true if the geometry is valid */ public static boolean isValid(final Geometry geom) { final IsValidOp isValidOp = new IsValidOp(geom); return isValidOp.isValid(); } private final List<GeometryValidationError> errors = new ArrayList<>(); private final Geometry geometry; // the base Geometry to be validated /** * If the following condition is TRUE JTS will validate inverted shells and exverted holes * (the ESRI SDE model) */ private boolean isSelfTouchingRingFormingHoleValid = false; private boolean shortCircuit = true; public IsValidOp(final Geometry geometry) { this.geometry = geometry; } public IsValidOp(final Geometry geometry, final boolean shortCircuit) { this.geometry = geometry; this.shortCircuit = shortCircuit; } private void addError(final GeometryValidationError error) { this.errors.add(error); } private boolean checkClosedRing(final LinearRing ring) { if (ring.isClosed()) { return true; } else { Point point = null; if (ring.getVertexCount() >= 1) { point = ring.getPoint(0); } addError(new TopologyValidationError(TopologyValidationError.RING_NOT_CLOSED, point)); return false; } } private boolean checkClosedRings(final Polygon poly) { boolean valid = checkClosedRing(poly.getShell()); if (isErrorReturn()) { return false; } for (int i = 0; i < poly.getHoleCount(); i++) { valid &= checkClosedRing(poly.getHole(i)); if (isErrorReturn()) { return false; } } return valid; } private boolean checkConnectedInteriors(final GeometryGraph graph) { final ConnectedInteriorTester cit = new ConnectedInteriorTester(graph); if (cit.isInteriorsConnected()) { return true; } else { addError(new TopologyValidationError(TopologyValidationError.DISCONNECTED_INTERIOR, cit.getCoordinate())); return false; } } /** * Checks that the arrangement of edges in a polygonal geometry graph * forms a consistent area. * * @param graph * * @see ConsistentAreaTester */ private boolean checkConsistentArea(final GeometryGraph graph) { final ConsistentAreaTester cat = new ConsistentAreaTester(graph); final boolean isValidArea = cat.isNodeConsistentArea(); if (!isValidArea) { addError(new TopologyValidationError(TopologyValidationError.SELF_INTERSECTION, cat.getInvalidPoint())); return false; } else if (cat.hasDuplicateRings()) { addError(new TopologyValidationError(TopologyValidationError.DUPLICATE_RINGS, cat.getInvalidPoint())); return false; } else { return true; } } /** * 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 oon the same side of it * (i.e that the hole rings do not cross the shell ring). * In other words, this test is only correct if the ConsistentArea test is passed first. * 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 polygon the polygon to be tested for hole inclusion * @param graph a GeometryGraph incorporating the polygon */ private boolean checkHolesInShell(final Polygon polygon, final GeometryGraph graph) { boolean valid = true; final LinearRing shell = polygon.getShell(); final PointInRing pir = new MCPointInRing(shell); for (final LinearRing hole : polygon.holes()) { final Point holePt = findPtNotNode(hole, 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 != null) { final boolean outside = !pir.isInside(holePt); if (outside) { valid = false; addError(new TopologyValidationError(TopologyValidationError.HOLE_OUTSIDE_SHELL, holePt)); if (isErrorReturn()) { return false; } } } } return valid; } /** * 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: * <ul> * <li>they do not partially overlap * (checked by <code>checkRelateConsistency</code>) * <li>they are not identical * (checked by <code>checkRelateConsistency</code>) * </ul> */ private boolean checkHolesNotNested(final Polygon p, final GeometryGraph graph) { final IndexedNestedRingTester nestedTester = new IndexedNestedRingTester(graph); for (int i = 0; i < p.getHoleCount(); i++) { final LinearRing innerHole = p.getHole(i); nestedTester.add(innerHole); } final boolean isNonNested = nestedTester.isNonNested(); if (isNonNested) { return true; } else { addError(new TopologyValidationError(TopologyValidationError.NESTED_HOLES, nestedTester.getNestedPoint())); return false; } } private boolean checkInvalidCoordinates(final Geometry geometry) { boolean valid = true; for (final Vertex vertex : geometry.vertices()) { for (int axisIndex = 0; axisIndex < 2; axisIndex++) { final double value = vertex.getCoordinate(axisIndex); if (Double.isNaN(value)) { addError(new CoordinateNaNError(vertex, axisIndex)); if (isErrorReturn()) { return false; } else { valid = false; } } else if (Double.isInfinite(value)) { addError(new CoordinateInfiniteError(vertex, axisIndex)); if (isErrorReturn()) { return false; } else { valid = false; } } } } return valid; } /** * 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. */ private boolean checkNoSelfIntersectingRing(final EdgeIntersectionList eiList) { boolean valid = true; final Set<Point> nodeSet = new TreeSet<>(); boolean isFirst = true; for (final EdgeIntersection ei : eiList) { if (isFirst) { isFirst = false; } else if (nodeSet.contains(ei)) { valid = false; addError(new TopologyValidationError(TopologyValidationError.RING_SELF_INTERSECTION, ei.newPoint2D())); if (isErrorReturn()) { return false; } } else { nodeSet.add(ei.newPoint2D()); } } return valid; } /** * 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 */ private boolean checkNoSelfIntersectingRings(final GeometryGraph graph) { boolean valid = true; for (final Edge edge : graph.edges()) { final EdgeIntersectionList edgeIntersectionList = edge.getEdgeIntersectionList(); valid &= checkNoSelfIntersectingRing(edgeIntersectionList); if (isErrorReturn()) { return false; } } return valid; } /** * 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 Point which is not inside the hole if it is not * */ private Point checkShellInsideHole(final LinearRing shell, final LinearRing hole, final GeometryGraph graph) { // TODO: improve performance of this - by sorting LineStrings for instance? final Point shellPt = findPtNotNode(shell, hole, graph); // if point is on shell but not hole, check that the shell is inside the // hole if (shellPt != null) { final boolean insideHole = hole.isPointInRing(shellPt); if (!insideHole) { return shellPt; } } final Point holePt = findPtNotNode(hole, shell, graph); // if point is on hole but not shell, check that the hole is outside the // shell if (holePt != null) { final boolean insideShell = shell.isPointInRing(holePt); if (insideShell) { return holePt; } return null; } Assert.shouldNeverReachHere("points in shell and hole appear to be equal"); return null; } /** * 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.) * <p> * 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> ) */ private boolean checkShellNotNested(final LinearRing shell, final Polygon polygon, final GeometryGraph graph) { // test if shell is inside polygon shell final LinearRing polyShell = polygon.getShell(); final Point shellPt = findPtNotNode(shell, polyShell, graph); // if no point could be found, we can assume that the shell is outside the // polygon if (shellPt == null) { return true; } else { final boolean insidePolyShell = polyShell.isPointInRing(shellPt); if (!insidePolyShell) { return true; } // if no holes, this is an error! if (polygon.getHoleCount() <= 0) { addError(new TopologyValidationError(TopologyValidationError.NESTED_SHELLS, shellPt)); return false; } /** * 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. * Otherwise, the shell is not properly contained in a hole, which is an error. */ Point badNestedPt = null; for (int i = 0; i < polygon.getHoleCount(); i++) { final LinearRing hole = polygon.getHole(i); badNestedPt = checkShellInsideHole(shell, hole, graph); if (badNestedPt == null) { return true; } } addError(new TopologyValidationError(TopologyValidationError.NESTED_SHELLS, badNestedPt)); return false; } } /** * Tests that no element polygon is wholly in the interior of another element polygon. * <p> * Preconditions: * <ul> * <li>shells do not partially overlap * <li>shells do not touch along an edge * <li>no duplicate rings exist * </ul> * This routine relies on the fact that while polygon shells may touch at one or * more vertices, they cannot touch at ALL vertices. */ private boolean checkShellsNotNested(final Polygonal polygonal, final GeometryGraph graph) { boolean valid = true; final List<Polygon> polygons = polygonal.getPolygons(); final int polygonCount = polygons.size(); for (int i = 0; i < polygonCount; i++) { final Polygon polygon1 = polygons.get(i); final LinearRing shell = polygon1.getShell(); for (int j = 0; j < polygonCount; j++) { if (i != j) { final Polygon polygon2 = polygons.get(j); valid &= checkShellNotNested(shell, polygon2, graph); if (isErrorReturn()) { return false; } } } } return valid; } private boolean checkTooFewPoints(final GeometryGraph graph) { if (graph.hasTooFewPoints()) { addError(new TopologyValidationError(TopologyValidationError.TOO_FEW_POINTS, graph.getInvalidPoint())); return false; } else { return true; } } private boolean checkTooFewVertices(final LineString line, final int minVertexCount) { int edgeCount = 0; final int vertexCount = line.getVertexCount(); if (vertexCount > 0) { double x1 = line.getX(0); double y1 = line.getY(0); for (int vertexIndex = 1; vertexIndex < vertexCount; vertexIndex++) { final double x2 = line.getX(vertexIndex); final double y2 = line.getY(vertexIndex); if (x1 != x2 || y1 != y2) { edgeCount++; } x1 = x2; y1 = y2; } } if (edgeCount < minVertexCount - 1) { addError( new TopologyValidationError(TopologyValidationError.TOO_FEW_POINTS, line.getPoint(0))); return false; } else { return true; } } /** * Check geometries to see if they are valid. * * <ul> * <li>Empty geometries are valid</li> * <li>Geometries with x,y coordinates that are NaN or Infinity are invalid. Any other validation will not be performed for those geometries.</li> * <li>Other validation checks are performed based on the type of geometry.</li> * </ul> * @param geometry * @return If the geometry is valid. */ private boolean checkValidGeometry(final Geometry geometry) { if (geometry.isEmpty()) { return true; } else if (!checkInvalidCoordinates(geometry)) { return false; } else if (geometry instanceof Point) { return true; } else if (geometry instanceof Punctual) { return true; } else if (geometry instanceof LinearRing) { return checkValidLinearRing((LinearRing)geometry); } else if (geometry instanceof LineString) { return checkValidLineString((LineString)geometry); } else if (geometry instanceof Lineal) { return checkValidMultiLineString((Lineal)geometry); } else if (geometry instanceof Polygon) { return checkValidPolygon((Polygon)geometry); } else if (geometry instanceof Polygonal) { return checkValidMultiPolygon((Polygonal)geometry); } else if (geometry.isGeometryCollection()) { return checkValidGeometryCollection(geometry); } else { throw new UnsupportedOperationException(geometry.getClass().getName()); } } private boolean checkValidGeometryCollection(final Geometry geometryCollection) { boolean valid = true; for (final Geometry geometry : geometryCollection.geometries()) { valid &= checkValidGeometry(geometry); if (isErrorReturn()) { return false; } } return valid; } /** * Checks validity of a LinearRing. */ private boolean checkValidLinearRing(final LinearRing ring) { boolean valid = true; if (checkTooFewVertices(ring, 4)) { valid &= checkClosedRing(ring); if (isErrorReturn()) { return false; } final GeometryGraph graph = new GeometryGraph(0, ring); final LineIntersector li = new RobustLineIntersector(); graph.computeSelfNodes(li, true); valid &= checkNoSelfIntersectingRings(graph); return valid; } return false; } /** * {@link LineString} geometries require a minimum of 2 vertices. */ private boolean checkValidLineString(final LineString line) { return checkTooFewVertices(line, 2); } private boolean checkValidMultiLineString(final Lineal lineal) { boolean valid = true; for (final LineString lineString : lineal.lineStrings()) { valid &= checkValidLineString(lineString); if (isErrorReturn()) { return false; } } return valid; } private boolean checkValidMultiPolygon(final Polygonal polygonal) { boolean valid = true; for (final Polygon polygon : polygonal.polygons()) { valid &= checkClosedRings(polygon); if (isErrorReturn()) { return false; } } final GeometryGraph graph = new GeometryGraph(0, polygonal); valid &= checkTooFewPoints(graph); if (isErrorReturn()) { return false; } valid &= checkConsistentArea(graph); if (isErrorReturn()) { return false; } if (!this.isSelfTouchingRingFormingHoleValid) { valid &= checkNoSelfIntersectingRings(graph); if (isErrorReturn()) { return false; } } for (final Polygon polygon : polygonal.getPolygons()) { valid &= checkHolesInShell(polygon, graph); if (isErrorReturn()) { return false; } } for (final Polygon polygon : polygonal.getPolygons()) { valid &= checkHolesNotNested(polygon, graph); if (isErrorReturn()) { return false; } } valid &= checkShellsNotNested(polygonal, graph); if (isErrorReturn()) { return false; } valid &= checkConnectedInteriors(graph); return valid; } /** * Checks the validity of a polygon. * Sets the validErr flag. */ private boolean checkValidPolygon(final Polygon g) { boolean valid = true; valid &= checkClosedRings(g); if (isErrorReturn()) { return false; } try { final GeometryGraph graph = new GeometryGraph(0, g); valid &= checkTooFewPoints(graph); if (isErrorReturn()) { return false; } valid &= checkConsistentArea(graph); if (isErrorReturn()) { return false; } if (!this.isSelfTouchingRingFormingHoleValid) { valid &= checkNoSelfIntersectingRings(graph); if (isErrorReturn()) { return false; } } valid &= checkHolesInShell(g, graph); if (isErrorReturn()) { return false; } // SLOWcheckHolesNotNested(g); valid &= checkHolesNotNested(g, graph); if (isErrorReturn()) { return false; } valid &= checkConnectedInteriors(graph); return valid; } catch (final IllegalArgumentException e) { return false; } } public List<GeometryValidationError> getErrors() { return this.errors; } /** * 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 */ public GeometryValidationError getValidationError() { if (isValid()) { return null; } else { return this.errors.get(0); } } public boolean hasError() { if (this.errors.isEmpty()) { return false; } else { return true; } } private boolean isErrorReturn() { return this.shortCircuit && hasError(); } /** * Computes the validity of the geometry, * and returns <tt>true</tt> if it is valid. * * @return true if the geometry is valid */ public boolean isValid() { this.errors.clear(); return checkValidGeometry(this.geometry); } /** * 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: * <ul> * <li>the shell ring self-touches to Construct a new hole touching the shell * <li>a hole ring self-touches to create two holes touching at a point * </ul> * <p> * The default (following the OGC SFS standard) * is that this condition is <b>not</b> valid (<code>false</code>). * <p> * This does not affect whether Self-Touching Rings * disconnecting the polygon interior are considered valid * (these are considered to be <b>invalid</b> 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. * * @param isValid states whether geometry with this condition is valid */ public void setSelfTouchingRingFormingHoleValid(final boolean isValid) { this.isSelfTouchingRingFormingHoleValid = isValid; } @Override public String toString() { if (isErrorReturn()) { return Strings.toString("\n", this.errors); } else { return "Valid"; } } }