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
}
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"
]
chartCanvas <- Gtk.drawingAreaNew
void $ Gtk.widgetSetSizeRequest chartCanvas 80 80
latestOneToRenderMVar <- CC.newEmptyMVar :: IO (MVar (RectSize -> Render (), (Int, Int)))
latestSurfaceMVar <- CC.newMVar Nothing :: IO (MVar (Maybe (Surface, (Int, Int))))
void $ CC.forkIO (renderWorker latestOneToRenderMVar latestSurfaceMVar chartCanvas options)
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 options largestRangeMVar redraw
let handleDraw :: Render ()
handleDraw = do
debug "handleDraw: called"
Gtk.Rectangle _ _ width height <- liftIO $ Gtk.widgetGetAllocation chartCanvas
maybeLatestSurface <- liftIO $ CC.readMVar latestSurfaceMVar
needFirstDrawOrResizeDraw <- case maybeLatestSurface of
Just (latestSurface, (lastWidth, lastHeight)) -> do
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
needRedraw <- liftIO $ CC.swapMVar needRedrawMVar False
when (needRedraw || needFirstDrawOrResizeDraw) $ liftIO $ do
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 ()
let stageDataFromElement :: forall a . Element' a -> IO ()
stageDataFromElement element = do
let msgStore = eMsgStore element
mdatalog <- CC.takeMVar msgStore
case mdatalog of
Nothing -> CC.putMVar msgStore mdatalog
Just (datalog, msignalTree) -> do
case msignalTree of
Nothing -> return ()
Just newSignalTree -> case signalSelector of
SignalSelector {ssRebuildSignalTree = rebuildSignalTree} ->
rebuildSignalTree element newSignalTree
writeIORef (ePlotValueRef element) datalog
CC.putMVar msgStore (Just (datalog, Nothing))
mapM_ (\(Element e) -> stageDataFromElement e) elements
(mtitle, namedPlotPoints) <- ssToPlotValues signalSelector
debug "handleDraw: got title and plot points"
let
updateRanges :: XY (Double, Double) -> XY (Double, Double)
updateRanges oldRanges =
foldl' largestRange oldRanges (concatMap (concat . snd) namedPlotPoints)
newRanges <- modifyMVar' largestRangeMVar updateRanges
axes <- owGetAxes optionsWidget
let render :: RectSize -> Render ()
render = toChartRender axes newRanges mtitle namedPlotPoints
void $ CC.tryTakeMVar latestOneToRenderMVar
CC.putMVar latestOneToRenderMVar (render, (width, height))
void $ on chartCanvas Gtk.draw handleDraw
optionsExpander <- Gtk.expanderNew "opt"
Gtk.set optionsExpander
[ Gtk.containerChild := owVBox optionsWidget
, Gtk.expanderExpanded := False
]
treeviewScroll <- Gtk.scrolledWindowNew Nothing Nothing
Gtk.set treeviewScroll [Gtk.widgetVExpand := True]
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
]
vboxOptionsAndSignals <- Gtk.vBoxNew False 4
Gtk.set vboxOptionsAndSignals
[ Gtk.containerChild := optionsExpander
, Gtk.boxChildPacking optionsExpander := Gtk.PackNatural
, Gtk.containerChild := treeviewExpander
, Gtk.boxChildPacking treeviewExpander := Gtk.PackGrow
]
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 ]
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
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
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"
(render, (width, height)) <- CC.takeMVar latestOneToRenderMVar
renderStartTime <- getCurrentTime
debug "renderWorker: starting render"
surface <- liftIO $ Cairo.createImageSurface Cairo.FormatARGB32 width height
Cairo.renderWith surface (render (realToFrac width, realToFrac height))
debug "renderWorker: putting finished surface"
void $ CC.swapMVar latestSurfaceMVar (Just (surface, (width, height)))
debug "renderWorker: queuing draw"
Gtk.postGUIAsync (Gtk.widgetQueueDraw chartCanvas)
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))
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)
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