PageRenderTime 62ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/static/scripts/trackster.js

https://bitbucket.org/cistrome/cistrome-harvard/
JavaScript | 1716 lines | 1167 code | 116 blank | 433 comment | 156 complexity | 4ba25aa395361e2eecd6279cebcda7ab MD5 | raw file
  1. /* Trackster
  2. 2010-2011: James Taylor, Kanwei Li, Jeremy Goecks
  3. */
  4. var class_module = function(require, exports) {
  5. // Module is a placeholder for a more complete inheritence approach
  6. /** Simple extend function for inheritence */
  7. var extend = function() {
  8. var target = arguments[0];
  9. for ( var i = 1; i < arguments.length; i++ ) {
  10. var other = arguments[i];
  11. for ( key in other ) {
  12. target[key] = other[key];
  13. }
  14. }
  15. return target;
  16. };
  17. exports.extend = extend;
  18. // end class_module encapsulation
  19. };
  20. /**
  21. * Find browser's requestAnimationFrame method or fallback on a setTimeout
  22. */
  23. var requestAnimationFrame = (function(){
  24. return window.requestAnimationFrame ||
  25. window.webkitRequestAnimationFrame ||
  26. window.mozRequestAnimationFrame ||
  27. window.oRequestAnimationFrame ||
  28. window.msRequestAnimationFrame ||
  29. function( callback, element ) {
  30. window.setTimeout(callback, 1000 / 60);
  31. };
  32. })();
  33. /**
  34. * Compute the type of overlap between two regions. They are assumed to be on the same chrom/contig.
  35. * The overlap is computed relative to the second region; hence, OVERLAP_START indicates that the first
  36. * region overlaps the start (but not the end) of the second region.
  37. */
  38. var BEFORE = 1001, CONTAINS = 1002, OVERLAP_START = 1003, OVERLAP_END = 1004, CONTAINED_BY = 1005, AFTER = 1006;
  39. var compute_overlap = function(first_region, second_region) {
  40. var
  41. first_start = first_region[0], first_end = first_region[1],
  42. second_start = second_region[0], second_end = second_region[1],
  43. overlap;
  44. if (first_start < second_start) {
  45. if (first_end < second_start) {
  46. overlap = BEFORE;
  47. }
  48. else if (first_end <= second_end) {
  49. overlap = OVERLAP_START;
  50. }
  51. else { // first_end > second_end
  52. overlap = CONTAINS;
  53. }
  54. }
  55. else { // first_start >= second_start
  56. if (first_start > second_end) {
  57. overlap = AFTER;
  58. }
  59. else if (first_end <= second_end) {
  60. overlap = CONTAINED_BY;
  61. }
  62. else {
  63. overlap = OVERLAP_END;
  64. }
  65. }
  66. return overlap;
  67. };
  68. /**
  69. * Returns true if regions overlap.
  70. */
  71. var is_overlap = function(first_region, second_region) {
  72. var overlap = compute_overlap(first_region, second_region);
  73. return (overlap !== BEFORE && overlap !== AFTER);
  74. };
  75. // Encapsulate -- anything to be availabe outside this block is added to exports
  76. var trackster_module = function(require, exports) {
  77. var extend = require('class').extend,
  78. slotting = require('slotting'),
  79. painters = require('painters');
  80. // ---- Canvas management and extensions ----
  81. /**
  82. * Canvas manager is used to create canvases, for browsers, this deals with
  83. * backward comparibility using excanvas, as well as providing a pattern cache
  84. */
  85. var CanvasManager = function( document, default_font ) {
  86. this.document = document;
  87. this.default_font = default_font !== undefined ? default_font : "9px Monaco, Lucida Console, monospace";
  88. this.dummy_canvas = this.new_canvas();
  89. this.dummy_context = this.dummy_canvas.getContext('2d');
  90. this.dummy_context.font = this.default_font;
  91. this.char_width_px = this.dummy_context.measureText("A").width;
  92. this.patterns = {};
  93. // FIXME: move somewhere to make this more general
  94. this.load_pattern( 'right_strand', "/visualization/strand_right.png" );
  95. this.load_pattern( 'left_strand', "/visualization/strand_left.png" );
  96. this.load_pattern( 'right_strand_inv', "/visualization/strand_right_inv.png" );
  97. this.load_pattern( 'left_strand_inv', "/visualization/strand_left_inv.png" );
  98. }
  99. extend( CanvasManager.prototype, {
  100. load_pattern: function( key, path ) {
  101. var patterns = this.patterns,
  102. dummy_context = this.dummy_context,
  103. image = new Image();
  104. // FIXME: where does image_path come from? not in browser.mako...
  105. image.src = image_path + path;
  106. image.onload = function() {
  107. patterns[key] = dummy_context.createPattern( image, "repeat" );
  108. }
  109. },
  110. get_pattern: function( key ) {
  111. return this.patterns[key];
  112. },
  113. new_canvas: function() {
  114. var canvas = this.document.createElement("canvas");
  115. // If using excanvas in IE, we need to explicately attach the canvas
  116. // methods to the DOM element
  117. if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); }
  118. // Keep a reference back to the manager
  119. canvas.manager = this;
  120. return canvas;
  121. }
  122. });
  123. // ---- Web UI specific utilities ----
  124. /**
  125. * Dictionary of HTML element-JavaScript object relationships.
  126. */
  127. // TODO: probably should separate moveable objects from containers.
  128. var html_elt_js_obj_dict = {};
  129. /**
  130. * Designates an HTML as a container.
  131. */
  132. var is_container = function(element, obj) {
  133. html_elt_js_obj_dict[element.attr("id")] = obj;
  134. };
  135. /**
  136. * Make `element` moveable within parent and sibling elements by dragging `handle` (a selector).
  137. * Function manages JS objects, containers as well.
  138. *
  139. * @param element HTML element to make moveable
  140. * @param handle_class classname that denotes HTML element to be used as handle
  141. * @param container_selector selector used to identify possible containers for this element
  142. * @param element_js_obj JavaScript object associated with element; used
  143. */
  144. var moveable = function(element, handle_class, container_selector, element_js_obj) {
  145. // HACK: set default value for container selector.
  146. container_selector = ".group";
  147. var css_border_props = {};
  148. // Register element with its object.
  149. html_elt_js_obj_dict[element.attr("id")] = element_js_obj;
  150. // Need to provide selector for handle, not class.
  151. element.bind( "drag", { handle: "." + handle_class, relative: true }, function ( e, d ) {
  152. var element = $(this);
  153. var
  154. parent = $(this).parent(),
  155. children = parent.children(),
  156. this_obj = html_elt_js_obj_dict[$(this).attr("id")],
  157. child,
  158. container,
  159. top,
  160. bottom,
  161. i;
  162. //
  163. // Enable three types of dragging: (a) out of container; (b) into container;
  164. // (c) sibling movement, aka sorting. Handle in this order for simplicity.
  165. //
  166. // Handle dragging out of container.
  167. container = $(this).parents(container_selector);
  168. if (container.length !== 0) {
  169. top = container.position().top;
  170. bottom = top + container.outerHeight();
  171. if (d.offsetY < top) {
  172. // Moving above container.
  173. $(this).insertBefore(container);
  174. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  175. cur_container.remove_drawable(this_obj);
  176. cur_container.container.add_drawable_before(this_obj, cur_container);
  177. return;
  178. }
  179. else if (d.offsetY > bottom) {
  180. // Moving below container.
  181. $(this).insertAfter(container);
  182. var cur_container = html_elt_js_obj_dict[container.attr("id")];
  183. cur_container.remove_drawable(this_obj);
  184. cur_container.container.add_drawable(this_obj);
  185. return;
  186. }
  187. }
  188. // Handle dragging into container. Child is appended to container's content_div.
  189. container = null;
  190. for ( i = 0; i < children.length; i++ ) {
  191. child = $(children.get(i));
  192. top = child.position().top;
  193. bottom = top + child.outerHeight();
  194. // Dragging into container if child is a container and offset is inside container.
  195. if ( child.is(container_selector) && this !== child.get(0) &&
  196. d.offsetY >= top && d.offsetY <= bottom ) {
  197. // Append/prepend based on where offsetY is closest to and return.
  198. if (d.offsetY - top < bottom - d.offsetY) {
  199. child.find(".content-div").prepend(this);
  200. }
  201. else {
  202. child.find(".content-div").append(this);
  203. }
  204. // Update containers. Object may not have container if it is being moved quickly.
  205. if (this_obj.container) {
  206. this_obj.container.remove_drawable(this_obj);
  207. }
  208. html_elt_js_obj_dict[child.attr("id")].add_drawable(this_obj);
  209. return;
  210. }
  211. }
  212. // Handle sibling movement, aka sorting.
  213. // Determine new position
  214. for ( i = 0; i < children.length; i++ ) {
  215. if ( d.offsetY < $(children.get(i)).position().top ) {
  216. break;
  217. }
  218. }
  219. // If not already in the right place, move. Need
  220. // to handle the end specially since we don't have
  221. // insert at index
  222. if ( i === children.length ) {
  223. if ( this !== children.get(i - 1) ) {
  224. parent.append(this);
  225. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, i);
  226. }
  227. }
  228. else if ( this !== children.get(i) ) {
  229. $(this).insertBefore( children.get(i) );
  230. // Need to adjust insert position if moving down because move is changing
  231. // indices of all list items.
  232. html_elt_js_obj_dict[parent.attr("id")].move_drawable(this_obj, (d.deltaY > 0 ? i-1 : i) );
  233. }
  234. }).bind("dragstart", function() {
  235. css_border_props["border-top"] = element.css("border-top");
  236. css_border_props["border-bottom"] = element.css("border-bottom");
  237. $(this).css({
  238. "border-top": "1px solid blue",
  239. "border-bottom": "1px solid blue"
  240. });
  241. }).bind("dragend", function() {
  242. $(this).css(css_border_props);
  243. });
  244. };
  245. // TODO: do we need to export?
  246. exports.moveable = moveable;
  247. /**
  248. * Init constants & functions used throughout trackster.
  249. */
  250. var
  251. // Minimum height of a track's contents; this must correspond to the .track-content's minimum height.
  252. MIN_TRACK_HEIGHT = 16,
  253. // FIXME: font size may not be static
  254. CHAR_HEIGHT_PX = 9,
  255. // Padding at the top of tracks for error messages
  256. ERROR_PADDING = 20,
  257. SUMMARY_TREE_TOP_PADDING = CHAR_HEIGHT_PX + 2,
  258. // Maximum number of rows un a slotted track
  259. MAX_FEATURE_DEPTH = 100,
  260. // Minimum width for window for squish to be used.
  261. MIN_SQUISH_VIEW_WIDTH = 12000,
  262. // Other constants.
  263. DENSITY = 200,
  264. RESOLUTION = 5,
  265. FEATURE_LEVELS = 10,
  266. DEFAULT_DATA_QUERY_WAIT = 5000,
  267. // Maximum number of chromosomes that are selectable at any one time.
  268. MAX_CHROMS_SELECTABLE = 100,
  269. DATA_ERROR = "There was an error in indexing this dataset. ",
  270. DATA_NOCONVERTER = "A converter for this dataset is not installed. Please check your datatypes_conf.xml file.",
  271. DATA_NONE = "No data for this chrom/contig.",
  272. DATA_PENDING = "Currently indexing... please wait",
  273. DATA_CANNOT_RUN_TOOL = "Tool cannot be rerun: ",
  274. DATA_LOADING = "Loading data...",
  275. DATA_OK = "Ready for display",
  276. CACHED_TILES_FEATURE = 10,
  277. CACHED_TILES_LINE = 5,
  278. CACHED_DATA = 5;
  279. function round_1000(num) {
  280. return Math.round(num * 1000) / 1000;
  281. };
  282. /**
  283. * Generic cache that handles key/value pairs.
  284. */
  285. var Cache = function( num_elements ) {
  286. this.num_elements = num_elements;
  287. this.clear();
  288. };
  289. extend(Cache.prototype, {
  290. get: function(key) {
  291. var index = this.key_ary.indexOf(key);
  292. if (index !== -1) {
  293. if (this.obj_cache[key].stale) {
  294. // Object is stale, so remove key and object.
  295. this.key_ary.splice(index, 1);
  296. delete this.obj_cache[key];
  297. }
  298. else {
  299. this.move_key_to_end(key, index);
  300. }
  301. }
  302. return this.obj_cache[key];
  303. },
  304. set: function(key, value) {
  305. if (!this.obj_cache[key]) {
  306. if (this.key_ary.length >= this.num_elements) {
  307. // Remove first element
  308. var deleted_key = this.key_ary.shift();
  309. delete this.obj_cache[deleted_key];
  310. }
  311. this.key_ary.push(key);
  312. }
  313. this.obj_cache[key] = value;
  314. return value;
  315. },
  316. // Move key to end of cache. Keys are removed from the front, so moving a key to the end
  317. // delays the key's removal.
  318. move_key_to_end: function(key, index) {
  319. this.key_ary.splice(index, 1);
  320. this.key_ary.push(key);
  321. },
  322. clear: function() {
  323. this.obj_cache = {};
  324. this.key_ary = [];
  325. },
  326. // Returns the number of elements in the cache.
  327. size: function() {
  328. return this.key_ary.length;
  329. }
  330. });
  331. /**
  332. * Data manager for a track.
  333. */
  334. var DataManager = function(num_elements, track, subset) {
  335. Cache.call(this, num_elements);
  336. this.track = track;
  337. this.subset = (subset !== undefined ? subset : true);
  338. };
  339. extend(DataManager.prototype, Cache.prototype, {
  340. /**
  341. * Load data from server; returns AJAX object so that use of Deferred is possible.
  342. */
  343. load_data: function(low, high, mode, resolution, extra_params) {
  344. // Setup data request params.
  345. var
  346. chrom = this.track.view.chrom,
  347. params = {"chrom": chrom, "low": low, "high": high, "mode": mode,
  348. "resolution": resolution, "dataset_id" : this.track.dataset_id,
  349. "hda_ldda": this.track.hda_ldda};
  350. $.extend(params, extra_params);
  351. // Add track filters to params.
  352. if (this.track.filters_manager) {
  353. var filter_names = [];
  354. var filters = this.track.filters_manager.filters;
  355. for (var i = 0; i < filters.length; i++) {
  356. filter_names[filter_names.length] = filters[i].name;
  357. }
  358. params.filter_cols = JSON.stringify(filter_names);
  359. }
  360. // Do request.
  361. var manager = this;
  362. return $.getJSON(this.track.data_url, params, function (result) {
  363. manager.set_data(low, high, mode, result);
  364. });
  365. },
  366. /**
  367. * Get track data.
  368. */
  369. get_data: function(low, high, mode, resolution, extra_params) {
  370. // Debugging:
  371. //console.log("get_data", low, high, mode);
  372. /*
  373. console.log("cache contents:")
  374. for (var i = 0; i < this.key_ary.length; i++) {
  375. console.log("\t", this.key_ary[i], this.obj_cache[this.key_ary[i]]);
  376. }
  377. */
  378. // Look for entry and return if found.
  379. var entry = this.get_data_from_cache(low, high, mode);
  380. if (entry) { return entry; }
  381. //
  382. // If data supports subsetting:
  383. // Look in cache for data that can be used. Data can be reused if it
  384. // has the requested data and is not summary tree and has details.
  385. // TODO: this logic could be improved if the visualization knew whether
  386. // the data was "index" or "data." Also could slice the data so that
  387. // only data points in request are returned.
  388. //
  389. /* Disabling for now, more detailed data is never loaded for line tracks
  390. TODO: can using resolution in the key solve this problem?
  391. if (this.subset) {
  392. var key, split_key, entry_low, entry_high, mode, entry;
  393. for (var i = 0; i < this.key_ary.length; i++) {
  394. key = this.key_ary[i];
  395. split_key = this.split_key(key);
  396. entry_low = split_key[0];
  397. entry_high = split_key[1];
  398. if (low >= entry_low && high <= entry_high) {
  399. // This track has the range of data needed; check other attributes.
  400. entry = this.obj_cache[key];
  401. if (entry.dataset_type !== "summary_tree" && entry.extra_info !== "no_detail") {
  402. // Data is usable.
  403. this.move_key_to_end(key, i);
  404. return entry;
  405. }
  406. }
  407. }
  408. }
  409. */
  410. // Load data from server. The deferred is immediately saved until the
  411. // data is ready, it then replaces itself with the actual data
  412. entry = this.load_data(low, high, mode, resolution, extra_params);
  413. this.set_data(low, high, mode, entry);
  414. return entry
  415. },
  416. /** "Deep" data request; used as a parameter for DataManager.get_more_data() */
  417. DEEP_DATA_REQ: "deep",
  418. /** "Broad" data request; used as a parameter for DataManager.get_more_data() */
  419. BROAD_DATA_REQ: "breadth",
  420. /**
  421. * Gets more data for a region using either a depth-first or a breadth-first approach.
  422. */
  423. get_more_data: function(low, high, mode, resolution, extra_params, req_type) {
  424. //
  425. // Get current data from cache and mark as stale.
  426. //
  427. var cur_data = this.get_data_from_cache(low, high, mode);
  428. if (!cur_data) {
  429. console.log("ERROR: no current data for: ", this.track, low, high, mode, resolution, extra_params);
  430. return;
  431. }
  432. cur_data.stale = true;
  433. //
  434. // Set parameters based on request type.
  435. //
  436. var query_low = low;
  437. if (req_type === this.DEEP_DATA_REQ) {
  438. // Use same interval but set start_val to skip data that's already in cur_data.
  439. $.extend(extra_params, {start_val: cur_data.data.length + 1});
  440. }
  441. else if (req_type === this.BROAD_DATA_REQ) {
  442. // To get past an area of extreme feature depth, set query low to be after either
  443. // (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
  444. query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
  445. }
  446. //
  447. // Get additional data, append to current data, and set new data. Use a custom deferred object
  448. // to signal when new data is available.
  449. //
  450. var
  451. data_manager = this,
  452. new_data_request = this.load_data(query_low, high, mode, resolution, extra_params)
  453. new_data_available = $.Deferred();
  454. // load_data sets cache to new_data_request, but use custom deferred object so that signal and data
  455. // is all data, not just new data.
  456. this.set_data(low, high, mode, new_data_available);
  457. $.when(new_data_request).then(function(result) {
  458. // Update data and message.
  459. if (result.data) {
  460. result.data = cur_data.data.concat(result.data);
  461. if (result.max_low) {
  462. result.max_low = cur_data.max_low;
  463. }
  464. if (result.message) {
  465. // HACK: replace number in message with current data length. Works but is ugly.
  466. result.message = result.message.replace(/[0-9]+/, result.data.length);
  467. }
  468. }
  469. data_manager.set_data(low, high, mode, result);
  470. new_data_available.resolve(result);
  471. });
  472. return new_data_available;
  473. },
  474. /**
  475. * Gets data from the cache.
  476. */
  477. get_data_from_cache: function(low, high, mode) {
  478. return this.get(this.gen_key(low, high, mode));
  479. },
  480. /**
  481. * Sets data in the cache.
  482. */
  483. set_data: function(low, high, mode, result) {
  484. //console.log("set_data", low, high, mode, result);
  485. return this.set(this.gen_key(low, high, mode), result);
  486. },
  487. /**
  488. * Generate key for cache.
  489. */
  490. // TODO: use chrom in key so that (a) data is not thrown away when changing chroms and (b)
  491. // manager does not need to be cleared when changing chroms.
  492. // TODO: use resolution in key b/c summary tree data is dependent on resolution -- is this
  493. // necessary, i.e. will resolution change but not low/high/mode?
  494. gen_key: function(low, high, mode) {
  495. var key = low + "_" + high + "_" + mode;
  496. return key;
  497. },
  498. /**
  499. * Split key from cache into array with format [low, high, mode]
  500. */
  501. split_key: function(key) {
  502. return key.split("_");
  503. }
  504. });
  505. var ReferenceTrackDataManager = function(num_elements, track, subset) {
  506. DataManager.call(this, num_elements, track, subset);
  507. };
  508. extend(ReferenceTrackDataManager.prototype, DataManager.prototype, Cache.prototype, {
  509. load_data: function(chrom, low, high, mode, resolution, extra_params) {
  510. if (resolution > 1) {
  511. // Now that data is pre-fetched before draw, we don't load reference tracks
  512. // unless it's at the bottom level
  513. return;
  514. }
  515. return DataManager.prototype.load_data.call(this, chrom, low, high, mode, resolution, extra_params);
  516. }
  517. });
  518. /**
  519. * Drawables hierarchy:
  520. *
  521. * Drawable
  522. * --> DrawableCollection
  523. * --> DrawableGroup
  524. * --> View
  525. * --> Track
  526. */
  527. /**
  528. * Base interface for all drawable objects.
  529. */
  530. var Drawable = function(name, view, prefs, parent_element, drag_handle_class, container) {
  531. this.name = name;
  532. this.view = view;
  533. this.parent_element = parent_element;
  534. this.drag_handle_class = drag_handle_class;
  535. this.container = container;
  536. this.config = new DrawableConfig({
  537. track: this,
  538. params: [
  539. { key: 'name', label: 'Name', type: 'text', default_value: name }
  540. ],
  541. saved_values: prefs,
  542. onchange: function() {
  543. this.track.set_name(this.track.config.values.name);
  544. }
  545. });
  546. this.prefs = this.config.values;
  547. };
  548. extend(Drawable.prototype, {
  549. init: function() {},
  550. request_draw: function() {},
  551. _draw: function() {},
  552. to_json: function() {},
  553. make_name_popup_menu: function() {},
  554. /**
  555. * Set drawable name.
  556. */
  557. set_name: function(new_name) {
  558. this.old_name = this.name;
  559. this.name = new_name;
  560. this.name_div.text(this.name);
  561. },
  562. /**
  563. * Revert track name; currently name can be reverted only once.
  564. */
  565. revert_name: function() {
  566. this.name = this.old_name;
  567. this.name_div.text(this.name);
  568. },
  569. /**
  570. * Remove drawable (a) from its container and (b) from the HTML.
  571. */
  572. remove: function() {
  573. this.container.remove_drawable(this);
  574. this.container_div.fadeOut('slow', function() {
  575. $(this).remove();
  576. // HACK: is there a better way to update the view?
  577. view.update_intro_div();
  578. view.has_changes = true;
  579. });
  580. }
  581. });
  582. /**
  583. * A collection of drawable objects.
  584. */
  585. var DrawableCollection = function(obj_type, name, view, prefs, parent_element, drag_handle_class, container) {
  586. Drawable.call(this, name, view, prefs, parent_element, drag_handle_class, container);
  587. // Attribute init.
  588. this.obj_type = obj_type;
  589. this.drawables = [];
  590. };
  591. extend(DrawableCollection.prototype, Drawable.prototype, {
  592. /**
  593. * Init each drawable in the collection.
  594. */
  595. init: function() {
  596. for (var i = 0; i < this.drawables.length; i++) {
  597. this.drawables[i].init();
  598. }
  599. },
  600. /**
  601. * Draw each drawable in the collection.
  602. */
  603. _draw: function() {
  604. for (var i = 0; i < this.drawables.length; i++) {
  605. this.drawables[i]._draw();
  606. }
  607. },
  608. /**
  609. * Returns jsonified representation of collection.
  610. */
  611. to_json: function() {
  612. var jsonified_drawables = [];
  613. for (var i = 0; i < this.drawables.length; i++) {
  614. jsonified_drawables.push(this.drawables[i].to_json());
  615. }
  616. return {
  617. name: this.name,
  618. prefs: this.prefs,
  619. obj_type: this.obj_type,
  620. drawables: jsonified_drawables
  621. };
  622. },
  623. /**
  624. * Add a drawable to the end of the collection.
  625. */
  626. add_drawable: function(drawable) {
  627. this.drawables.push(drawable);
  628. drawable.container = this;
  629. },
  630. /**
  631. * Add a drawable before another drawable.
  632. */
  633. add_drawable_before: function(drawable, other) {
  634. var index = this.drawables.indexOf(other);
  635. if (index != -1) {
  636. this.drawables.splice(index, 0, drawable);
  637. return true;
  638. }
  639. return false;
  640. },
  641. /**
  642. * Remove drawable from this collection.
  643. */
  644. remove_drawable: function(drawable) {
  645. var index = this.drawables.indexOf(drawable);
  646. if (index != -1) {
  647. // Found drawable to remove.
  648. this.drawables.splice(index, 1);
  649. drawable.container = null;
  650. return true;
  651. }
  652. return false;
  653. },
  654. /**
  655. * Move drawable to another location in collection.
  656. */
  657. move_drawable: function(drawable, new_position) {
  658. var index = this.drawables.indexOf(drawable);
  659. if (index != -1) {
  660. // Remove from current position:
  661. this.drawables.splice(index, 1);
  662. // insert into new position:
  663. this.drawables.splice(new_position, 0, drawable);
  664. return true;
  665. }
  666. return false;
  667. }
  668. });
  669. /**
  670. * A group of drawables that are moveable, visible.
  671. */
  672. var DrawableGroup = function(name, view, prefs, parent_element, container) {
  673. DrawableCollection.call(this, "DrawableGroup", name, view, prefs, parent_element, "group-handle", container);
  674. // HTML elements.
  675. if (!DrawableGroup.id_counter) { DrawableGroup.id_counter = 0; }
  676. var group_id = DrawableGroup.id_counter++
  677. this.container_div = $("<div/>").addClass("group").attr("id", "group_" + group_id).appendTo(this.parent_element);
  678. this.header_div = $("<div/>").addClass("track-header").appendTo(this.container_div);
  679. this.header_div.append($("<div/>").addClass(this.drag_handle_class));
  680. this.name_div = $("<div/>").addClass("group-name menubutton popup").text(this.name).appendTo(this.header_div);
  681. this.content_div = $("<div/>").addClass("content-div").attr("id", "group_" + group_id + "_content_div").appendTo(this.container_div);
  682. // Set up containers/moving for group: register both container and content div as container
  683. // because both are used as containers. Group can be moved.
  684. is_container(this.container_div, this);
  685. is_container(this.content_div, this);
  686. moveable(this.container_div, this.drag_handle_class, ".group", this);
  687. this.make_name_popup_menu();
  688. };
  689. extend(DrawableGroup.prototype, Drawable.prototype, DrawableCollection.prototype, {
  690. /**
  691. * Make popup menu for group.
  692. */
  693. make_name_popup_menu: function() {
  694. var group = this;
  695. var group_dropdown = {};
  696. //
  697. // Edit config option.
  698. //
  699. group_dropdown["Edit configuration"] = function() {
  700. var cancel_fn = function() { hide_modal(); $(window).unbind("keypress.check_enter_esc"); },
  701. ok_fn = function() {
  702. group.config.update_from_form( $(".dialog-box") );
  703. hide_modal();
  704. $(window).unbind("keypress.check_enter_esc");
  705. },
  706. check_enter_esc = function(e) {
  707. if ((e.keyCode || e.which) === 27) { // Escape key
  708. cancel_fn();
  709. } else if ((e.keyCode || e.which) === 13) { // Enter key
  710. ok_fn();
  711. }
  712. };
  713. $(window).bind("keypress.check_enter_esc", check_enter_esc);
  714. show_modal("Configure Group", group.config.build_form(), {
  715. "Cancel": cancel_fn,
  716. "OK": ok_fn
  717. });
  718. };
  719. //
  720. // Remove option.
  721. //
  722. group_dropdown.Remove = function() {
  723. group.remove();
  724. };
  725. make_popupmenu(group.name_div, group_dropdown);
  726. }
  727. });
  728. /**
  729. * View object manages complete viz view, including tracks and user interactions.
  730. */
  731. var View = function(container, title, vis_id, dbkey) {
  732. DrawableCollection.call(this, "View");
  733. this.container = container;
  734. this.chrom = null;
  735. this.vis_id = vis_id;
  736. this.dbkey = dbkey;
  737. this.title = title;
  738. // Alias tracks to point at drawables. TODO: changes tracks to 'drawables' or something similar.
  739. this.tracks = this.drawables;
  740. this.label_tracks = [];
  741. this.tracks_to_be_redrawn = [];
  742. this.max_low = 0;
  743. this.max_high = 0;
  744. this.zoom_factor = 3;
  745. this.min_separation = 30;
  746. this.has_changes = false;
  747. // Deferred object that indicates when view's chrom data has been loaded.
  748. this.load_chroms_deferred = null;
  749. this.init();
  750. this.canvas_manager = new CanvasManager( container.get(0).ownerDocument );
  751. this.reset();
  752. };
  753. extend( View.prototype, DrawableCollection.prototype, {
  754. init: function() {
  755. // Create DOM elements
  756. var parent_element = this.container,
  757. view = this;
  758. // Top container for things that are fixed at the top
  759. this.top_container = $("<div/>").addClass("top-container").appendTo(parent_element);
  760. // Content container, primary tracks are contained in here
  761. this.content_div = $("<div/>").addClass("content").css("position", "relative").appendTo(parent_element);
  762. // Bottom container for things that are fixed at the bottom
  763. this.bottom_container = $("<div/>").addClass("bottom-container").appendTo(parent_element);
  764. // Label track fixed at top
  765. this.top_labeltrack = $("<div/>").addClass("top-labeltrack").appendTo(this.top_container);
  766. // Viewport for dragging tracks in center
  767. this.viewport_container = $("<div/>").addClass("viewport-container").attr("id", "viewport-container").appendTo(this.content_div);
  768. is_container(this.viewport_container, view);
  769. // Introduction div shown when there are no tracks.
  770. this.intro_div = $("<div/>").addClass("intro");
  771. var add_tracks_button = $("<div/>").text("Add Datasets to Visualization").addClass("action-button").appendTo(this.intro_div).click(function () {
  772. add_tracks();
  773. });
  774. // Another label track at bottom
  775. this.nav_labeltrack = $("<div/>").addClass("nav-labeltrack").appendTo(this.bottom_container);
  776. // Navigation at top
  777. this.nav_container = $("<div/>").addClass("nav-container").prependTo(this.top_container);
  778. this.nav = $("<div/>").addClass("nav").appendTo(this.nav_container);
  779. // Overview (scrollbar and overview plot) at bottom
  780. this.overview = $("<div/>").addClass("overview").appendTo(this.bottom_container);
  781. this.overview_viewport = $("<div/>").addClass("overview-viewport").appendTo(this.overview);
  782. this.overview_close = $("<a href='javascript:void(0);'>Close Overview</a>").addClass("overview-close").hide().appendTo(this.overview_viewport);
  783. this.overview_highlight = $("<div/>").addClass("overview-highlight").hide().appendTo(this.overview_viewport);
  784. this.overview_box_background = $("<div/>").addClass("overview-boxback").appendTo(this.overview_viewport);
  785. this.overview_box = $("<div/>").addClass("overview-box").appendTo(this.overview_viewport);
  786. this.default_overview_height = this.overview_box.height();
  787. this.nav_controls = $("<div/>").addClass("nav-controls").appendTo(this.nav);
  788. this.chrom_select = $("<select/>").attr({ "name": "chrom"}).css("width", "15em").addClass("no-autocomplete").append("<option value=''>Loading</option>").appendTo(this.nav_controls);
  789. var submit_nav = function(e) {
  790. if (e.type === "focusout" || (e.keyCode || e.which) === 13 || (e.keyCode || e.which) === 27 ) {
  791. if ((e.keyCode || e.which) !== 27) { // Not escape key
  792. view.go_to( $(this).val() );
  793. }
  794. $(this).hide();
  795. $(this).val('');
  796. view.location_span.show();
  797. view.chrom_select.show();
  798. }
  799. };
  800. this.nav_input = $("<input/>").addClass("nav-input").hide().bind("keyup focusout", submit_nav).appendTo(this.nav_controls);
  801. this.location_span = $("<span/>").addClass("location").appendTo(this.nav_controls);
  802. this.location_span.click(function() {
  803. view.location_span.hide();
  804. view.chrom_select.hide();
  805. view.nav_input.val(view.chrom + ":" + view.low + "-" + view.high);
  806. view.nav_input.css("display", "inline-block");
  807. view.nav_input.select();
  808. view.nav_input.focus();
  809. });
  810. if (this.vis_id !== undefined) {
  811. this.hidden_input = $("<input/>").attr("type", "hidden").val(this.vis_id).appendTo(this.nav_controls);
  812. }
  813. this.zo_link = $("<a id='zoom-out' />").click(function() { view.zoom_out(); view.request_redraw(); }).appendTo(this.nav_controls);
  814. this.zi_link = $("<a id='zoom-in' />").click(function() { view.zoom_in(); view.request_redraw(); }).appendTo(this.nav_controls);
  815. // Get initial set of chroms.
  816. this.load_chroms_deferred = this.load_chroms({low: 0});
  817. this.chrom_select.bind("change", function() {
  818. view.change_chrom(view.chrom_select.val());
  819. });
  820. /*
  821. this.content_div.bind("mousewheel", function( e, delta ) {
  822. if (Math.abs(delta) < 0.5) {
  823. return;
  824. }
  825. if (delta > 0) {
  826. view.zoom_in(e.pageX, this.viewport_container);
  827. } else {
  828. view.zoom_out();
  829. }
  830. e.preventDefault();
  831. });
  832. */
  833. // Blur tool/filter inputs when user clicks on content div.
  834. this.content_div.click(function( e ) {
  835. $(this).find("input").trigger("blur");
  836. });
  837. // Double clicking zooms in
  838. this.content_div.bind("dblclick", function( e ) {
  839. view.zoom_in(e.pageX, this.viewport_container);
  840. });
  841. // Dragging the overview box (~ horizontal scroll bar)
  842. this.overview_box.bind("dragstart", function( e, d ) {
  843. this.current_x = d.offsetX;
  844. }).bind("drag", function( e, d ) {
  845. var delta = d.offsetX - this.current_x;
  846. this.current_x = d.offsetX;
  847. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.max_high - view.max_low) );
  848. view.move_delta(-delta_chrom);
  849. });
  850. this.overview_close.click(function() {
  851. view.reset_overview();
  852. });
  853. // Dragging in the viewport scrolls
  854. this.viewport_container.bind( "draginit", function( e, d ) {
  855. // Disable interaction if started in scrollbar (for webkit)
  856. if ( e.clientX > view.viewport_container.width() - 16 ) {
  857. return false;
  858. }
  859. }).bind( "dragstart", function( e, d ) {
  860. d.original_low = view.low;
  861. d.current_height = e.clientY;
  862. d.current_x = d.offsetX;
  863. }).bind( "drag", function( e, d ) {
  864. var container = $(this);
  865. var delta = d.offsetX - d.current_x;
  866. var new_scroll = container.scrollTop() - (e.clientY - d.current_height);
  867. container.scrollTop(new_scroll);
  868. d.current_height = e.clientY;
  869. d.current_x = d.offsetX;
  870. var delta_chrom = Math.round(delta / view.viewport_container.width() * (view.high - view.low));
  871. view.move_delta(delta_chrom);
  872. // Also capture mouse wheel for left/right scrolling
  873. }).bind( 'mousewheel', function( e, d, dx, dy ) {
  874. // Only act on x axis scrolling if we see if, y will be i
  875. // handled by the browser when the event bubbles up
  876. if ( dx ) {
  877. var delta_chrom = Math.round( - dx / view.viewport_container.width() * (view.high - view.low) );
  878. view.move_delta( delta_chrom );
  879. }
  880. });
  881. // Dragging in the top label track allows selecting a region
  882. // to zoom in
  883. this.top_labeltrack.bind( "dragstart", function( e, d ) {
  884. return $("<div />").css( {
  885. "height": view.content_div.height() + view.top_labeltrack.height()
  886. + view.nav_labeltrack.height() + 1,
  887. "top": "0px",
  888. "position": "absolute",
  889. "background-color": "#ccf",
  890. "opacity": 0.5,
  891. "z-index": 1000
  892. } ).appendTo( $(this) );
  893. }).bind( "drag", function( e, d ) {
  894. $( d.proxy ).css({ left: Math.min( e.pageX, d.startX ), width: Math.abs( e.pageX - d.startX ) });
  895. var min = Math.min(e.pageX, d.startX ) - view.container.offset().left,
  896. max = Math.max(e.pageX, d.startX ) - view.container.offset().left,
  897. span = (view.high - view.low),
  898. width = view.viewport_container.width();
  899. view.update_location( Math.round(min / width * span) + view.low,
  900. Math.round(max / width * span) + view.low );
  901. }).bind( "dragend", function( e, d ) {
  902. var min = Math.min(e.pageX, d.startX),
  903. max = Math.max(e.pageX, d.startX),
  904. span = (view.high - view.low),
  905. width = view.viewport_container.width(),
  906. old_low = view.low;
  907. view.low = Math.round(min / width * span) + old_low;
  908. view.high = Math.round(max / width * span) + old_low;
  909. $(d.proxy).remove();
  910. view.request_redraw();
  911. });
  912. this.add_label_track( new LabelTrack( this, this.top_labeltrack ) );
  913. this.add_label_track( new LabelTrack( this, this.nav_labeltrack ) );
  914. $(window).bind("resize", function() { view.resize_window(); });
  915. $(document).bind("redraw", function() { view.redraw(); });
  916. this.reset();
  917. $(window).trigger("resize");
  918. this.update_intro_div();
  919. },
  920. /** Add or remove intro div depending on view state. */
  921. update_intro_div: function() {
  922. if (this.num_tracks === 0) {
  923. this.intro_div.appendTo(this.viewport_container);
  924. }
  925. else {
  926. this.intro_div.remove();
  927. }
  928. },
  929. update_location: function(low, high) {
  930. this.location_span.text( commatize(low) + ' - ' + commatize(high) );
  931. this.nav_input.val( this.chrom + ':' + commatize(low) + '-' + commatize(high) );
  932. },
  933. /**
  934. * Load chrom data for the view. Returns a jQuery Deferred.
  935. */
  936. load_chroms: function(url_parms) {
  937. url_parms['num'] = MAX_CHROMS_SELECTABLE;
  938. $.extend( url_parms, (this.vis_id !== undefined ? { vis_id: this.vis_id } : { dbkey: this.dbkey } ) );
  939. var
  940. view = this,
  941. chrom_data = $.Deferred();
  942. $.ajax({
  943. url: chrom_url,
  944. data: url_parms,
  945. dataType: "json",
  946. success: function (result) {
  947. // Show error if could not load chroms.
  948. if (result.chrom_info.length === 0) {
  949. alert("Invalid chromosome: " + url_parms.chrom);
  950. return;
  951. }
  952. // Load chroms.
  953. if (result.reference) {
  954. view.add_label_track( new ReferenceTrack(view) );
  955. }
  956. view.chrom_data = result.chrom_info;
  957. var chrom_options = '<option value="">Select Chrom/Contig</option>';
  958. for (var i = 0, len = view.chrom_data.length; i < len; i++) {
  959. var chrom = view.chrom_data[i].chrom;
  960. chrom_options += '<option value="' + chrom + '">' + chrom + '</option>';
  961. }
  962. if (result.prev_chroms) {
  963. chrom_options += '<option value="previous">Previous ' + MAX_CHROMS_SELECTABLE + '</option>';
  964. }
  965. if (result.next_chroms) {
  966. chrom_options += '<option value="next">Next ' + MAX_CHROMS_SELECTABLE + '</option>';
  967. }
  968. view.chrom_select.html(chrom_options);
  969. view.chrom_start_index = result.start_index;
  970. chrom_data.resolve(result);
  971. },
  972. error: function() {
  973. alert("Could not load chroms for this dbkey:", view.dbkey);
  974. }
  975. });
  976. return chrom_data;
  977. },
  978. change_chrom: function(chrom, low, high) {
  979. // Don't do anything if chrom is "None" (hackish but some browsers already have this set), or null/blank
  980. if (!chrom || chrom === "None") {
  981. return;
  982. }
  983. var view = this;
  984. //
  985. // If user is navigating to previous/next set of chroms, load new chrom set and return.
  986. //
  987. if (chrom === "previous") {
  988. view.load_chroms({low: this.chrom_start_index - MAX_CHROMS_SELECTABLE});
  989. return;
  990. }
  991. if (chrom === "next") {
  992. view.load_chroms({low: this.chrom_start_index + MAX_CHROMS_SELECTABLE});
  993. return;
  994. }
  995. //
  996. // User is loading a particular chrom. Look first in current set; if not in current set, load new
  997. // chrom set.
  998. //
  999. var found = $.grep(view.chrom_data, function(v, i) {
  1000. return v.chrom === chrom;
  1001. })[0];
  1002. if (found === undefined) {
  1003. // Try to load chrom and then change to chrom.
  1004. view.load_chroms({'chrom': chrom}, function() { view.change_chrom(chrom, low, high); });
  1005. return;
  1006. }
  1007. else {
  1008. // Switching to local chrom.
  1009. if (chrom !== view.chrom) {
  1010. view.chrom = chrom;
  1011. view.chrom_select.val(view.chrom);
  1012. view.max_high = found.len-1; // -1 because we're using 0-based indexing.
  1013. view.reset();
  1014. view.request_redraw(true);
  1015. for (var track_id = 0, len = view.tracks.length; track_id < len; track_id++) {
  1016. var track = view.tracks[track_id];
  1017. if (track.init) {
  1018. track.init();
  1019. }
  1020. }
  1021. }
  1022. if (low !== undefined && high !== undefined) {
  1023. view.low = Math.max(low, 0);
  1024. view.high = Math.min(high, view.max_high);
  1025. }
  1026. view.reset_overview();
  1027. view.request_redraw();
  1028. }
  1029. },
  1030. go_to: function(str) {
  1031. var view = this,
  1032. new_low,
  1033. new_high,
  1034. chrom_pos = str.split(":"),
  1035. chrom = chrom_pos[0],
  1036. pos = chrom_pos[1];
  1037. if (pos !== undefined) {
  1038. try {
  1039. var pos_split = pos.split("-");
  1040. new_low = parseInt(pos_split[0].replace(/,/g, ""), 10);
  1041. new_high = parseInt(pos_split[1].replace(/,/g, ""), 10);
  1042. } catch (e) {
  1043. return false;
  1044. }
  1045. }
  1046. view.change_chrom(chrom, new_low, new_high);
  1047. },
  1048. move_fraction : function( fraction ) {
  1049. var view = this;
  1050. var span = view.high - view.low;
  1051. this.move_delta( fraction * span );
  1052. },
  1053. move_delta: function(delta_chrom) {
  1054. var view = this;
  1055. var current_chrom_span = view.high - view.low;
  1056. // Check for left and right boundaries
  1057. if (view.low - delta_chrom < view.max_low) {
  1058. view.low = view.max_low;
  1059. view.high = view.max_low + current_chrom_span;
  1060. } else if (view.high - delta_chrom > view.max_high) {
  1061. view.high = view.max_high;
  1062. view.low = view.max_high - current_chrom_span;
  1063. } else {
  1064. view.high -= delta_chrom;
  1065. view.low -= delta_chrom;
  1066. }
  1067. view.request_redraw();
  1068. },
  1069. /**
  1070. * Add a drawable to the view.
  1071. */
  1072. add_drawable: function(drawable) {
  1073. DrawableCollection.prototype.add_drawable.call(this, drawable);
  1074. if (drawable.init) { drawable.init(); }
  1075. this.has_changes = true;
  1076. this.update_intro_div();
  1077. },
  1078. add_label_track: function (label_track) {
  1079. label_track.view = this;
  1080. this.label_tracks.push(label_track);
  1081. },
  1082. /**
  1083. * Remove drawable from the view.
  1084. */
  1085. remove_drawable: function(drawable, hide) {
  1086. DrawableCollection.prototype.remove_drawable.call(this, drawable);
  1087. if (hide) {
  1088. var view = this;
  1089. drawable.container_div.fadeOut('slow', function() {
  1090. $(this).remove();
  1091. view.update_intro_div();
  1092. });
  1093. this.has_changes = true;
  1094. }
  1095. },
  1096. reset: function() {
  1097. this.low = this.max_low;
  1098. this.high = this.max_high;
  1099. this.viewport_container.find(".yaxislabel").remove();
  1100. },
  1101. /**
  1102. * Request that view redraw some or all tracks. If a track is not specificied, redraw all tracks.
  1103. */
  1104. request_redraw: function(nodraw, force, clear_after, track) {
  1105. var
  1106. view = this,
  1107. // Either redrawing a single track or all view's tracks.
  1108. track_list = (track ? [track] : view.tracks),
  1109. track_index;
  1110. // Add/update tracks in track list to redraw list.
  1111. var track;
  1112. for (var i = 0; i < track_list.length; i++) {
  1113. track = track_list[i];
  1114. // Because list elements are arrays, need to look for track index manually.
  1115. track_index = -1;
  1116. for (var j = 0; j < view.tracks_to_be_redrawn.length; j++) {
  1117. if (view.tracks_to_be_redrawn[j][0] === track) {
  1118. track_index = j;
  1119. break;
  1120. }
  1121. }
  1122. // Add track to list or update draw parameters.
  1123. if (track_index < 0) {
  1124. // Track not in list yet.
  1125. view.tracks_to_be_redrawn.push([track, force, clear_after]);
  1126. }
  1127. else {
  1128. // Track already in list; update force and clear_after.
  1129. view.tracks_to_be_redrawn[i][1] = force;
  1130. view.tracks_to_be_redrawn[i][2] = clear_after;
  1131. }
  1132. }
  1133. // Set up redraw.
  1134. requestAnimationFrame(function() { view._redraw(nodraw) });
  1135. },
  1136. /**
  1137. * Redraws view and tracks.
  1138. * NOTE: this method should never be called directly; request_redraw() should be used so
  1139. * that requestAnimationFrame can manage redrawing.
  1140. */
  1141. _redraw: function(nodraw) {
  1142. var low = this.low,
  1143. high = this.high;
  1144. if (low < this.max_low) {
  1145. low = this.max_low;
  1146. }
  1147. if (high > this.max_high) {
  1148. high = this.max_high;
  1149. }
  1150. var span = this.high - this.low;
  1151. if (this.high !== 0 && span < this.min_separation) {
  1152. high = low + this.min_separation;
  1153. }
  1154. this.low = Math.floor(low);
  1155. this.high = Math.ceil(high);
  1156. // 10^log10(range / DENSITY) Close approximation for browser window, assuming DENSITY = window width
  1157. this.resolution = Math.pow( RESOLUTION, Math.ceil( Math.log( (this.high - this.low) / DENSITY ) / Math.log(RESOLUTION) ) );
  1158. this.zoom_res = Math.pow( FEATURE_LEVELS, Math.max(0,Math.ceil( Math.log( this.resolution, FEATURE_LEVELS ) / Math.log(FEATURE_LEVELS) )));
  1159. // Overview
  1160. var left_px = ( this.low / (this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1161. var width_px = ( (this.high - this.low)/(this.max_high - this.max_low) * this.overview_viewport.width() ) || 0;
  1162. var min_width_px = 13;
  1163. this.overview_box.css({ left: left_px, width: Math.max(min_width_px, width_px) }).show();
  1164. if (width_px < min_width_px) {
  1165. this.overview_box.css("left", left_px - (min_width_px - width_px)/2);
  1166. }
  1167. if (this.overview_highlight) {
  1168. this.overview_highlight.css({ left: left_px, width: width_px });
  1169. }
  1170. this.update_location(this.low, this.high);
  1171. if (!nodraw) {
  1172. var track, force, clear_after;
  1173. for (var i = 0, len = this.tracks_to_be_redrawn.length; i < len; i++) {
  1174. track = this.tracks_to_be_redrawn[i][0];
  1175. force = this.tracks_to_be_redrawn[i][1];
  1176. clear_after = this.tracks_to_be_redrawn[i][2];
  1177. if (track) {
  1178. track._draw(force, clear_after);
  1179. }
  1180. }
  1181. this.tracks_to_be_redrawn = [];
  1182. for (i = 0, len = this.label_tracks.length; i < len; i++) {
  1183. this.label_tracks[i]._draw();
  1184. }
  1185. }
  1186. },
  1187. zoom_in: function (point, container) {
  1188. if (this.max_high === 0 || this.high - this.low < this.min_separation) {
  1189. return;
  1190. }
  1191. var span = this.high - this.low,
  1192. cur_center = span / 2 + this.low,
  1193. new_half = (span / this.zoom_factor) / 2;
  1194. if (point) {
  1195. cur_center = point / this.viewport_container.width() * (this.high - this.low) + this.low;
  1196. }
  1197. this.low = Math.round(cur_center - new_half);
  1198. this.high = Math.round(cur_center + new_half);
  1199. this.request_redraw();
  1200. },
  1201. zoom_out: function () {
  1202. if (this.max_high === 0) {
  1203. return;
  1204. }
  1205. var span = this.high - this.low,
  1206. cur_center = span / 2 + this.low,
  1207. new_half = (span * this.zoom_factor) / 2;
  1208. this.low = Math.round(cur_center - new_half);
  1209. this.high = Math.round(cur_center + new_half);
  1210. this.request_redraw();
  1211. },
  1212. resize_window: function() {
  1213. this.viewport_container.height( this.container.height() - this.top_container.height() - this.bottom_container.height() );
  1214. this.nav_container.width( this.container.width() );
  1215. this.request_redraw();
  1216. },
  1217. /** Show a track in the overview. */
  1218. set_overview: function(track) {
  1219. // Get data and draw tile.
  1220. $.when(track.get_overview_tile()).then(function(tile) {
  1221. // Update UI.
  1222. view.overview_viewport.find(".track-tile").remove();
  1223. view.overview_close.show();
  1224. view.overview_viewport.append(tile.canvas);
  1225. view.overview_highlight.show().height(tile.canvas.height());
  1226. view.overview_viewport.height(tile.canvas.height() + view.overview_box.outerHeight());
  1227. view.resize_window();
  1228. // Update view, track states.
  1229. if (view.overview_track) {
  1230. view.overview_track.set_is_overview(false);
  1231. }
  1232. view.overview_track = track;
  1233. track.set_is_overview(true);
  1234. });
  1235. view.has_changes = true;
  1236. },
  1237. /** Close and reset overview. */
  1238. reset_overview: function() {
  1239. // Update UI.
  1240. this.overview_viewport.find(".track-tile").remove();
  1241. this.overview_viewport.height(this.default_overview_height);
  1242. this.overview_box.height(this.default_overview_height);
  1243. this.overview_close.hide();
  1244. this.overview_highlight.hide();
  1245. view.resize_window();
  1246. // Update view, track states.
  1247. if (view.overview_track) {
  1248. view.overview_track.set_is_overview(false);
  1249. }
  1250. view.overview_track = null;
  1251. }
  1252. });
  1253. /**
  1254. * Encapsulation of a tool that users can apply to tracks/datasets.
  1255. */
  1256. var Tool = function(track, tool_dict) {
  1257. //
  1258. // Unpack tool information from dictionary.
  1259. //
  1260. this.track = track;
  1261. this.name = tool_dict.name;
  1262. this.params = [];
  1263. var params_dict = tool_dict.params;
  1264. for (var i = 0; i < params_dict.length; i++) {
  1265. var param_dict = params_dict[i],
  1266. name = param_dict.name,
  1267. label = param_dict.label,
  1268. html = unescape(param_dict.html),
  1269. value = param_dict.value,
  1270. type = param_dict.type;
  1271. if (type === "number") {
  1272. this.params[this.params.length] =
  1273. new NumberParameter(name, label, html, value, param_dict.min, param_dict.max);
  1274. }
  1275. else if (type == "select") {
  1276. this.params[this.params.length] = new ToolParameter(name, label, html, value);
  1277. }
  1278. else {
  1279. console.log("WARNING: unrecognized tool parameter type:", name, type);
  1280. }
  1281. }
  1282. //
  1283. // Create div elt for tool UI.
  1284. //
  1285. this.parent_div = $("<div/>").addClass("dynamic-tool").hide();
  1286. // Disable dragging, clicking, double clicking on div so that actions on slider do not impact viz.
  1287. this.parent_div.bind("drag", function(e) {
  1288. e.stopPropagation();
  1289. }).click(function(e) {
  1290. e.stopPropagation();
  1291. }).bind("dblclick", function(e) {
  1292. e.stopPropagation();
  1293. });
  1294. var name_div = $("<div class='tool-name'>").appendTo(this.parent_div).text(this.name);
  1295. var tool_params = this.params;
  1296. var tool = this;
  1297. $.each(this.params, function(index, param) {
  1298. var param_div = $("<div>").addClass("param-row").appendTo(tool.parent_div);
  1299. // Param label.
  1300. var label_div = $("<div>").addClass("param-label").text(param.label).appendTo(param_div);
  1301. // Param HTML.
  1302. // TODO: either generalize .slider CSS attributes or create new rule for tool input div.
  1303. var html_div = $("<div/>").addClass("slider").html(param.html).appendTo(param_div);
  1304. // Set initial value.
  1305. html_div.find(":input").val(param.value);
  1306. // Add to clear floating layout.
  1307. $("<div style='clear: both;'/>").appendTo(param_div);
  1308. });
  1309. // Highlight value for inputs for easy replacement.
  1310. this.parent_div.find("input").click(function() { $(this).select() });
  1311. // Add buttons for running on dataset, region.
  1312. var run_tool_row = $("<div>").addClass("param-row").appendTo(this.parent_div);
  1313. var run_on_dataset_button = $("<input type='submit'>").attr("value", "Run on complete dataset").appendTo(run_tool_row);
  1314. var run_on_region_button = $("<input type='submit'>").attr("value", "Run on visible region").css("margin-left", "3em").appendTo(run_tool_row);
  1315. var tool = this;
  1316. run_on_region_button.click( function() {
  1317. // Create group for track + new tracks and put track in group.
  1318. var
  1319. parent_elt = this.track.parent_element;
  1320. //new_group = new DrawableCollection("New Group", parent_elt);
  1321. //this.track.view.add_drawable(new_group);
  1322. // Run tool to create new track.
  1323. tool.run_on_region();
  1324. });
  1325. run_on_dataset_button.click( function() {
  1326. tool.run_on_dataset();
  1327. });
  1328. };
  1329. extend(Tool.prototype, {
  1330. /**
  1331. * Returns dictionary of parameter name-values.
  1332. */
  1333. get_param_values_dict: function() {
  1334. var param_dict = {};
  1335. this.parent_div.find(":input").each(function() {
  1336. var name = $(this).attr("name"), value = $(this).val();
  1337. param_dict[name] = JSON.stringify(value);
  1338. });
  1339. return param_dict;
  1340. },
  1341. /**
  1342. * Returns array of parameter values.
  1343. */
  1344. get_param_values: function() {
  1345. var param_values = [];
  1346. var param_dict = {};
  1347. this.parent_div.find(":input").each(function() {
  1348. // Only include inputs with names; this excludes Run button.
  1349. var name = $(this).attr("name"), value = $(this).val();
  1350. if (name) {
  1351. param_values[param_values.length] = value;
  1352. }
  1353. });
  1354. return param_values;
  1355. },
  1356. /**
  1357. * Run tool on dataset. Output is placed in dataset's history and no changes to viz are made.
  1358. */
  1359. run_on_dataset: function() {
  1360. var tool = this;
  1361. tool.run(
  1362. // URL params.
  1363. {
  1364. dataset_id: this.track.original_dataset_id,
  1365. tool_id: tool.name
  1366. },
  1367. null,
  1368. // Success callback.
  1369. function(track_data) {
  1370. show_modal(tool.name + " is Running",
  1371. tool.name + " is running on the complete dataset. Tool outputs are in dataset's history.",
  1372. { "Close" : hide_modal } );
  1373. }
  1374. );
  1375. },
  1376. /**
  1377. * Run dataset on visible region. This creates a new track and sets the track's contents
  1378. * to the tool's output.
  1379. */
  1380. run_on_region: function() {
  1381. //
  1382. // Create track for tool's output immediately to provide user feedback.
  1383. //
  1384. var
  1385. url_params =
  1386. {
  1387. dataset_id: this.track.original_dataset_id,
  1388. chrom: this.track.view.chrom,
  1389. low: this.track.view.low,
  1390. high: this.track.view.high,
  1391. tool_id: this.name
  1392. },
  1393. current_track = this.track,
  1394. // Set name of track to include tool name, parameters, and region used.
  1395. track_name = url_params.tool_id +
  1396. current_track.tool_region_and_parameters_str(url_params.chrom, url_params.low, url_params.high),
  1397. new_track;
  1398. // Create and add track.
  1399. // TODO: add support for other kinds of tool data tracks.
  1400. if (current_track instanceof FeatureTrack) {
  1401. new_track = new ToolDataFeatureTrack(track_name, view, current_track.hda_ldda, undefined, {}, {}, current_track);
  1402. new_track.change_mode(current_track.mode);
  1403. }
  1404. this.track.view.add_drawable(new_track);
  1405. new_track.content_div.text("Starting job.");
  1406. // Run tool.
  1407. this.run(url_params, new_track,
  1408. // Success callback.
  1409. function(track_data) {
  1410. new_track.dataset_id = track_data.dataset_id;
  1411. new_track.content_div.text("Running job.");
  1412. new_track.init();
  1413. }
  1414. );
  1415. },
  1416. /**
  1417. * Run tool using a set of URL params and a success callback.
  1418. */
  1419. run: function(url_params, new_track, success_callback) {
  1420. // Add tool params to URL params.
  1421. $.extend(url_params, this.get_param_values_dict());
  1422. // Run tool.
  1423. // TODO: rewrite to use $.when():
  1424. var json_run_tool = function() {
  1425. $.getJSON(rerun_tool_url, url_params, function(response) {
  1426. if (response === "no converter") {
  1427. // No converter available for input datasets, so cannot run tool.
  1428. new_track.container_div.addClass("error");
  1429. new_track.content_div.text(DATA_NOCONVERTER);
  1430. }
  1431. else if (response.error) {
  1432. // General error.
  1433. new_track.container_div.addClass("error");
  1434. new_track.content_div.text(DATA_CANNOT_RUN_TOOL + response.message);
  1435. }
  1436. else if (response === "pending") {
  1437. // Converting/indexing input datasets; show message and try again.
  1438. new_track.container_div.addClass("pending");
  1439. new_track.content_div.text("Converting input data so that it can be used quickly with tool.");
  1440. setTimeout(json_run_tool, 2000);
  1441. }
  1442. else {
  1443. // Job submitted and running.
  1444. success_callback(response);
  1445. }
  1446. });
  1447. };
  1448. json_run_tool();
  1449. }
  1450. });
  1451. /**
  1452. * Tool parameters.
  1453. */
  1454. var ToolParameter = function(name, label, html, value) {
  1455. this.name = name;
  1456. this.label = label;
  1457. this.html = html;
  1458. this.value = value;
  1459. };
  1460. var NumberParameter = function(name, label, html, value, min, max) {
  1461. ToolParameter.call(this, name, label, html, value)
  1462. this.min = min;
  1463. this.max = max;
  1464. };
  1465. /**
  1466. * Filters that enable users to show/hide data points dynamically.
  1467. */
  1468. var Filter = function(name, index, tool_id, tool_exp_name) {
  1469. this.name = name;
  1470. // Index into payload to filter.
  1471. this.index = index;
  1472. this.tool_id = tool_id;
  1473. // Name to use for filter when building expression for tool.
  1474. this.tool_exp_name = tool_exp_name;
  1475. };
  1476. /**
  1477. * Number filters have a min, max as well as a low, high; low and high are used
  1478. */
  1479. var NumberFilter = function(name, index, tool_id, tool_exp_name) {
  1480. Filter.call(this, name, index, tool_id, tool_exp_name);
  1481. // Filter low/high. These values are used to filter elements.
  1482. this.low = -Number.MAX_VALUE;
  1483. this.high = Number.MAX_VALUE;
  1484. // Slide min/max. These values are used to set/update slider.
  1485. this.min = Number.MAX_VALUE;
  1486. this.max = -Number.MAX_VALUE;
  1487. // UI elements associated with filter.
  1488. this.container = null;
  1489. this.slider = null;
  1490. this.slider_label = null;
  1491. };
  1492. extend(NumberFilter.prototype, {
  1493. /**
  1494. * Returns true if filter can be applied to element.
  1495. */
  1496. applies_to: function(element) {
  1497. if (element.length > this.index) {
  1498. return true;
  1499. }
  1500. return false;
  1501. },
  1502. /**
  1503. * Returns true if (a) element's value is in [low, high] (range is inclusive)
  1504. * or (b) if value is non-numeric and hence unfilterable.
  1505. */
  1506. keep: function(element) {
  1507. if ( !this.applies_to( element ) ) {
  1508. // No element to filter on.
  1509. return true;
  1510. }
  1511. var val = parseFloat(element[this.index]);
  1512. return (isNaN(val) || (val >= this.low && val <= this.high));
  1513. },
  1514. /**
  1515. * Update filter's min and max values based on element's values.
  1516. */
  1517. update_attrs: function(element) {
  1518. var updated = false;
  1519. if (!this.applies_to(element) ) {
  1520. return updated;
  1521. }
  1522. // Update filter's min, max based on element values.
  1523. if (element[this.index] < this.min) {
  1524. this.min = Math.floor(element[this.index]);
  1525. updated = true;
  1526. }
  1527. if (element[this.index] > this.max) {
  1528. this.max = Math.ceil(element[this.index]);
  1529. updated = true;
  1530. }
  1531. return updated;
  1532. },
  1533. /**
  1534. * Update filter's slider.
  1535. */
  1536. update_ui_elt: function () {
  1537. // Only show filter if min != max because filter is not useful otherwise.
  1538. if (this.min != this.max) {
  1539. this.container.show();
  1540. }
  1541. else {
  1542. this.container.hide();
  1543. }
  1544. var get_slider_step = function(min, max) {
  1545. var range = max - min;
  1546. return (range <= 2 ? 0.01 : 1);
  1547. };
  1548. var
  1549. slider_min = this.slider.slider("option", "min"),
  1550. slider_max = this.slider.slider("option", "max");
  1551. if (this.min < slider_min || this.max > slider_max) {
  1552. // Update slider min, max, step.
  1553. this.slider.slider("option", "min", this.min);
  1554. this.slider.slider("option", "max", this.max);
  1555. this.slider.slider("option", "step", get_slider_step(this.min, this.max));
  1556. // Refresh slider:
  1557. // TODO: do we want to keep current values or reset to min/max?
  1558. // Currently we reset values:
  1559. this.slider.slider("option", "values", [this.min, this.max]);
  1560. // To use the current values.
  1561. //var values = this.slider.slider( "option", "values" );
  1562. //this.slider.slider( "option", "values", values );
  1563. }
  1564. }
  1565. });
  1566. /**
  1567. * Manages a set of filters.
  1568. */
  1569. var FiltersManager = function(track, filters_list) {
  1570. //
  1571. // Unpack filters from dict.
  1572. //
  1573. this.track = track;
  1574. this.filters = [];
  1575. for (var i = 0; i < filters_list.length; i++) {
  1576. var
  1577. filter_dict = filters_list[i],
  1578. name = filter_dict.name,
  1579. type = filter_dict.type,
  1580. index = filter_dict.index,
  1581. tool_id = filter_dict.tool_id,
  1582. tool_exp_name = filter_dict.tool_exp_name;
  1583. if (type === 'int' || type === 'float') {
  1584. this.filters[i] =
  1585. new NumberFilter(name, index, tool_id, tool_exp_name);
  1586. } else {
  1587. console.log("ERROR: unsupported filter: ", name, type)
  1588. }
  1589. }
  1590. //
  1591. // Init HTML elements for filters.
  1592. //
  1593. // Function that supports inline text editing of slider values for tools, filters.
  1594. // Enable users to edit parameter's value via a text box.
  1595. var edit_slider_values = function(container, span, slider) {
  1596. container.click(function() {
  1597. var cur_value = span.text();
  1598. max = parseFloat(slider.slider("option", "max")),
  1599. input_size = (max <= 1 ? 4 : max <= 1000000 ? max.toString().length : 6),