PageRenderTime 75ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/viz/trackster/tracks.js

https://bitbucket.org/jmchilton/galaxy-central-reports-config-enhancements
JavaScript | 4204 lines | 2920 code | 322 blank | 962 comment | 389 complexity | 4de42d836fcdceb4af8a735569ef0ada MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. define( ["libs/underscore", "viz/visualization", "viz/trackster/util",
  2. "viz/trackster/slotting", "viz/trackster/painters", "mvc/data",
  3. "viz/trackster/filters" ],
  4. function( _, visualization, util, slotting, painters, data, filters_mod ) {
  5. var extend = _.extend;
  6. var get_random_color = util.get_random_color;
  7. /**
  8. * Helper to determine if object is jQuery deferred.
  9. */
  10. var is_deferred = function ( d ) {
  11. return ( 'isResolved' in d );
  12. };
  13. // ---- Web UI specific utilities ----
  14. /**
  15. * Dictionary of HTML element-JavaScript object relationships.
  16. */
  17. // TODO: probably should separate moveable objects from containers.
  18. var html_elt_js_obj_dict = {};
  19. /**
  20. * Designates an HTML as a container.
  21. */
  22. var is_container = function(element, obj) {
  23. html_elt_js_obj_dict[element.attr("id")] = obj;
  24. };
  25. /**
  26. * Make `element` moveable within parent and sibling elements by dragging `handle` (a selector).
  27. * Function manages JS objects, containers as well.
  28. *
  29. * @param element HTML element to make moveable
  30. * @param handle_class classname that denotes HTML element to be used as handle
  31. * @param container_selector selector used to identify possible containers for this element
  32. * @param element_js_obj JavaScript object associated with element; used
  33. */
  34. var moveable = function(element, handle_class, container_selector, element_js_obj) {
  35. // HACK: set default value for container selector.
  36. container_selector = ".group";
  37. var css_border_props = {};
  38. // Register element with its object.
  39. html_elt_js_obj_dict[element.attr("id")] = element_js_obj;
  40. // Need to provide selector for handle, not class.
  41. element.bind( "drag", { handle: "." + handle_class, relative: true }, function ( e, d ) {
  42. var element = $(this),
  43. parent = $(this).parent(),
  44. children = parent.children(),
  45. this_obj = html_elt_js_obj_dict[$(this).attr("id")],
  46. child,
  47. container,
  48. top,
  49. bottom,
  50. i;
  51. //
  52. // Enable three types of dragging: (a) out of container; (b) into container;
  53. // (c) sibling movement, aka sorting. Handle in this order for simplicity.
  54. //
  55. // Handle dragging out of container.
  56. container = $(this).parents(container_selector);
  57. if (container.length !== 0) {
  58. top = container.position().top;
  59. bottom = top + container.outerHeight();
  60. if (d.offsetY < top) {
  61. // Moving above container.
  62. $(this).insertBefore(container);
  63. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  64. cur_container.remove_drawable(this_obj);
  65. cur_container.container.add_drawable_before(this_obj, cur_container);
  66. return;
  67. }
  68. else if (d.offsetY > bottom) {
  69. // Moving below container.
  70. $(this).insertAfter(container);
  71. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  72. cur_container.remove_drawable(this_obj);
  73. cur_container.container.add_drawable(this_obj);
  74. return;
  75. }
  76. }
  77. // Handle dragging into container. Child is appended to container's content_div.
  78. container = null;
  79. for ( i = 0; i < children.length; i++ ) {
  80. child = $(children.get(i));
  81. top = child.position().top;
  82. bottom = top + child.outerHeight();
  83. // Dragging into container if child is a container and offset is inside container.
  84. if ( child.is(container_selector) && this !== child.get(0) &&
  85. d.offsetY >= top && d.offsetY <= bottom ) {
  86. // Append/prepend based on where offsetY is closest to and return.
  87. if (d.offsetY - top < bottom - d.offsetY) {
  88. child.find(".content-div").prepend(this);
  89. }
  90. else {
  91. child.find(".content-div").append(this);
  92. }
  93. // Update containers. Object may not have container if it is being moved quickly.
  94. if (this_obj.container) {
  95. this_obj.container.remove_drawable(this_obj);
  96. }
  97. html_elt_js_obj_dict[child.attr("id")].add_drawable(this_obj);
  98. return;
  99. }
  100. }
  101. // Handle sibling movement, aka sorting.
  102. // Determine new position
  103. for ( i = 0; i < children.length; i++ ) {
  104. child = $(children.get(i));
  105. if ( d.offsetY < child.position().top &&
  106. // Cannot move tracks above reference track or intro div.
  107. !(child.hasClass("reference-track") || child.hasClass("intro")) ) {
  108. break;
  109. }
  110. }
  111. // If not already in the right place, move. Need
  112. // to handle the end specially since we don't have
  113. // insert at index
  114. if ( i === children.length ) {
  115. if ( this !== children.get(i - 1) ) {
  116. parent.append(this);
  117. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, i);
  118. }
  119. }
  120. else if ( this !== children.get(i) ) {
  121. $(this).insertBefore( children.get(i) );
  122. // Need to adjust insert position if moving down because move is changing
  123. // indices of all list items.
  124. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, (d.deltaY > 0 ? i-1 : i) );
  125. }
  126. }).bind("dragstart", function() {
  127. css_border_props["border-top"] = element.css("border-top");
  128. css_border_props["border-bottom"] = element.css("border-bottom");
  129. $(this).css({
  130. "border-top": "1px solid blue",
  131. "border-bottom": "1px solid blue"
  132. });
  133. }).bind("dragend", function() {
  134. $(this).css(css_border_props);
  135. });
  136. };
  137. // TODO: do we need to export?
  138. exports.moveable = moveable;
  139. /**
  140. * Init constants & functions used throughout trackster.
  141. */
  142. var
  143. // Minimum height of a track's contents; this must correspond to the .track-content's minimum height.
  144. MIN_TRACK_HEIGHT = 16,
  145. // FIXME: font size may not be static
  146. CHAR_HEIGHT_PX = 9,
  147. // Padding at the top of tracks for error messages
  148. ERROR_PADDING = 20,
  149. // Maximum number of rows un a slotted track
  150. MAX_FEATURE_DEPTH = 100,
  151. // Minimum width for window for squish to be used.
  152. MIN_SQUISH_VIEW_WIDTH = 12000,
  153. // Other constants.
  154. // Number of pixels per tile, not including left offset.
  155. TILE_SIZE = 400,
  156. DEFAULT_DATA_QUERY_WAIT = 5000,
  157. // Maximum number of chromosomes that are selectable at any one time.
  158. MAX_CHROMS_SELECTABLE = 100,
  159. DATA_ERROR = "There was an error in indexing this dataset. ",
  160. DATA_NOCONVERTER = "A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",
  161. DATA_NONE = "No data for this chrom/contig.",
  162. DATA_PENDING = "Preparing data. This can take a while for a large dataset. " +
  163. "If the visualization is saved and closed, preparation will continue in the background.",
  164. DATA_CANNOT_RUN_TOOL = "Tool cannot be rerun: ",
  165. DATA_LOADING = "Loading data...",
  166. DATA_OK = "Ready for display",
  167. TILE_CACHE_SIZE = 10,
  168. DATA_CACHE_SIZE = 20;
  169. /**
  170. * Round a number to a given number of decimal places.
  171. */
  172. function round(num, places) {
  173. // Default rounding is to integer.
  174. if (!places) {
  175. places = 0;
  176. }
  177. var val = Math.pow(10, places);
  178. return Math.round(num * val) / val;
  179. }
  180. /**
  181. * Drawables hierarchy:
  182. *
  183. * Drawable
  184. * --> DrawableCollection
  185. * --> DrawableGroup
  186. * --> View
  187. * --> Track
  188. */
  189. /**
  190. * Base class for all drawable objects. Drawable objects are associated with a view and live in a
  191. * container. They have the following HTML elements and structure:
  192. * <container_div>
  193. * <header_div>
  194. * <content_div>
  195. *
  196. * They optionally have a drag handle class.
  197. */
  198. var Drawable = function(view, container, obj_dict) {
  199. if (!Drawable.id_counter) { Drawable.id_counter = 0; }
  200. this.id = Drawable.id_counter++;
  201. this.name = obj_dict.name;
  202. this.view = view;
  203. this.container = container;
  204. this.config = new DrawableConfig({
  205. track: this,
  206. params: [
  207. { key: 'name', label: 'Name', type: 'text', default_value: this.name }
  208. ],
  209. saved_values: obj_dict.prefs,
  210. onchange: function() {
  211. this.track.set_name(this.track.config.values.name);
  212. }
  213. });
  214. this.prefs = this.config.values;
  215. this.drag_handle_class = obj_dict.drag_handle_class;
  216. this.is_overview = false;
  217. this.action_icons = {};
  218. // FIXME: this should be a saved setting
  219. this.content_visible = true;
  220. // Build Drawable HTML and behaviors.
  221. this.container_div = this.build_container_div();
  222. this.header_div = this.build_header_div();
  223. if (this.header_div) {
  224. this.container_div.append(this.header_div);
  225. // Icons container.
  226. this.icons_div = $("<div/>").css("float", "left").hide().appendTo(this.header_div);
  227. this.build_action_icons(this.action_icons_def);
  228. this.header_div.append( $("<div style='clear: both'/>") );
  229. // Suppress double clicks in header so that they do not impact viz.
  230. this.header_div.dblclick( function(e) { e.stopPropagation(); } );
  231. // Show icons when users is hovering over track.
  232. var drawable = this;
  233. this.container_div.hover(
  234. function() { drawable.icons_div.show(); }, function() { drawable.icons_div.hide(); }
  235. );
  236. // Needed for floating elts in header.
  237. $("<div style='clear: both'/>").appendTo(this.container_div);
  238. }
  239. };
  240. Drawable.prototype.action_icons_def = [
  241. // Hide/show drawable content.
  242. // FIXME: make this an odict for easier lookup.
  243. {
  244. name: "toggle_icon",
  245. title: "Hide/show content",
  246. css_class: "toggle",
  247. on_click_fn: function(drawable) {
  248. if ( drawable.content_visible ) {
  249. drawable.action_icons.toggle_icon.addClass("toggle-expand").removeClass("toggle");
  250. drawable.hide_contents();
  251. drawable.content_visible = false;
  252. } else {
  253. drawable.action_icons.toggle_icon.addClass("toggle").removeClass("toggle-expand");
  254. drawable.content_visible = true;
  255. drawable.show_contents();
  256. }
  257. }
  258. },
  259. // Edit settings.
  260. {
  261. name: "settings_icon",
  262. title: "Edit settings",
  263. css_class: "settings-icon",
  264. on_click_fn: function(drawable) {
  265. var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
  266. ok_fn = function() {
  267. drawable.config.update_from_form( $(".dialog-box") );
  268. hide_modal();
  269. $(window).unbind("keypress.check_enter_esc");
  270. },
  271. check_enter_esc = function(e) {
  272. if ((e.keyCode || e.which) === 27) { // Escape key
  273. cancel_fn();
  274. } else if ((e.keyCode || e.which) === 13) { // Enter key
  275. ok_fn();
  276. }
  277. };
  278. $(window).bind("keypress.check_enter_esc", check_enter_esc);
  279. show_modal("Configure", drawable.config.build_form(), {
  280. "Cancel": cancel_fn,
  281. "OK": ok_fn
  282. });
  283. }
  284. },
  285. // Remove.
  286. {
  287. name: "remove_icon",
  288. title: "Remove",
  289. css_class: "remove-icon",
  290. on_click_fn: function(drawable) {
  291. // Tipsy for remove icon must be deleted when drawable is deleted.
  292. $(".bs-tooltip").remove();
  293. drawable.remove();
  294. }
  295. }
  296. ];
  297. extend(Drawable.prototype, {
  298. init: function() {},
  299. changed: function() {
  300. this.view.changed();
  301. },
  302. can_draw: function() {
  303. if (this.enabled && this.content_visible) {
  304. return true;
  305. }
  306. return false;
  307. },
  308. request_draw: function() {},
  309. _draw: function() {},
  310. /**
  311. * Returns representation of object in a dictionary for easy saving.
  312. * Use from_dict to recreate object.
  313. */
  314. to_dict: function() {},
  315. /**
  316. * Set drawable name.
  317. */
  318. set_name: function(new_name) {
  319. this.old_name = this.name;
  320. this.name = new_name;
  321. this.name_div.text(this.name);
  322. },
  323. /**
  324. * Revert track name; currently name can be reverted only once.
  325. */
  326. revert_name: function() {
  327. if (this.old_name) {
  328. this.name = this.old_name;
  329. this.name_div.text(this.name);
  330. }
  331. },
  332. /**
  333. * Remove drawable (a) from its container and (b) from the HTML.
  334. */
  335. remove: function() {
  336. this.changed();
  337. this.container.remove_drawable(this);
  338. var view = this.view;
  339. this.container_div.hide(0, function() {
  340. $(this).remove();
  341. // HACK: is there a better way to update the view?
  342. view.update_intro_div();
  343. });
  344. },
  345. /**
  346. * Build drawable's container div; this is the parent div for all drawable's elements.
  347. */
  348. build_container_div: function() {},
  349. /**
  350. * Build drawable's header div.
  351. */
  352. build_header_div: function() {},
  353. /**
  354. * Add an action icon to this object. Appends icon unless prepend flag is specified.
  355. */
  356. add_action_icon: function(name, title, css_class, on_click_fn, prepend, hide) {
  357. var drawable = this;
  358. this.action_icons[name] = $("<a/>").attr("href", "javascript:void(0);").attr("title", title)
  359. .addClass("icon-button").addClass(css_class).tooltip()
  360. .click( function() { on_click_fn(drawable); } )
  361. .appendTo(this.icons_div);
  362. if (hide) {
  363. this.action_icons[name].hide();
  364. }
  365. },
  366. /**
  367. * Build drawable's icons div from object's icons_dict.
  368. */
  369. build_action_icons: function(action_icons_def) {
  370. // Create icons.
  371. var icon_dict;
  372. for (var i = 0; i < action_icons_def.length; i++) {
  373. icon_dict = action_icons_def[i];
  374. this.add_action_icon(icon_dict.name, icon_dict.title, icon_dict.css_class,
  375. icon_dict.on_click_fn, icon_dict.prepend, icon_dict.hide);
  376. }
  377. },
  378. /**
  379. * Update icons.
  380. */
  381. update_icons: function() {},
  382. /**
  383. * Hide drawable's contents.
  384. */
  385. hide_contents: function () {},
  386. /**
  387. * Show drawable's contents.
  388. */
  389. show_contents: function() {},
  390. /**
  391. * Returns a shallow copy of all drawables in this drawable.
  392. */
  393. get_drawables: function() {}
  394. });
  395. /**
  396. * A collection of drawable objects.
  397. */
  398. var DrawableCollection = function(view, container, obj_dict) {
  399. Drawable.call(this, view, container, obj_dict);
  400. // Attribute init.
  401. this.obj_type = obj_dict.obj_type;
  402. this.drawables = [];
  403. };
  404. extend(DrawableCollection.prototype, Drawable.prototype, {
  405. /**
  406. * Unpack and add drawables to the collection.
  407. */
  408. unpack_drawables: function(drawables_array) {
  409. // Add drawables to collection.
  410. this.drawables = [];
  411. var drawable;
  412. for (var i = 0; i < drawables_array.length; i++) {
  413. drawable = object_from_template(drawables_array[i], this.view, this);
  414. this.add_drawable(drawable);
  415. }
  416. },
  417. /**
  418. * Init each drawable in the collection.
  419. */
  420. init: function() {
  421. for (var i = 0; i < this.drawables.length; i++) {
  422. this.drawables[i].init();
  423. }
  424. },
  425. /**
  426. * Draw each drawable in the collection.
  427. */
  428. _draw: function() {
  429. for (var i = 0; i < this.drawables.length; i++) {
  430. this.drawables[i]._draw();
  431. }
  432. },
  433. /**
  434. * Returns representation of object in a dictionary for easy saving.
  435. * Use from_dict to recreate object.
  436. */
  437. to_dict: function() {
  438. var dictified_drawables = [];
  439. for (var i = 0; i < this.drawables.length; i++) {
  440. dictified_drawables.push(this.drawables[i].to_dict());
  441. }
  442. return {
  443. name: this.name,
  444. prefs: this.prefs,
  445. obj_type: this.obj_type,
  446. drawables: dictified_drawables
  447. };
  448. },
  449. /**
  450. * Add a drawable to the end of the collection.
  451. */
  452. add_drawable: function(drawable) {
  453. this.drawables.push(drawable);
  454. drawable.container = this;
  455. this.changed();
  456. },
  457. /**
  458. * Add a drawable before another drawable.
  459. */
  460. add_drawable_before: function(drawable, other) {
  461. this.changed();
  462. var index = this.drawables.indexOf(other);
  463. if (index !== -1) {
  464. this.drawables.splice(index, 0, drawable);
  465. return true;
  466. }
  467. return false;
  468. },
  469. /**
  470. * Replace one drawable with another.
  471. */
  472. replace_drawable: function(old_drawable, new_drawable, update_html) {
  473. var index = this.drawables.indexOf(old_drawable);
  474. if (index !== -1) {
  475. this.drawables[index] = new_drawable;
  476. if (update_html) {
  477. old_drawable.container_div.replaceWith(new_drawable.container_div);
  478. }
  479. this.changed();
  480. }
  481. return index;
  482. },
  483. /**
  484. * Remove drawable from this collection.
  485. */
  486. remove_drawable: function(drawable) {
  487. var index = this.drawables.indexOf(drawable);
  488. if (index !== -1) {
  489. // Found drawable to remove.
  490. this.drawables.splice(index, 1);
  491. drawable.container = null;
  492. this.changed();
  493. return true;
  494. }
  495. return false;
  496. },
  497. /**
  498. * Move drawable to another location in collection.
  499. */
  500. move_drawable: function(drawable, new_position) {
  501. var index = this.drawables.indexOf(drawable);
  502. if (index !== -1) {
  503. // Remove from current position:
  504. this.drawables.splice(index, 1);
  505. // insert into new position:
  506. this.drawables.splice(new_position, 0, drawable);
  507. this.changed();
  508. return true;
  509. }
  510. return false;
  511. },
  512. /**
  513. * Returns all drawables in this drawable.
  514. */
  515. get_drawables: function() {
  516. return this.drawables;
  517. }
  518. });
  519. /**
  520. * A group of drawables that are moveable, visible.
  521. */
  522. var DrawableGroup = function(view, container, obj_dict) {
  523. extend(obj_dict, {
  524. obj_type: "DrawableGroup",
  525. drag_handle_class: "group-handle"
  526. });
  527. DrawableCollection.call(this, view, container, obj_dict);
  528. // Set up containers/moving for group: register both container_div and content div as container
  529. // because both are used as containers (container div to recognize container, content_div to
  530. // store elements). Group can be moved.
  531. this.content_div = $("<div/>").addClass("content-div").attr("id", "group_" + this.id + "_content_div").appendTo(this.container_div);
  532. is_container(this.container_div, this);
  533. is_container(this.content_div, this);
  534. moveable(this.container_div, this.drag_handle_class, ".group", this);
  535. // Set up filters.
  536. this.filters_manager = new filters_mod.FiltersManager(this);
  537. this.header_div.after(this.filters_manager.parent_div);
  538. // For saving drawables' filter managers when group-level filtering is done:
  539. this.saved_filters_managers = [];
  540. // Add drawables.
  541. if ('drawables' in obj_dict) {
  542. this.unpack_drawables(obj_dict.drawables);
  543. }
  544. // Restore filters.
  545. if ('filters' in obj_dict) {
  546. // FIXME: Pass collection_dict to DrawableCollection/Drawable will make this easier.
  547. var old_manager = this.filters_manager;
  548. this.filters_manager = new filters_mod.FiltersManager(this, obj_dict.filters);
  549. old_manager.parent_div.replaceWith(this.filters_manager.parent_div);
  550. if (obj_dict.filters.visible) {
  551. this.setup_multitrack_filtering();
  552. }
  553. }
  554. };
  555. extend(DrawableGroup.prototype, Drawable.prototype, DrawableCollection.prototype, {
  556. action_icons_def: [
  557. Drawable.prototype.action_icons_def[0],
  558. Drawable.prototype.action_icons_def[1],
  559. // Replace group with composite track.
  560. {
  561. name: "composite_icon",
  562. title: "Show composite track",
  563. css_class: "layers-stack",
  564. on_click_fn: function(group) {
  565. $(".bs-tooltip").remove();
  566. group.show_composite_track();
  567. }
  568. },
  569. // Toggle track filters.
  570. {
  571. name: "filters_icon",
  572. title: "Filters",
  573. css_class: "filters-icon",
  574. on_click_fn: function(group) {
  575. // TODO: update tipsy text.
  576. if (group.filters_manager.visible()) {
  577. // Hiding filters.
  578. group.filters_manager.clear_filters();
  579. group._restore_filter_managers();
  580. // TODO: maintain current filter by restoring and setting saved manager's
  581. // settings to current/shared manager's settings.
  582. // TODO: need to restore filter managers when moving drawable outside group.
  583. }
  584. else {
  585. // Showing filters.
  586. group.setup_multitrack_filtering();
  587. group.request_draw(true);
  588. }
  589. group.filters_manager.toggle();
  590. }
  591. },
  592. Drawable.prototype.action_icons_def[2]
  593. ],
  594. build_container_div: function() {
  595. var container_div = $("<div/>").addClass("group").attr("id", "group_" + this.id);
  596. if (this.container) {
  597. this.container.content_div.append(container_div);
  598. }
  599. return container_div;
  600. },
  601. build_header_div: function() {
  602. var header_div = $("<div/>").addClass("track-header");
  603. header_div.append($("<div/>").addClass(this.drag_handle_class));
  604. this.name_div = $("<div/>").addClass("track-name").text(this.name).appendTo(header_div);
  605. return header_div;
  606. },
  607. hide_contents: function () {
  608. this.tiles_div.hide();
  609. },
  610. show_contents: function() {
  611. // Show the contents div and labels (if present)
  612. this.tiles_div.show();
  613. // Request a redraw of the content
  614. this.request_draw();
  615. },
  616. update_icons: function() {
  617. //
  618. // Handle update when there are no tracks.
  619. //
  620. var num_drawables = this.drawables.length;
  621. if (num_drawables === 0) {
  622. this.action_icons.composite_icon.hide();
  623. this.action_icons.filters_icon.hide();
  624. }
  625. else if (num_drawables === 1) {
  626. if (this.drawables[0] instanceof CompositeTrack) {
  627. this.action_icons.composite_icon.show();
  628. }
  629. this.action_icons.filters_icon.hide();
  630. }
  631. else { // There are 2 or more tracks.
  632. //
  633. // Determine if a composite track can be created. Current criteria:
  634. // (a) all tracks are the same;
  635. // OR
  636. // (b) there is a single FeatureTrack.
  637. //
  638. /// All tracks the same?
  639. var i, j, drawable,
  640. same_type = true,
  641. a_type = this.drawables[0].get_type(),
  642. num_feature_tracks = 0;
  643. for (i = 0; i < num_drawables; i++) {
  644. drawable = this.drawables[i];
  645. if (drawable.get_type() !== a_type) {
  646. can_composite = false;
  647. break;
  648. }
  649. if (drawable instanceof FeatureTrack) {
  650. num_feature_tracks++;
  651. }
  652. }
  653. if (same_type || num_feature_tracks === 1) {
  654. this.action_icons.composite_icon.show();
  655. }
  656. else {
  657. this.action_icons.composite_icon.hide();
  658. $(".bs-tooltip").remove();
  659. }
  660. //
  661. // Set up group-level filtering and update filter icon.
  662. //
  663. if (num_feature_tracks > 1 && num_feature_tracks === this.drawables.length) {
  664. //
  665. // Find shared filters.
  666. //
  667. var shared_filters = {},
  668. filter;
  669. // Init shared filters with filters from first drawable.
  670. drawable = this.drawables[0];
  671. for (j = 0; j < drawable.filters_manager.filters.length; j++) {
  672. filter = drawable.filters_manager.filters[j];
  673. shared_filters[filter.name] = [filter];
  674. }
  675. // Create lists of shared filters.
  676. for (i = 1; i < this.drawables.length; i++) {
  677. drawable = this.drawables[i];
  678. for (j = 0; j < drawable.filters_manager.filters.length; j++) {
  679. filter = drawable.filters_manager.filters[j];
  680. if (filter.name in shared_filters) {
  681. shared_filters[filter.name].push(filter);
  682. }
  683. }
  684. }
  685. //
  686. // Create filters for shared filters manager. Shared filters manager is group's
  687. // manager.
  688. //
  689. this.filters_manager.remove_all();
  690. var
  691. filters,
  692. new_filter,
  693. min,
  694. max;
  695. for (var filter_name in shared_filters) {
  696. filters = shared_filters[filter_name];
  697. if (filters.length === num_feature_tracks) {
  698. // Add new filter.
  699. // FIXME: can filter.copy() be used?
  700. new_filter = new filters_mod.NumberFilter( {
  701. name: filters[0].name,
  702. index: filters[0].index
  703. } );
  704. this.filters_manager.add_filter(new_filter);
  705. }
  706. }
  707. // Show/hide icon based on filter availability.
  708. if (this.filters_manager.filters.length > 0) {
  709. this.action_icons.filters_icon.show();
  710. }
  711. else {
  712. this.action_icons.filters_icon.hide();
  713. }
  714. }
  715. else {
  716. this.action_icons.filters_icon.hide();
  717. }
  718. }
  719. },
  720. /**
  721. * Restore individual track filter managers.
  722. */
  723. _restore_filter_managers: function() {
  724. for (var i = 0; i < this.drawables.length; i++) {
  725. this.drawables[i].filters_manager = this.saved_filters_managers[i];
  726. }
  727. this.saved_filters_managers = [];
  728. },
  729. /**
  730. *
  731. */
  732. setup_multitrack_filtering: function() {
  733. // Save tracks' managers and set up shared manager.
  734. if (this.filters_manager.filters.length > 0) {
  735. // For all tracks, save current filter manager and set manager to shared (this object's) manager.
  736. this.saved_filters_managers = [];
  737. for (var i = 0; i < this.drawables.length; i++) {
  738. drawable = this.drawables[i];
  739. this.saved_filters_managers.push(drawable.filters_manager);
  740. drawable.filters_manager = this.filters_manager;
  741. }
  742. //TODO: hide filters icons for each drawable?
  743. }
  744. this.filters_manager.init_filters();
  745. },
  746. /**
  747. * Replace group with a single composite track that includes all group's tracks.
  748. */
  749. show_composite_track: function() {
  750. // Create composite track name.
  751. var drawables_names = [];
  752. for (var i = 0; i < this.drawables.length; i++) {
  753. drawables_names.push(this.drawables[i].name);
  754. }
  755. var new_track_name = "Composite Track of " + this.drawables.length + " tracks (" + drawables_names.join(", ") + ")";
  756. // Replace this group with composite track.
  757. var composite_track = new CompositeTrack(this.view, this.view, {
  758. name: new_track_name,
  759. drawables: this.drawables
  760. });
  761. var index = this.container.replace_drawable(this, composite_track, true);
  762. composite_track.request_draw();
  763. },
  764. add_drawable: function(drawable) {
  765. DrawableCollection.prototype.add_drawable.call(this, drawable);
  766. this.update_icons();
  767. },
  768. remove_drawable: function(drawable) {
  769. DrawableCollection.prototype.remove_drawable.call(this, drawable);
  770. this.update_icons();
  771. },
  772. to_dict: function() {
  773. // If filters are visible, need to restore original filter managers before converting to dict.
  774. if (this.filters_manager.visible()) {
  775. this._restore_filter_managers();
  776. }
  777. var obj_dict = extend(DrawableCollection.prototype.to_dict.call(this), { "filters": this.filters_manager.to_dict() });
  778. // Setup multi-track filtering again.
  779. if (this.filters_manager.visible()) {
  780. this.setup_multitrack_filtering();
  781. }
  782. return obj_dict;
  783. },
  784. request_draw: function(clear_after, force) {
  785. for (var i = 0; i < this.drawables.length; i++) {
  786. this.drawables[i].request_draw(clear_after, force);
  787. }
  788. }
  789. });
  790. /**
  791. * View object manages complete viz view, including tracks and user interactions.
  792. * Events triggered:
  793. * navigate: when browser view changes to a new locations
  794. */
  795. var View = function(obj_dict) {
  796. extend(obj_dict, {
  797. obj_type: "View"
  798. });
  799. DrawableCollection.call(this, "View", obj_dict.container, obj_dict);
  800. this.chrom = null;
  801. this.vis_id = obj_dict.vis_id;
  802. this.dbkey = obj_dict.dbkey;
  803. this.label_tracks = [];
  804. this.tracks_to_be_redrawn = [];
  805. this.max_low = 0;
  806. this.max_high = 0;
  807. this.zoom_factor = 3;
  808. this.min_separation = 30;
  809. this.has_changes = false;
  810. // Deferred object that indicates when view's chrom data has been loaded.
  811. this.load_chroms_deferred = null;
  812. this.init();
  813. this.canvas_manager = new visualization.CanvasManager( this.container.get(0).ownerDocument );
  814. this.reset();
  815. };
  816. _.extend( View.prototype, Backbone.Events);
  817. extend( View.prototype, DrawableCollection.prototype, {
  818. init: function() {
  819. // Attribute init.
  820. this.requested_redraw = false;
  821. // Create DOM elements
  822. var parent_element = this.container,
  823. view = this;
  824. // Top container for things that are fixed at the top
  825. this.top_container = $("<div/>").addClass("top-container").appendTo(parent_element);
  826. // Browser content, primary tracks are contained in here
  827. this.browser_content_div = $("<div/>").addClass("content").css("position", "relative").appendTo(parent_element);
  828. // Bottom container for things that are fixed at the bottom
  829. this.bottom_container = $("<div/>").addClass("bottom-container").appendTo(parent_element);
  830. // Label track fixed at top
  831. this.top_labeltrack = $("<div/>").addClass("top-labeltrack").appendTo(this.top_container);
  832. // Viewport for dragging tracks in center
  833. this.viewport_container = $("<div/>").addClass("viewport-container").attr("id", "viewport-container").appendTo(this.browser_content_div);
  834. // Alias viewport_container as content_div so that it matches function of DrawableCollection/Group content_div.
  835. this.content_div = this.viewport_container;
  836. is_container(this.viewport_container, view);
  837. // Introduction div shown when there are no tracks.
  838. this.intro_div = $("<div/>").addClass("intro").appendTo(this.viewport_container).hide();
  839. var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
  840. add_datasets(add_datasets_url, add_track_async_url, function(tracks) {
  841. _.each(tracks, function(track) {
  842. view.add_drawable( object_from_template(track, view, view) );
  843. });
  844. });
  845. });
  846. // Another label track at bottom
  847. this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);
  848. // Navigation at top
  849. this.nav_container = $("<div/>").addClass("trackster-nav-container").prependTo(this.top_container);
  850. this.nav = $("<div/>").addClass("trackster-nav").appendTo(this.nav_container);
  851. // Overview (scrollbar and overview plot) at bottom
  852. this.overview = $("<div/>").addClass("overview").appendTo(this.bottom_container);
  853. this.overview_viewport = $("<div/>").addClass("overview-viewport").appendTo(this.overview);
  854. this.overview_close = $("<a/>").attr("href", "javascript:void(0);").attr("title", "Close overview").addClass("icon-button overview-close tooltip").hide().appendTo(this.overview_viewport);
  855. this.overview_highlight = $("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);
  856. this.overview_box_background = $("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);
  857. this.overview_box = $("<div/>").addClass("overview-box").appendTo(this.overview_viewport);
  858. this.default_overview_height = this.overview_box.height();
  859. this.nav_controls = $("<div/>").addClass("nav-controls").appendTo(this.nav);
  860. this.chrom_select = $("<select/>").attr({ "name": "chrom"}).css("width", "15em").append("<option value=''>Loading</option>").appendTo(this.nav_controls);
  861. var submit_nav = function(e) {
  862. if (e.type === "focusout" || (e.keyCode || e.which) === 13 || (e.keyCode || e.which) === 27 ) {
  863. if ((e.keyCode || e.which) !== 27) { // Not escape key
  864. view.go_to( $(this).val() );
  865. }
  866. $(this).hide();
  867. $(this).val('');
  868. view.location_span.show();
  869. view.chrom_select.show();
  870. }
  871. };
  872. this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keyup focusout", submit_nav).appendTo(this.nav_controls);
  873. this.location_span = $("<span/>").addClass("location").attr('original-title', 'Click to change location').tooltip( { placement: 'bottom' } ).appendTo(this.nav_controls);
  874. this.location_span.click(function() {
  875. view.location_span.hide();
  876. view.chrom_select.hide();
  877. view.nav_input.val(view.chrom + ":" + view.low + "-" + view.high);
  878. view.nav_input.css("display", "inline-block");
  879. view.nav_input.select();
  880. view.nav_input.focus();
  881. // Set up autocomplete for tracks' features.
  882. view.nav_input.autocomplete({
  883. source: function(request, response) {
  884. // Using current text, query each track and create list of all matching features.
  885. var all_features = [],
  886. feature_search_deferreds = $.map(view.get_drawables(), function(drawable) {
  887. return drawable.data_manager.search_features(request.term).success(function(dataset_features) {
  888. all_features = all_features.concat(dataset_features);
  889. });
  890. });
  891. // When all searching is done, fill autocomplete.
  892. $.when.apply($, feature_search_deferreds).done(function() {
  893. response($.map(all_features, function(feature) {
  894. return {
  895. label: feature[0],
  896. value: feature[1]
  897. };
  898. }));
  899. });
  900. }
  901. });
  902. });
  903. if (this.vis_id !== undefined) {
  904. this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.nav_controls);
  905. }
  906. this.zo_link = $("<a/>").attr("id", "zoom-out").attr("title", "Zoom out").tooltip( {placement: 'bottom'} )
  907. .click(function() { view.zoom_out(); view.request_redraw(); }).appendTo(this.nav_controls);
  908. this.zi_link = $("<a/>").attr("id", "zoom-in").attr("title", "Zoom in").tooltip( {placement: 'bottom'} )
  909. .click(function() { view.zoom_in(); view.request_redraw(); }).appendTo(this.nav_controls);
  910. // Get initial set of chroms.
  911. this.load_chroms_deferred = this.load_chroms({low: 0});
  912. this.chrom_select.bind("change", function() {
  913. view.change_chrom(view.chrom_select.val());
  914. });
  915. /*
  916. this.browser_content_div.bind("mousewheel", function( e, delta ) {
  917. if (Math.abs(delta) < 0.5) {
  918. return;
  919. }
  920. if (delta > 0) {
  921. view.zoom_in(e.pageX, this.viewport_container);
  922. } else {
  923. view.zoom_out();
  924. }
  925. e.preventDefault();
  926. });
  927. */
  928. // Blur tool/filter inputs when user clicks on content div.
  929. this.browser_content_div.click(function( e ) {
  930. $(this).find("input").trigger("blur");
  931. });
  932. // Double clicking zooms in
  933. this.browser_content_div.bind("dblclick", function( e ) {
  934. view.zoom_in(e.pageX, this.viewport_container);
  935. });
  936. // Dragging the overview box (~ horizontal scroll bar)
  937. this.overview_box.bind("dragstart", function( e, d ) {
  938. this.current_x = d.offsetX;
  939. }).bind("drag", function( e, d ) {
  940. var delta = d.offsetX - this.current_x;
  941. this.current_x = d.offsetX;
  942. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.max_high - view.max_low) );
  943. view.move_delta(-delta_chrom);
  944. });
  945. this.overview_close.click(function() {
  946. view.reset_overview();
  947. });
  948. // Dragging in the viewport scrolls
  949. this.viewport_container.bind( "draginit", function( e, d ) {
  950. // Disable interaction if started in scrollbar (for webkit)
  951. if ( e.clientX > view.viewport_container.width() - 16 ) {
  952. return false;
  953. }
  954. }).bind( "dragstart", function( e, d ) {
  955. d.original_low = view.low;
  956. d.current_height = e.clientY;
  957. d.current_x = d.offsetX;
  958. }).bind( "drag", function( e, d ) {
  959. var container = $(this);
  960. var delta = d.offsetX - d.current_x;
  961. var new_scroll = container.scrollTop() - (e.clientY - d.current_height);
  962. container.scrollTop(new_scroll);
  963. d.current_height = e.clientY;
  964. d.current_x = d.offsetX;
  965. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low));
  966. view.move_delta(delta_chrom);
  967. // Also capture mouse wheel for left/right scrolling
  968. }).bind( 'mousewheel', function( e, d, dx, dy ) {
  969. // Only act on x axis scrolling if we see if, y will be i
  970. // handled by the browser when the event bubbles up
  971. if ( dx ) {
  972. dx *= 50;
  973. var delta_chrom = Math.round( - dx / view.viewport_container.width() * (view.high - view.low) );
  974. view.move_delta( delta_chrom );
  975. }
  976. });
  977. // Dragging in the top label track allows selecting a region
  978. // to zoom in
  979. this.top_labeltrack.bind( "dragstart", function( e, d ) {
  980. return $("<div />").css( {
  981. "height": view.browser_content_div.height() + view.top_labeltrack.height() + view.nav_labeltrack.height() + 1,
  982. "top": "0px",
  983. "position": "absolute",
  984. "background-color": "#ccf",
  985. "opacity": 0.5,
  986. "z-index": 1000
  987. } ).appendTo( $(this) );
  988. }).bind( "drag", function( e, d ) {
  989. $( d.proxy ).css({ left: Math.min( e.pageX, d.startX ) - view.container.offset().left, width: Math.abs( e.pageX - d.startX ) });
  990. var min = Math.min(e.pageX, d.startX ) - view.container.offset().left,
  991. max = Math.max(e.pageX, d.startX ) - view.container.offset().left,
  992. span = (view.high - view.low),
  993. width = view.viewport_container.width();
  994. view.update_location( Math.round(min / width * span) + view.low,
  995. Math.round(max / width * span) + view.low );
  996. }).bind( "dragend", function( e, d ) {
  997. var min = Math.min(e.pageX, d.startX),
  998. max = Math.max(e.pageX, d.startX),
  999. span = (view.high - view.low),
  1000. width = view.viewport_container.width(),
  1001. old_low = view.low;
  1002. view.low = Math.round(min / width * span) + old_low;
  1003. view.high = Math.round(max / width * span) + old_low;
  1004. $(d.proxy).remove();
  1005. view.request_redraw();
  1006. });
  1007. this.add_label_track( new LabelTrack( this, { content_div: this.top_labeltrack } ) );
  1008. this.add_label_track( new LabelTrack( this, { content_div: this.nav_labeltrack } ) );
  1009. $(window).bind("resize", function() {
  1010. // Stop previous timer.
  1011. if (this.resize_timer) {
  1012. clearTimeout(this.resize_timer);
  1013. }
  1014. // When function activated, resize window and redraw.
  1015. this.resize_timer = setTimeout(function () {
  1016. view.resize_window();
  1017. }, 500 );
  1018. });
  1019. $(document).bind("redraw", function() { view.redraw(); });
  1020. this.reset();
  1021. $(window).trigger("resize");
  1022. },
  1023. changed: function() {
  1024. this.has_changes = true;
  1025. },
  1026. /** Add or remove intro div depending on view state. */
  1027. update_intro_div: function() {
  1028. if (this.drawables.length === 0) {
  1029. this.intro_div.show();
  1030. }
  1031. else {
  1032. this.intro_div.hide();
  1033. }
  1034. },
  1035. /**
  1036. * Triggers navigate events as needed. If there is a delay,
  1037. * then event is triggered only after navigation has stopped.
  1038. */
  1039. trigger_navigate: function(new_chrom, new_low, new_high, delay) {
  1040. // Stop previous timer.
  1041. if (this.timer) {
  1042. clearTimeout(this.timer);
  1043. }
  1044. if (delay) {
  1045. // To aggregate calls, use timer and only navigate once
  1046. // location has stabilized.
  1047. var self = this;
  1048. this.timer = setTimeout(function () {
  1049. self.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
  1050. }, 500 );
  1051. }
  1052. else {
  1053. view.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
  1054. }
  1055. },
  1056. update_location: function(low, high) {
  1057. this.location_span.text( commatize(low) + ' - ' + commatize(high) );
  1058. this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
  1059. // Update location. Only update when there is a valid chrom; when loading vis, there may
  1060. // not be a valid chrom.
  1061. var chrom = view.chrom_select.val();
  1062. if (chrom !== "") {
  1063. this.trigger_navigate(chrom, view.low, view.high, true);
  1064. }
  1065. },
  1066. /**
  1067. * Load chrom data for the view. Returns a jQuery Deferred.
  1068. */
  1069. load_chroms: function(url_parms) {
  1070. url_parms.num = MAX_CHROMS_SELECTABLE;
  1071. var
  1072. view = this,
  1073. chrom_data = $.Deferred();
  1074. $.ajax({
  1075. url: chrom_url + "/" + this.dbkey,
  1076. data: url_parms,
  1077. dataType: "json",
  1078. success: function (result) {
  1079. // Do nothing if could not load chroms.
  1080. if (result.chrom_info.length === 0) {
  1081. return;
  1082. }
  1083. // Load chroms.
  1084. if (result.reference) {
  1085. view.add_label_track( new ReferenceTrack(view) );
  1086. }
  1087. view.chrom_data = result.chrom_info;
  1088. var chrom_options = '<option value="">Select Chrom/Contig</option>';
  1089. for (var i = 0, len = view.chrom_data.length; i < len; i++) {
  1090. var chrom = view.chrom_data[i].chrom;
  1091. chrom_options += '<option value="' + chrom + '">' + chrom + '</option>';
  1092. }
  1093. if (result.prev_chroms) {
  1094. chrom_options += '<option value="previous">Previous ' + MAX_CHROMS_SELECTABLE + '</option>';
  1095. }
  1096. if (result.next_chroms) {
  1097. chrom_options += '<option value="next">Next ' + MAX_CHROMS_SELECTABLE + '</option>';
  1098. }
  1099. view.chrom_select.html(chrom_options);
  1100. view.chrom_start_index = result.start_index;
  1101. chrom_data.resolve(result);
  1102. },
  1103. error: function() {
  1104. alert("Could not load chroms for this dbkey:", view.dbkey);
  1105. }
  1106. });
  1107. return chrom_data;
  1108. },
  1109. change_chrom: function(chrom, low, high) {
  1110. var view = this;
  1111. // If chrom data is still loading, wait for it.
  1112. if (!view.chrom_data) {
  1113. view.load_chroms_deferred.then(function() {
  1114. view.change_chrom(chrom, low, high);
  1115. });
  1116. return;
  1117. }
  1118. // Don't do anything if chrom is "None" (hackish but some browsers already have this set), or null/blank
  1119. if (!chrom || chrom === "None") {
  1120. return;
  1121. }
  1122. //
  1123. // If user is navigating to previous/next set of chroms, load new chrom set and return.
  1124. //
  1125. if (chrom === "previous") {
  1126. view.load_chroms({low: this.chrom_start_index - MAX_CHROMS_SELECTABLE});
  1127. return;
  1128. }
  1129. if (chrom === "next") {
  1130. view.load_chroms({low: this.chrom_start_index + MAX_CHROMS_SELECTABLE});
  1131. return;
  1132. }
  1133. //
  1134. // User is loading a particular chrom. Look first in current set; if not in current set, load new
  1135. // chrom set.
  1136. //
  1137. var found = $.grep(view.chrom_data, function(v, i) {
  1138. return v.chrom === chrom;
  1139. })[0];
  1140. if (found === undefined) {
  1141. // Try to load chrom and then change to chrom.
  1142. view.load_chroms({'chrom': chrom}, function() { view.change_chrom(chrom, low, high); });
  1143. return;
  1144. }
  1145. else {
  1146. // Switching to local chrom.
  1147. if (chrom !== view.chrom) {
  1148. view.chrom = chrom;
  1149. view.chrom_select.val(view.chrom);
  1150. view.max_high = found.len-1; // -1 because we're using 0-based indexing.
  1151. view.reset();
  1152. view.request_redraw(true);
  1153. for (var i = 0, len = view.drawables.length; i < len; i++) {
  1154. var drawable = view.drawables[i];
  1155. if (drawable.init) {
  1156. drawable.init();
  1157. }
  1158. }
  1159. if (view.reference_track) {
  1160. view.reference_track.init();
  1161. }
  1162. }
  1163. if (low !== undefined && high !== undefined) {
  1164. view.low = Math.max(low, 0);
  1165. view.high = Math.min(high, view.max_high);
  1166. }
  1167. else {
  1168. // Low and high undefined, so view is whole chome.
  1169. view.low = 0;
  1170. view.high = view.max_high;
  1171. }
  1172. view.reset_overview();
  1173. view.request_redraw();
  1174. }
  1175. },
  1176. go_to: function(str) {
  1177. // Preprocess str to remove spaces and commas.
  1178. str = str.replace(/ |,/g, "");
  1179. // Go to new location.
  1180. var view = this,
  1181. new_low,
  1182. new_high,
  1183. chrom_pos = str.split(":"),
  1184. chrom = chrom_pos[0],
  1185. pos = chrom_pos[1];
  1186. if (pos !== undefined) {
  1187. try {
  1188. var pos_split = pos.split("-");
  1189. new_low = parseInt(pos_split[0], 10);
  1190. new_high = parseInt(pos_split[1], 10);
  1191. } catch (e) {
  1192. return false;
  1193. }
  1194. }
  1195. view.change_chrom(chrom, new_low, new_high);
  1196. },
  1197. move_fraction: function(fraction) {
  1198. var view = this;
  1199. var span = view.high - view.low;
  1200. this.move_delta(fraction * span);
  1201. },
  1202. move_delta: function(delta_chrom) {
  1203. // Update low, high.
  1204. var view = this;
  1205. var current_chrom_span = view.high - view.low;
  1206. // Check for left and right boundaries
  1207. if (vie

Large files files are truncated, but you can click here to view the full file