/static/scripts/trackster.js
JavaScript | 1716 lines | 1167 code | 116 blank | 433 comment | 156 complexity | 4ba25aa395361e2eecd6279cebcda7ab MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- /* Trackster
- 2010-2011: James Taylor, Kanwei Li, Jeremy Goecks
- */
- var class_module = function(require, exports) {
-
- // Module is a placeholder for a more complete inheritence approach
-
- /** Simple extend function for inheritence */
- var extend = function() {
- var target = arguments[0];
- for ( var i = 1; i < arguments.length; i++ ) {
- var other = arguments[i];
- for ( key in other ) {
- target[key] = other[key];
- }
- }
- return target;
- };
- exports.extend = extend;
- // end class_module encapsulation
- };
- /**
- * Find browser's requestAnimationFrame method or fallback on a setTimeout
- */
- var requestAnimationFrame = (function(){
- return window.requestAnimationFrame ||
- window.webkitRequestAnimationFrame ||
- window.mozRequestAnimationFrame ||
- window.oRequestAnimationFrame ||
- window.msRequestAnimationFrame ||
- function( callback, element ) {
- window.setTimeout(callback, 1000 / 60);
- };
- })();
- /**
- * Compute the type of overlap between two regions. They are assumed to be on the same chrom/contig.
- * The overlap is computed relative to the second region; hence, OVERLAP_START indicates that the first
- * region overlaps the start (but not the end) of the second region.
- */
- var BEFORE = 1001, CONTAINS = 1002, OVERLAP_START = 1003, OVERLAP_END = 1004, CONTAINED_BY = 1005, AFTER = 1006;
- var compute_overlap = function(first_region, second_region) {
- var
- first_start = first_region[0], first_end = first_region[1],
- second_start = second_region[0], second_end = second_region[1],
- overlap;
- if (first_start < second_start) {
- if (first_end < second_start) {
- overlap = BEFORE;
- }
- else if (first_end <= second_end) {
- overlap = OVERLAP_START;
- }
- else { // first_end > second_end
- overlap = CONTAINS;
- }
- }
- else { // first_start >= second_start
- if (first_start > second_end) {
- overlap = AFTER;
- }
- else if (first_end <= second_end) {
- overlap = CONTAINED_BY;
- }
- else {
- overlap = OVERLAP_END;
- }
- }
-
- return overlap;
- };
- /**
- * Returns true if regions overlap.
- */
- var is_overlap = function(first_region, second_region) {
- var overlap = compute_overlap(first_region, second_region);
- return (overlap !== BEFORE && overlap !== AFTER);
- };
- // Encapsulate -- anything to be availabe outside this block is added to exports
- var trackster_module = function(require, exports) {
- var extend = require('class').extend,
- slotting = require('slotting'),
- painters = require('painters');
-
-
- // ---- Canvas management and extensions ----
- /**
- * Canvas manager is used to create canvases, for browsers, this deals with
- * backward comparibility using excanvas, as well as providing a pattern cache
- */
- var CanvasManager = function( document, default_font ) {
- this.document = document;
- this.default_font = default_font !== undefined ? default_font : "9px Monaco, Lucida Console, monospace";
-
- this.dummy_canvas = this.new_canvas();
- this.dummy_context = this.dummy_canvas.getContext('2d');
- this.dummy_context.font = this.default_font;
-
- this.char_width_px = this.dummy_context.measureText("A").width;
-
- this.patterns = {};
- // FIXME: move somewhere to make this more general
- this.load_pattern( 'right_strand', "/visualization/strand_right.png" );
- this.load_pattern( 'left_strand', "/visualization/strand_left.png" );
- this.load_pattern( 'right_strand_inv', "/visualization/strand_right_inv.png" );
- this.load_pattern( 'left_strand_inv', "/visualization/strand_left_inv.png" );
- }
- extend( CanvasManager.prototype, {
- load_pattern: function( key, path ) {
- var patterns = this.patterns,
- dummy_context = this.dummy_context,
- image = new Image();
- // FIXME: where does image_path come from? not in browser.mako...
- image.src = image_path + path;
- image.onload = function() {
- patterns[key] = dummy_context.createPattern( image, "repeat" );
- }
- },
- get_pattern: function( key ) {
- return this.patterns[key];
- },
- new_canvas: function() {
- var canvas = this.document.createElement("canvas");
- // If using excanvas in IE, we need to explicately attach the canvas
- // methods to the DOM element
- if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); }
- // Keep a reference back to the manager
- canvas.manager = this;
- return canvas;
- }
- });
- // ---- Web UI specific utilities ----
- /**
- * Dictionary of HTML element-JavaScript object relationships.
- */
- // TODO: probably should separate moveable objects from containers.
- var html_elt_js_obj_dict = {};
- /**
- * Designates an HTML as a container.
- */
- var is_container = function(element, obj) {
- html_elt_js_obj_dict[element.attr("id")] = obj;
- };
- /**
- * Make `element` moveable within parent and sibling elements by dragging `handle` (a selector).
- * Function manages JS objects, containers as well.
- *
- * @param element HTML element to make moveable
- * @param handle_class classname that denotes HTML element to be used as handle
- * @param container_selector selector used to identify possible containers for this element
- * @param element_js_obj JavaScript object associated with element; used
- */
- var moveable = function(element, handle_class, container_selector, element_js_obj) {
- // HACK: set default value for container selector.
- container_selector = ".group";
- var css_border_props = {};
- // Register element with its object.
- html_elt_js_obj_dict[element.attr("id")] = element_js_obj;
-
- // Need to provide selector for handle, not class.
- element.bind( "drag", { handle: "." + handle_class, relative: true }, function ( e, d ) {
- var element = $(this);
- var
- parent = $(this).parent(),
- children = parent.children(),
- this_obj = html_elt_js_obj_dict[$(this).attr("id")],
- child,
- container,
- top,
- bottom,
- i;
-
- //
- // Enable three types of dragging: (a) out of container; (b) into container;
- // (c) sibling movement, aka sorting. Handle in this order for simplicity.
- //
-
- // Handle dragging out of container.
- container = $(this).parents(container_selector);
- if (container.length !== 0) {
- top = container.position().top;
- bottom = top + container.outerHeight();
- if (d.offsetY < top) {
- // Moving above container.
- $(this).insertBefore(container);
- var cur_container = html_elt_js_obj_dict[container.attr("id")];
- cur_container.remove_drawable(this_obj);
- cur_container.container.add_drawable_before(this_obj, cur_container);
- return;
- }
- else if (d.offsetY > bottom) {
- // Moving below container.
- $(this).insertAfter(container);
- var cur_container = html_elt_js_obj_dict[container.attr("id")];
- cur_container.remove_drawable(this_obj);
- cur_container.container.add_drawable(this_obj);
- return;
- }
- }
-
- // Handle dragging into container. Child is appended to container's content_div.
- container = null;
- for ( i = 0; i < children.length; i++ ) {
- child = $(children.get(i));
- top = child.position().top;
- bottom = top + child.outerHeight();
- // Dragging into container if child is a container and offset is inside container.
- if ( child.is(container_selector) && this !== child.get(0) &&
- d.offsetY >= top && d.offsetY <= bottom ) {
- // Append/prepend based on where offsetY is closest to and return.
- if (d.offsetY - top < bottom - d.offsetY) {
- child.find(".content-div").prepend(this);
- }
- else {
- child.find(".content-div").append(this);
- }
- // Update containers. Object may not have container if it is being moved quickly.
- if (this_obj.container) {
- this_obj.container.remove_drawable(this_obj);
- }
- html_elt_js_obj_dict[child.attr("id")].add_drawable(this_obj);
- return;
- }
- }
- // Handle sibling movement, aka sorting.
-
- // Determine new position
- for ( i = 0; i < children.length; i++ ) {
- if ( d.offsetY < $(children.get(i)).position().top ) {
- break;
- }
- }
- // If not already in the right place, move. Need
- // to handle the end specially since we don't have
- // insert at index
- if ( i === children.length ) {
- if ( this !== children.get(i - 1) ) {
- parent.append(this);
- html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, i);
- }
- }
- else if ( this !== children.get(i) ) {
- $(this).insertBefore( children.get(i) );
- // Need to adjust insert position if moving down because move is changing
- // indices of all list items.
- html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, (d.deltaY > 0 ? i-1 : i) );
- }
- }).bind("dragstart", function() {
- css_border_props["border-top"] = element.css("border-top");
- css_border_props["border-bottom"] = element.css("border-bottom");
- $(this).css({
- "border-top": "1px solid blue",
- "border-bottom": "1px solid blue"
- });
- }).bind("dragend", function() {
- $(this).css(css_border_props);
- });
- };
- // TODO: do we need to export?
- exports.moveable = moveable;
- /**
- * Init constants & functions used throughout trackster.
- */
- var
- // Minimum height of a track's contents; this must correspond to the .track-content's minimum height.
- MIN_TRACK_HEIGHT = 16,
- // FIXME: font size may not be static
- CHAR_HEIGHT_PX = 9,
- // Padding at the top of tracks for error messages
- ERROR_PADDING = 20,
- SUMMARY_TREE_TOP_PADDING = CHAR_HEIGHT_PX + 2,
- // Maximum number of rows un a slotted track
- MAX_FEATURE_DEPTH = 100,
- // Minimum width for window for squish to be used.
- MIN_SQUISH_VIEW_WIDTH = 12000,
-
- // Other constants.
- DENSITY = 200,
- RESOLUTION = 5,
- FEATURE_LEVELS = 10,
- DEFAULT_DATA_QUERY_WAIT = 5000,
- // Maximum number of chromosomes that are selectable at any one time.
- MAX_CHROMS_SELECTABLE = 100,
- 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_CANNOT_RUN_TOOL = "Tool cannot be rerun: ",
- DATA_LOADING = "Loading data...",
- DATA_OK = "Ready for display",
- CACHED_TILES_FEATURE = 10,
- CACHED_TILES_LINE = 5,
- CACHED_DATA = 5;
- function round_1000(num) {
- return Math.round(num * 1000) / 1000;
- };
- /**
- * Generic cache that handles key/value pairs.
- */
- 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) {
- if (this.obj_cache[key].stale) {
- // Object is stale, so remove key and object.
- this.key_ary.splice(index, 1);
- delete this.obj_cache[key];
- }
- else {
- this.move_key_to_end(key, index);
- }
- }
- 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;
- },
- // Move key to end of cache. Keys are removed from the front, so moving a key to the end
- // delays the key's removal.
- move_key_to_end: function(key, index) {
- this.key_ary.splice(index, 1);
- this.key_ary.push(key);
- },
- clear: function() {
- this.obj_cache = {};
- this.key_ary = [];
- },
- // Returns the number of elements in the cache.
- size: function() {
- return this.key_ary.length;
- }
- });
- /**
- * Data manager for a track.
- */
- var DataManager = function(num_elements, track, subset) {
- Cache.call(this, num_elements);
- this.track = track;
- this.subset = (subset !== undefined ? subset : true);
- };
- extend(DataManager.prototype, Cache.prototype, {
- /**
- * Load data from server; returns AJAX object so that use of Deferred is possible.
- */
- load_data: function(low, high, mode, resolution, extra_params) {
- // Setup data request params.
- var
- chrom = this.track.view.chrom,
- params = {"chrom": chrom, "low": low, "high": high, "mode": mode,
- "resolution": resolution, "dataset_id" : this.track.dataset_id,
- "hda_ldda": this.track.hda_ldda};
- $.extend(params, extra_params);
-
- // Add track filters to params.
- if (this.track.filters_manager) {
- var filter_names = [];
- var filters = this.track.filters_manager.filters;
- for (var i = 0; i < filters.length; i++) {
- filter_names[filter_names.length] = filters[i].name;
- }
- params.filter_cols = JSON.stringify(filter_names);
- }
-
- // Do request.
- var manager = this;
- return $.getJSON(this.track.data_url, params, function (result) {
- manager.set_data(low, high, mode, result);
- });
- },
- /**
- * Get track data.
- */
- get_data: function(low, high, mode, resolution, extra_params) {
- // Debugging:
- //console.log("get_data", low, high, mode);
- /*
- console.log("cache contents:")
- for (var i = 0; i < this.key_ary.length; i++) {
- console.log("\t", this.key_ary[i], this.obj_cache[this.key_ary[i]]);
- }
- */
-
- // Look for entry and return if found.
- var entry = this.get_data_from_cache(low, high, mode);
- if (entry) { return entry; }
- //
- // If data supports subsetting:
- // Look in cache for data that can be used. Data can be reused if it
- // has the requested data and is not summary tree and has details.
- // TODO: this logic could be improved if the visualization knew whether
- // the data was "index" or "data." Also could slice the data so that
- // only data points in request are returned.
- //
-
- /* Disabling for now, more detailed data is never loaded for line tracks
- TODO: can using resolution in the key solve this problem?
- if (this.subset) {
- var key, split_key, entry_low, entry_high, mode, entry;
- for (var i = 0; i < this.key_ary.length; i++) {
- key = this.key_ary[i];
- split_key = this.split_key(key);
- entry_low = split_key[0];
- entry_high = split_key[1];
-
- if (low >= entry_low && high <= entry_high) {
- // This track has the range of data needed; check other attributes.
- entry = this.obj_cache[key];
- if (entry.dataset_type !== "summary_tree" && entry.extra_info !== "no_detail") {
- // Data is usable.
- this.move_key_to_end(key, i);
- return entry;
- }
- }
- }
- }
- */
-
- // Load data from server. The deferred is immediately saved until the
- // data is ready, it then replaces itself with the actual data
- entry = this.load_data(low, high, mode, resolution, extra_params);
- this.set_data(low, high, mode, entry);
- return entry
- },
- /** "Deep" data request; used as a parameter for DataManager.get_more_data() */
- DEEP_DATA_REQ: "deep",
- /** "Broad" data request; used as a parameter for DataManager.get_more_data() */
- BROAD_DATA_REQ: "breadth",
- /**
- * Gets more data for a region using either a depth-first or a breadth-first approach.
- */
- get_more_data: function(low, high, mode, resolution, extra_params, req_type) {
- //
- // Get current data from cache and mark as stale.
- //
- var cur_data = this.get_data_from_cache(low, high, mode);
- if (!cur_data) {
- console.log("ERROR: no current data for: ", this.track, low, high, mode, resolution, extra_params);
- return;
- }
- cur_data.stale = true;
-
- //
- // Set parameters based on request type.
- //
- var query_low = low;
- if (req_type === this.DEEP_DATA_REQ) {
- // Use same interval but set start_val to skip data that's already in cur_data.
- $.extend(extra_params, {start_val: cur_data.data.length + 1});
- }
- else if (req_type === this.BROAD_DATA_REQ) {
- // To get past an area of extreme feature depth, set query low to be after either
- // (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
- query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
- }
-
- //
- // Get additional data, append to current data, and set new data. Use a custom deferred object
- // to signal when new data is available.
- //
- var
- data_manager = this,
- new_data_request = this.load_data(query_low, high, mode, resolution, extra_params)
- new_data_available = $.Deferred();
- // load_data sets cache to new_data_request, but use custom deferred object so that signal and data
- // is all data, not just new data.
- this.set_data(low, high, mode, new_data_available);
- $.when(new_data_request).then(function(result) {
- // Update data and message.
- if (result.data) {
- result.data = cur_data.data.concat(result.data);
- if (result.max_low) {
- result.max_low = cur_data.max_low;
- }
- if (result.message) {
- // HACK: replace number in message with current data length. Works but is ugly.
- result.message = result.message.replace(/[0-9]+/, result.data.length);
- }
- }
- data_manager.set_data(low, high, mode, result);
- new_data_available.resolve(result);
- });
- return new_data_available;
- },
- /**
- * Gets data from the cache.
- */
- get_data_from_cache: function(low, high, mode) {
- return this.get(this.gen_key(low, high, mode));
- },
- /**
- * Sets data in the cache.
- */
- set_data: function(low, high, mode, result) {
- //console.log("set_data", low, high, mode, result);
- return this.set(this.gen_key(low, high, mode), result);
- },
- /**
- * Generate key for cache.
- */
- // TODO: use chrom in key so that (a) data is not thrown away when changing chroms and (b)
- // manager does not need to be cleared when changing chroms.
- // TODO: use resolution in key b/c summary tree data is dependent on resolution -- is this
- // necessary, i.e. will resolution change but not low/high/mode?
- gen_key: function(low, high, mode) {
- var key = low + "_" + high + "_" + mode;
- return key;
- },
- /**
- * Split key from cache into array with format [low, high, mode]
- */
- split_key: function(key) {
- return key.split("_");
- }
- });
- var ReferenceTrackDataManager = function(num_elements, track, subset) {
- DataManager.call(this, num_elements, track, subset);
- };
- extend(ReferenceTrackDataManager.prototype, DataManager.prototype, Cache.prototype, {
- load_data: function(chrom, low, high, mode, resolution, extra_params) {
- if (resolution > 1) {
- // Now that data is pre-fetched before draw, we don't load reference tracks
- // unless it's at the bottom level
- return;
- }
- return DataManager.prototype.load_data.call(this, chrom, low, high, mode, resolution, extra_params);
- }
- });
- /**
- * Drawables hierarchy:
- *
- * Drawable
- * --> DrawableCollection
- * --> DrawableGroup
- * --> View
- * --> Track
- */
- /**
- * Base interface for all drawable objects.
- */
- var Drawable = function(name, view, prefs, parent_element, drag_handle_class, container) {
- this.name = name;
- this.view = view;
- this.parent_element = parent_element;
- this.drag_handle_class = drag_handle_class;
- this.container = container;
- this.config = new DrawableConfig({
- track: this,
- params: [
- { key: 'name', label: 'Name', type: 'text', default_value: name }
- ],
- saved_values: prefs,
- onchange: function() {
- this.track.set_name(this.track.config.values.name);
- }
- });
- this.prefs = this.config.values;
- };
- extend(Drawable.prototype, {
- init: function() {},
- request_draw: function() {},
- _draw: function() {},
- to_json: function() {},
- make_name_popup_menu: function() {},
- /**
- * Set drawable name.
- */
- set_name: function(new_name) {
- this.old_name = this.name;
- this.name = new_name;
- this.name_div.text(this.name);
- },
- /**
- * Revert track name; currently name can be reverted only once.
- */
- revert_name: function() {
- this.name = this.old_name;
- this.name_div.text(this.name);
- },
- /**
- * Remove drawable (a) from its container and (b) from the HTML.
- */
- remove: function() {
- this.container.remove_drawable(this);
-
- this.container_div.fadeOut('slow', function() {
- $(this).remove();
- // HACK: is there a better way to update the view?
- view.update_intro_div();
- view.has_changes = true;
- });
- }
- });
- /**
- * A collection of drawable objects.
- */
- var DrawableCollection = function(obj_type, name, view, prefs, parent_element, drag_handle_class, container) {
- Drawable.call(this, name, view, prefs, parent_element, drag_handle_class, container);
-
- // Attribute init.
- this.obj_type = obj_type;
- this.drawables = [];
- };
- extend(DrawableCollection.prototype, Drawable.prototype, {
- /**
- * Init each drawable in the collection.
- */
- init: function() {
- for (var i = 0; i < this.drawables.length; i++) {
- this.drawables[i].init();
- }
- },
- /**
- * Draw each drawable in the collection.
- */
- _draw: function() {
- for (var i = 0; i < this.drawables.length; i++) {
- this.drawables[i]._draw();
- }
- },
- /**
- * Returns jsonified representation of collection.
- */
- to_json: function() {
- var jsonified_drawables = [];
- for (var i = 0; i < this.drawables.length; i++) {
- jsonified_drawables.push(this.drawables[i].to_json());
- }
- return {
- name: this.name,
- prefs: this.prefs,
- obj_type: this.obj_type,
- drawables: jsonified_drawables
- };
- },
- /**
- * Add a drawable to the end of the collection.
- */
- add_drawable: function(drawable) {
- this.drawables.push(drawable);
- drawable.container = this;
- },
- /**
- * Add a drawable before another drawable.
- */
- add_drawable_before: function(drawable, other) {
- var index = this.drawables.indexOf(other);
- if (index != -1) {
- this.drawables.splice(index, 0, drawable);
- return true;
- }
- return false;
- },
- /**
- * Remove drawable from this collection.
- */
- remove_drawable: function(drawable) {
- var index = this.drawables.indexOf(drawable);
- if (index != -1) {
- // Found drawable to remove.
- this.drawables.splice(index, 1);
- drawable.container = null;
- return true;
- }
- return false;
- },
- /**
- * Move drawable to another location in collection.
- */
- move_drawable: function(drawable, new_position) {
- var index = this.drawables.indexOf(drawable);
- if (index != -1) {
- // Remove from current position:
- this.drawables.splice(index, 1);
- // insert into new position:
- this.drawables.splice(new_position, 0, drawable);
- return true;
- }
- return false;
- }
- });
- /**
- * A group of drawables that are moveable, visible.
- */
- var DrawableGroup = function(name, view, prefs, parent_element, container) {
- DrawableCollection.call(this, "DrawableGroup", name, view, prefs, parent_element, "group-handle", container);
-
- // HTML elements.
- if (!DrawableGroup.id_counter) { DrawableGroup.id_counter = 0; }
- var group_id = DrawableGroup.id_counter++
- this.container_div = $("<div/>").addClass("group").attr("id", "group_" + group_id).appendTo(this.parent_element);
- this.header_div = $("<div/>").addClass("track-header").appendTo(this.container_div);
- this.header_div.append($("<div/>").addClass(this.drag_handle_class));
- this.name_div = $("<div/>").addClass("group-name menubutton popup").text(this.name).appendTo(this.header_div);
- this.content_div = $("<div/>").addClass("content-div").attr("id", "group_" + group_id + "_content_div").appendTo(this.container_div);
-
- // Set up containers/moving for group: register both container and content div as container
- // because both are used as containers. Group can be moved.
- is_container(this.container_div, this);
- is_container(this.content_div, this);
- moveable(this.container_div, this.drag_handle_class, ".group", this);
-
- this.make_name_popup_menu();
- };
- extend(DrawableGroup.prototype, Drawable.prototype, DrawableCollection.prototype, {
- /**
- * Make popup menu for group.
- */
- make_name_popup_menu: function() {
- var group = this;
-
- var group_dropdown = {};
-
- //
- // Edit config option.
- //
- group_dropdown["Edit configuration"] = function() {
- var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
- ok_fn = function() {
- group.config.update_from_form( $(".dialog-box") );
- hide_modal();
- $(window).unbind("keypress.check_enter_esc");
- },
- check_enter_esc = function(e) {
- if ((e.keyCode || e.which) === 27) { // Escape key
- cancel_fn();
- } else if ((e.keyCode || e.which) === 13) { // Enter key
- ok_fn();
- }
- };
- $(window).bind("keypress.check_enter_esc", check_enter_esc);
- show_modal("Configure Group", group.config.build_form(), {
- "Cancel": cancel_fn,
- "OK": ok_fn
- });
- };
-
- //
- // Remove option.
- //
- group_dropdown.Remove = function() {
- group.remove();
- };
-
- make_popupmenu(group.name_div, group_dropdown);
- }
- });
- /**
- * View object manages complete viz view, including tracks and user interactions.
- */
- var View = function(container, title, vis_id, dbkey) {
- DrawableCollection.call(this, "View");
- this.container = container;
- this.chrom = null;
- this.vis_id = vis_id;
- this.dbkey = dbkey;
- this.title = title;
- // Alias tracks to point at drawables. TODO: changes tracks to 'drawables' or something similar.
- this.tracks = this.drawables;
- this.label_tracks = [];
- this.tracks_to_be_redrawn = [];
- this.max_low = 0;
- this.max_high = 0;
- this.zoom_factor = 3;
- this.min_separation = 30;
- this.has_changes = false;
- // Deferred object that indicates when view's chrom data has been loaded.
- this.load_chroms_deferred = null;
- this.init();
- this.canvas_manager = new CanvasManager( container.get(0).ownerDocument );
- this.reset();
- };
- extend( View.prototype, DrawableCollection.prototype, {
- init: function() {
- // Create DOM elements
- var parent_element = this.container,
- view = this;
- // Top container for things that are fixed at the top
- this.top_container = $("<div/>").addClass("top-container").appendTo(parent_element);
- // Content container, primary tracks are contained in here
- this.content_div = $("<div/>").addClass("content").css("position", "relative").appendTo(parent_element);
- // Bottom container for things that are fixed at the bottom
- this.bottom_container = $("<div/>").addClass("bottom-container").appendTo(parent_element);
- // Label track fixed at top
- this.top_labeltrack = $("<div/>").addClass("top-labeltrack").appendTo(this.top_container);
- // Viewport for dragging tracks in center
- this.viewport_container = $("<div/>").addClass("viewport-container").attr("id", "viewport-container").appendTo(this.content_div);
- is_container(this.viewport_container, view);
- // Introduction div shown when there are no tracks.
- this.intro_div = $("<div/>").addClass("intro");
- var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
- add_tracks();
- });
- // Another label track at bottom
- this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);
- // Navigation at top
- this.nav_container = $("<div/>").addClass("nav-container").prependTo(this.top_container);
- this.nav = $("<div/>").addClass("nav").appendTo(this.nav_container);
- // Overview (scrollbar and overview plot) at bottom
- this.overview = $("<div/>").addClass("overview").appendTo(this.bottom_container);
- this.overview_viewport = $("<div/>").addClass("overview-viewport").appendTo(this.overview);
- this.overview_close = $("<a href='javascript:void(0);'>Close Overview</a>").addClass("overview-close").hide().appendTo(this.overview_viewport);
- this.overview_highlight = $("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);
- this.overview_box_background = $("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);
- this.overview_box = $("<div/>").addClass("overview-box").appendTo(this.overview_viewport);
- this.default_overview_height = this.overview_box.height();
-
- this.nav_controls = $("<div/>").addClass("nav-controls").appendTo(this.nav);
- this.chrom_select = $("<select/>").attr({ "name": "chrom"}).css("width", "15em").addClass("no-autocomplete").append("<option value=''>Loading</option>").appendTo(this.nav_controls);
- 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();
- $(this).val('');
- view.location_span.show();
- view.chrom_select.show();
- }
- };
- this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keyup focusout", submit_nav).appendTo(this.nav_controls);
- this.location_span = $("<span/>").addClass("location").appendTo(this.nav_controls);
- this.location_span.click(function() {
- view.location_span.hide();
- view.chrom_select.hide();
- view.nav_input.val(view.chrom + ":" + view.low + "-" + view.high);
- view.nav_input.css("display", "inline-block");
- view.nav_input.select();
- view.nav_input.focus();
- });
- if (this.vis_id !== undefined) {
- this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.nav_controls);
- }
- this.zo_link = $("<a id='zoom-out' />").click(function() { view.zoom_out(); view.request_redraw(); }).appendTo(this.nav_controls);
- this.zi_link = $("<a id='zoom-in' />").click(function() { view.zoom_in(); view.request_redraw(); }).appendTo(this.nav_controls);
-
- // Get initial set of chroms.
- this.load_chroms_deferred = this.load_chroms({low: 0});
- this.chrom_select.bind("change", function() {
- view.change_chrom(view.chrom_select.val());
- });
-
- /*
- 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();
- });
- */
-
- // Blur tool/filter inputs when user clicks on content div.
- this.content_div.click(function( e ) {
- $(this).find("input").trigger("blur");
- });
- // Double clicking zooms in
- this.content_div.bind("dblclick", function( e ) {
- view.zoom_in(e.pageX, this.viewport_container);
- });
- // Dragging the overview box (~ horizontal scroll bar)
- this.overview_box.bind("dragstart", function( e, d ) {
- this.current_x = d.offsetX;
- }).bind("drag", function( e, d ) {
- var delta = d.offsetX - this.current_x;
- this.current_x = d.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.click(function() {
- view.reset_overview();
- });
-
- // Dragging in the viewport scrolls
- this.viewport_container.bind( "draginit", function( e, d ) {
- // Disable interaction if started in scrollbar (for webkit)
- if ( e.clientX > view.viewport_container.width() - 16 ) {
- return false;
- }
- }).bind( "dragstart", function( e, d ) {
- d.original_low = view.low;
- d.current_height = e.clientY;
- d.current_x = d.offsetX;
- }).bind( "drag", function( e, d ) {
- var container = $(this);
- var delta = d.offsetX - d.current_x;
- var new_scroll = container.scrollTop() - (e.clientY - d.current_height);
- container.scrollTop(new_scroll);
- d.current_height = e.clientY;
- d.current_x = d.offsetX;
- var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low));
- view.move_delta(delta_chrom);
- // Also capture mouse wheel for left/right scrolling
- }).bind( 'mousewheel', function( e, d, dx, dy ) {
- // Only act on x axis scrolling if we see if, y will be i
- // handled by the browser when the event bubbles up
- if ( dx ) {
- var delta_chrom = Math.round( - dx / view.viewport_container.width() * (view.high - view.low) );
- view.move_delta( delta_chrom );
- }
- });
-
- // Dragging in the top label track allows selecting a region
- // to zoom in
- this.top_labeltrack.bind( "dragstart", function( e, d ) {
- return $("<div />").css( {
- "height": view.content_div.height() + view.top_labeltrack.height()
- + view.nav_labeltrack.height() + 1,
- "top": "0px",
- "position": "absolute",
- "background-color": "#ccf",
- "opacity": 0.5,
- "z-index": 1000
- } ).appendTo( $(this) );
- }).bind( "drag", function( e, d ) {
- $( d.proxy ).css({ left: Math.min( e.pageX, d.startX ), width: Math.abs( e.pageX - d.startX ) });
- var min = Math.min(e.pageX, d.startX ) - view.container.offset().left,
- max = Math.max(e.pageX, d.startX ) - 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 );
- }).bind( "dragend", function( e, d ) {
- var min = Math.min(e.pageX, d.startX),
- max = Math.max(e.pageX, d.startX),
- 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;
- $(d.proxy).remove();
- view.request_redraw();
- });
-
- this.add_label_track( new LabelTrack( this, this.top_labeltrack ) );
- this.add_label_track( new LabelTrack( this, this.nav_labeltrack ) );
-
- $(window).bind("resize", function() { view.resize_window(); });
- $(document).bind("redraw", function() { view.redraw(); });
-
- this.reset();
- $(window).trigger("resize");
- this.update_intro_div();
- },
- /** Add or remove intro div depending on view state. */
- update_intro_div: function() {
- if (this.num_tracks === 0) {
- this.intro_div.appendTo(this.viewport_container);
- }
- else {
- this.intro_div.remove();
- }
- },
- update_location: function(low, high) {
- this.location_span.text( commatize(low) + ' - ' + commatize(high) );
- this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
- },
- /**
- * Load chrom data for the view. Returns a jQuery Deferred.
- */
- load_chroms: function(url_parms) {
- url_parms['num'] = MAX_CHROMS_SELECTABLE;
- $.extend( url_parms, (this.vis_id !== undefined ? { vis_id: this.vis_id } : { dbkey: this.dbkey } ) );
- var
- view = this,
- chrom_data = $.Deferred();
- $.ajax({
- url: chrom_url,
- data: url_parms,
- dataType: "json",
- success: function (result) {
- // Show error if could not load chroms.
- if (result.chrom_info.length === 0) {
- alert("Invalid chromosome: " + url_parms.chrom);
- return;
- }
-
- // Load chroms.
- if (result.reference) {
- view.add_label_track( new ReferenceTrack(view) );
- }
- view.chrom_data = result.chrom_info;
- var chrom_options = '<option value="">Select Chrom/Contig</option>';
- for (var i = 0, len = view.chrom_data.length; i < len; i++) {
- var chrom = view.chrom_data[i].chrom;
- chrom_options += '<option value="' + chrom + '">' + chrom + '</option>';
- }
- if (result.prev_chroms) {
- chrom_options += '<option value="previous">Previous ' + MAX_CHROMS_SELECTABLE + '</option>';
- }
- if (result.next_chroms) {
- chrom_options += '<option value="next">Next ' + MAX_CHROMS_SELECTABLE + '</option>';
- }
- view.chrom_select.html(chrom_options);
- view.chrom_start_index = result.start_index;
-
- chrom_data.resolve(result);
- },
- error: function() {
- alert("Could not load chroms for this dbkey:", view.dbkey);
- }
- });
-
- return chrom_data;
- },
- change_chrom: function(chrom, low, high) {
- // Don't do anything if chrom is "None" (hackish but some browsers already have this set), or null/blank
- if (!chrom || chrom === "None") {
- return;
- }
-
- var view = this;
-
- //
- // If user is navigating to previous/next set of chroms, load new chrom set and return.
- //
- if (chrom === "previous") {
- view.load_chroms({low: this.chrom_start_index - MAX_CHROMS_SELECTABLE});
- return;
- }
- if (chrom === "next") {
- view.load_chroms({low: this.chrom_start_index + MAX_CHROMS_SELECTABLE});
- return;
- }
-
- //
- // User is loading a particular chrom. Look first in current set; if not in current set, load new
- // chrom set.
- //
- var found = $.grep(view.chrom_data, function(v, i) {
- return v.chrom === chrom;
- })[0];
- if (found === undefined) {
- // Try to load chrom and then change to chrom.
- view.load_chroms({'chrom': chrom}, function() { view.change_chrom(chrom, low, high); });
- return;
- }
- else {
- // Switching to local chrom.
- if (chrom !== view.chrom) {
- view.chrom = chrom;
- view.chrom_select.val(view.chrom);
- view.max_high = found.len-1; // -1 because we're using 0-based indexing.
- view.reset();
- view.request_redraw(true);
- for (var track_id = 0, len = view.tracks.length; track_id < len; track_id++) {
- 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.request_redraw();
- }
- },
- go_to: function(str) {
- var view = this,
- new_low,
- new_high,
- 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, ""), 10);
- new_high = parseInt(pos_split[1].replace(/,/g, ""), 10);
- } catch (e) {
- return false;
- }
- }
- view.change_chrom(chrom, new_low, new_high);
- },
- move_fraction : function( fraction ) {
- var view = this;
- var span = view.high - view.low;
- this.move_delta( fraction * span );
- },
- 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.request_redraw();
- },
- /**
- * Add a drawable to the view.
- */
- add_drawable: function(drawable) {
- DrawableCollection.prototype.add_drawable.call(this, drawable);
- if (drawable.init) { drawable.init(); }
- this.has_changes = true;
- this.update_intro_div();
- },
- add_label_track: function (label_track) {
- label_track.view = this;
- this.label_tracks.push(label_track);
- },
- /**
- * Remove drawable from the view.
- */
- remove_drawable: function(drawable, hide) {
- DrawableCollection.prototype.remove_drawable.call(this, drawable);
- if (hide) {
- var view = this;
- drawable.container_div.fadeOut('slow', function() {
- $(this).remove();
- view.update_intro_div();
- });
- this.has_changes = true;
- }
- },
- reset: function() {
- this.low = this.max_low;
- this.high = this.max_high;
- this.viewport_container.find(".yaxislabel").remove();
- },
- /**
- * Request that view redraw some or all tracks. If a track is not specificied, redraw all tracks.
- */
- request_redraw: function(nodraw, force, clear_after, track) {
- var
- view = this,
- // Either redrawing a single track or all view's tracks.
- track_list = (track ? [track] : view.tracks),
- track_index;
-
- // Add/update tracks in track list to redraw list.
- var track;
- for (var i = 0; i < track_list.length; i++) {
- track = track_list[i];
-
- // Because list elements are arrays, need to look for track index manually.
- track_index = -1;
- for (var j = 0; j < view.tracks_to_be_redrawn.length; j++) {
- if (view.tracks_to_be_redrawn[j][0] === track) {
- track_index = j;
- break;
- }
- }
-
- // Add track to list or update draw parameters.
- if (track_index < 0) {
- // Track not in list yet.
- view.tracks_to_be_redrawn.push([track, force, clear_after]);
- }
- else {
- // Track already in list; update force and clear_after.
- view.tracks_to_be_redrawn[i][1] = force;
- view.tracks_to_be_redrawn[i][2] = clear_after;
- }
- }
- // Set up redraw.
- requestAnimationFrame(function() { view._redraw(nodraw) });
- },
- /**
- * Redraws view and tracks.
- * NOTE: this method should never be called directly; request_redraw() should be used so
- * that requestAnimationFrame can manage redrawing.
- */
- _redraw: function(nodraw) {
-
- var low = this.low,
- high = this.high;
-
- if (low < this.max_low) {
- low = this.max_low;
- }
- if (high > this.max_high) {
- high = this.max_high;
- }
- var span = this.high - this.low;
- 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( RESOLUTION, Math.ceil( Math.log( (this.high - this.low) / DENSITY ) / Math.log(RESOLUTION) ) );
- 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() ) || 0;
- var width_px = ( (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
- 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) {
- var track, force, clear_after;
- for (var i = 0, len = this.tracks_to_be_redrawn.length; i < len; i++) {
- track = this.tracks_to_be_redrawn[i][0];
- force = this.tracks_to_be_redrawn[i][1];
- clear_after = this.tracks_to_be_redrawn[i][2];
- if (track) {
- track._draw(force, clear_after);
- }
- }
- this.tracks_to_be_redrawn = [];
- for (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.request_redraw();
- },
- zoom_out: function ()…
Large files files are truncated, but you can click here to view the full file