/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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;
* version 2.1 of the License.
*
* 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.
*/
package org.geotools.geometry.jts;
import java.util.ArrayList;
import java.util.List;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.CoordinateSequenceFactory;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryCollection;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.LinearRing;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* A stateful geometry clipper, can clip linestring on a specified rectangle. Trivial benchmarks
* report a speedup factor between 20 and 60 compared to JTS generic intersection algorithm. The
* class is not thread safe.
*
* @author Andrea Aime - OpenGeo
*
* @source $URL: http://svn.osgeo.org/geotools/trunk/modules/library/main/src/main/java/org/geotools/geometry/jts/GeometryClipper.java $
*/
public class GeometryClipper {
static private int RIGHT = 2;
static private int TOP = 8;
static private int BOTTOM = 4;
static private int LEFT = 1;
final double xmin;
final double ymin;
final double xmax;
final double ymax;
final Envelope bounds;
public Envelope getBounds() {
return bounds;
}
public GeometryClipper(Envelope bounds) {
this.xmin = bounds.getMinX();
this.ymin = bounds.getMinY();
this.xmax = bounds.getMaxX();
this.ymax = bounds.getMaxY();
this.bounds = bounds;
}
/**
* Clips the geometry on the specified bounds.
*
* @param g
* The geometry to be clipped
* @param ensureValid
* If false there is no guarantee the polygons returned will be valid according to
* JTS rules (but should still be good enough to be used for pure rendering)
* @return
*/
public Geometry clip(Geometry g, boolean ensureValid) {
// basic pre-flight checks
if (g == null) {
return null;
}
Envelope geomEnvelope = g.getEnvelopeInternal();
if (geomEnvelope.isNull()) {
return null;
}
if (bounds.contains(geomEnvelope)) {
return g;
} else if (!bounds.intersects(geomEnvelope)) {
return null;
}
// clip for good
if (g instanceof LineString) {
return clipLineString((LineString) g);
} else if (g instanceof Polygon) {
if(ensureValid) {
GeometryFactory gf = g.getFactory();
CoordinateSequenceFactory csf = gf.getCoordinateSequenceFactory();
return g.intersection(gf.createPolygon(buildBoundsString(gf, csf), null));
} else {
return clipPolygon((Polygon) g);
}
} else if (g instanceof GeometryCollection) {
return clipCollection((GeometryCollection) g, ensureValid);
} else {
// still don't know how to clip this
return g;
}
}
/**
* Cohen-Sutherland outcode, see http://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland
* @param x
* @param y
* @param xmin
* @param ymin
* @param xmax
* @param ymax
* @return
*/
private int computeOutCode(double x, double y, double xmin, double ymin, double xmax,
double ymax) {
int code = 0;
if (y > ymax)
code |= TOP;
else if (y < ymin)
code |= BOTTOM;
if (x > xmax)
code |= RIGHT;
else if (x < xmin)
code |= LEFT;
return code;
}
/**
* Cohen sutherland based segment clipping
* @param segment
* @return
*/
private double[] clipSegment(double[] segment) {
// dump to local variables to avoid the array access check overhead
double x0 = segment[0];
double y0 = segment[1];
double x1 = segment[2];
double y1 = segment[3];
// compute outcodes
int outcode0 = computeOutCode(x0, y0, xmin, ymin, xmax, ymax);
int outcode1 = computeOutCode(x1, y1, xmin, ymin, xmax, ymax);
int step = 0;
do {
if ((outcode0 | outcode1) == 0) {
// check if we got a degenerate segment
if (x0 == x1 && y0 == y1) {
return null;
}
// both points are inside the clip area
segment[0] = x0;
segment[1] = y0;
segment[2] = x1;
segment[3] = y1;
return segment;
} else if ((outcode0 & outcode1) > 0) {
// both points are outside of the clip area,
// and on a same side (both top, both bottom, etc)
return null;
} else {
// failed both tests, so calculate the line segment to clip
// from an outside point to an intersection with clip edge
// At least one endpoint is outside the clip rectangle; pick it.
int outcodeOut = outcode0 != 0 ? outcode0 : outcode1;
// Now find the intersection point;
// use formulas y = y0 + slope * (x - x0),
// x = x0 + (1/slope) * (y - y0)
// depending on which side we're clipping
// Note we might end up getting a point that is still outside (touches one side
// but out on the other)
double x, y;
if ((outcodeOut & TOP) > 0) {
x = x0 + (x1 - x0) * (ymax - y0) / (y1 - y0);
y = ymax;
} else if ((outcodeOut & BOTTOM) > 0) {
x = x0 + (x1 - x0) * (ymin - y0) / (y1 - y0);
y = ymin;
} else if ((outcodeOut & RIGHT) > 0) {
y = y0 + (y1 - y0) * (xmax - x0) / (x1 - x0);
x = xmax;
} else { // LEFT
y = y0 + (y1 - y0) * (xmin - x0) / (x1 - x0);
x = xmin;
}
// We sliced at least one ordinate, recompute the outcode for the end we
// modified
if (outcodeOut == outcode0) {
x0 = x;
y0 = y;
outcode0 = computeOutCode(x0, y0, xmin, ymin, xmax, ymax);
} else {
x1 = x;
y1 = y;
outcode1 = computeOutCode(x1, y1, xmin, ymin, xmax, ymax);
}
}
step++;
} while (step < 5);
// we should really never get here, the algorithm must at most clip two ends,
// at worst one ordinate at a time, so at most 4 steps
throw new RuntimeException("Algorithm did not converge");
}
/**
* Checks if the specified segment it outside the clipping bounds
* @return
*/
private boolean outside(double x0, double y0, double x1, double y1) {
int outcode0 = computeOutCode(x0, y0, xmin, ymin, xmax, ymax);
int outcode1 = computeOutCode(x1, y1, xmin, ymin, xmax, ymax);
return ((outcode0 & outcode1) > 0);
}
/**
* Checks if the point is inside the clipping bounds
* @param x
* @param y
* @return
*/
private boolean contained(final double x, final double y) {
return x > xmin && x < xmax && y > ymin && y < ymax;
}
/**
* Clips a polygon using the Liang-Barsky helper routine. Does not generate, in general,
* valid polygons (but still does generate polygons good enough for rendering)
* @param polygon
* @return
*/
private Geometry clipPolygon(Polygon polygon) {
final GeometryFactory gf = polygon.getFactory();
LinearRing exterior = (LinearRing) polygon.getExteriorRing();
LinearRing shell = polygonClip(exterior);
if(shell == null || shell.isEmpty()) {
return null;
}
List<LinearRing> holes = new ArrayList<LinearRing>();
for (int i = 0; i < polygon.getNumInteriorRing(); i++) {
LinearRing hole = (LinearRing) polygon.getInteriorRingN(i);
hole = polygonClip(hole);
if(hole != null && !hole.isEmpty()) {
holes.add(hole);
}
}
return gf.createPolygon(shell, (LinearRing[]) holes.toArray(new LinearRing[holes.size()]));
}
/**
* This routine uses the Liang-Barsky algorithm for polygon clipping as described in Foley & van
* Dam. It's more efficient Sutherland-Hodgman version, but produces redundent turning vertices
* at the corners of the clip region. This can make rendering as a series of triangles very
* awkward, but it's fine of your underlying graphics mechanism has a forgiving drawPolygon
* routine.
* This algorithm comes from http://www.longsteve.com/fixmybugs/?p=359, under a
* "DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE" (no kidding!)
*/
private LinearRing polygonClip(LinearRing ring) {
final double INFINITY = Double.MAX_VALUE;
CoordinateSequence cs = ring.getCoordinateSequence();
Ordinates out = new Ordinates();
// Coordinates of intersection between the infinite line hosting the segment and the clip area
double xIn, xOut, yIn, yOut;
// Parameter values of same, they are in [0,1] if the intersections are inside the segment, < 0 or > 1 otherwise
double tInX, tOutX, tInY, tOutY;
// tOut2: max between tOutX and tOutY, tIn2: max between tInX and tinY
double tOut1, tOut2, tIn1, tIn2;
// Direction of edge
double deltaX, deltaY;
int i;
// for each edge
for (i = 0; i < cs.size() - 1; i++) {
// extract the edge
double x0 = cs.getOrdinate(i, 0);
double x1 = cs.getOrdinate(i + 1, 0);
double y0 = cs.getOrdinate(i, 1);
double y1 = cs.getOrdinate(i + 1, 1);
// determine direction of edge
deltaX = x1 - x0;
deltaY = y1 - y0;
// use this to determine which bounding lines for the clip region the
// containing line hits first (from which side, to which other side)
if ((deltaX > 0) || (deltaX == 0 && x0 > xmax)) {
xIn = xmin;
xOut = xmax;
} else {
xIn = xmax;
xOut = xmin;
}
if ((deltaY > 0) || (deltaY == 0 && y0 > ymax)) {
yIn = ymin;
yOut = ymax;
} else {
yIn = ymax;
yOut = ymin;
}
// find the t values for the x and y exit points
if (deltaX != 0) {
tOutX = (xOut - x0) / deltaX;
} else if (x0 <= xmax && xmin <= x0) {
// vertical line crossing the clip box
tOutX = INFINITY;
} else {
// vertical line outside the clip box
tOutX = -INFINITY;
}
if (deltaY != 0) {
tOutY = (yOut - y0) / deltaY;
} else if (y0 <= ymax && ymin <= y0) {
// horizontal line crossing the clip box
tOutY = INFINITY;
} else {
// horizontal line outside the clip box
tOutY = -INFINITY;
}
// Order the two exit points
if (tOutX < tOutY) {
tOut1 = tOutX;
tOut2 = tOutY;
} else {
tOut1 = tOutY;
tOut2 = tOutX;
}
// skip tests if exit intersection points are before the
// beginning of the segment
if (tOut2 > 0) {
// now compute the params of the first intersection point
if (deltaX != 0) {
tInX = (xIn - x0) / deltaX;
} else {
tInX = -INFINITY;
}
if (deltaY != 0) {
tInY = (yIn - y0) / deltaY;
} else {
tInY = -INFINITY;
}
// sort them
if (tInX < tInY) {
tIn1 = tInX;
tIn2 = tInY;
} else {
tIn1 = tInY;
tIn2 = tInX;
}
if (tOut1 < tIn2) {
// no visible segment
if (0 < tOut1 && tOut1 <= 1.0) {
// line crosses over intermediate corner region
if (tInX < tInY) {
out.add(xOut, yIn);
} else {
out.add(xIn, yOut);
}
}
} else {
// line crosses though window
if (0 < tOut1 && tIn2 <= 1.0) {
if (0 <= tIn2) {// visible segment
if (tInX > tInY) {
out.add(xIn, y0 + (tInX * deltaY));
} else {
out.add(x0 + (tInY * deltaX), yIn);
}
}
if (1.0 >= tOut1) {
if (tOutX < tOutY) {
out.add(xOut, y0 + (tOutX * deltaY));
} else {
out.add(x0 + (tOutY * deltaX), yOut);
}
} else {
out.add(x1, y1);
}
}
}
if ((0 < tOut2 && tOut2 <= 1.0)) {
out.add(xOut, yOut);
}
}
}
if(out.size() < 3) {
return null;
}
if (out.getOrdinate(0, 0) != out.getOrdinate(out.size() - 1, 0)
|| out.getOrdinate(0, 1) != out.getOrdinate(out.size() - 1, 1)) {
out.add(out.getOrdinate(0, 0), out.getOrdinate(0, 1));
} else if(out.size() == 3) {
return null;
}
return ring.getFactory().createLinearRing(
out.toCoordinateSequence(ring.getFactory().getCoordinateSequenceFactory()));
}
/**
* Builds a linear ring representing the clipping area
* @param gf
* @param csf
* @return
*/
LinearRing buildBoundsString(final GeometryFactory gf, final CoordinateSequenceFactory csf) {
CoordinateSequence cs = csf.create(5, 2);
cs.setOrdinate(0, 0, xmin);
cs.setOrdinate(0, 1, ymin);
cs.setOrdinate(1, 0, xmin);
cs.setOrdinate(1, 1, ymax);
cs.setOrdinate(2, 0, xmax);
cs.setOrdinate(2, 1, ymax);
cs.setOrdinate(3, 0, xmax);
cs.setOrdinate(3, 1, ymin);
cs.setOrdinate(4, 0, xmin);
cs.setOrdinate(4, 1, ymin);
return gf.createLinearRing(cs);
}
/**
* Recursively clips a collection
* @param gc
* @param ensureValid
* @return
*/
private Geometry clipCollection(GeometryCollection gc, boolean ensureValid) {
if (gc.getNumGeometries() == 1) {
return clip(gc.getGeometryN(0), ensureValid);
} else {
List<Geometry> result = new ArrayList<Geometry>(gc.getNumGeometries());
for (int i = 0; i < gc.getNumGeometries(); i++) {
Geometry clipped = clip(gc.getGeometryN(i), ensureValid);
if (clipped != null) {
result.add(clipped);
}
}
// Class targetGeometry = Geometry.class;
// if(gc instanceof MultiPoint) {
// targetGeometry = Point.class;
// } else if(gc instanceof MultiLineString) {
// targetGeometry = LineString.class;
// } else if(gc instanceof MultiPolygon) {
// targetGeometry = Polygon.class;
// }
if (result.size() == 0) {
return null;
} else if (result.size() == 1) {
return result.get(0);
}
flattenCollection(result);
if (gc instanceof MultiPoint) {
return gc.getFactory().createMultiPoint(
(Point[]) result.toArray(new Point[result.size()]));
} else if (gc instanceof MultiLineString) {
return gc.getFactory().createMultiLineString(
(LineString[]) result.toArray(new LineString[result.size()]));
} else if (gc instanceof MultiPolygon) {
return gc.getFactory().createMultiPolygon(
(Polygon[]) result.toArray(new Polygon[result.size()]));
} else {
return gc.getFactory().createGeometryCollection(
(Geometry[]) result.toArray(new Geometry[result.size()]));
}
}
}
private void flattenCollection(List<Geometry> result) {
for (int i = 0; i < result.size();) {
Geometry g = result.get(i);
if(g instanceof GeometryCollection) {
GeometryCollection gc = (GeometryCollection) g;
for (int j = 0; j < gc.getNumGeometries(); j++) {
result.add(gc.getGeometryN(j));
}
result.remove(i);
} else {
i++;
}
}
}
/**
* Clips a linestring using the Cohen-Sutherlan segment clipping helper method
* @param line
* @param closed
* @param shell
* @return
*/
Geometry clipLineString(LineString line) {
// the result
List<LineString> clipped = new ArrayList<LineString>();
// grab all the factories a
final GeometryFactory gf = line.getFactory();
final CoordinateSequenceFactory csf = gf.getCoordinateSequenceFactory();
final CoordinateSequence coords = line.getCoordinateSequence();
// first step
final Ordinates ordinates = new Ordinates(coords.size());
double x0 = coords.getX(0);
double y0 = coords.getY(0);
boolean prevInside = contained(x0, y0);
if (prevInside) {
ordinates.add(x0, y0);
}
double[] segment = new double[4];
final int size = coords.size();
// loop over the other coordinates
for (int i = 1; i < size; i++) {
final double x1 = coords.getX(i);
final double y1 = coords.getY(i);
boolean inside = contained(x1, y1);
if (inside == prevInside) {
if (inside) {
// both segments were inside, not need for clipping
ordinates.add(x1, y1);
} else {
// both were outside, this might still be caused by a line
// crossing the envelope but whose endpoints lie outside
if (!outside(x0, y0, x1, y1)) {
segment[0] = x0;
segment[1] = y0;
segment[2] = x1;
segment[3] = y1;
double[] clippedSegment = clipSegment(segment);
if (clippedSegment != null) {
CoordinateSequence cs = csf.create(2, 2);
cs.setOrdinate(0, 0, clippedSegment[0]);
cs.setOrdinate(0, 1, clippedSegment[1]);
cs.setOrdinate(1, 0, clippedSegment[2]);
cs.setOrdinate(1, 1, clippedSegment[3]);
clipped.add(gf.createLineString(cs));
}
}
}
} else {
// one inside, the other outside, a clip must occurr
segment[0] = x0;
segment[1] = y0;
segment[2] = x1;
segment[3] = y1;
double[] clippedSegment = clipSegment(segment);
if (clippedSegment != null) {
if (prevInside) {
ordinates.add(clippedSegment[2], clippedSegment[3]);
} else {
ordinates.add(clippedSegment[0], clippedSegment[1]);
ordinates.add(clippedSegment[2], clippedSegment[3]);
}
// if we are going from inside to outside it's time to cut a linestring
// into the results
if (prevInside) {
// if(closed) {
// addClosingPoints(ordinates, shell);
// clipped.add(gf.createLinearRing(ordinates.toCoordinateSequence(csf)));
// } else {
clipped.add(gf.createLineString(ordinates.toCoordinateSequence(csf)));
// }
ordinates.clear();
}
} else {
prevInside = false;
}
}
prevInside = inside;
x0 = x1;
y0 = y1;
}
// don't forget the last linestring
if (ordinates.size() > 1) {
clipped.add(gf.createLineString(ordinates.toCoordinateSequence(csf)));
}
if (line.isClosed() && clipped.size() > 1) {
// the first and last strings might be adjacent, in that case fuse them
CoordinateSequence cs0 = clipped.get(0).getCoordinateSequence();
CoordinateSequence cs1 = clipped.get(clipped.size() - 1).getCoordinateSequence();
if (cs0.getOrdinate(0, 0) == cs1.getOrdinate(cs1.size() - 1, 0)
&& cs0.getOrdinate(0, 1) == cs1.getOrdinate(cs1.size() - 1, 1)) {
CoordinateSequence cs = csf.create(cs0.size() + cs1.size() - 1, 2);
for (int i = 0; i < cs1.size(); i++) {
cs.setOrdinate(i, 0, cs1.getOrdinate(i, 0));
cs.setOrdinate(i, 1, cs1.getOrdinate(i, 1));
}
for (int i = 1; i < cs0.size(); i++) {
cs.setOrdinate(i + cs1.size() - 1, 0, cs0.getOrdinate(i, 0));
cs.setOrdinate(i + cs1.size() - 1, 1, cs0.getOrdinate(i, 1));
}
clipped.remove(0);
clipped.remove(clipped.size() - 1);
clipped.add(gf.createLineString(cs));
}
}
// return the results
if (clipped.size() > 1) {
return gf.createMultiLineString((LineString[]) clipped.toArray(new LineString[clipped
.size()]));
} else if (clipped.size() == 1) {
return clipped.get(0);
} else {
return null;
}
}
}