Thanks Williams!@MasterGy
I liked to work with B4J and so I transferred your code. It works perfectly - and fast, 2 to 20 msecs per hull computation.
Your code is compact, and I can read each line. But for the life of me, I can't figure out how you're doing it - I'll study it more.
Can you outline the algorithm you use?
It's great that you tested it, because I didn't even think about that. Indeed, such forms do not work well. You can increase 'accurary' in the code as you like, but it is not a good solution, because the outside and inside of the horseshoe will be different. I will still think of a solution to rule out this problem.@MasterGy
Thanks, that helps a lot.
Step 1 is finding what is typically called a convex hull. I like your metaphor of a "fence" - as a youth I worked on a farm and spend a lot of time mending fences.
Almost all algorithms for a Concave Hull start with a Convex Hull, so that's good. It is also the basis for the algorithm referenced by the OP. Your code for that is fast and it works.
Step 2 is the tricky part, as your experimentation shows. Deep concave shapes will require a lot of steps. In your code, 100% accuracy is sometimes not enough.
View attachment 158565
You'll find a very useful PDF discussing the issues of 'digging' into the Convex Hull.
#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
I think I have this all worked out.Thanks, that sounds like a simple approach and it will be an interesting exercise to code that.
Just 2 questions spring to mind, related to the Java code in the mentioned link:
https://github.com/mapbox/concaveman
1. Will it be quicker to translate that Java code to B4A code?
2. What will run quicker, code based on your idea or code based on that Java code?
I am away for 5 days from today, but will give both a go and report back.
RBS
Sub Process_Globals
Type TMapLatLng(fLat As Double, fLng As Double)
Dim iMapPoints As Int ' Pontok száma
Dim arrPoints() As TMapLatLng
Dim arrHull() As Int
Dim arrbHull_Busy() As Boolean
Dim iHull_C As Int
Dim pi As Float = 3.14159265
'Dim arrLimits(6) As Double 'not needed anymore
Dim bWorking As Boolean
End Sub
'return a TLatLng from lat/lng
Public Sub initLatLng(aLat As Double,aLng As Double) As TMapLatLng
Dim ll As TMapLatLng
ll.Initialize
ll.fLat=validLat(aLat)
ll.fLng=validLng(aLng)
Return ll
End Sub
'ConcaveMan code
'-------------------------------------------------------------------------------------------
Sub Reset(iPoints As Int)
Dim arrHull(iPoints) As Int
Dim arrbHull_Busy(iPoints) As Boolean
Dim arrPoints(iPoints) As TMapLatLng
'Dim arrLimits(6) As Double 'not needed anymore
iHull_C = 0
iMapPoints = iPoints
bWorking = False
End Sub
Sub GetBorderPoints(lstMapLatLonPoints As List, fAcc As Double) As List
Dim i As Int
Dim fMinY As Double = 99999999
Dim fRang As Double = 2 * pi
Dim iMinAng_Ind As Int
Dim iMinAng_NoLimit_Ind As Int
Dim fMinAng_Ang As Double
Dim fMinAng_NoLimit_Ang As Double
Dim fMinAng_NoLimit_Dis As Double
Dim fDis As Double
Dim fDis0 As Double
Dim fDis1 As Double
Dim fang As Double
Dim iAktP As Int
Dim bChange As Boolean
Dim bNoOK As Boolean
Dim iH1 As Int
Dim iH2 As Int
Dim iMinDis_Ind As Int
Dim fMinDis As Double
Dim fDis0 As Double
Dim fDis1 As Double
Dim fDis3 As Double
Dim fDis4 As Double
Dim tLL As TMapLatLng
Dim lstBorderPoints As List
Reset(lstMapLatLonPoints.Size)
'this used to be done by Sub AddPoints
'-------------------------------------
For i = 0 To lstMapLatLonPoints.Size - 1
tLL = lstMapLatLonPoints.Get(i)
arrPoints(i) = tLL
Next
bWorking = True
For i = 0 To iMapPoints - 1
If arrPoints(i).fLat < fMinY Then
iAktP = i
fMinY = arrPoints(i).fLat
End If
Next
' A legalsó pontot kiemelni (piros szín)
Add_Hull(iAktP, 0)
'-------------------------------------------------------------- STANDARD FRAME
Do While True
'keresse a legkozelebbit,aminek a legkisebb a szoge,
iMinAng_Ind = -1
fMinAng_Ang = 999999
iMinAng_NoLimit_Ind = -1
fMinAng_NoLimit_Ang = 999999
fMinAng_NoLimit_Dis = 999999
For i = 0 To iMapPoints - 1
If i = iAktP Then Continue
fDis0 = arrPoints(i).fLng - arrPoints(iAktP).fLng
fDis1 = arrPoints(i).fLat - arrPoints(iAktP).fLat
fDis = Sqrt(fDis0 * fDis0 + fDis1 * fDis1)
fang = DifAng(fRang, ATan2(fDis0, fDis1))
If fang < fMinAng_NoLimit_Ang And fDis < fMinAng_NoLimit_Dis Then
fMinAng_NoLimit_Ang = fang
fMinAng_NoLimit_Dis = fDis
iMinAng_NoLimit_Ind = i
End If
If fang < fMinAng_Ang Then
fMinAng_Ang = fang
iMinAng_Ind = i
End If
Next
If iMinAng_Ind = -1 Then
iMinAng_Ind = iMinAng_NoLimit_Ind
fMinAng_Ang = fMinAng_NoLimit_Ang
End If
' Line (points(iAktP, 0), points(iAktP, 1))-(points(iMinAng_Ind, 0), points(iMinAng_Ind, 1))
If iAktP = arrHull(0) And iHull_C > 2 Then Exit
Add_Hull(iAktP, iHull_C)
fRang = ATan2(arrPoints(iAktP).fLng - arrPoints(iMinAng_Ind).fLng, arrPoints(iAktP).fLat - arrPoints(iMinAng_Ind).fLat)
iAktP = iMinAng_Ind
Loop
'------------------------------------- FRAME RESOLUTION
Do While True 'For qqw = 0 To 100
bChange = False
iH1 = 0
Do While True
iH2 = (iH1 + 1) Mod (iHull_C - 0)
fMinDis = 999999
iMinDis_Ind = -1
fDis3 = Sqrt(Power(arrPoints(arrHull(iH2)).fLng - arrPoints(arrHull(iH1)).fLng,2) + Power((arrPoints(arrHull(iH2)).fLat - arrPoints(arrHull(iH1)).fLat),2))
For ind = 0 To iMapPoints - 1
If arrbHull_Busy(ind) Then Continue
fDis0 = Sqrt(Power(arrPoints(ind).fLng - arrPoints(arrHull(iH1)).fLng, 2) + Power((arrPoints(ind).fLat - arrPoints(arrHull(iH1)).fLat), 2))
fDis1 = Sqrt(Power(arrPoints(ind).fLng - arrPoints(arrHull(iH2)).fLng, 2) + Power((arrPoints(ind).fLat - arrPoints(arrHull(iH2)).fLat), 2))
fDis4 = fDis0 + fDis1
If fDis3 * fAcc > fDis4 And (fDis4 < fMinDis) Then
fMinDis = fDis4
iMinDis_Ind = ind
End If
Next
If iMinDis_Ind <> -1 Then
bNoOK = Add_Hull(iMinDis_Ind, iH2)
If bNoOK Then
iH1 = iH1 + 1
bChange = True
End If
End If
iH1 = iH1 + 1
If iH1 > iHull_C - 1 Then Exit
Loop
If bChange = False Then Exit
Loop
'this is not needed anymore
'--------------------------
'Calc_Limits
bWorking = False
lstBorderPoints.Initialize
For i = 0 To iHull_C - 1
tLL = initLatLng(arrPoints(arrHull(i)).fLat, arrPoints(arrHull(i)).fLng)
lstBorderPoints.Add(tLL)
Next
tLL = initLatLng(arrPoints(arrHull(0)).fLat, arrPoints(arrHull(0)).fLng)
lstBorderPoints.Add(tLL)
Return lstBorderPoints
End Sub
Sub Add_Hull (iInd As Int, iHova As Int ) As Boolean
'vizsgalat
Dim bOK As Boolean= True
Dim bFelt As Boolean
Dim iT0 As Int
Dim iT1 As Int
Dim iT3 As Int
'mentes elkeszitese
Dim arrmHull(iHull_C + 10) As Int
For t = 0 To iHull_C - 1
arrmHull(t) = arrHull(t)
Next
'beiktat
iHull_C = iHull_C + 1
For t = iHull_C - 2 To iHova Step -1
arrHull(t + 1) = arrHull(t)
Next
arrHull(iHova) = iInd
' GoTo 66
For iT0 = 0 To iHull_C - 1: iT1 = (iT0 + 1) Mod iHull_C
For t2 = 0 To iHull_C - 1: iT3 = (t2 + 1) Mod iHull_C
bFelt = (arrHull(iT0) = arrHull(t2) Or arrHull(iT0) = arrHull(iT3))
bFelt = (arrHull(iT1) = arrHull(t2) Or arrHull(iT1) = arrHull(iT3)) Or bFelt
If bFelt Then Continue
If IsIntersection(arrPoints(arrHull(iT0)).fLng, arrPoints(arrHull(iT0)).fLat, arrPoints(arrHull(iT1)).fLng, arrPoints(arrHull(iT1)).fLat, arrPoints(arrHull(t2)).fLng, arrPoints(arrHull(t2)).fLat, arrPoints(arrHull(iT3)).fLng, arrPoints(arrHull(iT3)).fLat) Then bOK = False
Next
Next
' ok = 1
If bOK = False Then
For t = 0 To iHull_C - 1
arrHull(t) = arrmHull(t)
Next
iHull_C = iHull_C - 1
Else
arrbHull_Busy(iInd) = True
End If
Return bOK
End Sub
Sub DifAng (fAngle1 As Double, fAngle2 As Double) As Double
Dim fDiff As Double
Dim fC As Double = 2 * pi
Do Until fAngle1 < fC
fAngle1 = fAngle1 - fC
Loop
Do Until fAngle1 >= 0
fAngle1 = fAngle1 + fC
Loop
Do Until fAngle2 < fC
fAngle2 = fAngle2 - fC
Loop
Do Until fAngle2 >= 0
fAngle2 = fAngle2 + fC
Loop
fDiff = fAngle1 - fAngle2
If fDiff < -pi Then fDiff = fDiff + 2 * pi
If fDiff > pi Then fDiff = fDiff - 2 * pi
Return fDiff
End Sub
Sub IsIntersection (fX1 As Double, fY1 As Double, fX2 As Double, fY2 As Double, fX3 As Double, fY3 As Double, fX4 As Double, fY4 As Double) As Boolean
Dim fDenominator As Double
Dim fUA As Double
Dim fUB As Double
fDenominator = (fY4 - fY3) * (fX2 - fX1) - (fX4 - fX3) * (fY2 - fY1)
If fDenominator = 0 Then
Return False 'IsIntersection = 0
Else
fUA = ((fX4 - fX3) * (fY1 - fY3) - (fY4 - fY3) * (fX1 - fX3)) / fDenominator
fUB = ((fX2 - fX1) * (fY1 - fY3) - (fY2 - fY1) * (fX1 - fX3)) / fDenominator
Return fUA >= 0 And fUA <= 1 And fUB >= 0 And fUB <= 1
End If
End Sub
'the fAcc argument will determine the number of border points.
'The higher this number the more border points and that will produce a more tighter border.
'------------------------------------------------------------------------------------------
lstPolygonPoints = MapUtilities.GetBorderPoints(lstMapPoints, 1.5)
I think I have this all worked out.
For now I have gone with code as in this link:
https://github.com/mapbox/concaveman
This is the code as I have it now and that is all working fine.
As I just need a return list of lat/lng types based on a passed list of map lat/lng points I have
left out all the bitmap related code.
In a code module called MapUtilities:
B4X:Sub Process_Globals Type TMapLatLng(fLat As Double, fLng As Double) Dim iMapPoints As Int ' Pontok száma Dim arrPoints() As TMapLatLng Dim arrHull() As Int Dim arrbHull_Busy() As Boolean Dim iHull_C As Int Dim pi As Float = 3.14159265 'Dim arrLimits(6) As Double 'not needed anymore Dim bWorking As Boolean End Sub 'return a TLatLng from lat/lng Public Sub initLatLng(aLat As Double,aLng As Double) As TMapLatLng Dim ll As TMapLatLng ll.Initialize ll.fLat=validLat(aLat) ll.fLng=validLng(aLng) Return ll End Sub 'ConcaveMan code '------------------------------------------------------------------------------------------- Sub Reset(iPoints As Int) Dim arrHull(iPoints) As Int Dim arrbHull_Busy(iPoints) As Boolean Dim arrPoints(iPoints) As TMapLatLng 'Dim arrLimits(6) As Double 'not needed anymore iHull_C = 0 iMapPoints = iPoints bWorking = False End Sub Sub GetBorderPoints(lstMapLatLonPoints As List, fAcc As Double) As List Dim i As Int Dim fMinY As Double = 99999999 Dim fRang As Double = 2 * pi Dim iMinAng_Ind As Int Dim iMinAng_NoLimit_Ind As Int Dim fMinAng_Ang As Double Dim fMinAng_NoLimit_Ang As Double Dim fMinAng_NoLimit_Dis As Double Dim fDis As Double Dim fDis0 As Double Dim fDis1 As Double Dim fang As Double Dim iAktP As Int Dim bChange As Boolean Dim bNoOK As Boolean Dim iH1 As Int Dim iH2 As Int Dim iMinDis_Ind As Int Dim fMinDis As Double Dim fDis0 As Double Dim fDis1 As Double Dim fDis3 As Double Dim fDis4 As Double Dim tLL As TMapLatLng Dim lstBorderPoints As List Reset(lstMapLatLonPoints.Size) 'this used to be done by Sub AddPoints '------------------------------------- For i = 0 To lstMapLatLonPoints.Size - 1 tLL = lstMapLatLonPoints.Get(i) arrPoints(i) = tLL Next bWorking = True For i = 0 To iMapPoints - 1 If arrPoints(i).fLat < fMinY Then iAktP = i fMinY = arrPoints(i).fLat End If Next ' A legalsó pontot kiemelni (piros szín) Add_Hull(iAktP, 0) '-------------------------------------------------------------- STANDARD FRAME Do While True 'keresse a legkozelebbit,aminek a legkisebb a szoge, iMinAng_Ind = -1 fMinAng_Ang = 999999 iMinAng_NoLimit_Ind = -1 fMinAng_NoLimit_Ang = 999999 fMinAng_NoLimit_Dis = 999999 For i = 0 To iMapPoints - 1 If i = iAktP Then Continue fDis0 = arrPoints(i).fLng - arrPoints(iAktP).fLng fDis1 = arrPoints(i).fLat - arrPoints(iAktP).fLat fDis = Sqrt(fDis0 * fDis0 + fDis1 * fDis1) fang = DifAng(fRang, ATan2(fDis0, fDis1)) If fang < fMinAng_NoLimit_Ang And fDis < fMinAng_NoLimit_Dis Then fMinAng_NoLimit_Ang = fang fMinAng_NoLimit_Dis = fDis iMinAng_NoLimit_Ind = i End If If fang < fMinAng_Ang Then fMinAng_Ang = fang iMinAng_Ind = i End If Next If iMinAng_Ind = -1 Then iMinAng_Ind = iMinAng_NoLimit_Ind fMinAng_Ang = fMinAng_NoLimit_Ang End If ' Line (points(iAktP, 0), points(iAktP, 1))-(points(iMinAng_Ind, 0), points(iMinAng_Ind, 1)) If iAktP = arrHull(0) And iHull_C > 2 Then Exit Add_Hull(iAktP, iHull_C) fRang = ATan2(arrPoints(iAktP).fLng - arrPoints(iMinAng_Ind).fLng, arrPoints(iAktP).fLat - arrPoints(iMinAng_Ind).fLat) iAktP = iMinAng_Ind Loop '------------------------------------- FRAME RESOLUTION Do While True 'For qqw = 0 To 100 bChange = False iH1 = 0 Do While True iH2 = (iH1 + 1) Mod (iHull_C - 0) fMinDis = 999999 iMinDis_Ind = -1 fDis3 = Sqrt(Power(arrPoints(arrHull(iH2)).fLng - arrPoints(arrHull(iH1)).fLng,2) + Power((arrPoints(arrHull(iH2)).fLat - arrPoints(arrHull(iH1)).fLat),2)) For ind = 0 To iMapPoints - 1 If arrbHull_Busy(ind) Then Continue fDis0 = Sqrt(Power(arrPoints(ind).fLng - arrPoints(arrHull(iH1)).fLng, 2) + Power((arrPoints(ind).fLat - arrPoints(arrHull(iH1)).fLat), 2)) fDis1 = Sqrt(Power(arrPoints(ind).fLng - arrPoints(arrHull(iH2)).fLng, 2) + Power((arrPoints(ind).fLat - arrPoints(arrHull(iH2)).fLat), 2)) fDis4 = fDis0 + fDis1 If fDis3 * fAcc > fDis4 And (fDis4 < fMinDis) Then fMinDis = fDis4 iMinDis_Ind = ind End If Next If iMinDis_Ind <> -1 Then bNoOK = Add_Hull(iMinDis_Ind, iH2) If bNoOK Then iH1 = iH1 + 1 bChange = True End If End If iH1 = iH1 + 1 If iH1 > iHull_C - 1 Then Exit Loop If bChange = False Then Exit Loop 'this is not needed anymore '-------------------------- 'Calc_Limits bWorking = False lstBorderPoints.Initialize For i = 0 To iHull_C - 1 tLL = initLatLng(arrPoints(arrHull(i)).fLat, arrPoints(arrHull(i)).fLng) lstBorderPoints.Add(tLL) Next tLL = initLatLng(arrPoints(arrHull(0)).fLat, arrPoints(arrHull(0)).fLng) lstBorderPoints.Add(tLL) Return lstBorderPoints End Sub Sub Add_Hull (iInd As Int, iHova As Int ) As Boolean 'vizsgalat Dim bOK As Boolean= True Dim bFelt As Boolean Dim iT0 As Int Dim iT1 As Int Dim iT3 As Int 'mentes elkeszitese Dim arrmHull(iHull_C + 10) As Int For t = 0 To iHull_C - 1 arrmHull(t) = arrHull(t) Next 'beiktat iHull_C = iHull_C + 1 For t = iHull_C - 2 To iHova Step -1 arrHull(t + 1) = arrHull(t) Next arrHull(iHova) = iInd ' GoTo 66 For iT0 = 0 To iHull_C - 1: iT1 = (iT0 + 1) Mod iHull_C For t2 = 0 To iHull_C - 1: iT3 = (t2 + 1) Mod iHull_C bFelt = (arrHull(iT0) = arrHull(t2) Or arrHull(iT0) = arrHull(iT3)) bFelt = (arrHull(iT1) = arrHull(t2) Or arrHull(iT1) = arrHull(iT3)) Or bFelt If bFelt Then Continue If IsIntersection(arrPoints(arrHull(iT0)).fLng, arrPoints(arrHull(iT0)).fLat, arrPoints(arrHull(iT1)).fLng, arrPoints(arrHull(iT1)).fLat, arrPoints(arrHull(t2)).fLng, arrPoints(arrHull(t2)).fLat, arrPoints(arrHull(iT3)).fLng, arrPoints(arrHull(iT3)).fLat) Then bOK = False Next Next ' ok = 1 If bOK = False Then For t = 0 To iHull_C - 1 arrHull(t) = arrmHull(t) Next iHull_C = iHull_C - 1 Else arrbHull_Busy(iInd) = True End If Return bOK End Sub Sub DifAng (fAngle1 As Double, fAngle2 As Double) As Double Dim fDiff As Double Dim fC As Double = 2 * pi Do Until fAngle1 < fC fAngle1 = fAngle1 - fC Loop Do Until fAngle1 >= 0 fAngle1 = fAngle1 + fC Loop Do Until fAngle2 < fC fAngle2 = fAngle2 - fC Loop Do Until fAngle2 >= 0 fAngle2 = fAngle2 + fC Loop fDiff = fAngle1 - fAngle2 If fDiff < -pi Then fDiff = fDiff + 2 * pi If fDiff > pi Then fDiff = fDiff - 2 * pi Return fDiff End Sub Sub IsIntersection (fX1 As Double, fY1 As Double, fX2 As Double, fY2 As Double, fX3 As Double, fY3 As Double, fX4 As Double, fY4 As Double) As Boolean Dim fDenominator As Double Dim fUA As Double Dim fUB As Double fDenominator = (fY4 - fY3) * (fX2 - fX1) - (fX4 - fX3) * (fY2 - fY1) If fDenominator = 0 Then Return False 'IsIntersection = 0 Else fUA = ((fX4 - fX3) * (fY1 - fY3) - (fY4 - fY3) * (fX1 - fX3)) / fDenominator fUB = ((fX2 - fX1) * (fY1 - fY3) - (fY2 - fY1) * (fX1 - fX3)) / fDenominator Return fUA >= 0 And fUA <= 1 And fUB >= 0 And fUB <= 1 End If End Sub
Called from a B4XPage like this:
B4X:'the fAcc argument will determine the number of border points. 'The higher this number the more border points and that will produce a more tighter border. '------------------------------------------------------------------------------------------ lstPolygonPoints = MapUtilities.GetBorderPoints(lstMapPoints, 1.5)
I will have a look at all the other suggestions as well, but I think this will do OK.
RBS
Thanks, will give this a go and see how it compares to my current code.I worked on it a bit more, but now I'm done. It is much faster than the previous version. I recommend that you try this one as well, you can easily insert it into your program and it makes nicer triangles than the first version you want to use. It follows the edge of the shape better.
fence_maker.initialise(2000,100) 'points array, fences array
fence_maker.add_points(x,y) 'upload points
fence_maker.way(100) 'parameter: set maxlinelength <---- fence maker
If this has run, you can read:
fence_maker_N - we have uploaded so many points
fence_maker_points(n,0/1) - read point X,Y
fence_maker.fence_c 'how many fences are there?
fence_maker.fence_length(index) 'length of fences
fence_maker.fence(index,n) 'which fence, which point The outer fence has an index of 0, which encloses the entire shape
fence_maker.limits(n) 'n:0,1,2,3,4,5 minX,maxX, X extent, minY,maxY,Y extent
fence_maker.lonely_c 'Number of lonely points with no section connected
fence_maker.lonely(index) as boolean 'The given point is pagan or not
fence_maker.triangle_c 'so many triangles have been made
fence_maker.triangles(n,0/1/2) '3 points of n-triangle
I had a go with your code, but it is quite a bit of work to understand it all and change it to make it accept a list of lat/lng types and make it return another list of lat/lng types.I worked on it a bit more, but now I'm done. It is much faster than the previous version. I recommend that you try this one as well, you can easily insert it into your program and it makes nicer triangles than the first version you want to use. It follows the edge of the shape better.
fence_maker.initialise(2000,100) 'points array, fences array
fence_maker.add_points(x,y) 'upload points
fence_maker.way(100) 'parameter: set maxlinelength <---- fence maker
If this has run, you can read:
fence_maker_N - we have uploaded so many points
fence_maker_points(n,0/1) - read point X,Y
fence_maker.fence_c 'how many fences are there?
fence_maker.fence_length(index) 'length of fences
fence_maker.fence(index,n) 'which fence, which point The outer fence has an index of 0, which encloses the entire shape
fence_maker.limits(n) 'n:0,1,2,3,4,5 minX,maxX, X extent, minY,maxY,Y extent
fence_maker.lonely_c 'Number of lonely points with no section connected
fence_maker.lonely(index) as boolean 'The given point is pagan or not
fence_maker.triangle_c 'so many triangles have been made
fence_maker.triangles(n,0/1/2) '3 points of n-triangle
I only left what was needed on the surface. I added the csv file to the file list.I had a go with your code, but it is quite a bit of work to understand it all and change it to make it accept a list of lat/lng types and make it return another list of lat/lng types.
The code as I have it is very fast and gives nice shapes.
I tested with a list of 260 lat/lng types and that returns the border points (in the right order) in 10 milli-secs as a list of 41 lat/lng types.
I am not sure it can be done any faster.
Attached my list of those 260 lat/lng values.
RBS
Sub upload_points_from_csv(csvfilename As String)
fence_maker.N = 0 'clear points
Dim list As List
list. Initialize
list = File.ReadList(File.DirAssets, csvfilename)
For i = 0 To list.Size - 1
Dim line As String = list.Get(i)
Dim parts() As String = Regex.Split(",", line)
fence_maker.add_points(parts(0),parts(1))
Next
End Sub
Thanks, it does work very well.I only left what was needed on the surface. I added the csv file to the file list.
If your cvs file records the xy values line by line and separated by commas, you can upload the points like this:
After that comes the fence generation.cvs file upload:Sub upload_points_from_csv(csvfilename As String) fence_maker.N = 0 'clear points Dim list As List list. Initialize list = File.ReadList(File.DirAssets, csvfilename) For i = 0 To list.Size - 1 Dim line As String = list.Get(i) Dim parts() As String = Regex.Split(",", line) fence_maker.add_points(parts(0),parts(1)) Next End Sub
fence_maker.way(maxlineheight)
maxlineheight is a percentage. 100% means the largest distance resulting from the boundary values of the uploaded points. This is good if you want to surround the entire shape with the largest possible fence. However, then the indentations will be small and it will not follow the edges very well. If you take this value smaller, the triangle formation becomes more and more detailed, it follows the edges more and more.
You have to find what is right for you.
Once that's done, just read it. Since you only need the outer fence, its index is 0.
fence_maker.length(0) 'number of points, the fence, in order
fence_maker.fence(0,n) 'point index
show your csv file
Have double-checked and the Concaveman code does indeed run faster.Thanks, it does work very well.
It seems though my posted code is faster as when I ran it with the posted .csv it took 134 milli-secs.
I will double check this as sometimes it seems to get accurate timings you need to run code as ResumableSub.
Will have a look at this.
RBS
I'm glad that the problem was finally solved!Have double-checked and the Concaveman code does indeed run faster.
Attached a backup file of the project comparing the 2 approaches.
I haven't looked at the fence shape of the Concaveman code as shown on a bitmap, but it looks fine
in my OSM map.
RBS
Does this bug happen with the Concaveman code?I'm glad that the problem was finally solved!
You may have read it, but it may have escaped your attention. You are using my first solution. Fellow forum member William Lancee, however, found a bug. If the shape is too concave, no fence is formed in the central direction.
If your project will not encounter such a shape, then this is a good solution. I wanted to eliminate this error, so I tried the other approach, which is slower (more calculations), but works better. The disadvantage of my latter solution is that if the points are not homogeneously distributed, the "maxlineheight" must be set higher in order not to divide it into several areas.
Anyway, I'm happy about it, the main thing is that it worked!
yes it happens because the two codes are the same ! Try it yourself and give it a strongly concave shape, such as a "C" shape.Does this bug happen with the Concaveman code?
If not then that might be the best approach.
RBS
yes it happens because the two codes are the same ! Try it yourself and give it a strongly concave shape, such as a "C" shape.
To avoid this error, I created the second version.
OK, will check that.yes it happens because the two codes are the same ! Try it yourself and give it a strongly concave shape, such as a "C" shape.
To avoid this error, I created the second version.
I don't understand this. This is very interesting. You posted "concaveman.bas".OK, will check that.
Never realised that your code (one of your posted codes) was the same as the code as in the quoted concaveman link:
!GitHub - mapbox/concaveman: A very fast 2D concave hull algorithm in JavaScript
A very fast 2D concave hull algorithm in JavaScript - mapbox/concavemangithub.com
RBS
Have checked this now (in my OSM map app, actually it is a medical app) and indeed the C-shaped collection of points causes the bug you described, where the indentation is missed.OK, will check that.
Never realised that your code (one of your posted codes) was the same as the code as in the quoted concaveman link:
!GitHub - mapbox/concaveman: A very fast 2D concave hull algorithm in JavaScript
A very fast 2D concave hull algorithm in JavaScript - mapbox/concavemangithub.com
RBS
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?
We use cookies and similar technologies for the following purposes:
Do you accept cookies and these technologies?