/** This program calculates the convex hull of a list of points. It uses Graham's scan algorithm which is approximately O(n log n) with respect to the number of points. You wil generally call this with ConvexHull.hull[points] where points is an array of [x,y] points. These points can be obtained or passed to Frink's graphics polygon class: https://frinklang.org/#PolygonMethods To graph your solution, call: ConvexHull.plotHull[points].show[] where points is an array of [x,y] points. This somewhat follows the discussion at: http://www.geeksforgeeks.org/convex-hull-set-2-graham-scan/ It should be noted that the sample implementation at that site is broken in several ways. */ class ConvexHull { // This functions calculates the convex hull of an array of points specified // as [x,y] coordinates. It returns the result as an array of [x,y] points. // These can be obtained or passed to Frink's graphics polygon class: // https://frinklang.org/#PolygonMethods class hull[points] := { points = toArray[points] // Points can come in as a set or enumeration size = length[points] // Find the smallest y value, smallest right if there's a tie ymin = points@0@1 minIndex = 0 for i = 1 to size-1 { [x, y] = points@i if ((y < ymin) or (ymin == y and x < points@minIndex@0)) { ymin = y minIndex = i } } // Place the smallest y value at index 0 by swapping temp = points@0 points@0 = points@minIndex points@minIndex = temp // Sort items 1 through size-1 with respect to point 0. // A point p1 comes betwen p2 in sorted output if p2 has a larger polar // angle (in counterclockwise direction) than p1. // TODO: Implement sort with sub-region begin and end specified result = sort[rest[points], getFunction["compare", 3], points@0] result.pushFirst[points@0] points = result // If two or more points make the same angle with p0, remove all but // the farthest. The sort above made the farthest last in the list. m = 1 for i=1 to size-1 { // Keep removing element i while i and i+1 are colinear with p0 while i < size-1 and orientation[points@0, points@i, points@(i+1)] == 0 i = i + 1 points@m = points@i m = m + 1 // Update size of modified array } // If modified array has fewer than 3 points, no convex hull is possible if m < 3 return undef // Create an empty stack and push first three points onto it. // TODO: Should first[array] return an array? It currently returns an // EnumeratingExpression which led to confusion s = toArray[first[points, 3]] // Process remaining n-3 points for i = 2 to m-1 { // Keep removing top while the angle formed by points next-to-top, // top, and points@i makes a non-left turn. The first clause is // apparently necessary to fix a bug in the original // TODO: Factor out multiple calls to length[s] while length[s] >= 2 and orientation[s@(length[s]-2), s@(length[s]-1), points@i] != 2 s.pop[] s.push[points@i] } return s } // This is a comparison function that compares points with respect to the // first point p0. (Which is passed as an extra "data" argument to the sort // function) class compare[p1, p2, p0] := { o = orientation[p0, p1, p2] // If points are colinear, put nearest point first if o == 0 return -(distanceSquared[p0, p2] <=> distanceSquared[p0, p1]) return o==2 ? -1 : 1 } /** Returns the squared distance between two points. */ class distanceSquared[p1, p2] := { [x1,y1] = p1 [x2,y2] = p2 // TODO: This doesn't make sense with disparate units return (x1-x2)^2 + (y1-y2)^2 } /** Finds the orientation of the ordered triplet of points [p,q,r]. Returns: 0 if p, q, and r are colinear 1 if clockwise 2 if counterclockwise */ class orientation[p, q, r] := { [px, py] = p [qx, qy] = q [rx, ry] = r v = (qy - py) * (rx - qx) - (qx - px) * (ry - qy) units = px py if v == 0 units // This makes units come out right return 0 // Colinear else return v > 0 units ? 1 : 2 // Clockwise or counterclockwise } /** This is a function to calculate and plot a hull of points. It returns a graphics object which you can display or print with its show[] or print[] methods. */ class plotHull[points] := { g = new graphics for [x,y] = points g.fillEllipseCenter[x,y,.1, .1] hull = hull[points] ph = new polygon[hull] g.color[0,0,1] g.add[ph] g.color[1,0,0] // Draw the actually used hull points in red for [x,y] = hull g.fillEllipseCenter[x,y,.2, .2] return g } } /* // Testing stuff. p = [ [0,3], [1,1], [2,2], [4,4], [0,0], [1,2], [3,1], [3,3] ] ConvexHull.plotHull[p].show[] println[ConvexHull.hull[p]] // Sample from website above (in reverse order) p = [ [0,3], [1,1], [2,2], [4,4], [0,0], [1,2], [3,1], [3,3], [2,4] ] ConvexHull.plotHull[p].show[] println[ConvexHull.hull[p]] println["Colinear3"] p = [ [0,0], [1,1], [2,2]] ConvexHull.plotHull[p].show[] println[ConvexHull.hull[p]] println["Colinear4"] p = [ [0,0], [1,1], [2,2], [3,3]] ConvexHull.plotHull[p].show[] println[ConvexHull.hull[p]] mean = 5 sd = 1 d = new array for p = 1 to 2000 d.push[ [randomGaussian[mean, sd], randomGaussian[mean, sd]] ] g = ConvexHull.plotHull[d] g.show[] */ // Testing with units of measure. Not sure if this can be made to make sense //p2 = [ [0,3s], [1,1s], [2,2s], [4,4s], [0,0s], [1,2s], [3,1s], [3,3s], [2,4s] ] //println[ConvexHull.hull[p2]]