/*
* 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;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import org.locationtech.jts.algorithm.BoundaryNodeRule;
import org.locationtech.jts.algorithm.LineIntersector;
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.Lineal;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.MultiLineString;
import org.locationtech.jts.geom.MultiPoint;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygonal;
import org.locationtech.jts.geom.util.LinearComponentExtracter;
import org.locationtech.jts.geomgraph.Edge;
import org.locationtech.jts.geomgraph.EdgeIntersection;
import org.locationtech.jts.geomgraph.GeometryGraph;
import org.locationtech.jts.geomgraph.index.SegmentIntersector;
/**
* Tests whether a <code>Geometry</code> is simple.
* In general, the SFS specification of simplicity
* follows the rule:
* <ul>
* <li> A Geometry is simple if and only if the only self-intersections are at
* boundary points.
* </ul>
* <p>
* Simplicity is defined for each {@link Geometry} type as follows:
* <ul>
* <li><b>Polygonal</b> geometries are simple by definition, so
* <code>isSimple</code> trivially returns true.
* (Note: this means that <tt>isSimple</tt> cannot be used to test
* for (invalid) self-intersections in <tt>Polygon</tt>s.
* In order to check if a <tt>Polygonal</tt> geometry has self-intersections,
* use {@link Geometry#isValid()}).
* <li><b>Linear</b> geometries are simple iff they do <i>not</i> self-intersect at interior points
* (i.e. points other than boundary points).
* This is equivalent to saying that no two linear components satisfy the SFS {@link Geometry#touches(Geometry)}
* predicate.
* <li><b>Zero-dimensional (point)</b> geometries are simple if and only if they have no
* repeated points.
* <li><b>Empty</b> geometries are <i>always</i> simple, by definition
* </ul>
* For {@link Lineal} geometries the evaluation of simplicity
* can be customized by supplying a {@link BoundaryNodeRule}
* to define how boundary points are determined.
* The default is the SFS-standard {@link BoundaryNodeRule#MOD2_BOUNDARY_RULE}.
* Note that under the <tt>Mod-2</tt> rule, closed <tt>LineString</tt>s (rings)
* will never satisfy the <tt>touches</tt> predicate at their endpoints, since these are
* interior points, not boundary points.
* If it is required to test whether a set of <code>LineString</code>s touch
* only at their endpoints, use <code>IsSimpleOp</code> with {@link BoundaryNodeRule#ENDPOINT_BOUNDARY_RULE}.
* For example, this can be used to validate that a set of lines form a topologically valid
* linear network.
*
* @see BoundaryNodeRule
*
* @version 1.7
*/
public class IsSimpleOp
{
private Geometry inputGeom;
private boolean isClosedEndpointsInInterior = true;
private Coordinate nonSimpleLocation = null;
/**
* Creates a simplicity checker using the default SFS Mod-2 Boundary Node Rule
*
* @deprecated use IsSimpleOp(Geometry)
*/
public IsSimpleOp() {
}
/**
* Creates a simplicity checker using the default SFS Mod-2 Boundary Node Rule
*
* @param geom the geometry to test
*/
public IsSimpleOp(Geometry geom) {
this.inputGeom = geom;
}
/**
* Creates a simplicity checker using a given {@link BoundaryNodeRule}
*
* @param geom the geometry to test
* @param boundaryNodeRule the rule to use.
*/
public IsSimpleOp(Geometry geom, BoundaryNodeRule boundaryNodeRule)
{
this.inputGeom = geom;
isClosedEndpointsInInterior = ! boundaryNodeRule.isInBoundary(2);
}
/**
* Tests whether the geometry is simple.
*
* @return true if the geometry is simple
*/
public boolean isSimple()
{
nonSimpleLocation = null;
return computeSimple(inputGeom);
}
private boolean computeSimple(Geometry geom)
{
nonSimpleLocation = null;
if (geom.isEmpty()) return true;
if (geom instanceof LineString) return isSimpleLinearGeometry(geom);
if (geom instanceof MultiLineString) return isSimpleLinearGeometry(geom);
if (geom instanceof MultiPoint) return isSimpleMultiPoint((MultiPoint) geom);
if (geom instanceof Polygonal) return isSimplePolygonal(geom);
if (geom instanceof GeometryCollection) return isSimpleGeometryCollection(geom);
// all other geometry types are simple by definition
return true;
}
/**
* Gets a coordinate for the location where the geometry
* fails to be simple.
* (i.e. where it has a non-boundary self-intersection).
* {@link #isSimple} must be called before this method is called.
*
* @return a coordinate for the location of the non-boundary self-intersection
* or null if the geometry is simple
*/
public Coordinate getNonSimpleLocation()
{
return nonSimpleLocation;
}
/**
* Reports whether a {@link LineString} is simple.
*
* @param geom the lineal geometry to test
* @return true if the geometry is simple
* @deprecated use isSimple()
*/
public boolean isSimple(LineString geom)
{
return isSimpleLinearGeometry(geom);
}
/**
* Reports whether a {@link MultiLineString} geometry is simple.
*
* @param geom the lineal geometry to test
* @return true if the geometry is simple
* @deprecated use isSimple()
*/
public boolean isSimple(MultiLineString geom)
{
return isSimpleLinearGeometry(geom);
}
/**
* A MultiPoint is simple iff it has no repeated points
* @deprecated use isSimple()
*/
public boolean isSimple(MultiPoint mp)
{
return isSimpleMultiPoint(mp);
}
private boolean isSimpleMultiPoint(MultiPoint mp)
{
if (mp.isEmpty()) return true;
Set points = new TreeSet();
for (int i = 0; i < mp.getNumGeometries(); i++) {
Point pt = (Point) mp.getGeometryN(i);
Coordinate p = pt.getCoordinate();
if (points.contains(p)) {
nonSimpleLocation = p;
return false;
}
points.add(p);
}
return true;
}
/**
* Computes simplicity for polygonal geometries.
* Polygonal geometries are simple if and only if
* all of their component rings are simple.
*
* @param geom a Polygonal geometry
* @return true if the geometry is simple
*/
private boolean isSimplePolygonal(Geometry geom)
{
List rings = LinearComponentExtracter.getLines(geom);
for (Iterator i = rings.iterator(); i.hasNext(); ) {
LinearRing ring = (LinearRing) i.next();
if (! isSimpleLinearGeometry(ring))
return false;
}
return true;
}
/**
* Semantics for GeometryCollection is
* simple iff all components are simple.
*
* @param geom
* @return true if the geometry is simple
*/
private boolean isSimpleGeometryCollection(Geometry geom)
{
for (int i = 0; i < geom.getNumGeometries(); i++ ) {
Geometry comp = geom.getGeometryN(i);
if (! computeSimple(comp))
return false;
}
return true;
}
private boolean isSimpleLinearGeometry(Geometry geom)
{
if (geom.isEmpty()) return true;
GeometryGraph graph = new GeometryGraph(0, geom);
LineIntersector li = new RobustLineIntersector();
SegmentIntersector si = graph.computeSelfNodes(li, true);
// if no self-intersection, must be simple
if (! si.hasIntersection()) return true;
if (si.hasProperIntersection()) {
nonSimpleLocation = si.getProperIntersectionPoint();
return false;
}
if (hasNonEndpointIntersection(graph)) return false;
if (isClosedEndpointsInInterior) {
if (hasClosedEndpointIntersection(graph)) return false;
}
return true;
}
/**
* For all edges, check if there are any intersections which are NOT at an endpoint.
* The Geometry is not simple if there are intersections not at endpoints.
*/
private boolean hasNonEndpointIntersection(GeometryGraph graph)
{
for (Iterator i = graph.getEdgeIterator(); i.hasNext(); ) {
Edge e = (Edge) i.next();
int maxSegmentIndex = e.getMaximumSegmentIndex();
for (Iterator eiIt = e.getEdgeIntersectionList().iterator(); eiIt.hasNext(); ) {
EdgeIntersection ei = (EdgeIntersection) eiIt.next();
if (! ei.isEndPoint(maxSegmentIndex)) {
nonSimpleLocation = ei.getCoordinate();
return true;
}
}
}
return false;
}
private static class EndpointInfo {
Coordinate pt;
boolean isClosed;
int degree;
public EndpointInfo(Coordinate pt)
{
this.pt = pt;
isClosed = false;
degree = 0;
}
public Coordinate getCoordinate() { return pt; }
public void addEndpoint(boolean isClosed)
{
degree++;
this.isClosed |= isClosed;
}
}
/**
* Tests that no edge intersection is the endpoint of a closed line.
* This ensures that closed lines are not touched at their endpoint,
* which is an interior point according to the Mod-2 rule
* To check this we compute the degree of each endpoint.
* The degree of endpoints of closed lines
* must be exactly 2.
*/
private boolean hasClosedEndpointIntersection(GeometryGraph graph)
{
Map endPoints = new TreeMap();
for (Iterator i = graph.getEdgeIterator(); i.hasNext(); ) {
Edge e = (Edge) i.next();
int maxSegmentIndex = e.getMaximumSegmentIndex();
boolean isClosed = e.isClosed();
Coordinate p0 = e.getCoordinate(0);
addEndpoint(endPoints, p0, isClosed);
Coordinate p1 = e.getCoordinate(e.getNumPoints() - 1);
addEndpoint(endPoints, p1, isClosed);
}
for (Iterator i = endPoints.values().iterator(); i.hasNext(); ) {
EndpointInfo eiInfo = (EndpointInfo) i.next();
if (eiInfo.isClosed && eiInfo.degree != 2) {
nonSimpleLocation = eiInfo.getCoordinate();
return true;
}
}
return false;
}
/**
* Add an endpoint to the map, creating an entry for it if none exists
*/
private void addEndpoint(Map endPoints, Coordinate p, boolean isClosed)
{
EndpointInfo eiInfo = (EndpointInfo) endPoints.get(p);
if (eiInfo == null) {
eiInfo = new EndpointInfo(p);
endPoints.put(p, eiInfo);
}
eiInfo.addEndpoint(isClosed);
}
}