packages feed

splot-0.1.0: Tools/StatePlot.hs

module Main where

import Control.Monad.Writer
import Control.Monad.State
import qualified Data.Map as M
import Data.List
import Data.Ord
import Data.Function
import Debug.Trace
import Data.Maybe
import Data.Time
import Data.Time.Parse
import System (getArgs)
import qualified Data.ByteString.Char8 as B
import Graphics.Rendering.Chart
import Graphics.Rendering.Chart.Renderable
import Graphics.Rendering.Chart.Gtk
import qualified Graphics.Rendering.Cairo as C
import Data.Colour
import Data.Colour.Names

data Event = Event {time :: LocalTime, track :: String, edge :: Edge} deriving (Show)
data Edge = Begin { color :: String } | End deriving (Show)

getArg :: String -> String -> [String] -> String
getArg name def args = case [(k,v) | (k,v) <- zip args (tail args), k==("-"++name)] of
  (_,v):_ -> v
  _       -> def

parse :: (B.ByteString -> (LocalTime, B.ByteString)) -> B.ByteString -> Event
parse parseTime s = Event { time = ts, track = B.unpack $ B.tail track', edge = edge }
  where
    (ts, s') = parseTime s
    (track', color) = B.break (==' ') (B.tail s')
    edge = case (B.head track') of
      '>' -> Begin (B.unpack (B.tail color))
      '<' -> End

diffToMillis :: LocalTime -> LocalTime -> Double
diffToMillis t2 t1 = fromIntegral (truncate (1000000*d)) / 1000
 where d = diffUTCTime (localTimeToUTC utc t2) (localTimeToUTC utc t1)

renderEvents :: Double -> Double -> [Event] -> Renderable ()
renderEvents bandHeight ticksIntervalMs es = Renderable {minsize = return (0,0), render = render'}
  where 
    events = sortBy (comparing time) es
    minTime = time $ head events
    maxTime = time $ last events
    time2ms t = diffToMillis t minTime
    rangeMs = time2ms maxTime
    ticks = takeWhile (<rangeMs) $ iterate (+ticksIntervalMs) 0
    tracks = sortBy (comparing (time . head)) . groupBy ((==) `on` track) . sortBy (comparing track) $ events

    maybeM :: (Monad m) => (a -> m b) -> Maybe a -> m ()
    maybeM f Nothing  = return ()
    maybeM f (Just x) = f x >> return ()

    bars track = execWriter $ evalStateT (mapM step track) Nothing
      where
        step (Event t _ (Begin c)) = do
          get >>= maybeM (\(t0,c0) -> tell [(time2ms t0, time2ms t, c0)])
          put (Just (t,c))
        step (Event t _ End      ) = do
          get >>= maybeM (\(t0,c0) -> tell [(time2ms t0, time2ms t, c0)])
          put Nothing

    render' (w,h) = do
      let ms2x ms = 10 + ms / rangeMs * (w - 10)
      let time2x t = ms2x (time2ms t)
      let numTracks = length tracks
      let yStep = (h-10) / fromIntegral (numTracks+1)
      let track2y i = fromIntegral (i+1) * yStep - bandHeight/2
      let drawTick ms = do {
          setLineStyle $ solidLine 1 (opaque black)
        ; moveTo $ Point (ms2x ms) (h-10)
        ; lineTo $ Point (ms2x ms) (h-5)
        ; c $ C.stroke
        }
      let drawBar i (ms1, ms2, color) = do {
          setLineStyle $ solidLine 1 transparent
        ; setFillStyle $ solidFillStyle $ opaque $ fromMaybe (error "unknown color") (readColourName color)
        ; fillPath (rectPath $ Rect (Point (ms2x ms1) (track2y i)) (Point (ms2x ms2) (track2y i + bandHeight)))
        }
      let drawTrack (i, es) = mapM_ (drawBar i) (bars es)

      setFillStyle $ solidFillStyle (opaque white)
      fillPath $ rectPath $ Rect (Point 0 0) (Point w h)
      setLineStyle $ solidLine 1 (opaque black)
      moveTo (Point 10 (h-10))
      lineTo (Point w  (h-10))
      c $ C.stroke
      moveTo (Point 10 (h-10))
      lineTo (Point 10 0)
      c $ C.stroke
      mapM_ drawTick ticks
      mapM_ drawTrack $ zip [0..] tracks
      return nullPickFn

main = do
  args <- getArgs
  let (w,h) = (read $ getArg "w" "640" args, read $ getArg "h" "480" args)
  let bandHeight = read $ getArg "bh" "5" args
  let ticksIntervalMs = read $ getArg "tickInterval" "10" args
  let timeFormat = getArg "tf" "%Y-%m-%d %H:%M:%OS" args
  let parseTime = fromMaybe (error "Invalid time") . strptime (B.pack timeFormat)
  let outPNG = getArg "o" "" args
  input <- B.getContents
  let events = parse parseTime `map` B.lines input
  let pic = renderEvents bandHeight ticksIntervalMs events
  case outPNG of
    "" -> renderableToWindow pic w h
    f  -> const () `fmap` renderableToPNGFile pic w h outPNG