File Coverage

lib/Graph/Easy/As_vcg.pm
Criterion Covered Total %
statement 144 178 80.9
branch 54 86 62.7
condition 21 41 51.2
subroutine 12 12 100.0
pod 0 2 0.0
total 231 319 72.4


line stmt bran cond sub pod time code
1             #############################################################################
2             # Output the graph as VCG or GDL text.
3             #
4             #############################################################################
5              
6             package Graph::Easy::As_vcg;
7              
8             $VERSION = '0.76';
9              
10             #############################################################################
11             #############################################################################
12              
13             package Graph::Easy;
14              
15 2     2   7 use strict;
  2         3  
  2         59  
16 2     2   7 use warnings;
  2         2  
  2         977  
17              
18             my $vcg_remap = {
19             node => {
20             align => \&_vcg_remap_align,
21             autolabel => undef,
22             autolink => undef,
23             autotitle => undef,
24             background => undef,
25             basename => undef,
26             class => undef,
27             colorscheme => undef,
28             columns => undef,
29             flow => undef,
30             fontsize => undef,
31             format => undef,
32             group => undef,
33             id => undef,
34             link => undef,
35             linkbase => undef,
36             offset => undef,
37             origin => undef,
38             pointstyle => undef,
39             rank => 'level',
40             rotate => undef,
41             rows => undef,
42             shape => \&_vcg_remap_shape,
43             size => undef,
44             textstyle => undef,
45             textwrap => undef,
46             title => undef,
47             },
48             edge => {
49             color => 'color', # this entry overrides 'all'!
50             align => undef,
51             arrowshape => undef,
52             arrowstyle => undef,
53             autojoin => undef,
54             autolabel => undef,
55             autolink => undef,
56             autosplit => undef,
57             autotitle => undef,
58             border => undef,
59             bordercolor => undef,
60             borderstyle => undef,
61             borderwidth => undef,
62             colorscheme => undef,
63             end => undef,
64             fontsize => undef,
65             format => undef,
66             id => undef,
67             labelcolor => 'textcolor',
68             link => undef,
69             linkbase => undef,
70             minlen => undef,
71             start => undef,
72             # XXX TODO: remap unknown styles
73             style => 'linestyle',
74             textstyle => undef,
75             textwrap => undef,
76             title => undef,
77             },
78             graph => {
79             align => \&_vcg_remap_align,
80             flow => \&_vcg_remap_flow,
81             label => 'title',
82             type => undef,
83             },
84             group => {
85             },
86             all => {
87             background => undef,
88             color => 'textcolor',
89             comment => undef,
90             fill => 'color',
91             font => 'fontname',
92             },
93             always => {
94             },
95             # this routine will handle all custom "x-dot-..." attributes
96             x => \&_remap_custom_vcg_attributes,
97             };
98              
99             sub _remap_custom_vcg_attributes
100             {
101 4     4   4 my ($self, $name, $value) = @_;
102              
103             # drop anything that is not starting with "x-vcg-..."
104 4 50       11 return (undef,undef) unless $name =~ /^x-vcg-/;
105              
106 4         8 $name =~ s/^x-vcg-//; # "x-vcg-foo" => "foo"
107 4         13 ($name,$value);
108             }
109              
110             my $vcg_shapes = {
111             rect => 'box',
112             diamond => 'rhomb',
113             triangle => 'triangle',
114             invtriangle => 'triangle',
115             ellipse => 'ellipse',
116             circle => 'circle',
117             hexagon => 'hexagon',
118             trapezium => 'trapeze',
119             invtrapezium => 'uptrapeze',
120             invparallelogram => 'lparallelogram',
121             parallelogram => 'rparallelogram',
122             };
123              
124             sub _vcg_remap_shape
125             {
126 3     3   3 my ($self, $name, $shape) = @_;
127              
128 3 100       7 return ('invisible','yes') if $shape eq 'invisible';
129              
130 2   50     8 ('shape', $vcg_shapes->{$shape} || 'box');
131             }
132              
133             sub _vcg_remap_align
134             {
135 4     4   5 my ($self, $name, $style) = @_;
136              
137             # center => center, left => left_justify, right => right_justify
138 4 50       14 $style .= '_justify' unless $style eq 'center';
139              
140 4         8 ('textmode', $style);
141             }
142              
143             my $vcg_flow = {
144             'south' => 'top_to_bottom',
145             'north' => 'bottom_to_top',
146             'down' => 'top_to_bottom',
147             'up' => 'bottom_to_top',
148             'east' => 'left_to_right',
149             'west' => 'right_to_left',
150             'right' => 'left_to_right',
151             'left' => 'right_to_left',
152             };
153              
154             sub _vcg_remap_flow
155             {
156 4     4   4 my ($self, $name, $style) = @_;
157              
158 4   50     15 ('orientation', $vcg_flow->{$style} || 'top_to_bottom');
159             }
160              
161             sub _class_attributes_as_vcg
162             {
163             # convert a hash with attribute => value mappings to a string
164 33     33   33 my ($self, $a, $class) = @_;
165              
166              
167 33         25 my $att = '';
168 33 100       48 $class = '' if $class eq 'graph';
169 33 100       52 $class .= '.' if $class ne '';
170              
171             # create the attributes as text:
172 33         91 for my $atr (sort keys %$a)
173             {
174 11         20 my $v = $a->{$atr};
175 11         13 $v =~ s/"/\\"/g; # '2"' => '2\"'
176 11 100       27 $v = '"' . $v . '"' unless $v =~ /^[0-9]+\z/; # 1, "1a"
177 11         22 $att .= " $class$atr: $v\n";
178             }
179 33         34 $att =~ s/,\s$//; # remove last ","
180              
181 33 100       47 $att = "\n$att" unless $att eq '';
182 33         68 $att;
183             }
184              
185             #############################################################################
186              
187             sub _generate_vcg_edge
188             {
189             # Given an edge, generate the VCG code for it
190 6     6   7 my ($self, $e, $indent) = @_;
191              
192             # skip links from/to groups, these will be done later
193             return '' if
194             $e->{from}->isa('Graph::Easy::Group') ||
195 6 50 33     40 $e->{to}->isa('Graph::Easy::Group');
196              
197 6         14 my $edge_att = $e->attributes_as_vcg();
198              
199 6         10 $e->{_p} = undef; # mark as processed
200 6         20 " edge:$edge_att\n"; # return edge text
201             }
202              
203 2     2   10 use Graph::Easy::Util qw(ord_values);
  2         2  
  2         2555  
204              
205             sub _as_vcg
206             {
207 11     11   12 my ($self) = @_;
208              
209             # convert the graph to a textual representation
210             # does not need a layout() beforehand!
211              
212             # gather all edge classes to build the classname attribute from them:
213 11         22 $self->{_vcg_edge_classes} = {};
214 11         25 for my $e (ord_values ( $self->{edges} ))
215             {
216 6         16 my $class = $e->sub_class();
217 6 100 66     27 $self->{_vcg_edge_classes}->{$class} = undef if defined $class && $class ne '';
218             }
219             # sort gathered class names and map them to integers
220 11         13 my $class_names = '';
221 11 100       7 if (keys %{$self->{_vcg_edge_classes}} > 0)
  11         30  
222             {
223 1         1 my $i = 1;
224 1         2 $class_names = "\n";
225 1         1 for my $ec (sort keys %{$self->{_vcg_edge_classes}})
  1         2  
226             {
227 1         2 $self->{_vcg_edge_classes}->{$ec} = $i; # remember mapping
228 1         3 $class_names .= " classname $i: \"$ec\"\n";
229 1         2 $i++;
230             }
231             }
232              
233             # generate the class attributes first
234 11         20 my $label = $self->label();
235 11 100       11 my $t = ''; $t = "\n title: \"$label\"" if $label ne '';
  11         18  
236              
237 11         390 my $txt = "graph: {$t\n\n" .
238             " // Generated by Graph::Easy $Graph::Easy::VERSION" .
239             " at " . scalar localtime() . "\n" .
240             $class_names;
241              
242 11         30 my $groups = $self->groups();
243              
244             # to keep track of invisible helper nodes
245 11         17 $self->{_vcg_invis} = {};
246             # name for invisible helper nodes
247 11         12 $self->{_vcg_invis_id} = 'joint0';
248              
249 11         10 my $atts = $self->{att};
250             # insert the class attributes
251 11         14 for my $class (qw/edge graph node/)
252             {
253 33 50       48 next if $class =~ /\./; # skip subclasses
254              
255 33         80 my $out = $self->_remap_attributes( $class, $atts->{$class}, $vcg_remap, 'noquote');
256 33         52 $txt .= $self->_class_attributes_as_vcg($out, $class);
257             }
258              
259 11 50       18 $txt .= "\n" if $txt ne ''; # insert newline
260              
261             ###########################################################################
262             # output groups as subgraphs
263              
264             # insert the edges into the proper group
265 11 50       18 $self->_edges_into_groups() if $groups > 0;
266              
267             # output the groups (aka subclusters)
268 11         9 my $indent = ' ';
269 11         9 for my $group (sort { $a->{name} cmp $b->{name} } values %{$self->{groups}})
  0         0  
  11         20  
270             {
271             # quote special chars in group name
272 0         0 my $name = $group->{name}; $name =~ s/([\[\]\(\)\{\}\#"])/\\$1/g;
  0         0  
273              
274             # # output group attributes first
275             # $txt .= " subgraph \"cluster$group->{id}\" {\n${indent}label=\"$name\";\n";
276              
277             # Make a copy of the attributes, including our class attributes:
278 0         0 my $copy = {};
279 0         0 my $attribs = $group->get_attributes();
280              
281 0         0 for my $a (keys %$attribs)
282             {
283 0         0 $copy->{$a} = $attribs->{$a};
284             }
285             # # set some defaults
286             # $copy->{'borderstyle'} = 'solid' unless defined $copy->{'borderstyle'};
287              
288 0         0 my $out = {};
289             # my $out = $self->_remap_attributes( $group->class(), $copy, $vcg_remap, 'noquote');
290              
291             # Set some defaults:
292 0 0       0 $out->{fillcolor} = '#a0d0ff' unless defined $out->{fillcolor};
293             # $out->{labeljust} = 'l' unless defined $out->{labeljust};
294              
295 0         0 my $att = '';
296             # we need to output style first ("filled" and "color" need come later)
297 0         0 for my $atr (reverse sort keys %$out)
298             {
299 0         0 my $v = $out->{$atr};
300 0         0 $v = '"' . $v . '"';
301 0         0 $att .= " $atr: $v\n";
302             }
303 0 0       0 $txt .= $att . "\n" if $att ne '';
304              
305             # # output nodes (w/ or w/o attributes) in that group
306             # for my $n ($group->sorted_nodes())
307             # {
308             # my $att = $n->attributes_as_vcg();
309             # $n->{_p} = undef; # mark as processed
310             # $txt .= $indent . $n->as_graphviz_txt() . $att . "\n";
311             # }
312              
313             # # output node connections in this group
314             # for my $e (ord_values ( $group->{edges} ))
315             # {
316             # next if exists $e->{_p};
317             # $txt .= $self->_generate_edge($e, $indent);
318             # }
319              
320 0         0 $txt .= " }\n";
321             }
322              
323 11         24 my $root = $self->attribute('root');
324 11 50       18 $root = '' unless defined $root;
325              
326 11         8 my $count = 0;
327             # output nodes with attributes first, sorted by their name
328 11         9 for my $n (sort { $a->{name} cmp $b->{name} } values %{$self->{nodes}})
  8         15  
  11         29  
329             {
330 13 50       22 next if exists $n->{_p};
331 13         18 my $att = $n->attributes_as_vcg($root);
332 13 50       21 if ($att ne '')
333             {
334 13         20 $n->{_p} = undef; # mark as processed
335 13         9 $count++;
336 13         26 $txt .= " node:" . $att . "\n";
337             }
338             }
339              
340 11 100       22 $txt .= "\n" if $count > 0; # insert a newline
341              
342 11         24 my @nodes = $self->sorted_nodes();
343              
344 11         14 foreach my $n (@nodes)
345             {
346 13         29 my @out = $n->successors();
347 13         24 my $first = $n->as_vcg_txt();
348 13 100 100     33 if ((@out == 0) && ( (scalar $n->predecessors() || 0) == 0))
      100        
349             {
350             # single node without any connections (unless already output)
351 1 50       3 $txt .= " node: { title: " . $first . " }\n" unless exists $n->{_p};
352             }
353             # for all outgoing connections
354 13         22 foreach my $other (reverse @out)
355             {
356             # in case there is more than one edge going from N to O
357 6         17 my @edges = $n->edges_to($other);
358 6         7 foreach my $e (@edges)
359             {
360 6 50       11 next if exists $e->{_p};
361 6         9 $txt .= $self->_generate_vcg_edge($e, ' ');
362             }
363             }
364             }
365              
366             # insert now edges between groups (clusters/subgraphs)
367              
368             # foreach my $e (ord_values ( $self->{edges} ))
369             # {
370             # $txt .= $self->_generate_group_edge($e, ' ')
371             # if $e->{from}->isa('Graph::Easy::Group') ||
372             # $e->{to}->isa('Graph::Easy::Group');
373             # }
374              
375             # clean up
376 11         24 for my $n ( ord_values ( $self->{nodes} ), ord_values ( $self->{edges} ))
377             {
378 19         19 delete $n->{_p};
379             }
380 11         15 delete $self->{_vcg_invis}; # invisible helper nodes for joints
381 11         14 delete $self->{_vcg_invis_id}; # invisible helper node name
382 11         14 delete $self->{_vcg_edge_classes};
383              
384 11         49 $txt . "\n}\n"; # close the graph
385             }
386              
387             package Graph::Easy::Node;
388              
389             sub attributes_as_vcg
390             {
391             # return the attributes of this node as text description
392 19     19 0 19 my ($self, $root) = @_;
393 19 100       26 $root = '' unless defined $root;
394              
395 19         21 my $att = '';
396 19         40 my $class = $self->class();
397              
398 19 50       35 return '' unless ref $self->{graph};
399              
400 19         14 my $g = $self->{graph};
401              
402             # get all attributes, excluding the class attributes
403 19         43 my $a = $self->raw_attributes();
404              
405             # add the attributes that are listed under "always":
406 19         19 my $attr = $self->{att};
407 19         13 my $base_class = $class; $base_class =~ s/\..*//;
  19         16  
408 19   33     40 my $list = $vcg_remap->{always}->{$class} || $vcg_remap->{always}->{$base_class};
409              
410 19         25 for my $name (@$list)
411             {
412             # for speed, try to look it up directly
413              
414             # look if we have a code ref, if yes, simple set the value to undef
415             # and let the coderef handle it later:
416 0 0 0     0 if ( ref($vcg_remap->{$base_class}->{$name}) ||
417             ref($vcg_remap->{all}->{$name}) )
418             {
419 0         0 $a->{$name} = $attr->{$name};
420             }
421             else
422             {
423 0         0 $a->{$name} = $attr->{$name};
424 0 0 0     0 $a->{$name} = $self->attribute($name) unless defined $a->{$name} && $a->{$name} ne 'inherit';
425             }
426             }
427              
428 19         37 $a = $g->_remap_attributes( $self, $a, $vcg_remap, 'noquote');
429              
430 19 100       67 if ($self->isa('Graph::Easy::Edge'))
431             {
432 6         11 $a->{sourcename} = $self->{from}->{name};
433 6         11 $a->{targetname} = $self->{to}->{name};
434 6         17 my $class = $self->sub_class();
435 6 100 66     26 $a->{class} = $self->{graph}->{_vcg_edge_classes}->{ $class } if defined $class && $class ne '';
436             }
437             else
438             {
439             # title: "Bonn"
440 13         21 $a->{title} = $self->{name};
441             }
442              
443             # do not needlessly output labels:
444             delete $a->{label} if !$self->isa('Graph::Easy::Edge') && # not an edge
445 19 50 66     100 exists $a->{label} && $a->{label} eq $self->{name};
      33        
446              
447             # bidirectional and undirected edges
448 19 50       32 if ($self->{bidirectional})
449             {
450 0         0 delete $a->{dir};
451 0         0 my ($n,$s) = Graph::Easy::_graphviz_remap_arrow_style(
452             $self,'', $self->attribute('arrowstyle'));
453 0         0 $a->{arrowhead} = $s;
454 0         0 $a->{arrowtail} = $s;
455             }
456 19 50       25 if ($self->{undirected})
457             {
458 0         0 delete $a->{dir};
459 0         0 $a->{arrowhead} = 'none';
460 0         0 $a->{arrowtail} = 'none';
461             }
462              
463             # borderstyle: double:
464 19 100       44 if (!$self->isa('Graph::Easy::Edge'))
465             {
466 13         20 my $style = $self->attribute('borderstyle');
467 13 50       21 $a->{peripheries} = 2 if $style =~ /^double/;
468             }
469              
470             # For nodes with shape plaintext, set the fillcolor to the background of
471             # the graph/group
472 19   100     48 my $shape = $a->{shape} || 'rect';
473 19 50 66     58 if ($class =~ /node/ && $shape eq 'plaintext')
474             {
475 0         0 my $p = $self->parent();
476 0         0 $a->{fillcolor} = $p->attribute('fill');
477 0 0       0 $a->{fillcolor} = 'white' if $a->{fillcolor} eq 'inherit';
478             }
479              
480 19 100       54 $shape = $self->attribute('shape') unless $self->isa_cell();
481              
482             # for point-shaped nodes, include the point as label and set width/height
483 19 50       25 if ($shape eq 'point')
484             {
485 0         0 require Graph::Easy::As_ascii; # for _u8 and point-style
486              
487 0         0 my $style = $self->_point_style( $self->attribute('pointstyle') );
488              
489 0         0 $a->{label} = $style;
490             # for point-shaped invisible nodes, set height/width = 0
491 0 0       0 $a->{width} = 0, $a->{height} = 0 if $style eq '';
492             }
493 19 100       25 if ($shape eq 'invisible')
494             {
495 1         1 $a->{label} = ' ';
496             }
497              
498 19 50 33     35 $a->{rank} = '0' if $root ne '' && $root eq $self->{name};
499              
500             # create the attributes as text:
501 19         50 for my $atr (sort keys %$a)
502             {
503 40         33 my $v = $a->{$atr};
504 40         32 $v =~ s/"/\\"/g; # '2"' => '2\"'
505 40 100       75 $v = '"' . $v . '"' unless $v =~ /^[0-9]+\z/; # 1, "1a"
506 40         57 $att .= "$atr: $v ";
507             }
508 19         20 $att =~ s/,\s$//; # remove last ","
509              
510             # generate attribute text if nec.
511 19 50       37 $att = ' { ' . $att . '}' if $att ne '';
512              
513 19         50 $att;
514             }
515              
516             sub as_vcg_txt
517             {
518             # return the node itself (w/o attributes) as VCG representation
519 13     13 0 12 my $self = shift;
520              
521 13         12 my $name = $self->{name};
522              
523             # escape special chars in name (including doublequote!)
524 13         17 $name =~ s/([\[\]\(\)\{\}"])/\\$1/g;
525              
526             # quote:
527 13         20 '"' . $name . '"';
528             }
529              
530             1;
531             __END__