/*
* 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.Collection;
import java.util.Iterator;
import java.util.List;
import com.revolsys.geometry.geomgraph.DirectedEdge;
import com.revolsys.geometry.geomgraph.Edge;
import com.revolsys.geometry.geomgraph.EdgeRing;
import com.revolsys.geometry.geomgraph.GeometryGraph;
import com.revolsys.geometry.geomgraph.Label;
import com.revolsys.geometry.geomgraph.PlanarGraph;
import com.revolsys.geometry.geomgraph.Position;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.Location;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.Polygon;
import com.revolsys.geometry.model.Polygonal;
import com.revolsys.geometry.model.impl.PointDoubleXY;
import com.revolsys.geometry.operation.overlay.MaximalEdgeRing;
import com.revolsys.geometry.operation.overlay.OverlayNodeFactory;
import com.revolsys.geometry.util.Assert;
/**
* This class tests that the interior of an area {@link Geometry}
* ( {@link Polygonal} )
* is connected.
* This can happen if:
* <ul>
* <li>a shell self-intersects
* <li>one or more holes form a connected chain touching a shell at two different points
* <li>one or more holes form a ring around a subset of the interior
* </ul>
* If a disconnected situation is found the location of the problem is recorded.
*
* @version 1.7
*/
public class ConnectedInteriorTester {
public static Point findDifferentPoint(final LineString line, final Point point) {
final int vertexCount = line.getVertexCount();
for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++) {
final double x = line.getX(vertexIndex);
final double y = line.getY(vertexIndex);
if (!point.equalsVertex(x, y)) {
return new PointDoubleXY(x, y);
}
}
return null;
}
// save a coordinate for any disconnected interior found
// the coordinate will be somewhere on the ring surrounding the disconnected
// interior
private Point disconnectedRingcoord;
private final GeometryFactory geometryFactory = GeometryFactory.DEFAULT_3D;
private final GeometryGraph geomGraph;
public ConnectedInteriorTester(final GeometryGraph geomGraph) {
this.geomGraph = geomGraph;
}
/**
* Form DirectedEdges in graph into Minimal EdgeRings.
* (Minimal Edgerings must be used, because only they are guaranteed to provide
* a correct isHole computation)
*/
private List buildEdgeRings(final Collection dirEdges) {
final List edgeRings = new ArrayList();
for (final Iterator it = dirEdges.iterator(); it.hasNext();) {
final DirectedEdge de = (DirectedEdge)it.next();
// if this edge has not yet been processed
if (de.isInResult() && de.getEdgeRing() == null) {
final MaximalEdgeRing er = new MaximalEdgeRing(de, this.geometryFactory);
er.linkDirectedEdgesForMinimalEdgeRings();
final List minEdgeRings = er.buildMinimalRings();
edgeRings.addAll(minEdgeRings);
}
}
return edgeRings;
}
public Point getCoordinate() {
return this.disconnectedRingcoord;
}
/**
* Check if any shell ring has an unvisited edge.
* A shell ring is a ring which is not a hole and which has the interior
* of the parent area on the RHS.
* (Note that there may be non-hole rings with the interior on the LHS,
* since the interior of holes will also be polygonized into CW rings
* by the linkAllDirectedEdges() step)
*
* @return true if there is an unvisited edge in a non-hole ring
*/
private boolean hasUnvisitedShellEdge(final List edgeRings) {
for (int i = 0; i < edgeRings.size(); i++) {
final EdgeRing er = (EdgeRing)edgeRings.get(i);
// don't check hole rings
if (er.isHole()) {
continue;
}
final List edges = er.getEdges();
DirectedEdge de = (DirectedEdge)edges.get(0);
// don't check CW rings which are holes
// (MD - this check may now be irrelevant)
if (de.getLabel().getLocation(0, Position.RIGHT) != Location.INTERIOR) {
continue;
}
/**
* the edgeRing is CW ring which surrounds the INT of the area, so check all
* edges have been visited. If any are unvisited, this is a disconnected part of the interior
*/
for (int j = 0; j < edges.size(); j++) {
de = (DirectedEdge)edges.get(j);
// Debug.print("visted? "); Debug.println(de);
if (!de.isVisited()) {
// Debug.print("not visited "); Debug.println(de);
this.disconnectedRingcoord = de.getCoordinate();
return true;
}
}
}
return false;
}
public boolean isInteriorsConnected() {
// node the edges, in case holes touch the shell
final List splitEdges = new ArrayList();
this.geomGraph.computeSplitEdges(splitEdges);
// form the edges into rings
final PlanarGraph graph = new PlanarGraph(new OverlayNodeFactory());
graph.addEdges(splitEdges);
setInteriorEdgesInResult(graph);
graph.linkResultDirectedEdges();
final List edgeRings = buildEdgeRings(graph.getEdgeEnds());
/**
* Mark all the edges for the edgeRings corresponding to the shells
* of the input polygons. Note only ONE ring gets marked for each shell.
*/
visitShellInteriors(this.geomGraph.getGeometry(), graph);
/**
* If there are any unvisited shell edges
* (i.e. a ring which is not a hole and which has the interior
* of the parent area on the RHS)
* this means that one or more holes must have split the interior of the
* polygon into at least two pieces. The polygon is thus invalid.
*/
return !hasUnvisitedShellEdge(edgeRings);
}
private void setInteriorEdgesInResult(final PlanarGraph graph) {
for (final Object element : graph.getEdgeEnds()) {
final DirectedEdge de = (DirectedEdge)element;
if (de.getLabel().getLocation(0, Position.RIGHT) == Location.INTERIOR) {
de.setInResult(true);
}
}
}
private void visitInteriorRing(final LineString ring, final PlanarGraph graph) {
final Point pt0 = ring.getVertex(0).newPoint2D();
/**
* Find first point in coord list different to initial point.
* Need special check since the first point may be repeated.
*/
final Point pt1 = findDifferentPoint(ring, pt0);
final Edge e = graph.findEdgeInSameDirection(pt0, pt1);
final DirectedEdge de = (DirectedEdge)graph.findEdgeEnd(e);
if (de != null) {
DirectedEdge intDe = null;
final Label label = de.getLabel();
if (label.getLocation(0, Position.RIGHT) == Location.INTERIOR) {
intDe = de;
} else if (de.getSym().getLabel().getLocation(0, Position.RIGHT) == Location.INTERIOR) {
intDe = de.getSym();
}
Assert.isTrue(intDe != null, "unable to find dirEdge with Interior on RHS");
visitLinkedDirectedEdges(intDe);
}
}
protected void visitLinkedDirectedEdges(final DirectedEdge start) {
final DirectedEdge startDe = start;
DirectedEdge de = start;
do {
Assert.isTrue(de != null, "found null Directed Edge");
de.setVisited(true);
de = de.getNext();
} while (de != startDe);
}
/**
* Mark all the edges for the edgeRings corresponding to the shells
* of the input polygons.
* Only ONE ring gets marked for each shell - if there are others which remain unmarked
* this indicates a disconnected interior.
*/
private void visitShellInteriors(final Geometry g, final PlanarGraph graph) {
if (g instanceof Polygon) {
final Polygon p = (Polygon)g;
visitInteriorRing(p.getShell(), graph);
} else if (g instanceof Polygonal) {
final Polygonal polygonal = (Polygonal)g;
for (final Polygon polygon : polygonal.polygons()) {
visitInteriorRing(polygon.getShell(), graph);
}
}
}
}