@gmod/jbrowse
Version:
JBrowse - client-side genome browser
344 lines (304 loc) • 12.9 kB
JavaScript
/**
* Store that gets data from any set of web services that implement
* the JBrowse REST API.
*/
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/io-query',
'dojo/request',
'dojo/Deferred',
'JBrowse/Store/LRUCache',
'JBrowse/Store/SeqFeature',
'JBrowse/Store/DeferredFeaturesMixin',
'JBrowse/Store/DeferredStatsMixin',
'JBrowse/Util',
'JBrowse/Model/SimpleFeature'
],
function(
declare,
lang,
array,
ioquery,
dojoRequest,
Deferred,
LRUCache,
SeqFeatureStore,
DeferredFeaturesMixin,
DeferredStatsMixin,
Util,
SimpleFeature
) {
return declare( SeqFeatureStore,
{
constructor: function( args ) {
this.region_cache_hits = 0; //< stats mostly for unit tests
// make sure the baseUrl has a trailing slash
this.baseUrl = args.baseUrl || this.config.baseUrl;
if( this.baseUrl.charAt( this.baseUrl.length-1 ) != '/' )
this.baseUrl = this.baseUrl + '/';
// enable feature density bin fetching if turned on
if( this.config.region_feature_densities && ! this.getRegionFeatureDensities ) {
this.getRegionFeatureDensities = this._getRegionFeatureDensities;
}
},
_defaultConfig: function() {
return {
noCache: false
};
},
getGlobalStats: function( callback, errorCallback ) {
var url = this._makeURL( 'stats/global' );
this._get({ url: url, type: 'globalStats' }, callback, errorCallback );
},
getRegionStats: function( query, successCallback, errorCallback ) {
if( ! this.config.region_stats ) {
this._getRegionStats.apply( this, arguments );
return;
}
query = this._assembleQuery( query );
var url = this._makeURL( 'stats/region', query );
this._get( { url: url, query: query, type: 'regionStats' }, successCallback, errorCallback );
},
getFeatures: function( query, featureCallback, endCallback, errorCallback ) {
var thisB = this;
query = this._assembleQuery( query );
var url = this._makeURL( 'features', query );
// look for cached feature regions if configured to do so
var cachedFeatureRegions;
if( this.config.feature_range_cache
&& ! this.config.noCache
&& ( cachedFeatureRegions = this._getCachedFeatureRegions( query ) )
) {
this.region_cache_hits++;
this._makeFeaturesFromCachedRegions( cachedFeatureRegions, query, featureCallback, endCallback, errorCallback );
}
// otherwise just fetch and cache like all the other requests
else {
this._get( { url: url, query: query, type: 'features' },
dojo.hitch( this, '_makeFeatures',
featureCallback, endCallback, errorCallback
),
errorCallback
);
}
},
// look in the REST backend's cache for cached feature requests
// that are relevant to the given query params (overlap the
// start/end region, and match other params). return an array
// like [ {features: [...], start: 123, end: 456 }, ... ]
_getCachedFeatureRegions: function( query ) {
var cache = this._getCache();
function tilingIsComplete( regions, start, end ) {
regions.sort( function(a,b) { return a.start - b.start; });
var coverStart = regions[0].start,
coverEnd;
var i;
var tilingComplete;
for( i = 0; !tilingComplete && i < regions.length; i++ ) {
if( coverEnd === undefined || regions[i].start <= coverEnd && regions[i].end > coverEnd ) {
coverEnd = regions[i].end;
tilingComplete = coverStart <= start && coverEnd >= end;
}
}
if( tilingComplete ) {
// touch all of the regions we processed in the cache,
// cause we are going to use them
for( i--; i >= 0; i-- )
cache.touchRecord( regions[i].cacheRecord );
return true;
}
return false;
}
function queriesMatch( q1, q2 ) {
var keys = Util.dojof.keys( q1 ).concat( Util.dojof.keys( q2 ) );
for( var k in q1 ) {
if( k == 'start' || k == 'end' ) continue;
if( q1[k] != q2[k] )
return false;
}
for( var k in q2 ) {
if( k == 'start' || k == 'end' ) continue;
if( q1[k] != q2[k] )
return false;
}
return true;
}
var relevantRegions = [];
if( cache.some(
function( cacheRecord ) {
var cachedRequest = cacheRecord.value.request;
var cachedResponse = cacheRecord.value.response;
if( cachedRequest.type != 'features' || ! cachedResponse )
return false;
if( ! queriesMatch( cachedRequest.query, query ) )
return false;
if( ! ( cachedRequest.query.end < query.start || cachedRequest.query.start > query.end ) ) {
relevantRegions.push(
{ features: cachedResponse.features,
start: cachedRequest.query.start,
end: cachedRequest.query.end,
cacheRecord: cacheRecord
});
if( tilingIsComplete( relevantRegions, query.start, query.end ) )
return true;
}
return false;
},
this )
) {
return relevantRegions;
}
return null;
},
// given an array of records of cached feature data like that
// returned by _getCachedFeatureRegions, make feature objects from
// them and emit them via the callbacks
_makeFeaturesFromCachedRegions: function( cachedFeatureRegions, query, featureCallback, endCallback, errorCallback ) {
// gather and uniqify all the relevant feature data objects from the cached regions
var seen = {};
var featureData = [];
array.forEach( cachedFeatureRegions, function( region ) {
if( region && region.features ) {
array.forEach( region.features, function( feature ) {
if( ! seen[ feature.uniqueID ] ) {
seen[feature.uniqueID] = true;
if( !( feature.start > query.end || feature.end < query.start ) )
featureData.push( feature );
}
});
}
});
// iterate over them and make feature objects from them
this._makeFeatures( featureCallback, endCallback, errorCallback, { features: featureData } );
},
// this method is copied to getRegionFeatureDensities in the
// constructor if config.region_feature_densities is true
_getRegionFeatureDensities: function( query, histDataCallback, errorCallback ) {
var url = this._makeURL( 'stats/regionFeatureDensities', this._assembleQuery( query ) );
this._get( { url: url}, histDataCallback, errorCallback );
// query like:
// { ref: 'ctgA, start: 123, end: 456, basesPerBin: 200 }
// callback like:
// histDataCallback({
// "bins": [ 51,50,58,63,57,57,65,66,63,61,56,49,50,47,39,38,54,41,50,71,61,44,64,60,42 ],
// "stats": { "basesPerBin":"200","max":88,"mean":57.772 } //< `max` used to set the Y scale
// });
// or error like:
// errorCallback( 'aieeee i died' );
},
// STUB method to satisfy requirements when setting the REST track to a VCF type
getVCFHeader: function( query, filterFunctionCallback, errorCallback ) {
return new Deferred(function() { /* console.log("REST store getVCFHeader"); */ });
},
clearCache: function() {
delete this._cache;
},
// HELPER METHODS
_get: function( request, callback, errorCallback ) {
var thisB = this;
if( this.config.noCache )
dojoRequest( request.url, {
method: 'GET',
handleAs: 'json'
}).then(
callback,
this._errorHandler( errorCallback )
);
else
this._getCache().get( request, function( record, error ) {
if( error )
thisB._errorHandler(errorCallback)(error);
else
callback( record.response );
});
},
_getCache: function() {
var thisB = this;
return this._cache || (
this._cache = new LRUCache(
{
name: 'REST data cache '+this.name,
maxSize: 25000, // cache up to about 5MB of data (assuming about 200B per feature)
sizeFunction: function( data ) { return data.length || 1; },
fillCallback: function( request, callback ) {
var get = dojoRequest( request.url, { method: 'GET', handleAs: 'json' },
true // work around dojo/request bug
);
get.then(
function(data) {
var nocacheResponse = /no-cache/.test(get.response.getHeader('Cache-Control'))
|| /no-cache/.test(get.response.getHeader('Pragma'));
callback({ response: data, request: request }, null, {nocache: nocacheResponse});
},
thisB._errorHandler( lang.partial( callback, null ) )
);
}
}));
},
_errorHandler: function( handler ) {
handler = handler || function(e) {
console.error( e, e.stack );
throw e;
};
return dojo.hitch( this, function( error ) {
var httpStatus = ((error||{}).response||{}).status;
if( httpStatus >= 400 ) {
handler( "HTTP " + httpStatus + " fetching "+error.response.url+" : "+error.response.text );
}
else {
handler( error );
}
});
},
_assembleQuery: function( query ) {
return lang.mixin(
{ ref: (this.refSeq||{}).name },
this.config.query || {},
query || {}
);
},
_makeURL: function( subpath, query ) {
var url = this.baseUrl + subpath;
if( query ) {
if( query.ref ) {
url += '/' + query.ref;
query = lang.mixin({}, query );
delete query.ref;
}
query = ioquery.objectToQuery( query );
if( query )
url += '?' + query;
}
return url;
},
_makeFeatures: function( featureCallback, endCallback, errorCallback, featureData ) {
let features
if( featureData && ( features = featureData.features ) ) {
for( let i = 0; i < features.length; i++ ) {
let f = this._makeFeature(features[i])
this.applyFeatureTransforms([f])
.forEach(featureCallback)
}
}
endCallback();
},
supportsFeatureTransforms: true,
_parseInt: function( data ) {
array.forEach(['start','end','strand'], function( field ) {
if( field in data )
data[field] = parseInt( data[field] );
});
if( 'score' in data )
data.score = parseFloat( data.score );
if( 'subfeatures' in data )
for( var i=0; i<data.subfeatures.length; i++ )
this._parseInt( data.subfeatures[i] );
},
_makeFeature: function( data, parent ) {
this._parseInt( data );
return new SimpleFeature( { data: data, parent: parent } );
}
});
});