PageRenderTime 80ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 1ms

/static/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
  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);
  1187. },
  1188. move_fraction: function(fraction) {
  1189. var view = this;
  1190. var span = view.high - view.low;
  1191. this.move_delta(fraction * span);
  1192. },
  1193. move_delta: function(delta_chrom) {
  1194. //
  1195. // Update low, high.
  1196. //
  1197. var view = this;
  1198. var current_chrom_span = view.high - view.low;
  1199. // Check for left and right boundaries
  1200. if (view.low - delta_chrom < view.max_low) {
  1201. view.low = view.max_low;
  1202. view.high = view.max_low + current_chrom_span;
  1203. } else if (view.high - delta_chrom > view.max_high) {
  1204. view.high = view.max_high;
  1205. view.low = view.max_high - current_chrom_span;
  1206. } else {
  1207. view.high -= delta_chrom;
  1208. view.low -= delta_chrom;
  1209. }
  1210. //
  1211. // Redraw view.
  1212. //
  1213. // Redraw without requesting more data immediately.
  1214. view.request_redraw({ data_fetch: false });
  1215. // Set up timeout to redraw with more data when moving stops.
  1216. if (this.redraw_on_move_fn) {
  1217. clearTimeout(this.redraw_on_move_fn);
  1218. }
  1219. this.redraw_on_move_fn = setTimeout(function() {
  1220. view.request_redraw();
  1221. }, 200);
  1222. // Navigate.
  1223. var chrom = view.chrom_select.val();
  1224. this.trigger_navigate(chrom, view.low, view.high, true);
  1225. },
  1226. /**
  1227. * Add a drawable to the view.
  1228. */
  1229. add_drawable: function(drawable) {
  1230. DrawableCollection.prototype.add_drawable.call(this, drawable);
  1231. drawable.init();
  1232. this.changed();
  1233. this.update_intro_div();
  1234. // When drawable config changes, mark view as changed. This
  1235. // captures most (all?) state change that needs to be saved.
  1236. var self = this;
  1237. drawable.config.on('change', function() {
  1238. self.changed();
  1239. });
  1240. },
  1241. add_label_track: function (label_track) {
  1242. label_track.view = this;
  1243. label_track.init();
  1244. this.label_tracks.push(label_track);
  1245. },
  1246. /**
  1247. * Remove drawable from the view.
  1248. */
  1249. remove_drawable: function(drawable, hide) {
  1250. DrawableCollection.prototype.remove_drawable.call(this, drawable);
  1251. if (hide) {
  1252. var view = this;
  1253. drawable.container_div.hide(0, function() {
  1254. $(this).remove();
  1255. view.update_intro_div();
  1256. });
  1257. }
  1258. },
  1259. reset: function() {
  1260. this.low = this.max_low;
  1261. this.high = this.max_high;
  1262. this.viewport_container.find(".yaxislabel").remove();
  1263. },
  1264. /**
  1265. * Request that view redraw one or more of view's drawables. If drawable is not specified,
  1266. * all drawables are redrawn.
  1267. */
  1268. request_redraw: function(options, drawable) {
  1269. var view = this,
  1270. // Either redrawing a single drawable or all view's drawables.
  1271. track_list = (drawable ? [drawable] : view.drawables);
  1272. // Add/update tracks in track list to redraw list.
  1273. _.each(track_list, function(track) {
  1274. var track_options = _.find(view.tracks_to_be_redrawn, function(to) {
  1275. return to[0] === track;
  1276. });
  1277. if (track_options) {
  1278. // Track already in list; update options.
  1279. track_options[1] = options;
  1280. }
  1281. else {
  1282. // Track not in list yet.
  1283. view.tracks_to_be_redrawn.push([track, options]);
  1284. }
  1285. });
  1286. // Set up redraw if it has not been requested since last redraw.
  1287. if (!this.requested_redraw) {
  1288. requestAnimationFrame(function() { view._redraw(); });
  1289. this.requested_redraw = true;
  1290. }
  1291. },
  1292. /**
  1293. * Redraws view and tracks.
  1294. * NOTE: this method should never be called directly; request_redraw() should be used so
  1295. * that requestAnimationFrame can manage redrawing.
  1296. */
  1297. _redraw: function() {
  1298. // TODO: move this code to function that does location setting.
  1299. // Clear because requested redraw is being handled now.
  1300. this.requested_redraw = false;
  1301. var low = this.low,
  1302. high = this.high;
  1303. if (low < this.max_low) {
  1304. low = this.max_low;
  1305. }
  1306. if (high > this.max_high) {
  1307. high = this.max_high;
  1308. }
  1309. var span = this.high - this.low;
  1310. if (this.high !== 0 && span < this.min_separation) {
  1311. high = low + this.min_separation;
  1312. }
  1313. this.low = Math.floor(low);
  1314. this.high = Math.ceil(high);
  1315. this.update_location(this.low, this.high);
  1316. // -- Drawing code --
  1317. // Resolution is a pixel density.
  1318. this.resolution_px_b = this.viewport_container.width() / (this.high - this.low);
  1319. // Overview
  1320. var left_px = ( this.low / (this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1321. var width_px = ( (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1322. var min_width_px = 13;
  1323. this.overview_box.css({ left: left_px, width: Math.max(min_width_px, width_px) }).show();
  1324. if (width_px < min_width_px) {
  1325. this.overview_box.css("left", left_px - (min_width_px - width_px)/2);
  1326. }
  1327. if (this.overview_highlight) {
  1328. this.overview_highlight.css({ left: left_px, width: width_px });
  1329. }
  1330. // Draw data tracks.
  1331. _.each(this.tracks_to_be_redrawn, function(track_options) {
  1332. var track = track_options[0],
  1333. options = track_options[1];
  1334. if (track) {
  1335. track._draw(options);
  1336. }
  1337. });
  1338. this.tracks_to_be_redrawn = [];
  1339. // Draw label tracks.
  1340. _.each(this.label_tracks, function(label_track) {
  1341. label_track._draw();
  1342. });
  1343. },
  1344. zoom_in: function (point, container) {
  1345. if (this.max_high === 0 || this.high - this.low <= this.min_separation) {
  1346. return;
  1347. }
  1348. var span = this.high - this.low,
  1349. cur_center = span / 2 + this.low,
  1350. new_half = (span / this.zoom_factor) / 2;
  1351. if (point) {
  1352. cur_center = point / this.viewport_container.width() * (this.high - this.low) + this.low;
  1353. }
  1354. this.low = Math.round(cur_center - new_half);
  1355. this.high = Math.round(cur_center + new_half);
  1356. this.changed();
  1357. this.request_redraw();
  1358. },
  1359. zoom_out: function () {
  1360. if (this.max_high === 0) {
  1361. return;
  1362. }
  1363. var span = this.high - this.low,
  1364. cur_center = span / 2 + this.low,
  1365. new_half = (span * this.zoom_factor) / 2;
  1366. this.low = Math.round(cur_center - new_half);
  1367. this.high = Math.round(cur_center + new_half);
  1368. this.changed();
  1369. this.request_redraw();
  1370. },
  1371. /** Resize viewport. Use this method if header/footer content has changed in size. */
  1372. resize_viewport: function() {
  1373. this.viewport_container.height( this.container.height() - this.top_container.height() - this.bottom_container.height() );
  1374. },
  1375. /** Called when window is resized. */
  1376. resize_window: function() {
  1377. this.resize_viewport();
  1378. this.request_redraw();
  1379. },
  1380. /** Show a Drawable in the overview. */
  1381. set_overview: function(drawable) {
  1382. if (this.overview_drawable) {
  1383. // If drawable to be set as overview is already in overview, do nothing.
  1384. // Otherwise, remove overview.
  1385. if (this.overview_drawable.dataset.id === drawable.dataset.id) {
  1386. return;
  1387. }
  1388. this.overview_viewport.find(".track").remove();
  1389. }
  1390. // Set new overview.
  1391. var
  1392. overview_drawable = drawable.copy( { content_div: this.overview_viewport } ),
  1393. view = this;
  1394. overview_drawable.header_div.hide();
  1395. overview_drawable.is_overview = true;
  1396. view.overview_drawable = overview_drawable;
  1397. this.overview_drawable.postdraw_actions = function() {
  1398. view.overview_highlight.show().height(view.overview_drawable.content_div.height());
  1399. view.overview_viewport.height(view.overview_drawable.content_div.height() + view.overview_box.outerHeight());
  1400. view.overview_close.show();
  1401. view.resize_window();
  1402. };
  1403. view.overview_drawable.request_draw();
  1404. this.changed();
  1405. },
  1406. /** Close and reset overview. */
  1407. reset_overview: function() {
  1408. // Update UI.
  1409. $(".tooltip").remove();
  1410. this.overview_viewport.find(".track-tile").remove();
  1411. this.overview_viewport.height(this.default_overview_height);
  1412. this.overview_box.height(this.default_overview_height);
  1413. this.overview_close.hide();
  1414. this.overview_highlight.hide();
  1415. view.resize_window();
  1416. view.overview_drawable = null;
  1417. }
  1418. });
  1419. /**
  1420. * Encapsulation of a tool that users can apply to tracks/datasets.
  1421. */
  1422. var TracksterTool = tools_mod.Tool.extend({
  1423. defaults: {
  1424. track: null
  1425. },
  1426. initialize: function(options) {
  1427. tools_mod.Tool.prototype.initialize.call(this, options);
  1428. // Restore tool visibility from state; default to hidden.
  1429. var hidden = true;
  1430. if (options.tool_state !== undefined && options.tool_state.hidden !== undefined) {
  1431. hidden = options.tool_state.hidden;
  1432. }
  1433. this.set('hidden', hidden);
  1434. // FIXME: need to restore tool values from options.tool_state
  1435. // HACK: remove some inputs because Trackster does yet not work with them.
  1436. this.remove_inputs( [ 'data', 'hidden_data', 'conditional' ] );
  1437. },
  1438. state_dict: function(options) {
  1439. return _.extend( this.get_inputs_dict(), { hidden: !this.is_visible() } );
  1440. }
  1441. });
  1442. /**
  1443. * View renders tool parameter HTML and updates parameter value as it is changed in the HTML.
  1444. */
  1445. var ToolParameterView = Backbone.View.extend({
  1446. events: {
  1447. 'change :input': 'update_value'
  1448. },
  1449. render: function() {
  1450. var param_div = this.$el.addClass("param-row"),
  1451. param = this.model;
  1452. // Param label.
  1453. var label_div = $("<div>").addClass("param-label").text(param.get('label')).appendTo(param_div);
  1454. // Param HTML.
  1455. var html_div = $("<div/>").addClass("param-input").html(param.get('html')).appendTo(param_div);
  1456. // Set initial value.
  1457. html_div.find(":input").val(param.get('value'));
  1458. // Add to clear floating layout.
  1459. $("<div style='clear: both;'/>").appendTo(param_div);
  1460. },
  1461. update_value: function(update_event) {
  1462. this.model.set_value($(update_event.target).val());
  1463. }
  1464. });
  1465. /**
  1466. * View for TracksterTool.
  1467. */
  1468. var TracksterToolView = Backbone.View.extend({
  1469. initialize: function(options) {
  1470. this.model.on('change:hidden', this.set_visible, this);
  1471. },
  1472. /**
  1473. * Render tool UI.
  1474. */
  1475. render: function() {
  1476. var self = this;
  1477. tool = this.model,
  1478. parent_div = this.$el.addClass("dynamic-tool").hide();
  1479. // Prevent div events from propogating to other elements.
  1480. parent_div.bind("drag", function(e) {
  1481. e.stopPropagation();
  1482. }).click(function(e) {
  1483. e.stopPropagation();
  1484. }).bind("dblclick", function(e) {
  1485. e.stopPropagation();
  1486. }).keydown(function(e) { e.stopPropagation(); });
  1487. // Add name, inputs.
  1488. var name_div = $("<div class='tool-name'>").appendTo(parent_div).text(tool.get('name'));
  1489. tool.get('inputs').each(function(param) {
  1490. // Render parameter.
  1491. var param_view = new ToolParameterView({ model: param });
  1492. param_view.render();
  1493. parent_div.append(param_view.$el);
  1494. });
  1495. // Highlight value for inputs for easy replacement.
  1496. parent_div.find("input").click(function() { $(this).select(); });
  1497. // Add buttons for running on dataset, region.
  1498. var run_tool_row = $("<div>").addClass("param-row").appendTo(parent_div);
  1499. var run_on_dataset_button = $("<input type='submit'>").attr("value", "Run on complete dataset").appendTo(run_tool_row);
  1500. var run_on_region_button = $("<input type='submit'>").attr("value", "Run on visible region").appendTo(run_tool_row);
  1501. run_on_region_button.click( function() {
  1502. // Run tool to create new track.
  1503. self.run_on_region();
  1504. });
  1505. run_on_dataset_button.click( function() {
  1506. self.run_on_dataset();
  1507. });
  1508. if (tool.is_visible()) {
  1509. this.$el.show();
  1510. }
  1511. },
  1512. /**
  1513. * Show or hide tool depending on tool visibility state.
  1514. */
  1515. set_visible: function() {
  1516. this.$el.toggle(this.model.is_visible());
  1517. },
  1518. /**
  1519. * Update tool parameters.
  1520. */
  1521. update_params: function() {
  1522. for (var i = 0; i < this.params.length; i++) {
  1523. this.params[i].update_value();
  1524. }
  1525. },
  1526. /**
  1527. * Run tool on dataset. Output is placed in dataset's history and no changes to viz are made.
  1528. */
  1529. run_on_dataset: function() {
  1530. var tool = this.model;
  1531. this.run(
  1532. // URL params.
  1533. {
  1534. target_dataset_id: this.model.get('track').dataset.id,
  1535. action: 'rerun',
  1536. tool_id: tool.id
  1537. },
  1538. null,
  1539. // Success callback.
  1540. function(track_data) {
  1541. Galaxy.modal.show({title: tool.get('name') + " is Running", body: tool.get('name') + " is running on the complete dataset. Tool outputs are in dataset's history.", buttons : {'Close' : function() { Galaxy.modal.hide(); } } });
  1542. }
  1543. );
  1544. },
  1545. /**
  1546. * Run dataset on visible region. This creates a new track and sets the track's contents
  1547. * to the tool's output.
  1548. */
  1549. run_on_region: function() {
  1550. //
  1551. // Create track for tool's output immediately to provide user feedback.
  1552. //
  1553. var track = this.model.get('track'),
  1554. tool = this.model,
  1555. region = new visualization.GenomeRegion({
  1556. chrom: track.view.chrom,
  1557. start: track.view.low,
  1558. end: track.view.high
  1559. }),
  1560. url_params =
  1561. {
  1562. target_dataset_id: track.dataset.id,
  1563. action: 'rerun',
  1564. tool_id: tool.id,
  1565. regions: [
  1566. region.toJSON()
  1567. ]
  1568. },
  1569. current_track = track,
  1570. // Set name of track to include tool name, parameters, and region used.
  1571. track_name = tool.get('name') +
  1572. current_track.tool_region_and_parameters_str(region),
  1573. container;
  1574. // If track not in a group, create a group for it and add new track to group. If track
  1575. // already in group, add track to group.
  1576. if (current_track.container === view) {
  1577. // Create new group.
  1578. var group = new DrawableGroup(view, view, { name: track.config.get_value('name') });
  1579. // Replace track with group.
  1580. var index = current_track.container.replace_drawable(current_track, group, false);
  1581. // Update HTML.
  1582. // FIXME: this is ugly way to replace a track with a group -- make this easier via
  1583. // a Drawable or DrawableCollection function.
  1584. group.container_div.insertBefore(current_track.view.content_div.children()[index]);
  1585. group.add_drawable(current_track);
  1586. current_track.container_div.appendTo(group.content_div);
  1587. container = group;
  1588. }
  1589. else {
  1590. // Use current group.
  1591. container = current_track.container;
  1592. }
  1593. // Create and init new track.
  1594. var new_track = new current_track.constructor(view, container, {
  1595. name: track_name,
  1596. hda_ldda: "hda"
  1597. });
  1598. new_track.init_for_tool_data();
  1599. new_track.change_mode(current_track.mode);
  1600. new_track.set_filters_manager(current_track.filters_manager.copy(new_track));
  1601. new_track.update_icons();
  1602. container.add_drawable(new_track);
  1603. new_track.tiles_div.text("Starting job.");
  1604. // Run tool.
  1605. this.run(url_params, new_track,
  1606. // Success callback.
  1607. function(track_data) {
  1608. new_track.set_dataset(new data.Dataset(track_data));
  1609. new_track.tiles_div.text("Running job.");
  1610. new_track.init();
  1611. }
  1612. );
  1613. },
  1614. /**
  1615. * Run tool using a set of URL params and a success callback.
  1616. */
  1617. run: function(url_params, new_track, success_callback) {
  1618. // Run tool.
  1619. url_params.inputs = this.model.get_inputs_dict();
  1620. var ss_deferred = new util.ServerStateDeferred({
  1621. ajax_settings: {
  1622. url: galaxy_config.root + "api/tools",
  1623. data: JSON.stringify(url_params),
  1624. dataType: "json",
  1625. contentType: 'application/json',
  1626. type: "POST"
  1627. },
  1628. interval: 2000,
  1629. success_fn: function(response) {
  1630. return response !== "pending";
  1631. }
  1632. });
  1633. // Start with this status message.
  1634. //new_track.container_div.addClass("pending");
  1635. //new_track.content_div.html(DATA_PENDING);
  1636. $.when(ss_deferred.go()).then(function(response) {
  1637. if (response === "no converter") {
  1638. // No converter available for input datasets, so cannot run tool.
  1639. new_track.container_div.addClass("error");
  1640. new_track.content_div.text(DATA_NOCONVERTER);
  1641. }
  1642. else if (response.error) {
  1643. // General error.
  1644. new_track.container_div.addClass("error");
  1645. new_track.content_div.text(DATA_CANNOT_RUN_TOOL + response.message);
  1646. }
  1647. else {
  1648. // Job submitted and running.
  1649. success_callback(response);
  1650. }
  1651. });
  1652. }
  1653. });
  1654. /**
  1655. * Generates scale values based on filter and feature's value for filter.
  1656. */
  1657. var FilterScaler = function(filter, default_val) {
  1658. painters.Scaler.call(this, default_val);
  1659. this.filter = filter;
  1660. };
  1661. FilterScaler.prototype.gen_val = function(feature_data) {
  1662. // If filter is not initalized yet, return default val.
  1663. if (this.filter.high === Number.MAX_VALUE || this.filter.low === -Number.MAX_VALUE || this.filter.low === this.filter.high) {
  1664. return this.default_val;
  1665. }
  1666. // Scaling value is ratio of (filter's value compared to low) to (complete filter range).
  1667. return ( ( parseFloat(feature_data[this.filter.index]) - this.filter.low ) / ( this.filter.high - this.filter.low ) );
  1668. };
  1669. /**
  1670. * Tiles drawn by tracks.
  1671. */
  1672. var Tile = function(track, region, w_scale, canvas, data) {
  1673. this.track = track;
  1674. this.region = region;
  1675. this.low = region.get('start');
  1676. this.high = region.get('end');
  1677. this.w_scale = w_scale;
  1678. this.canvas = canvas;
  1679. // Wrap element in div for background and to provide container for tile-specific elements.
  1680. this.html_elt = $("<div class='track-tile'/>").append(canvas);
  1681. this.data = data;
  1682. this.stale = false;
  1683. };
  1684. /**
  1685. * Perform pre-display actions.
  1686. */
  1687. Tile.prototype.predisplay_actions = function() {};
  1688. var LineTrackTile = function(track, region, w_scale, canvas, data) {
  1689. Tile.call(this, track, region, w_scale, canvas, data);
  1690. };
  1691. LineTrackTile.prototype.predisplay_actions = function() {};
  1692. var FeatureTrackTile = function(track, region, w_scale, canvas, data, mode, message, all_slotted,
  1693. feature_mapper, incomplete_features, seq_data) {
  1694. // Attribute init.
  1695. Tile.call(this, track, region, w_scale, canvas, data);
  1696. this.mode = mode;
  1697. this.all_slotted = all_slotted;
  1698. this.feature_mapper = feature_mapper;
  1699. this.has_icons = false;
  1700. this.incomplete_features = incomplete_features;
  1701. // Features drawn based on data from other tiles.
  1702. this.other_tiles_features_drawn = {};
  1703. this.seq_data = seq_data;
  1704. // Add message + action icons to tile's html.
  1705. /*
  1706. This does not work right now because a random set of reads is returned by the server.
  1707. When the server can respond with more data systematically, renable these icons.
  1708. if (message) {
  1709. this.has_icons = true;
  1710. var
  1711. tile = this;
  1712. canvas = this.html_elt.children()[0],
  1713. message_div = $("<div/>").addClass("tile-message")
  1714. // -1 to account for border.
  1715. .css({'height': ERROR_PADDING, 'width': canvas.width}).prependTo(this.html_elt);
  1716. // Handle message; only message currently is that only the first N elements are displayed.
  1717. var tile_region = new visualization.GenomeRegion({
  1718. chrom: track.view.chrom,
  1719. start: this.low,
  1720. end: this.high
  1721. }),
  1722. num_features = data.length,
  1723. more_down_icon = $("<a/>").addClass("icon more-down")
  1724. .attr("title", "For speed, only the first " + num_features + " features in this region were obtained from server. Click to get more data including depth")
  1725. .tooltip().appendTo(message_div),
  1726. more_across_icon = $("<a/>").addClass("icon more-across")
  1727. .attr("title", "For speed, only the first " + num_features + " features in this region were obtained from server. Click to get more data excluding depth")
  1728. .tooltip().appendTo(message_div);
  1729. // Set up actions for icons.
  1730. more_down_icon.click(function() {
  1731. // Mark tile as stale, request more data, and redraw track.
  1732. tile.stale = true;
  1733. track.data_manager.get_more_data(tile_region, track.mode, 1 / tile.w_scale, {}, track.data_manager.DEEP_DATA_REQ);
  1734. $(".tooltip").hide();
  1735. track.request_draw();
  1736. }).dblclick(function(e) {
  1737. // Do not propogate as this would normally zoom in.
  1738. e.stopPropagation();
  1739. });
  1740. more_across_icon.click(function() {
  1741. // Mark tile as stale, request more data, and redraw track.
  1742. tile.stale = true;
  1743. track.data_manager.get_more_data(tile_region, track.mode, 1 / tile.w_scale, {}, track.data_manager.BROAD_DATA_REQ);
  1744. $(".tooltip").hide();
  1745. track.request_draw();
  1746. }).dblclick(function(e) {
  1747. // Do not propogate as this would normally zoom in.
  1748. e.stopPropagation();
  1749. });
  1750. }
  1751. */
  1752. };
  1753. extend(FeatureTrackTile.prototype, Tile.prototype);
  1754. /**
  1755. * Sets up support for popups.
  1756. */
  1757. FeatureTrackTile.prototype.predisplay_actions = function() {
  1758. /*
  1759. FIXME: use a canvas library to handle popups.
  1760. //
  1761. // Add support for popups.
  1762. //
  1763. var tile = this,
  1764. popups = {};
  1765. // Only show popups in Pack mode.
  1766. if (tile.mode !== "Pack") { return; }
  1767. $(this.html_elt).hover(
  1768. function() {
  1769. this.hovered = true;
  1770. $(this).mousemove();
  1771. },
  1772. function() {
  1773. this.hovered = false;
  1774. // Clear popup if it is still hanging around (this is probably not needed)
  1775. $(this).parents(".track-content").children(".overlay").children(".feature-popup").remove();
  1776. } ).mousemove(function (e) {
  1777. // Use the hover plugin to get a delay before showing popup
  1778. if ( !this.hovered ) { return; }
  1779. // Get feature data for position.
  1780. var
  1781. this_offset = $(this).offset(),
  1782. offsetX = e.pageX - this_offset.left,
  1783. offsetY = e.pageY - this_offset.top,
  1784. feature_data = tile.feature_mapper.get_feature_data(offsetX, offsetY),
  1785. feature_uid = (feature_data ? feature_data[0] : null);
  1786. // Hide visible popup if not over a feature or over a different feature.
  1787. $(this).parents(".track-content").children(".overlay").children(".feature-popup").each(function() {
  1788. if ( !feature_uid ||
  1789. $(this).attr("id") !== feature_uid.toString() ) {
  1790. $(this).remove();
  1791. }
  1792. });
  1793. if (feature_data) {
  1794. // Get or create popup.
  1795. var popup = popups[feature_uid];
  1796. if (!popup) {
  1797. // Create feature's popup element.
  1798. var feature_dict = {
  1799. name: feature_data[3],
  1800. start: feature_data[1],
  1801. end: feature_data[2],
  1802. strand: feature_data[4]
  1803. },
  1804. filters = tile.track.filters_manager.filters,
  1805. filter;
  1806. // Add filter values to feature dict.
  1807. for (var i = 0; i < filters.length; i++) {
  1808. filter = filters[i];
  1809. feature_dict[filter.name] = feature_data[filter.index];
  1810. }
  1811. // Build popup.
  1812. popup = $("<div/>").attr("id", feature_uid).addClass("feature-popup");
  1813. var table = $("<table/>"),
  1814. key, value, row;
  1815. for (key in feature_dict) {
  1816. value = feature_dict[key];
  1817. row = $("<tr/>").appendTo(table);
  1818. $("<th/>").appendTo(row).text(key);
  1819. $("<td/>").attr("align", "left").appendTo(row)
  1820. .text(typeof(value) === 'number' ? round(value, 2) : value);
  1821. }
  1822. popup.append( $("<div class='feature-popup-inner'>").append( table ) );
  1823. popups[feature_uid] = popup;
  1824. }
  1825. // Attach popup to track's overlay.
  1826. popup.appendTo( $(this).parents(".track-content").children(".overlay") );
  1827. // Offsets are within canvas, but popup must be positioned relative to parent element.
  1828. // parseInt strips "px" from left, top measurements. +7 so that mouse pointer does not
  1829. // overlap popup.
  1830. var
  1831. popupX = offsetX + parseInt( tile.html_elt.css("left"), 10 ) - popup.width() / 2,
  1832. popupY = offsetY + parseInt( tile.html_elt.css("top"), 10 ) + 7;
  1833. popup.css("left", popupX + "px").css("top", popupY + "px");
  1834. }
  1835. else if (!e.isPropagationStopped()) {
  1836. // Propogate event to other tiles because overlapping tiles prevent mousemove from being
  1837. // called on tiles under this tile.
  1838. e.stopPropagation();
  1839. $(this).siblings().each(function() {
  1840. $(this).trigger(e);
  1841. });
  1842. }
  1843. })
  1844. .mouseleave(function() {
  1845. $(this).parents(".track-content").children(".overlay").children(".feature-popup").remove();
  1846. });
  1847. */
  1848. };
  1849. /**
  1850. * Tracks are objects can be added to the View.
  1851. *
  1852. * Track object hierarchy:
  1853. * Track
  1854. * -> LabelTrack
  1855. * -> TiledTrack
  1856. * ----> LineTrack
  1857. * ----> ReferenceTrack
  1858. * ----> FeatureTrack
  1859. * -------> ReadTrack
  1860. * ----> VariantTrack
  1861. */
  1862. var Track = function(view, container, obj_dict) {
  1863. // For now, track's container is always view.
  1864. extend(obj_dict, {
  1865. drag_handle_class: "draghandle"
  1866. });
  1867. Drawable.call(this, view, container, obj_dict);
  1868. //
  1869. // Attribute init.
  1870. //
  1871. // Set or create dataset.
  1872. this.dataset = null;
  1873. if (obj_dict.dataset) {
  1874. // Dataset can be a Backbone model or a dict that can be used to create a model.
  1875. this.dataset = (obj_dict.dataset instanceof Backbone.Model ? obj_dict.dataset : new data.Dataset(obj_dict.dataset) );
  1876. }
  1877. this.dataset_check_type = 'converted_datasets_state';
  1878. this.data_url_extra_params = {};
  1879. this.data_query_wait = ('data_query_wait' in obj_dict ? obj_dict.data_query_wait : DEFAULT_DATA_QUERY_WAIT);
  1880. // A little ugly creating data manager right now due to transition to Backbone-based objects.
  1881. this.data_manager = ('data_manager' in obj_dict ?
  1882. obj_dict.data_manager :
  1883. new visualization.GenomeDataManager({
  1884. dataset: this.dataset,
  1885. // HACK: simulate 'genome' attributes from view for now.
  1886. // View should eventually use Genome object.
  1887. genome: new visualization.Genome({
  1888. key: view.dbkey,
  1889. chroms_info: {
  1890. chrom_info: view.chrom_data
  1891. }
  1892. }),
  1893. data_mode_compatible: this.data_and_mode_compatible,
  1894. can_subset: this.can_subset
  1895. }));
  1896. // Height attributes: min height, max height, and visible height.
  1897. this.min_height_px = 16;
  1898. this.max_height_px = 800;
  1899. this.visible_height_px = this.config.get_value('height');
  1900. //
  1901. // Create content div, which is where track is displayed, and add to container if available.
  1902. //
  1903. this.content_div = $("<div class='track-content'>").appendTo(this.container_div);
  1904. if (this.container) {
  1905. this.container.content_div.append(this.container_div);
  1906. if ( !("resize" in obj_dict) || obj_dict.resize ) {
  1907. this.add_resize_handle();
  1908. }
  1909. }
  1910. };
  1911. extend(Track.prototype, Drawable.prototype, {
  1912. action_icons_def: [
  1913. // Change track mode.
  1914. {
  1915. name: "mode_icon",
  1916. title: "Set display mode",
  1917. css_class: "chevron-expand",
  1918. on_click_fn: function() {}
  1919. },
  1920. // Hide/show content.
  1921. Drawable.prototype.action_icons_def[0],
  1922. // Set track as overview.
  1923. {
  1924. name: "overview_icon",
  1925. title: "Set as overview",
  1926. css_class: "application-dock-270",
  1927. on_click_fn: function(track) {
  1928. track.view.set_overview(track);
  1929. }
  1930. },
  1931. // Edit config.
  1932. Drawable.prototype.action_icons_def[1],
  1933. // Toggle track filters.
  1934. {
  1935. name: "filters_icon",
  1936. title: "Filters",
  1937. css_class: "ui-slider-050",
  1938. on_click_fn: function(drawable) {
  1939. // TODO: update Tooltip text.
  1940. if (drawable.filters_manager.visible()) {
  1941. drawable.filters_manager.clear_filters();
  1942. }
  1943. else {
  1944. drawable.filters_manager.init_filters();
  1945. }
  1946. drawable.filters_manager.toggle();
  1947. }
  1948. },
  1949. // Toggle track tool.
  1950. {
  1951. name: "tools_icon",
  1952. title: "Tool",
  1953. css_class: "hammer",
  1954. on_click_fn: function(track) {
  1955. // TODO: update Tooltip text.
  1956. track.tool.toggle();
  1957. // Update track name.
  1958. if (track.tool.is_visible()) {
  1959. track.set_name(track.config.get_value('name') + track.tool_region_and_parameters_str());
  1960. }
  1961. else {
  1962. track.revert_name();
  1963. }
  1964. // HACK: name change modifies icon placement, which leaves tooltip incorrectly placed.
  1965. $(".tooltip").remove();
  1966. }
  1967. },
  1968. // Go to parameter exploration visualization.
  1969. {
  1970. name: "param_space_viz_icon",
  1971. title: "Tool parameter space visualization",
  1972. css_class: "arrow-split",
  1973. on_click_fn: function(track) {
  1974. var template =
  1975. '<strong>Tool</strong>: <%= track.tool.get("name") %><br/>' +
  1976. '<strong>Dataset</strong>: <%= track.config.get_value("name") %><br/>' +
  1977. '<strong>Region(s)</strong>: <select name="regions">' +
  1978. '<option value="cur">current viewing area</option>' +
  1979. '<option value="bookmarks">bookmarks</option>' +
  1980. '<option value="both">current viewing area and bookmarks</option>' +
  1981. '</select>',
  1982. html = _.template(template, { track: track });
  1983. var cancel_fn = function() { Galaxy.modal.hide(); $(window).unbind("keypress.check_enter_esc"); },
  1984. ok_fn = function() {
  1985. var regions_to_use = $('select[name="regions"] option:selected').val(),
  1986. regions,
  1987. view_region = new visualization.GenomeRegion({
  1988. chrom: view.chrom,
  1989. start: view.low,
  1990. end: view.high
  1991. }),
  1992. bookmarked_regions = _.map($(".bookmark"), function(elt) {
  1993. return new visualization.GenomeRegion({from_str: $(elt).children(".position").text()});
  1994. });
  1995. // Get regions for visualization.
  1996. if (regions_to_use === 'cur') {
  1997. // Use only current region.
  1998. regions = [ view_region ];
  1999. }
  2000. else if (regions_to_use === 'bookmarks') {
  2001. // Use only bookmarks.
  2002. regions = bookmarked_regions;
  2003. }
  2004. else {
  2005. // Use both current region and bookmarks.
  2006. regions = [ view_region ].concat(bookmarked_regions);
  2007. }
  2008. Galaxy.modal.hide();
  2009. // Go to visualization.
  2010. window.location.href =
  2011. galaxy_config.root + "visualization/sweepster" + "?" +
  2012. $.param({
  2013. dataset_id: track.dataset.id,
  2014. hda_ldda: track.dataset.get('hda_ldda'),
  2015. regions: JSON.stringify(new Backbone.Collection(regions).toJSON())
  2016. });
  2017. },
  2018. check_enter_esc = function(e) {
  2019. if ((e.keyCode || e.which) === 27) { // Escape key
  2020. cancel_fn();
  2021. } else if ((e.keyCode || e.which) === 13) { // Enter key
  2022. ok_fn();
  2023. }
  2024. };
  2025. // show dialog
  2026. Galaxy.modal.show({title: "Visualize tool parameter space and output from different parameter settings?", body: html, buttons : {'No' : cancel_fn, 'Yes' : ok_fn } });
  2027. }
  2028. },
  2029. // Remove track.
  2030. Drawable.prototype.action_icons_def[2]
  2031. ],
  2032. can_draw: function() {
  2033. return this.dataset && Drawable.prototype.can_draw.call(this);
  2034. },
  2035. build_container_div: function () {
  2036. return $("<div/>").addClass('track').attr("id", "track_" + this.id);
  2037. },
  2038. /**
  2039. * Set track's dataset.
  2040. */
  2041. set_dataset: function(dataset) {
  2042. this.dataset = dataset;
  2043. this.data_manager.set('dataset', dataset);
  2044. },
  2045. /**
  2046. * Action to take during resize.
  2047. */
  2048. on_resize: function() {
  2049. this.request_draw({ clear_tile_cache: true });
  2050. },
  2051. /**
  2052. * Add resizing handle to drawable's container_div.
  2053. */
  2054. add_resize_handle: function () {
  2055. var track = this;
  2056. var in_handle = false;
  2057. var in_drag = false;
  2058. var drag_control = $( "<div class='track-resize'>" );
  2059. // Control shows on hover over track, stays while dragging
  2060. $(track.container_div).hover( function() {
  2061. if ( track.config.get_value('content_visible') ) {
  2062. in_handle = true;
  2063. drag_control.show();
  2064. }
  2065. }, function() {
  2066. in_handle = false;
  2067. if ( ! in_drag ) { drag_control.hide(); }
  2068. });
  2069. // Update height and force redraw of current view while dragging,
  2070. // clear cache to force redraw of other tiles.
  2071. drag_control.hide().bind( "dragstart", function( e, d ) {
  2072. in_drag = true;
  2073. d.original_height = $(track.content_div).height();
  2074. }).bind( "drag", function( e, d ) {
  2075. var new_height = Math.min( Math.max( d.original_height + d.deltaY, track.min_height_px ), track.max_height_px );
  2076. $(track.tiles_div).css( 'height', new_height );
  2077. track.visible_height_px = (track.max_height_px === new_height ? 0 : new_height);
  2078. track.on_resize();
  2079. }).bind( "dragend", function( e, d ) {
  2080. track.tile_cache.clear();
  2081. in_drag = false;
  2082. if (!in_handle) { drag_control.hide(); }
  2083. track.config.set_value('height', track.visible_height_px);
  2084. track.changed();
  2085. }).appendTo(track.container_div);
  2086. },
  2087. /**
  2088. * Hide any elements that are part of the tracks contents area. Should
  2089. * remove as approprite, the track will be redrawn by show_contents.
  2090. */
  2091. hide_contents: function () {
  2092. // Hide tiles.
  2093. this.tiles_div.hide();
  2094. // Hide any y axis labels (common to several track types)
  2095. this.container_div.find(".yaxislabel, .track-resize").hide();
  2096. },
  2097. show_contents: function() {
  2098. // Show the contents div and labels (if present)
  2099. this.tiles_div.show();
  2100. this.container_div.find(".yaxislabel, .track-resize").show();
  2101. // Request a redraw of the content
  2102. this.request_draw();
  2103. },
  2104. /**
  2105. * Returns track type.
  2106. */
  2107. get_type: function() {
  2108. // Order is important: start with most-specific classes and go up the track hierarchy.
  2109. if (this instanceof LabelTrack) {
  2110. return "LabelTrack";
  2111. }
  2112. else if (this instanceof ReferenceTrack) {
  2113. return "ReferenceTrack";
  2114. }
  2115. else if (this instanceof LineTrack) {
  2116. return "LineTrack";
  2117. }
  2118. else if (this instanceof ReadTrack) {
  2119. return "ReadTrack";
  2120. }
  2121. else if (this instanceof VariantTrack) {
  2122. return "VariantTrack";
  2123. }
  2124. else if (this instanceof CompositeTrack) {
  2125. return "CompositeTrack";
  2126. }
  2127. else if (this instanceof FeatureTrack) {
  2128. return "FeatureTrack";
  2129. }
  2130. return "";
  2131. },
  2132. /**
  2133. * Remove visualization content and display message.
  2134. */
  2135. show_message: function(msg_html) {
  2136. this.tiles_div.remove();
  2137. return $('<span/>').addClass('message').html(msg_html).appendTo(this.content_div);
  2138. },
  2139. /**
  2140. * Initialize and draw the track.
  2141. */
  2142. init: function(retry) {
  2143. // FIXME: track should have a 'state' attribute that is checked on load; this state attribute should be
  2144. // used in this function to determine what action(s) to take.
  2145. var track = this;
  2146. track.enabled = false;
  2147. track.tile_cache.clear();
  2148. track.data_manager.clear();
  2149. /*
  2150. if (!track.content_div.text()) {
  2151. track.content_div.text(DATA_LOADING);
  2152. }
  2153. */
  2154. // Remove old track content (e.g. tiles, messages).
  2155. track.content_div.children().remove();
  2156. track.container_div.removeClass("nodata error pending");
  2157. track.tiles_div = $("<div/>").addClass("tiles").appendTo(track.content_div);
  2158. //
  2159. // Tracks with no dataset id are handled differently.
  2160. // FIXME: is this really necessary?
  2161. //
  2162. if (!track.dataset.id) {
  2163. return;
  2164. }
  2165. // Get dataset state; if state is fine, enable and draw track. Otherwise, show message
  2166. // about track status.
  2167. var init_deferred = $.Deferred(),
  2168. params = {
  2169. hda_ldda: track.dataset.get('hda_ldda'),
  2170. data_type: this.dataset_check_type,
  2171. chrom: track.view.chrom,
  2172. retry: retry
  2173. };
  2174. $.getJSON(this.dataset.url(), params, function (result) {
  2175. if (!result || result === "error" || result.kind === "error") {
  2176. // Dataset is in error state.
  2177. track.container_div.addClass("error");
  2178. var msg_elt = track.show_message(DATA_ERROR);
  2179. if (result.message) {
  2180. // Add links to (a) show error and (b) try again.
  2181. msg_elt.append(
  2182. $("<a href='javascript:void(0);'></a>").text("View error").click(function() {
  2183. Galaxy.modal.show({title: "Trackster Error", body: "<pre>" + result.message + "</pre>", buttons : {'Close' : function() { Galaxy.modal.hide(); } } });
  2184. })
  2185. );
  2186. msg_elt.append( $('<span/>').text(' ') );
  2187. msg_elt.append(
  2188. $("<a href='javascript:void(0);'></a>").text("Try again").click(function() {
  2189. track.init(true);
  2190. })
  2191. );
  2192. }
  2193. }
  2194. else if (result === "no converter") {
  2195. track.container_div.addClass("error");
  2196. track.show_message(DATA_NOCONVERTER);
  2197. }
  2198. else if (result === "no data" || (result.data !== undefined && (result.data === null || result.data.length === 0))) {
  2199. track.container_div.addClass("nodata");
  2200. track.show_message(DATA_NONE);
  2201. }
  2202. else if (result === "pending") {
  2203. track.container_div.addClass("pending");
  2204. track.show_message(DATA_PENDING);
  2205. //$("<img/>").attr("src", image_path + "/yui/rel_interstitial_loading.gif").appendTo(track.tiles_div);
  2206. setTimeout(function() { track.init(); }, track.data_query_wait);
  2207. }
  2208. else if (result === "data" || result.status === "data") {
  2209. if (result.valid_chroms) {
  2210. track.valid_chroms = result.valid_chroms;
  2211. track.update_icons();
  2212. }
  2213. track.tiles_div.text(DATA_OK);
  2214. if (track.view.chrom) {
  2215. track.tiles_div.text("");
  2216. track.tiles_div.css( "height", track.visible_height_px + "px" );
  2217. track.enabled = true;
  2218. // predraw_init may be asynchronous, wait for it and then draw
  2219. $.when.apply($, track.predraw_init()).done(function() {
  2220. init_deferred.resolve();
  2221. track.container_div.removeClass("nodata error pending");
  2222. track.request_draw();
  2223. });
  2224. }
  2225. else {
  2226. init_deferred.resolve();
  2227. }
  2228. }
  2229. });
  2230. this.update_icons();
  2231. return init_deferred;
  2232. },
  2233. /**
  2234. * Additional initialization required before drawing track for the first time.
  2235. */
  2236. predraw_init: function() {
  2237. var track = this;
  2238. return $.getJSON( track.dataset.url(),
  2239. { data_type: 'data', stats: true, chrom: track.view.chrom, low: 0,
  2240. high: track.view.max_high, hda_ldda: track.dataset.get('hda_ldda') }, function(result) {
  2241. var data = result.data;
  2242. // Tracks may not have stat data either because there is no data or data is not yet ready.
  2243. if (data !== undefined && data.min !== undefined && data.max !== undefined) {
  2244. // Compute default minimum and maximum values
  2245. var min_value = data.min,
  2246. max_value = data.max;
  2247. // If mean and sd are present, use them to compute a ~95% window
  2248. // but only if it would shrink the range on one side
  2249. min_value = Math.floor( Math.min( 0, Math.max( min_value, data.mean - 2 * data.sd ) ) );
  2250. max_value = Math.ceil( Math.max( 0, Math.min( max_value, data.mean + 2 * data.sd ) ) );
  2251. // Update config, prefs
  2252. track.config.set_default_value('min_value', min_value);
  2253. track.config.set_default_value('max_value', max_value);
  2254. track.config.set_value('min_value', min_value);
  2255. track.config.set_value('max_value', max_value);
  2256. }
  2257. });
  2258. },
  2259. /**
  2260. * Returns all drawables in this drawable.
  2261. */
  2262. get_drawables: function() {
  2263. return this;
  2264. }
  2265. });
  2266. var TiledTrack = function(view, container, obj_dict) {
  2267. Track.call(this, view, container, obj_dict);
  2268. var track = this;
  2269. // Make track moveable.
  2270. moveable(track.container_div, track.drag_handle_class, ".group", track);
  2271. // Attribute init.
  2272. this.filters_manager = new filters_mod.FiltersManager(this, ('filters' in obj_dict ? obj_dict.filters : null));
  2273. // HACK: set filters manager for data manager.
  2274. // FIXME: prolly need function to set filters and update data_manager reference.
  2275. this.data_manager.set('filters_manager', this.filters_manager);
  2276. this.filters_available = false;
  2277. this.tool = (obj_dict.tool ? new TracksterTool( _.extend( obj_dict.tool, {
  2278. 'track': this,
  2279. 'tool_state': obj_dict.tool_state
  2280. } ) )
  2281. : null);
  2282. this.tile_cache = new visualization.Cache(TILE_CACHE_SIZE);
  2283. this.left_offset = 0;
  2284. if (this.header_div) {
  2285. //
  2286. // Setup filters.
  2287. //
  2288. this.set_filters_manager(this.filters_manager);
  2289. //
  2290. // Create dynamic tool view.
  2291. //
  2292. if (this.tool) {
  2293. var tool_view = new TracksterToolView({ model: this.tool });
  2294. tool_view.render();
  2295. this.dynamic_tool_div = tool_view.$el;
  2296. this.header_div.after(this.dynamic_tool_div);
  2297. }
  2298. }
  2299. // Add tiles_div, overlay_div to content_div.
  2300. this.tiles_div = $("<div/>").addClass("tiles").appendTo(this.content_div);
  2301. if (!this.config.get_value('content_visible')) {
  2302. this.tiles_div.hide();
  2303. }
  2304. this.overlay_div = $("<div/>").addClass("overlay").appendTo(this.content_div);
  2305. if (obj_dict.mode) {
  2306. this.change_mode(obj_dict.mode);
  2307. }
  2308. };
  2309. extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
  2310. action_icons_def: Track.prototype.action_icons_def.concat( [
  2311. // Show more rows when all features are not slotted.
  2312. {
  2313. name: "show_more_rows_icon",
  2314. title: "To minimize track height, not all feature rows are displayed. Click to display more rows.",
  2315. css_class: "exclamation",
  2316. on_click_fn: function(track) {
  2317. $(".tooltip").remove();
  2318. track.slotters[ track.view.resolution_px_b ].max_rows *= 2;
  2319. track.request_draw({ clear_tile_cache: true });
  2320. },
  2321. hide: true
  2322. }
  2323. ] ),
  2324. /**
  2325. * Returns a copy of the track. The copy uses the same data manager so that the tracks can share data.
  2326. */
  2327. copy: function(container) {
  2328. // Create copy.
  2329. var obj_dict = this.to_dict();
  2330. extend(obj_dict, {
  2331. data_manager: this.data_manager
  2332. });
  2333. var new_track = new this.constructor(this.view, container, obj_dict);
  2334. // Misc. init and return.
  2335. new_track.change_mode(this.mode);
  2336. new_track.enabled = this.enabled;
  2337. return new_track;
  2338. },
  2339. /**
  2340. * Set filters manager + HTML elements.
  2341. */
  2342. set_filters_manager: function(filters_manager) {
  2343. this.filters_manager = filters_manager;
  2344. this.header_div.after(this.filters_manager.parent_div);
  2345. },
  2346. /**
  2347. * Returns representation of object in a dictionary for easy saving.
  2348. * Use from_dict to recreate object.
  2349. */
  2350. to_dict: function() {
  2351. return {
  2352. track_type: this.get_type(),
  2353. dataset: {
  2354. id: this.dataset.id,
  2355. hda_ldda: this.dataset.get('hda_ldda')
  2356. },
  2357. prefs: this.config.to_key_value_dict(),
  2358. mode: this.mode,
  2359. filters: this.filters_manager.to_dict(),
  2360. tool_state: (this.tool ? this.tool.state_dict() : {})
  2361. };
  2362. },
  2363. /**
  2364. * Set track bounds for current chromosome.
  2365. */
  2366. set_min_max: function() {
  2367. var track = this;
  2368. return $.getJSON( track.dataset.url(),
  2369. { data_type: 'data', stats: true, chrom: track.view.chrom, low: 0,
  2370. high: track.view.max_high, hda_ldda: track.dataset.get('hda_ldda') },
  2371. function(result) {
  2372. var data = result.data;
  2373. if ( isNaN(parseFloat(track.config.get_value('min_value'))) ||
  2374. isNaN(parseFloat(track.config.get_value('max_value'))) ) {
  2375. // Compute default minimum and maximum values
  2376. var min_value = data.min,
  2377. max_value = data.max;
  2378. // If mean and sd are present, use them to compute a ~95% window
  2379. // but only if it would shrink the range on one side
  2380. min_value = Math.floor( Math.min( 0, Math.max( min_value, data.mean - 2 * data.sd ) ) );
  2381. max_value = Math.ceil( Math.max( 0, Math.min( max_value, data.mean + 2 * data.sd ) ) );
  2382. // Update the prefs
  2383. track.config.set_value('min_value', min_value);
  2384. track.config.set_value('max_value', max_value);
  2385. }
  2386. });
  2387. },
  2388. /**
  2389. * Change track's mode.
  2390. */
  2391. change_mode: function(new_mode) {
  2392. var track = this;
  2393. // TODO: is it necessary to store the mode in two places (.mode and track_config)?
  2394. track.mode = new_mode;
  2395. track.config.set_value('mode', new_mode);
  2396. // FIXME: find a better way to get Auto data w/o clearing cache; using mode in the
  2397. // data manager would work if Auto data were checked for compatibility when a specific
  2398. // mode is chosen.
  2399. if (new_mode === 'Auto') { this.data_manager.clear(); }
  2400. track.request_draw({ clear_tile_cache: true });
  2401. this.action_icons.mode_icon.attr("title", "Set display mode (now: " + track.mode + ")");
  2402. return track;
  2403. },
  2404. /**
  2405. * Update track's buttons.
  2406. */
  2407. update_icons: function() {
  2408. var track = this;
  2409. //
  2410. // Show/hide filter icon.
  2411. //
  2412. track.action_icons.filters_icon.toggle(track.filters_available);
  2413. //
  2414. // Show/hide tool icons.
  2415. //
  2416. track.action_icons.tools_icon.toggle(track.tool !== null);
  2417. track.action_icons.param_space_viz_icon.toggle(track.tool !== null);
  2418. },
  2419. /**
  2420. * Generate a key for the tile cache.
  2421. * TODO: create a TileCache object (like DataCache) and generate key internally.
  2422. */
  2423. _gen_tile_cache_key: function(w_scale, tile_region) {
  2424. return w_scale + '_' + tile_region;
  2425. },
  2426. /**
  2427. * Request that track be drawn.
  2428. */
  2429. request_draw: function(options) {
  2430. if (options && options.clear_tile_cache) {
  2431. this.tile_cache.clear();
  2432. }
  2433. this.view.request_redraw(options, this);
  2434. },
  2435. /**
  2436. * Actions to be taken before drawing.
  2437. */
  2438. before_draw: function() {
  2439. // Clear because this is set when drawing.
  2440. this.max_height_px = 0;
  2441. },
  2442. /**
  2443. * Draw track. Options include:
  2444. * -force: force a redraw rather than use cached tiles (default: false)
  2445. * -clear_after: clear old tiles after drawing new tiles (default: false)
  2446. * -data_fetch: fetch data if necessary (default: true)
  2447. *
  2448. * NOTE: this function should never be called directly; use request_draw() so that drawing
  2449. * management can be used.
  2450. */
  2451. _draw: function(options) {
  2452. if ( !this.can_draw() ) { return; }
  2453. var clear_after = options && options.clear_after,
  2454. low = this.view.low,
  2455. high = this.view.high,
  2456. range = high - low,
  2457. width = this.view.container.width(),
  2458. w_scale = this.view.resolution_px_b,
  2459. resolution = 1 / w_scale;
  2460. // For overview, adjust high, low, resolution, and w_scale.
  2461. if (this.is_overview) {
  2462. low = this.view.max_low;
  2463. high = this.view.max_high;
  2464. w_scale = width / (view.max_high - view.max_low);
  2465. resolution = 1 / w_scale;
  2466. }
  2467. this.before_draw();
  2468. //
  2469. // Method for moving and/or removing tiles:
  2470. // (a) mark all elements for removal using class 'remove'
  2471. // (b) during tile drawing/placement, remove class for elements that are moved;
  2472. // this occurs in show_tile()
  2473. // (c) after drawing tiles, remove elements still marked for removal
  2474. // (i.e. that still have class 'remove').
  2475. //
  2476. // Step (a) for (re)moving tiles.
  2477. this.tiles_div.children().addClass("remove");
  2478. var
  2479. // Tile width in bases.
  2480. tile_width = Math.floor(TILE_SIZE * resolution),
  2481. // Index of first tile that overlaps visible region.
  2482. tile_index = Math.floor(low / tile_width),
  2483. tile_region,
  2484. tile_promise,
  2485. tile_promises = [],
  2486. tiles = [];
  2487. // Draw tiles.
  2488. while ( (tile_index * tile_width) < high ) {
  2489. // Get tile region.
  2490. tile_region = new visualization.GenomeRegion({
  2491. chrom: this.view.chrom,
  2492. start: tile_index * tile_width,
  2493. // Tile high cannot be larger than view.max_high, which the chromosome length.
  2494. end: Math.min( (tile_index + 1) * tile_width, this.view.max_high)
  2495. });
  2496. tile_promise = this.draw_helper(tile_region, w_scale, options);
  2497. tile_promises.push(tile_promise);
  2498. $.when(tile_promise).then(function(tile) {
  2499. tiles.push(tile);
  2500. });
  2501. // Go to next tile.
  2502. tile_index += 1;
  2503. }
  2504. // Step (c) for (re)moving tiles when clear_after is false.
  2505. if (!clear_after) { this.tiles_div.children(".remove").removeClass("remove").remove(); }
  2506. // When all tiles are drawn, call post-draw actions.
  2507. var track = this;
  2508. $.when.apply($, tile_promises).then(function() {
  2509. // Step (c) for (re)moving tiles when clear_after is true:
  2510. track.tiles_div.children(".remove").remove();
  2511. // Only do postdraw actions for tiles; instances where tiles may not be drawn include:
  2512. // (a) ReferenceTrack without sufficient resolution;
  2513. // (b) data_fetch = false.
  2514. tiles = _.filter(tiles, function(t) {
  2515. return t !== null;
  2516. });
  2517. if (tiles.length !== 0) {
  2518. track.postdraw_actions(tiles, width, w_scale, clear_after);
  2519. }
  2520. });
  2521. },
  2522. /**
  2523. * Add a maximum/minimum label to track.
  2524. */
  2525. _add_yaxis_label: function(type, on_change) {
  2526. var track = this,
  2527. css_class = (type === 'max' ? 'top' : 'bottom'),
  2528. text = (type === 'max' ? 'max' : 'min'),
  2529. pref_name = (type === 'max' ? 'max_value' : 'min_value'),
  2530. label = this.container_div.find(".yaxislabel." + css_class);
  2531. // Default action for on_change is to redraw track.
  2532. on_change = on_change || function() {
  2533. track.request_draw({ clear_tile_cache: true });
  2534. };
  2535. if (label.length !== 0) {
  2536. // Label already exists, so update value.
  2537. label.text(track.config.get_value(pref_name));
  2538. }
  2539. else {
  2540. // Add label.
  2541. label = $("<div/>").text(track.config.get_value(pref_name)).make_text_editable({
  2542. num_cols: 12,
  2543. on_finish: function(new_val) {
  2544. $(".tooltip").remove();
  2545. track.config.set_value(pref_name, new_val);
  2546. on_change();
  2547. },
  2548. help_text: "Set " + text + " value"
  2549. }).addClass('yaxislabel ' + css_class).css("color", this.config.get_value('label_color'));
  2550. this.container_div.prepend(label);
  2551. }
  2552. },
  2553. /**
  2554. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  2555. * drawn/fetched and shown.
  2556. */
  2557. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  2558. var line_track_tiles = _.filter(tiles, function(tile) {
  2559. return (tile instanceof LineTrackTile);
  2560. });
  2561. //
  2562. // Take different actions depending on whether there are LineTrack/Coverage tiles.
  2563. //
  2564. if (line_track_tiles.length > 0) {
  2565. // -- Drawing in Coverage mode. --
  2566. // Clear because this is set when drawing.
  2567. this.max_height_px = 0;
  2568. var track = this;
  2569. _.each(tiles, function(tile) {
  2570. if (!(tile instanceof LineTrackTile)) {
  2571. tile.html_elt.remove();
  2572. track.draw_helper(tile.region, w_scale, { force: true, mode: 'Coverage' });
  2573. }
  2574. });
  2575. track._add_yaxis_label('max');
  2576. }
  2577. else {
  2578. // -- Drawing in non-Coverage mode. --
  2579. // Remove Y-axis labels because there are no line track tiles.
  2580. this.container_div.find('.yaxislabel').remove();
  2581. //
  2582. // If some tiles have icons, set padding of tiles without icons so features and rows align.
  2583. //
  2584. var icons_present = _.find(tiles, function(tile) {
  2585. return tile.has_icons;
  2586. });
  2587. if (icons_present) {
  2588. _.each(tiles, function(tile) {
  2589. if (!tile.has_icons) {
  2590. // Need to align with other tile(s) that have icons.
  2591. tile.html_elt.css("padding-top", ERROR_PADDING);
  2592. }
  2593. });
  2594. }
  2595. }
  2596. },
  2597. /**
  2598. * Returns appropriate display mode based on data.
  2599. */
  2600. get_mode: function(data) {
  2601. return this.mode;
  2602. },
  2603. /**
  2604. * Update track interface to show display mode being used.
  2605. */
  2606. update_auto_mode: function( display_mode ) {
  2607. // FIXME: needs to be implemented.
  2608. },
  2609. /**
  2610. * Returns a list of drawables to draw. Defaults to current track.
  2611. */
  2612. _get_drawables: function() {
  2613. return [ this ];
  2614. },
  2615. /**
  2616. * Retrieves from cache, draws, or sets up drawing for a single tile. Returns either a Tile object or a
  2617. * jQuery.Deferred object that is fulfilled when tile can be drawn again. Options include:
  2618. * -force: force a redraw rather than use cached tiles (default: false)
  2619. * -data_fetch: fetch data if necessary (default: true)
  2620. */
  2621. draw_helper: function(region, w_scale, options) {
  2622. // Init options if necessary to avoid having to check if options defined.
  2623. if (!options) { options = {}; }
  2624. var force = options.force,
  2625. mode = options.mode || this.mode,
  2626. resolution = 1 / w_scale,
  2627. // Useful vars.
  2628. track = this,
  2629. drawables = this._get_drawables(),
  2630. key = this._gen_tile_cache_key(w_scale, region),
  2631. is_tile = function(o) { return (o && 'track' in o); };
  2632. // Check tile cache, if found show existing tile in correct position
  2633. var tile = (force ? undefined : track.tile_cache.get_elt(key));
  2634. if (tile) {
  2635. if (is_tile(tile)) {
  2636. track.show_tile(tile, w_scale);
  2637. }
  2638. return tile;
  2639. }
  2640. // If not fetching data, nothing more to do because data is needed to draw tile.
  2641. if (options.data_fetch === false) { return null; }
  2642. // Function that returns data/Deferreds needed to draw tile.
  2643. var get_tile_data = function() {
  2644. // HACK: if display mode (mode) is in continuous data modes, data mode must be coverage to get coverage data.
  2645. var data_mode = (_.find(CONTINUOUS_DATA_MODES, function(m) { return m === mode; }) ? "Coverage" : mode);
  2646. // Map drawable object to data needed for drawing.
  2647. var tile_data = _.map(drawables, function(d) {
  2648. // Get the track data/promise.
  2649. return d.data_manager.get_data(region, data_mode, resolution, track.data_url_extra_params);
  2650. });
  2651. // Get reference data/promise.
  2652. if (view.reference_track) {
  2653. tile_data.push(view.reference_track.data_manager.get_data(region, mode, resolution, view.reference_track.data_url_extra_params));
  2654. }
  2655. return tile_data;
  2656. };
  2657. //
  2658. // When data is available, draw tile.
  2659. //
  2660. var tile_drawn = $.Deferred();
  2661. track.tile_cache.set_elt(key, tile_drawn);
  2662. $.when.apply($, get_tile_data()).then( function() {
  2663. var tile_data = get_tile_data(),
  2664. tracks_data = tile_data,
  2665. seq_data;
  2666. // Deferreds may show up here if trying to fetch a subset of data from a superset data chunk
  2667. // that cannot be subsetted. This may occur if the superset has a message. If there is a
  2668. // Deferred, try again from the top. NOTE: this condition could (should?) be handled by the
  2669. // GenomeDataManager in visualization module.
  2670. if (_.find(tile_data, function(d) { return util.is_deferred(d); })) {
  2671. track.tile_cache.set_elt(key, undefined);
  2672. $.when(track.draw_helper(region, w_scale, options)).then(function(tile) {
  2673. tile_drawn.resolve(tile);
  2674. });
  2675. return;
  2676. }
  2677. // If sequence data is available, subset to get only data in region.
  2678. if (view.reference_track) {
  2679. seq_data = view.reference_track.data_manager.subset_entry(tile_data.pop(), region);
  2680. }
  2681. // Get drawing modes, heights for all tracks.
  2682. var drawing_modes = [],
  2683. drawing_heights = [];
  2684. _.each(drawables, function(d, i) {
  2685. var mode = d.mode,
  2686. data = tracks_data[i];
  2687. if (mode === "Auto") {
  2688. mode = d.get_mode(data);
  2689. d.update_auto_mode(mode);
  2690. }
  2691. drawing_modes.push(mode);
  2692. drawing_heights.push(d.get_canvas_height(data, mode, w_scale, width));
  2693. });
  2694. var canvas = track.view.canvas_manager.new_canvas(),
  2695. tile_low = region.get('start'),
  2696. tile_high = region.get('end'),
  2697. all_data_index = 0,
  2698. width = Math.ceil( (tile_high - tile_low) * w_scale ) + track.left_offset,
  2699. height = _.max(drawing_heights),
  2700. tile;
  2701. //
  2702. // Draw all tracks on tile.
  2703. //
  2704. canvas.width = width;
  2705. // Height is specified in options or is the height found above.
  2706. canvas.height = (options.height || height);
  2707. var ctx = canvas.getContext('2d');
  2708. ctx.translate(track.left_offset, 0);
  2709. if (drawables.length > 1) {
  2710. ctx.globalAlpha = 0.5;
  2711. ctx.globalCompositeOperation = "source-over";
  2712. }
  2713. _.each(drawables, function(d, i) {
  2714. tile = d.draw_tile(tracks_data[i], ctx, drawing_modes[i], region, w_scale, seq_data);
  2715. });
  2716. // Don't cache, show if no tile.
  2717. if (tile !== undefined) {
  2718. track.tile_cache.set_elt(key, tile);
  2719. track.show_tile(tile, w_scale);
  2720. }
  2721. tile_drawn.resolve(tile);
  2722. });
  2723. return tile_drawn;
  2724. },
  2725. /**
  2726. * Returns canvas height needed to display data; return value is an integer that denotes the
  2727. * number of pixels required.
  2728. */
  2729. get_canvas_height: function(result, mode, w_scale, canvas_width) {
  2730. return this.visible_height_px;
  2731. },
  2732. /**
  2733. * Draw line (bigwig) data onto tile.
  2734. */
  2735. _draw_line_track_tile: function(result, ctx, mode, region, w_scale) {
  2736. var canvas = ctx.canvas,
  2737. painter = new painters.LinePainter(result.data, region.get('start'), region.get('end'), this.config.to_key_value_dict(), mode);
  2738. painter.draw(ctx, canvas.width, canvas.height, w_scale);
  2739. return new LineTrackTile(this, region, w_scale, canvas, result.data);
  2740. },
  2741. /**
  2742. * Draw a track tile.
  2743. * @param result result from server
  2744. * @param ctx canvas context to draw on
  2745. * @param mode mode to draw in
  2746. * @param region region to draw on tile
  2747. * @param w_scale pixels per base
  2748. * @param ref_seq reference sequence data
  2749. */
  2750. draw_tile: function(result, ctx, mode, region, w_scale, ref_seq) {},
  2751. /**
  2752. * Show track tile and perform associated actions. Showing tile may actually move
  2753. * an existing tile rather than reshowing it.
  2754. */
  2755. show_tile: function(tile, w_scale) {
  2756. var track = this,
  2757. tile_element = tile.html_elt;
  2758. // -- Show/move tile element. --
  2759. tile.predisplay_actions();
  2760. // Position tile element based on current viewport.
  2761. var left = Math.round( ( tile.low - (this.is_overview? this.view.max_low : this.view.low) ) * w_scale );
  2762. if (this.left_offset) {
  2763. left -= this.left_offset;
  2764. }
  2765. tile_element.css('left', left);
  2766. if ( tile_element.hasClass("remove") ) {
  2767. // Step (b) for (re)moving tiles. See _draw() function for description of algorithm
  2768. // for removing tiles.
  2769. tile_element.removeClass("remove");
  2770. }
  2771. else {
  2772. // Showing new tile.
  2773. this.tiles_div.append(tile_element);
  2774. }
  2775. // -- Update track, tile heights based on new tile. --
  2776. tile_element.css('height', 'auto');
  2777. // Update max height based on current tile's height.
  2778. // BUG/HACK: tile_element.height() returns a height that is always 2 pixels too big, so
  2779. // -2 to get the correct height.
  2780. this.max_height_px = Math.max(this.max_height_px, tile_element.height() - 2);
  2781. // Update height for all tiles based on max height.
  2782. tile_element.parent().children().css("height", this.max_height_px + "px");
  2783. // Update track height based on max height and visible height.
  2784. var track_height = this.max_height_px;
  2785. if (this.visible_height_px !== 0) {
  2786. track_height = Math.min(this.max_height_px, this.visible_height_px);
  2787. }
  2788. this.tiles_div.css("height", track_height + "px");
  2789. },
  2790. /**
  2791. * Utility function that creates a label string describing the region and parameters of a track's tool.
  2792. */
  2793. tool_region_and_parameters_str: function(region) {
  2794. var track = this,
  2795. region_str = (region !== undefined ? region.toString() : "all"),
  2796. param_str = _.values( track.tool.get_inputs_dict()).join(', ');
  2797. return " - region=[" + region_str + "], parameters=[" + param_str + "]";
  2798. },
  2799. /**
  2800. * Returns true if data is compatible with a given mode.
  2801. */
  2802. data_and_mode_compatible: function(data, mode) {
  2803. // Only handle modes that user can set.
  2804. if (mode === "Auto") {
  2805. return true;
  2806. }
  2807. // Histogram mode requires bigwig data.
  2808. else if (mode === "Coverage") {
  2809. return data.dataset_type === "bigwig";
  2810. }
  2811. // All other modes--Dense, Squish, Pack--require data + details.
  2812. else if (data.dataset_type === "bigwig" ||
  2813. data.extra_info === "no_detail") {
  2814. return false;
  2815. }
  2816. else {
  2817. return true;
  2818. }
  2819. },
  2820. /**
  2821. * Returns true if entry can be subsetted.
  2822. */
  2823. can_subset: function(entry) {
  2824. // Do not subset entries with a message or data with no detail.
  2825. if (entry.message || entry.extra_info === "no_detail") {
  2826. return false;
  2827. }
  2828. // Subset only if data is single-bp resolution.
  2829. else if (entry.dataset_type === 'bigwig') {
  2830. return (entry.data[1][0] - entry.data[0][0] === 1);
  2831. }
  2832. return true;
  2833. },
  2834. /**
  2835. * Set up track to receive tool data.
  2836. */
  2837. init_for_tool_data: function() {
  2838. // Set up track to fetch raw data rather than converted data.
  2839. this.data_manager.set('data_type', 'raw_data');
  2840. this.data_query_wait = 1000;
  2841. this.dataset_check_type = 'state';
  2842. // FIXME: this is optional and is disabled for now because it creates
  2843. // additional converter jobs without a clear benefit because indexing
  2844. // such a small dataset provides little benefit.
  2845. //
  2846. // Set up one-time, post-draw to clear tool execution settings.
  2847. //
  2848. /*
  2849. this.normal_postdraw_actions = this.postdraw_actions;
  2850. this.postdraw_actions = function(tiles, width, w_scale, clear_after) {
  2851. var self = this;
  2852. // Do normal postdraw init.
  2853. self.normal_postdraw_actions(tiles, width, w_scale, clear_after);
  2854. // Tool-execution specific post-draw init:
  2855. // Reset dataset check, wait time.
  2856. self.dataset_check_type = 'converted_datasets_state';
  2857. self.data_query_wait = DEFAULT_DATA_QUERY_WAIT;
  2858. // Reset data URL when dataset indexing has completed/when not pending.
  2859. var ss_deferred = new util.ServerStateDeferred({
  2860. url: self.dataset_state_url,
  2861. url_params: {dataset_id : self.dataset.id, hda_ldda: self.dataset.get('hda_ldda')},
  2862. interval: self.data_query_wait,
  2863. // Set up deferred to check dataset state until it is not pending.
  2864. success_fn: function(result) { return result !== "pending"; }
  2865. });
  2866. $.when(ss_deferred.go()).then(function() {
  2867. // Dataset is indexed, so use converted data.
  2868. self.data_manager.set('data_type', 'data');
  2869. });
  2870. // Reset post-draw actions function.
  2871. self.postdraw_actions = self.normal_postdraw_actions;
  2872. };
  2873. */
  2874. }
  2875. });
  2876. var LabelTrack = function (view, container) {
  2877. Track.call(this, view, container, {
  2878. resize: false,
  2879. header: false
  2880. });
  2881. this.container_div.addClass( "label-track" );
  2882. };
  2883. extend(LabelTrack.prototype, Track.prototype, {
  2884. init: function() {
  2885. // Enable by default because there should always be data when drawing track.
  2886. this.enabled = true;
  2887. },
  2888. /**
  2889. * Additional initialization required before drawing track for the first time.
  2890. */
  2891. predraw_init: function() {},
  2892. _draw: function(options) {
  2893. var view = this.view,
  2894. range = view.high - view.low,
  2895. tickDistance = Math.floor( Math.pow( 10, Math.floor( Math.log( range ) / Math.log( 10 ) ) ) ),
  2896. position = Math.floor( view.low / tickDistance ) * tickDistance,
  2897. width = this.view.container.width(),
  2898. new_div = $("<div/>").addClass('label-container');
  2899. while ( position < view.high ) {
  2900. var screenPosition = Math.floor( ( position - view.low ) / range * width );
  2901. new_div.append( $("<div/>").addClass('label').text(commatize( position )).css( {
  2902. left: screenPosition
  2903. }));
  2904. position += tickDistance;
  2905. }
  2906. this.content_div.children( ":first" ).remove();
  2907. this.content_div.append( new_div );
  2908. }
  2909. });
  2910. // FIXME: Composite tracks have code for showing composite tracks with line tracks and
  2911. // composite tracks with line + feature tracks. It's probably best if different classes
  2912. // are created for each type of composite track.
  2913. /**
  2914. * A tiled track composed of multiple other tracks. Composite tracks only work with
  2915. * bigwig data for now.
  2916. */
  2917. var CompositeTrack = function(view, container, obj_dict) {
  2918. TiledTrack.call(this, view, container, obj_dict);
  2919. // Init drawables; each drawable is a copy so that config/preferences
  2920. // are independent of each other. Also init left offset.
  2921. this.drawables = [];
  2922. if ('drawables' in obj_dict) {
  2923. var drawable;
  2924. for (var i = 0; i < obj_dict.drawables.length; i++) {
  2925. drawable = obj_dict.drawables[i];
  2926. this.drawables[i] = object_from_template(drawable, view, null);
  2927. // Track's left offset is the max of all tracks.
  2928. if (drawable.left_offset > this.left_offset) {
  2929. this.left_offset = drawable.left_offset;
  2930. }
  2931. }
  2932. this.enabled = true;
  2933. }
  2934. // Set all feature tracks to use Coverage mode.
  2935. _.each(this.drawables, function(d) {
  2936. if (d instanceof FeatureTrack || d instanceof ReadTrack) {
  2937. d.change_mode("Coverage");
  2938. }
  2939. });
  2940. this.update_icons();
  2941. // HACK: needed for saving object for now. Need to generalize get_type() to all Drawables and use
  2942. // that for object type.
  2943. this.obj_type = "CompositeTrack";
  2944. };
  2945. extend(CompositeTrack.prototype, TiledTrack.prototype, {
  2946. display_modes: CONTINUOUS_DATA_MODES,
  2947. config_params: _.union( Drawable.prototype.config_params, [
  2948. { key: 'min_value', label: 'Min Value', type: 'float', default_value: undefined },
  2949. { key: 'max_value', label: 'Max Value', type: 'float', default_value: undefined },
  2950. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  2951. { key: 'height', type: 'int', default_value: 30, hidden: true }
  2952. ] ),
  2953. action_icons_def:
  2954. [
  2955. // Create composite track from group's tracks.
  2956. {
  2957. name: "composite_icon",
  2958. title: "Show individual tracks",
  2959. css_class: "layers-stack",
  2960. on_click_fn: function(track) {
  2961. $(".tooltip").remove();
  2962. track.show_group();
  2963. }
  2964. }
  2965. ].concat(TiledTrack.prototype.action_icons_def),
  2966. // HACK: CompositeTrack should inherit from DrawableCollection as well.
  2967. /**
  2968. * Returns representation of object in a dictionary for easy saving.
  2969. * Use from_dict to recreate object.
  2970. */
  2971. to_dict: DrawableCollection.prototype.to_dict,
  2972. add_drawable: DrawableCollection.prototype.add_drawable,
  2973. unpack_drawables: DrawableCollection.prototype.unpack_drawables,
  2974. config_onchange: function() {
  2975. this.set_name(this.config.get_value('name'));
  2976. this.request_draw({ clear_tile_cache: true });
  2977. },
  2978. /**
  2979. * Action to take during resize.
  2980. */
  2981. on_resize: function() {
  2982. // Propogate visible height to other tracks.
  2983. var visible_height = this.visible_height_px;
  2984. _.each(this.drawables, function(d) {
  2985. d.visible_height_px = visible_height;
  2986. });
  2987. Track.prototype.on_resize.call(this);
  2988. },
  2989. /**
  2990. * Change mode for all tracks.
  2991. */
  2992. change_mode: function(new_mode) {
  2993. TiledTrack.prototype.change_mode.call(this, new_mode);
  2994. for (var i = 0; i < this.drawables.length; i++) {
  2995. this.drawables[i].change_mode(new_mode);
  2996. }
  2997. },
  2998. /**
  2999. * Initialize component tracks and draw composite track when all components are initialized.
  3000. */
  3001. init: function() {
  3002. // Init components.
  3003. var init_deferreds = [];
  3004. for (var i = 0; i < this.drawables.length; i++) {
  3005. init_deferreds.push(this.drawables[i].init());
  3006. }
  3007. // Draw composite when all tracks available.
  3008. var track = this;
  3009. $.when.apply($, init_deferreds).then(function() {
  3010. track.enabled = true;
  3011. track.request_draw();
  3012. });
  3013. },
  3014. update_icons: function() {
  3015. // For now, hide filters and tool.
  3016. this.action_icons.filters_icon.hide();
  3017. this.action_icons.tools_icon.hide();
  3018. this.action_icons.param_space_viz_icon.hide();
  3019. },
  3020. can_draw: Drawable.prototype.can_draw,
  3021. _get_drawables: function() {
  3022. return this.drawables;
  3023. },
  3024. /**
  3025. * Replace this track with group that includes individual tracks.
  3026. */
  3027. show_group: function() {
  3028. // Create group with individual tracks.
  3029. var group = new DrawableGroup(this.view, this.container, {
  3030. name: this.config.get_value('name')
  3031. }),
  3032. track;
  3033. for (var i = 0; i < this.drawables.length; i++) {
  3034. track = this.drawables[i];
  3035. track.update_icons();
  3036. group.add_drawable(track);
  3037. track.container = group;
  3038. group.content_div.append(track.container_div);
  3039. }
  3040. // Replace track with group.
  3041. var index = this.container.replace_drawable(this, group, true);
  3042. group.request_draw({ clear_tile_cache: true });
  3043. },
  3044. /**
  3045. * Actions taken before drawing.
  3046. */
  3047. before_draw: function() {
  3048. // FIXME: this is needed only if there are feature tracks in the composite track.
  3049. // TiledTrack.prototype.before_draw.call(this);
  3050. //
  3051. // Set min, max for tracks to be largest min, max.
  3052. //
  3053. // Get smallest min, biggest max.
  3054. var min = _.min(_.map(this.drawables, function(d) { return d.config.get_value('min_value'); })),
  3055. max = _.max(_.map(this.drawables, function(d) { return d.config.get_value('max_value'); }));
  3056. this.config.set_value('min_value', min);
  3057. this.config.set_value('max_value', max);
  3058. // Set all tracks to smallest min, biggest max.
  3059. _.each(this.drawables, function(d) {
  3060. d.config.set_value('min_value', min);
  3061. d.config.set_value('max_value', max);
  3062. });
  3063. },
  3064. /**
  3065. * Update minimum, maximum for component tracks.
  3066. */
  3067. update_all_min_max: function() {
  3068. var track = this,
  3069. min_value = this.config.get_value('min_value'),
  3070. max_value = this.config.get_value('max_value');
  3071. _.each(this.drawables, function(d) {
  3072. d.config.set_value('min_value', min_value);
  3073. d.config.set_value('max_value', max_value);
  3074. });
  3075. this.request_draw({ clear_tile_cache: true });
  3076. },
  3077. /**
  3078. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  3079. * drawn/fetched and shown.
  3080. */
  3081. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3082. // All tiles must be the same height in order to draw LineTracks, so redraw tiles as needed.
  3083. var max_height = -1, i;
  3084. for (i = 0; i < tiles.length; i++) {
  3085. var height = tiles[i].html_elt.find("canvas").height();
  3086. if (height > max_height) {
  3087. max_height = height;
  3088. }
  3089. }
  3090. for (i = 0; i < tiles.length; i++) {
  3091. var tile = tiles[i];
  3092. if (tile.html_elt.find("canvas").height() !== max_height) {
  3093. this.draw_helper(tile.region, w_scale, { force: true, height: max_height } );
  3094. tile.html_elt.remove();
  3095. }
  3096. }
  3097. // Wrap function so that it can be called without object reference.
  3098. var track = this,
  3099. t = function() { track.update_all_min_max(); };
  3100. // Add min, max labels.
  3101. this._add_yaxis_label('min', t);
  3102. this._add_yaxis_label('max', t);
  3103. }
  3104. });
  3105. /**
  3106. * Displays reference genome data.
  3107. */
  3108. var ReferenceTrack = function (view) {
  3109. TiledTrack.call(this, view, { content_div: view.top_labeltrack }, { resize: false, header: false });
  3110. // Use offset to ensure that bases at tile edges are drawn.
  3111. this.left_offset = view.canvas_manager.char_width_px;
  3112. this.container_div.addClass("reference-track");
  3113. this.data_url = galaxy_config.root + "api/genomes/" + this.view.dbkey;
  3114. this.data_url_extra_params = {reference: true};
  3115. this.data_manager = new visualization.GenomeReferenceDataManager({
  3116. data_url: this.data_url,
  3117. can_subset: this.can_subset
  3118. });
  3119. this.hide_contents();
  3120. };
  3121. extend(ReferenceTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3122. config_params: _.union( Drawable.prototype.config_params, [
  3123. { key: 'height', type: 'int', default_value: 13, hidden: true }
  3124. ] ),
  3125. init: function() {
  3126. this.data_manager.clear();
  3127. // Enable by default because there should always be data when drawing track.
  3128. this.enabled = true;
  3129. },
  3130. /**
  3131. * Additional initialization required before drawing track for the first time.
  3132. */
  3133. predraw_init: function() {},
  3134. can_draw: Drawable.prototype.can_draw,
  3135. /**
  3136. * Draws and shows tile if reference data can be displayed; otherwise track is hidden.
  3137. */
  3138. draw_helper: function(region, w_scale, options) {
  3139. var cur_visible = this.tiles_div.is(':visible'),
  3140. new_visible,
  3141. tile = null;
  3142. if (w_scale > this.view.canvas_manager.char_width_px) {
  3143. this.tiles_div.show();
  3144. new_visible = true;
  3145. tile = TiledTrack.prototype.draw_helper.call(this, region, w_scale, options);
  3146. }
  3147. else {
  3148. new_visible = false;
  3149. this.tiles_div.hide();
  3150. }
  3151. // NOTE: viewport resizing conceptually belongs in postdraw_actions(), but currently
  3152. // postdraw_actions is not called when reference track not shown due to no tiles. If
  3153. // it is moved to postdraw_actions, resize must be called each time because cannot
  3154. // easily detect showing/hiding.
  3155. // If showing or hiding reference track, resize viewport.
  3156. if (cur_visible !== new_visible) {
  3157. this.view.resize_viewport();
  3158. }
  3159. return tile;
  3160. },
  3161. can_subset: function(entry) { return true; },
  3162. /**
  3163. * Draw ReferenceTrack tile.
  3164. */
  3165. draw_tile: function(data, ctx, mode, region, w_scale) {
  3166. // Try to subset data.
  3167. var subset = this.data_manager.subset_entry(data, region),
  3168. seq_data = subset.data;
  3169. // Draw sequence data.
  3170. var canvas = ctx.canvas;
  3171. ctx.font = ctx.canvas.manager.default_font;
  3172. ctx.textAlign = "center";
  3173. for (var c = 0, str_len = seq_data.length; c < str_len; c++) {
  3174. ctx.fillStyle = this.view.get_base_color(seq_data[c]);
  3175. ctx.fillText(seq_data[c], Math.floor(c * w_scale), 10);
  3176. }
  3177. return new Tile(this, region, w_scale, canvas, subset);
  3178. }
  3179. });
  3180. /**
  3181. * Track displays continuous/numerical data. Track expects position data in 1-based format, i.e. wiggle format.
  3182. */
  3183. var LineTrack = function (view, container, obj_dict) {
  3184. this.mode = "Histogram";
  3185. TiledTrack.call(this, view, container, obj_dict);
  3186. };
  3187. extend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3188. display_modes: CONTINUOUS_DATA_MODES,
  3189. config_params: _.union( Drawable.prototype.config_params, [
  3190. { key: 'color', label: 'Color', type: 'color' },
  3191. { key: 'min_value', label: 'Min Value', type: 'float', default_value: undefined },
  3192. { key: 'max_value', label: 'Max Value', type: 'float', default_value: undefined },
  3193. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3194. { key: 'height', type: 'int', default_value: 30, hidden: true }
  3195. ] ),
  3196. config_onchange: function() {
  3197. this.set_name(this.config.get_value('name'));
  3198. this.request_draw({ clear_tile_cache: true });
  3199. },
  3200. /**
  3201. * Actions to be taken before drawing.
  3202. */
  3203. // FIXME: can the default behavior be used; right now it breaks during resize.
  3204. before_draw: function() {},
  3205. /**
  3206. * Draw track tile.
  3207. */
  3208. draw_tile: function(result, ctx, mode, region, w_scale) {
  3209. return this._draw_line_track_tile(result, ctx, mode, region, w_scale);
  3210. },
  3211. /**
  3212. * Subset data only if data is at single-base pair resolution.
  3213. */
  3214. can_subset: function(entry) {
  3215. return (entry.data[1][0] - entry.data[0][0] === 1);
  3216. },
  3217. /**
  3218. * Add min, max labels.
  3219. */
  3220. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3221. // Add min, max labels.
  3222. this._add_yaxis_label('max');
  3223. this._add_yaxis_label('min');
  3224. }
  3225. });
  3226. /**
  3227. * Diagonal heatmap for showing interactions data.
  3228. */
  3229. var DiagonalHeatmapTrack = function (view, container, obj_dict) {
  3230. this.mode = "Heatmap";
  3231. TiledTrack.call(this, view, container, obj_dict);
  3232. };
  3233. extend(DiagonalHeatmapTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3234. display_modes: ["Heatmap"],
  3235. config_params: _.union( Drawable.prototype.config_params, [
  3236. { key: 'pos_color', label: 'Positive Color', type: 'color', default_value: "#FF8C00" },
  3237. { key: 'neg_color', label: 'Negative Color', type: 'color', default_value: "#4169E1" },
  3238. { key: 'min_value', label: 'Min Value', type: 'float', default_value: -1 },
  3239. { key: 'max_value', label: 'Max Value', type: 'float', default_value: 1 },
  3240. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3241. { key: 'height', type: 'int', default_value: 500, hidden: true }
  3242. ] ),
  3243. config_onchange: function() {
  3244. this.set_name(this.config.get_value('name'));
  3245. this.request_draw({ clear_tile_cache: true });
  3246. },
  3247. /**
  3248. * Draw tile.
  3249. */
  3250. draw_tile: function(result, ctx, mode, region, w_scale) {
  3251. // Paint onto canvas.
  3252. var canvas = ctx.canvas,
  3253. painter = new painters.DiagonalHeatmapPainter(result.data, region.get('start'), region.get('end'), this.config.to_key_value_dict(), mode);
  3254. painter.draw(ctx, canvas.width, canvas.height, w_scale);
  3255. return new Tile(this, region, w_scale, canvas, result.data);
  3256. }
  3257. });
  3258. /**
  3259. * A track that displays features/regions. Track expects position data in BED format, i.e. 0-based, half-open.
  3260. */
  3261. var FeatureTrack = function(view, container, obj_dict) {
  3262. TiledTrack.call(this, view, container, obj_dict);
  3263. this.container_div.addClass( "feature-track" );
  3264. this.summary_draw_height = 30;
  3265. this.slotters = {};
  3266. this.start_end_dct = {};
  3267. this.left_offset = 200;
  3268. // this.painter = painters.LinkedFeaturePainter;
  3269. this.set_painter_from_config();
  3270. };
  3271. extend(FeatureTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3272. display_modes: ["Auto", "Coverage", "Dense", "Squish", "Pack"],
  3273. config_params: _.union( Drawable.prototype.config_params, [
  3274. { key: 'block_color', label: 'Block color', type: 'color' },
  3275. { key: 'reverse_strand_color', label: 'Antisense strand color', type: 'color' },
  3276. { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' },
  3277. { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true,
  3278. help: 'Show the number of items in each bin when drawing summary histogram' },
  3279. { key: 'min_value', label: 'Histogram minimum', type: 'float', default_value: null, help: 'clear value to set automatically' },
  3280. { key: 'max_value', label: 'Histogram maximum', type: 'float', default_value: null, help: 'clear value to set automatically' },
  3281. { key: 'connector_style', label: 'Connector style', type: 'select', default_value: 'fishbones',
  3282. options: [ { label: 'Line with arrows', value: 'fishbone' }, { label: 'Arcs', value: 'arcs' } ] },
  3283. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3284. { key: 'height', type: 'int', default_value: 0, hidden: true}
  3285. ] ),
  3286. config_onchange: function() {
  3287. this.set_name(this.config.get_value('name'));
  3288. this.set_painter_from_config();
  3289. this.request_draw({ clear_tile_cache: true });
  3290. },
  3291. set_painter_from_config: function() {
  3292. if ( this.config.get_value('connector_style') === 'arcs' ) {
  3293. this.painter = painters.ArcLinkedFeaturePainter;
  3294. } else {
  3295. this.painter = painters.LinkedFeaturePainter;
  3296. }
  3297. },
  3298. /**
  3299. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  3300. * drawn/fetched and shown.
  3301. */
  3302. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3303. TiledTrack.prototype.postdraw_actions.call(this, tiles, width, w_scale, clear_after);
  3304. var track = this,
  3305. i,
  3306. line_track_tiles = _.filter(tiles, function(t) {
  3307. return (t instanceof LineTrackTile);
  3308. });
  3309. //
  3310. // Finish drawing of features that span multiple tiles. Features that span multiple tiles
  3311. // are labeled incomplete on the tile level because they cannot be completely drawn.
  3312. //
  3313. if (line_track_tiles.length === 0) {
  3314. // Gather incomplete features together.
  3315. var all_incomplete_features = {};
  3316. _.each(_.pluck(tiles, 'incomplete_features'), function(inc_features) {
  3317. _.each(inc_features, function(feature) {
  3318. all_incomplete_features[feature[0]] = feature;
  3319. });
  3320. });
  3321. // Draw incomplete features on each tile.
  3322. var self = this;
  3323. _.each(tiles, function(tile) {
  3324. // Remove features already drawn on tile originally.
  3325. var tile_incomplete_features =_.omit(all_incomplete_features,
  3326. _.map(tile.incomplete_features, function(f) { return f[0]; }));
  3327. // Remove features already drawn on tile in past postdraw actions.
  3328. tile_incomplete_features = _.omit(tile_incomplete_features, _.keys(tile.other_tiles_features_drawn));
  3329. // Draw tile's incomplete features.
  3330. if (_.size(tile_incomplete_features) !== 0) {
  3331. // To draw incomplete features, create new canvas, copy original canvas/tile onto new
  3332. // canvas, and then draw incomplete features on the new canvas.
  3333. var features = { data: _.values( tile_incomplete_features ) },
  3334. new_canvas = self.view.canvas_manager.new_canvas(),
  3335. new_canvas_ctx = new_canvas.getContext('2d');
  3336. new_canvas.height = Math.max(tile.canvas.height,
  3337. self.get_canvas_height(features, tile.mode, tile.w_scale, 100));
  3338. new_canvas.width = tile.canvas.width;
  3339. new_canvas_ctx.drawImage(tile.canvas, 0, 0);
  3340. new_canvas_ctx.translate(track.left_offset, 0);
  3341. var new_tile = self.draw_tile(features, new_canvas_ctx, tile.mode,
  3342. tile.region, tile.w_scale, tile.seq_data);
  3343. $(tile.canvas).replaceWith($(new_tile.canvas));
  3344. tile.canvas = new_canvas;
  3345. _.extend(tile.other_tiles_features_drawn, all_incomplete_features);
  3346. }
  3347. });
  3348. }
  3349. // If mode is Coverage and tiles do not share max, redraw tiles as necessary using new max.
  3350. /*
  3351. This code isn't used right now because Coverage mode uses predefined max in preferences.
  3352. if (track.mode === "Coverage") {
  3353. // Get global max.
  3354. var global_max = -1;
  3355. for (i = 0; i < tiles.length; i++) {
  3356. var cur_max = tiles[i].max_val;
  3357. if (cur_max > global_max) {
  3358. global_max = cur_max;
  3359. }
  3360. }
  3361. for (i = 0; i < tiles.length; i++) {
  3362. var tile = tiles[i];
  3363. if (tile.max_val !== global_max) {
  3364. tile.html_elt.remove();
  3365. track.draw_helper(tile.index, w_scale, { more_tile_data: { force: true, max: global_max } } );
  3366. }
  3367. }
  3368. }
  3369. */
  3370. //
  3371. // Update filter attributes, UI.
  3372. //
  3373. // Update filtering UI.
  3374. if (track.filters_manager) {
  3375. var filters = track.filters_manager.filters,
  3376. f;
  3377. for (f = 0; f < filters.length; f++) {
  3378. filters[f].update_ui_elt();
  3379. }
  3380. // Determine if filters are available; this is based on the tiles' data.
  3381. // Criteria for filter to be available: (a) it is applicable to tile data and (b) filter min != filter max.
  3382. var filters_available = false,
  3383. example_feature,
  3384. filter;
  3385. for (i = 0; i < tiles.length; i++) {
  3386. if (tiles[i].data.length) {
  3387. example_feature = tiles[i].data[0];
  3388. for (f = 0; f < filters.length; f++) {
  3389. filter = filters[f];
  3390. if ( filter.applies_to(example_feature) &&
  3391. filter.min !== filter.max ) {
  3392. filters_available = true;
  3393. break;
  3394. }
  3395. }
  3396. }
  3397. }
  3398. // If filter availability changed, hide filter div if necessary and update menu.
  3399. if (track.filters_available !== filters_available) {
  3400. track.filters_available = filters_available;
  3401. if (!track.filters_available) {
  3402. track.filters_manager.hide();
  3403. }
  3404. track.update_icons();
  3405. }
  3406. }
  3407. //
  3408. // If not all features slotted, show icon for showing more rows (slots).
  3409. //
  3410. if (tiles[0] instanceof FeatureTrackTile) {
  3411. var all_slotted = true;
  3412. for (i = 0; i < tiles.length; i++) {
  3413. if (!tiles[i].all_slotted) {
  3414. all_slotted = false;
  3415. break;
  3416. }
  3417. }
  3418. this.action_icons.show_more_rows_icon.toggle(!all_slotted);
  3419. }
  3420. else {
  3421. this.action_icons.show_more_rows_icon.hide();
  3422. }
  3423. },
  3424. /**
  3425. * Update track interface to show display mode being used.
  3426. */
  3427. update_auto_mode: function(mode) {
  3428. if (this.mode === "Auto") {
  3429. if (mode === "no_detail") {
  3430. mode = "feature spans";
  3431. }
  3432. this.action_icons.mode_icon.attr("title", "Set display mode (now: Auto/" + mode + ")");
  3433. }
  3434. },
  3435. /**
  3436. * Place features in slots for drawing (i.e. pack features).
  3437. * this.slotters[level] is created in this method. this.slotters[level]
  3438. * is a Slotter object. Returns the number of slots used to pack features.
  3439. */
  3440. incremental_slots: function(level, features, mode) {
  3441. // Get/create incremental slots for level. If display mode changed,
  3442. // need to create new slots.
  3443. var dummy_context = this.view.canvas_manager.dummy_context,
  3444. slotter = this.slotters[level];
  3445. if (!slotter || (slotter.mode !== mode)) {
  3446. slotter = new (slotting.FeatureSlotter)( level, mode, MAX_FEATURE_DEPTH, function ( x ) { return dummy_context.measureText( x ); } );
  3447. this.slotters[level] = slotter;
  3448. }
  3449. return slotter.slot_features( features );
  3450. },
  3451. /**
  3452. * Returns appropriate display mode based on data.
  3453. */
  3454. get_mode: function(data) {
  3455. // HACK: use no_detail mode track is in overview to prevent overview from being too large.
  3456. if (data.extra_info === "no_detail" || this.is_overview) {
  3457. mode = "no_detail";
  3458. }
  3459. else {
  3460. // Choose b/t Squish and Pack.
  3461. // Proxy measures for using Squish:
  3462. // (a) error message re: limiting number of features shown;
  3463. // (b) X number of features shown;
  3464. // (c) size of view shown.
  3465. // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles;
  3466. // fix this so that tiles are redrawn as necessary to use the same mode.
  3467. //if ( (result.message && result.message.match(/^Only the first [\d]+/)) ||
  3468. // (result.data && result.data.length > 2000) ||
  3469. //var data = result.data;
  3470. // if ( (data.length && data.length < 4) ||
  3471. // (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) {
  3472. if ( this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH ) {
  3473. mode = "Squish";
  3474. } else {
  3475. mode = "Pack";
  3476. }
  3477. }
  3478. return mode;
  3479. },
  3480. /**
  3481. * Returns canvas height needed to display data; return value is an integer that denotes the
  3482. * number of pixels required.
  3483. */
  3484. get_canvas_height: function(result, mode, w_scale, canvas_width) {
  3485. if (mode === "Coverage" || result.dataset_type === 'bigwig') {
  3486. return this.summary_draw_height;
  3487. }
  3488. else {
  3489. // All other modes require slotting.
  3490. var rows_required = this.incremental_slots(w_scale, result.data, mode);
  3491. // HACK: use dummy painter to get required height. Painter should be extended so that get_required_height
  3492. // works as a static function.
  3493. var dummy_painter = new (this.painter)(null, null, null, this.config.to_key_value_dict(), mode);
  3494. return Math.max(this.min_height_px, dummy_painter.get_required_height(rows_required, canvas_width) );
  3495. }
  3496. },
  3497. /**
  3498. * Draw FeatureTrack tile.
  3499. * @param result result from server
  3500. * @param cxt canvas context to draw on
  3501. * @param mode mode to draw in
  3502. * @param region region to draw on tile
  3503. * @param w_scale pixels per base
  3504. * @param ref_seq reference sequence data
  3505. * @param cur_tile true if drawing is occurring on a currently visible tile.
  3506. */
  3507. draw_tile: function(result, ctx, mode, region, w_scale, ref_seq, cur_tile) {
  3508. var track = this,
  3509. canvas = ctx.canvas,
  3510. tile_low = region.get('start'),
  3511. tile_high = region.get('end'),
  3512. left_offset = this.left_offset;
  3513. // If data is line track data, draw line track tile.
  3514. if (result.dataset_type === 'bigwig') {
  3515. return this._draw_line_track_tile(result, ctx, mode, region, w_scale);
  3516. }
  3517. // Handle row-by-row tracks
  3518. // Preprocessing: filter features and determine whether all unfiltered features have been slotted.
  3519. var
  3520. filtered = [],
  3521. slots = this.slotters[w_scale].slots;
  3522. all_slotted = true;
  3523. if ( result.data ) {
  3524. var filters = this.filters_manager.filters;
  3525. for (var i = 0, len = result.data.length; i < len; i++) {
  3526. var feature = result.data[i];
  3527. var hide_feature = false;
  3528. var filter;
  3529. for (var f = 0, flen = filters.length; f < flen; f++) {
  3530. filter = filters[f];
  3531. filter.update_attrs(feature);
  3532. if (!filter.keep(feature)) {
  3533. hide_feature = true;
  3534. break;
  3535. }
  3536. }
  3537. if (!hide_feature) {
  3538. // Feature visible.
  3539. filtered.push(feature);
  3540. // Set flag if not slotted.
  3541. if ( !(feature[0] in slots) ) {
  3542. all_slotted = false;
  3543. }
  3544. }
  3545. }
  3546. }
  3547. // Create painter.
  3548. var filter_alpha_scaler = (this.filters_manager.alpha_filter ? new FilterScaler(this.filters_manager.alpha_filter) : null),
  3549. filter_height_scaler = (this.filters_manager.height_filter ? new FilterScaler(this.filters_manager.height_filter) : null),
  3550. painter = new (this.painter)(filtered, tile_low, tile_high, this.config.to_key_value_dict(), mode, filter_alpha_scaler, filter_height_scaler,
  3551. // HACK: ref_seq only be defined for ReadTracks, and only the ReadPainter accepts that argument
  3552. ref_seq,
  3553. // Only the ReadPainer will use this function
  3554. function(b) { return track.view.get_base_color(b); });
  3555. var feature_mapper = null;
  3556. ctx.fillStyle = this.config.get_value('block_color');
  3557. ctx.font = ctx.canvas.manager.default_font;
  3558. ctx.textAlign = "right";
  3559. if (result.data) {
  3560. // Draw features.
  3561. var draw_results = painter.draw(ctx, canvas.width, canvas.height, w_scale, slots);
  3562. feature_mapper = draw_results.feature_mapper;
  3563. incomplete_features = draw_results.incomplete_features;
  3564. feature_mapper.translation = -left_offset;
  3565. }
  3566. // If not drawing on current tile, create new tile.
  3567. if (!cur_tile) {
  3568. return new FeatureTrackTile(track, region, w_scale, canvas, result.data, mode,
  3569. result.message, all_slotted, feature_mapper,
  3570. incomplete_features, ref_seq);
  3571. }
  3572. }
  3573. });
  3574. /**
  3575. * Displays variant data.
  3576. */
  3577. var VariantTrack = function(view, container, obj_dict) {
  3578. TiledTrack.call(this, view, container, obj_dict);
  3579. this.painter = painters.VariantPainter;
  3580. this.summary_draw_height = 30;
  3581. // Maximum resolution is ~45 pixels/base, so use this size left offset to ensure that full
  3582. // variant is drawn when variant is at start of tile.
  3583. this.left_offset = 30;
  3584. };
  3585. extend(VariantTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3586. display_modes: ["Auto", "Coverage", "Dense", "Squish", "Pack"],
  3587. config_params: _.union( Drawable.prototype.config_params, [
  3588. { key: 'color', label: 'Histogram color', type: 'color' },
  3589. { key: 'show_sample_data', label: 'Show sample data', type: 'bool', default_value: true },
  3590. { key: 'show_labels', label: 'Show summary and sample labels', type: 'bool', default_value: true },
  3591. { key: 'summary_height', label: 'Locus summary height', type: 'float', default_value: 20 },
  3592. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3593. { key: 'height', type: 'int', default_value: 0, hidden: true }
  3594. ] ),
  3595. config_onchange: function() {
  3596. this.set_name(this.config.get_value('name'));
  3597. this.request_draw({ clear_tile_cache: true });
  3598. },
  3599. /**
  3600. * Draw tile.
  3601. */
  3602. draw_tile: function(result, ctx, mode, region, w_scale) {
  3603. // Data could be coverage data or variant data.
  3604. if (result.dataset_type === 'bigwig') {
  3605. return this._draw_line_track_tile(result, ctx, "Histogram", region, w_scale);
  3606. }
  3607. else { // result.dataset_type === 'variant'
  3608. var view = this.view,
  3609. painter = new (this.painter)(result.data, region.get('start'), region.get('end'), this.config.to_key_value_dict(), mode,
  3610. function(b) { return view.get_base_color(b); });
  3611. painter.draw(ctx, ctx.canvas.width, ctx.canvas.height, w_scale);
  3612. return new Tile(this, region, w_scale, ctx.canvas, result.data);
  3613. }
  3614. },
  3615. /**
  3616. * Returns canvas height needed to display data; return value is an integer that denotes the
  3617. * number of pixels required.
  3618. */
  3619. get_canvas_height: function(result, mode, w_scale, canvas_width) {
  3620. if (result.dataset_type === 'bigwig') {
  3621. return this.summary_draw_height;
  3622. }
  3623. else {
  3624. // HACK: sample_names is not be defined when dataset definition is fetched before
  3625. // dataset is complete (as is done when running tools). In that case, fall back on
  3626. // # of samples in data. This can be fixed by re-requesting dataset definition
  3627. // in init.
  3628. var num_samples = ( this.dataset.get_metadata('sample_names') ? this.dataset.get_metadata('sample_names').length : 0);
  3629. if (num_samples === 0 && result.data.length !== 0) {
  3630. // Sample data is separated by commas, so this computes # of samples:
  3631. num_samples = result.data[0][7].match(/,/g);
  3632. if ( num_samples === null ) {
  3633. num_samples = 1;
  3634. }
  3635. else {
  3636. num_samples = num_samples.length + 1;
  3637. }
  3638. }
  3639. var dummy_painter = new (this.painter)(null, null, null, this.config.to_key_value_dict(), mode);
  3640. return dummy_painter.get_required_height(num_samples);
  3641. }
  3642. },
  3643. /**
  3644. * Additional initialization required before drawing track for the first time.
  3645. */
  3646. predraw_init: function() {
  3647. var deferreds = [ Track.prototype.predraw_init.call(this) ];
  3648. // FIXME: updating dataset metadata is only needed for visual analysis. Can
  3649. // this be moved somewhere else?
  3650. if (!this.dataset.get_metadata('sample_names')) {
  3651. deferreds.push(this.dataset.fetch());
  3652. }
  3653. return deferreds;
  3654. },
  3655. /**
  3656. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  3657. * drawn/fetched and shown.
  3658. */
  3659. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3660. TiledTrack.prototype.postdraw_actions.call(this, tiles, width, w_scale, clear_after);
  3661. var line_track_tiles = _.filter(tiles, function(t) {
  3662. return (t instanceof LineTrackTile);
  3663. });
  3664. // Add summary/sample labels if needed and not already included.
  3665. var sample_names = this.dataset.get_metadata('sample_names');
  3666. if (line_track_tiles.length === 0 && this.config.get_value('show_labels') && sample_names && sample_names.length > 1) {
  3667. var font_size;
  3668. // Add and/or style labels.
  3669. if (this.container_div.find('.yaxislabel.variant').length === 0) {
  3670. // Add summary and sample labels.
  3671. // Add summary label to middle of summary area.
  3672. font_size = this.config.get_value('summary_height') / 2;
  3673. this.tiles_div.prepend(
  3674. $("<div/>").text('Summary').addClass('yaxislabel variant top').css({
  3675. 'font-size': font_size + 'px',
  3676. 'top': (this.config.get_value('summary_height') - font_size) / 2 + 'px'
  3677. })
  3678. );
  3679. // Show sample labels.
  3680. if (this.config.get_value('show_sample_data')) {
  3681. var samples_div_html = sample_names.join('<br/>');
  3682. this.tiles_div.prepend(
  3683. $("<div/>").html(samples_div_html).addClass('yaxislabel variant top sample').css({
  3684. 'top': this.config.get_value('summary_height')
  3685. })
  3686. );
  3687. }
  3688. }
  3689. // Style labels.
  3690. // Match sample font size to mode.
  3691. font_size = (this.mode === 'Squish' ? 5 : 10) + 'px';
  3692. $(this.tiles_div).find('.sample').css({
  3693. 'font-size': font_size,
  3694. 'line-height': font_size
  3695. });
  3696. // Color labels to preference color.
  3697. $(this.tiles_div).find('.yaxislabel').css('color', this.config.get_value('label_color'));
  3698. }
  3699. else {
  3700. // Remove all labels.
  3701. this.container_div.find('.yaxislabel.variant').remove();
  3702. }
  3703. }
  3704. });
  3705. /**
  3706. * Track that displays mapped reads. Track expects position data in 1-based, closed format, i.e. SAM/BAM format.
  3707. */
  3708. var ReadTrack = function (view, container, obj_dict) {
  3709. FeatureTrack.call(this, view, container, obj_dict);
  3710. this.painter = painters.ReadPainter;
  3711. this.update_icons();
  3712. };
  3713. extend(ReadTrack.prototype, Drawable.prototype, TiledTrack.prototype, FeatureTrack.prototype, {
  3714. config_params: _.union( Drawable.prototype.config_params, [
  3715. { key: 'block_color', label: 'Histogram color', type: 'color' },
  3716. { key: 'detail_block_color', label: 'Sense strand block color', type: 'color', 'default_value': '#AAAAAA' },
  3717. { key: 'reverse_strand_color', label: 'Antisense strand block color', type: 'color', 'default_value': '#DDDDDD' },
  3718. { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' },
  3719. { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false },
  3720. { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true },
  3721. { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true },
  3722. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3723. { key: 'min_value', label: 'Histogram minimum', type: 'float', default_value: null, help: 'clear value to set automatically' },
  3724. { key: 'max_value', label: 'Histogram maximum', type: 'float', default_value: null, help: 'clear value to set automatically' },
  3725. { key: 'height', type: 'int', default_value: 0, hidden: true}
  3726. ] ),
  3727. config_onchange: function() {
  3728. this.set_name(this.config.get_value('name'));
  3729. this.request_draw({ clear_tile_cache: true });
  3730. }
  3731. });
  3732. /**
  3733. * Objects that can be added to a view.
  3734. */
  3735. var addable_objects = {
  3736. "CompositeTrack": CompositeTrack,
  3737. "DrawableGroup": DrawableGroup,
  3738. "DiagonalHeatmapTrack": DiagonalHeatmapTrack,
  3739. "FeatureTrack": FeatureTrack,
  3740. "LineTrack": LineTrack,
  3741. "ReadTrack": ReadTrack,
  3742. "VariantTrack": VariantTrack,
  3743. // For backward compatibility, map vcf track to variant.
  3744. "VcfTrack": VariantTrack
  3745. };
  3746. /**
  3747. * Create new object from a template. A template can be either an object dictionary or an
  3748. * object itself.
  3749. */
  3750. var object_from_template = function(template, view, container) {
  3751. if ('copy' in template) {
  3752. // Template is an object.
  3753. return template.copy(container);
  3754. }
  3755. else {
  3756. // Template is a dictionary.
  3757. var
  3758. drawable_type = template.obj_type;
  3759. // For backward compatibility:
  3760. if (!drawable_type) {
  3761. drawable_type = template.track_type;
  3762. }
  3763. return new addable_objects[ drawable_type ](view, container, template);
  3764. }
  3765. };
  3766. return {
  3767. TracksterView: TracksterView,
  3768. DrawableGroup: DrawableGroup,
  3769. LineTrack: LineTrack,
  3770. FeatureTrack: FeatureTrack,
  3771. DiagonalHeatmapTrack: DiagonalHeatmapTrack,
  3772. ReadTrack: ReadTrack,
  3773. VariantTrack: VariantTrack,
  3774. CompositeTrack: CompositeTrack,
  3775. object_from_template: object_from_template
  3776. };
  3777. });