start fixing test for multi cat phenotype upload.
[sgn.git] / lib / CXGN / Transcript / DrawContigAlign.pm
blobb936a735c64bb5bc3068526d34258a868fa1e270
1 =head1 NAME
3 CXGN::Transcript::DrawContigAlign - draws alignment graphs of unigenes.
5 =head1 SYNOPSIS
7 This should be instantiated (without arguments), have alignment data fed to it, and instructed where to put the image and imagemap.
9 $imageProgam = CXGN::Transcript::DrawContigAlign->new();
10 $imageProgram->addAlignment('SGN-E1199194', 'SGN-U508096', '+', 0, 477, 0, 0, 1);
11 $imageProgram->addAlignment('SGN-E1189844', 'SGN-U508096', '+', 75, 760, 30, 0, 0);
12 #add more alignments
13 $imageProgram->writeImageToFile('Image.png', 'Mapfile', 'http://some.domain.name/some/url/thingie/', 'Image name');
15 =head1 DESCRIPTION
17 DrawContigAlign receives information about sequences stored in a unigene and produces a graph of the aligned sequences.
19 =head1 AUTHORS
21 Rafael Lizarralde <xarbiogeek@gmail.com> (July 2009)
23 Paraphrased from a C script (draw_contigalign.c) of unknown origin.
25 =head1 MEMBER FUNCTIONS
27 This class implements the following functions:
29 =head2 constructor C<new>
31 =over 10
33 =item Usage:
35 $imageProgram = CXGN::Transcript::DrawContigAlign->new();
37 =item Ret:
39 a CXGN::Transcript::DrawContigAlign object
41 =item Args:
43 none
45 =back
47 =cut
49 package CXGN::Transcript::DrawContigAlign;
51 use Moose;
52 use MooseX::Method::Signatures;
54 use GD;
55 use CXGN::Transcript::DrawContigAlign::Pane;
56 use CXGN::Transcript::DrawContigAlign::ContigAlign;
57 use CXGN::Transcript::DrawContigAlign::DepthData;
59 use constant Pane => 'CXGN::Transcript::DrawContigAlign::Pane';
60 use constant ContigAlign => 'CXGN::Transcript::DrawContigAlign::ContigAlign';
61 use constant DepthData => 'CXGN::Transcript::DrawContigAlign::DepthData';
63 #The list of sequences to be aligned
64 has contigAligns => (is => 'rw', isa => 'ArrayRef', default => sub { my @array = (); return \@array; });
66 =head2 mutator C<addAlignment>
68 =over 10
70 =item Usage:
72 $imageProgram->addAlignment('SGN-E1189844', 'SGN-U508096', '+', 75, 760, 30, 0, 0);
74 =item Desc:
76 adds alignment data to the list of data that will be incorporated in the graph
78 =item Ret:
80 nothing
82 =item Args:
84 =over 18
86 =item Str $sourceID
88 the source ID tag of the segment in a Unigene
90 =item Str $sequenceID
92 the sequence ID tag of the Unigene
94 =item Str $strand
96 an identifier determining whether it is complementary or not (if not, it should be '+')
98 =item Int $startLoc
100 the starting base pair of the segment
102 =item Int $endLoc
104 the ending base pair of the segment
106 =item Int $startTrim
108 the number of base pairs that have been trimmed (do not match) at the start
110 =item Int $endTrim
112 the number of base pairs that have been trimmed (do not match) at the end
114 =item Bool $highlight
116 whether or not the segment should be highlighted
118 =back
120 =back
122 =cut
124 #Adds a sequence to the list of contiguous alignments
125 method addAlignment (Str $sourceID!, Str $sequenceID!, Str $strand!,
126 Int $startLoc!, Int $endLoc!, Int $startTrim!, Int $endTrim!, Bool $highlight?) {
127 my @contigs = @{ $self->contigAligns };
128 my $contig = ContigAlign->new($sourceID, $sequenceID, $strand,
129 $startLoc, $endLoc, $startTrim, $endTrim, $highlight);
130 push @contigs, $contig;
131 $self->contigAligns( [ @contigs ] );
134 =head2 accessor C<writeImageToFile>
136 =over 10
138 =item Usage:
140 $imageProgram->writeImageToFile('Image.png', 'Mapfile', 'http://some.domain.name/some/url/thingie/', 'Image name');
142 =item Desc:
144 produces an image file and map file with the alignment information
146 =item Ret:
148 nothing
150 =item Args:
152 =over 20
154 =item Str $imageFilename
156 the path of the image file
158 =item Str $mapFilename
160 the path of the map file
162 =item Str $linkBasename
164 the url stub for the EST pages
166 =item Str $imageName
168 the name for the image
170 =back
172 =back
174 =cut
176 #Constants
177 my ($HISTOGRAM, $ALIGN, $INFO, $LEGEND) = (0..3);
178 my ($BORDERWIDTH, $PAD, $HISTOGRAMWIDTH) = (5, 10, 350);
179 my $XTICS = 10;
180 my ($BLACK, $BLUE, $RED, $YELLOW, $LIGHTGRAY, $DARKGRAY, $FRONTSHADE, $BACKSHADE);
182 #Creates the image based on the currently-possessed data and stores it in an image and map file
183 method writeImageToFile (Str $imageFilename!, Str $mapFilename!, Str $linkBasename!, Str $imageName) {
184 $self->contigAligns( [ sort { $a->compare($b) } @{ $self->contigAligns } ] );
185 $imageName = '(Unspecified)' if(!defined($imageName));
187 my @depths = @{ $self->computeDepths() };
188 my $yMax = $self->computeYMax(\@depths);
189 my $yScale = $self->computeYScale($yMax);
190 my $xMax = $self->computeXMax(\@depths);
191 my $xScale = $self->computeXScale($xMax);
193 my $font = &gdSmallFont;
194 my $textHeight = $font->height;
195 #makes the font height odd so the center is an integer, to match up with lines
196 $textHeight++ if($textHeight%2 == 0);
198 my @panes = $self->computePanes($font, $textHeight, $yMax);
200 #space needed for the histogram, plus labels (3), plus each segment, plus the border, plus padding for each pane
201 my $imageHeight = $panes[$ALIGN]->south + $PAD + $BORDERWIDTH;
202 #the rightmost edge of a right pane, plus the padding and the border
203 my $imageWidth = $panes[$INFO]->east + $PAD + $BORDERWIDTH;
204 #the width and height, and a true value to use a 24-bit color scheme (RGB 0-255)
205 my $image = GD::Image->new($imageWidth + 1,$imageHeight + 1);
207 $self->setColors($image);
208 $self->drawBorder($image, $imageWidth, $imageHeight);
209 $self->drawGrayBars($image, \@panes, $textHeight);
210 $self->drawHighlight($image, \@panes, $textHeight);
211 $self->drawHistogram(\@depths, $image, $panes[$HISTOGRAM], $font, $textHeight, $xMax, $xScale, $yMax, $yScale);
212 $self->drawAlignments($image, $panes[$ALIGN], $font, $textHeight, $xMax, $xScale);
213 $self->drawInfo($image, $panes[$INFO], $font, $textHeight, $mapFilename, $imageName, $linkBasename);
214 $self->drawLegend($image, $panes[$LEGEND], $font, $imageName, $depths[$#depths]->position);
216 #prints the image
217 open my $IMAGEFILE, ">" , $imageFilename or die "Could not create image file: $!";
218 print $IMAGEFILE $image->png;
219 close $IMAGEFILE;
222 #Calculates the total depth in each region of the sequence
223 method computeDepths {
224 my @contigAligns = @{ $self->contigAligns };
225 my %depthHash;
227 #adds all the positions using a hash to eliminate duplicates
228 for my $est (@contigAligns) {
229 $depthHash{$est->start} = CXGN::Transcript::DrawContigAlign::DepthData->new($est->start);
230 $depthHash{$est->end } = CXGN::Transcript::DrawContigAlign::DepthData->new($est->end );
232 my @depths = sort { $a->compare($b) } values %depthHash;
234 #increments the depths of each position for each sequence that covers it
235 for my $est (@contigAligns) {
236 for my $depth (@depths) {
237 $depth->increment if($depth->position >= $est->start and $depth->position < $est->end);
241 return \@depths;
244 #This probably needs to be rewritten to be more intelligent--60 is way bigger than most unigenes
245 method computeYMax (ArrayRef $depthList!) {
246 my @depths = @{ $depthList };
248 my $maxDepth = 0;
249 for my $depth (@depths) { $maxDepth = $depth->depth if($depth->depth > $maxDepth); }
251 return 60 if($maxDepth < 60);
252 return $self->roundTo($maxDepth, 30);
255 #This probably needs to be rewritten to be more intelligent--60 is way bigger than most unigenes
256 method computeYScale (Int $maxDepth!) {
257 return (($maxDepth <= 60) ? 15 : 20);
260 #Calculates the total length of the aligned sequence, rounded up to the next 100
261 method computeXMax (ArrayRef $depthList!) {
262 my @depths = @{ $depthList };
263 my $xMax = $depths[$#depths]->position;
264 return $self->roundTo($xMax, 100);
267 #Calculates the distance between tics on the x-axis
268 method computeXScale (Int $xMax!) {
269 return $xMax / $XTICS;
272 #Computes the dimensions of the panes, which are arranged like this:
273 # --------------------------------
274 # | | |
275 # | Histogram | Legend |
276 # | | |
277 # |--------------------------------|
278 # | | |
279 # | Sequence | Sequence |
280 # | Alignment | Info |
281 # | | |
282 # --------------------------------
283 method computePanes (Ref $font!, Int $textHeight!, Int $histogramHeight!) {
284 my $sequences = @{ $self->contigAligns };
286 #the 4*width accounts for the depth tic labels
287 my $histogram = Pane->new($BORDERWIDTH+$PAD,
288 $BORDERWIDTH+(2 * $PAD)+$histogramHeight+(3 * $textHeight),
289 $BORDERWIDTH+$PAD, $BORDERWIDTH+$PAD+$HISTOGRAMWIDTH+(4 * $font->width)+$textHeight);
291 my $alignment = Pane->new($histogram->south + $PAD,
292 $histogram->south + $PAD + ($sequences * $textHeight),
293 $histogram->west, $histogram->east);
295 my $info = Pane->new($alignment->north, $alignment->south,
296 $alignment->east, $alignment->east + (48 * $font->width));
298 my $legend = Pane->new($histogram->north, $histogram->south,
299 $info->west, $info->east);
301 return ($histogram, $alignment, $info, $legend);
305 #Initializes the colors that are used in the graph
306 method setColors (Ref $image!) {
307 #sets the background to white
308 my $bg = 255;
309 $image->colorAllocate($bg, $bg, $bg);
311 $BLACK = $image->colorResolve(0, 0, 0);
312 $BLUE = $image->colorResolve(0, 0, 255);
313 $RED = $image->colorResolve(255, 0, 0);
314 $YELLOW = $image->colorResolve(255, 255, 100);
315 $LIGHTGRAY = $image->colorResolve(200, 200, 200);
316 $FRONTSHADE = $image->colorResolve($bg - 50, $bg - 50, $bg - 50);
317 $BACKSHADE = $image->colorResolve($bg - 100, $bg - 100, $bg - 100);
320 #Draws the bevel border
321 method drawBorder (Ref $image!, Int $width!, Int $height!) {
322 for my $i (0..($BORDERWIDTH-1)) {
323 #draws a lighter border on the north and west sides
324 $image->line($i, $i, $width - ($i + 1), $i, $FRONTSHADE);
325 $image->line($i, $i, $i, $height - ($i + 1), $FRONTSHADE);
327 #draws a darker border on the south and east sides
328 $image->line($i, $height - $i, $width - $i, $height - $i, $BACKSHADE);
329 $image->line($width - $i, $i, $width - $i, $height - $i, $BACKSHADE);
333 #Draws the gray bars in the alignment pane as a visual aid
334 method drawGrayBars (Ref $image!, ArrayRef $paneList!, Int $textHeight!) {
335 my @panes = @{ $paneList };
336 my $sequences = @{ $self->contigAligns };
338 if($sequences > 6) {
339 for(my $i = 0; $i < $sequences; $i += 6) {
340 my $width = ( ($i < $sequences - 3) ? 3 : ($sequences - $i) );
341 $image->filledRectangle($panes[$ALIGN]->west, $panes[$ALIGN]->north + ($i * $textHeight),
342 $panes[$INFO]->east, $panes[$ALIGN]->north + (($i + $width) * $textHeight),
343 $LIGHTGRAY);
348 #Draws the highlight
349 method drawHighlight (Ref $image!, ArrayRef $paneList!, Int $textHeight!) {
350 my @panes = @{ $paneList };
351 my @contigAligns = @{ $self->contigAligns };
353 for my $i (0..(@contigAligns - 1)) {
354 if($contigAligns[$i]->highlight) {
355 $image->filledRectangle($panes[$ALIGN]->west, $panes[$ALIGN]->north + ($i * $textHeight),
356 $panes[$INFO]->east, $panes[$ALIGN]->north + (($i+1) * $textHeight), $YELLOW);
361 #Draws everything in the histogram pane of the image
362 method drawHistogram (ArrayRef $depthList!, Ref $image!, Object $pane!, Ref $font!,
363 Int $textHeight!, Int $xMax!, Int $xScale!, Int $yMax!, Int $yScale!) {
364 my @depths = @{ $depthList };
365 #defines the place where the histogram is drawn, instead of the axis labels and such
366 my $grid = Pane->new($pane->north + (3 * $textHeight), $pane->south - $PAD,
367 $self->round($pane->west + ($font->height * 1.5) + ($font->width * 4)), $pane->east - $PAD);
369 $self->prepareGraph($image, $font, $grid, $xMax, $xScale, $yMax, $yScale);
370 my $unitsPerPixel = $xMax/($grid->east - $grid->west);
372 my $darkGray = $image->colorResolve(128, 128, 128);
373 for my $i (0..(@depths - 2)) {
374 my $left = $grid->west + $self->round($depths[$i]->position/$unitsPerPixel);
375 $left++ if $left == $grid->west;
376 my $right = $grid->west + $self->round($depths[$i+1]->position/$unitsPerPixel);
377 my $top = $grid->north + 1;
378 my $bottom = $grid->north + $depths[$i]->depth + 1;
379 $image->filledRectangle($left, $top, $right, $bottom, $darkGray);
383 #Prepares the grid and axes for the histogram
384 method prepareGraph (Ref $image!, Ref $font!, Object $pane!,
385 Int $xMax!, Int $xScale!, Int $yMax!, Int $yScale!) {
386 my ($x, $y, $label);
388 #draws the surrounding rectangle
389 $image->rectangle($pane->west, $pane->north, $pane->east, $pane->south, $BLACK);
391 #draws the gridlines and tic marks
392 my $unitsPerPixel = $xMax / ($pane->east - $pane->west);
393 for my $i (0..$XTICS) {
394 #draws vertical gridlines
395 $x = $pane->west + $self->round(($i * $xScale)/$unitsPerPixel);
396 if($i > 0 and $i < $XTICS) {
397 for($y = $pane->north; $y < $pane->south; $y += 3) {
398 $image->setPixel($x, $y, $BLACK);
402 #draws horizontal tic marks
403 $image->line($x, $pane->north, $x, $pane->north - 3, $BLACK);
404 $image->line($x, $pane->south, $x, $pane->south + 1, $BLACK);
406 #draws the corresponding x-axis label
407 $label = $i * $xScale;
408 $x -= (length($label) * $font->width) / 2;
409 $y = $pane->north - 4 - $font->height;
410 $image->string($font, $x, $y, $label, $BLACK);
413 #draws the x-axis description
414 $label = 'position (bp)';
415 $x = $pane->west + ($pane->east - $pane->west)/2 - (length($label) * $font->width)/2;
416 $y = $pane->north - 5 - (2 * $font->height);
417 $image->string($font, $x, $y, $label, $BLACK);
419 for(my $i = 0; $i <= $yMax; $i += $yScale) {
420 #draws horizontal gridlines
421 $y = $i + $pane->north;
422 if($i > 0 and $i < $yMax) {
423 for($x = $pane->west; $x < $pane->east; $x += 3) {
424 $image->setPixel($x, $y, $BLACK);
428 #draws vertical tic marks
429 $image->line($pane->west, $y, $pane->west - 3, $y, $BLACK);
430 $image->line($pane->east, $y, $pane->east + 3, $y, $BLACK);
432 #draws the corresponding y-axis label
433 $label = $i;
434 $x = $pane->west - (length($label) * $font->width) - 4;
435 $y -= $font->height / 2;
436 $image->string($font, $x, $y, $label, $BLACK);
439 #draws the y-axis description
440 $label = "Depth";
441 $x = $pane->west - (4 * $font->width) - $font->height - 1;
442 $y -= $font->height / 2;
443 $image->stringUp($font, $x, $y, $label, $BLACK);
446 #Draws the alignment of the sequences
447 method drawAlignments (Ref $image!, Object $pane!, Ref $font!,
448 Int $textHeight!, Int $xMax!, Int $xScale!) {
449 my @contigAligns = @{ $self->contigAligns };
451 #computes the left and right bounds of the histogram above
452 my $west = $pane->west + (1.5 * $font->height) + (4 * $font->width);
453 my $east = $pane->east - $PAD;
454 my $unitsPerPixel = $xMax / ($east - $west);
455 my ($x, $y);
457 #draws the dotted lines corresponding to histogram depth changes
458 for my $i (0..$XTICS) {
459 $x = $west + $self->round(($i * $xScale) / $unitsPerPixel);
460 for($y = $pane->north; $y < $pane->south; $y += 3) {
461 $image->setPixel($x, $y, $BLACK);
465 #draws the lines for each sequence
466 for my $i (0..(@contigAligns - 1)) {
467 $y = $pane->north + $self->round($textHeight * ($i + .5));
469 my $contigAlign = $contigAligns[$i];
471 #the beginning and end of the matching sequence, excluding trim
472 my $startCover = $west + $self->round($contigAlign->start / $unitsPerPixel);
473 my $endCover = $west + $self->round($contigAlign->end / $unitsPerPixel);
474 my ($start, $end);
476 #draws a black line if it's the same strand, or a blue one if it's the complementary strand
477 $image->line($startCover, $y, $endCover, $y, (($contigAlign->strand eq '+') ? $BLACK : $BLUE));
479 #draws a leading red segment if the sequence was trimmed
480 if($contigAlign->startTrim > 0) {
481 if($contigAlign->startLoc < 0) {#This should never happen
482 $self->drawDottedLine($image, $west - 18, $y, $RED);
483 my $label = $contigAlign->startLoc . "bp";
484 $x = $west - ((length($label) + 1) * &gdTinyFont->width);
485 $image->string(&gdTinyFont, $x, $y - &gdTinyFont->height, $label, $RED);
486 $start = $west;
487 } else {#This should always happen
488 $start = $west + $self->round($contigAlign->startLoc / $unitsPerPixel);
490 $image->line($start, $y, $startCover - 1, $y, $RED);
493 if($contigAlign->endTrim > 0) {
494 if($contigAlign->endLoc > $xMax) {#This should never happen
495 $self->drawDottedLine($image, $east, $y, $RED);
496 my $label = ($contigAlign->endLoc - $xMax) . "bp";
497 $x = $east + &gdTinyFont->width;
498 $image->string(&gdTinyFont, $x, $y - &gdTinyFont->height, $label, $RED);
499 $end = $east;
500 } else {#This should always happen
501 $end = $west + $self->round($contigAlign->endLoc / $unitsPerPixel);
503 $image->line($endCover + 1, $y, $end, $y, $RED);
508 #This should never be called
509 method drawDottedLine(Ref $image!, Int $x!, Int $y!, Int $color!) {
510 for(my $i = 0; $i < 18; $i += 6) {
511 $image->line($x + $i, $y, $x + $i + 2, $y, $color);
515 #Draws the info for each of the aligned sequences
516 method drawInfo (Ref $image!, Object $pane!, Ref $font!,
517 Int $textHeight!, Str $mapFilename!, Str $imageName!, Str $linkBasename!) {
518 my @contigAligns = @{ $self->contigAligns };
520 my $west = $pane->west + (2 * $font->width);
522 open my $MAPFILE, ">" , $mapFilename or die "$! writing map file $mapFilename";
524 print $MAPFILE "<map name=\"contigmap_$imageName\">";
526 #draws the info for each strand in the alignment
527 for my $i (0..(@contigAligns - 1)) {
528 my $y = $pane->north + ($i * $textHeight);
529 my $contigAlign = $contigAligns[$i];
531 my $total = $contigAlign->endLoc - $contigAlign->startLoc;
532 my $used = $total - ($contigAlign->startTrim + $contigAlign->endTrim);
534 #sets the source ID to a 30-character string
535 my $sourceID = $contigAlign->sourceID;
536 $sourceID = substr($sourceID, 0, 26) . '... ' if(length($sourceID) > 29);
537 $sourceID = sprintf("%-30.30s", $sourceID);
539 #round down to avoid displaying 100% where there it's not a complete match
540 my $percent = $self->round((($used * 100) / $total ) - .5);
542 #set the base pair count to a 6-digit string (to even out 3-digit and 4-digit numbers)
543 my $bpCount = sprintf('%-6.6s', $total . 'bp');
545 my $label = "$sourceID $bpCount ($percent%)";
547 $image->string($font, $west, $y, $label, (($contigAlign->strand eq '+') ? $BLACK : $BLUE));
549 #die $label . "\n" . $contigAlign->sequenceID . "\n" . $linkBasename;
551 printf $MAPFILE "<area coords=\"%d,%d,%d,%d\" href=\"%s%s\">\n",
552 ($west, $y, $west + (length($label) * $font->width), $y + $font->height,
553 $linkBasename, $contigAlign->sequenceID);
556 print $MAPFILE "</map>\n";
557 close $MAPFILE;
560 #Draws the legend for the graph
561 method drawLegend (Ref $image!, Object $pane!,
562 Ref $font!, Str $imageName!, Int $length!) {
563 my $north = $pane->north + $PAD;
564 my $west = $pane->west + (5 * $PAD);
565 my $line = $font->height;
566 my $label;
568 $label = "Alignment Image: $imageName";
569 $image->string($font, $west, $north, $label, $BLACK);
571 my @labels = ("Alignment Image: $imageName", "Given Strand",
572 "Reverse Complement Strand", "Trimmed (non-matching) Sequence");
573 my @colors = ($BLACK, $BLACK, $BLUE, $RED);
575 for my $i (0..($#labels)) {
576 $image->string($font, $west + 20, $north + (($i + 1.5) * $line), $labels[$i], $colors[$i]);
577 $image->line($west, $north + (($i + 2) * $line), $west + 15, $north + (($i + 2) * $line), $colors[$i]);
580 $label = "Highlighted Strand";
581 $image->filledRectangle($west, $north + ((@labels + 1.5) * $line),
582 $pane->east, $north + ((@labels + 2.5) * $line),
583 $YELLOW);
584 $image->string($font, $west + 20, $north + ((@labels + 1.5) * $line), $label, $BLACK);
586 $label = "Total Length: $length base pairs";
587 $image->string($font, $west + 20, $north + (7 * $line), $label, $BLACK);
590 #returns the number rounded to the nearest integer
591 method round (Num $num!) {
592 $num += .5;
593 $num =~ s/(\d*)\.\d*/$1/;
594 return $num;
597 #returns the number rounded up to the next multiple of block
598 method roundTo (Int $num!, Int $block!) { return $num + $block - ($num % $block); }
599 ###########
600 no Moose; # Here #
601 __PACKAGE__->meta->make_immutable; # be #
602 return 1; # dragons #
603 ###########
604 =head1 SEE ALSO
606 CXGN::Transcript::Unigene calls this module.
608 This has been written with Moose, a postmodern object-orientation system for Perl, and uses the Moose extension MooseX::Method::Signatures.
610 =cut