UNPKG

@gmod/jbrowse

Version:

JBrowse - client-side genome browser

1,424 lines (1,232 loc) 91.6 kB
define([ 'dojo/_base/declare', 'dojo/_base/array', 'dojo/dom-construct', 'JBrowse/Util', 'JBrowse/has', 'dojo/dnd/move', 'dojo/dnd/Source', 'dijit/focus', 'JBrowse/Component', 'JBrowse/FeatureFiltererMixin', 'JBrowse/View/Track/LocationScale', 'JBrowse/View/Track/GridLines', 'JBrowse/BehaviorManager', 'JBrowse/View/Animation/Zoomer', 'JBrowse/View/Animation/Slider', 'JBrowse/View/InfoDialog' ], function( declare, array, domConstruct, Util, has, dndMove, dndSource, dijitFocus, Component, FeatureFiltererMixin, LocationScaleTrack, GridLinesTrack, BehaviorManager, Zoomer, Slider, InfoDialog ) { var dojof = Util.dojof; // weird subclass of dojo dnd constrained mover to make the location // thumb behave better var locationThumbMover = declare( dndMove.constrainedMoveable, { constructor: function(node, params){ this.constraints = function(){ var n = this.node.parentNode, mb = dojo.marginBox(n); mb.t = 0; return mb; }; } }); /** * Main view class, shows a scrollable, horizontal view of annotation * tracks. NOTE: All coordinates are interbase. * @class * @constructor */ return declare( [Component,FeatureFiltererMixin], { constructor: function( args ) { var browser = args.browser; var elem = args.elem; var stripeWidth = args.stripeWidth; var refseq = args.refSeq; var zoomLevel = args.zoomLevel; this.desiredTracks = {}; // keep a reference to the main browser object this.browser = browser; this.setFeatureFilterParentComponent( this.browser ); this.focusTrack = null; //the page element that the GenomeView lives in this.elem = elem; this.posHeight = this.calculatePositionLabelHeight( elem ); // Add an arbitrary 50% padding between the position labels and the // topmost track this.topSpace = this.posHeight*1.5; // handle trackLabels option if (typeof browser.config.trackLabels !== 'undefined' && browser.config.trackLabels === "no-block") { this.config.trackPadding = 35; this.topSpace = this.posHeight*3; } // WebApollo needs max zoom level to be sequence residues char width this.maxPxPerBp = this.config.maxPxPerBp; //the reference sequence this.ref = refseq; //current scale, in pixels per bp this.pxPerBp = zoomLevel; //width, in pixels, of the vertical stripes this.stripeWidth = stripeWidth; // the scrollContainer is the element that changes position // when the user scrolls this.scrollContainer = dojo.create( 'div', { id: 'container', style: { position: 'absolute', left: '0px', top: '0px' } }, elem ); this._renderVerticalScrollBar(); // we have a separate zoomContainer as a child of the scrollContainer. // they used to be the same element, but making zoomContainer separate // enables it to be narrower than this.elem. this.zoomContainer = document.createElement("div"); this.zoomContainer.id = "zoomContainer"; this.zoomContainer.style.cssText = "position: absolute; left: 0px; top: 0px; height: 100%;"; this.scrollContainer.appendChild(this.zoomContainer); this.outerTrackContainer = document.createElement("div"); this.outerTrackContainer.className = "trackContainer outerTrackContainer"; this.outerTrackContainer.style.cssText = "height: 100%;"; this.zoomContainer.appendChild( this.outerTrackContainer ); this.trackContainer = document.createElement("div"); this.trackContainer.className = "trackContainer innerTrackContainer draggable"; this.trackContainer.style.cssText = "height: 100%;"; this.outerTrackContainer.appendChild( this.trackContainer ); //width, in pixels of the "regular" (not min or max zoom) stripe this.regularStripe = stripeWidth; this.overview = this.browser.overviewDiv; this.overviewBox = dojo.marginBox(this.overview); this.tracks = []; this.uiTracks = []; this.trackIndices = {}; //set up size state (zoom levels, stripe percentage, etc.) this.sizeInit(); //distance, in pixels, from the beginning of the reference sequence //to the beginning of the first active stripe // should always be a multiple of stripeWidth this.offset = 0; //largest value for the sum of this.offset and this.getX() //this prevents us from scrolling off the right end of the ref seq this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth(); //smallest value for the sum of this.offset and this.getX() //this prevents us from scrolling off the left end of the ref seq this.minLeft = this.bpToPx(this.ref.start); //extra margin to draw around the visible area, in multiples of the visible area //0: draw only the visible area; 0.1: draw an extra 10% around the visible area, etc. this.drawMargin = 0.2; //slide distance (pixels) * slideTimeMultiple + 200 = milliseconds for slide //1=1 pixel per millisecond average slide speed, larger numbers are slower this.slideTimeMultiple = 0.8; this.trackHeights = []; this.trackTops = []; this.waitElems = dojo.filter( [ dojo.byId("moveLeft"), dojo.byId("moveRight"), dojo.byId("zoomIn"), dojo.byId("zoomOut"), dojo.byId("bigZoomIn"), dojo.byId("bigZoomOut"), document.body, elem ], function(e) { return e; } ); this.prevCursors = []; this.locationThumb = document.createElement("div"); this.locationThumb.className = "locationThumb"; this.overview.appendChild(this.locationThumb); this.locationThumbMover = new locationThumbMover(this.locationThumb, {area: "content", within: true}); this.x = this.elem.scrollLeft; this.y = 0; var scaleTrackDiv = document.createElement("div"); scaleTrackDiv.className = "track static_track rubberBandAvailable"; scaleTrackDiv.style.height = this.posHeight + "px"; scaleTrackDiv.id = "static_track"; this.scaleTrackDiv = scaleTrackDiv; this.staticTrack = new LocationScaleTrack({ label: "static_track", labelClass: "pos-label", posHeight: this.posHeight, browser: this.browser, refSeq: this.ref }); this.staticTrack.setViewInfo( this, function(height) {}, this.stripeCount, this.scaleTrackDiv, this.stripePercent, this.stripeWidth, this.pxPerBp, this.config.trackPadding); this.zoomContainer.appendChild(this.scaleTrackDiv); this.waitElems.push(this.scaleTrackDiv); var gridTrackDiv = document.createElement("div"); gridTrackDiv.className = "track"; gridTrackDiv.style.cssText = "top: 0px; height: 100%;"; gridTrackDiv.id = "gridtrack"; var gridTrack = new GridLinesTrack({ browser: this.browser, refSeq: this.ref }); gridTrack.setViewInfo( this, function(height) {}, this.stripeCount, gridTrackDiv, this.stripePercent, this.stripeWidth, this.pxPerBp, this.config.trackPadding); this.trackContainer.appendChild(gridTrackDiv); this.uiTracks = [this.staticTrack, gridTrack]; // accept tracks being dragged into this this.trackDndWidget = new dndSource( this.trackContainer, { accept: ["track"], //accepts only tracks into the viewing field withHandles: true, creator: dojo.hitch( this, function( trackConfig, hint ) { return { data: trackConfig, type: ["track"], node: hint == 'avatar' ? dojo.create('div', { innerHTML: trackConfig.key || trackConfig.label, className: 'track-label dragging' }) : this.renderTrack( trackConfig ) }; }) }); // subscribe to showTracks commands this.browser.subscribe( '/dnd/drop', dojo.hitch( this, function( source, nodes, copy, target ) { this.updateTrackList(); if( target.node === this.trackContainer ) { // if dragging into the trackcontainer, we are showing some tracks // get the configs from the tracks being dragged in var confs = dojo.filter( dojo.map( nodes, function(n) { return n.track && n.track.config; }), function(c) {return c;} ); this.browser.publish( '/jbrowse/v1/v/tracks/show', confs ); } } ) ); this.browser.subscribe( '/jbrowse/v1/c/tracks/show', dojo.hitch( this, 'showTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/hide', dojo.hitch( this, 'hideTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/replace', dojo.hitch( this, 'replaceTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/delete', dojo.hitch( this, 'hideTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/pin', dojo.hitch( this, 'pinTracks' )); this.browser.subscribe( '/jbrowse/v1/c/tracks/unpin', dojo.hitch( this, 'unpinTracks' )); // render our UI tracks (horizontal scale tracks, grid lines, and so forth) dojo.forEach(this.uiTracks, function(track) { track.showRange(0, this.stripeCount - 1, Math.round(this.pxToBp(this.offset)), Math.round(this.stripeWidth / this.pxPerBp), this.pxPerBp); }, this); this.addOverviewTrack(new LocationScaleTrack({ label: "overview_loc_track", labelClass: "overview-pos", posHeight: this.overviewPosHeight, browser: this.browser, refSeq: this.ref })); this.showFine(); this.showCoarse(); // initialize the behavior manager used for setting what this view // does (i.e. the behavior it has) for mouse and keyboard events this.behaviorManager = new BehaviorManager({ context: this, behaviors: this._behaviors() }); this.behaviorManager.initialize(); }, _defaultConfig: function() { return { maxPxPerBp: 20, trackPadding: 20 // distance in pixels between each track }; }, /** * @returns {Object} containing ref, start, and end members for the currently displayed location */ visibleRegion: function() { return { ref: this.ref.name, start: this.minVisible(), end: this.maxVisible() }; }, /** * @returns {String} locstring representation of the current location<br> * (suitable for passing to the browser's navigateTo) */ visibleRegionLocString: function() { return Util.assembleLocString( this.visibleRegion() ); }, /** * Create and place the elements for the vertical scrollbar. * @private */ _renderVerticalScrollBar: function() { var container = dojo.create( 'div', { className: 'vertical_scrollbar', style: { position: 'absolute', right: '0px', bottom: '0px', height: '100%', width: '10px', zIndex: 1000 } }, this.browser.container ); var positionMarker = dojo.create( 'div', { className: 'vertical_position_marker', style: { position: 'absolute', height: '100%' } }, container ); this.verticalScrollBar = { container: container, positionMarker: positionMarker, width: container.offsetWidth }; }, /** * Update the position and look of the vertical scroll bar as our * y-scroll offset changes. * @private */ _updateVerticalScrollBar: function( newDims ) { if( typeof newDims.height == 'number' ) { var heightAdjust = this.staticTrack ? -this.staticTrack.div.offsetHeight : 0; var trackPaneHeight = newDims.height + heightAdjust; this.verticalScrollBar.container.style.height = trackPaneHeight-(this.pinUnderlay ? this.pinUnderlay.offsetHeight+heightAdjust : 0 ) +'px'; var markerHeight = newDims.height / (this.containerHeight||1) * 100; this.verticalScrollBar.positionMarker.style.height = markerHeight > 0.5 ? markerHeight+'%' : '1px'; if( newDims.height / (this.containerHeight||1) > 0.98 ) { this.verticalScrollBar.container.style.display = 'none'; this.verticalScrollBar.visible = false; } else { this.verticalScrollBar.container.style.display = 'block'; this.verticalScrollBar.visible = true; } } if( typeof newDims.y == 'number' || typeof newDims.height == 'number' ) { this.verticalScrollBar.positionMarker.style.top = (((newDims.y || this.getY() || 0) / (this.containerHeight||1) * 100 )||0)+'%'; } }, verticalScrollBarVisibleWidth: function() { return this.verticalScrollBar.visible && this.verticalScrollBar.width || 0; }, /** * @returns {Array[Track]} of the tracks that are currently visible in * this genomeview */ visibleTracks: function() { return this.tracks; }, /** * @returns {Array[String]} of the names of tracks that are currently visible in this genomeview */ visibleTrackNames: function() { return dojo.map( this.visibleTracks(), function(t){ return t.name; } ); }, /** * Called in response to a keyboard or mouse event to slide the view * left or right. */ keySlideX: function( offset ) { this.setX( this.getX() + offset ); var thisB = this; if( ! this._keySlideTimeout ) this._keySlideTimeout = window.setTimeout( function() { thisB.afterSlide(); delete thisB._keySlideTimeout; }, 300 ); }, /** * Behaviors (event handler bundles) for various states that the * GenomeView might be in. * @private * @returns {Object} description of behaviors */ _behaviors: function() { return { // behaviors that don't change always: { apply_on_init: true, apply: function() { var handles = []; handles.push( dojo.connect( this.overview, 'mousedown', dojo.hitch( this, 'startRubberZoom', dojo.hitch(this,'overview_absXtoBp'), this.overview, this.overview ) )); var wheelevent = "onwheel" in document.createElement("div") ? "wheel" : document.onmousewheel !== undefined ? "mousewheel" : "DOMMouseScroll"; handles.push( dojo.connect( this.scrollContainer, wheelevent, this, 'wheelScroll', false ), dojo.connect( this.verticalScrollBar.container, 'onclick', this, 'scrollBarClickScroll', false ), dojo.connect( this.scaleTrackDiv, "mousedown", dojo.hitch( this, 'startRubberZoom', dojo.hitch( this,'absXtoBp'), this.scrollContainer, this.scaleTrackDiv ) ), dojo.connect( this.outerTrackContainer, "dblclick", this, 'doubleClickZoom' ), dojo.connect( this.locationThumbMover, "onMoveStop", this, 'thumbMoved' ), dojo.connect( this.overview, "onclick", this, 'overviewClicked' ), dojo.connect( this.scaleTrackDiv, "onclick", this, 'scaleClicked' ), dojo.connect( this.scaleTrackDiv, "mouseover", this, 'scaleMouseOver' ), dojo.connect( this.scaleTrackDiv, "mouseout", this, 'scaleMouseOut' ), dojo.connect( this.scaleTrackDiv, "mousemove", this, 'scaleMouseMove' ), dojo.connect( document.body, 'onkeyup', this, function(evt) { if( evt.keyCode == dojo.keys.SHIFT ) // shift this.behaviorManager.swapBehaviors( 'shiftMouse', 'normalMouse' ); }), dojo.connect( document.body, 'onkeydown', this, function(evt) { if( evt.keyCode == dojo.keys.SHIFT ) // shift this.behaviorManager.swapBehaviors( 'normalMouse', 'shiftMouse' ); }), // scroll the view around in response to keyboard arrow keys dojo.connect( document.body, 'onkeypress', this, function(evt) { // if some digit widget is focused, don't move the // genome view with arrow keys if( dijitFocus.curNode ) return; var that = this; if( evt.keyCode == dojo.keys.LEFT_ARROW || evt.keyCode == dojo.keys.RIGHT_ARROW ) { var offset = evt.keyCode == dojo.keys.LEFT_ARROW ? -40 : 40; if( evt.shiftKey ) offset *= 5; this.keySlideX( offset ); } else if( evt.keyCode == dojo.keys.DOWN_ARROW || evt.keyCode == dojo.keys.UP_ARROW ) { // shift-up/down zooms in and out if( evt.shiftKey ) { this[ evt.keyCode == dojo.keys.UP_ARROW ? 'zoomIn' : 'zoomOut' ]( evt, 0.5, evt.altKey ? 2 : 1 ); } // without shift, scrolls up and down else { var offset = evt.keyCode == dojo.keys.UP_ARROW ? -40 : 40; this.setY( this.getY() + offset ); } } }), // when the track pane is clicked, unfocus any dijit // widgets that would otherwise not give up the focus dojo.connect( this.scrollContainer, 'onclick', this, function(evt) { dijitFocus.curNode && dijitFocus.curNode.blur(); }) ); return handles; } }, // mouse events connected for "normal" behavior normalMouse: { apply_on_init: true, apply: function() { return [ dojo.connect( this.outerTrackContainer, "mousedown", this, 'startMouseDragScroll' ), dojo.connect( this.verticalScrollBar.container, "mousedown", this, 'startVerticalMouseDragScroll') ]; } }, // mouse events connected when we are in 'highlighting' mode, // where dragging the mouse sets the global highlight highlightingMouse: { apply: function() { dojo.removeClass(this.trackContainer,'draggable'); dojo.addClass(this.trackContainer,'highlightingAvailable'); return [ dojo.connect( this.outerTrackContainer, "mousedown", dojo.hitch( this, 'startMouseHighlight', dojo.hitch(this,'absXtoBp'), this.scrollContainer, this.scaleTrackDiv ) ), dojo.connect( this.outerTrackContainer, "mouseover", this, 'maybeDrawVerticalPositionLine' ), dojo.connect( this.outerTrackContainer, "mousemove", this, 'maybeDrawVerticalPositionLine' ) ]; }, remove: function( mgr, handles ) { dojo.forEach( handles, dojo.disconnect, dojo ); dojo.removeClass(this.trackContainer,'highlightingAvailable'); dojo.addClass(this.trackContainer,'draggable'); } }, // mouse events connected when the shift button is being held down shiftMouse: { apply: function() { if ( !dojo.hasClass(this.trackContainer, 'highlightingAvailable') ){ dojo.removeClass(this.trackContainer,'draggable'); dojo.addClass(this.trackContainer,'rubberBandAvailable'); return [ dojo.connect( this.outerTrackContainer, "mousedown", dojo.hitch( this, 'startRubberZoom', dojo.hitch(this,'absXtoBp'), this.scrollContainer, this.scaleTrackDiv ) ), dojo.connect( this.outerTrackContainer, "onclick", this, 'scaleClicked' ), dojo.connect( this.outerTrackContainer, "mouseover", this, 'maybeDrawVerticalPositionLine' ), dojo.connect( this.outerTrackContainer, "mousemove", this, 'maybeDrawVerticalPositionLine' ) ]; } }, remove: function( mgr, handles ) { this.clearBasePairLabels(); this.clearVerticalPositionLine(); dojo.forEach( handles, dojo.disconnect, dojo ); dojo.removeClass(this.trackContainer,'rubberBandAvailable'); dojo.addClass(this.trackContainer,'draggable'); } }, // mouse events that are connected when we are in the middle of a // drag-scrolling operation mouseDragScrolling: { apply: function() { return [ dojo.connect(document.body, "mouseup", this, 'dragEnd' ), dojo.connect(document.body, "mousemove", this, 'dragMove' ), dojo.connect(document.body, "mouseout", this, 'checkDragOut' ) ]; } }, // mouse events that are connected when we are in the middle of a // vertical-drag-scrolling operation verticalMouseDragScrolling: { apply: function() { return [ dojo.connect(document.body, "mouseup", this, 'dragEnd' ), dojo.connect(document.body, "mousemove", this, 'verticalDragMove'), dojo.connect(document.body, "mouseout", this, 'checkDragOut' ) ]; } }, // mouse events that are connected when we are in the middle of a // rubber-band zooming operation mouseRubberBanding: { apply: function() { return [ dojo.connect(document.body, "mouseup", this, 'rubberExecute' ), dojo.connect(document.body, "mousemove", this, 'rubberMove' ), dojo.connect(document.body, "mouseout", this, 'rubberCancel' ), dojo.connect(window, "onkeydown", this, function(e){ if( e.keyCode !== dojo.keys.SHIFT ) this.rubberCancel(e); }) ]; } } };}, /** * Conduct a DOM test to calculate the height of div.pos-label * elements with a line of text in them. */ calculatePositionLabelHeight: function( containerElement ) { // measure the height of some arbitrary text in whatever font this // shows up in (set by an external CSS file) var heightTest = document.createElement("div"); heightTest.className = "pos-label"; heightTest.style.visibility = "hidden"; heightTest.appendChild(document.createTextNode("42")); containerElement.appendChild(heightTest); var h = heightTest.clientHeight; containerElement.removeChild(heightTest); return h; }, scrollBarClickScroll : function( event ) { if ( !event ) event = window.event; var containerHeight = parseInt( this.verticalScrollBar.container.style.height,10 ); var markerHeight = parseInt( this.verticalScrollBar.positionMarker.style.height,10 ); var trackContainerHeight = this.trackContainer.clientHeight; var absY = this.getY()*( trackContainerHeight/containerHeight ); if ( absY > event.clientY ) this.setY( this.getY() - 300 ); else if (absY + markerHeight < event.clientY) this.setY( this.getY() + 300 ); //the timeout is so that we don't have to run showVisibleBlocks //for every scroll wheel click (we just wait until so many ms //after the last one). if ( this.wheelScrollTimeout ) window.clearTimeout( this.wheelScrollTimeout ); // 100 milliseconds since the last scroll event is an arbitrary // cutoff for deciding when the user is done scrolling // (set by a bit of experimentation) this.wheelScrollTimeout = window.setTimeout( dojo.hitch( this, function() { this.showVisibleBlocks(true); this.wheelScrollTimeout = null; }, 100)); dojo.stopEvent(event); }, wheelScroll: function( event ) { if ( !event ) event = window.event; // if( window.WheelEvent ) // event = window.WheelEvent; var delta = { x: 0, y: 0 }; if( 'wheelDeltaX' in event ) { delta.x = event.wheelDeltaX/2; delta.y = event.wheelDeltaY/2; } else if( 'deltaX' in event ) { var multiplier = navigator.userAgent.indexOf("OS X 10.9")!==-1 ? -5 : -40; delta.x = Math.abs(event.deltaY) > Math.abs(2*event.deltaX) ? 0 : event.deltaX*multiplier; delta.y = event.deltaY*-10; } else if( event.wheelDelta ) { delta.y = event.wheelDelta/2; if( window.opera ) delta.y = -delta.y; } else if( event.detail ) { delta.y = -event.detail*100; } delta.x = Math.round( delta.x * 2 ); delta.y = Math.round( delta.y ); var didScroll = false if( delta.x ) { this.keySlideX( -delta.x ); didScroll = true } if( delta.y ) { // 60 pixels per mouse wheel event var prevY = this.getY() var currY = this.setY( prevY - delta.y ); // check if clamping happened if(currY !== prevY) { didScroll = true } } //the timeout is so that we don't have to run showVisibleBlocks //for every scroll wheel click (we just wait until so many ms //after the last one). if ( this.wheelScrollTimeout ) window.clearTimeout( this.wheelScrollTimeout ); // 100 milliseconds since the last scroll event is an arbitrary // cutoff for deciding when the user is done scrolling // (set by a bit of experimentation) this.wheelScrollTimeout = window.setTimeout( dojo.hitch( this, function() { this.showVisibleBlocks(true); this.wheelScrollTimeout = null; }, 100)); // allow event to bubble out of iframe for example if(didScroll || this.browser.config.alwaysStopScrollBubble) dojo.stopEvent(event); }, getX: function() { return this.x || 0; }, getY: function() { return this.y || 0; }, getHeight: function() { return this.elemBox.h; }, getWidth: function() { return this.elemBox.w; }, clampX: function(x) { return Math.round( Math.max( Math.min( this.maxLeft - this.offset, x || 0), this.minLeft - this.offset ) ); }, clampY: function(y) { return Math.round( Math.min( Math.max( 0, y || 0 ), this.containerHeight- this.getHeight() ) ); }, rawSetX: function(x) { this.elem.scrollLeft = x; this.x = x; }, /** * @returns the new x value that was set */ setX: function(x) { x = this.clampX(x); this.rawSetX( x ); this.updateStaticElements( { x: x } ); this.showFine(); return x; }, rawSetY: function(y) { this.y = y; this.layoutTracks(); }, /** * @returns the new y value that was set */ setY: function(y) { y = this.clampY(y); this.rawSetY(y); this.updateStaticElements( { y: y } ); return y; }, /** * @private */ rawSetPosition: function(pos) { this.rawSetX( pos.x ); this.rawSetY( pos.y ); return pos; }, /** * @param pos.x new x position * @param pos.y new y position */ setPosition: function(pos) { var x = this.clampX( pos.x ); var y = this.clampY( pos.y ); this.updateStaticElements( {x: x, y: y} ); this.rawSetX( x ); this.rawSetY( y ); this.showFine(); }, /** * @returns {Object} as <code>{ x: 123, y: 456 }</code> */ getPosition: function() { return { x: this.x, y: this.y }; }, zoomCallback: function() { this.zoomUpdate(); }, afterSlide: function() { this.showCoarse(); this.scrollUpdate(); this.showVisibleBlocks(true); }, /** * Suppress double-click events in the genome view for a certain amount of time, default 100 ms. */ suppressDoubleClick: function( /** Number */ time ) { if( this._noDoubleClick ) { window.clearTimeout( this._noDoubleClick ); } var thisB = this; this._noDoubleClick = window.setTimeout( function(){ delete thisB._noDoubleClick; }, time || 100 ); }, doubleClickZoom: function(event) { if( this._noDoubleClick ) return; if( this.dragging ) return; if( "animation" in this ) return; // if we have a timeout in flight from a scaleClicked click, // cancel it, cause it looks now like the user has actually // double-clicked if( this.scaleClickedTimeout ) window.clearTimeout( this.scaleClickedTimeout ); var zoomLoc = (event.pageX - dojo.position(this.elem, true).x) / this.getWidth(); if (event.shiftKey) { this.zoomOut(event, zoomLoc, 2); } else { this.zoomIn(event, zoomLoc, 2); } dojo.stopEvent(event); }, /** @private */ _beforeMouseDrag: function( event ) { if ( this.animation ) { if (this.animation instanceof Zoomer) { dojo.stopEvent(event); return 0; } else { this.animation.stop(); } } if (Util.isRightButton(event)) return 0; dojo.stopEvent(event); return 1; }, /** * Event fired when a user's mouse button goes down inside the main * element of the genomeview. */ startMouseDragScroll: function(event) { if( ! this._beforeMouseDrag(event) ) return; this.behaviorManager.applyBehaviors('mouseDragScrolling'); this.dragStartPos = {x: event.clientX, y: event.clientY}; this.winStartPos = this.getPosition(); }, /** * Event fired when a user's mouse button goes down inside the vertical * scroll bar element of the genomeview. */ startVerticalMouseDragScroll: function(event) { if( ! this._beforeMouseDrag(event) ) return; // not sure what this is for. this.behaviorManager.applyBehaviors('verticalMouseDragScrolling'); this.dragStartPos = {x: event.clientX, y: event.clientY}; this.winStartPos = this.getPosition(); }, startMouseHighlight: function( absToBp, container, scaleDiv, event ) { if( ! this._beforeMouseDrag(event) ) return; this.behaviorManager.applyBehaviors('mouseRubberBanding'); this.rubberbanding = { absFunc: absToBp, container: container, scaleDiv: scaleDiv, message: 'Highlight region', start: { x: event.clientX, y: event.clientY }, execute: function( start, end ) { this.browser.setHighlightAndRedraw({ ref: this.ref.name, start: start, end: end }); } }; this.winStartPos = this.getPosition(); }, /** * Start a rubber-band dynamic zoom. * * @param {Function} absToBp function to convert page X coordinates to * base pair positions on the reference sequence. Called in the * context of the GenomeView object. * @param {HTMLElement} container element in which to draw the * rubberbanding highlight * @param {Event} event the mouse event that's starting the zoom */ startRubberZoom: function( absToBp, container, scaleDiv, event ) { if( ! this._beforeMouseDrag(event) ) return; this.behaviorManager.applyBehaviors('mouseRubberBanding'); this.rubberbanding = { absFunc: absToBp, container: container, scaleDiv: scaleDiv, message: 'Zoom to region', start: { x: event.clientX, y: event.clientY }, execute: function( h_start_bp, h_end_bp ) { this.setLocation( this.ref, h_start_bp, h_end_bp ); } }; this.winStartPos = this.getPosition(); this.clearVerticalPositionLine(); this.clearBasePairLabels(); }, _rubberStop: function(event) { this.behaviorManager.removeBehaviors('mouseRubberBanding'); this.hideRubberHighlight(); this.clearBasePairLabels(); if( event ) dojo.stopEvent(event); delete this.rubberbanding; }, rubberCancel: function(event) { var htmlNode = document.body.parentNode; var bodyNode = document.body; if ( !event || !(event.relatedTarget || event.toElement) || (htmlNode === (event.relatedTarget || event.toElement)) || (bodyNode === (event.relatedTarget || event.toElement))) { this._rubberStop(event); } }, rubberMove: function(event) { this.setRubberHighlight( this.rubberbanding.start, { x: event.clientX, y: event.clientY } ); }, rubberExecute: function( event) { var start = this.rubberbanding.start; var end = { x: event.clientX, y: event.clientY }; var h_start_bp = Math.floor( this.rubberbanding.absFunc( Math.min(start.x,end.x) ) ); var h_end_bp = Math.ceil( this.rubberbanding.absFunc( Math.max(start.x,end.x) ) ); var exec = this.rubberbanding.execute; this._rubberStop(event); // cancel the rubber-zoom if the user has moved less than 3 pixels if( Math.abs( start.x - end.x ) < 3 ) { return; } exec.call( this, h_start_bp, h_end_bp ); }, // draws the rubber-banding highlight region from start.x to end.x setRubberHighlight: function( start, end ) { var container = this.rubberbanding.container, container_coords = dojo.position(container,true); var h = this.rubberHighlight || (function(){ var main = this.rubberHighlight = document.createElement("div"); main.className = 'rubber-highlight'; main.style.position = 'absolute'; main.style.zIndex = 20; var text = document.createElement('div'); text.appendChild( document.createTextNode( this.rubberbanding.message ) ); main.appendChild(text); text.style.position = 'relative'; text.style.top = (50-container_coords.y) + "px"; container.appendChild( main ); return main; }).call(this); h.style.visibility = 'visible'; h.style.left = Math.min( start.x, end.x ) - container_coords.x + 'px'; h.style.width = Math.abs( end.x - start.x ) + 'px'; // draw basepair-position labels for the start and end of the highlight this.drawBasePairLabel({ name: 'rubberLeft', xToBp: this.rubberbanding.absFunc, scaleDiv: this.rubberbanding.scaleDiv, offset: 0, x: Math.min( start.x, end.x ), parent: container, className: 'rubber' }); this.drawBasePairLabel({ name: 'rubberRight', xToBp: this.rubberbanding.absFunc, scaleDiv: this.rubberbanding.scaleDiv, offset: 0, x: Math.max( start.x, end.x ) + 1, parent: container, className: 'rubber' }); // turn off the red position line if it's on this.clearVerticalPositionLine(); }, dragEnd: function(event) { this.behaviorManager.removeBehaviors('mouseDragScrolling', 'verticalMouseDragScrolling'); dojo.stopEvent(event); this.showCoarse(); this.scrollUpdate(); this.showVisibleBlocks(true); // wait 100 ms before releasing our drag indication, since onclick // events from during the drag might fire after the dragEnd event window.setTimeout( dojo.hitch(this,function() {this.dragging = false;}), 100 ); }, /** stop the drag if we mouse out of the view */ checkDragOut: function( event ) { var htmlNode = document.body.parentNode; var bodyNode = document.body; if (!(event.relatedTarget || event.toElement) || (htmlNode === (event.relatedTarget || event.toElement)) || (bodyNode === (event.relatedTarget || event.toElement)) ) { this.dragEnd(event); } }, dragMove: function(event) { this.dragging = true; this.setPosition({ x: this.winStartPos.x - (event.clientX - this.dragStartPos.x), y: this.winStartPos.y - (event.clientY - this.dragStartPos.y) }); dojo.stopEvent(event); }, // Similar to "dragMove". Consider merging. verticalDragMove: function(event) { this.dragging = true; var containerHeight = parseInt(this.verticalScrollBar.container.style.height,10); var trackContainerHeight = this.trackContainer.clientHeight; this.setPosition({ x: this.winStartPos.x, y: this.winStartPos.y + (event.clientY - this.dragStartPos.y)*(trackContainerHeight/containerHeight) }); dojo.stopEvent(event); }, hideRubberHighlight: function( start, end ) { if( this.rubberHighlight ) { this.rubberHighlight.parentNode.removeChild( this.rubberHighlight ); delete this.rubberHighlight; } }, /* moves the view by (distance times the width of the view) pixels */ slide: function(distance) { if (this.animation) this.animation.stop(); this.trimVertical(); // slide for an amount of time that's a function of the distance being // traveled plus an arbitrary extra 200 milliseconds so that // short slides aren't too fast (200 chosen by experimentation) new Slider(this, this.afterSlide, Math.abs(distance) * this.getWidth() * this.slideTimeMultiple + 200, distance * this.getWidth()); }, setLocation: function(refseq, startbp, endbp) { if (startbp === undefined) startbp = this.minVisible(); if (endbp === undefined) endbp = this.maxVisible(); if( typeof refseq == 'string' ) { // if a string was passed, need to get the refseq object for it refseq = this.browser.getRefSeq( refseq ); } if( ! refseq ) refseq = this.ref; if ((startbp < refseq.start) || (startbp > refseq.end)) startbp = refseq.start; if ((endbp < refseq.start) || (endbp > refseq.end)) endbp = refseq.end; function removeTrack( track ) { delete thisB.desiredTracks[track.name]; if (track.div && track.div.parentNode) track.div.parentNode.removeChild(track.div); }; if( this.ref !== refseq ) { var thisB = this; this.ref = refseq; this._unsetPosBeforeZoom(); // if switching to different sequence, flush zoom position tracking array.forEach( this.tracks, removeTrack ); this.tracks = []; this.trackIndices = {}; this.trackHeights = []; this.trackTops = []; array.forEach(this.uiTracks, function(track) { track.refSeq = thisB.ref; track.clear(); }); this.overviewTrackIterate( removeTrack); this.addOverviewTrack(new LocationScaleTrack({ label: "overview_loc_track", labelClass: "overview-pos", posHeight: this.overviewPosHeight, browser: this.browser, refSeq: this.ref })); this.sizeInit(); this.setY(0); this.behaviorManager.initialize(); } this.pxPerBp = Math.min(this.getWidth() / (endbp - startbp), this.maxPxPerBp ); this.curZoom = Util.findNearest(this.zoomLevels, this.pxPerBp); if (Math.abs(this.pxPerBp - this.zoomLevels[this.zoomLevels.length - 1]) < 0.2) { //the cookie-saved location is in round bases, so if the saved //location was at the highest zoom level, the new zoom level probably //won't be exactly at the highest zoom (which is necessary to trigger //the sequence track), so we nudge the zoom level to be exactly at //the highest level if it's close. //Exactly how close is arbitrary; 0.2 was chosen to be close //enough that people wouldn't notice if we fudged that much. //console.log("nudging zoom level from %d to %d", this.pxPerBp, this.zoomLevels[this.zoomLevels.length - 1]); this.pxPerBp = this.zoomLevels[this.zoomLevels.length - 1]; } this.stripeWidth = (this.stripeWidthForZoom(this.curZoom) / this.zoomLevels[this.curZoom]) * this.pxPerBp; this.instantZoomUpdate(); this.centerAtBase((startbp + endbp) / 2, true); }, stripeWidthForZoom: function(zoomLevel) { if ((this.zoomLevels.length - 1) == zoomLevel) { // width, in pixels, of stripes at full zoom, is 10bp return this.regularStripe / 10 * this.maxPxPerBp; } else if (0 == zoomLevel) { return this.minZoomStripe; } else { return this.regularStripe; } }, instantZoomUpdate: function() { this.scrollContainer.style.width = (this.stripeCount * this.stripeWidth) + "px"; this.zoomContainer.style.width = (this.stripeCount * this.stripeWidth) + "px"; this.maxOffset = this.bpToPx(this.ref.end) - this.stripeCount * this.stripeWidth; this.maxLeft = this.bpToPx(this.ref.end+1) - this.getWidth(); this.minLeft = this.bpToPx(this.ref.start); }, centerAtBase: function(base, instantly) { base = Math.min(Math.max(base, this.ref.start), this.ref.end); if (instantly) { var pxDist = this.bpToPx(base); var containerWidth = this.stripeCount * this.stripeWidth; var stripesLeft = Math.floor((pxDist - (containerWidth / 2)) / this.stripeWidth); this.offset = stripesLeft * this.stripeWidth; this.setX(pxDist - this.offset - (this.getWidth() / 2)); this.trackIterate(function(track) { track.clear(); }); this.showVisibleBlocks(true); this.showCoarse(); } else { var startbp = this.pxToBp(this.x + this.offset); var halfWidth = (this.getWidth() / this.pxPerBp) / 2; var endbp = startbp + halfWidth + halfWidth; var center = startbp + halfWidth; if ((base >= (startbp - halfWidth)) && (base <= (endbp + halfWidth))) { //we're moving somewhere nearby, so move smoothly if (this.animation) this.animation.stop(); var distance = (center - base) * this.pxPerBp; this.trimVertical(); // slide for an amount of time that's a function of the // distance being traveled plus an arbitrary extra 200 // milliseconds so that short slides aren't too fast // (200 chosen by experimentation) new Slider(this, this.afterSlide, Math.abs(distance) * this.slideTimeMultiple + 200, distance); } else { //we're moving far away, move instantly this.centerAtBase(base, true); } } }, /** * @returns {Number} minimum basepair coordinate of the current * reference sequence visible in the genome view */ minVisible: function() { var mv = this.pxToBp(this.x + this.offset); // if we are less than one pixel from the beginning of the ref // seq, just say we are at the beginning. if( mv < this.pxToBp(1) ) return 0; else return Math.round(mv); }, /** * @returns {Number} maximum basepair coordinate of the current * reference sequence visible in the genome view */ maxVisible: function() { var mv = this.pxToBp(this.x + this.offset + this.getWidth()); var scrollbar = Math.round(this.pxToBp( this.verticalScrollBarVisibleWidth() )); // if we are less than one pixel from the end of the ref // seq, just say we are at the end. if( mv > this.ref.end - this.pxToBp(1) ) return this.ref.end - scrollbar; else return Math.round(mv) - scrollbar; }, showFine: function() { this.onFineMove(this.minVisible(), this.maxVisible()); }, showCoarse: function() { this.onCoarseMove(this.minVisible(), this.maxVisible()); }, /** * Hook for other components to dojo.connect to. */ onFineMove: function( startbp, endbp ) { this.updateLocationThumb(); }, /** * Hook for other components to dojo.connect to. */ onCoarseMove: function( startbp, endbp ) { this.updateLocationThumb(); }, /** * Hook to be called on a window resize. */ onResize: function() { this.sizeInit(); this.showVisibleBlocks(); this.showFine(); this.showCoarse(); }, /** * Event handler fired when the overview bar is single-clicked. */ overviewClicked: function( evt ) { this.centerAtBase( this.overview_absXtoBp( evt.clientX ) ); }, /** * Event handler fired when mouse is over the scale bar. */ scaleMouseOver: function( evt ) { if( ! this.rubberbanding ) this.drawVerticalPositionLine( this.scaleTrackDiv, evt); }, /** * Event handler fired when mouse moves over the scale bar. */ scaleMouseMove: function( evt ) { if( ! this.rubberbanding ) this.drawVerticalPositionLine( this.scaleTrackDiv, evt); }, /** * Event handler fired when mouse leaves the scale bar. */ scaleMouseOut: function( evt ) { this.clearVerticalPositionLine(); this.clearBasePairLabels(); }, /** * draws the vertical position line only if * we are not rubberbanding */ maybeDrawVerticalPositionLine: function( evt ) { if( this.rubberbanding ) return; this.drawVerticalPositionLine( this.scaleTrackDiv, evt ); }, /** * Draws the red line across the work area, or updates it if it already exists. */ drawVerticalPositionLine: function( parent, evt){ var numX = evt.pageX + 2; if( ! this.verticalPositionLine ){ // if line does not exist, create it this.verticalPositionLine = dojo.create( 'div', { className: 'trackVerticalPositionIndicatorMain' }, this.staticTrack.div ); } var line = this.verticalPositionLine; line.style.display = 'block'; //make line visible line.style.left = numX+'px'; //set location on screen var scaleTrackPos = dojo.position( this.scaleTrackDiv ); line.style.top = scaleTrackPos.y + 'px'; this.drawBasePairLabel({ name: 'single', offset: 0, x: numX, parent: parent, scaleDiv: parent }); }, /** * Draws the label for the line. * @param {Number} args.numX X-coordinate at which to draw the label's origin * @param {Number} args.name unique name used to cache this label * @param {Number} args.offset offset in pixels from numX at which the label should actually be drawn * @param {HTMLElement} args.scaleDiv * @param {Function} args.xToBp */ drawBasePairLabel: function ( args ){ var name = args.name || 0; var offset = args.offset || 0; var numX = args.x; this.basePairLabels = this.basePairLabels || {}; if( ! this.basePairLabels[name] ) { var scaleTrackPos = dojo.position( args.scaleDiv || this.scaleTrackDiv ); this.basePairLabels[name] = dojo.create( 'div', { className: 'basePairLabel'+(args.className ? ' '+args.className : '' ), style: { top: scaleTrackPos.y + scaleTrackPos.h - 3 + 'px' } }, this.browser.container); } var label = this.basePairLabels[name]; if (typeof numX == 'object'){ numX = numX.clientX; } label.style.display = 'block'; //make label visible var absfunc = args.xToBp || dojo.hitch(this,'absXtoBp'); //set text to BP location (adding 1 to convert from interbase) label.innerHTML = Util.addCommas( Math.floor( absfunc(numX) )+1); //label.style.top = args.top + 'px'; // 15 pixels on either side of the label if( window.innerWidth - numX > 8 + label.offsetWidth ) { label.style.left = numX + offset + 'px'; //set location on screen to the right } else { label.style.left = numX + 1 - offset - label.offsetWidth + 'px'; //set location on screen to the left } }, /** * Turn off the basepair-position line if it is being displayed. */ clearVerticalPositionLine: function(){ if( this.verticalPositionLine ) this.verticalPositionLine.style.display = 'none'; }, /** * Delete any base pair labels that are being displayed. */ clearBasePairLabels: function(){ for( var name in this.basePairLabels ) { var label = th