Here is an implementation of Graham Scan algorithm for computing the convex hull of a finite set of points:
data Point = Point {
xcoord :: Double,
ycoord :: Double
} deriving (Show)
data Direction = ToRight | Straight | ToLeft deriving (Show, Eq)
distance :: Point -> Point -> Double
distance a b = sqrt (((xcoord a) - (xcoord b))^2 + ((ycoord a) - (ycoord b))^2)
-- The direction function can be simplified after some algebraic analysis:
direction :: Point -> Point -> Point -> Direction
direction a b c
| s < 0 = ToRight
| s == 0 = Straight
| s > 0 = ToLeft
where
s = ((xcoord c) - (xcoord a)) * ((ycoord a) - (ycoord b)) + ((ycoord c) - (ycoord a)) * ((xcoord b) - (xcoord a))
directions_ :: [Point] -> [Direction]
directions_ [] = []
directions_ [_] = []
directions_ [_, _] = []
directions_ (a:b:c:xs) = (direction a b c) : (directions_ (b:c:xs))
directions :: [Point] -> [Direction]
directions xs = ToLeft : ToLeft : (directions_ xs)
lowestYPoint :: [Point] -> Point
lowestYPoint [x] = x
lowestYPoint (x:xs)
| (ycoord x) <= (ycoord lowestRest) = x
| otherwise = lowestRest
where
lowestRest = lowestYPoint xs
cosineAB :: Point -> Point -> Double
cosineAB a b = ((xcoord b) - (xcoord a)) / (distance a b)
sortByAngle :: Point -> [Point] -> [Point]
sortByAngle x xs = sortBy comparePoints xs where
comparePoints :: Point -> Point -> Ordering
comparePoints a b
| cA > cB = LT
| cA < cB = GT
| otherwise = case () of
_ | xA < xB -> LT
| xA > xB -> GT
| xA == xB -> case () of
_ | yA < yB -> LT
| yA > yB -> GT
| yA == yB -> EQ
where
yA = (ycoord a)
yB = (ycoord b)
xA = (xcoord a)
xB = (xcoord b)
cA = (cosineAB x a)
cB = (cosineAB x b)
grahamScanDirections :: [Direction] -> [Bool]
grahamScanDirections [] = []
grahamScanDirections [_] = [True]
grahamScanDirections (ToLeft:ToRight:xs) = False : (grahamScanDirections (ToLeft:xs))
grahamScanDirections (ToLeft:ToLeft:xs) = True : (grahamScanDirections (ToLeft:xs))
grahamScanDirections (ToLeft:Straight:xs) = False : (grahamScanDirections (ToLeft:xs))
grahamScanDirections _ = error "Impossible scenario"
filterByList :: [Bool] -> [a] -> [a]
filterByList [] _ = []
filterByList _ [] = []
filterByList (x:xs) (y:ys)
| x = y : (filterByList xs ys)
| otherwise = (filterByList xs ys)
grahamScan :: [Point] -> [Point]
grahamScan ps = filterByList bool_list ps1 where
-- sort ps by their angles (or cosine values) formed with the lowest point
ps1 = sortByAngle (lowestYPoint ps) ps
bool_list = grahamScanDirections $ directions ps1
Test:
*Main> let ps = [(Point 1 0), (Point 1 2), (Point 0 3), (Point 2 3), (Point 2 2), (Point 3 2), (Point 2 1)]
*Main> grahamScan ps
[Point {xcoord = 1.0, ycoord = 0.0},Point {xcoord = 3.0, ycoord = 2.0},Point {xcoord = 2.0, ycoord = 3.0},Point {xcoord = 0.0, ycoord = 3.0}]
Any suggestion about correctness/performance or any other possible improvement is appreciated.