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

/static/scripts/viz/visualization.js

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