using Jobberwocky.GeometryAlgorithms.Source.Core;
using System;
using System.Collections.Generic;
namespace Jobberwocky.GeometryAlgorithms.Source.Algorithms.Hull2D
{
///
/// Class that contains the algorithm for creating a hull around a set of points
///
public class Hull2DAlgorithm
{
private readonly double constAngleCos = Math.Cos(90.0 / (180.0 / Math.PI));
///
/// Constructor
///
public Hull2DAlgorithm()
{
}
///
/// Calculates the cross product of three vectors
///
///
///
///
///
private double Cross(Vertex o, Vertex a, Vertex b)
{
return (a.Position.x - o.Position.x) * (b.Position.y - o.Position.y)
- (a.Position.y - o.Position.y) * (b.Position.x - o.Position.x);
}
///
/// Calculates the point set in the upper tangent
///
///
///
private Vertex[] UpperTangent(Vertex[] pointSet)
{
var lower = new List();
for (var l = 0; l < pointSet.Length; l++)
{
while (lower.Count >= 2 && (Cross(lower[lower.Count - 2], lower[lower.Count - 1], pointSet[l]) <= 0))
{
lower.RemoveAt(lower.Count - 1);
}
lower.Add(pointSet[l]);
}
lower.RemoveAt(lower.Count - 1);
return lower.ToArray();
}
///
/// Calculates the point set for the lower tangent
///
///
///
private Vertex[] LowerTangent(ref Vertex[] pointSet)
{
Vertex[] reversed = new Vertex[pointSet.Length];
var upper = new List();
for (var u = 0; u < pointSet.Length; u++)
{
reversed[u] = (pointSet[pointSet.Length - 1 - u]);
while (upper.Count >= 2 && (Cross(upper[upper.Count - 2], upper[upper.Count - 1], reversed[u]) <= 0))
{
upper.RemoveAt(upper.Count - 1);
}
upper.Add(reversed[u]);
}
upper.RemoveAt(upper.Count - 1);
pointSet = reversed;
return upper.ToArray();
}
///
/// Calculates the convex hull of a given input point set
///
///
///
private List Convex(ref Vertex[] pointSet)
{
var upper = UpperTangent(pointSet);
var lower = LowerTangent(ref pointSet);
var convexHull = new List(lower.Length + upper.Length + 1);
convexHull.AddRange(lower);
convexHull.AddRange(upper);
convexHull.Add(pointSet[0]);
return convexHull;
}
///
/// Are the vectors counter clock wise?
///
///
///
///
///
private bool Ccw(Vertex p1, Vertex p2, Vertex p3)
{
var cw = ((p3.Position.y - p1.Position.y) * (p2.Position.x - p1.Position.x))
- ((p2.Position.y - p1.Position.y) * (p3.Position.x - p1.Position.x));
return cw > 0 ? true : cw < 0 ? false : true;
}
///
/// Check whether the two segments intersect
///
///
///
///
private bool Intersect(Vertex seg1P1, Vertex seg1P2, Vertex seg2P1, Vertex seg2P2)
{
return Ccw(seg1P1, seg2P1, seg2P2) != Ccw(seg1P2, seg2P1, seg2P2)
&& Ccw(seg1P1, seg1P2, seg2P1) != Ccw(seg1P1, seg1P2, seg2P2);
}
///
/// Sort the point set by the X coordinate and then by the Y coordinate
///
///
private void SortByX(Vertex[] pointSet)
{
Array.Sort(pointSet, delegate (Vertex p1, Vertex p2)
{
if (p1.Position.x == p2.Position.x)
{
return p1.Position.y.CompareTo(p2.Position.y);
}
else
{
return p1.Position.x.CompareTo(p2.Position.x);
}
});
}
///
/// Calculate the square length between two vectors
///
///
///
///
private double SqLength(Vertex a, Vertex b)
{
return (b.Position.x - a.Position.x) * (b.Position.x - a.Position.x)
+ (b.Position.y - a.Position.y) * (b.Position.y - a.Position.y);
}
///
/// Calculates the angle
///
///
///
///
///
private double Cos(Vertex o, Vertex a, Vertex b)
{
var aShiftedX = a.Position.x - o.Position.x;
var aShiftedY = a.Position.y - o.Position.y;
var bShiftedX = b.Position.x - o.Position.x;
var bShiftedY = b.Position.y - o.Position.y;
var sqALen = SqLength(o, a);
var sqBLen = SqLength(o, b);
var dot = aShiftedX * bShiftedX + aShiftedY * bShiftedY;
return dot / Math.Sqrt(sqALen * sqBLen);
}
///
/// Check whether the segment intersects with the point set
///
///
///
///
private bool Intersect(Vertex seg1P1, Vertex seg1P2, List pointSet)
{
Vertex seg2P1, seg2P2;
for (var i = 0; i < pointSet.Count - 1; i++)
{
seg2P1 = pointSet[i];
seg2P2 = pointSet[i + 1];
if (seg1P1.Equals(seg2P1) || seg1P1.Equals(seg2P2))
{
continue;
}
if (Intersect(seg1P1, seg1P2, seg2P1, seg2P2))
{
return true;
}
}
return false;
}
///
/// Calculates the occupied area of a given point set
///
///
///
private double[] OccupiedArea(Vertex[] pointSet)
{
var minX = double.MaxValue;
var minY = double.MaxValue;
var maxX = double.MinValue;
var maxY = double.MinValue;
for (var i = pointSet.Length - 1; i >= 0; i--)
{
if (pointSet[i].Position.x < minX)
{
minX = pointSet[i].Position.x;
}
if (pointSet[i].Position.y < minY)
{
minY = pointSet[i].Position.y;
}
if (pointSet[i].Position.x > maxX)
{
maxX = pointSet[i].Position.x;
}
if (pointSet[i].Position.y > maxY)
{
maxY = pointSet[i].Position.y;
}
}
return new double[2] { maxX - minX, maxY - minY };
}
///
/// Calculates the bounding box of an edge
///
///
///
private double[] BboxAround(Vertex[] edge)
{
return new double[4] {
Math.Min(edge[0].Position.x, edge[1].Position.x),
Math.Min(edge[0].Position.y, edge[1].Position.y),
Math.Max(edge[0].Position.x, edge[1].Position.x),
Math.Max(edge[0].Position.y, edge[1].Position.y)
};
}
///
/// Calculates if one of the innerpoints could further detail the given edge
///
///
///
///
///
private Vertex MidPoint(Vertex[] edge, List innerPoints, List hullPoints)
{
var angle1Cos = constAngleCos;
var angle2Cos = constAngleCos;
double a1Cos, a2Cos;
Vertex midPoint = null, point = null;
for (var i = 0; i < innerPoints.Count; i++)
{
point = innerPoints[i];
a1Cos = Cos(edge[0], edge[1], point);
a2Cos = Cos(edge[1], edge[0], point);
if (a1Cos > angle1Cos && a2Cos > angle2Cos &&
!Intersect(edge[0], point, hullPoints) &&
!Intersect(edge[1], point, hullPoints))
{
angle1Cos = a1Cos;
angle2Cos = a2Cos;
midPoint = point;
}
}
return midPoint;
}
///
/// Calculates the concave hull based on an exsiting convex hull
///
///
///
///
///
///
///
private Vertex[] Concave(List convex, double maxSqEdgeLen, double[] maxSearchArea, Grid grid)
{
bool midPointInserted, hasValue;
int scaleFactor;
double bboxWidth, bboxHeight;
double[] bboxMax = new double[4];
Vertex midPoint;
Vertex[] edge = new Vertex[2];
EdgeKey keyInSkipList;
Dictionary edgeSkipList = new Dictionary();
do
{
midPointInserted = false;
for (var i = 0; i < convex.Count - 1; i++)
{
midPoint = null;
edge[0] = convex[i];
edge[1] = convex[i + 1];
keyInSkipList.point1 = edge[0].Id;
keyInSkipList.point2 = edge[1].Id;
if (SqLength(edge[0], edge[1]) < maxSqEdgeLen ||
(edgeSkipList.TryGetValue(keyInSkipList, out hasValue) == true)) { continue; }
scaleFactor = 0;
bboxMax = BboxAround(edge);
do
{
grid.ExtendBbox(bboxMax, scaleFactor);
bboxWidth = bboxMax[2] - bboxMax[0];
bboxHeight = bboxMax[3] - bboxMax[1];
midPoint = MidPoint(edge, grid.RangePoints(bboxMax), convex);
scaleFactor++;
} while (midPoint == null && (maxSearchArea[0] > bboxWidth || maxSearchArea[1] > bboxHeight));
if (bboxWidth >= maxSearchArea[0] && bboxHeight >= maxSearchArea[1])
{
edgeSkipList[keyInSkipList] = true;
}
if (midPoint != null)
{
convex.Insert(i + 1, midPoint);
grid.RemovePoint(midPoint);
midPointInserted = true;
}
}
}
while (midPointInserted);
return convex.ToArray();
}
///
/// Calculates the hull for a given input set and given the max edge length
///
///
///
///
public Vertex[] GenerateHull(Vertex[] points, double concavity)
{
// When we have a very small data set, just return the points
if (points.Length < 4)
{
return points;
}
// Sort the points by their X coordinate
SortByX(points);
// Create the Convex hull
List convex = Convex(ref points);
Vertex[] hull;
if (concavity != double.MaxValue) {
// Calculate the occupied area of all the points and the relative search area
var occupiedArea = OccupiedArea(points);
var maxSearchArea = new double[2] {occupiedArea[0] * 0.6, occupiedArea[1] * 0.6};
// Calculate metrics and store the points in a spatial grid structure
var maxEdgeLen = concavity * concavity;
var cellSize = (int) Math.Ceiling(1 / (points.Length / (occupiedArea[0] * occupiedArea[1])));
var grid = new Grid(points, cellSize);
// Remove the convex hull points from the grid
for (var i = 0; i < convex.Count; i++) {
grid.RemovePoint(convex[i]);
}
// Generate the concave hull
hull = Concave(convex, maxEdgeLen, maxSearchArea, grid);
} else {
hull = convex.ToArray();
}
return hull;
}
}
///
/// Grid class used to store points for calculating the hull
///
public class Grid
{
private Dictionary> Cells;
private int CellSize;
///
/// Constructor
///
///
///
public Grid(Vertex[] points, int cellSize)
{
Cells = new Dictionary>();
CellSize = cellSize;
GridKey key = new GridKey();
for (var i = 0; i < points.Length; i++)
{
key = Point2CellXY(points[i].Position.x, points[i].Position.y);
if (!Cells.ContainsKey(key))
{
Cells.Add(key, new List());
}
Cells[key].Add(points[i]);
}
}
///
/// Get the point stored in a given cell
///
///
///
///
public void CellPoints(GridKey key, ref List points)
{
if (Cells.ContainsKey(key))
{
points.AddRange(Cells[key]);
}
}
///
/// Get all the point within the given bounding box
///
///
///
public List RangePoints(double[] bbox)
{
GridKey keyTl = Point2CellXY(bbox[0], bbox[1]);
GridKey keyBr = Point2CellXY(bbox[2], bbox[3]);
GridKey key = new GridKey();
var gridPoints = new List();
for (var x = keyTl.X; x <= keyBr.X; x += 1)
{
for (var y = keyTl.Y; y <= keyBr.Y; y += 1)
{
key.X = x;
key.Y = y;
CellPoints(key, ref gridPoints);
}
}
return gridPoints;
}
///
/// Remove a given point from the grid
///
///
///
public void RemovePoint(Vertex point)
{
GridKey key = Point2CellXY(point.Position.x, point.Position.y);
var cell = Cells[key];
Vertex v;
for (var i = 0; i < cell.Count; i++)
{
v = cell[i];
if (point.Equals(v))
{
cell.RemoveAt(i);
return;
}
}
}
///
/// Get the indices in the grid from a given input point
///
///
///
public GridKey Point2CellXY(double px, double py)
{
return new GridKey((int)(px / CellSize), (int)(py / CellSize));
}
///
/// Extend a bounding box with a given scale factor
///
///
///
///
public void ExtendBbox(double[] bbox, float scaleFactor)
{
bbox[0] -= (scaleFactor * CellSize);
bbox[1] -= (scaleFactor * CellSize);
bbox[2] += (scaleFactor * CellSize);
bbox[3] += (scaleFactor * CellSize);
}
}
public struct GridKey : IEquatable
{
public int X;
public int Y;
public GridKey(int x, int y)
{
X = x;
Y = y;
}
public bool Equals(GridKey key)
{
return X == key.X && Y == key.Y;
}
public override int GetHashCode()
{
return X.GetHashCode() * 17 + Y.GetHashCode();
}
}
public struct EdgeKey : IEquatable
{
public int point1;
public int point2;
public EdgeKey(int p1, int p2)
{
point1 = p1;
point2 = p2;
}
public bool Equals(EdgeKey key)
{
return point2 == key.point2 && point1 == key.point1;
}
public override int GetHashCode()
{
return point1.GetHashCode() * 17 + point2.GetHashCode();
}
}
}