PageRenderTime 42ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

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

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