UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

344 lines (304 loc) 12.9 kB
/** * 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 } ); } }); });