@gmod/jbrowse
Version:
JBrowse - client-side genome browser
1,230 lines (1,086 loc) • 133 kB
JavaScript
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