{-# LANGUAGE MultiParamTypeClasses #-} -- | -- Module : Simulation.Aivika.Experiment.Base.TableView -- Copyright : Copyright (c) 2012-2017, David Sorokin -- License : BSD3 -- Maintainer : David Sorokin -- Stability : experimental -- Tested with: GHC 8.0.1 -- -- The module defines 'TableView' that saves the simulation -- results in the CSV file(s). -- module Simulation.Aivika.Experiment.Base.TableView (TableView(..), defaultTableView) where import Control.Monad import Control.Monad.Trans import qualified Data.Map as M import Data.IORef import Data.Maybe import Data.Monoid import System.IO import System.FilePath import Simulation.Aivika import Simulation.Aivika.Experiment.Types import Simulation.Aivika.Experiment.Base.WebPageRenderer import Simulation.Aivika.Experiment.Base.FileRenderer import Simulation.Aivika.Experiment.Base.ExperimentWriter import Simulation.Aivika.Experiment.Base.HtmlWriter import Simulation.Aivika.Experiment.Utils (replace) -- | Defines the 'View' that saves the simulation results -- in the CSV file(s). data TableView = TableView { tableTitle :: String, -- ^ This is a title used in HTML. tableDescription :: String, -- ^ This is a description in the HTML. tableLinkText :: String, -- ^ It specifies the text for the link -- which is displayed in the HTML page -- if there is only one simulation run. -- The link downloads the corresponded -- CSV file in the browser. If there are -- more simulation runs, then this link -- is not shown. -- -- An example is -- -- @ -- tableLinkText = \"Download the CSV file\" -- @ tableRunLinkText :: String, -- ^ It specifies the link text which is -- displayed in the HTML page if there are -- many simulation runs. Such a link downloads -- the CSV file for the corresponded run. -- To define the text, you can use special -- variables @$LINK@, @$RUN_INDEX@ and @$RUN_COUNT@. -- -- An example is -- -- @ -- tableRunLinkText = \"$LINK / Run $RUN_INDEX of $RUN_COUNT\" -- @ -- -- If there is only one run, then the link of -- this kind is not displayed. Instead, only one -- link is shown, which text is defined by the -- 'tableLinkText' field. tableFileName :: ExperimentFilePath, -- ^ It defines the file name for each CSV file. -- It may include special variables @$TITLE@, -- @$RUN_INDEX@ and @$RUN_COUNT@. -- -- An example is -- -- @ -- tableFileName = UniqueFilePath \"$TITLE - $RUN_INDEX.csv\" -- @ tableSeparator :: String, -- ^ It defines the separator for the view. -- It delimits the cells in the rows of the CSV file. tableFormatter :: ShowS, -- ^ It defines the formatter which is applied -- to all values before they will be written -- in the CSV file(s). tablePredicate :: Event Bool, -- ^ It specifies the predicate that defines -- when we can save data in the table. tableTransform :: ResultTransform, -- ^ The transform applied to the results before receiving series. tableSeries :: ResultTransform, -- ^ It defines the series to save in the CSV file(s). tableSeriesGridSize :: Maybe Int -- ^ The size of the grid, where the series data are saved. } -- | The default table view. defaultTableView :: TableView defaultTableView = TableView { tableTitle = "Table", tableDescription = "This section contains the CSV file(s) with the simulation results.", tableLinkText = "Download the CSV file", tableRunLinkText = "$LINK / Run $RUN_INDEX of $RUN_COUNT", tableFileName = UniqueFilePath "Table($RUN_INDEX).csv", tableSeparator = ",", tableFormatter = id, tablePredicate = return True, tableTransform = expandResults, tableSeries = id, tableSeriesGridSize = Nothing } instance ExperimentView TableView (WebPageRenderer a) where outputView v = let reporter exp renderer dir = do st <- newTable v exp dir let context = WebPageContext $ WebPageWriter { reporterWriteTOCHtml = tableTOCHtml st, reporterWriteHtml = tableHtml st } return ExperimentReporter { reporterInitialise = return (), reporterFinalise = return (), reporterSimulate = simulateTable st, reporterContext = context } in ExperimentGenerator { generateReporter = reporter } instance ExperimentView TableView (FileRenderer a) where outputView v = let reporter exp renderer dir = do st <- newTable v exp dir return ExperimentReporter { reporterInitialise = return (), reporterFinalise = return (), reporterSimulate = simulateTable st, reporterContext = FileContext } in ExperimentGenerator { generateReporter = reporter } -- | The state of the view. data TableViewState = TableViewState { tableView :: TableView, tableExperiment :: Experiment, tableDir :: FilePath, tableMap :: M.Map Int FilePath } -- | Create a new state of the view. newTable :: TableView -> Experiment -> FilePath -> ExperimentWriter TableViewState newTable view exp dir = do let n = experimentRunCount exp fs <- forM [0..(n - 1)] $ \i -> resolveFilePath dir $ mapFilePath (flip replaceExtension ".csv") $ expandFilePath (tableFileName view) $ M.fromList [("$TITLE", tableTitle view), ("$RUN_INDEX", show $ i + 1), ("$RUN_COUNT", show n)] liftIO $ forM_ fs $ flip writeFile [] -- reserve the file names let m = M.fromList $ zip [0..(n - 1)] fs return TableViewState { tableView = view, tableExperiment = exp, tableDir = dir, tableMap = m } -- | Write the tables during the simulation. simulateTable :: TableViewState -> ExperimentData -> Composite () simulateTable st expdata = do let view = tableView st rs = tableSeries view $ tableTransform view $ experimentResults expdata loc = localisePathResultTitle $ experimentLocalisation $ tableExperiment st exts = resultsToStringValues rs signals = experimentPredefinedSignals expdata separator = tableSeparator view formatter = tableFormatter view predicate = tablePredicate view i <- liftParameter simulationIndex signal <- case tableSeriesGridSize view of Just m -> liftEvent $ fmap (mapSignal $ const ()) $ newSignalInTimeGrid m Nothing -> return $ pureResultSignal signals $ resultSignal rs -- create a new file let f = fromJust $ M.lookup (i - 1) (tableMap st) h <- liftIO $ openFile f WriteMode -- write a header liftIO $ do forM_ (zip [0..] exts) $ \(column, ext) -> do when (column > 0) $ hPutStr h separator hPutStr h $ show $ loc $ resultValueIdPath ext hPutStrLn h "" handleSignalComposite signal $ \t -> do p <- predicate when p $ do forM_ (zip [0..] exts) $ \(column, ext) -> -- write the row do x <- resultValueData ext -- write the column liftIO $ do when (column > 0) $ hPutStr h separator hPutStr h $ formatter x liftIO $ hPutStrLn h "" disposableComposite $ DisposableEvent $ liftIO $ do when (experimentVerbose $ tableExperiment st) $ putStr "Generated file " >> putStrLn f hClose h -- close the file -- | Get the HTML code. tableHtml :: TableViewState -> Int -> HtmlWriter () tableHtml st index = let n = experimentRunCount $ tableExperiment st in if n == 1 then tableHtmlSingle st index else tableHtmlMultiple st index -- | Get the HTML code for a single run. tableHtmlSingle :: TableViewState -> Int -> HtmlWriter () tableHtmlSingle st index = do header st index let f = fromJust $ M.lookup 0 (tableMap st) writeHtmlParagraph $ writeHtmlLink (makeRelative (tableDir st) f) $ writeHtmlText (tableLinkText $ tableView st) -- | Get the HTML code for multiple runs tableHtmlMultiple :: TableViewState -> Int -> HtmlWriter () tableHtmlMultiple st index = do header st index let n = experimentRunCount $ tableExperiment st forM_ [0..(n - 1)] $ \i -> do let f = fromJust $ M.lookup i (tableMap st) sublink = replace "$RUN_INDEX" (show $ i + 1) $ replace "$RUN_COUNT" (show n) $ replace "$LINK" (tableLinkText $ tableView st) (tableRunLinkText $ tableView st) writeHtmlParagraph $ writeHtmlLink (makeRelative (tableDir st) f) $ writeHtmlText sublink header :: TableViewState -> Int -> HtmlWriter () header st index = do writeHtmlHeader3WithId ("id" ++ show index) $ writeHtmlText (tableTitle $ tableView st) let description = tableDescription $ tableView st unless (null description) $ writeHtmlParagraph $ writeHtmlText description -- | Get the TOC item tableTOCHtml :: TableViewState -> Int -> HtmlWriter () tableTOCHtml st index = writeHtmlListItem $ writeHtmlLink ("#id" ++ show index) $ writeHtmlText (tableTitle $ tableView st)