/* 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 = $("
").appendTo(this.container_div);
this.filtering_div.hide();
// Dragging is disabled on div so that actions on slider do not impact viz.
this.filtering_div.bind( "drag", function(e) {
e.stopPropagation();
});
var filters_table = $("
").appendTo(this.filtering_div);
var track = this;
for (var i = 0; i < this.filters.length; i++) {
var filter = this.filters[i];
var table_row = $("
").appendTo(filters_table);
var filter_th = $("
").appendTo(table_row);
var name_span = $("").appendTo(filter_th);
name_span.text(filter.name + " "); // Extra spacing to separate name and values
var values_span = $("").appendTo(filter_th);
// TODO: generate custom interaction elements based on filter type.
var table_data = $("