UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

1,230 lines (1,086 loc) 133 kB
const url = cjsRequire('url') import packagejson from './package.json' define( [ 'dojo/_base/declare', 'dojo/_base/lang', 'dojo/on', 'dojo/html', 'dojo/query', 'dojo/dom-construct', 'dojo/keys', 'dojo/Deferred', 'dojo/DeferredList', 'dojo/topic', 'dojo/aspect', 'dojo/request', 'dojo/io-query', 'JBrowse/has', 'dojo/_base/array', 'dijit/layout/ContentPane', 'dijit/layout/BorderContainer', 'dijit/Dialog', 'dijit/form/ComboBox', 'dojo/store/Memory', 'dijit/form/Button', 'dijit/form/Select', 'dijit/form/ToggleButton', 'dijit/form/DropDownButton', 'dijit/DropDownMenu', 'dijit/CheckedMenuItem', 'dijit/MenuItem', 'dijit/MenuSeparator', 'dojox/form/TriStateCheckBox', 'dojox/html/entities', 'JBrowse/Util', 'JBrowse/Store/LazyTrie', 'JBrowse/Store/Names/LazyTrieDojoData', 'dojo/store/DataStore', 'JBrowse/FeatureFiltererMixin', 'JBrowse/GenomeView', 'JBrowse/TouchScreenSupport', 'JBrowse/ConfigManager', 'JBrowse/View/InfoDialog', 'JBrowse/View/FileDialog', 'JBrowse/View/FastaFileDialog', 'JBrowse/Model/Location', 'JBrowse/View/LocationChoiceDialog', 'JBrowse/View/Dialog/SetHighlight', 'JBrowse/View/Dialog/Preferences', 'JBrowse/View/Dialog/OpenDirectory', 'JBrowse/View/Dialog/SetTrackHeight', 'JBrowse/View/Dialog/QuickHelp', 'JBrowse/View/StandaloneDatasetList', 'JBrowse/Store/SeqFeature/ChromSizes', 'JBrowse/Store/SeqFeature/UnindexedFasta', 'JBrowse/Store/SeqFeature/IndexedFasta', 'JBrowse/Store/SeqFeature/BgzipIndexedFasta', 'JBrowse/Store/SeqFeature/TwoBit', 'dijit/focus', '../lazyload.js', // for dynamic CSS loading // extras for webpack 'dojox/data/CsvStore', 'dojox/data/JsonRestStore' ], function( declare, lang, on, html, query, domConstruct, keys, Deferred, DeferredList, topic, aspect, request, ioQuery, has, array, dijitContentPane, dijitBorderContainer, dijitDialog, dijitComboBox, dojoMemoryStore, dijitButton, dijitSelectBox, dijitToggleButton, dijitDropDownButton, dijitDropDownMenu, dijitCheckedMenuItem, dijitMenuItem, dijitMenuSeparator, dojoxTriStateCheckBox, dojoxHtmlEntities, Util, LazyTrie, NamesLazyTrieDojoDataStore, DojoDataStore, FeatureFiltererMixin, GenomeView, Touch, ConfigManager, InfoDialog, FileDialog, FastaFileDialog, Location, LocationChoiceDialog, SetHighlightDialog, PreferencesDialog, OpenDirectoryDialog, SetTrackHeightDialog, HelpDialog, StandaloneDatasetList, ChromSizes, UnindexedFasta, IndexedFasta, BgzipIndexedFasta, TwoBit, dijitFocus, LazyLoad ) { var dojof = Util.dojof; require.on('error', function(error) { let errString = error.info && error.info[0] && error.info[0].mid ? error.info.map(({mid})=>mid).join(', ') : error; window.JBrowse.fatalError('Failed to load resource: '+errString); }); /** * Construct a new Browser object. * @class This class is the main interface between JBrowse and embedders * @constructor * @param params an object with the following properties:<br> * <ul> * <li><code>config</code> - list of objects with "url" property that points to a config JSON file</li> * <li><code>containerID</code> - ID of the HTML element that contains the browser</li> * <li><code>refSeqs</code> - object with "url" property that is the URL to list of reference sequence information items</li> * <li><code>browserRoot</code> - (optional) URL prefix for the browser code</li> * <li><code>tracks</code> - (optional) comma-delimited string containing initial list of tracks to view</li> * <li><code>location</code> - (optional) string describing the initial location</li> * <li><code>defaultTracks</code> - (optional) comma-delimited string containing initial list of tracks to view if there are no cookies and no "tracks" parameter</li> * <li><code>defaultLocation</code> - (optional) string describing the initial location if there are no cookies and no "location" parameter</li> * <li><code>show_nav</code> - (optional) string describing the on/off state of navigation box</li> * <li><code>show_tracklist</code> - (optional) string describing the on/off state of track bar</li> * <li><code>show_overview</code> - (optional) string describing the on/off state of overview</li> * </ul> */ return declare( FeatureFiltererMixin, { constructor: function(params) { this.globalKeyboardShortcuts = {}; this.config = params || {}; // if we're in the unit tests, stop here and don't do any more initialization if( this.config.unitTestMode ) return; // hook for externally applied initialization that can be setup in index.html if (typeof this.config.initExtra === 'function') this.config.initExtra(this,params); this.startTime = new Date(); // start the initialization process var thisB = this; dojo.addOnLoad( function() { if(Util.isElectron() && !thisB.config.dataRoot) { dojo.addClass(document.body, "jbrowse"); dojo.addClass(document.body, thisB.config.theme || "tundra"); thisB.welcomeScreen(document.body); return; } thisB.loadConfig().then( function() { thisB.container = dojo.byId( thisB.config.containerID ); thisB.container.onselectstart = function() { return false; }; // initialize our highlight if one was set in the config if( thisB.config.initialHighlight && thisB.config.initialHighlight != "/" ) thisB.setHighlight( new Location( thisB.config.initialHighlight ) ); thisB.initPlugins().then( function() { thisB.loadNames(); thisB.loadUserCSS().then( function() { thisB.initTrackMetadata(); thisB.loadRefSeqs().then( function() { // figure out our initial location var initialLocString = thisB._initialLocation(); var initialLoc = Util.parseLocString( initialLocString ); if (initialLoc && initialLoc.ref && thisB.allRefs[initialLoc.ref]) { thisB.refSeq = thisB.allRefs[initialLoc.ref]; } // before we init the view, make sure that our container has nonzero height and width thisB.ensureNonzeroContainerDimensions() thisB.initView().then( function() { Touch.loadTouch(); // init touch device support if( initialLocString ) { thisB.navigateTo( initialLocString, true ); } // figure out what initial track list we will use: var tracksToShow = []; // always add alwaysOnTracks, regardless of any other track params if (thisB.config.alwaysOnTracks) { tracksToShow = tracksToShow.concat(thisB.config.alwaysOnTracks.split(",")); } // add tracks specified in URL track param, // if no URL track param then add last viewed tracks via tracks cookie // if no URL param and no tracks cookie, then use defaultTracks if (thisB.config.forceTracks) { tracksToShow = tracksToShow.concat(thisB.config.forceTracks.split(",")); } else if (thisB.cookie("tracks")) { tracksToShow = tracksToShow.concat(thisB.cookie("tracks").split(",")); } else if (thisB.config.defaultTracks) { // In rare cases thisB.config.defaultTracks already contained an array that appeared to // have been split in a previous invocation of this function. Thus, we only try and split // it if it isn't already split. if (!(thisB.config.defaultTracks instanceof Array)) { tracksToShow = tracksToShow.concat(thisB.config.defaultTracks.split(",")); } } // currently, force "DNA" _only_ if no other guides as to what to show? // or should this be changed to always force DNA to show? if (tracksToShow.length == 0) { tracksToShow.push("DNA"); } // eliminate track duplicates (may have specified in both alwaysOnTracks and defaultTracks) tracksToShow = Util.uniq(tracksToShow); thisB.showTracks( tracksToShow ); thisB.passMilestone( 'completely initialized', { success: true } ); }); thisB.reportUsageStats(); }); }); }); }); }); }, _initialLocation: function() { var oldLocMap = dojo.fromJson( this.cookie('location') ) || {}; if( this.config.location ) { return this.config.location; } else if( oldLocMap[this.refSeq.name] ) { return oldLocMap[this.refSeq.name].l || oldLocMap[this.refSeq.name]; } else if( this.config.defaultLocation ){ return this.config.defaultLocation; } else { return Util.assembleLocString({ ref: this.refSeq.name, start: 0.4 * ( this.refSeq.start + this.refSeq.end ), end: 0.6 * ( this.refSeq.start + this.refSeq.end ) }); } }, version: function() { // when a build is put together, the build system assigns a string // to the variable below. return packagejson.version; }.call(), /** * Get a plugin, if it is present. Note that, if plugin * initialization is not yet complete, it may be a while before the * callback is called. * * Callback is called with one parameter, the desired plugin object, * or undefined if it does not exist. */ getPlugin: function( name, callback ) { this.afterMilestone( 'initPlugins', dojo.hitch( this, function() { callback( this.plugins[name] ); })); }, _corePlugins: function() { return [ 'RegexSequenceSearch' ]; }, /** * Load and instantiate any plugins defined in the configuration. */ initPlugins: function() { return this._milestoneFunction( 'initPlugins', function( deferred ) { this.plugins = {}; var plugins = this.config.plugins || this.config.Plugins || {}; // coerce plugins to array of objects if( ! lang.isArray(plugins) && ! plugins.name ) { // plugins like { Foo: {...}, Bar: {...} } plugins = function() { var newplugins = []; for( var pname in plugins ) { if( lang.isObject(plugins[pname]) && !( 'name' in plugins[pname] ) ) { plugins[pname].name = pname; } newplugins.push( plugins[pname] ); } return newplugins; }.call(this); } if( ! lang.isArray( plugins ) ) plugins = [ plugins ]; plugins.unshift.apply( plugins, this._corePlugins() ); // coerce string plugin names to {name: 'Name'} plugins = array.map( plugins, function( p ) { return typeof p == 'object' ? p : { 'name': p }; }); if( ! plugins.length ) { deferred.resolve({success: true}); return; } // set default locations for each plugin plugins.forEach( p => { // find the entry in the dojoConfig for this plugin let configEntry = dojoConfig.packages.find(c => c.name === p.name) if( configEntry ) { p.css = configEntry.css ? configEntry.pluginDir+'/'+configEntry.css : false p.js = configEntry.location } else { this.fatalError(`plugin ${p.name} not found. You can rebuild JBrowse with a -dev release or github clone with this plugin in the plugin folder`) } }); var pluginDeferreds = array.map( plugins, function(p) { return new Deferred(); }); // fire the "all plugins done" deferred when all of the plugins are done loading (new DeferredList( pluginDeferreds )) .then( function() { deferred.resolve({success: true}); }); dojo.global.require( array.map( plugins, function(p) { return p.name+'/main' } ), dojo.hitch( this, function() { array.forEach( arguments, function( pluginClass, i ) { var plugin = plugins[i]; var thisPluginDone = pluginDeferreds[i]; if( typeof pluginClass == 'string' ) { console.error("could not load plugin "+plugin.name+": "+pluginClass); } else { // make the plugin's arguments out of // its little obj in 'plugins', and // also anything in the top-level // conf under its plugin name var args = dojo.mixin( dojo.clone( plugins[i] ), { config: this.config[ plugin.name ]||{} }); args.browser = this; args = dojo.mixin( args, { browser: this } ); // load its css var cssLoaded; if (plugin.css) { cssLoaded = this._loadCSS({ url: this.resolveUrl(plugin.css + '/main.css') }) } else { cssLoaded = new Deferred() cssLoaded.resolve() } cssLoaded.then( function() { thisPluginDone.resolve({success:true}); }); // give the plugin access to the CSS // promise so it can know when its // CSS is ready args.cssLoaded = cssLoaded; // instantiate the plugin this.plugins[ plugin.name ] = new pluginClass( args ); } }, this ); })); }); }, /** * Resolve a URL relative to the browserRoot. */ resolveUrl: function( url ) { var browserRoot = this.config.browserRoot || this.config.baseUrl || ""; return Util.resolveUrl( browserRoot, url ); }, welcomeScreen: function( container, error ) { var thisB = this; require(['dojo/text!JBrowse/View/Resource/Welcome.html'], function(Welcome) { container.innerHTML = Welcome var topPane = dojo.create( 'div',{ style: {overflow: 'hidden'}}, thisB.container ); dojo.byId('welcome').innerHTML="Welcome! To get started with <i>JBrowse-"+thisB.version+"</i>, select a sequence file or an existing data directory"; on( dojo.byId('newOpen'), 'click', dojo.hitch( thisB, 'openFastaElectron' )); on( dojo.byId('newOpenDirectory'), 'click', function() { new OpenDirectoryDialog({ browser: thisB, setCallback: dojo.hitch( thisB, 'openDirectoryElectron' ) }).show(); }) try { thisB.loadSessions(); } catch(e) { console.error(e); } if( error ) { console.log(error); var errors_div = dojo.byId('fatal_error_list'); dojo.create('div', { className: 'error', innerHTML: error }, errors_div ); } request(thisB.resolveUrl('sample_data/json/volvox/successfully_run')).then( function() { try { document.getElementById('volvox_data_placeholder') .innerHTML = 'The example dataset is also available. View <a href="?data=sample_data/json/volvox">Volvox test data here</a>.'; } catch(e) {} }); }); }, /** * Make sure the browser container has nonzero container dimensions. If not, * set some hardcoded dimensions and log a warning. */ ensureNonzeroContainerDimensions() { const containerWidth = this.container.offsetWidth const containerHeight = this.container.offsetHeight if (!containerWidth) { console.warn(`JBrowse container element #${this.config.containerID} has no width, please set one with CSS. Setting fallback width of 640 pixels`) this.container.style.width = '640px' } if (!containerHeight) { console.warn(`JBrowse container element #${this.config.containerID} has no height, please set one with CSS. Setting fallback height of 480 pixels`) this.container.style.height = '480px' } }, /** * Main error handler. Displays links to configuration help or a * dataset selector in the main window. Called when the main browser * cannot run at all, because of configuration errors or whatever. */ fatalError: function( error ) { function formatError(error) { if( error ) { if( error.status ) { error = error.status +' ('+error.statusText+') when attempting to fetch '+error.url; } console.error( error.stack || ''+error ); error = error+''; if( ! /\.$/.exec(error) ) error = error + '.'; error = dojoxHtmlEntities.encode(error); } return error; } if( ! this.renderedFatalErrors ) { // if the error is just that there are no ref seqs defined, // and there are datasets defined in the conf file, then just // show a little HTML list of available datasets if( /^Could not load reference sequence/.test( error ) && this.config.datasets && ! this.config.datasets._DEFAULT_EXAMPLES ) { dojo.empty(this.container) new StandaloneDatasetList({ datasets: this.config.datasets }) .placeAt( this.container ); } else { var container = this.container || document.body; var thisB = this; dojo.addClass( document.body, this.config.theme || "tundra"); //< tundra dijit theme if( !Util.isElectron() ) { require([ 'dojo/text!JBrowse/View/Resource/Welcome_old.html' ], function(Welcome_old) { container.innerHTML = Welcome_old; if( error ) { var errors_div = dojo.byId('fatal_error_list'); dojo.create('div', { className: 'error', innerHTML: formatError(error)+'' }, errors_div ); } request( thisB.resolveUrl('sample_data/json/volvox/successfully_run') ).then( function() { try { dojo.byId('volvox_data_placeholder').innerHTML = 'However, it appears you have successfully run <code>./setup.sh</code>, so you can see the <a href="?data=sample_data/json/volvox">Volvox test data here</a>.'; } catch(e) {} }); }); } else { this.welcomeScreen( container, formatError(error) ); } this.renderedFatalErrors = true; } } else { var errors_div = dojo.byId('fatal_error_list') || document.body; dojo.create('div', { className: 'error', innerHTML: formatError(error)+'' }, errors_div ); } }, loadSessions: function() { var fs = electronRequire('fs'); var app = electronRequire('electron').remote.app; var path = this.config.electronData + '/sessions.json'; var obj = JSON.parse( fs.readFileSync( path, 'utf8' ) ); var table = dojo.create( 'table', { id: 'previousSessionsTable', style: { overflow: 'hidden', width: '90%' } }, dojo.byId('previousSessions') ); var thisB = this; if( ! obj.length ) { var tr = dojo.create( 'tr', {}, table ); dojo.create('div', { 'innerHTML': '<ul><li>No sessions yet!</li></ul>'}, tr); } array.forEach( obj, function( session ) { var tr = dojo.create( 'tr', {}, table ); var url = window.location.href.split('?')[0] + "?data=" + Util.replacePath( session.session ); dojo.create('div', { "class": "dijitIconDelete", onclick: function(e) { if( confirm( "This will simply delete your session from the list, it won't remove any data files. Are you sure you want to continue?" ) ) { dojo.empty(table); var index = obj.indexOf(session); if( index != -1 ) { obj.splice(index, 1); } fs.writeFileSync(path, JSON.stringify(obj, null, 2), 'utf8') thisB.loadSessions(); } } }, tr); dojo.create('td', { 'innerHTML': '<a href="'+url+'">'+session.session+'</a>' }, tr); }); }, loadRefSeqs: function() { var thisB = this; return this._milestoneFunction( 'loadRefSeqs', function( deferred ) { // load our ref seqs if( typeof this.config.refSeqs == 'string' ) { // assume this.config.refSeqs is a url if it is string this.config.refSeqs = { url: this.config.refSeqs }; } // check refseq urls if( this.config.refSeqs.url && this.config.refSeqs.url.match(/.fai$/) ) { new IndexedFasta({browser: this, faiUrlTemplate: this.config.refSeqs.url}) .getRefSeqs(function(refSeqs) { thisB.addRefseqs(refSeqs); deferred.resolve({success:true}); }, function(error) { deferred.reject(error); }); return; } else if( this.config.refSeqs.url && this.config.refSeqs.url.match(/.2bit$/) ) { new TwoBit({browser: this, urlTemplate: this.config.refSeqs.url}) .getRefSeqs(function(refSeqs) { thisB.addRefseqs(refSeqs); deferred.resolve({success:true}); }, function(error) { deferred.reject(error); }); } else if( this.config.refSeqs.url && this.config.refSeqs.url.match(/.fa$/) ) { new UnindexedFasta({browser: this, urlTemplate: this.config.refSeqs.url}) .getRefSeqs(function(refSeqs) { thisB.addRefseqs(refSeqs); deferred.resolve({success:true}); }, function(error) { deferred.reject(error); }); } else if( this.config.refSeqs.url && this.config.refSeqs.url.match(/.sizes/) ) { new ChromSizes({browser: this, urlTemplate: this.config.refSeqs.url}) .getRefSeqs(function(refSeqs) { thisB.addRefseqs(refSeqs); deferred.resolve({success:true}); }, function(error) { deferred.reject(error); }); } else if( 'data' in this.config.refSeqs ) { this.addRefseqs( this.config.refSeqs.data ); deferred.resolve({success:true}); } else { request(this.resolveUrl(this.config.refSeqs.url), { handleAs: 'text', headers: { 'X-Requested-With': null } } ) .then( function(o) { thisB.addRefseqs( dojo.fromJson(o) ); deferred.resolve({success:true}); }, function( e ) { deferred.reject( 'Could not load reference sequence definitions. '+e ); } ); } }); }, loadUserCSS: function() { return this._milestoneFunction( 'loadUserCSS', function( deferred ) { if( this.config.css && ! lang.isArray( this.config.css ) ) this.config.css = [ this.config.css ]; var css = this.config.css || []; if( ! css.length ) { deferred.resolve({success:true}); return; } var that = this; var cssDeferreds = array.map( css, function( css ) { return that._loadCSS( css ); }); new DeferredList(cssDeferreds) .then( function() { deferred.resolve({success:true}); } ); }); }, _loadCSS: function( css ) { var deferred = new Deferred(); if( typeof css == 'string' ) { // if it has '{' in it, it probably is not a URL, but is a string of CSS statements if( css.indexOf('{') > -1 ) { dojo.create('style', { "data-from": 'JBrowse Config', type: 'text/css', innerHTML: css }, document.head ); deferred.resolve(true); } // otherwise, it must be a URL else { css = { url: css }; } } if( typeof css == 'object' ) { LazyLoad.css( css.url, function() { deferred.resolve(true); } ); } return deferred; }, /** * Load our name index. */ loadNames: function() { return this._milestoneFunction( 'loadNames', function( deferred ) { var conf = dojo.mixin( dojo.clone( this.config.names || {} ), this.config.autocomplete || {} ); if( ! conf.url ) conf.url = this.config.nameUrl || 'data/names/'; if( conf.baseUrl ) conf.url = Util.resolveUrl( conf.baseUrl, conf.url ); var type; if(( type = conf.type )) { var thisB = this; if( type.indexOf('/') == -1 ) type = 'JBrowse/Store/Names/'+type; dojo.global.require ([type], function (CLASS){ thisB.nameStore = new CLASS( dojo.mixin({ browser: thisB }, conf) ); deferred.resolve({success: true}); }); } // no name type setting, must be the legacy store else { // wrap the older LazyTrieDojoDataStore with // dojo.store.DataStore to conform with the dojo/store API this.nameStore = new DojoDataStore({ store: new NamesLazyTrieDojoDataStore({ browser: this, namesTrie: new LazyTrie( conf.url, "lazy-{Chunk}.json"), stopPrefixes: conf.stopPrefixes, resultLimit: conf.resultLimit || 15, tooManyMatchesMessage: conf.tooManyMatchesMessage }) }); deferred.resolve({success: true}); } }); }, /** * Compare two reference sequence names, returning -1, 0, or 1 * depending on the result. Case insensitive, insensitive to the * presence or absence of prefixes like 'chr', 'chrom', 'ctg', * 'contig', 'scaffold', etc */ compareReferenceNames: function( a, b ) { return this.regularizeReferenceName(a).localeCompare( this.regularizeReferenceName( b ) ); }, /** * Regularize the reference sequence name in a location. */ regularizeLocation: function( location ) { var ref = this.findReferenceSequence( location.ref || location.objectName ); if( ref ) location.ref = ref.name; return location; }, regularizeReferenceName: function( refname ) { if( this.config.exactReferenceSequenceNames ) return refname; refname = refname.toLowerCase() // special case of double regularizing behaving badly if(refname.match(/^chrm/)) { return 'chrm' } refname = refname .replace(/^chro?m?(osome)?/,'chr') .replace(/^co?n?ti?g/,'ctg') .replace(/^scaff?o?l?d?/,'scaffold') .replace(/^([a-z]*)0+/,'$1') .replace(/^(\d+l?r?|x|y)$/, 'chr$1' ) .replace(/^(x?)(ix|iv|v?i{0,3})$/, 'chr$1$2' ) .replace(/^mt?$/, 'chrm'); return refname; }, initView: function() { var thisObj = this; return this._milestoneFunction('initView', function( deferred ) { //set up top nav/overview pane and main GenomeView pane dojo.addClass( this.container, "jbrowse"); // browser container has an overall .jbrowse class dojo.addClass( document.body, this.config.theme || "tundra"); //< tundra dijit theme var topPane = dojo.create( 'div',{ style: {overflow: 'hidden'}}, this.container ); var about = this.browserMeta(); var aboutDialog = new InfoDialog( { title: 'About '+about.title, content: about.description, className: 'about-dialog' }); // make our top menu bar var menuBar = dojo.create( 'div', { className: this.config.show_nav ? 'menuBar' : 'topLink' } ); thisObj.menuBar = menuBar; if( this.config.show_menu ) { ( this.config.show_nav ? topPane : this.container ).appendChild( menuBar ); } var overview = dojo.create( 'div', { className: 'overview', id: 'overview' }, topPane ); this.overviewDiv = overview; // overview=0 hides the overview, but we still need it to exist if( ! this.config.show_overview ) overview.style.cssText = "display: none"; if( Util.isElectron() && !this.config.hideGenomeOptions ) { this.addGlobalMenuItem(this.config.classicMenu ? 'file':'dataset', new dijitMenuItem( { id: 'menubar_dataset_file', label: "Open sequence file", iconClass: 'dijitIconFolderOpen', onClick: dojo.hitch( this, 'openFastaElectron' ) } ) ); this.addGlobalMenuItem(this.config.classicMenu ? 'file':'dataset', new dijitMenuItem( { id: 'menubar_dataset_directory', label: "Open data directory", iconClass: 'dijitIconFolderOpen', onClick: function() { new OpenDirectoryDialog({ browser: thisObj, setCallback: dojo.hitch( thisObj, 'openDirectoryElectron' ) }).show(); } } ) ); this.addGlobalMenuItem(this.config.classicMenu ? 'file':'dataset', new dijitMenuItem( { id: 'menubar_dataset_save', label: "Save session", iconClass: 'dijitIconSave', onClick: dojo.hitch( this, 'saveData' ) } ) ); this.addGlobalMenuItem(this.config.classicMenu ? 'file':'dataset', new dijitMenuItem( { id: 'menubar_dataset_home', label: "Return to main menu", iconClass: 'dijitIconTask', onClick: dojo.hitch( this, function() { var container = thisObj.container || document.body;thisObj.welcomeScreen(container); } ) } ) ); } else if( !this.config.hideGenomeOptions ) { this.addGlobalMenuItem(this.config.classicMenu ? 'file':'dataset', new dijitMenuItem( { id: 'menubar_dataset_open', label: "Open sequence file", iconClass: 'dijitIconFolderOpen', onClick: dojo.hitch( this, 'openFasta' ) }) ); } if( this.config.show_nav ) { this.navbox = this.createNavBox( topPane ); // make the dataset menu if(this.config.classicMenu) { if( this.config.datasets && ! this.config.dataset_id ) { console.warn("In JBrowse configuration, datasets specified, but dataset_id not set. Dataset selector will not be shown."); } if( this.config.datasets && this.config.dataset_id ) { this.renderDatasetSelect( menuBar ); } else { this.poweredByLink = dojo.create('a', { className: 'powered_by', innerHTML: this.browserMeta().title, title: 'powered by JBrowse' }, menuBar ); thisObj.poweredBy_clickHandle = dojo.connect(this.poweredByLink, "onclick", dojo.hitch( aboutDialog, 'show') ); } } else this.renderDatasetSelect( menuBar ); // make the file menu this.addGlobalMenuItem( 'file', new dijitMenuItem( { id: 'menubar_fileopen', label: 'Open track file or URL', iconClass: 'dijitIconFolderOpen', onClick: dojo.hitch( this, 'openFileDialog' ) }) ); this.addGlobalMenuItem( 'file', new dijitMenuSeparator() ); this.fileDialog = new FileDialog({ browser: this }); this.addGlobalMenuItem( 'file', new dijitMenuItem( { id: 'menubar_combotrack', label: 'Add combination track', iconClass: 'dijitIconSample', onClick: dojo.hitch(this, 'createCombinationTrack') })); this.renderGlobalMenu( 'file', {text: this.config.classicMenu?'File':'Track'}, menuBar ); // make the view menu this.addGlobalMenuItem( 'view', new dijitMenuItem({ id: 'menubar_sethighlight', label: 'Set highlight', iconClass: 'dijitIconFilter', onClick: function() { new SetHighlightDialog({ browser: thisObj, setCallback: dojo.hitch( thisObj, 'setHighlightAndRedraw' ) }).show(); } })); // make the menu item for clearing the current highlight this._highlightClearButton = new dijitMenuItem( { id: 'menubar_clearhighlight', label: 'Clear highlight', iconClass: 'dijitIconFilter', onClick: dojo.hitch( this, function() { var h = this.getHighlight(); if( h ) { this.clearHighlight(); this.view.redrawRegion( h ); } }) }); this._updateHighlightClearButton(); //< sets the label and disabled status // update it every time the highlight changes this.subscribe( '/jbrowse/v1/n/globalHighlightChanged', dojo.hitch( this, '_updateHighlightClearButton' ) ); this.addGlobalMenuItem( 'view', this._highlightClearButton ); // add a global menu item for resizing all visible quantitative tracks this.addGlobalMenuItem( 'view', new dijitMenuItem({ label: 'Resize quant. tracks', id: 'menubar_settrackheight', title: 'Set all visible quantitative tracks to a new height', iconClass: 'jbrowseIconVerticalResize', onClick: function() { new SetTrackHeightDialog({ setCallback: function( height ) { var tracks = thisObj.view.visibleTracks(); array.forEach( tracks, function( track ) { // operate only on XYPlot or Density tracks if( ! /\b(XYPlot|Density)/.test( track.config.type ) ) return; track.trackHeightChanged=true; track.updateUserStyles({ height: height }); }); } }).show(); } })); if (!this.config.disableSearch) { this.addGlobalMenuItem( 'view', new dijitMenuItem({ label: 'Search features', id: 'menubar_search', title: 'Search for features', onClick: () => { var conf = dojo.mixin( dojo.clone( this.config.names || {} ), this.config.autocomplete || {} ); var type = conf.dialog || 'JBrowse/View/Dialog/Search'; dojo.global.require ([type], CLASS => { new CLASS(dojo.mixin({ browser: this }, conf)).show(); }); } })); } this.renderGlobalMenu( 'view', {text: 'View'}, menuBar ); // make the options menu this.renderGlobalMenu( 'options', { text: 'Options', title: 'configure JBrowse' }, menuBar ); } function showHelp() { new HelpDialog( lang.mixin(thisObj.config.quickHelp || {}, { browser: thisObj } )).show(); } if( this.config.show_nav ) { // make the help menu this.addGlobalMenuItem( 'help', new dijitMenuItem( { id: 'menubar_about', label: 'About', //iconClass: 'dijitIconFolderOpen', onClick: dojo.hitch( aboutDialog, 'show' ) }) ); this.setGlobalKeyboardShortcut( '?', showHelp ); this.addGlobalMenuItem( 'help', new dijitMenuItem( { id: 'menubar_generalhelp', label: 'General', iconClass: 'jbrowseIconHelp', onClick: showHelp }) ); this.renderGlobalMenu( 'help', {}, menuBar ); if (!this.config.classicMenu) { let datasetName = lang.getObject(`config.datasets.${this.config.dataset_id}.name`, false, this) this.menuBarDatasetName = dojo.create('div', { className: 'dataset-name', innerHTML: datasetName, title: 'name of current dataset', style: { display: datasetName ? 'inline-block' : 'none' } }, menuBar ); } } if( this.config.show_nav && this.config.show_tracklist && this.config.show_overview && !Util.isElectron() ) { var shareLink = this.makeShareLink(); if (shareLink) { menuBar.appendChild( shareLink ); } } else if(Util.isElectron()) { var snapLink = this.makeSnapLink(); if(snapLink) { menuBar.appendChild( snapLink ); } } else { if ( this.config.show_fullviewlink ) menuBar.appendChild( this.makeFullViewLink() ); } this.viewElem = document.createElement("div"); this.viewElem.className = "dragWindow"; this.container.appendChild( this.viewElem); this.containerWidget = new dijitBorderContainer({ liveSplitters: false, design: "sidebar", gutters: false }, this.container); var contentWidget = new dijitContentPane({region: "top"}, topPane); // hook up GenomeView this.view = this.viewElem.view = new GenomeView( { browser: this, elem: this.viewElem, config: this.config.view, stripeWidth: 250, refSeq: this.refSeq }); dojo.connect( this.view, "onFineMove", this, "onFineMove" ); dojo.connect( this.view, "onCoarseMove", this, "onCoarseMove" ); this.browserWidget = new dijitContentPane({region: "center"}, this.viewElem); dojo.connect( this.browserWidget, "resize", this, 'onResize' ); dojo.connect( this.browserWidget, "resize", this.view, 'onResize' ); //connect events to update the URL in the location bar function updateLocationBar() { var shareURL = thisObj.makeCurrentViewURL(); if( thisObj.config.updateBrowserURL && window.history && window.history.replaceState ) window.history.replaceState( {},"", shareURL ); if (thisObj.config.update_browser_title) document.title = thisObj.browserMeta().title + ' ' + thisObj.view.visibleRegionLocString(); }; dojo.connect( this, "onCoarseMove", updateLocationBar ); this.subscribe( '/jbrowse/v1/n/tracks/visibleChanged', updateLocationBar ); this.subscribe( '/jbrowse/v1/n/globalHighlightChanged', updateLocationBar ); //set initial location this.afterMilestone( 'loadRefSeqs', dojo.hitch( this, function() { this.afterMilestone( 'initTrackMetadata', dojo.hitch( this, function() { this.createTrackList().then( dojo.hitch( this, function() { this.containerWidget.startup(); this.onResize(); // make our global keyboard shortcut handler on( document.body, 'keypress', dojo.hitch( this, 'globalKeyHandler' )); // configure our event routing this._initEventRouting(); // done with initView deferred.resolve({ success: true }); })); })); })); }); }, createCombinationTrack: function() { if(this._combinationTrackCount === undefined) this._combinationTrackCount = 0; var d = new Deferred(); var storeConf = { browser: this, refSeq: this.refSeq, type: 'JBrowse/Store/SeqFeature/Combination' }; var storeName = this.addStoreConfig(undefined, storeConf); storeConf.name = storeName; this.getStore(storeName, function(store) { d.resolve(true); }); var thisB = this; d.promise.then(function(){ var combTrackConfig = { type: 'JBrowse/View/Track/Combination', label: "combination_track" + (thisB._combinationTrackCount++), key: "Combination Track " + (thisB._combinationTrackCount), metadata: {Description: "Drag-and-drop interface that creates a track out of combinations of other tracks."}, store: storeName }; // send out a message about how the user wants to create the new tracks thisB.publish( '/jbrowse/v1/v/tracks/new', [combTrackConfig] ); // Open the track immediately thisB.publish( '/jbrowse/v1/v/tracks/show', [combTrackConfig] ); }); }, renderDatasetSelect: function( parent ) { var thisB=this; if(this.config.classicMenu) { var dsconfig = this.config.datasets || {}; var datasetChoices = []; for( var id in dsconfig ) { if( ! /^_/.test(id) ) datasetChoices.push( Object.assign({ id: id }, dsconfig[id] ) ); } const combobox = new dijitComboBox( { name: 'dataset', className: 'dataset_select', value: this.config.datasets[this.config.dataset_id].name, store: new dojoMemoryStore({ data: datasetChoices, }), onChange: dsName => { if (!dsName) return false const dsID = datasetChoices.find(d => d.name === dsName).id const ds = (this.config.datasets||{})[dsID] let conf = this.config; if (ds) { let link2Parent = conf.datasetLinkToParentIframe || false; if (link2Parent) window.parent.location = ds.url; else window.location = ds.url; } return false }, }) combobox.placeAt( parent ) combobox.focusNode.onclick = function() { this.select() } if (this.config.datasetSelectorWidth) { combobox.domNode.style.width = this.config.datasetSelectorWidth combobox.focusNode.style.width = this.config.datasetSelectorWidth } } else { let conf = this.config; if( this.config.datasets && this.config.dataset_id ) { this.addGlobalMenuItem( 'dataset', new dijitMenuSeparator() ); for( var id in this.config.datasets ) { if( ! /^_/.test(id) ) { var dataset = this.config.datasets[id] this.addGlobalMenuItem( 'dataset', new dijitMenuItem( { id: 'menubar_dataset_bookmark_' + id, label: id == this.config.dataset_id ? ('<b>' + dataset.name + '</b>') : dataset.name, iconClass: 'dijitIconBookmark', onClick: dojo.hitch( dataset, function() { // if datasetLinkToParentIframe=true, link to parent of iframe. let link2Parent = conf.datasetLinkToParentIframe || false; if (link2Parent) window.parent.location = this.url; else window.location = this.url; }) }) ); } } } this.renderGlobalMenu( 'dataset', {text: 'Genome'}, parent ); } }, saveSessionDir: function( directory ) { var fs = electronRequire('fs'); var path = this.config.electronData + '/sessions.json'; var obj = []; try { var obj = JSON.parse( fs.readFileSync(path, 'utf8') ); } catch(e) { console.error(e); } var dir = Util.replacePath( directory ); if( array.every(obj, function(elt) { return elt.session != dir; }) ) obj.push({ session: dir }); fs.writeFileSync(path, JSON.stringify( obj, null, 2 ), 'utf8'); }, openDirectoryElectron: function( directory ) { this.saveSessionDir( directory ); window.location = "?data=" + Util.replacePath( directory ); }, openConfig: function( plugins ) { if( !confirm("If you have opened any new tracks, please save them before continuing. Are you sure you want to continue?") ) re