Source file xDot.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
(**************************************************************************)
(*                                                                        *)
(*  This file is part of OcamlGraph.                                      *)
(*                                                                        *)
(*  Copyright (C) 2009-2010                                               *)
(*    CEA (Commissariat � l'�nergie Atomique)                             *)
(*                                                                        *)
(*  you can redistribute it and/or modify it under the terms of the GNU   *)
(*  Lesser General Public License as published by the Free Software       *)
(*  Foundation, version 2.1, with a linking exception.                    *)
(*                                                                        *)
(*  It is distributed in the hope that it will be useful,                 *)
(*  but WITHOUT ANY WARRANTY; without even the implied warranty of        *)
(*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *)
(*  GNU Lesser General Public License for more details.                   *)
(*                                                                        *)
(*  See the file ../LICENSE for more details.                             *)
(*                                                                        *)
(*  Authors:                                                              *)
(*    - Julien Signoles  (Julien.Signoles@cea.fr)                         *)
(*    - Jean-Denis Koeck (jdkoeck@gmail.com)                              *)
(*    - Benoit Bataille  (benoit.bataille@gmail.com)                      *)
(*                                                                        *)
(**************************************************************************)

(** Reading XDot files *)

open Dot_ast
open Printf

(* Layout types *)
(* This file is responsible for converting the coordinates from dot coordinates
   to GnomeCanvas world coordinates. 
   The matrix transformation to apply is: 
   (1  0)
   (0 -1)
   Care must be taken to exchange max and min values on the y axis.
   Outside this module all coordinates are assumed to be in canvas 
   world coordinates.
*)
type pos = float * float      (* coordinates *)
type bounding_box = pos * pos (* bounding box   *)

type node_layout = {
  n_name : string;
  n_pos : pos;
  n_bbox   : bounding_box;
  n_draw   : XDotDraw.operation list;
  n_ldraw  : XDotDraw.operation list;
}

type cluster_layout = {
  c_pos : pos;
  c_bbox   : bounding_box;
  c_draw   : XDotDraw.operation list;
  c_ldraw  : XDotDraw.operation list;
}

type edge_layout = {
  e_draw   : XDotDraw.operation list;
  e_ldraw  : XDotDraw.operation list;
  e_hdraw  : XDotDraw.operation list;
  e_tdraw  : XDotDraw.operation list;
  e_hldraw : XDotDraw.operation list;
  e_tldraw : XDotDraw.operation list;
}

let mk_node_layout ~name ~pos ~bbox ~draw ~ldraw =
  { n_name    = name;
    n_pos   = pos;
    n_bbox  = bbox;
    n_draw  = draw;
    n_ldraw = ldraw }

let mk_cluster_layout ~pos ~bbox ~draw ~ldraw =
  { c_pos   = pos;
    c_bbox  = bbox;
    c_draw  = draw;
    c_ldraw = ldraw }

let mk_edge_layout ~draw ~ldraw ~hdraw ~tdraw ~hldraw ~tldraw =
  { e_draw   = draw;
    e_ldraw  = ldraw;
    e_hdraw  = hdraw;
    e_tdraw  = tdraw;
    e_hldraw = hldraw;
    e_tldraw = tldraw;
  }

exception ParseError of string

(* MISCELLANEOUS FUNCTIONS *)

let read_pos s = Scanf.sscanf s "%f,%f" (fun x y -> x, -.y)

let bounding_box (x, y) w h =
  let lower_left = x -. w , y -. h in
  let upper_right =  x+.w,y+.h in
  lower_left,upper_right

let get_dot_string = function
  | Dot_ast.String s -> s
  | Dot_ast.Ident s -> s
  | Dot_ast.Number s -> s
  | Dot_ast.Html s -> s

(* READING VERTEX LAYOUTS *)

(** Finds the attributes [pos], [width] and [height] of a node
    in the attribute list *)
let read_common_layout mk_layout attr_list =
  (* Iter on the attributes *)
  (* shape, position, width, height, color, filled *)
  let fold ((p,w,h, draw,ldraw) as attrs) = function
    | (Dot_ast.Ident "pos"), Some (Dot_ast.String s) ->
      (Some s), w, h, draw,ldraw
    | (Dot_ast.Ident "width"), Some (Dot_ast.String s) ->
      p, (Some s), h, draw,ldraw
    | (Dot_ast.Ident "height"), Some (Dot_ast.String s) ->
      p, w, (Some s), draw,ldraw
    | (Dot_ast.Ident "_draw_"), Some (Dot_ast.String draw) ->
      p,w,h, XDotDraw.parse draw, ldraw
    | (Dot_ast.Ident "_ldraw_"), Some (Dot_ast.String ldraw) ->
      p,w,h, draw, XDotDraw.parse ldraw
    | _ -> attrs in

  let fold_attr acc attr_list =
    List.fold_left fold acc attr_list in
  let attrs = List.fold_left fold_attr (None, None, None, [], [])
      attr_list in

  (* Check if we have position, width and height *)
  match attrs with
  | Some pos, Some w, Some h, draw,ldraw->
    let pos = read_pos pos in
    let coord = bounding_box pos
        (float_of_string w) (-.(float_of_string h)) in
    (* Return the node model *)
    mk_layout ~pos ~bbox:coord ~draw ~ldraw
  | _,_,_, draw, ldraw ->
    let pos = (0.,0.) in
    let bbox = (0.,0.),(0.,0.) in
    mk_layout ~pos ~bbox ~draw ~ldraw

let read_node_layout (id,_) attrs =
  let f = read_common_layout
      (fun ~pos ~bbox ~draw ~ldraw -> mk_node_layout ~pos ~bbox ~draw ~ldraw)
      attrs in
  f ~name:(get_dot_string id)
let read_cluster_layout = read_common_layout mk_cluster_layout

(* READING EDGE LAYOUTS *)

(** Reads the spline control points of a curve in an xdot file
    example : "c 5 -black B 4 65 296 65 288 65 279 65 270 "
*)

(* The edge drawing operations are in the following attributes :
   _hdraw_  Head arrowhead
   _tdraw_  Tail arrowhead
   _hldraw_  Head label
   _tldraw_  Tail label
*)

(** Gets the layout of an edge out of the dot ast *)
let read_edge_layout attr_list =
  let draw   = ref [] in
  let ldraw  = ref [] in
  let hdraw  = ref [] in
  let tdraw  = ref [] in
  let hldraw = ref [] in
  let tldraw = ref [] in
  let fill_draw_ops = function
    | (Dot_ast.Ident "_draw_"),   Some (Dot_ast.String s) ->
      draw   := XDotDraw.parse s
    | (Dot_ast.Ident "_ldraw_"),  Some (Dot_ast.String s) ->
      ldraw  := XDotDraw.parse s
    | (Dot_ast.Ident "_hdraw_"),  Some (Dot_ast.String s) ->
      hdraw  := XDotDraw.parse s
    | (Dot_ast.Ident "_tdraw_"),  Some (Dot_ast.String s) ->
      tdraw  := XDotDraw.parse s
    | (Dot_ast.Ident "_hldraw_"), Some (Dot_ast.String s) ->
      hldraw := XDotDraw.parse s
    | (Dot_ast.Ident "_tldraw_"), Some (Dot_ast.String s) ->
      tldraw := XDotDraw.parse s
    | _ -> () in
  List.iter (List.iter fill_draw_ops) attr_list;
  let draw, ldraw = !draw, !ldraw in
  let hdraw, tdraw, hldraw, tldraw = !hdraw, !tdraw, !hldraw, !tldraw in
  mk_edge_layout ~draw ~ldraw ~hdraw ~tdraw ~hldraw ~tldraw

(* Computes the bounding box *)
let read_bounding_box str =
  let x1,y1,x2,y2 = Scanf.sscanf str "%f,%f,%f,%f" (fun a b c d -> a,b,c,d) in
  (* Convert coordinates to the world canvas coordinates *)
  let lower_left = (x1, -.y2) and upper_right = x2, -.y1 in
  lower_left,upper_right

module Make(G : Graphviz.GraphWithDotAttrs) = struct

  module HV = Hashtbl.Make(G.V)

  (* cannot use an hashtable because no hash function for edges *)
  module HE =
    Map.Make
      (struct
        type t = G.E.t
        let compare = G.E.compare
      end)

  module HT =
    Hashtbl.Make
      (Util.HTProduct
         (Util.HTProduct(G.V)(G.V))
         (struct type t = string let equal = (=) let hash = Hashtbl.hash end))

  type graph_layout =
    { vertex_layouts  : node_layout HV.t;
      edge_layouts    : edge_layout HE.t;
      cluster_layouts : (string, cluster_layout) Hashtbl.t;
      bbox : bounding_box }

  exception Found of string

  let get_edge_comment e =
    let al = G.edge_attributes e in
    try
      List.iter (function `Comment c -> raise (Found c) | _ -> ()) al;
      None
    with Found c ->
      Some c

  let get_dot_comment (al : Dot_ast.attr list) =
    try
      List.iter
        (List.iter
           (function
             | Ident "comment", Some c -> raise (Found (get_dot_string c))
             | _ -> ()))
        al;
      ""
    with Found c ->
      c

  let strip_quotes = function
    | "" -> ""
    | s ->
      let len = String.length s in
      if s.[0] = '"' && s.[len -1] = '"' then String.sub s 1 (len - 2)
      else s

  (* Parses the graph attribute named id, and converts it with conv *)
  let parse_graph_attr id conv stmts =
    let read_attr = function
      | Ident ident , Some (String attr) when ident = id ->
        raise (Found attr)
      | _ -> ()
    in
    let read_stmt = function
      | Attr_graph attrs -> List.iter (List.iter read_attr) attrs
      | _ -> ()
    in
    try
      List.iter read_stmt stmts;
      failwith ("Could not find the graph attribute named " ^ id)
    with Found attr ->
      conv attr

  let parse_bounding_box = parse_graph_attr "bb" read_bounding_box
  (*let parse_bgcolor = parse_graph_attr "bgcolor" XDotDraw.normalize_color*)

  let parse_layouts g stmts =
    let name_to_vertex = Hashtbl.create 97 in
    let vertices_comment_to_edge = HT.create 97 in

    let vertex_layouts = HV.create 97 in
    let edge_layouts = ref HE.empty in
    let cluster_layouts = Hashtbl.create 97 in

    G.iter_vertex
      (fun v ->
         let name = strip_quotes (G.vertex_name v) in
         Hashtbl.add name_to_vertex name v)
      g;

    G.iter_edges_e
      (fun e ->
         let comment = match get_edge_comment e with
           | Some c -> strip_quotes c
           | None -> ""
         in
         let vs = G.E.src e, G.E.dst e in
         HT.add vertices_comment_to_edge (vs, comment) e)
      g;

    let find_vertex (id,_) =
      let name = get_dot_string id in
      try Hashtbl.find name_to_vertex name
      with Not_found -> failwith ("Could not find vertex named " ^ name)
    in

    let find_edge v v' comment =
      try HT.find vertices_comment_to_edge ((v, v'), comment)
      with Not_found ->
        (*  Printf.printf "Did not find edge from %s to %s with comment %s\n"
            (G.vertex_name v) (G.vertex_name v')
            (match comment with Some c -> c | None -> "none");*)
        raise Not_found
    in

    let rec collect_layouts cluster stmt =
      try
        match stmt with
        | Node_stmt (node_id, al) ->
          let v = find_vertex node_id in
          HV.add vertex_layouts v (read_node_layout node_id al)
        | Edge_stmt (NodeId id, [NodeId id'], al) ->
          let v  = find_vertex id  in
          let v' = find_vertex id' in
          let comment = get_dot_comment al in
          let e = find_edge v v' comment in
          edge_layouts := HE.add e (read_edge_layout al) !edge_layouts
        | Subgraph (SubgraphDef (Some id, stmts)) ->
          let cluster = get_dot_string id in
          List.iter (collect_layouts (Some cluster)) stmts
        (* Anonymous subgraph *)
        | Subgraph (SubgraphDef (_, stmts)) ->
          List.iter (collect_layouts cluster) stmts
        | Attr_graph al ->
          (match cluster with
           | Some c -> Hashtbl.add cluster_layouts c (read_cluster_layout al)
           | None -> ())
        |  _ -> ()
      with Not_found ->
        ()
    in
    List.iter (collect_layouts None) stmts;
    vertex_layouts, edge_layouts, cluster_layouts

  let parse g dot_ast =
    let v_layouts, e_layouts, c_layouts = parse_layouts g dot_ast.stmts in
    let bbox = parse_bounding_box dot_ast.stmts in
    (* let bgcolor = parse_bgcolor dot_ast.stmts in*)
    { vertex_layouts  = v_layouts;
      edge_layouts    = !e_layouts;
      cluster_layouts = c_layouts;
      bbox = bbox }

  exception DotError of string

  let layout_of_xdot ~xdot_file g =
    let dot_ast = Dot.parse_dot_ast xdot_file in
    parse g dot_ast

  let layout_of_dot ?(cmd="dot") ~dot_file g =
    let base_name =
      try Filename.basename (Filename.chop_extension dot_file)
      with Invalid_argument _ -> dot_file
    in
    let xdot_file = Filename.temp_file base_name ".xdot" in
    (* Run graphviz to get xdot file *)
    let dot_cmd = sprintf "%s -Txdot %s > %s" cmd dot_file xdot_file in
    match Sys.command dot_cmd with
    | 0 ->
      let l = layout_of_xdot ~xdot_file g in
      Sys.remove xdot_file;
      l
    | _ ->
      Sys.remove xdot_file;
      raise (DotError "Error during dot execution")

end