@gmod/jbrowse
Version:
JBrowse - client-side genome browser
1,424 lines (1,232 loc) • 91.6 kB
JavaScript
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