/* 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 = $("").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 = $("