#Region Project Attributes
#MainFormWidth: 600
#MainFormHeight: 600
#End Region
Sub Process_Globals
Private fx As JFX
Private MainForm As Form
Private xui As XUI
Dim pointList As List
Private cv As Canvas
Dim nativeMe As JavaObject
End Sub
Sub AppStart (Form1 As Form, Args() As String)
MainForm = Form1
MainForm.RootPane.LoadLayout("Layout1")
MainForm.Show
pointList.Initialize
nativeMe = Me
Dim aa As JavaObject
For i = 0 To 200
Dim x As Double = Rnd(50, 520).As(Double)
Dim y As Double = Rnd(200,400).As(Double)
aa.InitializeNewInstance("b4j.example.main.Point", Array(x, y))
pointList.Add(aa)
cv.DrawCircle(x, y, 2, fx.Colors.Black, True, 0)
Next
Dim k As Int = 3
Dim returnlist As List
returnlist.Initialize
returnlist = nativeMe.RunMethod("calculateConcaveHull", Array(pointList, k))
Dim prevX, prevY As Double
For i = 0 To returnlist.Size - 1
Dim dd(2) As String = Regex.Split(" ", returnlist.Get(i))
Dim bbx As Double = dd(0).Replace("(", "")
Dim bby As Double = dd(1).Replace(")", "")
cv.DrawCircle(bbx, bby, 2, fx.Colors.Blue, True, 0)
If i = 0 Then
prevX = bbx
prevY = bby
End If
If i > 0 Then
cv.Drawline(prevX, prevY, bbx, bby, fx.Colors.Red, 2)
End If
prevX = bbx
prevY = bby
Next
Log("FINISH")
End Sub
#If Java
import javafx.util.Pair;
import java.util.*;
/**
* ConcaveHull.java - 14/10/16
*
* @author Udo Schlegel - Udo.3.Schlegel(at)uni-konstanz.de
* @version 1.0
*
* This is an implementation of the algorithm described by Adriano Moreira and Maribel Yasmina Santos:
* CONCAVE HULL: A K-NEAREST NEIGHBOURS APPROACH FOR THE COMPUTATION OF THE REGION OCCUPIED BY A SET OF POINTS.
* GRAPP 2007 - International Conference on Computer Graphics Theory and Applications; pp 61-68.
*
* https://repositorium.sdum.uminho.pt/bitstream/1822/6429/1/ConcaveHull_ACM_MYS.pdf
*
* With help from https://github.com/detlevn/QGIS-ConcaveHull-Plugin/blob/master/concavehull.py
*/
public static class Point {
private final Double x;
private final Double y;
public Point(Double x, Double y) {
this.x = x;
this.y = y;
}
public Double getX() {
return x;
}
public Double getY() {
return y;
}
public String toString() {
return "(" + x + " " + y + ")";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
if (x.equals(((Point) obj).getX()) && y.equals(((Point) obj).getY())) {
return true;
} else {
return false;
}
} else {
return false;
}
}
@Override
public int hashCode() {
// http://stackoverflow.com/questions/22826326/good-hashcode-function-for-2d-coordinates
// http://www.cs.upc.edu/~alvarez/calculabilitat/enumerabilitat.pdf
int tmp = (int) (y + ((x + 1) / 2));
return Math.abs((int) (x + (tmp * tmp)));
}
}
private static Double euclideanDistance(Point a, Point b) {
return Math.sqrt(Math.pow(a.getX() - b.getX(), 2) + Math.pow(a.getY() - b.getY(), 2));
}
private static ArrayList<Point> kNearestNeighbors(ArrayList<Point> l, Point q, Integer k) {
ArrayList<Pair<Double, Point>> nearestList = new ArrayList<>();
for (Point o : l) {
nearestList.add(new Pair<>(euclideanDistance(q, o), o));
}
Collections.sort(nearestList, new Comparator<Pair<Double, Point>>() {
@Override
public int compare(Pair<Double, Point> o1, Pair<Double, Point> o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
ArrayList<Point> result = new ArrayList<>();
for (int i = 0; i < Math.min(k, nearestList.size()); i++) {
result.add(nearestList.get(i).getValue());
}
return result;
}
private static Point findMinYPoint(ArrayList<Point> l) {
Collections.sort(l, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
return o1.getY().compareTo(o2.getY());
}
});
return l.get(0);
}
private static Double calculateAngle(Point o1, Point o2) {
return Math.atan2(o2.getY() - o1.getY(), o2.getX() - o1.getX());
}
private static Double angleDifference(Double a1, Double a2) {
// calculate angle difference in clockwise directions as radians
if ((a1 > 0 && a2 >= 0) && a1 > a2) {
return Math.abs(a1 - a2);
} else if ((a1 >= 0 && a2 > 0) && a1 < a2) {
return 2 * Math.PI + a1 - a2;
} else if ((a1 < 0 && a2 <= 0) && a1 < a2) {
return 2 * Math.PI + a1 + Math.abs(a2);
} else if ((a1 <= 0 && a2 < 0) && a1 > a2) {
return Math.abs(a1 - a2);
} else if (a1 <= 0 && 0 < a2) {
return 2 * Math.PI + a1 - a2;
} else if (a1 >= 0 && 0 >= a2) {
return a1 + Math.abs(a2);
} else {
return 0.0;
}
}
private static ArrayList<Point> sortByAngle(ArrayList<Point> l, Point q, Double a) {
// Sort by angle descending
Collections.sort(l, new Comparator<Point>() {
@Override
public int compare(final Point o1, final Point o2) {
Double a1 = angleDifference(a, calculateAngle(q, o1));
Double a2 = angleDifference(a, calculateAngle(q, o2));
return a2.compareTo(a1);
}
});
return l;
}
private static Boolean intersect(Point l1p1, Point l1p2, Point l2p1, Point l2p2) {
// calculate part equations for line-line intersection
Double a1 = l1p2.getY() - l1p1.getY();
Double b1 = l1p1.getX() - l1p2.getX();
Double c1 = a1 * l1p1.getX() + b1 * l1p1.getY();
Double a2 = l2p2.getY() - l2p1.getY();
Double b2 = l2p1.getX() - l2p2.getX();
Double c2 = a2 * l2p1.getX() + b2 * l2p1.getY();
// calculate the divisor
Double tmp = (a1 * b2 - a2 * b1);
// calculate intersection point x coordinate
Double pX = (c1 * b2 - c2 * b1) / tmp;
// check if intersection x coordinate lies in line line segment
if ((pX > l1p1.getX() && pX > l1p2.getX()) || (pX > l2p1.getX() && pX > l2p2.getX())
|| (pX < l1p1.getX() && pX < l1p2.getX()) || (pX < l2p1.getX() && pX < l2p2.getX())) {
return false;
}
// calculate intersection point y coordinate
Double pY = (a1 * c2 - a2 * c1) / tmp;
// check if intersection y coordinate lies in line line segment
if ((pY > l1p1.getY() && pY > l1p2.getY()) || (pY > l2p1.getY() && pY > l2p2.getY())
|| (pY < l1p1.getY() && pY < l1p2.getY()) || (pY < l2p1.getY() && pY < l2p2.getY())) {
return false;
}
return true;
}
private static boolean pointInPolygon(Point p, ArrayList<Point> pp) {
boolean result = false;
for (int i = 0, j = pp.size() - 1; i < pp.size(); j = i++) {
if ((pp.get(i).getY() > p.getY()) != (pp.get(j).getY() > p.getY()) &&
(p.getX() < (pp.get(j).getX() - pp.get(i).getX()) * (p.getY() - pp.get(i).getY()) / (pp.get(j).getY() - pp.get(i).getY()) + pp.get(i).getX())) {
result = !result;
}
}
return result;
}
public static ArrayList<Point> calculateConcaveHull(ArrayList<Point> pointArrayList, Integer k) {
// the resulting concave hull
ArrayList<Point> concaveHull = new ArrayList<>();
// optional remove duplicates
HashSet<Point> set = new HashSet<>(pointArrayList);
ArrayList<Point> pointArraySet = new ArrayList<>(set);
// k has to be greater than 3 to execute the algorithm
Integer kk = Math.max(k, 3);
// return Points if already Concave Hull
if (pointArraySet.size() < 3) {
return pointArraySet;
}
// make sure that k neighbors can be found
kk = Math.min(kk, pointArraySet.size() - 1);
// find first point and remove from point list
Point firstPoint = findMinYPoint(pointArraySet);
concaveHull.add(firstPoint);
Point currentPoint = firstPoint;
pointArraySet.remove(firstPoint);
Double previousAngle = 0.0;
Integer step = 2;
while ((currentPoint != firstPoint || step == 2) && pointArraySet.size() > 0) {
// after 3 steps add first point to dataset, otherwise hull cannot be closed
if (step == 5) {
pointArraySet.add(firstPoint);
}
// get k nearest neighbors of current point
ArrayList<Point> kNearestPoints = kNearestNeighbors(pointArraySet, currentPoint, kk);
// sort points by angle clockwise
ArrayList<Point> clockwisePoints = sortByAngle(kNearestPoints, currentPoint, previousAngle);
// check if clockwise angle nearest neighbors are candidates for concave hull
Boolean its = true;
int i = -1;
while (its && i < clockwisePoints.size() - 1) {
i++;
int lastPoint = 0;
if (clockwisePoints.get(i) == firstPoint) {
lastPoint = 1;
}
// check if possible new concave hull point intersects with others
int j = 2;
its = false;
while (!its && j < concaveHull.size() - lastPoint) {
its = intersect(concaveHull.get(step - 2), clockwisePoints.get(i), concaveHull.get(step - 2 - j), concaveHull.get(step - 1 - j));
j++;
}
}
// if there is no candidate increase k - try again
if (its) {
return calculateConcaveHull(pointArrayList, k + 1);
}
// add candidate to concave hull and remove from dataset
currentPoint = clockwisePoints.get(i);
concaveHull.add(currentPoint);
pointArraySet.remove(currentPoint);
// calculate last angle of the concave hull line
previousAngle = calculateAngle(concaveHull.get(step - 1), concaveHull.get(step - 2));
step++;
}
// Check if all points are contained in the concave hull
Boolean insideCheck = true;
int i = pointArraySet.size() - 1;
while (insideCheck && i > 0) {
insideCheck = pointInPolygon(pointArraySet.get(i), concaveHull);
i--;
}
// if not all points inside - try again
if (!insideCheck) {
return calculateConcaveHull(pointArrayList, k + 1);
} else {
return concaveHull;
}
}
#End If