Plot-ho-matic-0.10.0.0: src/PlotHo/GraphWidget.hs
{-# OPTIONS_GHC -Wall #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PackageImports #-}
module PlotHo.GraphWidget
( newGraph
) where
import Control.Concurrent ( MVar )
import qualified Control.Concurrent as CC
import Control.Monad ( forever, void, when, zipWithM )
import Control.Monad.IO.Class ( liftIO )
import Data.IORef ( newIORef, writeIORef )
import Data.List ( foldl' )
import qualified Data.Map.Strict as M
import Data.Time.Clock ( getCurrentTime, diffUTCTime )
import Graphics.Rendering.Cairo ( Render, Surface )
import qualified Graphics.Rendering.Cairo as Cairo
import "gtk3" Graphics.UI.Gtk ( AttrOp( (:=) ) )
import qualified "gtk3" Graphics.UI.Gtk as Gtk
import System.Glib.Signals ( on )
import Text.Printf ( printf )
import Graphics.Rendering.Chart ( RectSize )
import PlotHo.ChartRender ( toChartRender )
import PlotHo.OptionsWidget ( OptionsWidget(..), makeOptionsWidget )
import PlotHo.PlotTypes
import PlotHo.SignalSelector ( SignalSelector(..), newSignalSelectorArea )
toElement' :: Int -> Channel' a -> IO (Element' a)
toElement' index channel = do
mlatestValue <- CC.readMVar (chanLatestValueMVar channel)
let latestValue = case mlatestValue of
Nothing -> Nothing
Just (val, signalTree) -> Just (val, Just signalTree)
msgStore <- CC.newMVar latestValue
plotValueRef <- newIORef $
error $ unlines
[ "The impossible happened."
, "Element plot value reference is initially undefined until a signal tree and data come in."
, "There is no getter. How was this accessed?"
]
return
Element'
{ eChannel = channel
, eMsgStore = msgStore
, eIndex = index
, ePlotValueRef = plotValueRef
}
-- make a new graph window
newGraph :: PlotterOptions -> [Channel] -> IO Gtk.Window
newGraph options channels = do
win <- Gtk.windowNew
elements <- zipWithM (\k (Channel c) -> Element <$> toElement' k c) [0..] channels
void $ Gtk.set win
[ Gtk.containerBorderWidth := 8
, Gtk.windowTitle := "plot-ho-graphic"
]
-- chart drawing area
chartCanvas <- Gtk.drawingAreaNew
void $ Gtk.widgetSetSizeRequest chartCanvas 80 80
-- mvars for drawing thread inputs/outputs
latestOneToRenderMVar <- CC.newEmptyMVar :: IO (MVar (RectSize -> Render (), (Int, Int)))
latestSurfaceMVar <- CC.newMVar Nothing :: IO (MVar (Maybe (Surface, (Int, Int))))
-- fork the thread which continuously draws
void $ CC.forkIO (renderWorker latestOneToRenderMVar latestSurfaceMVar chartCanvas options)
-- Flag which marks if someone has called for a redraw.
-- We have the MVar in addition to the GTK signal so that if multiple sources
-- request a redraw and multiple signals are in the queue, we can draw once and then
-- ignore the rest of the signals..
needRedrawMVar <- CC.newMVar False
let redraw :: IO ()
redraw = do
debug "redraw called"
void $ CC.swapMVar needRedrawMVar True
Gtk.postGUIAsync (Gtk.widgetQueueDraw chartCanvas)
signalSelector <- newSignalSelectorArea elements redraw
largestRangeMVar <- CC.newMVar (XY defaultHistoryRange defaultHistoryRange)
optionsWidget <- makeOptionsWidget largestRangeMVar redraw
let handleDraw :: Render ()
handleDraw = do
debug "handleDraw: called"
-- get the size of the surface we have to draw
Gtk.Rectangle _ _ width height <- liftIO $ Gtk.widgetGetAllocation chartCanvas
-- handleDraw always immediately takes the last rendered surface and draws it
-- this is just a copy and very efficient
maybeLatestSurface <- liftIO $ CC.readMVar latestSurfaceMVar
needFirstDrawOrResizeDraw <- case maybeLatestSurface of
Just (latestSurface, (lastWidth, lastHeight)) -> do
-- TODO(greg): Should we be drawing if the width/height don't match?
-- I wonder if this could cause a buffer overrun.
debug "handleDraw: painting latest surface"
Cairo.setSourceSurface latestSurface 0 0
Cairo.paint
return ((lastWidth, lastHeight) /= (width, height))
Nothing -> do
debug "handleDraw: no surface yet"
return True
-- then we determine if we need to re-generate a new surface
needRedraw <- liftIO $ CC.swapMVar needRedrawMVar False
when (needRedraw || needFirstDrawOrResizeDraw) $ liftIO $ do
-- if we need to redraw for whatever reason
case (needRedraw, needFirstDrawOrResizeDraw) of
(True, True) -> debug $ "handleDraw: putting a redraw in because " ++
"needRedraw && needFirstDrawOrResizeDraw"
(True, False) -> debug $ "handleDraw: putting a redraw in because " ++
"needRedraw"
(False, True) -> debug $ "handleDraw: putting a redraw in because " ++
"needFirstDrawOrResizeDraw"
_ -> return () -- (impossible)
-- Now we have to take the latest data from the channels and put it in the IORefs
-- so that the signal tree can apply the getters. Phew.
let stageDataFromElement :: forall a . Element' a -> IO ()
stageDataFromElement element = do
let msgStore = eMsgStore element
-- get the latest data, just block if they're not available
mdatalog <- CC.takeMVar msgStore
case mdatalog of
-- no data yet, do nothing
Nothing -> CC.putMVar msgStore mdatalog
Just (datalog, msignalTree) -> do
case msignalTree of
-- No new signal tree, no action necessary
Nothing -> return ()
-- If there is a new signal tree, we have to merge it with the old one.
Just newSignalTree -> case signalSelector of
SignalSelector {ssRebuildSignalTree = rebuildSignalTree} ->
rebuildSignalTree element newSignalTree
-- write the data to the IORef so that the getters get the right stuff
writeIORef (ePlotValueRef element) datalog
-- Put the data back. Put Nothing to signify that the signal tree is up to date.
CC.putMVar msgStore (Just (datalog, Nothing))
-- stage the values
mapM_ (\(Element e) -> stageDataFromElement e) elements
-- get the latest plot points
-- Now we have rebuild the signal tree if necessary, and staged the latest plot values
-- To the geter IORefs. It is safe to get the plot points.
(mtitle, namedPlotPoints) <- ssToPlotValues signalSelector
debug "handleDraw: got title and plot points"
let -- update the min/max plot ranges
updateRanges :: XY (Double, Double) -> XY (Double, Double)
updateRanges oldRanges =
foldl' largestRange oldRanges (concatMap (concat . snd) namedPlotPoints)
newRanges <- modifyMVar' largestRangeMVar updateRanges
axes <- owGetAxes optionsWidget
-- prepare the next render
let render :: RectSize -> Render ()
render = toChartRender axes newRanges mtitle namedPlotPoints
-- Empty the mvar if it is full.
-- If we are getting lots of messages quickly this
-- will descard any undrawn requests.
void $ CC.tryTakeMVar latestOneToRenderMVar
-- Put the latest request in the draw thread's queue
-- The MVar is now definitely empty so we will never block
-- by putting something in it.
CC.putMVar latestOneToRenderMVar (render, (width, height))
-- connect the draw signal to our draw handler
void $ on chartCanvas Gtk.draw handleDraw
-- the options widget
optionsExpander <- Gtk.expanderNew "opt"
Gtk.set optionsExpander
[ Gtk.containerChild := owVBox optionsWidget
, Gtk.expanderExpanded := False
]
-- the signal selector
treeviewScroll <- Gtk.scrolledWindowNew Nothing Nothing
Gtk.set treeviewScroll [Gtk.widgetVExpand := True] -- make sure it expands vertically
Gtk.containerAdd treeviewScroll (ssTreeView signalSelector)
Gtk.set treeviewScroll
[ Gtk.scrolledWindowHscrollbarPolicy := Gtk.PolicyNever
, Gtk.scrolledWindowVscrollbarPolicy := Gtk.PolicyAutomatic
]
treeviewExpander <- Gtk.expanderNew "sig"
Gtk.set treeviewExpander
[ Gtk.containerChild := treeviewScroll
, Gtk.expanderExpanded := True
]
-- options and signal selector packed in vbox
vboxOptionsAndSignals <- Gtk.vBoxNew False 4
Gtk.set vboxOptionsAndSignals
[ Gtk.containerChild := optionsExpander
, Gtk.boxChildPacking optionsExpander := Gtk.PackNatural
, Gtk.containerChild := treeviewExpander
, Gtk.boxChildPacking treeviewExpander := Gtk.PackGrow
]
-- hbox to hold eveything
hboxEverything <- Gtk.hBoxNew False 4
Gtk.set hboxEverything
[ Gtk.containerChild := vboxOptionsAndSignals
, Gtk.boxChildPacking vboxOptionsAndSignals := Gtk.PackNatural
, Gtk.containerChild := chartCanvas
, Gtk.boxChildPacking chartCanvas := Gtk.PackGrow
]
void $ Gtk.set win
[ Gtk.containerChild := hboxEverything ]
-- add this window to the set of windows that get redraw signals on new messages
let registerElement :: Element' a -> IO ()
registerElement element = do
let graphComms =
GraphComms
{ gcRedrawSignal = redraw
, gcMsgStore = eMsgStore element
}
CC.modifyMVar_ (chanGraphCommsMap (eChannel element)) (return . M.insert win graphComms)
mapM_ (\(Element e) -> registerElement e) elements
-- when the window is closed, remove it from the set which get redraw signals on new messages
void $ on win Gtk.deleteEvent $ do
debug "removing window from redrawSignalMap"
let removeElement :: Element' a -> IO ()
removeElement element = do
CC.modifyMVar_ (chanGraphCommsMap (eChannel element)) (return . M.delete win)
liftIO $ mapM_ (\(Element e) -> removeElement e) elements
return False
-- show the window and return
Gtk.widgetShowAll win
return win
renderWorker
:: MVar (RectSize -> Render (), (Int, Int))
-> MVar (Maybe (Surface, (Int, Int)))
-> Gtk.DrawingArea
-> PlotterOptions
-> IO ()
renderWorker latestOneToRenderMVar latestSurfaceMVar chartCanvas options = forever $ do
debug "renderWorker: waiting for new render"
-- block until we have to render something
(render, (width, height)) <- CC.takeMVar latestOneToRenderMVar
renderStartTime <- getCurrentTime
debug "renderWorker: starting render"
-- create an image to draw on
surface <- liftIO $ Cairo.createImageSurface Cairo.FormatARGB32 width height
-- do the drawing
Cairo.renderWith surface (render (realToFrac width, realToFrac height))
-- put our new drawing in the latest surface variable
debug "renderWorker: putting finished surface"
void $ CC.swapMVar latestSurfaceMVar (Just (surface, (width, height)))
-- queue another draw
debug "renderWorker: queuing draw"
Gtk.postGUIAsync (Gtk.widgetQueueDraw chartCanvas)
-- At this point the render worker would immediately start the next render if one was available.
-- This could cause us to draw at an unneccesarily high rate which would could
-- overload the system. So we only draw at maximum rate given by 'maxDrawRate'.
-- If we are already slower than 'maxDrawRate' we don't sleep,
-- we just update as quickly as possible.
renderFinishTime <- getCurrentTime
let renderTime :: Double
renderTime = realToFrac $ diffUTCTime renderFinishTime renderStartTime
sleepTime = 1 / maxDrawRate options - renderTime
debug $ printf "sleep time: %.2g\n" sleepTime
when (sleepTime > 0) $
CC.threadDelay (round (1e6 * sleepTime))
-- evaluate
forceRange :: XY (Double, Double) -> XY (Double, Double)
forceRange (XY (minX, maxX) (minY, maxY)) =
minX `seq` maxX `seq` minY `seq` maxY `seq`
(XY (minX, maxX) (minY, maxY))
largestRange :: XY (Double, Double) -> (Double, Double) -> XY (Double, Double)
largestRange (XY (minX, maxX) (minY, maxY)) (x, y) =
forceRange $ XY (min minX x, max maxX x) (min minY y, max maxY y)
-- same behavior as 'Control.Concurrent.modifyMVar' with a different interface
modifyMVar' :: forall a . MVar a -> (a -> a) -> IO a
modifyMVar' mvar f = CC.modifyMVar mvar g
where
g :: a -> IO (a, a)
g x = return (y, y)
where
y = f x