UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

324 lines (280 loc) 9.5 kB
define( [ 'dojo/_base/declare', 'dojo/_base/lang', 'dojo/_base/array', 'dojo/Deferred', 'dojo/promise/all', 'JBrowse/Util', 'JBrowse/ConfigAdaptor/AdaptorUtil' ], function( declare, lang, array, Deferred, all, Util, AdaptorUtil ) { return declare(null, /** * @lends JBrowse.ConfigManager.prototype */ { /** * @constructs */ constructor: function( args ) { this.bootConfig = lang.clone( args.bootConfig || {} ); this.defaults = lang.clone( args.defaults || {} ); this.browser = args.browser; this.skipValidation = args.skipValidation; this.bootConfig = (this._regularizeIncludes ([ this.bootConfig ])) [0]; if(this.bootConfig.cacheBuster === false) { this.bootConfig.cacheBuster = false } else { this.bootConfig.cacheBuster = true } var thisB = this; this._getConfigAdaptor( this.bootConfig ) .then( function( adaptor ) { thisB.bootConfig = adaptor.regularizeTrackConfigs ( thisB.bootConfig ); }); // this.topLevelIncludes = this._fillTemplates( // lang.clone( this.config.include || this.defaults.include ), // this._applyDefaults( lang.clone( this.config ), this.defaults ) // ); // delete this.defaults.include; // delete this.config.include; }, /** * @param callback {Function} callback, receives a single arguments, * which is the final processed configuration object */ getFinalConfig: function() { return this.finalConfig || ( this.finalConfig = function() { var thisB = this; var bootstrapConf = this._applyDefaults( lang.clone( this.bootConfig ), this.defaults ); return this._loadIncludes( bootstrapConf ) .then( function( includedConfig ) { // merge the boot config *into* the included config last, so // that values in the boot config override the others var finalConf = thisB._mergeConfigs( includedConfig, thisB.bootConfig ); thisB._fillTemplates( finalConf, finalConf ); finalConf = AdaptorUtil.evalHooks( finalConf ); if( ! thisB.skipValidation ) thisB._validateConfig( finalConf ); return finalConf; }); }.call(this) ); }, /** * Instantiate the right config adaptor for a given configuration source. * @param {Object} config the configuraiton * @param {Function} callback called with the new config object * @returns {Object} the right configuration adaptor to use, or * undefined if one could not be found * @private */ _getConfigAdaptor: function( config_def, callback ) { var adaptor_name = "JBrowse/ConfigAdaptor/" + config_def.format; if( 'version' in config_def ) adaptor_name += '_v'+config_def.version; adaptor_name.replace( /\W/g,'' ); return Util.loadJS( [adaptor_name] ) .then( function( modules ) { return new (modules[0])( config_def ); }); }, _fillTemplates: function( subconfig, config ) { // skip "menuTemplate" keys to prevent messing // up their feature-based {} interpolation //var skip = { menuTemplate: true }; var skip = {}; var type = typeof subconfig; if( lang.isArray( subconfig ) ) { for( var i = 0; i<subconfig.length; i++ ) subconfig[i] = this._fillTemplates( subconfig[i], config ); } else if( type == 'object' ) { for( var name in subconfig ) { if( subconfig.hasOwnProperty( name ) && !skip[name] ) subconfig[name] = this._fillTemplates( subconfig[name], config ); } } else if( type == 'string' ) { return Util.fillTemplate( subconfig, config ); } return subconfig; }, /** * Recursively fetch, parse, and merge all the includes in the given * config object. Calls the callback with the resulting configuration * when finished. * @private */ _loadIncludes: function( inputConfig ) { var thisB = this; inputConfig = lang.clone( inputConfig ); function _loadRecur( config, upstreamConf ) { var sourceUrl = config.sourceUrl || config.baseUrl; var newUpstreamConf = thisB._mergeConfigs( lang.clone( upstreamConf ), config ); var includes = thisB._fillTemplates( thisB._regularizeIncludes( config.include || [] ), newUpstreamConf ); delete config.include; var loads = includes.map(include => { include.cacheBuster = inputConfig.cacheBuster return thisB._loadInclude( include, sourceUrl ) .then(includedData => _loadRecur(includedData, newUpstreamConf)) }); return all( loads ) .then( function( includedDataObjects ) { array.forEach( includedDataObjects, function( includedData ) { config = thisB._mergeConfigs( config, includedData ); }); return config; }); } return _loadRecur( inputConfig, {} ); }, _loadInclude: function( include, baseUrl ) { var thisB = this; // instantiate the adaptor and load the config return this._getConfigAdaptor( include ) .then( function( adaptor ) { if( !adaptor ) throw new Error( "Could not load config "+include.url+", " + "no configuration adaptor found for config format " +include.format+' version '+include.version ); return adaptor.load( { config: include, baseUrl: baseUrl }); } ) .then( null, function(error) { try { if( error.response.status == 404 ) return {}; } catch(e) {} throw error; }); }, _regularizeIncludes: function( includes ) { if( ! includes ) return []; // coerce include to an array if( typeof includes != 'object' ) includes = [ includes ]; // include array might have undefined elements in it if // somebody left a trailing comma in and we are running under // IE includes = array.filter( includes, function(r) { return r; } ); return array.map( includes, function( include ) { // coerce bare strings in the includes to URLs if( typeof include == 'string' ) include = { url: include }; // set defaults for format and version if( ! ('format' in include) ) { include.format = /\.conf$/.test( include.url ) ? 'conf' : 'JB_json'; } if( include.format == 'JB_json' && ! ('version' in include) ) { include.version = 1; } return include; }); }, /** * @private */ _applyDefaults: function( config, defaults ) { return Util.deepUpdate( dojo.clone(defaults), config ); }, /** * Examine the loaded and merged configuration for errors. Throws * exceptions if it finds anything amiss. * @private * @returns nothing meaningful */ _validateConfig: function( c ) { if( ! c.tracks ) c.tracks = []; if( ! c.baseUrl ) { this._fatalError( 'Must provide a `baseUrl` in configuration' ); } if( this.hasFatalErrors ) throw "Errors in configuration, cannot start."; }, /** * @private */ _fatalError: function( error ) { this.hasFatalErrors = true; // if( error.url ) // error = error + ' when loading '+error.url; this.browser.fatalError( error ); }, // list of config properties that should not be recursively merged _noRecursiveMerge: function( propName ) { return propName == 'datasets'; }, /** * Merges config object b into a. a <- b * @private */ _mergeConfigs: function( a, b ) { if( b === null ) return null; if( a === null ) a = {}; for (var prop in b) { if( prop == 'tracks' && (prop in a) ) { a[prop] = this._mergeTrackConfigs( a[prop] || [], b[prop] || [] ); } else if ( ! this._noRecursiveMerge( prop ) &&(prop in a) && ("object" == typeof b[prop]) && ("object" == typeof a[prop]) ) { a[prop] = Util.deepUpdate( a[prop], b[prop] ); } else if(prop == 'dataRoot') { if(a[prop] === undefined || a[prop] == 'data' && b[prop] !== undefined ){ a[prop] = b[prop]; } } else if( a[prop] === undefined || b[prop] !== undefined ){ a[prop] = b[prop]; } } return a; }, /** * Special-case merging of two <code>tracks</code> configuration * arrays. * @private */ _mergeTrackConfigs: function( a, b ) { if( ! b.length ) return a; // index the tracks in `a` by track label var aTracks = {}; array.forEach( a, function(t,i) { t.index = i; aTracks[t.label] = t; }); array.forEach( b, function(bT) { var aT = aTracks[bT.label]; if( aT ) { this._mergeConfigs( aT, bT ); } else { a.push( bT ); } },this); return a; } }); });