{-| Module : MasterPlan.Backend.Graph Description : a backend that renders to PNG diagram Copyright : (c) Rodrigo Setti, 2017 License : MIT Maintainer : rodrigosetti@gmail.com Stability : experimental Portability : POSIX -} {-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TupleSections #-} module MasterPlan.Backend.Graph (render, RenderOptions(..)) where import MasterPlan.Data import Diagrams.Prelude hiding (render, Product, Sum) import Diagrams.Backend.Rasterific import Data.List (intersperse) import Control.Applicative ((<|>)) import Control.Monad.State import qualified Data.Map as M import Data.Tree import Data.Maybe (fromMaybe, catMaybes) import qualified Data.List.NonEmpty as NE import Text.Printf (printf) import Diagrams.TwoD.Text (Text) -- text :: (TypeableFloat n, Renderable (Text n) b) => String -> QDiagram b V2 n Any -- text = texterific leftText :: (TypeableFloat n, Renderable (Text n) b) => String -> QDiagram b V2 n Any leftText = alignedText 0 0.5 rightText :: (TypeableFloat n, Renderable (Text n) b) => String -> QDiagram b V2 n Any rightText = alignedText 1 0.5 -- * Types data NodeType = SumNode | ProductNode | SequenceNode | AtomicNode -- |Data type used by the tree data PNode = PNode (Maybe ProjectKey) (Maybe ProjectProperties) Cost Trust Progress | NodeRef ProjectKey type RenderModel = Tree (NodeType, PNode) mkLeaf :: PNode -> RenderModel mkLeaf a = Node (AtomicNode, a) [] -- |Translates a ProjectSystem into a Tree PNode toRenderModel :: ProjectSystem -> ProjectKey -> State [ProjectKey] (Maybe RenderModel) toRenderModel sys rootK = case M.lookup rootK (bindings sys) of Nothing -> pure Nothing Just b -> Just <$> bindingToRM rootK b where bindingToRM :: ProjectKey -> Binding -> State [ProjectKey] RenderModel bindingToRM key (BindingExpr prop p) = projToRM p (Just key) (Just prop) bindingToRM key (BindingAtomic prop c t p) = pure $ mkLeaf $ PNode (Just key) (Just prop) c t p bindingToRM key (BindingPlaceholder prop) = pure $ mkLeaf $ PNode (Just key) (Just prop) defaultCost defaultTrust defaultProgress mkNode :: (PNode -> [RenderModel] -> RenderModel) -> ProjectExpr -> NE.NonEmpty ProjectExpr -> Maybe ProjectKey -> Maybe ProjectProperties -> State [ProjectKey] RenderModel mkNode f p ps key prop = f (PNode key prop (cost sys p) (trust sys p) (progress sys p)) <$> mapM (\p' -> projToRM p' Nothing Nothing) (NE.toList ps) projToRM :: ProjectExpr -> Maybe ProjectKey -> Maybe ProjectProperties -> State [ProjectKey] RenderModel projToRM p@(Sum ps) = mkNode (\x -> Node (SumNode, x)) p ps projToRM p@(Sequence ps) = mkNode (\x -> Node (SequenceNode, x)) p ps projToRM p@(Product ps) = mkNode (\x -> Node (ProductNode, x)) p ps projToRM (Reference n) = \k p -> case M.lookup n $ bindings sys of Nothing -> pure $ Node (AtomicNode, PNode k (p <|> pure defaultProjectProps {title=n}) defaultCost defaultTrust defaultProgress) [] Just b -> do alreadyProcessed <- gets (n `elem`) if alreadyProcessed then pure $ Node (AtomicNode, NodeRef $ bindingTitle b) [] else modify (n:) >> bindingToRM n b -- |how many children treeSize :: Tree a -> Double treeSize (Node _ []) = 1 treeSize (Node _ ts) = sum $ treeSize <$> ts -- |Options for rendering data RenderOptions = RenderOptions { colorByProgress :: Bool -- ^Whether to color boxes depending on progress , renderWidth :: Integer -- ^The width of the output image , renderHeight :: Integer -- ^The height of the output image , rootKey :: ProjectKey -- ^The name of the root project , whitelistedProps :: [ProjProperty] -- ^Properties that should be rendered } deriving (Eq, Show) -- | The main rendering function render ∷ FilePath -> RenderOptions-> ProjectSystem → IO () render fp (RenderOptions colorByP w h rootK props) sys = let noRootEroor = text $ "no project named \"" ++ rootK ++ "\" found." dia = fromMaybe noRootEroor $ renderTree colorByP props <$> evalState (toRenderModel sys rootK) [] in renderRasterific fp (dims2D (fromInteger w) (fromInteger h)) $ bgFrame 1 white $ centerXY dia renderTree :: Bool -> [ProjProperty] -> RenderModel -> QDiagram B V2 Double Any renderTree colorByP props (Node (_, n) []) = alignL $ renderNode colorByP props n renderTree colorByP props x@(Node (ty, n) ts@(t:_)) = (strutY (12 * treeSize x) <> alignL (centerY $ renderNode colorByP props n)) ||| (translateX 2 typeSymbol # withEnvelope (mempty :: D V2 Double) <> hrule 4 # lwO 2) ||| centerY (headBar === treeBar sizes) ||| centerY (vcat $ map renderSubTree ts) where sizes = map ((* 6) . treeSize) ts renderSubTree subtree = hrule 4 # lwO 2 ||| renderTree colorByP props subtree headBar = strutY $ treeSize t * 6 treeBar :: [Double] -> QDiagram B V2 Double Any treeBar (s1:s2:ss) = vrule s1 # lwO 2 === vrule s2 # lwO 2 === treeBar (s2:ss) treeBar [s1] = strutY s1 treeBar _ = mempty typeSymbol = let txt = case ty of SumNode -> text "+" ProductNode -> text "x" SequenceNode -> text "->" AtomicNode -> mempty in txt # fontSizeL 2 # bold <> circle 2 # fc white # lwO 1 renderNode :: Bool -> [ProjProperty] -> PNode -> QDiagram B V2 Double Any renderNode _ _ (NodeRef n) = text n <> roundedRect 30 12 0.5 # lwO 2 # fc white # dashingN [0.005, 0.005] 0 renderNode colorByP props (PNode _ prop c t p) = centerY nodeDia # withEnvelope (rect 30 12 :: D V2 Double) where nodeDia = let hSizeAndSections = catMaybes [ (,2) <$> headerSection , (,6) <$> descriptionSection , (,2) <$> urlSection , (,2) <$> bottomSection] sections = map (\s -> strutY (snd s) <> fst s) hSizeAndSections outerRect = rect 30 (sum $ map snd hSizeAndSections) # lwO 2 sectionsWithSep = vcat (intersperse (hrule 30 # dashingN [0.005, 0.005] 0 # lwO 1) sections) in outerRect # fcColor `beneath` centerY sectionsWithSep givenProp :: ProjProperty -> Maybe a -> Maybe a givenProp pro x = if pro `elem` props then x else Nothing headerSection = case [progressHeader, titleHeader, costHeader] of [Nothing, Nothing, Nothing] -> Nothing l -> Just $ strutX 30 <> mconcat (catMaybes l) progressHeader = givenProp PProgress $ Just $ displayProgress p # translateX (-14) titleHeader = givenProp PTitle $ (bold . text . title) <$> prop costHeader = givenProp PCost $ Just $ displayCost c # translateX 14 descriptionSection, urlSection, bottomSection :: Maybe (QDiagram B V2 Double Any) descriptionSection = givenProp PDescription $ prop >>= description >>= (pure . text) -- TODO line breaks urlSection = givenProp PUrl $ prop >>= url >>= (pure . text) -- TODO ellipsis bottomSection = case [trustSubSection, ownerSubSection] of [Nothing, Nothing] -> Nothing l -> Just $ strutX 30 <> mconcat (catMaybes l) ownerSubSection = prop >>= owner >>= (pure . translateX 14 . rightText) trustSubSection = translateX (-14) <$> case t of _ | PTrust `notElem` props -> Nothing t' | t' == 1 -> Nothing t' | t' == 0 -> Just $ leftText "impossible" _ -> Just $ leftText ("trust = " ++ percentageText t) displayCost c' | c' == 0 = mempty | otherwise = rightText $ "(" ++ printf "%.1f" c' ++ ")" displayProgress p' | p' == 0 = mempty | p' == 1 = leftText "done" | otherwise = leftText $ percentageText p' -- color is red if the project hasn't started, green if it's done, or yellow -- otherwise (i.e. in progress) fcColor = fc $ if colorByP then (if p == 0 then pink else if p == 1 then lightgreen else lightyellow) else white percentageText pct = show ((round $ pct * 100) :: Integer) ++ "%"