@gmod/jbrowse
Version:
JBrowse - client-side genome browser
207 lines (178 loc) • 7.36 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/array',
'dojo/Deferred',
'dojo/when',
'dojo/promise/all',
'JBrowse/Store/SeqFeature',
'JBrowse/Store/DeferredStatsMixin',
'JBrowse/Store/DeferredFeaturesMixin',
'JBrowse/Store/SeqFeature/GlobalStatsEstimationMixin',
'JBrowse/Util',
'JBrowse/Model/BinaryTreeNode'
],
function(
declare,
array,
Deferred,
when,
all,
SeqFeatureStore,
DeferredStatsMixin,
DeferredFeaturesMixin,
GlobalStatsEstimationMixin,
Util,
TreeNode
) {
// Helper object that wraps a feature and which store it comes from
var featureWrapper = Util.fastDeclare(
{
get: function( arg ) {
return this.feature.get(arg);
},
id: function() {
return this.feature.id()+this.storeName;
},
parent: function() {
return this.feature.parent();
},
children: function() {
return this.feature.children();
},
tags: function() {
return this.feature.tags();
},
constructor: function( feat, storeName ) {
this.feature = feat;
this.storeName = storeName;
this.source = feat ? feat.source : undefined;
}
}
);
return declare([SeqFeatureStore, DeferredFeaturesMixin, DeferredStatsMixin, GlobalStatsEstimationMixin], {
// The base class for combination stores. A combination store is one that pulls feature data from other stores
// and combines it according to a binary tree of operations in order to produce new features.
constructor: function( args ) {
// Objects can access this to know if a given store is a combination store of some kind
this.isCombinationStore = true;
this.defaultOp = args.op;
// If constructed with an opTree already included, might as well try to get all the store info from that opTree.
if(args.opTree) {
this.reload(args.opTree);
}
},
// Loads an operation tree (opTree).
reload: function( optree ) {
this._deferred.features = new Deferred();
this._deferred.stats = new Deferred();
var refSeq;
// Load in opTree
if( !optree) {
optree = new TreeNode({ Value: this.defaultOp});
}
this.opTree = optree;
this.stores = optree.getLeaves() || [];
// If any of the stores doesn't have a name, then something weird is happening...
for(var store in this.stores) {
if(!this.stores[store].name) {
this.stores = [];
}
}
var thisB = this;
this._deferred.features.resolve(true);
delete this._regionStatsCache;
this._estimateGlobalStats().then( dojo.hitch(
this,
function( stats ) {
this.globalStats = stats;
this._deferred.stats.resolve({success:true});
}
),
dojo.hitch( this, '_failAllDeferred' )
);
},
// Filters the featureArrays to return the list of features for the query, and then calls finish() to pass to the callback
_getFeatures: function( query, featCallback, doneCallback, errorCallback ) {
var thisB = this;
if(this.stores.length == 1) {
this.stores[0].getFeatures( query, featCallback, doneCallback, errorCallback);
return;
}
if(this.regionLoaded) {
var spans = array.filter(this.regionLoaded.spans, function(span) {
return span.start <= query.end && span.end >= query.start;
});
var features = this.createFeatures(spans);
this.finish(features, spans, featCallback, doneCallback);
return;
}
// featureArrays will be a map from the names of the stores to an array of each store's features
var featureArrays = {};
// Generate map
var fetchAllFeatures = thisB.stores.map(
function (store) {
var d = new Deferred();
if ( !featureArrays[store.name] ) {
featureArrays[store.name] = [];
store.getFeatures(
query,
dojo.hitch( this, function( feature ) {
var feat = new featureWrapper( feature, store.name );
featureArrays[store.name].push( feat );
}),
function(){d.resolve( featureArrays[store.name] ); },
function(){d.reject("Error fetching features for store " + store.name);}
);
} else {
d.resolve(featureArrays[store.name], true);
}
d.then(function(){}, errorCallback); // Makes sure that none of the rejected deferred promises keep propagating
return d.promise;
}
);
// Once we have all features, combine them according to the operation tree and create new features based on them.
when( all( fetchAllFeatures ), function() {
// Create a set of spans based on the evaluation of the operation tree
var spans = thisB.evalTree(featureArrays, thisB.opTree, query);
var features = thisB.createFeatures(spans);
thisB.finish(features, spans, featCallback, doneCallback);
}, errorCallback);
},
// Evaluate (recursively) an operation tree to create a list of spans (essentially pseudo-features)
evalTree: function(featureArrays, tree, query) {
if(!tree) {
return false;
} else if(tree.isLeaf()) {
return this.toSpan(featureArrays[tree.get().name], query);
} else if(!tree.hasLeft()) {
return this.toSpan(featureArrays[tree.right().get().name], query);
} else if(!tree.hasRight()) {
return this.toSpan(featureArrays[tree.left().get().name], query);
}
return this.opSpan(
tree.get(),
this.evalTree(featureArrays, tree.left(), query),
this.evalTree(featureArrays, tree.right(), query),
query
);
},
// Passes the list of combined features to the getFeatures() callbacks
finish: function( features, spans, featCallback, doneCallback ) {
/* Pass features to the track's original featCallback, and pass spans to the doneCallback.
*/
for ( var key in features ) {
if ( features.hasOwnProperty(key) ) {
featCallback( features[key] );
}
}
doneCallback( { spans: spans} );
},
// These last four functions are stubbed out because each derived class should have its own implementation of them.
// Converts a list of spans into a list of features.
createFeatures: function(spans) {},
// Transforms a set of features into a set of spans
toSpan: function(features, query) {},
// Defines the various operations that may occur and assigns each to a span-making function.
opSpan: function(op, span1, span2, query) {}
});
});