1 // CONTROLLER 2 3 /** 4 * Construct a new Browser object. 5 * @class This class is the main interface between JBrowse and embedders 6 * @constructor 7 * @param params an object with the following properties:<br> 8 * <ul> 9 * <li><code>config</code> - list of objects with "url" property that points to a config JSON file</li> 10 * <li><code>containerID</code> - ID of the HTML element that contains the browser</li> 11 * <li><code>refSeqs</code> - object with "url" property that is the URL to list of reference sequence information items</li> 12 * <li><code>browserRoot</code> - (optional) URL prefix for the browser code</li> 13 * <li><code>tracks</code> - (optional) comma-delimited string containing initial list of tracks to view</li> 14 * <li><code>location</code> - (optional) string describing the initial location</li> 15 * <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> 16 * <li><code>defaultLocation</code> - (optional) string describing the initial location if there are no cookies and no "location" parameter</li> 17 * <li><code>show_nav</code> - (optional) string describing the on/off state of navigation box</li> 18 * <li><code>show_tracklist</code> - (optional) string describing the on/off state of track bar</li> 19 * <li><code>show_overview</code> - (optional) string describing the on/off state of overview</li> 20 * </ul> 21 */ 22 23 24 var Browser = function(params) { 25 dojo.require("dojo.dnd.Source"); 26 dojo.require("dojo.dnd.Moveable"); 27 dojo.require("dojo.dnd.Mover"); 28 dojo.require("dojo.dnd.move"); 29 dojo.require("dijit.layout.ContentPane"); 30 dojo.require("dijit.layout.BorderContainer"); 31 dojo.require("dijit.Dialog"); 32 33 this.deferredFunctions = []; 34 this.tracks = []; 35 this.isInitialized = false; 36 37 this.config = params; 38 39 // load our touch device support 40 // TODO: refactor this 41 this.deferredFunctions.push(function() { loadTouch(); }); 42 43 // schedule the config load, the first step in the initialization 44 // process, to happen when the page is done loading 45 var browser = this; 46 dojo.addOnLoad( function() { browser.loadConfig(); } ); 47 48 dojo.connect( this, 'onConfigLoaded', this, 'loadRefSeqs' ); 49 dojo.connect( this, 'onConfigLoaded', this, 'loadNames' ); 50 dojo.connect( this, 'onRefSeqsLoaded', this, 'initView' ); 51 }; 52 53 /** 54 * Displays links to configuration help in the main window. Called 55 * when the main browser cannot run at all, due to configuration 56 * errors or whatever. 57 */ 58 Browser.prototype.fatalError = function( error ) { 59 if( error ) { 60 error = error+''; 61 if( ! /\.$/.exec(error) ) 62 error = error + '.'; 63 } 64 if( ! this.hasFatalErrors ) { 65 var container = 66 dojo.byId(this.config.containerID || 'GenomeBrowser') 67 || document.body; 68 container.innerHTML = '' 69 + '<div class="fatal_error">' 70 + ' <h1>Congratulations, JBrowse is on the web!</h1>' 71 + " <p>However, JBrowse could not start, either because it has not yet been configured or because of an error.</p>" 72 + " <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>" 73 + " <p>Otherwise, please refer to the following resources for help in getting JBrowse up and running.</p>" 74 + ' <ul><li><a target="_blank" href="docs/tutorial/">Quick-start tutorial</a></li>' 75 + ' <li><a target="_blank" href="http://gmod.org/wiki/JBrowse">JBrowse wiki</a></li>' 76 + ' <li><a target="_blank" href="docs/config.html">Configuration reference</a></li>' 77 + ' <li><a target="_blank" href="docs/featureglyphs.html">Feature glyph reference</a></li>' 78 + ' </ul>' 79 80 + ' <div id="fatal_error_list" class="errors"> <h2>Error message(s):</h2>' 81 + ( error ? '<div class="error"> '+error+'</div>' : '' ) 82 + ' </div>' 83 + '</div>' 84 ; 85 this.hasFatalErrors = true; 86 } else { 87 var errors_div = dojo.byId('fatal_error_list') || document.body; 88 dojo.create('div', { className: 'error', innerHTML: error+'' }, errors_div ); 89 } 90 }; 91 92 Browser.prototype.loadRefSeqs = function() { 93 // load our ref seqs 94 if( typeof this.config.refSeqs == 'string' ) 95 this.config.refSeqs = { url: this.config.refSeqs }; 96 dojo.xhrGet( 97 { 98 url: this.config.refSeqs.url, 99 handleAs: 'json', 100 load: dojo.hitch( this, function(o) { 101 this.addRefseqs(o); 102 this.onRefSeqsLoaded(); 103 }) 104 }); 105 }; 106 107 /** 108 * Event that fires when the reference sequences have been loaded. 109 */ 110 Browser.prototype.onRefSeqsLoaded = function() {}; 111 112 /** 113 * Load our name index. 114 */ 115 Browser.prototype.loadNames = function() { 116 // load our name index 117 if (this.config.nameUrl) 118 this.names = new LazyTrie(this.config.nameUrl, "lazy-{Chunk}.json"); 119 }; 120 121 Browser.prototype.initView = function() { 122 //set up top nav/overview pane and main GenomeView pane 123 dojo.addClass(document.body, "tundra"); 124 this.container = dojo.byId(this.config.containerID); 125 this.container.onselectstart = function() { return false; }; 126 this.container.genomeBrowser = this; 127 var topPane = document.createElement("div"); 128 this.container.appendChild(topPane); 129 130 var overview = document.createElement("div"); 131 overview.className = "overview"; 132 overview.id = "overview"; 133 // overview=0 hides the overview, but we still need it to exist 134 if( this.config.show_overview == 0 ) overview.style.cssText = "display: none"; 135 topPane.appendChild(overview); 136 137 this.navbox = this.createNavBox( topPane, 25 ); 138 139 this.viewElem = document.createElement("div"); 140 this.viewElem.className = "dragWindow"; 141 this.container.appendChild( this.viewElem); 142 143 this.containerWidget = new dijit.layout.BorderContainer({ 144 liveSplitters: false, 145 design: "sidebar", 146 gutters: false 147 }, this.container); 148 var contentWidget = 149 new dijit.layout.ContentPane({region: "top"}, topPane); 150 this.browserWidget = 151 new dijit.layout.ContentPane({region: "center"}, this.viewElem); 152 153 //create location trapezoid 154 this.locationTrap = document.createElement("div"); 155 this.locationTrap.className = "locationTrap"; 156 topPane.appendChild(this.locationTrap); 157 topPane.style.overflow="hidden"; 158 159 // figure out what initial track list we will use: 160 // from a param passed to our instance, or from a cookie, or 161 // the passed defaults, or the last-resort default of "DNA"? 162 this.origTracklist = 163 this.config.forceTracks 164 || dojo.cookie( this.container.id + "-tracks" ) 165 || this.config.defaultTracks 166 || "DNA"; 167 168 // hook up GenomeView 169 this.view = this.viewElem.view = 170 new GenomeView(this.viewElem, 250, this.refSeq, 1/200, 171 this.config.browserRoot); 172 dojo.connect( this.view, "onFineMove", this, "onFineMove" ); 173 dojo.connect( this.view, "onCoarseMove", this, "onCoarseMove" ); 174 175 //set up track list 176 var trackListDiv = this.createTrackList( this.container ); 177 this.containerWidget.startup(); 178 dojo.connect( this.browserWidget, "resize", this, 'onResize' ); 179 dojo.connect( this.browserWidget, "resize", this.view, 'onResize' ); 180 this.onResize(); 181 this.view.onResize(); 182 183 //set initial location 184 var oldLocMap = dojo.fromJson(dojo.cookie(this.container.id + "-location")) || {}; 185 if (this.config.location) { 186 this.navigateTo(this.config.location); 187 } else if (oldLocMap[this.refSeq.name]) { 188 this.navigateTo( oldLocMap[this.refSeq.name] ); 189 } else if (this.config.defaultLocation){ 190 this.navigateTo(this.config.defaultLocation); 191 } else { 192 this.navigateTo( Util.assembleLocString({ 193 ref: this.refSeq.name, 194 start: 0.4 * ( this.refSeq.start + this.refSeq.end ), 195 end: 0.6 * ( this.refSeq.start + this.refSeq.end ) 196 }) 197 ); 198 } 199 200 dojo.connect(this.chromList, "onchange", this, function(event) { 201 var newRef = this.allRefs[this.chromList.options[this.chromList.selectedIndex].value]; 202 this.navigateTo( newRef.name ); 203 }); 204 205 this.isInitialized = true; 206 207 //if someone calls methods on this browser object 208 //before it's fully initialized, then we defer 209 //those functions until now 210 for (var i = 0; i < this.deferredFunctions.length; i++) 211 this.deferredFunctions[i](); 212 this.deferredFunctions = []; 213 }; 214 215 Browser.prototype.onResize = function() { 216 this.view.locationTrapHeight = dojo.marginBox( this.navbox ).h; 217 }; 218 219 /** 220 * Load our configuration file(s) based on the parameters thex 221 * constructor was passed. Does not return until all files are 222 * loaded and merged in. 223 * @returns nothing meaningful 224 */ 225 Browser.prototype.loadConfig = function () { 226 var that = this; 227 228 // coerce include to an array 229 if( typeof this.config.include != 'object' || !this.config.include.length ) 230 this.config.include = [ this.config.include ]; 231 232 // coerce bare strings in the configs to URLs 233 for (var i = 0; i < this.config.include.length; i++) { 234 if( typeof this.config.include[i] == 'string' ) 235 this.config.include[i] = { url: this.config.include[i] }; 236 } 237 238 // fetch and parse all the configuration data 239 var configs_remaining = this.config.include.length; 240 dojo.forEach( this.config.include, function(config) { 241 // include array might have undefined elements in it if 242 // somebody left a trailing comma in and we are running under 243 // IE 244 if( !config ) 245 return; 246 247 // set defaults for format and version 248 if( ! ('format' in config) ) { 249 config.format = 'JB_json'; 250 } 251 if( config.format == 'JB_json' && ! ('version' in config) ) { 252 config.version = 1; 253 } 254 255 // instantiate the adaptor and load the config 256 var adaptor = this.getConfigAdaptor( config ); 257 if( !adaptor ) { 258 this.fatalError( "Could not load config "+config.url+", no configuration adaptor found for config format "+config.format+' version '+config.version ); 259 return; 260 } 261 262 adaptor.load({ 263 config: config, 264 context: this, 265 onSuccess: function( config_data, request_info ) { 266 config.data = config_data; 267 config.loaded = true; 268 if( ! --configs_remaining ) 269 this.onConfigLoaded(); 270 //if you need a backtrace: window.setTimeout( function() { that.onConfigLoaded(); }, 1 ); 271 }, 272 onFailure: function( error ) { 273 config.loaded = false; 274 this.fatalError( error ); 275 if( ! --configs_remaining ) 276 this.onConfigLoaded(); 277 //if you need a backtrace: window.setTimeout( function() { that.onConfigLoaded(); }, 1 ); 278 } 279 }); 280 281 }, this); 282 }; 283 284 Browser.prototype.onConfigLoaded = function() { 285 286 var initial_config = this.config; 287 this.config = {}; 288 289 // load all the configuration data in order 290 dojo.forEach( initial_config.include, function( config ) { 291 if( config.loaded && config.data ) 292 this.addConfigData( config.data ); 293 }, this ); 294 295 // load the initial config (i.e. constructor params) last so that 296 // it overrides the other config 297 this.addConfigData( initial_config ); 298 299 this.validateConfig(); 300 }; 301 302 /** 303 * Examine the loaded and merged configuration for errors. Throws 304 * exceptions if it finds anything amiss. 305 * @returns nothing meaningful 306 */ 307 Browser.prototype.validateConfig = function() { 308 var c = this.config; 309 if( ! c.tracks ) { 310 this.fatalError( 'No tracks defined in configuration' ); 311 } 312 if( ! c.baseUrl ) { 313 this.fatalError( 'Must provide a <code>baseUrl</code> in configuration' ); 314 } 315 if( this.hasFatalErrors ) 316 throw "Errors in configuration, aborting."; 317 }; 318 319 /** 320 * Instantiate the right config adaptor for a given configuration source. 321 * @param {Object} config the configuraiton 322 * @returns {Object} the right configuration adaptor to use, or 323 * undefined if one could not be found 324 */ 325 326 Browser.prototype.getConfigAdaptor = function( config_def ) { 327 var adaptor_name = "ConfigAdaptor." + config_def.format; 328 if( 'version' in config_def ) 329 adaptor_name += '_v'+config_def.version; 330 adaptor_name.replace( /\W/g,'' ); 331 var adaptor_class = eval( adaptor_name ); 332 if( ! adaptor_class ) 333 return undefined; 334 335 return new adaptor_class( config_def ); 336 }; 337 338 /** 339 * Add a function to be executed once JBrowse is initialized 340 * @param f function to be executed 341 */ 342 Browser.prototype.addDeferred = function(f) { 343 if (this.isInitialized) 344 f(); 345 else 346 this.deferredFunctions.push(f); 347 }; 348 349 /** 350 * Merge in some additional configuration data. Properties in the 351 * passed configuration will override those properties in the existing 352 * configuration. 353 */ 354 Browser.prototype.addConfigData = function( /**Object*/ config_data ) { 355 Util.deepUpdate( this.config, config_data ); 356 }; 357 358 /** 359 * @param refSeqs {Array} array of refseq records to add to the browser 360 */ 361 Browser.prototype.addRefseqs = function( refSeqs ) { 362 this.allRefs = this.allRefs || {}; 363 this.refSeq = this.refSeq || refSeqs[0]; 364 dojo.forEach( refSeqs, function(r) { 365 this.allRefs[r.name] = r; 366 },this); 367 }; 368 369 /** 370 * @private 371 */ 372 373 374 Browser.prototype.onFineMove = function(startbp, endbp) { 375 var length = this.view.ref.end - this.view.ref.start; 376 var trapLeft = Math.round((((startbp - this.view.ref.start) / length) 377 * this.view.overviewBox.w) + this.view.overviewBox.l); 378 var trapRight = Math.round((((endbp - this.view.ref.start) / length) 379 * this.view.overviewBox.w) + this.view.overviewBox.l); 380 var locationTrapStyle; 381 if (dojo.isIE) { 382 //IE apparently doesn't like borders thicker than 1024px 383 locationTrapStyle = 384 "top: " + this.view.overviewBox.t + "px;" 385 + "height: " + this.view.overviewBox.h + "px;" 386 + "left: " + trapLeft + "px;" 387 + "width: " + (trapRight - trapLeft) + "px;" 388 + "border-width: 0px"; 389 } else { 390 locationTrapStyle = 391 "top: " + this.view.overviewBox.t + "px;" 392 + "height: " + this.view.overviewBox.h + "px;" 393 + "left: " + this.view.overviewBox.l + "px;" 394 + "width: " + (trapRight - trapLeft) + "px;" 395 + "border-width: " + "0px " 396 + (this.view.overviewBox.w - trapRight) + "px " 397 + this.view.locationTrapHeight + "px " + trapLeft + "px;"; 398 } 399 400 this.locationTrap.style.cssText = locationTrapStyle; 401 }; 402 403 /** 404 * @private 405 */ 406 407 Browser.prototype.createTrackList = function( /**Element*/ parent ) { 408 var leftPane = document.createElement("div"); 409 leftPane.id = "trackPane"; 410 leftPane.style.cssText= this.config.show_tracklist == 0 ? "width: 0": "width: 10em"; 411 parent.appendChild(leftPane); 412 //splitter on left side 413 var leftWidget = new dijit.layout.ContentPane({region: "left", splitter: true}, leftPane); 414 var trackListDiv = document.createElement("div"); 415 trackListDiv.id = "tracksAvail"; 416 trackListDiv.className = "container handles"; 417 trackListDiv.style.cssText = 418 "width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto;"; 419 trackListDiv.innerHTML = "<h2>Available Tracks</h2>"; 420 leftPane.appendChild(trackListDiv); 421 422 var brwsr = this; 423 424 var changeCallback = function() { 425 brwsr.view.showVisibleBlocks(true); 426 }; 427 428 var trackListCreate = function( trackConfig, hint ) { 429 var node = document.createElement("div"); 430 node.className = "tracklist-label"; 431 node.title = "to turn on, drag into track area"; 432 node.innerHTML = trackConfig.key; 433 //in the list, wrap the list item in a container for 434 //border drag-insertion-point monkeying 435 if ("avatar" != hint) { 436 var container = document.createElement("div"); 437 container.className = "tracklist-container"; 438 container.appendChild(node); 439 node = container; 440 } 441 node.id = dojo.dnd.getUniqueId(); 442 return {node: node, data: trackConfig, type: ["track"]}; 443 }; 444 this.trackListWidget = new dojo.dnd.Source(trackListDiv, 445 {creator: trackListCreate, 446 accept: ["track"], // accepts tracks into left div 447 withHandles: false}); 448 449 // instantiate our track objects 450 if( this.config.tracks ) { 451 if( this.config.sourceUrl ) { 452 for (var i = 0; i < this.config.tracks.length; i++) 453 if( ! this.config.tracks[i].baseUrl ) 454 this.config.tracks[i].baseUrl = this.config.baseUrl; 455 } 456 this.trackListWidget.insertNodes(false, this.config.tracks); 457 this.showTracks(this.origTracklist); 458 } 459 460 var trackCreate = /**@inner*/ function( trackConfig, hint) { 461 var node; 462 if ("avatar" == hint) { 463 return trackListCreate( trackConfig, hint); 464 } else { 465 var klass = eval( trackConfig.type); 466 var newTrack = new klass( trackConfig, brwsr.refSeq, 467 { 468 changeCallback: changeCallback, 469 trackPadding: brwsr.view.trackPadding, 470 charWidth: brwsr.view.charWidth, 471 seqHeight: brwsr.view.seqHeight 472 }); 473 node = brwsr.view.addTrack(newTrack); 474 } 475 return {node: node, data: trackConfig, type: ["track"]}; 476 }; 477 478 479 this.viewDndWidget = new dojo.dnd.Source(this.view.trackContainer, 480 { 481 creator: trackCreate, 482 accept: ["track"], //accepts tracks into the viewing field 483 withHandles: true 484 }); 485 dojo.subscribe("/dnd/drop", function(source,nodes,iscopy){ 486 brwsr.onVisibleTracksChanged(); 487 //multi-select too confusing? 488 //brwsr.viewDndWidget.selectNone(); 489 }); 490 491 return trackListDiv; 492 }; 493 494 /** 495 * @private 496 */ 497 498 499 Browser.prototype.onVisibleTracksChanged = function() { 500 this.view.updateTrackList(); 501 var trackLabels = dojo.map(this.view.tracks, 502 function( trackConfig ) { return trackConfig.name; }); 503 dojo.cookie(this.container.id + "-tracks", 504 trackLabels.join(","), 505 {expires: 60}); 506 this.view.showVisibleBlocks(); 507 }; 508 509 /** 510 * navigate to a given location 511 * @example 512 * gb=dojo.byId("GenomeBrowser").genomeBrowser 513 * gb.navigateTo("ctgA:100..200") 514 * gb.navigateTo("f14") 515 * @param loc can be either:<br> 516 * <chromosome>:<start> .. <end><br> 517 * <start> .. <end><br> 518 * <center base><br> 519 * <feature name/ID> 520 */ 521 522 Browser.prototype.navigateTo = function(loc) { 523 if (!this.isInitialized) { 524 var brwsr = this; 525 this.deferredFunctions.push(function() { brwsr.navigateTo(loc); }); 526 return; 527 } 528 529 // if it's a foo:123..456 location, go there 530 var location = Util.parseLocString( loc ); 531 if( location ) { 532 this.navigateToLocation( location ); 533 } 534 // otherwise, if it's just a word, try to figure out what it is 535 else { 536 537 // is it just the name of one of our ref seqs? 538 var ref = Util.matchRefSeqName( loc, this.allRefs ); 539 if( ref ) { 540 // see if we have a stored location for this ref seq in a 541 // cookie, and go there if we do 542 try { 543 var oldLoc = Util.parseLocString( 544 dojo.fromJson( 545 dojo.cookie(brwsr.container.id + "-location") 546 )[ref.name] 547 ); 548 oldLoc.ref = ref.name; // force the refseq name; older cookies don't have it 549 this.navigateToLocation( oldLoc ); 550 } 551 // if we don't just go to the middle 80% of that refseq 552 catch(x) { 553 this.navigateToLocation({ref: ref.name, start: ref.end*0.1, end: ref.end*0.9 }); 554 } 555 } 556 557 // lastly, try to search our feature names for it 558 this.searchNames( loc ); 559 } 560 }; 561 562 // given an object like { ref: 'foo', start: 2, end: 100 }, set the 563 // browser's view to that location. any of ref, start, or end may be 564 // missing, in which case the function will try set the view to 565 // something that seems intelligent 566 Browser.prototype.navigateToLocation = function( location ) { 567 568 // validate the ref seq we were passed 569 var ref = location.ref ? Util.matchRefSeqName( location.ref, this.allRefs ) 570 : this.refSeq; 571 if( !ref ) 572 return; 573 location.ref = ref.name; 574 575 // clamp the start and end to the size of the ref seq 576 location.start = Math.max( 0, location.start || 0 ); 577 location.end = Math.max( location.start, 578 Math.min( ref.end, location.end || ref.end ) 579 ); 580 581 // if it's the same sequence, just go there 582 if( location.ref == this.refSeq.name) { 583 this.view.setLocation( this.refSeq, 584 location.start, 585 location.end 586 ); 587 } 588 // if different, we need to poke some other things before going there 589 else { 590 // record open tracks and re-open on new refseq 591 var curTracks = []; 592 this.viewDndWidget.forInItems(function(obj, id, map) { 593 curTracks.push(obj.data); 594 }); 595 596 for (var i = 0; i < this.chromList.options.length; i++) 597 if (this.chromList.options[i].text == location.ref ) 598 this.chromList.selectedIndex = i; 599 600 this.refSeq = this.allRefs[location.ref]; 601 602 this.view.setLocation( this.refSeq, 603 location.start, 604 location.end ); 605 606 this.viewDndWidget.insertNodes( false, curTracks ); 607 this.onVisibleTracksChanged(); 608 } 609 610 return; 611 //this.view.centerAtBase( location.end ); 612 }; 613 614 // given a string name, search for matching feature names and set the 615 // view location to any that match 616 Browser.prototype.searchNames = function( loc ) { 617 var brwsr = this; 618 this.names.exactMatch( loc, function(nameMatches) { 619 var goingTo; 620 //first check for exact case match 621 for (var i = 0; i < nameMatches.length; i++) { 622 if (nameMatches[i][1] == loc) 623 goingTo = nameMatches[i]; 624 } 625 //if no exact case match, try a case-insentitive match 626 if (!goingTo) { 627 for (var i = 0; i < nameMatches.length; i++) { 628 if (nameMatches[i][1].toLowerCase() == loc.toLowerCase()) 629 goingTo = nameMatches[i]; 630 } 631 } 632 //else just pick a match 633 if (!goingTo) goingTo = nameMatches[0]; 634 var startbp = parseInt(goingTo[3]); 635 var endbp = parseInt(goingTo[4]); 636 var flank = Math.round((endbp - startbp) * .2); 637 //go to location, with some flanking region 638 brwsr.navigateTo(goingTo[2] 639 + ":" + (startbp - flank) 640 + ".." + (endbp + flank)); 641 brwsr.showTracks(brwsr.names.extra[nameMatches[0][0]]); 642 }); 643 }; 644 645 646 /** 647 * load and display the given tracks 648 * @example 649 * gb=dojo.byId("GenomeBrowser").genomeBrowser 650 * gb.showTracks("DNA,gene,mRNA,noncodingRNA") 651 * @param trackNameList {String} comma-delimited string containing track names, 652 * each of which should correspond to the "label" element of the track 653 * information dictionaries 654 */ 655 656 Browser.prototype.showTracks = function(trackNameList) { 657 if (!this.isInitialized) { 658 var brwsr = this; 659 this.deferredFunctions.push( 660 function() { brwsr.showTracks(trackNameList); } 661 ); 662 return; 663 } 664 665 var trackNames = trackNameList.split(","); 666 var removeFromList = []; 667 var brwsr = this; 668 for (var n = 0; n < trackNames.length; n++) { 669 this.trackListWidget.forInItems(function(obj, id, map) { 670 if (trackNames[n] == obj.data.label) { 671 brwsr.viewDndWidget.insertNodes(false, [obj.data]); 672 removeFromList.push(id); 673 } 674 675 }); 676 } 677 var movedNode; 678 for (var i = 0; i < removeFromList.length; i++) { 679 this.trackListWidget.delItem(removeFromList[i]); 680 movedNode = dojo.byId(removeFromList[i]); 681 movedNode.parentNode.removeChild(movedNode); 682 } 683 this.onVisibleTracksChanged(); 684 }; 685 686 /** 687 * @returns {String} locstring representation of the current location<br> 688 * (suitable for passing to navigateTo) 689 */ 690 691 Browser.prototype.visibleRegion = function() { 692 return Util.assembleLocString({ 693 ref: this.view.ref.name, 694 start: this.view.minVisible(), 695 end: this.view.maxVisible() 696 }); 697 }; 698 699 /** 700 * @returns {String} containing comma-separated list of currently-viewed tracks<br> 701 * (suitable for passing to showTracks) 702 */ 703 704 Browser.prototype.visibleTracks = function() { 705 var trackLabels = dojo.map( this.view.tracks, 706 function( trackConfig ) { return trackConfig.name; }); 707 return trackLabels.join(","); 708 }; 709 710 Browser.prototype.makeHelpDialog = function () { 711 712 // make a div containing our help text 713 var browserRoot = this.config.browserRoot || ""; 714 var helpdiv = document.createElement('div'); 715 helpdiv.style.display = 'none'; 716 helpdiv.className = "helpDialog"; 717 helpdiv.innerHTML = '' 718 + '<div class="main" style="float: left">' 719 720 + '<dl>' 721 + '<dt>Moving</dt>' 722 + '<dd><ul>' 723 + ' <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>' 724 + ' <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>' 725 + '</ul></dd>' 726 + '<dt>Zooming</dt>' 727 + '<dd><ul>' 728 + ' <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>' 729 + ' <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>' 730 + ' </ul>' 731 + '</dd>' 732 + '<dt>Selecting Tracks</dt>' 733 + '<dd><ul><li>Turn a track off by dragging its track label from the "Available Tracks" area into the track area.</li>' 734 + ' <li>Turn a track on by dragging its track label from the track area back into the "Available Tracks" area.</li>' 735 + ' </ul>' 736 + '</dd>' 737 + '</dl>' 738 + '</div>' 739 740 + '<div class="main" style="float: right">' 741 + '<dl>' 742 + '<dt>Searching</dt>' 743 + '<dd><ul>' 744 + ' <li>Jump to a feature or reference sequence by typing its name in the search box and pressing Enter.</li>' 745 + ' <li>Jump to a specific region by typing the region into the search box as: <span class="example">ref:start..end</span>.</li>' 746 + ' </ul>' 747 + '</dd>' 748 + '<dt>Example Searches</dt>' 749 + '<dd>' 750 + ' <dl class="searchexample">' 751 + ' <dt>uc0031k.2</dt><dd>jumps to the feature named <span class="example">uc0031k.2</span>.</dd>' 752 + ' <dt>chr4</dt><dd>jumps to chromosome 4</dd>' 753 + ' <dt>chr4:79,500,000..80,000,000</dt><dd>jumps the region on chromosome 4 between 79.5Mb and 80Mb.</dd>' 754 + ' </dl>' 755 + '</dd>' 756 + '<dt>JBrowse Configuration</dt>' 757 + '<dd><ul><li><a target="_blank" href="docs/tutorial/">Quick-start tutorial</a></li>' 758 + ' <li><a target="_blank" href="http://gmod.org/wiki/JBrowse">JBrowse wiki</a></li>' 759 + ' <li><a target="_blank" href="docs/config.html">Configuration reference</a></li>' 760 + ' <li><a target="_blank" href="docs/featureglyphs.html">Feature glyph reference</a></li>' 761 + ' </ul>' 762 + '</dd>' 763 + '</dl>' 764 + '</div>' 765 ; 766 this.container.appendChild( helpdiv ); 767 768 var dialog = new dijit.Dialog({ 769 id: "help_dialog", 770 refocus: false, 771 draggable: false, 772 title: "JBrowse Help" 773 }, helpdiv ); 774 775 // make a Help link that will show the dialog and set a handler on it 776 var helplink = document.createElement('a'); 777 helplink.className = 'topLink'; 778 helplink.title = 'Help'; 779 helplink.style.cursor = 'help'; 780 helplink.appendChild( document.createTextNode('Help')); 781 dojo.connect(helplink, 'onclick', function() { dialog.show(); }); 782 dojo.connect(document.body, 'onkeydown', function( evt ) { 783 if( evt.keyCode != dojo.keys.SHIFT && evt.keyCode != dojo.keys.CTRL && evt.keyCode != dojo.keys.ALT ) 784 dialog.hide(); 785 }); 786 dojo.connect(document.body, 'onkeypress', function( evt ) { 787 if( evt.keyChar == '?' ) 788 dialog.show(); 789 else if( evt.keyCode != dojo.keys.SHIFT && evt.keyCode != dojo.keys.CTRL && evt.keyCode != dojo.keys.ALT ) 790 dialog.hide(); 791 }); 792 793 return helplink; 794 }; 795 796 797 Browser.prototype.makeBookmarkLink = function (area) { 798 // don't make the link if we were explicitly passed a 'bookmark' 799 // param of 'false' 800 if( typeof this.config.bookmark != 'undefined' && !this.config.bookmark ) 801 return null; 802 803 // if a function was not passed, make a default bookmarking function 804 if( typeof this.config.bookmark != 'function' ) 805 this.config.bookmark = function( browser_obj ) { 806 return "".concat( 807 window.location.protocol, 808 "//", 809 window.location.host, 810 window.location.pathname, 811 "?", 812 dojo.objectToQuery({ 813 loc: browser_obj.visibleRegion(), 814 tracks: browser_obj.visibleTracks(), 815 data: browser_obj.config.queryParams.data 816 }) 817 ); 818 }; 819 820 // make the bookmark link 821 var fullview = this.config.show_nav == 0 || this.config.show_tracklist == 0 || this.config.show_overview == 0; 822 this.link = document.createElement("a"); 823 this.link.className = "topLink"; 824 this.link.href = window.location.href; 825 if( fullview ) 826 this.link.target = "_blank"; 827 this.link.title = fullview ? "View in full browser" : "Bookmarkable link to this view"; 828 this.link.appendChild( document.createTextNode( fullview ? "Full view" : "Bookmark" ) ); 829 830 // connect moving events to update it 831 var update_bookmark = function() { 832 this.link.href = this.config.bookmark.call( this, this ); 833 }; 834 dojo.connect( this, "onCoarseMove", update_bookmark ); 835 dojo.connect( this, "onVisibleTracksChanged", update_bookmark ); 836 837 return this.link; 838 }; 839 840 /** 841 * @private 842 */ 843 844 Browser.prototype.onCoarseMove = function(startbp, endbp) { 845 var length = this.view.ref.end - this.view.ref.start; 846 var trapLeft = Math.round((((startbp - this.view.ref.start) / length) 847 * this.view.overviewBox.w) + this.view.overviewBox.l); 848 var trapRight = Math.round((((endbp - this.view.ref.start) / length) 849 * this.view.overviewBox.w) + this.view.overviewBox.l); 850 851 this.view.locationThumb.style.cssText = 852 "height: " + (this.view.overviewBox.h - 4) + "px; " 853 + "left: " + trapLeft + "px; " 854 + "width: " + (trapRight - trapLeft) + "px;" 855 + "z-index: 20"; 856 857 //since this method gets triggered by the initial GenomeView.sizeInit, 858 //we don't want to save whatever location we happen to start at 859 if (! this.isInitialized) return; 860 var locString = Util.assembleLocString({ start: startbp, end: endbp, ref: this.refSeq.name }); 861 this.locationBox.value = locString; 862 this.goButton.disabled = true; 863 this.locationBox.blur(); 864 865 // update the location cookie 866 var ckname = this.container.id + "-location"; 867 var oldLocMap = dojo.fromJson( dojo.cookie(ckname ) ) || {}; 868 oldLocMap[this.refSeq.name] = locString; 869 dojo.cookie( ckname, dojo.toJson(oldLocMap), {expires: 60}); 870 871 document.title = locString; 872 }; 873 874 875 876 /** 877 * @private 878 */ 879 880 Browser.prototype.createNavBox = function( parent, locLength ) { 881 var brwsr = this; 882 var navbox = document.createElement("div"); 883 var browserRoot = this.config.browserRoot ? this.config.browserRoot : ""; 884 navbox.id = "navbox"; 885 parent.appendChild(navbox); 886 navbox.style.cssText = "text-align: center; z-index: 10;"; 887 888 var linkContainer = document.createElement('div'); 889 dojo.create('a', { 890 className: 'powered_by', 891 innerHTML: 'JBrowse', 892 href: 'http://jbrowse.org', 893 title: 'powered by JBrowse' 894 }, linkContainer ); 895 linkContainer.className = 'topLink'; 896 linkContainer.appendChild( this.makeBookmarkLink() ); 897 if( this.config.show_nav != 0 ) 898 linkContainer.appendChild( this.makeHelpDialog() ); 899 900 this.container.appendChild( linkContainer ); 901 902 var moveLeft = document.createElement("input"); 903 moveLeft.type = "image"; 904 moveLeft.src = browserRoot + "img/slide-left.png"; 905 moveLeft.id = "moveLeft"; 906 moveLeft.className = "icon nav"; 907 moveLeft.style.height = "40px"; 908 if( this.config.show_nav != 0 ) { 909 dojo.connect(moveLeft, "click", 910 function(event) { 911 dojo.stopEvent(event); 912 brwsr.view.slide(0.9); 913 }); 914 } 915 916 var moveRight = document.createElement("input"); 917 moveRight.type = "image"; 918 moveRight.src = browserRoot + "img/slide-right.png"; 919 moveRight.id="moveRight"; 920 moveRight.className = "icon nav"; 921 moveRight.style.height = "40px"; 922 if( this.config.show_nav != 0 ) { 923 dojo.connect(moveRight, "click", 924 function(event) { 925 dojo.stopEvent(event); 926 brwsr.view.slide(-0.9); 927 }); 928 }; 929 930 var bigZoomOut = document.createElement("input"); 931 bigZoomOut.type = "image"; 932 bigZoomOut.src = browserRoot + "img/zoom-out-2.png"; 933 bigZoomOut.id = "bigZoomOut"; 934 bigZoomOut.className = "icon nav"; 935 bigZoomOut.style.height = "40px"; 936 if( this.config.show_nav != 0 ) { 937 dojo.connect(bigZoomOut, "click", 938 function(event) { 939 dojo.stopEvent(event); 940 brwsr.view.zoomOut(undefined, undefined, 2); 941 }); 942 } 943 944 var zoomOut = document.createElement("input"); 945 zoomOut.type = "image"; 946 zoomOut.src = browserRoot + "img/zoom-out-1.png"; 947 zoomOut.id = "zoomOut"; 948 zoomOut.className = "icon nav"; 949 zoomOut.style.height = "40px"; 950 if( this.config.show_nav != 0 ) { 951 dojo.connect(zoomOut, "click", 952 function(event) { 953 dojo.stopEvent(event); 954 brwsr.view.zoomOut(); 955 }); 956 } 957 958 var zoomIn = document.createElement("input"); 959 zoomIn.type = "image"; 960 zoomIn.src = browserRoot + "img/zoom-in-1.png"; 961 zoomIn.id = "zoomIn"; 962 zoomIn.className = "icon nav"; 963 zoomIn.style.height = "40px"; 964 if( this.config.show_nav != 0 ) { 965 dojo.connect(zoomIn, "click", 966 function(event) { 967 dojo.stopEvent(event); 968 brwsr.view.zoomIn(); 969 }); 970 } 971 972 var bigZoomIn = document.createElement("input"); 973 bigZoomIn.type = "image"; 974 bigZoomIn.src = browserRoot + "img/zoom-in-2.png"; 975 bigZoomIn.id = "bigZoomIn"; 976 bigZoomIn.className = "icon nav"; 977 bigZoomIn.style.height = "40px"; 978 if( this.config.show_nav != 0 ) { 979 dojo.connect(bigZoomIn, "click", 980 function(event) { 981 dojo.stopEvent(event); 982 brwsr.view.zoomIn(undefined, undefined, 2); 983 }); 984 }; 985 986 this.chromList = document.createElement("select"); 987 this.chromList.id="chrom"; 988 var refCookie = dojo.cookie(this.config.containerID + "-refseq"); 989 var i = 0; 990 var refnames = []; 991 for ( var name in this.allRefs ) { 992 if( this.allRefs.hasOwnProperty(name) ) 993 refnames.push( name ); 994 } 995 refnames = refnames.sort(); 996 dojo.forEach( refnames, function(name) { 997 this.chromList.add( new Option( name, name) ); 998 if ( name.toUpperCase() == String(refCookie).toUpperCase()) { 999 this.refSeq = this.allRefs[name]; 1000 this.chromList.selectedIndex = i; 1001 } 1002 i++; 1003 }, this ); 1004 1005 this.locationBox = document.createElement("input"); 1006 this.locationBox.size=locLength; 1007 this.locationBox.type="text"; 1008 this.locationBox.id="location"; 1009 if( this.config.show_nav != 0 ) { 1010 dojo.connect(this.locationBox, "keydown", function(event) { 1011 if (event.keyCode == dojo.keys.ENTER) { 1012 brwsr.navigateTo(brwsr.locationBox.value); 1013 //brwsr.locationBox.blur(); 1014 brwsr.goButton.disabled = true; 1015 dojo.stopEvent(event); 1016 } else { 1017 brwsr.goButton.disabled = false; 1018 } 1019 }); 1020 } 1021 1022 this.goButton = document.createElement("button"); 1023 this.goButton.appendChild(document.createTextNode("Go")); 1024 this.goButton.disabled = true; 1025 if( this.config.show_nav != 0 ) { 1026 dojo.connect(this.goButton, "click", function(event) { 1027 brwsr.navigateTo(brwsr.locationBox.value); 1028 //brwsr.locationBox.blur(); 1029 brwsr.goButton.disabled = true; 1030 dojo.stopEvent(event); 1031 }); 1032 }; 1033 1034 if( this.config.show_nav != 0 ) { 1035 navbox.appendChild(document.createTextNode("\u00a0\u00a0\u00a0\u00a0")); 1036 navbox.appendChild(moveLeft); 1037 navbox.appendChild(moveRight); 1038 navbox.appendChild(document.createTextNode("\u00a0\u00a0\u00a0\u00a0")); 1039 navbox.appendChild(bigZoomOut); 1040 navbox.appendChild(zoomOut); 1041 navbox.appendChild(zoomIn); 1042 navbox.appendChild(bigZoomIn); 1043 navbox.appendChild(document.createTextNode("\u00a0\u00a0\u00a0\u00a0")); 1044 navbox.appendChild(this.chromList); 1045 navbox.appendChild(this.locationBox); 1046 navbox.appendChild(this.goButton); 1047 }; 1048 1049 return navbox; 1050 }; 1051 1052 /* 1053 1054 Copyright (c) 2007-2009 The Evolutionary Software Foundation 1055 1056 Created by Mitchell Skinner <mitch_skinner@berkeley.edu> 1057 1058 This package and its accompanying libraries are free software; you can 1059 redistribute it and/or modify it under the terms of the LGPL (either 1060 version 2.1, or at your option, any later version) or the Artistic 1061 License 2.0. Refer to LICENSE for the full license text. 1062 1063 */ 1064