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.75';
9              
10             #############################################################################
11             #############################################################################
12              
13             package Graph::Easy;
14              
15 3     3   4733 use strict;
  3         7  
  3         137  
16 3     3   16 use warnings;
  3         4  
  3         2445  
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   8 my ($self, $name, $value) = @_;
102              
103             # drop anything that is not starting with "x-vcg-..."
104 4 50       15 return (undef,undef) unless $name =~ /^x-vcg-/;
105              
106 4         13 $name =~ s/^x-vcg-//; # "x-vcg-foo" => "foo"
107 4         18 ($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   9 my ($self, $name, $shape) = @_;
127              
128 3 100       11 return ('invisible','yes') if $shape eq 'invisible';
129              
130 2   50     15 ('shape', $vcg_shapes->{$shape} || 'box');
131             }
132              
133             sub _vcg_remap_align
134             {
135 4     4   9 my ($self, $name, $style) = @_;
136              
137             # center => center, left => left_justify, right => right_justify
138 4 50       15 $style .= '_justify' unless $style eq 'center';
139              
140 4         14 ('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   11 my ($self, $name, $style) = @_;
157              
158 4   50     27 ('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   41 my ($self, $a, $class) = @_;
165              
166              
167 33         35 my $att = '';
168 33 100       68 $class = '' if $class eq 'graph';
169 33 100       59 $class .= '.' if $class ne '';
170            
171             # create the attributes as text:
172 33         87 for my $atr (sort keys %$a)
173             {
174 11         20 my $v = $a->{$atr};
175 11         20 $v =~ s/"/\\"/g; # '2"' => '2\"'
176 11 100       43 $v = '"' . $v . '"' unless $v =~ /^[0-9]+\z/; # 1, "1a"
177 11         35 $att .= " $class$atr: $v\n";
178             }
179 33         49 $att =~ s/,\s$//; # remove last ","
180              
181 33 100       70 $att = "\n$att" unless $att eq '';
182 33         151 $att;
183             }
184              
185             #############################################################################
186              
187             sub _generate_vcg_edge
188             {
189             # Given an edge, generate the VCG code for it
190 6     6   13 my ($self, $e, $indent) = @_;
191              
192             # skip links from/to groups, these will be done later
193 6 50 33     91 return '' if
194             $e->{from}->isa('Graph::Easy::Group') ||
195             $e->{to}->isa('Graph::Easy::Group');
196              
197 6         25 my $edge_att = $e->attributes_as_vcg();
198              
199 6         21 $e->{_p} = undef; # mark as processed
200 6         36 " edge:$edge_att\n"; # return edge text
201             }
202              
203 3     3   21 use Graph::Easy::Util qw(ord_values);
  3         7  
  3         6039  
204              
205             sub _as_vcg
206             {
207 11     11   22 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         48 $self->{_vcg_edge_classes} = {};
214 11         43 for my $e (ord_values ( $self->{edges} ))
215             {
216 6         36 my $class = $e->sub_class();
217 6 100 66     49 $self->{_vcg_edge_classes}->{$class} = undef if defined $class && $class ne '';
218             }
219             # sort gathered class names and map them to integers
220 11         24 my $class_names = '';
221 11 100       17 if (keys %{$self->{_vcg_edge_classes}} > 0)
  11         41  
222             {
223 1         3 my $i = 1;
224 1         3 $class_names = "\n";
225 1         2 for my $ec (sort keys %{$self->{_vcg_edge_classes}})
  1         5  
226             {
227 1         2 $self->{_vcg_edge_classes}->{$ec} = $i; # remember mapping
228 1         5 $class_names .= " classname $i: \"$ec\"\n";
229 1         4 $i++;
230             }
231             }
232              
233             # generate the class attributes first
234 11         53 my $label = $self->label();
235 11 100       20 my $t = ''; $t = "\n title: \"$label\"" if $label ne '';
  11         63  
236              
237 11         765 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         53 my $groups = $self->groups();
243              
244             # to keep track of invisible helper nodes
245 11         33 $self->{_vcg_invis} = {};
246             # name for invisible helper nodes
247 11         25 $self->{_vcg_invis_id} = 'joint0';
248              
249 11         19 my $atts = $self->{att};
250             # insert the class attributes
251 11         26 for my $class (qw/edge graph node/)
252             {
253 33 50       87 next if $class =~ /\./; # skip subclasses
254              
255 33         124 my $out = $self->_remap_attributes( $class, $atts->{$class}, $vcg_remap, 'noquote');
256 33         94 $txt .= $self->_class_attributes_as_vcg($out, $class);
257             }
258              
259 11 50       39 $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       23 $self->_edges_into_groups() if $groups > 0;
266              
267             # output the groups (aka subclusters)
268 11         13 my $indent = ' ';
269 11         15 for my $group (sort { $a->{name} cmp $b->{name} } values %{$self->{groups}})
  0         0  
  11         33  
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         36 my $root = $self->attribute('root');
324 11 50       30 $root = '' unless defined $root;
325              
326 11         13 my $count = 0;
327             # output nodes with attributes first, sorted by their name
328 11         19 for my $n (sort { $a->{name} cmp $b->{name} } values %{$self->{nodes}})
  8         33  
  11         49  
329             {
330 13 50       31 next if exists $n->{_p};
331 13         40 my $att = $n->attributes_as_vcg($root);
332 13 50       38 if ($att ne '')
333             {
334 13         34 $n->{_p} = undef; # mark as processed
335 13         18 $count++;
336 13         33 $txt .= " node:" . $att . "\n";
337             }
338             }
339            
340 11 100       27 $txt .= "\n" if $count > 0; # insert a newline
341              
342 11         47 my @nodes = $self->sorted_nodes();
343              
344 11         23 foreach my $n (@nodes)
345             {
346 13         55 my @out = $n->successors();
347 13         57 my $first = $n->as_vcg_txt();
348 13 100 100     68 if ((@out == 0) && ( (scalar $n->predecessors() || 0) == 0))
      100        
349             {
350             # single node without any connections (unless already output)
351 1 50       5 $txt .= " node: { title: " . $first . " }\n" unless exists $n->{_p};
352             }
353             # for all outgoing connections
354 13         35 foreach my $other (reverse @out)
355             {
356             # in case there is more than one edge going from N to O
357 6         28 my @edges = $n->edges_to($other);
358 6         14 foreach my $e (@edges)
359             {
360 6 50       17 next if exists $e->{_p};
361 6         42 $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         35 for my $n ( ord_values ( $self->{nodes} ), ord_values ( $self->{edges} ))
377             {
378 19         35 delete $n->{_p};
379             }
380 11         28 delete $self->{_vcg_invis}; # invisible helper nodes for joints
381 11         19 delete $self->{_vcg_invis_id}; # invisible helper node name
382 11         21 delete $self->{_vcg_edge_classes};
383              
384 11         84 $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 28 my ($self, $root) = @_;
393 19 100       130 $root = '' unless defined $root;
394              
395 19         22 my $att = '';
396 19         64 my $class = $self->class();
397              
398 19 50       86 return '' unless ref $self->{graph};
399              
400 19         36 my $g = $self->{graph};
401              
402             # get all attributes, excluding the class attributes
403 19         69 my $a = $self->raw_attributes();
404              
405             # add the attributes that are listed under "always":
406 19         28 my $attr = $self->{att};
407 19         24 my $base_class = $class; $base_class =~ s/\..*//;
  19         41  
408 19   33     90 my $list = $vcg_remap->{always}->{$class} || $vcg_remap->{always}->{$base_class};
409              
410 19         41 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         62 $a = $g->_remap_attributes( $self, $a, $vcg_remap, 'noquote');
429              
430 19 100       142 if ($self->isa('Graph::Easy::Edge'))
431             {
432 6         22 $a->{sourcename} = $self->{from}->{name};
433 6         17 $a->{targetname} = $self->{to}->{name};
434 6         19 my $class = $self->sub_class();
435 6 100 66     36 $a->{class} = $self->{graph}->{_vcg_edge_classes}->{ $class } if defined $class && $class ne '';
436             }
437             else
438             {
439             # title: "Bonn"
440 13         50 $a->{title} = $self->{name};
441             }
442              
443             # do not needlessly output labels:
444 19 50 66     131 delete $a->{label} if !$self->isa('Graph::Easy::Edge') && # not an edge
      33        
445             exists $a->{label} && $a->{label} eq $self->{name};
446              
447             # bidirectional and undirected edges
448 19 50       48 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       42 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       80 if (!$self->isa('Graph::Easy::Edge'))
465             {
466 13         35 my $style = $self->attribute('borderstyle');
467 13 50       40 $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     75 my $shape = $a->{shape} || 'rect';
473 19 50 66     102 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       93 $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       51 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       41 if ($shape eq 'invisible')
494             {
495 1         2 $a->{label} = ' ';
496             }
497              
498 19 50 33     55 $a->{rank} = '0' if $root ne '' && $root eq $self->{name};
499              
500             # create the attributes as text:
501 19         68 for my $atr (sort keys %$a)
502             {
503 40         56 my $v = $a->{$atr};
504 40         50 $v =~ s/"/\\"/g; # '2"' => '2\"'
505 40 100       115 $v = '"' . $v . '"' unless $v =~ /^[0-9]+\z/; # 1, "1a"
506 40         93 $att .= "$atr: $v ";
507             }
508 19         36 $att =~ s/,\s$//; # remove last ","
509              
510             # generate attribute text if nec.
511 19 50       49 $att = ' { ' . $att . '}' if $att ne '';
512              
513 19         71 $att;
514             }
515              
516             sub as_vcg_txt
517             {
518             # return the node itself (w/o attributes) as VCG representation
519 13     13 0 22 my $self = shift;
520              
521 13         22 my $name = $self->{name};
522              
523             # escape special chars in name (including doublequote!)
524 13         26 $name =~ s/([\[\]\(\)\{\}"])/\\$1/g;
525              
526             # quote:
527 13         39 '"' . $name . '"';
528             }
529            
530             1;
531             __END__