1 new commit in galaxy-central: https://bitbucket.org/galaxy/galaxy-central/commits/581d871b8102/ Changeset: 581d871b8102 User: carlfeberhard Date: 2015-02-10 23:15:43+00:00 Summary: UI, paired collection creator: refactor autopairing and add tests Affected #: 4 files diff -r cd41c43e857f7db0d21a093f0d59ce8bf82a049e -r 581d871b810205f6a02e994983ffe316e206b746 client/galaxy/scripts/mvc/collection/paired-collection-creator.js --- a/client/galaxy/scripts/mvc/collection/paired-collection-creator.js +++ b/client/galaxy/scripts/mvc/collection/paired-collection-creator.js @@ -3,7 +3,7 @@ "utils/natural-sort", "mvc/base-mvc", "utils/localization" -], function( levelshteinDistance, naturalSort, baseMVC, _l ){ +], function( levenshteinDistance, naturalSort, baseMVC, _l ){ /* ============================================================================ TODO: _adjPairedOnScrollBar @@ -28,7 +28,6 @@ className : 'dataset paired', initialize : function( attributes ){ - //console.debug( 'PairView.initialize:', attributes ); this.pair = attributes.pair || {}; }, @@ -46,9 +45,6 @@ .data( 'pair', this.pair ) .html( this.template({ pair: this.pair }) ) .addClass( 'flex-column-container' ); - -//TODO: would be good to get the unpair-btn into this view - but haven't found a way with css - return this; }, @@ -61,24 +57,17 @@ /** dragging pairs for re-ordering */ _dragstart : function( ev ){ - //console.debug( this, '_dragstartPair', ev ); ev.currentTarget.style.opacity = '0.4'; if( ev.originalEvent ){ ev = ev.originalEvent; } ev.dataTransfer.effectAllowed = 'move'; ev.dataTransfer.setData( 'text/plain', JSON.stringify( this.pair ) ); - //ev.dataTransfer.setDragImage( null, 0, 0 ); - - // the canvas can be used to create the image - //ev.dataTransfer.setDragImage( canvasCrossHairs(), 25, 25 ); - - //console.debug( 'ev.dataTransfer:', ev.dataTransfer ); this.$el.parent().trigger( 'pair.dragstart', [ this ] ); }, + /** dragging pairs for re-ordering */ _dragend : function( ev ){ - //console.debug( this, '_dragendPair', ev ); ev.currentTarget.style.opacity = '1.0'; this.$el.parent().trigger( 'pair.dragend', [ this ] ); }, @@ -92,8 +81,106 @@ toString : function(){ return 'PairView(' + this.pair.name + ')'; } +}); -}); + +/** returns an autopair function that uses the provided options.match function */ +function autoPairFnBuilder( options ){ + options = options || {}; + options.createPair = options.createPair || function _defaultCreatePair( params ){ + this.debug( 'creating pair:', params.listA[ params.indexA ].name, params.listB[ params.indexB ].name ); + params = params || {}; + return this._pair( + params.listA.splice( params.indexA, 1 )[0], + params.listB.splice( params.indexB, 1 )[0], + { silent: true } + ); + }; + // compile these here outside of the loop + var _regexps = []; + function getRegExps(){ + if( !_regexps.length ){ + _regexps = [ + new RegExp( this.filters[0] ), + new RegExp( this.filters[1] ) + ]; + } + return _regexps; + } + // mangle params as needed + options.preprocessMatch = options.preprocessMatch || function _defaultPreprocessMatch( params ){ + var regexps = getRegExps.call( this ); + return _.extend( params, { + matchTo : params.matchTo.name.replace( regexps[0], '' ), + possible : params.possible.name.replace( regexps[1], '' ) + }); + }; + + return function _strategy( params ){ + this.debug( 'autopair _strategy ---------------------------' ); + params = params || {}; + var listA = params.listA, + listB = params.listB, + indexA = 0, indexB, + bestMatch = { + score : 0.0, + index : null + }, + paired = []; + //console.debug( 'params:', JSON.stringify( params, null, ' ' ) ); + this.debug( 'starting list lens:', listA.length, listB.length ); + this.debug( 'bestMatch (starting):', JSON.stringify( bestMatch, null, ' ' ) ); + + while( indexA < listA.length ){ + var matchTo = listA[ indexA ]; + bestMatch.score = 0.0; + + for( indexB=0; indexB<listB.length; indexB++ ){ + var possible = listB[ indexB ]; + this.debug( indexA + ':' + matchTo.name ); + this.debug( indexB + ':' + possible.name ); + + // no matching with self + if( listA[ indexA ] !== listB[ indexB ] ){ + bestMatch = options.match.call( this, options.preprocessMatch.call( this, { + matchTo : matchTo, + possible: possible, + index : indexB, + bestMatch : bestMatch + })); + this.debug( 'bestMatch:', JSON.stringify( bestMatch, null, ' ' ) ); + if( bestMatch.score === 1.0 ){ + this.debug( 'breaking early due to perfect match' ); + break; + } + } + } + var scoreThreshold = options.scoreThreshold.call( this ); + this.debug( 'scoreThreshold:', scoreThreshold ); + this.debug( 'bestMatch.score:', bestMatch.score ); + + if( bestMatch.score >= scoreThreshold ){ + this.debug( 'creating pair' ); + paired.push( options.createPair.call( this, { + listA : listA, + indexA : indexA, + listB : listB, + indexB : bestMatch.index + })); + this.debug( 'list lens now:', listA.length, listB.length ); + } else { + indexA += 1; + } + if( !listA.length || !listB.length ){ + return paired; + } + } + this.debug( 'paired:', JSON.stringify( paired, null, ' ' ) ); + this.debug( 'autopair _strategy ---------------------------' ); + return paired; + }; +} + /** An interface for building collections of paired datasets. */ @@ -108,15 +195,11 @@ attributes = _.defaults( attributes, { datasets : [], filters : this.DEFAULT_FILTERS, - //automaticallyPair : false, automaticallyPair : true, - matchPercentage : 1.0, - //matchPercentage : 0.9, - //matchPercentage : 0.8, - //strategy : 'levenshtein' - strategy : 'lcs' + strategy : 'lcs', + matchPercentage : 0.9, + twoPassAutopairing : true }); - //this.debug( 'attributes now:', attributes ); /** unordered, original list */ this.initialList = attributes.datasets; @@ -133,15 +216,18 @@ /** try to auto pair the unpaired datasets on load? */ this.automaticallyPair = attributes.automaticallyPair; - /** distance/mismatch level allowed for autopairing */ - this.matchPercentage = attributes.matchPercentage; - /** what method to use for auto pairing (will be passed aggression level) */ this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ]; if( _.isFunction( attributes.strategy ) ){ this.strategy = attributes.strategy; } + /** distance/mismatch level allowed for autopairing */ + this.matchPercentage = attributes.matchPercentage; + + /** try to autopair using simple first, then this.strategy on the remainder */ + this.twoPassAutopairing = attributes.twoPassAutopairing; + /** remove file extensions (\.*) from created pair names? */ this.removeExtensions = true; //this.removeExtensions = false; @@ -173,12 +259,12 @@ }, /** which commonFilter to use by default */ DEFAULT_FILTERS : 'illumina', - //DEFAULT_FILTERS : 'none', /** map of name->fn for autopairing */ strategies : { - 'lcs' : 'autoPairLCSs', - 'levenshtein' : 'autoPairLevenshtein' + 'simple' : 'autopairSimple', + 'lcs' : 'autopairLCS', + 'levenshtein' : 'autopairLevenshtein' }, /** default autopair strategy name */ DEFAULT_STRATEGY : 'lcs', @@ -191,8 +277,6 @@ this.paired = []; this.unpaired = []; - //this.fwdSelectedIds = []; - //this.revSelectedIds = []; this.selectedIds = []; // sort initial list, add ids if needed, and save new working copy to unpaired @@ -212,7 +296,6 @@ _sortInitialList : function(){ //this.debug( '-- _sortInitialList' ); this._sortDatasetList( this.initialList ); - //this._printList( this.unpaired ); }, /** sort a list of datasets */ @@ -229,39 +312,31 @@ dataset.id = _.uniqueId(); } }); - //this._printList( this.unpaired ); return this.initialList; }, /** split initial list into two lists, those that pass forward filters & those passing reverse */ - _splitByFilters : function( filters ){ - var fwd = [], - rev = []; + _splitByFilters : function(){ + var regexFilters = this.filters.map( function( stringFilter ){ + return new RegExp( stringFilter ); + }), + split = [ [], [] ]; - function _addToFwdOrRev( unpaired ){ - if( this._filterFwdFn( unpaired ) ){ - fwd.push( unpaired ); - } - if( this._filterRevFn( unpaired ) ){ - rev.push( unpaired ); - } + function _filter( unpaired, filter ){ + return filter.test( unpaired.name ); + //return dataset.name.indexOf( filter ) >= 0; } - this.unpaired.forEach( _.bind( _addToFwdOrRev, this ) ); - return [ fwd, rev ]; - }, - - /** filter fn to apply to forward datasets */ - _filterFwdFn : function( dataset ){ -//TODO: this treats *all* strings as regex which may confuse people - var regexp = new RegExp( this.filters[0] ); - return regexp.test( dataset.name ); - //return dataset.name.indexOf( this.filters[0] ) >= 0; - }, - - /** filter fn to apply to reverse datasets */ - _filterRevFn : function( dataset ){ - var regexp = new RegExp( this.filters[1] ); - return regexp.test( dataset.name ); + this.unpaired.forEach( function _filterEach( unpaired ){ + // 90% of the time this seems to work, but: + //TODO: this treats *all* strings as regex which may confuse people - possibly check for // surrounding? + // would need explanation in help as well + regexFilters.forEach( function( filter, i ){ + if( _filter( unpaired, filter ) ){ + split[i].push( unpaired ); + } + }); + }); + return split; }, /** add a dataset to the unpaired list in it's proper order */ @@ -288,202 +363,79 @@ }, // ------------------------------------------------------------------------ auto pairing -//TODO: lots of boiler plate btwn the three auto pair fns /** two passes to automatically create pairs: * use both simpleAutoPair, then the fn mentioned in strategy */ autoPair : function( strategy ){ + // split first using exact matching + var split = this._splitByFilters(), + paired = []; + if( this.twoPassAutopairing ){ + paired = this.autopairSimple({ + listA : split[0], + listB : split[1] + }); + split = this._splitByFilters(); + } + + // uncomment to see printlns while running tests + //this.debug = function(){ console.log.apply( console, arguments ); }; + + // then try the remainder with something less strict strategy = strategy || this.strategy; - //this.debug( '-- autoPair', strategy ); - var paired = this.simpleAutoPair(); - paired = paired.concat( this[ strategy ].call( this ) ); + split = this._splitByFilters(); + paired = paired.concat( this[ strategy ].call( this, { + listA : split[0], + listB : split[1] + })); return paired; }, - /** attempts to pair forward with reverse when names exactly match (after removing filters) */ - simpleAutoPair : function(){ - //this.debug( '-- simpleAutoPair' ); - // simplified auto pair that moves down unpaired lists *in order*, - // removes filters' strings from fwd and rev, - // and, if names w/o filters *exactly* match, creates a pair - // possibly good as a first pass - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - matchFound = false, - paired = []; + autopairSimple : autoPairFnBuilder({ + scoreThreshold: function(){ return 1.0; }, + match : function _match( params ){ + params = params || {}; + if( params.matchTo === params.possible ){ + return { + index: params.index, + score: 1.0 + }; + } + return params.bestMatch; + } + }), - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - //TODO: go through the filterFwdFn - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - matchFound = false; + autopairLevenshtein : autoPairFnBuilder({ + scoreThreshold: function(){ return this.matchPercentage; }, + match : function _matches( params ){ + params = params || {}; + var distance = levenshteinDistance( params.matchTo, params.possible ), + score = 1.0 - ( distance / ( Math.max( params.matchTo.length, params.possible.length ) ) ); + if( score > params.bestMatch.score ){ + return { + index: params.index, + score: score + }; + } + return params.bestMatch; + } + }), - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev && fwdName === revName ){ - matchFound = true; - // if it is a match, keep i at current, pop fwd, pop rev and break - //this.debug( '---->', fwdName, revName ); - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( j, 1 )[0], - { silent: true } - ); - paired.push( pair ); - break; - } + autopairLCS : autoPairFnBuilder({ + scoreThreshold: function(){ return this.matchPercentage; }, + match : function _matches( params ){ + params = params || {}; + var match = this._naiveStartingAndEndingLCS( params.matchTo, params.possible ).length, + score = match / ( Math.max( params.matchTo.length, params.possible.length ) ); + if( score > params.bestMatch.score ){ + return { + index: params.index, + score: score + }; } - if( !matchFound ){ i += 1; } + return params.bestMatch; } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, - - /** attempt to autopair using edit distance between forward and reverse (after removing filters) */ - autoPairLevenshtein : function(){ - //precondition: filters are set, both lists are not empty, and all filenames.length > filters[?].length - //this.debug( '-- autoPairLevenshtein' ); - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - distance, bestIndex, bestDist, - paired = []; - - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - //TODO: go through the filterFwdFn - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - bestDist = Number.MAX_VALUE; - - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev ){ - if( fwdName === revName ){ - //this.debug( '\t\t exactmatch:', fwdName, revName ); - bestIndex = j; - bestDist = 0; - break; - } - distance = levenshteinDistance( fwdName, revName ); - //this.debug( '\t\t distance:', distance, 'bestDist:', bestDist ); - if( distance < bestDist ){ - bestIndex = j; - bestDist = distance; - } - } - } - //this.debug( '---->', fwd.name, bestIndex, bestDist ); - //this.debug( '---->', fwd.name, revList[ bestIndex ].name, bestDist ); - - var percentage = 1.0 - ( bestDist / ( Math.max( fwdName.length, revName.length ) ) ); - //this.debug( '----> %', percentage * 100 ); - - if( percentage >= this.matchPercentage ){ - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( bestIndex, 1 )[0], - { silent: true } - ); - paired.push( pair ); - if( fwdList.length <= 0 || revList.length <= 0 ){ - return paired; - } - } else { - i += 1; - } - } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, - - /** attempt to auto pair using common substrings from both front and back (after removing filters) */ - autoPairLCSs : function(){ - //precondition: filters are set, both lists are not empty - //this.debug( '-- autoPairLCSs' ); - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - currMatch, bestIndex, bestMatch, - paired = []; - if( !fwdList.length || !revList.length ){ return paired; } - //this.debug( fwdList, revList ); - - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - bestMatch = 0; - - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev ){ - if( fwdName === revName ){ - //this.debug( '\t\t exactmatch:', fwdName, revName ); - bestIndex = j; - bestMatch = fwdName.length; - break; - } - var match = this._naiveStartingAndEndingLCS( fwdName, revName ); - currMatch = match.length; - //this.debug( '\t\t match:', match, 'currMatch:', currMatch, 'bestMatch:', bestMatch ); - if( currMatch > bestMatch ){ - bestIndex = j; - bestMatch = currMatch; - } - } - } - //this.debug( '---->', i, fwd.name, bestIndex, revList[ bestIndex ].name, bestMatch ); - - var percentage = bestMatch / ( Math.min( fwdName.length, revName.length ) ); - //this.debug( '----> %', percentage * 100 ); - - if( percentage >= this.matchPercentage ){ - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( bestIndex, 1 )[0], - { silent: true } - ); - paired.push( pair ); - if( fwdList.length <= 0 || revList.length <= 0 ){ - return paired; - } - } else { - i += 1; - } - } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, + }), /** return the concat'd longest common prefix and suffix from two strings */ _naiveStartingAndEndingLCS : function( s1, s2 ){ @@ -1338,19 +1290,19 @@ // ........................................................................ paired - drag and drop re-ordering //_dragenterPairedColumns : function( ev ){ - // console.debug( '_dragenterPairedColumns:', ev ); + // this.debug( '_dragenterPairedColumns:', ev ); //}, //_dragleavePairedColumns : function( ev ){ - // //console.debug( '_dragleavePairedColumns:', ev ); + // //this.debug( '_dragleavePairedColumns:', ev ); //}, /** track the mouse drag over the paired list adding a placeholder to show where the drop would occur */ _dragoverPairedColumns : function( ev ){ - //console.debug( '_dragoverPairedColumns:', ev ); + //this.debug( '_dragoverPairedColumns:', ev ); ev.preventDefault(); var $list = this.$( '.paired-columns .column-datasets' ); this._checkForAutoscroll( $list, ev.originalEvent.clientY ); - //console.debug( ev.originalEvent.clientX, ev.originalEvent.clientY ); + //this.debug( ev.originalEvent.clientX, ev.originalEvent.clientY ); var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY ); $( '.paired-drop-placeholder' ).remove(); @@ -1369,7 +1321,7 @@ scrollTop = $element.scrollTop(), upperDist = y - offset.top, lowerDist = ( offset.top + $element.outerHeight() ) - y; - //console.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist ); + //this.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist ); if( upperDist >= 0 && upperDist < this.autoscrollDist ){ $element.scrollTop( scrollTop - AUTOSCROLL_SPEED ); } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){ @@ -1388,7 +1340,7 @@ top = $li.offset().top, halfHeight = Math.floor( $li.outerHeight() / 2 ) + WIGGLE; if( top + halfHeight > y && top - halfHeight < y ){ - //console.debug( y, top + halfHeight, top - halfHeight ) + //this.debug( y, top + halfHeight, top - halfHeight ) return $li; } } @@ -1419,13 +1371,13 @@ this.$( '.paired-columns .dataset.paired' ).each( function(){ newPaired.push( $( this ).data( 'pair' ) ); }); - //console.debug( newPaired ); + //this.debug( newPaired ); this.paired = newPaired; this._renderPaired(); }, /** drag communication with pair sub-views: dragstart */ _pairDragstart : function( ev, pair ){ - //console.debug( '_pairDragstart', ev, pair ) + //this.debug( '_pairDragstart', ev, pair ) // auto select the pair causing the event and move all selected pair.$el.addClass( 'selected' ); var $selected = this.$( '.paired-columns .dataset.selected' ); @@ -1433,7 +1385,7 @@ }, /** drag communication with pair sub-views: dragend - remove the placeholder */ _pairDragend : function( ev, pair ){ - //console.debug( '_pairDragend', ev, pair ) + //this.debug( '_pairDragend', ev, pair ) $( '.paired-drop-placeholder' ).remove(); this.$dragging = null; }, @@ -1736,7 +1688,6 @@ //============================================================================= /** a modal version of the paired collection creator */ var pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){ - console.log( datasets ); options = _.defaults( options || {}, { datasets : datasets, diff -r cd41c43e857f7db0d21a093f0d59ce8bf82a049e -r 581d871b810205f6a02e994983ffe316e206b746 static/scripts/mvc/collection/paired-collection-creator.js --- a/static/scripts/mvc/collection/paired-collection-creator.js +++ b/static/scripts/mvc/collection/paired-collection-creator.js @@ -3,7 +3,7 @@ "utils/natural-sort", "mvc/base-mvc", "utils/localization" -], function( levelshteinDistance, naturalSort, baseMVC, _l ){ +], function( levenshteinDistance, naturalSort, baseMVC, _l ){ /* ============================================================================ TODO: _adjPairedOnScrollBar @@ -28,7 +28,6 @@ className : 'dataset paired', initialize : function( attributes ){ - //console.debug( 'PairView.initialize:', attributes ); this.pair = attributes.pair || {}; }, @@ -46,9 +45,6 @@ .data( 'pair', this.pair ) .html( this.template({ pair: this.pair }) ) .addClass( 'flex-column-container' ); - -//TODO: would be good to get the unpair-btn into this view - but haven't found a way with css - return this; }, @@ -61,24 +57,17 @@ /** dragging pairs for re-ordering */ _dragstart : function( ev ){ - //console.debug( this, '_dragstartPair', ev ); ev.currentTarget.style.opacity = '0.4'; if( ev.originalEvent ){ ev = ev.originalEvent; } ev.dataTransfer.effectAllowed = 'move'; ev.dataTransfer.setData( 'text/plain', JSON.stringify( this.pair ) ); - //ev.dataTransfer.setDragImage( null, 0, 0 ); - - // the canvas can be used to create the image - //ev.dataTransfer.setDragImage( canvasCrossHairs(), 25, 25 ); - - //console.debug( 'ev.dataTransfer:', ev.dataTransfer ); this.$el.parent().trigger( 'pair.dragstart', [ this ] ); }, + /** dragging pairs for re-ordering */ _dragend : function( ev ){ - //console.debug( this, '_dragendPair', ev ); ev.currentTarget.style.opacity = '1.0'; this.$el.parent().trigger( 'pair.dragend', [ this ] ); }, @@ -92,8 +81,106 @@ toString : function(){ return 'PairView(' + this.pair.name + ')'; } +}); -}); + +/** returns an autopair function that uses the provided options.match function */ +function autoPairFnBuilder( options ){ + options = options || {}; + options.createPair = options.createPair || function _defaultCreatePair( params ){ + this.debug( 'creating pair:', params.listA[ params.indexA ].name, params.listB[ params.indexB ].name ); + params = params || {}; + return this._pair( + params.listA.splice( params.indexA, 1 )[0], + params.listB.splice( params.indexB, 1 )[0], + { silent: true } + ); + }; + // compile these here outside of the loop + var _regexps = []; + function getRegExps(){ + if( !_regexps.length ){ + _regexps = [ + new RegExp( this.filters[0] ), + new RegExp( this.filters[1] ) + ]; + } + return _regexps; + } + // mangle params as needed + options.preprocessMatch = options.preprocessMatch || function _defaultPreprocessMatch( params ){ + var regexps = getRegExps.call( this ); + return _.extend( params, { + matchTo : params.matchTo.name.replace( regexps[0], '' ), + possible : params.possible.name.replace( regexps[1], '' ) + }); + }; + + return function _strategy( params ){ + this.debug( 'autopair _strategy ---------------------------' ); + params = params || {}; + var listA = params.listA, + listB = params.listB, + indexA = 0, indexB, + bestMatch = { + score : 0.0, + index : null + }, + paired = []; + //console.debug( 'params:', JSON.stringify( params, null, ' ' ) ); + this.debug( 'starting list lens:', listA.length, listB.length ); + this.debug( 'bestMatch (starting):', JSON.stringify( bestMatch, null, ' ' ) ); + + while( indexA < listA.length ){ + var matchTo = listA[ indexA ]; + bestMatch.score = 0.0; + + for( indexB=0; indexB<listB.length; indexB++ ){ + var possible = listB[ indexB ]; + this.debug( indexA + ':' + matchTo.name ); + this.debug( indexB + ':' + possible.name ); + + // no matching with self + if( listA[ indexA ] !== listB[ indexB ] ){ + bestMatch = options.match.call( this, options.preprocessMatch.call( this, { + matchTo : matchTo, + possible: possible, + index : indexB, + bestMatch : bestMatch + })); + this.debug( 'bestMatch:', JSON.stringify( bestMatch, null, ' ' ) ); + if( bestMatch.score === 1.0 ){ + this.debug( 'breaking early due to perfect match' ); + break; + } + } + } + var scoreThreshold = options.scoreThreshold.call( this ); + this.debug( 'scoreThreshold:', scoreThreshold ); + this.debug( 'bestMatch.score:', bestMatch.score ); + + if( bestMatch.score >= scoreThreshold ){ + this.debug( 'creating pair' ); + paired.push( options.createPair.call( this, { + listA : listA, + indexA : indexA, + listB : listB, + indexB : bestMatch.index + })); + this.debug( 'list lens now:', listA.length, listB.length ); + } else { + indexA += 1; + } + if( !listA.length || !listB.length ){ + return paired; + } + } + this.debug( 'paired:', JSON.stringify( paired, null, ' ' ) ); + this.debug( 'autopair _strategy ---------------------------' ); + return paired; + }; +} + /** An interface for building collections of paired datasets. */ @@ -108,15 +195,11 @@ attributes = _.defaults( attributes, { datasets : [], filters : this.DEFAULT_FILTERS, - //automaticallyPair : false, automaticallyPair : true, - matchPercentage : 1.0, - //matchPercentage : 0.9, - //matchPercentage : 0.8, - //strategy : 'levenshtein' - strategy : 'lcs' + strategy : 'lcs', + matchPercentage : 0.9, + twoPassAutopairing : true }); - //this.debug( 'attributes now:', attributes ); /** unordered, original list */ this.initialList = attributes.datasets; @@ -133,15 +216,18 @@ /** try to auto pair the unpaired datasets on load? */ this.automaticallyPair = attributes.automaticallyPair; - /** distance/mismatch level allowed for autopairing */ - this.matchPercentage = attributes.matchPercentage; - /** what method to use for auto pairing (will be passed aggression level) */ this.strategy = this.strategies[ attributes.strategy ] || this.strategies[ this.DEFAULT_STRATEGY ]; if( _.isFunction( attributes.strategy ) ){ this.strategy = attributes.strategy; } + /** distance/mismatch level allowed for autopairing */ + this.matchPercentage = attributes.matchPercentage; + + /** try to autopair using simple first, then this.strategy on the remainder */ + this.twoPassAutopairing = attributes.twoPassAutopairing; + /** remove file extensions (\.*) from created pair names? */ this.removeExtensions = true; //this.removeExtensions = false; @@ -173,12 +259,12 @@ }, /** which commonFilter to use by default */ DEFAULT_FILTERS : 'illumina', - //DEFAULT_FILTERS : 'none', /** map of name->fn for autopairing */ strategies : { - 'lcs' : 'autoPairLCSs', - 'levenshtein' : 'autoPairLevenshtein' + 'simple' : 'autopairSimple', + 'lcs' : 'autopairLCS', + 'levenshtein' : 'autopairLevenshtein' }, /** default autopair strategy name */ DEFAULT_STRATEGY : 'lcs', @@ -191,8 +277,6 @@ this.paired = []; this.unpaired = []; - //this.fwdSelectedIds = []; - //this.revSelectedIds = []; this.selectedIds = []; // sort initial list, add ids if needed, and save new working copy to unpaired @@ -212,7 +296,6 @@ _sortInitialList : function(){ //this.debug( '-- _sortInitialList' ); this._sortDatasetList( this.initialList ); - //this._printList( this.unpaired ); }, /** sort a list of datasets */ @@ -229,39 +312,31 @@ dataset.id = _.uniqueId(); } }); - //this._printList( this.unpaired ); return this.initialList; }, /** split initial list into two lists, those that pass forward filters & those passing reverse */ - _splitByFilters : function( filters ){ - var fwd = [], - rev = []; + _splitByFilters : function(){ + var regexFilters = this.filters.map( function( stringFilter ){ + return new RegExp( stringFilter ); + }), + split = [ [], [] ]; - function _addToFwdOrRev( unpaired ){ - if( this._filterFwdFn( unpaired ) ){ - fwd.push( unpaired ); - } - if( this._filterRevFn( unpaired ) ){ - rev.push( unpaired ); - } + function _filter( unpaired, filter ){ + return filter.test( unpaired.name ); + //return dataset.name.indexOf( filter ) >= 0; } - this.unpaired.forEach( _.bind( _addToFwdOrRev, this ) ); - return [ fwd, rev ]; - }, - - /** filter fn to apply to forward datasets */ - _filterFwdFn : function( dataset ){ -//TODO: this treats *all* strings as regex which may confuse people - var regexp = new RegExp( this.filters[0] ); - return regexp.test( dataset.name ); - //return dataset.name.indexOf( this.filters[0] ) >= 0; - }, - - /** filter fn to apply to reverse datasets */ - _filterRevFn : function( dataset ){ - var regexp = new RegExp( this.filters[1] ); - return regexp.test( dataset.name ); + this.unpaired.forEach( function _filterEach( unpaired ){ + // 90% of the time this seems to work, but: + //TODO: this treats *all* strings as regex which may confuse people - possibly check for // surrounding? + // would need explanation in help as well + regexFilters.forEach( function( filter, i ){ + if( _filter( unpaired, filter ) ){ + split[i].push( unpaired ); + } + }); + }); + return split; }, /** add a dataset to the unpaired list in it's proper order */ @@ -288,202 +363,79 @@ }, // ------------------------------------------------------------------------ auto pairing -//TODO: lots of boiler plate btwn the three auto pair fns /** two passes to automatically create pairs: * use both simpleAutoPair, then the fn mentioned in strategy */ autoPair : function( strategy ){ + // split first using exact matching + var split = this._splitByFilters(), + paired = []; + if( this.twoPassAutopairing ){ + paired = this.autopairSimple({ + listA : split[0], + listB : split[1] + }); + split = this._splitByFilters(); + } + + // uncomment to see printlns while running tests + //this.debug = function(){ console.log.apply( console, arguments ); }; + + // then try the remainder with something less strict strategy = strategy || this.strategy; - //this.debug( '-- autoPair', strategy ); - var paired = this.simpleAutoPair(); - paired = paired.concat( this[ strategy ].call( this ) ); + split = this._splitByFilters(); + paired = paired.concat( this[ strategy ].call( this, { + listA : split[0], + listB : split[1] + })); return paired; }, - /** attempts to pair forward with reverse when names exactly match (after removing filters) */ - simpleAutoPair : function(){ - //this.debug( '-- simpleAutoPair' ); - // simplified auto pair that moves down unpaired lists *in order*, - // removes filters' strings from fwd and rev, - // and, if names w/o filters *exactly* match, creates a pair - // possibly good as a first pass - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - matchFound = false, - paired = []; + autopairSimple : autoPairFnBuilder({ + scoreThreshold: function(){ return 1.0; }, + match : function _match( params ){ + params = params || {}; + if( params.matchTo === params.possible ){ + return { + index: params.index, + score: 1.0 + }; + } + return params.bestMatch; + } + }), - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - //TODO: go through the filterFwdFn - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - matchFound = false; + autopairLevenshtein : autoPairFnBuilder({ + scoreThreshold: function(){ return this.matchPercentage; }, + match : function _matches( params ){ + params = params || {}; + var distance = levenshteinDistance( params.matchTo, params.possible ), + score = 1.0 - ( distance / ( Math.max( params.matchTo.length, params.possible.length ) ) ); + if( score > params.bestMatch.score ){ + return { + index: params.index, + score: score + }; + } + return params.bestMatch; + } + }), - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev && fwdName === revName ){ - matchFound = true; - // if it is a match, keep i at current, pop fwd, pop rev and break - //this.debug( '---->', fwdName, revName ); - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( j, 1 )[0], - { silent: true } - ); - paired.push( pair ); - break; - } + autopairLCS : autoPairFnBuilder({ + scoreThreshold: function(){ return this.matchPercentage; }, + match : function _matches( params ){ + params = params || {}; + var match = this._naiveStartingAndEndingLCS( params.matchTo, params.possible ).length, + score = match / ( Math.max( params.matchTo.length, params.possible.length ) ); + if( score > params.bestMatch.score ){ + return { + index: params.index, + score: score + }; } - if( !matchFound ){ i += 1; } + return params.bestMatch; } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, - - /** attempt to autopair using edit distance between forward and reverse (after removing filters) */ - autoPairLevenshtein : function(){ - //precondition: filters are set, both lists are not empty, and all filenames.length > filters[?].length - //this.debug( '-- autoPairLevenshtein' ); - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - distance, bestIndex, bestDist, - paired = []; - - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - //TODO: go through the filterFwdFn - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - bestDist = Number.MAX_VALUE; - - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev ){ - if( fwdName === revName ){ - //this.debug( '\t\t exactmatch:', fwdName, revName ); - bestIndex = j; - bestDist = 0; - break; - } - distance = levenshteinDistance( fwdName, revName ); - //this.debug( '\t\t distance:', distance, 'bestDist:', bestDist ); - if( distance < bestDist ){ - bestIndex = j; - bestDist = distance; - } - } - } - //this.debug( '---->', fwd.name, bestIndex, bestDist ); - //this.debug( '---->', fwd.name, revList[ bestIndex ].name, bestDist ); - - var percentage = 1.0 - ( bestDist / ( Math.max( fwdName.length, revName.length ) ) ); - //this.debug( '----> %', percentage * 100 ); - - if( percentage >= this.matchPercentage ){ - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( bestIndex, 1 )[0], - { silent: true } - ); - paired.push( pair ); - if( fwdList.length <= 0 || revList.length <= 0 ){ - return paired; - } - } else { - i += 1; - } - } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, - - /** attempt to auto pair using common substrings from both front and back (after removing filters) */ - autoPairLCSs : function(){ - //precondition: filters are set, both lists are not empty - //this.debug( '-- autoPairLCSs' ); - var i = 0, j, - split = this._splitByFilters(), - fwdList = split[0], - revList = split[1], - fwdName, revName, - currMatch, bestIndex, bestMatch, - paired = []; - if( !fwdList.length || !revList.length ){ return paired; } - //this.debug( fwdList, revList ); - - while( i<fwdList.length ){ - var fwd = fwdList[ i ]; - fwdName = fwd.name.replace( this.filters[0], '' ); - //this.debug( i, 'fwd:', fwdName ); - bestMatch = 0; - - for( j=0; j<revList.length; j++ ){ - var rev = revList[ j ]; - revName = rev.name.replace( this.filters[1], '' ); - //this.debug( '\t ', j, 'rev:', revName ); - - if( fwd !== rev ){ - if( fwdName === revName ){ - //this.debug( '\t\t exactmatch:', fwdName, revName ); - bestIndex = j; - bestMatch = fwdName.length; - break; - } - var match = this._naiveStartingAndEndingLCS( fwdName, revName ); - currMatch = match.length; - //this.debug( '\t\t match:', match, 'currMatch:', currMatch, 'bestMatch:', bestMatch ); - if( currMatch > bestMatch ){ - bestIndex = j; - bestMatch = currMatch; - } - } - } - //this.debug( '---->', i, fwd.name, bestIndex, revList[ bestIndex ].name, bestMatch ); - - var percentage = bestMatch / ( Math.min( fwdName.length, revName.length ) ); - //this.debug( '----> %', percentage * 100 ); - - if( percentage >= this.matchPercentage ){ - var pair = this._pair( - fwdList.splice( i, 1 )[0], - revList.splice( bestIndex, 1 )[0], - { silent: true } - ); - paired.push( pair ); - if( fwdList.length <= 0 || revList.length <= 0 ){ - return paired; - } - } else { - i += 1; - } - } - //this.debug( 'remaining Forward:' ); - //this._printList( this.unpairedForward ); - //this.debug( 'remaining Reverse:' ); - //this._printList( this.unpairedReverse ); - //this.debug( '' ); - return paired; - }, + }), /** return the concat'd longest common prefix and suffix from two strings */ _naiveStartingAndEndingLCS : function( s1, s2 ){ @@ -1338,19 +1290,19 @@ // ........................................................................ paired - drag and drop re-ordering //_dragenterPairedColumns : function( ev ){ - // console.debug( '_dragenterPairedColumns:', ev ); + // this.debug( '_dragenterPairedColumns:', ev ); //}, //_dragleavePairedColumns : function( ev ){ - // //console.debug( '_dragleavePairedColumns:', ev ); + // //this.debug( '_dragleavePairedColumns:', ev ); //}, /** track the mouse drag over the paired list adding a placeholder to show where the drop would occur */ _dragoverPairedColumns : function( ev ){ - //console.debug( '_dragoverPairedColumns:', ev ); + //this.debug( '_dragoverPairedColumns:', ev ); ev.preventDefault(); var $list = this.$( '.paired-columns .column-datasets' ); this._checkForAutoscroll( $list, ev.originalEvent.clientY ); - //console.debug( ev.originalEvent.clientX, ev.originalEvent.clientY ); + //this.debug( ev.originalEvent.clientX, ev.originalEvent.clientY ); var $nearest = this._getNearestPairedDatasetLi( ev.originalEvent.clientY ); $( '.paired-drop-placeholder' ).remove(); @@ -1369,7 +1321,7 @@ scrollTop = $element.scrollTop(), upperDist = y - offset.top, lowerDist = ( offset.top + $element.outerHeight() ) - y; - //console.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist ); + //this.debug( '_checkForAutoscroll:', scrollTop, upperDist, lowerDist ); if( upperDist >= 0 && upperDist < this.autoscrollDist ){ $element.scrollTop( scrollTop - AUTOSCROLL_SPEED ); } else if( lowerDist >= 0 && lowerDist < this.autoscrollDist ){ @@ -1388,7 +1340,7 @@ top = $li.offset().top, halfHeight = Math.floor( $li.outerHeight() / 2 ) + WIGGLE; if( top + halfHeight > y && top - halfHeight < y ){ - //console.debug( y, top + halfHeight, top - halfHeight ) + //this.debug( y, top + halfHeight, top - halfHeight ) return $li; } } @@ -1419,13 +1371,13 @@ this.$( '.paired-columns .dataset.paired' ).each( function(){ newPaired.push( $( this ).data( 'pair' ) ); }); - //console.debug( newPaired ); + //this.debug( newPaired ); this.paired = newPaired; this._renderPaired(); }, /** drag communication with pair sub-views: dragstart */ _pairDragstart : function( ev, pair ){ - //console.debug( '_pairDragstart', ev, pair ) + //this.debug( '_pairDragstart', ev, pair ) // auto select the pair causing the event and move all selected pair.$el.addClass( 'selected' ); var $selected = this.$( '.paired-columns .dataset.selected' ); @@ -1433,7 +1385,7 @@ }, /** drag communication with pair sub-views: dragend - remove the placeholder */ _pairDragend : function( ev, pair ){ - //console.debug( '_pairDragend', ev, pair ) + //this.debug( '_pairDragend', ev, pair ) $( '.paired-drop-placeholder' ).remove(); this.$dragging = null; }, @@ -1736,7 +1688,6 @@ //============================================================================= /** a modal version of the paired collection creator */ var pairedCollectionCreatorModal = function _pairedCollectionCreatorModal( datasets, options ){ - console.log( datasets ); options = _.defaults( options || {}, { datasets : datasets, diff -r cd41c43e857f7db0d21a093f0d59ce8bf82a049e -r 581d871b810205f6a02e994983ffe316e206b746 static/scripts/packed/mvc/collection/paired-collection-creator.js --- a/static/scripts/packed/mvc/collection/paired-collection-creator.js +++ b/static/scripts/packed/mvc/collection/paired-collection-creator.js @@ -1,1 +1,1 @@ -define(["utils/levenshtein","utils/natural-sort","mvc/base-mvc","utils/localization"],function(h,g,a,d){var f=Backbone.View.extend(a.LoggableMixin).extend({tagName:"li",className:"dataset paired",initialize:function(i){this.pair=i.pair||{}},template:_.template(['<span class="forward-dataset-name flex-column"><%= pair.forward.name %></span>','<span class="pair-name-column flex-column">','<span class="pair-name"><%= pair.name %></span>',"</span>",'<span class="reverse-dataset-name flex-column"><%= pair.reverse.name %></span>'].join("")),render:function(){this.$el.attr("draggable",true).data("pair",this.pair).html(this.template({pair:this.pair})).addClass("flex-column-container");return this},events:{dragstart:"_dragstart",dragend:"_dragend",dragover:"_sendToParent",drop:"_sendToParent"},_dragstart:function(i){i.currentTarget.style.opacity="0.4";if(i.originalEvent){i=i.originalEvent}i.dataTransfer.effectAllowed="move";i.dataTransfer.setData("text/plain",JSON.stringify(this.pair));this.$el.parent().trigger("pair.dragstart",[this])},_dragend:function(i){i.currentTarget.style.opacity="1.0";this.$el.parent().trigger("pair.dragend",[this])},_sendToParent:function(i){this.$el.parent().trigger(i)},toString:function(){return"PairView("+this.pair.name+")"}});var e=Backbone.View.extend(a.LoggableMixin).extend({className:"collection-creator flex-row-container",initialize:function(i){i=_.defaults(i,{datasets:[],filters:this.DEFAULT_FILTERS,automaticallyPair:true,matchPercentage:1,strategy:"lcs"});this.initialList=i.datasets;this.historyId=i.historyId;this.filters=this.commonFilters[i.filters]||this.commonFilters[this.DEFAULT_FILTERS];if(_.isArray(i.filters)){this.filters=i.filters}this.automaticallyPair=i.automaticallyPair;this.matchPercentage=i.matchPercentage;this.strategy=this.strategies[i.strategy]||this.strategies[this.DEFAULT_STRATEGY];if(_.isFunction(i.strategy)){this.strategy=i.strategy}this.removeExtensions=true;this.oncancel=i.oncancel;this.oncreate=i.oncreate;this.autoscrollDist=i.autoscrollDist||24;this.unpairedPanelHidden=false;this.pairedPanelHidden=false;this.$dragging=null;this._setUpBehaviors();this._dataSetUp()},commonFilters:{none:["",""],illumina:["_1","_2"]},DEFAULT_FILTERS:"illumina",strategies:{lcs:"autoPairLCSs",levenshtein:"autoPairLevenshtein"},DEFAULT_STRATEGY:"lcs",_dataSetUp:function(){this.paired=[];this.unpaired=[];this.selectedIds=[];this._sortInitialList();this._ensureIds();this.unpaired=this.initialList.slice(0);if(this.automaticallyPair){this.autoPair();this.once("rendered:initial",function(){this.trigger("autopair")})}},_sortInitialList:function(){this._sortDatasetList(this.initialList)},_sortDatasetList:function(i){i.sort(function(k,j){return g(k.name,j.name)});return i},_ensureIds:function(){this.initialList.forEach(function(i){if(!i.hasOwnProperty("id")){i.id=_.uniqueId()}});return this.initialList},_splitByFilters:function(l){var k=[],i=[];function j(m){if(this._filterFwdFn(m)){k.push(m)}if(this._filterRevFn(m)){i.push(m)}}this.unpaired.forEach(_.bind(j,this));return[k,i]},_filterFwdFn:function(j){var i=new RegExp(this.filters[0]);return i.test(j.name)},_filterRevFn:function(j){var i=new RegExp(this.filters[1]);return i.test(j.name)},_addToUnpaired:function(j){var i=function(k,m){if(k===m){return k}var l=Math.floor((m-k)/2)+k,n=g(j.name,this.unpaired[l].name);if(n<0){return i(k,l)}else{if(n>0){return i(l+1,m)}}while(this.unpaired[l]&&this.unpaired[l].name===j.name){l++}return l}.bind(this);this.unpaired.splice(i(0,this.unpaired.length),0,j)},autoPair:function(j){j=j||this.strategy;var i=this.simpleAutoPair();i=i.concat(this[j].call(this));return i},simpleAutoPair:function(){var p=0,n,t=this._splitByFilters(),k=t[0],s=t[1],r,v,l=false,u=[];while(p<k.length){var o=k[p];r=o.name.replace(this.filters[0],"");l=false;for(n=0;n<s.length;n++){var q=s[n];v=q.name.replace(this.filters[1],"");if(o!==q&&r===v){l=true;var m=this._pair(k.splice(p,1)[0],s.splice(n,1)[0],{silent:true});u.push(m);break}}if(!l){p+=1}}return u},autoPairLevenshtein:function(){var q=0,o,v=this._splitByFilters(),k=v[0],t=v[1],s,y,l,u,m,w=[];while(q<k.length){var p=k[q];s=p.name.replace(this.filters[0],"");m=Number.MAX_VALUE;for(o=0;o<t.length;o++){var r=t[o];y=r.name.replace(this.filters[1],"");if(p!==r){if(s===y){u=o;m=0;break}l=levenshteinDistance(s,y);if(l<m){u=o;m=l}}}var x=1-(m/(Math.max(s.length,y.length)));if(x>=this.matchPercentage){var n=this._pair(k.splice(q,1)[0],t.splice(u,1)[0],{silent:true});w.push(n);if(k.length<=0||t.length<=0){return w}}else{q+=1}}return w},autoPairLCSs:function(){var o=0,m,v=this._splitByFilters(),k=v[0],t=v[1],s,z,y,u,q,w=[];if(!k.length||!t.length){return w}while(o<k.length){var n=k[o];s=n.name.replace(this.filters[0],"");q=0;for(m=0;m<t.length;m++){var r=t[m];z=r.name.replace(this.filters[1],"");if(n!==r){if(s===z){u=m;q=s.length;break}var p=this._naiveStartingAndEndingLCS(s,z);y=p.length;if(y>q){u=m;q=y}}}var x=q/(Math.min(s.length,z.length));if(x>=this.matchPercentage){var l=this._pair(k.splice(o,1)[0],t.splice(u,1)[0],{silent:true});w.push(l);if(k.length<=0||t.length<=0){return w}}else{o+=1}}return w},_naiveStartingAndEndingLCS:function(n,l){var o="",p="",m=0,k=0;while(m<n.length&&m<l.length){if(n[m]!==l[m]){break}o+=n[m];m+=1}if(m===n.length){return n}if(m===l.length){return l}m=(n.length-1);k=(l.length-1);while(m>=0&&k>=0){if(n[m]!==l[k]){break}p=[n[m],p].join("");m-=1;k-=1}return o+p},_pair:function(k,i,j){j=j||{};var l=this._createPair(k,i,j.name);this.paired.push(l);this.unpaired=_.without(this.unpaired,k,i);if(!j.silent){this.trigger("pair:new",l)}return l},_createPair:function(k,i,j){if(!(k&&i)||(k===i)){throw new Error("Bad pairing: "+[JSON.stringify(k),JSON.stringify(i)])}j=j||this._guessNameForPair(k,i);return{forward:k,name:j,reverse:i}},_guessNameForPair:function(k,i,l){l=(l!==undefined)?(l):(this.removeExtensions);var j=this._naiveStartingAndEndingLCS(k.name.replace(this.filters[0],""),i.name.replace(this.filters[1],""));if(l){var m=j.lastIndexOf(".");if(m>0){j=j.slice(0,m)}}return j||(k.name+" & "+i.name)},_unpair:function(j,i){i=i||{};if(!j){throw new Error("Bad pair: "+JSON.stringify(j))}this.paired=_.without(this.paired,j);this._addToUnpaired(j.forward);this._addToUnpaired(j.reverse);if(!i.silent){this.trigger("pair:unpair",[j])}return j},unpairAll:function(){var i=[];while(this.paired.length){i.push(this._unpair(this.paired[0],{silent:true}))}this.trigger("pair:unpair",i)},_pairToJSON:function(i){return{collection_type:"paired",src:"new_collection",name:i.name,element_identifiers:[{name:"forward",id:i.forward.id,src:"hda"},{name:"reverse",id:i.reverse.id,src:"hda"}]}},createList:function(k){var l=this,j;if(l.historyId){j="/api/histories/"+this.historyId+"/contents/dataset_collections"}var i={type:"dataset_collection",collection_type:"list:paired",name:_.escape(k||l.$(".collection-name").val()),element_identifiers:l.paired.map(function(m){return l._pairToJSON(m)})};return jQuery.ajax(j,{type:"POST",contentType:"application/json",dataType:"json",data:JSON.stringify(i)}).fail(function(o,m,n){l._ajaxErrHandler(o,m,n)}).done(function(m,n,o){l.trigger("collection:created",m,n,o);if(typeof l.oncreate==="function"){l.oncreate.call(this,m,n,o)}})},_ajaxErrHandler:function(l,i,k){this.error(l,i,k);var j=d("An error occurred while creating this collection");if(l){if(l.readyState===0&&l.status===0){j+=": "+d("Galaxy could not be reached and may be updating.")+d(" Try again in a few minutes.")}else{if(l.responseJSON){j+="<br /><pre>"+JSON.stringify(l.responseJSON)+"</pre>"}else{j+=": "+k}}}creator._showAlert(j,"alert-danger")},render:function(i,j){this.$el.empty().html(e.templates.main());this._renderHeader(i);this._renderMiddle(i);this._renderFooter(i);this._addPluginComponents();this.trigger("rendered",this);return this},_renderHeader:function(j,k){var i=this.$(".header").empty().html(e.templates.header()).find(".help-content").prepend($(e.templates.helpContent()));this._renderFilters();return i},_renderFilters:function(){return this.$(".forward-column .column-header input").val(this.filters[0]).add(this.$(".reverse-column .column-header input").val(this.filters[1]))},_renderMiddle:function(j,k){var i=this.$(".middle").empty().html(e.templates.middle());if(this.unpairedPanelHidden){this.$(".unpaired-columns").hide()}else{if(this.pairedPanelHidden){this.$(".paired-columns").hide()}}this._renderUnpaired();this._renderPaired();return i},_renderUnpaired:function(n,o){var l=this,m,j,i=[],k=this._splitByFilters();this.$(".forward-column .title").text([k[0].length,d("unpaired forward")].join(" "));this.$(".forward-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-k[0].length));this.$(".reverse-column .title").text([k[1].length,d("unpaired reverse")].join(" "));this.$(".reverse-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-k[1].length));this.$(".unpaired-columns .column-datasets").empty();this.$(".autopair-link").toggle(this.unpaired.length!==0);if(this.unpaired.length===0){this._renderUnpairedEmpty();return}j=k[1].map(function(q,p){if((k[0][p]!==undefined)&&(k[0][p]!==q)){i.push(l._renderPairButton())}return l._renderUnpairedDataset(q)});m=k[0].map(function(p){return l._renderUnpairedDataset(p)});if(!m.length&&!j.length){this._renderUnpairedNotShown();return}this.$(".unpaired-columns .forward-column .column-datasets").append(m).add(this.$(".unpaired-columns .paired-column .column-datasets").append(i)).add(this.$(".unpaired-columns .reverse-column .column-datasets").append(j));this._adjUnpairedOnScrollbar()},_renderUnpairedDisplayStr:function(i){return["(",i," ",d("filtered out"),")"].join("")},_renderUnpairedDataset:function(i){return $("<li/>").attr("id","dataset-"+i.id).addClass("dataset unpaired").attr("draggable",true).addClass(i.selected?"selected":"").append($("<span/>").addClass("dataset-name").text(i.name)).data("dataset",i)},_renderPairButton:function(){return $("<li/>").addClass("dataset unpaired").append($("<span/>").addClass("dataset-name").text(d("Pair these datasets")))},_renderUnpairedEmpty:function(){var i=$('<div class="empty-message"></div>').text("("+d("no remaining unpaired datasets")+")");this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(i);return i},_renderUnpairedNotShown:function(){var i=$('<div class="empty-message"></div>').text("("+d("no datasets were found matching the current filters")+")");this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(i);return i},_adjUnpairedOnScrollbar:function(){var l=this.$(".unpaired-columns").last(),m=this.$(".unpaired-columns .reverse-column .dataset").first();if(!m.size()){return}var i=l.offset().left+l.outerWidth(),k=m.offset().left+m.outerWidth(),j=Math.floor(i)-Math.floor(k);this.$(".unpaired-columns .forward-column").css("margin-left",(j>0)?j:0)},_renderPaired:function(j,k){this.$(".paired-column-title .title").text([this.paired.length,d("paired")].join(" "));this.$(".unpair-all-link").toggle(this.paired.length!==0);if(this.paired.length===0){this._renderPairedEmpty();return}else{this.$(".remove-extensions-link").show()}this.$(".paired-columns .column-datasets").empty();var i=this;this.paired.forEach(function(n,l){var m=new f({pair:n});i.$(".paired-columns .column-datasets").append(m.render().$el).append(['<button class="unpair-btn">','<span class="fa fa-unlink" title="',d("Unpair"),'"></span>',"</button>"].join(""))})},_renderPairedEmpty:function(){var i=$('<div class="empty-message"></div>').text("("+d("no paired datasets yet")+")");this.$(".paired-columns .column-datasets").empty().prepend(i);return i},_renderFooter:function(j,k){var i=this.$(".footer").empty().html(e.templates.footer());this.$(".remove-extensions").prop("checked",this.removeExtensions);if(typeof this.oncancel==="function"){this.$(".cancel-create.btn").show()}return i},_addPluginComponents:function(){this._chooseFiltersPopover(".choose-filters-link");this.$(".help-content i").hoverhighlight(".collection-creator","rgba( 64, 255, 255, 1.0 )")},_chooseFiltersPopover:function(i){function j(m,l){return['<button class="filter-choice btn" ','data-forward="',m,'" data-reverse="',l,'">',d("Forward"),": ",m,", ",d("Reverse"),": ",l,"</button>"].join("")}var k=$(_.template(['<div class="choose-filters">','<div class="help">',d("Choose from the following filters to change which unpaired reads are shown in the display"),":</div>",j("_1","_2"),j("_R1","_R2"),"</div>"].join(""))({}));return this.$(i).popover({container:".collection-creator",placement:"bottom",html:true,content:k})},_validationWarning:function(j,i){var k="validation-warning";if(j==="name"){j=this.$(".collection-name").add(this.$(".collection-name-prompt"));this.$(".collection-name").focus().select()}if(i){j=j||this.$("."+k);j.removeClass(k)}else{j.addClass(k)}},_setUpBehaviors:function(){this.once("rendered",function(){this.trigger("rendered:initial",this)});this.on("pair:new",function(){this._renderUnpaired();this._renderPaired();this.$(".paired-columns").scrollTop(8000000)});this.on("pair:unpair",function(i){this._renderUnpaired();this._renderPaired();this.splitView()});this.on("filter-change",function(){this.filters=[this.$(".forward-unpaired-filter input").val(),this.$(".reverse-unpaired-filter input").val()];this._renderFilters();this._renderUnpaired()});this.on("autopair",function(){this._renderUnpaired();this._renderPaired();var i,j=null;if(this.paired.length){j="alert-success";i=this.paired.length+" "+d("pairs created");if(!this.unpaired.length){i+=": "+d("all datasets have been successfully paired");this.hideUnpaired();this.$(".collection-name").focus()}}else{i=d("Could not automatically create any pairs from the given dataset names")}this._showAlert(i,j)});return this},events:{"click .more-help":"_clickMoreHelp","click .less-help":"_clickLessHelp","click .header .alert button":"_hideAlert","click .forward-column .column-title":"_clickShowOnlyUnpaired","click .reverse-column .column-title":"_clickShowOnlyUnpaired","click .unpair-all-link":"_clickUnpairAll","change .forward-unpaired-filter input":function(i){this.trigger("filter-change")},"focus .forward-unpaired-filter input":function(i){$(i.currentTarget).select()},"click .autopair-link":"_clickAutopair","click .choose-filters .filter-choice":"_clickFilterChoice","click .clear-filters-link":"_clearFilters","change .reverse-unpaired-filter input":function(i){this.trigger("filter-change")},"focus .reverse-unpaired-filter input":function(i){$(i.currentTarget).select()},"click .forward-column .dataset.unpaired":"_clickUnpairedDataset","click .reverse-column .dataset.unpaired":"_clickUnpairedDataset","click .paired-column .dataset.unpaired":"_clickPairRow","click .unpaired-columns":"clearSelectedUnpaired","mousedown .unpaired-columns .dataset":"_mousedownUnpaired","click .paired-column-title":"_clickShowOnlyPaired","mousedown .flexible-partition-drag":"_startPartitionDrag","click .paired-columns .dataset.paired":"selectPair","click .paired-columns":"clearSelectedPaired","click .paired-columns .pair-name":"_clickPairName","click .unpair-btn":"_clickUnpair","dragover .paired-columns .column-datasets":"_dragoverPairedColumns","drop .paired-columns .column-datasets":"_dropPairedColumns","pair.dragstart .paired-columns .column-datasets":"_pairDragstart","pair.dragend .paired-columns .column-datasets":"_pairDragend","change .remove-extensions":function(i){this.toggleExtensions()},"change .collection-name":"_changeName","keydown .collection-name":"_nameCheckForEnter","click .cancel-create":function(i){if(typeof this.oncancel==="function"){this.oncancel.call(this)}},"click .create-collection":"_clickCreate"},_clickMoreHelp:function(i){this.$(".main-help").addClass("expanded");this.$(".more-help").hide()},_clickLessHelp:function(i){this.$(".main-help").removeClass("expanded");this.$(".more-help").show()},_showAlert:function(j,i){i=i||"alert-danger";this.$(".main-help").hide();this.$(".header .alert").attr("class","alert alert-dismissable").addClass(i).show().find(".alert-message").html(j)},_hideAlert:function(i){this.$(".main-help").show();this.$(".header .alert").hide()},_clickShowOnlyUnpaired:function(i){if(this.$(".paired-columns").is(":visible")){this.hidePaired()}else{this.splitView()}},_clickShowOnlyPaired:function(i){if(this.$(".unpaired-columns").is(":visible")){this.hideUnpaired()}else{this.splitView()}},hideUnpaired:function(i,j){this.unpairedPanelHidden=true;this.pairedPanelHidden=false;this._renderMiddle(i,j)},hidePaired:function(i,j){this.unpairedPanelHidden=false;this.pairedPanelHidden=true;this._renderMiddle(i,j)},splitView:function(i,j){this.unpairedPanelHidden=this.pairedPanelHidden=false;this._renderMiddle(i,j);return this},_clickUnpairAll:function(i){this.unpairAll()},_clickAutopair:function(i){this.autoPair();this.trigger("autopair")},_clickFilterChoice:function(j){var i=$(j.currentTarget);this.$(".forward-unpaired-filter input").val(i.data("forward"));this.$(".reverse-unpaired-filter input").val(i.data("reverse"));this._hideChooseFilters();this.trigger("filter-change")},_hideChooseFilters:function(){this.$(".choose-filters-link").popover("hide");this.$(".popover").css("display","none")},_clearFilters:function(i){this.$(".forward-unpaired-filter input").val("");this.$(".reverse-unpaired-filter input").val("");this.trigger("filter-change")},_clickUnpairedDataset:function(i){i.stopPropagation();return this.toggleSelectUnpaired($(i.currentTarget))},toggleSelectUnpaired:function(k,j){j=j||{};var l=k.data("dataset"),i=j.force!==undefined?j.force:!k.hasClass("selected");if(!k.size()||l===undefined){return k}if(i){k.addClass("selected");if(!j.waitToPair){this.pairAllSelected()}}else{k.removeClass("selected")}return k},pairAllSelected:function(j){j=j||{};var k=this,l=[],i=[],m=[];k.$(".unpaired-columns .forward-column .dataset.selected").each(function(){l.push($(this).data("dataset"))});k.$(".unpaired-columns .reverse-column .dataset.selected").each(function(){i.push($(this).data("dataset"))});l.length=i.length=Math.min(l.length,i.length);l.forEach(function(o,n){try{m.push(k._pair(o,i[n],{silent:true}))}catch(p){k.error(p)}});if(m.length&&!j.silent){this.trigger("pair:new",m)}return m},clearSelectedUnpaired:function(){this.$(".unpaired-columns .dataset.selected").removeClass("selected")},_mousedownUnpaired:function(k){if(k.shiftKey){var j=this,i=$(k.target).addClass("selected"),l=function(m){j.$(m.target).filter(".dataset").addClass("selected")};i.parent().on("mousemove",l);$(document).one("mouseup",function(m){i.parent().off("mousemove",l);j.pairAllSelected()})}},_clickPairRow:function(k){var l=$(k.currentTarget).index(),j=$(".unpaired-columns .forward-column .dataset").eq(l).data("dataset"),i=$(".unpaired-columns .reverse-column .dataset").eq(l).data("dataset");this._pair(j,i)},_startPartitionDrag:function(j){var i=this,m=j.pageY;$("body").css("cursor","ns-resize");i.$(".flexible-partition-drag").css("color","black");function l(n){i.$(".flexible-partition-drag").css("color","");$("body").css("cursor","").unbind("mousemove",k)}function k(n){var o=n.pageY-m;if(!i.adjPartition(o)){$("body").trigger("mouseup")}i._adjUnpairedOnScrollbar();m+=o}$("body").mousemove(k);$("body").one("mouseup",l)},adjPartition:function(j){var i=this.$(".unpaired-columns"),k=this.$(".paired-columns"),l=parseInt(i.css("height"),10),m=parseInt(k.css("height"),10);l=Math.max(10,l+j);m=m-j;var n=j<0;if(n){if(this.unpairedPanelHidden){return false}else{if(l<=10){this.hideUnpaired();return false}}}else{if(this.unpairedPanelHidden){i.show();this.unpairedPanelHidden=false}}if(!n){if(this.pairedPanelHidden){return false}else{if(m<=15){this.hidePaired();return false}}}else{if(this.pairedPanelHidden){k.show();this.pairedPanelHidden=false}}i.css({height:l+"px",flex:"0 0 auto"});return true},selectPair:function(i){i.stopPropagation();$(i.currentTarget).toggleClass("selected")},clearSelectedPaired:function(i){this.$(".paired-columns .dataset.selected").removeClass("selected")},_clickPairName:function(l){l.stopPropagation();var n=$(l.currentTarget),k=n.parent().parent(),j=k.index(".dataset.paired"),m=this.paired[j],i=prompt("Enter a new name for the pair:",m.name);if(i){m.name=i;m.customizedName=true;n.text(m.name)}},_clickUnpair:function(j){var i=Math.floor($(j.currentTarget).index(".unpair-btn"));this._unpair(this.paired[i])},_dragoverPairedColumns:function(l){l.preventDefault();var j=this.$(".paired-columns .column-datasets");this._checkForAutoscroll(j,l.originalEvent.clientY);var k=this._getNearestPairedDatasetLi(l.originalEvent.clientY);$(".paired-drop-placeholder").remove();var i=$('<div class="paired-drop-placeholder"></div>');if(!k.size()){j.append(i)}else{k.before(i)}},_checkForAutoscroll:function(i,o){var m=2;var n=i.offset(),l=i.scrollTop(),j=o-n.top,k=(n.top+i.outerHeight())-o;if(j>=0&&j<this.autoscrollDist){i.scrollTop(l-m)}else{if(k>=0&&k<this.autoscrollDist){i.scrollTop(l+m)}}},_getNearestPairedDatasetLi:function(p){var m=4,k=this.$(".paired-columns .column-datasets li").toArray();for(var l=0;l<k.length;l++){var o=$(k[l]),n=o.offset().top,j=Math.floor(o.outerHeight()/2)+m;if(n+j>p&&n-j<p){return o}}return $()},_dropPairedColumns:function(j){j.preventDefault();j.dataTransfer.dropEffect="move";var i=this._getNearestPairedDatasetLi(j.originalEvent.clientY);if(i.size()){this.$dragging.insertBefore(i)}else{this.$dragging.insertAfter(this.$(".paired-columns .unpair-btn").last())}this._syncPairsToDom();return false},_syncPairsToDom:function(){var i=[];this.$(".paired-columns .dataset.paired").each(function(){i.push($(this).data("pair"))});this.paired=i;this._renderPaired()},_pairDragstart:function(j,k){k.$el.addClass("selected");var i=this.$(".paired-columns .dataset.selected");this.$dragging=i},_pairDragend:function(i,j){$(".paired-drop-placeholder").remove();this.$dragging=null},toggleExtensions:function(j){var i=this;i.removeExtensions=(j!==undefined)?(j):(!i.removeExtensions);_.each(i.paired,function(k){if(k.customizedName){return}k.name=i._guessNameForPair(k.forward,k.reverse)});i._renderPaired();i._renderFooter()},_changeName:function(i){this._validationWarning("name",!!this._getName())},_nameCheckForEnter:function(i){if(i.keyCode===13){this._clickCreate()}},_getName:function(){return _.escape(this.$(".collection-name").val())},_clickCreate:function(j){var i=this._getName();if(!i){this._validationWarning("name")}else{this.createList()}},_printList:function(j){var i=this;_.each(j,function(k){if(j===i.paired){i._printPair(k)}else{}})},_printPair:function(i){this.debug(i.forward.name,i.reverse.name,": ->",i.name)},toString:function(){return"PairedCollectionCreator"}});e.templates=e.templates||{main:_.template(['<div class="header flex-row no-flex"></div>','<div class="middle flex-row flex-row-container"></div>','<div class="footer flex-row no-flex">'].join("")),header:_.template(['<div class="main-help well clear">','<a class="more-help" href="javascript:void(0);">',d("More help"),"</a>",'<div class="help-content">','<a class="less-help" href="javascript:void(0);">',d("Less"),"</a>","</div>","</div>",'<div class="alert alert-dismissable">','<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>','<span class="alert-message"></span>',"</div>",'<div class="column-headers vertically-spaced flex-column-container">','<div class="forward-column flex-column column">','<div class="column-header">','<div class="column-title">','<span class="title">',d("Unpaired forward"),"</span>",'<span class="title-info unpaired-info"></span>',"</div>",'<div class="unpaired-filter forward-unpaired-filter pull-left">','<input class="search-query" placeholder="',d("Filter this list"),'" />',"</div>","</div>","</div>",'<div class="paired-column flex-column no-flex column">','<div class="column-header">','<a class="choose-filters-link" href="javascript:void(0)">',d("Choose filters"),"</a>",'<a class="clear-filters-link" href="javascript:void(0);">',d("Clear filters"),"</a><br />",'<a class="autopair-link" href="javascript:void(0);">',d("Auto-pair"),"</a>","</div>","</div>",'<div class="reverse-column flex-column column">','<div class="column-header">','<div class="column-title">','<span class="title">',d("Unpaired reverse"),"</span>",'<span class="title-info unpaired-info"></span>',"</div>",'<div class="unpaired-filter reverse-unpaired-filter pull-left">','<input class="search-query" placeholder="',d("Filter this list"),'" />',"</div>","</div>","</div>","</div>"].join("")),middle:_.template(['<div class="unpaired-columns flex-column-container scroll-container flex-row">','<div class="forward-column flex-column column">','<ol class="column-datasets"></ol>',"</div>",'<div class="paired-column flex-column no-flex column">','<ol class="column-datasets"></ol>',"</div>",'<div class="reverse-column flex-column column">','<ol class="column-datasets"></ol>',"</div>","</div>",'<div class="flexible-partition">','<div class="flexible-partition-drag" title="',d("Drag to change"),'"></div>','<div class="column-header">','<div class="column-title paired-column-title">','<span class="title"></span>',"</div>",'<a class="unpair-all-link" href="javascript:void(0);">',d("Unpair all"),"</a>","</div>","</div>",'<div class="paired-columns flex-column-container scroll-container flex-row">','<ol class="column-datasets"></ol>',"</div>"].join("")),footer:_.template(['<div class="attributes clear">','<div class="clear">','<label class="remove-extensions-prompt pull-right">',d("Remove file extensions from pair names"),"?",'<input class="remove-extensions pull-right" type="checkbox" />',"</label>","</div>",'<div class="clear">','<input class="collection-name form-control pull-right" ','placeholder="',d("Enter a name for your new list"),'" />','<div class="collection-name-prompt pull-right">',d("Name"),":</div>","</div>","</div>",'<div class="actions clear vertically-spaced">','<div class="other-options pull-left">','<button class="cancel-create btn" tabindex="-1">',d("Cancel"),"</button>",'<div class="create-other btn-group dropup">','<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">',d("Create a different kind of collection"),' <span class="caret"></span>',"</button>",'<ul class="dropdown-menu" role="menu">','<li><a href="#">',d("Create a <i>single</i> pair"),"</a></li>",'<li><a href="#">',d("Create a list of <i>unpaired</i> datasets"),"</a></li>","</ul>","</div>","</div>",'<div class="main-options pull-right">','<button class="create-collection btn btn-primary">',d("Create list"),"</button>","</div>","</div>"].join("")),helpContent:_.template(["<p>",d(["Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ","These collections can be passed to tools and workflows in order to have analyses done on each member of ","the entire group. This interface allows you to create a collection, choose which datasets are paired, ","and re-order the final collection."].join("")),"</p>","<p>",d(['Unpaired datasets are shown in the <i data-target=".unpaired-columns">unpaired section</i> ',"(hover over the underlined words to highlight below). ",'Paired datasets are shown in the <i data-target=".paired-columns">paired section</i>.',"<ul>To pair datasets, you can:","<li>Click a dataset in the ",'<i data-target=".unpaired-columns .forward-column .column-datasets,','.unpaired-columns .forward-column">forward column</i> ',"to select it then click a dataset in the ",'<i data-target=".unpaired-columns .reverse-column .column-datasets,','.unpaired-columns .reverse-column">reverse column</i>.',"</li>",'<li>Click one of the "Pair these datasets" buttons in the ','<i data-target=".unpaired-columns .paired-column .column-datasets,','.unpaired-columns .paired-column">middle column</i> ',"to pair the datasets in a particular row.","</li>",'<li>Click <i data-target=".autopair-link">"Auto-pair"</i> ',"to have your datasets automatically paired based on name.","</li>","</ul>"].join("")),"</p>","<p>",d(["<ul>You can filter what is shown in the unpaired sections by:","<li>Entering partial dataset names in either the ",'<i data-target=".forward-unpaired-filter input">forward filter</i> or ','<i data-target=".reverse-unpaired-filter input">reverse filter</i>.',"</li>","<li>Choosing from a list of preset filters by clicking the ",'<i data-target=".choose-filters-link">"Choose filters" link</i>.',"</li>","<li>Entering regular expressions to match dataset names. See: ",'<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expres..."',' target="_blank">MDN\'s JavaScript Regular Expression Tutorial</a>. ',"Note: forward slashes (\\) are not needed.","</li>","<li>Clearing the filters by clicking the ",'<i data-target=".clear-filters-link">"Clear filters" link</i>.',"</li>","</ul>"].join("")),"</p>","<p>",d(["To unpair individual dataset pairs, click the ",'<i data-target=".unpair-btn">unpair buttons ( <span class="fa fa-unlink"></span> )</i>. ','Click the <i data-target=".unpair-all-link">"Unpair all" link</i> to unpair all pairs.'].join("")),"</p>","<p>",d(['You can include or remove the file extensions (e.g. ".fastq") from your pair names by toggling the ','<i data-target=".remove-extensions-prompt">"Remove file extensions from pair names?"</i> control.'].join("")),"</p>","<p>",d(['Once your collection is complete, enter a <i data-target=".collection-name">name</i> and ','click <i data-target=".create-collection">"Create list"</i>. ',"(Note: you do not have to pair all unpaired datasets to finish.)"].join("")),"</p>"].join(""))};(function(){jQuery.fn.extend({hoverhighlight:function i(k,j){k=k||"body";if(!this.size()){return this}$(this).each(function(){var m=$(this),l=m.data("target");if(l){m.mouseover(function(n){$(l,k).css({background:j})}).mouseout(function(n){$(l).css({background:""})})}});return this}})}());var b=function c(k,i){console.log(k);i=_.defaults(i||{},{datasets:k,oncancel:function(){Galaxy.modal.hide()},oncreate:function(){Galaxy.modal.hide();Galaxy.currHistoryPanel.refreshContents()}});if(!window.Galaxy||!Galaxy.modal){throw new Error("Galaxy or Galaxy.modal not found")}var j=new e(i).render();Galaxy.modal.show({title:"Create a collection of paired datasets",body:j.$el,width:"80%",height:"800px",closing_events:true});window.PCC=j;return j};return{PairedCollectionCreator:e,pairedCollectionCreatorModal:b}}); \ No newline at end of file +define(["utils/levenshtein","utils/natural-sort","mvc/base-mvc","utils/localization"],function(h,b,f,c){var i=Backbone.View.extend(f.LoggableMixin).extend({tagName:"li",className:"dataset paired",initialize:function(l){this.pair=l.pair||{}},template:_.template(['<span class="forward-dataset-name flex-column"><%= pair.forward.name %></span>','<span class="pair-name-column flex-column">','<span class="pair-name"><%= pair.name %></span>',"</span>",'<span class="reverse-dataset-name flex-column"><%= pair.reverse.name %></span>'].join("")),render:function(){this.$el.attr("draggable",true).data("pair",this.pair).html(this.template({pair:this.pair})).addClass("flex-column-container");return this},events:{dragstart:"_dragstart",dragend:"_dragend",dragover:"_sendToParent",drop:"_sendToParent"},_dragstart:function(l){l.currentTarget.style.opacity="0.4";if(l.originalEvent){l=l.originalEvent}l.dataTransfer.effectAllowed="move";l.dataTransfer.setData("text/plain",JSON.stringify(this.pair));this.$el.parent().trigger("pair.dragstart",[this])},_dragend:function(l){l.currentTarget.style.opacity="1.0";this.$el.parent().trigger("pair.dragend",[this])},_sendToParent:function(l){this.$el.parent().trigger(l)},toString:function(){return"PairView("+this.pair.name+")"}});function g(m){m=m||{};m.createPair=m.createPair||function l(r){this.debug("creating pair:",r.listA[r.indexA].name,r.listB[r.indexB].name);r=r||{};return this._pair(r.listA.splice(r.indexA,1)[0],r.listB.splice(r.indexB,1)[0],{silent:true})};var o=[];function q(){if(!o.length){o=[new RegExp(this.filters[0]),new RegExp(this.filters[1])]}return o}m.preprocessMatch=m.preprocessMatch||function n(s){var r=q.call(this);return _.extend(s,{matchTo:s.matchTo.name.replace(r[0],""),possible:s.possible.name.replace(r[1],"")})};return function p(t){this.debug("autopair _strategy ---------------------------");t=t||{};var r=t.listA,A=t.listB,z=0,y,v={score:0,index:null},x=[];this.debug("starting list lens:",r.length,A.length);this.debug("bestMatch (starting):",JSON.stringify(v,null," "));while(z<r.length){var w=r[z];v.score=0;for(y=0;y<A.length;y++){var u=A[y];this.debug(z+":"+w.name);this.debug(y+":"+u.name);if(r[z]!==A[y]){v=m.match.call(this,m.preprocessMatch.call(this,{matchTo:w,possible:u,index:y,bestMatch:v}));this.debug("bestMatch:",JSON.stringify(v,null," "));if(v.score===1){this.debug("breaking early due to perfect match");break}}}var s=m.scoreThreshold.call(this);this.debug("scoreThreshold:",s);this.debug("bestMatch.score:",v.score);if(v.score>=s){this.debug("creating pair");x.push(m.createPair.call(this,{listA:r,indexA:z,listB:A,indexB:v.index}));this.debug("list lens now:",r.length,A.length)}else{z+=1}if(!r.length||!A.length){return x}}this.debug("paired:",JSON.stringify(x,null," "));this.debug("autopair _strategy ---------------------------");return x}}var k=Backbone.View.extend(f.LoggableMixin).extend({className:"collection-creator flex-row-container",initialize:function(l){l=_.defaults(l,{datasets:[],filters:this.DEFAULT_FILTERS,automaticallyPair:true,strategy:"lcs",matchPercentage:0.9,twoPassAutopairing:true});this.initialList=l.datasets;this.historyId=l.historyId;this.filters=this.commonFilters[l.filters]||this.commonFilters[this.DEFAULT_FILTERS];if(_.isArray(l.filters)){this.filters=l.filters}this.automaticallyPair=l.automaticallyPair;this.strategy=this.strategies[l.strategy]||this.strategies[this.DEFAULT_STRATEGY];if(_.isFunction(l.strategy)){this.strategy=l.strategy}this.matchPercentage=l.matchPercentage;this.twoPassAutopairing=l.twoPassAutopairing;this.removeExtensions=true;this.oncancel=l.oncancel;this.oncreate=l.oncreate;this.autoscrollDist=l.autoscrollDist||24;this.unpairedPanelHidden=false;this.pairedPanelHidden=false;this.$dragging=null;this._setUpBehaviors();this._dataSetUp()},commonFilters:{none:["",""],illumina:["_1","_2"]},DEFAULT_FILTERS:"illumina",strategies:{simple:"autopairSimple",lcs:"autopairLCS",levenshtein:"autopairLevenshtein"},DEFAULT_STRATEGY:"lcs",_dataSetUp:function(){this.paired=[];this.unpaired=[];this.selectedIds=[];this._sortInitialList();this._ensureIds();this.unpaired=this.initialList.slice(0);if(this.automaticallyPair){this.autoPair();this.once("rendered:initial",function(){this.trigger("autopair")})}},_sortInitialList:function(){this._sortDatasetList(this.initialList)},_sortDatasetList:function(l){l.sort(function(n,m){return b(n.name,m.name)});return l},_ensureIds:function(){this.initialList.forEach(function(l){if(!l.hasOwnProperty("id")){l.id=_.uniqueId()}});return this.initialList},_splitByFilters:function(){var o=this.filters.map(function(p){return new RegExp(p)}),m=[[],[]];function n(p,q){return q.test(p.name)}this.unpaired.forEach(function l(p){o.forEach(function(r,q){if(n(p,r)){m[q].push(p)}})});return m},_addToUnpaired:function(m){var l=function(n,p){if(n===p){return n}var o=Math.floor((p-n)/2)+n,q=b(m.name,this.unpaired[o].name);if(q<0){return l(n,o)}else{if(q>0){return l(o+1,p)}}while(this.unpaired[o]&&this.unpaired[o].name===m.name){o++}return o}.bind(this);this.unpaired.splice(l(0,this.unpaired.length),0,m)},autoPair:function(n){var m=this._splitByFilters(),l=[];if(this.twoPassAutopairing){l=this.autopairSimple({listA:m[0],listB:m[1]});m=this._splitByFilters()}n=n||this.strategy;m=this._splitByFilters();l=l.concat(this[n].call(this,{listA:m[0],listB:m[1]}));return l},autopairSimple:g({scoreThreshold:function(){return 1},match:function j(l){l=l||{};if(l.matchTo===l.possible){return{index:l.index,score:1}}return l.bestMatch}}),autopairLevenshtein:g({scoreThreshold:function(){return this.matchPercentage},match:function e(m){m=m||{};var n=h(m.matchTo,m.possible),l=1-(n/(Math.max(m.matchTo.length,m.possible.length)));if(l>m.bestMatch.score){return{index:m.index,score:l}}return m.bestMatch}}),autopairLCS:g({scoreThreshold:function(){return this.matchPercentage},match:function e(n){n=n||{};var l=this._naiveStartingAndEndingLCS(n.matchTo,n.possible).length,m=l/(Math.max(n.matchTo.length,n.possible.length));if(m>n.bestMatch.score){return{index:n.index,score:m}}return n.bestMatch}}),_naiveStartingAndEndingLCS:function(o,m){var p="",q="",n=0,l=0;while(n<o.length&&n<m.length){if(o[n]!==m[n]){break}p+=o[n];n+=1}if(n===o.length){return o}if(n===m.length){return m}n=(o.length-1);l=(m.length-1);while(n>=0&&l>=0){if(o[n]!==m[l]){break}q=[o[n],q].join("");n-=1;l-=1}return p+q},_pair:function(n,l,m){m=m||{};var o=this._createPair(n,l,m.name);this.paired.push(o);this.unpaired=_.without(this.unpaired,n,l);if(!m.silent){this.trigger("pair:new",o)}return o},_createPair:function(n,l,m){if(!(n&&l)||(n===l)){throw new Error("Bad pairing: "+[JSON.stringify(n),JSON.stringify(l)])}m=m||this._guessNameForPair(n,l);return{forward:n,name:m,reverse:l}},_guessNameForPair:function(n,l,o){o=(o!==undefined)?(o):(this.removeExtensions);var m=this._naiveStartingAndEndingLCS(n.name.replace(this.filters[0],""),l.name.replace(this.filters[1],""));if(o){var p=m.lastIndexOf(".");if(p>0){m=m.slice(0,p)}}return m||(n.name+" & "+l.name)},_unpair:function(m,l){l=l||{};if(!m){throw new Error("Bad pair: "+JSON.stringify(m))}this.paired=_.without(this.paired,m);this._addToUnpaired(m.forward);this._addToUnpaired(m.reverse);if(!l.silent){this.trigger("pair:unpair",[m])}return m},unpairAll:function(){var l=[];while(this.paired.length){l.push(this._unpair(this.paired[0],{silent:true}))}this.trigger("pair:unpair",l)},_pairToJSON:function(l){return{collection_type:"paired",src:"new_collection",name:l.name,element_identifiers:[{name:"forward",id:l.forward.id,src:"hda"},{name:"reverse",id:l.reverse.id,src:"hda"}]}},createList:function(n){var o=this,m;if(o.historyId){m="/api/histories/"+this.historyId+"/contents/dataset_collections"}var l={type:"dataset_collection",collection_type:"list:paired",name:_.escape(n||o.$(".collection-name").val()),element_identifiers:o.paired.map(function(p){return o._pairToJSON(p)})};return jQuery.ajax(m,{type:"POST",contentType:"application/json",dataType:"json",data:JSON.stringify(l)}).fail(function(r,p,q){o._ajaxErrHandler(r,p,q)}).done(function(p,q,r){o.trigger("collection:created",p,q,r);if(typeof o.oncreate==="function"){o.oncreate.call(this,p,q,r)}})},_ajaxErrHandler:function(o,l,n){this.error(o,l,n);var m=c("An error occurred while creating this collection");if(o){if(o.readyState===0&&o.status===0){m+=": "+c("Galaxy could not be reached and may be updating.")+c(" Try again in a few minutes.")}else{if(o.responseJSON){m+="<br /><pre>"+JSON.stringify(o.responseJSON)+"</pre>"}else{m+=": "+n}}}creator._showAlert(m,"alert-danger")},render:function(l,m){this.$el.empty().html(k.templates.main());this._renderHeader(l);this._renderMiddle(l);this._renderFooter(l);this._addPluginComponents();this.trigger("rendered",this);return this},_renderHeader:function(m,n){var l=this.$(".header").empty().html(k.templates.header()).find(".help-content").prepend($(k.templates.helpContent()));this._renderFilters();return l},_renderFilters:function(){return this.$(".forward-column .column-header input").val(this.filters[0]).add(this.$(".reverse-column .column-header input").val(this.filters[1]))},_renderMiddle:function(m,n){var l=this.$(".middle").empty().html(k.templates.middle());if(this.unpairedPanelHidden){this.$(".unpaired-columns").hide()}else{if(this.pairedPanelHidden){this.$(".paired-columns").hide()}}this._renderUnpaired();this._renderPaired();return l},_renderUnpaired:function(q,r){var o=this,p,m,l=[],n=this._splitByFilters();this.$(".forward-column .title").text([n[0].length,c("unpaired forward")].join(" "));this.$(".forward-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-n[0].length));this.$(".reverse-column .title").text([n[1].length,c("unpaired reverse")].join(" "));this.$(".reverse-column .unpaired-info").text(this._renderUnpairedDisplayStr(this.unpaired.length-n[1].length));this.$(".unpaired-columns .column-datasets").empty();this.$(".autopair-link").toggle(this.unpaired.length!==0);if(this.unpaired.length===0){this._renderUnpairedEmpty();return}m=n[1].map(function(t,s){if((n[0][s]!==undefined)&&(n[0][s]!==t)){l.push(o._renderPairButton())}return o._renderUnpairedDataset(t)});p=n[0].map(function(s){return o._renderUnpairedDataset(s)});if(!p.length&&!m.length){this._renderUnpairedNotShown();return}this.$(".unpaired-columns .forward-column .column-datasets").append(p).add(this.$(".unpaired-columns .paired-column .column-datasets").append(l)).add(this.$(".unpaired-columns .reverse-column .column-datasets").append(m));this._adjUnpairedOnScrollbar()},_renderUnpairedDisplayStr:function(l){return["(",l," ",c("filtered out"),")"].join("")},_renderUnpairedDataset:function(l){return $("<li/>").attr("id","dataset-"+l.id).addClass("dataset unpaired").attr("draggable",true).addClass(l.selected?"selected":"").append($("<span/>").addClass("dataset-name").text(l.name)).data("dataset",l)},_renderPairButton:function(){return $("<li/>").addClass("dataset unpaired").append($("<span/>").addClass("dataset-name").text(c("Pair these datasets")))},_renderUnpairedEmpty:function(){var l=$('<div class="empty-message"></div>').text("("+c("no remaining unpaired datasets")+")");this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(l);return l},_renderUnpairedNotShown:function(){var l=$('<div class="empty-message"></div>').text("("+c("no datasets were found matching the current filters")+")");this.$(".unpaired-columns .paired-column .column-datasets").empty().prepend(l);return l},_adjUnpairedOnScrollbar:function(){var o=this.$(".unpaired-columns").last(),p=this.$(".unpaired-columns .reverse-column .dataset").first();if(!p.size()){return}var l=o.offset().left+o.outerWidth(),n=p.offset().left+p.outerWidth(),m=Math.floor(l)-Math.floor(n);this.$(".unpaired-columns .forward-column").css("margin-left",(m>0)?m:0)},_renderPaired:function(m,n){this.$(".paired-column-title .title").text([this.paired.length,c("paired")].join(" "));this.$(".unpair-all-link").toggle(this.paired.length!==0);if(this.paired.length===0){this._renderPairedEmpty();return}else{this.$(".remove-extensions-link").show()}this.$(".paired-columns .column-datasets").empty();var l=this;this.paired.forEach(function(q,o){var p=new i({pair:q});l.$(".paired-columns .column-datasets").append(p.render().$el).append(['<button class="unpair-btn">','<span class="fa fa-unlink" title="',c("Unpair"),'"></span>',"</button>"].join(""))})},_renderPairedEmpty:function(){var l=$('<div class="empty-message"></div>').text("("+c("no paired datasets yet")+")");this.$(".paired-columns .column-datasets").empty().prepend(l);return l},_renderFooter:function(m,n){var l=this.$(".footer").empty().html(k.templates.footer());this.$(".remove-extensions").prop("checked",this.removeExtensions);if(typeof this.oncancel==="function"){this.$(".cancel-create.btn").show()}return l},_addPluginComponents:function(){this._chooseFiltersPopover(".choose-filters-link");this.$(".help-content i").hoverhighlight(".collection-creator","rgba( 64, 255, 255, 1.0 )")},_chooseFiltersPopover:function(l){function m(p,o){return['<button class="filter-choice btn" ','data-forward="',p,'" data-reverse="',o,'">',c("Forward"),": ",p,", ",c("Reverse"),": ",o,"</button>"].join("")}var n=$(_.template(['<div class="choose-filters">','<div class="help">',c("Choose from the following filters to change which unpaired reads are shown in the display"),":</div>",m("_1","_2"),m("_R1","_R2"),"</div>"].join(""))({}));return this.$(l).popover({container:".collection-creator",placement:"bottom",html:true,content:n})},_validationWarning:function(m,l){var n="validation-warning";if(m==="name"){m=this.$(".collection-name").add(this.$(".collection-name-prompt"));this.$(".collection-name").focus().select()}if(l){m=m||this.$("."+n);m.removeClass(n)}else{m.addClass(n)}},_setUpBehaviors:function(){this.once("rendered",function(){this.trigger("rendered:initial",this)});this.on("pair:new",function(){this._renderUnpaired();this._renderPaired();this.$(".paired-columns").scrollTop(8000000)});this.on("pair:unpair",function(l){this._renderUnpaired();this._renderPaired();this.splitView()});this.on("filter-change",function(){this.filters=[this.$(".forward-unpaired-filter input").val(),this.$(".reverse-unpaired-filter input").val()];this._renderFilters();this._renderUnpaired()});this.on("autopair",function(){this._renderUnpaired();this._renderPaired();var l,m=null;if(this.paired.length){m="alert-success";l=this.paired.length+" "+c("pairs created");if(!this.unpaired.length){l+=": "+c("all datasets have been successfully paired");this.hideUnpaired();this.$(".collection-name").focus()}}else{l=c("Could not automatically create any pairs from the given dataset names")}this._showAlert(l,m)});return this},events:{"click .more-help":"_clickMoreHelp","click .less-help":"_clickLessHelp","click .header .alert button":"_hideAlert","click .forward-column .column-title":"_clickShowOnlyUnpaired","click .reverse-column .column-title":"_clickShowOnlyUnpaired","click .unpair-all-link":"_clickUnpairAll","change .forward-unpaired-filter input":function(l){this.trigger("filter-change")},"focus .forward-unpaired-filter input":function(l){$(l.currentTarget).select()},"click .autopair-link":"_clickAutopair","click .choose-filters .filter-choice":"_clickFilterChoice","click .clear-filters-link":"_clearFilters","change .reverse-unpaired-filter input":function(l){this.trigger("filter-change")},"focus .reverse-unpaired-filter input":function(l){$(l.currentTarget).select()},"click .forward-column .dataset.unpaired":"_clickUnpairedDataset","click .reverse-column .dataset.unpaired":"_clickUnpairedDataset","click .paired-column .dataset.unpaired":"_clickPairRow","click .unpaired-columns":"clearSelectedUnpaired","mousedown .unpaired-columns .dataset":"_mousedownUnpaired","click .paired-column-title":"_clickShowOnlyPaired","mousedown .flexible-partition-drag":"_startPartitionDrag","click .paired-columns .dataset.paired":"selectPair","click .paired-columns":"clearSelectedPaired","click .paired-columns .pair-name":"_clickPairName","click .unpair-btn":"_clickUnpair","dragover .paired-columns .column-datasets":"_dragoverPairedColumns","drop .paired-columns .column-datasets":"_dropPairedColumns","pair.dragstart .paired-columns .column-datasets":"_pairDragstart","pair.dragend .paired-columns .column-datasets":"_pairDragend","change .remove-extensions":function(l){this.toggleExtensions()},"change .collection-name":"_changeName","keydown .collection-name":"_nameCheckForEnter","click .cancel-create":function(l){if(typeof this.oncancel==="function"){this.oncancel.call(this)}},"click .create-collection":"_clickCreate"},_clickMoreHelp:function(l){this.$(".main-help").addClass("expanded");this.$(".more-help").hide()},_clickLessHelp:function(l){this.$(".main-help").removeClass("expanded");this.$(".more-help").show()},_showAlert:function(m,l){l=l||"alert-danger";this.$(".main-help").hide();this.$(".header .alert").attr("class","alert alert-dismissable").addClass(l).show().find(".alert-message").html(m)},_hideAlert:function(l){this.$(".main-help").show();this.$(".header .alert").hide()},_clickShowOnlyUnpaired:function(l){if(this.$(".paired-columns").is(":visible")){this.hidePaired()}else{this.splitView()}},_clickShowOnlyPaired:function(l){if(this.$(".unpaired-columns").is(":visible")){this.hideUnpaired()}else{this.splitView()}},hideUnpaired:function(l,m){this.unpairedPanelHidden=true;this.pairedPanelHidden=false;this._renderMiddle(l,m)},hidePaired:function(l,m){this.unpairedPanelHidden=false;this.pairedPanelHidden=true;this._renderMiddle(l,m)},splitView:function(l,m){this.unpairedPanelHidden=this.pairedPanelHidden=false;this._renderMiddle(l,m);return this},_clickUnpairAll:function(l){this.unpairAll()},_clickAutopair:function(l){this.autoPair();this.trigger("autopair")},_clickFilterChoice:function(m){var l=$(m.currentTarget);this.$(".forward-unpaired-filter input").val(l.data("forward"));this.$(".reverse-unpaired-filter input").val(l.data("reverse"));this._hideChooseFilters();this.trigger("filter-change")},_hideChooseFilters:function(){this.$(".choose-filters-link").popover("hide");this.$(".popover").css("display","none")},_clearFilters:function(l){this.$(".forward-unpaired-filter input").val("");this.$(".reverse-unpaired-filter input").val("");this.trigger("filter-change")},_clickUnpairedDataset:function(l){l.stopPropagation();return this.toggleSelectUnpaired($(l.currentTarget))},toggleSelectUnpaired:function(n,m){m=m||{};var o=n.data("dataset"),l=m.force!==undefined?m.force:!n.hasClass("selected");if(!n.size()||o===undefined){return n}if(l){n.addClass("selected");if(!m.waitToPair){this.pairAllSelected()}}else{n.removeClass("selected")}return n},pairAllSelected:function(m){m=m||{};var n=this,o=[],l=[],p=[];n.$(".unpaired-columns .forward-column .dataset.selected").each(function(){o.push($(this).data("dataset"))});n.$(".unpaired-columns .reverse-column .dataset.selected").each(function(){l.push($(this).data("dataset"))});o.length=l.length=Math.min(o.length,l.length);o.forEach(function(r,q){try{p.push(n._pair(r,l[q],{silent:true}))}catch(s){n.error(s)}});if(p.length&&!m.silent){this.trigger("pair:new",p)}return p},clearSelectedUnpaired:function(){this.$(".unpaired-columns .dataset.selected").removeClass("selected")},_mousedownUnpaired:function(n){if(n.shiftKey){var m=this,l=$(n.target).addClass("selected"),o=function(p){m.$(p.target).filter(".dataset").addClass("selected")};l.parent().on("mousemove",o);$(document).one("mouseup",function(p){l.parent().off("mousemove",o);m.pairAllSelected()})}},_clickPairRow:function(n){var o=$(n.currentTarget).index(),m=$(".unpaired-columns .forward-column .dataset").eq(o).data("dataset"),l=$(".unpaired-columns .reverse-column .dataset").eq(o).data("dataset");this._pair(m,l)},_startPartitionDrag:function(m){var l=this,p=m.pageY;$("body").css("cursor","ns-resize");l.$(".flexible-partition-drag").css("color","black");function o(q){l.$(".flexible-partition-drag").css("color","");$("body").css("cursor","").unbind("mousemove",n)}function n(q){var r=q.pageY-p;if(!l.adjPartition(r)){$("body").trigger("mouseup")}l._adjUnpairedOnScrollbar();p+=r}$("body").mousemove(n);$("body").one("mouseup",o)},adjPartition:function(m){var l=this.$(".unpaired-columns"),n=this.$(".paired-columns"),o=parseInt(l.css("height"),10),p=parseInt(n.css("height"),10);o=Math.max(10,o+m);p=p-m;var q=m<0;if(q){if(this.unpairedPanelHidden){return false}else{if(o<=10){this.hideUnpaired();return false}}}else{if(this.unpairedPanelHidden){l.show();this.unpairedPanelHidden=false}}if(!q){if(this.pairedPanelHidden){return false}else{if(p<=15){this.hidePaired();return false}}}else{if(this.pairedPanelHidden){n.show();this.pairedPanelHidden=false}}l.css({height:o+"px",flex:"0 0 auto"});return true},selectPair:function(l){l.stopPropagation();$(l.currentTarget).toggleClass("selected")},clearSelectedPaired:function(l){this.$(".paired-columns .dataset.selected").removeClass("selected")},_clickPairName:function(o){o.stopPropagation();var q=$(o.currentTarget),n=q.parent().parent(),m=n.index(".dataset.paired"),p=this.paired[m],l=prompt("Enter a new name for the pair:",p.name);if(l){p.name=l;p.customizedName=true;q.text(p.name)}},_clickUnpair:function(m){var l=Math.floor($(m.currentTarget).index(".unpair-btn"));this._unpair(this.paired[l])},_dragoverPairedColumns:function(o){o.preventDefault();var m=this.$(".paired-columns .column-datasets");this._checkForAutoscroll(m,o.originalEvent.clientY);var n=this._getNearestPairedDatasetLi(o.originalEvent.clientY);$(".paired-drop-placeholder").remove();var l=$('<div class="paired-drop-placeholder"></div>');if(!n.size()){m.append(l)}else{n.before(l)}},_checkForAutoscroll:function(l,r){var p=2;var q=l.offset(),o=l.scrollTop(),m=r-q.top,n=(q.top+l.outerHeight())-r;if(m>=0&&m<this.autoscrollDist){l.scrollTop(o-p)}else{if(n>=0&&n<this.autoscrollDist){l.scrollTop(o+p)}}},_getNearestPairedDatasetLi:function(r){var o=4,m=this.$(".paired-columns .column-datasets li").toArray();for(var n=0;n<m.length;n++){var q=$(m[n]),p=q.offset().top,l=Math.floor(q.outerHeight()/2)+o;if(p+l>r&&p-l<r){return q}}return $()},_dropPairedColumns:function(m){m.preventDefault();m.dataTransfer.dropEffect="move";var l=this._getNearestPairedDatasetLi(m.originalEvent.clientY);if(l.size()){this.$dragging.insertBefore(l)}else{this.$dragging.insertAfter(this.$(".paired-columns .unpair-btn").last())}this._syncPairsToDom();return false},_syncPairsToDom:function(){var l=[];this.$(".paired-columns .dataset.paired").each(function(){l.push($(this).data("pair"))});this.paired=l;this._renderPaired()},_pairDragstart:function(m,n){n.$el.addClass("selected");var l=this.$(".paired-columns .dataset.selected");this.$dragging=l},_pairDragend:function(l,m){$(".paired-drop-placeholder").remove();this.$dragging=null},toggleExtensions:function(m){var l=this;l.removeExtensions=(m!==undefined)?(m):(!l.removeExtensions);_.each(l.paired,function(n){if(n.customizedName){return}n.name=l._guessNameForPair(n.forward,n.reverse)});l._renderPaired();l._renderFooter()},_changeName:function(l){this._validationWarning("name",!!this._getName())},_nameCheckForEnter:function(l){if(l.keyCode===13){this._clickCreate()}},_getName:function(){return _.escape(this.$(".collection-name").val())},_clickCreate:function(m){var l=this._getName();if(!l){this._validationWarning("name")}else{this.createList()}},_printList:function(m){var l=this;_.each(m,function(n){if(m===l.paired){l._printPair(n)}else{}})},_printPair:function(l){this.debug(l.forward.name,l.reverse.name,": ->",l.name)},toString:function(){return"PairedCollectionCreator"}});k.templates=k.templates||{main:_.template(['<div class="header flex-row no-flex"></div>','<div class="middle flex-row flex-row-container"></div>','<div class="footer flex-row no-flex">'].join("")),header:_.template(['<div class="main-help well clear">','<a class="more-help" href="javascript:void(0);">',c("More help"),"</a>",'<div class="help-content">','<a class="less-help" href="javascript:void(0);">',c("Less"),"</a>","</div>","</div>",'<div class="alert alert-dismissable">','<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>','<span class="alert-message"></span>',"</div>",'<div class="column-headers vertically-spaced flex-column-container">','<div class="forward-column flex-column column">','<div class="column-header">','<div class="column-title">','<span class="title">',c("Unpaired forward"),"</span>",'<span class="title-info unpaired-info"></span>',"</div>",'<div class="unpaired-filter forward-unpaired-filter pull-left">','<input class="search-query" placeholder="',c("Filter this list"),'" />',"</div>","</div>","</div>",'<div class="paired-column flex-column no-flex column">','<div class="column-header">','<a class="choose-filters-link" href="javascript:void(0)">',c("Choose filters"),"</a>",'<a class="clear-filters-link" href="javascript:void(0);">',c("Clear filters"),"</a><br />",'<a class="autopair-link" href="javascript:void(0);">',c("Auto-pair"),"</a>","</div>","</div>",'<div class="reverse-column flex-column column">','<div class="column-header">','<div class="column-title">','<span class="title">',c("Unpaired reverse"),"</span>",'<span class="title-info unpaired-info"></span>',"</div>",'<div class="unpaired-filter reverse-unpaired-filter pull-left">','<input class="search-query" placeholder="',c("Filter this list"),'" />',"</div>","</div>","</div>","</div>"].join("")),middle:_.template(['<div class="unpaired-columns flex-column-container scroll-container flex-row">','<div class="forward-column flex-column column">','<ol class="column-datasets"></ol>',"</div>",'<div class="paired-column flex-column no-flex column">','<ol class="column-datasets"></ol>',"</div>",'<div class="reverse-column flex-column column">','<ol class="column-datasets"></ol>',"</div>","</div>",'<div class="flexible-partition">','<div class="flexible-partition-drag" title="',c("Drag to change"),'"></div>','<div class="column-header">','<div class="column-title paired-column-title">','<span class="title"></span>',"</div>",'<a class="unpair-all-link" href="javascript:void(0);">',c("Unpair all"),"</a>","</div>","</div>",'<div class="paired-columns flex-column-container scroll-container flex-row">','<ol class="column-datasets"></ol>',"</div>"].join("")),footer:_.template(['<div class="attributes clear">','<div class="clear">','<label class="remove-extensions-prompt pull-right">',c("Remove file extensions from pair names"),"?",'<input class="remove-extensions pull-right" type="checkbox" />',"</label>","</div>",'<div class="clear">','<input class="collection-name form-control pull-right" ','placeholder="',c("Enter a name for your new list"),'" />','<div class="collection-name-prompt pull-right">',c("Name"),":</div>","</div>","</div>",'<div class="actions clear vertically-spaced">','<div class="other-options pull-left">','<button class="cancel-create btn" tabindex="-1">',c("Cancel"),"</button>",'<div class="create-other btn-group dropup">','<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">',c("Create a different kind of collection"),' <span class="caret"></span>',"</button>",'<ul class="dropdown-menu" role="menu">','<li><a href="#">',c("Create a <i>single</i> pair"),"</a></li>",'<li><a href="#">',c("Create a list of <i>unpaired</i> datasets"),"</a></li>","</ul>","</div>","</div>",'<div class="main-options pull-right">','<button class="create-collection btn btn-primary">',c("Create list"),"</button>","</div>","</div>"].join("")),helpContent:_.template(["<p>",c(["Collections of paired datasets are ordered lists of dataset pairs (often forward and reverse reads). ","These collections can be passed to tools and workflows in order to have analyses done on each member of ","the entire group. This interface allows you to create a collection, choose which datasets are paired, ","and re-order the final collection."].join("")),"</p>","<p>",c(['Unpaired datasets are shown in the <i data-target=".unpaired-columns">unpaired section</i> ',"(hover over the underlined words to highlight below). ",'Paired datasets are shown in the <i data-target=".paired-columns">paired section</i>.',"<ul>To pair datasets, you can:","<li>Click a dataset in the ",'<i data-target=".unpaired-columns .forward-column .column-datasets,','.unpaired-columns .forward-column">forward column</i> ',"to select it then click a dataset in the ",'<i data-target=".unpaired-columns .reverse-column .column-datasets,','.unpaired-columns .reverse-column">reverse column</i>.',"</li>",'<li>Click one of the "Pair these datasets" buttons in the ','<i data-target=".unpaired-columns .paired-column .column-datasets,','.unpaired-columns .paired-column">middle column</i> ',"to pair the datasets in a particular row.","</li>",'<li>Click <i data-target=".autopair-link">"Auto-pair"</i> ',"to have your datasets automatically paired based on name.","</li>","</ul>"].join("")),"</p>","<p>",c(["<ul>You can filter what is shown in the unpaired sections by:","<li>Entering partial dataset names in either the ",'<i data-target=".forward-unpaired-filter input">forward filter</i> or ','<i data-target=".reverse-unpaired-filter input">reverse filter</i>.',"</li>","<li>Choosing from a list of preset filters by clicking the ",'<i data-target=".choose-filters-link">"Choose filters" link</i>.',"</li>","<li>Entering regular expressions to match dataset names. See: ",'<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expres..."',' target="_blank">MDN\'s JavaScript Regular Expression Tutorial</a>. ',"Note: forward slashes (\\) are not needed.","</li>","<li>Clearing the filters by clicking the ",'<i data-target=".clear-filters-link">"Clear filters" link</i>.',"</li>","</ul>"].join("")),"</p>","<p>",c(["To unpair individual dataset pairs, click the ",'<i data-target=".unpair-btn">unpair buttons ( <span class="fa fa-unlink"></span> )</i>. ','Click the <i data-target=".unpair-all-link">"Unpair all" link</i> to unpair all pairs.'].join("")),"</p>","<p>",c(['You can include or remove the file extensions (e.g. ".fastq") from your pair names by toggling the ','<i data-target=".remove-extensions-prompt">"Remove file extensions from pair names?"</i> control.'].join("")),"</p>","<p>",c(['Once your collection is complete, enter a <i data-target=".collection-name">name</i> and ','click <i data-target=".create-collection">"Create list"</i>. ',"(Note: you do not have to pair all unpaired datasets to finish.)"].join("")),"</p>"].join(""))};(function(){jQuery.fn.extend({hoverhighlight:function l(n,m){n=n||"body";if(!this.size()){return this}$(this).each(function(){var p=$(this),o=p.data("target");if(o){p.mouseover(function(q){$(o,n).css({background:m})}).mouseout(function(q){$(o).css({background:""})})}});return this}})}());var d=function a(n,l){l=_.defaults(l||{},{datasets:n,oncancel:function(){Galaxy.modal.hide()},oncreate:function(){Galaxy.modal.hide();Galaxy.currHistoryPanel.refreshContents()}});if(!window.Galaxy||!Galaxy.modal){throw new Error("Galaxy or Galaxy.modal not found")}var m=new k(l).render();Galaxy.modal.show({title:"Create a collection of paired datasets",body:m.$el,width:"80%",height:"800px",closing_events:true});window.PCC=m;return m};return{PairedCollectionCreator:k,pairedCollectionCreatorModal:d}}); \ No newline at end of file diff -r cd41c43e857f7db0d21a093f0d59ce8bf82a049e -r 581d871b810205f6a02e994983ffe316e206b746 test/qunit/tests/paired-collection-creator.js --- a/test/qunit/tests/paired-collection-creator.js +++ b/test/qunit/tests/paired-collection-creator.js @@ -22,7 +22,7 @@ ok( pcc.hasOwnProperty( 'options' ) && typeof pcc.options === 'object' ); deepEqual( pcc.options.filters, pcc.DEFAULT_FILTERS ); ok( pcc.options.automaticallyPair ); - equal( pcc.options.matchPercentage, 1.0 ); + equal( pcc.options.matchPercentage, 0.9 ); equal( pcc.options.strategy, 'lcs' ); }); @@ -44,6 +44,36 @@ equal( pcc.paired.length, pcc.initialList.length / 2 ); }); + test( "Try easy autopairing with simple exact matching", function() { + var pcc = new PCC({ + datasets : DATA._1, + strategy : 'simple', + twoPassAutopairing : false + }); + equal( pcc.unpaired.length, 0 ); + equal( pcc.paired.length, pcc.initialList.length / 2 ); + }); + + test( "Try easy autopairing with LCS", function() { + var pcc = new PCC({ + datasets : DATA._1, + strategy : 'lcs', + twoPassAutopairing : false + }); + equal( pcc.unpaired.length, 0 ); + equal( pcc.paired.length, pcc.initialList.length / 2 ); + }); + + test( "Try easy autopairing with Levenshtein", function() { + var pcc = new PCC({ + datasets : DATA._1, + strategy : 'levenshtein', + twoPassAutopairing : false + }); + equal( pcc.unpaired.length, 0 ); + equal( pcc.paired.length, pcc.initialList.length / 2 ); + }); + //TODO: // filters: clearing, setting via popover, regex // partition: maximize paired, maximize unpaired, split evenly @@ -69,10 +99,9 @@ ); }); - console.debug( 'requestBody:', JSON.stringify( requestJSON, null, ' ' ) ); + //console.debug( 'requestBody:', JSON.stringify( requestJSON, null, ' ' ) ); pcc.createList( 'Heres a collection' ); server.respond(); deepEqual( requestJSON, DATA._1requestJSON ); }); - }); Repository URL: https://bitbucket.org/galaxy/galaxy-central/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.