/*
* 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.overlay.snap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
import com.revolsys.geometry.model.BoundingBox;
import com.revolsys.geometry.model.Geometry;
import com.revolsys.geometry.model.GeometryFactory;
import com.revolsys.geometry.model.LineString;
import com.revolsys.geometry.model.LinearRing;
import com.revolsys.geometry.model.Point;
import com.revolsys.geometry.model.Polygonal;
import com.revolsys.geometry.model.coordinates.LineSegmentUtil;
import com.revolsys.geometry.model.impl.LineStringDoubleBuilder;
import com.revolsys.geometry.model.util.GeometryTransformer;
import com.revolsys.geometry.model.vertex.Vertex;
/**
* Snaps the vertices and segments of a {@link Geometry}
* to another Geometry's vertices.
* A snap distance tolerance is used to control where snapping is performed.
* Snapping one geometry to another can improve
* robustness for overlay operations by eliminating
* nearly-coincident edges
* (which cause problems during noding and intersection calculation).
* It can also be used to eliminate artifacts such as narrow slivers, spikes and gores.
* <p>
* Too much snapping can result in invalid topology
* being created, so the number and location of snapped vertices
* is decided using heuristics to determine when it
* is safe to snap.
* This can result in some potential snaps being omitted, however.
*
* @author Martin Davis
* @version 1.7
*/
public class GeometrySnapper {
private static final double SNAP_PRECISION_FACTOR = 1e-9;
/**
* Estimates the snap tolerance for a Geometry, taking into account its precision model.
*
* @param g a Geometry
* @return the estimated snap tolerance
*/
public static double computeOverlaySnapTolerance(final Geometry g) {
double snapTolerance = computeSizeBasedSnapTolerance(g);
/**
* Overlay is carried out in the precision model
* of the two inputs.
* If this precision model is of type FIXED, then the snap tolerance
* must reflect the precision grid size.
* Specifically, the snap tolerance should be at least
* the distance from a corner of a precision grid cell
* to the centre point of the cell.
*/
final GeometryFactory geometryFactory = g.getGeometryFactory();
if (!geometryFactory.isFloating()) {
final double fixedSnapTol = 1 / geometryFactory.getScaleXY() * 2 / 1.415;
if (fixedSnapTol > snapTolerance) {
snapTolerance = fixedSnapTol;
}
}
return snapTolerance;
}
public static double computeOverlaySnapTolerance(final Geometry g0, final Geometry g1) {
return Math.min(computeOverlaySnapTolerance(g0), computeOverlaySnapTolerance(g1));
}
public static double computeSizeBasedSnapTolerance(final Geometry g) {
final BoundingBox env = g.getBoundingBox();
final double minDimension = Math.min(env.getHeight(), env.getWidth());
final double snapTol = minDimension * SNAP_PRECISION_FACTOR;
return snapTol;
}
/**
* Snaps two geometries together with a given tolerance.
*
* @param g0 a geometry to snap
* @param g1 a geometry to snap
* @param snapTolerance the tolerance to use
* @return the snapped geometries
*/
public static Geometry[] snap(final Geometry g0, final Geometry g1, final double snapTolerance) {
final Geometry[] snapGeom = new Geometry[2];
final GeometrySnapper snapper0 = new GeometrySnapper(g0);
snapGeom[0] = snapper0.snapTo(g1, snapTolerance);
/**
* Snap the second geometry to the snapped first geometry
* (this strategy minimizes the number of possible different points in the result)
*/
final GeometrySnapper snapper1 = new GeometrySnapper(g1);
snapGeom[1] = snapper1.snapTo(snapGeom[0], snapTolerance);
// System.out.println(snap[0]);
// System.out.println(snap[1]);
return snapGeom;
}
/**
* Snaps a geometry to itself.
* Allows optionally cleaning the result to ensure it is
* topologically valid
* (which fixes issues such as topology collapses in polygonal inputs).
* <p>
* Snapping a geometry to itself can remove artifacts such as very narrow slivers, gores and spikes.
*
*@param geom the geometry to snap
*@param snapTolerance the snapping tolerance
*@param cleanResult whether the result should be made valid
* @return a new snapped Geometry
*/
public static Geometry snapToSelf(final Geometry geom, final double snapTolerance,
final boolean cleanResult) {
final GeometrySnapper snapper0 = new GeometrySnapper(geom);
return snapper0.snapToSelf(snapTolerance, cleanResult);
}
private final Geometry srcGeom;
/**
* Creates a new snapper acting on the given geometry
*
* @param srcGeom the geometry to snap
*/
public GeometrySnapper(final Geometry srcGeom) {
this.srcGeom = srcGeom;
}
private Collection<Point> extractTargetCoordinates(final Geometry geometry) {
// TODO: should do this more efficiently. Use CoordSeq filter to get points,
// KDTree for uniqueness & queries
final Set<Point> points = new TreeSet<>();
for (final Vertex vertex : geometry.vertices()) {
points.add(vertex.newPoint2D());
}
return new ArrayList<>(points);
}
/**
* Snaps the vertices in the component {@link LineString}s
* of the source geometry
* to the vertices of the given snap geometry.
*
* @param snapGeom a geometry to snap the source to
* @return a new snapped Geometry
*/
public Geometry snapTo(final Geometry snapGeom, final double snapTolerance) {
final Collection<Point> snapPoints = extractTargetCoordinates(snapGeom);
if (snapPoints.isEmpty()) {
return this.srcGeom;
} else {
final SnapTransformer snapTrans = new SnapTransformer(snapTolerance, snapPoints);
return snapTrans.transform(this.srcGeom);
}
}
/**
* Snaps the vertices in the component {@link LineString}s
* of the source geometry
* to the vertices of the same geometry.
* Allows optionally cleaning the result to ensure it is
* topologically valid
* (which fixes issues such as topology collapses in polygonal inputs).
*
*@param snapTolerance the snapping tolerance
*@param cleanResult whether the result should be made valid
* @return a new snapped Geometry
*/
public Geometry snapToSelf(final double snapTolerance, final boolean cleanResult) {
final Collection<Point> snapPoints = extractTargetCoordinates(this.srcGeom);
if (snapPoints.isEmpty()) {
return this.srcGeom;
} else {
final SnapTransformer snapTrans = new SnapTransformer(snapTolerance, snapPoints, true);
final Geometry snappedGeom = snapTrans.transform(this.srcGeom);
Geometry result = snappedGeom;
if (cleanResult && result instanceof Polygonal) {
// TODO: use better cleaning approach
result = snappedGeom.buffer(0);
}
return result;
}
}
}
class SnapTransformer extends GeometryTransformer {
private final boolean isSelfSnap;
private final Collection<Point> snapPoints;
private final double snapTolerance;
SnapTransformer(final double snapTolerance, final Collection<Point> snapPoints) {
this(snapTolerance, snapPoints, false);
}
SnapTransformer(final double snapTolerance, final Collection<Point> snapPoints,
final boolean isSelfSnap) {
this.snapTolerance = snapTolerance;
this.snapPoints = snapPoints;
this.isSelfSnap = isSelfSnap;
}
/**
* Finds a src segment which snaps to (is close to) the given snap point.
* <p>
* Only a single segment is selected for snapping.
* This prevents multiple segments snapping to the same snap vertex,
* which would almost certainly cause invalid geometry
* to be created.
* (The heuristic approach to snapping used here
* is really only appropriate when
* snap pts snap to a unique spot on the src geometry.)
* <p>
* Also, if the snap vertex occurs as a vertex in the src coordinate list,
* no snapping is performed.
*
* @param snapPt the point to snap to
* @param line the source segment coordinates
* @param axisCount
* @return the index of the snapped segment
* or -1 if no segment snaps to the snap point
*/
private int findSegmentIndexToSnap(final Point snapPt, final LineString line) {
double minDist = Double.MAX_VALUE;
int snapIndex = -1;
final double snapX = snapPt.getX();
final double snapY = snapPt.getY();
final int vertexCount = line.getVertexCount();
double x1 = line.getX(0);
double y1 = line.getY(0);
for (int i = 0; i < vertexCount - 1; i++) {
final double x2 = line.getX(i + 1);
final double y2 = line.getY(i + 1);
/**
* Check if the snap pt is equal to one of the segment endpoints.
*
* If the snap pt is already in the src list, don't snap at all.
*/
if (snapPt.equalsVertex(x1, y1) || snapPt.equalsVertex(x2, y2)) {
if (this.isSelfSnap) {
continue;
} else {
return -1;
}
}
final double dist = LineSegmentUtil.distanceLinePoint(x1, y1, x2, y2, snapX, snapY);
if (dist < this.snapTolerance && dist < minDist) {
minDist = dist;
snapIndex = i;
}
x1 = x2;
y1 = y2;
}
return snapIndex;
}
private Point findSnapForVertex(final double x, final double y) {
for (final Point snapPt : this.snapPoints) {
// if point is already equal to a src pt, don't snap
if (snapPt.equalsVertex(x, y)) {
return null;
} else if (snapPt.distance(x, y) < this.snapTolerance) {
return snapPt;
}
}
return null;
}
private LineString snapLine(final LineString line) {
final LineString newLine = snapVertices(line);
return snapSegments(newLine);
}
/**
* Snap segments of the source to nearby snap vertices.
* Source segments are "cracked" at a snap vertex.
* A single input segment may be snapped several times
* to different snap vertices.
* <p>
* For each distinct snap vertex, at most one source segment
* is snapped to. This prevents "cracking" multiple segments
* at the same point, which would likely cause
* topology collapse when being used on polygonal linework.
*
* @param newCoordinates the coordinates of the source linestring to be snapped
* @param snapPoints the target snap vertices
*/
private LineString snapSegments(LineString line) {
LineStringDoubleBuilder newLine = null;
for (final Point snapPoint : this.snapPoints) {
final int index = findSegmentIndexToSnap(snapPoint, line);
/**
* If a segment to snap to was found, "crack" it at the snap pt.
* The new pt is inserted immediately into the src segment list,
* so that subsequent snapping will take place on the modified segments.
* Duplicate points are not added.
*/
if (index >= 0) {
if (newLine == null) {
if (line instanceof LineStringDoubleBuilder) {
newLine = (LineStringDoubleBuilder)line;
} else {
newLine = LineStringDoubleBuilder.newLineStringDoubleBuilder(line);
line = newLine;
}
}
newLine.insertVertex(index + 1, snapPoint, false);
}
}
if (newLine == null) {
return line;
} else {
return newLine;
}
}
/**
* Snap source vertices to vertices in the target.
*
* @param newCoordinates the points to snap
* @param snapPoints the points to snap to
*/
private LineString snapVertices(final LineString line) {
LineStringDoubleBuilder newLine = null;
final int vertexCount = line.getVertexCount();
final boolean closed = line.isClosed();
// if src is a ring then don't snap final vertex
final int end = closed ? vertexCount - 1 : vertexCount;
for (int i = 0; i < end; i++) {
final double x = line.getX(i);
final double y = line.getY(i);
final Point snapVert = findSnapForVertex(x, y);
if (snapVert != null) {
if (newLine == null) {
newLine = LineStringDoubleBuilder.newLineStringDoubleBuilder(line);
if (i == 0 && closed) {
// keep final closing point in synch (rings only)
newLine.setVertex(vertexCount - 1, snapVert);
}
}
newLine.setVertex(i, snapVert);
}
}
if (newLine == null) {
return line;
} else {
return newLine;
}
}
@Override
protected LineString transformCoordinates(final LineString line, final Geometry parent) {
final LineString newLine = snapLine(line);
return newLine;
}
/**
* Transforms a LinearRing.
* The transformation of a LinearRing may result in a coordinate sequence
* which does not form a structurally valid ring (i.e. a degnerate ring of 3 or fewer points).
* In this case a LineString is returned.
* Subclasses may wish to override this method and check for this situation
* (e.g. a subclass may choose to eliminate degenerate linear rings)
*
* @param geom the ring to simplify
* @param parent the parent geometry
* @return a LinearRing if the transformation resulted in a structurally valid ring
* @return a LineString if the transformation caused the LinearRing to collapse to 3 or fewer points
*/
@Override
protected Geometry transformLinearRing(final LinearRing geometry, final Geometry parent) {
if (geometry == null) {
return this.factory.linearRing();
} else {
final LineString newLine = transformCoordinates(geometry, geometry);
if (newLine == geometry) {
return geometry;
} else {
final int vertexCount = newLine.getVertexCount();
// ensure a valid LinearRing
if (vertexCount > 0 && vertexCount < 4 && !isPreserveType()) {
return newLine.newLineString();
} else {
return newLine.newLinearRing();
}
}
}
}
/**
* Transforms a {@link LineString} geometry.
*
* @param line
* @return
*/
@Override
protected LineString transformLineString(final LineString line) {
final LineString newLine = transformCoordinates(line, line);
if (newLine == line) {
return line;
} else {
return newLine.newLineString();
}
}
@Override
protected Point transformPoint(final Point point) {
final double x = point.getX();
final double y = point.getY();
final Point snapVert = findSnapForVertex(x, y);
if (snapVert == null) {
return point;
} else {
return snapVert;
}
}
}