PageRenderTime 55ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/client/galaxy/scripts/viz/trackster/tracks.js

https://bitbucket.org/remy_d1/galaxy-central-manageapi
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

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

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