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