1 package Bio
::Graphics
::Ruler
;
12 Bio::Graphics::Ruler - functions for drawing a ruler on a L<GD::Image>
17 -scale => 'log' or 'linear',
24 -length => 100, #pixels
27 -font => 'sans:normal',
28 -tt => 1, #use truetype fonts
34 Function for drawing a ruler on a L<GD::Image>.
38 Robert Buels - rmb32 at cornell dot edu
42 All functions below are EXPORT_OK.
46 use base qw
/Exporter/;
57 Usage: ruler($image, arg => value, ...)
58 Desc : draw a ruler on a GD::Image
59 Args : a GD::Image, then the args:
60 -scale => 'log' or 'linear',
69 -label_pos => 'above' 'below' 'left' 'right'
70 -length => 100, #pixels
73 -font => 'sans:normal', #or a gd font like gdSmallFont
74 -tt => 1, #use truetype fonts
77 Side Effects: draws a ruler on the given image
84 UNIVERSAL
::isa
($im,'GD::Image')
85 or croak
"first argument must be a GD::Image or a subclass";
87 #process options and validate args
88 my @valid_args = qw
/-scale -log_base -label -units -range -ticks -tick_vals -start -dir -label_pos -length -width -font -tt/;
89 my %valid_args; $valid_args{$_} = 1 foreach @valid_args;
90 while(my ($k,$v) = each %args) {
91 $valid_args{$k} or croak
"invalid argument '$k'";
93 $v eq 'log' or $v eq 'linear'
94 or croak
"invalid -scale '$v'";
95 } elsif($k eq 'log_base') {
96 $v eq 'e' || $v =~ /^\d+$/
97 or croak
"invalid -log_base '$v', must be a number or 'e'";
98 } elsif($k eq '-range') {
99 ref($v) && ref($v) eq 'ARRAY' && @
$v ==2
101 or croak
"invalid -range '$v'";
102 } elsif($k eq '-ticks') {
103 $v == 0 or $v == 1 or $v == 2
104 or croak
"invalid -ticks '$v', must be 0, 1, or 2";
105 } elsif($k eq '-tick_vals') {
107 or croak
"invalid -tick_vals, must be an arrayref";
108 } elsif($k eq '-dir') {
109 grep {$v eq $_} qw
/up down left right/
110 or croak
"-dir must be either up, down, left, or right";
111 } elsif($k eq '-label_pos') {
112 grep {$v eq $_} qw
/above below left right/
113 or croak
"-label_pos must be either above, below, left, or right";
114 } elsif($k eq '-length') {
115 $v > 0 or croak
"invalid length '$v'";
116 } elsif($k eq '-width') {
117 $v > 0 or croak
"invalid width '$v'";
118 } elsif($k eq '-start') {
119 ref($v) && ref($v) eq 'ARRAY' && @
$v ==2
120 && $v->[0] >= 0 && $v->[1] >= 0
121 or croak
"invalid start '$v'";
125 $args{-scale
} ||= 'linear';
126 $args{-log_base
} ||= 10;
127 $args{-label
} ||= '';
128 $args{-units
} ||= '';
129 $args{-range
} ||= [0,1000];
131 $args{-start
} ||= [0,0];
132 $args{-dir
} ||= 'horizontal';
133 $args{-label_pos
} ||= $args{-dir
} eq 'up' || $args{-dir
} eq 'down' ?
'left' : 'below';
134 $args{-length} ||= 100;
135 $args{-width
} ||= 10;
137 $args{-font
} ||= $args{-tt
} ?
'sans:normal' : gdSmallFont
;
139 #draw the base line and end caps
140 my $black = $im->colorAllocate(0,0,0);
141 $im->setThickness(1);
143 my $majwidth = $args{-width
};
144 my $minwidth = $majwidth*0.75;
145 my $len = $args{-length};
146 my $s = $args{-start
};
147 my $e = $args{-dir
} eq 'up' ?
[$s->[0], $s->[1]-$len] :
148 $args{-dir
} eq 'down' ?
[$s->[0], $s->[1]+$len] :
149 $args{-dir
} eq 'left' ?
[$s->[0]-$len, $s->[1]] :
150 [$s->[0]+$len, $s->[1]];
151 my ($range_start,$range_end) = @
{$args{-range
}};
152 my $range_length = abs($range_end-$range_start);
154 $im->line(@
$s,@
$e,$black);
156 #now draw some tick marks and label them
158 if($args{-scale
} eq 'linear') {
159 my $px_per_num = $args{-length}/$range_length;
160 sub {$_[0]*$px_per_num}
161 } elsif($args{-scale
} eq 'log') {
162 my $logdiff = abs(_log
($range_end/($range_start||1),$args{-log_base
}));
163 my $px_per_num = $args{-length}/$logdiff;
164 # warn "logdiff is $logdiff, ppn is $px_per_num\n";
165 sub {_log
($_[0]||1,$args{-log_base
})*$px_per_num}
169 #now get the numerical values at which to draw ticks,
170 #either calculate them or get them from our arguments
172 if($args{-tick_vals
}) {
174 } elsif($args{-scale
} eq 'linear') {
175 my $maj_tick_interval = _pick_linear_tick_interval
($range_length,$args{-length},$args{-dir
});
176 my $maj_tick_step = $range_start > $range_end ?
-$maj_tick_interval : $maj_tick_interval;
177 # warn "for $args{-range}->[0],$args{-range}->[1], picked maj tick interval of $maj_tick_interval\n";
179 for(my $i = 0; $i*$maj_tick_interval<=$range_length; $i++) {
180 push @t,$range_start + $i*$maj_tick_step;
183 } elsif($args{-scale
} eq 'log') {
184 my $log_range_length = abs _log
($range_end/($range_start||1),$args{-log_base
});
185 my $log_range_start = _log
($range_start||1,$args{-log_base
});
186 my $maj_tick_interval = 1;
187 my $maj_tick_step = $range_start > $range_end ?
-$maj_tick_interval : $maj_tick_interval;
188 # warn "for log range length $log_range_length, picked maj tick interval of $maj_tick_interval\n";
190 for(my $i = 0; ($i*$maj_tick_interval)<=$log_range_length; $i++) {
191 push @t,_exp
($args{-log_base
}, $log_range_start + $i*$maj_tick_step);
193 # warn "made ticks ".join(',',@t)."\n";
199 foreach my $tickval (@ticks) {
200 # warn "$tickval => ".$num2px->($tickval)."\n";
201 my @offset_coords = _offset
(@
$s,$num2px->($tickval) - $num2px->($range_start) ,$args{-dir
});
202 _tick
($im,$black,,@offset_coords, $majwidth,$args{-dir
});
203 _label
($im,$black, _commify_number
($tickval), @offset_coords,$majwidth,$args{-label_pos
});
207 sub _commify_number
{
209 return undef unless defined $_;
210 1 while s/^(-?\d+)(\d{3})/$1,$2/;
215 my ($x,$y,$distance,$dir) = @_;
218 } elsif($dir eq 'down') {
220 } elsif($dir eq 'left') {
222 } elsif($dir eq 'right') {
225 confess
"invalid dir '$dir'";
231 my ($base,$exp) = @_;
235 $base =~ /^\d+$/ or croak
"invalid log base '$base'";
240 my ($num,$base) = @_;
243 } elsif($base == 10) {
246 $base =~ /^\d+$/ or croak
"invalid log base '$base'";
247 return log($num)/log($base)
251 #given a range, and the length of pixels we have to cover it,
252 #pick a good tick interval for it
253 sub _pick_linear_tick_interval
{
254 my ($range_length, $length_px, $dir) = @_;
255 $range_length = abs $range_length;
257 my $tick_spacing_px = $dir eq 'up' || $dir eq 'down' ?
15 : 25;
258 my $ticks_we_have_room_for = int($length_px / $tick_spacing_px);
259 # warn "in $length_px, we have room for $ticks_we_have_room_for\n";
261 foreach my $modifier (25,20,5,3,2,1) {
262 foreach my $basep (reverse -10..10) {
263 my $interval = 10**$basep*$modifier;
264 my $numticks = $range_length/$interval;
265 # warn "for $range_length, $interval results in $numticks ticks, we have room for $ticks_we_have_room_for\n";
266 if ($numticks <= $ticks_we_have_room_for) {
267 $possible{$interval} = 1;
272 die "could not pick a tick interval\n"
275 return (sort {$a <=> $b} keys %possible)[0];
279 my ($im,$color,$x,$y,$width,$dir,$label,$label_pos) = @_;
281 if($dir eq 'up' || $dir eq 'down') {
282 $im->line($x+$width/2,$y,
285 } elsif($dir eq 'left' || $dir eq 'right' ) {
286 $im->line($x,$y+$width/2,
290 confess
"invalid direction '$dir'";
295 my ($im,$color,$text,$x,$y,$width,$label_rel) = @_;
296 my $offset = $width/2+2;
297 $im->useFontConfig(1);
298 # sub dp(@) { print join(',',@_),"\n"; @_ }
299 if($label_rel eq 'above') {
300 _stringFT_origin
('lc',$im,$color,'sans:normal',10, 0,
303 } elsif($label_rel eq 'left') {
304 _stringFT_origin
('cr',$im,$color,'sans:normal',10, 0,
307 } elsif($label_rel eq 'right' ) {
308 _stringFT_origin
('cl',$im,$color,'sans:normal',10, 0,
311 } elsif($label_rel eq 'below' ) {
312 _stringFT_origin
('uc',$im,$color,'sans:normal',10, 0,
316 confess
"invalid label direction '$label_rel'";
320 #just like stringFT, but using a different coordinate origin for the text
321 #does not allow rotated text
322 sub _stringFT_origin
{
323 my ($origin,$im,@stringargs) = @_;
326 if($origin eq 'll') {
327 #lower-left is the native origin for stringFT, so we don't have to do anything
328 return $im->stringFT(@stringargs);
330 my ($yor,$xor) = split '',$origin;
331 my @bounds = GD
::Image
->stringFT(@stringargs);
332 my $width = $bounds[2]-$bounds[0];
333 my $height = $bounds[3]-$bounds[5];
335 $yor eq 'u' ?
$height :
336 $yor eq 'c' ?
$height/2 :
338 confess
"invalid y origin '$yor'";
340 $xor eq 'r' ?
$width :
341 $xor eq 'c' ?
$width/2 :
343 confess
"invalid x origin '$xor'";
345 return $im->stringFT(@stringargs);