module GraphMod.Dot(make_dot
, AllEdges
, Nodes
, noEdges
, insMod
, insSet
, sourceEdges
, normalEdges
, maybePrune
, collapseAll
) where
import GraphMod.Utils
import qualified GraphMod.Trie as Trie
import GraphMod.Args
import Text.Dot
import Control.Monad(forM_,msum,unless)
import Data.List(intersperse,transpose)
import Data.Maybe(isJust,fromMaybe,listToMaybe)
import qualified Data.IntMap as IMap
import qualified Data.Map as Map
import qualified Data.IntSet as ISet
import System.IO(hPutStrLn,stderr)
import System.Console.GetOpt
import Numeric(showHex)
version = "0"
type Nodes = Trie.Trie String [((NodeT,String),Int)]
type Edges = IMap.IntMap ISet.IntSet
data NodeT = ModuleNode
| ModuleInItsCluster
| Redirect
| Deleted
| CollapsedNode Bool
deriving (Show,Eq,Ord)
data AllEdges = AllEdges
{ normalEdges :: Edges
, sourceEdges :: Edges
}
noEdges :: AllEdges
noEdges = AllEdges { normalEdges = IMap.empty
, sourceEdges = IMap.empty
}
insMod :: ModName -> Int -> Nodes -> Nodes
insMod (q,m) n t = Trie.insert q ins t
where
ins xs = case xs of
Nothing -> [ ((ModuleNode,m),n) ]
Just ys -> ((ModuleNode,m),n) : ys
insSet :: Int -> Int -> Edges -> Edges
insSet x y m = IMap.insertWith ISet.union x (ISet.singleton y) m
maybePrune :: Opts -> (AllEdges, Nodes) -> (AllEdges, Nodes)
maybePrune opts (es,ns)
| prune_edges opts = (es { normalEdges = pruneEdges (normalEdges es) }, ns)
| otherwise = (es,ns)
pruneEdges :: Edges -> Edges
pruneEdges es = foldr checkEdges es (IMap.toList es)
where
reachIn _ _ _ [] = False
reachIn g tgt visited (x : xs)
| x `ISet.member` visited = reachIn g tgt visited xs
| x == tgt = True
| otherwise = let vs = neighbours g x
in reachIn g tgt (ISet.insert x visited) (vs ++ xs)
neighbours g x = ISet.toList (IMap.findWithDefault ISet.empty x g)
reachableIn g x y = reachIn g y ISet.empty [x]
rmEdge x y g = IMap.adjust (ISet.delete y) x g
checkEdge x y g = let g1 = rmEdge x y g
in if reachableIn g1 x y then g1 else g
checkEdges (x,vs) g = foldr (checkEdge x) g (ISet.toList vs)
isIgnored :: IgnoreSet -> ModName -> Bool
isIgnored (Trie.Sub _ (Just IgnoreAll)) _ = True
isIgnored (Trie.Sub _ (Just (IgnoreSome ms))) ([],m) = elem m ms
isIgnored (Trie.Sub _ Nothing) ([],_) = False
isIgnored (Trie.Sub ts _) (q:qs,m) =
case Map.lookup q ts of
Nothing -> False
Just t -> isIgnored t (qs,m)
collapseAll :: Opts -> Nodes -> Trie.Trie String Bool -> Nodes
collapseAll opts t0 =
foldr (\q t -> fromMaybe t (collapse opts t q)) t0 . toList
where
toList (Trie.Sub _ (Just x)) = return ([], x)
toList (Trie.Sub as Nothing) = do (q,t) <- Map.toList as
(qs,x) <- toList t
return (q:qs, x)
collapse :: Opts -> Nodes -> (Qualifier,Bool) -> Maybe Nodes
collapse _ _ ([],_) = return Trie.empty
collapse opts (Trie.Sub ts mb) ([q],alsoMod) =
do t <- Map.lookup q ts
let will_move = mod_in_cluster opts && Map.member q ts
(thisMod,otherMods)
| alsoMod || will_move = case findThisMod =<< mb of
Nothing -> (Nothing, [])
Just (nid,rest) -> (Just nid, rest)
| otherwise = (Nothing, fromMaybe [] mb)
rep <- msum [ thisMod, getFirst t ]
let close ((_,nm),_) = ((if will_move then Deleted else Redirect,nm),rep)
ts' = Map.insert q (fmap (map close) t) ts
newT | alsoMod || not will_move = CollapsedNode (isJust thisMod)
| otherwise = ModuleNode
return (Trie.Sub ts' (Just (((newT,q),rep) : otherMods)))
where
findThisMod (((_,nm),nid) : more) | nm == q = Just (nid,more)
findThisMod (x : more) = do (yes,more') <- findThisMod more
return (yes, x:more')
findThisMod [] = Nothing
getFirst (Trie.Sub ts1 ms) =
msum (fmap snd (listToMaybe =<< ms) : map getFirst (Map.elems ts1))
collapse opts (Trie.Sub ts ms) (q : qs,x) =
do t <- Map.lookup q ts
t1 <- collapse opts t (qs,x)
return (Trie.Sub (Map.insert q t1 ts) ms)
moveModulesInCluster :: Nodes -> Nodes
moveModulesInCluster (Trie.Sub su0 ms0) =
goMb (fmap moveModulesInCluster su0) ms0
where
goMb su mb =
case mb of
Nothing -> Trie.Sub su Nothing
Just xs -> go [] su xs
go ns su xs =
case xs of
[] -> Trie.Sub su $ if null ns then Nothing else Just ns
y : ys ->
case check y su of
Left it -> go (it : ns) su ys
Right su1 -> go ns su1 ys
check it@((nt,s),i) mps =
case nt of
ModuleNode ->
case Map.lookup s mps of
Nothing -> Left it
Just t -> Right (Map.insert s (Trie.insert [] add t) mps)
where
newM = ((ModuleInItsCluster,s),i)
add xs = [newM] ++ fromMaybe [] xs
ModuleInItsCluster -> Left it
CollapsedNode _ -> Left it
Redirect -> Left it
Deleted -> Left it
make_dot :: Opts -> (AllEdges,Nodes) -> String
make_dot opts (es,t) =
showDot $
do attribute ("size", graph_size opts)
attribute ("ratio", "fill")
let cols = colors (color_scheme opts)
if use_clusters opts
then make_clustered_dot cols $
if mod_in_cluster opts then moveModulesInCluster t else t
else make_unclustered_dot cols "" t >> return ()
genEdges normalAttr (normalEdges es)
genEdges sourceAttr (sourceEdges es)
where
normalAttr _x _y = []
sourceAttr _x _y = [("style","dashed")]
genEdges attr edges =
forM_ (IMap.toList edges) $ \(x,ys) ->
forM_ (ISet.toList ys) $ \y ->
edge (userNodeId x) (userNodeId y) (attr x y)
make_clustered_dot :: [Color] -> Nodes -> Dot ()
make_clustered_dot cs0 su = go (0,0,0) cs0 su >> return ()
where
clusterC = "#0000000F"
go outer_col ~(this_col:more) (Trie.Sub xs ys) =
do let outerC = renderColor outer_col
thisC = renderColor this_col
forM_ (fromMaybe [] ys) $ \((t,ls),n) ->
unless (t == Redirect || t == Deleted) $
userNode (userNodeId n) $
[ ("label",ls) ] ++
case t of
CollapsedNode False -> [ ("shape", "box")
, ("style","filled")
, ("color", clusterC)
]
CollapsedNode True -> [ ("style","filled")
, ("fillcolor", clusterC)
]
ModuleInItsCluster -> [ ("style","filled,bold")
, ("fillcolor", outerC)
]
ModuleNode -> [ ("style", "filled")
, ("fillcolor", thisC)
, ("penwidth","0")
]
Redirect -> []
Deleted -> []
goSub this_col more (Map.toList xs)
goSub _ cs [] = return cs
goSub outer_col cs ((name,sub) : more) =
do (_,cs1) <- cluster $ do attribute ("label", name)
attribute ("color" , clusterC)
attribute ("style", "filled")
go outer_col cs sub
goSub outer_col cs1 more
make_unclustered_dot :: [Color] -> String -> Nodes -> Dot [Color]
make_unclustered_dot c pre (Trie.Sub xs ys') =
do let col = renderColor (head c)
let ys = fromMaybe [] ys'
forM_ ys $ \((t,ls),n) ->
userNode (userNodeId n) $
[ ("fillcolor", col)
, ("style", "filled")
, ("label", pre ++ ls)
] ++
case t of
CollapsedNode False -> [ ("shape", "box"), ("color", col) ]
CollapsedNode True -> [ ("shape", "box") ]
Redirect -> []
ModuleInItsCluster -> []
ModuleNode -> []
Deleted -> []
let c1 = if null ys then c else tail c
c1 `seq` loop (Map.toList xs) c1
where
loop ((name,sub):ms) c1 =
do let pre1 = pre ++ name ++ "."
c2 <- make_unclustered_dot c1 pre1 sub
loop ms c2
loop [] c2 = return c2
type Color = (Int,Int,Int)
colors :: Int -> [Color]
colors n = cycle $ mix_colors $ drop n $ palettes
renderColor :: Color -> String
renderColor (x,y,z) = '#' : showHex (mk x) (showHex (mk y) (showHex (mk z) ""))
where mk n = 0xFF - n * 0x44
mix_colors :: [[a]] -> [a]
mix_colors css = mk set1 ++ mk set2
where
(set1,set2) = unzip $ map (splitAt 3) css
mk = concat . transpose
palettes :: [[Color]]
palettes = [green, yellow, blue, red, cyan, magenta ]
where
red :: [Color]
red = [ (0,1,1), (0,2,2), (0,3,3), (1,2,3), (1,3,3), (2,3,3) ]
green = map rotR red
blue = map rotR green
[cyan,magenta,yellow] = map (map compl . reverse) [red, green, blue]
rotR (x,y,z) = (z,x,y)
compl (x,y,z) = (3-x,3-y,3-z)