@gmod/jbrowse
Version:
JBrowse - client-side genome browser
289 lines (255 loc) • 11.3 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/Deferred',
'dojo/request/xhr',
'JBrowse/Store/SeqFeature',
'JBrowse/Store/DeferredFeaturesMixin',
'JBrowse/Store/DeferredStatsMixin',
'JBrowse/Util',
'JBrowse/Model/ArrayRepr',
'JBrowse/Store/NCList',
'JBrowse/Store/LazyArray'
],
function(
declare,
lang,
Deferred,
xhr,
SeqFeatureStore,
DeferredFeaturesMixin,
DeferredStatsMixin,
Util,
ArrayRepr,
GenericNCList,
LazyArray
) {
/**
* Implementation of SeqFeatureStore using nested containment
* lists held in static files that are lazily fetched from the web
* server.
*
* @class JBrowse.Store.SeqFeature.NCList
* @extends SeqFeatureStore
*/
var idfunc = function() { return this._uniqueID; };
var parentfunc = function() { return this._parent; };
var childrenfunc = function() { return this.get('subfeatures'); };
return declare( SeqFeatureStore,
{
constructor: function(args) {
this.args = args;
this.baseUrl = args.baseUrl;
this.urlTemplates = { root: args.urlTemplate };
this._deferred = {};
},
makeNCList: function() {
return new GenericNCList();
},
loadNCList: function( refData, trackInfo, url ) {
refData.nclist.importExisting( trackInfo.intervals.nclist,
refData.attrs,
url,
trackInfo.intervals.urlTemplate,
trackInfo.intervals.lazyClass
);
},
getDataRoot: function( refName ) {
if( ! this._deferred.root || this.curRefName != refName ) {
var d = this._deferred.root = new Deferred();
this.curRefName = refName;
var refData = {
nclist: this.makeNCList()
};
var url = this.resolveUrl(
this._evalConf(this.urlTemplates.root),
{ refseq: refName }
);
// fetch the trackdata
var thisB = this;
xhr.get( url, {
handleAs: 'json',
failOk: true,
headers: {
'X-Requested-With': null
}
})
.then( function( trackInfo, request ) {
//trackInfo = JSON.parse( trackInfo );
thisB._handleTrackInfo( refData, trackInfo, url );
},
function(error) {
if( Util.isElectron() || error.response.status == 404 ) {
thisB._handleTrackInfo( refData, {}, url );
} else if( error.response.status != 200) {
thisB._failAllDeferred( "Server returned an HTTP " + error.response.status + " error" );
}
else
thisB._failAllDeferred( error );
}
);
}
return this._deferred.root;
},
_handleTrackInfo: function( refData, trackInfo, url ) {
refData.stats = {
featureCount: trackInfo.featureCount || 0,
featureDensity: ( trackInfo.featureCount || 0 ) / this.refSeq.length
};
this.empty = !trackInfo.featureCount;
if( trackInfo.intervals ) {
refData.attrs = new ArrayRepr( trackInfo.intervals.classes );
this.loadNCList( refData, trackInfo, url );
}
var histograms = trackInfo.histograms;
if( histograms && histograms.meta ) {
for (var i = 0; i < histograms.meta.length; i++) {
histograms.meta[i].lazyArray =
new LazyArray( histograms.meta[i].arrayParams, url );
}
refData._histograms = histograms;
}
this._deferred.root.resolve( refData );
},
getGlobalStats: function( successCallback, errorCallback ) {
return ( this._deferred.root || this.getDataRoot( this.browser.refSeq.name ) )
.then( function( data ) { successCallback( data.stats ); },
errorCallback
);
},
getRegionStats: function( query, successCallback, errorCallback ) {
this.getDataRoot( query.ref )
.then( function( data ) { successCallback( data.stats ); },
errorCallback
);
},
getRegionFeatureDensities: function( query, successCallback, errorCallback ) {
this.getDataRoot( query.ref )
.then( function( data ) {
var numBins, basesPerBin;
if( query.numBins ) {
numBins = query.numBins;
basesPerBin = (query.end - query.start)/numBins;
}
else if( query.basesPerBin ) {
basesPerBin = query.basesPerBin;
numBins = Math.ceil( (query.end-query.start)/basesPerBin );
}
else {
throw new Error('numBins or basesPerBin arg required for getRegionFeatureDensities');
}
// pick the relevant entry in our pre-calculated stats
var statEntry = (function( basesPerBin, stats ) {
for (var i = 0; i < stats.length; i++) {
if( stats[i].basesPerBin >= basesPerBin ) {
return stats[i];
}
}
return undefined;
})( basesPerBin, data._histograms.stats || [] );
// The histogramMeta array describes multiple levels of histogram detail,
// going from the finest (smallest number of bases per bin) to the
// coarsest (largest number of bases per bin).
// We want to use coarsest histogramMeta that's at least as fine as the
// one we're currently rendering.
// TODO: take into account that the histogramMeta chosen here might not
// fit neatly into the current histogram (e.g., if the current histogram
// is at 50,000 bases/bin, and we have server histograms at 20,000
// and 2,000 bases/bin, then we should choose the 2,000 histogramMeta
// rather than the 20,000)
var histogramMeta = data._histograms.meta[0];
for (var i = 0; i < data._histograms.meta.length; i++) {
if( basesPerBin >= data._histograms.meta[i].basesPerBin )
histogramMeta = data._histograms.meta[i];
}
// number of bins in the server-supplied histogram for each current bin
var binRatio = basesPerBin / histogramMeta.basesPerBin;
// if the server-supplied histogram fits neatly into our requested
if ( binRatio > 0.9
&&
Math.abs(binRatio - Math.round(binRatio)) < 0.0001
) {
//console.log('server-supplied',query);
// we can use the server-supplied counts
var firstServerBin = Math.floor( query.start / histogramMeta.basesPerBin);
binRatio = Math.round(binRatio);
var histogram = [];
for (var bin = 0; bin < numBins; bin++)
histogram[bin] = 0;
histogramMeta.lazyArray.range(
firstServerBin,
firstServerBin + binRatio*numBins,
function(i, val) {
// this will count features that span the boundaries of
// the original histogram multiple times, so it's not
// perfectly quantitative. Hopefully it's still useful, though.
histogram[ Math.floor( (i - firstServerBin) / binRatio ) ] += val;
},
function() {
successCallback({ bins: histogram, stats: statEntry });
}
);
} else {
//console.log('make own',query);
// make our own counts
data.nclist.histogram.call(
data.nclist,
query.start,
query.end,
numBins,
function( hist ) {
successCallback({ bins: hist, stats: statEntry });
});
}
}, errorCallback );
},
getFeatures: function( query, origFeatCallback, finishCallback, errorCallback ) {
if( this.empty ) {
finishCallback();
return;
}
var thisB = this;
this.getDataRoot( query.ref )
.then( function( data ) {
thisB._getFeatures( data, query, origFeatCallback, finishCallback, errorCallback );
}, errorCallback);
},
_getFeatures: function( data, query, origFeatCallback, finishCallback, errorCallback ) {
var thisB = this;
var startBase = query.start;
var endBase = query.end;
var accessors = data.attrs.accessors(),
/** @inner */
featCallBack = function( feature, path ) {
// the unique ID is a stringification of the path in the
// NCList where the feature lives; it's unique across the
// top-level NCList (the top-level NCList covers a
// track/chromosome combination)
// only need to decorate a feature once
if (! feature.decorated) {
var uniqueID = path.join(",");
thisB._decorate_feature( accessors, feature, uniqueID );
}
return origFeatCallback( feature );
};
data.nclist.iterate.call( data.nclist, startBase, endBase, featCallBack, finishCallback, errorCallback );
},
// helper method to recursively add .get and .tags methods to a feature and its
// subfeatures
_decorate_feature: function( accessors, feature, id, parent ) {
feature.get = accessors.get;
// possibly include set method in decorations? not currently
// feature.set = accessors.set;
feature.tags = accessors.tags;
feature._uniqueID = id;
feature.id = idfunc;
feature._parent = parent;
feature.parent = parentfunc;
feature.children = childrenfunc;
dojo.forEach( feature.get('subfeatures'), function(f,i) {
this._decorate_feature( accessors, f, id+'-'+i, feature );
},this);
feature.decorated = true;
}
});
});