File Coverage

blib/lib/GD/OrgChart.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package GD::OrgChart;
2              
3             our $VERSION = '0.03';
4              
5             # Copyright 2002, Gary A. Algier. All rights reserved. This module is
6             # free software; you can redistribute it or modify it under the same
7             # terms as Perl itself.
8              
9 5     5   44149 use 5.006;
  5         20  
  5         203  
10 5     5   30 use strict;
  5         22  
  5         173  
11 5     5   28 use warnings;
  5         13  
  5         625  
12              
13             require Exporter;
14              
15             our @ISA = qw(Exporter);
16              
17             # This allows declaration use GD::OrgChart ':all';
18             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
19             # will save memory.
20             our %EXPORT_TAGS = (
21             all => [ qw( ) ]
22             );
23              
24             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
25              
26             our @EXPORT = qw(
27             );
28              
29 5     5   11689 use GD;
  0            
  0            
30              
31             sub new
32             {
33             my $proto = shift;
34             my $class = ref($proto) || $proto;
35             my $self = {};
36             $self->{image} = undef;
37             $self->{params} = {
38             boxbgcolor => [255,255,255],
39             boxfgcolor => [0,0,0],
40             boxtextcolor => [255,0,0],
41             boxtop => 4,
42             boxbottom => 4,
43             boxleft => 4,
44             boxright => 4,
45             boxborder => 1,
46             linespacing => 4,
47             size => 12,
48             font => gdLargeFont,
49             top => 10,
50             bottom => 10,
51             left => 10,
52             right => 10,
53             horzspacing => 20,
54             vertspacing => 20,
55             linewidth => 1,
56             linecolor => [0,0,255],
57             depth => 0,
58             debug => 0,
59             };
60             if (@_ > 0 && ref($_[0]) eq "HASH") {
61             my $p = shift;
62             @{$self->{params}}{keys %$p} = values %$p;
63             }
64             bless($self,$class);
65             return $self;
66             }
67              
68             sub Image
69             {
70             my $self = shift;
71              
72             if (@_) {
73             $self->{image} = shift;
74             }
75             return $self->{image};
76             }
77              
78            
79             # BoundTree
80             # usage:
81             # $chart->BoundTree($node,{ params...})
82             sub BoundTree
83             {
84             my $self = shift;
85             my $node = shift;
86             my %params = %{$self->{params}};
87              
88             if (@_ == 1 && ref($_[0]) eq "HASH") {
89             my $p = shift;
90             @params{keys %$p} = values %$p;
91             }
92              
93             # XXX: should we barf on left over arguments?
94              
95             return $self->_BoundTree($node,
96             $params{depth} > 0 ? $params{depth} : 0,
97             0,%params);
98             }
99              
100             sub _BoundTree
101             {
102             my $self = shift;
103             my $node = shift;
104             my $maxdepth = shift;;
105             my $curdepth = 1 + shift;;
106             my %params = @_;
107              
108             if ($node->{params} && ref($node->{params}) eq "HASH") {
109             my $p = $node->{params};
110             @params{keys %$p} = values %$p;
111             }
112              
113             my (@box);
114             my (@tree,$treeleft,$treeright,$treetop,$treebottom);
115              
116             @tree = @box = $self->BoundBox($node,$maxdepth,$curdepth,\%params);
117             $node->{BoxBounds} = [ @box ];
118             $node->{BoxSize} = sprintf("%dx%d",_height(@box),_width(@box));
119             $treetop = _top(@tree);
120             $treeleft = _left(@tree);
121             $treebottom = _bottom(@tree);
122             $treeright = _right(@tree);
123              
124             # if no subs or we are deep enough, we are done.
125             if (!defined($node->{subs}) || ($maxdepth && $curdepth >= $maxdepth)) {
126             $node->{TreeBounds} = [ @tree ];
127             $node->{TreeSize} = sprintf("%dx%d",_height(@tree),_width(@tree));
128             return @tree;
129             }
130              
131             my $totalwidth = 0;
132             my $highest = 0;
133             foreach my $sub (@{$node->{subs}}) {
134             my @sub = $self->_BoundTree($sub,$maxdepth,$curdepth,%params);
135             $totalwidth += _width(@sub);
136             $highest = _max($highest,_height(@sub));
137             }
138             $totalwidth += $params{horzspacing} * (scalar @{$node->{subs}} - 1);
139             $treebottom += $params{vertspacing} * 2 + $highest;
140             if (_width(@box) < $totalwidth) {
141             my $diff = $totalwidth - _width(@box);
142             $treeleft -= _firsthalf($diff);
143             $treeright += _secondhalf($diff);
144             }
145              
146             @tree = ($treeleft,$treebottom,$treeright,$treetop);
147              
148             $node->{TreeBounds} = [ @tree ];
149             $node->{TreeSize} = sprintf("%dx%d",_height(@tree),_width(@tree));
150             return @tree;
151             }
152              
153            
154             # DrawTree:
155             # usage:
156             # $chart->DrawTree($node,{ params ...});
157             sub DrawTree
158             {
159             my $self = shift;
160             my $node = shift;
161             my %params = %{$self->{params}};
162              
163             my ($x,$y);
164              
165             if (@_ == 1 && ref($_[0]) eq "HASH") {
166             my $p = shift;
167             @params{keys %$p} = values %$p;
168             }
169              
170             # XXX: If there are arguments left we should produce a warning.
171              
172             # if this has not been done, do it now:
173             if (!defined($node->{TreeBounds})) {
174             $self->BoundTree($node,%params);
175             }
176              
177             if (!defined($self->Image)) {
178             my @b = @{$node->{TreeBounds}};
179             my $w = _width(@b) + $params{left} + $params{right};
180             my $h = _height(@b) + $params{top} + $params{bottom};
181             $self->Image(GD::Image->new($w,$h));
182             # use the box bg color as the first color allocated
183             # so it becomes the image bg color
184             $self->{image}->colorAllocate(@{$params{boxbgcolor}});
185             }
186              
187             if (!defined($params{x}) || !defined($params{y})) {
188             my $treewidth = _width(@{$node->{TreeBounds}})
189             + $params{left} + $params{right};
190             my $boxheight = _height(@{$node->{BoxBounds}});
191             $x = _firsthalf($treewidth);
192             $y = _firsthalf($boxheight) + $params{top};
193             }
194              
195             return $self->_DrawTree($node,$x,$y,
196             $params{depth} > 0 ? $params{depth} : 0,
197             0,%params);
198             }
199              
200             sub _DrawTree
201             {
202             my $self = shift;
203             my $node = shift;
204             my $x = shift;
205             my $y = shift;
206             my $maxdepth = shift;
207             my $curdepth = 1 + shift;
208             my %params = @_;
209              
210             if ($node->{params} && ref($node->{params}) eq "HASH") {
211             my $p = $node->{params};
212             @params{keys %$p} = values %$p;
213             }
214              
215             my (@box);
216             my (@tree,$treeleft,$treeright,$treetop,$treebottom);
217             my ($temp,$junction,$subtop,$linecolor);
218              
219             # draw our box
220             @box = $self->DrawBox($node,$x,$y,$maxdepth,$curdepth,\%params);
221             $node->{BoxBounds} = [ @box ];
222             $node->{BoxSize} = sprintf("%dx%d",_height(@box),_width(@box));
223              
224             @tree = @box;
225             $treetop = _top(@tree);
226             $treeleft = _left(@tree);
227             $treebottom = _bottom(@tree);
228             $treeright = _right(@tree);
229             $node->{TreeBounds} = [ @tree ];
230             $node->{TreeSize} = sprintf("%dx%d",_height(@tree),_width(@tree));
231              
232             # if no subs or we are deep enough, we are done.
233             if (!defined($node->{subs}) || ($maxdepth && $curdepth >= $maxdepth)) {
234             $node->{TreeBounds} = [ @tree ];
235             $node->{TreeSize} = sprintf("%dx%d",_height(@tree),_width(@tree));
236             return @tree;
237             }
238              
239             # we have subs, so let us draw some lines
240             $linecolor = $self->{image}->colorAllocate(@{$params{linecolor}});
241              
242             # this is the line from the bottom of our box to the horizontal line
243             $temp = $y + _secondhalf(_height(@box));
244             $junction = $temp + $params{vertspacing};
245             $subtop = $junction + $params{vertspacing};
246             $self->{image}->line($x,$temp,$x,$junction,$linecolor);
247              
248             $treebottom = $junction;
249              
250             my @widths = map {
251             defined($_->{TreeBounds})
252             ? _width(@{$_->{TreeBounds}})
253             : ();
254             } @{$node->{subs}};
255             my $subx = $x;
256              
257             if (@widths > 1) {
258             my $totalwidth = 0;
259             # there is more than one sub, so we need a horizontal line
260             my $left = $widths[0];
261             my $right = $widths[@widths-1];
262             for my $w (@widths) {
263             $totalwidth += $w;
264             }
265             $totalwidth += $params{horzspacing} * (@widths - 1);
266              
267             # the horizontal line is not centered, the tree below the
268             # line is centered.
269             $subx = $x - _firsthalf($totalwidth) + _firsthalf($left);
270             $temp = $x + _secondhalf($totalwidth) - _secondhalf($right);
271              
272             $self->{image}->line($subx,$junction,
273             $temp,$junction,$linecolor);
274             $treeleft = _min($treeleft,$x - _firsthalf($totalwidth));
275             $treeright = _max($treeleft,$x + _secondhalf($totalwidth));
276             }
277              
278             # draw lines down to the sub trees and draw the trees
279             for my $sub (@{$node->{subs}}) {
280             my $width = shift @widths;
281             $self->{image}->line($subx,$junction,
282             $subx,$junction+$params{vertspacing},$linecolor);
283             $temp = $junction + $params{vertspacing}
284             + _firsthalf(_height(@{$sub->{BoxBounds}}));
285             my @sub = $self->_DrawTree($sub,$subx,$temp,
286             $maxdepth,$curdepth,%params);
287             $treeleft = _min($treeleft,_left(@sub));
288             $treeright = _max($treeright,_right(@sub));
289             $treebottom = _max($treebottom,_bottom(@sub));
290             if (@widths) {
291             $subx += _secondhalf($width);
292             $subx += $params{horzspacing};
293             $subx += _firsthalf($widths[0]);
294             }
295             }
296              
297             @tree = ($treeleft,$treebottom,$treeright,$treetop);
298             $node->{TreeBounds} = [ @tree ];
299             $node->{TreeSize} = sprintf("%dx%d",_height(@tree),_width(@tree));
300             return @tree;
301             }
302              
303            
304             sub BoundBox
305             {
306             my $self = shift;
307              
308             my $node = shift;
309             my $maxdepth = shift;
310             my $curdepth = shift;
311              
312             my %params = %{$self->{params}};
313             if (@_ == 1) {
314             my $p = shift;
315             @params{keys %$p} = values %$p;
316             }
317              
318             if ($node->{params} && ref($node->{params}) eq "HASH") {
319             my $p = $node->{params};
320             @params{keys %$p} = values %$p;
321             }
322              
323             my ($width,$height);
324             $width = $height = 0;
325              
326             if ($params{size} != 0 && defined($node->{text})) {
327             my @text = split("\n",$node->{text});
328             for my $text (@text) {
329             my @bounds = _string(undef,0,
330             $params{font},$params{size},0,0,$text);
331             $width = _max($width,_width(@bounds));
332             $height += _height(@bounds);
333             }
334             $height += (@text - 1) * $params{linespacing};
335             }
336              
337             $width += $params{boxleft} + $params{boxright}
338             + 2 * $params{boxborder};
339             $height += $params{boxtop} + $params{boxbottom}
340             + 2 * $params{boxborder};
341              
342             my ($left,$bottom,$right,$top);
343             $left = -_firsthalf($width);
344             $right = $left + $width;
345             $top = -_firsthalf($height);
346             $bottom = $top + $height;
347              
348             return ($left,$bottom,$right,$top);
349             }
350              
351            
352             sub DrawBox
353             {
354             my $self = shift;
355              
356             my $node = shift;
357             my $x = shift;
358             my $y = shift;
359             my $maxdepth = shift;
360             my $curdepth = shift;
361              
362             my %params = %{$self->{params}};
363             if (@_ == 1) {
364             my $p = shift;
365             @params{keys %$p} = values %$p;
366             }
367              
368             if ($node->{params} && ref($node->{params}) eq "HASH") {
369             my $p = $node->{params};
370             @params{keys %$p} = values %$p;
371             }
372              
373             my ($width,$height,@width,@height);
374             $width = $height = 0;
375              
376             if ($params{size} != 0 && defined($node->{text})) {
377             my @text = split("\n",$node->{text});
378             for my $text (@text) {
379             my @bounds = _string(undef,0,
380             $params{font},$params{size},
381             0,0,$text);
382             push @width,_width(@bounds);
383             push @height,_height(@bounds);
384             $width = _max($width,_width(@bounds));
385             $height += _height(@bounds);
386             }
387             $height += (@text - 1) * $params{linespacing};
388             }
389              
390             $width += $params{boxleft} + $params{boxright}
391             + 2 * $params{boxborder};
392             $height += $params{boxtop} + $params{boxbottom}
393             + 2 * $params{boxborder};
394              
395             my ($left,$bottom,$right,$top);
396             $left = $x - _firsthalf($width);
397             $right = $left + $width;
398             $top = $y - _firsthalf($height);
399             $bottom = $top + $height;
400              
401             my $bgcolor = $self->{image}->colorAllocate(@{$params{boxbgcolor}});
402             my $fgcolor = $self->{image}->colorAllocate(@{$params{boxfgcolor}});
403             my $textcolor = $self->{image}->colorAllocate(@{$params{boxtextcolor}});
404              
405             # make a "black" rectangle with a "white" fill
406             $self->{image}->filledRectangle($left,$top,$right,$bottom,$fgcolor);
407             $self->{image}->filledRectangle($left+$params{boxborder},
408             $top+$params{boxborder},
409             $right-$params{boxborder},
410             $bottom-$params{boxborder},
411             $bgcolor);
412              
413             if ($params{size} != 0 && defined($node->{text})) {
414             my $ytemp = $top + $params{boxborder} + $params{boxtop};
415             my @text = split("\n",$node->{text});
416             for my $text (@text) {
417             my $h = shift @height;
418             # Note:
419             # The y coordinate supplied to stringFT must be the bottom
420             # of the text, however, the y coordinate supplied to
421             # string is the top of the text. To deal with this
422             # we pass (y + height). This gets adjusted back before
423             # string is called (see below in _string).
424             _string($self->{image},$textcolor,
425             $params{font},$params{size},
426             $x - _firsthalf(shift @width),
427             $ytemp + $h,$text);
428             $ytemp += $h + $params{linespacing};
429             }
430             }
431              
432             return ($left,$bottom,$right,$top);
433             }
434              
435            
436             sub _string
437             {
438             my $image = shift;
439             my $color = shift;
440             my $font = shift;
441             my $size = shift;
442             my $x = shift;
443             my $y = shift;
444             my $text = shift;
445              
446             my @b;
447              
448             if (ref($font)) {
449             # must be builtin font
450             @b = ($x,$y + $font->height,
451             $x + $font->width * length($text),$y);
452             if (defined($image)) {
453             $image->string($font,$x,$y - $font->height
454             ,$text,$color);
455             }
456             }
457             else {
458             if (defined($image)) {
459             @b = $image->stringFT($color,$font,
460             $size,0,$x,$y,$text);
461             }
462             else {
463             @b = GD::Image->stringFT($color,$font,
464             $size,0,$x,$y,$text);
465             }
466             @b = _rebound(@b);
467             }
468             return @b;
469             }
470              
471            
472             # The GD package returns bounds as in:
473             # (left,bottom,right,bottom,right,top,left,top)
474             # This is redundant. I use the Postscript idea of:
475             # (left,bottom,right,top)
476             # aka:
477             # (llx,lly,urx,ury)
478             # This function does the conversion
479             sub _rebound
480             {
481             if (@_ == 8) {
482             return @_[0,1,4,5];
483             }
484             else {
485             return (0,0,0,0);
486             }
487             }
488              
489             # in many cases we need two different
490             # "half" values such that the sum equals the whole.
491             sub _firsthalf
492             {
493             return int($_[0] / 2);
494             }
495              
496             sub _secondhalf
497             {
498             return $_[0] - int($_[0] / 2);
499             }
500              
501             sub _top
502             {
503             return $_[3];
504             }
505             sub _bottom
506             {
507             return $_[1];
508             }
509             sub _left
510             {
511             return $_[0];
512             }
513             sub _right
514             {
515             return $_[2];
516             }
517             sub _width
518             {
519             return abs($_[0] - $_[2]);
520             }
521             sub _height
522             {
523             return abs($_[1] - $_[3]);
524             }
525             sub _min
526             {
527             my $min = shift;
528             my $x;
529              
530             while (@_) {
531             $x = shift;
532             $min = $x if ($x < $min);
533             }
534             return $min;
535             }
536             sub _max
537             {
538             my $max = shift;
539             my $x;
540              
541             while (@_) {
542             $x = shift;
543             $max = $x if ($x > $max);
544             }
545             return $max;
546             }
547              
548             1;
549             __END__