1 /**
  2  * Main view class, shows a scrollable, horizontal view of annotation
  3  * tracks.  NOTE: All coordinates are interbase.
  4  * @class
  5  * @constructor
  6  */
  7 function GenomeView( browser, elem, stripeWidth, refseq, zoomLevel, browserRoot) {
  8 
  9     // keep a reference to the main browser object
 10     this.browser = browser;
 11 
 12     var seqCharSize = this.calculateSequenceCharacterSize( elem );
 13     this.charWidth = seqCharSize.width;
 14     this.seqHeight = seqCharSize.height;
 15 
 16     this.posHeight = this.calculatePositionLabelHeight( elem );
 17     // Add an arbitrary 50% padding between the position labels and the
 18     // topmost track
 19     this.topSpace = 1.5 * this.posHeight;
 20 
 21     //the reference sequence
 22     this.ref = refseq;
 23     //current scale, in pixels per bp
 24     this.pxPerBp = zoomLevel;
 25     //path prefix for static assets (e.g., cursors)
 26     this.browserRoot = browserRoot ? browserRoot : "";
 27     //width, in pixels, of the vertical stripes
 28     this.stripeWidth = stripeWidth;
 29     //the page element that the GenomeView lives in
 30     this.elem = elem;
 31 
 32     // the scrollContainer is the element that changes position
 33     // when the user scrolls
 34     this.scrollContainer = dojo.create(
 35         'div', {
 36             id: 'container',
 37             style: { position: 'absolute',
 38                      left: '0px',
 39                      top: '0px'
 40                    }
 41         }, elem
 42     );
 43 
 44     this._renderVerticalScrollBar();
 45 
 46     // we have a separate zoomContainer as a child of the scrollContainer.
 47     // they used to be the same element, but making zoomContainer separate
 48     // enables it to be narrower than this.elem.
 49     this.zoomContainer = document.createElement("div");
 50     this.zoomContainer.id = "zoomContainer";
 51     this.zoomContainer.style.cssText =
 52         "position: absolute; left: 0px; top: 0px; height: 100%;";
 53     this.scrollContainer.appendChild(this.zoomContainer);
 54 
 55     this.outerTrackContainer = document.createElement("div");
 56     this.outerTrackContainer.className = "trackContainer outerTrackContainer";
 57     this.outerTrackContainer.style.cssText = "height: 100%;";
 58     this.zoomContainer.appendChild( this.outerTrackContainer );
 59 
 60     this.trackContainer = document.createElement("div");
 61     this.trackContainer.className = "trackContainer innerTrackContainer draggable";
 62     this.trackContainer.style.cssText = "height: 100%;";
 63     this.outerTrackContainer.appendChild( this.trackContainer );
 64 
 65     //width, in pixels of the "regular" (not min or max zoom) stripe
 66     this.regularStripe = stripeWidth;
 67     //width, in pixels, of stripes at full zoom (based on the sequence
 68     //character width)
 69     //The number of characters per stripe is somewhat arbitrarily set
 70     //at stripeWidth / 10
 71     this.fullZoomStripe = this.charWidth * (stripeWidth / 10);
 72 
 73     this.overview = dojo.byId("overview");
 74     this.overviewBox = dojo.coords(this.overview);
 75 
 76     this.tracks = [];
 77     this.uiTracks = [];
 78     this.trackIndices = {};
 79 
 80     //set up size state (zoom levels, stripe percentage, etc.)
 81     this.sizeInit();
 82 
 83     //distance, in pixels, from the beginning of the reference sequence
 84     //to the beginning of the first active stripe
 85     //  should always be a multiple of stripeWidth
 86     this.offset = 0;
 87     //largest value for the sum of this.offset and this.getX()
 88     //this prevents us from scrolling off the right end of the ref seq
 89     this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth();
 90     //smallest value for the sum of this.offset and this.getX()
 91     //this prevents us from scrolling off the left end of the ref seq
 92     this.minLeft = this.bpToPx(this.ref.start);
 93     //distance, in pixels, between each track
 94     this.trackPadding = 20;
 95     //extra margin to draw around the visible area, in multiples of the visible area
 96     //0: draw only the visible area; 0.1: draw an extra 10% around the visible area, etc.
 97     this.drawMargin = 0.2;
 98     //slide distance (pixels) * slideTimeMultiple + 200 = milliseconds for slide
 99     //1=1 pixel per millisecond average slide speed, larger numbers are slower
100     this.slideTimeMultiple = 0.8;
101     this.trackHeights = [];
102     this.trackTops = [];
103     this.trackLabels = [];
104     this.waitElems = dojo.filter( [ dojo.byId("moveLeft"), dojo.byId("moveRight"),
105                                     dojo.byId("zoomIn"), dojo.byId("zoomOut"),
106                                     dojo.byId("bigZoomIn"), dojo.byId("bigZoomOut"),
107                                     document.body, elem ],
108                                   function(e) { return e; }
109                                 );
110     this.prevCursors = [];
111     this.locationThumb = document.createElement("div");
112     this.locationThumb.className = "locationThumb";
113     this.overview.appendChild(this.locationThumb);
114     this.locationThumbMover = new dojo.dnd.move.parentConstrainedMoveable(this.locationThumb, {area: "margin", within: true});
115 
116     if ( dojo.isIE ) {
117         // if using IE, we have to do scrolling with CSS
118         this.x = -parseInt( this.scrollContainer.style.left );
119         this.y = -parseInt( this.scrollContainer.style.top );
120         this.rawSetX = function(x) {
121             this.scrollContainer.style.left = -x + "px";
122             this.x = x;
123         };
124         this.rawSetY = function(y) {
125             this.scrollContainer.style.top = -y + "px";
126             this.y = y;
127         };
128     } else {
129 	this.x = this.elem.scrollLeft;
130 	this.y = this.elem.scrollTop;
131         this.rawSetX = function(x) {
132             this.elem.scrollLeft = x;
133             this.x = x;
134         };
135         this.rawSetY = function(y) {
136             this.elem.scrollTop = y;
137             this.y = y;
138         };
139     }
140 
141     var scaleTrackDiv = document.createElement("div");
142     scaleTrackDiv.className = "track static_track rubberBandAvailable";
143     scaleTrackDiv.style.height = this.posHeight + "px";
144     scaleTrackDiv.id = "static_track";
145 
146     this.scaleTrackDiv = scaleTrackDiv;
147     this.staticTrack = new StaticTrack("static_track", "pos-label", this.posHeight);
148     this.staticTrack.setViewInfo(function(height) {}, this.stripeCount,
149                                  this.scaleTrackDiv, undefined, this.stripePercent,
150                                  this.stripeWidth, this.pxPerBp,
151                                  this.trackPadding);
152     this.zoomContainer.appendChild(this.scaleTrackDiv);
153     this.waitElems.push(this.scaleTrackDiv);
154 
155     var gridTrackDiv = document.createElement("div");
156     gridTrackDiv.className = "track";
157     gridTrackDiv.style.cssText = "top: 0px; height: 100%;";
158     gridTrackDiv.id = "gridtrack";
159     var gridTrack = new GridTrack("gridtrack");
160     gridTrack.setViewInfo(function(height) {}, this.stripeCount,
161                           gridTrackDiv, undefined, this.stripePercent,
162                           this.stripeWidth, this.pxPerBp,
163                           this.trackPadding);
164     this.trackContainer.appendChild(gridTrackDiv);
165     this.uiTracks = [this.staticTrack, gridTrack];
166 
167     // accept tracks being dragged into this
168     this.trackDndWidget =
169         new dojo.dnd.Source(
170             this.trackContainer,
171             {
172                 accept: ["track"], //accepts only tracks into the viewing field
173                 withHandles: true,
174                 creator: dojo.hitch( this, function( trackConfig, hint ) {
175                     return {
176                         data: trackConfig,
177                         type: ["track"],
178                         node: hint == 'avatar'
179                                  ? dojo.create('div', { innerHTML: trackConfig.key || trackConfig.label, className: 'track-label dragging' })
180                                  : this.renderTrack( trackConfig )
181                     };
182                 })
183             });
184 
185     // subscribe to showTracks commands
186     this.browser.subscribe( '/dnd/drop', this, function( source, nodes, copy, target ) {
187         this.updateTrackList();
188         if( target.node === this.trackContainer ) {
189             // if dragging into the trackcontainer, we are showing some tracks
190             // get the configs from the tracks being dragged in
191             var confs = dojo.filter( dojo.map( nodes, function(n) {
192                                          return n.track && n.track.config;
193                                      }),
194                                      function(c) {return c;}
195                                    );
196             this.browser.publish( '/jbrowse/v1/v/tracks/show', [confs] );
197         }
198     });
199     this.browser.subscribe( '/jbrowse/v1/c/tracks/show', this, 'showTracks' );
200     this.browser.subscribe( '/jbrowse/v1/c/tracks/hide', this, 'hideTracks' );
201 
202     // render our UI tracks (horizontal scale tracks, grid lines, and so forth)
203     dojo.forEach(this.uiTracks, function(track) {
204         track.showRange(0, this.stripeCount - 1,
205                         Math.round(this.pxToBp(this.offset)),
206                         Math.round(this.stripeWidth / this.pxPerBp),
207                         this.pxPerBp);
208     }, this);
209 
210     this.zoomContainer.style.paddingTop = this.topSpace + "px";
211 
212     this.addOverviewTrack(new StaticTrack("overview_loc_track", "overview-pos", this.overviewPosHeight));
213     this.showFine();
214     this.showCoarse();
215 
216     // initialize the behavior manager used for setting what this view
217     // does (i.e. the behavior it has) for mouse and keyboard events
218     this.behaviorManager = new BehaviorManager({ context: this, behaviors: this._behaviors() });
219     this.behaviorManager.initialize();
220 };
221 
222 /**
223  * Create and place the elements for the vertical scrollbar.
224  * @private
225  */
226 GenomeView.prototype._renderVerticalScrollBar = function() {
227     var container = dojo.create(
228         'div',
229         {
230             className: 'vertical_scrollbar',
231             style: { position: 'fixed',
232                      right: '0px',
233                      bottom: '0px',
234                      height: '100%',
235                      zIndex: 1000
236                    }
237         },
238         this.elem
239     );
240 
241     var positionMarker = dojo.create(
242         'div',
243         {
244             className: 'vertical_position_marker',
245             style: {
246                 position: 'absolute',
247                 height: '100%'
248             }
249         },
250         container
251     );
252     this.verticalScrollBar = { container: container, positionMarker: positionMarker };
253 };
254 
255 /**
256  * Update the position and look of the vertical scroll bar as our
257  * y-scroll offset changes.
258  * @private
259  */
260 GenomeView.prototype._updateVerticalScrollBar = function( newDims ) {
261     if( typeof newDims.height == 'number' ) {
262         var heightAdjust = this.staticTrack ? -this.staticTrack.div.offsetHeight : 0;
263         var trackPaneHeight = newDims.height + heightAdjust;
264         this.verticalScrollBar.container.style.height = trackPaneHeight+'px';
265         var markerHeight = newDims.height / this.containerHeight * 100;
266         this.verticalScrollBar.positionMarker.style.height = markerHeight > 0.5 ? markerHeight+'%' :  '1px';
267         this.verticalScrollBar.container.style.display = newDims.height / this.containerHeight > 0.98 ? 'none' : 'block';
268     }
269 
270     if( typeof newDims.y == 'number' || typeof newDims.height == 'number' ) {
271         this.verticalScrollBar.positionMarker.style.top    = ( (newDims.y || this.getY()) / this.containerHeight * 100 )+'%';
272     }
273 
274 };
275 
276 /**
277  * @returns {Array[Track]} of the tracks that are currently visible in
278  * this genomeview
279  */
280 GenomeView.prototype.visibleTracks = function() {
281     return this.tracks;
282 };
283 
284 /**
285  * Behaviors (event handler bundles) for various states that the
286  * GenomeView might be in.
287  * @private
288  * @returns {Object} description of behaviors
289  */
290 GenomeView.prototype._behaviors = function() { return {
291 
292     // behaviors that don't change
293     always: {
294         apply_on_init: true,
295         apply: function() {
296             var handles = [];
297             this.overviewTrackIterate( function(t) {
298                 handles.push( dojo.connect(
299                     t.div, 'mousedown', dojo.hitch( this, 'startRubberZoom', this.overview_absXtoBp, t.div )
300                 ));
301             });
302             handles.push(
303                 dojo.connect( this.scrollContainer,     "mousewheel",     this, 'wheelScroll', false ),
304                 dojo.connect( this.scrollContainer,     "DOMMouseScroll", this, 'wheelScroll', false ),
305 
306                 dojo.connect( this.scaleTrackDiv,       "mousedown",      dojo.hitch( this, 'startRubberZoom', this.absXtoBp, this.scrollContainer )),
307 
308                 dojo.connect( this.outerTrackContainer, "dblclick",       this, 'doubleClickZoom'    ),
309 
310                 dojo.connect( this.locationThumbMover,  "onMoveStop",     this, 'thumbMoved'         ),
311 
312                 dojo.connect( this.overview,            "onclick",        this, 'overviewClicked'    ),
313                 dojo.connect( this.scaleTrackDiv,       "onclick",        this, 'scaleClicked'       ),
314 
315                 // when the mouse leaves the document, need to cancel
316                 // any keyboard-modifier-holding-down state
317                 dojo.connect( document.body,            'onmouseleave',       this, function() {
318                     this.behaviorManager.swapBehaviors('shiftMouse','normalMouse');
319                 }),
320 
321                 // when the mouse leaves the document, need to cancel
322                 // any keyboard-modifier-holding-down state
323                 dojo.connect( document.body,            'onmouseenter',       this, function(evt) {
324                     if( evt.shiftKey )
325                         this.behaviorManager.swapBehaviors( 'normalMouse', 'shiftMouse' );
326                 }),
327 
328                 dojo.connect( window, 'onkeyup', this, function(evt) {
329                     if( evt.keyCode == dojo.keys.SHIFT ) // shift
330                         this.behaviorManager.swapBehaviors( 'shiftMouse', 'normalMouse' );
331                 }),
332                 dojo.connect( window, 'onkeydown', this, function(evt) {
333                     if( evt.keyCode == dojo.keys.SHIFT ) // shift
334                         this.behaviorManager.swapBehaviors( 'normalMouse', 'shiftMouse' );
335                 })
336             );
337             return handles;
338         }
339     },
340 
341     // mouse events connected for "normal" behavior
342     normalMouse: {
343         apply_on_init: true,
344         apply: function() {
345             return [
346                 dojo.connect( this.outerTrackContainer, "mousedown", this, 'startMouseDragScroll' )
347             ];
348         }
349     },
350 
351     // mouse events connected when the shift button is being held down
352     shiftMouse: {
353         apply: function() {
354             dojo.removeClass(this.trackContainer,'draggable');
355             dojo.addClass(this.trackContainer,'rubberBandAvailable');
356             return [
357                 dojo.connect( this.outerTrackContainer, "mousedown", dojo.hitch( this, 'startRubberZoom', this.absXtoBp, this.scrollContainer )),
358                 dojo.connect( this.outerTrackContainer, "onclick",   this, 'scaleClicked'    )
359             ];
360         },
361         remove: function( mgr, handles ) {
362             dojo.forEach( handles, dojo.disconnect, dojo );
363             dojo.removeClass(this.trackContainer,'rubberBandAvailable');
364             dojo.addClass(this.trackContainer,'draggable');
365         }
366     },
367 
368     // mouse events that are connected when we are in the middle of a
369     // drag-scrolling operation
370     mouseDragScrolling: {
371         apply: function() {
372             return [
373                 dojo.connect(document.body, "mouseup",   this, 'dragEnd'      ),
374                 dojo.connect(document.body, "mousemove", this, 'dragMove'     ),
375                 dojo.connect(document.body, "mouseout",  this, 'checkDragOut' )
376             ];
377         }
378     },
379 
380     // mouse events that are connected when we are in the middle of a
381     // rubber-band zooming operation
382     mouseRubberBandZooming: {
383         apply: function() {
384             return [
385                 dojo.connect(document.body, "mouseup",    this, 'rubberExecute'  ),
386                 dojo.connect(document.body, "mousemove",  this, 'rubberMove'     ),
387                 dojo.connect(document.body, "mouseout",   this, 'rubberCancel'   ),
388                 dojo.connect(window,        "onkeydown",  this, 'rubberCancel'   )
389             ];
390         }
391     }
392 };};
393 
394 /**
395  * Conducts a test with DOM elements to measure sequence text width
396  * and height.
397  */
398 GenomeView.prototype.calculateSequenceCharacterSize = function( containerElement ) {
399     var widthTest = document.createElement("div");
400     widthTest.className = "sequence";
401     widthTest.style.visibility = "hidden";
402     var widthText = "12345678901234567890123456789012345678901234567890";
403     widthTest.appendChild(document.createTextNode(widthText));
404     containerElement.appendChild(widthTest);
405 
406     var result = {
407         width:  widthTest.clientWidth / widthText.length,
408         height: widthTest.clientHeight
409     };
410 
411     containerElement.removeChild(widthTest);
412     return result;
413 };
414 
415 /**
416  * Conduct a DOM test to calculate the height of div.pos-label
417  * elements with a line of text in them.
418  */
419 GenomeView.prototype.calculatePositionLabelHeight = function( containerElement ) {
420     // measure the height of some arbitrary text in whatever font this
421     // shows up in (set by an external CSS file)
422     var heightTest = document.createElement("div");
423     heightTest.className = "pos-label";
424     heightTest.style.visibility = "hidden";
425     heightTest.appendChild(document.createTextNode("42"));
426     containerElement.appendChild(heightTest);
427     var h = heightTest.clientHeight;
428     containerElement.removeChild(heightTest);
429     return h;
430 };
431 
432 GenomeView.prototype.wheelScroll = function(e) {
433 
434     // 60 pixels per mouse wheel event
435     this.setY( this.getY() - 60 * Util.wheel(e) );
436 
437     //the timeout is so that we don't have to run showVisibleBlocks
438     //for every scroll wheel click (we just wait until so many ms
439     //after the last one).
440     if ( this.wheelScrollTimeout )
441         window.clearTimeout( this.wheelScrollTimeout );
442 
443     // 100 milliseconds since the last scroll event is an arbitrary
444     // cutoff for deciding when the user is done scrolling
445     // (set by a bit of experimentation)
446     this.wheelScrollTimeout = window.setTimeout( dojo.hitch( this, function() {
447         this.showVisibleBlocks(true);
448         this.wheelScrollTimeout = null;
449     }, 100));
450 
451     dojo.stopEvent(e);
452 };
453 
454 GenomeView.prototype.getX = function() {
455     return this.x;
456 };
457 
458 GenomeView.prototype.getY = function() {
459     return this.y;
460 };
461 GenomeView.prototype.getHeight = function() {
462     return this.elem.offsetHeight;
463 };
464 GenomeView.prototype.getWidth = function() {
465     return this.elem.offsetWidth;
466 };
467 
468 GenomeView.prototype.clampX = function(x) {
469     return Math.round( Math.max( Math.min( this.maxLeft - this.offset, x || 0),
470                                  this.minLeft - this.offset
471                                )
472                      );
473 };
474 
475 GenomeView.prototype.clampY = function(y) {
476     return Math.round( Math.min( Math.max( 0, y || 0 ),
477                                  this.containerHeight- this.getHeight()
478                                )
479                      );
480 };
481 
482 /**
483  * @returns the new x value that was set
484  */
485 GenomeView.prototype.setX = function(x) {
486     x = this.clampX(x);
487     this.rawSetX( x );
488     this.updateStaticElements( { x: x } );
489     this.showFine();
490     return x;
491 };
492 
493 /**
494  * @returns the new y value that was set
495  */
496 GenomeView.prototype.setY = function(y) {
497     y = this.clampY(y);
498     this.rawSetY(y);
499     this.updateStaticElements( { y: y } );
500     return y;
501 };
502 
503 /**
504  * @private
505  */
506 GenomeView.prototype.rawSetPosition = function(pos) {
507     this.rawSetX( pos.x );
508     this.rawSetY( pos.y );
509     return pos;
510 };
511 
512 /**
513  * @param pos.x new x position
514  * @param pos.y new y position
515  */
516 GenomeView.prototype.setPosition = function(pos) {
517     var x = this.clampX( pos.x );
518     var y = this.clampY( pos.y );
519     this.updateStaticElements( {x: x, y: y} );
520     this.rawSetX( x );
521     this.rawSetY( y );
522     this.showFine();
523 };
524 
525 /**
526  * @returns {Object} as <code>{ x: 123, y: 456 }</code>
527  */
528 GenomeView.prototype.getPosition = function() {
529     return { x: this.x, y: this.y };
530 };
531 
532 GenomeView.prototype.zoomCallback = function() {
533     this.zoomUpdate();
534 };
535 
536 GenomeView.prototype.afterSlide = function() {
537     this.showCoarse();
538     this.scrollUpdate();
539     this.showVisibleBlocks(true);
540 };
541 
542 GenomeView.prototype.doubleClickZoom = function(event) {
543     if( this.dragging ) return;
544     if( "animation" in this ) return;
545 
546     // if we have a timeout in flight from a scaleClicked click,
547     // cancel it, cause it looks now like the user has actually
548     // double-clicked
549     if( this.scaleClickedTimeout ) window.clearTimeout( this.scaleClickedTimeout );
550 
551     var zoomLoc = (event.pageX - dojo.coords(this.elem, true).x) / this.getWidth();
552     if (event.shiftKey) {
553 	this.zoomOut(event, zoomLoc, 2);
554     } else {
555 	this.zoomIn(event, zoomLoc, 2);
556     }
557     dojo.stopEvent(event);
558 };
559 
560 /** @private */
561 GenomeView.prototype._beforeMouseDrag = function( event ) {
562     if ( this.animation ) {
563         if (this.animation instanceof Zoomer) {
564             dojo.stopEvent(event);
565             return 0;
566 
567         } else {
568             this.animation.stop();
569         }
570     }
571     if (Util.isRightButton(event)) return 0;
572     dojo.stopEvent(event);
573     return 1;
574 };
575 
576 /**
577  * Event fired when a user's mouse button goes down inside the main
578  * element of the genomeview.
579  */
580 GenomeView.prototype.startMouseDragScroll = function(event) {
581     if( ! this._beforeMouseDrag(event) ) return;
582 
583     this.behaviorManager.applyBehaviors('mouseDragScrolling');
584 
585     this.dragging = true;
586     this.dragStartPos = {x: event.clientX,
587                          y: event.clientY};
588     this.winStartPos = this.getPosition();
589 };
590 
591 /**
592  * Start a rubber-band dynamic zoom.
593  *
594  * @param {Function} absToBp function to convert page X coordinates to
595  *   base pair positions on the reference sequence.  Called in the
596  *   context of the GenomeView object.
597  * @param {HTMLElement} container element in which to draw the
598  *   rubberbanding highlight
599  * @param {Event} event the mouse event that's starting the zoom
600  */
601 GenomeView.prototype.startRubberZoom = function( absToBp, container, event ) {
602     if( ! this._beforeMouseDrag(event) ) return;
603 
604     this.behaviorManager.applyBehaviors('mouseRubberBandZooming');
605 
606     this.rubberbanding = { absFunc: absToBp, container: container };
607     this.rubberbandStartPos = {x: event.clientX,
608                                y: event.clientY};
609     this.winStartPos = this.getPosition();
610 };
611 
612 GenomeView.prototype._rubberStop = function(event) {
613     this.behaviorManager.removeBehaviors('mouseRubberBandZooming');
614     this.hideRubberHighlight();
615     dojo.stopEvent(event);
616 };
617 
618 GenomeView.prototype.rubberCancel = function(event) {
619     var htmlNode = document.body.parentNode;
620     var bodyNode = document.body;
621 
622     if ( !event || !(event.relatedTarget || event.toElement)
623         || (htmlNode === (event.relatedTarget || event.toElement))
624         || (bodyNode === (event.relatedTarget || event.toElement))) {
625         this._rubberStop(event);
626     }
627 };
628 
629 GenomeView.prototype.rubberMove = function(event) {
630     this.setRubberHighlight( this.rubberbandStartPos, { x: event.clientX, y: event.clientY } );
631 };
632 
633 GenomeView.prototype.rubberExecute = function(event) {
634     this._rubberStop(event);
635 
636     var start = this.rubberbandStartPos;
637     var end   = { x: event.clientX, y: event.clientY };
638 
639     // cancel the rubber-zoom if the user has moved less than 3 pixels
640     if( Math.abs( start.x - end.x ) < 3 ) {
641         return this._rubberStop(event);
642     }
643 
644     var h_start_bp = this.rubberbanding.absFunc.call( this, Math.min(start.x,end.x) );
645     var h_end_bp   = this.rubberbanding.absFunc.call( this, Math.max(start.x,end.x) );
646     delete this.rubberbanding;
647     this.setLocation( this.ref, h_start_bp, h_end_bp );
648 };
649 
650 // draws the rubber-banding highlight region from start.x to end.x
651 GenomeView.prototype.setRubberHighlight = function( start, end ) {
652     var container = this.rubberbanding.container,
653         container_coords = dojo.coords(container,true);
654 
655     var h = this.rubberHighlight || (function(){
656         var main = this.rubberHighlight = document.createElement("div");
657         main.className = 'rubber-highlight';
658         main.style.position = 'absolute';
659         main.style.zIndex = 1000;
660         var text = document.createElement('div');
661         text.appendChild( document.createTextNode("Zoom to region") );
662         main.appendChild(text);
663         text.style.position = 'relative';
664         text.style.top = (50-container_coords.y) + "px";
665 
666         container.appendChild( main );
667         return main;
668     }).call(this);
669 
670     h.style.visibility  = 'visible';
671     h.style.left   = Math.min(start.x,end.x) - container_coords.x + 'px';
672     h.style.width  = Math.abs(end.x-start.x) + 'px';
673     //console.log({ left: h.style.left, end: end.x });
674 };
675 
676 GenomeView.prototype.dragEnd = function(event) {
677     this.behaviorManager.removeBehaviors('mouseDragScrolling');
678 
679     this.dragging = false;
680     dojo.stopEvent(event);
681     this.showCoarse();
682 
683     this.scrollUpdate();
684     this.showVisibleBlocks(true);
685 };
686 
687 /** stop the drag if we mouse out of the view */
688 GenomeView.prototype.checkDragOut = function( event ) {
689     var htmlNode = document.body.parentNode;
690     var bodyNode = document.body;
691 
692     if (!(event.relatedTarget || event.toElement)
693         || (htmlNode === (event.relatedTarget || event.toElement))
694         || (bodyNode === (event.relatedTarget || event.toElement))
695        ) {
696            this.dragEnd(event);
697     }
698 };
699 
700 GenomeView.prototype.dragMove = function(event) {
701     this.setPosition({
702     	x: this.winStartPos.x - (event.clientX - this.dragStartPos.x),
703     	y: this.winStartPos.y - (event.clientY - this.dragStartPos.y)
704         });
705     dojo.stopEvent(event);
706 };
707 
708 GenomeView.prototype.hideRubberHighlight = function( start, end ) {
709     if( this.rubberHighlight ) {
710        this.rubberHighlight.parentNode.removeChild( this.rubberHighlight );
711        delete this.rubberHighlight;
712     }
713 };
714 
715 /* moves the view by (distance times the width of the view) pixels */
716 GenomeView.prototype.slide = function(distance) {
717     if (this.animation) this.animation.stop();
718     this.trimVertical();
719     // slide for an amount of time that's a function of the distance being
720     // traveled plus an arbitrary extra 200 milliseconds so that
721     // short slides aren't too fast (200 chosen by experimentation)
722     new Slider(this,
723                this.afterSlide,
724                Math.abs(distance) * this.getWidth() * this.slideTimeMultiple + 200,
725                distance * this.getWidth());
726 };
727 
728 GenomeView.prototype.setLocation = function(refseq, startbp, endbp) {
729     if (startbp === undefined) startbp = this.minVisible();
730     if (endbp === undefined) endbp = this.maxVisible();
731     if ((startbp < refseq.start) || (startbp > refseq.end))
732         startbp = refseq.start;
733     if ((endbp < refseq.start) || (endbp > refseq.end))
734         endbp = refseq.end;
735 
736     if (this.ref != refseq) {
737 	this.ref = refseq;
738 	var removeTrack = function(track) {
739             if (track.div && track.div.parentNode)
740                 track.div.parentNode.removeChild(track.div);
741 	};
742 	dojo.forEach(this.tracks, removeTrack);
743 
744         this.tracks = [];
745         this.trackIndices = {};
746         this.trackHeights = [];
747         this.trackTops = [];
748         this.trackLabels = [];
749 
750         dojo.forEach(this.uiTracks, function(track) { track.clear(); });
751 	this.overviewTrackIterate(removeTrack);
752 
753 	this.addOverviewTrack(new StaticTrack("overview_loc_track", "overview-pos", this.overviewPosHeight));
754         this.sizeInit();
755         this.setY(0);
756         //this.containerHeight = this.topSpace;
757 
758         this.behaviorManager.initialize();
759     }
760 
761     this.pxPerBp = Math.min(this.getWidth() / (endbp - startbp), this.charWidth);
762     this.curZoom = Util.findNearest(this.zoomLevels, this.pxPerBp);
763     if (Math.abs(this.pxPerBp - this.zoomLevels[this.zoomLevels.length - 1]) < 0.2) {
764         //the cookie-saved location is in round bases, so if the saved
765         //location was at the highest zoom level, the new zoom level probably
766         //won't be exactly at the highest zoom (which is necessary to trigger
767         //the sequence track), so we nudge the zoom level to be exactly at
768         //the highest level if it's close.
769         //Exactly how close is arbitrary; 0.2 was chosen to be close
770         //enough that people wouldn't notice if we fudged that much.
771         console.log("nudging zoom level from %d to %d", this.pxPerBp, this.zoomLevels[this.zoomLevels.length - 1]);
772         this.pxPerBp = this.zoomLevels[this.zoomLevels.length - 1];
773     }
774     this.stripeWidth = (this.stripeWidthForZoom(this.curZoom) / this.zoomLevels[this.curZoom]) * this.pxPerBp;
775     this.instantZoomUpdate();
776 
777     this.centerAtBase((startbp + endbp) / 2, true);
778 };
779 
780 GenomeView.prototype.stripeWidthForZoom = function(zoomLevel) {
781     if ((this.zoomLevels.length - 1) == zoomLevel) {
782         return this.fullZoomStripe;
783     } else if (0 == zoomLevel) {
784         return this.minZoomStripe;
785     } else {
786         return this.regularStripe;
787     }
788 };
789 
790 GenomeView.prototype.instantZoomUpdate = function() {
791     this.scrollContainer.style.width =
792         (this.stripeCount * this.stripeWidth) + "px";
793     this.zoomContainer.style.width =
794         (this.stripeCount * this.stripeWidth) + "px";
795     this.maxOffset =
796         this.bpToPx(this.ref.end) - this.stripeCount * this.stripeWidth;
797     this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth();
798     this.minLeft = this.bpToPx(this.ref.start);
799 };
800 
801 GenomeView.prototype.centerAtBase = function(base, instantly) {
802     base = Math.min(Math.max(base, this.ref.start), this.ref.end);
803     if (instantly) {
804 	var pxDist = this.bpToPx(base);
805 	var containerWidth = this.stripeCount * this.stripeWidth;
806 	var stripesLeft = Math.floor((pxDist - (containerWidth / 2)) / this.stripeWidth);
807 	this.offset = stripesLeft * this.stripeWidth;
808 	this.setX(pxDist - this.offset - (this.getWidth() / 2));
809 	this.trackIterate(function(track) { track.clear(); });
810 	this.showVisibleBlocks(true);
811         this.showCoarse();
812     } else {
813 	var startbp = this.pxToBp(this.x + this.offset);
814 	var halfWidth = (this.getWidth() / this.pxPerBp) / 2;
815 	var endbp = startbp + halfWidth + halfWidth;
816 	var center = startbp + halfWidth;
817 	if ((base >= (startbp  - halfWidth))
818 	    && (base <= (endbp + halfWidth))) {
819 	    //we're moving somewhere nearby, so move smoothly
820             if (this.animation) this.animation.stop();
821             var distance = (center - base) * this.pxPerBp;
822 	    this.trimVertical();
823             // slide for an amount of time that's a function of the
824             // distance being traveled plus an arbitrary extra 200
825             // milliseconds so that short slides aren't too fast
826             // (200 chosen by experimentation)
827             new Slider(this, this.afterSlide,
828                        Math.abs(distance) * this.slideTimeMultiple + 200,
829 		       distance);
830 	} else {
831 	    //we're moving far away, move instantly
832 	    this.centerAtBase(base, true);
833 	}
834     }
835 };
836 
837 /**
838  * @returns {Number} minimum basepair coordinate of the current
839  * reference sequence visible in the genome view
840  */
841 GenomeView.prototype.minVisible = function() {
842     var mv = this.pxToBp(this.x + this.offset);
843 
844     // if we are less than one pixel from the beginning of the ref
845     // seq, just say we are at the beginning.
846     if( mv < this.pxToBp(1) )
847         return 0;
848     else
849         return mv;
850 };
851 
852 /**
853  * @returns {Number} maximum basepair coordinate of the current
854  * reference sequence visible in the genome view
855  */
856 GenomeView.prototype.maxVisible = function() {
857     var mv = this.pxToBp(this.x + this.offset + this.getWidth());
858     // if we are less than one pixel from the end of the ref
859     // seq, just say we are at the end.
860     if( mv > this.ref.end - this.pxToBp(1) )
861         return this.ref.end;
862     else
863         return mv;
864 };
865 
866 GenomeView.prototype.showFine = function() {
867     this.onFineMove(this.minVisible(), this.maxVisible());
868 };
869 GenomeView.prototype.showCoarse = function() {
870     this.onCoarseMove(this.minVisible(), this.maxVisible());
871 };
872 
873 /**
874  * Hook for other components to dojo.connect to.
875  */
876 GenomeView.prototype.onFineMove = function( startbp, endbp ) {};
877 /**
878  * Hook for other components to dojo.connect to.
879  */
880 GenomeView.prototype.onCoarseMove = function( startbp, endbp ) {};
881 
882 /**
883  * Hook to be called on a window resize.
884  */
885 GenomeView.prototype.onResize = function() {
886     this.sizeInit();
887     this.showVisibleBlocks();
888     this.showFine();
889     this.showCoarse();
890 };
891 
892 
893 /**
894  * Event handler fired when the overview bar is single-clicked.
895  */
896 GenomeView.prototype.overviewClicked = function( evt ) {
897     this.centerAtBase( this.overview_absXtoBp( evt.clientX ) );
898 };
899 
900 /**
901  * Convert absolute X pixel position to base pair position on the
902  * <b>overview</b> track.  This needs refactoring; a scale bar should
903  * itself know how to convert an absolute X position to base pairs.
904  * @param {Number} x absolute pixel X position (for example, from a click event's clientX property)
905  */
906 GenomeView.prototype.overview_absXtoBp = function(x) {
907     return ( x - this.overviewBox.x ) / this.overviewBox.w * (this.ref.end - this.ref.start) + this.ref.start;
908 };
909 
910 /**
911  * Event handler fired when the track scale bar is single-clicked.
912  */
913 GenomeView.prototype.scaleClicked = function( evt ) {
914     var bp = this.absXtoBp(evt.clientX);
915 
916     this.scaleClickedTimeout = window.setTimeout( dojo.hitch( this, function() {
917         this.centerAtBase( bp );
918     },100));
919 };
920 
921 /**
922  * Event handler fired when the region thumbnail in the overview bar
923  * is dragged.
924  */
925 GenomeView.prototype.thumbMoved = function(mover) {
926     var pxLeft = parseInt(this.locationThumb.style.left);
927     var pxWidth = parseInt(this.locationThumb.style.width);
928     var pxCenter = pxLeft + (pxWidth / 2);
929     this.centerAtBase(((pxCenter / this.overviewBox.w) * (this.ref.end - this.ref.start)) + this.ref.start);
930 };
931 
932 GenomeView.prototype.checkY = function(y) {
933     return Math.min((y < 0 ? 0 : y), this.containerHeight - this.getHeight());
934 };
935 
936 /**
937  * Given a new X and Y pixels position for the main track container,
938  * reposition static elements that "float" over it, like track labels,
939  * Y axis labels, the main track ruler, and so on.
940  *
941  * @param [args.x] the new X coordinate.  if not provided,
942  *   elements that only need updates on the X position are not
943  *   updated.
944  * @param [args.y] the new Y coordinate.  if not provided,
945  *   elements that only need updates on the Y position are not
946  *   updated.
947  * @param [args.width] the new width of the view.  if not provided,
948  *   elements that only need updates on the width are not
949  *   updated.
950  * @param [args.height] the new height of the view. if not provided,
951  *   elements that only need updates on the height are not
952  *   updated.
953  */
954 GenomeView.prototype.updateStaticElements = function( args ) {
955     this.trackIterate( function(t) {
956         t.updateStaticElements( args );
957     },this);
958 
959     this._updateVerticalScrollBar( args );
960 
961     if( typeof args.x == 'number' ) {
962         dojo.forEach( this.trackLabels, function(l) {
963             l.style.left = args.x+"px";
964         });
965     }
966 
967     if( typeof args.y == 'number' ) {
968         this.staticTrack.div.style.top = args.y + "px";
969     }
970 };
971 
972 GenomeView.prototype.showWait = function() {
973     var oldCursors = [];
974     for (var i = 0; i < this.waitElems.length; i++) {
975         oldCursors[i] = this.waitElems[i].style.cursor;
976         this.waitElems[i].style.cursor = "wait";
977     }
978     this.prevCursors.push(oldCursors);
979 };
980 
981 GenomeView.prototype.showDone = function() {
982     var oldCursors = this.prevCursors.pop();
983     for (var i = 0; i < this.waitElems.length; i++) {
984         this.waitElems[i].style.cursor = oldCursors[i];
985     }
986 };
987 
988 GenomeView.prototype.pxToBp = function(pixels) {
989     return pixels / this.pxPerBp;
990 };
991 
992 /**
993  * Convert absolute pixels X position to base pair position on the
994  * current reference sequence.
995  * @returns {Number}
996  */
997 GenomeView.prototype.absXtoBp = function( /**Number*/ pixels) {
998     return this.pxToBp( this.getPosition().x + this.offset - dojo.coords(this.elem, true).x + pixels );
999 };
1000 
1001 GenomeView.prototype.bpToPx = function(bp) {
1002     return bp * this.pxPerBp;
1003 };
1004 
1005 
1006 /**
1007  * Update the view's state, and that of its tracks, for the current
1008  * width and height of its container.
1009  * @returns nothing
1010  */
1011 GenomeView.prototype.sizeInit = function() {
1012     this.overviewBox = dojo.coords(this.overview);
1013 
1014     //scale values, in pixels per bp, for all zoom levels
1015     this.zoomLevels = [1/500000, 1/200000, 1/100000, 1/50000, 1/20000, 1/10000, 1/5000, 1/2000, 1/1000, 1/500, 1/200, 1/100, 1/50, 1/20, 1/10, 1/5, 1/2, 1, 2, 5, this.charWidth];
1016     //make sure we don't zoom out too far
1017     while (((this.ref.end - this.ref.start) * this.zoomLevels[0])
1018            < this.getWidth()) {
1019         this.zoomLevels.shift();
1020     }
1021     this.zoomLevels.unshift(this.getWidth() / (this.ref.end - this.ref.start));
1022 
1023     //width, in pixels, of stripes at min zoom (so the view covers
1024     //the whole ref seq)
1025     this.minZoomStripe = this.regularStripe * (this.zoomLevels[0] / this.zoomLevels[1]);
1026 
1027     this.curZoom = 0;
1028     while (this.pxPerBp > this.zoomLevels[this.curZoom])
1029         this.curZoom++;
1030     this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth();
1031 
1032     delete this.stripePercent;
1033     //25, 50, 100 don't work as well due to the way scrollUpdate works
1034     var possiblePercents = [20, 10, 5, 4, 2, 1];
1035     for (var i = 0; i < possiblePercents.length; i++) {
1036         // we'll have (100 / possiblePercents[i]) stripes.
1037         // multiplying that number of stripes by the minimum stripe width
1038         // gives us the total width of the "container" div.
1039         // (or what that width would be if we used possiblePercents[i]
1040         // as our stripePercent)
1041         // That width should be wide enough to make sure that the user can
1042         // scroll at least one page-width in either direction without making
1043         // the container div bump into the edge of its parent element, taking
1044         // into account the fact that the container won't always be perfectly
1045         // centered (it may be as much as 1/2 stripe width off center)
1046         // So, (this.getWidth() * 3) gives one screen-width on either side,
1047         // and we add a regularStripe width to handle the slightly off-center
1048         // cases.
1049         // The minimum stripe width is going to be halfway between
1050         // "canonical" zoom levels; the widest distance between those
1051         // zoom levels is 2.5-fold, so halfway between them is 0.7 times
1052         // the stripe width at the higher zoom level
1053         if (((100 / possiblePercents[i]) * (this.regularStripe * 0.7))
1054             > ((this.getWidth() * 3) + this.regularStripe)) {
1055             this.stripePercent = possiblePercents[i];
1056             break;
1057         }
1058     }
1059 
1060     if (this.stripePercent === undefined) {
1061 	console.warn("stripeWidth too small: " + this.stripeWidth + ", " + this.getWidth());
1062 	this.stripePercent = 1;
1063     }
1064 
1065     var oldX;
1066     var oldStripeCount = this.stripeCount;
1067     if (oldStripeCount) oldX = this.getX();
1068     this.stripeCount = Math.round(100 / this.stripePercent);
1069 
1070     this.scrollContainer.style.width =
1071         (this.stripeCount * this.stripeWidth) + "px";
1072     this.zoomContainer.style.width =
1073         (this.stripeCount * this.stripeWidth) + "px";
1074 
1075     var blockDelta = undefined;
1076     if (oldStripeCount && (oldStripeCount != this.stripeCount)) {
1077         blockDelta = Math.floor((oldStripeCount - this.stripeCount) / 2);
1078         var delta = (blockDelta * this.stripeWidth);
1079         var newX = this.getX() - delta;
1080         this.offset += delta;
1081         this.updateStaticElements( { x: newX } );
1082         this.rawSetX(newX);
1083     }
1084 
1085     // update the sizes for each of the tracks
1086     this.trackIterate(function(track, view) {
1087                           track.sizeInit(view.stripeCount,
1088                                          view.stripePercent,
1089                                          blockDelta);
1090                       });
1091 
1092     var newHeight =
1093         this.trackHeights && this.trackHeights.length
1094           ? Math.max(
1095               dojof.reduce( this.trackHeights, '+') + this.trackPadding * this.trackHeights.length,
1096               this.getHeight()
1097             )
1098           : this.getHeight();
1099     this.scrollContainer.style.height = newHeight + "px";
1100     this.containerHeight = newHeight;
1101 
1102     var refLength = this.ref.end - this.ref.start;
1103     var posSize = document.createElement("div");
1104     posSize.className = "overview-pos";
1105     posSize.appendChild(document.createTextNode(Util.addCommas(this.ref.end)));
1106     posSize.style.visibility = "hidden";
1107     this.overview.appendChild(posSize);
1108     // we want the stripes to be at least as wide as the position labels,
1109     // plus an arbitrary 20% padding so it's clear which grid line
1110     // a position label corresponds to.
1111     var minStripe = posSize.clientWidth * 1.2;
1112     this.overviewPosHeight = posSize.clientHeight;
1113     this.overview.removeChild(posSize);
1114     for (var n = 1; n < 30; n++) {
1115 	//http://research.att.com/~njas/sequences/A051109
1116         // JBrowse uses this sequence (1, 2, 5, 10, 20, 50, 100, 200, 500...)
1117         // as its set of zoom levels.  That gives nice round numbers for
1118         // bases per block, and it gives zoom transitions that feel about the
1119         // right size to me. -MS
1120 	this.overviewStripeBases = (Math.pow(n % 3, 2) + 1) * Math.pow(10, Math.floor(n/3));
1121 	this.overviewStripes = Math.ceil(refLength / this.overviewStripeBases);
1122 	if ((this.overviewBox.w / this.overviewStripes) > minStripe) break;
1123 	if (this.overviewStripes < 2) break;
1124     }
1125 
1126     // update our overview tracks
1127     var overviewStripePct = 100 / (refLength / this.overviewStripeBases);
1128     var overviewHeight = 0;
1129     this.overviewTrackIterate(function (track, view) {
1130 	    track.clear();
1131 	    track.sizeInit(view.overviewStripes,
1132 			   overviewStripePct);
1133             track.showRange(0, view.overviewStripes - 1,
1134                             -1, view.overviewStripeBases,
1135                             view.overviewBox.w /
1136                             (view.ref.end - view.ref.start));
1137 	});
1138     this.updateOverviewHeight();
1139 
1140     this.updateScroll();
1141 };
1142 
1143 /**
1144  * @private
1145  */
1146 GenomeView.prototype.updateScroll = function() {
1147 
1148     // may need to update our Y position if our height has changed
1149     var update = { height: this.getHeight() };
1150     if( this.getY() > 0 ) {
1151         if( this.containerHeight - this.getY() < update.height ) {
1152             //console.log( this.totalTrackHeight, update.height, this.getY() );
1153             update.y = this.setY( Math.max( 0, this.containerHeight - update.height ));
1154         }
1155     }
1156 
1157     // update any static (i.e. fixed-position) elements that need to
1158     // float in one position over the scrolling track div (can't use
1159     // CSS position:fixed for these)
1160     this.updateStaticElements( update );
1161 };
1162 
1163 GenomeView.prototype.overviewTrackIterate = function(callback) {
1164     var overviewTrack = this.overview.firstChild;
1165     do {
1166         if (overviewTrack && overviewTrack.track)
1167 	    callback.call( this, overviewTrack.track, this);
1168     } while (overviewTrack && (overviewTrack = overviewTrack.nextSibling));
1169 };
1170 
1171 GenomeView.prototype.updateOverviewHeight = function(trackName, height) {
1172     var overviewHeight = 0;
1173     this.overviewTrackIterate(function (track, view) {
1174 	    overviewHeight += track.height;
1175 	});
1176     this.overview.style.height = overviewHeight + "px";
1177     this.overviewBox = dojo.coords(this.overview);
1178 };
1179 
1180 GenomeView.prototype.addOverviewTrack = function(track) {
1181     var refLength = this.ref.end - this.ref.start;
1182 
1183     var overviewStripePct = 100 / (refLength / this.overviewStripeBases);
1184     var trackDiv = document.createElement("div");
1185     trackDiv.className = "track";
1186     trackDiv.style.height = this.overviewBox.h + "px";
1187     trackDiv.style.left = (((-this.ref.start) / refLength) * this.overviewBox.w) + "px";
1188     trackDiv.id = "overviewtrack_" + track.name;
1189     trackDiv.track = track;
1190     var view = this;
1191     var heightUpdate = function(height) {
1192         view.updateOverviewHeight();
1193     };
1194     track.setViewInfo(heightUpdate, this.overviewStripes, trackDiv,
1195 		      undefined,
1196 		      overviewStripePct,
1197 		      this.overviewStripeBases,
1198                       this.pxPerBp,
1199                       this.trackPadding);
1200     this.overview.appendChild(trackDiv);
1201     this.updateOverviewHeight();
1202 
1203     return trackDiv;
1204 };
1205 
1206 GenomeView.prototype.trimVertical = function(y) {
1207     if (y === undefined) y = this.getY();
1208     var trackBottom;
1209     var trackTop = this.topSpace;
1210     var bottom = y + this.getHeight();
1211     for (var i = 0; i < this.tracks.length; i++) {
1212         if (this.tracks[i].shown) {
1213             trackBottom = trackTop + this.trackHeights[i];
1214             if (!((trackBottom > y) && (trackTop < bottom))) {
1215                 this.tracks[i].hideAll();
1216             }
1217             trackTop = trackBottom + this.trackPadding;
1218         }
1219     }
1220 };
1221 
1222 GenomeView.prototype.zoomIn = function(e, zoomLoc, steps) {
1223     if (this.animation) return;
1224     if (zoomLoc === undefined) zoomLoc = 0.5;
1225     if (steps === undefined) steps = 1;
1226     steps = Math.min(steps, (this.zoomLevels.length - 1) - this.curZoom);
1227     if ((0 == steps) && (this.pxPerBp == this.zoomLevels[this.curZoom]))
1228         return;
1229 
1230     this.showWait();
1231     var pos = this.getPosition();
1232     this.trimVertical(pos.y);
1233 
1234     var scale = this.zoomLevels[this.curZoom + steps] / this.pxPerBp;
1235     var fixedBp = this.pxToBp(pos.x + this.offset + (zoomLoc * this.getWidth()));
1236     this.curZoom += steps;
1237     this.pxPerBp = this.zoomLevels[this.curZoom];
1238     this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth();
1239 
1240     for (var track = 0; track < this.tracks.length; track++)
1241 	this.tracks[track].startZoom(this.pxPerBp,
1242 				     fixedBp - ((zoomLoc * this.getWidth())
1243                                                 / this.pxPerBp),
1244 				     fixedBp + (((1 - zoomLoc) * this.getWidth())
1245                                                 / this.pxPerBp));
1246 	//YAHOO.log("centerBp: " + centerBp + "; estimated post-zoom start base: " + (centerBp - ((zoomLoc * this.getWidth()) / this.pxPerBp)) + ", end base: " + (centerBp + (((1 - zoomLoc) * this.getWidth()) / this.pxPerBp)));
1247 
1248     // Zooms take an arbitrary 700 milliseconds, which feels about right
1249     // to me, although if the zooms were smoother they could probably
1250     // get faster without becoming off-putting. -MS
1251     new Zoomer(scale, this,
1252                function() {this.zoomUpdate(zoomLoc, fixedBp);},
1253                700, zoomLoc);
1254 };
1255 
1256 GenomeView.prototype.zoomOut = function(e, zoomLoc, steps) {
1257     if (this.animation) return;
1258     if (steps === undefined) steps = 1;
1259     steps = Math.min(steps, this.curZoom);
1260     if (0 == steps) return;
1261 
1262     this.showWait();
1263     var pos = this.getPosition();
1264     this.trimVertical(pos.y);
1265     if (zoomLoc === undefined) zoomLoc = 0.5;
1266     var scale = this.zoomLevels[this.curZoom - steps] / this.pxPerBp;
1267     var edgeDist = this.bpToPx(this.ref.end) - (this.offset + pos.x + this.getWidth());
1268         //zoomLoc is a number on [0,1] that indicates
1269         //the fixed point of the zoom
1270     zoomLoc = Math.max(zoomLoc, 1 - (((edgeDist * scale) / (1 - scale)) / this.getWidth()));
1271     edgeDist = pos.x + this.offset - this.bpToPx(this.ref.start);
1272     zoomLoc = Math.min(zoomLoc, ((edgeDist * scale) / (1 - scale)) / this.getWidth());
1273     var fixedBp = this.pxToBp(pos.x + this.offset + (zoomLoc * this.getWidth()));
1274     this.curZoom -= steps;
1275     this.pxPerBp = this.zoomLevels[this.curZoom];
1276 
1277     for (var track = 0; track < this.tracks.length; track++)
1278 	this.tracks[track].startZoom(this.pxPerBp,
1279 				     fixedBp - ((zoomLoc * this.getWidth())
1280                                                 / this.pxPerBp),
1281 				     fixedBp + (((1 - zoomLoc) * this.getWidth())
1282                                                 / this.pxPerBp));
1283 
1284 	//YAHOO.log("centerBp: " + centerBp + "; estimated post-zoom start base: " + (centerBp - ((zoomLoc * this.getWidth()) / this.pxPerBp)) + ", end base: " + (centerBp + (((1 - zoomLoc) * this.getWidth()) / this.pxPerBp)));
1285     this.minLeft = this.pxPerBp * this.ref.start;
1286 
1287     // Zooms take an arbitrary 700 milliseconds, which feels about right
1288     // to me, although if the zooms were smoother they could probably
1289     // get faster without becoming off-putting. -MS
1290     new Zoomer(scale, this,
1291                function() {this.zoomUpdate(zoomLoc, fixedBp);},
1292                700, zoomLoc);
1293 };
1294 
1295 GenomeView.prototype.zoomUpdate = function(zoomLoc, fixedBp) {
1296     var eWidth = this.elem.clientWidth;
1297     var centerPx = this.bpToPx(fixedBp) - (zoomLoc * eWidth) + (eWidth / 2);
1298     this.stripeWidth = this.stripeWidthForZoom(this.curZoom);
1299     this.scrollContainer.style.width =
1300         (this.stripeCount * this.stripeWidth) + "px";
1301     this.zoomContainer.style.width =
1302         (this.stripeCount * this.stripeWidth) + "px";
1303     var centerStripe = Math.round(centerPx / this.stripeWidth);
1304     var firstStripe = (centerStripe - ((this.stripeCount) / 2)) | 0;
1305     this.offset = firstStripe * this.stripeWidth;
1306     this.maxOffset = this.bpToPx(this.ref.end+1) - this.stripeCount * this.stripeWidth;
1307     this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth();
1308     this.minLeft = this.bpToPx(this.ref.start);
1309     this.zoomContainer.style.left = "0px";
1310     this.setX((centerPx - this.offset) - (eWidth / 2));
1311     dojo.forEach(this.uiTracks, function(track) { track.clear(); });
1312     for (var track = 0; track < this.tracks.length; track++)
1313 	this.tracks[track].endZoom(this.pxPerBp, Math.round(this.stripeWidth / this.pxPerBp));
1314     //YAHOO.log("post-zoom start base: " + this.pxToBp(this.offset + this.getX()) + ", end base: " + this.pxToBp(this.offset + this.getX() + this.getWidth()));
1315     this.showVisibleBlocks(true);
1316     this.showDone();
1317     this.showCoarse();
1318 };
1319 
1320 GenomeView.prototype.scrollUpdate = function() {
1321     var x = this.getX();
1322     var numStripes = this.stripeCount;
1323     var cWidth = numStripes * this.stripeWidth;
1324     var eWidth = this.getWidth();
1325     //dx: horizontal distance between the centers of
1326     //this.scrollContainer and this.elem
1327     var dx = (cWidth / 2) - ((eWidth / 2) + x);
1328     //If dx is negative, we add stripes on the right, if positive,
1329     //add on the left.
1330     //We remove stripes from the other side to keep cWidth the same.
1331     //The end goal is to minimize dx while making sure the surviving
1332     //stripes end up in the same place.
1333 
1334     var dStripes = (dx / this.stripeWidth) | 0;
1335     if (0 == dStripes) return;
1336     var changedStripes = Math.abs(dStripes);
1337 
1338     var newOffset = this.offset - (dStripes * this.stripeWidth);
1339 
1340     if (this.offset == newOffset) return;
1341     this.offset = newOffset;
1342 
1343     this.trackIterate(function(track) { track.moveBlocks(dStripes); });
1344 
1345     var newX = x + (dStripes * this.stripeWidth);
1346     this.updateStaticElements( { x: newX } );
1347     this.rawSetX(newX);
1348     var firstVisible = (newX / this.stripeWidth) | 0;
1349 };
1350 
1351 GenomeView.prototype.trackHeightUpdate = function(trackName, height) {
1352     var y = this.getY();
1353     if ( ! (trackName in this.trackIndices)) return;
1354     var track = this.trackIndices[trackName];
1355     if (Math.abs(height - this.trackHeights[track]) < 1) return;
1356 
1357     //console.log("trackHeightUpdate: " + trackName + " " + this.trackHeights[track] + " -> " + height);
1358     // if the bottom of this track is a above the halfway point,
1359     // and we're not all the way at the top,
1360     if ((((this.trackTops[track] + this.trackHeights[track]) - y)
1361          <  (this.getHeight() / 2))
1362         && (y > 0) ) {
1363         // scroll so that lower tracks stay in place on screen
1364         this.setY(y + (height - this.trackHeights[track]));
1365         //console.log("track " + trackName + ": " + this.trackHeights[track] + " -> " + height + "; y: " + y + " -> " + this.getY());
1366     }
1367     this.trackHeights[track] = height;
1368     this.tracks[track].div.style.height = (height + this.trackPadding) + "px";
1369     var nextTop = this.trackTops[track];
1370     if (this.tracks[track].shown) nextTop += height + this.trackPadding;
1371     for (var i = track + 1; i < this.tracks.length; i++) {
1372         this.trackTops[i] = nextTop;
1373         this.tracks[i].div.style.top = nextTop + "px";
1374         if (this.tracks[i].shown)
1375             nextTop += this.trackHeights[i] + this.trackPadding;
1376     }
1377     this.containerHeight = Math.max( nextTop||0, this.getY() + this.getHeight() );
1378     this.scrollContainer.style.height = this.containerHeight + "px";
1379 
1380     this.updateStaticElements({ height: this.getHeight() });
1381 };
1382 
1383 GenomeView.prototype.showVisibleBlocks = function(updateHeight, pos, startX, endX) {
1384     if (pos === undefined) pos = this.getPosition();
1385     if (startX === undefined) startX = pos.x - (this.drawMargin * this.getWidth());
1386     if (endX === undefined) endX = pos.x + ((1 + this.drawMargin) * this.getWidth());
1387     var leftVisible = Math.max(0, (startX / this.stripeWidth) | 0);
1388     var rightVisible = Math.min(this.stripeCount - 1,
1389                                (endX / this.stripeWidth) | 0);
1390 
1391     var bpPerBlock = Math.round(this.stripeWidth / this.pxPerBp);
1392 
1393     var startBase = Math.round(this.pxToBp((leftVisible * this.stripeWidth)
1394                                            + this.offset));
1395     startBase -= 1;
1396     var containerStart = Math.round(this.pxToBp(this.offset));
1397     var containerEnd =
1398         Math.round(this.pxToBp(this.offset
1399                                + (this.stripeCount * this.stripeWidth)));
1400 
1401     this.trackIterate(function(track, view) {
1402                           track.showRange(leftVisible, rightVisible,
1403                                           startBase, bpPerBlock,
1404                                           view.pxPerBp,
1405                                           containerStart, containerEnd);
1406                       });
1407 };
1408 
1409 /**
1410  * Add the given track configurations to the genome view.
1411  * @param trackConfigs {Array[Object]} array of track configuration
1412  * objects to add
1413  */
1414 GenomeView.prototype.showTracks = function( trackConfigs ) {
1415     // filter out any track configs that are already displayed
1416     var needed = dojo.filter( trackConfigs, function(conf) {
1417         return this._getTracks( [conf.label] ).length == 0;
1418     },this);
1419     if( ! needed.length ) return;
1420 
1421     // insert the track configs into the trackDndWidget ( the widget
1422     // will call create() on the confs to render them)
1423     this.trackDndWidget.insertNodes( false, needed );
1424 
1425     this.updateTrackList();
1426 };
1427 
1428 /**
1429  * Remove the given track (configs) from the genome view.
1430  * @param trackConfigs {Array[Object]} array of track configurations
1431  */
1432 GenomeView.prototype.hideTracks = function( /**Array[String]*/ trackConfigs ) {
1433 
1434     // filter out any track configs that are not displayed
1435     var displayed = dojo.filter( trackConfigs, function(conf) {
1436         return this._getTracks( [conf.label] ).length != 0;
1437     },this);
1438     if( ! displayed.length ) return;
1439 
1440     // insert the track configs into the trackDndWidget ( the widget
1441     // will call create() on the confs to render them)
1442     dojo.forEach( displayed, function( conf ) {
1443         this.trackDndWidget.forInItems(function(obj, id, map) {
1444             if( conf.label === obj.data.label ) {
1445                 this.trackDndWidget.delItem( id );
1446                 var item = dojo.byId(id);
1447                 if( item && item.parentNode )
1448                     item.parentNode.removeChild(item);
1449             }
1450         },this);
1451     },this);
1452 
1453     this.updateTrackList();
1454 };
1455 
1456 /**
1457  * For an array of track names, get the track object if it exists.
1458  * @private
1459  * @returns {Array[Track]} the track objects that were found
1460  */
1461 GenomeView.prototype._getTracks = function( /**Array[String]*/ trackNames ) {
1462     var tracks = [],
1463         tn = { count: trackNames.length };
1464     dojo.forEach( trackNames, function(n) { tn[n] = 1;} );
1465     dojo.some( this.tracks, function(t) {
1466         if( tn[t.name] ) {
1467             tracks.push(t);
1468             tn.count--;
1469         }
1470         return ! tn.count;
1471     }, this);
1472     return tracks;
1473 };
1474 
1475 /**
1476  * Create the DOM elements that will contain the rendering of the
1477  * given track in this genome view.
1478  * @private
1479  * @returns {HTMLElement} the HTML element that will contain the
1480  *                        rendering of this track
1481  */
1482 GenomeView.prototype.renderTrack = function( /**Object*/ trackConfig ) {
1483 
1484     if( !trackConfig )
1485         return null;
1486 
1487     // just return its div if this track is already on
1488     var existingTrack;
1489     if( dojo.some( this.tracks, function(t) {
1490             if( t.name == trackConfig.label ) {
1491                 existingTrack = t;
1492                 return true;
1493             }
1494             return false;
1495         })
1496       ) {
1497           return existingTrack.div;
1498       }
1499 
1500     var class_ = eval( trackConfig.type ),
1501         track = new class_(
1502             trackConfig,
1503             this.ref,
1504             {
1505                 changeCallback: dojo.hitch( this, 'showVisibleBlocks', true ),
1506                 trackPadding: this.trackPadding,
1507                 charWidth: this.charWidth,
1508                 seqHeight: this.seqHeight
1509             });
1510 
1511 
1512     // tell the track to get its data, since we're going to display it.
1513     track.load();
1514 
1515     var trackDiv = dojo.create('div', {
1516         className: 'track track_'+track.name,
1517         id: "track_" + track.name
1518     });
1519     trackDiv.trackName = track.name;
1520     trackDiv.track = track;
1521 
1522     var labelDiv = dojo.create(
1523         'div', {
1524             className: "track-label dojoDndHandle",
1525             id: "label_" + track.name,
1526             style: {
1527                 position: 'absolute',
1528                 top: 0,
1529                 left: this.getX() + 'px'
1530             }
1531         },trackDiv);
1532     var closeButton = dojo.create('div',{
1533         className: 'track-close-button',
1534         onclick: dojo.hitch(this,function(evt){
1535             this.browser.publish( '/jbrowse/v1/v/tracks/hide', [[trackConfig]]);
1536             evt.stopPropagation();
1537         })
1538     },labelDiv);
1539 
1540     var labelText = dojo.create('span', { className: 'track-label-text' }, labelDiv );
1541     this.trackLabels.push(labelDiv);
1542 
1543     var heightUpdate = dojo.hitch( this, 'trackHeightUpdate', track.name );
1544     track.setViewInfo(heightUpdate, this.stripeCount, trackDiv, labelDiv,
1545 		      this.stripePercent, this.stripeWidth,
1546                       this.pxPerBp, this.trackPadding);
1547 
1548     track.updateStaticElements({
1549         x: this.getX(),
1550         y: this.getY(),
1551         height: this.getHeight(),
1552         width: this.getWidth()
1553      });
1554 
1555     this.updateTrackList();
1556 
1557     return trackDiv;
1558 };
1559 
1560 GenomeView.prototype.trackIterate = function(callback) {
1561     var i;
1562     for (i = 0; i < this.uiTracks.length; i++)
1563         callback(this.uiTracks[i], this);
1564     for (i = 0; i < this.tracks.length; i++)
1565         callback(this.tracks[i], this);
1566 };
1567 
1568 
1569 /* this function must be called whenever tracks in the GenomeView
1570  * are added, removed, or reordered
1571  */
1572 GenomeView.prototype.updateTrackList = function() {
1573     var tracks = [];
1574     // after a track has been dragged, the DOM is the only place
1575     // that knows the new ordering
1576     var containerChild = this.trackContainer.firstChild;
1577     do {
1578         // this test excludes UI tracks, whose divs don't have a track property
1579         if (containerChild.track) tracks.push(containerChild.track);
1580     } while ((containerChild = containerChild.nextSibling));
1581     this.tracks = tracks;
1582 
1583     var newIndices = {};
1584     var newHeights = new Array(this.tracks.length);
1585     var totalHeight = 0;
1586     for (var i = 0; i < tracks.length; i++) {
1587         newIndices[tracks[i].name] = i;
1588         if (tracks[i].name in this.trackIndices) {
1589             newHeights[i] = this.trackHeights[this.trackIndices[tracks[i].name]];
1590         } else {
1591             newHeights[i] = 0;
1592         }
1593         totalHeight += newHeights[i];
1594         this.trackIndices[tracks[i].name] = i;
1595     }
1596     this.trackIndices = newIndices;
1597     this.trackHeights = newHeights;
1598     var nextTop = this.topSpace;
1599     for (var i = 0; i < this.tracks.length; i++) {
1600         this.trackTops[i] = nextTop;
1601         this.tracks[i].div.style.top = nextTop + "px";
1602         if (this.tracks[i].shown)
1603             nextTop += this.trackHeights[i] + this.trackPadding;
1604     }
1605 
1606     this.containerHeight = Math.max( nextTop || 0, this.getHeight() );
1607     this.scrollContainer.style.height = this.containerHeight + "px";
1608 
1609     this.updateScroll();
1610 };
1611 
1612 /*
1613 
1614 Copyright (c) 2007-2009 The Evolutionary Software Foundation
1615 
1616 Created by Mitchell Skinner <mitch_skinner@berkeley.edu>
1617 
1618 This package and its accompanying libraries are free software; you can
1619 redistribute it and/or modify it under the terms of the LGPL (either
1620 version 2.1, or at your option, any later version) or the Artistic
1621 License 2.0.  Refer to LICENSE for the full license text.
1622 
1623 */
1624