using System;
using System.Collections.Generic;
using GeoAPI.Geometries;
using GisSharpBlog.NetTopologySuite.Geometries;
using GisSharpBlog.NetTopologySuite.Utilities;
using Wintellect.PowerCollections;
namespace GisSharpBlog.NetTopologySuite.Algorithm
{
///
/// Computes the convex hull of a .
/// The convex hull is the smallest convex Geometry that contains all the
/// points in the input Geometry.
/// Uses the Graham Scan algorithm.
///
public class ConvexHull
{
private readonly IGeometryFactory geomFactory = null;
private readonly ICoordinate[] inputPts = null;
///
/// Create a new convex hull construction for the input Geometry.
///
///
public ConvexHull(IGeometry geometry)
: this(ExtractCoordinates(geometry), geometry.Factory) {}
///
/// Create a new convex hull construction for the input array.
///
///
///
public ConvexHull(ICoordinate[] pts, IGeometryFactory geomFactory)
{
inputPts = pts;
this.geomFactory = geomFactory;
}
///
/// Returns a Geometry that represents the convex hull of the input point.
/// The point will contain the minimal number of points needed to
/// represent the convex hull. In particular, no more than two consecutive
/// points will be collinear.
///
///
/// If the convex hull contains 3 or more points, a Polygon;
/// 2 points, a LineString;
/// 1 point, a Point;
/// 0 points, an empty GeometryCollection.
///
public IGeometry GetConvexHull()
{
if (inputPts.Length == 0)
{
return geomFactory.CreateGeometryCollection(null);
}
if (inputPts.Length == 1)
{
return geomFactory.CreatePoint(inputPts[0]);
}
if (inputPts.Length == 2)
{
return geomFactory.CreateLineString(inputPts);
}
ICoordinate[] reducedPts = inputPts;
// use heuristic to reduce points, if large
if (inputPts.Length > 50)
{
reducedPts = Reduce(inputPts);
}
// sort points for Graham scan.
ICoordinate[] sortedPts = PreSort(reducedPts);
// Use Graham scan to find convex hull.
Stack cHS = GrahamScan(sortedPts);
// Convert stack to an array.
ICoordinate[] cH = cHS.ToArray();
// Convert array to appropriate output geometry.
return LineOrPolygon(cH);
}
///
///
///
///
///
private static ICoordinate[] ExtractCoordinates(IGeometry geom)
{
UniqueCoordinateArrayFilter filter = new UniqueCoordinateArrayFilter();
geom.Apply(filter);
return filter.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.
/// 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.
///
///
///
private ICoordinate[] Reduce(ICoordinate[] pts)
{
ICoordinate[] polyPts = ComputeOctRing(inputPts);
// unable to compute interior polygon for some reason
if (polyPts == null)
{
return inputPts;
}
// add points defining polygon
OrderedSet reducedSet = new OrderedSet();
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]);
}
}
ICoordinate[] arr = new ICoordinate[reducedSet.Count];
reducedSet.CopyTo(arr, 0);
return arr;
}
///
///
///
///
///
private ICoordinate[] PreSort(ICoordinate[] pts)
{
ICoordinate 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.
Array.Sort(pts, 1, pts.Length - 1, new RadialComparator(pts[0]));
return pts;
}
///
///
///
///
///
private Stack GrahamScan(ICoordinate[] c)
{
ICoordinate p;
Stack ps = new Stack(c.Length);
ps.Push(c[0]);
ps.Push(c[1]);
ps.Push(c[2]);
for (int i = 3; i < c.Length; i++)
{
p = ps.Pop();
while (CGAlgorithms.ComputeOrientation(ps.Peek(), p, c[i]) > 0)
{
p = ps.Pop();
}
ps.Push(p);
ps.Push(c[i]);
}
ps.Push(c[0]);
return ps;
}
///
///
///
///
///
private Stack ReverseStack(Stack ps)
{
// Do a manual reverse of the stack
int size = ps.Count;
ICoordinate[] tempArray = new ICoordinate[size];
for (int i = 0; i < size; i++)
{
tempArray[i] = ps.Pop();
}
Stack returnStack = new Stack(size);
foreach (ICoordinate obj in tempArray)
{
returnStack.Push(obj);
}
return returnStack;
}
///
///
///
///
///
///
///
/// Whether the three coordinates are collinear
/// and c2 lies between c1 and c3 inclusive.
///
private bool IsBetween(ICoordinate c1, ICoordinate c2, ICoordinate 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 ICoordinate[] ComputeOctRing(ICoordinate[] inputPts)
{
ICoordinate[] octPts = ComputeOctPts(inputPts);
CoordinateList coordList = new CoordinateList();
coordList.Add(octPts, false);
// points must all lie in a line
if (coordList.Count < 3)
{
return null;
}
coordList.CloseRing();
return coordList.ToCoordinateArray();
}
///
///
///
///
///
private ICoordinate[] ComputeOctPts(ICoordinate[] inputPts)
{
ICoordinate[] pts = new ICoordinate[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;
}
///
///
///
/// The vertices of a linear ring, which may or may not be flattened (i.e. vertices collinear).
/// A 2-vertex LineString if the vertices are collinear;
/// otherwise, a Polygon with unnecessary (collinear) vertices removed.
private IGeometry LineOrPolygon(ICoordinate[] coordinates)
{
coordinates = CleanRing(coordinates);
if (coordinates.Length == 3)
{
return geomFactory.CreateLineString(new ICoordinate[]
{
coordinates[0],
coordinates[1]
});
}
ILinearRing linearRing = geomFactory.CreateLinearRing(coordinates);
return geomFactory.CreatePolygon(linearRing, null);
}
///
///
///
/// The vertices of a linear ring, which may or may not be flattened (i.e. vertices collinear).
/// The coordinates with unnecessary (collinear) vertices removed.
private ICoordinate[] CleanRing(ICoordinate[] original)
{
Equals(original[0], original[original.Length - 1]);
List cleanedRing = new List();
ICoordinate previousDistinctCoordinate = null;
for (int i = 0; i <= original.Length - 2; i++)
{
ICoordinate currentCoordinate = original[i];
ICoordinate 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]);
return cleanedRing.ToArray();
}
///
/// Compares s for their angle and distance
/// relative to an origin.
///
private class RadialComparator : IComparer
{
private readonly ICoordinate origin = null;
///
/// Initializes a new instance of the class.
///
///
public RadialComparator(ICoordinate origin)
{
this.origin = origin;
}
///
///
///
///
///
///
public int Compare(ICoordinate p1, ICoordinate p2)
{
return PolarCompare(origin, p1, p2);
}
///
///
///
///
///
///
///
private static int PolarCompare(ICoordinate o, ICoordinate p, ICoordinate 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;
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;
}
}
}
}