PageRenderTime 54ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/client/galaxy/scripts/viz/visualization.js

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