File Coverage

lib/Bio/Graphics/Panel.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 Bio::Graphics::Panel;
2              
3 1     1   1615 use strict;
  1         2  
  1         33  
4 1     1   451 use Bio::Graphics::Glyph::Factory;
  1         3  
  1         28  
5 1     1   8 use Bio::Graphics::Feature;
  1         2  
  1         32  
6 1     1   520 use Bio::Graphics::GDWrapper;
  0            
  0            
7              
8             # KEYLABELFONT must be treated as string until image_class is established
9             use constant KEYLABELFONT => 'gdMediumBoldFont';
10             use constant KEYSPACING => 5; # extra space between key columns
11             use constant KEYPADTOP => 5; # extra padding before the key starts
12             use constant KEYCOLOR => 'wheat';
13             use constant KEYSTYLE => 'bottom';
14             use constant KEYALIGN => 'left';
15             use constant GRIDCOLOR => 'lightcyan';
16             use constant GRIDMAJORCOLOR => 'lightgrey';
17             use constant MISSING_TRACK_COLOR =>'gray';
18             use constant EXTRA_RIGHT_PADDING => 30;
19              
20             use base qw(Bio::Root::Root);
21             our $GlyphScratch;
22              
23             my %COLORS; # translation table for symbolic color names to RGB triple
24             my $IMAGEMAP = 'bgmap00001';
25             read_colors();
26              
27             sub api_version { 1.8 }
28              
29             # Create a new panel of a given width and height, and add lists of features
30             # one by one
31             sub new {
32             my $class = shift;
33             $class = ref($class) || $class;
34             my %options = @_;
35              
36             $class->read_colors() unless %COLORS;
37              
38             my $length = $options{-length} || 0;
39             my $offset = $options{-offset} || 0;
40             my $spacing = $options{-spacing} || 5;
41             my $bgcolor = $options{-bgcolor} || 'white';
42             my $keyfont = $options{-key_font} || KEYLABELFONT;
43             my $keycolor = $options{-key_color} || KEYCOLOR;
44             my $keyspacing = $options{-key_spacing} || KEYSPACING;
45             my $keystyle = $options{-key_style} || KEYSTYLE;
46             my $keyalign = $options{-key_align} || KEYALIGN;
47             my $suppress_key = $options{-suppress_key} || 0;
48             my $allcallbacks = $options{-all_callbacks} || 0;
49             my $gridcolor = $options{-gridcolor} || GRIDCOLOR;
50             my $gridmajorcolor = $options{-gridmajorcolor} || GRIDMAJORCOLOR;
51             my $grid = $options{-grid} || 0;
52             my $extend_grid = $options{-extend_grid}|| 0;
53             my $flip = $options{-flip} || 0;
54             my $empty_track_style = $options{-empty_tracks} || 'key';
55             my $autopad = defined $options{-auto_pad} ? $options{-auto_pad} : 1;
56             my $truecolor = $options{-truecolor} || 0;
57             my $truetype = $options{-truetype} || 0;
58             my $image_class = ($options{-image_class} && $options{-image_class} =~ /SVG/)
59             ? 'GD::SVG'
60             : $options{-image_class} || 'GD'; # Allow users to specify GD::SVG using SVG
61             my $linkrule = $options{-link};
62             my $titlerule = $options{-title};
63             my $targetrule = $options{-target};
64             my $background = $options{-background};
65             my $postgrid = $options{-postgrid};
66             $options{-stop}||= $options{-end}; # damn damn damn
67             my $add_categories= $options{-add_category_labels};
68              
69             if (my $seg = $options{-segment}) {
70             $offset = eval {$seg->start-1} || 0;
71             $length = $seg->length;
72             }
73              
74             $offset ||= $options{-start}-1 if defined $options{-start};
75             $length ||= $options{-stop}-$options{-start}+1
76             if defined $options{-start} && defined $options{-stop};
77              
78             # bring in the image generator class, since we will need it soon anyway
79             eval "require $image_class; 1" or $class->throw($@);
80              
81             return bless {
82             tracks => [],
83             width => $options{-width} || 600,
84             pad_top => $options{-pad_top}||0,
85             pad_bottom => $options{-pad_bottom}||0,
86             pad_left => $options{-pad_left}||0,
87             pad_right => $options{-pad_right}||0,
88             global_alpha => $options{-alpha} || 0,
89             length => $length,
90             offset => $offset,
91             gridcolor => $gridcolor,
92             gridmajorcolor => $gridmajorcolor,
93             grid => $grid,
94             extend_grid => $extend_grid,
95             bgcolor => $bgcolor,
96             height => 0, # AUTO
97             spacing => $spacing,
98             key_font => $keyfont,
99             key_color => $keycolor,
100             key_spacing => $keyspacing,
101             key_style => $keystyle,
102             key_align => $keyalign,
103             suppress_key => $suppress_key,
104             background => $background,
105             postgrid => $postgrid,
106             autopad => $autopad,
107             all_callbacks => $allcallbacks,
108             truecolor => $truecolor,
109             truetype => $truetype,
110             flip => $flip,
111             linkrule => $linkrule,
112             titlerule => $titlerule,
113             targetrule => $targetrule,
114             empty_track_style => $empty_track_style,
115             image_class => $image_class,
116             image_package => $image_class . '::Image', # Accessors
117             polygon_package => $image_class . '::Polygon',
118             add_category_labels => $add_categories,
119             key_boxes => [],
120             },$class;
121             }
122              
123             sub rotate {
124             my $self = shift;
125             my $d = $self->{rotate};
126             $self->{rotate} = shift if @_;
127             $d;
128             }
129              
130             sub pad_left {
131             my $self = shift;
132             my $g = $self->{pad_left};
133             $self->{pad_left} = shift if @_;
134             $g;
135             }
136             sub pad_right {
137             my $self = shift;
138             my $g = $self->{pad_right};
139             $self->{pad_right} = shift if @_;
140             $g;
141             }
142             sub pad_top {
143             my $self = shift;
144             my $g = $self->{pad_top};
145             $self->{pad_top} = shift if @_;
146             $g;
147             }
148             sub pad_bottom {
149             my $self = shift;
150             my $g = $self->{pad_bottom};
151             $self->{pad_bottom} = shift if @_;
152             $g;
153             }
154             sub extend_grid {
155             my $self = shift;
156             my $g = $self->{extend_grid};
157             $self->{extend_grid} = shift if @_;
158             $g;
159             }
160             sub flip {
161             my $self = shift;
162             my $g = $self->{flip};
163             $self->{flip} = shift if @_;
164             $g;
165             }
166             sub truetype {
167             my $self = shift;
168             my $g = $self->{truetype};
169             $self->{truetype} = shift if @_;
170             $g;
171             }
172              
173             # values of empty_track_style are:
174             # "suppress" -- suppress empty tracks entirely (default)
175             # "key" -- show just the key in "between" mode
176             # "line" -- draw a thin grey line
177             # "dashed" -- draw a dashed line
178             sub empty_track_style {
179             my $self = shift;
180             my $g = $self->{empty_track_style};
181             $self->{empty_track_style} = shift if @_;
182             $g;
183             }
184              
185             sub key_style {
186             my $self = shift;
187             my $g = $self->{key_style};
188             $self->{key_style} = shift if @_;
189             $g;
190             }
191              
192             sub auto_pad {
193             my $self = shift;
194             my $g = $self->{autopad};
195             $self->{autopad} = shift if @_;
196             $g;
197             }
198              
199             # public routine for mapping from a base pair
200             # location to pixel coordinates
201             sub location2pixel {
202             my $self = shift;
203             my $end = $self->end + 1;
204             my @coords = $self->{flip} ? map { $end-$_ } @_ : @_;
205             $self->map_pt(@coords);
206             }
207              
208             # numerous direct calls into array used here for performance considerations
209             sub map_pt {
210             my $self = shift;
211             my $offset = $self->{offset};
212             my $scale = $self->{scale} || $self->scale;
213             my $pl = $self->{pad_left};
214             my $pr = $self->{width};
215             my $flip = $self->{flip};
216             my $length = $self->{length};
217             my @result;
218             foreach (@_) {
219             my $val = $flip
220             ? $pr - ($length - ($_- 1)) * $scale
221             : ($_-$offset-1) * $scale;
222             $val = int($val + 0.5 * ($val<=>0));
223             $val = -1 if $val < 0;
224             $val = $pr+1 if $val > $pr;
225             push @result,$val;
226             }
227             @result;
228             }
229              
230             sub map_no_trunc {
231             my $self = shift;
232             my $offset = $self->{offset};
233             my $scale = $self->scale;
234             my $pl = $self->{pad_left};
235             my $pr = $pl + $self->{width}; # - $self->{pad_right};
236             my $flip = $self->{flip};
237             my $length = $self->{length};
238             my $end = $offset+$length;
239             my @result;
240             foreach (@_) {
241             my $val = $flip ? int (0.5 + $pl + ($end - ($_- 1)) * $scale) : int (0.5 + $pl + ($_-$offset-1) * $scale);
242             push @result,$val;
243             }
244             @result;
245             }
246              
247             sub scale {
248             my $self = shift;
249             $self->{scale} ||= $self->width/($self->length);
250             }
251              
252             sub start { shift->{offset}+1}
253             sub end { $_[0]->start + $_[0]->{length}-1}
254              
255             sub offset { shift->{offset} }
256             sub width {
257             my $self = shift;
258             my $d = $self->{width};
259             $self->{width} = shift if @_;
260             $d;
261             }
262              
263             sub left {
264             my $self = shift;
265             $self->pad_left;
266             }
267             sub right {
268             my $self = shift;
269             $self->pad_left + $self->width; # - $self->pad_right;
270             }
271             sub top {
272             shift->pad_top;
273             }
274             sub bottom {
275             my $self = shift;
276             $self->height - $self->pad_bottom;
277             }
278              
279             sub spacing {
280             my $self = shift;
281             my $d = $self->{spacing};
282             $self->{spacing} = shift if @_;
283             $d;
284             }
285              
286             sub key_spacing {
287             my $self = shift;
288             my $d = $self->{key_spacing};
289             $self->{key_spacing} = shift if @_;
290             $d;
291             }
292              
293             sub length {
294             my $self = shift;
295             my $d = $self->{length};
296             if (@_) {
297             my $l = shift;
298             $l = $l->length if ref($l) && $l->can('length');
299             $self->{length} = $l;
300             }
301             $d;
302             }
303              
304             sub gridcolor {shift->{gridcolor}}
305              
306             sub gridmajorcolor {shift->{gridmajorcolor}}
307              
308             sub all_callbacks { shift->{all_callbacks} }
309              
310             sub add_track {
311             my $self = shift;
312             $self->_do_add_track(scalar(@{$self->{tracks}}),@_);
313             }
314              
315             sub unshift_track {
316             my $self = shift;
317             $self->_do_add_track(0,@_);
318             }
319              
320             sub insert_track {
321             my $self = shift;
322             my $position = shift;
323             $self->_do_add_track($position,@_);
324             }
325              
326              
327             # create a feature and factory pair
328             # see Factory.pm for the format of the options
329             # The thing returned is actually a generic Glyph
330             sub _do_add_track {
331             my $self = shift;
332             my $position = shift;
333              
334             # due to indecision, we accept features
335             # and/or glyph types in the first two arguments
336             my ($features,$glyph_name) = ([],undef);
337             while ( @_ && $_[0] !~ /^-/) {
338             my $arg = shift;
339             $features = $arg and next if ref($arg);
340             $glyph_name = $arg and next unless ref($arg);
341             }
342              
343             my %args = @_;
344             my ($map,$ss,%options);
345              
346             foreach (keys %args) {
347             (my $canonical = lc $_) =~ s/^-//;
348             if ($canonical eq 'glyph') {
349             $map = $args{$_};
350             delete $args{$_};
351             } elsif ($canonical eq 'stylesheet') {
352             $ss = $args{$_};
353             delete $args{$_};
354             } else {
355             $options{$canonical} = $args{$_};
356             }
357             }
358              
359             $glyph_name = $map if defined $map;
360             $glyph_name ||= 'generic';
361              
362             local $^W = 0; # uninitialized variable warnings under 5.00503
363              
364             my $panel_map =
365             ref($map) eq 'CODE' ? sub {
366             my $feature = shift;
367             return 'track' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'track' };
368             return 'group' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'group' };
369             return 'scale' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'scale' };
370             return $map->($feature,'glyph',$self);
371             }
372             : ref($map) eq 'HASH' ? sub {
373             my $feature = shift;
374             return 'track' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'track' };
375             return 'group' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'group' };
376             return 'scale' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'scale' };
377             return eval {$map->{$feature->primary_tag}} || 'generic';
378             }
379             : sub {
380             my $feature = shift;
381             return 'track' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'track' };
382             return 'group' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'group' };
383             return 'scale' if eval { defined $feature->primary_tag && $feature->primary_tag eq 'scale' };
384             return $glyph_name;
385             };
386             $self->_add_track($position,$features,-map=>$panel_map,-stylesheet=>$ss,-options=>\%options);
387             }
388              
389             sub _add_track {
390             my $self = shift;
391             my ($position,$features,@options) = @_;
392              
393             # build the list of features into a Bio::Graphics::Feature object
394             $features = [$features] unless ref $features eq 'ARRAY';
395              
396             # optional middle-level glyph is the group
397             foreach my $f (grep {ref $_ eq 'ARRAY'} @$features) {
398             next unless ref $f eq 'ARRAY';
399             $f = Bio::Graphics::Feature->new(
400             -segments=>$f,
401             -type => 'group'
402             );
403             }
404              
405             # top-level glyph is the track
406             my $feature = Bio::Graphics::Feature->new(
407             -segments=>$features,
408             -start => $self->offset+1,
409             -stop => $self->offset+$self->length,
410             -type => 'track'
411             );
412              
413             my $factory = Bio::Graphics::Glyph::Factory->new($self,@options);
414             my $track = $factory->make_glyph(-1,$feature);
415              
416             splice(@{$self->{tracks}},$position,0,$track);
417             return $track;
418             }
419              
420             sub _expand_padding {
421             my $self = shift;
422             my $track = shift;
423             my $extra_padding = $self->extra_right_padding;
424              
425             my $keystyle = $self->key_style;
426             my $empty_track_style = $self->empty_track_style;
427              
428             return unless $keystyle eq 'left' or $keystyle eq 'right';
429             return unless $self->auto_pad;
430              
431             $self->setup_fonts();
432             my $width = $self->{key_font}->width;
433              
434             my $key = $self->track2key($track);
435             return unless defined $key;
436              
437             my $has_parts = $track->parts;
438             next if !$has_parts && $empty_track_style eq 'suppress';
439              
440             my $width_needed = $self->{key_font}->width * CORE::length($key)+3;
441             if ($keystyle eq 'left') {
442             my $width_i_have = $self->pad_left;
443             $self->pad_left($width_needed) if $width_needed > $width_i_have;
444             } elsif ($keystyle eq 'right') {
445             $width_needed += $extra_padding;
446             my $width_i_have = $self->pad_right;
447             $self->pad_right($width_needed) if $width_needed > $width_i_have;
448             }
449             }
450              
451             sub extra_right_padding { EXTRA_RIGHT_PADDING }
452              
453             sub height {
454             my $self = shift;
455             $self->setup_fonts;
456              
457             for my $track (@{$self->{tracks}}) {
458             $self->_expand_padding($track);
459             }
460              
461             my $spacing = $self->spacing;
462             my $key_height = $self->format_key;
463             my $empty_track_style = $self->empty_track_style;
464             my $key_style = $self->key_style;
465             my $bottom_key = $key_style eq 'bottom';
466             my $between_key = $key_style eq 'between';
467             my $side_key = $key_style =~ /left|right/;
468             my $draw_empty = $empty_track_style =~ /^(line|dashed)$/;
469             my $keyheight = $self->{key_font}->height;
470             my $height = 0;
471             for my $track (@{$self->{tracks}}) {
472             my $draw_between = $between_key && $track->option('key');
473             my $has_parts = $track->parts;
474             next if !$has_parts && ($empty_track_style eq 'suppress'
475             or $empty_track_style eq 'key' && $bottom_key);
476             $height += $keyheight if $draw_between;
477             $height += $self->spacing;
478             my $layout_height = $track->layout_height;
479             $height += ($side_key && $keyheight > $layout_height) ? $keyheight : $layout_height;
480             }
481              
482             # get rid of spacing under last track
483             $height -= $self->spacing unless $bottom_key;
484             return $height + $key_height + $self->pad_top + $self->pad_bottom + 2;
485             }
486              
487             sub setup_fonts {
488             my $self = shift;
489             return if ref $self->{key_font};
490              
491             my $image_class = $self->image_class;
492             my $keyfont = $self->{key_font};
493             my $font_obj = $image_class->$keyfont;
494             $self->{key_font} = $font_obj;
495             }
496              
497             sub gd {
498             my $self = shift;
499             my $existing_gd = shift;
500              
501             local $^W = 0; # can't track down the uninitialized variable warning
502              
503             return $self->{gd} if $self->{gd};
504              
505             $self->setup_fonts;
506              
507             unless ($existing_gd) {
508             my $image_class = $self->image_class;
509             eval "require $image_class; 1" or $self->throw($@);
510             }
511              
512             my $height = $self->height;
513             my $width = $self->width + $self->pad_left + $self->pad_right;
514              
515             my $pkg = $self->image_package;
516              
517             $height = 12 if $height < 1; # so GD doesn't crash
518             $width = 1 if $width < 1; # ditto
519              
520             my $gd = $existing_gd || $pkg->new($width,$height,
521             ($self->{truecolor} && $pkg->can('isTrueColor') ? 1 : ())
522             );
523             $gd->{debug} = 0 if $gd->isa('GD::SVG::Image'); # hack
524             $self->{gd} = $gd;
525              
526             if ($self->{truecolor}
527             && $pkg->can('saveAlpha')) {
528             $gd->saveAlpha(1);
529             }
530              
531             my %translation_table;
532             my $global_alpha = $self->{global_alpha} || 0;
533             for my $name (keys %COLORS) {
534             my $idx = $gd->colorAllocate(@{$COLORS{$name}});
535             $translation_table{$name} = $idx;
536             }
537              
538             $self->{translations} = \%translation_table;
539             $self->{gd} = $gd->isa('GD::SVG::Image') ? $gd
540             : $self->truetype ? Bio::Graphics::GDWrapper->new($gd,$self->truetype)
541             : $gd;
542            
543             eval {$gd->alphaBlending(0)};
544             if ($self->bgcolor) {
545             $gd->fill(0,0,$self->bgcolor);
546             } elsif (eval {$gd->isTrueColor}) {
547             $gd->fill(0,0,$translation_table{'white'});
548             }
549             eval {$gd->alphaBlending(1)};
550              
551             my $pl = $self->pad_left;
552             my $pt = $self->pad_top;
553             my $offset = $pt;
554             my $keyheight = $self->{key_font}->height;
555             my $bottom_key = $self->{key_style} eq 'bottom';
556             my $between_key = $self->{key_style} eq 'between';
557             my $left_key = $self->{key_style} eq 'left';
558             my $right_key = $self->{key_style} eq 'right';
559             my $empty_track_style = $self->empty_track_style;
560             my $spacing = $self->spacing;
561              
562             # we draw in two steps, once for background of tracks, and once for
563             # the contents. This allows the grid to sit on top of the track background.
564             for my $track (@{$self->{tracks}}) {
565             my $draw_between = $between_key && $track->option('key');
566             next if !$track->parts && ($empty_track_style eq 'suppress'
567             or $empty_track_style eq 'key' && $bottom_key);
568             $gd->filledRectangle($pl,
569             $offset,
570             $width-$self->pad_right,
571             $offset+$track->layout_height
572             + ($between_key ? $self->{key_font}->height : 0),
573             $track->tkcolor)
574             if defined $track->tkcolor;
575             $offset += $keyheight if $draw_between;
576             $offset += $track->layout_height + $spacing;
577             }
578              
579             $self->startGroup($gd);
580             $self->draw_background($gd,$self->{background}) if $self->{background};
581             $self->draw_grid($gd) if $self->{grid};
582             $self->draw_background($gd,$self->{postgrid}) if $self->{postgrid};
583             $self->endGroup($gd);
584              
585             $offset = $pt;
586             for my $track (@{$self->{tracks}}) {
587             $self->startGroup($gd);
588             my $draw_between = $between_key && $track->option('key');
589             my $has_parts = $track->parts;
590             my $side_key_height = 0;
591              
592             next if !$has_parts && ($empty_track_style eq 'suppress'
593             or $empty_track_style eq 'key' && $bottom_key);
594              
595             if ($draw_between) {
596             $offset += $self->draw_between_key($gd,$track,$offset);
597             }
598              
599             $self->draw_empty($gd,$offset,$empty_track_style)
600             if !$has_parts && $empty_track_style=~/^(line|dashed)$/;
601              
602             $track->draw($gd,$pl,$offset,0,1);
603              
604             if ($self->{key_style} =~ /^(left|right)$/) {
605             $side_key_height = $self->draw_side_key($gd,$track,$offset,$self->{key_style});
606             }
607              
608             $self->track_position($track,$offset);
609             my $layout_height = $track->layout_height;
610             $offset += ($side_key_height > $layout_height ? $side_key_height : $layout_height)+$spacing;
611              
612             $self->endGroup($gd);
613             }
614              
615              
616             $self->startGroup($gd);
617             $self->draw_bottom_key($gd,$pl,$offset) if $self->{key_style} eq 'bottom';
618             $self->endGroup($gd);
619              
620             return $self->{gd} = $self->rotate ? $gd->copyRotate90 : $gd;
621             }
622              
623             sub gdfont {
624             my $self = shift;
625             my $font = shift;
626             my $img_class = $self->image_class;
627              
628             if (!UNIVERSAL::isa($font,$img_class . '::Font') && $font =~ /^(gd|sanserif)/) {
629             my $ref = $self->{gdfonts} ||= {
630             gdTinyFont => $img_class->gdTinyFont(),
631             gdSmallFont => $img_class->gdSmallFont(),
632             gdMediumBoldFont => $img_class->gdMediumBoldFont(),
633             gdLargeFont => $img_class->gdLargeFont(),
634             gdGiantFont => $img_class->gdGiantFont(),
635             sanserif => $img_class->gdSmallFont(),
636             };
637             return $ref->{$font} || $ref->{gdSmallFont};
638             } else {
639             return $font;
640             }
641             }
642              
643             sub string_width {
644             my $self = shift;
645             my ($font,$string) = @_;
646              
647             my $class = $self->image_class;
648              
649             return $font->width*CORE::length($string)
650             unless $self->truetype && $class ne 'GD::SVG';
651             return Bio::Graphics::GDWrapper->string_width($font,$string);
652             }
653              
654             sub string_height {
655             my $self = shift;
656             my ($font,$string) = @_;
657              
658             my $class = $self->image_class;
659              
660             return $font->height
661             unless $self->truetype
662             && eval{$class eq 'GD' || $class->isa('GD::Image')};
663              
664             return Bio::Graphics::GDWrapper->string_height($font,$string);
665             }
666              
667             sub startGroup {
668             my $self = shift;
669             my $gd = shift;
670             $gd->startGroup if $gd->can('startGroup');
671             }
672              
673             sub endGroup {
674             my $self = shift;
675             my $gd = shift;
676             $gd->endGroup if $gd->can('endGroup');
677             }
678              
679              
680             # Package accessors
681             # GD (and GD::SVG)'s new() resides in GD::Image
682             sub image_class { return shift->{image_class}; }
683             sub image_package { return shift->{image_package}; }
684             sub polygon_package { return shift->{polygon_package}; }
685              
686             sub boxes {
687             my $self = shift;
688              
689             if (my $boxes = $self->{boxes}){ # cached result
690             return wantarray ? @$boxes : $boxes;
691             }
692              
693             my @boxes;
694             my $offset = 0;
695              
696             $self->setup_fonts;
697              
698             my $pl = $self->pad_left;
699             my $pt = $self->pad_top;
700              
701             my $between_key = $self->{key_style} eq 'between';
702             my $bottom_key = $self->{key_style} eq 'bottom';
703             my $empty_track_style = $self->empty_track_style;
704             my $keyheight = $self->{key_font}->height;
705             my $spacing = $self->spacing;
706             my $rotate = $self->rotate;
707              
708             for my $track (@{$self->{tracks}}) {
709             my $draw_between = $between_key && $track->option('key');
710             next if !$track->parts && ($empty_track_style eq 'suppress'
711             or $empty_track_style eq 'key' && $bottom_key);
712             $offset += $keyheight if $draw_between;
713             my $height = $track->layout_height;
714             my $boxes = $track->boxes($pl,$offset+$pt);
715             $self->track_position($track,$offset);
716             push @boxes,@$boxes;
717             $offset += $track->layout_height + $self->spacing;
718             }
719              
720             if ($rotate) {
721             my $x_offset = $self->height-1;
722             @boxes = map {
723             @{$_}[1,2,3,4] = @{$_}[4,1,2,3];
724             ($_->[1],$_->[3]) = map {$x_offset - $_} @{$_}[1,3];
725             $_;
726             } @boxes;
727             }
728             $self->{boxes} = \@boxes;
729             return wantarray ? @boxes : \@boxes;
730             }
731              
732             sub track_position {
733             my $self = shift;
734             my $track = shift;
735             my $d = $self->{_track_position}{$track};
736             $self->{_track_position}{$track} = shift if @_;
737             $d;
738             }
739              
740             # draw the keys -- between
741             sub draw_between_key {
742             my $self = shift;
743             my ($gd,$track,$offset) = @_;
744             my $key = $self->track2key($track) or return 0;
745             my $x = $self->{key_align} eq 'center' ? $self->width - (CORE::length($key) * $self->{key_font}->width)/2
746             : $self->{key_align} eq 'right' ? $self->width - CORE::length($key)
747             : $self->pad_left;
748              
749             # Key color hard-coded. Should be configurable for the control freaks.
750             my $color = $self->translate_color('black');
751             $gd->string($self->{key_font},$x,$offset,$key,$color) unless $self->{suppress_key};
752             $self->add_key_box($track,$key,$x,$offset);
753             return $self->{key_font}->height;
754             }
755              
756             # draw the keys -- left or right side
757             sub draw_side_key {
758             my $self = shift;
759             my ($gd,$track,$offset,$side) = @_;
760             my $key = $self->track2key($track) or return;
761             my $pos = $side eq 'left' ? $self->pad_left - $self->{key_font}->width * CORE::length($key)-3
762             : $self->pad_left + $self->width + EXTRA_RIGHT_PADDING;
763             my $color = $self->translate_color('black');
764             unless ($self->{suppress_key}) {
765             $gd->filledRectangle($pos,$offset,
766             $pos+$self->{key_font}->width*CORE::length($key),$offset,#-$self->{key_font}->height)/2,
767             $self->bgcolor);
768             $gd->string($self->{key_font},$pos,$offset,$key,$color);
769             }
770             $self->add_key_box($track,$key,$pos,$offset);
771             return $self->{key_font}->height;
772             }
773              
774             # draw the keys -- bottom
775             sub draw_bottom_key {
776             my $self = shift;
777             my ($gd,$left,$top) = @_;
778             my $key_glyphs = $self->{key_glyphs} or return;
779              
780             my $color = $self->translate_color($self->{key_color});
781             $gd->filledRectangle($left,$top,$self->width - $self->pad_right,$self->height-$self->pad_bottom,$color);
782             my $text_color = $self->translate_color('black');
783             $gd->string($self->{key_font},$left,KEYPADTOP+$top,"KEY:",$text_color) unless $self->{suppress_key};
784             $top += $self->{key_font}->height + KEYPADTOP;
785             $_->draw($gd,$left,$top) foreach @$key_glyphs;
786             }
787              
788             # Format the key section, and return its height
789             sub format_key {
790             my $self = shift;
791             return 0 unless $self->key_style eq 'bottom';
792              
793             return $self->{key_height} if defined $self->{key_height};
794              
795             my $suppress = $self->{empty_track_style} eq 'suppress';
796             my $between = $self->{key_style} eq 'between';
797              
798             if ($between) {
799             my @key_tracks = $suppress
800             ? grep {$_->option('key') && $_->parts} @{$self->{tracks}}
801             : grep {$_->option('key')} @{$self->{tracks}};
802             return $self->{key_height} = @key_tracks * $self->{key_font}->height;
803             }
804              
805             elsif ($self->{key_style} eq 'bottom') {
806              
807             my ($height,$width) = (0,0);
808             my %tracks;
809             my @glyphs;
810             local $self->{flip} = 0; # don't want to worry about flipped keys!
811              
812             # determine how many glyphs become part of the key
813             # and their max size
814             for my $track (@{$self->{tracks}}) {
815              
816             next unless $track->option('key');
817             next if $suppress && !$track->parts;
818              
819             my $glyph;
820             if (my @parts = $track->parts) {
821             $glyph = $parts[0]->keyglyph;
822             } else {
823             my $t = Bio::Graphics::Feature->new(-segments=>
824             [Bio::Graphics::Feature->new(-start => $self->offset,
825             -stop => $self->offset+$self->length)]);
826             my $g = $track->factory->make_glyph(0,$t);
827             $glyph = $g->keyglyph;
828             }
829             next unless $glyph;
830              
831              
832             $tracks{$track} = $glyph;
833             my ($h,$w) = ($glyph->layout_height,
834             $glyph->layout_width);
835             $height = $h if $h > $height;
836             $width = $w if $w > $width;
837             push @glyphs,$glyph;
838              
839             }
840              
841             $width += $self->key_spacing;
842              
843             # no key glyphs, no key
844             return $self->{key_height} = 0 unless @glyphs;
845              
846             # now height and width hold the largest glyph, and $glyph_count
847             # contains the number of glyphs. We will format them into a
848             # box that is roughly 3 height/4 width (golden mean)
849             my $rows = 0;
850             my $cols = 0;
851             my $maxwidth = $self->width - $self->pad_left - $self->pad_right;
852             while (++$rows) {
853             $cols = @glyphs / $rows;
854             $cols = int ($cols+1) if $cols =~ /\./; # round upward for fractions
855             my $total_width = $cols * $width;
856             my $total_height = $rows * $width;
857             last if $total_width < $maxwidth;
858             }
859              
860             # move glyphs into row-major format
861             my $spacing = $self->key_spacing;
862             my $i = 0;
863             for (my $c = 0; $c < $cols; $c++) {
864             for (my $r = 0; $r < $rows; $r++) {
865             my $x = $c * ($width + $spacing);
866             my $y = $r * ($height + $spacing);
867             next unless defined $glyphs[$i];
868             $glyphs[$i]->move($x,$y);
869             $i++;
870             }
871             }
872              
873             $self->{key_glyphs} = \@glyphs; # remember our key glyphs
874             # remember our key height
875             return $self->{key_height} =
876             ($height+$spacing) * $rows + $self->{key_font}->height +KEYPADTOP;
877             }
878              
879             else { # no known key style, neither "between" nor "bottom"
880             return $self->{key_height} = 0;
881             }
882             }
883              
884             sub add_key_box {
885             my $self = shift;
886             my ($track,$label,$x,$y, $is_legend) = @_;
887             my $value = [$label,$x,$y,$x+$self->{key_font}->width*CORE::length($label),$y+$self->{key_font}->height,$track];
888             push @{$self->{key_boxes}},$value;
889             }
890              
891             sub key_boxes {
892             my $ref = shift->{key_boxes};
893             return wantarray ? @$ref : $ref;
894             }
895              
896             sub add_category_labels {
897             my $self = shift;
898             my $d = $self->{add_category_labels};
899             $self->{add_category_labels} = shift if @_;
900             $d;
901             }
902              
903             sub track2key {
904             my $self = shift;
905             my $track = shift;
906             return $track->make_key_name();
907             }
908              
909             sub draw_empty {
910             my $self = shift;
911             my ($gd,$offset,$style) = @_;
912             $offset += $self->spacing/2;
913             my $left = $self->pad_left;
914             my $right = $self->width-$self->pad_right;
915             my $color = $self->translate_color(MISSING_TRACK_COLOR);
916             my $ic = $self->image_class;
917             if ($style eq 'dashed') {
918             $gd->setStyle($color,$color,$ic->gdTransparent(),$ic->gdTransparent());
919             $gd->line($left,$offset,$right,$offset,$ic->gdStyled());
920             } else {
921             $gd->line($left,$offset,$right,$offset,$color);
922             }
923             $offset;
924             }
925              
926             # draw a grid
927             sub draw_grid {
928             my $self = shift;
929             my $gd = shift;
930              
931             my $gridcolor = $self->translate_color($self->{gridcolor});
932             my $gridmajorcolor = $self->translate_color($self->{gridmajorcolor});
933             my @positions;
934             my ($major,$minor);
935             if (ref $self->{grid} eq 'ARRAY') {
936             @positions = @{$self->{grid}};
937             } else {
938             ($major,$minor) = $self->ticks;
939             my $first_tick = $minor * int($self->start/$minor);
940             for (my $i = $first_tick; $i <= $self->end+1; $i += $minor) {
941             push @positions,$i;
942             }
943             }
944             my $pl = $self->pad_left;
945             my $pt = $self->extend_grid ? 0 : $self->pad_top;
946             my $pr = $self->right;
947             my $pb = $self->extend_grid ? $self->height : $self->height - $self->pad_bottom;
948             my $offset = $self->{offset}+$self->{length}+1;
949             for my $tick (@positions) {
950             my ($pos) = $self->map_pt($self->{flip} ? $offset - $tick
951             : $tick);
952             my $color = (defined $major && $tick % $major == 0) ? $gridmajorcolor : $gridcolor;
953             $gd->line($pl+$pos,$pt,$pl+$pos,$pb,$color);
954             }
955             }
956              
957             # draw an image (or invoke a drawing routine)
958             sub draw_background {
959             my $self = shift;
960             my ($gd,$image_or_routine) = @_;
961             if (ref $image_or_routine eq 'CODE') {
962             return $image_or_routine->($gd,$self);
963             }
964             if (-f $image_or_routine) { # a file to draw
965             my $method = $image_or_routine =~ /\.png$/i ? 'newFromPng'
966             : $image_or_routine =~ /\.jpe?g$/i ? 'newFromJpeg'
967             : $image_or_routine =~ /\.gd$/i ? 'newFromGd'
968             : $image_or_routine =~ /\.gif$/i ? 'newFromGif'
969             : $image_or_routine =~ /\.xbm$/i ? 'newFromXbm'
970             : '';
971             return unless $method;
972             my $image = eval {$self->image_package->$method($image_or_routine)};
973             unless ($image) {
974             warn $@;
975             return;
976             }
977             my ($src_width,$src_height) = $image->getBounds;
978             my ($dst_width,$dst_height) = $gd->getBounds;
979             # tile the thing on
980             for (my $x = 0; $x < $dst_width; $x += $src_width) {
981             for (my $y = 0; $y < $dst_height; $y += $src_height) {
982             $gd->copy($image,$x,$y,0,0,$src_width,$src_height);
983             }
984             }
985             }
986             }
987              
988             # calculate major and minor ticks, given a start position
989             sub ticks {
990             my $self = shift;
991             my ($length,$minwidth) = @_;
992              
993             my $img = $self->image_class;
994             $length = $self->{length} unless defined $length;
995             $minwidth = $img->gdSmallFont->width*7 unless defined $minwidth;
996              
997             my ($major,$minor);
998              
999             # figure out tick mark scale
1000             # we want no more than 1 major tick mark every 40 pixels
1001             # and enough room for the labels
1002             my $scale = $self->scale;
1003              
1004             my $interval = 10;
1005              
1006             while (1) {
1007             my $pixels = $interval * $scale;
1008             last if $pixels >= $minwidth;
1009             $interval *= 10;
1010             }
1011              
1012             # to make sure a major tick shows up somewhere in the first half
1013             #
1014             # $interval *= .5 if ($interval > 0.5*$length);
1015              
1016             return ($interval,$interval/10);
1017             }
1018              
1019             # reverse of translate(); given index, return rgb triplet
1020             sub rgb {
1021             my $self = shift;
1022             my $idx = shift;
1023             my $gd = $self->{gd} or return;
1024             return $gd->rgb($idx);
1025             }
1026              
1027             sub transparent_color {
1028             my $self = shift;
1029             my ($opacity,@colors) = @_;
1030             return $self->_translate_color($opacity,@colors);
1031             }
1032              
1033             sub translate_color {
1034             my $self = shift;
1035             my @colors = @_;
1036             return $self->_translate_color(1.0,@colors);
1037             }
1038              
1039             sub _translate_color {
1040             my $self = shift;
1041             my ($opacity,@colors) = @_;
1042             $opacity = '1.0' if $opacity == 1;
1043             my $default_alpha = $self->adjust_alpha($opacity);
1044             $default_alpha ||= 0;
1045              
1046             my $ckey = "@{colors}_${default_alpha}";
1047             return $self->{closestcache}{$ckey} if exists $self->{closestcache}{$ckey};
1048              
1049             my $index;
1050             my $gd = $self->gd or return 1;
1051             my $table = $self->{translations} or return 1;
1052              
1053             if (@colors == 3) {
1054             $index = $gd->colorAllocateAlpha(@colors,$default_alpha);
1055             }
1056             elsif ($colors[0] =~ /^\#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i) {
1057             my ($r,$g,$b,$alpha) = (hex($1),hex($2),hex($3),hex($4));
1058             $index = $gd->colorAllocateAlpha($r,$g,$b,$alpha);
1059             }
1060             elsif ($colors[0] =~ /^\#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})$/i) {
1061             my ($r,$g,$b) = (hex($1),hex($2),hex($3));
1062             $index = $gd->colorAllocateAlpha($r,$g,$b,$default_alpha);
1063             }
1064             elsif ($colors[0] =~ /^(\d+),(\d+),(\d+),([\d.]+)$/i ||
1065             $colors[0] =~ /^rgba\((\d+),(\d+),(\d+),([\d.]+)\)$/) {
1066             my $alpha = $self->adjust_alpha($4);
1067             my (@rgb) = map {/(\d+)%/ ? int(255 * $1 / 100) : $_} ($1,$2,$3);
1068             $index = $gd->colorAllocateAlpha(@rgb,$4);
1069             }
1070             elsif ($colors[0] =~ /^(\d+),(\d+),(\d+)$/i ||
1071             $colors[0] =~ /^rgb\((\d+),(\d+),(\d+)\)$/i
1072             ) {
1073             my (@rgb) = map {/(\d+)%/ ? int(255 * $1 / 100) : $_} ($1,$2,$3);
1074             $index = $gd->colorAllocateAlpha(@rgb,$default_alpha);
1075             }
1076             elsif ($colors[0] eq 'transparent') {
1077             $index = $gd->colorAllocateAlpha(255,255,255,127);
1078             }
1079             elsif ($colors[0] =~ /^(\w+):([\d.]+)/) { # color:alpha
1080             my @rgb = $self->color_name_to_rgb($1);
1081             @rgb = (0,0,0) unless @rgb;
1082             my $alpha = $self->adjust_alpha($2);
1083             $index = $gd->colorAllocateAlpha(@rgb,$alpha);
1084             }
1085             elsif ($default_alpha < 127) {
1086             my @rgb = $self->color_name_to_rgb($colors[0]);
1087             @rgb = (0,0,0) unless @rgb;
1088             $index = $gd->colorAllocateAlpha(@rgb,$default_alpha);
1089             }
1090             else {
1091             $index = defined $table->{$colors[0]} ? $table->{$colors[0]} : 1;
1092             }
1093             return $self->{closestcache}{$ckey} = $index;
1094             }
1095              
1096             # change CSS opacity values (0-1.0) into GD opacity values (127-0)
1097             sub adjust_alpha {
1098             my $self = shift;
1099             my $value = shift;
1100             my $alpha = $value =~ /\./ # floating point
1101             ? int(127-($value*127)+0.5)
1102             : $value;
1103             $alpha = 0 if $alpha < 0;
1104             $alpha = 127 if $alpha > 127;
1105             return $alpha;
1106             }
1107              
1108             # workaround for bad GD
1109             sub colorClosest {
1110             my ($self,$gd,@c) = @_;
1111             return $gd->colorResolve(@c) if $GD::VERSION < 2.04;
1112              
1113             my $index = $gd->colorResolve(@c);
1114             return $index if $index >= 0;
1115              
1116             my $value;
1117             for (keys %COLORS) {
1118             my ($r,$g,$b) = @{$COLORS{$_}};
1119             my $dist = ($r-$c[0])**2 + ($g-$c[1])**2 + ($b-$c[2])**2;
1120             ($value,$index) = ($dist,$_) if !defined($value) || $dist < $value;
1121             }
1122             return $self->{translations}{$index};
1123             }
1124              
1125             sub bgcolor {
1126             my $self = shift;
1127             return unless $self->{bgcolor};
1128             return $self->translate_color($self->{bgcolor});
1129             }
1130              
1131             sub set_pen {
1132             my $self = shift;
1133             my ($linewidth,$color) = @_;
1134             return $self->{pens}{$linewidth,$color} if $self->{pens}{$linewidth,$color};
1135             my $gd = $self->{gd};
1136             my $pkg = $self->image_package;
1137             my $pen = $self->{pens}{$linewidth} = $pkg->new($linewidth,$linewidth);
1138             my @rgb = $self->rgb($color);
1139             my $bg = $pen->colorAllocate(255,255,255);
1140             my $fg = $pen->colorAllocate(@rgb);
1141             $pen->fill(0,0,$fg);
1142             $gd->setBrush($pen);
1143             return $self->image_class->gdBrushed();
1144             }
1145              
1146             sub png {
1147             my $gd = shift->gd;
1148             $gd->png;
1149             }
1150              
1151             sub svg {
1152             my $gd = shift->gd;
1153             $gd->svg;
1154             }
1155              
1156              
1157             # WARNING: THIS STUFF IS COPIED FROM Bio::Graphics::Browser.pm AND
1158             # Bio::Graphics::FeatureFile AND MUST BE REFACTORED
1159             # write a png image to disk and generate an image map in a convenient
1160             # CGIish way.
1161             sub image_and_map {
1162             my $self = shift;
1163             my %args = @_;
1164             my $link_rule = $args{-link} || $self->{linkrule};
1165             my $title_rule = $args{-title} || $self->{titlerule};
1166             my $target_rule = $args{-target} || $self->{targetrule};
1167             my $tmpurl = $args{-url} || '/tmp';
1168             my $docroot = $args{-root} || $ENV{DOCUMENT_ROOT} || '';
1169             my $mapname = $args{-mapname} || $IMAGEMAP++;
1170             $docroot .= '/' if $docroot && $docroot !~ m!/$!;
1171              
1172             # get rid of any netstat part please
1173             (my $tmpurlbase = $tmpurl) =~ s!^\w+://[^/]+!!;
1174              
1175             my $tmpdir = "${docroot}${tmpurlbase}";
1176              
1177             my $url = $self->create_web_image($tmpurl,$tmpdir);
1178             my $map = $self->create_web_map($mapname,$link_rule,$title_rule,$target_rule);
1179             return ($url,$map,$mapname);
1180             }
1181              
1182             sub create_web_image {
1183             my $self = shift;
1184             my ($tmpurl,$tmpdir) = @_;
1185              
1186             # create directory if it isn't there already
1187             # we need to untaint tmpdir before calling mkpath()
1188             return unless $tmpdir =~ /^(.+)$/;
1189             my $path = $1;
1190             unless (-d $path) {
1191             require File::Path unless defined &File::Path::mkpath;
1192             File::Path::mkpath($path,0,0777) or $self->throw("Couldn't create temporary image directory $path: $!");
1193             }
1194              
1195             unless (defined &Digest::MD5::md5_hex) {
1196             eval "require Digest::MD5; 1"
1197             or $self->throw("Sorry, but the image_and_map() method requires the Digest::MD5 module.");
1198             }
1199             my $data = $self->png;
1200             my $signature = Digest::MD5::md5_hex($data);
1201             my $extension = 'png';
1202              
1203             # untaint signature for use in open
1204             $signature =~ /^([0-9A-Fa-f]+)$/g or return;
1205             $signature = $1;
1206              
1207             my $url = sprintf("%s/%s.%s",$tmpurl,$signature,$extension);
1208             my $imagefile = sprintf("%s/%s.%s",$tmpdir,$signature,$extension);
1209              
1210             open (my $F,">", $imagefile) || $self->throw("Can't open image file $imagefile for writing: $!\n");
1211             binmode($F);
1212             print $F $data;
1213              
1214             return $url;
1215             }
1216              
1217             sub create_web_map {
1218             my $self = shift;
1219             my ($name,$linkrule,$titlerule,$targetrule) = @_;
1220             $name ||= 'map';
1221             my $boxes = $self->boxes;
1222             my (%track2link,%track2title,%track2target);
1223              
1224             eval "require CGI" unless CGI->can('escapeHTML');
1225              
1226             my $map = qq(\n);
1227             foreach (@$boxes){
1228             my ($feature,$left,$top,$right,$bottom,$track) = @$_;
1229             next unless $feature->can('primary_tag');
1230              
1231             my $lr = $track2link{$track} ||= (defined $track->option('link') ? $track->option('link') : $linkrule);
1232             next unless $lr;
1233              
1234             my $tr = exists $track2title{$track}
1235             ? $track2title{$track}
1236             : $track2title{$track} ||= (defined $track->option('title') ? $track->option('title') : $titlerule);
1237             my $tgr = exists $track2target{$track}
1238             ? $track2target{$track}
1239             : $track2target{$track} ||= (defined $track->option('target')? $track->option('target') : $targetrule);
1240              
1241             my $href = $self->make_link($lr,$feature);
1242             my $title = CGI::escapeHTML($self->make_link($tr,$feature,1));
1243             my $target = CGI::escapeHTML($self->make_link($tgr,$feature,1));
1244              
1245              
1246             my $a = $title ? qq(title="$title") : '';
1247             my $t = $target ? qq(target="$target") : '';
1248             $map .= qq(\n) if $href;
1249             }
1250             $map .= "\n";
1251             $map;
1252             }
1253              
1254             sub make_link {
1255             my $self = shift;
1256             my ($linkrule,$feature,$escapeHTML) = @_;
1257             eval "require Bio::Graphics::FeatureFile;1"
1258             unless Bio::Graphics::FeatureFile->can('link_pattern');
1259             return Bio::Graphics::FeatureFile->link_pattern($linkrule,$feature,$self,$escapeHTML);
1260             }
1261              
1262             sub make_title {
1263             my $self = shift;
1264             my $feature = shift;
1265             eval "require Bio::Graphics::FeatureFile;1"
1266             unless Bio::Graphics::FeatureFile->can('make_title');
1267             return Bio::Graphics::FeatureFile->make_title($feature);
1268             }
1269              
1270             sub read_colors {
1271             my $class = shift;
1272             local ($/) = "\n";
1273             local $_;
1274             while () {
1275             chomp;
1276             last if /^__END__/;
1277             my ($name,$r,$g,$b) = split /\s+/;
1278             @{$COLORS{$name}} = (hex $r,hex $g, hex $b);
1279             }
1280             }
1281              
1282             sub color_name_to_rgb {
1283             my $class = shift;
1284             my $color_name = shift;
1285             $class->read_colors() unless %COLORS;
1286             return unless $COLORS{$color_name};
1287             return wantarray ? @{$COLORS{$color_name}}
1288             : $COLORS{$color_name};
1289             }
1290              
1291             sub color_names {
1292             my $class = shift;
1293             $class->read_colors unless %COLORS;
1294             return wantarray ? keys %COLORS : [keys %COLORS];
1295             }
1296              
1297             sub glyph_scratch {
1298             my $self = shift;
1299             my $d = $GlyphScratch;
1300             $GlyphScratch = shift if @_;
1301             $d;
1302             }
1303              
1304             sub finished {
1305             my $self = shift;
1306             for my $track (@{$self->{tracks} || []}) {
1307             $track->finished();
1308             }
1309             delete $self->{tracks};
1310             }
1311              
1312             1;
1313              
1314             __DATA__