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

/static/scripts/mvc/collection/paired-collection-creator.js

https://bitbucket.org/hbc/galaxy-central-hbc/
JavaScript | 1488 lines | 1053 code | 141 blank | 294 comment | 122 complexity | 9062354cdae9f1bab57ac483c5f4d564 MD5 | raw file
Possible License(s): CC-BY-3.0
  1. define([
  2. "mvc/base-mvc",
  3. "utils/localization"
  4. ], function( baseMVC, _l ){
  5. //=============================================================================
  6. /** An interface for building collections of paired datasets.
  7. */
  8. var PairedCollectionCreator = Backbone.View.extend( baseMVC.LoggableMixin ).extend({
  9. className: 'collection-creator flex-row-container',
  10. /** set up initial options, instance vars, behaviors, and autopair (if set to do so) */
  11. initialize : function( attributes ){
  12. //console.debug( '-- PairedCollectionCreator:', attributes );
  13. attributes = _.defaults( attributes, {
  14. datasets : [],
  15. filters : this.DEFAULT_FILTERS,
  16. automaticallyPair : true,
  17. matchPercentage : 1.0,
  18. //matchPercentage : 0.9,
  19. //matchPercentage : 0.8,
  20. //strategy : 'levenshtein'
  21. strategy : 'lcs'
  22. });
  23. //console.debug( 'attributes now:', attributes );
  24. /** unordered, original list */
  25. this.initialList = attributes.datasets;
  26. /** is this from a history? if so, what's its id? */
  27. this.historyId = attributes.historyId;
  28. /** which filters should be used initially? (String[2] or name in commonFilters) */
  29. this.filters = this.commonFilters[ attributes.filters ] || this.commonFilters[ this.DEFAULT_FILTERS ];
  30. if( _.isArray( attributes.filters ) ){
  31. this.filters = attributes.filters;
  32. }
  33. /** try to auto pair the unpaired datasets on load? */
  34. this.automaticallyPair = attributes.automaticallyPair;
  35. /** distance/mismatch level allowed for autopairing */
  36. this.matchPercentage = attributes.matchPercentage;
  37. /** what method to use for auto pairing (will be passed aggression level) */
  38. this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ];
  39. if( _.isFunction( attributes.strategy ) ){
  40. this.strategy = attributes.strategy;
  41. }
  42. /** fn to call when the cancel button is clicked (scoped to this) - if falsy, no btn is displayed */
  43. this.oncancel = attributes.oncancel;
  44. /** fn to call when the collection is created (scoped to this) */
  45. this.oncreate = attributes.oncreate;
  46. /** is the unpaired panel shown? */
  47. this.unpairedPanelHidden = false;
  48. /** is the paired panel shown? */
  49. this.pairedPanelHidden = false;
  50. this._dataSetUp();
  51. this._setUpBehaviors();
  52. },
  53. /** map of common filter pairs by name */
  54. commonFilters : {
  55. none : [ '', '' ],
  56. illumina : [ '_1', '_2' ]
  57. },
  58. /** which commonFilter to use by default */
  59. DEFAULT_FILTERS : 'illumina',
  60. //DEFAULT_FILTERS : 'none',
  61. /** map of name->fn for autopairing */
  62. strategies : {
  63. 'lcs' : 'autoPairLCSs',
  64. 'levenshtein' : 'autoPairLevenshtein'
  65. },
  66. /** default autopair strategy name */
  67. DEFAULT_STRATEGY : 'lcs',
  68. // ------------------------------------------------------------------------ process raw list
  69. /** set up main data: cache initialList, sort, and autopair */
  70. _dataSetUp : function(){
  71. //console.debug( '-- _dataSetUp' );
  72. this.paired = [];
  73. this.unpaired = [];
  74. //this.fwdSelectedIds = [];
  75. //this.revSelectedIds = [];
  76. this.selectedIds = [];
  77. // sort initial list, add ids if needed, and save new working copy to unpaired
  78. this._sortInitialList();
  79. this._ensureIds();
  80. this.unpaired = this.initialList.slice( 0 );
  81. if( this.automaticallyPair ){
  82. this.autoPair();
  83. }
  84. },
  85. /** sort initial list */
  86. _sortInitialList : function(){
  87. //console.debug( '-- _sortInitialList' );
  88. this._sortDatasetList( this.initialList );
  89. //this._printList( this.unpaired );
  90. },
  91. /** sort a list of datasets */
  92. _sortDatasetList : function( list ){
  93. // currently only natural sort by name
  94. list.sort( function( a, b ){ return naturalSort( a.name, b.name ); });
  95. return list;
  96. },
  97. /** add ids to dataset objs in initial list if none */
  98. _ensureIds : function(){
  99. this.initialList.forEach( function( dataset ){
  100. if( !dataset.hasOwnProperty( 'id' ) ){
  101. dataset.id = _.uniqueId();
  102. }
  103. });
  104. //this._printList( this.unpaired );
  105. return this.initialList;
  106. },
  107. /** split initial list into two lists, those that pass forward filters & those passing reverse */
  108. _splitByFilters : function( filters ){
  109. var fwd = [],
  110. rev = [];
  111. this.unpaired.forEach( function( unpaired ){
  112. if( this._filterFwdFn( unpaired ) ){
  113. fwd.push( unpaired );
  114. }
  115. if( this._filterRevFn( unpaired ) ){
  116. rev.push( unpaired );
  117. }
  118. }.bind( this ) );
  119. return [ fwd, rev ];
  120. },
  121. /** filter fn to apply to forward datasets */
  122. _filterFwdFn : function( dataset ){
  123. // simple contains for now - add more when new feat. are added (regex, etc.)
  124. return dataset.name.indexOf( this.filters[0] ) >= 0;
  125. },
  126. /** filter fn to apply to reverse datasets */
  127. _filterRevFn : function( dataset ){
  128. return dataset.name.indexOf( this.filters[1] ) >= 0;
  129. },
  130. /** add a dataset to the unpaired list in it's proper order */
  131. _addToUnpaired : function( dataset ){
  132. // currently, unpaired is natural sorted by name, use binary search to find insertion point
  133. var binSearchSortedIndex = function( low, hi ){
  134. if( low === hi ){ return low; }
  135. var mid = Math.floor( ( hi - low ) / 2 ) + low,
  136. compared = naturalSort( dataset.name, this.unpaired[ mid ].name );
  137. if( compared < 0 ){
  138. return binSearchSortedIndex( low, mid );
  139. } else if( compared > 0 ){
  140. return binSearchSortedIndex( mid + 1, hi );
  141. }
  142. // walk the equal to find the last
  143. while( this.unpaired[ mid ] && this.unpaired[ mid ].name === dataset.name ){ mid++; }
  144. return mid;
  145. }.bind( this );
  146. this.unpaired.splice( binSearchSortedIndex( 0, this.unpaired.length ), 0, dataset );
  147. },
  148. // ------------------------------------------------------------------------ auto pairing
  149. //TODO: lots of boiler plate btwn the three auto pair fns
  150. /** two passes to automatically create pairs:
  151. * use both simpleAutoPair, then the fn mentioned in strategy
  152. */
  153. autoPair : function( strategy ){
  154. strategy = strategy || this.strategy;
  155. //console.debug( '-- autoPair', strategy );
  156. this.simpleAutoPair();
  157. return this[ strategy ].call( this );
  158. },
  159. /** attempts to pair forward with reverse when names exactly match (after removing filters) */
  160. simpleAutoPair : function(){
  161. //console.debug( '-- simpleAutoPair' );
  162. // simplified auto pair that moves down unpaired lists *in order*,
  163. // removes filters' strings from fwd and rev,
  164. // and, if names w/o filters *exactly* match, creates a pair
  165. // possibly good as a first pass
  166. var i = 0, j,
  167. split = this._splitByFilters(),
  168. fwdList = split[0],
  169. revList = split[1],
  170. fwdName, revName,
  171. matchFound = false;
  172. while( i<fwdList.length ){
  173. var fwd = fwdList[ i ];
  174. //TODO: go through the filterFwdFn
  175. fwdName = fwd.name.replace( this.filters[0], '' );
  176. //console.debug( i, 'fwd:', fwdName );
  177. matchFound = false;
  178. for( j=0; j<revList.length; j++ ){
  179. var rev = revList[ j ];
  180. revName = rev.name.replace( this.filters[1], '' );
  181. //console.debug( '\t ', j, 'rev:', revName );
  182. if( fwd !== rev && fwdName === revName ){
  183. matchFound = true;
  184. // if it is a match, keep i at current, pop fwd, pop rev and break
  185. //console.debug( '---->', fwdName, revName );
  186. this._pair(
  187. fwdList.splice( i, 1 )[0],
  188. revList.splice( j, 1 )[0],
  189. { silent: true }
  190. );
  191. break;
  192. }
  193. }
  194. if( !matchFound ){ i += 1; }
  195. }
  196. //console.debug( 'remaining Forward:' );
  197. //this._printList( this.unpairedForward );
  198. //console.debug( 'remaining Reverse:' );
  199. //this._printList( this.unpairedReverse );
  200. //console.debug( '' );
  201. },
  202. /** attempt to autopair using edit distance between forward and reverse (after removing filters) */
  203. autoPairLevenshtein : function(){
  204. //precondition: filters are set, both lists are not empty, and all filenames.length > filters[?].length
  205. //console.debug( '-- autoPairLevenshtein' );
  206. var i = 0, j,
  207. split = this._splitByFilters(),
  208. fwdList = split[0],
  209. revList = split[1],
  210. fwdName, revName,
  211. distance, bestIndex, bestDist;
  212. while( i<fwdList.length ){
  213. var fwd = fwdList[ i ];
  214. //TODO: go through the filterFwdFn
  215. fwdName = fwd.name.replace( this.filters[0], '' );
  216. //console.debug( i, 'fwd:', fwdName );
  217. bestDist = Number.MAX_VALUE;
  218. for( j=0; j<revList.length; j++ ){
  219. var rev = revList[ j ];
  220. revName = rev.name.replace( this.filters[1], '' );
  221. //console.debug( '\t ', j, 'rev:', revName );
  222. if( fwd !== rev ){
  223. if( fwdName === revName ){
  224. //console.debug( '\t\t exactmatch:', fwdName, revName );
  225. bestIndex = j;
  226. bestDist = 0;
  227. break;
  228. }
  229. distance = levenshteinDistance( fwdName, revName );
  230. //console.debug( '\t\t distance:', distance, 'bestDist:', bestDist );
  231. if( distance < bestDist ){
  232. bestIndex = j;
  233. bestDist = distance;
  234. }
  235. }
  236. }
  237. //console.debug( '---->', fwd.name, bestIndex, bestDist );
  238. //console.debug( '---->', fwd.name, revList[ bestIndex ].name, bestDist );
  239. var percentage = 1.0 - ( bestDist / ( Math.max( fwdName.length, revName.length ) ) );
  240. //console.debug( '----> %', percentage * 100 );
  241. if( percentage >= this.matchPercentage ){
  242. this._pair(
  243. fwdList.splice( i, 1 )[0],
  244. revList.splice( bestIndex, 1 )[0],
  245. { silent: true }
  246. );
  247. if( fwdList.length <= 0 || revList.length <= 0 ){
  248. return;
  249. }
  250. } else {
  251. i += 1;
  252. }
  253. }
  254. //console.debug( 'remaining Forward:' );
  255. //this._printList( this.unpairedForward );
  256. //console.debug( 'remaining Reverse:' );
  257. //this._printList( this.unpairedReverse );
  258. //console.debug( '' );
  259. },
  260. /** attempt to auto pair using common substrings from both front and back (after removing filters) */
  261. autoPairLCSs : function(){
  262. //precondition: filters are set, both lists are not empty
  263. //console.debug( '-- autoPairLCSs' );
  264. var i = 0, j,
  265. split = this._splitByFilters(),
  266. fwdList = split[0],
  267. revList = split[1],
  268. fwdName, revName,
  269. currMatch, bestIndex, bestMatch;
  270. if( !fwdList.length || !revList.length ){ return; }
  271. //console.debug( fwdList, revList );
  272. while( i<fwdList.length ){
  273. var fwd = fwdList[ i ];
  274. fwdName = fwd.name.replace( this.filters[0], '' );
  275. //console.debug( i, 'fwd:', fwdName );
  276. bestMatch = 0;
  277. for( j=0; j<revList.length; j++ ){
  278. var rev = revList[ j ];
  279. revName = rev.name.replace( this.filters[1], '' );
  280. //console.debug( '\t ', j, 'rev:', revName );
  281. if( fwd !== rev ){
  282. if( fwdName === revName ){
  283. //console.debug( '\t\t exactmatch:', fwdName, revName );
  284. bestIndex = j;
  285. bestMatch = fwdName.length;
  286. break;
  287. }
  288. var match = this._naiveStartingAndEndingLCS( fwdName, revName );
  289. currMatch = match.length;
  290. //console.debug( '\t\t match:', match, 'currMatch:', currMatch, 'bestMatch:', bestMatch );
  291. if( currMatch > bestMatch ){
  292. bestIndex = j;
  293. bestMatch = currMatch;
  294. }
  295. }
  296. }
  297. //console.debug( '---->', i, fwd.name, bestIndex, revList[ bestIndex ].name, bestMatch );
  298. var percentage = bestMatch / ( Math.min( fwdName.length, revName.length ) );
  299. //console.debug( '----> %', percentage * 100 );
  300. if( percentage >= this.matchPercentage ){
  301. this._pair(
  302. fwdList.splice( i, 1 )[0],
  303. revList.splice( bestIndex, 1 )[0],
  304. { silent: true }
  305. );
  306. if( fwdList.length <= 0 || revList.length <= 0 ){
  307. return;
  308. }
  309. } else {
  310. i += 1;
  311. }
  312. }
  313. //console.debug( 'remaining Forward:' );
  314. //this._printList( this.unpairedForward );
  315. //console.debug( 'remaining Reverse:' );
  316. //this._printList( this.unpairedReverse );
  317. //console.debug( '' );
  318. },
  319. /** return the concat'd longest common prefix and suffix from two strings */
  320. _naiveStartingAndEndingLCS : function( s1, s2 ){
  321. var fwdLCS = '',
  322. revLCS = '',
  323. i = 0, j = 0;
  324. while( i < s1.length && i < s2.length ){
  325. if( s1[ i ] !== s2[ i ] ){
  326. break;
  327. }
  328. fwdLCS += s1[ i ];
  329. i += 1;
  330. }
  331. if( i === s1.length ){ return s1; }
  332. if( i === s2.length ){ return s2; }
  333. i = ( s1.length - 1 );
  334. j = ( s2.length - 1 );
  335. while( i >= 0 && j >= 0 ){
  336. if( s1[ i ] !== s2[ j ] ){
  337. break;
  338. }
  339. revLCS = [ s1[ i ], revLCS ].join( '' );
  340. i -= 1;
  341. j -= 1;
  342. }
  343. return fwdLCS + revLCS;
  344. },
  345. // ------------------------------------------------------------------------ pairing / unpairing
  346. /** create a pair from fwd and rev, removing them from unpaired, and placing the new pair in paired */
  347. _pair : function( fwd, rev, options ){
  348. options = options || {};
  349. //TODO: eventing, options
  350. //console.debug( '_pair:', fwd, rev );
  351. var pair = this._createPair( fwd, rev, options.name );
  352. this.paired.push( pair );
  353. this.unpaired = _.without( this.unpaired, fwd, rev );
  354. if( !options.silent ){
  355. this.trigger( 'pair:new', pair );
  356. }
  357. return pair;
  358. },
  359. /** create a pair Object from fwd and rev, adding the name attribute (will guess if not given) */
  360. _createPair : function( fwd, rev, name ){
  361. // ensure existance and don't pair something with itself
  362. if( !( fwd && rev ) || ( fwd === rev ) ){
  363. throw new Error( 'Bad pairing: ' + [ JSON.stringify( fwd ), JSON.stringify( rev ) ] );
  364. }
  365. name = name || this._guessNameForPair( fwd, rev );
  366. return { forward : fwd, name : name, reverse : rev };
  367. },
  368. /** try to find a good pair name for the given fwd and rev datasets */
  369. _guessNameForPair : function( fwd, rev ){
  370. var lcs = this._naiveStartingAndEndingLCS(
  371. fwd.name.replace( this.filters[0], '' ),
  372. rev.name.replace( this.filters[1], '' )
  373. );
  374. //TODO: optionally remove extension
  375. return lcs || ( fwd.name + ' & ' + rev.name );
  376. },
  377. ///** find datasets with fwdId and revID and pair them */
  378. //_pairById : function( fwdId, revId, name ){
  379. // var both = this.unpaired.filter( function( unpaired ){
  380. // return unpaired.id === fwdId || unpaired.id === revId;
  381. // }),
  382. // fwd = both[0], rev = both[1];
  383. // if( both[0].id === revId ){
  384. // fwd = rev; rev = both[0];
  385. // }
  386. // return this._pair( fwd, rev, name );
  387. //},
  388. /** unpair a pair, removing it from paired, and adding the fwd,rev datasets back into unpaired */
  389. _unpair : function( pair, options ){
  390. options = options || {};
  391. if( !pair ){
  392. throw new Error( 'Bad pair: ' + JSON.stringify( pair ) );
  393. }
  394. this.paired = _.without( this.paired, pair );
  395. this._addToUnpaired( pair.forward );
  396. this._addToUnpaired( pair.reverse );
  397. if( !options.silent ){
  398. this.trigger( 'pair:unpair', [ pair ] );
  399. }
  400. return pair;
  401. },
  402. /** unpair all paired datasets */
  403. unpairAll : function(){
  404. var pairs = [];
  405. while( this.paired.length ){
  406. pairs.push( this._unpair( this.paired[ 0 ], { silent: true }) );
  407. }
  408. this.trigger( 'pair:unpair', pairs );
  409. },
  410. // ------------------------------------------------------------------------ API
  411. /** convert a pair into JSON compatible with the collections API */
  412. _pairToJSON : function( pair ){
  413. //TODO: consider making this the pair structure when created instead
  414. return {
  415. collection_type : 'paired',
  416. src : 'new_collection',
  417. name : pair.name,
  418. element_identifiers : [{
  419. name : 'forward',
  420. id : pair.forward.id,
  421. //TODO: isn't necessarily true
  422. src : 'hda'
  423. }, {
  424. name : 'reverse',
  425. id : pair.reverse.id,
  426. //TODO: isn't necessarily true
  427. src : 'hda'
  428. }]
  429. };
  430. },
  431. /** create the collection via the API
  432. * @returns {jQuery.xhr Object} the jquery ajax request
  433. */
  434. createList : function(){
  435. var creator = this,
  436. url;
  437. if( creator.historyId ){
  438. url = '/api/histories/' + this.historyId + '/contents/dataset_collections';
  439. //} else {
  440. //
  441. }
  442. //var xhr = jQuery.ajax({
  443. var ajaxData = {
  444. type : 'dataset_collection',
  445. collection_type : 'list:paired',
  446. name : _.escape( creator.$( '.collection-name' ).val() ),
  447. element_identifiers : creator.paired.map( function( pair ){
  448. return creator._pairToJSON( pair );
  449. })
  450. };
  451. //console.debug( JSON.stringify( ajaxData ) );
  452. return jQuery.ajax( url, {
  453. type : 'POST',
  454. contentType : 'application/json',
  455. data : JSON.stringify( ajaxData )
  456. })
  457. .fail( function( xhr, status, message ){
  458. //TODO: extract method
  459. console.error( xhr, status, message );
  460. var content = _l( 'An error occurred while creating this collection' );
  461. if( xhr ){
  462. if( xhr.readyState === 0 && xhr.status === 0 ){
  463. content += ': ' + _l( 'Galaxy could not be reached and may be updating.' )
  464. + _l( ' Try again in a few minutes.' );
  465. } else if( xhr.responseJSON ){
  466. content += '<br /><pre>' + JSON.stringify( xhr.responseJSON ) + '</pre>';
  467. } else {
  468. content += ': ' + message;
  469. }
  470. }
  471. creator._showAlert( content, 'alert-danger' );
  472. })
  473. .done( function( response, message, xhr ){
  474. //console.info( 'ok', response, message, xhr );
  475. creator.trigger( 'collection:created', response, message, xhr );
  476. if( typeof creator.oncreate === 'function' ){
  477. creator.oncreate.call( this, response, message, xhr );
  478. }
  479. });
  480. },
  481. // ------------------------------------------------------------------------ rendering
  482. /** render the entire interface */
  483. render : function( speed, callback ){
  484. //console.debug( '-- _render' );
  485. //this.$el.empty().html( PairedCollectionCreator.templates.main() );
  486. this.$el.empty().html( PairedCollectionCreator.templates.main() );
  487. this._renderHeader( speed );
  488. this._renderMiddle( speed );
  489. this._renderFooter( speed );
  490. this._addPluginComponents();
  491. return this;
  492. },
  493. /** render the header section */
  494. _renderHeader : function( speed, callback ){
  495. //console.debug( '-- _renderHeader' );
  496. var $header = this.$( '.header' ).empty().html( PairedCollectionCreator.templates.header() )
  497. .find( '.help-content' ).prepend( $( PairedCollectionCreator.templates.helpContent() ) );
  498. this._renderFilters();
  499. return $header;
  500. },
  501. /** fill the filter inputs with the filter values */
  502. _renderFilters : function(){
  503. return this.$( '.forward-column .column-header input' ).val( this.filters[0] )
  504. .add( this.$( '.reverse-column .column-header input' ).val( this.filters[1] ) );
  505. },
  506. /** render the middle including unpaired and paired sections (which may be hidden) */
  507. _renderMiddle : function( speed, callback ){
  508. var $middle = this.$( '.middle' ).empty().html( PairedCollectionCreator.templates.middle() );
  509. //TODO: use replaceWith
  510. if( this.unpairedPanelHidden ){
  511. this.$( '.unpaired-columns' ).hide();
  512. } else if( this.pairedPanelHidden ){
  513. this.$( '.paired-columns' ).hide();
  514. }
  515. this._renderUnpaired();
  516. this._renderPaired();
  517. return $middle;
  518. },
  519. /** render the unpaired section, showing datasets accrd. to filters, update the unpaired counts */
  520. _renderUnpaired : function( speed, callback ){
  521. //console.debug( '-- _renderUnpaired' );
  522. var creator = this,
  523. $fwd, $rev, $prd = [],
  524. split = this._splitByFilters();
  525. // update unpaired counts
  526. this.$( '.forward-column .title' )
  527. .text([ split[0].length, _l( 'unpaired forward' ) ].join( ' ' ));
  528. this.$( '.forward-column .unpaired-info' )
  529. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[0].length ) );
  530. this.$( '.reverse-column .title' )
  531. .text([ split[1].length, _l( 'unpaired reverse' ) ].join( ' ' ));
  532. this.$( '.reverse-column .unpaired-info' )
  533. .text( this._renderUnpairedDisplayStr( this.unpaired.length - split[1].length ) );
  534. // show/hide the auto pair button if any unpaired are left
  535. this.$( '.autopair-link' ).toggle( this.unpaired.length !== 0 );
  536. if( this.unpaired.length === 0 ){
  537. this._renderUnpairedEmpty();
  538. //TODO: would be best to return here (the $columns)
  539. }
  540. // create the dataset dom arrays
  541. $rev = split[1].map( function( dataset, i ){
  542. // if there'll be a fwd dataset across the way, add a button to pair the row
  543. if( ( split[0][ i ] !== undefined )
  544. && ( split[0][ i ] !== dataset ) ){
  545. $prd.push( creator._renderPairButton() );
  546. }
  547. return creator._renderUnpairedDataset( dataset );
  548. });
  549. $fwd = split[0].map( function( dataset ){
  550. return creator._renderUnpairedDataset( dataset );
  551. });
  552. // add to appropo cols
  553. //TODO: not the best way to render
  554. this.$( '.unpaired-columns .column-datasets' ).empty();
  555. return this.$( '.unpaired-columns .forward-column .column-datasets' ).append( $fwd )
  556. .add( this.$( '.unpaired-columns .paired-column .column-datasets' ).append( $prd ) )
  557. .add( this.$( '.unpaired-columns .reverse-column .column-datasets' ).append( $rev ) );
  558. },
  559. /** return a string to display the count of filtered out datasets */
  560. _renderUnpairedDisplayStr : function( numFiltered ){
  561. return [ '(', numFiltered, ' ', _l( 'filtered out' ), ')' ].join('');
  562. },
  563. /** return an unattached jQuery DOM element to represent an unpaired dataset */
  564. _renderUnpairedDataset : function( dataset ){
  565. //TODO: to underscore template
  566. return $( '<li/>')
  567. .attr( 'id', 'dataset-' + dataset.id )
  568. .addClass( 'dataset unpaired' )
  569. .attr( 'draggable', true )
  570. .addClass( dataset.selected? 'selected': '' )
  571. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( dataset.name ) )
  572. //??
  573. .data( 'dataset', dataset );
  574. },
  575. /** render the button that may go between unpaired datasets, allowing the user to pair a row */
  576. _renderPairButton : function(){
  577. //TODO: *not* a dataset - don't pretend like it is
  578. return $( '<li/>').addClass( 'dataset unpaired' )
  579. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( _l( 'Pair these datasets' ) ) );
  580. },
  581. /** a message to display when no unpaired left */
  582. _renderUnpairedEmpty : function(){
  583. //console.debug( '-- renderUnpairedEmpty' );
  584. var $msg = $( '<div class="empty-message"></div>' )
  585. .text( '(' + _l( 'no remaining unpaired datasets' ) + ')' );
  586. this.$( '.unpaired-columns .paired-column .column-datasets' ).prepend( $msg );
  587. return $msg;
  588. },
  589. /** render the paired section and update counts of paired datasets */
  590. _renderPaired : function( speed, callback ){
  591. //console.debug( '-- _renderPaired' );
  592. var $fwd = [],
  593. $prd = [],
  594. $rev = [];
  595. this.$( '.paired-column-title .title' ).text([ this.paired.length, _l( 'paired' ) ].join( ' ' ) );
  596. // show/hide the unpair all link
  597. this.$( '.unpair-all-link' ).toggle( this.paired.length !== 0 );
  598. if( this.paired.length === 0 ){
  599. this._renderPairedEmpty();
  600. //TODO: would be best to return here (the $columns)
  601. }
  602. this.paired.forEach( function( pair, i ){
  603. $fwd.push( $( '<li/>').addClass( 'dataset paired' )
  604. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.forward.name ) ) );
  605. $prd.push( $( '<li/>').addClass( 'dataset paired' )
  606. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.name ) ) );
  607. $rev.push( $( '<li/>').addClass( 'dataset paired' )
  608. .append( $( '<span/>' ).addClass( 'dataset-name' ).text( pair.reverse.name ) ) );
  609. $rev.push( $( '<button/>' ).addClass( 'unpair-btn' )
  610. .append( $( '<span/>' ).addClass( 'fa fa-unlink' ).attr( 'title', _l( 'Unpair' ) ) ) );
  611. });
  612. //TODO: better as one swell foop (instead of 4 repaints)
  613. this.$( '.paired-columns .column-datasets' ).empty();
  614. return this.$( '.paired-columns .forward-column .column-datasets' ).prepend( $fwd )
  615. .add( this.$( '.paired-columns .paired-column .column-datasets' ).prepend( $prd ) )
  616. .add( this.$( '.paired-columns .reverse-column .column-datasets' ).prepend( $rev ) );
  617. },
  618. /** a message to display when none paired */
  619. _renderPairedEmpty : function(){
  620. var $msg = $( '<div class="empty-message"></div>' )
  621. .text( '(' + _l( 'no paired datasets yet' ) + ')' );
  622. this.$( '.paired-columns .paired-column .column-datasets' ).prepend( $msg );
  623. return $msg;
  624. },
  625. /** render the footer, completion controls, and cancel controls */
  626. _renderFooter : function( speed, callback ){
  627. var $footer = this.$( '.footer' ).empty().html( PairedCollectionCreator.templates.footer() );
  628. if( typeof this.oncancel === 'function' ){
  629. this.$( '.cancel-create.btn' ).show();
  630. }
  631. return $footer;
  632. },
  633. /** add any jQuery/bootstrap/custom plugins to elements rendered */
  634. _addPluginComponents : function(){
  635. this._chooseFiltersPopover( '.choose-filters-link' );
  636. this.$( '.help-content i' ).hoverhighlight( '.collection-creator', 'rgba( 192, 255, 255, 1.0 )' );
  637. },
  638. /** build a filter selection popover allowing selection of common filter pairs */
  639. _chooseFiltersPopover : function( selector ){
  640. function filterChoice( val1, val2 ){
  641. return [
  642. '<button class="filter-choice btn" ',
  643. 'data-forward="', val1, '" data-reverse="', val2, '">',
  644. _l( 'Forward' ), ': ', val1, ', ',
  645. _l( 'Reverse' ), ': ', val2,
  646. '</button>'
  647. ].join('');
  648. }
  649. var $popoverContent = $( _.template([
  650. '<div class="choose-filters">',
  651. '<div class="help">',
  652. _l( 'Choose from the following filters to change which unpaired reads are shown in the display' ),
  653. ':</div>',
  654. //TODO: connect with common filters
  655. filterChoice( '_1', '_2' ),
  656. filterChoice( '_R1', '_R2' ),
  657. '</div>'
  658. ].join(''), {}));
  659. return this.$( selector ).popover({
  660. container : '.collection-creator',
  661. placement : 'bottom',
  662. html : true,
  663. //animation : false,
  664. content : $popoverContent
  665. });
  666. },
  667. /** add (or clear if clear is truthy) a validation warning to what */
  668. _validationWarning : function( what, clear ){
  669. var VALIDATION_CLASS = 'validation-warning';
  670. if( what === 'name' ){
  671. what = this.$( '.collection-name' ).add( this.$( '.collection-name-prompt' ) );
  672. this.$( '.collection-name' ).focus().select();
  673. }
  674. if( clear ){
  675. what = what || this.$( '.' + VALIDATION_CLASS );
  676. what.removeClass( VALIDATION_CLASS );
  677. } else {
  678. what.addClass( VALIDATION_CLASS );
  679. }
  680. },
  681. // ------------------------------------------------------------------------ events
  682. /** set up event handlers on self */
  683. _setUpBehaviors : function(){
  684. this.on( 'pair:new', function(){
  685. //TODO: ideally only re-render the columns (or even elements) involved
  686. this._renderUnpaired();
  687. this._renderPaired();
  688. // scroll to bottom where new pairs are added
  689. //TODO: this doesn't seem to work - innerHeight sticks at 133...
  690. // may have to do with improper flex columns
  691. //var $pairedView = this.$( '.paired-columns' );
  692. //$pairedView.scrollTop( $pairedView.innerHeight() );
  693. //console.debug( $pairedView.height() )
  694. this.$( '.paired-columns' ).scrollTop( 8000000 );
  695. });
  696. this.on( 'pair:unpair', function( pairs ){
  697. //TODO: ideally only re-render the columns (or even elements) involved
  698. this._renderUnpaired();
  699. this._renderPaired();
  700. });
  701. this.on( 'filter-change', function(){
  702. this.filters = [
  703. this.$( '.forward-unpaired-filter input' ).val(),
  704. this.$( '.reverse-unpaired-filter input' ).val()
  705. ];
  706. this._renderFilters();
  707. this._renderUnpaired();
  708. });
  709. this.on( 'autopair', function(){
  710. this._renderUnpaired();
  711. this._renderPaired();
  712. });
  713. //this.on( 'all', function(){
  714. // console.info( arguments );
  715. //});
  716. return this;
  717. },
  718. events : {
  719. // header
  720. 'click .more-help' : '_clickMoreHelp',
  721. 'click .less-help' : '_clickLessHelp',
  722. 'click .header .alert button' : '_hideAlert',
  723. 'click .forward-column .column-title' : '_clickShowOnlyUnpaired',
  724. 'click .reverse-column .column-title' : '_clickShowOnlyUnpaired',
  725. 'click .unpair-all-link' : '_clickUnpairAll',
  726. //TODO: this seems kinda backasswards
  727. 'change .forward-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  728. 'focus .forward-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  729. 'click .autopair-link' : '_clickAutopair',
  730. 'click .choose-filters .filter-choice' : '_clickFilterChoice',
  731. 'click .clear-filters-link' : '_clearFilters',
  732. 'change .reverse-unpaired-filter input' : function( ev ){ this.trigger( 'filter-change' ); },
  733. 'focus .reverse-unpaired-filter input' : function( ev ){ $( ev.currentTarget ).select(); },
  734. // unpaired
  735. 'click .forward-column .dataset.unpaired' : '_clickUnpairedDataset',
  736. 'click .reverse-column .dataset.unpaired' : '_clickUnpairedDataset',
  737. 'click .paired-column .dataset.unpaired' : '_clickPairRow',
  738. 'click .unpaired-columns' : 'clearSelectedUnpaired',
  739. // divider
  740. 'click .paired-column-title' : '_clickShowOnlyPaired',
  741. 'mousedown .flexible-partition-drag' : '_startPartitionDrag',
  742. // paired
  743. 'click .paired-columns .paired-column .dataset-name' : '_clickPairName',
  744. 'click .unpair-btn' : '_clickUnpair',
  745. 'mouseover .dataset.paired' : '_hoverPaired',
  746. 'mouseout .dataset.paired' : '_hoverOutPaired',
  747. // footer
  748. 'change .collection-name' : '_changeName',
  749. 'click .cancel-create' : function( ev ){
  750. if( typeof this.oncancel === 'function' ){
  751. this.oncancel.call( this );
  752. }
  753. },
  754. 'click .create-collection' : '_clickCreate'
  755. },
  756. // ........................................................................ header
  757. /** expand help */
  758. _clickMoreHelp : function( ev ){
  759. this.$( '.main-help' ).css( 'max-height', 'none' );
  760. this.$( '.main-help .help-content p:first-child' ).css( 'white-space', 'normal' );
  761. this.$( '.more-help' ).hide();
  762. },
  763. /** collapse help */
  764. _clickLessHelp : function( ev ){
  765. this.$( '.main-help' ).css( 'max-height', '' );
  766. this.$( '.main-help .help-content p:first-child' ).css( 'white-space', '' );
  767. this.$( '.more-help' ).show();
  768. },
  769. /** show an alert on the top of the interface containing message (alertClass is bootstrap's alert-*)*/
  770. _showAlert : function( message, alertClass ){
  771. alertClass = alertClass || 'alert-danger';
  772. this.$( '.main-help' ).hide();
  773. this.$( '.header .alert' ).attr( 'class', 'alert alert-dismissable' ).addClass( alertClass ).show()
  774. .find( '.alert-message' ).html( message );
  775. },
  776. /** hide the alerts at the top */
  777. _hideAlert : function( message ){
  778. this.$( '.main-help' ).show();
  779. this.$( '.header .alert' ).hide();
  780. },
  781. //TODO: consolidate these
  782. /** toggle between showing only unpaired and split view */
  783. _clickShowOnlyUnpaired : function( ev ){
  784. //console.debug( 'click unpaired', ev.currentTarget );
  785. if( this.$( '.paired-columns' ).is( ':visible' ) ){
  786. this.hidePaired();
  787. } else {
  788. this.splitView();
  789. }
  790. },
  791. /** toggle between showing only paired and split view */
  792. _clickShowOnlyPaired : function( ev ){
  793. //console.debug( 'click paired' );
  794. if( this.$( '.unpaired-columns' ).is( ':visible' ) ){
  795. this.hideUnpaired();
  796. } else {
  797. this.splitView();
  798. }
  799. },
  800. /** hide unpaired, show paired */
  801. hideUnpaired : function( speed, callback ){
  802. speed = speed || 0;
  803. this.$( '.unpaired-columns' ).hide( speed, callback );
  804. //this.$( '.unpaired-filter' ).hide( speed );
  805. this.$( '.paired-columns' ).show( speed ).css( 'flex', '1 0 auto' );
  806. this.unpairedPanelHidden = true;
  807. },
  808. /** hide paired, show unpaired */
  809. hidePaired : function( speed, callback ){
  810. speed = speed || 0;
  811. this.$( '.unpaired-columns' ).show( speed ).css( 'flex', '1 0 auto' );
  812. //this.$( '.unpaired-filter' ).show( speed );
  813. this.$( '.paired-columns' ).hide( speed, callback );
  814. this.pairedPanelHidden = true;
  815. },
  816. /** show both paired and unpaired (splitting evenly) */
  817. splitView : function( speed, callback ){
  818. speed = speed || 0;
  819. this.unpairedPanelHidden = this.pairedPanelHidden = false;
  820. //this.$( '.unpaired-filter' ).show( speed );
  821. this._renderMiddle( speed );
  822. return this;
  823. },
  824. /** unpair all paired and do other super neat stuff which I'm not really sure about yet... */
  825. _clickUnpairAll : function( ev ){
  826. this.unpairAll();
  827. },
  828. /** attempt to autopair */
  829. _clickAutopair : function( ev ){
  830. var paired = this.autoPair();
  831. //console.debug( 'autopaired', paired );
  832. //TODO: an indication of how many pairs were found - if 0, assist
  833. this.trigger( 'autopair', paired );
  834. },
  835. /** set the filters based on the data attributes of the button click target */
  836. _clickFilterChoice : function( ev ){
  837. var $selected = $( ev.currentTarget );
  838. this.$( '.forward-unpaired-filter input' ).val( $selected.data( 'forward' ) );
  839. this.$( '.reverse-unpaired-filter input' ).val( $selected.data( 'reverse' ) );
  840. this._hideChooseFilters();
  841. this.trigger( 'filter-change' );
  842. },
  843. /** hide the choose filters popover */
  844. _hideChooseFilters : function(){
  845. //TODO: update bootstrap and remove the following hack
  846. // see also: https://github.com/twbs/bootstrap/issues/10260
  847. this.$( '.choose-filters-link' ).popover( 'hide' );
  848. this.$( '.popover' ).css( 'display', 'none' );
  849. },
  850. /** clear both filters */
  851. _clearFilters : function( ev ){
  852. this.$( '.forward-unpaired-filter input' ).val( '' );
  853. this.$( '.reverse-unpaired-filter input' ).val( '' );
  854. this.trigger( 'filter-change' );
  855. },
  856. // ........................................................................ unpaired
  857. /** select an unpaired dataset */
  858. _clickUnpairedDataset : function( ev ){
  859. ev.stopPropagation();
  860. return this.toggleSelectUnpaired( $( ev.currentTarget ) );
  861. },
  862. /** Toggle the selection of an unpaired dataset representation.
  863. * @param [jQuery] $dataset the unpaired dataset dom rep to select
  864. * @param [Boolean] options.force if defined, force selection based on T/F; otherwise, toggle
  865. */
  866. toggleSelectUnpaired : function( $dataset, options ){
  867. options = options || {};
  868. var dataset = $dataset.data( 'dataset' ),
  869. select = options.force !== undefined? options.force: !$dataset.hasClass( 'selected' );
  870. //console.debug( id, options.force, $dataset, dataset );
  871. if( !$dataset.size() || dataset === undefined ){ return $dataset; }
  872. if( select ){
  873. $dataset.addClass( 'selected' );
  874. if( !options.waitToPair ){
  875. this.pairAllSelected();
  876. }
  877. } else {
  878. $dataset.removeClass( 'selected' );
  879. //delete dataset.selected;
  880. }
  881. return $dataset;
  882. },
  883. /** pair all the currently selected unpaired datasets */
  884. pairAllSelected : function( options ){
  885. options = options || {};
  886. var creator = this,
  887. fwds = [],
  888. revs = [],
  889. pairs = [];
  890. //TODO: could be made more concise
  891. creator.$( '.unpaired-columns .forward-column .dataset.selected' ).each( function(){
  892. fwds.push( $( this ).data( 'dataset' ) );
  893. });
  894. creator.$( '.unpaired-columns .reverse-column .dataset.selected' ).each( function(){
  895. revs.push( $( this ).data( 'dataset' ) );
  896. });
  897. fwds.length = revs.length = Math.min( fwds.length, revs.length );
  898. //console.debug( fwds );
  899. //console.debug( revs );
  900. fwds.forEach( function( fwd, i ){
  901. try {
  902. pairs.push( creator._pair( fwd, revs[i], { silent: true }) );
  903. } catch( err ){
  904. //TODO: preserve selected state of those that couldn't be paired
  905. //TODO: warn that some could not be paired
  906. console.error( err );
  907. }
  908. });
  909. if( pairs.length && !options.silent ){
  910. this.trigger( 'pair:new', pairs );
  911. }
  912. return pairs;
  913. },
  914. /** clear the selection on all unpaired datasets */
  915. clearSelectedUnpaired : function(){
  916. this.$( '.unpaired-columns .dataset.selected' ).removeClass( 'selected' );
  917. },
  918. /** attempt to pair two datasets directly across from one another */
  919. _clickPairRow : function( ev ){
  920. //if( !ev.currentTarget ){ return true; }
  921. var rowIndex = $( ev.currentTarget ).index(),
  922. fwd = $( '.unpaired-columns .forward-column .dataset' ).eq( rowIndex ).data( 'dataset' ),
  923. rev = $( '.unpaired-columns .reverse-column .dataset' ).eq( rowIndex ).data( 'dataset' );
  924. //console.debug( 'row:', rowIndex, fwd, rev );
  925. //TODO: animate
  926. this._pair( fwd, rev );
  927. },
  928. // ........................................................................ divider/partition
  929. /** start dragging the visible divider/partition between unpaired and paired panes */
  930. _startPartitionDrag : function( ev ){
  931. var creator = this,
  932. startingY = ev.pageY;
  933. //console.debug( 'partition drag START:', ev );
  934. $( 'body' ).css( 'cursor', 'ns-resize' );
  935. creator.$( '.flexible-partition-drag' ).css( 'color', 'black' );
  936. function endDrag( ev ){
  937. //console.debug( 'partition drag STOP:', ev );
  938. // doing this by an added class didn't really work well - kept flashing still
  939. creator.$( '.flexible-partition-drag' ).css( 'color', '' );
  940. $( 'body' ).css( 'cursor', '' ).unbind( 'mousemove', trackMouse );
  941. }
  942. function trackMouse( ev ){
  943. var offset = ev.pageY - startingY;
  944. if( !creator.adjPartition( offset ) ){
  945. $( 'body' ).trigger( 'mouseup' );
  946. }
  947. startingY += offset;
  948. }
  949. $( 'body' ).mousemove( trackMouse );
  950. $( 'body' ).one( 'mouseup', endDrag );
  951. },
  952. /** adjust the parition up/down +/-adj pixels */
  953. adjPartition : function( adj ){
  954. var $unpaired = this.$( '.unpaired-columns' ),
  955. $paired = this.$( '.paired-columns' ),
  956. unpairedHi = parseInt( $unpaired.css( 'height' ), 10 ),
  957. pairedHi = parseInt( $paired.css( 'height' ), 10 );
  958. //console.debug( adj, 'hi\'s:', unpairedHi, pairedHi, unpairedHi + adj, pairedHi - adj );
  959. unpairedHi = Math.max( 10, unpairedHi + adj );
  960. pairedHi = pairedHi - adj;
  961. if( unpairedHi <= 10 ){
  962. this.hideUnpaired();
  963. return false;
  964. } else if( !$unpaired.is( 'visible' ) ){
  965. $unpaired.show();
  966. //this.$( '.unpaired-filter' ).show();
  967. }
  968. if( pairedHi <= 15 ){
  969. this.hidePaired();
  970. if( pairedHi < 10 ){ return false; }
  971. } else if( !$paired.is( 'visible' ) ){
  972. $paired.show();
  973. }
  974. $unpaired.css({
  975. height : unpairedHi + 'px',
  976. flex : '0 0 auto'
  977. });
  978. return true;
  979. },
  980. // ........................................................................ paired
  981. /** rename a pair when the pair name is clicked */
  982. _clickPairName : function( ev ){
  983. var $control = $( ev.currentTarget ),
  984. pair = this.paired[ $control.parent().index() ],
  985. response = prompt( 'Enter a new name for the pair:', pair.name );
  986. if( response ){
  987. pair.name = response;
  988. $control.text( pair.name );
  989. }
  990. },
  991. /** unpair this pair */
  992. _clickUnpair : function( ev ){
  993. //if( !ev.currentTarget ){ return true; }
  994. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  995. var pairIndex = Math.floor( $( ev.currentTarget ).index() / 2 );
  996. //console.debug( 'pair:', pairIndex );
  997. //TODO: animate
  998. this._unpair( this.paired[ pairIndex ] );
  999. },
  1000. //TODO: the following is ridiculous
  1001. _hoverPaired : function( ev ){
  1002. var $target = $( ev.currentTarget ),
  1003. index = $target.index();
  1004. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  1005. if( $target.parents( '.reverse-column' ).size() ){
  1006. index = Math.floor( index / 2 );
  1007. }
  1008. //console.debug( 'index:', index );
  1009. this.emphasizePair( index );
  1010. },
  1011. _hoverOutPaired : function( ev ){
  1012. var $target = $( ev.currentTarget ),
  1013. index = $target.index();
  1014. //TODO: this is a hack bc each paired rev now has two elems (dataset, button)
  1015. if( $target.parents( '.reverse-column' ).size() ){
  1016. index = Math.floor( index / 2 );
  1017. }
  1018. //console.debug( 'index:', index );
  1019. this.deemphasizePair( index );
  1020. },
  1021. emphasizePair : function( index ){
  1022. //console.debug( 'emphasizePairedBorder:', index );
  1023. this.$( '.paired-columns .forward-column .dataset.paired' ).eq( index )
  1024. .add( this.$( '.paired-columns .paired-column .dataset.paired' ).eq( index ) )
  1025. .add( this.$( '.paired-columns .reverse-column .dataset.paired' ).eq( index ) )
  1026. .addClass( 'emphasized' );
  1027. },
  1028. deemphasizePair : function( index ){
  1029. //console.debug( 'deemphasizePairedBorder:', index );
  1030. this.$( '.paired-columns .forward-column .dataset.paired' ).eq( index )
  1031. .add( this.$( '.paired-columns .paired-column .dataset.paired' ).eq( index ) )
  1032. .add( this.$( '.paired-columns .reverse-column .dataset.paired' ).eq( index ) )
  1033. .removeClass( 'emphasized' );
  1034. },
  1035. // ........................................................................ footer
  1036. /** handle a collection name change */
  1037. _changeName : function( ev ){
  1038. this._validationWarning( 'name', !!this._getName() );
  1039. },
  1040. /** get the current collection name */
  1041. _getName : function(){
  1042. return _.escape( this.$( '.collection-name' ).val() );
  1043. },
  1044. /** attempt to create the current collection */
  1045. _clickCreate : function( ev ){
  1046. var name = this._getName();
  1047. if( !name ){
  1048. this._validationWarning( 'name' );
  1049. } else {
  1050. this.createList();
  1051. }
  1052. },
  1053. // ------------------------------------------------------------------------ misc
  1054. /** debug a dataset list */
  1055. _printList : function( list ){
  1056. var creator = this;
  1057. _.each( list, function( e ){
  1058. if( list === creator.paired ){
  1059. creator._printPair( e );
  1060. } else {
  1061. //console.debug( e );
  1062. }
  1063. });
  1064. },
  1065. /** print a pair Object */
  1066. _printPair : function( pair ){
  1067. console.debug( pair.forward.name, pair.reverse.name, ': ->', pair.name );
  1068. },
  1069. /** string rep */
  1070. toString : function(){ return 'PairedCollectionCreator'; }
  1071. });
  1072. //TODO: move to require text plugin and load these as text
  1073. //TODO: underscore currently unnecc. bc no vars are used
  1074. //TODO: better way of localizing text-nodes in long strings
  1075. /** underscore template fns attached to class */
  1076. PairedCollectionCreator.templates = PairedCollectionCreator.templates || {
  1077. /** the skeleton */
  1078. main : _.template([
  1079. '<div class="header flex-row no-flex"></div>',
  1080. '<div class="middle flex-row flex-row-container"></div>',
  1081. '<div class="footer flex-row no-flex">'
  1082. ].join('')),
  1083. /** the header (not including help text) */
  1084. header : _.template([
  1085. '<div class="main-help well clear">',
  1086. '<a class="more-help" href="javascript:void(0);">', _l( 'More help' ), '</a>',
  1087. '<div class="help-content">',
  1088. '<a class="less-help" href="javascript:void(0);">', _l( 'Less' ), '</a>',
  1089. '</div>',
  1090. '</div>',
  1091. '<div class="alert alert-dismissable">',
  1092. '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>',
  1093. '<span class="alert-message"></span>',
  1094. '</div>',
  1095. '<div class="column-headers vertically-spaced flex-column-container">',
  1096. '<div class="forward-column flex-column column">',
  1097. '<div class="column-header">',
  1098. '<div class="column-title">',
  1099. '<span class="title">', _l( 'Unpaired forward' ), '</span>',
  1100. '<span class="title-info unpaired-info"></span>',
  1101. '</div>',
  1102. '<div class="unpaired-filter forward-unpaired-filter pull-left">',
  1103. '<input class="search-query" placeholder="', _l( 'Filter this list' ), '" />',
  1104. '</div>',
  1105. '</div>',
  1106. '</div>',
  1107. '<div class="paired-column flex-column no-flex column">',
  1108. '<div class="column-header">',
  1109. '<a class="choose-filters-link" href="javascript:void(0)">',
  1110. _l( 'Choose filters' ),
  1111. '</a>',
  1112. '<a class="clear-filters-link" href="javascript:void(0);">',
  1113. _l( 'Clear filters' ),
  1114. '</a><br />',
  1115. '<a class="autopair-link" href="javascript:void(0);">',
  1116. _l( 'Auto-pair' ),
  1117. '</a>',
  1118. '</div>',
  1119. '</div>',
  1120. '<div class="reverse-column flex-column column">',
  1121. '<div class="column-header">',
  1122. '<div class="column-title">',
  1123. '<span class="title">', _l( 'Unpaired reverse' ), '</span>',
  1124. '<span class="title-info unpaired-info"></span>',
  1125. '</div>',
  1126. '<div class="unpaired-filter reverse-unpaired-filter pull-left">',
  1127. '<input class="search-query" placeholder="', _l( 'Filter this list' ), '" />',
  1128. '</div>',
  1129. '</div>',
  1130. '</div>',
  1131. '</div>'
  1132. ].join('')),
  1133. /** the middle: unpaired, divider, and paired */
  1134. middle : _.template([
  1135. // contains two flex rows (rows that fill available space) and a divider btwn
  1136. '<div class="unpaired-columns flex-column-container scroll-container flex-row">',
  1137. '<div class="forward-column flex-column column">',
  1138. '<ol class="column-datasets"></ol>',
  1139. '</div>',
  1140. '<div class="paired-column flex-column no-flex column">',
  1141. '<ol class="column-datasets"></ol>',
  1142. '</div>',
  1143. '<div class="reverse-column flex-column column">',
  1144. '<ol class="column-datasets"></ol>',
  1145. '</div>',
  1146. '</div>',
  1147. '<div class="flexible-partition">',
  1148. '<div class="flexible-partition-drag"></div>',
  1149. '<div class="column-header">',
  1150. '<div class="column-title paired-column-title">',
  1151. '<span class="title"></span>',
  1152. '</div>',
  1153. '<a class="unpair-all-link" href="javascript:void(0);">',
  1154. _l( 'Unpair all' ),
  1155. '</a>',
  1156. '</div>',
  1157. '</div>',
  1158. '<div class="paired-columns flex-column-container scroll-container flex-row">',
  1159. '<div class="forward-column flex-column column">',
  1160. '<ol class="column-datasets"></ol>',
  1161. '</div>',
  1162. '<div class="paired-column flex-column no-flex column">',
  1163. '<ol class="column-datasets"></ol>',
  1164. '</div>',
  1165. '<div class="reverse-column flex-column column">',
  1166. '<ol class="column-datasets"></ol>',
  1167. '</div>',
  1168. '</div>'
  1169. ].join('')),
  1170. /** creation and cancel controls */
  1171. footer : _.template([
  1172. '<div class="attributes clear">',
  1173. '<input class="collection-name form-control pull-right" ',
  1174. 'placeholder="', _l( 'Enter a name for your new list' ), '" />',
  1175. '<div class="collection-name-prompt pull-right">', _l( 'Name' ), ':</div>',
  1176. '</div>',
  1177. '<div class="actions clear vertically-spaced">',
  1178. '<div class="other-options pull-left">',
  1179. '<button class="cancel-create btn" tabindex="-1">', _l( 'Cancel' ), '</button>',
  1180. '<div class="create-other btn-group dropup">',
  1181. '<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">',
  1182. _l( 'Create a different kind of collection' ),
  1183. ' <span class="caret"></span>',
  1184. '</button>',
  1185. '<ul class="dropdown-menu" role="menu">',
  1186. '<li><a href="#">', _l( 'Create a <i>single</i> pair' ), '</a></li>',
  1187. '<li><a href="#">', _l( 'Create a list of <i>unpaired</i> datasets' ), '</a></li>',
  1188. '</ul>',
  1189. '</div>',
  1190. '</div>',
  1191. '<div class="main-options pull-right">',
  1192. '<button class="create-collection btn btn-primary">', _l( 'Create list' ), '</button>',
  1193. '</div>',
  1194. '</div>'
  1195. ].join('')),
  1196. /** help content */
  1197. helpContent : _.template([
  1198. '<p>', _l([
  1199. 'Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse ',
  1200. 'reads) that can be passed to tools and workflows in order to have analyses done on the entire ',
  1201. 'group. This interface allows you to create a collection, choose which datasets are paired, ',
  1202. 'and re-order the final collection.'
  1203. ].join( '' )), '</p>',
  1204. '<p>', _l([
  1205. 'Unpaired datasets are shown in the <i data-target=".unpaired-columns">unpaired section</i> ',
  1206. '(hover over the underlined words to highlight below). ',
  1207. 'Paired datasets are shown in the <i data-target=".paired-columns">paired section</i>.',
  1208. '<ul>To pair datasets, you can:',
  1209. '<li>Click a dataset in the ',
  1210. '<i data-target=".unpaired-columns .forward-column .column-datasets,',
  1211. '.unpaired-columns .forward-column">forward column</i> ',
  1212. 'to select it then click a dataset in the ',
  1213. '<i data-target=".unpaired-columns .reverse-column .column-datasets,',
  1214. '.unpaired-columns .reverse-column">reverse column</i>.',
  1215. '</li>',
  1216. '<li>Click one of the "Pair these datasets" buttons in the ',
  1217. '<i data-target=".unpaired-columns .paired-column .column-datasets,',
  1218. '.unpaired-columns .paired-column">middle column</i> ',
  1219. 'to pair the datasets in a particular row.',
  1220. '</li>',
  1221. '<li>Click <i data-target=".autopair-link">"Auto-pair"</i> ',
  1222. 'to have your datasets automatically paired based on name.',
  1223. '</li>',
  1224. '</ul>'
  1225. ].join( '' )), '</p>',
  1226. '<p>', _l([
  1227. '<ul>You can filter what is shown in the unpaired sections by:',
  1228. '<li>Entering partial dataset names in either the ',
  1229. '<i data-target=".forward-unpaired-filter input">forward filter</i> or ',
  1230. '<i data-target=".reverse-unpaired-filter input">reverse filter</i>.',
  1231. '</li>',
  1232. '<li>Choosing from a list of preset filters by clicking the ',
  1233. '<i data-target=".choose-filters-link">"Choose filters" link</i>.',
  1234. '</li>',
  1235. '<li>Clearing the filters by clicking the ',
  1236. '<i data-target=".clear-filters-link">"Clear filters" link</i>.',
  1237. '</li>',
  1238. '</ul>'
  1239. ].join( '' )), '</p>',
  1240. '<p>', _l([
  1241. 'To unpair individual dataset pairs, click the ',
  1242. '<i data-target=".unpair-btn">unpair buttons ( <span class="fa fa-unlink"></span> )</i>. ',
  1243. 'Click the <i data-target=".unpair-all-link">"Unpair all" link</i> to unpair all pairs.'
  1244. ].join( '' )), '</p>',
  1245. '<p>', _l([
  1246. 'Once your collection is complete, enter a <i data-target=".collection-name">name</i> and ',
  1247. 'click <i data-target=".create-collection">"Create list"</i>.',
  1248. '(Note: you do not have to pair all unpaired datasets to finish.)'
  1249. ].join( '' )), '</p>'
  1250. ].join(''))
  1251. }
  1252. //=============================================================================
  1253. //TODO: to own file
  1254. /*
  1255. (Imported for edit distance algorith. From: https://gist.github.com/andrei-m/982927)
  1256. Copyright (c) 2011 Andrei Mackenzie
  1257. */
  1258. // Compute the edit distance between the two given strings
  1259. //exports.getEditDistance = function(a, b){
  1260. function levenshteinDistance(a, b){
  1261. if(a.length === 0){ return b.length; }
  1262. if(b.length === 0){ return a.length; }
  1263. var matrix = [];
  1264. // increment along the first column of each row
  1265. var i;
  1266. for(i = 0; i <= b.length; i++){
  1267. matrix[i] = [i];
  1268. }
  1269. // increment each column in the first row
  1270. var j;
  1271. for(j = 0; j <= a.length; j++){
  1272. matrix[0][j] = j;
  1273. }
  1274. // Fill in the rest of the matrix
  1275. for(i = 1; i <= b.length; i++){
  1276. for(j = 1; j <= a.length; j++){
  1277. if(b.charAt(i-1) === a.charAt(j-1)){
  1278. matrix[i][j] = matrix[i-1][j-1];
  1279. } else {
  1280. matrix[i][j] = Math.min(matrix[i-1][j-1] + 1, // substitution
  1281. Math.min(matrix[i][j-1] + 1, // insertion
  1282. matrix[i-1][j] + 1)); // deletion
  1283. }
  1284. }
  1285. }
  1286. //console.debug( '\t\t levenshteinDistance', a, b, matrix[b.length][a.length] );
  1287. return matrix[b.length][a.length];
  1288. }
  1289. //=============================================================================
  1290. (function(){
  1291. /** plugin that will highlight an element when this element is hovered over */
  1292. jQuery.fn.extend({
  1293. hoverhighlight : function $hoverhighlight( scope, color ){
  1294. scope = scope || 'body';
  1295. if( !this.size() ){ return this; }
  1296. $( this ).each( function(){
  1297. var $this = $( this ),
  1298. targetSelector = $this.data( 'target' );
  1299. if( targetSelector ){
  1300. $this.mouseover( function( ev ){
  1301. $( targetSelector, scope ).css({
  1302. background: color
  1303. });
  1304. })
  1305. .mouseout( function( ev ){
  1306. $( targetSelector ).css({
  1307. background: ''
  1308. });
  1309. });
  1310. }
  1311. });
  1312. return this;
  1313. }
  1314. });
  1315. }());
  1316. //=============================================================================
  1317. //TODO: to own file
  1318. /** a modal version of the paired collection creator */
  1319. var pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){
  1320. options = _.defaults( options || {}, {
  1321. datasets : datasets,
  1322. oncancel : function(){ Galaxy.modal.hide(); },
  1323. oncreate : function(){
  1324. Galaxy.modal.hide();
  1325. Galaxy.currHistoryPanel.refreshContents();
  1326. }
  1327. });
  1328. if( !window.Galaxy || !Galaxy.modal ){
  1329. throw new Error( 'Galaxy or Galaxy.modal not found' );
  1330. }
  1331. var creator = new PairedCollectionCreator( options ).render();
  1332. Galaxy.modal.show({
  1333. title : 'Create a collection of paired datasets',
  1334. body : creator.$el,
  1335. width : '80%',
  1336. height : '700px',
  1337. closing_events: true
  1338. });
  1339. //TODO: remove modal header
  1340. return creator;
  1341. };
  1342. //=============================================================================
  1343. return {
  1344. PairedCollectionCreator : PairedCollectionCreator,
  1345. pairedCollectionCreatorModal : pairedCollectionCreatorModal
  1346. };
  1347. });