PageRenderTime 39ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

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

https://bitbucket.org/jmchilton/galaxy-central-reports-config-enhancements
JavaScript | 4204 lines | 2920 code | 322 blank | 962 comment | 389 complexity | 4de42d836fcdceb4af8a735569ef0ada MD5 | raw file
  1. define( ["libs/underscore", "viz/visualization", "viz/trackster/util",
  2. "viz/trackster/slotting", "viz/trackster/painters", "mvc/data",
  3. "viz/trackster/filters" ],
  4. function( _, visualization, util, slotting, painters, data, filters_mod ) {
  5. var extend = _.extend;
  6. var get_random_color = util.get_random_color;
  7. /**
  8. * Helper to determine if object is jQuery deferred.
  9. */
  10. var is_deferred = function ( d ) {
  11. return ( 'isResolved' in d );
  12. };
  13. // ---- Web UI specific utilities ----
  14. /**
  15. * Dictionary of HTML element-JavaScript object relationships.
  16. */
  17. // TODO: probably should separate moveable objects from containers.
  18. var html_elt_js_obj_dict = {};
  19. /**
  20. * Designates an HTML as a container.
  21. */
  22. var is_container = function(element, obj) {
  23. html_elt_js_obj_dict[element.attr("id")] = obj;
  24. };
  25. /**
  26. * Make `element` moveable within parent and sibling elements by dragging `handle` (a selector).
  27. * Function manages JS objects, containers as well.
  28. *
  29. * @param element HTML element to make moveable
  30. * @param handle_class classname that denotes HTML element to be used as handle
  31. * @param container_selector selector used to identify possible containers for this element
  32. * @param element_js_obj JavaScript object associated with element; used
  33. */
  34. var moveable = function(element, handle_class, container_selector, element_js_obj) {
  35. // HACK: set default value for container selector.
  36. container_selector = ".group";
  37. var css_border_props = {};
  38. // Register element with its object.
  39. html_elt_js_obj_dict[element.attr("id")] = element_js_obj;
  40. // Need to provide selector for handle, not class.
  41. element.bind( "drag", { handle: "." + handle_class, relative: true }, function ( e, d ) {
  42. var element = $(this),
  43. parent = $(this).parent(),
  44. children = parent.children(),
  45. this_obj = html_elt_js_obj_dict[$(this).attr("id")],
  46. child,
  47. container,
  48. top,
  49. bottom,
  50. i;
  51. //
  52. // Enable three types of dragging: (a) out of container; (b) into container;
  53. // (c) sibling movement, aka sorting. Handle in this order for simplicity.
  54. //
  55. // Handle dragging out of container.
  56. container = $(this).parents(container_selector);
  57. if (container.length !== 0) {
  58. top = container.position().top;
  59. bottom = top + container.outerHeight();
  60. if (d.offsetY < top) {
  61. // Moving above container.
  62. $(this).insertBefore(container);
  63. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  64. cur_container.remove_drawable(this_obj);
  65. cur_container.container.add_drawable_before(this_obj, cur_container);
  66. return;
  67. }
  68. else if (d.offsetY > bottom) {
  69. // Moving below container.
  70. $(this).insertAfter(container);
  71. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  72. cur_container.remove_drawable(this_obj);
  73. cur_container.container.add_drawable(this_obj);
  74. return;
  75. }
  76. }
  77. // Handle dragging into container. Child is appended to container's content_div.
  78. container = null;
  79. for ( i = 0; i < children.length; i++ ) {
  80. child = $(children.get(i));
  81. top = child.position().top;
  82. bottom = top + child.outerHeight();
  83. // Dragging into container if child is a container and offset is inside container.
  84. if ( child.is(container_selector) && this !== child.get(0) &&
  85. d.offsetY >= top && d.offsetY <= bottom ) {
  86. // Append/prepend based on where offsetY is closest to and return.
  87. if (d.offsetY - top < bottom - d.offsetY) {
  88. child.find(".content-div").prepend(this);
  89. }
  90. else {
  91. child.find(".content-div").append(this);
  92. }
  93. // Update containers. Object may not have container if it is being moved quickly.
  94. if (this_obj.container) {
  95. this_obj.container.remove_drawable(this_obj);
  96. }
  97. html_elt_js_obj_dict[child.attr("id")].add_drawable(this_obj);
  98. return;
  99. }
  100. }
  101. // Handle sibling movement, aka sorting.
  102. // Determine new position
  103. for ( i = 0; i < children.length; i++ ) {
  104. child = $(children.get(i));
  105. if ( d.offsetY < child.position().top &&
  106. // Cannot move tracks above reference track or intro div.
  107. !(child.hasClass("reference-track") || child.hasClass("intro")) ) {
  108. break;
  109. }
  110. }
  111. // If not already in the right place, move. Need
  112. // to handle the end specially since we don't have
  113. // insert at index
  114. if ( i === children.length ) {
  115. if ( this !== children.get(i - 1) ) {
  116. parent.append(this);
  117. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, i);
  118. }
  119. }
  120. else if ( this !== children.get(i) ) {
  121. $(this).insertBefore( children.get(i) );
  122. // Need to adjust insert position if moving down because move is changing
  123. // indices of all list items.
  124. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, (d.deltaY > 0 ? i-1 : i) );
  125. }
  126. }).bind("dragstart", function() {
  127. css_border_props["border-top"] = element.css("border-top");
  128. css_border_props["border-bottom"] = element.css("border-bottom");
  129. $(this).css({
  130. "border-top": "1px solid blue",
  131. "border-bottom": "1px solid blue"
  132. });
  133. }).bind("dragend", function() {
  134. $(this).css(css_border_props);
  135. });
  136. };
  137. // TODO: do we need to export?
  138. exports.moveable = moveable;
  139. /**
  140. * Init constants & functions used throughout trackster.
  141. */
  142. var
  143. // Minimum height of a track's contents; this must correspond to the .track-content's minimum height.
  144. MIN_TRACK_HEIGHT = 16,
  145. // FIXME: font size may not be static
  146. CHAR_HEIGHT_PX = 9,
  147. // Padding at the top of tracks for error messages
  148. ERROR_PADDING = 20,
  149. // Maximum number of rows un a slotted track
  150. MAX_FEATURE_DEPTH = 100,
  151. // Minimum width for window for squish to be used.
  152. MIN_SQUISH_VIEW_WIDTH = 12000,
  153. // Other constants.
  154. // Number of pixels per tile, not including left offset.
  155. TILE_SIZE = 400,
  156. DEFAULT_DATA_QUERY_WAIT = 5000,
  157. // Maximum number of chromosomes that are selectable at any one time.
  158. MAX_CHROMS_SELECTABLE = 100,
  159. DATA_ERROR = "There was an error in indexing this dataset. ",
  160. DATA_NOCONVERTER = "A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",
  161. DATA_NONE = "No data for this chrom/contig.",
  162. DATA_PENDING = "Preparing data. This can take a while for a large dataset. " +
  163. "If the visualization is saved and closed, preparation will continue in the background.",
  164. DATA_CANNOT_RUN_TOOL = "Tool cannot be rerun: ",
  165. DATA_LOADING = "Loading data...",
  166. DATA_OK = "Ready for display",
  167. TILE_CACHE_SIZE = 10,
  168. DATA_CACHE_SIZE = 20;
  169. /**
  170. * Round a number to a given number of decimal places.
  171. */
  172. function round(num, places) {
  173. // Default rounding is to integer.
  174. if (!places) {
  175. places = 0;
  176. }
  177. var val = Math.pow(10, places);
  178. return Math.round(num * val) / val;
  179. }
  180. /**
  181. * Drawables hierarchy:
  182. *
  183. * Drawable
  184. * --> DrawableCollection
  185. * --> DrawableGroup
  186. * --> View
  187. * --> Track
  188. */
  189. /**
  190. * Base class for all drawable objects. Drawable objects are associated with a view and live in a
  191. * container. They have the following HTML elements and structure:
  192. * <container_div>
  193. * <header_div>
  194. * <content_div>
  195. *
  196. * They optionally have a drag handle class.
  197. */
  198. var Drawable = function(view, container, obj_dict) {
  199. if (!Drawable.id_counter) { Drawable.id_counter = 0; }
  200. this.id = Drawable.id_counter++;
  201. this.name = obj_dict.name;
  202. this.view = view;
  203. this.container = container;
  204. this.config = new DrawableConfig({
  205. track: this,
  206. params: [
  207. { key: 'name', label: 'Name', type: 'text', default_value: this.name }
  208. ],
  209. saved_values: obj_dict.prefs,
  210. onchange: function() {
  211. this.track.set_name(this.track.config.values.name);
  212. }
  213. });
  214. this.prefs = this.config.values;
  215. this.drag_handle_class = obj_dict.drag_handle_class;
  216. this.is_overview = false;
  217. this.action_icons = {};
  218. // FIXME: this should be a saved setting
  219. this.content_visible = true;
  220. // Build Drawable HTML and behaviors.
  221. this.container_div = this.build_container_div();
  222. this.header_div = this.build_header_div();
  223. if (this.header_div) {
  224. this.container_div.append(this.header_div);
  225. // Icons container.
  226. this.icons_div = $("<div/>").css("float", "left").hide().appendTo(this.header_div);
  227. this.build_action_icons(this.action_icons_def);
  228. this.header_div.append( $("<div style='clear: both'/>") );
  229. // Suppress double clicks in header so that they do not impact viz.
  230. this.header_div.dblclick( function(e) { e.stopPropagation(); } );
  231. // Show icons when users is hovering over track.
  232. var drawable = this;
  233. this.container_div.hover(
  234. function() { drawable.icons_div.show(); }, function() { drawable.icons_div.hide(); }
  235. );
  236. // Needed for floating elts in header.
  237. $("<div style='clear: both'/>").appendTo(this.container_div);
  238. }
  239. };
  240. Drawable.prototype.action_icons_def = [
  241. // Hide/show drawable content.
  242. // FIXME: make this an odict for easier lookup.
  243. {
  244. name: "toggle_icon",
  245. title: "Hide/show content",
  246. css_class: "toggle",
  247. on_click_fn: function(drawable) {
  248. if ( drawable.content_visible ) {
  249. drawable.action_icons.toggle_icon.addClass("toggle-expand").removeClass("toggle");
  250. drawable.hide_contents();
  251. drawable.content_visible = false;
  252. } else {
  253. drawable.action_icons.toggle_icon.addClass("toggle").removeClass("toggle-expand");
  254. drawable.content_visible = true;
  255. drawable.show_contents();
  256. }
  257. }
  258. },
  259. // Edit settings.
  260. {
  261. name: "settings_icon",
  262. title: "Edit settings",
  263. css_class: "settings-icon",
  264. on_click_fn: function(drawable) {
  265. var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
  266. ok_fn = function() {
  267. drawable.config.update_from_form( $(".dialog-box") );
  268. hide_modal();
  269. $(window).unbind("keypress.check_enter_esc");
  270. },
  271. check_enter_esc = function(e) {
  272. if ((e.keyCode || e.which) === 27) { // Escape key
  273. cancel_fn();
  274. } else if ((e.keyCode || e.which) === 13) { // Enter key
  275. ok_fn();
  276. }
  277. };
  278. $(window).bind("keypress.check_enter_esc", check_enter_esc);
  279. show_modal("Configure", drawable.config.build_form(), {
  280. "Cancel": cancel_fn,
  281. "OK": ok_fn
  282. });
  283. }
  284. },
  285. // Remove.
  286. {
  287. name: "remove_icon",
  288. title: "Remove",
  289. css_class: "remove-icon",
  290. on_click_fn: function(drawable) {
  291. // Tipsy for remove icon must be deleted when drawable is deleted.
  292. $(".bs-tooltip").remove();
  293. drawable.remove();
  294. }
  295. }
  296. ];
  297. extend(Drawable.prototype, {
  298. init: function() {},
  299. changed: function() {
  300. this.view.changed();
  301. },
  302. can_draw: function() {
  303. if (this.enabled && this.content_visible) {
  304. return true;
  305. }
  306. return false;
  307. },
  308. request_draw: function() {},
  309. _draw: function() {},
  310. /**
  311. * Returns representation of object in a dictionary for easy saving.
  312. * Use from_dict to recreate object.
  313. */
  314. to_dict: function() {},
  315. /**
  316. * Set drawable name.
  317. */
  318. set_name: function(new_name) {
  319. this.old_name = this.name;
  320. this.name = new_name;
  321. this.name_div.text(this.name);
  322. },
  323. /**
  324. * Revert track name; currently name can be reverted only once.
  325. */
  326. revert_name: function() {
  327. if (this.old_name) {
  328. this.name = this.old_name;
  329. this.name_div.text(this.name);
  330. }
  331. },
  332. /**
  333. * Remove drawable (a) from its container and (b) from the HTML.
  334. */
  335. remove: function() {
  336. this.changed();
  337. this.container.remove_drawable(this);
  338. var view = this.view;
  339. this.container_div.hide(0, function() {
  340. $(this).remove();
  341. // HACK: is there a better way to update the view?
  342. view.update_intro_div();
  343. });
  344. },
  345. /**
  346. * Build drawable's container div; this is the parent div for all drawable's elements.
  347. */
  348. build_container_div: function() {},
  349. /**
  350. * Build drawable's header div.
  351. */
  352. build_header_div: function() {},
  353. /**
  354. * Add an action icon to this object. Appends icon unless prepend flag is specified.
  355. */
  356. add_action_icon: function(name, title, css_class, on_click_fn, prepend, hide) {
  357. var drawable = this;
  358. this.action_icons[name] = $("<a/>").attr("href", "javascript:void(0);").attr("title", title)
  359. .addClass("icon-button").addClass(css_class).tooltip()
  360. .click( function() { on_click_fn(drawable); } )
  361. .appendTo(this.icons_div);
  362. if (hide) {
  363. this.action_icons[name].hide();
  364. }
  365. },
  366. /**
  367. * Build drawable's icons div from object's icons_dict.
  368. */
  369. build_action_icons: function(action_icons_def) {
  370. // Create icons.
  371. var icon_dict;
  372. for (var i = 0; i < action_icons_def.length; i++) {
  373. icon_dict = action_icons_def[i];
  374. this.add_action_icon(icon_dict.name, icon_dict.title, icon_dict.css_class,
  375. icon_dict.on_click_fn, icon_dict.prepend, icon_dict.hide);
  376. }
  377. },
  378. /**
  379. * Update icons.
  380. */
  381. update_icons: function() {},
  382. /**
  383. * Hide drawable's contents.
  384. */
  385. hide_contents: function () {},
  386. /**
  387. * Show drawable's contents.
  388. */
  389. show_contents: function() {},
  390. /**
  391. * Returns a shallow copy of all drawables in this drawable.
  392. */
  393. get_drawables: function() {}
  394. });
  395. /**
  396. * A collection of drawable objects.
  397. */
  398. var DrawableCollection = function(view, container, obj_dict) {
  399. Drawable.call(this, view, container, obj_dict);
  400. // Attribute init.
  401. this.obj_type = obj_dict.obj_type;
  402. this.drawables = [];
  403. };
  404. extend(DrawableCollection.prototype, Drawable.prototype, {
  405. /**
  406. * Unpack and add drawables to the collection.
  407. */
  408. unpack_drawables: function(drawables_array) {
  409. // Add drawables to collection.
  410. this.drawables = [];
  411. var drawable;
  412. for (var i = 0; i < drawables_array.length; i++) {
  413. drawable = object_from_template(drawables_array[i], this.view, this);
  414. this.add_drawable(drawable);
  415. }
  416. },
  417. /**
  418. * Init each drawable in the collection.
  419. */
  420. init: function() {
  421. for (var i = 0; i < this.drawables.length; i++) {
  422. this.drawables[i].init();
  423. }
  424. },
  425. /**
  426. * Draw each drawable in the collection.
  427. */
  428. _draw: function() {
  429. for (var i = 0; i < this.drawables.length; i++) {
  430. this.drawables[i]._draw();
  431. }
  432. },
  433. /**
  434. * Returns representation of object in a dictionary for easy saving.
  435. * Use from_dict to recreate object.
  436. */
  437. to_dict: function() {
  438. var dictified_drawables = [];
  439. for (var i = 0; i < this.drawables.length; i++) {
  440. dictified_drawables.push(this.drawables[i].to_dict());
  441. }
  442. return {
  443. name: this.name,
  444. prefs: this.prefs,
  445. obj_type: this.obj_type,
  446. drawables: dictified_drawables
  447. };
  448. },
  449. /**
  450. * Add a drawable to the end of the collection.
  451. */
  452. add_drawable: function(drawable) {
  453. this.drawables.push(drawable);
  454. drawable.container = this;
  455. this.changed();
  456. },
  457. /**
  458. * Add a drawable before another drawable.
  459. */
  460. add_drawable_before: function(drawable, other) {
  461. this.changed();
  462. var index = this.drawables.indexOf(other);
  463. if (index !== -1) {
  464. this.drawables.splice(index, 0, drawable);
  465. return true;
  466. }
  467. return false;
  468. },
  469. /**
  470. * Replace one drawable with another.
  471. */
  472. replace_drawable: function(old_drawable, new_drawable, update_html) {
  473. var index = this.drawables.indexOf(old_drawable);
  474. if (index !== -1) {
  475. this.drawables[index] = new_drawable;
  476. if (update_html) {
  477. old_drawable.container_div.replaceWith(new_drawable.container_div);
  478. }
  479. this.changed();
  480. }
  481. return index;
  482. },
  483. /**
  484. * Remove drawable from this collection.
  485. */
  486. remove_drawable: function(drawable) {
  487. var index = this.drawables.indexOf(drawable);
  488. if (index !== -1) {
  489. // Found drawable to remove.
  490. this.drawables.splice(index, 1);
  491. drawable.container = null;
  492. this.changed();
  493. return true;
  494. }
  495. return false;
  496. },
  497. /**
  498. * Move drawable to another location in collection.
  499. */
  500. move_drawable: function(drawable, new_position) {
  501. var index = this.drawables.indexOf(drawable);
  502. if (index !== -1) {
  503. // Remove from current position:
  504. this.drawables.splice(index, 1);
  505. // insert into new position:
  506. this.drawables.splice(new_position, 0, drawable);
  507. this.changed();
  508. return true;
  509. }
  510. return false;
  511. },
  512. /**
  513. * Returns all drawables in this drawable.
  514. */
  515. get_drawables: function() {
  516. return this.drawables;
  517. }
  518. });
  519. /**
  520. * A group of drawables that are moveable, visible.
  521. */
  522. var DrawableGroup = function(view, container, obj_dict) {
  523. extend(obj_dict, {
  524. obj_type: "DrawableGroup",
  525. drag_handle_class: "group-handle"
  526. });
  527. DrawableCollection.call(this, view, container, obj_dict);
  528. // Set up containers/moving for group: register both container_div and content div as container
  529. // because both are used as containers (container div to recognize container, content_div to
  530. // store elements). Group can be moved.
  531. this.content_div = $("<div/>").addClass("content-div").attr("id", "group_" + this.id + "_content_div").appendTo(this.container_div);
  532. is_container(this.container_div, this);
  533. is_container(this.content_div, this);
  534. moveable(this.container_div, this.drag_handle_class, ".group", this);
  535. // Set up filters.
  536. this.filters_manager = new filters_mod.FiltersManager(this);
  537. this.header_div.after(this.filters_manager.parent_div);
  538. // For saving drawables' filter managers when group-level filtering is done:
  539. this.saved_filters_managers = [];
  540. // Add drawables.
  541. if ('drawables' in obj_dict) {
  542. this.unpack_drawables(obj_dict.drawables);
  543. }
  544. // Restore filters.
  545. if ('filters' in obj_dict) {
  546. // FIXME: Pass collection_dict to DrawableCollection/Drawable will make this easier.
  547. var old_manager = this.filters_manager;
  548. this.filters_manager = new filters_mod.FiltersManager(this, obj_dict.filters);
  549. old_manager.parent_div.replaceWith(this.filters_manager.parent_div);
  550. if (obj_dict.filters.visible) {
  551. this.setup_multitrack_filtering();
  552. }
  553. }
  554. };
  555. extend(DrawableGroup.prototype, Drawable.prototype, DrawableCollection.prototype, {
  556. action_icons_def: [
  557. Drawable.prototype.action_icons_def[0],
  558. Drawable.prototype.action_icons_def[1],
  559. // Replace group with composite track.
  560. {
  561. name: "composite_icon",
  562. title: "Show composite track",
  563. css_class: "layers-stack",
  564. on_click_fn: function(group) {
  565. $(".bs-tooltip").remove();
  566. group.show_composite_track();
  567. }
  568. },
  569. // Toggle track filters.
  570. {
  571. name: "filters_icon",
  572. title: "Filters",
  573. css_class: "filters-icon",
  574. on_click_fn: function(group) {
  575. // TODO: update tipsy text.
  576. if (group.filters_manager.visible()) {
  577. // Hiding filters.
  578. group.filters_manager.clear_filters();
  579. group._restore_filter_managers();
  580. // TODO: maintain current filter by restoring and setting saved manager's
  581. // settings to current/shared manager's settings.
  582. // TODO: need to restore filter managers when moving drawable outside group.
  583. }
  584. else {
  585. // Showing filters.
  586. group.setup_multitrack_filtering();
  587. group.request_draw(true);
  588. }
  589. group.filters_manager.toggle();
  590. }
  591. },
  592. Drawable.prototype.action_icons_def[2]
  593. ],
  594. build_container_div: function() {
  595. var container_div = $("<div/>").addClass("group").attr("id", "group_" + this.id);
  596. if (this.container) {
  597. this.container.content_div.append(container_div);
  598. }
  599. return container_div;
  600. },
  601. build_header_div: function() {
  602. var header_div = $("<div/>").addClass("track-header");
  603. header_div.append($("<div/>").addClass(this.drag_handle_class));
  604. this.name_div = $("<div/>").addClass("track-name").text(this.name).appendTo(header_div);
  605. return header_div;
  606. },
  607. hide_contents: function () {
  608. this.tiles_div.hide();
  609. },
  610. show_contents: function() {
  611. // Show the contents div and labels (if present)
  612. this.tiles_div.show();
  613. // Request a redraw of the content
  614. this.request_draw();
  615. },
  616. update_icons: function() {
  617. //
  618. // Handle update when there are no tracks.
  619. //
  620. var num_drawables = this.drawables.length;
  621. if (num_drawables === 0) {
  622. this.action_icons.composite_icon.hide();
  623. this.action_icons.filters_icon.hide();
  624. }
  625. else if (num_drawables === 1) {
  626. if (this.drawables[0] instanceof CompositeTrack) {
  627. this.action_icons.composite_icon.show();
  628. }
  629. this.action_icons.filters_icon.hide();
  630. }
  631. else { // There are 2 or more tracks.
  632. //
  633. // Determine if a composite track can be created. Current criteria:
  634. // (a) all tracks are the same;
  635. // OR
  636. // (b) there is a single FeatureTrack.
  637. //
  638. /// All tracks the same?
  639. var i, j, drawable,
  640. same_type = true,
  641. a_type = this.drawables[0].get_type(),
  642. num_feature_tracks = 0;
  643. for (i = 0; i < num_drawables; i++) {
  644. drawable = this.drawables[i];
  645. if (drawable.get_type() !== a_type) {
  646. can_composite = false;
  647. break;
  648. }
  649. if (drawable instanceof FeatureTrack) {
  650. num_feature_tracks++;
  651. }
  652. }
  653. if (same_type || num_feature_tracks === 1) {
  654. this.action_icons.composite_icon.show();
  655. }
  656. else {
  657. this.action_icons.composite_icon.hide();
  658. $(".bs-tooltip").remove();
  659. }
  660. //
  661. // Set up group-level filtering and update filter icon.
  662. //
  663. if (num_feature_tracks > 1 && num_feature_tracks === this.drawables.length) {
  664. //
  665. // Find shared filters.
  666. //
  667. var shared_filters = {},
  668. filter;
  669. // Init shared filters with filters from first drawable.
  670. drawable = this.drawables[0];
  671. for (j = 0; j < drawable.filters_manager.filters.length; j++) {
  672. filter = drawable.filters_manager.filters[j];
  673. shared_filters[filter.name] = [filter];
  674. }
  675. // Create lists of shared filters.
  676. for (i = 1; i < this.drawables.length; i++) {
  677. drawable = this.drawables[i];
  678. for (j = 0; j < drawable.filters_manager.filters.length; j++) {
  679. filter = drawable.filters_manager.filters[j];
  680. if (filter.name in shared_filters) {
  681. shared_filters[filter.name].push(filter);
  682. }
  683. }
  684. }
  685. //
  686. // Create filters for shared filters manager. Shared filters manager is group's
  687. // manager.
  688. //
  689. this.filters_manager.remove_all();
  690. var
  691. filters,
  692. new_filter,
  693. min,
  694. max;
  695. for (var filter_name in shared_filters) {
  696. filters = shared_filters[filter_name];
  697. if (filters.length === num_feature_tracks) {
  698. // Add new filter.
  699. // FIXME: can filter.copy() be used?
  700. new_filter = new filters_mod.NumberFilter( {
  701. name: filters[0].name,
  702. index: filters[0].index
  703. } );
  704. this.filters_manager.add_filter(new_filter);
  705. }
  706. }
  707. // Show/hide icon based on filter availability.
  708. if (this.filters_manager.filters.length > 0) {
  709. this.action_icons.filters_icon.show();
  710. }
  711. else {
  712. this.action_icons.filters_icon.hide();
  713. }
  714. }
  715. else {
  716. this.action_icons.filters_icon.hide();
  717. }
  718. }
  719. },
  720. /**
  721. * Restore individual track filter managers.
  722. */
  723. _restore_filter_managers: function() {
  724. for (var i = 0; i < this.drawables.length; i++) {
  725. this.drawables[i].filters_manager = this.saved_filters_managers[i];
  726. }
  727. this.saved_filters_managers = [];
  728. },
  729. /**
  730. *
  731. */
  732. setup_multitrack_filtering: function() {
  733. // Save tracks' managers and set up shared manager.
  734. if (this.filters_manager.filters.length > 0) {
  735. // For all tracks, save current filter manager and set manager to shared (this object's) manager.
  736. this.saved_filters_managers = [];
  737. for (var i = 0; i < this.drawables.length; i++) {
  738. drawable = this.drawables[i];
  739. this.saved_filters_managers.push(drawable.filters_manager);
  740. drawable.filters_manager = this.filters_manager;
  741. }
  742. //TODO: hide filters icons for each drawable?
  743. }
  744. this.filters_manager.init_filters();
  745. },
  746. /**
  747. * Replace group with a single composite track that includes all group's tracks.
  748. */
  749. show_composite_track: function() {
  750. // Create composite track name.
  751. var drawables_names = [];
  752. for (var i = 0; i < this.drawables.length; i++) {
  753. drawables_names.push(this.drawables[i].name);
  754. }
  755. var new_track_name = "Composite Track of " + this.drawables.length + " tracks (" + drawables_names.join(", ") + ")";
  756. // Replace this group with composite track.
  757. var composite_track = new CompositeTrack(this.view, this.view, {
  758. name: new_track_name,
  759. drawables: this.drawables
  760. });
  761. var index = this.container.replace_drawable(this, composite_track, true);
  762. composite_track.request_draw();
  763. },
  764. add_drawable: function(drawable) {
  765. DrawableCollection.prototype.add_drawable.call(this, drawable);
  766. this.update_icons();
  767. },
  768. remove_drawable: function(drawable) {
  769. DrawableCollection.prototype.remove_drawable.call(this, drawable);
  770. this.update_icons();
  771. },
  772. to_dict: function() {
  773. // If filters are visible, need to restore original filter managers before converting to dict.
  774. if (this.filters_manager.visible()) {
  775. this._restore_filter_managers();
  776. }
  777. var obj_dict = extend(DrawableCollection.prototype.to_dict.call(this), { "filters": this.filters_manager.to_dict() });
  778. // Setup multi-track filtering again.
  779. if (this.filters_manager.visible()) {
  780. this.setup_multitrack_filtering();
  781. }
  782. return obj_dict;
  783. },
  784. request_draw: function(clear_after, force) {
  785. for (var i = 0; i < this.drawables.length; i++) {
  786. this.drawables[i].request_draw(clear_after, force);
  787. }
  788. }
  789. });
  790. /**
  791. * View object manages complete viz view, including tracks and user interactions.
  792. * Events triggered:
  793. * navigate: when browser view changes to a new locations
  794. */
  795. var View = function(obj_dict) {
  796. extend(obj_dict, {
  797. obj_type: "View"
  798. });
  799. DrawableCollection.call(this, "View", obj_dict.container, obj_dict);
  800. this.chrom = null;
  801. this.vis_id = obj_dict.vis_id;
  802. this.dbkey = obj_dict.dbkey;
  803. this.label_tracks = [];
  804. this.tracks_to_be_redrawn = [];
  805. this.max_low = 0;
  806. this.max_high = 0;
  807. this.zoom_factor = 3;
  808. this.min_separation = 30;
  809. this.has_changes = false;
  810. // Deferred object that indicates when view's chrom data has been loaded.
  811. this.load_chroms_deferred = null;
  812. this.init();
  813. this.canvas_manager = new visualization.CanvasManager( this.container.get(0).ownerDocument );
  814. this.reset();
  815. };
  816. _.extend( View.prototype, Backbone.Events);
  817. extend( View.prototype, DrawableCollection.prototype, {
  818. init: function() {
  819. // Attribute init.
  820. this.requested_redraw = false;
  821. // Create DOM elements
  822. var parent_element = this.container,
  823. view = this;
  824. // Top container for things that are fixed at the top
  825. this.top_container = $("<div/>").addClass("top-container").appendTo(parent_element);
  826. // Browser content, primary tracks are contained in here
  827. this.browser_content_div = $("<div/>").addClass("content").css("position", "relative").appendTo(parent_element);
  828. // Bottom container for things that are fixed at the bottom
  829. this.bottom_container = $("<div/>").addClass("bottom-container").appendTo(parent_element);
  830. // Label track fixed at top
  831. this.top_labeltrack = $("<div/>").addClass("top-labeltrack").appendTo(this.top_container);
  832. // Viewport for dragging tracks in center
  833. this.viewport_container = $("<div/>").addClass("viewport-container").attr("id", "viewport-container").appendTo(this.browser_content_div);
  834. // Alias viewport_container as content_div so that it matches function of DrawableCollection/Group content_div.
  835. this.content_div = this.viewport_container;
  836. is_container(this.viewport_container, view);
  837. // Introduction div shown when there are no tracks.
  838. this.intro_div = $("<div/>").addClass("intro").appendTo(this.viewport_container).hide();
  839. var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
  840. add_datasets(add_datasets_url, add_track_async_url, function(tracks) {
  841. _.each(tracks, function(track) {
  842. view.add_drawable( object_from_template(track, view, view) );
  843. });
  844. });
  845. });
  846. // Another label track at bottom
  847. this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);
  848. // Navigation at top
  849. this.nav_container = $("<div/>").addClass("trackster-nav-container").prependTo(this.top_container);
  850. this.nav = $("<div/>").addClass("trackster-nav").appendTo(this.nav_container);
  851. // Overview (scrollbar and overview plot) at bottom
  852. this.overview = $("<div/>").addClass("overview").appendTo(this.bottom_container);
  853. this.overview_viewport = $("<div/>").addClass("overview-viewport").appendTo(this.overview);
  854. this.overview_close = $("<a/>").attr("href", "javascript:void(0);").attr("title", "Close overview").addClass("icon-button overview-close tooltip").hide().appendTo(this.overview_viewport);
  855. this.overview_highlight = $("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);
  856. this.overview_box_background = $("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);
  857. this.overview_box = $("<div/>").addClass("overview-box").appendTo(this.overview_viewport);
  858. this.default_overview_height = this.overview_box.height();
  859. this.nav_controls = $("<div/>").addClass("nav-controls").appendTo(this.nav);
  860. this.chrom_select = $("<select/>").attr({ "name": "chrom"}).css("width", "15em").append("<option value=''>Loading</option>").appendTo(this.nav_controls);
  861. var submit_nav = function(e) {
  862. if (e.type === "focusout" || (e.keyCode || e.which) === 13 || (e.keyCode || e.which) === 27 ) {
  863. if ((e.keyCode || e.which) !== 27) { // Not escape key
  864. view.go_to( $(this).val() );
  865. }
  866. $(this).hide();
  867. $(this).val('');
  868. view.location_span.show();
  869. view.chrom_select.show();
  870. }
  871. };
  872. this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keyup focusout", submit_nav).appendTo(this.nav_controls);
  873. this.location_span = $("<span/>").addClass("location").attr('original-title', 'Click to change location').tooltip( { placement: 'bottom' } ).appendTo(this.nav_controls);
  874. this.location_span.click(function() {
  875. view.location_span.hide();
  876. view.chrom_select.hide();
  877. view.nav_input.val(view.chrom + ":" + view.low + "-" + view.high);
  878. view.nav_input.css("display", "inline-block");
  879. view.nav_input.select();
  880. view.nav_input.focus();
  881. // Set up autocomplete for tracks' features.
  882. view.nav_input.autocomplete({
  883. source: function(request, response) {
  884. // Using current text, query each track and create list of all matching features.
  885. var all_features = [],
  886. feature_search_deferreds = $.map(view.get_drawables(), function(drawable) {
  887. return drawable.data_manager.search_features(request.term).success(function(dataset_features) {
  888. all_features = all_features.concat(dataset_features);
  889. });
  890. });
  891. // When all searching is done, fill autocomplete.
  892. $.when.apply($, feature_search_deferreds).done(function() {
  893. response($.map(all_features, function(feature) {
  894. return {
  895. label: feature[0],
  896. value: feature[1]
  897. };
  898. }));
  899. });
  900. }
  901. });
  902. });
  903. if (this.vis_id !== undefined) {
  904. this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.nav_controls);
  905. }
  906. this.zo_link = $("<a/>").attr("id", "zoom-out").attr("title", "Zoom out").tooltip( {placement: 'bottom'} )
  907. .click(function() { view.zoom_out(); view.request_redraw(); }).appendTo(this.nav_controls);
  908. this.zi_link = $("<a/>").attr("id", "zoom-in").attr("title", "Zoom in").tooltip( {placement: 'bottom'} )
  909. .click(function() { view.zoom_in(); view.request_redraw(); }).appendTo(this.nav_controls);
  910. // Get initial set of chroms.
  911. this.load_chroms_deferred = this.load_chroms({low: 0});
  912. this.chrom_select.bind("change", function() {
  913. view.change_chrom(view.chrom_select.val());
  914. });
  915. /*
  916. this.browser_content_div.bind("mousewheel", function( e, delta ) {
  917. if (Math.abs(delta) < 0.5) {
  918. return;
  919. }
  920. if (delta > 0) {
  921. view.zoom_in(e.pageX, this.viewport_container);
  922. } else {
  923. view.zoom_out();
  924. }
  925. e.preventDefault();
  926. });
  927. */
  928. // Blur tool/filter inputs when user clicks on content div.
  929. this.browser_content_div.click(function( e ) {
  930. $(this).find("input").trigger("blur");
  931. });
  932. // Double clicking zooms in
  933. this.browser_content_div.bind("dblclick", function( e ) {
  934. view.zoom_in(e.pageX, this.viewport_container);
  935. });
  936. // Dragging the overview box (~ horizontal scroll bar)
  937. this.overview_box.bind("dragstart", function( e, d ) {
  938. this.current_x = d.offsetX;
  939. }).bind("drag", function( e, d ) {
  940. var delta = d.offsetX - this.current_x;
  941. this.current_x = d.offsetX;
  942. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.max_high - view.max_low) );
  943. view.move_delta(-delta_chrom);
  944. });
  945. this.overview_close.click(function() {
  946. view.reset_overview();
  947. });
  948. // Dragging in the viewport scrolls
  949. this.viewport_container.bind( "draginit", function( e, d ) {
  950. // Disable interaction if started in scrollbar (for webkit)
  951. if ( e.clientX > view.viewport_container.width() - 16 ) {
  952. return false;
  953. }
  954. }).bind( "dragstart", function( e, d ) {
  955. d.original_low = view.low;
  956. d.current_height = e.clientY;
  957. d.current_x = d.offsetX;
  958. }).bind( "drag", function( e, d ) {
  959. var container = $(this);
  960. var delta = d.offsetX - d.current_x;
  961. var new_scroll = container.scrollTop() - (e.clientY - d.current_height);
  962. container.scrollTop(new_scroll);
  963. d.current_height = e.clientY;
  964. d.current_x = d.offsetX;
  965. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low));
  966. view.move_delta(delta_chrom);
  967. // Also capture mouse wheel for left/right scrolling
  968. }).bind( 'mousewheel', function( e, d, dx, dy ) {
  969. // Only act on x axis scrolling if we see if, y will be i
  970. // handled by the browser when the event bubbles up
  971. if ( dx ) {
  972. dx *= 50;
  973. var delta_chrom = Math.round( - dx / view.viewport_container.width() * (view.high - view.low) );
  974. view.move_delta( delta_chrom );
  975. }
  976. });
  977. // Dragging in the top label track allows selecting a region
  978. // to zoom in
  979. this.top_labeltrack.bind( "dragstart", function( e, d ) {
  980. return $("<div />").css( {
  981. "height": view.browser_content_div.height() + view.top_labeltrack.height() + view.nav_labeltrack.height() + 1,
  982. "top": "0px",
  983. "position": "absolute",
  984. "background-color": "#ccf",
  985. "opacity": 0.5,
  986. "z-index": 1000
  987. } ).appendTo( $(this) );
  988. }).bind( "drag", function( e, d ) {
  989. $( d.proxy ).css({ left: Math.min( e.pageX, d.startX ) - view.container.offset().left, width: Math.abs( e.pageX - d.startX ) });
  990. var min = Math.min(e.pageX, d.startX ) - view.container.offset().left,
  991. max = Math.max(e.pageX, d.startX ) - view.container.offset().left,
  992. span = (view.high - view.low),
  993. width = view.viewport_container.width();
  994. view.update_location( Math.round(min / width * span) + view.low,
  995. Math.round(max / width * span) + view.low );
  996. }).bind( "dragend", function( e, d ) {
  997. var min = Math.min(e.pageX, d.startX),
  998. max = Math.max(e.pageX, d.startX),
  999. span = (view.high - view.low),
  1000. width = view.viewport_container.width(),
  1001. old_low = view.low;
  1002. view.low = Math.round(min / width * span) + old_low;
  1003. view.high = Math.round(max / width * span) + old_low;
  1004. $(d.proxy).remove();
  1005. view.request_redraw();
  1006. });
  1007. this.add_label_track( new LabelTrack( this, { content_div: this.top_labeltrack } ) );
  1008. this.add_label_track( new LabelTrack( this, { content_div: this.nav_labeltrack } ) );
  1009. $(window).bind("resize", function() {
  1010. // Stop previous timer.
  1011. if (this.resize_timer) {
  1012. clearTimeout(this.resize_timer);
  1013. }
  1014. // When function activated, resize window and redraw.
  1015. this.resize_timer = setTimeout(function () {
  1016. view.resize_window();
  1017. }, 500 );
  1018. });
  1019. $(document).bind("redraw", function() { view.redraw(); });
  1020. this.reset();
  1021. $(window).trigger("resize");
  1022. },
  1023. changed: function() {
  1024. this.has_changes = true;
  1025. },
  1026. /** Add or remove intro div depending on view state. */
  1027. update_intro_div: function() {
  1028. if (this.drawables.length === 0) {
  1029. this.intro_div.show();
  1030. }
  1031. else {
  1032. this.intro_div.hide();
  1033. }
  1034. },
  1035. /**
  1036. * Triggers navigate events as needed. If there is a delay,
  1037. * then event is triggered only after navigation has stopped.
  1038. */
  1039. trigger_navigate: function(new_chrom, new_low, new_high, delay) {
  1040. // Stop previous timer.
  1041. if (this.timer) {
  1042. clearTimeout(this.timer);
  1043. }
  1044. if (delay) {
  1045. // To aggregate calls, use timer and only navigate once
  1046. // location has stabilized.
  1047. var self = this;
  1048. this.timer = setTimeout(function () {
  1049. self.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
  1050. }, 500 );
  1051. }
  1052. else {
  1053. view.trigger("navigate", new_chrom + ":" + new_low + "-" + new_high);
  1054. }
  1055. },
  1056. update_location: function(low, high) {
  1057. this.location_span.text( commatize(low) + ' - ' + commatize(high) );
  1058. this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
  1059. // Update location. Only update when there is a valid chrom; when loading vis, there may
  1060. // not be a valid chrom.
  1061. var chrom = view.chrom_select.val();
  1062. if (chrom !== "") {
  1063. this.trigger_navigate(chrom, view.low, view.high, true);
  1064. }
  1065. },
  1066. /**
  1067. * Load chrom data for the view. Returns a jQuery Deferred.
  1068. */
  1069. load_chroms: function(url_parms) {
  1070. url_parms.num = MAX_CHROMS_SELECTABLE;
  1071. var
  1072. view = this,
  1073. chrom_data = $.Deferred();
  1074. $.ajax({
  1075. url: chrom_url + "/" + this.dbkey,
  1076. data: url_parms,
  1077. dataType: "json",
  1078. success: function (result) {
  1079. // Do nothing if could not load chroms.
  1080. if (result.chrom_info.length === 0) {
  1081. return;
  1082. }
  1083. // Load chroms.
  1084. if (result.reference) {
  1085. view.add_label_track( new ReferenceTrack(view) );
  1086. }
  1087. view.chrom_data = result.chrom_info;
  1088. var chrom_options = '<option value="">Select Chrom/Contig</option>';
  1089. for (var i = 0, len = view.chrom_data.length; i < len; i++) {
  1090. var chrom = view.chrom_data[i].chrom;
  1091. chrom_options += '<option value="' + chrom + '">' + chrom + '</option>';
  1092. }
  1093. if (result.prev_chroms) {
  1094. chrom_options += '<option value="previous">Previous ' + MAX_CHROMS_SELECTABLE + '</option>';
  1095. }
  1096. if (result.next_chroms) {
  1097. chrom_options += '<option value="next">Next ' + MAX_CHROMS_SELECTABLE + '</option>';
  1098. }
  1099. view.chrom_select.html(chrom_options);
  1100. view.chrom_start_index = result.start_index;
  1101. chrom_data.resolve(result);
  1102. },
  1103. error: function() {
  1104. alert("Could not load chroms for this dbkey:", view.dbkey);
  1105. }
  1106. });
  1107. return chrom_data;
  1108. },
  1109. change_chrom: function(chrom, low, high) {
  1110. var view = this;
  1111. // If chrom data is still loading, wait for it.
  1112. if (!view.chrom_data) {
  1113. view.load_chroms_deferred.then(function() {
  1114. view.change_chrom(chrom, low, high);
  1115. });
  1116. return;
  1117. }
  1118. // Don't do anything if chrom is "None" (hackish but some browsers already have this set), or null/blank
  1119. if (!chrom || chrom === "None") {
  1120. return;
  1121. }
  1122. //
  1123. // If user is navigating to previous/next set of chroms, load new chrom set and return.
  1124. //
  1125. if (chrom === "previous") {
  1126. view.load_chroms({low: this.chrom_start_index - MAX_CHROMS_SELECTABLE});
  1127. return;
  1128. }
  1129. if (chrom === "next") {
  1130. view.load_chroms({low: this.chrom_start_index + MAX_CHROMS_SELECTABLE});
  1131. return;
  1132. }
  1133. //
  1134. // User is loading a particular chrom. Look first in current set; if not in current set, load new
  1135. // chrom set.
  1136. //
  1137. var found = $.grep(view.chrom_data, function(v, i) {
  1138. return v.chrom === chrom;
  1139. })[0];
  1140. if (found === undefined) {
  1141. // Try to load chrom and then change to chrom.
  1142. view.load_chroms({'chrom': chrom}, function() { view.change_chrom(chrom, low, high); });
  1143. return;
  1144. }
  1145. else {
  1146. // Switching to local chrom.
  1147. if (chrom !== view.chrom) {
  1148. view.chrom = chrom;
  1149. view.chrom_select.val(view.chrom);
  1150. view.max_high = found.len-1; // -1 because we're using 0-based indexing.
  1151. view.reset();
  1152. view.request_redraw(true);
  1153. for (var i = 0, len = view.drawables.length; i < len; i++) {
  1154. var drawable = view.drawables[i];
  1155. if (drawable.init) {
  1156. drawable.init();
  1157. }
  1158. }
  1159. if (view.reference_track) {
  1160. view.reference_track.init();
  1161. }
  1162. }
  1163. if (low !== undefined && high !== undefined) {
  1164. view.low = Math.max(low, 0);
  1165. view.high = Math.min(high, view.max_high);
  1166. }
  1167. else {
  1168. // Low and high undefined, so view is whole chome.
  1169. view.low = 0;
  1170. view.high = view.max_high;
  1171. }
  1172. view.reset_overview();
  1173. view.request_redraw();
  1174. }
  1175. },
  1176. go_to: function(str) {
  1177. // Preprocess str to remove spaces and commas.
  1178. str = str.replace(/ |,/g, "");
  1179. // Go to new location.
  1180. var view = this,
  1181. new_low,
  1182. new_high,
  1183. chrom_pos = str.split(":"),
  1184. chrom = chrom_pos[0],
  1185. pos = chrom_pos[1];
  1186. if (pos !== undefined) {
  1187. try {
  1188. var pos_split = pos.split("-");
  1189. new_low = parseInt(pos_split[0], 10);
  1190. new_high = parseInt(pos_split[1], 10);
  1191. } catch (e) {
  1192. return false;
  1193. }
  1194. }
  1195. view.change_chrom(chrom, new_low, new_high);
  1196. },
  1197. move_fraction: function(fraction) {
  1198. var view = this;
  1199. var span = view.high - view.low;
  1200. this.move_delta(fraction * span);
  1201. },
  1202. move_delta: function(delta_chrom) {
  1203. // Update low, high.
  1204. var view = this;
  1205. var current_chrom_span = view.high - view.low;
  1206. // Check for left and right boundaries
  1207. if (view.low - delta_chrom < view.max_low) {
  1208. view.low = view.max_low;
  1209. view.high = view.max_low + current_chrom_span;
  1210. } else if (view.high - delta_chrom > view.max_high) {
  1211. view.high = view.max_high;
  1212. view.low = view.max_high - current_chrom_span;
  1213. } else {
  1214. view.high -= delta_chrom;
  1215. view.low -= delta_chrom;
  1216. }
  1217. view.request_redraw();
  1218. // Navigate.
  1219. var chrom = view.chrom_select.val();
  1220. this.trigger_navigate(chrom, view.low, view.high, true);
  1221. },
  1222. /**
  1223. * Add a drawable to the view.
  1224. */
  1225. add_drawable: function(drawable) {
  1226. DrawableCollection.prototype.add_drawable.call(this, drawable);
  1227. drawable.init();
  1228. this.changed();
  1229. this.update_intro_div();
  1230. },
  1231. add_label_track: function (label_track) {
  1232. label_track.view = this;
  1233. label_track.init();
  1234. this.label_tracks.push(label_track);
  1235. },
  1236. /**
  1237. * Remove drawable from the view.
  1238. */
  1239. remove_drawable: function(drawable, hide) {
  1240. DrawableCollection.prototype.remove_drawable.call(this, drawable);
  1241. if (hide) {
  1242. var view = this;
  1243. drawable.container_div.hide(0, function() {
  1244. $(this).remove();
  1245. view.update_intro_div();
  1246. });
  1247. }
  1248. },
  1249. reset: function() {
  1250. this.low = this.max_low;
  1251. this.high = this.max_high;
  1252. this.viewport_container.find(".yaxislabel").remove();
  1253. },
  1254. /**
  1255. * Request that view redraw some or all tracks. If a track is not specificied, redraw all tracks.
  1256. */
  1257. // FIXME: change method call so that track is first and additional parameters are optional.
  1258. // FIXME: is nodraw parameter needed?
  1259. request_redraw: function(nodraw, force, clear_after, a_track) {
  1260. var
  1261. view = this,
  1262. // Either redrawing a single drawable or all view's drawables.
  1263. track_list = (a_track ? [a_track] : view.drawables),
  1264. track_index;
  1265. // Add/update tracks in track list to redraw list.
  1266. var track;
  1267. for (var i = 0; i < track_list.length; i++) {
  1268. track = track_list[i];
  1269. // Because list elements are arrays, need to look for track index manually.
  1270. track_index = -1;
  1271. for (var j = 0; j < view.tracks_to_be_redrawn.length; j++) {
  1272. if (view.tracks_to_be_redrawn[j][0] === track) {
  1273. track_index = j;
  1274. break;
  1275. }
  1276. }
  1277. // Add track to list or update draw parameters.
  1278. if (track_index < 0) {
  1279. // Track not in list yet.
  1280. view.tracks_to_be_redrawn.push([track, force, clear_after]);
  1281. }
  1282. else {
  1283. // Track already in list; update force and clear_after.
  1284. view.tracks_to_be_redrawn[i][1] = force;
  1285. view.tracks_to_be_redrawn[i][2] = clear_after;
  1286. }
  1287. }
  1288. // Set up redraw if it has not been requested since last redraw.
  1289. if (!this.requested_redraw) {
  1290. requestAnimationFrame(function() { view._redraw(nodraw); });
  1291. this.requested_redraw = true;
  1292. }
  1293. },
  1294. /**
  1295. * Redraws view and tracks.
  1296. * NOTE: this method should never be called directly; request_redraw() should be used so
  1297. * that requestAnimationFrame can manage redrawing.
  1298. */
  1299. _redraw: function(nodraw) {
  1300. // TODO: move this code to function that does location setting.
  1301. // Clear because requested redraw is being handled now.
  1302. this.requested_redraw = false;
  1303. var low = this.low,
  1304. high = this.high;
  1305. if (low < this.max_low) {
  1306. low = this.max_low;
  1307. }
  1308. if (high > this.max_high) {
  1309. high = this.max_high;
  1310. }
  1311. var span = this.high - this.low;
  1312. if (this.high !== 0 && span < this.min_separation) {
  1313. high = low + this.min_separation;
  1314. }
  1315. this.low = Math.floor(low);
  1316. this.high = Math.ceil(high);
  1317. this.update_location(this.low, this.high);
  1318. // -- Drawing code --
  1319. // Calculate resolution in both pixels/base and bases/pixel; round bases/pixels for tile calculations.
  1320. // TODO: require minimum difference in new resolution to update?
  1321. this.resolution_b_px = (this.high - this.low) / this.viewport_container.width();
  1322. this.resolution_px_b = this.viewport_container.width() / (this.high - this.low);
  1323. // Overview
  1324. var left_px = ( this.low / (this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1325. var width_px = ( (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1326. var min_width_px = 13;
  1327. this.overview_box.css({ left: left_px, width: Math.max(min_width_px, width_px) }).show();
  1328. if (width_px < min_width_px) {
  1329. this.overview_box.css("left", left_px - (min_width_px - width_px)/2);
  1330. }
  1331. if (this.overview_highlight) {
  1332. this.overview_highlight.css({ left: left_px, width: width_px });
  1333. }
  1334. if (!nodraw) {
  1335. var track, force, clear_after;
  1336. for (var i = 0, len = this.tracks_to_be_redrawn.length; i < len; i++) {
  1337. track = this.tracks_to_be_redrawn[i][0];
  1338. force = this.tracks_to_be_redrawn[i][1];
  1339. clear_after = this.tracks_to_be_redrawn[i][2];
  1340. if (track) {
  1341. track._draw(force, clear_after);
  1342. }
  1343. }
  1344. this.tracks_to_be_redrawn = [];
  1345. for (i = 0, len = this.label_tracks.length; i < len; i++) {
  1346. this.label_tracks[i]._draw();
  1347. }
  1348. }
  1349. },
  1350. zoom_in: function (point, container) {
  1351. if (this.max_high === 0 || this.high - this.low <= this.min_separation) {
  1352. return;
  1353. }
  1354. var span = this.high - this.low,
  1355. cur_center = span / 2 + this.low,
  1356. new_half = (span / this.zoom_factor) / 2;
  1357. if (point) {
  1358. cur_center = point / this.viewport_container.width() * (this.high - this.low) + this.low;
  1359. }
  1360. this.low = Math.round(cur_center - new_half);
  1361. this.high = Math.round(cur_center + new_half);
  1362. this.changed();
  1363. this.request_redraw();
  1364. },
  1365. zoom_out: function () {
  1366. if (this.max_high === 0) {
  1367. return;
  1368. }
  1369. var span = this.high - this.low,
  1370. cur_center = span / 2 + this.low,
  1371. new_half = (span * this.zoom_factor) / 2;
  1372. this.low = Math.round(cur_center - new_half);
  1373. this.high = Math.round(cur_center + new_half);
  1374. this.changed();
  1375. this.request_redraw();
  1376. },
  1377. resize_window: function() {
  1378. this.viewport_container.height( this.container.height() - this.top_container.height() - this.bottom_container.height() );
  1379. this.request_redraw();
  1380. },
  1381. /** Show a Drawable in the overview. */
  1382. set_overview: function(drawable) {
  1383. if (this.overview_drawable) {
  1384. // If drawable to be set as overview is already in overview, do nothing.
  1385. // Otherwise, remove overview.
  1386. if (this.overview_drawable.dataset_id === drawable.dataset_id) {
  1387. return;
  1388. }
  1389. this.overview_viewport.find(".track").remove();
  1390. }
  1391. // Set new overview.
  1392. var
  1393. overview_drawable = drawable.copy( { content_div: this.overview_viewport } ),
  1394. view = this;
  1395. overview_drawable.header_div.hide();
  1396. overview_drawable.is_overview = true;
  1397. view.overview_drawable = overview_drawable;
  1398. this.overview_drawable.postdraw_actions = function() {
  1399. view.overview_highlight.show().height(view.overview_drawable.content_div.height());
  1400. view.overview_viewport.height(view.overview_drawable.content_div.height() + view.overview_box.outerHeight());
  1401. view.overview_close.show();
  1402. view.resize_window();
  1403. };
  1404. view.overview_drawable.request_draw();
  1405. this.changed();
  1406. },
  1407. /** Close and reset overview. */
  1408. reset_overview: function() {
  1409. // Update UI.
  1410. $(".bs-tooltip").remove();
  1411. this.overview_viewport.find(".track-tile").remove();
  1412. this.overview_viewport.height(this.default_overview_height);
  1413. this.overview_box.height(this.default_overview_height);
  1414. this.overview_close.hide();
  1415. this.overview_highlight.hide();
  1416. view.resize_window();
  1417. view.overview_drawable = null;
  1418. }
  1419. });
  1420. /**
  1421. * Encapsulation of a tool that users can apply to tracks/datasets.
  1422. */
  1423. var Tool = function(track, tool_dict, tool_state_dict) {
  1424. //
  1425. // Unpack tool information from dictionary.
  1426. //
  1427. this.track = track;
  1428. this.name = tool_dict.name;
  1429. this.params = [];
  1430. var params_dict = tool_dict.params;
  1431. for (var i = 0; i < params_dict.length; i++) {
  1432. // FIXME: use dict for creating parameters.
  1433. var param_dict = params_dict[i],
  1434. name = param_dict.name,
  1435. label = param_dict.label,
  1436. html = unescape(param_dict.html),
  1437. value = param_dict.value,
  1438. type = param_dict.type;
  1439. if (type === "number") {
  1440. this.params.push(
  1441. new NumberParameter(name, label, html,
  1442. (name in tool_state_dict ? tool_state_dict[name] : value),
  1443. param_dict.min, param_dict.max)
  1444. );
  1445. }
  1446. else if (type === "select") {
  1447. this.params.push(
  1448. new ToolParameter(name, label, html,
  1449. (name in tool_state_dict ? tool_state_dict[name] : value))
  1450. );
  1451. }
  1452. else {
  1453. console.log("WARNING: unrecognized tool parameter type:", name, type);
  1454. }
  1455. }
  1456. //
  1457. // Create div elt for tool UI.
  1458. //
  1459. this.parent_div = $("<div/>").addClass("dynamic-tool").hide();
  1460. // Disable dragging, clicking, double clicking on div so that actions on slider do not impact viz.
  1461. this.parent_div.bind("drag", function(e) {
  1462. e.stopPropagation();
  1463. }).click(function(e) {
  1464. e.stopPropagation();
  1465. }).bind("dblclick", function(e) {
  1466. e.stopPropagation();
  1467. });
  1468. var name_div = $("<div class='tool-name'>").appendTo(this.parent_div).text(this.name);
  1469. var tool_params = this.params;
  1470. var tool = this;
  1471. $.each(this.params, function(index, param) {
  1472. var param_div = $("<div>").addClass("param-row").appendTo(tool.parent_div);
  1473. // Param label.
  1474. var label_div = $("<div>").addClass("param-label").text(param.label).appendTo(param_div);
  1475. // Param HTML.
  1476. var html_div = $("<div/>").addClass("param-input").html(param.html).appendTo(param_div);
  1477. // Set initial value.
  1478. html_div.find(":input").val(param.value);
  1479. // Add to clear floating layout.
  1480. $("<div style='clear: both;'/>").appendTo(param_div);
  1481. });
  1482. // Highlight value for inputs for easy replacement.
  1483. this.parent_div.find("input").click(function() { $(this).select(); });
  1484. // Add buttons for running on dataset, region.
  1485. var run_tool_row = $("<div>").addClass("param-row").appendTo(this.parent_div);
  1486. var run_on_dataset_button = $("<input type='submit'>").attr("value", "Run on complete dataset").appendTo(run_tool_row);
  1487. var run_on_region_button = $("<input type='submit'>").attr("value", "Run on visible region").css("margin-left", "3em").appendTo(run_tool_row);
  1488. run_on_region_button.click( function() {
  1489. // Run tool to create new track.
  1490. tool.run_on_region();
  1491. });
  1492. run_on_dataset_button.click( function() {
  1493. tool.run_on_dataset();
  1494. });
  1495. if ('visible' in tool_state_dict && tool_state_dict.visible) {
  1496. this.parent_div.show();
  1497. }
  1498. };
  1499. extend(Tool.prototype, {
  1500. /**
  1501. * Update tool parameters.
  1502. */
  1503. update_params: function() {
  1504. for (var i = 0; i < this.params.length; i++) {
  1505. this.params[i].update_value();
  1506. }
  1507. },
  1508. /**
  1509. * Returns a dict with tool state information.
  1510. */
  1511. state_dict: function() {
  1512. // Save parameter values.
  1513. var tool_state = {};
  1514. for (var i = 0; i < this.params.length; i++) {
  1515. tool_state[this.params[i].name] = this.params[i].value;
  1516. }
  1517. // Save visibility.
  1518. tool_state.visible = this.parent_div.is(":visible");
  1519. return tool_state;
  1520. },
  1521. /**
  1522. * Returns dictionary of parameter name-values.
  1523. */
  1524. get_param_values_dict: function() {
  1525. var param_dict = {};
  1526. this.parent_div.find(":input").each(function() {
  1527. var name = $(this).attr("name"), value = $(this).val();
  1528. param_dict[name] = value;
  1529. });
  1530. return param_dict;
  1531. },
  1532. /**
  1533. * Returns array of parameter values.
  1534. */
  1535. get_param_values: function() {
  1536. var param_values = [];
  1537. this.parent_div.find(":input").each(function() {
  1538. // Only include inputs with names; this excludes Run button.
  1539. var name = $(this).attr("name"), value = $(this).val();
  1540. if (name) {
  1541. param_values[param_values.length] = value;
  1542. }
  1543. });
  1544. return param_values;
  1545. },
  1546. /**
  1547. * Run tool on dataset. Output is placed in dataset's history and no changes to viz are made.
  1548. */
  1549. run_on_dataset: function() {
  1550. var tool = this;
  1551. tool.run(
  1552. // URL params.
  1553. {
  1554. target_dataset_id: this.track.original_dataset_id,
  1555. tool_id: tool.name
  1556. },
  1557. null,
  1558. // Success callback.
  1559. function(track_data) {
  1560. show_modal(tool.name + " is Running",
  1561. tool.name + " is running on the complete dataset. Tool outputs are in dataset's history.",
  1562. { "Close" : hide_modal } );
  1563. }
  1564. );
  1565. },
  1566. /**
  1567. * Run dataset on visible region. This creates a new track and sets the track's contents
  1568. * to the tool's output.
  1569. */
  1570. run_on_region: function() {
  1571. //
  1572. // Create track for tool's output immediately to provide user feedback.
  1573. //
  1574. var
  1575. url_params =
  1576. {
  1577. target_dataset_id: this.track.original_dataset_id,
  1578. action: 'rerun',
  1579. tool_id: this.name,
  1580. regions: [{
  1581. chrom: this.track.view.chrom,
  1582. start: this.track.view.low,
  1583. end: this.track.view.high
  1584. }]
  1585. },
  1586. current_track = this.track,
  1587. // Set name of track to include tool name, parameters, and region used.
  1588. track_name = url_params.tool_id +
  1589. current_track.tool_region_and_parameters_str(url_params.chrom, url_params.low, url_params.high),
  1590. container;
  1591. // If track not in a group, create a group for it and add new track to group. If track
  1592. // already in group, add track to group.
  1593. if (current_track.container === view) {
  1594. // Create new group.
  1595. var group = new DrawableGroup(view, view, { name: this.name });
  1596. // Replace track with group.
  1597. var index = current_track.container.replace_drawable(current_track, group, false);
  1598. // Update HTML.
  1599. // FIXME: this is ugly way to replace a track with a group -- make this easier via
  1600. // a Drawable or DrawableCollection function.
  1601. group.container_div.insertBefore(current_track.view.content_div.children()[index]);
  1602. group.add_drawable(current_track);
  1603. current_track.container_div.appendTo(group.content_div);
  1604. container = group;
  1605. }
  1606. else {
  1607. // Use current group.
  1608. container = current_track.container;
  1609. }
  1610. // Create and init new track.
  1611. var new_track = new current_track.constructor(view, container, {
  1612. name: track_name,
  1613. hda_ldda: "hda"
  1614. });
  1615. new_track.init_for_tool_data();
  1616. new_track.change_mode(current_track.mode);
  1617. new_track.set_filters_manager(current_track.filters_manager.copy(new_track));
  1618. new_track.update_icons();
  1619. container.add_drawable(new_track);
  1620. new_track.tiles_div.text("Starting job.");
  1621. // Run tool.
  1622. this.update_params();
  1623. this.run(url_params, new_track,
  1624. // Success callback.
  1625. function(track_data) {
  1626. new_track.set_dataset(new data.Dataset(track_data));
  1627. new_track.tiles_div.text("Running job.");
  1628. new_track.init();
  1629. }
  1630. );
  1631. },
  1632. /**
  1633. * Run tool using a set of URL params and a success callback.
  1634. */
  1635. run: function(url_params, new_track, success_callback) {
  1636. // Run tool.
  1637. url_params.inputs = this.get_param_values_dict();
  1638. var ss_deferred = new util.ServerStateDeferred({
  1639. ajax_settings: {
  1640. url: galaxy_paths.get('tool_url'),
  1641. data: JSON.stringify(url_params),
  1642. dataType: "json",
  1643. contentType: 'application/json',
  1644. type: "POST"
  1645. },
  1646. interval: 2000,
  1647. success_fn: function(response) {
  1648. return response !== "pending";
  1649. }
  1650. });
  1651. // Start with this status message.
  1652. //new_track.container_div.addClass("pending");
  1653. //new_track.content_div.text("Converting input data so that it can be used quickly with tool.");
  1654. $.when(ss_deferred.go()).then(function(response) {
  1655. if (response === "no converter") {
  1656. // No converter available for input datasets, so cannot run tool.
  1657. new_track.container_div.addClass("error");
  1658. new_track.content_div.text(DATA_NOCONVERTER);
  1659. }
  1660. else if (response.error) {
  1661. // General error.
  1662. new_track.container_div.addClass("error");
  1663. new_track.content_div.text(DATA_CANNOT_RUN_TOOL + response.message);
  1664. }
  1665. else {
  1666. // Job submitted and running.
  1667. success_callback(response);
  1668. }
  1669. });
  1670. }
  1671. });
  1672. /**
  1673. * Tool parameters.
  1674. */
  1675. var ToolParameter = function(name, label, html, value) {
  1676. this.name = name;
  1677. this.label = label;
  1678. // Need to use jQuery for HTML so that value can be queried and updated dynamically.
  1679. this.html = $(html);
  1680. this.value = value;
  1681. };
  1682. extend(ToolParameter.prototype, {
  1683. update_value: function() {
  1684. this.value = $(this.html).val();
  1685. }
  1686. });
  1687. var NumberParameter = function(name, label, html, value, min, max) {
  1688. ToolParameter.call(this, name, label, html, value);
  1689. this.min = min;
  1690. this.max = max;
  1691. };
  1692. extend(NumberParameter.prototype, ToolParameter.prototype, {
  1693. update_value: function() {
  1694. ToolParameter.prototype.update_value.call(this);
  1695. this.value = parseFloat(this.value);
  1696. }
  1697. });
  1698. /**
  1699. * Generates scale values based on filter and feature's value for filter.
  1700. */
  1701. var FilterScaler = function(filter, default_val) {
  1702. painters.Scaler.call(this, default_val);
  1703. this.filter = filter;
  1704. };
  1705. FilterScaler.prototype.gen_val = function(feature_data) {
  1706. // If filter is not initalized yet, return default val.
  1707. if (this.filter.high === Number.MAX_VALUE || this.filter.low === -Number.MAX_VALUE || this.filter.low === this.filter.high) {
  1708. return this.default_val;
  1709. }
  1710. // Scaling value is ratio of (filter's value compared to low) to (complete filter range).
  1711. return ( ( parseFloat(feature_data[this.filter.index]) - this.filter.low ) / ( this.filter.high - this.filter.low ) );
  1712. };
  1713. /**
  1714. * Container for Drawable configuration data.
  1715. */
  1716. var DrawableConfig = function( options ) {
  1717. this.track = options.track;
  1718. this.params = options.params;
  1719. this.values = {};
  1720. this.restore_values( (options.saved_values ? options.saved_values : {}) );
  1721. this.onchange = options.onchange;
  1722. };
  1723. extend(DrawableConfig.prototype, {
  1724. restore_values: function( values ) {
  1725. var track_config = this;
  1726. $.each( this.params, function( index, param ) {
  1727. if ( values[ param.key ] !== undefined ) {
  1728. track_config.values[ param.key ] = values[ param.key ];
  1729. } else {
  1730. track_config.values[ param.key ] = param.default_value;
  1731. }
  1732. });
  1733. },
  1734. build_form: function() {
  1735. var track_config = this;
  1736. var container = $("<div />");
  1737. var param;
  1738. // Function to process parameters recursively
  1739. function handle_params( params, container ) {
  1740. for ( var index = 0; index < params.length; index++ ) {
  1741. param = params[index];
  1742. // Hidden params have no representation in the form
  1743. if ( param.hidden ) { continue; }
  1744. // Build row for param
  1745. var id = 'param_' + index;
  1746. var value = track_config.values[ param.key ];
  1747. var row = $("<div class='form-row' />").appendTo( container );
  1748. row.append( $('<label />').attr("for", id ).text( param.label + ":" ) );
  1749. // Draw parameter as checkbox
  1750. if ( param.type === 'bool' ) {
  1751. row.append( $('<input type="checkbox" />').attr("id", id ).attr("name", id ).attr( 'checked', value ) );
  1752. // Draw parameter as textbox
  1753. } else if ( param.type === 'text' ) {
  1754. row.append( $('<input type="text"/>').attr("id", id ).val(value).click( function() { $(this).select(); }));
  1755. // Draw paramter as select area
  1756. } else if ( param.type === 'select' ) {
  1757. var select = $('<select />').attr("id", id);
  1758. for ( var i = 0; i < param.options.length; i++ ) {
  1759. $("<option/>").text( param.options[i].label ).attr( "value", param.options[i].value ).appendTo( select );
  1760. }
  1761. select.val( value );
  1762. row.append( select );
  1763. // Draw parameter as color picker
  1764. } else if ( param.type === 'color' ) {
  1765. var
  1766. container_div = $("<div/>").appendTo(row),
  1767. input = $('<input />').attr("id", id ).attr("name", id ).val( value ).css("float", "left")
  1768. .appendTo(container_div).click(function(e) {
  1769. // Hide other pickers.
  1770. $(".bs-tooltip").removeClass( "in" );
  1771. // Show input's color picker.
  1772. var tip = $(this).siblings(".bs-tooltip").addClass( "in" );
  1773. tip.css( {
  1774. // left: $(this).position().left + ( $(input).width() / 2 ) - 60,
  1775. // top: $(this).position().top + $(this.height)
  1776. left: $(this).position().left + $(this).width() + 5,
  1777. top: $(this).position().top - ( $(tip).height() / 2 ) + ( $(this).height() / 2 )
  1778. } ).show();
  1779. // Click management:
  1780. // Keep showing tip if clicking in tip.
  1781. tip.click(function(e) {
  1782. e.stopPropagation();
  1783. });
  1784. // Hide tip if clicking outside of tip.
  1785. $(document).bind( "click.color-picker", function() {
  1786. tip.hide();
  1787. $(document).unbind( "click.color-picker" );
  1788. });
  1789. // No propagation to avoid triggering document click (and tip hiding) above.
  1790. e.stopPropagation();
  1791. }),
  1792. // Icon for setting a new random color; behavior set below.
  1793. new_color_icon = $("<a href='javascript:void(0)'/>").addClass("icon-button arrow-circle").appendTo(container_div)
  1794. .attr("title", "Set new random color").tooltip(),
  1795. // Color picker in tool tip style.
  1796. tip = $( "<div class='bs-tooltip right' style='position: absolute;' />" ).appendTo(container_div).hide(),
  1797. // Inner div for padding purposes
  1798. tip_inner = $("<div class='tooltip-inner' style='text-align: inherit'></div>").appendTo(tip),
  1799. tip_arrow = $("<div class='tooltip-arrow'></div>").appendTo(tip),
  1800. farb_obj = $.farbtastic(tip_inner, { width: 100, height: 100, callback: input, color: value });
  1801. // Clear floating.
  1802. container_div.append( $("<div/>").css("clear", "both"));
  1803. // Use function to fix farb_obj value.
  1804. (function(fixed_farb_obj) {
  1805. new_color_icon.click(function() {
  1806. fixed_farb_obj.setColor(get_random_color());
  1807. });
  1808. })(farb_obj);
  1809. }
  1810. else {
  1811. row.append( $('<input />').attr("id", id ).attr("name", id ).val( value ) );
  1812. }
  1813. // Help text
  1814. if ( param.help ) {
  1815. row.append( $("<div class='help'/>").text( param.help ) );
  1816. }
  1817. }
  1818. }
  1819. // Handle top level parameters in order
  1820. handle_params( this.params, container );
  1821. // Return element containing constructed form
  1822. return container;
  1823. },
  1824. update_from_form: function( container ) {
  1825. var track_config = this;
  1826. var changed = false;
  1827. $.each( this.params, function( index, param ) {
  1828. if ( ! param.hidden ) {
  1829. // Parse value from form element
  1830. var id = 'param_' + index;
  1831. var value = container.find( '#' + id ).val();
  1832. if ( param.type === 'float' ) {
  1833. value = parseFloat( value );
  1834. } else if ( param.type === 'int' ) {
  1835. value = parseInt( value );
  1836. } else if ( param.type === 'bool' ) {
  1837. value = container.find( '#' + id ).is( ':checked' );
  1838. }
  1839. // Save value only if changed
  1840. if ( value !== track_config.values[ param.key ] ) {
  1841. track_config.values[ param.key ] = value;
  1842. changed = true;
  1843. }
  1844. }
  1845. });
  1846. if ( changed ) {
  1847. this.onchange();
  1848. this.track.changed();
  1849. }
  1850. }
  1851. });
  1852. /**
  1853. * Tiles drawn by tracks.
  1854. */
  1855. var Tile = function(track, region, resolution, canvas, data) {
  1856. this.track = track;
  1857. this.region = region;
  1858. this.low = region.get('start');
  1859. this.high = region.get('end');
  1860. this.resolution = resolution;
  1861. // Wrap element in div for background and explicitly set height. Use canvas
  1862. // height attribute because canvas may not have height if it is not in document yet.
  1863. this.html_elt = $("<div class='track-tile'/>").append(canvas).height( $(canvas).attr("height") );
  1864. this.data = data;
  1865. this.stale = false;
  1866. };
  1867. /**
  1868. * Perform pre-display actions.
  1869. */
  1870. Tile.prototype.predisplay_actions = function() {};
  1871. var SummaryTreeTile = function(track, region, resolution, canvas, data, max_val) {
  1872. Tile.call(this, track, region, resolution, canvas, data);
  1873. this.max_val = max_val;
  1874. };
  1875. extend(SummaryTreeTile.prototype, Tile.prototype);
  1876. var FeatureTrackTile = function(track, region, resolution, canvas, data, w_scale, mode, message, all_slotted, feature_mapper) {
  1877. // Attribute init.
  1878. Tile.call(this, track, region, resolution, canvas, data);
  1879. this.mode = mode;
  1880. this.all_slotted = all_slotted;
  1881. this.feature_mapper = feature_mapper;
  1882. this.has_icons = false;
  1883. // Add message + action icons to tile's html.
  1884. if (message) {
  1885. this.has_icons = true;
  1886. var
  1887. tile = this;
  1888. canvas = this.html_elt.children()[0],
  1889. message_div = $("<div/>").addClass("tile-message")
  1890. // -1 to account for border.
  1891. .css({'height': ERROR_PADDING-1, 'width': canvas.width}).prependTo(this.html_elt);
  1892. // Handle message; only message currently is that only the first N elements are displayed.
  1893. var tile_region = new visualization.GenomeRegion({
  1894. chrom: track.view.chrom,
  1895. start: this.low,
  1896. end: this.high
  1897. }),
  1898. num_features = data.length,
  1899. more_down_icon = $("<a href='javascript:void(0);'/>").addClass("icon more-down")
  1900. .attr("title", "For speed, only the first " + num_features + " features in this region were obtained from server. Click to get more data including depth")
  1901. .tooltip().appendTo(message_div),
  1902. more_across_icon = $("<a href='javascript:void(0);'/>").addClass("icon more-across")
  1903. .attr("title", "For speed, only the first " + num_features + " features in this region were obtained from server. Click to get more data excluding depth")
  1904. .tooltip().appendTo(message_div);
  1905. // Set up actions for icons.
  1906. more_down_icon.click(function() {
  1907. // Mark tile as stale, request more data, and redraw track.
  1908. tile.stale = true;
  1909. track.data_manager.get_more_data(tile_region, track.mode, tile.resolution, {}, track.data_manager.DEEP_DATA_REQ);
  1910. $(".bs-tooltip").hide();
  1911. track.request_draw(true);
  1912. }).dblclick(function(e) {
  1913. // Do not propogate as this would normally zoom in.
  1914. e.stopPropagation();
  1915. });
  1916. more_across_icon.click(function() {
  1917. // Mark tile as stale, request more data, and redraw track.
  1918. tile.stale = true;
  1919. track.data_manager.get_more_data(tile_region, track.mode, tile.resolution, {}, track.data_manager.BROAD_DATA_REQ);
  1920. $(".bs-tooltip").hide();
  1921. track.request_draw(true);
  1922. }).dblclick(function(e) {
  1923. // Do not propogate as this would normally zoom in.
  1924. e.stopPropagation();
  1925. });
  1926. }
  1927. };
  1928. extend(FeatureTrackTile.prototype, Tile.prototype);
  1929. /**
  1930. * Sets up support for popups.
  1931. */
  1932. FeatureTrackTile.prototype.predisplay_actions = function() {
  1933. //
  1934. // Add support for popups.
  1935. //
  1936. var tile = this,
  1937. popups = {};
  1938. // Only show popups in Pack mode.
  1939. if (tile.mode !== "Pack") { return; }
  1940. $(this.html_elt).hover( function() {
  1941. this.hovered = true;
  1942. $(this).mousemove();
  1943. }, function() {
  1944. this.hovered = false;
  1945. // Clear popup if it is still hanging around (this is probably not needed)
  1946. $(this).parents(".track-content").children(".overlay").children(".feature-popup").remove();
  1947. } ).mousemove(function (e) {
  1948. // Use the hover plugin to get a delay before showing popup
  1949. if ( !this.hovered ) { return; }
  1950. // Get feature data for position.
  1951. var
  1952. this_offset = $(this).offset(),
  1953. offsetX = e.pageX - this_offset.left,
  1954. offsetY = e.pageY - this_offset.top,
  1955. feature_data = tile.feature_mapper.get_feature_data(offsetX, offsetY),
  1956. feature_uid = (feature_data ? feature_data[0] : null);
  1957. // Hide visible popup if not over a feature or over a different feature.
  1958. $(this).parents(".track-content").children(".overlay").children(".feature-popup").each(function() {
  1959. if ( !feature_uid ||
  1960. $(this).attr("id") !== feature_uid.toString() ) {
  1961. $(this).remove();
  1962. }
  1963. });
  1964. if (feature_data) {
  1965. // Get or create popup.
  1966. var popup = popups[feature_uid];
  1967. if (!popup) {
  1968. // Create feature's popup element.
  1969. var feature_uid = feature_data[0],
  1970. feature_dict = {
  1971. name: feature_data[3],
  1972. start: feature_data[1],
  1973. end: feature_data[2],
  1974. strand: feature_data[4]
  1975. },
  1976. filters = tile.track.filters_manager.filters,
  1977. filter;
  1978. // Add filter values to feature dict.
  1979. for (var i = 0; i < filters.length; i++) {
  1980. filter = filters[i];
  1981. feature_dict[filter.name] = feature_data[filter.index];
  1982. }
  1983. // Build popup.
  1984. var popup = $("<div/>").attr("id", feature_uid).addClass("feature-popup"),
  1985. table = $("<table/>"),
  1986. key, value, row;
  1987. for (key in feature_dict) {
  1988. value = feature_dict[key];
  1989. row = $("<tr/>").appendTo(table);
  1990. $("<th/>").appendTo(row).text(key);
  1991. $("<td/>").attr("align", "left").appendTo(row)
  1992. .text(typeof(value) === 'number' ? round(value, 2) : value);
  1993. }
  1994. popup.append( $("<div class='feature-popup-inner'>").append( table ) );
  1995. popups[feature_uid] = popup;
  1996. }
  1997. // Attach popup to track's overlay.
  1998. popup.appendTo( $(this).parents(".track-content").children(".overlay") );
  1999. // Offsets are within canvas, but popup must be positioned relative to parent element.
  2000. // parseInt strips "px" from left, top measurements. +7 so that mouse pointer does not
  2001. // overlap popup.
  2002. var
  2003. popupX = offsetX + parseInt( tile.html_elt.css("left") ) - popup.width() / 2,
  2004. popupY = offsetY + parseInt( tile.html_elt.css("top") ) + 7;
  2005. popup.css("left", popupX + "px").css("top", popupY + "px");
  2006. }
  2007. else if (!e.isPropagationStopped()) {
  2008. // Propogate event to other tiles because overlapping tiles prevent mousemove from being
  2009. // called on tiles under this tile.
  2010. e.stopPropagation();
  2011. $(this).siblings().each(function() {
  2012. $(this).trigger(e);
  2013. });
  2014. }
  2015. })
  2016. .mouseleave(function() {
  2017. $(this).parents(".track-content").children(".overlay").children(".feature-popup").remove();
  2018. });
  2019. };
  2020. /**
  2021. * Tracks are objects can be added to the View.
  2022. *
  2023. * Track object hierarchy:
  2024. * Track
  2025. * -> LabelTrack
  2026. * -> TiledTrack
  2027. * ----> LineTrack
  2028. * ----> ReferenceTrack
  2029. * ----> FeatureTrack
  2030. * -------> ReadTrack
  2031. * -------> VcfTrack
  2032. */
  2033. var Track = function(view, container, obj_dict) {
  2034. // For now, track's container is always view.
  2035. extend(obj_dict, {
  2036. drag_handle_class: "draghandle"
  2037. });
  2038. Drawable.call(this, view, container, obj_dict);
  2039. //
  2040. // Attribute init.
  2041. //
  2042. this.dataset = new data.Dataset({
  2043. id: obj_dict.dataset_id,
  2044. hda_ldda: obj_dict.hda_ldda
  2045. });
  2046. this.dataset_check_type = 'converted_datasets_state';
  2047. this.data_url_extra_params = {};
  2048. this.data_query_wait = ('data_query_wait' in obj_dict ? obj_dict.data_query_wait : DEFAULT_DATA_QUERY_WAIT);
  2049. // A little ugly creating data manager right now due to transition to Backbone-based objects.
  2050. this.data_manager = ('data_manager' in obj_dict ?
  2051. obj_dict.data_manager :
  2052. new visualization.GenomeDataManager({
  2053. dataset: this.dataset,
  2054. data_mode_compatible: this.data_and_mode_compatible,
  2055. can_subset: this.can_subset
  2056. }));
  2057. // Height attributes: min height, max height, and visible height.
  2058. this.min_height_px = 16;
  2059. this.max_height_px = 800;
  2060. this.visible_height_px = 0;
  2061. //
  2062. // Create content div, which is where track is displayed, and add to container if available.
  2063. //
  2064. this.content_div = $("<div class='track-content'>").appendTo(this.container_div);
  2065. if (this.container) {
  2066. this.container.content_div.append(this.container_div);
  2067. if ( !("resize" in obj_dict) || obj_dict.resize ) {
  2068. this.add_resize_handle();
  2069. }
  2070. }
  2071. };
  2072. extend(Track.prototype, Drawable.prototype, {
  2073. action_icons_def: [
  2074. // Change track mode.
  2075. {
  2076. name: "mode_icon",
  2077. title: "Set display mode",
  2078. css_class: "chevron-expand",
  2079. on_click_fn: function() {}
  2080. },
  2081. // Hide/show content.
  2082. Drawable.prototype.action_icons_def[0],
  2083. // Set track as overview.
  2084. {
  2085. name: "overview_icon",
  2086. title: "Set as overview",
  2087. css_class: "overview-icon",
  2088. on_click_fn: function(track) {
  2089. track.view.set_overview(track);
  2090. }
  2091. },
  2092. // Edit config.
  2093. Drawable.prototype.action_icons_def[1],
  2094. // Toggle track filters.
  2095. {
  2096. name: "filters_icon",
  2097. title: "Filters",
  2098. css_class: "filters-icon",
  2099. on_click_fn: function(drawable) {
  2100. // TODO: update tipsy text.
  2101. if (drawable.filters_manager.visible()) {
  2102. drawable.filters_manager.clear_filters();
  2103. }
  2104. else {
  2105. drawable.filters_manager.init_filters();
  2106. }
  2107. drawable.filters_manager.toggle();
  2108. }
  2109. },
  2110. // Toggle track tool.
  2111. {
  2112. name: "tools_icon",
  2113. title: "Tool",
  2114. css_class: "hammer",
  2115. on_click_fn: function(track) {
  2116. // TODO: update tipsy text.
  2117. track.dynamic_tool_div.toggle();
  2118. // Update track name.
  2119. if (track.dynamic_tool_div.is(":visible")) {
  2120. track.set_name(track.name + track.tool_region_and_parameters_str());
  2121. }
  2122. else {
  2123. track.revert_name();
  2124. }
  2125. // HACK: name change modifies icon placement, which leaves tooltip incorrectly placed.
  2126. $(".bs-tooltip").remove();
  2127. }
  2128. },
  2129. // Go to parameter exploration visualization.
  2130. {
  2131. name: "param_space_viz_icon",
  2132. title: "Tool parameter space visualization",
  2133. css_class: "arrow-split",
  2134. on_click_fn: function(track) {
  2135. var template =
  2136. '<strong>Tool</strong>: <%= track.tool.name %><br/>' +
  2137. '<strong>Dataset</strong>: <%= track.name %><br/>' +
  2138. '<strong>Region(s)</strong>: <select name="regions">' +
  2139. '<option value="cur">current viewing area</option>' +
  2140. '<option value="bookmarks">bookmarks</option>' +
  2141. '<option value="both">current viewing area and bookmarks</option>' +
  2142. '</select>',
  2143. html = _.template(template, { track: track });
  2144. var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
  2145. ok_fn = function() {
  2146. var regions_to_use = $('select[name="regions"] option:selected').val(),
  2147. regions,
  2148. view_region = new visualization.GenomeRegion({
  2149. chrom: view.chrom,
  2150. start: view.low,
  2151. end: view.high
  2152. }),
  2153. bookmarked_regions = _.map($(".bookmark"), function(elt) {
  2154. return new visualization.GenomeRegion({from_str: $(elt).children(".position").text()});
  2155. });
  2156. // Get regions for visualization.
  2157. if (regions_to_use === 'cur') {
  2158. // Use only current region.
  2159. regions = [ view_region ];
  2160. }
  2161. else if (regions_to_use === 'bookmarks') {
  2162. // Use only bookmarks.
  2163. regions = bookmarked_regions;
  2164. }
  2165. else {
  2166. // Use both current region and bookmarks.
  2167. regions = [ view_region ].concat(bookmarked_regions);
  2168. }
  2169. hide_modal();
  2170. // Go to visualization.
  2171. window.location.href =
  2172. galaxy_paths.get('sweepster_url') + "?" +
  2173. $.param({
  2174. dataset_id: track.dataset_id,
  2175. hda_ldda: track.hda_ldda,
  2176. regions: JSON.stringify(new Backbone.Collection(regions).toJSON())
  2177. });
  2178. },
  2179. check_enter_esc = function(e) {
  2180. if ((e.keyCode || e.which) === 27) { // Escape key
  2181. cancel_fn();
  2182. } else if ((e.keyCode || e.which) === 13) { // Enter key
  2183. ok_fn();
  2184. }
  2185. };
  2186. show_modal("Visualize tool parameter space and output from different parameter settings?", html, {
  2187. "No": cancel_fn,
  2188. "Yes": ok_fn
  2189. });
  2190. }
  2191. },
  2192. // Remove track.
  2193. Drawable.prototype.action_icons_def[2]
  2194. ],
  2195. can_draw: function() {
  2196. if ( this.dataset_id && Drawable.prototype.can_draw.call(this) ) {
  2197. return true;
  2198. }
  2199. return false;
  2200. },
  2201. build_container_div: function () {
  2202. return $("<div/>").addClass('track').attr("id", "track_" + this.id).css("position", "relative");
  2203. },
  2204. build_header_div: function() {
  2205. var header_div = $("<div class='track-header'/>");
  2206. if (this.view.editor) { this.drag_div = $("<div/>").addClass(this.drag_handle_class).appendTo(header_div); }
  2207. this.name_div = $("<div/>").addClass("track-name").appendTo(header_div).text(this.name)
  2208. .attr( "id", this.name.replace(/\s+/g,'-').replace(/[^a-zA-Z0-9\-]/g,'').toLowerCase() );
  2209. return header_div;
  2210. },
  2211. /**
  2212. * Action to take during resize.
  2213. */
  2214. on_resize: function() {},
  2215. /**
  2216. * Add resizing handle to drawable's container_div.
  2217. */
  2218. add_resize_handle: function () {
  2219. var track = this;
  2220. var in_handle = false;
  2221. var in_drag = false;
  2222. var drag_control = $( "<div class='track-resize'>" );
  2223. // Control shows on hover over track, stays while dragging
  2224. $(track.container_div).hover( function() {
  2225. if ( track.content_visible ) {
  2226. in_handle = true;
  2227. drag_control.show();
  2228. }
  2229. }, function() {
  2230. in_handle = false;
  2231. if ( ! in_drag ) { drag_control.hide(); }
  2232. });
  2233. // Update height and force redraw of current view while dragging,
  2234. // clear cache to force redraw of other tiles.
  2235. drag_control.hide().bind( "dragstart", function( e, d ) {
  2236. in_drag = true;
  2237. d.original_height = $(track.content_div).height();
  2238. }).bind( "drag", function( e, d ) {
  2239. var new_height = Math.min( Math.max( d.original_height + d.deltaY, track.min_height_px ), track.max_height_px );
  2240. $(track.tiles_div).css( 'height', new_height );
  2241. track.visible_height_px = (track.max_height_px === new_height ? 0 : new_height);
  2242. track.on_resize();
  2243. }).bind( "dragend", function( e, d ) {
  2244. track.tile_cache.clear();
  2245. in_drag = false;
  2246. if (!in_handle) { drag_control.hide(); }
  2247. track.config.values.height = track.visible_height_px;
  2248. track.changed();
  2249. }).appendTo(track.container_div);
  2250. },
  2251. /**
  2252. * Set track's modes and update mode icon popup.
  2253. */
  2254. set_display_modes: function(new_modes, init_mode) {
  2255. // Set modes, init mode.
  2256. this.display_modes = new_modes;
  2257. this.mode = (init_mode ? init_mode :
  2258. (this.config && this.config.values['mode'] ?
  2259. this.config.values['mode'] : this.display_modes[0])
  2260. );
  2261. this.action_icons.mode_icon.attr("title", "Set display mode (now: " + this.mode + ")");
  2262. // Setup popup menu for changing modes.
  2263. var
  2264. track = this,
  2265. mode_mapping = {};
  2266. for (var i = 0, len = track.display_modes.length; i < len; i++) {
  2267. var mode = track.display_modes[i];
  2268. mode_mapping[mode] = function(mode) {
  2269. return function() {
  2270. track.change_mode(mode);
  2271. // HACK: the popup menu messes with the track's hover event, so manually show/hide
  2272. // icons div for now.
  2273. track.icons_div.show();
  2274. track.container_div.mouseleave(function() { track.icons_div.hide(); } ); };
  2275. }(mode);
  2276. }
  2277. make_popupmenu(this.action_icons.mode_icon, mode_mapping);
  2278. },
  2279. build_action_icons: function() {
  2280. Drawable.prototype.build_action_icons.call(this, this.action_icons_def);
  2281. // Set up behavior for modes popup.
  2282. if (this.display_modes !== undefined) {
  2283. this.set_display_modes(this.display_modes);
  2284. }
  2285. },
  2286. /**
  2287. * Hide any elements that are part of the tracks contents area. Should
  2288. * remove as approprite, the track will be redrawn by show_contents.
  2289. */
  2290. hide_contents: function () {
  2291. // Hide tiles.
  2292. this.tiles_div.hide();
  2293. // Hide any y axis labels (common to several track types)
  2294. this.container_div.find(".yaxislabel, .track-resize").hide();
  2295. },
  2296. show_contents: function() {
  2297. // Show the contents div and labels (if present)
  2298. this.tiles_div.show();
  2299. this.container_div.find(".yaxislabel, .track-resize").show();
  2300. // Request a redraw of the content
  2301. this.request_draw();
  2302. },
  2303. /**
  2304. * Returns track type.
  2305. */
  2306. get_type: function() {
  2307. // Order is important: start with most-specific classes and go up the track hierarchy.
  2308. if (this instanceof LabelTrack) {
  2309. return "LabelTrack";
  2310. }
  2311. else if (this instanceof ReferenceTrack) {
  2312. return "ReferenceTrack";
  2313. }
  2314. else if (this instanceof LineTrack) {
  2315. return "LineTrack";
  2316. }
  2317. else if (this instanceof ReadTrack) {
  2318. return "ReadTrack";
  2319. }
  2320. else if (this instanceof VcfTrack) {
  2321. return "VcfTrack";
  2322. }
  2323. else if (this instanceof CompositeTrack) {
  2324. return "CompositeTrack";
  2325. }
  2326. else if (this instanceof FeatureTrack) {
  2327. return "FeatureTrack";
  2328. }
  2329. return "";
  2330. },
  2331. /**
  2332. * Initialize and draw the track.
  2333. */
  2334. init: function() {
  2335. var track = this;
  2336. track.enabled = false;
  2337. track.tile_cache.clear();
  2338. track.data_manager.clear();
  2339. track.content_div.css("height", "auto");
  2340. /*
  2341. if (!track.content_div.text()) {
  2342. track.content_div.text(DATA_LOADING);
  2343. }
  2344. */
  2345. // Remove old track content (e.g. tiles, messages).
  2346. track.tiles_div.children().remove();
  2347. track.container_div.removeClass("nodata error pending");
  2348. //
  2349. // Tracks with no dataset id are handled differently.
  2350. // FIXME: is this really necessary?
  2351. //
  2352. if (!track.dataset_id) {
  2353. return;
  2354. }
  2355. // Get dataset state; if state is fine, enable and draw track. Otherwise, show message
  2356. // about track status.
  2357. var init_deferred = $.Deferred(),
  2358. params = {
  2359. hda_ldda: track.hda_ldda,
  2360. data_type: this.dataset_check_type,
  2361. chrom: track.view.chrom };
  2362. $.getJSON(this.dataset.url(), params, function (result) {
  2363. if (!result || result === "error" || result.kind === "error") {
  2364. track.container_div.addClass("error");
  2365. track.tiles_div.text(DATA_ERROR);
  2366. if (result.message) {
  2367. var error_link = $(" <a href='javascript:void(0);'></a>").text("View error").click(function() {
  2368. show_modal( "Trackster Error", "<pre>" + result.message + "</pre>", { "Close" : hide_modal } );
  2369. });
  2370. track.tiles_div.append(error_link);
  2371. }
  2372. } else if (result === "no converter") {
  2373. track.container_div.addClass("error");
  2374. track.tiles_div.text(DATA_NOCONVERTER);
  2375. } else if (result === "no data" || (result.data !== undefined && (result.data === null || result.data.length === 0))) {
  2376. track.container_div.addClass("nodata");
  2377. track.tiles_div.text(DATA_NONE);
  2378. } else if (result === "pending") {
  2379. track.container_div.addClass("pending");
  2380. track.tiles_div.html(DATA_PENDING);
  2381. //$("<img/>").attr("src", image_path + "/yui/rel_interstitial_loading.gif").appendTo(track.tiles_div);
  2382. setTimeout(function() { track.init(); }, track.data_query_wait);
  2383. } else if (result === "data" || result['status'] === "data") {
  2384. if (result['valid_chroms']) {
  2385. track.valid_chroms = result['valid_chroms'];
  2386. track.update_icons();
  2387. }
  2388. track.tiles_div.text(DATA_OK);
  2389. if (track.view.chrom) {
  2390. track.tiles_div.text("");
  2391. track.tiles_div.css( "height", track.visible_height_px + "px" );
  2392. track.enabled = true;
  2393. // predraw_init may be asynchronous, wait for it and then draw
  2394. $.when(track.predraw_init()).done(function() {
  2395. init_deferred.resolve();
  2396. track.container_div.removeClass("nodata error pending");
  2397. track.request_draw();
  2398. });
  2399. }
  2400. else {
  2401. init_deferred.resolve();
  2402. }
  2403. }
  2404. });
  2405. this.update_icons();
  2406. return init_deferred;
  2407. },
  2408. /**
  2409. * Additional initialization required before drawing track for the first time.
  2410. */
  2411. predraw_init: function() {},
  2412. /**
  2413. * Returns all drawables in this drawable.
  2414. */
  2415. get_drawables: function() {
  2416. return this;
  2417. }
  2418. });
  2419. var TiledTrack = function(view, container, obj_dict) {
  2420. Track.call(this, view, container, obj_dict);
  2421. var track = this;
  2422. // Make track moveable.
  2423. moveable(track.container_div, track.drag_handle_class, ".group", track);
  2424. // Attribute init.
  2425. this.filters_manager = new filters_mod.FiltersManager(this, ('filters' in obj_dict ? obj_dict.filters : null));
  2426. // HACK: set filters manager for data manager.
  2427. // FIXME: prolly need function to set filters and update data_manager reference.
  2428. this.data_manager.set('filters_manager', this.filters_manager);
  2429. this.filters_available = false;
  2430. this.tool = ('tool' in obj_dict && obj_dict.tool ? new Tool(this, obj_dict.tool, obj_dict.tool_state) : null);
  2431. this.tile_cache = new visualization.Cache(TILE_CACHE_SIZE);
  2432. if (this.header_div) {
  2433. //
  2434. // Setup filters.
  2435. //
  2436. this.set_filters_manager(this.filters_manager);
  2437. //
  2438. // Create dynamic tool div.
  2439. //
  2440. if (this.tool) {
  2441. this.dynamic_tool_div = this.tool.parent_div;
  2442. this.header_div.after(this.dynamic_tool_div);
  2443. }
  2444. }
  2445. // Add tiles_div, overlay_div to content_div.
  2446. this.tiles_div = $("<div/>").addClass("tiles").appendTo(this.content_div);
  2447. this.overlay_div = $("<div/>").addClass("overlay").appendTo(this.content_div);
  2448. if (obj_dict.mode) {
  2449. this.change_mode(obj_dict.mode);
  2450. }
  2451. };
  2452. extend(TiledTrack.prototype, Drawable.prototype, Track.prototype, {
  2453. action_icons_def: Track.prototype.action_icons_def.concat( [
  2454. // Show more rows when all features are not slotted.
  2455. {
  2456. name: "show_more_rows_icon",
  2457. title: "To minimize track height, not all feature rows are displayed. Click to display more rows.",
  2458. css_class: "exclamation",
  2459. on_click_fn: function(track) {
  2460. $(".bs-tooltip").remove();
  2461. // HACKish: is it always reasonble to use view to get w_scale/current resolution?
  2462. track.slotters[ track.view.resolution_px_b ].max_rows *= 2;
  2463. track.request_draw(true);
  2464. },
  2465. hide: true
  2466. }
  2467. ] ),
  2468. /**
  2469. * Returns a copy of the track. The copy uses the same data manager so that the tracks can share data.
  2470. */
  2471. copy: function(container) {
  2472. // Create copy.
  2473. var obj_dict = this.to_dict();
  2474. extend(obj_dict, {
  2475. data_manager: this.data_manager
  2476. });
  2477. var new_track = new this.constructor(this.view, container, obj_dict);
  2478. // Misc. init and return.
  2479. new_track.change_mode(this.mode);
  2480. new_track.enabled = this.enabled;
  2481. return new_track;
  2482. },
  2483. /**
  2484. * Set filters manager + HTML elements.
  2485. */
  2486. set_filters_manager: function(filters_manager) {
  2487. this.filters_manager = filters_manager;
  2488. this.header_div.after(this.filters_manager.parent_div);
  2489. },
  2490. /**
  2491. * Returns representation of object in a dictionary for easy saving.
  2492. * Use from_dict to recreate object.
  2493. */
  2494. to_dict: function() {
  2495. return {
  2496. "track_type": this.get_type(),
  2497. "name": this.name,
  2498. "hda_ldda": this.hda_ldda,
  2499. "dataset_id": this.dataset_id,
  2500. "prefs": this.prefs,
  2501. "mode": this.mode,
  2502. "filters": this.filters_manager.to_dict(),
  2503. "tool_state": (this.tool ? this.tool.state_dict() : {})
  2504. };
  2505. },
  2506. /**
  2507. * Change track's mode.
  2508. */
  2509. change_mode: function(new_mode) {
  2510. var track = this;
  2511. // TODO: is it necessary to store the mode in two places (.mode and track_config)?
  2512. track.mode = new_mode;
  2513. track.config.values['mode'] = new_mode;
  2514. track.tile_cache.clear();
  2515. track.request_draw();
  2516. this.action_icons.mode_icon.attr("title", "Set display mode (now: " + track.mode + ")");
  2517. return track;
  2518. },
  2519. /**
  2520. * Update track's buttons.
  2521. */
  2522. update_icons: function() {
  2523. var track = this;
  2524. //
  2525. // Show/hide filter icon.
  2526. //
  2527. if (track.filters_available) {
  2528. track.action_icons.filters_icon.show();
  2529. }
  2530. else {
  2531. track.action_icons.filters_icon.hide();
  2532. }
  2533. //
  2534. // Show/hide tool icons.
  2535. //
  2536. if (track.tool) {
  2537. track.action_icons.tools_icon.show();
  2538. track.action_icons.param_space_viz_icon.show();
  2539. }
  2540. else {
  2541. track.action_icons.tools_icon.hide();
  2542. track.action_icons.param_space_viz_icon.hide();
  2543. }
  2544. //
  2545. // List chrom/contigs with data option.
  2546. //
  2547. /*
  2548. if (track.valid_chroms) {
  2549. track_dropdown["List chrom/contigs with data"] = function() {
  2550. show_modal("Chrom/contigs with data", "<p>" + track.valid_chroms.join("<br/>") + "</p>", { "Close": function() { hide_modal(); } });
  2551. };
  2552. }
  2553. */
  2554. },
  2555. /**
  2556. * Generate a key for the tile cache.
  2557. * TODO: create a TileCache object (like DataCache) and generate key internally.
  2558. */
  2559. _gen_tile_cache_key: function(width, w_scale, tile_index) {
  2560. return width + '_' + w_scale + '_' + tile_index;
  2561. },
  2562. /**
  2563. * Request that track be drawn.
  2564. */
  2565. request_draw: function(force, clear_after) {
  2566. this.view.request_redraw(false, force, clear_after, this);
  2567. },
  2568. /**
  2569. * Actions to be taken before drawing.
  2570. */
  2571. before_draw: function() {},
  2572. /**
  2573. * Draw track. It is possible to force a redraw rather than use cached tiles and/or clear old
  2574. * tiles after drawing new tiles.
  2575. * NOTE: this function should never be called directly; use request_draw() so that requestAnimationFrame
  2576. * can manage drawing.
  2577. */
  2578. _draw: function(force, clear_after) {
  2579. if ( !this.can_draw() ) { return; }
  2580. var low = this.view.low,
  2581. high = this.view.high,
  2582. range = high - low,
  2583. width = this.view.container.width(),
  2584. w_scale = this.view.resolution_px_b,
  2585. resolution = this.view.resolution_b_px;
  2586. // For overview, adjust high, low, resolution, and w_scale.
  2587. if (this.is_overview) {
  2588. low = this.view.max_low;
  2589. high = this.view.max_high;
  2590. resolution = ( view.max_high - view.max_low ) / width;
  2591. w_scale = 1 / resolution;
  2592. }
  2593. this.before_draw();
  2594. //
  2595. // Method for moving and/or removing tiles:
  2596. // (a) mark all elements for removal using class 'remove'
  2597. // (b) during tile drawing/placement, remove class for elements that are moved;
  2598. // this occurs in show_tile()
  2599. // (c) after drawing tiles, remove elements still marked for removal
  2600. // (i.e. that still have class 'remove').
  2601. //
  2602. // Step (a) for (re)moving tiles.
  2603. this.tiles_div.children().addClass("remove");
  2604. var
  2605. // Index of first tile that overlaps visible region.
  2606. tile_index = Math.floor( low / (resolution * TILE_SIZE) ),
  2607. // If any tile could not be drawn yet, this will be set to false.
  2608. all_tiles_drawn = true,
  2609. drawn_tiles = [],
  2610. is_tile = function(o) { return (o && 'track' in o); };
  2611. // Draw tiles.
  2612. while ( ( tile_index * TILE_SIZE * resolution ) < high ) {
  2613. var draw_result = this.draw_helper( force, width, tile_index, resolution, this.tiles_div, w_scale );
  2614. if ( is_tile(draw_result) ) {
  2615. drawn_tiles.push( draw_result );
  2616. } else {
  2617. all_tiles_drawn = false;
  2618. }
  2619. tile_index += 1;
  2620. }
  2621. // Step (c) for (re)moving tiles when clear_after is false.
  2622. if (!clear_after) { this.tiles_div.children(".remove").removeClass("remove").remove(); }
  2623. // Use interval to check if tiles have been drawn. When all tiles are drawn, call post-draw actions.
  2624. var track = this;
  2625. if (all_tiles_drawn) {
  2626. // Step (c) for (re)moving tiles when clear_after is true:
  2627. this.tiles_div.children(".remove").remove();
  2628. track.postdraw_actions(drawn_tiles, width, w_scale, clear_after);
  2629. }
  2630. },
  2631. /**
  2632. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  2633. * drawn/fetched and shown.
  2634. */
  2635. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  2636. //
  2637. // If some tiles have icons, set padding of tiles without icons so features and rows align.
  2638. //
  2639. var icons_present = false;
  2640. for (var tile_index = 0; tile_index < tiles.length; tile_index++) {
  2641. if (tiles[tile_index].has_icons) {
  2642. icons_present = true;
  2643. break;
  2644. }
  2645. }
  2646. if (icons_present) {
  2647. for (var tile_index = 0; tile_index < tiles.length; tile_index++) {
  2648. tile = tiles[tile_index];
  2649. if (!tile.has_icons) {
  2650. // Need to align with other tile(s) that have icons.
  2651. tile.html_elt.css("padding-top", ERROR_PADDING);
  2652. }
  2653. }
  2654. }
  2655. },
  2656. /**
  2657. * Retrieves from cache, draws, or sets up drawing for a single tile. Returns either a Tile object or a
  2658. * jQuery.Deferred object that is fulfilled when tile can be drawn again.
  2659. */
  2660. draw_helper: function(force, width, tile_index, resolution, parent_element, w_scale, kwargs) {
  2661. var track = this,
  2662. key = this._gen_tile_cache_key(width, w_scale, tile_index),
  2663. region = this._get_tile_bounds(tile_index, resolution);
  2664. // Init kwargs if necessary to avoid having to check if kwargs defined.
  2665. if (!kwargs) { kwargs = {}; }
  2666. // Check tile cache, if found show existing tile in correct position
  2667. var tile = (force ? undefined : track.tile_cache.get_elt(key));
  2668. if (tile) {
  2669. track.show_tile(tile, parent_element, w_scale);
  2670. return tile;
  2671. }
  2672. // Flag to track whether we can draw everything now
  2673. var can_draw_now = true;
  2674. // Get the track data, maybe a deferred
  2675. var tile_data = track.data_manager.get_data(region, track.mode, resolution, track.data_url_extra_params);
  2676. if ( is_deferred( tile_data ) ) {
  2677. can_draw_now = false;
  2678. }
  2679. // Get seq data if needed, maybe a deferred
  2680. var seq_data;
  2681. if ( view.reference_track && w_scale > view.canvas_manager.char_width_px ) {
  2682. seq_data = view.reference_track.data_manager.get_data(region, track.mode, resolution, view.reference_track.data_url_extra_params);
  2683. if ( is_deferred( seq_data ) ) {
  2684. can_draw_now = false;
  2685. }
  2686. }
  2687. // If we can draw now, do so.
  2688. if ( can_draw_now ) {
  2689. // Set up and draw tile.
  2690. extend(tile_data, kwargs[ 'more_tile_data' ] );
  2691. // HACK: this is FeatureTrack-specific.
  2692. // If track mode is Auto, determine mode and update.
  2693. var mode = track.mode;
  2694. if (mode === "Auto") {
  2695. mode = track.get_mode(tile_data);
  2696. track.update_auto_mode(mode);
  2697. }
  2698. // Draw canvas.
  2699. var
  2700. canvas = track.view.canvas_manager.new_canvas(),
  2701. tile_low = region.get('start'),
  2702. tile_high = region.get('end'),
  2703. width = Math.ceil( (tile_high - tile_low) * w_scale ) + track.left_offset,
  2704. height = track.get_canvas_height(tile_data, mode, w_scale, width);
  2705. canvas.width = width;
  2706. canvas.height = height;
  2707. var ctx = canvas.getContext('2d');
  2708. ctx.translate(this.left_offset, 0);
  2709. var tile = track.draw_tile(tile_data, ctx, mode, resolution, region, w_scale, seq_data);
  2710. // Don't cache, show if no tile.
  2711. if (tile !== undefined) {
  2712. track.tile_cache.set_elt(key, tile);
  2713. track.show_tile(tile, parent_element, w_scale);
  2714. }
  2715. return tile;
  2716. }
  2717. // Can't draw now, so trigger another redraw when the data is ready
  2718. var can_draw = $.Deferred();
  2719. $.when( tile_data, seq_data ).then( function() {
  2720. view.request_redraw(false, false, false, track);
  2721. can_draw.resolve();
  2722. });
  2723. // Returned Deferred is resolved when tile can be drawn.
  2724. return can_draw;
  2725. },
  2726. /**
  2727. * Returns canvas height needed to display data; return value is an integer that denotes the
  2728. * number of pixels required.
  2729. */
  2730. get_canvas_height: function(result, mode, w_scale, canvas_width) {
  2731. return this.visible_height_px;
  2732. },
  2733. /**
  2734. * Draw a track tile.
  2735. * @param result result from server
  2736. * @param ctx canvas context to draw on
  2737. * @param mode mode to draw in
  2738. * @param resolution view resolution
  2739. * @param region region to draw on tile
  2740. * @param w_scale pixels per base
  2741. * @param ref_seq reference sequence data
  2742. */
  2743. draw_tile: function(result, ctx, mode, resolution, region, w_scale, ref_seq) {
  2744. console.log("Warning: TiledTrack.draw_tile() not implemented.");
  2745. },
  2746. /**
  2747. * Show track tile and perform associated actions. Showing tile may actually move
  2748. * an existing tile rather than reshowing it.
  2749. */
  2750. show_tile: function(tile, parent_element, w_scale) {
  2751. var
  2752. track = this,
  2753. tile_element = tile.html_elt;
  2754. //
  2755. // Show/move tile element.
  2756. //
  2757. tile.predisplay_actions();
  2758. // Position tile element based on current viewport.
  2759. var left = ( tile.low - (this.is_overview? this.view.max_low : this.view.low) ) * w_scale;
  2760. if (this.left_offset) {
  2761. left -= this.left_offset;
  2762. }
  2763. tile_element.css({ position: 'absolute', top: 0, left: left });
  2764. if ( tile_element.hasClass("remove") ) {
  2765. // Step (b) for (re)moving tiles. See _draw() function for description of algorithm
  2766. // for removing tiles.
  2767. tile_element.removeClass("remove");
  2768. }
  2769. else {
  2770. // Showing new tile.
  2771. parent_element.append(tile_element);
  2772. }
  2773. track.after_show_tile(tile);
  2774. },
  2775. /**
  2776. * Actions to be taken after showing tile.
  2777. */
  2778. after_show_tile: function(tile) {
  2779. // Update max height based on current tile.
  2780. this.max_height_px = Math.max(this.max_height_px, tile.html_elt.height());
  2781. // Update height for all tiles based on max height.
  2782. tile.html_elt.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. * Returns a genome region that corresponds to a tile at a particular resolution
  2792. */
  2793. _get_tile_bounds: function(tile_index, resolution) {
  2794. var tile_low = Math.floor( tile_index * TILE_SIZE * resolution ),
  2795. tile_length = Math.ceil( TILE_SIZE * resolution ),
  2796. // Tile high cannot be larger than view.max_high, which the chromosome length.
  2797. tile_high = (tile_low + tile_length <= this.view.max_high ? tile_low + tile_length : this.view.max_high);
  2798. return new visualization.GenomeRegion({
  2799. chrom: this.view.chrom,
  2800. start: tile_low,
  2801. end: tile_high
  2802. });
  2803. },
  2804. /**
  2805. * Utility function that creates a label string describing the region and parameters of a track's tool.
  2806. */
  2807. tool_region_and_parameters_str: function(chrom, low, high) {
  2808. // Region is chrom:low-high or 'all.'
  2809. var
  2810. track = this,
  2811. region = (chrom !== undefined && low !== undefined && high !== undefined ?
  2812. chrom + ":" + low + "-" + high : "all");
  2813. return " - region=[" + region + "], parameters=[" + track.tool.get_param_values().join(", ") + "]";
  2814. },
  2815. /**
  2816. * Returns true if data is compatible with a given mode. Defaults to true because, for many tracks,
  2817. * all data is compatible with all modes.
  2818. */
  2819. data_and_mode_compatible: function(data, mode) {
  2820. return true;
  2821. },
  2822. /**
  2823. * Returns true if data can be subsetted. Defaults to false to ensure data is fetched when needed.
  2824. */
  2825. can_subset: function(data) {
  2826. return false;
  2827. },
  2828. /**
  2829. * Set up track to receive tool data.
  2830. */
  2831. init_for_tool_data: function() {
  2832. // Set up track to fetch raw data rather than converted data.
  2833. this.data_manager.set('data_type', 'raw_data');
  2834. this.data_query_wait = 1000;
  2835. this.dataset_check_type = 'state';
  2836. //
  2837. // Set up one-time, post-draw to clear tool execution settings.
  2838. //
  2839. this.normal_postdraw_actions = this.postdraw_actions;
  2840. this.postdraw_actions = function(tiles, width, w_scale, clear_after) {
  2841. var self = this;
  2842. // Do normal postdraw init.
  2843. self.normal_postdraw_actions(tiles, width, w_scale, clear_after);
  2844. // Tool-execution specific post-draw init:
  2845. // Reset dataset check, wait time.
  2846. self.dataset_check_type = 'converted_datasets_state';
  2847. self.data_query_wait = DEFAULT_DATA_QUERY_WAIT;
  2848. // Reset data URL when dataset indexing has completed/when not pending.
  2849. var ss_deferred = new util.ServerStateDeferred({
  2850. url: self.dataset_state_url,
  2851. url_params: {dataset_id : self.dataset_id, hda_ldda: self.hda_ldda},
  2852. interval: self.data_query_wait,
  2853. // Set up deferred to check dataset state until it is not pending.
  2854. success_fn: function(result) { return result !== "pending"; }
  2855. });
  2856. $.when(ss_deferred.go()).then(function() {
  2857. // Dataset is indexed, so use converted data.
  2858. self.data_manager.set('data_type', 'data');
  2859. });
  2860. // Reset post-draw actions function.
  2861. self.postdraw_actions = self.normal_postdraw_actions;
  2862. };
  2863. }
  2864. });
  2865. var LabelTrack = function (view, container) {
  2866. var obj_dict = {
  2867. resize: false
  2868. };
  2869. Track.call(this, view, container, obj_dict);
  2870. this.container_div.addClass( "label-track" );
  2871. };
  2872. extend(LabelTrack.prototype, Track.prototype, {
  2873. build_header_div: function() {},
  2874. init: function() {
  2875. // Enable by default because there should always be data when drawing track.
  2876. this.enabled = true;
  2877. },
  2878. _draw: function() {
  2879. var view = this.view,
  2880. range = view.high - view.low,
  2881. tickDistance = Math.floor( Math.pow( 10, Math.floor( Math.log( range ) / Math.log( 10 ) ) ) ),
  2882. position = Math.floor( view.low / tickDistance ) * tickDistance,
  2883. width = this.view.container.width(),
  2884. new_div = $("<div style='position: relative; height: 1.3em;'></div>");
  2885. while ( position < view.high ) {
  2886. var screenPosition = ( position - view.low ) / range * width;
  2887. new_div.append( $("<div class='label'>" + commatize( position ) + "</div>").css( {
  2888. position: "absolute",
  2889. // Reduce by one to account for border
  2890. left: screenPosition - 1
  2891. }));
  2892. position += tickDistance;
  2893. }
  2894. this.content_div.children( ":first" ).remove();
  2895. this.content_div.append( new_div );
  2896. }
  2897. });
  2898. /**
  2899. * A tiled track composed of multiple other tracks.
  2900. */
  2901. var CompositeTrack = function(view, container, obj_dict) {
  2902. TiledTrack.call(this, view, container, obj_dict);
  2903. // Init drawables; each drawable is a copy so that config/preferences
  2904. // are independent of each other. Also init left offset.
  2905. this.drawables = [];
  2906. this.left_offset = 0;
  2907. if ('drawables' in obj_dict) {
  2908. var drawable;
  2909. for (var i = 0; i < obj_dict.drawables.length; i++) {
  2910. drawable = obj_dict.drawables[i];
  2911. this.drawables[i] = object_from_template(drawable, view, null);
  2912. // Track's left offset is the max of all tracks.
  2913. if (drawable.left_offset > this.left_offset) {
  2914. this.left_offset = drawable.left_offset;
  2915. }
  2916. }
  2917. this.enabled = true;
  2918. }
  2919. // HACK: modes should be static class vars for most tracks and should update as
  2920. // needed for CompositeTracks
  2921. if (this.drawables.length !== 0) {
  2922. this.set_display_modes(this.drawables[0].display_modes, this.drawables[0].mode);
  2923. }
  2924. this.update_icons();
  2925. // HACK: needed for saving object for now. Need to generalize get_type() to all Drawables and use
  2926. // that for object type.
  2927. this.obj_type = "CompositeTrack";
  2928. };
  2929. extend(CompositeTrack.prototype, TiledTrack.prototype, {
  2930. action_icons_def:
  2931. [
  2932. // Create composite track from group's tracks.
  2933. {
  2934. name: "composite_icon",
  2935. title: "Show individual tracks",
  2936. css_class: "layers-stack",
  2937. on_click_fn: function(track) {
  2938. $(".bs-tooltip").remove();
  2939. track.show_group();
  2940. }
  2941. }
  2942. ].concat(TiledTrack.prototype.action_icons_def),
  2943. // HACK: CompositeTrack should inherit from DrawableCollection as well.
  2944. /**
  2945. * Returns representation of object in a dictionary for easy saving.
  2946. * Use from_dict to recreate object.
  2947. */
  2948. to_dict: DrawableCollection.prototype.to_dict,
  2949. add_drawable: DrawableCollection.prototype.add_drawable,
  2950. unpack_drawables: DrawableCollection.prototype.unpack_drawables,
  2951. change_mode: function(new_mode) {
  2952. TiledTrack.prototype.change_mode.call(this, new_mode);
  2953. for (var i = 0; i < this.drawables.length; i++) {
  2954. this.drawables[i].change_mode(new_mode);
  2955. }
  2956. },
  2957. /**
  2958. * Initialize component tracks and draw composite track when all components are initialized.
  2959. */
  2960. init: function() {
  2961. // Init components.
  2962. var init_deferreds = [];
  2963. for (var i = 0; i < this.drawables.length; i++) {
  2964. init_deferreds.push(this.drawables[i].init());
  2965. }
  2966. // Draw composite when all tracks available.
  2967. var track = this;
  2968. $.when.apply($, init_deferreds).then(function() {
  2969. track.enabled = true;
  2970. track.request_draw();
  2971. });
  2972. },
  2973. update_icons: function() {
  2974. // For now, hide filters and tool.
  2975. this.action_icons.filters_icon.hide();
  2976. this.action_icons.tools_icon.hide();
  2977. this.action_icons.param_space_viz_icon.hide();
  2978. },
  2979. can_draw: Drawable.prototype.can_draw,
  2980. draw_helper: function(force, width, tile_index, resolution, parent_element, w_scale, kwargs) {
  2981. // FIXME: this function is similar to TiledTrack.draw_helper -- can the two be merged/refactored?
  2982. var track = this,
  2983. key = this._gen_tile_cache_key(width, w_scale, tile_index),
  2984. region = this._get_tile_bounds(tile_index, resolution);
  2985. // Init kwargs if necessary to avoid having to check if kwargs defined.
  2986. if (!kwargs) { kwargs = {}; }
  2987. // Check tile cache, if found show existing tile in correct position
  2988. var tile = (force ? undefined : track.tile_cache.get_elt(key));
  2989. if (tile) {
  2990. track.show_tile(tile, parent_element, w_scale);
  2991. return tile;
  2992. }
  2993. // Try to get drawables' data.
  2994. var all_data = [],
  2995. track,
  2996. // Flag to track whether we can draw everything now
  2997. can_draw_now = true,
  2998. tile_data,
  2999. seq_data;
  3000. for (var i = 0; i < this.drawables.length; i++) {
  3001. track = this.drawables[i];
  3002. // Get the track data, maybe a deferred.
  3003. tile_data = track.data_manager.get_data(region, track.mode, resolution, track.data_url_extra_params);
  3004. if ( is_deferred( tile_data ) ) {
  3005. can_draw_now = false;
  3006. }
  3007. all_data.push(tile_data);
  3008. // Get seq data if needed, maybe a deferred.
  3009. seq_data = null;
  3010. if ( view.reference_track && w_scale > view.canvas_manager.char_width_px ) {
  3011. seq_data = view.reference_track.data_manager.get_data(region, track.mode, resolution, view.reference_track.data_url_extra_params);
  3012. if ( is_deferred( seq_data ) ) {
  3013. can_draw_now = false;
  3014. }
  3015. }
  3016. all_data.push(seq_data);
  3017. }
  3018. // If we can draw now, do so.
  3019. if ( can_draw_now ) {
  3020. // Set up and draw tile.
  3021. extend(tile_data, kwargs[ 'more_tile_data' ] );
  3022. this.tile_predraw_init();
  3023. var canvas = track.view.canvas_manager.new_canvas(),
  3024. tile_bounds = track._get_tile_bounds(tile_index, resolution),
  3025. tile_low = region.get('start'),
  3026. tile_high = region.get('end'),
  3027. all_data_index = 0,
  3028. width = Math.ceil( (tile_high - tile_low) * w_scale ) + this.left_offset,
  3029. height = 0,
  3030. track_modes = [],
  3031. i;
  3032. // Get max height for all tracks and record track modes.
  3033. var track_canvas_height = 0;
  3034. for (i = 0; i < this.drawables.length; i++, all_data_index += 2) {
  3035. track = this.drawables[i];
  3036. tile_data = all_data[ all_data_index ];
  3037. // HACK: this is FeatureTrack-specific.
  3038. // If track mode is Auto, determine mode and update.
  3039. var mode = track.mode;
  3040. if (mode === "Auto") {
  3041. mode = track.get_mode(tile_data);
  3042. track.update_auto_mode(mode);
  3043. }
  3044. track_modes.push(mode);
  3045. track_canvas_height = track.get_canvas_height(tile_data, mode, w_scale, width);
  3046. if (track_canvas_height > height) { height = track_canvas_height; }
  3047. }
  3048. //
  3049. // Draw all tracks on a single tile.
  3050. //
  3051. canvas.width = width;
  3052. // Height is specified in kwargs or is the height found above.
  3053. canvas.height = (kwargs.height ? kwargs.height : height);
  3054. all_data_index = 0;
  3055. var ctx = canvas.getContext('2d');
  3056. ctx.translate(this.left_offset, 0);
  3057. ctx.globalAlpha = 0.5;
  3058. ctx.globalCompositeOperation = "source-over";
  3059. for (i = 0; i < this.drawables.length; i++, all_data_index += 2) {
  3060. track = this.drawables[i];
  3061. tile_data = all_data[ all_data_index ];
  3062. seq_data = all_data[ all_data_index + 1 ];
  3063. tile = track.draw_tile(tile_data, ctx, track_modes[i], resolution, region, w_scale, seq_data);
  3064. }
  3065. // Don't cache, show if no tile.
  3066. this.tile_cache.set_elt(key, tile);
  3067. this.show_tile(tile, parent_element, w_scale);
  3068. return tile;
  3069. }
  3070. // Can't draw now, so trigger another redraw when the data is ready
  3071. var can_draw = $.Deferred(),
  3072. track = this;
  3073. $.when.apply($, all_data).then(function() {
  3074. view.request_redraw(false, false, false, track);
  3075. can_draw.resolve();
  3076. });
  3077. // Returned Deferred that is resolved when tile can be drawn.
  3078. return can_draw;
  3079. },
  3080. /**
  3081. * Replace this track with group that includes individual tracks.
  3082. */
  3083. show_group: function() {
  3084. // Create group with individual tracks.
  3085. var group = new DrawableGroup(this.view, this.container, {
  3086. name: this.name
  3087. }),
  3088. track;
  3089. for (var i = 0; i < this.drawables.length; i++) {
  3090. track = this.drawables[i];
  3091. track.update_icons();
  3092. group.add_drawable(track);
  3093. track.container = group;
  3094. group.content_div.append(track.container_div);
  3095. }
  3096. // Replace track with group.
  3097. var index = this.container.replace_drawable(this, group, true);
  3098. group.request_draw();
  3099. },
  3100. /**
  3101. * Actions taken before drawing a tile.
  3102. */
  3103. tile_predraw_init: function() {
  3104. //
  3105. // Set min, max for LineTracks to be largest min, max.
  3106. //
  3107. // Get smallest min, biggest max.
  3108. var
  3109. min = Number.MAX_VALUE,
  3110. max = -min,
  3111. track;
  3112. for (var i = 0; i < this.drawables.length; i++) {
  3113. track = this.drawables[i];
  3114. if (track instanceof LineTrack) {
  3115. if (track.prefs.min_value < min) {
  3116. min = track.prefs.min_value;
  3117. }
  3118. if (track.prefs.max_value > max) {
  3119. max = track.prefs.max_value;
  3120. }
  3121. }
  3122. }
  3123. // Set all tracks to smallest min, biggest max.
  3124. for (var i = 0; i < this.drawables.length; i++) {
  3125. track = this.drawables[i];
  3126. track.prefs.min_value = min;
  3127. track.prefs.max_value = max;
  3128. }
  3129. },
  3130. /**
  3131. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  3132. * drawn/fetched and shown.
  3133. */
  3134. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3135. TiledTrack.prototype.postdraw_actions.call(this, tiles, width, w_scale, clear_after);
  3136. // All tiles must be the same height in order to draw LineTracks, so redraw tiles as needed.
  3137. var max_height = -1;
  3138. for (var i = 0; i < tiles.length; i++) {
  3139. var height = tiles[i].html_elt.find("canvas").height();
  3140. if (height > max_height) {
  3141. max_height = height;
  3142. }
  3143. }
  3144. for (var i = 0; i < tiles.length; i++) {
  3145. var tile = tiles[i];
  3146. if (tile.html_elt.find("canvas").height() !== max_height) {
  3147. this.draw_helper(true, width, tile.index, tile.resolution, tile.html_elt.parent(), w_scale, { height: max_height } );
  3148. tile.html_elt.remove();
  3149. }
  3150. }
  3151. }
  3152. });
  3153. var ReferenceTrack = function (view) {
  3154. TiledTrack.call(this, view, { content_div: view.top_labeltrack }, { resize: false });
  3155. view.reference_track = this;
  3156. this.left_offset = 200;
  3157. this.visible_height_px = 12;
  3158. this.container_div.addClass("reference-track");
  3159. this.content_div.css("background", "none");
  3160. this.content_div.css("min-height", "0px");
  3161. this.content_div.css("border", "none");
  3162. this.data_url = reference_url + "/" + this.view.dbkey;
  3163. this.data_url_extra_params = {reference: true};
  3164. this.data_manager = new ReferenceTrackDataManager({
  3165. data_url: this.data_url
  3166. });
  3167. this.hide_contents();
  3168. };
  3169. extend(ReferenceTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3170. build_header_div: function() {},
  3171. init: function() {
  3172. this.data_manager.clear();
  3173. // Enable by default because there should always be data when drawing track.
  3174. this.enabled = true;
  3175. },
  3176. can_draw: Drawable.prototype.can_draw,
  3177. /**
  3178. * Only retrieves data and draws tile if reference data can be displayed.
  3179. */
  3180. draw_helper: function(force, width, tile_index, resolution, parent_element, w_scale, kwargs) {
  3181. if (w_scale > this.view.canvas_manager.char_width_px) {
  3182. return TiledTrack.prototype.draw_helper.call(this, force, width, tile_index, resolution, parent_element, w_scale, kwargs);
  3183. }
  3184. else {
  3185. this.hide_contents();
  3186. return null;
  3187. }
  3188. },
  3189. /**
  3190. * Draw ReferenceTrack tile.
  3191. */
  3192. draw_tile: function(seq, ctx, mode, resolution, region, w_scale) {
  3193. var track = this;
  3194. if (w_scale > this.view.canvas_manager.char_width_px) {
  3195. if (seq.data === null) {
  3196. this.hide_contents();
  3197. return;
  3198. }
  3199. var canvas = ctx.canvas;
  3200. ctx.font = ctx.canvas.manager.default_font;
  3201. ctx.textAlign = "center";
  3202. seq = seq.data;
  3203. for (var c = 0, str_len = seq.length; c < str_len; c++) {
  3204. var c_start = Math.floor(c * w_scale);
  3205. ctx.fillText(seq[c], c_start, 10);
  3206. }
  3207. this.show_contents();
  3208. return new Tile(track, region, resolution, canvas, seq);
  3209. }
  3210. this.hide_contents();
  3211. }
  3212. });
  3213. /**
  3214. * Track displays continuous/numerical data. Track expects position data in 1-based format, i.e. wiggle format.
  3215. */
  3216. var LineTrack = function (view, container, obj_dict) {
  3217. var track = this;
  3218. this.display_modes = ["Histogram", "Line", "Filled", "Intensity"];
  3219. this.mode = "Histogram";
  3220. TiledTrack.call(this, view, container, obj_dict);
  3221. this.hda_ldda = obj_dict.hda_ldda;
  3222. this.dataset_id = obj_dict.dataset_id;
  3223. this.original_dataset_id = this.dataset_id;
  3224. this.left_offset = 0;
  3225. // Define track configuration
  3226. this.config = new DrawableConfig( {
  3227. track: this,
  3228. params: [
  3229. { key: 'name', label: 'Name', type: 'text', default_value: this.name },
  3230. { key: 'color', label: 'Color', type: 'color', default_value: get_random_color() },
  3231. { key: 'min_value', label: 'Min Value', type: 'float', default_value: undefined },
  3232. { key: 'max_value', label: 'Max Value', type: 'float', default_value: undefined },
  3233. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3234. { key: 'height', type: 'int', default_value: 32, hidden: true }
  3235. ],
  3236. saved_values: obj_dict.prefs,
  3237. onchange: function() {
  3238. track.set_name(track.prefs.name);
  3239. track.vertical_range = track.prefs.max_value - track.prefs.min_value;
  3240. track.set_min_value(track.prefs.min_value);
  3241. track.set_max_value(track.prefs.max_value);
  3242. }
  3243. });
  3244. this.prefs = this.config.values;
  3245. this.visible_height_px = this.config.values.height;
  3246. this.vertical_range = this.config.values.max_value - this.config.values.min_value;
  3247. };
  3248. extend(LineTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3249. /**
  3250. * Action to take during resize.
  3251. */
  3252. on_resize: function() {
  3253. this.request_draw(true);
  3254. },
  3255. /**
  3256. * Set track minimum value.
  3257. */
  3258. set_min_value: function(new_val) {
  3259. this.prefs.min_value = new_val;
  3260. $('#linetrack_' + this.dataset_id + '_minval').text(this.prefs.min_value);
  3261. this.tile_cache.clear();
  3262. this.request_draw();
  3263. },
  3264. /**
  3265. * Set track maximum value.
  3266. */
  3267. set_max_value: function(new_val) {
  3268. this.prefs.max_value = new_val;
  3269. $('#linetrack_' + this.dataset_id + '_maxval').text(this.prefs.max_value);
  3270. this.tile_cache.clear();
  3271. this.request_draw();
  3272. },
  3273. predraw_init: function() {
  3274. var track = this;
  3275. track.vertical_range = undefined;
  3276. return $.getJSON( track.dataset.url(),
  3277. { data_type: 'data', stats: true, chrom: track.view.chrom, low: 0,
  3278. high: track.view.max_high, hda_ldda: track.hda_ldda, dataset_id:
  3279. track.dataset_id }, function(result) {
  3280. track.container_div.addClass( "line-track" );
  3281. var data = result.data;
  3282. if ( isNaN(parseFloat(track.prefs.min_value)) || isNaN(parseFloat(track.prefs.max_value)) ) {
  3283. // Compute default minimum and maximum values
  3284. var min_value = data.min,
  3285. max_value = data.max;
  3286. // If mean and sd are present, use them to compute a ~95% window
  3287. // but only if it would shrink the range on one side
  3288. min_value = Math.floor( Math.min( 0, Math.max( min_value, data.mean - 2 * data.sd ) ) );
  3289. max_value = Math.ceil( Math.max( 0, Math.min( max_value, data.mean + 2 * data.sd ) ) );
  3290. // Update the prefs
  3291. track.prefs.min_value = min_value;
  3292. track.prefs.max_value = max_value;
  3293. // Update the config
  3294. // FIXME: we should probably only save this when the user explicately sets it
  3295. // since we lose the ability to compute it on the fly (when changing
  3296. // chromosomes for example).
  3297. $('#track_' + track.dataset_id + '_minval').val(track.prefs.min_value);
  3298. $('#track_' + track.dataset_id + '_maxval').val(track.prefs.max_value);
  3299. }
  3300. track.vertical_range = track.prefs.max_value - track.prefs.min_value;
  3301. track.total_frequency = data.total_frequency;
  3302. // Draw y-axis labels if necessary
  3303. track.container_div.find(".yaxislabel").remove();
  3304. // Add min, max labels.
  3305. var
  3306. min_label = $("<div/>").text(round(track.prefs.min_value, 3)).make_text_editable({
  3307. num_cols: 6,
  3308. on_finish: function(new_val) {
  3309. $(".bs-tooltip").remove();
  3310. var new_val = parseFloat(new_val);
  3311. if (!isNaN(new_val)) {
  3312. track.set_min_value(new_val);
  3313. }
  3314. },
  3315. help_text: "Set min value"
  3316. }).addClass('yaxislabel bottom').attr("id", 'linetrack_' + track.dataset_id + '_minval')
  3317. .prependTo(track.container_div),
  3318. max_label = $("<div/>").text(round(track.prefs.max_value, 3)).make_text_editable({
  3319. num_cols: 6,
  3320. on_finish: function(new_val) {
  3321. $(".bs-tooltip").remove();
  3322. var new_val = parseFloat(new_val);
  3323. if (!isNaN(new_val)) {
  3324. track.set_max_value(new_val);
  3325. }
  3326. },
  3327. help_text: "Set max value"
  3328. }).addClass('yaxislabel top').attr("id", 'linetrack_' + track.dataset_id + '_maxval')
  3329. .prependTo(track.container_div);
  3330. });
  3331. },
  3332. /**
  3333. * Draw LineTrack tile.
  3334. */
  3335. draw_tile: function(result, ctx, mode, resolution, region, w_scale) {
  3336. // Paint onto canvas.
  3337. var
  3338. canvas = ctx.canvas,
  3339. tile_low = region.get('start'),
  3340. tile_high = region.get('end'),
  3341. painter = new painters.LinePainter(result.data, tile_low, tile_high, this.prefs, mode);
  3342. painter.draw(ctx, canvas.width, canvas.height, w_scale);
  3343. return new Tile(this, region, resolution, canvas, result.data);
  3344. },
  3345. /**
  3346. * LineTrack data cannot currently be subsetted.
  3347. */
  3348. can_subset: function(data) {
  3349. return false;
  3350. }
  3351. });
  3352. var DiagonalHeatmapTrack = function (view, container, obj_dict) {
  3353. var track = this;
  3354. this.display_modes = ["Heatmap"];
  3355. this.mode = "Heatmap";
  3356. TiledTrack.call(this, view, container, obj_dict);
  3357. // This all seems to be duplicated
  3358. this.hda_ldda = obj_dict.hda_ldda;
  3359. this.dataset_id = obj_dict.dataset_id;
  3360. this.original_dataset_id = this.dataset_id;
  3361. this.left_offset = 0;
  3362. // Define track configuration
  3363. this.config = new DrawableConfig( {
  3364. track: this,
  3365. params: [
  3366. { key: 'name', label: 'Name', type: 'text', default_value: this.name },
  3367. { key: 'pos_color', label: 'Positive Color', type: 'color', default_value: "4169E1" },
  3368. { key: 'negative_color', label: 'Negative Color', type: 'color', default_value: "FF8C00" },
  3369. { key: 'min_value', label: 'Min Value', type: 'float', default_value: 0 },
  3370. { key: 'max_value', label: 'Max Value', type: 'float', default_value: 1 },
  3371. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3372. { key: 'height', type: 'int', default_value: 500, hidden: true }
  3373. ],
  3374. saved_values: obj_dict.prefs,
  3375. onchange: function() {
  3376. track.set_name(track.prefs.name);
  3377. track.vertical_range = track.prefs.max_value - track.prefs.min_value;
  3378. track.set_min_value(track.prefs.min_value);
  3379. track.set_max_value(track.prefs.max_value);
  3380. }
  3381. });
  3382. this.prefs = this.config.values;
  3383. this.visible_height_px = this.config.values.height;
  3384. this.vertical_range = this.config.values.max_value - this.config.values.min_value;
  3385. };
  3386. extend(DiagonalHeatmapTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3387. /**
  3388. * Action to take during resize.
  3389. */
  3390. on_resize: function() {
  3391. this.request_draw(true);
  3392. },
  3393. /**
  3394. * Set track minimum value.
  3395. */
  3396. set_min_value: function(new_val) {
  3397. this.prefs.min_value = new_val;
  3398. this.tile_cache.clear();
  3399. this.request_draw();
  3400. },
  3401. /**
  3402. * Set track maximum value.
  3403. */
  3404. set_max_value: function(new_val) {
  3405. this.prefs.max_value = new_val;
  3406. this.tile_cache.clear();
  3407. this.request_draw();
  3408. },
  3409. /**
  3410. * Draw LineTrack tile.
  3411. */
  3412. draw_tile: function(result, ctx, mode, resolution, tile_index, w_scale) {
  3413. // Paint onto canvas.
  3414. var
  3415. canvas = ctx.canvas,
  3416. tile_bounds = this._get_tile_bounds(tile_index, resolution),
  3417. tile_low = tile_bounds[0],
  3418. tile_high = tile_bounds[1],
  3419. painter = new painters.DiagonalHeatmapPainter(result.data, tile_low, tile_high, this.prefs, mode);
  3420. painter.draw(ctx, canvas.width, canvas.height, w_scale);
  3421. return new Tile(this, tile_index, resolution, canvas, result.data);
  3422. }
  3423. });
  3424. /**
  3425. * A track that displays features/regions. Track expects position data in BED format, i.e. 0-based, half-open.
  3426. */
  3427. var FeatureTrack = function(view, container, obj_dict) {
  3428. //
  3429. // Preinitialization: do things that need to be done before calling Track and TiledTrack
  3430. // initialization code.
  3431. //
  3432. var track = this;
  3433. this.display_modes = ["Auto", "Coverage", "Dense", "Squish", "Pack"];
  3434. //
  3435. // Initialization.
  3436. //
  3437. TiledTrack.call(this, view, container, obj_dict);
  3438. // Define and restore track configuration.
  3439. var
  3440. block_color = get_random_color(),
  3441. reverse_strand_color = get_random_color( [ block_color, "#ffffff" ] );
  3442. this.config = new DrawableConfig( {
  3443. track: this,
  3444. params: [
  3445. { key: 'name', label: 'Name', type: 'text', default_value: this.name },
  3446. { key: 'block_color', label: 'Block color', type: 'color', default_value: block_color },
  3447. { key: 'reverse_strand_color', label: 'Antisense strand color', type: 'color', default_value: reverse_strand_color },
  3448. { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' },
  3449. { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true,
  3450. help: 'Show the number of items in each bin when drawing summary histogram' },
  3451. { key: 'histogram_max', label: 'Histogram maximum', type: 'float', default_value: null, help: 'clear value to set automatically' },
  3452. { key: 'connector_style', label: 'Connector style', type: 'select', default_value: 'fishbones',
  3453. options: [ { label: 'Line with arrows', value: 'fishbone' }, { label: 'Arcs', value: 'arcs' } ] },
  3454. { key: 'mode', type: 'string', default_value: this.mode, hidden: true },
  3455. { key: 'height', type: 'int', default_value: this.visible_height_px, hidden: true}
  3456. ],
  3457. saved_values: obj_dict.prefs,
  3458. onchange: function() {
  3459. track.set_name(track.prefs.name);
  3460. track.tile_cache.clear();
  3461. track.set_painter_from_config();
  3462. track.request_draw();
  3463. }
  3464. });
  3465. this.prefs = this.config.values;
  3466. this.visible_height_px = this.config.values.height;
  3467. this.container_div.addClass( "feature-track" );
  3468. this.hda_ldda = obj_dict.hda_ldda;
  3469. this.dataset_id = obj_dict.dataset_id;
  3470. this.original_dataset_id = obj_dict.dataset_id;
  3471. this.show_labels_scale = 0.001;
  3472. this.showing_details = false;
  3473. this.summary_draw_height = 30;
  3474. this.slotters = {};
  3475. this.start_end_dct = {};
  3476. this.left_offset = 200;
  3477. // this.painter = painters.LinkedFeaturePainter;
  3478. this.set_painter_from_config();
  3479. };
  3480. extend(FeatureTrack.prototype, Drawable.prototype, TiledTrack.prototype, {
  3481. set_dataset: function(dataset) {
  3482. this.dataset_id = dataset.get('id');
  3483. this.hda_ldda = dataset.get('hda_ldda');
  3484. this.dataset = dataset;
  3485. this.data_manager.set('dataset', dataset);
  3486. },
  3487. set_painter_from_config: function() {
  3488. if ( this.config.values['connector_style'] === 'arcs' ) {
  3489. this.painter = painters.ArcLinkedFeaturePainter;
  3490. } else {
  3491. this.painter = painters.LinkedFeaturePainter;
  3492. }
  3493. },
  3494. /**
  3495. * Actions to be taken before drawing.
  3496. */
  3497. before_draw: function() {
  3498. // Clear because this is set when drawing.
  3499. this.max_height_px = 0;
  3500. },
  3501. /**
  3502. * Actions to be taken after draw has been completed. Draw is completed when all tiles have been
  3503. * drawn/fetched and shown.
  3504. */
  3505. postdraw_actions: function(tiles, width, w_scale, clear_after) {
  3506. TiledTrack.prototype.postdraw_actions.call(this, tiles, clear_after);
  3507. var track = this,
  3508. i;
  3509. // If mode is Coverage and tiles do not share max, redraw tiles as necessary using new max.
  3510. if (track.mode === "Coverage") {
  3511. // Get global max.
  3512. var global_max = -1;
  3513. for (i = 0; i < tiles.length; i++) {
  3514. var cur_max = tiles[i].max_val;
  3515. if (cur_max > global_max) {
  3516. global_max = cur_max;
  3517. }
  3518. }
  3519. for (i = 0; i < tiles.length; i++) {
  3520. var tile = tiles[i];
  3521. if (tile.max_val !== global_max) {
  3522. tile.html_elt.remove();
  3523. track.draw_helper(true, width, tile.index, tile.resolution, tile.html_elt.parent(), w_scale, { more_tile_data: { max: global_max } } );
  3524. }
  3525. }
  3526. }
  3527. //
  3528. // Update filter attributes, UI.
  3529. //
  3530. // Update filtering UI.
  3531. if (track.filters_manager) {
  3532. var filters = track.filters_manager.filters;
  3533. for (var f = 0; f < filters.length; f++) {
  3534. filters[f].update_ui_elt();
  3535. }
  3536. // Determine if filters are available; this is based on the tiles' data.
  3537. // Criteria for filter to be available: (a) it is applicable to tile data and (b) filter min != filter max.
  3538. var filters_available = false,
  3539. example_feature,
  3540. filter;
  3541. for (i = 0; i < tiles.length; i++) {
  3542. if (tiles[i].data.length) {
  3543. example_feature = tiles[i].data[0];
  3544. for (var f = 0; f < filters.length; f++) {
  3545. filter = filters[f];
  3546. if ( filter.applies_to(example_feature) &&
  3547. filter.min !== filter.max ) {
  3548. filters_available = true;
  3549. break;
  3550. }
  3551. }
  3552. }
  3553. }
  3554. // If filter availability changed, hide filter div if necessary and update menu.
  3555. if (track.filters_available !== filters_available) {
  3556. track.filters_available = filters_available;
  3557. if (!track.filters_available) {
  3558. track.filters_manager.hide();
  3559. }
  3560. track.update_icons();
  3561. }
  3562. }
  3563. //
  3564. // If using SummaryTree tiles, show max and make it editable.
  3565. //
  3566. this.container_div.find(".yaxislabel").remove();
  3567. var first_tile = tiles[0];
  3568. if (first_tile instanceof SummaryTreeTile) {
  3569. var max_val = (this.prefs.histogram_max ? this.prefs.histogram_max : first_tile.max_val),
  3570. max_label = $("<div/>").text(max_val).make_text_editable({
  3571. num_cols: 12,
  3572. on_finish: function(new_val) {
  3573. $(".bs-tooltip").remove();
  3574. var new_val = parseFloat(new_val);
  3575. track.prefs.histogram_max = (!isNaN(new_val) ? new_val : null);
  3576. track.tile_cache.clear();
  3577. track.request_draw();
  3578. },
  3579. help_text: "Set max value; leave blank to use default"
  3580. }).addClass('yaxislabel top').css("color", this.prefs.label_color);
  3581. this.container_div.prepend(max_label);
  3582. }
  3583. //
  3584. // If not all features slotted, show icon for showing more rows (slots).
  3585. //
  3586. if (first_tile instanceof FeatureTrackTile) {
  3587. var all_slotted = true;
  3588. for (i = 0; i < tiles.length; i++) {
  3589. if (!tiles[i].all_slotted) {
  3590. all_slotted = false;
  3591. break;
  3592. }
  3593. }
  3594. if (!all_slotted) {
  3595. this.action_icons.show_more_rows_icon.show();
  3596. }
  3597. else {
  3598. this.action_icons.show_more_rows_icon.hide();
  3599. }
  3600. }
  3601. else {
  3602. this.action_icons.show_more_rows_icon.hide();
  3603. }
  3604. },
  3605. update_auto_mode: function( mode ) {
  3606. var mode;
  3607. if ( this.mode === "Auto" ) {
  3608. if ( mode === "no_detail" ) {
  3609. mode = "feature spans";
  3610. } else if ( mode === "summary_tree" ) {
  3611. mode = "coverage histogram";
  3612. }
  3613. this.action_icons.mode_icon.attr("title", "Set display mode (now: Auto/" + mode + ")");
  3614. }
  3615. },
  3616. /**
  3617. * Place features in slots for drawing (i.e. pack features).
  3618. * this.slotters[level] is created in this method. this.slotters[level]
  3619. * is a Slotter object. Returns the number of slots used to pack features.
  3620. */
  3621. incremental_slots: function(level, features, mode) {
  3622. // Get/create incremental slots for level. If display mode changed,
  3623. // need to create new slots.
  3624. var dummy_context = this.view.canvas_manager.dummy_context,
  3625. slotter = this.slotters[level];
  3626. if (!slotter || (slotter.mode !== mode)) {
  3627. slotter = new (slotting.FeatureSlotter)( level, mode, MAX_FEATURE_DEPTH, function ( x ) { return dummy_context.measureText( x ); } );
  3628. this.slotters[level] = slotter;
  3629. }
  3630. return slotter.slot_features( features );
  3631. },
  3632. /**
  3633. * Returns appropriate display mode based on data.
  3634. */
  3635. get_mode: function(data) {
  3636. if (data.dataset_type === "summary_tree") {
  3637. mode = "summary_tree";
  3638. }
  3639. // HACK: use no_detail mode track is in overview to prevent overview from being too large.
  3640. else if (data.extra_info === "no_detail" || this.is_overview) {
  3641. mode = "no_detail";
  3642. }
  3643. else {
  3644. // Choose b/t Squish and Pack.
  3645. // Proxy measures for using Squish:
  3646. // (a) error message re: limiting number of features shown;
  3647. // (b) X number of features shown;
  3648. // (c) size of view shown.
  3649. // TODO: cannot use (a) and (b) because it requires coordinating mode across tiles;
  3650. // fix this so that tiles are redrawn as necessary to use the same mode.
  3651. //if ( (result.message && result.message.match(/^Only the first [\d]+/)) ||
  3652. // (result.data && result.data.length > 2000) ||
  3653. //var data = result.data;
  3654. // if ( (data.length && data.length < 4) ||
  3655. // (this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH) ) {
  3656. if ( this.view.high - this.view.low > MIN_SQUISH_VIEW_WIDTH ) {
  3657. mode = "Squish";
  3658. } else {
  3659. mode = "Pack";
  3660. }
  3661. }
  3662. return mode;
  3663. },
  3664. /**
  3665. * Returns canvas height needed to display data; return value is an integer that denotes the
  3666. * number of pixels required.
  3667. */
  3668. get_canvas_height: function(result, mode, w_scale, canvas_width) {
  3669. if (mode === "summary_tree" || mode === "Coverage") {
  3670. return this.summary_draw_height;
  3671. }
  3672. else {
  3673. // All other modes require slotting.
  3674. var rows_required = this.incremental_slots(w_scale, result.data, mode);
  3675. // HACK: use dummy painter to get required height. Painter should be extended so that get_required_height
  3676. // works as a static function.
  3677. var dummy_painter = new (this.painter)(null, null, null, this.prefs, mode);
  3678. return Math.max(MIN_TRACK_HEIGHT, dummy_painter.get_required_height(rows_required, canvas_width) );
  3679. }
  3680. },
  3681. /**
  3682. * Draw FeatureTrack tile.
  3683. * @param result result from server
  3684. * @param cxt canvas context to draw on
  3685. * @param mode mode to draw in
  3686. * @param resolution view resolution
  3687. * @param region region to draw on tile
  3688. * @param w_scale pixels per base
  3689. * @param ref_seq reference sequence data
  3690. */
  3691. draw_tile: function(result, ctx, mode, resolution, region, w_scale, ref_seq) {
  3692. var track = this,
  3693. canvas = ctx.canvas,
  3694. tile_low = region.get('start'),
  3695. tile_high = region.get('end'),
  3696. left_offset = this.left_offset;
  3697. // Drawing the summary tree.
  3698. if (mode === "summary_tree" || mode === "Coverage") {
  3699. // Paint summary tree into canvas
  3700. var painter = new painters.SummaryTreePainter(result, tile_low, tile_high, this.prefs);
  3701. painter.draw(ctx, canvas.width, canvas.height, w_scale);
  3702. return new SummaryTreeTile(track, region, resolution, canvas, result.data, result.max);
  3703. }
  3704. // Handle row-by-row tracks
  3705. // Preprocessing: filter features and determine whether all unfiltered features have been slotted.
  3706. var
  3707. filtered = [],
  3708. slots = this.slotters[w_scale].slots;
  3709. all_slotted = true;
  3710. if ( result.data ) {
  3711. var filters = this.filters_manager.filters;
  3712. for (var i = 0, len = result.data.length; i < len; i++) {
  3713. var feature = result.data[i];
  3714. var hide_feature = false;
  3715. var filter;
  3716. for (var f = 0, flen = filters.length; f < flen; f++) {
  3717. filter = filters[f];
  3718. filter.update_attrs(feature);
  3719. if (!filter.keep(feature)) {
  3720. hide_feature = true;
  3721. break;
  3722. }
  3723. }
  3724. if (!hide_feature) {
  3725. // Feature visible.
  3726. filtered.push(feature);
  3727. // Set flag if not slotted.
  3728. if ( !(feature[0] in slots) ) {
  3729. all_slotted = false;
  3730. }
  3731. }
  3732. }
  3733. }
  3734. // Create painter.
  3735. var filter_alpha_scaler = (this.filters_manager.alpha_filter ? new FilterScaler(this.filters_manager.alpha_filter) : null);
  3736. var filter_height_scaler = (this.filters_manager.height_filter ? new FilterScaler(this.filters_manager.height_filter) : null);
  3737. // HACK: ref_seq will only be defined for ReadTracks, and only the ReadPainter accepts that argument
  3738. var painter = new (this.painter)(filtered, tile_low, tile_high, this.prefs, mode, filter_alpha_scaler, filter_height_scaler, ref_seq);
  3739. var feature_mapper = null;
  3740. // console.log(( tile_low - this.view.low ) * w_scale, tile_index, w_scale);
  3741. ctx.fillStyle = this.prefs.block_color;
  3742. ctx.font = ctx.canvas.manager.default_font;
  3743. ctx.textAlign = "right";
  3744. if (result.data) {
  3745. // Draw features.
  3746. feature_mapper = painter.draw(ctx, canvas.width, canvas.height, w_scale, slots);
  3747. feature_mapper.translation = -left_offset;
  3748. }
  3749. return new FeatureTrackTile(track, region, resolution, canvas, result.data, w_scale, mode, result.message, all_slotted, feature_mapper);
  3750. },
  3751. /**
  3752. * Returns true if data is compatible with a given mode.
  3753. */
  3754. data_and_mode_compatible: function(data, mode) {
  3755. // Only handle modes that user can set.
  3756. if (mode === "Auto") {
  3757. return true;
  3758. }
  3759. // Histogram mode requires summary_tree data.
  3760. else if (mode === "Coverage") {
  3761. return data.dataset_type === "summary_tree";
  3762. }
  3763. // All other modes--Dense, Squish, Pack--require data + details.
  3764. else if (data.extra_info === "no_detail" || data.dataset_type === "summary_tree") {
  3765. return false;
  3766. }
  3767. else {
  3768. return true;
  3769. }
  3770. },
  3771. /**
  3772. * Returns true if data can be subsetted.
  3773. */
  3774. can_subset: function(data) {
  3775. // Do not subset summary tree data, entries with a message, or data with no detail.
  3776. if (data.dataset_type === "summary_tree" || data.message || data.extra_info === "no_detail") {
  3777. return false;
  3778. }
  3779. return true;
  3780. }
  3781. });
  3782. var VcfTrack = function(view, container, obj_dict) {
  3783. FeatureTrack.call(this, view, container, obj_dict);
  3784. this.config = new DrawableConfig( {
  3785. track: this,
  3786. params: [
  3787. { key: 'name', label: 'Name', type: 'text', default_value: this.name },
  3788. { key: 'block_color', label: 'Block color', type: 'color', default_value: get_random_color() },
  3789. { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' },
  3790. { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false },
  3791. { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true },
  3792. { key: 'mode', type: 'string', default_value: this.mode, hidden: true }
  3793. ],
  3794. saved_values: obj_dict.prefs,
  3795. onchange: function() {
  3796. this.track.set_name(this.track.prefs.name);
  3797. this.track.tile_cache.clear();
  3798. this.track.request_draw();
  3799. }
  3800. });
  3801. this.prefs = this.config.values;
  3802. this.painter = painters.ReadPainter;
  3803. };
  3804. extend(VcfTrack.prototype, Drawable.prototype, TiledTrack.prototype, FeatureTrack.prototype);
  3805. /**
  3806. * Track that displays mapped reads. Track expects position data in 1-based, closed format, i.e. SAM/BAM format.
  3807. */
  3808. var ReadTrack = function (view, container, obj_dict) {
  3809. FeatureTrack.call(this, view, container, obj_dict);
  3810. var
  3811. block_color = get_random_color(),
  3812. reverse_strand_color = get_random_color( [ block_color, "#ffffff" ] );
  3813. this.config = new DrawableConfig( {
  3814. track: this,
  3815. params: [
  3816. { key: 'name', label: 'Name', type: 'text', default_value: this.name },
  3817. { key: 'block_color', label: 'Block and sense strand color', type: 'color', default_value: block_color },
  3818. { key: 'reverse_strand_color', label: 'Antisense strand color', type: 'color', default_value: reverse_strand_color },
  3819. { key: 'label_color', label: 'Label color', type: 'color', default_value: 'black' },
  3820. { key: 'show_insertions', label: 'Show insertions', type: 'bool', default_value: false },
  3821. { key: 'show_differences', label: 'Show differences only', type: 'bool', default_value: true },
  3822. { key: 'show_counts', label: 'Show summary counts', type: 'bool', default_value: true },
  3823. { key: 'histogram_max', label: 'Histogram maximum', type: 'float', default_value: null, help: 'Clear value to set automatically' },
  3824. { key: 'mode', type: 'string', default_value: this.mode, hidden: true }
  3825. ],
  3826. saved_values: obj_dict.prefs,
  3827. onchange: function() {
  3828. this.track.set_name(this.track.prefs.name);
  3829. this.track.tile_cache.clear();
  3830. this.track.request_draw();
  3831. }
  3832. });
  3833. this.prefs = this.config.values;
  3834. this.painter = painters.ReadPainter;
  3835. this.update_icons();
  3836. };
  3837. extend(ReadTrack.prototype, Drawable.prototype, TiledTrack.prototype, FeatureTrack.prototype);
  3838. /**
  3839. * Objects that can be added to a view.
  3840. */
  3841. var addable_objects = {
  3842. "LineTrack": LineTrack,
  3843. "FeatureTrack": FeatureTrack,
  3844. "VcfTrack": VcfTrack,
  3845. "ReadTrack": ReadTrack,
  3846. // "DiagonalHeatmapTrack": DiagonalHeatmapTrack,
  3847. "CompositeTrack": CompositeTrack,
  3848. "DrawableGroup": DrawableGroup
  3849. };
  3850. /**
  3851. * Create new object from a template. A template can be either an object dictionary or an
  3852. * object itself.
  3853. */
  3854. var object_from_template = function(template, view, container) {
  3855. if ('copy' in template) {
  3856. // Template is an object.
  3857. return template.copy(container);
  3858. }
  3859. else {
  3860. // Template is a dictionary.
  3861. var
  3862. drawable_type = template['obj_type'];
  3863. // For backward compatibility:
  3864. if (!drawable_type) {
  3865. drawable_type = template['track_type'];
  3866. }
  3867. return new addable_objects[ drawable_type ](view, container, template);
  3868. }
  3869. };
  3870. return {
  3871. View: View,
  3872. DrawableGroup: DrawableGroup,
  3873. LineTrack: LineTrack,
  3874. FeatureTrack: FeatureTrack,
  3875. DiagonalHeatmapTrack: DiagonalHeatmapTrack,
  3876. ReadTrack: ReadTrack,
  3877. VcfTrack: VcfTrack,
  3878. CompositeTrack: CompositeTrack,
  3879. object_from_template: object_from_template
  3880. };
  3881. // End trackster_module encapsulation
  3882. });