#!/usr/bin/perl -w
# $Id: gbrowse_img,v 1.7 2009-08-31 19:46:38 lstein Exp $
use strict;
use Bio::Graphics::Browser2;
use Bio::Graphics::Browser2::Render::HTML;
use Bio::Graphics::Browser2::Util;
use Bio::Graphics::Karyotype;
use Digest::MD5 'md5_hex';
# call with following arguments:
# source database source
# type list of feature mnemonics
# options track options, in format mnemonic+option+mnemonic+option...
# name landmark or range to display, in format Name:start..stop
# width desired width of image, in pixels (height cannot be set)
# add a feature to superimpose on top of the image
# in format: reference+type+name+start..stop,start..stop,start..stop
# multiple "add" arguments are allowed
# style stylesheet for added features
# h_region region(s) to hilight
# h_feat feature(s) to hilight
umask 022;
my $fcgi = Bio::Graphics::Browser2::Render->fcgi_request;
my $modperl = $ENV{MOD_PERL};
my $init;
if ($modperl && !$init++) {
$SIG{USR1} = $SIG{PIPE} = $SIG{TERM} = sub {
my $sig = shift;
my $time = localtime;
print STDERR "[$time] [notice] GBrowse FastCGI process caught sig$sig. Exiting... (pid $$)\n";
CORE::exit 0
};
}
if ($fcgi) {
my $FCGI_DONE = 0;
$SIG{USR1} = $SIG{PIPE} = $SIG{TERM} = sub {
my $sig = shift;
my $time = localtime;
print STDERR "[$time] [notice] gbrowse_img FastCGI process caught sig$sig. Exiting... (pid $$)\n";
$FCGI_DONE = 1;
};
my %sys_env = %ENV;
while (!$FCGI_DONE) {
my $status = $fcgi->Accept;
next unless $status >= 0;
%ENV = ( %sys_env, %ENV );
my $globals = Bio::Graphics::Browser2->open_globals;
CGI->initialize_globals();
my $gbi = GBrowse_img->new($globals);
$gbi->run();
$gbi->destroy;
}
}
else {
my $globals = Bio::Graphics::Browser2->open_globals;
GBrowse_img->new($globals)->run();
}
exit 0;
package GBrowse_img;
use CGI qw(param start_html end_html
p h1 path_info escape img);
use File::Spec;
use File::Temp 'tmpnam','tempfile','tempdir';
use Bio::Graphics::Browser2::Shellwords;
use constant DEBUG => 0;
use constant MAX_SEGMENT => 1_000_000;
use constant TOO_MANY_SEGMENTS => 500;
use constant PANEL_GAP => 3;
sub new {
my $class = shift;
my $globals = shift;
my $render = Bio::Graphics::Browser2::Render::HTML->new($globals);
return bless {render => $render},ref $class || $class;
}
sub run {
my $self = shift;
my $render = $self->render;
$render->set_details_multiplier(1);
unless (param()) {
print $self->header();
print $self->usage();
return;
}
$render->set_source() && exit; # may cause a redirect and exit
$render->init();
if ($render->data_source->must_authenticate
&& !$self->session->private) {
my $base = $render->globals->gbrowse_url;
print $self->header();
print CGI->h1('Authentication required. Please log into',CGI->a({-href=>$base},'GBrowse'),'from this computer.');
return;
}
$render->add_user_tracks($render->data_source);
$render->update_state();
# notice that there is no explicit flush() of the session/state, and therefore the
# changes to the session are not stored.
warn join ' ',$render->potential_tracks if DEBUG;
# self-documentation feature: dump out tab-delimited list of mnemonics and keys
if (param('list')) {
$self->dump_sources($render) if param('list') eq 'sources';
$self->dump_types($render) if param('list') eq 'types';
return;
}
if (param('debug')) {
$self->dump_state($render);
return;
}
$self->render_image();
return;
}
sub render { shift->{render} }
sub session { shift->render->session }
sub cookie { shift->render->create_cookie }
sub source { shift->render->data_source }
sub destroy { my $self = shift;
$self->{render}->destroy;
undef $self->{render};
}
sub header {
my $self = shift;
if (@_ == 1) {
unshift @_,'-type';
}
my $cookie = $self->cookie();
return CGI::header(-cookie => $cookie,
@_);
}
sub dump_sources {
my $self = shift;
my $source = $self->source;
print $self->header('text/plain');
print "## Sources\n";
print join "\n",$source->globals->data_sources,"\n";
}
sub dump_types {
my $self = shift;
my $render = $self->render;
my $source = $self->source;
print $self->header('text/plain');
print "## Feature types for source ",$source->name,"\n";
my @labels = grep {!/:/ and !/^_/} $source->labels;
my %default = map {$_=>1} $source->default_labels;
for my $l (@labels) {
my $d = $default{$l} ? 'default' : '';
my $key = $render->setting($l=>'key')||'';
print join("\t",$l,$key,$d),"\n";
}
}
sub dump_state {
my $self = shift;
my $render = $self->render;
print $self->header('text/plain');
print "## Current state for debugging\n";
print "Source: ",$self->source->name,"\n";
print "Segment: ",$render->segment,"\n";
print "Labels: ",join(' ',$render->detail_tracks),"\n";
}
sub render_image {
my $self = shift;
my $render = $self->render;
my $renderer = $render->get_panel_renderer;
my $format = param('format') || 'GD';
my $flip = param('flip') || 0;
my $embed = param('embed');
if (my @regions = param('h_region')) {
$render->state->{h_region} = \@regions;
}
my $convert_to_pdf;
if ($format eq 'PDF' && `which inkscape`) {
$convert_to_pdf++;
$format = 'GD::SVG';
}
$format = 'GD::SVG' if $format eq 'SVG';
$format = 'GD' if $embed;
my ($img_data,$map) = $render->region->feature_count > 1
? $self->render_multiple($renderer,$format,$flip,$embed)
: $self->render_tracks ($renderer,$format,$flip,$embed);
my $seg = $render->segment;
my $fname = $seg ? $seg->seq_id.':'.$seg->start.'..'.$seg->end : "NA";
if ($embed) {
my $url = $renderer->source->generate_image($img_data);
my $js = $render->data_source->globals->js_url;
my @scripts = map { {src=>"$js/$_"} }
qw(balloon.js balloon.config.js yahoo-dom-event.js);
print $self->header(-type=>'text/html');
print start_html(-script=>\@scripts),
$render->render_balloon_settings,
img(
{
-src => $url,
-usemap => "#gbrowse2_img_map"}
),
$map,
end_html();
}
elsif ($format eq 'GD') {
print $self->header(-type=>'image/png',
-content_disposition => "filename=$fname.png");
print $img_data->png;
}
elsif ($format eq 'GD::SVG' && $convert_to_pdf) {
print $self->header(-type=>'application/pdf',
-content_disposition => "filename=$fname.pdf");
my ($infh,$in) = tempfile(UNLINK=>0,SUFFIX=>'.svg');
my ($outfh,$out) = tempfile(UNLINK=>0,SUFFIX=>'.pdf');
print $infh $img_data or die "$in: $!";
close $infh;
system "inkscape -z --without-gui --export-pdf=$out $in 3<&1 1>&2 2>&3 | grep -v GDK_IS_DISPLAY";
open (my $fh,'<',$out) or die "$out: $!";
while (<$fh>) {print $_}
close $fh;
unlink $in,$out;
}
elsif ($format eq 'GD::SVG') {
print $self->header(-type => 'application/svg+xml',
-content_disposition => "filename=$fname.svg");
print $img_data;
}
else {
print $self->header('text/plain');
print "unknown format $format\n";
}
}
sub render_multiple {
my $self = shift;
my ($renderer,$format,$flip,$embed) = @_;
my $features = $self->render->region->features;
my $karyotype = Bio::Graphics::Karyotype->new(source => $self->render->data_source,
language => $self->render->language);
$karyotype->add_hits($features);
my $panels = $karyotype->generate_panels($format);
my (@gds,@seqids);
for my $seqid (keys %$panels) {
push @gds,$panels->{$seqid}{panel}->gd;
push @seqids,$seqid;
}
my $img_data = $self->consolidate_images(\@gds,undef,undef,'horizontal',\@seqids);
return ($img_data,undef);
}
sub render_tracks {
my $self = shift;
my ($renderer,$format,$flip,$embed) = @_;
my $render = $self->render;
my $external = $render->external_data;
warn 'visible = ',join ' ',$render->visible_tracks if DEBUG;
my @labels = $render->expand_track_names($render->detail_tracks);
warn "labels = ",join ',',@labels if DEBUG;
my @track_types = map {shellwords($_)} (param('t'),param('type'),param('track'));
my $h_callback = make_hilite_callback(param('h_feat'));
# If no tracks specified, we want to see all tracks with this feature
if (!@track_types) { @track_types = @labels; }
unshift @track_types,'_scale';
my $result = $renderer->render_track_images(
{
labels => \@track_types,
external_features => $external,
section => 'detail',
cache_extra => [$format,param('h_feat'),param('h_region')],
image_class => $format,
flip => $flip,
hilite_callback => $h_callback || undef,
-key_style => 'between',
-suppress_key => 0,
}
);
warn "returned labels = ",join ',',%$result if DEBUG;
# Previously - @labels (caused drawing more tracks than asked for)
my @image_data = @{$result}{grep {$result->{$_}} @track_types};
my @gds = map {$_->{gd} } @image_data;
my @map_data = map {$_->{map}} @image_data;
my $img_data = $self->consolidate_images(\@gds);
my $map = $self->consolidate_maps (\@map_data, \@gds) if $embed;
return ($img_data,$map);
}
sub calculate_composite_bounds {
my $self = shift;
my ($gds,$orientation) = @_;
warn "consolidating ",scalar @$gds," GD objects" if DEBUG;
my $height = 0;
my $width = 0;
if ($orientation eq 'vertical') {
for my $g (@$gds) {
warn "g=$g" if DEBUG;
next unless $g;
$height += ($g->getBounds)[1]; # because GD::SVG is missing the width() and height() methods
$width ||= ($g->getBounds)[0];
}
} elsif ($orientation eq 'horizontal') {
for my $g (@$gds) {
warn "g=$g" if DEBUG;
next unless $g;
$height = ($g->getBounds)[1] if $height < ($g->getBounds)[1];
$width += ($g->getBounds)[0];
}
}
return ($width,$height);
}
sub consolidate_images {
my $self = shift;
my ($gds,$width,$height,$orientation,$labels) = @_;
$orientation ||= 'vertical';
($width,$height) = $self->calculate_composite_bounds($gds,$orientation)
unless defined $width && defined $height;
warn "consolidating ",scalar @$gds," GD objects" if DEBUG;
return $gds->[0]->isa('GD::SVG::Image')
? $self->_consolidate_svg($width,$height,$gds,$orientation,$labels)
: $self->_consolidate_gd ($width,$height,$gds,$orientation,$labels);
}
sub make_hilite_callback {
my @feature_names = @_;
return unless @feature_names;
my %colors;
foreach (@feature_names) {
my ($name,$color) = split '@';
$color ||= 'yellow';
$colors{$name} = $color;
}
return sub {
my $feature = shift;
my $color;
# if we get here, we select the search term for highlighting
my %names = map {lc $_=> 1}
$feature->display_name,
eval{$feature->get_tag_values('Alias')};
return unless %names;
$color ||= $colors{$_} foreach keys %names;
return $color;
}
}
sub _consolidate_gd {
my $self = shift;
my ($width,$height,$gds,$orientation,$labels) = @_;
my $class = ref($gds->[0]);
(my $fontclass = $class)=~s/::Image//;
my $lineheight = $fontclass->gdMediumBoldFont->height;
my $charwidth = $fontclass->gdMediumBoldFont->width;
$height += $lineheight if $orientation eq 'horizontal';
my $gd = $class->new($width,$height,1);
my $white = $gd->colorAllocate(255,255,255);
my $black = $gd->colorAllocate(0,0,0);
eval {
my $bg = $gds->[0]->getPixel(0,0);
my @bg = $gds->[0]->rgb($bg);
my $i = $gd->colorAllocate(@bg);
$gd->filledRectangle(0,0,$width,$height,$i);
};
my $offset = 0;
if ($orientation eq 'vertical') {
for my $g (@$gds) {
next unless $g;
$gd->copy($g,0,$offset,0,0,$g->getBounds);
$offset += ($g->getBounds)[1];
}
} else {
for my $g (@$gds) {
next unless $g;
$gd->copy($g,$offset,$height-($g->getBounds)[1]-$lineheight,0,0,$g->getBounds);
if ($labels) {
my $l = shift @$labels;
$gd->string($fontclass->gdMediumBoldFont,
$offset+(($g->getBounds)[0]-$charwidth*length $l)/2,
$height-$lineheight,$l,$black);
}
$offset += ($g->getBounds)[0];
}
}
return $gd;
}
# because the GD::SVG copy() method is broken
sub _consolidate_svg {
my $self = shift;
my ($width,$height,$gds,$orientation,$labels) = @_;
my $image_height = $height;
if ($labels) {
my $font = GD::SVG->gdMediumBoldFont;
my $charwidth = $font->width;
my $lineheight=$font->height;
$image_height += $lineheight;
for my $gd (@$gds) {
my $l = shift @$labels;
my $black = $gd->colorAllocate(0,0,0);
$gd->string($font,
(($gd->getBounds)[0]-$charwidth*length $l)/2,
($gd->getBounds)[1],
$l,$black);
}
}
my $svg = qq(\n\n);
$svg .= qq(!) {
last;
}
elsif (/
This CGI script is an interface to the Generic Genome Browser for the
purpose of retrieving dynamic images of a region of the genome. It
can be used as the destination of an <img> tag like this:
The script can also be used to superimpose one or more external
features onto the display, for example for the purpose of displaying
BLAST hits, an STS or a knockout in the context of the genome.
The script recognizes the following CGI arguments, which can be passed
either as GET or POST argument=value pairs. Argument pairs must be
separated by semicolons (preferred) or by ampersands. Many of the
options have one-letter aliases that can be used to reduce URL
lengths.
The arguments are explained in more detail here
If the track name has a space in it, put quotes around the name:
The alias "o" can be used to shorten the length of the URL.
One or both of the type and name can be omitted. If omitted, type will
default to "Your Features" and the name will default to "Feature XX" where
XX is an integer. This allows for a very simple feature line:
Multiple add= arguments are allowed. The alias "a" can be used to
shorten the length of the URL.
gbrowse_img - CGI script to generate genome images via the Generic Genome Browser
SYNOPSIS
<img src="http://www.wormbase.org/db/gb2/gbrowse_img/c_elegans?name=mec-3;width=400">
Will generate this picture:
<a href="http://www.wormbase.org/db/gb2/gbrowse_img?list=sources">list</a>
Will return this document:
## Sources
b_malayi
c_brenneri
c_briggsae_cb25
c_briggsae
c_elegans
c_elegans_gmap
c_elegans_pmap
ws77
c_japonica
c_remanei
c_remanei_nGASP
nGASP_submissions
nGASP
Gbrowse_karyotype
p_pacificus
yeast
<a href="http://www.wormbase.org/db/gb2/gbrowse_img/c_elegans?list=types">types</a>
Will return this document:
## Feature types for source c_elegans
LOCI:overview Landmarks default
RNAZ RNAz non-coding RNA genes
CG Gene Models default
CDS Coding Segments
RNA Predicted non-coding RNAs
HISTORICAL Obsolete gene models
GENEFINDER GeneFinder Predictions
TWINSCAN Twinscan Predictions
GENEMARKHMM GeneMarkHMM Predictions
mSPLICER_TRANSCRIPT mSplicer
...
DESCRIPTION
<img src="http://www.wormbase.org/db/gb2/gbrowse_img/c_elegans?name=III:1..1000">
CGI arguments
Argument Alias Description
name q genomic landmark or range
type t tracks to include in image
width w desired width of image
options o list of track options (compact, labeled, etc)
abs b display position in absolute coordinates
add a added feature(s) to superimpose on the image
style s stylesheet for additional features
keystyle k where to place the image key
overview force an overview-style display
flip f flip image left to right
grid turn grid on (1) or off (0)
embed generate full HTML for image and imagemap for use in an embedded frame
format format for the image (use "SVG" for scaleable vector graphics)
list get certain types of configuration information
source database name
If you use multiple name options, then this script will generate an overview
image showing the position of each landmark. The alias "q" can be used to
shorten the length of the URL.
<img src="http://www.wormbase.org/db/gb2/gbrowse_img/c_elegans?name=mec-3;
type=tRNA+NG+WABA+CG+ESTB">
Multiple type= arguments will be combined to form a single space-delimited list.
The alias "t" can be used to shorten the length of the URL.
type="microbe tRNA"+NG+WABA+CG+ESTB
options=tRNA+3+NG+3+WABA+1
add=Landmark+Type+Name+start..end,start..end,start..end
"Landmark" is the landmark name, "Type" is a descriptive type that will be printed
in the image caption, "Name" is a name for the feature to be printed above it,
and start..end is a comma-delimited list of ranges for discontinuous feature.
Names that contain white space must be quoted, for example "BLAST hit".
Note that this all has to be URL-escaped, so an additional
feature named "Your Sequence", type "Blast Hit", that is located on chromosome III
in a gapped range between 20000 and 22000, will be formatted as:
add=III+%22Blast%20Hit%22+%22Your%20Sequence%22+20000..21000,21550..22000
add=III+20000..21000,21550..22000
Mnemonic <tab> Full description of feature <tab> [default]
The third column contains the word "default" if the track will be shown by default when no type argument is provided.
h_feat=SKT5@blueYou may omit "@color", in which case the highlight will default to yellow. You can specify multiple h_feat arguments in order to highlight several features with distinct colors.
h_region=Chr3:200000..250000@wheatYou may omit "@color", in which case the highlighted region will default to lightgrey. You can specify multiple h_region arguments in order to highlight several regions with distinct colors.
Putting it all together, here's a working (very long) URL:
http://www.wormbase.org/db/gb2/gbrowse_img/c_elegans?name=B0001;add=B0001+pcr+pcr1+20000..333000;add=B0001+%22cool%20knockout%22+kn2+30000..20000,10000..5000;type=add+CG+WTP;style=pcr+glyph=primers;style=%22cool%20knockout%22+glyph=transcript2+bgcolor=orange;abs=1
If you wish to associate the image with an imagemap so that clicking on a feature takes the user to the destination configured in the gbrowse config file, you may do so by placing the URL in an <iframe> section and using the embed=1 flag:
<iframe src="http://localhost/cgi-bin/gbrowse_img/c_elegans?name=B0001;embed=1" width="100%" height="250"> <img src="http://localhost/cgi-bin/gbrowse_img/c_elegans?name=B0001"/> </iframe>
Placing an <img> tag inside the <iframe> tag arranges for older browsers that don't know about iframes to display the static image instead. You may need to adjust the width and height attributes in order to avoid browsers placing scrollbars around the frame.
The cookie that stores the configuration options for plugins does not transfer from gbrowse to gbrowse_img, so tracks generated by annotation plugins, such as the Restriction site annotator, will not display correctly when the image URL is generated on one machine and then viewed on another. Uploaded files will transfer correctly, however.
Lincoln Stein lstein@cshl.org
Copyright (c) 2002-2004 Cold Spring Harbor Laboratory
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
For additional help, see The GMOD Project pages. END ; } __END__