/*
* Copyright (c) 2016 Vivid Solutions.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Eclipse Distribution License v. 1.0 which accompanies this distribution.
* The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
* and the Eclipse Distribution License is available at
*
* http://www.eclipse.org/org/documents/edl-v10.php.
*/
package org.locationtech.jts.operation.valid;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import org.locationtech.jts.algorithm.CGAlgorithms;
import org.locationtech.jts.algorithm.LineIntersector;
import org.locationtech.jts.algorithm.MCPointInRing;
import org.locationtech.jts.algorithm.PointInRing;
import org.locationtech.jts.algorithm.RobustLineIntersector;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.MultiPolygon;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geomgraph.Edge;
import org.locationtech.jts.geomgraph.EdgeIntersection;
import org.locationtech.jts.geomgraph.EdgeIntersectionList;
import org.locationtech.jts.geomgraph.GeometryGraph;
import org.locationtech.jts.util.Assert;
/**
* 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
{
/**
* Tests whether a {@link Geometry} is valid.
* @param geom the Geometry to test
* @return true if the geometry is valid
*/
public static boolean isValid(Geometry geom)
{
IsValidOp isValidOp = new IsValidOp(geom);
return isValidOp.isValid();
}
/**
* Checks whether a coordinate is valid for processing.
* Coordinates are valid iff 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
*/
public static boolean isValid(Coordinate coord)
{
if (Double.isNaN(coord.x)) return false;
if (Double.isInfinite(coord.x)) return false;
if (Double.isNaN(coord.y)) return false;
if (Double.isInfinite(coord.y)) return false;
return true;
}
/**
* 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 Coordinate findPtNotNode(
Coordinate[] testCoords,
LinearRing searchRing,
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?)
for (int i = 0 ; i < testCoords.length; i++) {
Coordinate pt = testCoords[i];
if (! eiList.isIntersection(pt))
return pt;
}
return null;
}
private Geometry parentGeometry; // 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 TopologyValidationError validErr;
public IsValidOp(Geometry parentGeometry)
{
this.parentGeometry = parentGeometry;
}
/**
* 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 create a 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(boolean isValid)
{
isSelfTouchingRingFormingHoleValid = isValid;
}
/**
* 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()
{
checkValid(parentGeometry);
return validErr == null;
}
/**
* 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 TopologyValidationError getValidationError()
{
checkValid(parentGeometry);
return validErr;
}
private void checkValid(Geometry g)
{
validErr = null;
// empty geometries are always valid!
if (g.isEmpty()) return;
if (g instanceof Point) checkValid((Point) g);
else if (g instanceof MultiPoint) checkValid((MultiPoint) g);
// LineString also handles LinearRings
else if (g instanceof LinearRing) checkValid( (LinearRing) g);
else if (g instanceof LineString) checkValid( (LineString) g);
else if (g instanceof Polygon) checkValid( (Polygon) g);
else if (g instanceof MultiPolygon) checkValid( (MultiPolygon) g);
else if (g instanceof GeometryCollection) checkValid( (GeometryCollection) g);
else throw new UnsupportedOperationException(g.getClass().getName());
}
/**
* Checks validity of a Point.
*/
private void checkValid(Point g)
{
checkInvalidCoordinates(g.getCoordinates());
}
/**
* Checks validity of a MultiPoint.
*/
private void checkValid(MultiPoint g)
{
checkInvalidCoordinates(g.getCoordinates());
}
/**
* Checks validity of a LineString. Almost anything goes for linestrings!
*/
private void checkValid(LineString g)
{
checkInvalidCoordinates(g.getCoordinates());
if (validErr != null) return;
GeometryGraph graph = new GeometryGraph(0, g);
checkTooFewPoints(graph);
}
/**
* Checks validity of a LinearRing.
*/
private void checkValid(LinearRing g)
{
checkInvalidCoordinates(g.getCoordinates());
if (validErr != null) return;
checkClosedRing(g);
if (validErr != null) return;
GeometryGraph graph = new GeometryGraph(0, g);
checkTooFewPoints(graph);
if (validErr != null) return;
LineIntersector li = new RobustLineIntersector();
graph.computeSelfNodes(li, true, true);
checkNoSelfIntersectingRings(graph);
}
/**
* Checks the validity of a polygon.
* Sets the validErr flag.
*/
private void checkValid(Polygon g)
{
checkInvalidCoordinates(g);
if (validErr != null) return;
checkClosedRings(g);
if (validErr != null) return;
GeometryGraph graph = new GeometryGraph(0, g);
checkTooFewPoints(graph);
if (validErr != null) return;
checkConsistentArea(graph);
if (validErr != null) return;
if (! isSelfTouchingRingFormingHoleValid) {
checkNoSelfIntersectingRings(graph);
if (validErr != null) return;
}
checkHolesInShell(g, graph);
if (validErr != null) return;
//SLOWcheckHolesNotNested(g);
checkHolesNotNested(g, graph);
if (validErr != null) return;
checkConnectedInteriors(graph);
}
private void checkValid(MultiPolygon g)
{
for (int i = 0; i < g.getNumGeometries(); i++) {
Polygon p = (Polygon) g.getGeometryN(i);
checkInvalidCoordinates(p);
if (validErr != null) return;
checkClosedRings(p);
if (validErr != null) return;
}
GeometryGraph graph = new GeometryGraph(0, g);
checkTooFewPoints(graph);
if (validErr != null) return;
checkConsistentArea(graph);
if (validErr != null) return;
if (! isSelfTouchingRingFormingHoleValid) {
checkNoSelfIntersectingRings(graph);
if (validErr != null) return;
}
for (int i = 0; i < g.getNumGeometries(); i++) {
Polygon p = (Polygon) g.getGeometryN(i);
checkHolesInShell(p, graph);
if (validErr != null) return;
}
for (int i = 0; i < g.getNumGeometries(); i++) {
Polygon p = (Polygon) g.getGeometryN(i);
checkHolesNotNested(p, graph);
if (validErr != null) return;
}
checkShellsNotNested(g, graph);
if (validErr != null) return;
checkConnectedInteriors(graph);
}
private void checkValid(GeometryCollection gc)
{
for (int i = 0; i < gc.getNumGeometries(); i++) {
Geometry g = gc.getGeometryN(i);
checkValid(g);
if (validErr != null) return;
}
}
private void checkInvalidCoordinates(Coordinate[] coords)
{
for (int i = 0; i < coords.length; i++) {
if (! isValid(coords[i])) {
validErr = new TopologyValidationError(
TopologyValidationError.INVALID_COORDINATE,
coords[i]);
return;
}
}
}
private void checkInvalidCoordinates(Polygon poly)
{
checkInvalidCoordinates(poly.getExteriorRing().getCoordinates());
if (validErr != null) return;
for (int i = 0; i < poly.getNumInteriorRing(); i++) {
checkInvalidCoordinates(poly.getInteriorRingN(i).getCoordinates());
if (validErr != null) return;
}
}
private void checkClosedRings(Polygon poly)
{
checkClosedRing((LinearRing) poly.getExteriorRing());
if (validErr != null) return;
for (int i = 0; i < poly.getNumInteriorRing(); i++) {
checkClosedRing((LinearRing) poly.getInteriorRingN(i));
if (validErr != null) return;
}
}
private void checkClosedRing(LinearRing ring)
{
if (! ring.isClosed() ) {
Coordinate pt = null;
if (ring.getNumPoints() >= 1)
pt = ring.getCoordinateN(0);
validErr = new TopologyValidationError(
TopologyValidationError.RING_NOT_CLOSED,
pt);
}
}
private void checkTooFewPoints(GeometryGraph graph)
{
if (graph.hasTooFewPoints()) {
validErr = new TopologyValidationError(
TopologyValidationError.TOO_FEW_POINTS,
graph.getInvalidPoint());
return;
}
}
/**
* Checks that the arrangement of edges in a polygonal geometry graph
* forms a consistent area.
*
* @param graph
*
* @see ConsistentAreaTester
*/
private void checkConsistentArea(GeometryGraph graph)
{
ConsistentAreaTester cat = new ConsistentAreaTester(graph);
boolean isValidArea = cat.isNodeConsistentArea();
if (! isValidArea) {
validErr = new TopologyValidationError(
TopologyValidationError.SELF_INTERSECTION,
cat.getInvalidPoint());
return;
}
if (cat.hasDuplicateRings()) {
validErr = new TopologyValidationError(
TopologyValidationError.DUPLICATE_RINGS,
cat.getInvalidPoint());
}
}
/**
* 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 void checkNoSelfIntersectingRings(GeometryGraph graph)
{
for (Iterator i = graph.getEdgeIterator(); i.hasNext(); ) {
Edge e = (Edge) i.next();
checkNoSelfIntersectingRing(e.getEdgeIntersectionList());
if (validErr != null)
return;
}
}
/**
* 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 void checkNoSelfIntersectingRing(EdgeIntersectionList eiList)
{
Set nodeSet = new TreeSet();
boolean isFirst = true;
for (Iterator i = eiList.iterator(); i.hasNext(); ) {
EdgeIntersection ei = (EdgeIntersection) i.next();
if (isFirst) {
isFirst = false;
continue;
}
if (nodeSet.contains(ei.coord)) {
validErr = new TopologyValidationError(
TopologyValidationError.RING_SELF_INTERSECTION,
ei.coord);
return;
}
else {
nodeSet.add(ei.coord);
}
}
}
/**
* 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).
* 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 p the polygon to be tested for hole inclusion
* @param graph a GeometryGraph incorporating the polygon
*/
private void checkHolesInShell(Polygon p, GeometryGraph graph)
{
LinearRing shell = (LinearRing) p.getExteriorRing();
//PointInRing pir = new SimplePointInRing(shell);
//PointInRing pir = new SIRtreePointInRing(shell);
PointInRing pir = new MCPointInRing(shell);
for (int i = 0; i < p.getNumInteriorRing(); i++) {
LinearRing hole = (LinearRing) p.getInteriorRingN(i);
Coordinate holePt = findPtNotNode(hole.getCoordinates(), 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) return;
boolean outside = ! pir.isInside(holePt);
if ( outside ) {
validErr = new TopologyValidationError(
TopologyValidationError.HOLE_OUTSIDE_SHELL,
holePt);
return;
}
}
}
/**
* 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 void checkHolesNotNested(Polygon p, GeometryGraph graph)
{
IndexedNestedRingTester nestedTester = new IndexedNestedRingTester(graph);
//SimpleNestedRingTester nestedTester = new SimpleNestedRingTester(arg[0]);
//SweeplineNestedRingTester nestedTester = new SweeplineNestedRingTester(arg[0]);
for (int i = 0; i < p.getNumInteriorRing(); i++) {
LinearRing innerHole = (LinearRing) p.getInteriorRingN(i);
nestedTester.add(innerHole);
}
boolean isNonNested = nestedTester.isNonNested();
if ( ! isNonNested ) {
validErr = new TopologyValidationError(
TopologyValidationError.NESTED_HOLES,
nestedTester.getNestedPoint());
}
}
/**
* 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 void checkShellsNotNested(MultiPolygon mp, GeometryGraph graph)
{
for (int i = 0; i < mp.getNumGeometries(); i++) {
Polygon p = (Polygon) mp.getGeometryN(i);
LinearRing shell = (LinearRing) p.getExteriorRing();
for (int j = 0; j < mp.getNumGeometries(); j++) {
if (i == j) continue;
Polygon p2 = (Polygon) mp.getGeometryN(j);
checkShellNotNested(shell, p2, graph);
if (validErr != null) return;
}
}
}
/**
* 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 void checkShellNotNested(LinearRing shell, Polygon p, GeometryGraph graph)
{
Coordinate[] shellPts = shell.getCoordinates();
// test if shell is inside polygon shell
LinearRing polyShell = (LinearRing) p.getExteriorRing();
Coordinate[] polyPts = polyShell.getCoordinates();
Coordinate shellPt = findPtNotNode(shellPts, polyShell, graph);
// if no point could be found, we can assume that the shell is outside the polygon
if (shellPt == null)
return;
boolean insidePolyShell = CGAlgorithms.isPointInRing(shellPt, polyPts);
if (! insidePolyShell) return;
// if no holes, this is an error!
if (p.getNumInteriorRing() <= 0) {
validErr = new TopologyValidationError(
TopologyValidationError.NESTED_SHELLS,
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.
* Otherwise, the shell is not properly contained in a hole, which is an error.
*/
Coordinate badNestedPt = null;
for (int i = 0; i < p.getNumInteriorRing(); i++) {
LinearRing hole = (LinearRing) p.getInteriorRingN(i);
badNestedPt = checkShellInsideHole(shell, hole, graph);
if (badNestedPt == null)
return;
}
validErr = new TopologyValidationError(
TopologyValidationError.NESTED_SHELLS,
badNestedPt);
}
/**
* 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
*
*/
private Coordinate checkShellInsideHole(LinearRing shell, LinearRing hole, GeometryGraph graph)
{
Coordinate[] shellPts = shell.getCoordinates();
Coordinate[] holePts = hole.getCoordinates();
// TODO: improve performance of this - by sorting pointlists for instance?
Coordinate shellPt = findPtNotNode(shellPts, hole, graph);
// if point is on shell but not hole, check that the shell is inside the hole
if (shellPt != null) {
boolean insideHole = CGAlgorithms.isPointInRing(shellPt, holePts);
if (! insideHole) {
return shellPt;
}
}
Coordinate holePt = findPtNotNode(holePts, shell, graph);
// if point is on hole but not shell, check that the hole is outside the shell
if (holePt != null) {
boolean insideShell = CGAlgorithms.isPointInRing(holePt, shellPts);
if (insideShell) {
return holePt;
}
return null;
}
Assert.shouldNeverReachHere("points in shell and hole appear to be equal");
return null;
}
private void checkConnectedInteriors(GeometryGraph graph)
{
ConnectedInteriorTester cit = new ConnectedInteriorTester(graph);
if (! cit.isInteriorsConnected())
validErr = new TopologyValidationError(
TopologyValidationError.DISCONNECTED_INTERIOR,
cit.getCoordinate());
}
}