@gmod/jbrowse
Version:
JBrowse - client-side genome browser
227 lines (200 loc) • 8.19 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/Deferred',
'dojo/when',
'dojo/promise/all',
'JBrowse/Store/SeqFeature',
'JBrowse/Model/SimpleFeature',
'JBrowse/Model/BinaryTreeNode',
'JBrowse/Util'
],
function(
declare,
array,
Deferred,
when,
all,
SeqFeatureStore,
SimpleFeature,
TreeNode,
Util
) {
return declare([SeqFeatureStore], {
// A store that takes in two feature stores (one of them set-based e.g. NCList) and uses the data from one store as a mask
// for the other. Although the design resembles those of combinationStores, differences are substantial enough that
// this class does not derive from CombinationBase.
constructor: function( args ) {
this.isCombinationStore = true;
this.inverse = args.inverse || false;
this.stores = {};
if(args.mask && args.display) {
this.reload(args.mask, args.display);
}
},
// Loads an opTree (optionally), and a mask and display store. Ensure all stores exist,
// and build an operation tree for the benefit of combination tracks.
reload: function(opTree, mask, display) {
var inverse;
this.gotAllStores = new Deferred();
if(opTree) {
this.opTree = opTree;
this.inverse = (inverse === undefined) ? (opTree.get() == "N") : inverse;
this.stores.mask = opTree.leftChild && !mask ? opTree.leftChild.get() : mask;
this.stores.display = opTree.rightChild && !display ? opTree.rightChild.get() : display;
this.gotAllStores.resolve(true);
}
else {
if(inverse !== undefined) {
this.inverse = inverse;
}
this.opTree = new TreeNode({Value: this.inverse ? "N" : "M"});
this.stores.mask = mask;
this.stores.display = display;
var thisB = this;
var grabStore = function(store) {
var haveStore = new Deferred();
if(typeof store == "string") {
thisB.browser.getStore(store, function(result) {
if(result) {
haveStore.resolve(result, true);
} else {
haveStore.reject("store " + store + " not found");
}
});
} else {
haveStore.resolve(store, true);
}
return haveStore.promise;
}
var haveMaskStore = grabStore(this.stores.mask).then( function(store) {
thisB.stores.mask = store;
});
var haveDisplayStore = grabStore(this.stores.display).then( function(store) {
thisB.stores.display = store;
});
this.gotAllStores = all([haveMaskStore, haveDisplayStore]);
this.gotAllStores.then( function() {
thisB.opTree.leftChild = thisB.stores.mask.isCombinationStore ? thisB.stores.mask.opTree : new TreeNode({Value: thisB.stores.mask});
thisB.opTree.rightChild = thisB.stores.display.isCombinationStore ? thisB.stores.display.opTree : new TreeNode({Value: thisB.stores.display});
});
}
},
// The global stats of this store should be the same as those for the display data.
getGlobalStats: function (callback, errorCallback) {
this.stores.display.getGlobalStats( callback, errorCallback );
},
// The regional stats of this store should be the same as those for the display data.
getRegionStats: function (query, callback, errorCallback) {
this.stores.display.getRegionStats( query, callback, errorCallback );
},
// Gets the features from the mask and display stores, and then returns the display store features with the mask store features
// added as masks
getFeatures: function( query, featCallback, doneCallback, errorCallback ) {
var thisB = this;
this.gotAllStores.then(
function() {
var featureArray = {};
// Get features from one particular store
var grabFeats = function( key ) {
var d = new Deferred( );
featureArray[key] = [];
thisB.stores[key].getFeatures( query,
function(feature) {
featureArray[key].push( feature );
},
function() { d.resolve( true ); },
function() { d.reject( "failed to load features for " + key + " store" ); }
);
return d.promise;
}
when(all([grabFeats( "mask" ), grabFeats( "display" )]),
function() {
// Convert mask features into simplified spans
var spans = thisB.toSpans( featureArray.mask, query );
// invert masking spans if necessary
spans = thisB.inverse ? thisB.notSpan( spans, query ) : spans;
var features = featureArray.display;
thisB.maskFeatures( features, spans, featCallback, doneCallback );
}, errorCallback
);
}, errorCallback);
},
// given a feature or pseudo-feature, returns true if the feature
// overlaps the span. False otherwise.
inSpan: function( feature, span ) {
if ( !feature || !span ) {
console.error("invalid arguments to inSpan function");
}
return feature.get ? !( feature.get('start') >= span.end || feature.get('end') <= span.start ) :
!( feature.start >= span.end || feature.end <= span.start );
},
maskFeatures: function( features, spans, featCallback, doneCallback ) {
/* Pass features to the track's original featCallback, and pass spans to the doneCallback.
* If the track has boolean support, the DoneCallback will use the spans to mask the features.
* For glyph based tracks, the masks passed to each feature will be used to do masking.
*/
for ( var key in features ) {
if ( features.hasOwnProperty( key ) ) {
var feat = features[key];
delete feat.masks;
for (var span in spans ) {
if ( spans.hasOwnProperty( span ) && this.inSpan( feat, spans[span] ) ) {
// add masks to the feature. Used by Glyphs to do masking.
feat.masks = feat.masks ? feat.masks.concat( [spans[span]] ) : [spans[span]];
}
}
featCallback( features[key] )
}
}
doneCallback( { maskingSpans: spans} );
},
notSpan: function( spans, query ) {
// creates the complement spans of the input spans
var invSpan = [];
invSpan[0] = { start: query.start };
var i = 0;
for (var span in spans) {
if ( spans.hasOwnProperty( span ) ) {
span = spans[span];
invSpan[i].end = span.start;
i++;
invSpan[i] = { start: span.end };
}
}
invSpan[i].end = query.end;
if (invSpan[i].end <= invSpan[i].start) {
invSpan.splice(i,1);
}
if (invSpan[0].end <= invSpan[0].start) {
invSpan.splice(0,1);
}
return invSpan;
},
toSpans: function(features, query) {
// given a set of features, takes the "union" of them and outputs a single set of nonoverlapping spans
var spans = [];
for (var feature in features) {
if (features.hasOwnProperty(feature)) {
spans.push( { start: features[feature].get('start'), //Math.max( features[feature].get('start'), query.start ),
end: features[feature].get('end') //Math.min( features[feature].get('end'), query.end )
} );
}
}
if(!spans.length) return [];
spans.sort(function(a,b) { return a.start - b.start; });
var retSpans = [];
var i = 0;
while(i < spans.length) {
var start = spans[i].start;
var end = spans[i].end;
while(i < spans.length && spans[i].start <= end) {
end = Math.max(end, spans[i].end);
i++;
}
retSpans.push( { start: start, end: end});
}
return retSpans;
}
});
});