/static/scripts/viz/trackster/tracks.js
JavaScript | 4246 lines | 2575 code | 450 blank | 1221 comment | 327 complexity | 6fa83526dd362e018a8eb9f2a8127e6b MD5 | raw file
Possible License(s): CC-BY-3.0
Large files files are truncated, but you can click here to view the full file
- define( ["libs/underscore", "viz/visualization", "viz/viz_views", "viz/trackster/util",
- "viz/trackster/slotting", "viz/trackster/painters", "viz/trackster/filters",
- "mvc/data", "mvc/tools", "utils/config" ],
- function(_, visualization, viz_views, util, slotting, painters, filters_mod, data, tools_mod, config_mod) {
- var extend = _.extend;
- // ---- 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";
-
- // 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(),
- // Only sorting amongst tracks and groups.
- children = parent.children('.track,.group'),
- 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();
- var cur_container = html_elt_js_obj_dict[container.attr("id")];
- if (d.offsetY < top) {
- // Moving above container.
- $(this).insertBefore(container);
- 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);
- 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() {
- $(this).addClass('dragging');
- }).bind("dragend", function() {
- $(this).removeClass('dragging');
- });
- };
- /**
- * Init constants & functions used throughout trackster.
- */
- var
- // 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,
- // 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 = "Cannot display dataset due to an error. ",
- 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,
- // Numerical/continuous data display modes.
- CONTINUOUS_DATA_MODES = ["Histogram", "Line", "Filled", "Intensity"];
-
- /**
- * 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.view = view;
- this.container = container;
- this.drag_handle_class = obj_dict.drag_handle_class;
- this.is_overview = false;
- this.action_icons = {};
-
- // -- Set up drawable configuration. --
- this.config = config_mod.ConfigSettingCollection.from_models_and_saved_values(this.config_params, obj_dict.prefs);
-
- // If there's no saved name, use object name.
- if (!this.config.get_value('name')) {
- this.config.set_value('name', obj_dict.name);
- }
- if (this.config_onchange) {
- this.config.on('change', this.config_onchange, this);
- }
-
- // Build Drawable HTML and behaviors.
- this.container_div = this.build_container_div();
- this.header_div = null;
- // Use opt-out policy on header creation because this is the more frequent approach:
- // unless flag set, create header.
- if (obj_dict.header !== false) {
- var header_view = new viz_views.TrackHeaderView({
- model: this,
- id: this.id
- });
-
- this.header_div = header_view.$el;
- this.container_div.append(this.header_div);
- // Show icons when users is hovering over track.
- var icons_div = header_view.icons_div;
- this.action_icons = header_view.action_icons;
- this.container_div.hover(
- function() { icons_div.show(); }, function() { icons_div.hide(); }
- );
- }
- };
- 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.config.get_value('content_visible') ) {
- drawable.action_icons.toggle_icon.addClass("toggle-expand").removeClass("toggle");
- drawable.hide_contents();
- drawable.config.set_value('content_visible', false);
- }
- else {
- drawable.action_icons.toggle_icon.addClass("toggle").removeClass("toggle-expand");
- drawable.config.set_value('content_visible', true);
- drawable.show_contents();
- }
- }
- },
- // Edit settings.
- {
- name: "settings_icon",
- title: "Edit settings",
- css_class: "gear",
- on_click_fn: function(drawable) {
- var view = new config_mod.ConfigSettingCollectionView({
- collection: drawable.config
- });
- view.render_in_modal('Configure Track');
- }
- },
- // Remove.
- {
- name: "remove_icon",
- title: "Remove",
- css_class: "remove-icon",
- on_click_fn: function(drawable) {
- // Tooltip for remove icon must be deleted when drawable is deleted.
- $(".tooltip").remove();
- drawable.remove();
- }
- }
- ];
- extend(Drawable.prototype, {
- config_params: [
- { key: 'name', label: 'Name', type: 'text', default_value: '' },
- { key: 'content_visible', type: 'bool', default_value: true, hidden: true }
- ],
- config_onchange: function() {},
-
- init: function() {},
- changed: function() {
- this.view.changed();
- },
- can_draw: function() {
- if (this.enabled && this.config.get_value('content_visible')) {
- return true;
- }
-
- return false;
- },
- request_draw: function() {},
- _draw: function(options) {},
- /**
- * 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.config.get_value('name');
- this.config.set_value('name', new_name);
- },
- /**
- * Revert track name; currently name can be reverted only once.
- */
- revert_name: function() {
- if (this.old_name) {
- this.config.set_value('name', this.old_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() {},
-
- /**
- * 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(options) {
- for (var i = 0; i < this.drawables.length; i++) {
- this.drawables[i]._draw(options);
- }
- },
-
- /**
- * 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 {
- prefs: this.config.to_key_value_dict(),
- 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;
- },
- /**
- * Returns all <track_type> tracks in collection.
- */
- get_tracks: function(track_type) {
- // Initialize queue with copy of drawables array.
- var queue = this.drawables.slice(0),
- tracks = [],
- drawable;
- while (queue.length !== 0) {
- drawable = queue.shift();
- if (drawable instanceof track_type) {
- tracks.push(drawable);
- }
- else if (drawable.drawables) {
- queue = queue.concat(drawable.drawables);
- }
- }
- return tracks;
- }
- });
- /**
- * 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 filters_mod.FiltersManager(this);
- this.header_div.after(this.filters_manager.parent_div);
- // HACK: add div to clear floating elements.
- this.filters_manager.parent_div.after( $("<div style='clear: both'/>") );
- // 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 filters_mod.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) {
- $(".tooltip").remove();
- group.show_composite_track();
- }
- },
- // Toggle track filters.
- {
- name: "filters_icon",
- title: "Filters",
- css_class: "ui-slider-050",
- on_click_fn: function(group) {
- // TODO: update Tooltip 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({ clear_tile_cache: 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;
- },
- 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) {
- this.action_icons.composite_icon.toggle(this.drawables[0] instanceof CompositeTrack);
- 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 line tracks;
- // OR
- // FIXME: this is not enabled right now because it has not been well tested:
- // (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 && this.drawables[0] instanceof LineTrack) {
- this.action_icons.composite_icon.show();
- }
- else {
- this.action_icons.composite_icon.hide();
- $(".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 filters_mod.NumberFilter( {
- name: filters[0].name,
- index: filters[0].index
- } );
- this.filters_manager.add_filter(new_filter);
- }
- }
-
- // Show/hide icon based on filter availability.
- this.action_icons.filters_icon.toggle(this.filters_manager.filters.length > 0);
- }
- 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() {
- var composite_track = new CompositeTrack(this.view, this.view, {
- name: this.config.get_value('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(options) {
- _.each(this.drawables, function(d) {
- d.request_draw(options);
- });
- }
- });
- /**
- * View object manages a trackster visualization, including tracks and user interactions.
- * Events triggered:
- * navigate: when browser view changes to a new locations
- */
- var TracksterView = Backbone.View.extend({
- initialize: 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.stand_alone = (obj_dict.stand_alone !== undefined ? obj_dict.stand_alone : true);
- 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.render();
- this.canvas_manager = new visualization.CanvasManager( this.container.get(0).ownerDocument );
- this.reset();
- // Define track configuration
- this.config = config_mod.ConfigSettingCollection.from_models_and_saved_values( [
- { key: 'name', label: 'Name', type: 'text', default_value: '' },
- { key: 'a_color', label: 'A Color', type: 'color', default_value: "#FF0000" },
- { key: 'c_color', label: 'C Color', type: 'color', default_value: "#00FF00" },
- { key: 'g_color', label: 'G Color', type: 'color', default_value: "#0000FF" },
- { key: 't_color', label: 'T Color', type: 'color', default_value: "#FF00FF" },
- { key: 'n_color', label: 'N Color', type: 'color', default_value: "#AAAAAA" }
- ], { name: obj_dict.name });
- },
- render: 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").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);
- var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
- visualization.select_datasets(galaxy_config.root + "visualization/list_current_history_datasets", galaxy_config.root + "api/datasets", { 'f-dbkey': view.dbkey }, function(tracks) {
- _.each(tracks, function(track) {
- view.add_drawable( object_from_template(track, view, view) );
- });
- });
- });
- // 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);
- if (this.stand_alone) {
- this.nav_container.addClass("stand-alone");
- this.nav.addClass("stand-alone");
- }
- // 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("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"}).addClass('chrom-nav').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();
- }
- // Suppress key presses so that they do impact viz.
- e.stopPropagation();
- };
- this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keyup focusout", submit_nav).appendTo(this.nav_controls);
- this.location_span = $("<span/>").addClass("location").attr('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_tracks(FeatureTrack), function(t) {
- return t.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]
- };
- }));
- });
- },
- minLength: 2
- });
- });
- 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(); }).appendTo(this.nav_controls);
- this.zi_link = $("<a/>").attr("id", "zoom-in").attr("title", "Zoom in").tooltip( {placement: 'bottom'} )
- .click(function() { view.zoom_in(); }).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);
- });
- /*
- FIXME: Do not do this for now because it's too jittery. Some kind of gravity approach is
- needed here because moving left/right should be difficult.
- // Also capture mouse wheel for left/right scrolling
- }).bind( 'mousewheel', function( e, d, dx, dy ) {
- // Only handle x axis scrolling; y axis scrolling is
- // 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 on selected region.
- this.top_labeltrack.bind( "dragstart", function( e, d ) {
- return $("<div/>").addClass('zoom-area').css(
- "height", view.browser_content_div.height() + view.top_labeltrack.height() + 1
- ).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();
- });
- // FIXME: this is still wonky for embedded visualizations.
- /*
- // For vertical alignment, track mouse with simple line.
- var mouse_tracker_div = $('<div/>').addClass('mouse-pos').appendTo(parent_element);
- // Show tracker only when hovering over view.
- parent_element.hover(
- function() {
- mouse_tracker_div.show();
- parent_element.mousemove(function(e) {
- mouse_tracker_div.css({
- // -1 makes line appear next to the mouse w/o preventing mouse actions.
- left: e.pageX - parent_element.offset().left - 1
- });
- });
- },
- function() {
- parent_element.off('mousemove');
- mouse_tracker_div.hide();
- }
- );
- */
-
- this.add_label_track( new LabelTrack( this, { content_div: this.top_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");
- },
- get_base_color: function(base) {
- return this.config.get_value(base.toLowerCase() + '_color') ||
- this.config.get_value('n_color');
- }
- });
- // FIXME: need to use this approach to enable inheritance of DrawableCollection functions.
- extend( TracksterView.prototype, DrawableCollection.prototype, {
- changed: function() {
- this.has_changes = true;
- },
- /** Add or remove intro div depending on view state. */
- update_intro_div: function() {
- this.intro_div.toggle(this.drawables.length === 0);
- },
- /**
- * 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 = this.chrom_select.val();
- if (chrom !== "") {
- this.trigger_navigate(chrom, this.low, this.high, true);
- }
- },
- /**
- * Load chrom data for the view. Returns a jQuery Deferred.
- */
- // FIXME: instead of loading chrom data, should load and store genome object.
- load_chroms: function(url_parms) {
- url_parms.num = MAX_CHROMS_SELECTABLE;
- var
- view = this,
- chrom_data = $.Deferred();
- $.ajax({
- url: galaxy_config.root + "api/genomes/" + 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) {
- var ref_track = new ReferenceTrack(view);
- view.add_label_track(ref_track);
- view.reference_track = ref_track;
- }
- 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.chrom_info);
- },
- 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();
-
- 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();
- }
- }
- // Resolve low, high.
- if (low === undefined && high === undefined) {
- // Both are undefined, so view is whole chromosome.
- view.low = 0;
- view.high = view.max_high;
- }
- else {
- // Low and/or high is defined.
- view.low = (low !== undefined ? Math.max(low, 0) : 0);
- if (high === undefined) {
- // Center visualization around low.
- // HACK: max resolution is currently 30 bases.
- view.low = Math.max(view.low - 15, 0);
- view.high = view.low + 30;
- }
- else {
- // High is defined.
- view.high = Math.min(high, view.max_high);
- }
- }
- view.request_redraw();
- }
- },
- /**
- * Change viewing region to that denoted by string. General format of string is:
- *
- * <chrom>[ {separator}<start>[-<end>] ]
- *
- * where separator can be whitespace or a colon. Examples:
- *
- * chr22
- * chr1:100-200
- * chr7 89999
- * chr8 90000 990000
- */
- go_to: function(str) {
- // Remove commas.
- str = str.replace(/,/g, '');
- // Replace colons and hyphens with space for easy parsing.
- str = str.replace(/:|\-/g, ' ');
- // Parse new location.
- var chrom_pos = str.split(/\s+/),
- chrom = chrom_pos[0],
- new_low = (chrom_pos[1] ? parseInt(chrom_pos[1], 10) : undefined),
- new_high = (chrom_pos[2] ? parseInt(chrom_pos[2], 10) : undefined);
- this.change_chrom(chrom, new_low, new_high);
- …
Large files files are truncated, but you can click here to view the full file