hmt-0.18: Music/Theory/Graph/Dot.hs
-- | Graph (dot) functions.
module Music.Theory.Graph.Dot where
import Control.Monad {- base -}
import Data.Char {- base -}
import Data.List {- base -}
import System.FilePath {- filepath -}
import System.Process {- process -}
import qualified Data.Graph.Inductive.Graph as G {- fgl -}
import qualified Music.Theory.Graph.FGL as T {- hmt -}
import qualified Music.Theory.Graph.Type as T {- hmt -}
import qualified Music.Theory.List as List {- hmt -}
import qualified Music.Theory.Show as Show {- hmt -}
-- * UTIL
-- | Classify /s/ using a first element predicate, a remainder predicate and a unit predicate.
s_classify :: (t -> Bool) -> (t -> Bool) -> ([t] -> Bool) -> [t] -> Bool
s_classify p q r s =
case s of
c0:s' -> p c0 && all q s' && r s
[] -> False
-- | Symbol rule.
--
-- > map is_symbol ["sym","Sym2","3sym","1",""] == [True,True,False,False,False]
is_symbol :: String -> Bool
is_symbol = s_classify isAlpha isAlphaNum (const True)
-- | Number rule.
--
-- > map is_number ["123","123.45",".25","1.","1.2.3",""] == [True,True,False,True,False,False]
is_number :: String -> Bool
is_number = s_classify isDigit (\c -> isDigit c || c == '.') ((< 2) . length . filter ((==) '.'))
-- | Quote /s/ if 'is_symbol' or 'is_number'.
--
-- > map maybe_quote ["abc","a b c","12","12.3"] == ["abc","\"a b c\"","12","12.3"]
maybe_quote :: String -> String
maybe_quote s = if is_symbol s || is_number s then s else concat ["\"",s,"\""]
-- * ATTR/KEY
type DOT_KEY = String
type DOT_VALUE = String
type DOT_ATTR = (DOT_KEY,DOT_VALUE)
-- | Format 'DOT_ATTR'.
dot_attr_pp :: DOT_ATTR -> String
dot_attr_pp (lhs,rhs) = concat [lhs,"=",maybe_quote rhs]
-- | Format sequence of DOT_ATTR.
--
-- > dot_attr_seq_pp [("layout","neato"),("epsilon","0.0001")]
dot_attr_seq_pp :: [DOT_ATTR] -> String
dot_attr_seq_pp opt =
if null opt
then ""
else concat ["[",intercalate "," (map dot_attr_pp opt),"]"]
-- | Merge attributes, left-biased.
dot_attr_ext :: [DOT_ATTR] -> [DOT_ATTR] -> [DOT_ATTR]
dot_attr_ext = List.assoc_merge
-- | graph|node|edge
type DOT_TYPE = String
-- | (type,[attr])
type DOT_ATTR_SET = (DOT_TYPE,[DOT_ATTR])
-- | Format DOT_ATTR_SET.
--
-- > a = ("graph",[("layout","neato"),("epsilon","0.0001")])
-- > dot_attr_set_pp a == "graph [layout=neato,epsilon=0.0001]"
dot_attr_set_pp :: DOT_ATTR_SET -> String
dot_attr_set_pp (ty,opt) = concat [ty," ",dot_attr_seq_pp opt]
-- | type:attr (type = graph|node|edge)
type DOT_META_KEY = String
type DOT_META_ATTR = (DOT_META_KEY,DOT_VALUE)
-- | Keys are given as "type:attr".
--
-- > dot_key_sep "graph:layout" == ("graph","layout")
dot_key_sep :: DOT_META_KEY -> (DOT_TYPE,DOT_KEY)
dot_key_sep = List.split_on_1_err ":"
-- | Collate DOT_KEY attribute set to DOT_ATTR_SET.
dot_attr_collate :: [DOT_META_ATTR] -> [DOT_ATTR_SET]
dot_attr_collate opt =
let f (k,v) = let (ty,nm) = dot_key_sep k in (ty,(nm,v))
c = map f opt
in List.collate c
-- | Default values for default meta-keys.
--
-- > k = dot_attr_def ("neato","century schoolbook",10,"plaintext")
-- > map dot_attr_set_pp (dot_attr_collate k)
dot_attr_def :: (String,String,Double,String) -> [(DOT_META_ATTR)]
dot_attr_def (ly,fn,fs,sh) =
[("graph:layout",ly)
,("node:fontname",fn)
,("node:fontsize",show fs)
,("node:shape",sh)]
-- * GRAPH
-- | Graph pretty-printer, (v -> [attr],e -> [attr])
type GR_PP v e = ((Int,v) -> [DOT_ATTR],((Int,Int),e) -> [DOT_ATTR])
gr_pp_label_m :: Maybe (v -> DOT_VALUE) -> Maybe (e -> DOT_VALUE) -> GR_PP v e
gr_pp_label_m f_v f_e =
let lift m (_,x) = case m of
Nothing -> []
Just f -> [("label",f x)]
in (lift f_v,lift f_e)
-- | Label V & E.
gr_pp_label :: (v -> DOT_VALUE) -> (e -> DOT_VALUE) -> GR_PP v e
gr_pp_label f_v f_e = gr_pp_label_m (Just f_v) (Just f_e)
-- | Label V only.
gr_pp_label_v :: (v -> DOT_VALUE) -> GR_PP v e
gr_pp_label_v f = gr_pp_label_m (Just f) Nothing
-- | br = brace, csl = comma separated list
br_csl_pp :: Show t => [t] -> String
br_csl_pp l =
case l of
[e] -> show e
_ -> List.bracket ('{','}') (intercalate "," (map show l))
-- | Graph type, directed or un-directed.
data G_TYPE = G_DIGRAPH | G_UGRAPH
g_type_to_string :: G_TYPE -> String
g_type_to_string ty =
case ty of
G_DIGRAPH -> "digraph"
G_UGRAPH -> "graph"
g_type_to_edge_symbol :: G_TYPE -> String
g_type_to_edge_symbol ty =
case ty of
G_DIGRAPH -> " -> "
G_UGRAPH -> " -- "
node_pos_attr :: (Show n, Real n) => (n,n) -> DOT_ATTR
node_pos_attr (x,y) = let pp = Show.real_pp_trunc 2 in ("pos",concat [pp x,",",pp y])
-- | Edge POS attributes are sets of cubic bezier control points.
edge_pos_attr :: Real t => [(t,t)] -> DOT_ATTR
edge_pos_attr pt =
let r_pp = Show.real_pp_trunc 2
pt_pp (x,y) = concat [r_pp x,",",r_pp y]
in ("pos",unwords (map pt_pp pt))
-- | Variant that accepts single cubic bezier data set.
edge_pos_attr_1 :: Real t => ((t,t),(t,t),(t,t),(t,t)) -> DOT_ATTR
edge_pos_attr_1 (p1,p2,p3,p4) = edge_pos_attr [p1,p2,p3,p4]
{-
-- | Vertex position function.
type POS_FN v = (v -> (Int,Int))
g_lift_pos_fn :: (v -> (Int,Int)) -> v -> [DOT_ATTR]
g_lift_pos_fn f v = let (c,r) = f v in [node_pos_attr (c * 100,r * 100)]
-}
lbl_to_dot :: G_TYPE -> [DOT_META_ATTR] -> GR_PP v e -> T.LBL v e -> [String]
lbl_to_dot g_typ opt (v_attr,e_attr) (v,e) =
let ws s = if null s then "" else " " ++ s
v_f (k,lbl) = concat [show k,ws (dot_attr_seq_pp (v_attr (k,lbl))),";"]
e_f ((lhs,rhs),lbl) = concat [show lhs,g_type_to_edge_symbol g_typ,show rhs
,ws (dot_attr_seq_pp (e_attr ((lhs,rhs),lbl))),";"]
in concat [[g_type_to_string g_typ," g {"]
,map dot_attr_set_pp (dot_attr_collate opt)
,map v_f v
,map e_f e
,["}"]]
lbl_to_udot :: [DOT_META_ATTR] -> GR_PP v e -> T.LBL v e -> [String]
lbl_to_udot o pp = lbl_to_dot G_UGRAPH o pp
fgl_to_dot :: G.Graph gr => G_TYPE -> [DOT_META_ATTR] -> GR_PP v e -> gr v e -> [String]
fgl_to_dot typ opt pp gr = lbl_to_dot typ opt pp (T.fgl_to_lbl gr)
fgl_to_udot :: G.Graph gr => [DOT_META_ATTR] -> GR_PP v e -> gr v e -> [String]
fgl_to_udot opt pp gr = lbl_to_udot opt pp (T.fgl_to_lbl gr)
-- * DOT-PROCESS
{- | Run /dot/ to generate a file type based on the output file extension
(ie. .svg, .png, .jpeg, .gif)
/-n/ must be given to not run the layout algorithm and to use position data in the /dot/ file.
-}
dot_to_ext :: [String] -> FilePath -> FilePath -> IO ()
dot_to_ext opt dot_fn ext_fn =
let arg = opt ++ ["-T",tail (takeExtension ext_fn),"-o",ext_fn,dot_fn]
in void (rawSystem "dot" arg)
-- | Alias for 'dot_to_ext'
dot_to_svg :: [String] -> FilePath -> FilePath -> IO ()
dot_to_svg = dot_to_ext