File Coverage

blib/lib/Tk/GraphViz.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             # -*-Perl-*-
2 2     2   92415 use strict;
  2         7  
  2         143  
3              
4             $Tk::GraphViz::VERSION = '1.01';
5              
6             package Tk::GraphViz;
7              
8 2     2   1097 use Tk 800.020;
  0            
  0            
9             use Tk::Font;
10              
11             # Parse::Yapp-generated Parser for parsing record node labels
12             use Tk::GraphViz::parseRecordLabel;
13              
14              
15             use base qw(Tk::Derived Tk::Canvas);
16              
17             #use warnings;
18             use IO qw(Handle File Pipe);
19             use Carp;
20             use Reaper qw( reapPid pidStatus );
21              
22             use IPC::Open3;
23             use POSIX qw( :sys_wait_h :errno_h );
24             use Fcntl;
25              
26              
27             # Initialize as a derived Tk widget
28             Construct Tk::Widget 'GraphViz';
29              
30              
31             ######################################################################
32             # Class initializer
33             #
34             ######################################################################
35             sub ClassInit
36             {
37             my ($class, $mw) = @_;
38              
39              
40             $class->SUPER::ClassInit($mw);
41             }
42              
43              
44             ######################################################################
45             # Instance initializer
46             #
47             ######################################################################
48             sub Populate
49             {
50             my ($self, $args) = @_;
51              
52             $self->SUPER::Populate($args);
53              
54              
55             # Default resolution, for scaling
56             $self->{dpi} = 72;
57             $self->{margin} = .15 * $self->{dpi};
58              
59             # Keep track of fonts used, so they can be scaled
60             # when the canvas is scaled
61             $self->{fonts} = {};
62             }
63              
64              
65             ######################################################################
66             # Show a GraphViz graph
67             #
68             # Major steps:
69             # - generate layout of the graph, which includes
70             # locations / color info
71             # - clear canvas
72             # - parse layout to add nodes, edges, subgraphs, etc
73             # - resize to fit the graph
74             ######################################################################
75             sub show
76             {
77             my ($self, $graph, %opt) = @_;
78              
79             die __PACKAGE__.": Nothing to show" unless defined $graph;
80              
81             # Layout is actually done in the background, so the graph
82             # will get updated when the new layout is ready
83             $self->_startGraphLayout ( $graph, fit => 1, %opt );
84             }
85              
86              
87             ######################################################################
88             # Begin the process of creating the graph layout.
89             # Layout is done with a separate process, and it can be time
90             # consuming. So allow the background task to run to completion
91             # without blocking this process. When the layout task is complete,
92             # the graph display is actually updated.
93             ######################################################################
94             sub _startGraphLayout
95             {
96             my ($self, $graph, %opt) = @_;
97              
98             my ($filename,$delete_file) = $self->_createDotFile ( $graph, %opt );
99              
100             # If a previous layout process is running, it needs to be killed
101             $self->_stopGraphLayout( %opt );
102              
103             $self->{layout} = [];
104              
105             if ( ($self->{layout_process} =
106             $self->_startDot ( $filename, delete_file => $delete_file,
107             %opt )) ) {
108             $self->{layout_process}{filename} = $filename;
109             $self->{layout_process}{delete_file} = $delete_file;
110             $self->{layout_process}{opt} = \%opt;
111             $self->_checkGraphLayout ();
112             } else {
113             $self->_showGraphLayout( %opt );
114             }
115             }
116              
117              
118             ######################################################################
119             # Stop a layout task running in the background.
120             # It is important to do a waitpid() on all the background processes
121             # to prevent them from becoming orphans/zombies
122             ######################################################################{
123             sub _stopGraphLayout
124             {
125             my ($self, %opt) = @_;
126              
127             my $proc = $self->{layout_process};
128             return 0 unless defined $proc;
129              
130             if ( defined $proc->{pid} ) {
131             my @sig = qw( TERM TERM TERM TERM KILL );
132             for ( my $i = 0; $i < 5; ++$i ) {
133             last unless defined $proc->{pid};
134             kill $sig[$i], $proc->{pid};
135             if ( $self->_checkGraphLayout( noafter => 1 ) ) {
136             sleep $i+1;
137             }
138             }
139             }
140              
141             unlink $proc->{filename} if ( $proc->{delete_file} );
142             delete $self->{layout_process};
143             }
144              
145              
146             ######################################################################
147             # Check whether the background layout task has finished
148             # Also reads any available output the command has generated to
149             # this point.
150             # If the command is not finished, schedules for this method to be
151             # called again in the future, after some period.
152             ######################################################################
153             sub _checkGraphLayout
154             {
155             my ($self, %opt) = @_;
156              
157             my $proc = $self->{layout_process};
158             if ( !defined $proc ) { return 0; }
159              
160             if ( !defined $proc->{pid} ) { return 0; }
161              
162             my $finished = 0;
163             if ( defined(my $stat = pidStatus($proc->{pid})) ) {
164             # Process has exited
165             if ( $stat == 0xff00 ) {
166             $proc->{error} = "exec failed";
167             }
168             elsif ( $stat > 0x80 ) {
169             $stat >>= 8;
170             }
171             else {
172             if ( $stat & 0x80 ) {
173             $stat &= ~0x80;
174             $proc->{error} = "Killed by signal $stat (coredump)";
175             } else {
176             $proc->{error} = "Kill by signal $stat";
177             }
178             }
179             $proc->{status} = $stat;
180             $finished = 1;
181             }
182              
183             else {
184             my $kill = kill ( 0 => $proc->{pid} );
185             if ( !$kill ) {
186             $proc->{status} = 127;
187             $proc->{error} = "pid $proc->{pid} gone, but no status!";
188             $finished = 1;
189             }
190             }
191              
192             # Read available output...
193             while ( $self->_readGraphLayout () ) { last if !$finished; }
194              
195             # When finished, show the new contents
196             if ( $finished ) {
197             $proc->{pid} = undef;
198             $self->_stopGraphLayout();
199              
200             $self->_showGraphLayout ( %{$proc->{opt}} );
201             return 0;
202             }
203              
204             else {
205             # Not yet finished, so schedule to check again soon
206             if ( !defined($opt{noafter}) || !$opt{noafter} ) {
207             my $checkDelay = 500;
208             if ( defined($proc->{goodread}) ) { $checkDelay = 0; }
209             $self->after ( $checkDelay, sub { $self->_checkGraphLayout(%opt); } );
210             }
211              
212             return 1;
213             }
214             }
215              
216              
217             ######################################################################
218             # Display the new graph layout.
219             # This is called once the layout of the graph has been completed.
220             # The layout data itself is stored as a list layout elements,
221             # typically read directly from the background layout task
222             ######################################################################
223             sub _showGraphLayout
224             {
225             my ($self, %opt) = @_;
226              
227             # Erase old contents
228             unless ( defined $opt{keep} && $opt{keep} ) {
229             $self->delete ( 'all' );
230             delete $self->{fonts}{_default} if exists $self->{fonts}{_default};
231             }
232              
233             # Display new contents
234             $self->_parseLayout ( $self->{layout}, %opt );
235              
236             # Update scroll-region to new bounds
237             $self->_updateScrollRegion( %opt );
238              
239             if ( defined $opt{fit} && $opt{fit} ) {
240             $self->fit();
241             }
242              
243             1;
244             }
245              
246              
247              
248             ######################################################################
249             # Create a (temporary) file on disk containing the graph
250             # in canonical GraphViz/dot format.
251             #
252             # '$graph' can be
253             # - a GraphViz instance
254             # - a scalar containing graph in dot format:
255             # must match /^\s*(?:di)?graph /
256             # - a IO::Handle from which to read a graph in dot format
257             # (contents will be read and converted to a scalar)
258             # - a filename giving a file that contains a graph in dot format
259             #
260             # Returns a filename that contains the DOT description for the graph,
261             # and an additional flag to indicate if the file is temprary
262             ######################################################################
263             sub _createDotFile
264             {
265             my ($self, $graph, %opt) = @_;
266              
267             my $filename = undef;
268             my $delete_file = undef;
269              
270             my $ref = ref($graph);
271             if ( $ref ne '' ) {
272             # A blessed reference
273             if ( $ref->isa('GraphViz') ||
274             UNIVERSAL::can( $graph, 'as_canon') ) {
275             ($filename, my $fh) = $self->_mktemp();
276             eval { $graph->as_canon ( $fh ); };
277             if ( $@ ) {
278             die __PACKAGE__.": Error calling GraphViz::as_canon on $graph: $@";
279             }
280             $fh->close;
281             $delete_file = 1;
282             }
283              
284             elsif ( $ref->isa('IO::Handle') ) {
285             ($filename, my $fh) = $self->_mktemp();
286             while ( <$graph> ) { $fh->print; }
287             $fh->close;
288             $delete_file = 1;
289             }
290             }
291              
292             else {
293             # Not a blessed reference
294              
295             # Try it as a filename
296             # Skip the filename test if it has newlines
297             if ( $graph !~ /\n/m &&
298             -r $graph ) {
299             $filename = $graph;
300             $delete_file = 0;
301             }
302              
303             # Try it as a scalar
304             elsif ( $graph =~ /^\s*(?:di)?graph / ) {
305             ($filename, my $fh) = $self->_mktemp();
306             $fh->print ( $graph );
307             $fh->close;
308             $delete_file = 1;
309             }
310              
311             else {
312             die __PACKAGE__.": Bad graph";
313             }
314             }
315              
316             confess unless defined($filename) && defined($delete_file);
317             ($filename, $delete_file);
318             }
319              
320              
321             ######################################################################
322             # Create a temp file for writing, open a handle to it
323             #
324             ######################################################################
325             {
326             my $_mktemp_count = 0;
327             sub _mktemp
328             {
329             my $tempDir = $ENV{TEMP} || $ENV{TMP} || '/tmp';
330             my $filename = sprintf ( "%s/Tk-GraphViz.dot.$$.%d.dot",
331             $tempDir, $_mktemp_count++ );
332             my $fh = new IO::File ( $filename, 'w' ) ||
333             confess "Can't write temp file: $filename: $!";
334             binmode($fh);
335             ($filename, $fh);
336             }
337             }
338              
339              
340             ######################################################################
341             # Starting running 'dot' (or some other layout command) in the
342             # background, to convert a dot file to layout output format.
343             #
344             ######################################################################
345             sub _startDot
346             {
347             my ($self, $filename, %opt) = @_;
348              
349             confess "Can't read file: $filename"
350             unless -r $filename;
351              
352             my @layout_cmd = $self->_makeLayoutCommand ( $filename, %opt );
353              
354             # Simple, non-asynchronous mode: execute the
355             # process synchnronously and wait for all its output
356             if ( !defined($opt{async}) || !$opt{async} ) {
357             my $pipe = new IO::Pipe;
358             $pipe->reader ( @layout_cmd );
359             while ( <$pipe> ) { push @{$self->{layout}}, $_; }
360             if ( $opt{delete_file} ) {
361             unlink $filename;
362             }
363             return undef;
364             }
365              
366             # Now execute it
367             my $in = new IO::Handle;
368             my $out = new IO::Handle;
369             $in->autoflush;
370              
371             local $@ = undef;
372             my $proc = {};
373             my $ppid = $$;
374             eval {
375             $proc->{pid} = open3 ( $in, $out, '>&STDERR', @layout_cmd );
376             reapPid ( $proc->{pid} );
377              
378             # Fork failure?
379             exit(127) if ( $$ != $ppid );
380             };
381             if ( defined($@) && $@ ne '' ) {
382             $self->{error} = $@;
383             }
384              
385             # Close stdin so child process sees eof on its input
386             $in->close;
387              
388             $proc->{output} = $out;
389             $proc->{buf} = '';
390             $proc->{buflen} = 0;
391             $proc->{eof} = 0;
392              
393             # Enable non-blocking reads on the output
394             $self->_disableBlocking ( $out );
395              
396             return $proc;
397             }
398              
399              
400             ######################################################################
401             # $self->_disableBlocking ( $fh )
402             #
403             # Turn off blocking-mode for the given handle
404             ######################################################################
405             sub _disableBlocking
406             {
407             my ($self, $fh) = @_;
408              
409             my $flags = 0;
410             fcntl ( $fh, &F_GETFL, $flags ) or
411             confess "Can't get flags for handle";
412             $flags = ($flags+0) | O_NONBLOCK;
413             fcntl ( $fh, &F_SETFL, $flags ) or
414             confess "Can't set flags for handle";
415              
416             1;
417             }
418              
419              
420             ######################################################################
421             # Assemble the command for executing dot/neato/etc as a child process
422             # to generate the layout. The layout of the graph will be read from
423             # the command's stdout
424             ######################################################################
425             sub _makeLayoutCommand
426             {
427             my ($self, $filename, %opt) = @_;
428              
429             my $layout_cmd = $opt{layout} || 'dot';
430             my @opts = ();
431              
432             if ( defined $opt{graphattrs} ) {
433             # Add -Gname=value settings to command line
434             my $list = $opt{graphattrs};
435             my $ref = ref($list);
436             die __PACKAGE__.": Expected array reference for graphattrs"
437             unless defined $ref && $ref eq 'ARRAY';
438             while ( my ($key, $val) = splice @$list, 0, 2 ) {
439             push @opts, "-G$key=\"$val\"";
440             }
441             }
442              
443             if ( defined $opt{nodeattrs} ) {
444             # Add -Gname=value settings to command line
445             my $list = $opt{nodeattrs};
446             my $ref = ref($list);
447             die __PACKAGE__.": Expected array reference for nodeattrs"
448             unless defined $ref && $ref eq 'ARRAY';
449             while ( my ($key, $val) = splice @$list, 0, 2 ) {
450             push @opts, "-N$key=\"$val\"";
451             }
452             }
453              
454             if ( defined $opt{edgeattrs} ) {
455             # Add -Gname=value settings to command line
456             my $list = $opt{edgeattrs};
457             my $ref = ref($list);
458             die __PACKAGE__.": Expected array reference for edgeattrs"
459             unless defined $ref && $ref eq 'ARRAY';
460             while ( my ($key, $val) = splice @$list, 0, 2 ) {
461             push @opts, "-E$key=\"$val\"";
462             }
463             }
464              
465             return ($layout_cmd, @opts, '-Tdot', $filename);
466             }
467              
468              
469             ######################################################################
470             # Read data from the background layout process, in a non-blocking
471             # mode. Reads all the data currently available, up to some reasonable
472             # buffer size.
473             ######################################################################
474             sub _readGraphLayout
475             {
476             my ($self) = @_;
477              
478             my $proc = $self->{layout_process};
479             if ( !defined $proc ) { return; }
480              
481             delete $proc->{goodread};
482             my $rv = sysread ( $proc->{output}, $proc->{buf}, 10240,
483             $proc->{buflen} );
484             if ( !defined($rv) && $! == EAGAIN ) {
485             # Would block, don't do anything right now
486             return 0;
487             }
488              
489             elsif ( $rv == 0 ) {
490             # 0 bytes read -- EOF
491             $proc->{eof} = 1;
492             return 0;
493             }
494              
495             else {
496             $proc->{buflen} += $rv;
497             $proc->{goodread} = 1;
498              
499             # Go ahead and split the output that's available now,
500             # so that this part at least is potentially spread out in time
501             # while the background process keeps running.
502             $self->_splitGraphLayout ();
503              
504             return $rv;
505             }
506             }
507              
508              
509             ######################################################################
510             # Split the buffered data read from the background layout task
511             # into individual lines
512             ######################################################################
513             sub _splitGraphLayout
514             {
515             my ($self) = @_;
516              
517             my $proc = $self->{layout_process};
518             if ( !defined $proc ) { return; }
519              
520             my @lines = split ( /\n/, $proc->{buf} );
521            
522             # If not at eof, keep the last line in the buffer
523             if ( !$proc->{eof} ) {
524             $proc->{buf} = pop @lines;
525             $proc->{buflen} = length($proc->{buf});
526             }
527              
528             push @{$self->{layout}}, @lines;
529             }
530              
531              
532             ######################################################################
533             # Parse the layout data in dot 'text' format, as returned
534             # by _dot2layout. Nodes / edges / etc defined in the layout
535             # are added as object in the canvas
536             ######################################################################
537             sub _parseLayout
538             {
539             my ($self, $layoutLines, %opt) = @_;
540              
541             my $directed = 1;
542             my %allNodeAttrs = ();
543             my %allEdgeAttrs = ();
544             my %graphAttrs = ();
545             my ($minX, $minY, $maxX, $maxY) = ( undef, undef, undef, undef );
546             my @saveStack = ();
547              
548             my $accum = undef;
549              
550             foreach ( @$layoutLines ) {
551             s/\r//g; # get rid of any returns ( dos text files)
552              
553             chomp;
554              
555             # Handle line-continuation that gets put in for longer lines,
556             # as well as lines that are continued with commas at the end
557             if ( defined $accum ) {
558             $_ = $accum . $_;
559             $accum = undef;
560             }
561             if ( s/\\\s*$// ||
562             /\,\s*$/ ) {
563             $accum = $_;
564             next;
565             }
566              
567             #STDERR->print ( "gv _parse: $_\n" );
568              
569             if ( /^\s+node \[(.+)\];/ ) {
570             $self->_parseAttrs ( "$1", \%allNodeAttrs );
571             next;
572             }
573              
574             if ( /^\s+edge \[(.+)\];/ ) {
575             $self->_parseAttrs ( "$1", \%allEdgeAttrs );
576             next;
577             }
578              
579             if ( /^\s+graph \[(.+)\];/ ) {
580             $self->_parseAttrs ( "$1", \%graphAttrs );
581             next;
582             }
583              
584             if ( /^\s+subgraph \S+ \{/ ||
585             /^\s+\{/ ) {
586             push @saveStack, [ {%graphAttrs},
587             {%allNodeAttrs},
588             {%allEdgeAttrs} ];
589             delete $graphAttrs{label};
590             delete $graphAttrs{bb};
591             next;
592             }
593              
594             if ( /^\s*\}/ ) {
595             # End of a graph section
596             if ( @saveStack ) {
597             # Subgraph
598             if ( defined($graphAttrs{bb}) && $graphAttrs{bb} ne '' ) {
599             my ($x1,$y1,$x2,$y2) = split ( /\s*,\s*/, $graphAttrs{bb} );
600             $minX = min($minX,$x1);
601             $minY = min($minY,$y1);
602             $maxX = max($maxX,$x2);
603             $maxY = max($maxY,$y2);
604             $self->_createSubgraph ( $x1, $y1, $x2, $y2, %graphAttrs );
605             }
606              
607             my ($g,$n,$e) = @{pop @saveStack};
608             %graphAttrs = %$g;
609             %allNodeAttrs = %$n;
610             %allEdgeAttrs = %$e;
611             next;
612             } else {
613             # End of the graph
614             # Create any whole-graph label
615             if ( defined($graphAttrs{bb}) ) {
616             my ($x1,$y1,$x2,$y2) = split ( /\s*,\s*/, $graphAttrs{bb} );
617             $minX = min($minX,$x1);
618             $minY = min($minY,$y1);
619             $maxX = max($maxX,$x2);
620             $maxY = max($maxY,$y2);
621              
622             # delete bb attribute so rectangle is not drawn around whole graph
623             delete $graphAttrs{bb};
624              
625             $self->_createSubgraph ( $x1, $y1, $x2, $y2, %graphAttrs );
626             }
627             last;
628             }
629             }
630              
631             if ( /\s+(.+) \-[\>\-] (.+) \[(.+)\];/ ) {
632             # Edge
633             my ($n1,$n2,$attrs) = ($1,$2,$3);
634             my %edgeAttrs = %allEdgeAttrs;
635             $self->_parseAttrs ( $attrs, \%edgeAttrs );
636              
637             my ($x1,$y1,$x2,$y2) = $self->_createEdge ( $n1, $n2, %edgeAttrs );
638             $minX = min($minX,$x1);
639             $minY = min($minY,$y1);
640             $maxX = max($maxX,$x2);
641             $maxY = max($maxY,$y2);
642             next;
643             }
644              
645             if ( /\s+(.+) \[(.+)\];/ ) {
646             # Node
647             my ($name,$attrs) = ($1,$2);
648              
649             # Get rid of any leading/tailing quotes
650             $name =~ s/^\"//;
651             $name =~ s/\"$//;
652              
653             my %nodeAttrs = %allNodeAttrs;
654             $self->_parseAttrs ( $attrs, \%nodeAttrs );
655              
656             my ($x1,$y1,$x2,$y2) = $self->_createNode ( $name, %nodeAttrs );
657             $minX = min($minX,$x1);
658             $minY = min($minY,$y1);
659             $maxX = max($maxX,$x2);
660             $maxY = max($maxY,$y2);
661             next;
662             }
663              
664             }
665              
666             }
667              
668              
669             ######################################################################
670             # Parse attributes of a node / edge / graph / etc,
671             # store the values in a hash
672             ######################################################################
673             sub _parseAttrs
674             {
675             my ($self, $attrs, $attrHash) = @_;
676              
677             while ( $attrs =~ s/^,?\s*([^=]+)=// ) {
678             my ($key) = ($1);
679              
680             # Scan forward until end of value reached -- the first
681             # comma not in a quoted string.
682             # Probably a more efficient method for doing this, but...
683             my @chars = split(//, $attrs);
684             my $quoted = 0;
685             my $val = '';
686             my $last = '';
687             my ($i,$n);
688             for ( ($i,$n) = (0, scalar(@chars)); $i < $n; ++$i ) {
689             my $ch = $chars[$i];
690             last if $ch eq ',' && !$quoted;
691             if ( $ch eq '"' ) { $quoted = !$quoted unless $last eq '\\'; }
692             $val .= $ch;
693             $last = $ch;
694             }
695             $attrs = join('', splice ( @chars, $i ) );
696              
697             # Strip leading and trailing ws in key and value
698             $key =~ s/^\s+|\s+$//g;
699             $val =~ s/^\s+|\s+$//g;
700              
701             if ( $val =~ /^\"(.*)\"$/ ) { $val = $1; }
702             $val =~ s/\\\"/\"/g; # Un-escape quotes
703             $attrHash->{$key} = $val;
704             }
705              
706             }
707              
708              
709             ######################################################################
710             # Create a subgraph / cluster
711             #
712             ######################################################################
713             sub _createSubgraph
714             {
715             my ($self, $x1, $y1, $x2, $y2, %attrs) = @_;
716              
717             my $label = $attrs{label};
718             my $color = $attrs{color} || 'black';
719              
720             # Want box to be filled with background color by default, so that
721             # it is 'clickable'
722             my $fill = $self->cget('-background');
723              
724             my $tags = [ subgraph => $label, %attrs ];
725              
726             # Get/Check a valid color
727             $color = $self->_tryColor($color);
728              
729             my @styleArgs;
730             if( $attrs{style} ){
731             my $style = $attrs{style};
732             if ( $style =~ /dashed/i ) {
733             @styleArgs = (-dash => '-');
734             }
735             elsif ( $style =~ /dotted/ ) {
736             @styleArgs = (-dash => '.');
737             }
738             elsif ( $style =~ /filled/ ) {
739             $fill = ( $self->_tryColor($attrs{fillcolor}) || $color );
740             }
741             elsif( $style =~ /bold/ ) {
742             # Bold outline, gets wider line
743             push @styleArgs, (-width => 2);
744             }
745             }
746              
747             # Create the box if coords are defined
748             if( $attrs{bb} ) {
749             my $id = $self->createRectangle ( $x1, -1 * $y2, $x2, -1 * $y1,
750             -outline => $color,
751             -fill => $fill, @styleArgs,
752             -tags => $tags );
753             $self->lower($id); # make sure it doesn't obscure anything
754             }
755              
756             # Create the label, if defined
757             if ( defined($attrs{label}) ) {
758             my $lp = $attrs{lp} || '';
759             my ($x,$y) = split(/\s*,\s*/,$lp);
760             if ( $lp eq '' ) { ($x,$y) = ($x1, $y2); }
761              
762             $label =~ s/\\n/\n/g;
763             $tags->[0] = 'subgraphlabel'; # Replace 'subgraph' w/ 'subgraphlabel'
764             my @args = ( $x, -1 * $y,
765             -text => $label,
766             -tags => $tags );
767             push @args, ( -state => 'disabled' );
768             if ( $lp eq '' ) { push @args, ( -anchor => 'nw' ); }
769              
770             $self->createText ( @args );
771             }
772             }
773              
774              
775             ######################################################################
776             # Create a node
777             #
778             ######################################################################
779             sub _createNode
780             {
781             my ($self, $name, %attrs) = @_;
782              
783             my ($x,$y) = split(/,/, $attrs{pos});
784             my $dpi = $self->{dpi};
785             my $w = $attrs{width} * $dpi; #inches
786             my $h = $attrs{height} * $dpi; #inches
787             my $x1 = $x - $w/2.0;
788             my $y1 = $y - $h/2.0;
789             my $x2 = $x + $w/2.0;
790             my $y2 = $y + $h/2.0;
791              
792             my $label = $attrs{label};
793             $label = $attrs{label} = $name unless defined $label;
794             if ( $label eq '\N' ) { $label = $attrs{label} = $name; }
795              
796             #STDERR->printf ( "createNode: $name \"$label\" ($x1,$y1) ($x2,$y2)\n" );
797              
798              
799             # Node shape
800             my $tags = [ node => $name, %attrs ];
801              
802             my @args = ();
803              
804             my $outline = $self->_tryColor($attrs{color}) || 'black';
805             my $fill = $self->_tryColor($attrs{fillcolor}) || $self->cget('-background');
806             my $fontcolor = $self->_tryColor($attrs{fontcolor}) || 'black';
807             my $shape = $attrs{shape} || '';
808              
809             foreach my $style ( split ( /,/, $attrs{style}||'' ) ) {
810             if ( $style eq 'filled' ) {
811             $fill = ( $self->_tryColor($attrs{fillcolor}) ||
812             $self->_tryColor($attrs{color}) ||
813             'lightgrey' );
814             }
815             elsif ( $style eq 'invis' ) {
816             $outline = undef;
817             $fill = undef;
818             }
819             elsif ( $style eq 'dashed' ) {
820             push @args, -dash => '--';
821             }
822             elsif ( $style eq 'dotted' ) {
823             push @args, -dash => '.';
824             }
825             elsif ( $style eq 'bold' ) {
826             push @args, -width => 2.0;
827             }
828             elsif ( $style =~ /setlinewidth\((\d+)\)/ ) {
829             push @args, -width => "$1";
830             }
831             }
832              
833             push @args, -outline => $outline if ( defined($outline) );
834             push @args, -fill => $fill if ( defined($fill) );
835              
836             my $orient = $attrs{orientation} || 0.0;
837              
838             # Node label
839             $label =~ s/\\n/\n/g;
840              
841             unless ( $shape eq 'record' ) {
842             # Normal non-record node types
843             $self->_createShapeNode ( $shape, $x1, -1*$y2, $x2, -1*$y1,
844             $orient, @args, -tags => $tags );
845              
846             $label = undef if ( $shape eq 'point' );
847              
848             # Node label
849             if ( defined $label ) {
850             $tags->[0] = 'nodelabel'; # Replace 'node' w/ 'nodelabel'
851             @args = ( ($x1 + $x2)/2, -1*($y2 + $y1)/2, -text => $label,
852             -anchor => 'center', -justify => 'center',
853             -tags => $tags, -fill => $fontcolor );
854             push @args, ( -state => 'disabled' );
855             $self->createText ( @args );
856             }
857             }
858             else {
859             # Record node types
860             $self->_createRecordNode ( $label, %attrs, tags => $tags );
861             }
862              
863             # Return the bounding box of the node
864             ($x1,$y1,$x2,$y2);
865             }
866              
867              
868             ######################################################################
869             # Create an item of a specific shape, generally used for creating
870             # node shapes.
871             ######################################################################
872             my %polyShapes =
873             ( box => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
874             rect => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
875             rectangle => [ [ 0, 0 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
876             triangle => [ [ 0, .75 ], [ 0.5, 0 ], [ 1, .75 ] ],
877             invtriangle => [ [ 0, .25 ], [ 0.5, 1 ], [ 1, .25 ] ],
878             diamond => [ [ 0, 0.5 ], [ 0.5, 1.0 ], [ 1.0, 0.5 ], [ 0.5, 0.0 ] ],
879             pentagon => [ [ .5, 0 ], [ 1, .4 ], [ .75, 1 ], [ .25, 1 ], [ 0, .4 ] ],
880             hexagon => [ [ 0, .5 ], [ .33, 0 ], [ .66, 0 ],
881             [ 1, .5 ], [ .66, 1 ], [ .33, 1 ] ],
882             septagon => [ [ .5, 0 ], [ .85, .3 ], [ 1, .7 ], [ .75, 1 ],
883             [ .25, 1 ], [ 0, .7 ], [ .15, .3 ] ],
884             octagon => [ [ 0, .3 ], [ 0, .7 ], [ .3, 1 ], [ .7, 1 ],
885             [ 1, .7 ], [ 1, .3 ], [ .7, 0 ], [ .3, 0 ] ],
886             trapezium => [ [ 0, 1 ], [ .21, 0 ], [ .79, 0 ], [ 1, 1 ] ],
887             invtrapezium => [ [ 0, 0], [ .21, 1 ], [ .79, 1 ], [ 1, 0 ] ],
888             parallelogram => [ [ 0, 1 ], [ .20, 0 ], [ 1, 0 ], [ .80, 1 ] ],
889             house => [ [ 0, .9 ], [ 0, .5 ], [ .5, 0 ], [ 1, .5 ], [ 1, .9 ] ],
890             invhouse => [ [ 0, .1 ], [ 0, .5 ], [ .5, 1 ], [ 1, .5 ], [ 1, .1 ] ],
891             folder => [ [ 0, 0.1 ], [ 0, 1 ], [ 1, 1 ], [ 1, 0.1 ],
892             [0.9, 0 ], [0.7 , 0 ] , [0.6, 0.1 ] ],
893             component => [ [ 0, 0 ], [ 0, 0.1 ], [ 0.03, 0.1 ], [ -0.03, 0.1 ],
894             [ -0.03, 0.3 ], [ 0.03 , 0.3 ], [ 0.03, 0.1 ],
895             [ 0.03 , 0.3 ], [ 0 , 0.3 ], [ 0, 0.7 ], [ 0.03, 0.7 ],
896             [ -0.03, 0.7 ], [ -0.03, 0.9 ], [ 0.03 , 0.9 ],
897             [ 0.03, 0.7 ], [ 0.03 , 0.9 ], [ 0 , 0.9 ],
898             [ 0, 1 ], [ 1, 1 ], [ 1, 0 ] ],
899             );
900              
901             sub _createShapeNode
902             {
903             my ($self, $shape, $x1, $y1, $x2, $y2, $orient, %args) = @_;
904              
905             #STDERR->printf ( "createShape: $shape ($x1,$y1) ($x2,$y2)\n" );
906             my $id = undef;
907              
908             my @extraArgs = ();
909              
910             # Special handling for recursive calls to create periphery shapes
911             # (for double-, triple-, etc)
912             my $periphShape = $args{_periph};
913             if ( defined $periphShape ) {
914             delete $args{_periph};
915              
916             # Periphery shapes are drawn non-filled, so they are
917             # not clickable
918             push @extraArgs, ( -fill => undef, -state => 'disabled' );
919             };
920              
921              
922             # Simple shapes: defined in the polyShape hash
923             if ( exists $polyShapes{$shape} ) {
924             $id = $self->_createPolyShape ( $polyShapes{$shape},
925             $x1, $y1, $x2, $y2, $orient,
926             %args, @extraArgs );
927             }
928              
929             # Other special-case shapes:
930              
931             elsif ( $shape =~ s/^double// ) {
932             my $diam = max(abs($x2-$x1),abs($y2-$y1));
933             my $inset = max(2,min(5,$diam*.1));
934             return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
935             %args, _periph => [ 1, $inset ] );
936             }
937              
938             elsif ( $shape =~ s/^triple// ) {
939             my $diam = max(abs($x2-$x1),abs($y2-$y1));
940             my $inset = min(5,$diam*.1);
941             return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
942             %args, _periph => [ 2, $inset ] );
943             }
944              
945             elsif ( $shape eq 'plaintext' ) {
946             # Don't draw an outline for plaintext
947             $id = 0;
948             }
949              
950             elsif ( $shape eq 'point' ) {
951             # Draw point as a small oval
952             $shape = 'oval';
953             }
954              
955             elsif ( $shape eq 'ellipse' || $shape eq 'circle' ) {
956             $shape = 'oval';
957             }
958              
959             elsif ( $shape eq 'oval' ) {
960              
961             }
962              
963             elsif ( $shape eq '' ) {
964             # Default shape = ellipse
965             $shape = 'oval';
966             }
967              
968             else {
969             warn __PACKAGE__.": Unsupported shape type: '$shape', using box";
970             }
971              
972             if ( !defined $id ) {
973             if ( $shape eq 'oval' ) {
974             $id = $self->createOval ( $x1, $y1, $x2, $y2, %args, @extraArgs );
975             } else {
976             $id = $self->createRectangle ( $x1, $y1, $x2, $y2, %args, @extraArgs );
977             }
978             }
979              
980             # Need to create additional periphery shapes?
981             if ( defined $periphShape ) {
982             # This method of stepping in a fixed ammount in x and y is not
983             # correct, because the aspect of the overall shape changes...
984             my $inset = $periphShape->[1];
985             $x1 += $inset;
986             $y1 += $inset;
987             $x2 -= $inset;
988             $y2 -= $inset;
989             if ( --$periphShape->[0] > 0 ) {
990             @extraArgs = ( _periph => $periphShape );
991             } else {
992             @extraArgs = ();
993             }
994             return $self->_createShapeNode ( $shape, $x1, $y1, $x2, $y2, $orient,
995             %args, @extraArgs );
996             }
997              
998             $id;
999             }
1000              
1001              
1002             ######################################################################
1003             # Create an arbitrary polygonal shape, using a set of unit points.
1004             # The points will be scaled to fit the given bounding box.
1005             ######################################################################
1006             sub _createPolyShape
1007             {
1008             my ($self, $upts, $x1, $y1, $x2, $y2, $orient, %args) = @_;
1009              
1010             my ($ox, $oy) = 1.0;
1011             if ( $orient != 0 ) {
1012             $orient %= 360.0;
1013              
1014             # Convert to radians, and rotate ccw instead of cw
1015             $orient *= 0.017453; # pi / 180.0
1016             my $c = cos($orient);
1017             my $s = sin($orient);
1018             my $s_plus_c = $s + $c;
1019             my @rupts = ();
1020             foreach my $upt ( @$upts ) {
1021             my ($ux, $uy) = @$upt;
1022             $ux -= 0.5;
1023             $uy -= 0.5;
1024              
1025             #STDERR->printf ( "orient: rotate (%.2f,%.2f) by %g deg\n",
1026             # $ux, $uy, $orient / 0.017453 );
1027             $ux = $ux * $c - $uy * $s; # x' = x cos(t) - y sin(t)
1028             $uy = $uy * $s_plus_c; # y' = y sin(t) + y cos(t)
1029             #STDERR->printf ( " --> (%.2f,%.2f)\n", $ux, $uy );
1030              
1031             $ux += 0.5;
1032             $uy += 0.5;
1033              
1034             push @rupts, [ $ux, $uy ];
1035             }
1036             $upts = \@rupts;
1037             }
1038              
1039             my $dx = $x2 - $x1;
1040             my $dy = $y2 - $y1;
1041             my @pts = ();
1042             foreach my $upt ( @$upts ) {
1043             my ($ux, $uy ) = @$upt;
1044              
1045             push @pts, ( $x1 + $ux*$dx, $y1 + $uy*$dy );
1046             }
1047             $self->createPolygon ( @pts, %args );
1048             }
1049              
1050              
1051             ######################################################################
1052             # Draw the node record shapes
1053             ######################################################################
1054             sub _createRecordNode
1055             {
1056             my ($self, $label, %attrs) = @_;
1057              
1058             my $tags = $attrs{tags};
1059              
1060             # Get Rectangle Coords
1061             my $rects = $attrs{rects};
1062             my @rects = split(' ', $rects);
1063             my @rectsCoords = map [ split(',',$_) ], @rects;
1064              
1065             # Setup to parse the label (Label parser object created using Parse::Yapp)
1066             my $parser = new Tk::GraphViz::parseRecordLabel();
1067             $parser->YYData->{INPUT} = $label;
1068              
1069             # And parse it...
1070             my $structure = $parser->YYParse
1071             ( yylex => \&Tk::GraphViz::parseRecordLabel::Lexer,
1072             yyerror => \&Tk::GraphViz::parseRecordLabel::Error,
1073             yydebug => 0 );
1074             die __PACKAGE__.": Error Parsing Record Node Label '$label'\n"
1075             unless $structure;
1076              
1077             my @labels = @$structure;
1078              
1079             # Draw the rectangles
1080             my $portIndex = 1; # Ports numbered from 1. This is used for the port name
1081             # in the tags, if no port name is defined in the dot file
1082             foreach my $rectCoords ( @rectsCoords ) {
1083             my ($port, $text) = %{shift @labels};
1084              
1085             # use port index for name, if one not defined
1086             $port = $portIndex unless ( $port =~ /\S/);
1087              
1088             my %portTags = (@$tags); # copy of tags
1089             $portTags{port} = $port;
1090              
1091             # get rid of leading trailing whitespace
1092             $text =~ s/^\s+//;
1093             $text =~ s/\s+$//;
1094              
1095             $portTags{label} = $text;
1096              
1097             my ($x1,$y1,$x2,$y2) = @$rectCoords;
1098             $self->createRectangle ( $x1, -$y1, $x2, -$y2, -tags => [%portTags] );
1099              
1100             # Find midpoint for label anchor point
1101             my $midX = ($x1 + $x2)/2;
1102             my $midY = ($y1 + $y2)/2;
1103             $portTags{nodelabel} = delete $portTags{node}; # Replace 'node' w/ 'nodelabel'
1104             $self->createText ( $midX, -$midY, -text => $text, -tags => [%portTags]);
1105              
1106             $portIndex++;
1107             }
1108             }
1109              
1110              
1111             ######################################################################
1112             # Create a edge
1113             #
1114             ######################################################################
1115             sub _createEdge
1116             {
1117             my ($self, $n1, $n2, %attrs) = @_;
1118              
1119             my $x1 = undef;
1120             my $y1 = undef;
1121             my $x2 = undef;
1122             my $y2 = undef;
1123              
1124             my $tags = [ edge => "$n1 $n2",
1125             node1 => $n1, node2 => $n2,
1126             %attrs ];
1127              
1128             # Parse the edge position
1129             my $pos = $attrs{pos} || return;
1130             my ($startEndCoords,@coords) = $self->_parseEdgePos ( $pos );
1131             my $arrowhead = $attrs{arrowhead};
1132             my $arrowtail = $attrs{arrowtail};
1133              
1134             my @args = ();
1135              
1136             # Convert Biezer control points to 4 real points to smooth against
1137             # Canvas line smoothing doesn't use beizers, so we supply more points
1138             # along the manually-calculated bezier points.
1139              
1140             @coords = map @$_, @coords; #flatten coords array
1141              
1142             my @newCoords;
1143             my ($startIndex, $stopIndex);
1144             $startIndex = 0;
1145             $stopIndex = 7;
1146             my $lastFlag = 0;
1147             my @controlPoints;
1148             while($stopIndex <= $#coords){
1149             @controlPoints = @coords[$startIndex..$stopIndex];
1150              
1151             # If this is the last set, set the flag, so we will get
1152             # the last point
1153             $lastFlag = 1 if( $stopIndex == $#coords);
1154              
1155             push @newCoords,
1156             $self->_bezierInterpolate(\@controlPoints, 0.1, $lastFlag);
1157              
1158             $startIndex += 6;
1159             $stopIndex += 6;
1160             }
1161              
1162             # Add start/end coords
1163             if(defined($startEndCoords->{s})){
1164             unshift @newCoords, @{ $startEndCoords->{s} }; # put at the begining
1165             }
1166             if(defined($startEndCoords->{e})){
1167             push @newCoords, @{ $startEndCoords->{e}}; # put at the end
1168             }
1169              
1170             # Convert Sign of y-values of coords, record min/max
1171             for( my $i = 0; $i < @newCoords; $i+= 2){
1172             my ($x,$y) = @newCoords[$i, $i+1];
1173             push @args, $x, -1*$y;
1174             #printf ( " $x,$y\n" );
1175             $x1 = min($x1, $x);
1176             $y1 = min($y1, $y);
1177             $x2 = max($x2, $x);
1178             $y2 = max($y2, $y);
1179             }
1180              
1181             #STDERR->printf ( "createEdge: $n1->$n2 ($x1,$y1) ($x2,$y2)\n" );
1182             if ( defined($startEndCoords->{s}) &&
1183             defined($startEndCoords->{e}) &&
1184             (not defined $arrowhead) &&
1185             (not defined $arrowtail) ) { # two-sided arrow
1186             push @args, -arrow => 'both';
1187             }
1188             elsif ( defined($startEndCoords->{e}) &&
1189             (not defined $arrowhead) ) { # arrow just at the end
1190             push @args, -arrow => 'last';
1191             }
1192             elsif ( defined($startEndCoords->{s}) &&
1193             (not defined $arrowtail) ) { # arrow just at the start
1194             push @args, -arrow => 'first';
1195             }
1196              
1197             my $color = $attrs{color};
1198              
1199             foreach my $style ( split(/,/, $attrs{style}||'') ) {
1200             if ( $style eq 'dashed' ) {
1201             push @args, -dash => '--';
1202             }
1203             elsif ( $style eq 'dotted' ) {
1204             push @args, -dash => ',';
1205             }
1206             elsif ( $style =~ /setlinewidth\((\d+)\)/ ) {
1207             push @args, -width => "$1";
1208             }
1209             elsif ( $style =~ /invis/ ) {
1210             # invisible edge, make same as background
1211             $color = $self->cget('-background');
1212             }
1213             }
1214              
1215             push @args, -fill => ( $self->_tryColor($color) || 'black' );
1216              
1217             # Create the line
1218             $self->createLine ( @args, -smooth => 1, -tags => $tags );
1219              
1220             # Create the arrowhead (at end of line)
1221             if ( defined($arrowhead) && $arrowhead =~ /^(.*)dot$/ ) {
1222             my $modifier = $1;
1223              
1224             # easy implementation for calculating the arrow position
1225             my ($x1, $y1) = @newCoords[(@newCoords-2), (@newCoords-1)];
1226             my ($x2, $y2) = @newCoords[(@newCoords-4), (@newCoords-3)];
1227             my $x = ($x1 + $x2)/2;
1228             my $y = ($y1 + $y2)/2;
1229             my @args = ($x-4, -1*($y-4), $x+4, -1*($y+4));
1230              
1231             # check for modifiers
1232             if ($modifier eq "o") {
1233             push @args, -fill => $self->cget('-background');
1234             } else {
1235             push @args, -fill => ($self->_tryColor($color) || 'black');
1236             }
1237              
1238             # draw
1239             $self->createOval ( @args );
1240             }
1241              
1242             # Create the arrowtail (at start of line)
1243             if ( defined($arrowtail) && $arrowtail =~ /^(.*)dot$/ ) {
1244             my $modifier = $1;
1245              
1246             # easy implementation for calculating the arrow position
1247             my ($x1, $y1) = @newCoords[0, 1];
1248             my ($x2, $y2) = @newCoords[2, 3];
1249             my $x = ($x1 + $x2)/2;
1250             my $y = ($y1 + $y2)/2;
1251             my @args = ($x-4, -1*($y-4), $x+4, -1*($y+4));
1252              
1253             # check for modifiers
1254             if ($modifier eq "o") {
1255             push @args, -fill => $self->cget('-background');
1256             } else {
1257             push @args, -fill => ($self->_tryColor($color) || 'black');
1258             }
1259              
1260             # draw
1261             $self->createOval ( @args );
1262             }
1263              
1264             # Create optional label
1265             my $label = $attrs{label};
1266             my $lp = $attrs{lp};
1267             if ( defined($label) && defined($lp) ) {
1268             $label =~ s/\\n/\n/g;
1269             $tags->[0] = 'edgelabel'; # Replace 'edge' w/ 'edgelabel'
1270             my ($x,$y) = split(/,/, $lp);
1271             my @args = ( $x, -1*$y, -text => $label, -tags => $tags,
1272             -justify => 'center' );
1273             push @args, ( -state => 'disabled' );
1274             $self->createText ( @args );
1275             }
1276              
1277              
1278             # Return the bounding box of the edge
1279             ($x1,$y1,$x2,$y2);
1280             }
1281              
1282              
1283             ######################################################################
1284             # Parse the coordinates for an edge from the 'pos' string
1285             #
1286             ######################################################################
1287             sub _parseEdgePos
1288             {
1289             my ($self, $pos) = @_;
1290              
1291             # Note: Arrows can be at the start and end, i.e.
1292             # pos = s,410,104 e,558,59 417,98 ...
1293             # (See example graph 'graphs/directed/ldbxtried.dot')
1294              
1295             # hash of start/end coords
1296             # Example: e => [ 12, 3 ], s = [ 1, 3 ]
1297             my %startEnd;
1298              
1299             # Process all start/end points (could be none, 1, or 2)
1300             while ( $pos =~ s/^([se])\s*\,\s*(\d+)\s*\,\s*(\d+)\s+// ) {
1301             my ($where, $x, $y) = ($1, $2, $3);
1302             $startEnd{$where} = [ $x, $y ];
1303             }
1304              
1305             my @loc = split(/ |,/, $pos);
1306             my @coords = ();
1307             while ( @loc >= 2 ) {
1308             my ($x,$y) = splice(@loc,0,2);
1309             push @coords, [$x,$y];
1310             }
1311              
1312             (\%startEnd, @coords);
1313             }
1314              
1315              
1316             ######################################################################
1317             # Sub to make points on a curve, based on Bezier control points
1318             # Inputs:
1319             # $controlPoints: Array of control points (x/y P0,1,2,3)
1320             # $tinc: Increment to use for t (t = 0 to 1 )
1321             # $lastFlag: Flag = 1 to generate the last point (where t = 1)
1322             #
1323             # Output;
1324             # @outputPoints: Array of points along the biezier curve
1325             #
1326             # Equations used
1327             #Found Bezier Equations at http://pfaedit.sourceforge.net/bezier.html
1328             #
1329             # A cubic Bezier curve may be viewed as:
1330             # x = ax*t3 + bx*t2 + cx*t +dx
1331             # y = ay*t3 + by*t2 + cy*t +dy
1332             #
1333             # Where
1334             #
1335             # dx = P0.x
1336             # dy = P0.y
1337             # cx = 3*P1.x-3*P0.x
1338             # cy = 3*P1.y-3*P0.y
1339             # bx = 3*P2.x-6*P1.x+3*P0.x
1340             # by = 3*P2.y-6*P1.y+3*P0.y
1341             # ax = P3.x-3*P2.x+3*P1.x-P0.x
1342             # ay = P3.y-3*P2.y+3*P1.y-P0.y
1343             ######################################################################
1344             sub _bezierInterpolate
1345             {
1346             my ($self,$controlPoints, $tinc, $lastFlag) = @_;
1347              
1348             # interpolation constants
1349             my ($ax,$bx,$cx,$dx);
1350             my ($ay,$by,$cy,$dy);
1351              
1352             $dx = $controlPoints->[0];
1353             $cx = 3*$controlPoints->[2] - 3*$controlPoints->[0];
1354             $bx = 3*$controlPoints->[4] - 6*$controlPoints->[2] + 3*$controlPoints->[0];
1355             $ax = ( $controlPoints->[6] - 3*$controlPoints->[4] + 3*$controlPoints->[2]
1356             - $controlPoints->[0] );
1357              
1358             $dy = $controlPoints->[1];
1359             $cy = 3*$controlPoints->[3] - 3*$controlPoints->[1];
1360             $by = 3*$controlPoints->[5] - 6*$controlPoints->[3] + 3*$controlPoints->[1];
1361             $ay = ( $controlPoints->[7] - 3*$controlPoints->[5] + 3*$controlPoints->[3]
1362             - $controlPoints->[1] );
1363              
1364             my @outputPoints;
1365             for( my $t=0; $t <= 1; $t+=$tinc ){
1366             # don't do the last point unless lastflag set
1367             next if($t == 1 && !$lastFlag);
1368              
1369             # Compute X point
1370             push @outputPoints, ($ax*$t**3 + $bx*$t**2 + $cx*$t +$dx);
1371              
1372             # Compute Y point
1373             push @outputPoints, ($ay*$t**3 + $by*$t**2 + $cy*$t +$dy);
1374             }
1375              
1376             return @outputPoints;
1377             }
1378              
1379              
1380             ######################################################################
1381             # Update scroll region to new bounds, to encompass
1382             # the entire contents of the canvas
1383             ######################################################################
1384             sub _updateScrollRegion
1385             {
1386             my ($self) = @_;
1387              
1388             # Ignore passed in in bbox, get a new one
1389             my ($x1,$y1,$x2,$y2) = $self->bbox('all');
1390             return 0 unless defined $x1;
1391              
1392             # Set canvas size from graph bounding box
1393             my $m = 0;#$self->{margin};
1394             $self->configure ( -scrollregion => [ $x1-$m, $y1-$m, $x2+$m, $y2+$m ],
1395             -confine => 1 );
1396              
1397             # Reset original scale factor
1398             $self->{_scaled} = 1.0;
1399              
1400             1;
1401             }
1402              
1403              
1404             ######################################################################
1405             # Update the scale factor
1406             #
1407             # Called by operations that do scaling
1408             ######################################################################
1409             sub _scaleAndMoveView
1410             {
1411             my ($self, $scale, $x, $y) = @_;
1412              
1413             $self->scale ( 'all' => 0, 0, $scale, $scale );
1414             my $new_scaled = $self->{_scaled} * $scale;
1415             #STDERR->printf ( "\nscaled: %s -> %s\n",
1416             # $self->{_scaled}, $new_scaled );
1417              
1418             # Scale the fonts:
1419             my $fonts = $self->{fonts};
1420             #print "new_scaled = $new_scaled\n";
1421             foreach my $fontName ( keys %$fonts ) {
1422             my $font = $fonts->{$fontName}{font};
1423             my $origSize = $fonts->{$fontName}{origSize};
1424              
1425             # Flag to indicate size is negative (i.e. specified in pixels)
1426             my $negativeSize = $origSize < 0 ? -1 : 1;
1427             $origSize = abs($origSize); # Make abs value for finding scale
1428              
1429             # Fonts can't go below size 2, or they suddenly jump up to size 6...
1430             my $newSize = max(2,int( $origSize*$new_scaled + 0.5));
1431              
1432             $newSize *= $negativeSize;
1433              
1434             $font->configure ( -size => $newSize );
1435             #print "Font '$fontName' Origsize = $origSize, newsize $newSize, actual size ".$font->actual(-size)."\n";
1436             }
1437              
1438             $self->{_scaled} = $new_scaled;
1439              
1440             # Reset scroll region
1441             my @sr = $self->cget( '-scrollregion' );
1442             my $sr = \@sr;
1443             if ( @sr == 1 ) { $sr = $sr[0]; }
1444             $_ *= $scale foreach ( @$sr );
1445             $self->configure ( -scrollregion => $sr );
1446              
1447             # Change the view to center on correct area
1448             # $x and $y are expected to be coords in the pre-scaled system
1449             my ($left, $right) = $self->xview;
1450             my ($top, $bot) = $self->yview;
1451             my $xpos = ($x*$scale-$sr->[0])/($sr->[2]-$sr->[0]) - ($right-$left)/2.0;
1452             my $ypos = ($y*$scale-$sr->[1])/($sr->[3]-$sr->[1]) - ($bot-$top)/2.0;
1453             $self->xview( moveto => $xpos );
1454             $self->yview( moveto => $ypos );
1455              
1456             #($left, $right) = $self->xview;
1457             #($top, $bot) = $self->yview;
1458             #STDERR->printf( "scaled: midx=%s midy=%s\n",
1459             # ($left+$right)/2.0, ($top+$bot)/2.0 );
1460             1;
1461             }
1462              
1463              
1464             ######################################################################
1465             # Setup some standard bindings.
1466             #
1467             # This enables some standard useful functionality for scrolling,
1468             # zooming, etc.
1469             #
1470             # The bindings need to interfere as little as possible with typical
1471             # bindings that might be employed in an application using this
1472             # widget (e.g. Button-1).
1473             #
1474             # Also, creating these bindings (by calling this method) is strictly
1475             # optional.
1476             ######################################################################
1477             sub createBindings
1478             {
1479             my ($self, %opt) = @_;
1480              
1481             if ( scalar(keys %opt) == 0 # Empty options list
1482             || defined $opt{'-default'} && $opt{'-default'} ) {
1483              
1484             # Default zoom bindings
1485             $opt{'-zoom'} = 1;
1486              
1487             # Default scroll bindings
1488             $opt{'-scroll'} = 1;
1489              
1490             # Key-pad bindings
1491             $opt{'-keypad'} = 1;
1492             }
1493              
1494             if ( defined $opt{'-zoom'} ) {
1495             $self->_createZoomBindings( %opt );
1496             }
1497              
1498             if ( defined $opt{'-scroll'} ) {
1499             $self->_createScrollBindings( %opt );
1500             }
1501              
1502             if ( defined $opt{'-keypad'} ) {
1503             $self->_createKeypadBindings( %opt );
1504             }
1505              
1506             }
1507              
1508              
1509             ######################################################################
1510             # Setup bindings for zooming operations
1511             #
1512             # These are bound to a specific mouse button and optional modifiers.
1513             # - To zoom in: drag out a box from top-left/right to bottom-right/left
1514             # enclosing the new region to display
1515             # - To zoom out: drag out a box from bottom-left/right to top-right/left.
1516             # size of the box determines zoom out factor.
1517             ######################################################################
1518             sub _createZoomBindings
1519             {
1520             my ($self, %opt) = @_;
1521              
1522             # Interpret zooming options
1523              
1524             # What mouse button + modifiers starts zoom?
1525             my $zoomSpec = $opt{'-zoom'};
1526             die __PACKAGE__.": No -zoom option" unless defined $zoomSpec;
1527             if ( $zoomSpec =~ /^\<.+\>$/ ) {
1528             # This should be a partial bind event spec, e.g. <1>, or
1529             # -- it must end in a button number
1530             die __PACKAGE__.": Illegal -zoom option"
1531             unless ( $zoomSpec =~ /^\<.+\-\d\>$/ ||
1532             $zoomSpec =~ /^\<\d\>$/ );
1533             }
1534             else {
1535             # Anything else: use the default
1536             $zoomSpec = '';
1537             }
1538              
1539             # Color for zoom rect
1540             my $zoomColor = $opt{'-zoomcolor'} || 'red';
1541              
1542             # Initial press starts drawing zoom rect
1543             my $startEvent = $zoomSpec;
1544             $startEvent =~ s/(\d\>)$/ButtonPress-$1/;
1545             #STDERR->printf ( "startEvent = $startEvent\n" );
1546             $self->Tk::bind ( $startEvent => sub { $self->_startZoom ( $zoomSpec,
1547             $zoomColor ) });
1548             }
1549              
1550              
1551             ######################################################################
1552             # Called whenever a zoom event is started. This creates the initial
1553             # zoom rectangle, and installs (temporary) bindings for mouse motion
1554             # and release to drag out the zoom rect and then compute the zoom
1555             # operation.
1556             #
1557             # The motion / button release bindings have to be installed temporarily
1558             # so they don't conflict with other bindings (such as for scrolling
1559             # or panning). The original bindings for those events have to be
1560             # restored once the zoom operation is completed.
1561             ######################################################################
1562             sub _startZoom
1563             {
1564             my ($self, $zoomSpec, $zoomColor) = @_;
1565              
1566             # Start of the zoom rectangle
1567             my $x = $self->canvasx ( $Tk::event->x );
1568             my $y = $self->canvasy ( $Tk::event->y );
1569             my @zoomCoords = ( $x, $y, $x, $y );
1570             my $zoomRect = $self->createRectangle
1571             ( @zoomCoords, -outline => $zoomColor );
1572              
1573             # Install the Motion binding to drag out the rectangle -- store the
1574             # origin binding.
1575             my $dragEvent = '';
1576             #STDERR->printf ( "dragEvent = $dragEvent\n" );
1577             my $origDragBind = $self->Tk::bind ( $dragEvent );
1578             $self->Tk::bind ( $dragEvent => sub {
1579             $zoomCoords[2] = $self->canvasx ( $Tk::event->x );
1580             $zoomCoords[3] = $self->canvasy ( $Tk::event->y );
1581             $self->coords ( $zoomRect => @zoomCoords );
1582             } );
1583              
1584             # Releasing button finishes zoom rect, and causes zoom to happen.
1585             my $stopEvent = $zoomSpec;
1586             $stopEvent =~ s/^\<.*(\d\>)$/
1587             #STDERR->printf ( "stopEvent = $stopEvent\n" );
1588             my $threshold = 10;
1589             my $origStopBind = $self->Tk::bind ( $stopEvent );
1590             $self->Tk::bind ( $stopEvent => sub {
1591             # Delete the rect
1592             $self->delete ( $zoomRect );
1593              
1594             # Restore original bindings
1595             $self->Tk::bind ( $dragEvent => $origDragBind );
1596             $self->Tk::bind ( $stopEvent => $origStopBind );
1597              
1598             # Was the rectangle big enough?
1599             my $dx = $zoomCoords[2] - $zoomCoords[0];
1600             my $dy = $zoomCoords[3] - $zoomCoords[1];
1601              
1602             return if ( abs($dx) < $threshold ||
1603             abs($dy) < $threshold );
1604              
1605             # Find the zooming factor
1606             my $zx = $self->width() / abs($dx);
1607             my $zy = $self->height() / abs($dy);
1608             my $scale = min($zx, $zy);
1609              
1610             # Zoom in our out?
1611             # top->bottom drag means out,
1612             # bottom->top drag means in.
1613             # (0,0) is top left, so $dy > 0 means top->bottom
1614             if ( $dy > 0 ) {
1615             # Zooming in!
1616             #STDERR->printf ( "Zooming in: $scale\n" );
1617             } else {
1618             # Zooming out!
1619             $scale = 1 - 1.0 / $scale;
1620             #STDERR->printf ( "Zooming out: $scale\n" );
1621             }
1622              
1623             # Scale everying up / down
1624             $self->_scaleAndMoveView
1625             ( $scale,
1626             ($zoomCoords[0]+$zoomCoords[2])/2.0,
1627             ($zoomCoords[1]+$zoomCoords[3])/2.0 );
1628             });
1629              
1630             1;
1631             }
1632              
1633              
1634             ######################################################################
1635             # Setup bindings for scrolling / panning operations
1636             #
1637             ######################################################################
1638             sub _createScrollBindings
1639             {
1640             my ($self, %opt) = @_;
1641              
1642             # Interpret scrolling options
1643              
1644             # What mouse button + modifiers starts scroll?
1645             my $scrollSpec = $opt{'-scroll'};
1646             die __PACKAGE__.": No -scroll option" unless defined $scrollSpec;
1647             if ( $scrollSpec =~ /^\<.+\>$/ ) {
1648             # This should be a partial bind event spec, e.g. <1>, or
1649             # -- it must end in a button number
1650             die __PACKAGE__.": Illegal -scroll option"
1651             unless ( $scrollSpec =~ /^\<.+\-\d\>$/ ||
1652             $scrollSpec =~ /^\<\d\>$/ );
1653             }
1654             else {
1655             # Anything else: use the default
1656             $scrollSpec = '<2>';
1657             }
1658              
1659             # Initial press starts panning
1660             my $startEvent = $scrollSpec;
1661             $startEvent =~ s/(\d\>)$/ButtonPress-$1/;
1662             #STDERR->printf ( "startEvent = $startEvent\n" );
1663             $self->Tk::bind ( $startEvent => sub { $self->_startScroll
1664             ( $scrollSpec ) } );
1665             }
1666              
1667              
1668             ######################################################################
1669             # Called whenever a scroll event is started. This installs (temporary)
1670             # bindings for mouse motion and release to complete the scrolling.
1671             #
1672             # The motion / button release bindings have to be installed temporarily
1673             # so they don't conflict with other bindings (such as for zooming)
1674             # The original bindings for those events have to be restored once the
1675             # zoom operation is completed.
1676             ######################################################################
1677             sub _startScroll
1678             {
1679             my ($self, $scrollSpec) = @_;
1680              
1681             # State data to keep track of scroll operation
1682             my $startx = $self->canvasx ( $Tk::event->x );
1683             my $starty = $self->canvasy ( $Tk::event->y );
1684              
1685             # Dragging causes scroll to happen
1686             my $dragEvent = '';
1687             #STDERR->printf ( "dragEvent = $dragEvent\n" );
1688             my $origDragBind = $self->Tk::bind ( $dragEvent );
1689             $self->Tk::bind ( $dragEvent => sub {
1690             my $x = $self->canvasx ( $Tk::event->x );
1691             my $y = $self->canvasy ( $Tk::event->y );
1692              
1693             # Compute scroll ammount
1694             my $dx = $x - $startx;
1695             my $dy = $y - $starty;
1696             #STDERR->printf ( "Scrolling: dx=$dx, dy=$dy\n" );
1697              
1698             # Feels better is scroll speed is reduced.
1699             # Also is more natural inverted, feeld like dragging
1700             # the canvas
1701             $dx *= -.9;
1702             $dy *= -.9;
1703              
1704             my ($xv) = $self->xview();
1705             my ($yv) = $self->yview();
1706             my @sr = $self->cget( '-scrollregion' );
1707             #STDERR->printf ( " xv=$xv, yv=$yv\n" );
1708             my $xpct = $xv + $dx/($sr[2]-$sr[0]);
1709             my $ypct = $yv + $dy/($sr[3]-$sr[1]);
1710             #STDERR->printf ( " xpct=$xpct, ypct=$ypct\n" );
1711             $self->xview ( moveto => $xpct );
1712             $self->yview ( moveto => $ypct );
1713              
1714             # This is the new reference point for
1715             # next motion event
1716             $startx = $x;
1717             $starty = $y;
1718             #STDERR->printf ( " scrolled\n" );
1719              
1720             } );
1721              
1722             # Releasing button finishes scrolling
1723             my $stopEvent = $scrollSpec;
1724             $stopEvent =~ s/^\<.*(\d\>)$/
1725             #STDERR->printf ( "stopEvent = $stopEvent\n" );
1726             my $origStopBind = $self->Tk::bind ( $stopEvent );
1727             $self->Tk::bind ( $stopEvent => sub {
1728              
1729             # Restore original bindings
1730             $self->Tk::bind ( $dragEvent => $origDragBind );
1731             $self->Tk::bind ( $stopEvent => $origStopBind );
1732              
1733             } );
1734              
1735             1;
1736             }
1737              
1738              
1739             ######################################################################
1740             # Setup bindings for keypad keys to do zooming and scrolling
1741             #
1742             # This binds +/- on the keypad to zoom in and out, and the arrow/number
1743             # keys to scroll.
1744             ######################################################################
1745             sub _createKeypadBindings
1746             {
1747             my ($self, %opt) = @_;
1748              
1749             $self->Tk::bind ( '' =>
1750             sub { $self->zoom( -in => 1.15 ) } );
1751             $self->Tk::bind ( '' =>
1752             sub { $self->zoom( -out => 1.15 ) } );
1753              
1754             $self->Tk::bind ( '' =>
1755             sub { $self->xview( scroll => -1, 'units' );
1756             $self->yview( scroll => 1, 'units' ) } );
1757             $self->Tk::bind ( '' =>
1758             sub { $self->yview( scroll => 1, 'units' ) } );
1759             $self->Tk::bind ( '' =>
1760             sub { $self->xview( scroll => 1, 'units' );
1761             $self->yview( scroll => 1, 'units' ) } );
1762             $self->Tk::bind ( '' =>
1763             sub { $self->xview( scroll => -1, 'units' ) } );
1764             $self->Tk::bind ( '' =>
1765             sub { $self->xview( scroll => 1, 'units' ) } );
1766             $self->Tk::bind ( '' =>
1767             sub { $self->xview( scroll => -1, 'units' );
1768             $self->yview( scroll => -1, 'units' ) } );
1769             $self->Tk::bind ( '' =>
1770             sub { $self->yview( scroll => -1, 'units' ) } );
1771             $self->Tk::bind ( '' =>
1772             sub { $self->xview( scroll => 1, 'units' );
1773             $self->yview( scroll => -1, 'units' ) } );
1774              
1775             1;
1776             }
1777              
1778              
1779             #######################################################################
1780             ## Setup binding for 'fit' operation
1781             ##
1782             ## 'fit' scales the entire contents of the graph to fit within the
1783             ## visible portion of the canvas.
1784             #######################################################################
1785             #sub _createFitBindings
1786             #{
1787             # my ($self, %opt) = @_;
1788             #
1789             # # Interpret options
1790             #
1791             # # What event to bind to?
1792             # my $fitEvent = $opt{'-fit'};
1793             # die __PACKAGE__.": No -fit option" unless defined $fitEvent;
1794             # if ( $fitEvent =~ /^\<.+\>$/ ) {
1795             # die __PACKAGE__.": Illegal -fit option"
1796             # unless ( $fitEvent =~ /^\<.+\>$/ );
1797             # }
1798             # else {
1799             # # Anything else: use the default
1800             # $fitEvent = '';
1801             # }
1802             #
1803             # STDERR->printf ( "fit event = $fitEvent\n" );
1804             # $self->Tk::bind ( $fitEvent => sub { $self->fit( 'all' ) });
1805             # 1;
1806             #}
1807              
1808              
1809             ######################################################################
1810             # Scale the graph to fit within the canvas
1811             #
1812             ######################################################################
1813             sub fit
1814             {
1815             my ($self, $idOrTag) = @_;
1816             $idOrTag = 'all' unless defined $idOrTag;
1817              
1818             my $w = $self->width();
1819             my $h = $self->height();
1820             my ($x1,$y1,$x2,$y2) = $self->bbox( $idOrTag );
1821             return 0 unless ( defined $x1 && defined $x2 &&
1822             defined $y1 && defined $y2 );
1823              
1824             my $dx = abs($x2 - $x1);
1825             my $dy = abs($y2 - $y1);
1826              
1827             my $scalex = $w / $dx;
1828             my $scaley = $h / $dy;
1829             my $scale = min ( $scalex, $scaley );
1830             if ( $scalex >= 1.0 && $scaley >= 1.0 ) {
1831             $scale = max ( $scalex, $scaley );
1832             }
1833              
1834             $self->_scaleAndMoveView ( $scale, 0, 0 );
1835             $self->xview( moveto => 0 );
1836             $self->yview( moveto => 0 );
1837              
1838             1;
1839             }
1840              
1841              
1842             ######################################################################
1843             # Zoom in or out, keep top-level centered.
1844             #
1845             ######################################################################
1846             sub zoom
1847             {
1848             my ($self, $dir, $scale) = @_;
1849              
1850             if ( $dir eq '-in' ) {
1851             # Make things bigger
1852             }
1853             elsif ( $dir eq '-out' ) {
1854             # Make things smaller
1855             $scale = 1 / $scale;
1856             }
1857              
1858             my ($xv1,$xv2) = $self->xview();
1859             my ($yv1,$yv2) = $self->yview();
1860             my $xvm = ($xv2 + $xv1)/2.0;
1861             my $yvm = ($yv2 + $yv1)/2.0;
1862             my ($l, $t, $r, $b) = $self->cget( -scrollregion );
1863              
1864             $self->_scaleAndMoveView ( $scale,
1865             $l + $xvm *($r - $l),
1866             $t + $yvm *($b - $t) );
1867              
1868             1;
1869             }
1870              
1871              
1872             sub zoomTo
1873             {
1874             my ($self, $tagOrId) = @_;
1875              
1876             $self->fit();
1877              
1878             my @bb = $self->bbox( $tagOrId );
1879             return unless @bb == 4 && defined($bb[0]);
1880              
1881             my $w = $bb[2] - $bb[0];
1882             my $h = $bb[3] - $bb[1];
1883             my $scale = 2;
1884             my $x1 = $bb[0] - $scale * $w;
1885             my $y1 = $bb[1] - $scale * $h;
1886             my $x2 = $bb[2] + $scale * $w;
1887             my $y2 = $bb[3] + $scale * $h;
1888              
1889             #STDERR->printf("zoomTo: bb = @bb\n".
1890             # " w=$w h=$h\n".
1891             # " x1,$y1, $x2,$y2\n" );
1892              
1893             $self->zoomToRect( $x1, $y1, $x2, $y2 );
1894             }
1895              
1896              
1897             sub zoomToRect
1898             {
1899             my ($self, @box) = @_;
1900              
1901             # make sure x1,y1 = lower left, x2,y2 = upper right
1902             ($box[0],$box[2]) = ($box[2],$box[0]) if $box[2] < $box[0];
1903             ($box[1],$box[3]) = ($box[3],$box[1]) if $box[3] < $box[1];
1904              
1905             # What is the scale relative to current bounds?
1906             my ($l,$r) = $self->xview;
1907             my ($t,$b) = $self->yview;
1908             my $curr_w = $r - $l;
1909             my $curr_h = $b - $t;
1910              
1911             my @sr = $self->cget( -scrollregion );
1912             my $sr_w = $sr[2] - $sr[0];
1913             my $sr_h = $sr[3] - $sr[1];
1914             my $new_l = max(0.0,$box[0] / $sr_w);
1915             my $new_t = max(0.0,$box[1] / $sr_h);
1916             my $new_r = min(1.0,$box[2] / $sr_w);
1917             my $new_b = min(1.0,$box[3] / $sr_h);
1918              
1919             my $new_w = $new_r - $new_l;
1920             my $new_h = $new_b - $new_t;
1921              
1922             my $scale = max( $curr_w/$new_w, $curr_h/$new_h );
1923              
1924             $self->_scaleAndMoveView( $scale,
1925             ($box[0] + $box[2])/2.0,
1926             ($box[1] + $box[3])/2.0 );
1927              
1928             1;
1929             }
1930              
1931              
1932             ######################################################################
1933             # Over-ridden createText Method
1934             #
1935             # Handles the embedded \l\r\n graphViz control characters
1936             ######################################################################
1937             sub createText
1938             {
1939             my ($self, $x, $y, %attrs) = @_;
1940              
1941             if( defined($attrs{-text}) ) {
1942              
1943             # Set Justification, based on any \n \l \r in the text label
1944             my $label = $attrs{-text};
1945             my $justify = 'center';
1946              
1947             # Per the dotguide.pdf, a '\l', '\r', or '\n' is
1948             # just a line terminator, not a newline. So in cases
1949             # where the label ends in one of these characters, we are
1950             # going to remove the newline char later
1951             my $removeNewline;
1952             if( $label =~ /\\[nlr]$/){
1953             $removeNewline = 1;
1954             }
1955              
1956             if( $label =~ s/\\l/\n/g ){
1957             $justify = 'left';
1958             }
1959             if( $label =~ s/\\r/\n/g ){
1960             $justify = 'right';
1961             }
1962              
1963             # Change \n to actual \n
1964             if( $label =~ s/\\n/\n/g ){
1965             $justify = 'center';
1966             }
1967              
1968             # remove ending newline if flag set
1969             if( $removeNewline){
1970             $label =~ s/\n$//;
1971             }
1972              
1973             # Fix any escaped chars
1974             # like \} to }, and \\{ to \{
1975             $label =~ s/\\(?!\\)(.)/$1/g;
1976              
1977             $attrs{-text} = $label;
1978             $attrs{-justify} = $justify;
1979              
1980             # Fix the label tag, if there is one
1981             my $tags;
1982             if( defined($tags = $attrs{-tags})){
1983             my %tags = (@$tags);
1984             $tags{label} = $label if(defined($tags{label}));
1985             $attrs{-tags} = [%tags];
1986             }
1987              
1988             # Get the default font, if not defined already
1989             my $fonts = $self->{fonts};
1990             unless(defined($fonts->{_default}) ){
1991              
1992             # Create dummy item, so we can see what font is used
1993             my $dummyID = $self->SUPER::createText
1994             ( 100,25, -text => "You should never see this" );
1995             my $defaultfont = $self->itemcget($dummyID,-font);
1996              
1997             # Make a copy that we will mess with:
1998             $defaultfont = $defaultfont->Clone;
1999             $fonts->{_default}{font} = $defaultfont;
2000             $fonts->{_default}{origSize} = $defaultfont->actual(-size);
2001              
2002             # Delete the dummy item
2003             $self->delete($dummyID);
2004             }
2005              
2006             # Assign the default font
2007             unless( defined($attrs{-font}) ){
2008             $attrs{-font} = $fonts->{_default}{font};
2009             }
2010              
2011             }
2012              
2013             # Call Inherited createText
2014             $self->SUPER::createText ( $x, $y, %attrs );
2015             }
2016              
2017              
2018             ######################################################################
2019             # Sub to try a color name, returns the color name if recognized
2020             # 'black' and issues a warning if not
2021             ######################################################################
2022             sub _tryColor
2023             {
2024             my ($self,$color) = @_;
2025              
2026             return undef unless defined($color);
2027              
2028             # Special cases
2029             if( $color eq 'crimson' ) {
2030             # crimison not defined in Tk, so use GraphViz's definition
2031             return sprintf("#%02X%02x%02X", 246,231,220);
2032             }
2033             elsif( $color =~ /^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s+(-?\d+\.?\d*)\s*$/ ) {
2034             # three color numbers
2035             my($hue,$sat,$bright) = ($1,$2,$3);
2036             return $self->_hsb2rgb($hue,$sat,$bright);
2037             }
2038              
2039             # Don't check color if it is a hex rgb value
2040             unless( $color =~ /^\#\w+/ ) {
2041             my $tryColor = $color;
2042             $tryColor =~ s/\_//g; # get rid of any underscores
2043             my @rgb;
2044             eval { @rgb = $self->rgb($tryColor); };
2045             if ($@) {
2046             warn __PACKAGE__.": Unkown color $color, using black instead\n";
2047             $color = 'black';
2048             } else {
2049             $color = $tryColor;
2050             }
2051             }
2052              
2053             $color;
2054             }
2055              
2056              
2057             ######################################################################
2058             # Sub to convert from Hue-Sat-Brightness to RGB hex number
2059             #
2060             ######################################################################
2061             sub _hsb2rgb
2062             {
2063             my ($self,$h,$s,$v) = @_;
2064              
2065             my ($r,$g,$b);
2066             if( $s <= 0){
2067             $v = int($v);
2068             ($r,$g,$b) = ($v,$v,$v);
2069             }
2070             else{
2071             if( $h >= 1){
2072             $h = 0;
2073             }
2074             $h = 6*$h;
2075             my $f = $h - int($h);
2076             my $p = $v * (1 - $s);
2077             my $q = $v * ( 1 - ($s * $f));
2078             my $t = $v * ( 1 - ($s * (1-$f)));
2079             my $i = int($h);
2080             if( $i == 0){ ($r,$g,$b) = ($v, $t, $p);}
2081             elsif( $i == 1){ ($r,$g,$b) = ($q, $v, $p);}
2082             elsif( $i == 2){($r,$g,$b) = ($p, $v, $t);}
2083             elsif( $i == 3){($r,$g,$b) = ($p, $q, $v);}
2084             elsif( $i == 4){($r,$g,$b) = ($t, $p, $v);}
2085             elsif( $i == 5){($r,$g,$b) = ($v, $p, $q);}
2086              
2087             }
2088              
2089             sprintf("#%02X%02x%02X", 255*$r, 255*$g, 244*$b);
2090             }
2091              
2092              
2093             ######################################################################
2094             # Utility functions
2095             ######################################################################
2096              
2097             sub min {
2098             if ( defined($_[0]) ) {
2099             if ( defined($_[1]) ) { return ($_[0] < $_[1])? $_[0] : $_[1]; }
2100             else { return $_[0]; }
2101             } else {
2102             if ( defined($_[1]) ) { return $_[1]; }
2103             else { return undef; }
2104             }
2105             }
2106              
2107             sub max {
2108             if ( defined($_[0]) ) {
2109             if ( defined($_[1]) ) { return ($_[0] > $_[1])? $_[0] : $_[1]; }
2110             else { return $_[0]; }
2111             } else {
2112             if ( defined($_[1]) ) { return $_[1]; }
2113             else { return undef; }
2114             }
2115             }
2116              
2117             __END__