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