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