{-# 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 options 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