/static/scripts/viz/trackster/tracks.js
JavaScript | 4815 lines | 3332 code | 368 blank | 1115 comment | 449 complexity | b10324d5a97c9560242ff69881e9f229 MD5 | raw file
Large files files are truncated, but you can click here to view the full file
- define( ["libs/underscore", "viz/visualization", "viz/trackster/util", "viz/trackster/slotting", "viz/trackster/painters" ], function( _, visualization, util, slotting, painters ) {
- var extend = _.extend;
- var get_random_color = util.get_random_color;
- /**
- * Helper to determine if object is jQuery deferred.
- */
- var is_deferred = function ( d ) {
- return ( 'isResolved' in d );
- };
- // ---- 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),
- 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++ ) {
- child = $(children.get(i));
- if ( d.offsetY < child.position().top &&
- // Cannot move tracks above reference track or intro div.
- !(child.hasClass("reference-track") || child.hasClass("intro")) ) {
- 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,
- // 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.
-
- // Number of pixels per tile, not including left offset.
- TILE_SIZE = 400,
- 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 = "Preparing data. This can take a while for a large dataset. " +
- "If the visualization is saved and closed, preparation will continue in the background.",
- DATA_CANNOT_RUN_TOOL = "Tool cannot be rerun: ",
- DATA_LOADING = "Loading data...",
- DATA_OK = "Ready for display",
- TILE_CACHE_SIZE = 10,
- DATA_CACHE_SIZE = 20;
-
- /**
- * Round a number to a given number of decimal places.
- */
- function round(num, places) {
- // Default rounding is to integer.
- if (!places) {
- places = 0;
- }
-
- var val = Math.pow(10, places);
- return Math.round(num * val) / val;
- }
- /**
- * Drawables hierarchy:
- *
- * Drawable
- * --> DrawableCollection
- * --> DrawableGroup
- * --> View
- * --> Track
- */
- /**
- * Base class for all drawable objects. Drawable objects are associated with a view and live in a
- * container. They have the following HTML elements and structure:
- * <container_div>
- * <header_div>
- * <content_div>
- *
- * They optionally have a drag handle class.
- */
- var Drawable = function(view, container, obj_dict) {
- if (!Drawable.id_counter) { Drawable.id_counter = 0; }
- this.id = Drawable.id_counter++;
- this.name = obj_dict.name;
- this.view = view;
- this.container = container;
- this.config = new DrawableConfig({
- track: this,
- params: [
- { key: 'name', label: 'Name', type: 'text', default_value: this.name }
- ],
- saved_values: obj_dict.prefs,
- onchange: function() {
- this.track.set_name(this.track.config.values.name);
- }
- });
- this.prefs = this.config.values;
- this.drag_handle_class = obj_dict.drag_handle_class;
- this.is_overview = false;
- this.action_icons = {};
-
- // FIXME: this should be a saved setting
- this.content_visible = true;
-
- // Build Drawable HTML and behaviors.
- this.container_div = this.build_container_div();
- this.header_div = this.build_header_div();
-
- if (this.header_div) {
- this.container_div.append(this.header_div);
-
- // Icons container.
- this.icons_div = $("<div/>").css("float", "left").hide().appendTo(this.header_div);
- this.build_action_icons(this.action_icons_def);
-
- this.header_div.append( $("<div style='clear: both'/>") );
-
- // Suppress double clicks in header so that they do not impact viz.
- this.header_div.dblclick( function(e) { e.stopPropagation(); } );
-
- // Show icons when users is hovering over track.
- var drawable = this;
- this.container_div.hover(
- function() { drawable.icons_div.show(); }, function() { drawable.icons_div.hide(); }
- );
-
- // Needed for floating elts in header.
- $("<div style='clear: both'/>").appendTo(this.container_div);
- }
- };
- Drawable.prototype.action_icons_def = [
- // Hide/show drawable content.
- // FIXME: make this an odict for easier lookup.
- {
- name: "toggle_icon",
- title: "Hide/show content",
- css_class: "toggle",
- on_click_fn: function(drawable) {
- if ( drawable.content_visible ) {
- drawable.action_icons.toggle_icon.addClass("toggle-expand").removeClass("toggle");
- drawable.hide_contents();
- drawable.content_visible = false;
- } else {
- drawable.action_icons.toggle_icon.addClass("toggle").removeClass("toggle-expand");
- drawable.content_visible = true;
- drawable.show_contents();
- }
- }
- },
- // Edit settings.
- {
- name: "settings_icon",
- title: "Edit settings",
- css_class: "settings-icon",
- on_click_fn: function(drawable) {
- var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
- ok_fn = function() {
- drawable.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", drawable.config.build_form(), {
- "Cancel": cancel_fn,
- "OK": ok_fn
- });
- }
- },
- // Remove.
- {
- name: "remove_icon",
- title: "Remove",
- css_class: "remove-icon",
- on_click_fn: function(drawable) {
- // Tipsy for remove icon must be deleted when drawable is deleted.
- $(".bs-tooltip").remove();
- drawable.remove();
- }
- }
- ];
- extend(Drawable.prototype, {
- init: function() {},
- changed: function() {
- this.view.changed();
- },
- can_draw: function() {
- if (this.enabled && this.content_visible) {
- return true;
- }
-
- return false;
- },
- request_draw: function() {},
- _draw: function() {},
- /**
- * Returns representation of object in a dictionary for easy saving.
- * Use from_dict to recreate object.
- */
- to_dict: 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() {
- if (this.old_name) {
- 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.changed();
-
- this.container.remove_drawable(this);
- var view = this.view;
- this.container_div.hide(0, function() {
- $(this).remove();
- // HACK: is there a better way to update the view?
- view.update_intro_div();
- });
- },
- /**
- * Build drawable's container div; this is the parent div for all drawable's elements.
- */
- build_container_div: function() {},
- /**
- * Build drawable's header div.
- */
- build_header_div: function() {},
- /**
- * Add an action icon to this object. Appends icon unless prepend flag is specified.
- */
- add_action_icon: function(name, title, css_class, on_click_fn, prepend, hide) {
- var drawable = this;
- this.action_icons[name] = $("<a/>").attr("href", "javascript:void(0);").attr("title", title)
- .addClass("icon-button").addClass(css_class).tooltip()
- .click( function() { on_click_fn(drawable); } )
- .appendTo(this.icons_div);
- if (hide) {
- this.action_icons[name].hide();
- }
- },
- /**
- * Build drawable's icons div from object's icons_dict.
- */
- build_action_icons: function(action_icons_def) {
- // Create icons.
- var icon_dict;
- for (var i = 0; i < action_icons_def.length; i++) {
- icon_dict = action_icons_def[i];
- this.add_action_icon(icon_dict.name, icon_dict.title, icon_dict.css_class,
- icon_dict.on_click_fn, icon_dict.prepend, icon_dict.hide);
- }
- },
-
- /**
- * Update icons.
- */
- update_icons: function() {},
-
- /**
- * Hide drawable's contents.
- */
- hide_contents: function () {},
-
- /**
- * Show drawable's contents.
- */
- show_contents: function() {},
- /**
- * Returns a shallow copy of all drawables in this drawable.
- */
- get_drawables: function() {}
- });
- /**
- * A collection of drawable objects.
- */
- var DrawableCollection = function(view, container, obj_dict) {
- Drawable.call(this, view, container, obj_dict);
-
- // Attribute init.
- this.obj_type = obj_dict.obj_type;
- this.drawables = [];
- };
- extend(DrawableCollection.prototype, Drawable.prototype, {
- /**
- * Unpack and add drawables to the collection.
- */
- unpack_drawables: function(drawables_array) {
- // Add drawables to collection.
- this.drawables = [];
- var drawable;
- for (var i = 0; i < drawables_array.length; i++) {
- drawable = object_from_template(drawables_array[i], this.view, this);
- this.add_drawable(drawable);
- }
- },
-
- /**
- * 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 representation of object in a dictionary for easy saving.
- * Use from_dict to recreate object.
- */
- to_dict: function() {
- var dictified_drawables = [];
- for (var i = 0; i < this.drawables.length; i++) {
- dictified_drawables.push(this.drawables[i].to_dict());
- }
- return {
- name: this.name,
- prefs: this.prefs,
- obj_type: this.obj_type,
- drawables: dictified_drawables
- };
- },
-
- /**
- * Add a drawable to the end of the collection.
- */
- add_drawable: function(drawable) {
- this.drawables.push(drawable);
- drawable.container = this;
- this.changed();
- },
-
- /**
- * Add a drawable before another drawable.
- */
- add_drawable_before: function(drawable, other) {
- this.changed();
- var index = this.drawables.indexOf(other);
- if (index !== -1) {
- this.drawables.splice(index, 0, drawable);
- return true;
- }
- return false;
- },
-
- /**
- * Replace one drawable with another.
- */
- replace_drawable: function(old_drawable, new_drawable, update_html) {
- var index = this.drawables.indexOf(old_drawable);
- if (index !== -1) {
- this.drawables[index] = new_drawable;
- if (update_html) {
- old_drawable.container_div.replaceWith(new_drawable.container_div);
- }
- this.changed();
- }
- return index;
- },
-
- /**
- * 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;
- this.changed();
- 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);
- this.changed();
- return true;
- }
- return false;
- },
- /**
- * Returns all drawables in this drawable.
- */
- get_drawables: function() {
- return this.drawables;
- }
- });
- /**
- * A group of drawables that are moveable, visible.
- */
- var DrawableGroup = function(view, container, obj_dict) {
- extend(obj_dict, {
- obj_type: "DrawableGroup",
- drag_handle_class: "group-handle"
- });
- DrawableCollection.call(this, view, container, obj_dict);
-
- // Set up containers/moving for group: register both container_div and content div as container
- // because both are used as containers (container div to recognize container, content_div to
- // store elements). Group can be moved.
- this.content_div = $("<div/>").addClass("content-div").attr("id", "group_" + this.id + "_content_div").appendTo(this.container_div);
- is_container(this.container_div, this);
- is_container(this.content_div, this);
- moveable(this.container_div, this.drag_handle_class, ".group", this);
-
- // Set up filters.
- this.filters_manager = new FiltersManager(this);
- this.header_div.after(this.filters_manager.parent_div);
- // For saving drawables' filter managers when group-level filtering is done:
- this.saved_filters_managers = [];
-
- // Add drawables.
- if ('drawables' in obj_dict) {
- this.unpack_drawables(obj_dict.drawables);
- }
-
- // Restore filters.
- if ('filters' in obj_dict) {
- // FIXME: Pass collection_dict to DrawableCollection/Drawable will make this easier.
- var old_manager = this.filters_manager;
- this.filters_manager = new FiltersManager(this, obj_dict.filters);
- old_manager.parent_div.replaceWith(this.filters_manager.parent_div);
-
- if (obj_dict.filters.visible) {
- this.setup_multitrack_filtering();
- }
- }
- };
- extend(DrawableGroup.prototype, Drawable.prototype, DrawableCollection.prototype, {
- action_icons_def: [
- Drawable.prototype.action_icons_def[0],
- Drawable.prototype.action_icons_def[1],
- // Replace group with composite track.
- {
- name: "composite_icon",
- title: "Show composite track",
- css_class: "layers-stack",
- on_click_fn: function(group) {
- $(".bs-tooltip").remove();
- group.show_composite_track();
- }
- },
- // Toggle track filters.
- {
- name: "filters_icon",
- title: "Filters",
- css_class: "filters-icon",
- on_click_fn: function(group) {
- // TODO: update tipsy text.
- if (group.filters_manager.visible()) {
- // Hiding filters.
- group.filters_manager.clear_filters();
- group._restore_filter_managers();
- // TODO: maintain current filter by restoring and setting saved manager's
- // settings to current/shared manager's settings.
- // TODO: need to restore filter managers when moving drawable outside group.
- }
- else {
- // Showing filters.
- group.setup_multitrack_filtering();
- group.request_draw(true);
- }
- group.filters_manager.toggle();
- }
- },
- Drawable.prototype.action_icons_def[2]
- ],
- build_container_div: function() {
- var container_div = $("<div/>").addClass("group").attr("id", "group_" + this.id);
- if (this.container) {
- this.container.content_div.append(container_div);
- }
- return container_div;
- },
- build_header_div: function() {
- var header_div = $("<div/>").addClass("track-header");
- header_div.append($("<div/>").addClass(this.drag_handle_class));
- this.name_div = $("<div/>").addClass("track-name").text(this.name).appendTo(header_div);
- return header_div;
- },
- hide_contents: function () {
- this.tiles_div.hide();
- },
- show_contents: function() {
- // Show the contents div and labels (if present)
- this.tiles_div.show();
- // Request a redraw of the content
- this.request_draw();
- },
- update_icons: function() {
- //
- // Handle update when there are no tracks.
- //
- var num_drawables = this.drawables.length;
- if (num_drawables === 0) {
- this.action_icons.composite_icon.hide();
- this.action_icons.filters_icon.hide();
- }
- else if (num_drawables === 1) {
- if (this.drawables[0] instanceof CompositeTrack) {
- this.action_icons.composite_icon.show();
- }
- this.action_icons.filters_icon.hide();
- }
- else { // There are 2 or more tracks.
- //
- // Determine if a composite track can be created. Current criteria:
- // (a) all tracks are the same;
- // OR
- // (b) there is a single FeatureTrack.
- //
- /// All tracks the same?
- var i, j, drawable,
- same_type = true,
- a_type = this.drawables[0].get_type(),
- num_feature_tracks = 0;
- for (i = 0; i < num_drawables; i++) {
- drawable = this.drawables[i];
- if (drawable.get_type() !== a_type) {
- can_composite = false;
- break;
- }
- if (drawable instanceof FeatureTrack) {
- num_feature_tracks++;
- }
- }
-
- if (same_type || num_feature_tracks === 1) {
- this.action_icons.composite_icon.show();
- }
- else {
- this.action_icons.composite_icon.hide();
- $(".bs-tooltip").remove();
- }
-
- //
- // Set up group-level filtering and update filter icon.
- //
- if (num_feature_tracks > 1 && num_feature_tracks === this.drawables.length) {
- //
- // Find shared filters.
- //
- var shared_filters = {},
- filter;
-
- // Init shared filters with filters from first drawable.
- drawable = this.drawables[0];
- for (j = 0; j < drawable.filters_manager.filters.length; j++) {
- filter = drawable.filters_manager.filters[j];
- shared_filters[filter.name] = [filter];
- }
-
- // Create lists of shared filters.
- for (i = 1; i < this.drawables.length; i++) {
- drawable = this.drawables[i];
- for (j = 0; j < drawable.filters_manager.filters.length; j++) {
- filter = drawable.filters_manager.filters[j];
- if (filter.name in shared_filters) {
- shared_filters[filter.name].push(filter);
- }
- }
- }
-
- //
- // Create filters for shared filters manager. Shared filters manager is group's
- // manager.
- //
- this.filters_manager.remove_all();
- var
- filters,
- new_filter,
- min,
- max;
- for (var filter_name in shared_filters) {
- filters = shared_filters[filter_name];
- if (filters.length === num_feature_tracks) {
- // Add new filter.
- // FIXME: can filter.copy() be used?
- new_filter = new NumberFilter( {
- name: filters[0].name,
- index: filters[0].index
- } );
- this.filters_manager.add_filter(new_filter);
- }
- }
-
- // Show/hide icon based on filter availability.
- if (this.filters_manager.filters.length > 0) {
- this.action_icons.filters_icon.show();
- }
- else {
- this.action_icons.filters_icon.hide();
- }
- }
- else {
- this.action_icons.filters_icon.hide();
- }
- }
- },
- /**
- * Restore individual track filter managers.
- */
- _restore_filter_managers: function() {
- for (var i = 0; i < this.drawables.length; i++) {
- this.drawables[i].filters_manager = this.saved_filters_managers[i];
- }
- this.saved_filters_managers = [];
- },
- /**
- *
- */
- setup_multitrack_filtering: function() {
- // Save tracks' managers and set up shared manager.
- if (this.filters_manager.filters.length > 0) {
- // For all tracks, save current filter manager and set manager to shared (this object's) manager.
- this.saved_filters_managers = [];
- for (var i = 0; i < this.drawables.length; i++) {
- drawable = this.drawables[i];
- this.saved_filters_managers.push(drawable.filters_manager);
- drawable.filters_manager = this.filters_manager;
- }
- //TODO: hide filters icons for each drawable?
- }
- this.filters_manager.init_filters();
- },
- /**
- * Replace group with a single composite track that includes all group's tracks.
- */
- show_composite_track: function() {
- // Create composite track name.
- var drawables_names = [];
- for (var i = 0; i < this.drawables.length; i++) {
- drawables_names.push(this.drawables[i].name);
- }
- var new_track_name = "Composite Track of " + this.drawables.length + " tracks (" + drawables_names.join(", ") + ")";
-
- // Replace this group with composite track.
- var composite_track = new CompositeTrack(this.view, this.view, {
- name: new_track_name,
- drawables: this.drawables
- });
- var index = this.container.replace_drawable(this, composite_track, true);
- composite_track.request_draw();
- },
- add_drawable: function(drawable) {
- DrawableCollection.prototype.add_drawable.call(this, drawable);
- this.update_icons();
- },
- remove_drawable: function(drawable) {
- DrawableCollection.prototype.remove_drawable.call(this, drawable);
- this.update_icons();
- },
- to_dict: function() {
- // If filters are visible, need to restore original filter managers before converting to dict.
- if (this.filters_manager.visible()) {
- this._restore_filter_managers();
- }
- var obj_dict = extend(DrawableCollection.prototype.to_dict.call(this), { "filters": this.filters_manager.to_dict() });
-
- // Setup multi-track filtering again.
- if (this.filters_manager.visible()) {
- this.setup_multitrack_filtering();
- }
-
- return obj_dict;
- },
- request_draw: function(clear_after, force) {
- for (var i = 0; i < this.drawables.length; i++) {
- this.drawables[i].request_draw(clear_after, force);
- }
- }
- });
- /**
- * View object manages complete viz view, including tracks and user interactions.
- * Events triggered:
- * navigate: when browser view changes to a new locations
- */
- var View = function(obj_dict) {
- extend(obj_dict, {
- obj_type: "View"
- });
- DrawableCollection.call(this, "View", obj_dict.container, obj_dict);
- this.chrom = null;
- this.vis_id = obj_dict.vis_id;
- this.dbkey = obj_dict.dbkey;
- 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( this.container.get(0).ownerDocument );
- this.reset();
- };
- _.extend( View.prototype, Backbone.Events);
- extend( View.prototype, DrawableCollection.prototype, {
- init: function() {
- // Attribute init.
- this.requested_redraw = false;
-
- // 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);
- // Browser content, primary tracks are contained in here
- this.browser_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.browser_content_div);
- // Alias viewport_container as content_div so that it matches function of DrawableCollection/Group content_div.
- this.content_div = this.viewport_container;
- is_container(this.viewport_container, view);
- // Introduction div shown when there are no tracks.
- this.intro_div = $("<div/>").addClass("intro").appendTo(this.viewport_container).hide();
- var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
- add_datasets(add_datasets_url, add_track_async_url, function(tracks) {
- _.each(tracks, function(track) {
- view.add_drawable( object_from_template(track, view, view) );
- });
- });
- });
- // Another label track at bottom
- this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);
- // Navigation at top
- this.nav_container = $("<div/>").addClass("trackster-nav-container").prependTo(this.top_container);
- this.nav = $("<div/>").addClass("trackster-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/>").attr("href", "javascript:void(0);").attr("title", "Close overview").addClass("icon-button overview-close tooltip").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").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").attr('original-title', 'Click to change location').tooltip( { placement: 'bottom' } ).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();
- // Set up autocomplete for tracks' features.
- view.nav_input.autocomplete({
- source: function(request, response) {
- // Using current text, query each track and create list of all matching features.
- var all_features = [],
- feature_search_deferreds = $.map(view.get_drawables(), function(drawable) {
- return drawable.data_manager.search_features(request.term).success(function(dataset_features) {
- all_features = all_features.concat(dataset_features);
- });
- });
- // When all searching is done, fill autocomplete.
- $.when.apply($, feature_search_deferreds).done(function() {
- response($.map(all_features, function(feature) {
- return {
- label: feature[0],
- value: feature[1]
- };
- }));
- });
- }
- });
- });
- if (this.vis_id !== undefined) {
- this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.nav_controls);
- }
-
- this.zo_link = $("<a/>").attr("id", "zoom-out").attr("title", "Zoom out").tooltip( {placement: 'bottom'} )
- .click(function() { view.zoom_out(); view.request_redraw(); }).appendTo(this.nav_controls);
- this.zi_link = $("<a/>").attr("id", "zoom-in").attr("title", "Zoom in").tooltip( {placement: 'bottom'} )
- .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.browser_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.browser_content_div.click(function( e ) {
- $(this).find("input").trigger("blur");
- });
- // Double clicking zooms in
- this.browser_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 ) {
- dx *= 50;
- 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.browser_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 ) - view.container.offset().left, 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, { content_div: this.top_labeltrack } ) );
- this.add_label_track( new LabelTrack( this, { content_div: this.nav_labeltrack } ) );
-
- $(window).bind("resize", function() {
- // Stop previous timer.
- if (this.resize_timer) {
- clearTimeout(this.resize_timer);
- }
-
- // When function activated, resize window and redraw.
- this.resize_timer = setTimeout(function () {
- view.resize_window();
- }, 500 );
- });
- $(document).bind("redraw", function() { view.redraw(); });
-
- this.reset();
- $(window).trigger("resize");
- },
- changed: function() {
- this.has_changes = true;
- },
- /** Add or remove intro div depending on view state. */
- update_intro_div: function() {
- if (this.drawables.length === 0) {
- this.intro_div.show();
- }
- else {
- this.intro_div.hide();
- }
- },
- /**
- * Triggers navigate events as needed. If there is a delay,
- * then event is triggered only after navigation has stopped.
- */
- trigger_navigate: function(new_chrom, new_low, new_high, delay) {
- // Stop previous timer.
- if (this.timer) {
- clearTimeout(this.timer);
- }
-
- if (delay) {
- // To aggregate calls, use timer and only navigate once
- // location has stabilized.
- var self = this;
- this.timer = setTimeout(function () {
- self.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
- }, 500 );
- }
- else {
- view.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
- }
- },
- update_location: function(low, high) {
- this.location_span.text( commatize(low) + ' - ' + commatize(high) );
- this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
-
- // Update location. Only update when there is a valid chrom; when loading vis, there may
- // not be a valid chrom.
- var chrom = view.chrom_select.val();
- if (chrom !== "") {
- this.trigger_navigate(chrom, view.low, view.high, true);
- }
- },
- /**
- * Load chrom data for the view. Returns a jQuery Deferred.
- */
- load_chroms: function(url_parms) {
- url_parms.num = MAX_CHROMS_SELECTABLE;
- var
- view = this,
- chrom_data = $.Deferred();
- $.ajax({
- url: chrom_url + "/" + this.dbkey,
- data: url_parms,
- dataType: "json",
- success: function (result) {
- // Do nothing if could not load chroms.
- if (result.chrom_info.length === 0) {
- 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) {
- var view = this;
- // If chrom data is still loading, wait for it.
- if (!view.chrom_data) {
- view.load_chroms_deferred.then(function() {
- view.change_chrom(chrom, low, high);
- });
- return;
- }
-
- // Don't do anything if chrom is "None" (hackish but some browsers already have this set), or null/blank
- if (!chrom || chrom === "None") {
- return;
- }
-
- //
- // 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 i = 0, len = view.drawables.length; i < len; i++) {
- var drawable = view.drawables[i];
- if (drawable.init) {
- drawable.init();
- }
- }
- if (view.reference_track) {
- view.reference_track.init();
- }
- }
- if (low !== undefined && high !== undefined) {
- view.low = Math.max(low, 0);
- view.high = Math.min(high, view.max_high);
- }
- else {
- // Low and high undefined, so view is whole chome.
- view.low = 0;
- view.high = view.max_high;
- }
- view.reset_overview();
- view.request_redraw();
- }
- },
- go_to: function(str) {
- // Preprocess str to remove spaces and commas.
- str = str.replace(/ |,/g, "");
-
- // Go to new location.
- 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], 10);
- new_high = parseInt(pos_split[1], 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) {
- // Update low, high.
- 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;
- …
Large files files are truncated, but you can click here to view the full file