/* Trackster 2010, James Taylor, Kanwei Li */ var DENSITY = 200, FEATURE_LEVELS = 10, MAX_FEATURE_DEPTH = 50, CONNECTOR_COLOR = "#ccc", DATA_ERROR = "There was an error in indexing this dataset.", DATA_NOCONVERTER = "A converter for this dataset is not installed. Please check your datatypes_conf.xml file.", DATA_NONE = "No data for this chrom/contig.", DATA_PENDING = "Currently indexing... please wait", DATA_LOADING = "Loading data...", FILTERABLE_CLASS = "filterable", CACHED_TILES_FEATURE = 10, CACHED_TILES_LINE = 30, CACHED_DATA = 5, CONTEXT = $("").get(0).getContext("2d"), PX_PER_CHAR = CONTEXT.measureText("A").width, RIGHT_STRAND, LEFT_STRAND; var right_img = new Image(); right_img.src = image_path + "/visualization/strand_right.png"; right_img.onload = function() { RIGHT_STRAND = CONTEXT.createPattern(right_img, "repeat"); }; var left_img = new Image(); left_img.src = image_path + "/visualization/strand_left.png"; left_img.onload = function() { LEFT_STRAND = CONTEXT.createPattern(left_img, "repeat"); }; var right_img_inv = new Image(); right_img_inv.src = image_path + "/visualization/strand_right_inv.png"; right_img_inv.onload = function() { RIGHT_STRAND_INV = CONTEXT.createPattern(right_img_inv, "repeat"); }; var left_img_inv = new Image(); left_img_inv.src = image_path + "/visualization/strand_left_inv.png"; left_img_inv.onload = function() { LEFT_STRAND_INV = CONTEXT.createPattern(left_img_inv, "repeat"); }; function round_1000(num) { return Math.round(num * 1000) / 1000; } var Cache = function( num_elements ) { this.num_elements = num_elements; this.clear(); }; $.extend( Cache.prototype, { get: function( key ) { var index = this.key_ary.indexOf(key); if (index != -1) { // Move to the end this.key_ary.splice(index, 1); this.key_ary.push(key); } return this.obj_cache[key]; }, set: function( key, value ) { if (!this.obj_cache[key]) { if (this.key_ary.length >= this.num_elements) { // Remove first element var deleted_key = this.key_ary.shift(); delete this.obj_cache[deleted_key]; } this.key_ary.push(key); } this.obj_cache[key] = value; return value; }, clear: function() { this.obj_cache = {}; this.key_ary = []; } }); var View = function( container, title, vis_id, dbkey ) { this.container = container; this.vis_id = vis_id; this.dbkey = dbkey; this.title = title; this.tracks = []; this.label_tracks = []; this.max_low = 0; this.max_high = 0; this.num_tracks = 0; this.track_id_counter = 0; this.zoom_factor = 3; this.min_separation = 30; this.has_changes = false; this.init(); this.reset(); }; $.extend( View.prototype, { init: function() { // Create DOM elements var parent_element = this.container, view = this; this.top_labeltrack = $("
").addClass("top-labeltrack").appendTo(parent_element); this.content_div = $("").addClass("content").css("position", "relative").appendTo(parent_element); this.viewport_container = $("").addClass("viewport-container").addClass("viewport-container").appendTo(this.content_div); this.intro_div = $("").addClass("intro").text("Select a chrom from the dropdown below").hide(); // Future overlay this.nav_container = $("").addClass("nav-container").appendTo(parent_element); this.nav_labeltrack = $("").addClass("nav-labeltrack").appendTo(this.nav_container); this.nav = $("").addClass("nav").appendTo(this.nav_container); this.overview = $("").addClass("overview").appendTo(this.nav); this.overview_viewport = $("").addClass("overview-viewport").appendTo(this.overview); this.overview_close = $("Close Overview").addClass("overview-close").hide().appendTo(this.overview_viewport); this.overview_highlight = $("").addClass("overview-highlight").hide().appendTo(this.overview_viewport); this.overview_box_background = $("").addClass("overview-boxback").appendTo(this.overview_viewport); this.overview_box = $("").addClass("overview-box").appendTo(this.overview_viewport); this.default_overview_height = this.overview_box.height(); this.nav_controls = $("").addClass("nav-controls").appendTo(this.nav); this.chrom_form = $("").attr("action", function() { void(0); } ).appendTo(this.nav_controls); this.chrom_select = $("").attr({ "name": "chrom"}).css("width", "15em").addClass("no-autocomplete").append("").appendTo(this.chrom_form); var submit_nav = function(e) { if (e.type === "focusout" || (e.keyCode || e.which) === 13 || (e.keyCode || e.which) === 27 ) { if ((e.keyCode || e.which) !== 27) { // Not escape key view.go_to( $(this).val() ); } $(this).hide(); view.location_span.show(); view.chrom_select.show(); return false; } }; this.nav_input = $("").addClass("nav-input").hide().bind("keypress focusout", submit_nav).appendTo(this.chrom_form); this.location_span = $("").addClass("location").appendTo(this.chrom_form); this.location_span.bind("click", function() { view.location_span.hide(); view.chrom_select.hide(); view.nav_input.css("display", "inline-block"); view.nav_input.select(); view.nav_input.focus(); }); if (this.vis_id !== undefined) { this.hidden_input = $("").attr("type", "hidden").val(this.vis_id).appendTo(this.chrom_form); } this.zo_link = $("").click(function() { view.zoom_out(); view.redraw() }).html('
').appendTo(this.chrom_form);
this.zi_link = $("").click(function() { view.zoom_in(); view.redraw() }).html('
').appendTo(this.chrom_form);
$.ajax({
url: chrom_url,
data: (this.vis_id !== undefined ? { vis_id: this.vis_id } : { dbkey: this.dbkey }),
dataType: "json",
success: function ( result ) {
if (result['reference']) {
view.add_label_track( new ReferenceTrack(view) );
}
view.chrom_data = result['chrom_info'];
var chrom_options = '';
for (i in view.chrom_data) {
var chrom = view.chrom_data[i]['chrom'];
chrom_options += '';
}
view.chrom_select.html(chrom_options);
view.intro_div.show();
view.chrom_select.bind("change", function() {
view.change_chrom(view.chrom_select.val());
});
},
error: function() {
alert( "Could not load chroms for this dbkey:", view.dbkey );
}
});
/*
this.content_div.bind("mousewheel", function( e, delta ) {
if (Math.abs(delta) < 0.5) {
return;
}
if (delta > 0) {
view.zoom_in(e.pageX, this.viewport_container);
} else {
view.zoom_out();
}
e.preventDefault();
});
*/
this.content_div.bind("dblclick", function( e ) {
view.zoom_in(e.pageX, this.viewport_container);
});
// To let the overview box be draggable
this.overview_box.bind("dragstart", function( e ) {
this.current_x = e.offsetX;
}).bind("drag", function( e ) {
var delta = e.offsetX - this.current_x;
this.current_x = e.offsetX;
var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.max_high - view.max_low) );
view.move_delta(-delta_chrom);
});
this.overview_close.bind("click", function() {
for (var track_id in view.tracks) {
view.tracks[track_id].is_overview = false;
}
$(this).siblings().filter("canvas").remove();
$(this).parent().css("height", view.overview_box.height());
view.overview_highlight.hide();
$(this).hide();
});
this.viewport_container.bind( "dragstart", function( e ) {
this.original_low = view.low;
this.current_height = e.clientY;
this.current_x = e.offsetX;
this.enable_pan = (e.clientX < view.viewport_container.width() - 16) ? true : false; // Fix webkit scrollbar
}).bind( "drag", function( e ) {
if (!this.enable_pan || this.in_reordering) { return; }
var container = $(this);
var delta = e.offsetX - this.current_x;
var new_scroll = container.scrollTop() - (e.clientY - this.current_height);
container.scrollTop(new_scroll);
this.current_height = e.clientY;
this.current_x = e.offsetX;
var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low));
view.move_delta(delta_chrom);
});
this.top_labeltrack.bind( "dragstart", function(e) {
this.drag_origin_x = e.clientX;
this.drag_origin_pos = e.clientX / view.viewport_container.width() * (view.high - view.low) + view.low;
this.drag_div = $("").css( {
"height": view.content_div.height()+30, "top": "0px", "position": "absolute",
"background-color": "#cfc", "border": "1px solid #6a6", "opacity": 0.5, "z-index": 1000
} ).appendTo( $(this) );
}).bind( "drag", function(e) {
var min = Math.min(e.clientX, this.drag_origin_x) - view.container.offset().left,
max = Math.max(e.clientX, this.drag_origin_x) - view.container.offset().left,
span = (view.high - view.low),
width = view.viewport_container.width();
view.update_location(Math.round(min / width * span) + view.low, Math.round(max / width * span) + view.low);
this.drag_div.css( { "left": min + "px", "width": (max - min) + "px" } );
}).bind( "dragend", function(e) {
var min = Math.min(e.clientX, this.drag_origin_x),
max = Math.max(e.clientX, this.drag_origin_x),
span = (view.high - view.low),
width = view.viewport_container.width(),
old_low = view.low;
view.low = Math.round(min / width * span) + old_low;
view.high = Math.round(max / width * span) + old_low;
this.drag_div.remove();
view.redraw();
});
this.add_label_track( new LabelTrack( this, this.top_labeltrack ) );
this.add_label_track( new LabelTrack( this, this.nav_labeltrack ) );
},
update_location: function(low, high) {
this.location_span.text( commatize(low) + ' - ' + commatize(high) );
this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
},
change_chrom: function(chrom, low, high) {
var view = this;
var found = $.grep(view.chrom_data, function(v, i) {
return v.chrom === chrom;
})[0];
if (found === undefined) {
// Invalid chrom
return;
}
if (chrom !== view.chrom) {
view.chrom = chrom;
if (!view.chrom) {
// No chrom selected
view.intro_div.show();
} else {
view.intro_div.hide();
}
view.chrom_select.val(view.chrom);
view.max_high = found.len;
view.reset();
view.redraw(true);
for (var track_id in view.tracks) {
var track = view.tracks[track_id];
if (track.init) {
track.init();
}
}
}
if (low !== undefined && high !== undefined) {
view.low = Math.max(low, 0);
view.high = Math.min(high, view.max_high);
}
view.reset_overview();
view.redraw();
},
go_to: function(str) {
var view = this,
chrom_pos = str.split(":"),
chrom = chrom_pos[0],
pos = chrom_pos[1];
if (pos !== undefined) {
try {
var pos_split = pos.split("-"),
new_low = parseInt(pos_split[0].replace(/,/g, "")),
new_high = parseInt(pos_split[1].replace(/,/g, ""));
} catch (e) {
return false;
}
}
view.change_chrom(chrom, new_low, new_high);
},
move_delta: function(delta_chrom) {
var view = this;
var current_chrom_span = view.high - view.low;
// Check for left and right boundaries
if (view.low - delta_chrom < view.max_low) {
view.low = view.max_low;
view.high = view.max_low + current_chrom_span;
} else if (view.high - delta_chrom > view.max_high) {
view.high = view.max_high;
view.low = view.max_high - current_chrom_span;
} else {
view.high -= delta_chrom;
view.low -= delta_chrom;
}
view.redraw();
},
add_track: function(track) {
track.view = this;
track.track_id = this.track_id_counter;
this.tracks.push(track);
if (track.init) { track.init(); }
track.container_div.attr('id', 'track_' + track.track_id);
this.track_id_counter += 1;
this.num_tracks += 1;
},
add_label_track: function (label_track) {
label_track.view = this;
this.label_tracks.push(label_track);
},
remove_track: function(track) {
this.has_changes = true;
track.container_div.fadeOut('slow', function() { $(this).remove(); });
delete this.tracks[this.tracks.indexOf(track)];
this.num_tracks -= 1;
},/* No longer needed as config is done inline, one track at a time
update_options: function() {
this.has_changes = true;
var sorted = $("ul#sortable-ul").sortable('toArray');
for (var id_i in sorted) {
var id = sorted[id_i].split("_li")[0].split("track_")[1];
this.viewport_container.append( $("#track_" + id) );
}
for (var track_id in view.tracks) {
var track = view.tracks[track_id];
if (track && track.update_options) {
track.update_options(track_id);
}
}
},*/
reset: function() {
this.low = this.max_low;
this.high = this.max_high;
this.viewport_container.find(".yaxislabel").remove();
},
redraw: function(nodraw) {
var span = this.high - this.low,
low = this.low,
high = this.high;
if (low < this.max_low) {
low = this.max_low;
}
if (high > this.max_high) {
high = this.max_high;
}
if (this.high !== 0 && span < this.min_separation) {
high = low + this.min_separation;
}
this.low = Math.floor(low);
this.high = Math.ceil(high);
// 10^log10(range / DENSITY) Close approximation for browser window, assuming DENSITY = window width
this.resolution = Math.pow( 10, Math.ceil( Math.log( (this.high - this.low) / 200 ) / Math.LN10 ) );
this.zoom_res = Math.pow( FEATURE_LEVELS, Math.max(0,Math.ceil( Math.log( this.resolution, FEATURE_LEVELS ) / Math.log(FEATURE_LEVELS) )));
// Overview
var left_px = this.low / (this.max_high - this.max_low) * this.overview_viewport.width();
var width_px = (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width();
var min_width_px = 13;
this.overview_box.css({ left: left_px, width: Math.max(min_width_px, width_px) }).show();
if (width_px < min_width_px) {
this.overview_box.css("left", left_px - (min_width_px - width_px)/2);
}
if (this.overview_highlight) {
this.overview_highlight.css({ left: left_px, width: width_px });
}
this.update_location(this.low, this.high);
if (!nodraw) {
for (var i = 0, len = this.tracks.length; i < len; i++) {
if (this.tracks[i] && this.tracks[i].enabled) {
this.tracks[i].draw();
}
}
for (var i = 0, len = this.label_tracks.length; i < len; i++) {
this.label_tracks[i].draw();
}
}
},
zoom_in: function (point, container) {
if (this.max_high === 0 || this.high - this.low < this.min_separation) {
return;
}
var span = this.high - this.low,
cur_center = span / 2 + this.low,
new_half = (span / this.zoom_factor) / 2;
if (point) {
cur_center = point / this.viewport_container.width() * (this.high - this.low) + this.low;
}
this.low = Math.round(cur_center - new_half);
this.high = Math.round(cur_center + new_half);
this.redraw();
},
zoom_out: function () {
if (this.max_high === 0) {
return;
}
var span = this.high - this.low,
cur_center = span / 2 + this.low,
new_half = (span * this.zoom_factor) / 2;
this.low = Math.round(cur_center - new_half);
this.high = Math.round(cur_center + new_half);
this.redraw();
},
reset_overview: function() {
this.overview_viewport.find("canvas").remove();
this.overview_viewport.height(this.default_overview_height);
this.overview_box.height(this.default_overview_height);
this.overview_close.hide();
this.overview_highlight.hide();
}
});
// Generic filter.
var Filter = function( name, index, value ) {
this.name = name;
this.index = index;
this.value = value;
};
// Number filter for a track.
var NumberFilter = function( name, index ) {
this.name = name;
// Index into payload to filter.
this.index = index;
// Filter low/high. These values are used to filter elements.
this.low = -Number.MAX_VALUE;
this.high = Number.MAX_VALUE;
// Slide min/max. These values are used to set/update slider.
this.slider_min = Number.MAX_VALUE;
this.slider_max = -Number.MAX_VALUE;
// UI Slider element and label that is associated with filter.
this.slider = null;
this.slider_label = null;
};
$.extend( NumberFilter.prototype, {
// Returns true if filter can be applied to element.
applies_to: function( element ) {
if ( element.length > this.index )
return true;
return false;
},
// Returns true iff element is in [low, high]; range is inclusive.
keep: function( element ) {
if ( !this.applies_to( element ) )
// No element to filter on.
return true;
return ( element[this.index] >= this.low && element[this.index] <= this.high );
},
// Update filter's min and max values based on element's values.
update_attrs: function( element ) {
var updated = false;
if ( !this.applies_to( element ) ) {
return updated;
}
// Update filter's min, max based on element values.
if ( element[this.index] < this.slider_min ) {
this.slider_min = element[this.index];
updated = true;
}
if ( element[this.index] > this.slider_max ) {
this.slider_max = element[this.index];
updated = false;
}
return updated;
},
// Update filter's slider.
update_ui_elt: function () {
var
slider_min = this.slider.slider( "option", "min" ),
slider_max = this.slider.slider( "option", "max" );
if (this.slider_min < slider_min || this.slider_max > slider_max) {
// Need to update slider.
this.slider.slider( "option", "min", this.slider_min );
this.slider.slider( "option", "max", this.slider_max );
// Refresh slider:
// TODO: do we want to keep current values or reset to min/max?
// Currently we reset values:
this.slider.slider( "option", "values", [ this.slider_min, this.slider_max ] );
// To use the current values.
//var values = this.slider.slider( "option", "values" );
//this.slider.slider( "option", "values", values );
}
}
});
// Parse filters dict and return filters.
var get_filters = function( filters_dict ) {
var filters = []
for (var i = 0; i < filters_dict.length; i++) {
var filter_dict = filters_dict[i];
var name = filter_dict['name'], type = filter_dict['type'], index = filter_dict['index'];
if ( type == 'int' || type == 'float' ) {
filters[i] = new NumberFilter( name, index );
}
else
filters[i] = new Filter( name, index, type );
}
return filters;
};
var Track = function (name, view, parent_element, filters) {
this.name = name;
this.view = view;
this.parent_element = parent_element;
this.filters = (filters !== undefined ? get_filters( filters ) : []);
this.init_global();
};
$.extend( Track.prototype, {
init_global: function () {
this.container_div = $("").addClass('track').css("position", "relative");
if (!this.hidden) {
this.header_div = $("").appendTo(this.container_div);
if (this.view.editor) { this.drag_div = $("").appendTo(this.header_div); }
this.name_div = $("").appendTo(this.header_div);
this.name_div.text(this.name);
this.name_div.attr( "id", this.name.replace(/\s+/g,'-').replace(/[^a-zA-Z0-9\-]/g,'').toLowerCase() );
}
// Create track filtering div.
this.filtering_div = $("