packages feed

tsp-viz-0.1.0.0: TSPViz.hs

{-# LANGUAGE ViewPatterns #-}

-- | Program providing real time visualization of TSP tours.
--   This module is compiled to a separate binary and then
--   used through an interface exposed over some input stream.
--   Actions are parsed using a separate thread which updates
--   a shared graph structure, which is then rendered.
--
--   Settings of interest are the 'vertexSize' and 'maxBend'
--   properties, which should be scaled based on the input instance.
--
--   The number of tours is currently restricted to 64, based on the
--   internal hashtable used for lookup of overlapping tours.
--
--   In order to use this as an external program, you will need to
--   retrieve the appropiate interface from the linked git repo.
--
module Main where

import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Monad
import Data.Bits (clearBit, setBit, complement, popCount, (.&.), shiftR)
import Data.HashTable
import Data.Int (Int32)
import Data.List (stripPrefix)
import Data.Maybe (fromMaybe)
import qualified Data.Vector as V
import Data.Word (Word64)
import Graphics.Gloss
import Graphics.Gloss.Data.Vector
import Graphics.Gloss.Geometry
import Graphics.Gloss.Interface.IO.Animate
import Prelude hiding (lookup)
import System.IO

-- | Vertex type, coordinates in R^2.
type Vertex = (Float, Float)
--
-- | Edge type, identifying two vertices.
type Edge = (Int, Int)

-- | Tour containing a list of edges.
type Tour = [Edge]

-- | Identifier of a tour. Limits internal hashtable to 64 tours.
type TourId = Word64

-- | Graph representation.
data Graph = Graph (V.Vector Vertex) (V.Vector Tour)

-- | Action parsed from input stream.
data Action
  -- | Add a vertex to the graph.
  = AddVertex Vertex
  -- | Add an edge to the graph, belonging to a specific tour.
  | AddEdge TourId Edge
  -- | Remove an edge from the graph, belonging to a specific tour.
  | RemoveEdge TourId Edge
  deriving Show

-- | Type of shared state between render part and input stream parser.
type SharedGraph = TVar (Graph, HashTable Edge Word64)

-- | API identifier used as prefix in input stream, parsed as the first word, with other content being ignored.
apiID :: String
apiID = "VIZ"

-- | Radius of a circle representing a vertex.
vertexSize :: Float
vertexSize = 1.0

-- | Radius of arc being rendered when overalpping edges occur. Lower values results in tightly coupled edges.
maxBend :: Float
maxBend = 0.5

-- | Available colors used in rendering of edges, one color for each tour.
colors :: [Color]
colors = [blue, red, green, yellow, cyan, magenta, rose, violet, azure,
          aquamarine, chartreuse, orange]

-- | Renders a graph using the current graph representation and hashtable indicating overlapping edges.
renderGraph :: Graph -> HashTable Edge Word64 -> IO [Picture]
renderGraph g@(Graph v tours) table = do
  edges <- (join . V.toList) <$> V.mapM (renderTour g table) tourList
  return $ vertices ++ edges
  where
  vertices = map (\(x,y) -> translate x y $ circleSolid vertexSize) $ V.toList v
  tourList =  V.zip tours $ V.enumFromN 0 tourCount
  tourCount = V.length tours

-- | Renders a tour.
renderTour :: Graph -> HashTable Edge Word64 -> (Tour, TourId) -> IO [Picture]
renderTour (Graph v _) table (e, num) = mapM processEdge e
  where
  processEdge (v1, v2) = do
    Just usage <- lookup table (v1, v2)
    let size = popCount usage -- gives number of edges
    let masked = usage .&. shiftR (complement 0) (64 - fromIntegral num)
    let curr = popCount masked -- gives count of current edge
    return . color (edgeColor num) $ drawEdge (v V.! v1) (v V.! v2) size curr

-- | Selects a color based on the tour identifier.
edgeColor :: TourId -> Color
edgeColor num = colors !! (fromIntegral num `mod` length colors)

-- | Draws an edge of a tour.
drawEdge :: Vertex -> Vertex -> Int -> Int -> Picture
drawEdge p1 p2 num curr = if odd num && curr == num - 1 then Line [p1, p2] else mapped
  where
  mapped = positionEdge $ bendEdge num curr

  positionEdge = uncurry translate p1 . rotate angle . translate len 0 . scale len 1
  angle = -(radToDeg $ argV p3)
  p3 = sub p2 p1
  len = magV p3 / 2
  sub (a1, a2) (b1, b2) = (a1 - b1, a2 - b2)

-- | Generates a bent arc used when overlapping edges occur.
bendEdge :: Int -> Int -> Picture
bendEdge num edge = flip . scale 1 yscale $ arc 0 180 1
  where
  flip | odd edge = id
       | otherwise = scale 1 (-1)
  yscale = maxBend - realToFrac edge / (realToFrac num / maxBend)

-- | Frame stepping function used in render.
frame :: SharedGraph -> Float -> IO Picture
frame shared _ = do
  (g, table) <- atomically $ readTVar shared
  Pictures <$> renderGraph g table

-- | Launches the actual visual application thread.
launch :: SharedGraph -> IO ()
launch shared = animateIO disp white (frame shared)
  where
  disp = InWindow "TSP tour visualization" (1000, 1000) (0, 0)

-- | Hashes an edge into representation used by the hash table.
--   Edges are stored for future lookup when detecting overlapping edges between tours.
--   Assumes ordered vertices, with min(v1, v2) being supplied as the first argument.
hashEdge :: Edge -> Int32
hashEdge (v1, v2) = hashInt v1 * hashInt v2

-- | Identifies the initial API ID prefix and consumes the rest of the line.
parseAction :: String -> Maybe Action
parseAction (stripPrefix (apiID ++ " ") -> Just t) = parse $ words t
parseAction _ = Nothing

-- | Parses a tuple of numerical values.
parseVertices :: (Read a, Ord a) => [String] -> (a, a)
parseVertices [a,b] = (min a' b', max a' b')
  where
  a' = read a
  b' = read b

-- | Parses an action from the tokenized input stream.
parse :: [String] -> Maybe Action
parse ("addv" : points) = Just . AddVertex $ parseVertices points
parse ("adde" : tour : t) = Just . AddEdge (read tour) $ parseVertices t
parse ("del" : tour : t) = Just . RemoveEdge (read tour) $ parseVertices t
parse _ = Nothing

-- | Updates the graph representation based on some action, either add vertex, add edge, or remove edge.
updateGraph :: (Graph, HashTable Edge Word64) -> Action -> IO Graph
updateGraph (Graph v e, _) (AddVertex v') = return $ Graph (V.snoc v v') e

updateGraph (Graph v tours, hash) (AddEdge tour e') = do
  prev <- fromMaybe 0 <$> lookup hash e'
  let prev' = setBit prev (fromIntegral tour)
  update hash e' prev'
  return graph
  where graph = Graph v $ updateTour tours tour (e':)

updateGraph (graph@(Graph v tours), hash) (RemoveEdge tour e') = updateHash >> return graph'
  where
  updateHash = do
    prev <- fromMaybe 0 <$> lookup hash e'
    when (prev /= 0) $ do
      let updated = clearBit prev (fromIntegral tour)
      if updated == 0 then delete hash e' else void $ update hash e' updated

  graph' = Graph v $ updateTour tours tour $ filter (/= e')

-- | Updates the tour representation by taking the current vector length into account.
updateTour :: V.Vector Tour -> TourId -> (Tour -> Tour) -> V.Vector Tour
updateTour curr num f | V.length curr >= num' + 1 = V.update curr $
                        V.singleton (num', f $ curr V.! num')
                      | otherwise = V.snoc curr $ f []
  where num' = fromIntegral num

-- | Thread consuming stream input and acting on the specified actions.
--   State shared with render thread is synchronized using a TVar primitive.
parseStream :: SharedGraph -> IO ()
parseStream shared = do
  action <- parseAction <$> getLine
  case action of
    Nothing -> hPutStrLn stderr "Failed to parse command"
    Just action' -> do
      res <- atomically $ readTVar shared
      g' <- updateGraph res action'
      atomically $ writeTVar shared (g', snd res)

  parseStream shared

-- | Main function launching the separate parser and render threads.
main :: IO ()
main = do
  hashTable <- new (==) hashEdge
  shared <- atomically $ newTVar (Graph V.empty V.empty, hashTable)
  _ <- forkIO $ parseStream shared
  launch shared