/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2006 Vivid Solutions
* (C) 2001-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.iso.util.algorithm2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;
import java.util.TreeSet;
import org.geotools.geometry.iso.aggregate.MultiCurveImpl;
import org.geotools.geometry.iso.aggregate.MultiPointImpl;
import org.geotools.geometry.iso.aggregate.MultiPrimitiveImpl;
import org.geotools.geometry.iso.aggregate.MultiSurfaceImpl;
import org.geotools.geometry.iso.complex.CompositeCurveImpl;
import org.geotools.geometry.iso.complex.CompositePointImpl;
import org.geotools.geometry.iso.complex.CompositeSurfaceImpl;
import org.geotools.geometry.iso.coordinate.DirectPositionImpl;
import org.geotools.geometry.iso.coordinate.LineStringImpl;
import org.geotools.geometry.iso.coordinate.PointArrayImpl;
import org.geotools.geometry.iso.primitive.CurveBoundaryImpl;
import org.geotools.geometry.iso.primitive.CurveImpl;
import org.geotools.geometry.iso.primitive.PointImpl;
import org.geotools.geometry.iso.primitive.PrimitiveImpl;
import org.geotools.geometry.iso.primitive.RingImpl;
import org.geotools.geometry.iso.primitive.RingImplUnsafe;
import org.geotools.geometry.iso.primitive.SurfaceBoundaryImpl;
import org.geotools.geometry.iso.primitive.SurfaceImpl;
import org.geotools.geometry.iso.root.GeometryImpl;
import org.geotools.geometry.iso.topograph2D.Coordinate;
import org.geotools.geometry.iso.topograph2D.CoordinateList;
import org.geotools.geometry.iso.topograph2D.util.CoordinateArrays;
import org.geotools.geometry.iso.topograph2D.util.UniqueCoordinateArrayFilter;
import org.geotools.geometry.iso.util.Assert;
import org.opengis.geometry.coordinate.Position;
import org.opengis.geometry.primitive.CurveSegment;
import org.opengis.geometry.primitive.OrientableCurve;
import org.opengis.geometry.primitive.OrientableSurface;
import org.opengis.geometry.primitive.Primitive;
import org.opengis.geometry.primitive.Ring;
import org.opengis.geometry.Geometry;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
/**
* Computes the convex hull of a {@link Geometry}. The convex hull is the
* smallest convex Geometry that contains all the points in the input Geometry.
* <p>
* Uses the Graham Scan algorithm. Asymptotic running time: O(n*log(n))
*
* @source $URL$
*/
public class ConvexHull {
//private FeatGeomFactoryImpl geomFactory;
private CoordinateReferenceSystem crs;
private Coordinate[] inputPts;
/**
* Create a new convex hull construction for the input {@link Geometry}.
*/
public ConvexHull(GeometryImpl geometry) {
this(extractCoordinates(geometry), geometry.getCoordinateReferenceSystem());
}
/**
* Create a new convex hull construction for the input {@link Coordinate}
* array.
*/
public ConvexHull(Coordinate[] pts, CoordinateReferenceSystem crs) {
inputPts = pts;
this.crs = crs;
}
/**
* Get coordinates from a geometry and eliminate positions with equal coordinates
*
* @param geom
* @return
*/
private static Coordinate[] extractCoordinates(GeometryImpl geom) {
// Get relevant coordinates from the geometry instance
Collection positions = null;
if (geom instanceof PointImpl) {
// Add point
positions = new ArrayList<DirectPositionImpl>();
positions.add(((PointImpl)geom).getPosition());
} else if (geom instanceof CurveImpl) {
// Add control points
positions = new ArrayList<DirectPositionImpl>();
positions = ((CurveImpl)geom).asDirectPositions();
} else if (geom instanceof RingImpl) {
// Add control points
positions = new ArrayList<DirectPositionImpl>();
positions = ((RingImplUnsafe)geom).asDirectPositions();
} else if (geom instanceof SurfaceImpl) {
// Add control points of exterior ring of boundary
positions = new ArrayList<DirectPositionImpl>();
positions = ((RingImplUnsafe)((SurfaceImpl)geom).getBoundary().getExterior()).asDirectPositions();
} else if (geom instanceof MultiPointImpl) {
// Add all points of the set
positions = new HashSet<PointImpl>();
positions = ((MultiPointImpl)geom).getElements();
} else if (geom instanceof MultiCurveImpl) {
// Add all curves of the set
positions = new HashSet<DirectPositionImpl>();
Iterator<OrientableCurve> curveIter = ((MultiCurveImpl)geom).getElements().iterator();
while(curveIter.hasNext()) {
positions.addAll(((CurveImpl)curveIter.next()).asDirectPositions());
}
} else if (geom instanceof MultiSurfaceImpl) {
// Add all exterior rings of the surfaceboundaries of the surfaces in the set
positions = new HashSet<DirectPositionImpl>();
Iterator<OrientableSurface> surfaceIter = ((MultiSurfaceImpl)geom).getElements().iterator();
while(surfaceIter.hasNext()) {
positions.addAll(((RingImplUnsafe)((SurfaceImpl)surfaceIter.next()).getBoundary().getExterior()).asDirectPositions());
}
} else if (geom instanceof MultiPrimitiveImpl) {
positions = new HashSet<PointImpl>();
Iterator<? extends Primitive> iterator = ((MultiPrimitiveImpl)geom).getElements().iterator();
while (iterator.hasNext()) {
PrimitiveImpl prim = ((PrimitiveImpl)iterator.next());
// if this is a point, it has no boundary so just add the point
if (prim instanceof PointImpl) {
positions.add(prim);
}
else {
// this is not a point, so get its convexhull and add those
// points to this hull
Geometry hull = prim.getConvexHull();
if (hull instanceof CurveImpl) {
CurveImpl curve = (CurveImpl) prim.getConvexHull();
positions.addAll( ((CurveImpl)curve).asDirectPositions() );
}
else if (hull instanceof SurfaceImpl) {
SurfaceImpl surface = (SurfaceImpl) prim.getConvexHull();
positions.addAll( ((RingImplUnsafe)((SurfaceImpl)surface).getBoundary().getExterior()).asDirectPositions() );
}
/*
// this is not a point, so get its boundary and add its
// positions to the list.
PrimitiveBoundaryImpl pb = (PrimitiveBoundaryImpl) (prim.getBoundary());
if (pb instanceof CurveBoundaryImpl) {
CurveBoundaryImpl boundary = (CurveBoundaryImpl) pb;
positions.add(boundary.getStartPoint());
positions.add(boundary.getEndPoint());
}
else if (pb instanceof SolidBoundaryImpl) {
SolidBoundaryImpl boundary = (SolidBoundaryImpl) pb;
positions.addAll(boundary.getExterior().getElements());
}
else if (pb instanceof SurfaceBoundaryImpl) {
SurfaceBoundaryImpl boundary = (SurfaceBoundaryImpl) pb;
positions.addAll(boundary.getExterior().asDirectPositions());
}
*/
}
}
//Assert.isTrue(false, "not implemented yet");
} else if (geom instanceof CompositePointImpl) {
positions = new ArrayList<DirectPositionImpl>();
positions.add(((CompositePointImpl)geom).getElements().iterator().next());
} else if (geom instanceof CompositeCurveImpl) {
Assert.isTrue(false, "not implemented yet");
} else if (geom instanceof CompositeSurfaceImpl) {
Assert.isTrue(false, "not implemented yet");
} else if (geom instanceof CurveBoundaryImpl) {
positions = new ArrayList<DirectPositionImpl>();
positions.add(((CurveBoundaryImpl)geom).getStartPoint());
positions.add(((CurveBoundaryImpl)geom).getEndPoint());
} else if (geom instanceof SurfaceBoundaryImpl) {
// Add control points of exterior ring
positions = new ArrayList<DirectPositionImpl>();
positions = ((RingImplUnsafe)((SurfaceBoundaryImpl)geom).getExterior()).asDirectPositions();
}
UniqueCoordinateArrayFilter filter = new UniqueCoordinateArrayFilter();
// Filter all coordinates to eleminate redudant coordinates
Iterator posIter = positions.iterator();
while (posIter.hasNext()) {
Object pos = posIter.next();
if (pos instanceof DirectPositionImpl) {
filter.filter(new Coordinate(((DirectPositionImpl)pos).getCoordinates()));
} else if (pos instanceof PointImpl) {
filter.filter(new Coordinate(((PointImpl)pos).getPosition().getCoordinates()));
} else
Assert.isTrue(false, "Invalid coordinate type");
}
return filter.getCoordinates();
}
/**
* Returns a {@link Geometry} that represents the convex hull of the input
* geometry. The returned geometry contains the minimal number of points
* needed to represent the convex hull. In particular, no more than two
* consecutive points will be collinear.
*
* @return if the convex hull contains 3 or more points, a {@link Polygon};
* 2 points, a {@link LineString}; 1 point, a {@link Point}; 0
* points, an empty {@link GeometryCollection}.
*/
public Geometry getConvexHull() {
if (inputPts.length == 0) {
// return geomFactory.createGeometryCollection(null);
// if no points, return null
return null;
}
if (inputPts.length == 1) {
// 1 point: return Point
return new PointImpl( new DirectPositionImpl(crs, inputPts[0].getCoordinates()) ); //this.geomFactory.getPrimitiveFactory().createPoint(inputPts[0].getCoordinates());
}
if (inputPts.length == 2) {
List<Position> positions = CoordinateArrays.toPositionList(this.crs, this.inputPts);
LineStringImpl lineString = new LineStringImpl(new PointArrayImpl(positions), 0.0);
List<CurveSegment> segments = new ArrayList<CurveSegment>();
segments.add(lineString);
return new CurveImpl(this.crs, segments);
//return this.geomFactory.getPrimitiveFactory().createCurveByPositions((List<Position>) positions);
}
Coordinate[] reducedPts = inputPts;
// use heuristic to reduce points, if large
if (inputPts.length > 50) {
reducedPts = reduce(inputPts);
}
// sort points for Graham scan.
Coordinate[] sortedPts = preSort(reducedPts);
// Use Graham scan to find convex hull.
Stack cHS = grahamScan(sortedPts);
// Convert stack to an array.
Coordinate[] cH = toCoordinateArray(cHS);
// Convert array to appropriate output geometry.
return lineOrPolygon(cH);
}
/**
* An alternative to Stack.toArray, which is not present in earlier versions
* of Java.
*/
protected Coordinate[] toCoordinateArray(Stack stack) {
Coordinate[] coordinates = new Coordinate[stack.size()];
for (int i = 0; i < stack.size(); i++) {
Coordinate coordinate = (Coordinate) stack.get(i);
coordinates[i] = coordinate;
}
return coordinates;
}
/**
* Uses a heuristic to reduce the number of points scanned to compute the
* hull. The heuristic is to find a polygon guaranteed to be in (or on) the
* hull, and eliminate all points inside it. A quadrilateral defined by the
* extremal points in the four orthogonal directions can be used, but even
* more inclusive is to use an octilateral defined by the points in the 8
* cardinal directions.
* <p>
* Note that even if the method used to determine the polygon vertices is
* not 100% robust, this does not affect the robustness of the convex hull.
*
* @param pts
* @return
*/
private Coordinate[] reduce(Coordinate[] inputPts) {
// Coordinate[] polyPts = computeQuad(inputPts);
Coordinate[] polyPts = computeOctRing(inputPts);
// Coordinate[] polyPts = null;
// unable to compute interior polygon for some reason
if (polyPts == null)
return inputPts;
// LinearRing ring = geomFactory.createLinearRing(polyPts);
// System.out.println(ring);
// add points defining polygon
TreeSet reducedSet = new TreeSet();
for (int i = 0; i < polyPts.length; i++) {
reducedSet.add(polyPts[i]);
}
/**
* Add all unique points not in the interior poly.
* CGAlgorithms.isPointInRing is not defined for points actually on the
* ring, but this doesn't matter since the points of the interior
* polygon are forced to be in the reduced set.
*/
for (int i = 0; i < inputPts.length; i++) {
if (!CGAlgorithms.isPointInRing(inputPts[i], polyPts)) {
reducedSet.add(inputPts[i]);
}
}
Coordinate[] reducedPts = CoordinateArrays
.toCoordinateArray(reducedSet);
return reducedPts;
}
private Coordinate[] preSort(Coordinate[] pts) {
Coordinate t;
// find the lowest point in the set. If two or more points have
// the same minimum y coordinate choose the one with the minimu x.
// This focal point is put in array location pts[0].
for (int i = 1; i < pts.length; i++) {
if ((pts[i].y < pts[0].y)
|| ((pts[i].y == pts[0].y) && (pts[i].x < pts[0].x))) {
t = pts[0];
pts[0] = pts[i];
pts[i] = t;
}
}
// sort the points radially around the focal point.
Arrays.sort(pts, 1, pts.length, new RadialComparator(pts[0]));
// radialSort(pts);
return pts;
}
private Stack grahamScan(Coordinate[] c) {
Coordinate p;
Stack ps = new Stack();
p = (Coordinate) ps.push(c[0]);
p = (Coordinate) ps.push(c[1]);
p = (Coordinate) ps.push(c[2]);
for (int i = 3; i < c.length; i++) {
p = (Coordinate) ps.pop();
while (CGAlgorithms.computeOrientation((Coordinate) ps.peek(), p,
c[i]) > 0) {
p = (Coordinate) ps.pop();
}
p = (Coordinate) ps.push(p);
p = (Coordinate) ps.push(c[i]);
}
p = (Coordinate) ps.push(c[0]);
return ps;
}
/**
* @return whether the three coordinates are collinear and c2 lies between
* c1 and c3 inclusive
*/
private boolean isBetween(Coordinate c1, Coordinate c2, Coordinate c3) {
if (CGAlgorithms.computeOrientation(c1, c2, c3) != 0) {
return false;
}
if (c1.x != c3.x) {
if (c1.x <= c2.x && c2.x <= c3.x) {
return true;
}
if (c3.x <= c2.x && c2.x <= c1.x) {
return true;
}
}
if (c1.y != c3.y) {
if (c1.y <= c2.y && c2.y <= c3.y) {
return true;
}
if (c3.y <= c2.y && c2.y <= c1.y) {
return true;
}
}
return false;
}
private Coordinate[] computeOctRing(Coordinate[] inputPts) {
Coordinate[] octPts = computeOctPts(inputPts);
CoordinateList coordList = new CoordinateList();
coordList.add(octPts, false);
// points must all lie in a line
if (coordList.size() < 3) {
return null;
}
coordList.closeRing();
return coordList.toCoordinateArray();
}
private Coordinate[] computeOctPts(Coordinate[] inputPts) {
Coordinate[] pts = new Coordinate[8];
for (int j = 0; j < pts.length; j++) {
pts[j] = inputPts[0];
}
for (int i = 1; i < inputPts.length; i++) {
if (inputPts[i].x < pts[0].x) {
pts[0] = inputPts[i];
}
if (inputPts[i].x - inputPts[i].y < pts[1].x - pts[1].y) {
pts[1] = inputPts[i];
}
if (inputPts[i].y > pts[2].y) {
pts[2] = inputPts[i];
}
if (inputPts[i].x + inputPts[i].y > pts[3].x + pts[3].y) {
pts[3] = inputPts[i];
}
if (inputPts[i].x > pts[4].x) {
pts[4] = inputPts[i];
}
if (inputPts[i].x - inputPts[i].y > pts[5].x - pts[5].y) {
pts[5] = inputPts[i];
}
if (inputPts[i].y < pts[6].y) {
pts[6] = inputPts[i];
}
if (inputPts[i].x + inputPts[i].y < pts[7].x + pts[7].y) {
pts[7] = inputPts[i];
}
}
return pts;
}
/**
* @param vertices
* the vertices of a linear ring, which may or may not be
* flattened (i.e. vertices collinear)
* @return a 2-vertex <code>LineString</code> if the vertices are
* collinear; otherwise, a <code>Polygon</code> with unnecessary
* (collinear) vertices removed
*/
private Geometry lineOrPolygon(Coordinate[] coordinates) {
coordinates = cleanRing(coordinates);
List<Position> positions = CoordinateArrays.toPositionList(this.crs, coordinates);
if (coordinates.length == 3) {
positions.remove(2);
LineStringImpl lineString = new LineStringImpl(new PointArrayImpl(
positions), 0.0);
List<CurveSegment> segments = new ArrayList<CurveSegment>();
segments.add(lineString);
return new CurveImpl(this.crs, segments);
//return this.geomFactory.getPrimitiveFactory().createCurveByDirectPositions((List<Position>) positions);
}
LineStringImpl lineString = new LineStringImpl(new PointArrayImpl(
positions), 0.0);
List<CurveSegment> segments = new ArrayList<CurveSegment>();
segments.add(lineString);
OrientableCurve curve = new CurveImpl(crs, segments);
List<OrientableCurve> orientableCurves = new ArrayList<OrientableCurve>();
orientableCurves.add(curve);
Ring exterior = new RingImpl(orientableCurves);
List<Ring> interiorList = new ArrayList<Ring>();
SurfaceBoundaryImpl sb =
new SurfaceBoundaryImpl(crs, exterior, interiorList);
return new SurfaceImpl(sb);
//return this.geomFactory.getPrimitiveFactory().createSurfaceByDirectPositions((List<DirectPosition>) positions);
}
/**
* @param vertices
* the vertices of a linear ring, which may or may not be
* flattened (i.e. vertices collinear)
* @return the coordinates with unnecessary (collinear) vertices removed
*/
private Coordinate[] cleanRing(Coordinate[] original) {
Assert.equals(original[0], original[original.length - 1]);
ArrayList cleanedRing = new ArrayList();
Coordinate previousDistinctCoordinate = null;
for (int i = 0; i <= original.length - 2; i++) {
Coordinate currentCoordinate = original[i];
Coordinate nextCoordinate = original[i + 1];
if (currentCoordinate.equals(nextCoordinate)) {
continue;
}
if (previousDistinctCoordinate != null
&& isBetween(previousDistinctCoordinate, currentCoordinate,
nextCoordinate)) {
continue;
}
cleanedRing.add(currentCoordinate);
previousDistinctCoordinate = currentCoordinate;
}
cleanedRing.add(original[original.length - 1]);
Coordinate[] cleanedRingCoordinates = new Coordinate[cleanedRing.size()];
return (Coordinate[]) cleanedRing.toArray(cleanedRingCoordinates);
}
/**
* Compares {@link Coordinate}s for their angle and distance relative to an
* origin.
*
* @author Martin Davis
* @version 1.7.2
*/
private static class RadialComparator implements Comparator {
private Coordinate origin;
public RadialComparator(Coordinate origin) {
this.origin = origin;
}
public int compare(Object o1, Object o2) {
Coordinate p1 = (Coordinate) o1;
Coordinate p2 = (Coordinate) o2;
return polarCompare(origin, p1, p2);
}
/**
* Given two points p and q compare them with respect to their radial
* ordering about point o. First checks radial ordering. If points are
* collinear, the comparison is based on their distance to the origin.
* <p>
* p < q iff
* <ul>
* <li>ang(o-p) < ang(o-q) (e.g. o-p-q is CCW)
* <li>or ang(o-p) == ang(o-q) && dist(o,p) < dist(o,q)
* </ul>
*
* @param o
* the origin
* @param p
* a point
* @param q
* another point
* @return -1, 0 or 1 depending on whether p is less than, equal to or
* greater than q
*/
private static int polarCompare(Coordinate o, Coordinate p, Coordinate q) {
double dxp = p.x - o.x;
double dyp = p.y - o.y;
double dxq = q.x - o.x;
double dyq = q.y - o.y;
/*
* // MD - non-robust int result = 0; double alph = Math.atan2(dxp,
* dyp); double beta = Math.atan2(dxq, dyq); if (alph < beta) {
* result = -1; } if (alph > beta) { result = 1; } if (result != 0)
* return result; //
*/
int orient = CGAlgorithms.computeOrientation(o, p, q);
if (orient == CGAlgorithms.COUNTERCLOCKWISE)
return 1;
if (orient == CGAlgorithms.CLOCKWISE)
return -1;
// points are collinear - check distance
double op = dxp * dxp + dyp * dyp;
double oq = dxq * dxq + dyq * dyq;
if (op < oq) {
return -1;
}
if (op > oq) {
return 1;
}
return 0;
}
}
}