package Bio::Graphics::Browser2::Render::HTML;
use strict;
use warnings;
use base 'Bio::Graphics::Browser2::Render';
use Bio::Graphics::Browser2::Shellwords;
use Bio::Graphics::Browser2::SubtrackTable;
use Bio::Graphics::Karyotype;
use Bio::Graphics::Browser2::Render::TrackConfig;
use Bio::Graphics::Browser2::Util qw[citation url_label segment_str];
use JSON;
use Digest::MD5 'md5_hex';
use Carp qw(croak cluck);
use CGI qw(:standard escape start_table end_table);
use Text::Tabs;
use POSIX qw(floor);
use constant JS => '/gbrowse2/js';
use constant ANNOTATION_EDIT_ROWS => 25;
use constant ANNOTATION_EDIT_COLS => 100;
use constant MAXIMUM_EDITABLE_UPLOAD => 1_000_000; # bytes
use constant DEBUG => 0;
use constant HAVE_SVG => eval "require GD::SVG; 1";
our $CAN_PDF;
# Render HTML Start - Returns the HTML for the browser's <head> section.
sub render_html_start {
my $self = shift;
my ($title,@actions) = @_;
my $dsn = $self->data_source;
my $html = $self->render_html_head($dsn,$title,@actions);
$html .= $self->render_js_controller_settings();
$html .= $self->render_balloon_settings();
$html .= "<div id='main'>";
$html .= $self->render_select_menus();
return $html;
}
# Render Top - Returns the HTML for the top banner of the page.
sub render_top {
my $self = shift;
my ($title,$features) = @_;
my $err = $self->render_error_div;
my $html = '';
$features ||= [];
$html .= $self->render_title($title,$self->state->{name}
&& @$features == 0);
# ***Render the snapshot title***
$html .= $self->snapshot_manager->render_title;
$html .= $self->html_frag('html1',$self->state);
return $err
. $self->toggle({nodiv=>1},'banner','',$html);
}
# Render Error DIV - Returns the HTML for the error display at the top of the page.
sub render_error_div {
my $self = shift;
my $error = $self->error_message;
my $display = $error ? 'block' : 'none';
my $button = button({-onClick=>'Controller.hide_error()',
-name=>'Ok'});
return div({-class=>'errorpanel',
-style=>"display:${display}",
-id=>'errordiv'},
table(
TR(
td(span({-class=>'error',-id=>'errormsg'},$error || 'no error')),
td({-align=>'right'},$button)
),
),
div({-class=>'errorpanel',
-style=>"display:none;margin: 6px 6px 6px 6px",
-id =>'errordetails'},
'no details'
)
).br();
}
sub render_login_required {
my $self = shift;
my $action = shift;
return div(
h1($self->translate('LOGIN_REQUIRED'),
button(-name => $self->translate('LOGIN'),
-onClick => $action)
));
}
# Render Tabbed Pages - Returns the HTML containing the tabs & the page DIVs to hold the content.
sub render_tabbed_pages {
my $self = shift;
my ($main_html,$tracks_html,$snapshot_html,$community_tracks_html,$custom_tracks_html,$settings_html) = @_;
my $uses_database = $self->user_tracks->database;
my $main_title = $self->translate('MAIN_PAGE');
my $tracks_title = $self->translate('SELECT_TRACKS');
my $snapshot_title = $self->translate('SNAPSHOT_SELECT');
my $community_tracks_title = $self->translate('COMMUNITY_TRACKS_PAGE') if $uses_database;
my $custom_tracks_title = $self->translate('CUSTOM_TRACKS_PAGE');
my $settings_title = $self->translate('SETTINGS_PAGE');
my $html = '';
$html .= div({-id=>'tabbed_section', -class=>'tabbed'},
div({-id=>'tabbed_menu',-class=>'tabmenu'},
span({id=>'main_page_select'}, $main_title),
span({id=>'track_page_select'}, $tracks_title),
span({id=>'snapshots_page_select'}, $snapshot_title),
$uses_database? span({id=>'community_tracks_page_select'}, $community_tracks_title) : "",
span({id=>'custom_tracks_page_select'}, $custom_tracks_title),,
span({id=>'settings_page_select'}, $settings_title)
),
div({-id=>'main_page', -class=>'tabbody'}, $main_html),
div({-id=>'track_page', -class=>'tabbody' ,-style=>'display:none'}, $tracks_html),
div({-id=>'snapshots_page', -class=>'tabbody' ,-style=>'display:none'}, $snapshot_html),
$uses_database?div({-id=>'community_tracks_page',-class=>'tabbody',-style=>'display:none'}, $community_tracks_html) : "",
div({-id=>'custom_tracks_page', -class=>'tabbody',-style=>'display:none'}, $custom_tracks_html),
div({-id=>'settings_page', -class=>'tabbody',-style=>'display:none'}, $settings_html),
);
return $html;
}
# Render User Head - Returns any HTML for the <head> section specified by the user in GBrowse.conf.
sub render_user_head {
my $self = shift;
my $settings = $self->state;
return '' unless $settings->{head};
my $a = $self->data_source->global_setting('head');
return $a->(@_) if ref $a eq 'CODE';
return $a || '';
}
# Render User Header - Returns any HTML for the top of the browser (the header) as specified by the user in GBrowse.conf.
sub render_user_header {
my $self = shift;
my $settings = $self->state;
return '' unless $settings->{head};
my $a = $self->data_source->global_setting('header');
return $a->(@_) if ref $a eq 'CODE';
return $a || '';
}
# Render Bottom - Returns any HTML included in the footer specified by the user in GBrowse.conf.
sub render_bottom {
my $self = shift;
my $features = shift; # not used
my $a = $self->data_source->global_setting('footer');
my $value = ref $a eq 'CODE' ? $a->(@_) : $a;
$value ||= '';
return $value."</div>".end_html();
}
# Render Navbar - Returns the HTML for the navigation bar along the top of the main browser page (in the "Search" node).
sub render_navbar {
my $self = shift;
my $segment = shift;
warn "render_navbar()" if DEBUG;
my $settings = $self->state;
my $source = '/'.$self->session->source.'/';
my $searchform = join '',(
start_form(
-name => 'searchform',
-id => 'searchform',
),
hidden(-name=>'force_submit',-value=>0),
hidden(-name=>'plugin_find', -value=>0),
div({ -id => 'search_form_objects' },
$self->render_search_form_objects(),
),
end_form
);
my $search = $self->setting('no search')
? '' : b($self->translate('Landmark')).':'.br().$searchform.$self->examples();
my $plugin_form = div({-id=>'plugin_form'},$self->plugin_form());
# *** Checks the state to see if a snapshot is active and assigns it ***
my $source_form = div({-id=>'source_form'},$self->source_form());
my $sliderform = div({-id=>'slider_form'},$self->sliderform($segment));
# *** Creates the save session button and assigns it to save_prompt ***
my $snapshot_options = $self->snapshot_manager->snapshot_options;
return $self->toggle('Search',
div({-class=>'searchbody'},
table({-border=>0,-width=>'95%'},
TR(td({-width=>'50%'},$search),td({-width=>'45%'},$plugin_form,$snapshot_options)),
TR(td({-align=>'left'},
$source_form,
),
td({-align=>'left'},
$sliderform || ' '
),
)
),
$self->html_frag('html3',$self->state)
)
)
. div( { -id => "plugin_configure_div"},'');
}
# Plugin Form - Returns the HTML for the plugin form in the main navigation bar.
sub plugin_form {
my $self = shift;
my $settings = $self->state;
return $settings->{GALAXY_URL}
? button(-name => $self->translate('SEND_TO_GALAXY'),
-onClick => $self->galaxy_link).
button(-name => $self->translate('CANCEL'),
-onClick => $self->galaxy_clear.";Controller.update_sections(['plugin_form'])",
)
: join '',(
start_form(-name=>'pluginform',
-id=>'pluginform',
-onSubmit=>'return false'),
$self->plugin_menu(),
end_form);
}
# Source Form - Returns the HTML for the source chooser in the main navigation bar.
sub source_form {
my $self = shift;
join '',(
start_form(-name=>'sourceform',
-id=>'sourceform',
-onSubmit=>''),
$self->source_menu(),
end_form
);
}
# *** Create the snapshot_form ***
sub snapshot_form {
return shift->snapshot_manager->snapshot_form;
}
# Slider Form - Returns the HTML for the zooming controls with the "Flip" checkbox.
sub sliderform {
my $self = shift;
my $segment = shift;
my $settings = $self->state;
if ($segment) {
return
join '',(
start_form(-name=>'sliderform',-id=>'sliderform',-onSubmit=>'return false'),
b($self->translate('Scroll'). ': '),
$self->slidertable($segment),
b(
checkbox(-name=>'flip',
-checked=>$settings->{flip},-value=>1,
-label=>$self->translate('Flip'),-override=>1,
-onClick => 'Controller.update_coordinates(this.name + " " + this.checked)',
)
),
hidden(-name=>'navigate',-value=>1,-override=>1),
end_form
);
}
else {
return '';
}
}
# Render Search For Objects - Returns the "Landmark or Region" search box on the browser tab.
sub render_search_form_objects {
my $self = shift;
my $settings = $self->state;
# avoid exposing the internal database ids to the public
my $search_value = $settings->{name} =~ /^id:/ && $self->region->features
? eval{$self->region->features->[0]->display_name}
: $settings->{name};
my $html = textfield(
-name => 'name',
-id => 'landmark_search_field',
-size => 25,
-default => $search_value,
-override=>1,
);
if ($self->setting('autocomplete')) {
my $spinner_url = $self->data_source->button_url.'/spinner.gif';
$html .= <<END
<span id="indicator1" style="display: none">
<img src="$spinner_url" alt="$self->translate('WORKING')" />
</span>
<div id="autocomplete_choices" class="autocomplete"></div>
END
}
$html .= submit( -name => $self->translate('Search') );
return $html;
}
# Render HTML Head - Returns the HTML for the beginning of the page (for CGI's start HTML function).
sub render_html_head {
my $self = shift;
my ($dsn,$title,@other_initialization) = @_;
my @plugin_list = $self->plugins->plugins;
my $uses_database = $self->user_tracks->database;
return if $self->{started_html}++;
$title =~ s/<[^>]+>//g; # no markup in the head
# pick scripts
my $js = $dsn->globals->js_url;
my @scripts;
# Set any onTabLoad functions
my $main_page_onLoads = "";
my $track_page_onLoads = '';
my $community_track_page_onLoads = '';
my $custom_track_page_onLoads = "";
my $settings_page_onLoads = "";
# Get plugin onTabLoad functions for each tab, if any
my %plugin_onLoads = map ($_->onLoads, @plugin_list);
$main_page_onLoads .= $plugin_onLoads{'main_page'} if $plugin_onLoads{'main_page'};
$track_page_onLoads .= $plugin_onLoads{'track_page'} if $plugin_onLoads{'track_page'};
$custom_track_page_onLoads .= $plugin_onLoads{'custom_track_page'} if $plugin_onLoads{'custom_track_page'} && $uses_database;
$community_track_page_onLoads .= $plugin_onLoads{'community_track_page'} if $plugin_onLoads{'community_track_page'};
$settings_page_onLoads .= $plugin_onLoads{'settings_page'} if $plugin_onLoads{'settings_page'};
my $onTabScript .= "function onTabLoad(tab_id) {\n";
$onTabScript .= "if (tab_id == 'main_page_select') {$main_page_onLoads}\n";
$onTabScript .= "if (tab_id == 'track_page_select') {$track_page_onLoads}\n";
$onTabScript .= "if (tab_id == 'community_track_page_select') {$community_track_page_onLoads}\n" if $uses_database;
$onTabScript .= "if (tab_id == 'custom_track_page_select') {$custom_track_page_onLoads}\n";
$onTabScript .= "if (tab_id == 'settings_page_select') {$settings_page_onLoads}\n";
$onTabScript .= "};";
push (@scripts,({type=>"text/javascript"}, $onTabScript));
my $url = "?action=get_translation_tables" . ( $self->language_code ? ';language='.($self->language_code)[0] : '' ); #Include language as a parameter to prevent browser from using wrong cache if user changes languages
push (@scripts,({src=>$url}));
# drag-and-drop functions from scriptaculous
push @scripts,{src=>"$js/$_"}
foreach qw(
prototype.js
scriptaculous.js
subtracktable.js
);
if ($self->setting('autocomplete')) {
push @scripts,{src=>"$js/$_"}
foreach qw(controls.js autocomplete.js);
}
if ($self->globals->user_accounts) {
push @scripts,{src=>"$js/$_"}
foreach qw(login.js);
}
# our own javascript files
push @scripts,map { {src=>"$js/$_"} } qw(
buttons.js
trackFavorites.js
karyotype.js
rubber.js
overviewSelect.js
detailSelect.js
regionSelect.js
track.js
balloon.js
balloon.config.js
GBox.js
ajax_upload.js
tabs.js
track_configure.js
track_pan.js
ruler.js
controller.js
snapshotManager.js
);
# add scripts needed by plugins. Looks in /js folder unless specified.
my @plugin_scripts = map ($_->scripts, @plugin_list);
# add a path if one isn't specified.
foreach (@plugin_scripts) {
if ($_ !~ /^\.{0,2}[\/\\]/) {
$_ = "$js/$_";
}
};
push @scripts,{src=>"$_"} foreach @plugin_scripts;
# pick stylesheets. Looks in /css folder unless specified.
my @stylesheets;
my $titlebar = 'css/titlebar-default.css';
my $stylesheet = $self->setting('stylesheet')||'/gbrowse2/css/gbrowse.css';
push @stylesheets,{src => $self->globals->resolve_path('css/tracks.css','url')};
push @stylesheets,{src => $self->globals->resolve_path('css/subtracktable.css','url')};
push @stylesheets,{src => $self->globals->resolve_path('css/snapshots.css','url')};
push @stylesheets,{src => $self->globals->resolve_path('css/karyotype.css','url')};
push @stylesheets,{src => $self->globals->resolve_path('css/dropdown/dropdown.css','url')};
push @stylesheets,{src => $self->globals->resolve_path('css/dropdown/default_theme.css','url')};
push @stylesheets,{src => $self->globals->resolve_path($titlebar,'url')};
# add stylesheets used by plugins
my @plugin_stylesheets = map ($_->stylesheets, @plugin_list);
# add a path if one isn't specified.
foreach (@plugin_stylesheets) {
if ($_ !~ /^\.{0,2}[\/\\]/) {
$_ = $self->globals->resolve_path("css/$_",'url');
}
};
push @stylesheets,{src => $_} foreach @plugin_stylesheets;
my @theme_stylesheets = shellwords($self->setting('stylesheet') || '/gbrowse2/css/gbrowse.css');
for my $s ( @theme_stylesheets ) {
my ($url,$media) = $s =~ /^([^(]+)(?:\((.+)\))?/;
$media ||= 'all';
push @stylesheets, {
src => $self->globals->resolve_path($url,'url'),
media => $media,
};
}
# colors for "rubberband" selection
my $set_dragcolors = '';
if (my $fill = $self->data_source->global_setting('hilite fill')) {
$fill =~ s/^(\d+,\d+,\d+)$/rgb($1)/;
$fill =~ s/^(#[0-9A-F]{2}[0-9A-F]{2}[0-9A-F]{2})[0-9A-F]{2}$/$1/;
$fill =~ s/^(\w+):[\d.]+$/$1/;
$set_dragcolors = "set_dragcolors('$fill')";
}
my $units = $self->data_source->setting('units') || 'bp';
my $divider = $self->data_source->unit_divider;
my $set_units = "set_dragunits('$units',$divider)";
my @extra_headers;
push @extra_headers, $self->render_user_head;
# put all the html head arguments together
my @args = (-title => $title,
-style => \@stylesheets,
-encoding => $self->translate('CHARSET'),
-script => \@scripts,
-head => \@extra_headers,
);
push @args,(-lang=>($self->language_code)[0]) if $self->language_code;
# add body's onload arguments, including ones used by plugins
my $autocomplete = '';
my $plugin_onloads = join ';',map {eval{$_->body_onloads}} @plugin_list;
my $other_actions = join ';',@other_initialization;
push @args,(-onLoad => "initialize_page(); $set_dragcolors; $set_units; $plugin_onloads; $other_actions");
return start_html(@args);
}
# Render JS Controller Settings - Renders a block of javascript that loads some of our global config settings into the main controller object for use in client-side code.
sub render_js_controller_settings {
my ( $self ) = @_;
my $globals = $self->globals;
my @export_keys = qw(
buttons
balloons
openid
js
gbrowse_help
stylesheet
);
my $controller_globals = JSON::to_json({
map { $_ => ( $self->globals->url_path($_) || undef ) } @export_keys
});
my $scripts = "Controller.set_globals( $controller_globals );";
if ($globals->user_accounts) {
my $userdb = $self->userdb;
my $openid = ($userdb->can_openid && $self->globals->user_accounts_allow_openid) ||0;
my $register = ($userdb->can_register && $self->globals->user_accounts_allow_registration)||0;
$scripts .= "Controller.can_openid = $openid;";
$scripts .= "Controller.can_register = $register;";
}
return script({-type=>'text/javascript'}, $scripts);
}
# Renders the settings which control the balloon styles on the page.
sub render_balloon_settings {
my $self = shift;
my $source = $self->data_source;
my $default_style = $source->setting('balloon style') || 'GBubble';;
my $custom_balloons = $source->setting('custom balloons') || "";
my %config_values = $custom_balloons =~ /\[([^\]]+)\]([^\[]+)/g;
# default image path is for the default balloon set
my $balloon_images = $self->globals->balloon_url() || '/gbrowse2/images/balloons';
my $default_images = "$balloon_images/$default_style";
my $balloon_settings = $self->default_balloon_settings($balloon_images);
# handle any custom balloons/config sets
# two args that are interpreted here and not passed
# to the JavaScript:
#
# class = Balloon (could also be Box)
# config set = GPlain (GBubble is the default)
for my $balloon ( keys %config_values ) {
my %config = $config_values{$balloon} =~ /(\w+)\s*=\s*(\S+)/g;
# which balloon configuration set to use?
my $bstyle = $config{'config set'} || $default_style;
delete $config{'config set'};
# which class to use (may be Box or Balloon)
my $bclass = $config{'class'} || 'Balloon';
delete $config{'class'};
# This creates a Ballon (or Box) object and loads the
# specified configuration set.
$balloon_settings .= "\nvar $balloon = new $bclass;\nBalloonConfig($balloon,'$bstyle')\n";
# Image path must be specified
$config{images} ||= $default_images;
for my $option ( keys %config ) {
my $value
= $config{$option} =~ /^[\d.-]+$/
? $config{$option}
: "'$config{$option}'";
$balloon_settings .= "$balloon.$option = $value;\n";
}
}
$balloon_settings =~ s/^/ /gm;
return "\n<script type=\"text/javascript\">\n$balloon_settings\n</script>\n";
}
sub default_balloon_settings {
my $self = shift;
my $balloon_images = shift;
# These are the four configured popup tooltip styles
# GBubble is the global default
# each type can be called using the [name] syntax
return <<END;
// Original GBrowse popup balloon style
var GBubble = new Balloon;
BalloonConfig(GBubble,'GBubble');
GBubble.images = "$balloon_images/GBubble";
GBubble.allowEventHandlers = true;
GBubble.opacity = 1;
GBubble.fontFamily = 'sans-serif';
// A simpler popup balloon style
var GPlain = new Balloon;
BalloonConfig(GPlain,'GPlain');
GPlain.images = "$balloon_images/GPlain";
GPlain.allowEventHandlers = true;
GPlain.opacity = 1;
GPlain.fontFamily = 'sans-serif';
// Like GBubble but fades in
var GFade = new Balloon;
BalloonConfig(GFade,'GFade');
GFade.images = "$balloon_images/GBubble";
GFade.opacity = 1;
GFade.allowEventHandlers = true;
GFade.fontFamily = 'sans-serif';
// A formatted box
// Note: Box is a subclass of Balloon
var GBox = new Box;
BalloonConfig(GBox,'GBox');
GBox.images = "$balloon_images/GBubble";
GBox.allowEventHandlers = true;
GBox.evalScripts = true;
GBox.opacity = 1;
GBox.fontFamily = 'sans-serif';
GBox.maxWidth = 1280;
GBox.stemHeight = 0;
END
}
sub render_select_menus { # for popup balloons
my $self = shift;
my $html = '';
$html .= $self->_render_select_menu($_)
foreach (qw(DETAIL OVERVIEW REGION));
return $html;
}
sub _render_select_menu {
my $self = shift;
my $view = shift || 'DETAIL';
my $config_label = uc($view).' SELECT MENU';
# HTML for the custom menu is required
my $menu_html = $self->setting($config_label => 'HTML')
|| $self->setting($config_label => 'html')
|| return '';
# should not be visible
my %style = (display => 'none');
# optional style attributes
for my $att (qw/width font background background-color border/) {
my $val = $self->setting($config_label => $att) || next;
$style{$att} = $val;
}
$style{width} .= 'px';
my $style = join('; ', map {"$_:$style{$_}"} keys %style);
# clean up the HTML just a bit
$menu_html =~ s/\</\n\</g;
return div( { -style => $style,
-id => lc($view).'SelectMenu' },
$menu_html );
}
# Returns the HTML for the page's title, as displayed at the top.
sub render_title {
my $self = shift;
my $title = shift;
my $error = shift;
my $settings = $self->state;
return $settings->{head}
? h1({-id=>'page_title',-class=>$error ? 'error' : 'normal'},$title)
: '';
}
# Renders the search & navigation instructions & examples.
sub render_instructions {
my $self = shift;
my $settings = $self->state;
my $oligo = $self->plugins->plugin('OligoFinder') ? ', oligonucleotide (15 bp minimum)' : '';
return $settings->{head}
? div({-class=>'searchtitle'},
$self->toggle('Instructions',
div({-style=>'margin-left:2em'},
$self->setting('search_instructions') ||
$self->translate('SEARCH_INSTRUCTIONS', $oligo),
$self->setting('navigation_instructions') ||
$self->translate('NAVIGATION_INSTRUCTIONS'),
br(),
$self->examples(),
br(),$self->html_frag('html2',$self->state)
)
)
)
: '';
}
# Renders the HTML for the spinning "busy" signal on the top-left corner of the page.
sub render_busy_signal {
my $self = shift;
return img({
-id => 'busy_indicator',
-src => $self->data_source->button_url.'/spinner.gif',
-style => 'position: fixed; top: 5px; left: 5px; display: none',
-alt => ($self->translate('WORKING')||'')
});
}
# Renders the menu bar across the top of the browser.
sub render_actionmenu {
my $self = shift;
my $settings = $self->state;
my @export_links=a({-href=>'#',-onclick=>'Controller.make_image_link("GD")'}, $self->translate('IMAGE_LINK'));
push @export_links,a({-href=>'#',-onclick=>'Controller.make_image_link("GD::SVG")'}, $self->translate('SVG_LINK'))
if HAVE_SVG;
push @export_links,a({-href=>'#',-onclick=>'Controller.make_image_link("PDF")'}, $self->translate('PDF_LINK'))
if HAVE_SVG && $self->can_generate_pdf;
# Pass the gff link to a javascript function which will add the missing parameters that are determined client-side
push @export_links,a({-href=>'#',-onclick=>"Controller.gbgff_link('". $self->gff_dump_link ."')"}, $self->translate('DUMP_GFF'));
push @export_links,a({-href=>'#',-onclick=>"Controller.gbgff_link('". $self->dna_dump_link ."')"}, $self->translate('DUMP_SEQ'));
push @export_links,a({-href=>'javascript:'.$self->galaxy_link}, $self->translate('SEND_TO_GALAXY'))
if $self->data_source->global_setting('galaxy outgoing');
my $bookmark_link = a({-href=>'#',-onclick=>'Controller.bookmark_link()'},$self->translate('BOOKMARK')),;
my $share_link = a({-href => '#',
-onMouseDown => "GBox.showTooltip(event,'url:?action=share_track;track=all')"},
($self->translate('SHARE_ALL') || "Share These Tracks" )),
my $help_link = a({-href=>$self->general_help(),
-target=>'_new'},$self->translate('HELP_WITH_BROWSER'));
my $about_gb_link = a({-onMouseDown => "Controller.show_info_message('about_gbrowse')",
-href => 'javascript:void(0)',
-style => 'cursor:pointer'
},
$self->translate('ABOUT'));
my $about_dsn_link = a({-onMouseDown => "Controller.show_info_message('about_dsn')",
-href => 'javascript:void(0)',
-style => 'cursor:pointer'
},
$self->translate('ABOUT_DSN'));
my $about_me_link = a({-onMouseDown => "Controller.show_info_message('about_me')",
-href => 'javascript:void(0)',
-style => 'cursor:pointer'
},
$self->translate('ABOUT_ME'));
my $plugin_link = $self->plugin_links($self->plugins);
my $chrom_sizes_link = a({-href=>'?action=chrom_sizes'},$self->translate('CHROM_SIZES'));
my $reset_link = a({-href=>'?reset=1',-class=>'reset_button'}, $self->translate('RESET'));
my $login = $self->globals->user_accounts ? $self->render_login : '';
my $file_menu = ul({-id => 'actionmenu',
-class => 'dropdown downdown-horizontal'},
li({-class=>'dir'},$self->translate('FILE'),
ul(li($bookmark_link),
li($share_link),
li({-class=>'dir'},a({-href=>'#'},$self->translate('EXPORT')),
ul(li(\@export_links))),
$plugin_link ? li($plugin_link) : (),
li($chrom_sizes_link),
li($reset_link),
)
),
li({-class=>'dir'},$self->translate('HELP'),
ul({-class=>'dropdown'},
li($help_link),
li({-class=>'divider'},''),
li($about_gb_link),
li($about_dsn_link),
li($about_me_link),
)),
);
return div({-class=>'datatitle'},$file_menu,$login,br({-clear=>'all'}));
}
# Render Login - Returns the HTML for the login links on the top-right corner of the screen.
sub render_login {
my $self = shift;
my $settings = $self->state;
return unless $settings->{head};
return $self->login_manager->render_login;
}
# For the subset of plugins that are named in the 'quicklink plugins' option, create quick links for them.
sub plugin_links {
my $self = shift;
my $plugins = shift;
my $quicklink_setting = $self->setting('quicklink plugins') or return '';
my @plugins = shellwords($quicklink_setting) or return '';
my $labels = $plugins->menu_labels;
my @result;
for my $p (@plugins) {
my $plugin = $plugins->plugin($p) or next;
my $action = "?plugin=$p;plugin_do=".$self->translate('Go');
push @result,a({-href=>$action,-target=>'_new'},"[$labels->{$p}]");
}
return \@result;
}
sub galaxy_form {
my $self = shift;
my $segment = shift;
my $settings = $self->state;
my $source = $self->data_source;
my $galaxy_url = $settings->{GALAXY_URL}
|| $source->global_setting('galaxy outgoing') ;
return '' unless $galaxy_url;
my $URL = $source->global_setting('galaxy incoming');
$URL ||= $self->globals->gbrowse_url();
# Make sure to include all necessary parameters in URL to ensure that gbrowse will retrieve the data
# when Galaxy posts the URL.
my $dbkey = $source->global_setting('galaxy build name') || $source->name;
my $labels = $self->join_selected_tracks;
my $seg = $segment->seq_id.':'.$segment->start.'..'.$segment->end;
my $action = $galaxy_url =~ /\?/ ? "$galaxy_url&URL=$URL" : "$galaxy_url?URL=$URL";
my $html = start_multipart_form(-name => 'galaxyform',
-action => $action,
-method => 'POST');
$html .= hidden(-name=>'dbkey',-value=>$dbkey);
$html .= hidden(-name=>'gbgff',-value=>'save gff3');
$html .= hidden(-name=>'id', -value=>$settings->{userid});
$html .= hidden(-name=>'q',-value=>$seg);
$html .= hidden(-name=>'l',-value=>$labels);
$html .= hidden(-name=>'s',-value=>0);
$html .= hidden(-name=>'d',-value=>'edit');
$html .= hidden(-name=>'m',-value=>'application/x-gff3');
$html .= endform();
return $html;
}
sub render_track_filter {
my $self = shift;
my $plugin = shift;
my $form = $plugin->configure_form();
my $plugin_type = $plugin->type;
my $action = $self->translate('Configure_plugin');
my $name = $plugin->name;
my $showfav = $self->translate('FAVORITES');
my $showall = $self->translate('SHOWALL');
return
div({-id=>'track select',-style=>'padding-top:8px'},
start_form({-id => 'track_filterform',
-name => 'configure_plugin',
-onSubmit=> 'return false'}),
$form,
hidden(-name=>'plugin',-value=>$name),
button(
-name => 'plugin_button',
-value => $self->translate('search'),
-onClick => 'doPluginUpdate()',
),
end_form(),
script({-type=>'text/javascript'},
"function doPluginUpdate() { Controller.reconfigure_plugin('$action',null,null,'$plugin_type',\$('track_filterform'));updateTitle(\$('show_all_link'),0);}")
);
}
# This surrounds the track table with a toggle
sub render_toggle_track_table {
my $self = shift;
my $source = $self->data_source;
my $filter = $self->track_filter_plugin;
my $settings = $self->state;
# $settings->{show_favorites} =0;
## adding javascript array at the top so we can pass it into a js array -- ugly but it works
my $html = '';
$html .= div({-style=>'font-weight:bold'},
span({-style=>'padding-right:80px'},'<<',$self->render_select_browser_link('link')),
span({-id=>'showselectedtext',-style=>'padding-right:80px'},$self->render_show_active_tracks_link()),
span({-id => 'showfavoritestext',-style=>'padding-right:80px'},
$self->render_select_favorites_link('link')),
span({-id => 'clearfavs'},
$self->render_select_clear_link('link')));
if (my $filter = $self->track_filter_plugin) {
$html .= $self->toggle({tight=>1},'track_select',div({class=>'searchtitle',
style=>"text-indent:2em;padding-top:8px; display:block;"},
$self->render_track_filter($filter)));
}
$html .= $self->toggle({nodiv=>1},'Tracks',$self->render_track_table);
$html .= div({-style=>'text-align:center'},$self->render_select_browser_link('button'));
return $html;
}
sub render_track_table {
my $self = shift;
my $listing_class = $self->data_source->track_listing_class;
eval "require $listing_class;1" or die $@ unless $listing_class->can('new');
my $tlr = $listing_class->new($self);
return $tlr->render_track_listing.$self->html_frag('html5',$self->state);
}
# Render Multiple Choices -
sub render_multiple_choices {
my $self = shift;
my $features = shift;
my $terms2hilite = shift;
my $karyotype = Bio::Graphics::Karyotype->new(source => $self->data_source,
language => $self->language);
$karyotype->add_hits($features);
return $karyotype->to_html($terms2hilite);
}
# Render Global Config - Returns the HTML for the Preferences page.
sub render_global_config {
my $self = shift;
my $settings = $self->state;
my @widths = split /\s+/, $self->setting('image widths');
@widths = ( 640, 800, 1024 ) unless @widths;
my @key_positions = qw(between bottom);
push @key_positions, qw(left right)
if Bio::Graphics::Panel->can('auto_pad');
my $feature_highlights = $settings->{h_feat}
? join ' ',
map {"$_\@$settings->{h_feat}{$_}"} keys %{ $settings->{h_feat} }
: '';
my $region_highlights = $settings->{h_region}
? join ' ', @{ $settings->{h_region} }
: '';
my %seen;
my $region_size = $settings->{region_size} || 0;
my @region_size = shellwords($self->data_source->global_setting('region sizes'));
my @region_sizes = grep {!$seen{$_}++}
sort {$b<=>$a}
grep {defined $_ && $_ > 0} ($region_size,@region_size);
my $content
= start_form( -name => 'display_settings', -id => 'display_settings' )
. div(
table ({-border => 0, -cellspacing=>0, -width=>'100%'},
TR(
td( b( checkbox(
-name => 'grid',
-label => $self->translate('SHOW_GRID'),
-override => 1,
-checked => $settings->{grid} || 0,
-onChange => 'Controller.set_display_option(this.name,this.checked ? 1 : 0)',
)
)
),
td( b( $self->translate('Image_width') ),
br,
radio_group(
-name => 'width',
-values => \@widths,
-default => $settings->{width},
-override => 1,
-onChange => 'Controller.set_display_option(this.name,this.value)',
),
),
td( span(
{ -title => $self->translate('FEATURES_TO_HIGHLIGHT_HINT') },
b( $self->translate('FEATURES_TO_HIGHLIGHT') ),
br,
textfield(
-id => 'h_feat',
-name => 'h_feat',
-value => $feature_highlights,
-size => 50,
-override => 1,
-onChange => 'Controller.set_display_option(this.name,this.value)',
),
a({-href=>'javascript:void(0)',
-onClick=>'Controller.set_display_option("h_feat","_clear_");$("h_feat").value=""'},
$self->translate('CLEAR_HIGHLIGHTING'))
),
),
),
TR(
td( $self->data_source->cache_time
? ( b( checkbox(
-name => 'cache',
-label => $self->translate('CACHE_TRACKS'),
-override => 1,
-checked => $settings->{cache},
-onChange => 'Controller.set_display_option(this.name,this.checked?1:0)'
)
)
)
: ()
),
td(' '),
td( span(
{ -title => $self->translate('REGIONS_TO_HIGHLIGHT_HINT') },
b( $self->translate('REGIONS_TO_HIGHLIGHT') ),
br,
textfield(
-id => 'h_region',
-name => 'h_region',
-value => $region_highlights,
-size => 50,
-override => 1,
-onChange => 'Controller.set_display_option(this.name,this.value)',
),
a({-href=>'javascript:void(0)',
-onClick=>'Controller.set_display_option("h_region","_clear_");$("h_region").value=""'
},
$self->translate('CLEAR_HIGHLIGHTING'))
),
),
),
TR(
td( { -align => 'left' },
b( checkbox(
-name => 'show_tooltips',
-label => $self->translate('SHOW_TOOLTIPS'),
-override => 1,
-checked => $settings->{show_tooltips},
-onChange => 'Controller.set_display_option(this.name,this.checked?1:0)'
),
)
),
td(' '),
td( $self->setting('region segment')
? ( b( $self->translate('Region_size') ),
br,
popup_menu(
-name => 'region_size',
-default => $settings->{region_size},
-values => \@region_sizes,
-override => 1,
-onChange => 'Controller.set_display_option(this.name,this.value)',
),
)
: (),
),
),
TR(
td( { -colspan => 3,
-align => 'right'
},
b( submit( -name => $self->translate('Update_settings') ) )
)
)
)
) . end_form();
return div($content);
}
# Clear Hilights - Returns the HTML for the "Clear Highligting" link.
sub clear_highlights {
my $self = shift;
my $link = a({-style => 'font-size:9pt',
-href => 'javascript:void(0)',
-onClick => 'Controller.set_display_option("h_feat","_clear_");Controller.set_display_option("h_region","_clear_")'
},
$self->translate('CLEAR_HIGHLIGHTING'));
}
# Render Select Track Link - Returns the HTML for the "Select Tracks" button on the main browser page.
sub render_select_track_link {
my $self = shift;
my $title = $self->translate('SELECT_TRACKS');
return button({-name=>$title,
-onClick => "Controller.select_tab('track_page')",
}
);
}
sub render_select_clear_link {
my $self = shift;
my $title = $self->translate('CLEAR_FAV');
my $settings = $self->state;
my $clear = 1;
warn "settings $settings->{show_favorites}" if DEBUG;
my $showicon = img({-src => $self->data_source->button_url."/ficon.png",-border=>0});
return span(a({-href=>'javascript:void(0)',
-onClick => "clearallfav($clear);",
},
$title),$showicon);
}
sub render_show_active_tracks_link {
my $self = shift;
my $active = $self->state->{active_only} ? 'true' : 'false';
return a({-href => 'javascript:void(0)',
-class => $active ? 'show_active' : 'inactive',
-onClick => "show_active_tracks(this,$active)"},
$self->translate('SHOW_ACTIVE_TRACKS'));
}
sub render_select_favorites_link {
my $self = shift;
my $showfav = $self->translate('FAVORITES');
my $showall = $self->translate('SHOWALL');
my $refresh = 'showrefresh';
my $settings = $self->state;
my $ison = $settings->{show_favorites};
my $title = $ison ? $showall : $showfav;
my $showicon = img({-src => $self->data_source->button_url."/ficon_2.png",-border=>0});
warn "settings $settings->{show_favorites}" if DEBUG;
warn "ison = $settings->{show_favorites}" if DEBUG;
return span(a({-id => 'show_all_link',
-class => $settings->{show_favorites} ? 'favorites_only' : '',
-href =>'javascript:void(0)',
-onClick => "updateTitle(this)"
},$title),$showicon);
}
# Render Select Browser Link - Returns the HTML for the "Back to Browser" button/link.
sub render_select_browser_link {
my $self = shift;
my $style = shift || 'button';
my $settings = $self->state;
my $title = $self->translate('BACK_TO_BROWSER');
if ($style eq 'button') {
return button({-name=>$title,
-onClick => "Controller.select_tab('main_page')",
}
);
} elsif ($style eq 'link') {
return a({-href=>'javascript:void(0)',
-onClick => "Controller.select_tab('main_page')"},
$title);
}
}
# Render Community Tracks Section - Returns the content of the "Community Tracks" tab.
sub render_community_tracks_section {
my $self = shift;
my $userdata = $self->user_tracks;
my $html = $self->is_admin? h2({-style=>'font-style:italic;background-color:yellow'}, $self->translate('ADMIN_MODE_WARNING')) : "";
$html .= div({-id => "community_tracks"}, $self->render_community_track_listing);
$html = div({-style => 'margin: 1em;'}, $html);
return $html;
}
# Render Custom Tracks Section - Returns the content of the "Custom Tracks" tab.
sub render_custom_tracks_section {
my $self = shift;
my $userdata = $self->user_tracks;
my $html = $self->is_admin? h2({-style=>'font-style:italic;background-color:yellow'}, $self->translate('ADMIN_MODE_WARNING')) : "";
$html .= div({-id => "custom_tracks"}, $self->render_custom_track_listing);
$html .= $self->userdata_upload;
$html = div({-style => 'margin: 1em;'}, $html);
return $html;
}
# Userdata Upload - Renders an "Add custom tracks" links in the Uploaded Tracks section.
sub userdata_upload {
my $self = shift;
my $url = url(-absolute=>1,-path_info=>1);
my $html = '';
my $upload_label = $self->translate('UPLOAD_FILE');
my $mirror_label = $self->translate('MIRROR_FILE');
my $remove_label = $self->translate('REMOVE');
my $new_label = $self->translate('NEW_TRACK');
my $from_text = $self->translate('FROM_TEXT');
my $from_file = $self->translate('FROM_FILE');
my $from_url = $self->translate('FROM_URL');
my $help_link = $self->annotation_help;
$html .= p({-style=>'margin-left:10pt;font-weight:bold'},
$self->translate('ADD_YOUR_OWN_TRACKS') ,':',
a({-href=>"javascript:addAnUploadField('custom_list_start', '$url', '$new_label', '$remove_label', 'edit','$help_link')"},
"[$from_text]"),
a({-href=>"javascript:addAnUploadField('custom_list_start', '$url', '$mirror_label', '$remove_label', 'url','$help_link')"},
"[$from_url]"),
a({-href=>"javascript:addAnUploadField('custom_list_start', '$url','$upload_label', '$remove_label' , 'upload','$help_link')",
-id=>'file_adder',
},"[$from_file]"));
$html .= div({-id=>'custom_list_start'},'');
return $html;
}
# Render Community Track Listing - Returns the HTML listing of public tracks available to a user.
sub render_community_track_listing {
my $self = shift;
my $globals = $self->globals;
my $html = h1({-style => "display: inline-block; margin-right: 1em;"}, $self->translate('COMMUNITY_TRACKS'));
my $search = $_[0] || "";
my $offset = $_[1] || 0;
my $usertracks = $self->user_tracks;
my @requested_tracks = $usertracks->get_public_files(@_) if @_;
# Calculate the value for the next pagination
my $max_files = $globals->public_files;
my $total_tracks = $usertracks->public_count($search);
my $track_limit = $search? @requested_tracks : $max_files;
my $tracks_displayed = ($track_limit < (@requested_tracks? @requested_tracks : $total_tracks))
? $track_limit : (@requested_tracks? @requested_tracks : $total_tracks);
my $tracks_remaining = $total_tracks - ($offset + $tracks_displayed);
my $tracks_before = $offset;
my $tracks_next = ($track_limit < $tracks_remaining)? $track_limit : $tracks_remaining;
my $next_offset = $offset + $track_limit;
my $tracks_previous = ($max_files < $tracks_before)? $max_files : $tracks_before;
my $previous_offset = $offset - $globals->public_files;
$previous_offset = 0 if $previous_offset < 0;
my $first_number = $offset + 1;
my $last_number = $offset + $tracks_displayed;
my $autocomplete = '';
if ($self->setting('autocomplete')) {
my $spinner_url = $self->data_source->button_url.'/spinner.gif';
$autocomplete = <<END
<span id="indicator2" style="display: none">
<img src="$spinner_url" />
</span>
<div id="autocomplete_upload_filter" class="autocomplete"></div>
END
}
# Create the HTML for the title & header
$html .= span({-style => "display: inline-block;"},
start_form({-action => "javascript:void(0);",
# The return here is necessary to stop the form from ACTUALLY submitting.
-onsubmit => "return searchPublic(\$('public_search_keyword').value);"
}),
input({-type => "hidden", -name => "offset",
-value => $offset, -id => "community_display_offset"}),
ucfirst $self->translate('FILTER') . ":",
input({
-type => "text",
-name => "keyword",
-id => "public_search_keyword",
-style => "width:200px",
-value => $search || ($self->globals->user_accounts? $self->translate('ENTER_KEYWORD') .
" " . $self->translate('OR_USER') : $self->translate('ENTER_KEYWORD')),
-onClick => "this.value='';"
}),
input({-type => "submit", -value => "Search"}),
($tracks_previous > 0)? a({-href => '#',
-onClick => "return searchPublic(\"$search\", $previous_offset);"},
"[" . $self->translate('PREVIOUS_N', $tracks_previous) . "]") . " " : "",
button(-label=>$self->tr('CLEAR'),
-onClick=>"\$('public_search_keyword').value='';searchPublic('',$previous_offset);"),
ucfirst $self->translate('SHOWING'). " "
. (($total_tracks > 0)? $self->translate('N_TO_N_OUT_OF', $first_number, $last_number) : "") . " "
. $self->translate('N_FILES', $total_tracks)
. ($search? (" " . $self->translate("MATCHING", $search)) : "")
. ".",
($tracks_next > 0)? " " . a({-href => '#', -onClick => "return searchPublic(\"$search\", $next_offset);"},
"[" . $self->translate('NEXT_N', $tracks_next) . "]") : "",
end_form(),
$autocomplete
);
# Add the results
if ($search || $offset) {
$html .= @requested_tracks
? $self->list_tracks("public", @requested_tracks)
: p($self->translate('NO_PUBLIC_RESULTS', $search));
} else {
$html .= $self->list_tracks("public");
}
return $html;
}
# Render Custom Track Listing - Returns the HTML listing of public, uploaded, imported and shared tracks added to a session, and a section to add more.
sub render_custom_track_listing {
my $self = shift;
my $html = h1($self->translate('UPLOADED_TRACKS'));
$html .= a( {
-href => $self->annotation_help,
-target => '_blank'
},
i('['.$self->translate('HELP_FORMAT_UPLOAD').']')
);
$html .= $self->list_tracks;
return $html;
}
# List Tracks - Renders a visual listing of an array of tracks. No arguments creates the standard "my tracks" listing.
sub list_tracks {
my $self = shift;
my $userdata = $self->user_tracks;
my $listing_type = shift || "";
# If we've been given input, use the input.
# If we've been given the public type, use that, or default to all of the current user's tracks.
my @tracks = @_? @_
: (($listing_type =~ /public/) && ($userdata->database == 1))
? $userdata->get_public_files
: $userdata->tracks;
my $track_type = $listing_type;
$track_type .= " available" if $listing_type =~ /public/;
# Main track roll code.
if (@tracks) {
my $count = 0;
my @rows = map {
my $fileid = $_;
my $type = $track_type || $userdata->file_type($fileid);
my $class = $self->track_class($count, $type);
my $controls = $self->render_track_controls($fileid, $type);
my $short_listing = $self->render_track_list_title($fileid, $type);
my $details = $self->render_track_details($fileid, @tracks? 1 : 0);
my $edit_field = div({-id => $fileid . "_editfield", -style => "display: none;"}, '');
$count++;
div({
-id => "$fileid",
-class => "custom_track $class",
-style => 'padding: 0.5em',
},
$controls,
$short_listing,
$details,
$edit_field
);
} @tracks;
return join '', @rows;
} else {
return p($self->translate(($track_type =~ /public/i ? 'THERE_ARE_NO_AVAILABLE_TRACKS':'THERE_ARE_NO_TRACKS_YET'), $track_type));
}
}
# Track Class (Count, Type) - Returns the class for a specific custom track.
sub track_class {
my $self = shift;
my $count = shift;
my $type = shift;
$type =~ s/\s?available//;
return $type . "_" . (($count % 2)? "even" : "odd");
}
# Render Track List Title (Track, Type) - Renders the visible HTML which is seen when the details are hidden.
sub render_track_list_title {
my $self = shift;
my $fileid = shift;
my $type = shift;
$type =~ s/\s?available//;
my $userdata = $self->user_tracks;
my $globals = $self->globals;
my $short_name = $userdata->title($fileid);
if ($short_name =~ /http_([^_]+).+_gbgff_.+_t_(.+)_s_/) {
my @tracks = split /\+/, $2;
$short_name = "Shared track from $1 (@tracks)";
} elsif (length $short_name > 40) {
$short_name =~ s/^(.{40}).+/$1.../;
}
my $is_mine = $userdata->is_mine($fileid);
my $cursor = $is_mine ? 'cursor:pointer' : 'cursor:auto';
my $uploaddb = $userdata->database;
my @track_labels = $userdata->labels($fileid);
my $track_labels = join '+', map {CGI::escape($_)} @track_labels;
my $source_note = span({-class => "source_note"}, $type);
my $go_there = join(' ',
map {
my $label = $_;
my $go_there_script = "Controller.select_tab('main_page',false);Controller.scroll_to_matching_track('$label')";
my $edit_label_script = "Controller.edit_upload_track_key('$fileid', '$label', this)";
my $script = $is_mine ? "if (event.shiftKey || event.ctrlKey) {$edit_label_script} else {$go_there_script}"
: $go_there_script;
my $key = $self->data_source->setting($label=>'key');
$key? (
'['.
span({-class => 'clickable',
-onClick => $script,
-contentEditable => ($is_mine ? 'true' : 'false'),
},
b($key)
).
']'
) : ''
} @track_labels);
my $stat = div(
{
-id => $fileid . "_stat",
-style=> "display: inline;"
},
''
);
my $title = h1(
{
($is_mine ? (-title => $self->tr('ADD_TITLE')) : ()),
-style => "display: inline; font-size: 14pt;$cursor",
-onClick => ($uploaddb && $is_mine)? "Controller.edit_upload_title('$fileid', this)" : "",
-contentEditable => ($uploaddb && $is_mine)? 'true' : 'false',
},
$short_name
);
my $owner;
if ($globals->user_accounts) {
my $owner_name = $userdata->owner_name($fileid);
my $users = $self->userdb;
my ($fullname,$email) = $users->accountinfo_from_username($owner_name);
$email ||= '';
my $email_link = a({-href=>"mailto:$email"},$email);
$fullname ||= $owner_name;
$owner = $self->translate("UPLOADED_BY") . " " . b($fullname).($email ? " <${email_link}>" : '');
} else {
$owner = '';
}
return span(
{-style => "display: inline;"},
$stat,
$title,
$owner,
br(),
$go_there,
) . $source_note;
}
# Render Track Controls (Track Name, Type) - Renders the HTML for the main track controls in the custom track listing.
sub render_track_controls {
my $self = shift;
my $fileid = shift;
my $type = shift;
my $userdata = $self->user_tracks;
my $userid = $userdata->{userid}||'';
my @track_labels = $userdata->labels($fileid);
my $track_labels = join '+', map {CGI::escape($_)} @track_labels;
my $globals = $self->globals;
my $buttons = $self->data_source->globals->button_url;
my $controls;
# Conditional controls, based on the type of track.
if ($userdata->is_mine($fileid)) {
# The delete icon,
$controls .= ' ' . img(
{
-src => "$buttons/trash.png",
-style => 'cursor:pointer',
-onMouseOver => 'GBubble.showTooltip(event,"'.$self->translate('DELETE').'",0)',
-onClick => "deleteUpload('$fileid')"
}
);
# The sharing icon, if it's an upload.
$controls .= ' ' . img(
{
-src => "$buttons/share.png",
-style => 'cursor:pointer',
-onMouseOver => 'GBubble.showTooltip(event,"'.$self->translate('SHARE_WITH_OTHERS').'",0)',
-onClick => "Controller.get_sharing(event,'url:?action=share_track;track=$track_labels')"
}
) if ($type =~ /upload/ && !$userdata->database);
}
if ($type !~ /available/) {
if ($type =~ /(public|shared)/) {
# The "remove" [x] link.
$controls .= ' ' . a(
{
-href => "javascript: void(0)",
-onMouseOver => 'GBubble.showTooltip(event,"'.$self->translate('REMOVE_FROM_MY_SESSION').'",0,200)',
-onClick => "unshareFile('$fileid', '$userid')"
},
"[X]"
);
}
} else {
$userid ||= '';
$controls .= ' ' . a(
{
-href => "javascript:void(0);",
-onClick => "shareFile('$fileid', '$userid')"
},
'['.$self->translate('SHARING_ADD_USER').']'
);
}
return span(
{
-class => "controls",
-style => "display: inline; padding: 0.3em;"
}, $controls
);
}
# Render Track Details (Track Name, Display?) - Renders the track listing details section.
sub render_track_details {
my $self = shift;
my $fileid = shift;
my $display = shift || 0;
my $userdata = $self->user_tracks;
my $globals = $self->globals;
my $random_id = 'upload_'.int rand(9999);
my $is_mine = $userdata->is_mine($fileid);
my $cursor = $is_mine ? 'cursor:pointer' : 'cursor:auto';
my $description = div(
{
($is_mine ? (-title => $self->tr('ADD_DESCRIPTION')) : ()),
-style => $cursor,
-id => $fileid . "_description",
-onClick => $is_mine ? "Controller.edit_upload_description('$fileid', this)" : "",
-contentEditable => $is_mine ? 'true' : 'false',
},
$userdata->description($fileid) || $self->translate($is_mine ? 'ADD_DESCRIPTION' : 'NO_DESCRIPTION')
);
my $source_listing = div(
{-style => "margin-left: 2em; display: inline-block;"},
$self->render_track_source_files($fileid)
);
my $sharing = ($userdata->database == 1)? div(
{
-style => "margin-left: 2em; display: inline;",
-class => "sharing"
},
$self->render_track_sharing($fileid)
) : "";
my $status = $userdata->status($fileid) || 'complete';
my $status_box = div(
div({-id=>"${random_id}_form", -style=>"display: none;"},' '),
div({-id=>"${random_id}_status", -style=>"display: none;"},
i($status),
a(
{
-href =>'javascript:void(0)',
-onClick => "Controller.monitor_upload('$random_id','$fileid')",
},
$self->translate('INTERRUPTED_RESUME')
)
)
);
return div(
{
-style => $display? "display: block;" : "display: none;",
-class => "details"
},
i($description),
$source_listing,
br(),
$sharing,
$status_box
);
}
# Render Track Source Files (Track) - Renders the HTML listing of a track's source files.
sub render_track_source_files {
my $self = shift;
my $fileid = shift;
my $userdata = $self->user_tracks();
my @source_files = $userdata->source_files($fileid);
my ($conf_name, $conf_modified, $conf_size) = $userdata->conf_metadata($fileid);
$conf_modified ||= 0;
$conf_size ||=0;
my $mirror_url = $userdata->is_mirrored($fileid);
my $source_listing =
b($self->translate('SOURCE_FILES')) .
ul(
{-style => "margin: 0; padding: 0; list-style: none;"},
li(
[map {
a( {
-href => $mirror_url || "?userdata_download=$_->[0];track=$fileid",
-style => "display: inline-block; width: 30em; overflow: hidden;"
},
$_->[0]
).
span({-style => "display: inline-block; width: 15em;"}, scalar localtime($_->[2])).
span({-style => "display: inline-block; width: 10em;"}, $_->[1],'bytes').
span(
($_->[1] <= MAXIMUM_EDITABLE_UPLOAD && -T $_->[3] && $userdata->is_mine($fileid))?
$mirror_url?
a( {
-href => "javascript:void(0)",
-onClick => "reloadURL('$fileid','$mirror_url')"
},
$self->translate('RELOAD_FROM', $mirror_url)
) :
a( {
-href => "javascript:void(0)",
-onClick => "editUploadData('$fileid','$_->[0]')"
},
$self->translate('EDIT_BUTTON')
)
: ' '
)
} @source_files]
),
li(
a( {
-href => "?userdata_download=conf;track=$fileid",
-style => "display: inline-block; width: 30em;"
},
$self->translate('CONFIGURATION')
).
span({-style => "display: inline-block; width: 15em;"}, scalar localtime $conf_modified).
span({-style => "display: inline-block; width: 10em;"}, "$conf_size bytes").
span(
($userdata->is_mine($fileid))
? a({
-href => "javascript:void(0)",
-onClick => "editUploadConf('$fileid')"
}, $self->translate('EDIT_BUTTON')
)
: " "
)
)
);
return $source_listing;
}
# Render Track Sharing (Track) - Renders the HTML listing of a track's sharing properties.
sub render_track_sharing {
my $self = shift;
my $fileid = shift;
my $globals = $self->globals;
my $userdb = $self->userdb if $globals->user_accounts;
my $userdata = $self->user_tracks;
#Building the users list.
my $sharing_policy = $userdata->permissions($fileid);
my @users = $userdata->shared_with($fileid);
$_ = b(($globals->user_accounts)
? $userdb->username_from_userid($_) : "an anonymous user") .
" " .
a({-href => "javascript:void(0)", -onClick => "unshareFile('$fileid', '$_')"}, "[X]") . "" foreach @users;
my $userlist = join (", ", @users);
my $sharing_content = b($self->translate('SHARING')) . br() . $self->translate('TRACK_IS') . " ";
if ($userdata->is_mine($fileid) == 0) {
my $count = ($sharing_policy eq "public")? $userdata->public_users($fileid) : $userdata->shared_with($fileid);
$sharing_content .= b(($sharing_policy =~ /(casual|group)/)? lc $self->translate('SHARED_WITH_YOU') : lc $self->translate('SHARING_PUBLIC'));
$sharing_content .= ", " . $self->translate('USED_BY') . " " . ($count? b($count) . " " . $self->translate('USERS') . "." : $self->translate('NO_ONE')) unless $sharing_policy =~ /casual/;
} else {
my %sharing_type_labels = ( private => $self->translate('SHARING_PRIVATE'),
casual => $self->translate('SHARING_CASUAL') ,
group => $self->translate('SHARING_GROUP') ,
public => $self->translate('SHARING_PUBLIC') );
$sharing_content .= Select(
{-onChange => "changePermissions('$fileid', this.options[this.selectedIndex].value.toLowerCase())"},
map {
option(
{
-value => $_,
($sharing_policy =~ /$_/i)? (-selected => "selected") : ()
},
$sharing_type_labels{$_}
)
} keys %sharing_type_labels
);
my $sharing_help = $self->translate('SHARING_HELP');
$sharing_content .= " " . a({-href => "javascript:void(0)",
-onMouseOver => "GBubble.showTooltip(event,'$sharing_help',0,300);"},
"[?]");
$sharing_content .= " " . $self->translate('SHARED_WITH') .
" " . ($userlist? "$userlist" : $self->translate('NO_ONE')
) if ($sharing_policy =~ /(casual|group)/);
if ($sharing_policy =~ /public/) {
my $count = $userdata->public_users($fileid);
$sharing_content .= " " .
$self->translate('USED_BY') .
" " . ($count? b($count) .
" " . $self->translate('USERS') . "." : $self->translate('NO_ONE'));
}
if ($sharing_policy =~ /casual/) {
my $sharing_url = $userdata->sharing_link($fileid);
my $sharing_link = a({-href => $sharing_url}, $sharing_url);
$sharing_content .= br() . $self->translate('SHARE_WITH_THIS_LINK');
$sharing_content .= $sharing_link;
}
if ($sharing_policy =~ /group/) {
my $id = 'username_entry_'.int(rand(100000));
my $add_box = " " . input(
{
-length => 60,
-style => 'width:300px',
-class => 'username_entry',
-id => $id,
-value => $self->translate('ENTER_SOMETHING_HERE',
(($globals->user_accounts)
? $self->translate('USERNAME_OR_USER_ID')
: $self->translate('USER_ID'))),
-onFocus => "this.clear()"
});
my $add_autocomplete = div({-id=>"${id}_choices",
-class=>'autocomplete usersearch'},'') if $self->setting('autocomplete');
my $add_link = " " . a(
{
-href => "javascript: void(0)",
-onClick => "shareFile('$fileid', this.previous('input').getValue())",
},
$self->translate('ADD_BUTTON') );
$sharing_content .= $add_box . $add_autocomplete . $add_link;
};
}
return $sharing_content;
}
sub segment2link {
my $self = shift;
my ($segment,$label) = @_;
my $source = $self->data_source;
return a({-href=>"?name=$segment"},$segment) unless ref $segment;
my $ref = $segment->seq_id;
my ($start,$stop) = ($segment->start,$segment->end);
my $bp = $stop - $start;
$label ||= segment_str($segment);
$ref||=''; # get rid of uninit warnings
return a({-href=>"?ref=$ref;start=$start;stop=$stop"},$label);
}
sub tableize {
my $self = shift;
my ($array,$cols,$labelnames,$row_labels,$column_labels) = @_;
return unless @$array;
my $settings = $self->state;
my $columns = $cols ||
$self->data_source->global_setting('config table columns') || 3;
my $rows = int( @$array/$columns + 0.99 );
# gets the data for the defined 'category table(s)'
my (@column_labels,@row_labels);
if ($row_labels && $column_labels) {
@row_labels = @$row_labels;
@column_labels = @$column_labels;
$rows = @row_labels;
$columns = @column_labels;
}
my $cwidth = int(100/$columns+0.5) . '%';
my $html = start_table({-border=>0,-width=>'100%'});
if (@column_labels) {
$html.="<tr valign='top'><td></td>";
for (my $column=0;$column<$columns;$column++) {
$html .= "<td><b>$column_labels[$column]</b> </td>";
}
$html.="</tr>";
}
for (my $row=0;$row<$rows;$row++) {
# do table headers
$html .= qq(<tr class="searchtitle" valign="top">);
$html .= "<td><b>$row_labels[$row]</b></td>" if @row_labels;
for (my $column=0;$column<$columns;$column++) {
my $label = $labelnames->[$column*$rows + $row] || ' ';
my $checkbox = $array->[$column*$rows + $row] || ' ';
# de-couple the checkbox and label click behaviors
$checkbox =~ s/\<\/?label\>//gi;
if ($label =~/^=/) {
$label = ' ';
$checkbox = ' ';
}
my $class = $settings->{features}{$label}{visible} ? 'activeTrack' : '';
$html .= td({-width=>$cwidth,-style => 'visibility:visible',-class=>$class},
span({ -id => "notselectedcheck_${label}",
-class => 'notselected_check'},$checkbox));
}
$html .= "</tr>\n";
}
$html .= end_table();
}
sub subtrack_counts {
my $self = shift;
my $label = shift;
my $stt = $self->create_subtrack_manager($label) or return;
return $stt->counts;
}
sub subtrack_table {
my $self = shift;
my $label = shift;
my $stt = $self->create_subtrack_manager($label);
return $stt->selection_table($self);
}
#### generate the fragment of HTML for printing out the examples
sub examples {
my $self = shift;
my $examples = $self->setting('examples') or return;
my @examples = shellwords($examples);
return unless @examples;
my @urls = map { a({-href=>"?name=".escape($_)},$_) } @examples;
return b($self->translate('Examples')).': '.join(', ',@urls).". ";
}
######################### code for the search box and navigation bar ###################
sub plugin_menu {
my $self = shift;
my $settings = $self->state;
my $plugins = $self->plugins;
my $labels = $plugins->menu_labels;
my @plugins = grep {$plugins->plugin($_)->type ne 'trackfilter'} # track filter gets its own special position
sort {$labels->{$a} cmp $labels->{$b}} keys %$labels;
# Add plugin types as attribute so the javascript controller knows what to do
# with each plug-in
my %attributes = map {
$_ => {
'plugin_type' => $plugins->plugin($_)->type(),
'track_name' => "plugin:".$plugins->plugin($_)->name(),
}
}
keys %$labels;
return unless @plugins;
return join(
'',
popup_menu(
-name => 'plugin',
-id => 'plugin',
-values => \@plugins,
-labels => $labels,
-attributes => \%attributes,
-default => $settings->{plugin},
),
' ',
button(
-name => 'plugin_action',
-id => 'configure_plugin_button',
-value => $self->translate('Configure'),
-onClick => 'Controller.configure_plugin("plugin_configure_div");'
),
' ',
button(
-name => 'plugin_action',
-value => $self->translate('Go'),
-onClick => 'var select_box = document.pluginform.plugin;'
. q{var plugin_type = select_box.options[select_box.selectedIndex].attributes.getNamedItem('plugin_type').value;}
. 'Controller.plugin_go('
. 'document.pluginform.plugin.value,'
. 'plugin_type,' . '"'
. $self->translate('Go') . '",'
. '"form"' . ');',
),
);
}
# plugin configuration form for balloon content
sub plugin_configuration_form {
my $self = shift;
my $plugin = shift;
my $plugin_type = $plugin->type;
my $plugin_name = $plugin->name;
my $plugin_id = $plugin->id;
print CGI::header(-type=>'text/html',
-cache_control =>'no-cache');
print start_form(
-name => 'configure_plugin',
-id => 'configure_plugin',
),
button(-value => $self->translate('Cancel'),
-onClick=>'Balloon.prototype.hideTooltip(1)'),
button(-value => $self->translate('Configure_plugin'),
-onClick=>'Controller.reconfigure_plugin('
. '"'.$self->translate('Configure_plugin').'"'
. qq(, "plugin:$plugin_id")
. qq(, "plugin_configure_div")
. qq(, "$plugin_type")
. qq(, this.parentNode)
. ');Balloon.prototype.hideTooltip(1)'),
$plugin->configure_form(),
end_form();
}
# wrap arbitrary HTML in a named div
sub wrap_in_div {
my $self = shift;
my $div_id = shift;
return div({-id=>$div_id},@_);
}
# Wrap the plugin configuration html into a form and tie it into the controller
sub wrap_plugin_configuration {
my $self = shift;
my $plugin_base = shift or return '';
my $plugin = shift or return '';
my $config_html = $plugin->configure_form();
my $return_html = start_form(
-name => 'configure_plugin',
-id => 'configure_plugin',
-onSubmit => 'return false;',
);
if ($config_html) {
my $plugin_type = $plugin->type;
my $plugin_name = $plugin->name;
my $plugin_id = $plugin->id;
my @plugin_description = $plugin->description;
my @buttons;
# Cancel Button
push @buttons,
button(
-name => 'plugin_button',
-value => $self->translate('CANCEL'),
-onClick => 'Controller.wipe_div("plugin_configure_div");'
);
# Configure Button
# Supplies the track name and the track div which I'm not really
# happy with
push @buttons,
button(
-name => 'plugin_button',
-value => $self->translate('Configure_plugin'),
-onClick => 'Controller.reconfigure_plugin("'
. $self->translate('Configure_plugin') . '", "'
. "plugin:$plugin_id"
. '","plugin_configure_div","'
. $plugin_type . '");'
);
if ( $plugin_type eq 'finder' ) {
push @buttons,
button(
-name => 'plugin_button',
-value => $self->translate('Find'),
-onClick => 'Controller.plugin_go("'
. $plugin_base . '","'
. $plugin_type . '","'
. $self->translate('Find') . '","'
. 'config' . '")',
);
}
elsif ( $plugin_type eq 'dumper' ) {
push @buttons,
button(
-name => 'plugin_button',
-value => $self->translate('Go'),
-onClick => 'Controller.plugin_go("'
. $plugin_base . '","'
. $plugin_type . '","'
. $self->translate('Go') . '","'
. 'config' . '")',
);
}
# Start adding to the html
$return_html .= h1(
$plugin_type eq 'finder'
? $self->translate('Find')
: $self->translate('Configure'),
$plugin_name,
);
$return_html .= div({-style=>'font-size:small'},@plugin_description);
my $button_html = join( ' ',
@buttons[ 0 .. @buttons - 2 ],
b( $buttons[-1] ),
);
$return_html .= join '', $button_html, $config_html, p(),
$button_html,;
}
else {
$return_html .= join '', p( $self->translate('Boring_plugin') ),
b(
button(
-name => 'plugin_button',
-value => $self->translate('CANCEL'),
-onClick => 'Controller.wipe_div("plugin_configure_div");'
)
);
}
$return_html .= end_form();
return $return_html;
}
sub wrap_track_in_track_div {
my $self = shift;
my %args = @_;
my $track_id = $args{'track_id'};
my $track_name = $args{'track_name'};
my $track_html = $args{'track_html'};
# track_type used in register_track() javascript method
my $track_type = $args{'track_type'} || 'standard';
my $section = $self->data_source->get_section_from_label($track_id);
my $class = $track_id =~ /scale/i ? 'scale' : 'track';
return div(
{ -id => "track_" . $track_id,
-class => $class,
},
$track_html
)
. qq[<script type="text/javascript" language="JavaScript">Controller.register_track("]
. $track_id . q[", "]
. $track_name . q[", "]
. $track_type . q[", "]
. $section
. q[");</script>];
}
sub do_plugin_header {
my $self = shift;
my $plugin = shift;
my $cookie = shift;
my ( $mime_type, $attachment ) = $plugin->mime_type;
print header(
-cookie => $cookie,
-type => $mime_type,
-charset => $self->translate('CHARSET'),
$attachment ? ( -attachment => $attachment ) : (),
);
}
# Slider Table - Returns the HTML for the zooming and panning controls.
sub slidertable {
my $self = shift;
my $state = $self->state;
# try to avoid reopening the database -- recover segment
# and whole segment lengths from our stored state if available
my $span = $state->{view_stop} - $state->{view_start} + 1;
my $max = $self->thin_whole_segment->length;
my $buttonsDir = $self->data_source->button_url;
my $half_title = $self->data_source->unit_label(int $span/2);
my $full_title = $self->data_source->unit_label($span);
my $half = int $span/2;
my $full = $span;
my $fine_zoom = $self->get_zoomincrement();
my $show = $self->translate('Show').' ';
my @lines =
(image_button(-src => "$buttonsDir/green_l2.gif",
-name=>"left $full",
-title => "left $full_title",
-onClick => "Controller.scroll('left', 1)"
),
' ',
image_button(-src=>"$buttonsDir/green_l1.gif",-name=>"left $half",
-title=>"left $half_title",
-onClick => "Controller.scroll('left', 0.5)"
),
' ',
image_button(-src=>"$buttonsDir/mminus.png",
-name=>"zoom out $fine_zoom",
-style=>'background-color: transparent',
-title=>"zoom out $fine_zoom",
-onClick => "Controller.update_coordinates(this.name)"
),
' ',
$self->zoomBar($span,$max,$show),
' ',
image_button(-src=>"$buttonsDir/mplus.png",
-name=>"zoom in $fine_zoom",
-style=>'background-color: transparent',
-title=>"zoom in $fine_zoom",
-onClick => "Controller.update_coordinates(this.name)",
),
' ',
image_button(-src=>"$buttonsDir/green_r1.gif",-name=>"right $half",
-title=>"right $half_title",
-onClick => "Controller.scroll('right', 0.5)"
),
' ',
image_button(-src=>"$buttonsDir/green_r2.gif",-name=>"right $full",
-title=>"right $full_title",
-onClick => "Controller.scroll('right', 1)"
),
' ',
);
my $str = join('', @lines);
return span({-id=>'span'},$str);
}
# this generates the popup zoom menu with the window sizes
sub zoomBar {
my $self = shift;
my ($length,$max,$item_label) = @_;
$item_label ||= '';
my %seen;
my @r = sort {$a<=>$b} $self->data_source->get_ranges();
$max *= $self->data_source->unit_divider;
my @ranges = grep {!$seen{$self->data_source->unit_label($_)}++ && $_<=$max} sort {$b<=>$a} @r,$length;
my %labels = map {$_=>$item_label.$self->data_source->unit_label($_)} @ranges;
return popup_menu(-class => 'searchtitle',
-name => 'span',
-values => \@ranges,
-labels => \%labels,
-default => $length,
-force => 1,
-onChange => 'Controller.update_coordinates("set span "+this.value)',
);
}
sub render_ruler_div {
my $self = shift;
my $ruler_js = <<RULER;
<script type="text/javascript">
// <![CDATA[
createRuler();
// ]]>
</script>
RULER
my $settings = $self->state;
my $width = $self->get_image_width($settings);
my $button_url = $self->data_source->button_url;
return div({-id => 'ruler_track',
-style => "position:relative; z-index: 100; width:${width}px; height:0px; margin-left:auto; margin-right:auto;"},
div({-id => 'ruler_handle',
-style => "width:51px; z-index: 100;"},
div({-id => 'ruler_label',
-onMouseUp => 'toggleRuler(false)',
-style => "height:17px; cursor:pointer; text-align:center; visibility:hidden;"},'') .
div({-id => 'ruler_icon',
-onMouseOver => 'GBubble.showTooltip(event,"'.$self->translate('RULER_TOGGLE_TOOLTIP').'",0)',
-onMouseUp => 'toggleRuler(true)',
-style => "height:17px; cursor:pointer; position:absolute; top:2px; left:3px;"},
img({-src=>"${button_url}/ruler-icon.png",-alt=>'Ruler'}) ) .
div({-id => 'ruler_image',
-style => "background-image: url(${button_url}/ruler.png); cursor:move; background-size: 100%; display:none;"},'')
)) . $ruler_js;
}
sub source_menu {
my $self = shift;
my $globals = $self->globals;
my $username = $self->session->username if $self->session->private;
my $p = eval{$self->plugins->auth_plugin};
my @sources = $globals->data_sources;
my $show_sources = $self->setting('show sources');
$show_sources = 1 unless defined $show_sources; # default to true
@sources = grep {$globals->data_source_show($_,$username,$p)} @sources;
my $sources = $show_sources && @sources > 1;
my %descriptions = map {$_=>$globals->data_source_description($_)} @sources;
@sources = sort {$descriptions{$a} cmp $descriptions{$b}} @sources;
my %sources = map {$_=>1} @sources;
my $current_source = $self->data_source->name;
if (!$sources{$current_source} &&
$globals->data_source_show($current_source,$username,$p) ) { # for regexp-based sources
$descriptions{$current_source} = $self->data_source->description;
@sources = sort {$descriptions{$a} cmp $descriptions{$b}} (@sources,$current_source);
}
return b($self->translate('DATA_SOURCE')).br.
( $sources ?
popup_menu(-name => 'source',
-values => \@sources,
-labels => \%descriptions,
-default => $self->data_source->name,
-onChange => 'this.form.submit()',
)
: $globals->data_source_description($self->session->source)
);
}
# This is the content of the popup balloon that describes the track and gives configuration settings
# This is currently somewhat hacky, hard to extend and needs to be generalized.
# NOTE: to add new configuration rows, the name of the form element must begin with "conf_" and
# the rest must correspond to a valid glyph option.
sub track_config {
my $self = shift;
my $label = shift;
my $revert_to_defaults = shift;
eval 'require Bio::Graphics::Browser2::TrackConfig'
unless Bio::Graphics::Browser2::Render::TrackConfig->can('new');
my $c = Bio::Graphics::Browser2::Render::TrackConfig->new($self);
return $c->config_dialog($label,$revert_to_defaults);
}
sub cit_link {
my $self = shift;
my $label = shift;
return "?display_citation=$label";
}
sub track_citation {
my $self = shift;
my $label = shift;
my $state = $self->state();
my $data_source = $self->data_source();
my $length = $self->thin_segment->length;
my $slabel = $data_source->semantic_label($label,$length);
my $key = $self->label2key($slabel);
# citation info:
my $cit_txt = citation( $data_source, $label, $self->language )
|| $self->tr('NO_TRACK_CITATION') || '';
my $cit_html;
my $cit_link = '';
# For verbose citations, add a link to a new window
if (length $cit_txt > 512) {
$cit_link = "?display_citation=$label";
$cit_link =~ s!gbrowse\?!gbrowse/$state->{source}/\?!;
$cit_link = a(
{
-href => $cit_link,
-target => "citation", #'_NEW',
-onclick => 'GBox.hideTooltip(1)'
},
'Click here to display in new window...');
$cit_link = p($cit_link);
}
$cit_html = div($cit_link||br,$cit_txt);
my $id = $self->tr('TRACK_ID',$label);
my $download = a({-href=>"?l=$label;f=save+datafile"},$self->tr('DOWNLOAD_TRACK_DATA_ALL'));
my $title = div({-style => 'background:gainsboro;padding:5px;font-weight:bold'},$key,br(),$id,br(),$download);
return join '',div({-style=>'text-align:center;font-size:small'},$title,$cit_html);
}
sub download_track_menu {
my $self = shift;
my $track = shift;
my $view_start = shift;
my $view_stop = shift;
my $state = $self->state();
my $data_source = $self->data_source();
my $segment;
if ($track =~ /:overview$/) {
$segment = $self->thin_whole_segment;
} elsif ($track =~ /:region$/) {
$segment = $self->thin_region_segment;
} else {
$segment = $self->thin_segment;
$segment->{start} = $view_start || $segment->{start};
$segment->{stop} = $view_stop || $segment->{stop};
$segment->{end} = $view_stop || $segment->{end};
}
my $seqid = $segment->seq_id;
my $start = $segment->start;
my $end = $segment->end;
my $key = $self->label2key($track);
my $unload = 'window.onbeforeunload=void(0)';
my $byebye = 'Balloon.prototype.hideTooltip(1)';
my $segment_str = segment_str($segment);
my $glyph = $data_source->setting($track=>'glyph') || 'generic';
my @format_options = Bio::Graphics::Browser2::TrackDumper->available_formats($data_source,$track);
my %foptions = map {$_=>1} @format_options;
my $default = $foptions{$state->{preferred_dump_format}||''} ? $state->{preferred_dump_format}
: $glyph =~ /vista/ && $foptions{vista} ? 'vista'
: $foptions{gff3} ? 'gff3'
: $foptions{bed} ? 'bed'
: $foptions{sam} ? 'sam'
: $foptions{vista} ? 'vista'
: 'fasta';
my @radios = radio_group(-name => 'format',
-values => \@format_options,
-default => $default,
-labels => {fasta => 'FASTA',
gff3 => 'GFF3',
genbank => 'Genbank',
vista => 'WIG (peaks+signal)',
vista_wiggle => 'WIG (signal)',
vista_peaks => 'WIG (peaks)',
bed => 'WIG',
sam => 'SAM alignment format'});
my $options = "gbgff=1;l=$track;s=0;f=save+gff3;'+\$('dump_form').serialize()";
my $html = '';
$html .= div({-align=>'center'},
div({-style => 'background:gainsboro;padding:5px;font-weight:bold'},$key).
hr().
start_form({-id=>'dump_form'}).
div($self->tableize(\@radios,3)).
end_form().
hr().
button(-value => $self->translate('DOWNLOAD_TRACK_DATA_REGION',$segment_str),
-onClick => "$unload;window.location='?q=$seqid:$start..$end;$options;$byebye",
),br(),
button(-value => $self->translate('DOWNLOAD_TRACK_DATA_CHROM',$seqid),
-onClick => "$unload;window.location='?q=$seqid;$options;$byebye",
),br(),
button(-value=> $self->translate('DOWNLOAD_TRACK_DATA_ALL'),
-onClick => "$unload;location.href='?$options;$byebye",
)).
button(-style=>"background:pink;float:right",-onClick=>"$byebye",-name=>$self->translate('CANCEL'));
return $html;
}
# this is the content of the popup balloon that describes how to share a track
sub share_track {
my $self = shift;
my $label = shift;
my $state = $self->state();
my $source = $self->data_source;
my $name = $source->name;
(my $lbase = $label) =~ s/:\w+$//;
my $description;
my $labels;
my $usertracks_present;
my @visible
= grep { $state->{features}{$_}{visible} && !/:(overview|region)$/ }
@{ $state->{tracks} };
if ( $label eq 'all' ) {
for my $l (@visible) {
$usertracks_present ||= $source->is_usertrack($l);
}
$labels = join '+', map { CGI::escape($_) } @visible;
$description = 'all selected tracks';
} else {
$description = $self->setting( $label => 'key' )
|| $self->setting( $lbase => 'key')
|| $label;
$usertracks_present ||= $source->is_usertrack($label);
$labels = $label;
}
my $base = url(-full=>1,-path_info=>1);
my $gbgff;
my $segment = $label =~ /:region$/ ? '$region'
:$label =~ /:overview$/ ? '$overview'
:'$segment';
my $session = $self->session;
my $upload_id = $session->uploadsid;
if ( $label =~ /^(http|ftp)/ ) { # reexporting an imported track!
$gbgff = $source->setting($label=>'remote feature');
$gbgff ||= $source->setting($lbase=>'remote feature');
} else {
$gbgff = $base;
$gbgff .= "?gbgff=1;q=$segment;t=$labels;s=1;format=gff3";
$gbgff .= ";uuid=$upload_id" if $usertracks_present;
}
my $das_types = join( ';',
map { "type=" . CGI::escape($_) }
grep { length $_ > 0 }
map { shellwords( $self->setting( $_ => 'feature' ) ) }
grep { $self->setting( $_ => 'das category' ) }
$label eq 'all'
? @visible
: $label );
my $das = url( -full => 1, -path_info => 1 );
$das =~ s/gbrowse/das/;
$das =~ s/$name/$name|$label/ if $label ne 'all';
$das .= "features";
$das .= "?$das_types";
my $return_html = start_html();
$return_html .= h1( $self->translate('SHARE', $description));
my $tsize = 72;
if ($source->is_usertrack($label)) {
my $usertracks = $self->user_tracks;
my $file = $usertracks->get_track_upload_id($label);
my $permissions = $usertracks->permissions($file);
my $is_mine = $usertracks->is_mine($file);
if ($is_mine || $permissions eq "public" || $permissions eq "casual") {
my $permissions_changed;
if ($permissions !~ /(casual|public)/) {
$usertracks->permissions($file, "casual");
$permissions_changed = 1;
}
if ($is_mine) {
$return_html .= p(($permissions_changed?
$self->translate('SHARE_CUSTOM_TRACK_CHANGED', "casual")
: $self->translate('SHARE_CUSTOM_TRACK_NO_CHANGE', $permissions))
. $self->translate('SHARE_INSTRUCTIONS_BOOKMARK'));
$return_html .= p($self->translate('OTHER_SHARE_METHODS'));
} elsif ($permissions =~ /(casual|public)/) {
$return_html .= p($self->translate('SHARE_SHARED_TRACK', $permissions)
. $self->translate('SHARE_INSTRUCTIONS_BOOKMARK'));
}
$return_html .= textfield(
-style => 'background-color: wheat',
-readonly => 1,
-size => $tsize,
-value => $usertracks->sharing_link($file),
-onClick => 'this.select()',
).br();
} else {
$return_html .= p($self->translate('CANT_SHARE'));
}
} else {
if ($label ne 'all' && $label !~ /^(http|ftp)/) {
my $shared = "$base?label=$label";
$return_html .= p(
$self->translate('SHARE_INSTRUCTIONS_BOOKMARK'),br(),
textfield(
-style => 'background-color: wheat',
-readonly => 1,
-size => $tsize,
-value => $shared,
-onClick => 'this.select()'
)
)
}
$return_html .=
p(
$self->translate(
$label eq 'all'
? 'SHARE_INSTRUCTIONS_ALL_TRACKS'
: 'SHARE_INSTRUCTIONS_ONE_TRACK'
).br().
textfield(
-style => 'background-color: wheat',
-readonly => 1,
-size => $tsize,
-value => $gbgff,
-onClick => 'this.select()'));
if ($das_types) {
$return_html .= p(
$self->translate(
$label eq 'all'
? 'SHARE_DAS_INSTRUCTIONS_ALL_TRACKS'
: 'SHARE_DAS_INSTRUCTIONS_ONE_TRACK'
)
)
. p( textfield(
-style => 'background-color: wheat',
-readonly => 1,
-size => $tsize,
-value => $das,
-onFocus => 'this.select()',
-onSelect => 'this.select()')
);
}
}
$return_html .=
button(
-name => $self->translate('OK'),
-onClick => 'Balloon.prototype.hideTooltip(1)'
);
$return_html .= end_html();
return div({-style=>'width:600px'},$return_html);
}
################### various utilities ###################
sub html_frag {
my $self = shift;
my $fragname = shift;
my $a = $self->data_source->global_setting($fragname);
return $a->(@_) if ref $a eq 'CODE';
return $a || '';
}
############################## toggle code ########################
sub toggle {
my $self = shift;
my %args = ();
if (ref $_[0]) {
%args = %{shift()};
}
my $title = shift;
my @body = @_;
my $page_settings = $self->state;
my $id = "\L${title}_panel\E";
my $label = $self->translate($title) || '';
my $state = $self->data_source->section_setting($title) or return '';
return '' if $state eq 'off';
my $visible = exists $page_settings->{section_visible}{$id} ?
$page_settings->{section_visible}{$id} : $state eq 'open';
return $self->toggle_section({on=>$visible,%args},
$id,
b($label),
@body);
}
sub toggle_section {
my $self = shift;
my %config = ref $_[0] eq 'HASH' ? %{shift()} : ();
my ($name,$section_title,@section_body) = @_;
my $visible = $config{on};
my $buttons = $self->data_source->button_url;
my $plus = "$buttons/plus.png";
my $minus = "$buttons/minus.png";
my $break = div({-id=>"${name}_break",
-style=>$visible ? 'display:none' : 'display:block'
},' ');
my $show_ctl = div({-id=>"${name}_show",
-class=>'ctl_hidden',
-style=>$visible ? 'display:none' : 'display:inline',
-onClick=>"visibility('$name',1)"
},
img({-src=>$plus,-alt=>'+'}).' '.span({-class=>'tctl'},$section_title));
my $hide_ctl = div({-id=>"${name}_hide",
-class=>'ctl_visible',
-style=>$visible ? 'display:inline' : 'display:none',
-onClick=>"visibility('$name',0)"
},
img({-src=>$minus,-alt=>'-'}).' '.span({-class=>'tctl',-id=>"${name}_title"},$section_title));
my $content = div({-id => $name,
-style=>$visible ? 'display:inline' : 'display:none',
-class => 'el_visible'},
@section_body);
my @class = (-class=>'toggleable');
my @result = $config{nodiv} ? (div({-style=>'float:left',@class},
$show_ctl.$hide_ctl),$content)
:$config{tight}? (div({-style=>'float:left;position:absolute;z-index:10',@class},
$show_ctl.$hide_ctl).$break,$content)
: div({@class},$show_ctl.$hide_ctl,$content);
return wantarray ? @result : "@result";
}
sub can_generate_pdf {
my $self = shift;
my $source = $self->data_source;
return $CAN_PDF if defined $CAN_PDF;
return $CAN_PDF = $source->global_setting('generate pdf')
if defined $source->global_setting('generate pdf');
return $CAN_PDF=0 unless `which inkscape`;
# see whether we have the needed .inkscape and .gnome2 directories
my $home = (getpwuid($<))[7];
my $user = (getpwuid($<))[0];
my $inkscape_dir = File::Spec->catfile($home,'.config','inkscape');
my $gnome2_dir = File::Spec->catfile($home,'.gnome2');
if (-e $inkscape_dir && -w $inkscape_dir
&& -e $gnome2_dir && -w $gnome2_dir) {
return $CAN_PDF=1;
} else {
print STDERR
join(' ',
qq(GBROWSE NOTICE: To enable PDF generation, please enter the directory "$home"),
qq(and run the commands:),
qq("sudo mkdir -p .config/inkscape .gnome2"),
qq(and "sudo chown $user .config/inkscape .gnome2". ),
qq(To turn off this message add "generate pdf = 0"),
qq(to the [GENERAL] section of your GBrowse.conf configuration file.)
);
return $CAN_PDF=0;
}
}
# Truncated version (of track_config) for displaying citation only:
sub display_citation {
my $self = shift;
my $label = shift;
my $state = $self->state();
my $data_source = $self->data_source();
my $segment = $self->thin_segment;
my $length = $segment ? $segment->length : 0;
my $slabel = $data_source->semantic_label($label,$length);
my $key = $self->label2key($slabel);
my @stylesheets;
my @style = shellwords($self->setting('stylesheet') || '/gbrowse2/gbrowse.css');
for my $s (@style) {
my ($url,$media) = $s =~ /^([^\(]+)(?:\((.+)\))?/;
$media ||= 'all';
push @stylesheets, CGI::Link({-rel=>'stylesheet',
-type=>'text/css',
-href=>$self->globals->resolve_path($url,'url'),
-media=>$media});
}
my $return_html = start_html(-title => $key, -head => \@stylesheets);
my $cit_txt = citation( $data_source, $label, $self->language ) || $self->translate('NO_CITATION');
if (my ($lim) = $slabel =~ /\:(\d+)$/) {
$key .= " (at >$lim bp)";
}
my $citation = div({-class => 'searchbody', -style => 'padding:10px;width:70%'}, h4($key), $cit_txt);
$return_html
.= table( TR( td( { -valign => 'top' }, $citation ) ) );
$return_html .= end_html();
return $return_html;
}
sub format_autocomplete {
my $self = shift;
my $features = shift;
my $partial = shift;
my %names;
for my $f (@$features) {
my ($name) = grep {/$partial/i} ($f->display_name,eval{$f->aliases});
$names{$name}++;
}
my $html = "<ul>\n";
for my $n (sort keys %names) {
$n =~ s/($partial)/<b>$1<\/b>/i;
$html .= "<li>$n</li>\n";
}
$html .= "</ul>\n";
return $html;
}
sub format_upload_autocomplete {
my $self = shift;
my $matches = shift;
my $partial = shift;
my %names;
for my $f (@$matches) {
my ($name) = grep {/$partial/i} $f;
$names{$name}++;
}
my $html = "<ul>\n";
for my $n (sort keys %names) {
$n =~ s/($partial)/<b>$1<\/b>/i;
$html .= "<li>$n</li>\n";
}
$html .= "</ul>\n";
return $html;
}
1;