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)
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
data NodeType = SumNode | ProductNode | SequenceNode | AtomicNode
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) []
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
treeSize :: Tree a -> Double
treeSize (Node _ []) = 1
treeSize (Node _ ts) = sum $ treeSize <$> ts
data RenderOptions = RenderOptions { colorByProgress :: Bool
, renderWidth :: Integer
, renderHeight :: Integer
, rootKey :: ProjectKey
, whitelistedProps :: [ProjProperty]
} deriving (Eq, Show)
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)
urlSection = givenProp PUrl $ prop >>= url >>= (pure . text)
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'
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) ++ "%"