1 // CONTROLLER 2 3 var dojof; 4 var _gaq = _gaq || []; 5 6 /** 7 * Construct a new Browser object. 8 * @class This class is the main interface between JBrowse and embedders 9 * @constructor 10 * @param params an object with the following properties:<br> 11 * <ul> 12 * <li><code>config</code> - list of objects with "url" property that points to a config JSON file</li> 13 * <li><code>containerID</code> - ID of the HTML element that contains the browser</li> 14 * <li><code>refSeqs</code> - object with "url" property that is the URL to list of reference sequence information items</li> 15 * <li><code>browserRoot</code> - (optional) URL prefix for the browser code</li> 16 * <li><code>tracks</code> - (optional) comma-delimited string containing initial list of tracks to view</li> 17 * <li><code>location</code> - (optional) string describing the initial location</li> 18 * <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> 19 * <li><code>defaultLocation</code> - (optional) string describing the initial location if there are no cookies and no "location" parameter</li> 20 * <li><code>show_nav</code> - (optional) string describing the on/off state of navigation box</li> 21 * <li><code>show_tracklist</code> - (optional) string describing the on/off state of track bar</li> 22 * <li><code>show_overview</code> - (optional) string describing the on/off state of overview</li> 23 * </ul> 24 */ 25 26 var Browser = function(params) { 27 dojo.require('dojox.lang.functional'); 28 dojo.require('dojox.lang.functional.array'); 29 dojo.require('dojox.lang.functional.fold'); 30 dojof = dojox.lang.functional; 31 32 dojo.require("dojo.dnd.Source"); 33 dojo.require("dojo.dnd.Moveable"); 34 dojo.require("dojo.dnd.Mover"); 35 dojo.require("dojo.dnd.move"); 36 dojo.require("dijit.layout.ContentPane"); 37 dojo.require("dijit.layout.BorderContainer"); 38 dojo.require("dijit.Dialog"); 39 40 this.deferredFunctions = []; 41 this.globalKeyboardShortcuts = {}; 42 this.isInitialized = false; 43 44 this.config = params; 45 46 this.startTime = Date.now(); 47 48 // load our touch device support 49 // TODO: refactor this 50 this.deferredFunctions.push(function() { loadTouch(); }); 51 52 // schedule the config load, the first step in the initialization 53 // process, to happen when the page is done loading 54 var browser = this; 55 dojo.addOnLoad( function() { browser.loadConfig(); } ); 56 57 dojo.connect( this, 'onConfigLoaded', Util.debugHandler( this, 'loadRefSeqs' )); 58 dojo.connect( this, 'onConfigLoaded', Util.debugHandler( this, 'loadNames' )); 59 dojo.connect( this, 'onRefSeqsLoaded', Util.debugHandler( this, 'initView' )); 60 dojo.connect( this, 'onRefSeqsLoaded', Util.debugHandler( this, 'reportUsageStats' )); 61 }; 62 63 /** 64 * Displays links to configuration help in the main window. Called 65 * when the main browser cannot run at all, due to configuration 66 * errors or whatever. 67 */ 68 Browser.prototype.fatalError = function( error ) { 69 if( error ) { 70 error = error+''; 71 if( ! /\.$/.exec(error) ) 72 error = error + '.'; 73 } 74 if( ! this.hasFatalErrors ) { 75 var container = 76 dojo.byId(this.config.containerID || 'GenomeBrowser') 77 || document.body; 78 container.innerHTML = '' 79 + '<div class="fatal_error">' 80 + ' <h1>Congratulations, JBrowse is on the web!</h1>' 81 + " <p>However, JBrowse could not start, either because it has not yet been configured or because of an error.</p>" 82 + " <p style=\"font-size: 110%; font-weight: bold\"><a title=\"View the tutorial\" href=\"docs/tutorial/\">If this is your first time running JBrowse, click here to follow the Quick-start Tutorial to get up and running.</a></p>" 83 + " <p>Otherwise, please refer to the following resources for help in getting JBrowse up and running.</p>" 84 + ' <ul><li><a target="_blank" href="docs/tutorial/">Quick-start tutorial</a></li>' 85 + ' <li><a target="_blank" href="http://gmod.org/wiki/JBrowse">JBrowse wiki</a></li>' 86 + ' <li><a target="_blank" href="docs/config.html">Configuration reference</a></li>' 87 + ' <li><a target="_blank" href="docs/featureglyphs.html">Feature glyph reference</a></li>' 88 + ' </ul>' 89 90 + ' <div id="fatal_error_list" class="errors"> <h2>Error message(s):</h2>' 91 + ( error ? '<div class="error"> '+error+'</div>' : '' ) 92 + ' </div>' 93 + '</div>' 94 ; 95 this.hasFatalErrors = true; 96 } else { 97 var errors_div = dojo.byId('fatal_error_list') || document.body; 98 dojo.create('div', { className: 'error', innerHTML: error+'' }, errors_div ); 99 } 100 }; 101 102 Browser.prototype.loadRefSeqs = function() { 103 // load our ref seqs 104 if( typeof this.config.refSeqs == 'string' ) 105 this.config.refSeqs = { url: this.config.refSeqs }; 106 dojo.xhrGet( 107 { 108 url: this.config.refSeqs.url, 109 handleAs: 'json', 110 load: dojo.hitch( this, function(o) { 111 this.addRefseqs(o); 112 this.onRefSeqsLoaded(); 113 }) 114 }); 115 }; 116 117 /** 118 * Event that fires when the reference sequences have been loaded. 119 */ 120 Browser.prototype.onRefSeqsLoaded = function() {}; 121 122 /** 123 * Load our name index. 124 */ 125 Browser.prototype.loadNames = function() { 126 // load our name index 127 if (this.config.nameUrl) 128 this.names = new LazyTrie(this.config.nameUrl, "lazy-{Chunk}.json"); 129 }; 130 131 Browser.prototype.initView = function() { 132 //set up top nav/overview pane and main GenomeView pane 133 dojo.addClass(document.body, "tundra"); 134 this.container = dojo.byId(this.config.containerID); 135 this.container.onselectstart = function() { return false; }; 136 this.container.genomeBrowser = this; 137 var topPane = dojo.create( 'div',{ style: {overflow: 'hidden'}}, this.container ); 138 139 var overview = dojo.create( 'div', { className: 'overview', id: 'overview' }, topPane ); 140 // overview=0 hides the overview, but we still need it to exist 141 if( this.config.show_overview == 0 ) 142 overview.style.cssText = "display: none"; 143 144 if( this.config.show_nav != 0 ) 145 this.navbox = this.createNavBox( topPane, 25 ); 146 147 // make our little top-links box with links to help, etc. 148 var linkContainer = dojo.create('div', { className: 'topLink' }); 149 dojo.create('a', { 150 className: 'powered_by', 151 innerHTML: 'JBrowse', 152 href: 'http://jbrowse.org', 153 title: 'powered by JBrowse' 154 }, linkContainer ); 155 linkContainer.appendChild( this.makeBookmarkLink() ); 156 if( this.config.show_nav != 0 ) 157 linkContainer.appendChild( this.makeHelpDialog() ); 158 ( this.config.show_nav == 0 ? this.container : this.navbox ).appendChild( linkContainer ); 159 160 161 this.viewElem = document.createElement("div"); 162 this.viewElem.className = "dragWindow"; 163 this.container.appendChild( this.viewElem); 164 165 this.containerWidget = new dijit.layout.BorderContainer({ 166 liveSplitters: false, 167 design: "sidebar", 168 gutters: false 169 }, this.container); 170 var contentWidget = 171 new dijit.layout.ContentPane({region: "top"}, topPane); 172 this.browserWidget = 173 new dijit.layout.ContentPane({region: "center"}, this.viewElem); 174 175 //create location trapezoid 176 if( this.config.show_nav != 0 ) { 177 this.locationTrap = dojo.create('div', {className: 'locationTrap'}, topPane ); 178 this.locationTrap.className = "locationTrap"; 179 } 180 181 // hook up GenomeView 182 this.view = this.viewElem.view = 183 new GenomeView(this, this.viewElem, 250, this.refSeq, 1/200, 184 this.config.browserRoot); 185 dojo.connect( this.view, "onFineMove", this, "onFineMove" ); 186 dojo.connect( this.view, "onCoarseMove", this, "onCoarseMove" ); 187 188 //set up track list 189 var trackListDiv = this.createTrackList(); 190 this.containerWidget.startup(); 191 dojo.connect( this.browserWidget, "resize", this, 'onResize' ); 192 dojo.connect( this.browserWidget, "resize", this.view, 'onResize' ); 193 this.onResize(); 194 this.view.onResize(); 195 196 //set initial location 197 var oldLocMap = dojo.fromJson( this.cookie('location') ) || {}; 198 if (this.config.location) { 199 this.navigateTo(this.config.location); 200 } else if (oldLocMap[this.refSeq.name]) { 201 this.navigateTo( oldLocMap[this.refSeq.name] ); 202 } else if (this.config.defaultLocation){ 203 this.navigateTo(this.config.defaultLocation); 204 } else { 205 this.navigateTo( Util.assembleLocString({ 206 ref: this.refSeq.name, 207 start: 0.4 * ( this.refSeq.start + this.refSeq.end ), 208 end: 0.6 * ( this.refSeq.start + this.refSeq.end ) 209 }) 210 ); 211 } 212 213 // make our global keyboard shortcut handler 214 dojo.connect( document.body, 'onkeypress', this, 'globalKeyHandler' ); 215 216 // configure our event routing 217 this._initEventRouting(); 218 219 this.isInitialized = true; 220 221 //if someone calls methods on this browser object 222 //before it's fully initialized, then we defer 223 //those functions until now 224 dojo.forEach( this.deferredFunctions, function(f) { 225 f.call(this); 226 },this ); 227 228 this.deferredFunctions = []; 229 }; 230 231 /** 232 * Initialize our event routing, which is mostly echoing logical 233 * commands from the user interacting with the views. 234 * @private 235 */ 236 Browser.prototype._initEventRouting = function() { 237 this.subscribe('/jbrowse/v1/v/tracks/hide', this, function() { 238 this.publish( '/jbrowse/v1/c/tracks/hide', arguments ); 239 }); 240 this.subscribe('/jbrowse/v1/v/tracks/show', this, function( trackConfigs ) { 241 this.addRecentlyUsedTracks( dojo.map(trackConfigs, function(c){ return c.label;}) ); 242 this.publish( '/jbrowse/v1/c/tracks/show', arguments ); 243 }); 244 }; 245 246 /** 247 * Reports some anonymous usage statistics about this browsing 248 * instance. Currently reports the number of tracks in the instance 249 * and their type (feature, wiggle, etc), and the number of reference 250 * sequences and their average length. 251 */ 252 Browser.prototype.reportUsageStats = function() { 253 if( this.config.suppressUsageStatistics ) 254 return; 255 256 var stats = this._calculateClientStats(); 257 this._reportGoogleUsageStats( stats ); 258 this._reportCustomUsageStats( stats ); 259 }; 260 261 // phones home to google analytics 262 Browser.prototype._reportGoogleUsageStats = function( stats ) { 263 _gaq.push.apply( _gaq, [ 264 ['_setAccount', 'UA-7115575-2'], 265 ['_setDomainName', 'none'], 266 ['_setAllowLinker', true], 267 ['_setCustomVar', 1, 'tracks-count', stats['tracks-count'], 3 ], 268 ['_setCustomVar', 2, 'refSeqs-count', stats['refSeqs-count'], 3 ], 269 ['_setCustomVar', 3, 'refSeqs-avgLen', stats['refSeqs-avgLen'], 3 ], 270 ['_setCustomVar', 4, 'jbrowse-version', stats['ver'], 3 ], 271 ['_setCustomVar', 5, 'loadTime', stats['loadTime'], 3 ], 272 ['_trackPageview'] 273 ]); 274 275 var ga = document.createElement('script'); 276 ga.type = 'text/javascript'; 277 ga.async = true; 278 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') 279 + '.google-analytics.com/ga.js'; 280 var s = document.getElementsByTagName('script')[0]; 281 s.parentNode.insertBefore(ga, s); 282 }; 283 284 // phones home to custom analytics at jbrowse.org 285 Browser.prototype._reportCustomUsageStats = function(stats) { 286 // phone home with a GET request made by a script tag 287 dojo.create( 288 'img', 289 { style: { 290 display: 'none' 291 }, 292 src: 'http://jbrowse.org/analytics/clientReport?' 293 + dojo.objectToQuery( stats ) 294 }, 295 document.body 296 ); 297 }; 298 299 Browser.prototype._calculateClientStats = function() { 300 301 var scn = screen || window.screen; 302 303 // make a flat (i.e. non-nested) object for the stats, so that it 304 // encodes compactly in the query string 305 var date = new Date(); 306 var stats = { 307 ver: this.version || 'dev', 308 'refSeqs-count': this.refSeqOrder.length, 309 'refSeqs-avgLen': 310 ! this.refSeqOrder.length 311 ? null 312 : dojof.reduce( 313 dojo.map( this.refSeqOrder, 314 function(name) { 315 var ref = this.allRefs[name]; 316 if( !ref ) 317 return 0; 318 return ref.end - ref.start; 319 }, 320 this 321 ), 322 '+' 323 ), 324 'tracks-count': this.config.tracks.length, 325 326 // screen geometry 327 'scn-h': scn ? scn.height : null, 328 'scn-w': scn ? scn.width : null, 329 // window geometry 330 'win-h':document.body.offsetHeight, 331 'win-w': document.body.offsetWidth, 332 // container geometry 333 'el-h': this.container.offsetHeight, 334 'el-w': this.container.offsetWidth, 335 336 // time param to prevent caching 337 t: date.getTime()/1000, 338 339 // also get local time zone offset 340 tzoffset: date.getTimezoneOffset(), 341 342 loadTime: (date.getTime() - this.startTime)/1000 343 }; 344 345 // count the number and types of tracks 346 dojo.forEach( this.config.tracks, function(trackConfig) { 347 var typeKey = 'track-types-'+ trackConfig.type || 'null'; 348 stats[ typeKey ] = 349 ( stats[ typeKey ] || 0 ) + 1; 350 }); 351 352 return stats; 353 }; 354 355 356 357 Browser.prototype.publish = function() { 358 return dojo.publish.apply( dojo, arguments ); 359 }; 360 Browser.prototype.subscribe = function() { 361 return dojo.subscribe.apply( dojo, arguments ); 362 }; 363 364 Browser.prototype.onResize = function() { 365 if( this.navbox ) 366 this.view.locationTrapHeight = dojo.marginBox( this.navbox ).h; 367 }; 368 369 /** 370 * Get the list of the most recently used tracks, stored for this user 371 * in a cookie. 372 * @returns {Array[Object]} as <code>[{ time: (integer), label: (track label)}]</code> 373 */ 374 Browser.prototype.getRecentlyUsedTracks = function() { 375 return dojo.fromJson( this.cookie( 'recentTracks' ) || '[]' ); 376 }; 377 378 /** 379 * Add the given list of tracks as being recently used. 380 * @param trackLabels {Array[String]} array of track labels to add 381 */ 382 Browser.prototype.addRecentlyUsedTracks = function( trackLabels ) { 383 var seen = {}; 384 var newRecent = 385 Util.uniq( 386 dojo.map( trackLabels, function(label) { 387 return { 388 label: label, 389 time: Math.round( new Date() / 1000 ) // secs since epoch 390 }; 391 },this) 392 .concat( dojo.fromJson( this.cookie('recentTracks')) || [] ), 393 function(entry) { 394 return entry.label; 395 } 396 ) 397 // limit by default to 20 recent tracks 398 .slice( 0, this.config.recentTracksLimit == undefined ? this.config.recentTracksLimit : 20 ); 399 400 // set the recentTracks cookie, good for one year 401 this.cookie( 'recentTracks', newRecent, { expires: 365 } ); 402 403 return newRecent; 404 }; 405 406 /** 407 * Load our configuration file(s) based on the parameters thex 408 * constructor was passed. Does not return until all files are 409 * loaded and merged in. 410 * @returns nothing meaningful 411 */ 412 Browser.prototype.loadConfig = function () { 413 var that = this; 414 415 // coerce include to an array 416 if( typeof this.config.include != 'object' || !this.config.include.length ) 417 this.config.include = [ this.config.include ]; 418 419 // coerce bare strings in the configs to URLs 420 for (var i = 0; i < this.config.include.length; i++) { 421 if( typeof this.config.include[i] == 'string' ) 422 this.config.include[i] = { url: this.config.include[i] }; 423 } 424 425 // fetch and parse all the configuration data 426 var configs_remaining = this.config.include.length; 427 dojo.forEach( this.config.include, function(config) { 428 // include array might have undefined elements in it if 429 // somebody left a trailing comma in and we are running under 430 // IE 431 if( !config ) 432 return; 433 434 // set defaults for format and version 435 if( ! ('format' in config) ) { 436 config.format = 'JB_json'; 437 } 438 if( config.format == 'JB_json' && ! ('version' in config) ) { 439 config.version = 1; 440 } 441 442 // instantiate the adaptor and load the config 443 var adaptor = this.getConfigAdaptor( config ); 444 if( !adaptor ) { 445 this.fatalError( "Could not load config "+config.url+", no configuration adaptor found for config format "+config.format+' version '+config.version ); 446 return; 447 } 448 449 adaptor.load({ 450 config: config, 451 context: this, 452 onSuccess: function( config_data, request_info ) { 453 config.data = config_data; 454 config.loaded = true; 455 if( ! --configs_remaining ) 456 this.onConfigLoaded(); 457 //if you need a backtrace: window.setTimeout( function() { that.onConfigLoaded(); }, 1 ); 458 }, 459 onFailure: function( error ) { 460 config.loaded = false; 461 this.fatalError( error ); 462 if( ! --configs_remaining ) 463 this.onConfigLoaded(); 464 //if you need a backtrace: window.setTimeout( function() { that.onConfigLoaded(); }, 1 ); 465 } 466 }); 467 468 }, this); 469 }; 470 471 Browser.prototype.onConfigLoaded = function() { 472 473 var initial_config = this.config; 474 this.config = {}; 475 476 // load all the configuration data in order 477 dojo.forEach( initial_config.include, function( config ) { 478 if( config.loaded && config.data ) 479 this.addConfigData( config.data ); 480 }, this ); 481 482 // load the initial config (i.e. constructor params) last so that 483 // it overrides the other config 484 this.addConfigData( initial_config ); 485 486 this.validateConfig(); 487 488 // index the track configurations by name 489 this.trackConfigsByName = {}; 490 dojo.forEach( this.config.tracks || [], function(conf){ 491 this.trackConfigsByName[conf.label] = conf; 492 },this); 493 494 }; 495 496 /** 497 * Examine the loaded and merged configuration for errors. Throws 498 * exceptions if it finds anything amiss. 499 * @returns nothing meaningful 500 */ 501 Browser.prototype.validateConfig = function() { 502 var c = this.config; 503 if( ! c.tracks ) { 504 this.fatalError( 'No tracks defined in configuration' ); 505 } 506 if( ! c.baseUrl ) { 507 this.fatalError( 'Must provide a <code>baseUrl</code> in configuration' ); 508 } 509 if( this.hasFatalErrors ) 510 throw "Errors in configuration, aborting."; 511 }; 512 513 /** 514 * Instantiate the right config adaptor for a given configuration source. 515 * @param {Object} config the configuraiton 516 * @returns {Object} the right configuration adaptor to use, or 517 * undefined if one could not be found 518 */ 519 520 Browser.prototype.getConfigAdaptor = function( config_def ) { 521 var adaptor_name = "ConfigAdaptor." + config_def.format; 522 if( 'version' in config_def ) 523 adaptor_name += '_v'+config_def.version; 524 adaptor_name.replace( /\W/g,'' ); 525 var adaptor_class = eval( adaptor_name ); 526 if( ! adaptor_class ) 527 return undefined; 528 529 return new adaptor_class( config_def ); 530 }; 531 532 /** 533 * Add a function to be executed once JBrowse is initialized 534 * @param f function to be executed 535 */ 536 Browser.prototype.addDeferred = function(f) { 537 if (this.isInitialized) 538 f(); 539 else 540 this.deferredFunctions.push(f); 541 }; 542 543 /** 544 * Merge in some additional configuration data. Properties in the 545 * passed configuration will override those properties in the existing 546 * configuration. 547 */ 548 Browser.prototype.addConfigData = function( /**Object*/ config_data ) { 549 Util.deepUpdate( this.config, config_data ); 550 }; 551 552 /** 553 * @param refSeqs {Array} array of refseq records to add to the browser 554 */ 555 Browser.prototype.addRefseqs = function( refSeqs ) { 556 this.allRefs = this.allRefs || {}; 557 this.refSeqOrder = this.refSeqOrder || []; 558 var refCookie = this.cookie('refseq'); 559 dojo.forEach( refSeqs, function(r) { 560 if( ! this.allRefs[r.name] ) 561 this.refSeqOrder.push(r.name); 562 this.allRefs[r.name] = r; 563 if( refCookie && r.name.toLowerCase() == refCookie.toLowerCase() ) { 564 this.refSeq = r; 565 } 566 },this); 567 this.refSeqOrder = this.refSeqOrder.sort(); 568 this.refSeq = this.refSeq || refSeqs[0]; 569 }; 570 571 /** 572 * @private 573 */ 574 575 576 Browser.prototype.onFineMove = function(startbp, endbp) { 577 578 if( this.locationTrap ) { 579 var length = this.view.ref.end - this.view.ref.start; 580 var trapLeft = Math.round((((startbp - this.view.ref.start) / length) 581 * this.view.overviewBox.w) + this.view.overviewBox.l); 582 var trapRight = Math.round((((endbp - this.view.ref.start) / length) 583 * this.view.overviewBox.w) + this.view.overviewBox.l); 584 585 var locationTrapStyle = dojo.isIE 586 ? "top: " + this.view.overviewBox.t + "px;" 587 + "height: " + this.view.overviewBox.h + "px;" 588 + "left: " + trapLeft + "px;" 589 + "width: " + (trapRight - trapLeft) + "px;" 590 + "border-width: 0px" 591 : "top: " + this.view.overviewBox.t + "px;" 592 + "height: " + this.view.overviewBox.h + "px;" 593 + "left: " + this.view.overviewBox.l + "px;" 594 + "width: " + (trapRight - trapLeft) + "px;" 595 + "border-width: " + "0px " 596 + (this.view.overviewBox.w - trapRight) + "px " 597 + this.view.locationTrapHeight + "px " + trapLeft + "px;"; 598 599 this.locationTrap.style.cssText = locationTrapStyle; 600 } 601 }; 602 603 /** 604 * @private 605 */ 606 607 Browser.prototype.createTrackList = function() { 608 609 if( ! this.config.tracks ) 610 this.config.tracks = []; 611 612 // set a default baseUrl in each of the track confs if needed 613 if( this.config.sourceUrl ) { 614 dojo.forEach( this.config.tracks, function(t) { 615 if( ! t.baseUrl ) 616 t.baseUrl = this.config.baseUrl; 617 },this); 618 } 619 620 // find the tracklist class to use 621 var resolved_tl_class = function() { 622 var tl_class = this.config.show_tracklist == 0 ? 'Null' : 623 (this.config.trackSelector||{}).type ? this.config.trackSelector.type : 624 'Simple'; 625 return JBrowse.View.TrackList[tl_class] || eval( tl_class.replace(/[^\.\w\d]/g, '') ); // sanitize tracklist class for a little security 626 }.call(this); 627 if( !resolved_tl_class ) { 628 console.error("configured trackSelector.type "+tl_class+" not found, falling back to JBrowse.View.TrackList.Simple"); 629 resolved_tl_class = JBrowse.View.TrackList.Simple; 630 } 631 632 var trackMeta = new JBrowse.Model.TrackMetaData( 633 dojo.mixin( this.config.trackMetadata || {}, { 634 trackConfigs: this.config.tracks, 635 browser: this, 636 metadataStores: dojo.map( 637 (this.config.trackMetadata||{}).sources || [], 638 function( sourceDef ) { 639 var url = sourceDef.url || 'trackMeta.csv'; 640 var type = sourceDef.type || ( 641 /\.csv$/i.test(url) ? 'csv' : 642 /\.js(on)?$/i.test(url) ? 'json' : 643 'csv' 644 ); 645 var storeClass = sourceDef['class'] 646 || { csv: 'dojox.data.CsvStore', json: 'dojox.data.JsonRestStore' }[type]; 647 if( !storeClass ) { 648 console.error( "No store class found for type '" 649 +type+"', cannot load track metadata from URL "+url); 650 return null; 651 } 652 653 try { eval(storeClass) || dojo.require(storeClass); } 654 catch (x) { console.error('Could not load trackMetaSource class '+storeClass+': ' + x); } 655 656 return new (eval(storeClass))({ url: url }); 657 },this) 658 }) 659 ); 660 661 662 // instantiate the tracklist and the track metadata object 663 this.trackListView = new resolved_tl_class( 664 dojo.mixin( 665 dojo.clone( this.config.trackSelector ) || {}, 666 { 667 trackConfigs: this.config.tracks, 668 browser: this, 669 trackMetaData: trackMeta 670 } 671 ) 672 ); 673 674 // bind the 't' key as a global keyboard shortcut 675 this.setGlobalKeyboardShortcut( 't', this.trackListView, 'toggle' ); 676 677 // listen for track-visibility-changing messages from views 678 this.subscribe( '/jbrowse/v1/v/tracks/hide', this, 'onVisibleTracksChanged' ); 679 this.subscribe( '/jbrowse/v1/v/tracks/show', this, 'onVisibleTracksChanged' ); 680 681 // figure out what initial track list we will use: 682 // from a param passed to our instance, or from a cookie, or 683 // the passed defaults, or the last-resort default of "DNA"? 684 var origTracklist = 685 this.config.forceTracks 686 || this.cookie( "tracks" ) 687 || this.config.defaultTracks 688 || "DNA"; 689 690 this.showTracks( origTracklist ); 691 }; 692 693 694 695 /** 696 * @private 697 */ 698 699 700 Browser.prototype.onVisibleTracksChanged = function() { 701 this.view.updateTrackList(); 702 this.cookie( "tracks", 703 this.visibleTracks().join(','), 704 {expires: 60}); 705 }; 706 707 /** 708 * navigate to a given location 709 * @example 710 * gb=dojo.byId("GenomeBrowser").genomeBrowser 711 * gb.navigateTo("ctgA:100..200") 712 * gb.navigateTo("f14") 713 * @param loc can be either:<br> 714 * <chromosome>:<start> .. <end><br> 715 * <start> .. <end><br> 716 * <center base><br> 717 * <feature name/ID> 718 */ 719 720 Browser.prototype.navigateTo = function(loc) { 721 if (!this.isInitialized) { 722 this.deferredFunctions.push(function() { this.navigateTo(loc); }); 723 return; 724 } 725 726 // if it's a foo:123..456 location, go there 727 var location = Util.parseLocString( loc ); 728 if( location ) { 729 this.navigateToLocation( location ); 730 } 731 // otherwise, if it's just a word, try to figure out what it is 732 else { 733 734 // is it just the name of one of our ref seqs? 735 var ref = Util.matchRefSeqName( loc, this.allRefs ); 736 if( ref ) { 737 // see if we have a stored location for this ref seq in a 738 // cookie, and go there if we do 739 var oldLoc; 740 try { 741 oldLoc = Util.parseLocString( 742 dojo.fromJson( 743 this.cookie("location") 744 )[ref.name] 745 ); 746 oldLoc.ref = ref.name; // force the refseq name; older cookies don't have it 747 } catch (x) {} 748 if( oldLoc ) { 749 this.navigateToLocation( oldLoc ); 750 } else { 751 // if we don't just go to the middle 80% of that refseq 752 this.navigateToLocation({ref: ref.name, start: ref.end*0.1, end: ref.end*0.9 }); 753 } 754 } 755 756 // lastly, try to search our feature names for it 757 this.searchNames( loc ); 758 } 759 }; 760 761 // given an object like { ref: 'foo', start: 2, end: 100 }, set the 762 // browser's view to that location. any of ref, start, or end may be 763 // missing, in which case the function will try set the view to 764 // something that seems intelligent 765 Browser.prototype.navigateToLocation = function( location ) { 766 767 // validate the ref seq we were passed 768 var ref = location.ref ? Util.matchRefSeqName( location.ref, this.allRefs ) 769 : this.refSeq; 770 if( !ref ) 771 return; 772 location.ref = ref.name; 773 774 // clamp the start and end to the size of the ref seq 775 location.start = Math.max( 0, location.start || 0 ); 776 location.end = Math.max( location.start, 777 Math.min( ref.end, location.end || ref.end ) 778 ); 779 780 // if it's the same sequence, just go there 781 if( location.ref == this.refSeq.name) { 782 this.view.setLocation( this.refSeq, 783 location.start, 784 location.end 785 ); 786 } 787 // if different, we need to poke some other things before going there 788 else { 789 // record names of open tracks and re-open on new refseq 790 var curTracks = this.visibleTracks(); 791 792 this.refSeq = this.allRefs[location.ref]; 793 794 this.view.setLocation( this.refSeq, 795 location.start, 796 location.end ); 797 this.showTracks( curTracks ); 798 } 799 800 return; 801 }; 802 803 /** 804 * Given a string name, search for matching feature names and set the 805 * view location to any that match. 806 */ 807 Browser.prototype.searchNames = function( /**String*/ loc ) { 808 var brwsr = this; 809 this.names.exactMatch( loc, function(nameMatches) { 810 var goingTo, 811 i; 812 813 var post1_4 = typeof nameMatches[0][0] == 'string'; 814 815 //first check for exact case match 816 for (i = 0; i < nameMatches.length; i++) { 817 if (nameMatches[i][ post1_4 ? 0 : 1 ] == loc) 818 goingTo = nameMatches[i]; 819 } 820 //if no exact case match, try a case-insentitive match 821 if (!goingTo) { 822 for (i = 0; i < nameMatches.length; i++) { 823 if (nameMatches[i][ post1_4 ? 0 : 1].toLowerCase() == loc.toLowerCase()) 824 goingTo = nameMatches[i]; 825 } 826 } 827 //else just pick a match 828 if (!goingTo) goingTo = nameMatches[0]; 829 var startbp = parseInt( goingTo[ post1_4 ? 4 : 3 ]); 830 var endbp = parseInt( goingTo[ post1_4 ? 5 : 4 ]); 831 var flank = Math.round((endbp - startbp) * .2); 832 //go to location, with some flanking region 833 brwsr.navigateTo( goingTo[ post1_4 ? 3 : 2] 834 + ":" + (startbp - flank) 835 + ".." + (endbp + flank)); 836 brwsr.showTracks(brwsr.names.extra[nameMatches[0][ post1_4 ? 1 : 0 ]]); 837 }); 838 }; 839 840 841 /** 842 * load and display the given tracks 843 * @example 844 * gb=dojo.byId("GenomeBrowser").genomeBrowser 845 * gb.showTracks(["DNA","gene","mRNA","noncodingRNA"]) 846 * @param trackNameList {Array|String} array or comma-separated string 847 * of track names, each of which should correspond to the "label" 848 * element of the track information 849 */ 850 851 Browser.prototype.showTracks = function( trackNames ) { 852 if( !this.isInitialized ) { 853 this.deferredFunctions.push( function() { this.showTracks(trackNames); } ); 854 return; 855 } 856 857 if( typeof trackNames == 'string' ) 858 trackNames = trackNames.split(','); 859 860 if( ! trackNames ) 861 return; 862 863 var trackConfs = dojo.filter( 864 dojo.map( trackNames, function(n) { 865 return this.trackConfigsByName[n]; 866 }, this), 867 function(c) {return c;} // filter out confs that are missing 868 ); 869 870 // publish some events with the tracks to instruct the views to show them. 871 dojo.publish( '/jbrowse/v1/c/tracks/show', [trackConfs] ); 872 dojo.publish( '/jbrowse/v1/n/tracks/visibleChanged' ); 873 }; 874 875 /** 876 * @returns {String} locstring representation of the current location<br> 877 * (suitable for passing to navigateTo) 878 */ 879 880 Browser.prototype.visibleRegion = function() { 881 return Util.assembleLocString({ 882 ref: this.view.ref.name, 883 start: this.view.minVisible(), 884 end: this.view.maxVisible() 885 }); 886 }; 887 888 /** 889 * @returns {Array[String]} of the <b>names</b> of currently-viewed 890 * tracks (suitable for passing to showTracks) 891 */ 892 893 Browser.prototype.visibleTracks = function() { 894 return dojo.map( this.view.visibleTracks(), function(t){ return t.name; } ); 895 }; 896 897 Browser.prototype.makeHelpDialog = function () { 898 899 // make a div containing our help text 900 var browserRoot = this.config.browserRoot || ""; 901 var helpdiv = document.createElement('div'); 902 helpdiv.style.display = 'none'; 903 helpdiv.className = "helpDialog"; 904 helpdiv.innerHTML = '' 905 + '<div class="main" style="float: left; width: 49%;">' 906 907 + '<dl>' 908 + '<dt>Moving</dt>' 909 + '<dd><ul>' 910 + ' <li>Move the view by clicking and dragging in the track area, or by clicking <img height="20px" src="'+browserRoot+'img/slide-left.png"> or <img height="20px" src="'+browserRoot+'img/slide-right.png"> in the navigation bar.</li>' 911 + ' <li>Center the view at a point by clicking on either the track scale bar or overview bar, or by shift-clicking in the track area.</li>' 912 + '</ul></dd>' 913 + '<dt>Zooming</dt>' 914 + '<dd><ul>' 915 + ' <li>Zoom in and out by clicking <img height="20px" src="'+browserRoot+'img/zoom-in-1.png"> or <img height="20px" src="'+browserRoot+'img/zoom-out-1.png"> in the navigation bar.</li>' 916 + ' <li>Select a region and zoom to it ("rubber-band" zoom) by clicking and dragging in the overview or track scale bar, or shift-clicking and dragging in the track area.</li>' 917 + ' </ul>' 918 + '</dd>' 919 + '<dt>Selecting Tracks</dt>' 920 + '<dd><ul><li>Turn a track off by dragging its track label from the "Available Tracks" area into the track area.</li>' 921 + ' <li>Turn a track on by dragging its track label from the track area back into the "Available Tracks" area.</li>' 922 + ' </ul>' 923 + '</dd>' 924 + '</dl>' 925 + '</div>' 926 927 + '<div class="main" style="float: right; width: 49%;">' 928 + '<dl>' 929 + '<dt>Searching</dt>' 930 + '<dd><ul>' 931 + ' <li>Jump to a feature or reference sequence by typing its name in the search box and pressing Enter.</li>' 932 + ' <li>Jump to a specific region by typing the region into the search box as: <span class="example">ref:start..end</span>.</li>' 933 + ' </ul>' 934 + '</dd>' 935 + '<dt>Example Searches</dt>' 936 + '<dd>' 937 + ' <dl class="searchexample">' 938 + ' <dt>uc0031k.2</dt><dd>jumps to the feature named <span class="example">uc0031k.2</span>.</dd>' 939 + ' <dt>chr4</dt><dd>jumps to chromosome 4</dd>' 940 + ' <dt>chr4:79,500,000..80,000,000</dt><dd>jumps the region on chromosome 4 between 79.5Mb and 80Mb.</dd>' 941 + ' </dl>' 942 + '</dd>' 943 + '<dt>JBrowse Configuration</dt>' 944 + '<dd><ul><li><a target="_blank" href="docs/tutorial/">Quick-start tutorial</a></li>' 945 + ' <li><a target="_blank" href="http://gmod.org/wiki/JBrowse">JBrowse wiki</a></li>' 946 + ' <li><a target="_blank" href="docs/config.html">Configuration reference</a></li>' 947 + ' <li><a target="_blank" href="docs/featureglyphs.html">Feature glyph reference</a></li>' 948 + ' </ul>' 949 + '</dd>' 950 + '</dl>' 951 + '</div>' 952 ; 953 this.container.appendChild( helpdiv ); 954 955 var dialog = new dijit.Dialog({ 956 "class": 'help_dialog', 957 refocus: false, 958 draggable: false, 959 title: "JBrowse Help" 960 }, helpdiv ); 961 962 // make a Help link that will show the dialog and set a handler on it 963 var helplink = document.createElement('a'); 964 helplink.className = 'topLink'; 965 helplink.title = 'Help'; 966 helplink.style.cursor = 'help'; 967 helplink.appendChild( document.createTextNode('Help')); 968 dojo.connect(helplink, 'onclick', function() { dialog.show(); }); 969 970 this.setGlobalKeyboardShortcut( '?', dialog, 'show' ); 971 dojo.connect( document.body, 'onkeydown', function(evt) { 972 if( evt.keyCode != dojo.keys.SHIFT && evt.keyCode != dojo.keys.CTRL && evt.keyCode != dojo.keys.ALT ) 973 dialog.hide(); 974 }); 975 976 return helplink; 977 }; 978 979 /** 980 * Create a global keyboard shortcut. 981 * @param keychar the character of the key that is typed 982 * @param [...] additional arguments passed to dojo.hitch for making the handler 983 */ 984 Browser.prototype.setGlobalKeyboardShortcut = function( keychar ) { 985 // warn if redefining 986 if( this.globalKeyboardShortcuts[ keychar ] ) 987 console.warn("WARNING: JBrowse global keyboard shortcut '"+keychar+"' redefined"); 988 989 // make the wrapped handler func 990 var func = dojo.hitch.apply( dojo, Array.prototype.slice.call( arguments, 1 ) ); 991 992 // remember it 993 this.globalKeyboardShortcuts[ keychar ] = func; 994 }; 995 996 /** 997 * Key event handler that implements all global keyboard shortcuts. 998 */ 999 Browser.prototype.globalKeyHandler = function( evt ) { 1000 var shortcut = this.globalKeyboardShortcuts[ evt.keyChar ]; 1001 if( shortcut ) { 1002 shortcut.call( this ); 1003 evt.stopPropagation(); 1004 } 1005 }; 1006 1007 Browser.prototype.makeBookmarkLink = function (area) { 1008 // don't make the link if we were explicitly passed a 'bookmark' 1009 // param of 'false' 1010 if( typeof this.config.bookmark != 'undefined' && !this.config.bookmark ) 1011 return null; 1012 1013 // if a function was not passed, make a default bookmarking function 1014 if( typeof this.config.bookmark != 'function' ) 1015 this.config.bookmark = function( browser_obj ) { 1016 return "".concat( 1017 window.location.protocol, 1018 "//", 1019 window.location.host, 1020 window.location.pathname, 1021 "?", 1022 dojo.objectToQuery({ 1023 loc: browser_obj.visibleRegion(), 1024 tracks: browser_obj.visibleTracks().join(','), 1025 data: browser_obj.config.queryParams.data 1026 }) 1027 ); 1028 }; 1029 1030 // make the bookmark link 1031 var fullview = this.config.show_nav == 0 || this.config.show_tracklist == 0 || this.config.show_overview == 0; 1032 this.link = document.createElement("a"); 1033 this.link.className = "topLink"; 1034 this.link.href = window.location.href; 1035 if( fullview ) 1036 this.link.target = "_blank"; 1037 this.link.title = fullview ? "View in full browser" : "Bookmarkable link to this view"; 1038 this.link.appendChild( document.createTextNode( fullview ? "Full view" : "Bookmark" ) ); 1039 1040 // connect moving events to update it 1041 var update_bookmark = function() { 1042 this.link.href = this.config.bookmark.call( this, this ); 1043 }; 1044 dojo.connect( this, "onCoarseMove", update_bookmark ); 1045 dojo.connect( this, 'onVisibleTracksChanged', update_bookmark ); 1046 1047 return this.link; 1048 }; 1049 1050 /** 1051 * @private 1052 */ 1053 1054 Browser.prototype.onCoarseMove = function(startbp, endbp) { 1055 var length = this.view.ref.end - this.view.ref.start; 1056 var trapLeft = Math.round((((startbp - this.view.ref.start) / length) 1057 * this.view.overviewBox.w) + this.view.overviewBox.l); 1058 var trapRight = Math.round((((endbp - this.view.ref.start) / length) 1059 * this.view.overviewBox.w) + this.view.overviewBox.l); 1060 1061 this.view.locationThumb.style.cssText = 1062 "height: " + (this.view.overviewBox.h - 4) + "px; " 1063 + "left: " + trapLeft + "px; " 1064 + "width: " + (trapRight - trapLeft) + "px;" 1065 + "z-index: 20"; 1066 1067 //since this method gets triggered by the initial GenomeView.sizeInit, 1068 //we don't want to save whatever location we happen to start at 1069 if (! this.isInitialized) return; 1070 var locString = Util.assembleLocString({ start: startbp, end: endbp, ref: this.refSeq.name }); 1071 if( this.locationBox ) { 1072 this.locationBox.set('value',locString,false); 1073 this.goButton.set('disabled',true); 1074 } 1075 1076 // update the location and refseq cookies 1077 var oldLocMap = dojo.fromJson( this.cookie('location') ) || {}; 1078 oldLocMap[this.refSeq.name] = locString; 1079 this.cookie( 'location', dojo.toJson(oldLocMap), {expires: 60}); 1080 this.cookie( 'refseq', this.refSeq.name ); 1081 1082 document.title = locString; 1083 }; 1084 1085 /** 1086 * Wrapper for dojo.cookie that namespaces our cookie names by 1087 * prefixing them with this.config.containerID. 1088 * 1089 * Has one additional bit of smarts: if an object or array is passed 1090 * instead of a string to set as the cookie contents, will serialize 1091 * it with dojo.toJson before storing. 1092 * 1093 * @param [...] same as dojo.cookie 1094 * @returns the new value of the cookie, same as dojo.cookie 1095 */ 1096 Browser.prototype.cookie = function() { 1097 arguments[0] = this.config.containerID + '-' + arguments[0]; 1098 if( typeof arguments[1] == 'object' ) 1099 arguments[1] = dojo.toJson( arguments[1] ); 1100 return dojo.cookie.apply( dojo.cookie, arguments ); 1101 }; 1102 1103 /** 1104 * @private 1105 */ 1106 1107 Browser.prototype.createNavBox = function( parent, locLength ) { 1108 var navbox = document.createElement("div"); 1109 var browserRoot = this.config.browserRoot ? this.config.browserRoot : ""; 1110 navbox.id = "navbox"; 1111 parent.appendChild(navbox); 1112 navbox.style.cssText = "text-align: center; z-index: 10;"; 1113 1114 var four_nbsp = String.fromCharCode(160); four_nbsp = four_nbsp + four_nbsp + four_nbsp + four_nbsp; 1115 navbox.appendChild(document.createTextNode( four_nbsp )); 1116 1117 var moveLeft = document.createElement("input"); 1118 moveLeft.type = "image"; 1119 moveLeft.src = browserRoot + "img/slide-left.png"; 1120 moveLeft.id = "moveLeft"; 1121 moveLeft.className = "icon nav"; 1122 moveLeft.style.height = "40px"; 1123 navbox.appendChild(moveLeft); 1124 dojo.connect( moveLeft, "click", this, 1125 function(event) { 1126 dojo.stopEvent(event); 1127 this.view.slide(0.9); 1128 }); 1129 1130 var moveRight = document.createElement("input"); 1131 moveRight.type = "image"; 1132 moveRight.src = browserRoot + "img/slide-right.png"; 1133 moveRight.id="moveRight"; 1134 moveRight.className = "icon nav"; 1135 moveRight.style.height = "40px"; 1136 navbox.appendChild(moveRight); 1137 dojo.connect( moveRight, "click", this, 1138 function(event) { 1139 dojo.stopEvent(event); 1140 this.view.slide(-0.9); 1141 }); 1142 1143 navbox.appendChild(document.createTextNode( four_nbsp )); 1144 1145 var bigZoomOut = document.createElement("input"); 1146 bigZoomOut.type = "image"; 1147 bigZoomOut.src = browserRoot + "img/zoom-out-2.png"; 1148 bigZoomOut.id = "bigZoomOut"; 1149 bigZoomOut.className = "icon nav"; 1150 bigZoomOut.style.height = "40px"; 1151 navbox.appendChild(bigZoomOut); 1152 dojo.connect( bigZoomOut, "click", this, 1153 function(event) { 1154 dojo.stopEvent(event); 1155 this.view.zoomOut(undefined, undefined, 2); 1156 }); 1157 1158 1159 var zoomOut = document.createElement("input"); 1160 zoomOut.type = "image"; 1161 zoomOut.src = browserRoot + "img/zoom-out-1.png"; 1162 zoomOut.id = "zoomOut"; 1163 zoomOut.className = "icon nav"; 1164 zoomOut.style.height = "40px"; 1165 navbox.appendChild(zoomOut); 1166 dojo.connect( zoomOut, "click", this, 1167 function(event) { 1168 dojo.stopEvent(event); 1169 this.view.zoomOut(); 1170 }); 1171 1172 var zoomIn = document.createElement("input"); 1173 zoomIn.type = "image"; 1174 zoomIn.src = browserRoot + "img/zoom-in-1.png"; 1175 zoomIn.id = "zoomIn"; 1176 zoomIn.className = "icon nav"; 1177 zoomIn.style.height = "40px"; 1178 navbox.appendChild(zoomIn); 1179 dojo.connect( zoomIn, "click", this, 1180 function(event) { 1181 dojo.stopEvent(event); 1182 this.view.zoomIn(); 1183 }); 1184 1185 var bigZoomIn = document.createElement("input"); 1186 bigZoomIn.type = "image"; 1187 bigZoomIn.src = browserRoot + "img/zoom-in-2.png"; 1188 bigZoomIn.id = "bigZoomIn"; 1189 bigZoomIn.className = "icon nav"; 1190 bigZoomIn.style.height = "40px"; 1191 navbox.appendChild(bigZoomIn); 1192 dojo.connect( bigZoomIn, "click", this, 1193 function(event) { 1194 dojo.stopEvent(event); 1195 this.view.zoomIn(undefined, undefined, 2); 1196 }); 1197 1198 navbox.appendChild(document.createTextNode( four_nbsp )); 1199 1200 // make the location box 1201 dojo.require('dijit.form.ComboBox'); 1202 this.locationBox = new dijit.form.ComboBox( 1203 { 1204 id: "location", 1205 name: "location", 1206 store: this._makeLocationAutocompleteStore(), 1207 searchAttr: "name" 1208 }, 1209 dojo.create('input',{ size: locLength },navbox) ); 1210 dojo.connect( this.locationBox.focusNode, "keydown", this, function(event) { 1211 if (event.keyCode == dojo.keys.ENTER) { 1212 this.locationBox.closeDropDown(false); 1213 this.navigateTo( this.locationBox.get('value') ); 1214 this.goButton.set('disabled',true); 1215 dojo.stopEvent(event); 1216 } else { 1217 this.goButton.set('disabled', false); 1218 } 1219 }); 1220 dojo.connect( navbox, 'onselectstart', function(evt) { evt.stopPropagation(); return true; }); 1221 // monkey-patch the combobox code to make a few modifications 1222 (function(){ 1223 1224 // add a moreMatches class to our hacked-in "more options" option 1225 var dropDownProto = eval(this.locationBox.dropDownClass).prototype; 1226 var oldCreateOption = dropDownProto._createOption; 1227 dropDownProto._createOption = function( item ) { 1228 var option = oldCreateOption.apply( this, arguments ); 1229 if( item.hitLimit ) 1230 dojo.addClass( option, 'moreMatches'); 1231 return option; 1232 }; 1233 1234 // prevent the "more matches" option from being clicked 1235 var oldSetValue = dropDownProto._setValueAttr; 1236 dropDownProto._setValueAttr = function( value ) { 1237 console.log( value.target.item ); 1238 if( value.target && value.target.item && value.target.item.hitLimit ) 1239 return null; 1240 return oldSetValue.apply( this, arguments ); 1241 }; 1242 }).call(this); 1243 1244 // make the 'Go' button' 1245 dojo.require('dijit.form.Button'); 1246 this.goButton = new dijit.form.Button( 1247 { 1248 label: 'Go', 1249 onClick: dojo.hitch( this, function(event) { 1250 this.navigateTo(this.locationBox.get('value')); 1251 this.goButton.set('disabled',true); 1252 dojo.stopEvent(event); 1253 }) 1254 }, dojo.create('button',{},navbox)); 1255 1256 return navbox; 1257 }; 1258 1259 Browser.prototype._makeLocationAutocompleteStore = function() { 1260 var conf = this.config.autocomplete||{}; 1261 return new JBrowse.Model.AutocompleteStore({ 1262 namesTrie: this.names, 1263 stopPrefixes: conf.stopPrefixes, 1264 resultLimit: conf.resultLimit || 15 1265 }); 1266 }; 1267 1268 /* 1269 1270 Copyright (c) 2007-2009 The Evolutionary Software Foundation 1271 1272 Created by Mitchell Skinner <mitch_skinner@berkeley.edu> 1273 1274 This package and its accompanying libraries are free software; you can 1275 redistribute it and/or modify it under the terms of the LGPL (either 1276 version 2.1, or at your option, any later version) or the Artistic 1277 License 2.0. Refer to LICENSE for the full license text. 1278 1279 */ 1280