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