package ImageTrackRenderer; =head1 NAME ImageTrackRenderer - render JBrowse image tracks using a chromosome-sized virtual GD canvas. =head1 SYNOPSIS my $renderer = ImageTrackRenderer->new( "datadir" => $outdir, "tilewidth" => $tileWidth, "trackheight" => $trackHeight, "tracklabel" => $trackLabel, "key" => $key, "link" => !$nolinks, "drawsub" => sub { my ($im, $seqInfo) = @_; my $seqname = $seqInfo->{"name"}; my @color; for my $rgb (@rgb) { push @color, $im->colorAllocate (@$rgb); } $im->setThickness ($thickness); for my $gff (@{$gff{$seqname}}) { my $start = $im->base_xpos ($gff->[0]) + $im->pixels_per_base / 2; my $end = $im->base_xpos ($gff->[1]) + $im->pixels_per_base / 2; my $arcMidX = ($start + $end) / 2; my $arcWidth = $end - $start; my $arcHeight = 2 * $trackHeight * ($gff->[1] - $gff->[0]) / $maxlen; # warn "Drawing arc from $start to $end, height $arcHeight"; $im->arc ($arcMidX, 0, $arcWidth, $arcHeight, 0, 180, $color[$gff->[2]]); } }); # run the renderer $renderer->render; =head1 METHODS =cut use strict; use warnings; use vars '@ISA'; use POSIX (); use base qw( Exporter ); our @EXPORT_OK = qw (new render); use File::Spec (); use File::Path (); use Bio::JBrowse::JSON; use GenomeDB; use TrackImage; =head2 new my $renderer = ImageTrackRenderer->new(%args); Creates a new ImageTrackRenderer object. %args is a key-value hash with the following keys: =over 2 =item B: root directory for all generated files. defaults to "data" =item B: width of tiles in pixels. default is 2000 (you should not need to change this) =item B: height of track in pixels. default is 100 =item B: the track label. defaults to "track" =item B: the key. defaults to whatever 'tracklabel' is =item B: reference to a subroutine taking two arguments ($im,$seqInfo) where $im is a TrackImage and $seqInfo is a reference to the sequence info hash (keys include "length" and "name"). This subroutine will be called for every refseq. =item B: flag indicating whether to use filesystem links to repeat identical tiles. True by default; set to zero to disable this feature =back =cut sub new { my ($class, %args) = @_; my $self = { 'datadir' => "data", 'trackdir' => "tracks", 'tiledir' => undef, #< ignored for backcompat 'refseqsfile' => undef, 'trackinfofile' => 'trackList.json', 'zooms' => [ 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000 ], 'tilewidth' => 2000, 'trackheight' => 100, 'tracklabel' => "track", 'key' => undef, 'link' => 1, 'drawsub' => undef, }; for my $arg ( keys %args ) { if( exists $self->{$arg} ) { $self->{$arg} = $args{$arg} } else { die "Unknown argument: $arg"; } } bless $self, $class; # lazily import the md5_hex function if we're to use MD5 identity-linking eval "require Digest::MD5" if $self->link; $self->{_genomedb} = GenomeDB->new( $self->datadir ); $self->{_imagetrack} = $self->_genomedb->createImageTrack( $self->tracklabel, {}, $self->key || $self->tracklabel, ); return $self; } =head2 render $renderer->render; Calls the supplied C coderef (via the C method, which can also be overridden) for all sequences and all zoomlevels, then adds the track to the data/trackList.json file. =cut sub render { my ($self) = @_; my @refSeqs = @{ $self->_genomedb->refSeqs } or die "No reference sequences defined"; foreach my $seqInfo (@refSeqs) { my $seqName = $seqInfo->{"name"}; my $seqLen = $seqInfo->{"length"}; #warn "starting seq $seqName\n"; $self->_imagetrack->startLoad( $seqName ); $self->write_trackfile( $seqName ); # loop over zoom levels for my $basesPerPixel ( @{ $self->zooms } ) { print "working on seq $seqName, bases per pixel $basesPerPixel\n"; # create virtual image my $im = TrackImage->new( '-width' => POSIX::ceil( $seqLen/$basesPerPixel ), '-height' => $self->trackheight, '-tile_width_hint' => $self->tilewidth, '-bases_per_pixel' => $basesPerPixel, ); # call drawsub coderef $self->drawzoom( $im, $seqInfo ); # break into tiles my $tile = 0; for( my $x = 0; $x < $im->width; $x += $self->tilewidth ) { my $gdIm = $im->renderTile( $x, 0, $self->tilewidth, $self->trackheight ); my $tilefile = $self->tilefilepath( $seqName, $basesPerPixel, $tile ); $self->write_image_file( $gdIm, $tilefile ); # increment the tile count. ++$tile; } # allow the TiledImage to clean up $im->cleanup(); } $self->_imagetrack->finishLoad; } $self->_genomedb->writeTrackEntry( $self->_imagetrack ); } =head2 drawzoom $im = new TiledImage ('-width'=>..., '-height'=>...); $seqInfo = { "name" => ..., "length" => ..., ... }; $renderer->drawzoom($im,$seqInfo); Calls the supplied C coderef with the specified arguments. You should not call this method directly (it is called by C), but you can override it in a subclass instead of placing a coderef in C, if you choose. The default implementation just passes the arguments to C, like so: $renderer->drawsub->($im,$seqInfo) =cut sub drawzoom { my ($self, $im, $seqInfo) = @_; $self->drawsub->( $im, $seqInfo ); } ############## HELPER METHODS #################### sub _md5_to_path { my $self = shift; if( @_ ) { $self->{md5_to_path} = $_[1]; } return $self->{md5_to_path}; } sub write_image_file { my ( $self, $gdIm, $tilefile ) = @_; my $png = $gdIm->png; # we will write the tile file if the MD5 hash is unique, # or if we don't create hardlinks between MD5-identical files my $writefile = 1; if( $self->link ) { # do we make hardlinks? my $md5_to_path = $self->_md5_to_path; # compute the hash of the image; if we've seen it before, # make a hardlink instead of writing the file. my $md5 = Digest::MD5::md5_hex ($png); if( exists $md5_to_path->{$md5} ) { my $oldtilefile = $md5_to_path->{$md5}; if( -f $tilefile ) { unlink $tilefile or die "Couldn't remove existing file $tilefile : $!"; } # warn "Tile $tilefile identical to $oldtilefile, creating a hard link\n"; if( link $oldtilefile, $tilefile ) { $writefile = 0; } else { die "Couldn't link $oldtilefile to $tilefile : $!"; } } else { $md5_to_path->{$md5} = $tilefile; } } # write the file, if we still need to. if( $writefile ) { open my $tile, '>', $tilefile or die "$! writing $tilefile"; binmode $tile; print $tile $png; } return; } sub write_trackfile { my ( $self, $seqName ) = @_; # open track description file my $trackfile = $self->trackfilepath( $seqName ); open my $trackfile_fh, '>', $trackfile or die "$! writing $trackfile"; print $trackfile_fh Bio::JBrowse::JSON->new->pretty->encode({ 'tileWidth' => $self->tilewidth, 'zoomLevels' => [ map { my $basesPerPixel = $_; { 'urlPrefix' => "$basesPerPixel/", 'height' => $self->trackheight, 'basesPerTile' => $basesPerPixel * $self->tilewidth, } } @{ $self->zooms } ], }); } ## relative sub tracksubdir { my ( $self ) = @_; $self->tracklabel; } sub seqsubdir { my ($self, $seqname) = @_; File::Spec->catdir( $self->tracksubdir, $seqname ); } sub zoomsubdir { my ($self, $seqname, $zoom) = @_; File::Spec->catdir( $self->seqsubdir($seqname), $zoom ); } sub tilefile { my ($self, $seqname, $zoom, $tile) = @_; File::Spec->catfile( $self->zoomsubdir($seqname,$zoom), "$tile.png" ); } sub trackfile { my ( $self, $seqname ) = @_; File::Spec->catfile( $self->trackdir, $seqname, 'trackData.json' ); } ### absolute sub trackpath { my ( $self ) = @_; $self->_dir( $self->datadir, $self->trackdir ); } sub trackfilepath { my ( $self, $seqname ) = @_; $self->_file( $self->datadir, $self->trackdir, $self->seqsubdir( $seqname ), "trackData.json" ); } sub trackinfopath { my ( $self ) = @_; $self->_file( $self->datadir, $self->trackinfofile ); } sub tilefilepath { my $self = shift; $self->_file( $self->datadir, $self->trackdir, $self->tilefile( @_ )); } ######### read-only accessors sub link { shift->{link} } sub datadir { shift->{datadir} } sub tracklabel { shift->{tracklabel} } sub key { shift->{key} } sub refseqsfile { undef } #< only for backcompat sub trackdir { shift->{trackdir} } sub tilewidth { shift->{tilewidth} } sub zooms { shift->{zooms} } sub trackheight { shift->{trackheight} } sub drawsub { shift->{drawsub} } sub _genomedb { shift->{_genomedb} } sub _imagetrack { shift->{_imagetrack} } ########################### # filename and dirname helpers that assemble file and dir names, and # create dirs if necessary sub _dir { my ( $self, @path ) = @_; my $dir = @path > 1 ? File::Spec->catdir( @path ) : $path[0]; #warn "checking dir $dir\n"; unless( -e $dir ) { File::Path::mkpath( $dir ) or die "$! creating directory $dir"; } return $dir; } sub _file { my ( $self, @path ) = @_; my $path = File::Spec->catfile( @path ); # create the dir if necessary my ($file,$dir) = File::Basename::fileparse( $path ); $self->_dir( $dir ) if $dir; return $path; } 1;