PageRenderTime 45ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/static/scripts/viz/visualization.js

https://bitbucket.org/dan/galaxy-central
JavaScript | 1080 lines | 702 code | 124 blank | 254 comment | 67 complexity | b707679599406fd0a803c97d5727f0ae MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define( ["libs/underscore", "mvc/data", "viz/trackster/util", "utils/config"], function(_, data_mod, util_mod, config_mod) {
  2. /**
  3. * Mixin for returning custom JSON representation from toJSON. Class attribute to_json_keys defines a set of attributes
  4. * to include in the representation; to_json_mappers defines mappers for returned objects.
  5. */
  6. var CustomToJSON = {
  7. /**
  8. * Returns JSON representation of object using to_json_keys and to_json_mappers.
  9. */
  10. toJSON: function() {
  11. var self = this,
  12. json = {};
  13. _.each(self.constructor.to_json_keys, function(k) {
  14. var val = self.get(k);
  15. if (k in self.constructor.to_json_mappers) {
  16. val = self.constructor.to_json_mappers[k](val, self);
  17. }
  18. json[k] = val;
  19. });
  20. return json;
  21. }
  22. };
  23. /**
  24. * Model, view, and controller objects for Galaxy visualization framework.
  25. *
  26. * Models have no references to views, instead using events to indicate state
  27. * changes; this is advantageous because multiple views can use the same object
  28. * and models can be used without views.
  29. */
  30. /**
  31. * Use a popup grid to select datasets from histories or libraries. After datasets are selected,
  32. * track definitions are obtained from the server and the success_fn is called with the list of
  33. * definitions for selected datasets.
  34. */
  35. var select_datasets = function(dataset_url, add_track_async_url, filters, success_fn) {
  36. $.ajax({
  37. url: dataset_url,
  38. data: filters,
  39. error: function() { alert( "Grid failed" ); },
  40. success: function(table_html) {
  41. Galaxy.modal.show({
  42. title : "Select datasets for new tracks",
  43. body : table_html,
  44. buttons :
  45. {
  46. "Cancel": function() {
  47. Galaxy.modal.hide();
  48. },
  49. "Add": function() {
  50. var requests = [];
  51. $('input[name=id]:checked,input[name=ldda_ids]:checked').each(function() {
  52. var data = {
  53. data_type: 'track_config',
  54. 'hda_ldda': 'hda'
  55. },
  56. id = $(this).val();
  57. if ($(this).attr("name") !== "id") {
  58. data.hda_ldda = 'ldda';
  59. }
  60. requests[requests.length] = $.ajax({
  61. url: add_track_async_url + "/" + id,
  62. data: data,
  63. dataType: "json"
  64. });
  65. });
  66. // To preserve order, wait until there are definitions for all tracks and then add
  67. // them sequentially.
  68. $.when.apply($, requests).then(function() {
  69. // jQuery always returns an Array for arguments, so need to look at first element
  70. // to determine whether multiple requests were made and consequently how to
  71. // map arguments to track definitions.
  72. var track_defs = (arguments[0] instanceof Array ?
  73. $.map(arguments, function(arg) { return arg[0]; }) :
  74. [ arguments[0] ]
  75. );
  76. success_fn(track_defs);
  77. });
  78. Galaxy.modal.hide();
  79. }
  80. }
  81. });
  82. }
  83. });
  84. };
  85. // --------- Models ---------
  86. /**
  87. * Canvas manager is used to create canvases, for browsers, this deals with
  88. * backward comparibility using excanvas, as well as providing a pattern cache
  89. */
  90. var CanvasManager = function(default_font) {
  91. this.default_font = default_font !== undefined ? default_font : "9px Monaco, Lucida Console, monospace";
  92. this.dummy_canvas = this.new_canvas();
  93. this.dummy_context = this.dummy_canvas.getContext('2d');
  94. this.dummy_context.font = this.default_font;
  95. this.char_width_px = this.dummy_context.measureText("A").width;
  96. this.patterns = {};
  97. // FIXME: move somewhere to make this more general
  98. this.load_pattern( 'right_strand', "/visualization/strand_right.png" );
  99. this.load_pattern( 'left_strand', "/visualization/strand_left.png" );
  100. this.load_pattern( 'right_strand_inv', "/visualization/strand_right_inv.png" );
  101. this.load_pattern( 'left_strand_inv', "/visualization/strand_left_inv.png" );
  102. };
  103. _.extend( CanvasManager.prototype, {
  104. load_pattern: function( key, path ) {
  105. var patterns = this.patterns,
  106. dummy_context = this.dummy_context,
  107. image = new Image();
  108. image.src = galaxy_config.root + "static/images" + path;
  109. image.onload = function() {
  110. patterns[key] = dummy_context.createPattern( image, "repeat" );
  111. };
  112. },
  113. get_pattern: function( key ) {
  114. return this.patterns[key];
  115. },
  116. new_canvas: function() {
  117. var canvas = $("<canvas/>")[0];
  118. // If using excanvas in IE, we need to explicately attach the canvas
  119. // methods to the DOM element
  120. if (window.G_vmlCanvasManager) { G_vmlCanvasManager.initElement(canvas); }
  121. // Keep a reference back to the manager
  122. canvas.manager = this;
  123. return canvas;
  124. }
  125. });
  126. /**
  127. * Generic cache that handles key/value pairs. Keys can be any object that can be
  128. * converted to a String and compared.
  129. */
  130. var Cache = Backbone.Model.extend({
  131. defaults: {
  132. num_elements: 20,
  133. // Objects in cache; indexes into cache are strings of keys.
  134. obj_cache: null,
  135. // key_ary contains keys for objects in cache.
  136. key_ary: null
  137. },
  138. initialize: function(options) {
  139. this.clear();
  140. },
  141. /**
  142. * Get an element from the cache using its key.
  143. */
  144. get_elt: function(key) {
  145. var obj_cache = this.attributes.obj_cache,
  146. key_ary = this.attributes.key_ary,
  147. key_str = key.toString(),
  148. index = _.indexOf(key_ary, function(k) {
  149. return k.toString() === key_str;
  150. });
  151. // Update cache.
  152. if (index !== -1) {
  153. // Object is in cache, so update it.
  154. if (obj_cache[key_str].stale) {
  155. // Object is stale: remove key and object.
  156. key_ary.splice(index, 1);
  157. delete obj_cache[key_str];
  158. }
  159. else {
  160. // Move key to back because it is most recently used.
  161. this.move_key_to_end(key, index);
  162. }
  163. }
  164. return obj_cache[key_str];
  165. },
  166. /**
  167. * Put an element into the cache.
  168. */
  169. set_elt: function(key, value) {
  170. var obj_cache = this.attributes.obj_cache,
  171. key_ary = this.attributes.key_ary,
  172. key_str = key.toString(),
  173. num_elements = this.attributes.num_elements;
  174. // Update keys, objects.
  175. if (!obj_cache[key_str]) {
  176. // Add object to cache.
  177. if (key_ary.length >= num_elements) {
  178. // Cache full, so remove first element.
  179. var deleted_key = key_ary.shift();
  180. delete obj_cache[deleted_key.toString()];
  181. }
  182. // Add key.
  183. key_ary.push(key);
  184. }
  185. // Add object.
  186. obj_cache[key_str] = value;
  187. return value;
  188. },
  189. /**
  190. * Move key to end of cache. Keys are removed from the front, so moving a key to the end
  191. * delays the key's removal.
  192. */
  193. move_key_to_end: function(key, index) {
  194. this.attributes.key_ary.splice(index, 1);
  195. this.attributes.key_ary.push(key);
  196. },
  197. /**
  198. * Clear all elements from the cache.
  199. */
  200. clear: function() {
  201. this.attributes.obj_cache = {};
  202. this.attributes.key_ary = [];
  203. },
  204. /** Returns the number of elements in the cache. */
  205. size: function() {
  206. return this.attributes.key_ary.length;
  207. },
  208. /** Returns key most recently added to cache. */
  209. most_recently_added: function() {
  210. return this.size() === 0 ? null :
  211. // Most recent key is at the end of key array.
  212. this.attributes.key_ary[this.attributes.key_ary.length - 1];
  213. }
  214. });
  215. /**
  216. * Data manager for genomic data. Data is connected to and queryable by genomic regions.
  217. */
  218. var GenomeDataManager = Cache.extend({
  219. defaults: _.extend({}, Cache.prototype.defaults, {
  220. dataset: null,
  221. genome: null,
  222. init_data: null,
  223. min_region_size: 200,
  224. filters_manager: null,
  225. data_type: "data",
  226. data_mode_compatible: function(entry, mode) { return true; },
  227. can_subset: function(entry) { return false; }
  228. }),
  229. /**
  230. * Initialization.
  231. */
  232. initialize: function(options) {
  233. Cache.prototype.initialize.call(this);
  234. // Set initial entries in data manager.
  235. var initial_entries = this.get('init_data');
  236. if (initial_entries) {
  237. this.add_data(initial_entries);
  238. }
  239. },
  240. /**
  241. * Add data entries to manager; each entry should be a dict with attributes region (key), data, and data_type.
  242. * If necessary, manager size is increased to hold all data.
  243. */
  244. add_data: function(entries) {
  245. // Increase size to accomodate all entries.
  246. if (this.get('num_elements') < entries.length) {
  247. this.set('num_elements', entries.length);
  248. }
  249. // Put data into manager.
  250. var self = this;
  251. _.each(entries, function(entry) {
  252. self.set_data(entry.region, entry);
  253. });
  254. },
  255. /**
  256. * Returns deferred that resolves to true when dataset is ready (or false if dataset
  257. * cannot be used).
  258. */
  259. data_is_ready: function() {
  260. var dataset = this.get('dataset'),
  261. ready_deferred = $.Deferred(),
  262. // If requesting raw data, query dataset state; if requesting (converted) data,
  263. // need to query converted datasets state.
  264. query_type = (this.get('data_type') === 'raw_data' ? 'state' :
  265. this.get('data_type') === 'data' ? 'converted_datasets_state' : "error" ),
  266. ss_deferred = new util_mod.ServerStateDeferred({
  267. ajax_settings: {
  268. url: this.get('dataset').url(),
  269. data: {
  270. hda_ldda: dataset.get('hda_ldda'),
  271. data_type: query_type
  272. },
  273. dataType: "json"
  274. },
  275. interval: 5000,
  276. success_fn: function(response) { return response !== "pending"; }
  277. });
  278. $.when(ss_deferred.go()).then(function(response) {
  279. ready_deferred.resolve(response === "ok" || response === "data" );
  280. });
  281. return ready_deferred;
  282. },
  283. /**
  284. * Perform a feature search from server; returns Deferred object that resolves when data is available.
  285. */
  286. search_features: function(query) {
  287. var dataset = this.get('dataset'),
  288. params = {
  289. query: query,
  290. hda_ldda: dataset.get('hda_ldda'),
  291. data_type: 'features'
  292. };
  293. return $.getJSON(dataset.url(), params);
  294. },
  295. /**
  296. * Load data from server and manages data entries. Adds a Deferred to manager
  297. * for region; when data becomes available, replaces Deferred with data.
  298. * Returns the Deferred that resolves when data is available.
  299. */
  300. load_data: function(region, mode, resolution, extra_params) {
  301. // Setup data request params.
  302. var dataset = this.get('dataset'),
  303. params = {
  304. "data_type": this.get('data_type'),
  305. "chrom": region.get('chrom'),
  306. "low": region.get('start'),
  307. "high": region.get('end'),
  308. "mode": mode,
  309. "resolution": resolution,
  310. "hda_ldda": dataset.get('hda_ldda')
  311. };
  312. $.extend(params, extra_params);
  313. // Add track filters to params.
  314. var filters_manager = this.get('filters_manager');
  315. if (filters_manager) {
  316. var filter_names = [];
  317. var filters = filters_manager.filters;
  318. for (var i = 0; i < filters.length; i++) {
  319. filter_names.push(filters[i].name);
  320. }
  321. params.filter_cols = JSON.stringify(filter_names);
  322. }
  323. // Do request.
  324. var manager = this,
  325. entry = $.getJSON(dataset.url(), params, function (result) {
  326. // Add region to the result.
  327. result.region = region;
  328. manager.set_data(region, result);
  329. });
  330. this.set_data(region, entry);
  331. return entry;
  332. },
  333. /**
  334. * Get data from dataset.
  335. */
  336. get_data: function(region, mode, resolution, extra_params) {
  337. // Look for entry and return if it's a deferred or if data available is compatible with mode.
  338. var entry = this.get_elt(region);
  339. if ( entry &&
  340. ( util_mod.is_deferred(entry) || this.get('data_mode_compatible')(entry, mode) ) ) {
  341. return entry;
  342. }
  343. //
  344. // Look in cache for data that can be used.
  345. // TODO: this logic could be improved if the visualization knew whether
  346. // the data was "index" or "data."
  347. //
  348. var key_ary = this.get('key_ary'),
  349. obj_cache = this.get('obj_cache'),
  350. entry_region, is_subregion;
  351. for (var i = 0; i < key_ary.length; i++) {
  352. entry_region = key_ary[i];
  353. if (entry_region.contains(region)) {
  354. is_subregion = true;
  355. // This entry has data in the requested range. Return if data
  356. // is compatible and can be subsetted.
  357. entry = obj_cache[entry_region.toString()];
  358. if ( util_mod.is_deferred(entry) ||
  359. ( this.get('data_mode_compatible')(entry, mode) && this.get('can_subset')(entry) ) ) {
  360. this.move_key_to_end(entry_region, i);
  361. // If there's data, subset it.
  362. if ( !util_mod.is_deferred(entry) ) {
  363. var subset_entry = this.subset_entry(entry, region);
  364. this.set_data(region, subset_entry);
  365. entry = subset_entry;
  366. }
  367. return entry;
  368. }
  369. }
  370. }
  371. // FIXME: There _may_ be instances where region is a subregion of another entry but cannot be
  372. // subsetted. For these cases, do not increase length because region will never be found (and
  373. // an infinite loop will occur.)
  374. // If needed, extend region to make it minimum size.
  375. if (!is_subregion && region.length() < this.attributes.min_region_size) {
  376. // IDEA: alternative heuristic is to find adjacent cache entry to region and use that to extend.
  377. // This would prevent bad extensions when zooming in/out while still preserving the behavior
  378. // below.
  379. // Use copy of region to avoid changing actual region.
  380. region = region.copy();
  381. // Use heuristic to extend region: extend relative to last data request.
  382. var last_request = this.most_recently_added();
  383. if (!last_request || (region.get('start') > last_request.get('start'))) {
  384. // This request is after the last request, so extend right.
  385. region.set('end', region.get('start') + this.attributes.min_region_size);
  386. }
  387. else {
  388. // This request is after the last request, so extend left.
  389. region.set('start', region.get('end') - this.attributes.min_region_size);
  390. }
  391. // Trim region to avoid invalid coordinates.
  392. region.set('genome', this.attributes.genome);
  393. region.trim();
  394. }
  395. return this.load_data(region, mode, resolution, extra_params);
  396. },
  397. /**
  398. * Alias for set_elt for readbility.
  399. */
  400. set_data: function(region, entry) {
  401. this.set_elt(region, entry);
  402. },
  403. /** "Deep" data request; used as a parameter for DataManager.get_more_data() */
  404. DEEP_DATA_REQ: "deep",
  405. /** "Broad" data request; used as a parameter for DataManager.get_more_data() */
  406. BROAD_DATA_REQ: "breadth",
  407. /**
  408. * Gets more data for a region using either a depth-first or a breadth-first approach.
  409. */
  410. get_more_data: function(region, mode, resolution, extra_params, req_type) {
  411. var cur_data = this._mark_stale(region);
  412. if (!(cur_data && this.get('data_mode_compatible')(cur_data, mode))) {
  413. console.log('ERROR: problem with getting more data: current data is not compatible');
  414. return;
  415. }
  416. //
  417. // Set parameters based on request type.
  418. //
  419. var query_low = region.get('start');
  420. if (req_type === this.DEEP_DATA_REQ) {
  421. // Use same interval but set start_val to skip data that's already in cur_data.
  422. $.extend(extra_params, {start_val: cur_data.data.length + 1});
  423. }
  424. else if (req_type === this.BROAD_DATA_REQ) {
  425. // To get past an area of extreme feature depth, set query low to be after either
  426. // (a) the maximum high or HACK/FIXME (b) the end of the last feature returned.
  427. query_low = (cur_data.max_high ? cur_data.max_high : cur_data.data[cur_data.data.length - 1][2]) + 1;
  428. }
  429. var query_region = region.copy().set('start', query_low);
  430. //
  431. // Get additional data, append to current data, and set new data. Use a custom deferred object
  432. // to signal when new data is available.
  433. //
  434. var data_manager = this,
  435. new_data_request = this.load_data(query_region, mode, resolution, extra_params),
  436. new_data_available = $.Deferred();
  437. // load_data sets cache to new_data_request, but use custom deferred object so that signal and data
  438. // is all data, not just new data.
  439. this.set_data(region, new_data_available);
  440. $.when(new_data_request).then(function(result) {
  441. // Update data and message.
  442. if (result.data) {
  443. result.data = cur_data.data.concat(result.data);
  444. if (result.max_low) {
  445. result.max_low = cur_data.max_low;
  446. }
  447. if (result.message) {
  448. // HACK: replace number in message with current data length. Works but is ugly.
  449. result.message = result.message.replace(/[0-9]+/, result.data.length);
  450. }
  451. }
  452. data_manager.set_data(region, result);
  453. new_data_available.resolve(result);
  454. });
  455. return new_data_available;
  456. },
  457. /**
  458. * Returns true if more detailed data can be obtained for entry.
  459. */
  460. can_get_more_detailed_data: function(region) {
  461. var cur_data = this.get_elt(region);
  462. // Can only get more detailed data for bigwig data that has less than 8000 data points.
  463. // Summary tree returns *way* too much data, and 8000 data points ~ 500KB.
  464. return (cur_data.dataset_type === 'bigwig' && cur_data.data.length < 8000);
  465. },
  466. /**
  467. * Returns more detailed data for an entry.
  468. */
  469. get_more_detailed_data: function(region, mode, resolution, detail_multiplier, extra_params) {
  470. // Mark current entry as stale.
  471. var cur_data = this._mark_stale(region);
  472. if (!cur_data) {
  473. console.log("ERROR getting more detailed data: no current data");
  474. return;
  475. }
  476. if (!extra_params) { extra_params = {}; }
  477. // Use additional parameters to get more detailed data.
  478. if (cur_data.dataset_type === 'bigwig') {
  479. // FIXME: constant should go somewhere.
  480. extra_params.num_samples = 1000 * detail_multiplier;
  481. }
  482. return this.load_data(region, mode, resolution, extra_params);
  483. },
  484. /**
  485. * Marks cache data as stale.
  486. */
  487. _mark_stale: function(region) {
  488. var entry = this.get_elt(region);
  489. if (!entry) {
  490. console.log("ERROR: no data to mark as stale: ", this.get('dataset'), region.toString());
  491. }
  492. entry.stale = true;
  493. return entry;
  494. },
  495. /**
  496. * Returns an array of data with each entry representing one chromosome/contig
  497. * of data or, if data is not available, returns a Deferred that resolves to the
  498. * data when it becomes available.
  499. */
  500. get_genome_wide_data: function(genome) {
  501. // -- Get all data. --
  502. var self = this,
  503. all_data_available = true,
  504. // Map chromosome info into genome data.
  505. gw_data = _.map(genome.get('chroms_info').chrom_info, function(chrom_info) {
  506. var chrom_data = self.get_elt(
  507. new GenomeRegion({
  508. chrom: chrom_info.chrom,
  509. start: 0,
  510. end: chrom_info.len
  511. })
  512. );
  513. // Set flag if data is not available.
  514. if (!chrom_data) { all_data_available = false; }
  515. return chrom_data;
  516. });
  517. // -- If all data is available, return it. --
  518. if (all_data_available) {
  519. return gw_data;
  520. }
  521. // -- All data is not available, so load from server. --
  522. var deferred = $.Deferred();
  523. $.getJSON(this.get('dataset').url(), { data_type: 'genome_data' }, function(genome_wide_data) {
  524. self.add_data(genome_wide_data.data);
  525. deferred.resolve(genome_wide_data.data);
  526. });
  527. return deferred;
  528. },
  529. /**
  530. * Returns entry with only data in the subregion.
  531. */
  532. subset_entry: function(entry, subregion) {
  533. // Dictionary from entry type to function for subsetting data.
  534. var subset_fns = {
  535. bigwig: function(data, subregion) {
  536. return _.filter(data, function(data_point) {
  537. return data_point[0] >= subregion.get('start') &&
  538. data_point[0] <= subregion.get('end');
  539. });
  540. },
  541. refseq: function(data, subregion) {
  542. var seq_start = subregion.get('start') - entry.region.get('start');
  543. return entry.data.slice(seq_start, seq_start + subregion.length());
  544. }
  545. };
  546. // Subset entry if there is a function for subsetting and regions are not the same.
  547. var subregion_data = entry.data;
  548. if (!entry.region.same(subregion) && entry.dataset_type in subset_fns) {
  549. subregion_data = subset_fns[entry.dataset_type](entry.data, subregion);
  550. }
  551. // Return entry with subregion's data.
  552. return {
  553. region: subregion,
  554. data: subregion_data,
  555. dataset_type: entry.dataset_type
  556. };
  557. }
  558. });
  559. var GenomeReferenceDataManager = GenomeDataManager.extend({
  560. initialize: function(options) {
  561. // Use generic object in place of dataset and set urlRoot to fetch data.
  562. var dataset_placeholder = new Backbone.Model();
  563. dataset_placeholder.urlRoot = options.data_url;
  564. this.set('dataset', dataset_placeholder);
  565. },
  566. load_data: function(region, mode, resolution, extra_params) {
  567. // Fetch data if region is not too large.
  568. return ( region.length() <= 100000 ?
  569. GenomeDataManager.prototype.load_data.call(this, region, mode, resolution, extra_params) :
  570. { data: null, region: region } );
  571. }
  572. });
  573. /**
  574. * A genome build.
  575. */
  576. var Genome = Backbone.Model.extend({
  577. defaults: {
  578. name: null,
  579. key: null,
  580. chroms_info: null
  581. },
  582. initialize: function(options) {
  583. this.id = options.dbkey;
  584. },
  585. /**
  586. * Shorthand for getting to chromosome information.
  587. */
  588. get_chroms_info: function() {
  589. return this.attributes.chroms_info.chrom_info;
  590. },
  591. /**
  592. * Returns a GenomeRegion object denoting a complete chromosome.
  593. */
  594. get_chrom_region: function(chr_name) {
  595. // FIXME: use findWhere in underscore 1.4
  596. var chrom_info = _.find(this.get_chroms_info(), function(chrom_info) {
  597. return chrom_info.chrom === chr_name;
  598. });
  599. return new GenomeRegion({
  600. chrom: chrom_info.chrom,
  601. end: chrom_info.len
  602. });
  603. },
  604. /** Returns the length of a chromosome. */
  605. get_chrom_len: function(chr_name) {
  606. // FIXME: use findWhere in underscore 1.4
  607. return _.find(this.get_chroms_info(), function(chrom_info) {
  608. return chrom_info.chrom === chr_name;
  609. }).len;
  610. }
  611. });
  612. /**
  613. * A genomic region.
  614. */
  615. var GenomeRegion = Backbone.Model.extend({
  616. defaults: {
  617. chrom: null,
  618. start: 0,
  619. end: 0,
  620. str_val: null,
  621. genome: null
  622. },
  623. /**
  624. * Returns true if this region is the same as a given region.
  625. * It does not test the genome right now.
  626. */
  627. same: function(region) {
  628. return this.attributes.chrom === region.get('chrom') &&
  629. this.attributes.start === region.get('start') &&
  630. this.attributes.end === region.get('end');
  631. },
  632. /**
  633. * If from_str specified, use it to initialize attributes.
  634. */
  635. initialize: function(options) {
  636. if (options.from_str) {
  637. var pieces = options.from_str.split(':'),
  638. chrom = pieces[0],
  639. start_end = pieces[1].split('-');
  640. this.set({
  641. chrom: chrom,
  642. start: parseInt(start_end[0], 10),
  643. end: parseInt(start_end[1], 10)
  644. });
  645. }
  646. // Keep a copy of region's string value for fast lookup.
  647. this.attributes.str_val = this.get('chrom') + ":" + this.get('start') + "-" + this.get('end');
  648. // Set str_val on attribute change.
  649. this.on('change', function() {
  650. this.attributes.str_val = this.get('chrom') + ":" + this.get('start') + "-" + this.get('end');
  651. }, this);
  652. },
  653. copy: function() {
  654. return new GenomeRegion({
  655. chrom: this.get('chrom'),
  656. start: this.get('start'),
  657. end: this.get('end')
  658. });
  659. },
  660. length: function() {
  661. return this.get('end') - this.get('start');
  662. },
  663. /** Returns region in canonical form chrom:start-end */
  664. toString: function() {
  665. return this.attributes.str_val;
  666. },
  667. toJSON: function() {
  668. return {
  669. chrom: this.get('chrom'),
  670. start: this.get('start'),
  671. end: this.get('end')
  672. };
  673. },
  674. /**
  675. * Compute the type of overlap between this region and another region. The overlap is computed relative to the given/second region;
  676. * hence, OVERLAP_START indicates that the first region overlaps the start (but not the end) of the second region.
  677. */
  678. compute_overlap: function(a_region) {
  679. var first_chrom = this.get('chrom'), second_chrom = a_region.get('chrom'),
  680. first_start = this.get('start'), second_start = a_region.get('start'),
  681. first_end = this.get('end'), second_end = a_region.get('end'),
  682. overlap;
  683. // Compare chroms.
  684. if (first_chrom && second_chrom && first_chrom !== second_chrom) {
  685. return GenomeRegion.overlap_results.DIF_CHROMS;
  686. }
  687. // Compare regions.
  688. if (first_start < second_start) {
  689. if (first_end < second_start) {
  690. overlap = GenomeRegion.overlap_results.BEFORE;
  691. }
  692. else if (first_end < second_end) {
  693. overlap = GenomeRegion.overlap_results.OVERLAP_START;
  694. }
  695. else { // first_end >= second_end
  696. overlap = GenomeRegion.overlap_results.CONTAINS;
  697. }
  698. }
  699. else if (first_start > second_start) {
  700. if (first_start > second_end) {
  701. overlap = GenomeRegion.overlap_results.AFTER;
  702. }
  703. else if (first_end <= second_end) {
  704. overlap = GenomeRegion.overlap_results.CONTAINED_BY;
  705. }
  706. else {
  707. overlap = GenomeRegion.overlap_results.OVERLAP_END;
  708. }
  709. }
  710. else { // first_start === second_start
  711. overlap = (first_end >= second_end ?
  712. GenomeRegion.overlap_results.CONTAINS :
  713. GenomeRegion.overlap_results.CONTAINED_BY);
  714. }
  715. return overlap;
  716. },
  717. /**
  718. * Trim a region to match genome's constraints.
  719. */
  720. trim: function(genome) {
  721. // Assume that all chromosome/contigs start at 0.
  722. if (this.attributes.start < 0) {
  723. this.attributes.start = 0;
  724. }
  725. // Only try to trim the end if genome is set.
  726. if (this.attributes.genome) {
  727. var chrom_len = this.attributes.genome.get_chrom_len(this.attributes.chrom);
  728. if (this.attributes.end > chrom_len) {
  729. this.attributes.end = chrom_len - 1;
  730. }
  731. }
  732. return this;
  733. },
  734. /**
  735. * Returns true if this region contains a given region.
  736. */
  737. contains: function(a_region) {
  738. return this.compute_overlap(a_region) === GenomeRegion.overlap_results.CONTAINS;
  739. },
  740. /**
  741. * Returns true if regions overlap.
  742. */
  743. overlaps: function(a_region) {
  744. return _.intersection( [this.compute_overlap(a_region)],
  745. [GenomeRegion.overlap_results.DIF_CHROMS, GenomeRegion.overlap_results.BEFORE, GenomeRegion.overlap_results.AFTER] ).length === 0;
  746. }
  747. },
  748. {
  749. overlap_results: {
  750. DIF_CHROMS: 1000,
  751. BEFORE: 1001,
  752. CONTAINS: 1002,
  753. OVERLAP_START: 1003,
  754. OVERLAP_END: 1004,
  755. CONTAINED_BY: 1005,
  756. AFTER: 1006
  757. }
  758. });
  759. var GenomeRegionCollection = Backbone.Collection.extend({
  760. model: GenomeRegion
  761. });
  762. /**
  763. * A genome browser bookmark.
  764. */
  765. var BrowserBookmark = Backbone.Model.extend({
  766. defaults: {
  767. region: null,
  768. note: ''
  769. },
  770. initialize: function(options) {
  771. this.set('region', new GenomeRegion(options.region));
  772. }
  773. });
  774. /**
  775. * Bookmarks collection.
  776. */
  777. var BrowserBookmarkCollection = Backbone.Collection.extend({
  778. model: BrowserBookmark
  779. });
  780. /**
  781. * A track of data in a genome visualization.
  782. */
  783. // TODO: rename to Track and merge with Trackster's Track object.
  784. var BackboneTrack = Backbone.Model.extend(CustomToJSON).extend({
  785. defaults: {
  786. mode: 'Auto'
  787. },
  788. initialize: function(options) {
  789. this.set('dataset', new data_mod.Dataset(options.dataset));
  790. // -- Set up config settings. --
  791. var models = [
  792. { key: 'name', default_value: this.get('dataset').get('name') },
  793. { key: 'color' },
  794. { key: 'min_value', label: 'Min Value', type: 'float', default_value: 0 },
  795. { key: 'max_value', label: 'Max Value', type: 'float', default_value: 1 }
  796. ];
  797. this.set('config', config_mod.ConfigSettingCollection.from_models_and_saved_values(models, options.prefs));
  798. // -- Set up data manager. --
  799. var preloaded_data = this.get('preloaded_data');
  800. if (preloaded_data) {
  801. preloaded_data = preloaded_data.data;
  802. }
  803. else {
  804. preloaded_data = [];
  805. }
  806. this.set('data_manager', new GenomeDataManager({
  807. dataset: this.get('dataset'),
  808. init_data: preloaded_data
  809. }));
  810. }
  811. },
  812. {
  813. // This definition matches that produced by to_dict() methods in tracks.js
  814. to_json_keys: [
  815. 'track_type',
  816. 'dataset',
  817. 'prefs',
  818. 'mode',
  819. 'filters',
  820. 'tool_state'
  821. ],
  822. to_json_mappers: {
  823. prefs: function(p, self) {
  824. if (_.size(p) === 0) {
  825. p = {
  826. name: self.get('config').get('name').get('value'),
  827. color: self.get('config').get('color').get('value')
  828. };
  829. }
  830. return p;
  831. },
  832. dataset: function(d) {
  833. return {
  834. id: d.id,
  835. hda_ldda: d.get('hda_ldda')
  836. };
  837. }
  838. }
  839. });
  840. var BackboneTrackCollection = Backbone.Collection.extend({
  841. model: BackboneTrack
  842. });
  843. /**
  844. * A visualization.
  845. */
  846. var Visualization = Backbone.Model.extend({
  847. defaults: {
  848. title: '',
  849. type: ''
  850. },
  851. urlRoot: galaxy_config.root + "api/visualizations",
  852. /**
  853. * POSTs visualization's JSON to its URL using the parameter 'vis_json'
  854. * Note: This is necessary because (a) Galaxy requires keyword args and
  855. * (b) Galaxy does not handle PUT now.
  856. */
  857. save: function() {
  858. return $.ajax({
  859. url: this.url(),
  860. type: "POST",
  861. dataType: "json",
  862. data: {
  863. vis_json: JSON.stringify(this)
  864. }
  865. });
  866. }
  867. });
  868. /**
  869. * A visualization of genome data.
  870. */
  871. var GenomeVisualization = Visualization.extend(CustomToJSON).extend({
  872. defaults: _.extend({}, Visualization.prototype.defaults, {
  873. dbkey: '',
  874. drawables: null,
  875. bookmarks: null,
  876. viewport: null
  877. }),
  878. initialize: function(options) {
  879. // Replace drawables with tracks.
  880. this.set('drawables', new BackboneTrackCollection(options.tracks));
  881. var models = [];
  882. this.set('config', config_mod.ConfigSettingCollection.from_models_and_saved_values(models, options.prefs));
  883. // Clear track and data definitions to avoid storing large objects.
  884. this.unset('tracks');
  885. this.get('drawables').each(function(d) {
  886. d.unset('preloaded_data');
  887. });
  888. },
  889. /**
  890. * Add a track or array of tracks to the visualization.
  891. */
  892. add_tracks: function(tracks) {
  893. this.get('drawables').add(tracks);
  894. }
  895. },
  896. {
  897. // This definition matches that produced by to_dict() methods in tracks.js
  898. to_json_keys: [
  899. 'view',
  900. 'viewport',
  901. 'bookmarks'
  902. ],
  903. to_json_mappers: {
  904. 'view': function(dummy, self) {
  905. return {
  906. obj_type: 'View',
  907. prefs: {
  908. name: self.get('title'),
  909. content_visible: true
  910. },
  911. drawables: self.get('drawables')
  912. };
  913. }
  914. }
  915. }
  916. );
  917. /**
  918. * -- Routers --
  919. */
  920. /**
  921. * Router for track browser.
  922. */
  923. var TrackBrowserRouter = Backbone.Router.extend({
  924. initialize: function(options) {
  925. this.view = options.view;
  926. // Can't put regular expression in routes dictionary.
  927. // NOTE: parentheses are used to denote parameters returned to callback.
  928. this.route(/([\w]+)$/, 'change_location');
  929. this.route(/([\w\+]+\:[\d,]+-[\d,]+)$/, 'change_location');
  930. // Handle navigate events from view.
  931. var self = this;
  932. self.view.on("navigate", function(new_loc) {
  933. self.navigate(new_loc);
  934. });
  935. },
  936. change_location: function(new_loc) {
  937. this.view.go_to(new_loc);
  938. }
  939. });
  940. return {
  941. BackboneTrack: BackboneTrack,
  942. BrowserBookmark: BrowserBookmark,
  943. BrowserBookmarkCollection: BrowserBookmarkCollection,
  944. Cache: Cache,
  945. CanvasManager: CanvasManager,
  946. Genome: Genome,
  947. GenomeDataManager: GenomeDataManager,
  948. GenomeRegion: GenomeRegion,
  949. GenomeRegionCollection: GenomeRegionCollection,
  950. GenomeVisualization: GenomeVisualization,
  951. GenomeReferenceDataManager: GenomeReferenceDataManager,
  952. TrackBrowserRouter: TrackBrowserRouter,
  953. Visualization: Visualization,
  954. select_datasets: select_datasets
  955. };
  956. });