packages feed

tracetree-0.1.0.1: ttrender/Debug/Trace/Tree/Render/Edged.hs

module Debug.Trace.Tree.Render.Edged (renderTree) where

import Data.Bifunctor
import Data.Maybe (fromMaybe)
import Diagrams.Backend.Cairo (B)
import Diagrams.Prelude hiding (coords, offset)
import Diagrams.TwoD.Layout.Tree (SymmLayoutOpts)
import Graphics.SVGFonts
import qualified Diagrams.TwoD.Layout.Tree as Diagrams.Tree

import Debug.Trace.Tree.Edged
import Debug.Trace.Tree.Render.Constants

{-------------------------------------------------------------------------------
  Main rendering algorithm
-------------------------------------------------------------------------------}

-- | Node IDs allocated by 'Diagrams.Tree.label'
type PID = Int

renderTree :: forall k v.
              (v -> v -> k -> (Diagram B, ArrowOpts Double))
           -> (v -> Diagram B)
           -> Bool                  -- ^ Show coordinates?
           -> ETree k (v, Metadata) -- ^ Tree with metadata
           -> Diagram B
renderTree drK' drV' showCoords t =
    addArrows (arrows positioned) (nodes positioned)
  where
    -- Step 1. Render the individual tree nodes and the annotations on the edges
    drawn :: ETree (Diagram B, ArrowOpts Double) (Diagram B, Metadata)
    drawn = second drV . mapEdges drK $ t
      where
        drK :: (v, meta) -> (v, meta) -> k -> (Diagram B, ArrowOpts Double)
        drK (v, _) (v', _) k = drK' v v' k

        drV :: (v, Metadata) -> (Diagram B, Metadata)
        drV (v, meta) =
          let rendered | showCoords = renderCoords (coords meta) <> drV' v
                       | otherwise  = drV' v
          in (rendered, meta)

    -- Step 2. Assign IDs to all nodes and pair each node with its ID
    labelled :: ETree (Diagram B, ArrowOpts Double) (Diagram B, Metadata, PID)
    labelled = fmap (\((d, meta), n) -> (d # named n, meta, n))
             $ Diagrams.Tree.label drawn -- pair with unique IDs

    -- Step 3. Compute tree layout and move all nodes to their final location
    positioned :: ETree (Diagram B, ArrowOpts Double) (Diagram B, Metadata, PID)
    positioned = fmap (\((d, meta, n), p) -> (d # moveTo p, meta, n))
               $ symmLayout'' symmOpts (mempty, with)
               $ labelled

    -- Step 4. Extract nodes from the positioned tree
    --(Just extract all nodes and mappend them all together)
    nodes :: ETree a (Diagram B, meta, pid) -> Diagram B
    nodes = foldMap $ \(node, _meta, _n) -> node

    -- Step 5. Extract arrows from the positioned tree
    arrows :: ETree (Diagram B, ArrowOpts Double) (a, Metadata, PID) -> Arrows
    arrows = mconcat . keys . mapEdges aux
      where
        aux :: (a, Metadata, PID) -> (a, Metadata, PID)
            -> (Diagram B, ArrowOpts Double) -> Arrows
        aux (_, _parent, n) (_, child, n') (lbl, arrOpts) =
          connectLabelled arrOpts lbl (isFirstChild child) n n'

    symmOpts :: SymmLayoutOpts Double ((Diagram B, ArrowOpts Double), (Diagram B, Metadata, PID))
    symmOpts = with & Diagrams.Tree.slWidth  .~ computeWidth
                    & Diagrams.Tree.slHeight .~ computeHeight
                    & Diagrams.Tree.slHSep   .~ constTreeHSep
                    & Diagrams.Tree.slVSep   .~ constTreeVSep
      where
        -- We don't know where the label will be placed. In the worst case,
        -- the label is positioned to start or end precisely at the center
        -- of the node, so we allow for the worst case here.
        computeWidth ((edge, _opts), (node, _meta, _n)) =
          let (edgeMinX, edgeMaxX) = fromMaybe (0,0) $ extentX edge
              (nodeMinX, nodeMaxX) = fromMaybe (0,0) $ extentX node
              edgeWidth = edgeMaxX - edgeMinX
          in (negate edgeWidth `min` nodeMinX, edgeWidth `max` nodeMaxX)

        -- For the height we ignore the edge labels
        computeHeight ((_edge, _opts), (node, _meta, _n)) =
          fromMaybe (0,0) $ extentY node

-- | Lift standard layout algorithm to edged trees
symmLayout'' :: SymmLayoutOpts Double (k, v) -> k -> ETree k v -> ETree k (v, P2 Double)
symmLayout'' opts = liftTree (Diagrams.Tree.symmLayout' opts)

-- | Render coordinates
--
-- Note that these coordinates are not expected ever in the final diagram; they
-- are used only during diagram construction. So we don't need to worry too much
-- about how exactly they look.
renderCoords :: Coords -> Diagram B
renderCoords Coords{..} =
      stroke (textSVG (show (depth, offset)) constCoordsOverlay)
    # fc red
    # lw (global 0.5)

{-------------------------------------------------------------------------------
  Diagrams auxiliary: labelled arrows
-------------------------------------------------------------------------------}

newtype Arrows = Arrows { addArrows :: Diagram B -> Diagram B }

instance Monoid Arrows where
  mempty = Arrows id
  Arrows f `mappend` Arrows g = Arrows (f . g)

-- based on 'connectOutside'
connectLabelled :: ArrowOpts Double -> Diagram B -> Bool -> Int -> Int -> Arrows
connectLabelled opts edgeLabel labelOnLeft n1 n2 = Arrows $
  withName n1 $ \b1 ->
  withName n2 $ \b2 ->
    let v = location b2 .-. location b1
        midpoint = location b1 .+^ (v/2)
        s' = fromMaybe (location b1) $ traceP midpoint (-v) b1
        e' = fromMaybe (location b2) $ traceP midpoint v b2
        lbl = edgeLabel
            # (if labelOnLeft
                 then translateX (negate constEdgePadding) . alignR
                 else translateX         constEdgePadding  . alignL
              )
            # moveTo midpoint
    in
      atop (lbl <> arrowBetween' opts s' e')