ConvexHull.frink

Download or view ConvexHull.frink in plain text format


/** 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]]


Download or view ConvexHull.frink in plain text format


This is a program written in the programming language Frink.
For more information, view the Frink Documentation or see More Sample Frink Programs.

Alan Eliasen was born 19966 days, 17 hours, 58 minutes ago.