File Coverage

blib/lib/Gtk2/Ex/GraphViz.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Gtk2::Ex::GraphViz;
2              
3             our $VERSION = '0.01';
4              
5 1     1   53686 use strict;
  1         2  
  1         51  
6 1     1   7 use warnings;
  1         3  
  1         45  
7 1     1   702 use Glib qw(TRUE FALSE);
  0            
  0            
8             use Data::Dumper;
9             use GraphViz;
10             use Gtk2;
11             use XML::Simple;
12             use Math::Geometry::Planar;
13             use GD;
14             use GD::Polyline;
15             use Carp;
16              
17             sub new {
18             my ($class, $graph) = @_;
19             my $self = {};
20             bless ($self, $class);
21             $self->_set_graph($graph);
22             return $self;
23             }
24              
25             sub get_widget {
26             my ($self) = @_;
27             my $vbox = Gtk2::VBox->new(FALSE, 0);
28             $vbox->pack_start ($self->{eventbox}, FALSE, FALSE, 0);
29             my $hbox = Gtk2::HBox->new(FALSE, 0);
30             $hbox->pack_start ($vbox, FALSE, FALSE, 0);
31             return $hbox;
32             }
33              
34             sub signal_connect {
35             my ($self, $signal, $callback) = @_;
36             my $allowedsignals = [
37             'mouse-enter-node',
38             'mouse-exit-node',
39             'mouse-enter-edge',
40             'mouse-exit-edge',
41             ];
42             my %hash = map { $_ => 1 } @$allowedsignals;
43             unless ($hash{$signal}) {
44             my $str = "Warning !! No such signal $signal. Allowed signals are\n";
45             $str .= join "\n", @$allowedsignals;
46             warn $str."\n";
47             }
48             $self->{signals}->{$signal} = $callback;
49             }
50              
51             sub _set_graph {
52             my ($self, $graph) = @_;
53             my $pngimage = GD::Image->newFromPngData($graph->as_png);
54             my $svgdata = XMLin($graph->as_svg);
55             my (@bounds) = split ' ', $svgdata->{viewBox};
56             my $width = $bounds[2] - $bounds[0];
57             my $height = $bounds[3] - $bounds[3];
58             $self->{pngimage} = $pngimage;
59             $self->{svgdata} = $svgdata;
60             $self->{node}->{polygons} = _extract_node_polygons($svgdata);
61             $self->{node}->{ellipses} = _extract_node_ellipses($svgdata);
62             $self->{edge}->{edges} = _extract_edge_coords($svgdata);
63             my $loader = Gtk2::Gdk::PixbufLoader->new;
64             $loader->write ($pngimage->png);
65             $loader->close;
66             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
67              
68             my $eventbox = Gtk2::EventBox->new;
69             $eventbox->add($image);
70              
71             my ($ratiox, $ratioy);
72             $eventbox->signal_connect('realize' =>
73             sub {
74             my @imageallocatedsize = $image->allocation->values;
75             $ratiox = $imageallocatedsize[2]/$width;
76             $ratioy = $imageallocatedsize[2]/$width;
77             $self->{ratiox} = $ratiox;
78             $self->{ratioy} = $ratioy;
79             }
80             );
81             $eventbox->add_events ('pointer-motion-mask');
82             $eventbox->signal_connect ('motion-notify-event' =>
83             sub {
84             my ($widget, $event) = @_;
85             #my $r = $self->{eventbox}->allocation;
86             #print $r->x." ".$r->y." ".$r->width." ".$r->height." \n";
87             my ($x, $y) = ($event->x, $event->y);
88             $x = int($x/$ratiox);
89             $y = int($y/$ratioy);
90             return if $self->_check_inside_node($x, $y);
91             return if $self->_check_on_edge($x, $y);
92             }
93             );
94             $self->{eventbox} = $eventbox;
95             }
96              
97              
98             sub _inside_ellipse {
99             my ($x0, $y0, $a, $b, $x, $y) = @_;
100             return TRUE if
101             ($b*$b*($x-$x0)*($x-$x0) + $a*$a*($y-$y0)*($y-$y0) <= $a*$a*$b*$b);
102             return FALSE;
103             }
104              
105             sub _highlight_edge {
106             my ($self, $line) = @_;
107             my $polyline = new GD::Polyline;
108             my ($ratiox, $ratioy) = ($self->{ratiox}, $self->{ratioy});
109             foreach my $bit (@$line) {
110             $polyline->addPt($bit->[0]*$ratiox, $bit->[1]*$ratioy);
111             }
112             my $im = $self->{pngimage}->clone;
113             $im->setThickness(3);
114             my $white = $im->colorAllocate(255,0,0);
115             my $eventbox = $self->{eventbox};
116             my @children = $eventbox->get_children;
117             foreach my $child (@children) {
118             $eventbox->remove($child);
119             }
120             my $spline = $polyline->toSpline();
121             $im->polydraw($spline, $white);
122             my $loader = Gtk2::Gdk::PixbufLoader->new;
123             $loader->write ($im->png);
124             $loader->close;
125             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
126             $eventbox->add($image);
127             $eventbox->show_all;
128             $self->{HIGHLIGHTED} = 1;
129             }
130              
131             sub _co_linear {
132             my ($p1, $p2, $a) = @_;
133             my ($x1, $y1) = @$p1;
134             my ($x2, $y2) = @$p2;
135             my ($xa, $ya) = @$a;
136             return FALSE if ($x1 > $xa && $x2 > $xa);
137             return FALSE if ($x1 < $xa && $x2 < $xa);
138             return FALSE if ($y1 > $ya && $y2 > $ya);
139             return FALSE if ($y1 < $ya && $y2 < $ya);
140             if (abs($x1-$x2) < 5) {
141             return TRUE if abs($x1-$xa) < 5;
142             return FALSE; #else
143             }
144             if (abs($y1-$y2) < 5) {
145             return TRUE if abs($y1-$ya) < 5;
146             return FALSE; #else
147             }
148             return FALSE if $y1 == $ya;
149              
150             return TRUE if (
151             abs(($x1-$x2)/($y1-$y2) - ($x1-$xa)/($y1-$ya)) < 1
152             );
153             return FALSE;
154             }
155              
156             sub _check_on_edge {
157             my ($self, $x, $y) = @_;
158             my $edges = $self->{edge}->{edges};
159             my $edgename;
160             foreach my $key (keys %$edges) {
161             if (_check_on_polyline($edges->{$key}, [$x, $y])) {
162             $edgename = $key;
163             last;
164             }
165             }
166             if ($edgename) {
167             $self->_highlight_edge($edges->{$edgename});
168             &{$self->{signals}->{'mouse-enter-edge'}}($self, $x, $y, $edgename)
169             if $self->{signals}->{'mouse-enter-edge'};
170             return TRUE;
171             } else {
172             if ($self->{HIGHLIGHTED}) {
173             my $eventbox = $self->{eventbox};
174             my @children = $eventbox->get_children;
175             foreach my $child (@children) {
176             $eventbox->remove($child);
177             }
178             my $loader = Gtk2::Gdk::PixbufLoader->new;
179             $loader->write ($self->{pngimage}->png);
180             $loader->close;
181             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
182             $eventbox->add($image);
183             $eventbox->show_all;
184             &{$self->{signals}->{'mouse-exit-edge'}}($self, $x, $y)
185             if $self->{signals}->{'mouse-exit-edge'};
186             }
187             $self->{HIGHLIGHTED} = 0;
188             return FALSE;
189             }
190             }
191              
192             sub _check_on_polyline {
193             my ($polyline, $p) = @_;
194             foreach my $i (0..$#{@$polyline}-1) {
195             return TRUE if _co_linear($polyline->[$i], $polyline->[$i+1], $p);
196             }
197             return FALSE;
198             }
199              
200             sub _check_inside_node {
201             my ($self, $x, $y) = @_;
202             my $nodename;
203             my $nodeshape;
204             my $polygons = $self->{node}->{polygons};
205             foreach my $key (%$polygons) {
206             if ($polygons->{$key} && $polygons->{$key}->isinside([$x,$y])) {
207             $nodename = $key;
208             $nodeshape = 'polygon';
209             last;
210             }
211             }
212             my $ellipses = $self->{node}->{ellipses};
213             foreach my $key (keys %$ellipses) {
214             if (_inside_ellipse(@{$ellipses->{$key}}, $x, $y)){
215             $nodename = $key;
216             $nodeshape = 'ellipse';
217             last;
218             }
219             }
220              
221             if ($nodename) {
222             $self->_highlight_polygon($polygons->{$nodename}->{points})
223             if $nodeshape eq 'polygon';
224             $self->_highlight_ellipse($ellipses->{$nodename})
225             if $nodeshape eq 'ellipse';
226             &{$self->{signals}->{'mouse-enter-node'}}($self, $x, $y, $nodename)
227             if $self->{signals}->{'mouse-enter-node'};
228             return TRUE;
229             } else {
230             if ($self->{HIGHLIGHTED}) {
231             my $eventbox = $self->{eventbox};
232             my @children = $eventbox->get_children;
233             foreach my $child (@children) {
234             $eventbox->remove($child);
235             }
236             my $loader = Gtk2::Gdk::PixbufLoader->new;
237             $loader->write ($self->{pngimage}->png);
238             $loader->close;
239             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
240             $eventbox->add($image);
241             $eventbox->show_all;
242             &{$self->{signals}->{'mouse-exit-node'}}($self, $x, $y)
243             if $self->{signals}->{'mouse-exit-node'};
244             }
245             $self->{HIGHLIGHTED} = 0;
246             return FALSE;
247             }
248             }
249              
250             sub _highlight_ellipse {
251             my ($self, $ellipse) = @_;
252             my $eventbox = $self->{eventbox};
253             my @children = $eventbox->get_children;
254             foreach my $child (@children) {
255             $eventbox->remove($child);
256             }
257             my $im = $self->{pngimage}->clone;
258             $im->setThickness(3);
259             my $white = $im->colorAllocate(255,0,0);
260             my ($ratiox, $ratioy) = ($self->{ratiox}, $self->{ratioy});
261             my ($cx, $cy, $w, $h) = @$ellipse;
262             $cx = int($cx*$ratiox);
263             $cy = int($cy*$ratioy);
264             $w = int( $w*$ratiox);
265             $h = int( $h*$ratioy);
266             $im->ellipse($cx,$cy,2*$w,2*$h,$white);
267             my $loader = Gtk2::Gdk::PixbufLoader->new;
268             $loader->write ($im->png);
269             $loader->close;
270             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
271             $eventbox->add($image);
272             $eventbox->show_all;
273             $self->{HIGHLIGHTED} = 1;
274             }
275              
276             sub _highlight_polygon {
277             my ($self, $rect) = @_;
278             my $eventbox = $self->{eventbox};
279             my @children = $eventbox->get_children;
280             foreach my $child (@children) {
281             $eventbox->remove($child);
282             }
283             my $im = $self->{pngimage}->clone;
284             $im->setThickness(3);
285             my $polygon = GD::Polygon->new;
286             my ($ratiox, $ratioy) = ($self->{ratiox}, $self->{ratioy});
287             foreach my $point (@$rect) {
288             my $x = int($point->[0]*$ratiox);
289             my $y = int($point->[1]*$ratioy);
290             $polygon->addPt($x, $y);
291             }
292             my $white = $im->colorAllocate(255,0,0);
293             $im->openPolygon($polygon,$white);
294             my $loader = Gtk2::Gdk::PixbufLoader->new;
295             $loader->write ($im->png);
296             $loader->close;
297             my $image = Gtk2::Image->new_from_pixbuf($loader->get_pixbuf);
298             $eventbox->add($image);
299             $eventbox->show_all;
300             $self->{HIGHLIGHTED} = 1;
301             }
302              
303             sub _extract_node_polygons {
304             my ($svgdata) = @_;
305             my $shapes = $svgdata->{g}->{g};
306             my $result;
307             foreach my $key (keys %$shapes) {
308             next unless $shapes->{$key}->{class} eq 'node';
309             if ($shapes->{$key}->{polygon}) {
310             my $str = $shapes->{$key}->{polygon}->{points};
311             my @coords = split ' ', $str;
312             my @thispoly;
313             foreach my $coord (@coords) {
314             my ($x, $y) = split ',', $coord;
315             push @thispoly, [$x, $y];
316             }
317             my $polygon = Math::Geometry::Planar->new;
318             $polygon->points(\@thispoly);
319             $result->{$key} = $polygon;
320             }
321             }
322             return $result;
323             }
324              
325             sub _extract_node_ellipses {
326             my ($svgdata) = @_;
327             my $shapes = $svgdata->{g}->{g};
328             my $result;
329             foreach my $key (keys %$shapes) {
330             next unless $shapes->{$key}->{class} eq 'node';
331             if ($shapes->{$key}->{ellipse}) {
332             my $ellipse = $shapes->{$key}->{ellipse};
333             my @thisellipse;
334             push @thisellipse, $ellipse->{cx};
335             push @thisellipse, $ellipse->{cy};
336             push @thisellipse, $ellipse->{rx};
337             push @thisellipse, $ellipse->{ry};
338             $result->{$key} = \@thisellipse;
339             }
340             }
341             return $result;
342             }
343              
344             sub _extract_edge_coords {
345             my ($svgdata) = @_;
346             my $shapes = $svgdata->{g}->{g};
347             my $result;
348             foreach my $key (keys %$shapes) {
349             next unless $shapes->{$key}->{class} eq 'edge';
350             my $str = $shapes->{$key}->{path}->{d};
351             $str =~ s/M/ /;
352             $str =~ s/C/ /;
353             my @coords = split ' ', $str;
354             my @thisline;
355             foreach my $coord (@coords) {
356             my ($x, $y) = split ',', $coord;
357             push @thisline, [$x, $y];
358             }
359             $result->{$key} = \@thisline;
360             }
361             return $result;
362             }
363              
364             1;
365              
366             __END__