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