@gmod/jbrowse
Version:
JBrowse - client-side genome browser
1,069 lines (928 loc) • 39.6 kB
JavaScript
/**
* Feature track that draws features using HTML5 canvas elements.
*/
define( [
'dojo/_base/declare',
'dojo/_base/array',
'dojo/_base/lang',
'dojo/_base/event',
'dojo/mouse',
'dojo/dom-construct',
'dojo/Deferred',
'dojo/on',
'JBrowse/has',
'JBrowse/Util',
'JBrowse/View/GranularRectLayout',
'JBrowse/View/Track/BlockBased',
'JBrowse/View/Track/_ExportMixin',
'JBrowse/Errors',
'JBrowse/View/Track/_FeatureDetailMixin',
'JBrowse/View/Track/_FeatureContextMenusMixin',
'JBrowse/View/Track/_YScaleMixin',
'JBrowse/Model/Location',
'JBrowse/Model/SimpleFeature'
],
function(
declare,
array,
lang,
domEvent,
mouse,
domConstruct,
Deferred,
on,
has,
Util,
Layout,
BlockBasedTrack,
ExportMixin,
Errors,
FeatureDetailMixin,
FeatureContextMenuMixin,
YScaleMixin,
Location,
SimpleFeature
) {
/**
* inner class that indexes feature layout rectangles (fRects) (which
* include features) by unique ID.
*
* We have one of these indexes in each block.
*/
var FRectIndex = declare( null, {
constructor: function( args ) {
var height = args.h;
var width = args.w;
this.dims = { h: height, w: width };
this.byID = {};
},
getByID: function( id ) {
return this.byID[id];
},
addAll: function( fRects ) {
var byID = this.byID;
var cW = this.dims.w;
var cH = this.dims.h;
array.forEach( fRects, function( fRect ) {
if( ! fRect )
return;
// by ID
byID[ fRect.f.id() ] = fRect;
}, this );
},
getAll: function( ) {
var fRects = [];
for( var id in this.byID ) {
fRects.push( this.byID[id] );
}
return fRects;
}
});
return declare(
[ BlockBasedTrack,
FeatureDetailMixin,
ExportMixin,
FeatureContextMenuMixin,
YScaleMixin
], {
constructor: function( args ) {
this.glyphsLoaded = {};
this.glyphsBeingLoaded = {};
this.regionStats = {};
this.showLabels = this.config.style.showLabels;
this.showTooltips = this.config.style.showTooltips;
this.displayMode = this.config.displayMode;
//setup displayMode style cookie
var cookie = this.browser.cookie("track-" + this.name);
if (cookie) {
this.displayMode = cookie;
}
this._setupEventHandlers();
},
_defaultConfig: function() {
return Util.deepUpdate(
lang.clone( this.inherited(arguments) ),
{
maxFeatureScreenDensity: 0.5,
enableCollapsedMouseover: false,
disableCollapsedClick: false,
// default glyph class to use
glyph: lang.hitch( this, 'guessGlyphType' ),
// maximum number of pixels on each side of a
// feature's bounding coordinates that a glyph is
// allowed to use
maxFeatureGlyphExpansion: 500,
// maximum height of the track, in pixels
maxHeight: 600,
histograms: {
description: 'feature density',
min: 0,
height: 100,
color: 'goldenrod',
clip_marker_color: 'red'
},
style: {
// not configured by users
_defaultHistScale: 4,
_defaultLabelScale: 30,
_defaultDescriptionScale: 120,
showLabels: true,
showTooltips: true,
label: 'name,id',
description: 'note, description'
},
displayMode: 'normal',
events: {
contextmenu: function( feature, fRect, block, track, evt ) {
evt = domEvent.fix( evt );
if( fRect && fRect.contextMenu )
fRect.contextMenu._openMyself({ target: block.featureCanvas, coords: { x: evt.pageX, y: evt.pageY }} );
domEvent.stop( evt );
}
},
menuTemplate: [
{ label: 'View details',
title: '{type} {name}',
action: 'contentDialog',
iconClass: 'dijitIconTask',
content: dojo.hitch( this, 'defaultFeatureDetail' )
},
{
"label" : function() {
return 'Zoom to this '+( this.feature.get('type') || 'feature' );
},
"action" : function(){
var ref = this.track.refSeq;
var paddingBp = Math.round( 10 /*pixels*/ / this.viewInfo.scale /* px/bp */ );
var start = Math.max( ref.start, this.feature.get('start') - paddingBp );
var end = Math.min( ref.end, this.feature.get('end') + paddingBp );
this.track.genomeView.setLocation( ref, start, end );
},
"iconClass" : "dijitIconConnector"
},
{
label : function() {
return 'Highlight this '+( this.feature.get('type') || 'feature' );
},
action: function() {
var loc = new Location({ feature: this.feature, tracks: [this.track] });
this.track.browser.setHighlightAndRedraw(loc);
},
iconClass: 'dijitIconFilter'
}
]
});
},
setViewInfo: function( genomeView, heightUpdate, numBlocks, trackDiv, widthPct, widthPx, scale ) {
this.inherited( arguments );
this.staticCanvas = domConstruct.create('canvas', { className: 'static-canvas', style: { height: "100%", cursor: "default", position: "absolute", zIndex: 15 }}, trackDiv);
let ctx = this.staticCanvas.getContext('2d')
let ratio = Util.getResolution( ctx, this.browser.config.highResolutionMode );
this.staticCanvas.height = this.staticCanvas.offsetHeight*ratio;
this._makeLabelTooltip( );
},
guessGlyphType: function(feature) {
// first try to guess by its SO type
let guess = {
'gene': 'Gene',
'mRNA': 'ProcessedTranscript',
'transcript': 'ProcessedTranscript'
}[feature.get('type')]
// otherwise, make it Segments if it has children,
// a BED if it has block_count/thick_start,
// or a Box otherwise
if (!guess) {
let children = feature.children()
if (children && children.length)
guess = 'Segments'
else if (feature.get('block_count') || feature.get('thick_start'))
guess = 'UCSC/BED'
else
guess = 'Box'
}
return 'JBrowse/View/FeatureGlyph/'+guess
},
fillBlock: function( args ) {
var blockIndex = args.blockIndex;
var block = args.block;
var leftBase = args.leftBase;
var rightBase = args.rightBase;
var scale = args.scale;
if( ! has('canvas') ) {
this.fatalError = 'This browser does not support HTML canvas elements.';
this.fillBlockError( blockIndex, block, this.fatalError );
return;
}
var fill = lang.hitch( this, function( stats ) {
// calculate some additional view parameters that
// might depend on the feature stats and add them to
// the view args we pass down
var renderArgs = lang.mixin(
{
stats: stats,
displayMode: this.displayMode,
showFeatures: scale >= ( this.config.style.featureScale
|| (stats.featureDensity||0) / this.config.maxFeatureScreenDensity ),
showLabels: this.showLabels && this.displayMode == "normal"
&& scale >= ( this.config.style.labelScale
|| (stats.featureDensity||0) * this.config.style._defaultLabelScale ),
showDescriptions: this.showLabels && this.displayMode == "normal"
&& scale >= ( this.config.style.descriptionScale
|| (stats.featureDensity||0) * this.config.style._defaultDescriptionScale)
},
args
);
if( renderArgs.showFeatures ) {
this.setLabel( this.key );
this.removeYScale();
this.fillFeatures( renderArgs );
}
else if( this.config.histograms.store || this.store.getRegionFeatureDensities ) {
this.fillHistograms( renderArgs );
}
else {
this.setLabel( this.key );
this.fillTooManyFeaturesMessage(
blockIndex,
block,
scale
);
args.finishCallback();
}
});
this.store.getGlobalStats(
fill,
dojo.hitch( this, function(e) {
this._handleError( e, args );
args.finishCallback(e);
})
);
},
// override the base error handler to try to draw histograms if
// it's a data overflow error and we know how to draw histograms
_handleError: function( error, viewArgs ) {
if( typeof error == 'object'
&& error instanceof Errors.DataOverflow
&& ( this.config.histograms.store || this.store.getRegionFeatureDensities )
) {
this.fillHistograms( viewArgs );
}
else
this.inherited(arguments);
},
// create the layout if we need to, and if we can
_getLayout: function( scale ) {
if( ! this.layout || this._layoutpitchX != 4/scale ) {
// if no layoutPitchY configured, calculate it from the
// height and marginBottom (parseInt in case one or both are functions), or default to 3 if the
// calculation didn't result in anything sensible.
var pitchY = this.getConf('layoutPitchY') || 4;
this.layout = new Layout({ pitchX: 4/scale, pitchY: pitchY, maxHeight: this.getConf('maxHeight'), displayMode: this.displayMode });
this._layoutpitchX = 4/scale;
}
return this.layout;
},
_clearLayout: function() {
delete this.layout;
},
hideAll: function() {
this._clearLayout();
return this.inherited( arguments );
},
/**
* Returns a promise for the appropriate glyph for the given
* feature and args.
*/
getGlyph: function( viewArgs, feature, callback, errorCallback ) {
var glyphClassName = this.getConfForFeature( 'glyph', feature );
var glyph, interestedParties;
if(( glyph = this.glyphsLoaded[glyphClassName] )) {
callback( glyph );
}
else if(( interestedParties = this.glyphsBeingLoaded[glyphClassName] )) {
interestedParties.push( callback );
}
else {
var thisB = this;
this.glyphsBeingLoaded[glyphClassName] = [callback];
dojo.global.require( [glyphClassName], function( GlyphClass ) {
if( typeof GlyphClass == 'string' ) {
thisB.fatalError = "could not load glyph "+glyphClassName;
thisB.redraw();
return;
}
// if this require came back after we are already destroyed, just ignore it
if( thisB.destroyed )
return;
glyph = thisB.glyphsLoaded[glyphClassName] =
new GlyphClass({ track: thisB, config: thisB.config, browser: thisB.browser });
array.forEach( thisB.glyphsBeingLoaded[glyphClassName], function( cb ) {
cb( glyph );
});
delete thisB.glyphsBeingLoaded[glyphClassName];
});
}
},
fillHistograms: function( args ) {
// set the track label if we have a description
if( this.config.histograms.description ) {
this.setLabel(
this.key + ' <span class="feature-density">('
+ this.config.histograms.description
+ ')</span>'
)
}
else {
this.setLabel(this.key)
}
const numBins = this.config.histograms.binsPerBlock || 25
const blockSizeBp = Math.abs( args.rightBase - args.leftBase )
const basesPerBin = blockSizeBp / numBins
const query = {
ref: this.refSeq.name,
start: args.leftBase,
end: args.rightBase,
basesPerSpan: basesPerBin,
basesPerBin: basesPerBin
}
if (!this.config.histograms.store && this.store.getRegionFeatureDensities) {
this.store.getRegionFeatureDensities(
query,
this._drawHistograms.bind(this,args),
)
} else {
const histData = { features: [], stats: {} }
const handleError = this._handleError.bind(this)
this.browser.getStore(
this.config.histograms.store,
histStore => {
histStore.getGlobalStats(
stats => {
histData.stats.max = stats.scoreMax
histStore.getFeatures(
query,
feature => {
histData.features.push(feature)
},
() => {
this._drawHistograms(args, histData)
args.finishCallback()
},
handleError
)
},
handleError
)
})
}
},
_scaleCanvas(c, pxWidth = c.width, pxHeight = c.height) {
let ctx = c.getContext('2d')
let ratio = Util.getResolution( ctx, this.browser.config.highResolutionMode );
c.width = pxWidth * ratio;
c.height = pxHeight * ratio;
c.style.width = pxWidth + 'px';
c.style.height = pxHeight + 'px';
// now scale the context to counter
// the fact that we've manually scaled
// our canvas element
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(ratio, ratio);
},
_drawHistograms: function( viewArgs, histData ) {
var maxScore = 'max' in this.config.histograms ? this.config.histograms.max : histData.stats.max;
// don't do anything if we don't know the score max
if( maxScore === undefined ) {
console.warn( 'no stats.max in hist data, not drawing histogram for block '+viewArgs.blockIndex );
return;
}
// don't do anything if we have no hist features
var features;
if(!( ( features = histData.features )
|| histData.bins && ( features = this._histBinsToFeatures( viewArgs, histData ) )
))
return;
var block = viewArgs.block;
var height = this.config.histograms.height;
var scale = viewArgs.scale;
var leftBase = viewArgs.leftBase;
var minVal = this.config.histograms.min;
domConstruct.empty( block.domNode );
var c = block.featureCanvas =
domConstruct.create(
'canvas',
{ height: height,
width: block.domNode.offsetWidth+1,
style: {
cursor: 'default',
height: height+'px',
position: 'absolute'
},
innerHTML: 'Your web browser cannot display this type of track.',
className: 'canvas-track canvas-track-histograms'
},
block.domNode
);
this.heightUpdate( height, viewArgs.blockIndex );
var ctx = c.getContext('2d');
// scale the canvas to work well with the various device pixel ratios
this._scaleCanvas(c)
ctx.fillStyle = this.config.histograms.color;
for( var i = 0; i<features.length; i++ ) {
var feature = features[i];
var barHeight = feature.get('score')/maxScore * height;
var barWidth = Math.ceil( ( feature.get('end')-feature.get('start') )*scale );
var barLeft = Math.round(( feature.get('start') - leftBase )*scale );
ctx.fillRect(
barLeft,
height-barHeight,
barWidth,
barHeight
);
if( barHeight > height ) {
ctx.fillStyle = this.config.histograms.clip_marker_color;
ctx.fillRect( barLeft, 0, barWidth, 3 );
ctx.fillStyle = this.config.histograms.color;
}
}
// make the y-axis scale for our histograms
this.makeHistogramYScale( height, minVal, maxScore );
},
_histBinsToFeatures: function( viewArgs, histData ) {
var bpPerBin = parseFloat( histData.stats.basesPerBin );
var leftBase = viewArgs.leftBase;
return array.map(
histData.bins,
function( bin, i ) {
return new SimpleFeature(
{ data: {
start: leftBase + i*bpPerBin,
end: leftBase + (i+1)*bpPerBin,
score: bin
}});
});
},
makeHistogramYScale: function( height, minVal, maxVal ) {
if( this.yscale_params
&& this.yscale_params.height == height
&& this.yscale_params.max == maxVal
&& this.yscale_params.min == minVal
)
return;
this.yscale_params = { height: height, min: minVal, max: maxVal };
this.makeYScale({ min: minVal, max: maxVal });
},
fillFeatures: function( args ) {
var blockIndex = args.blockIndex
var block = args.block
var blockWidthPx = block.domNode.offsetWidth
var scale = args.scale
var leftBase = args.leftBase
var rightBase = args.rightBase
var finishCallback = args.finishCallback
const fRects = []
// count of how many features are queued up to be laid out
let featuresInProgress = 0
// promise that resolved when all the features have gotten laid out by their glyphs
const featuresLaidOut = new Deferred()
// flag that tells when all features have been read from the
// store (not necessarily laid out yet)
let allFeaturesRead = false
const errorCallback = e => {
this._handleError(e, args)
finishCallback(e)
}
const layout = this._getLayout( scale )
// query for a slightly larger region than the block, so that
// we can draw any pieces of glyphs that overlap this block,
// but the feature of which does not actually lie in the block
// (long labels that extend outside the feature's bounds, for
// example)
const bpExpansion = Math.round( this.config.maxFeatureGlyphExpansion / scale )
const region = {
ref: this.refSeq.name,
start: Math.max( 0, leftBase - bpExpansion ),
end: rightBase + bpExpansion
}
const featCallback = feature => {
if( this.destroyed || ! this.filterFeature( feature ) )
return
fRects.push(null) // put a placeholder in the fRects array
featuresInProgress++
var rectNumber = fRects.length-1
// get the appropriate glyph object to render this feature
this.getGlyph(
args,
feature,
glyph => {
// have the glyph attempt
// to add a rendering of
// this feature to the
// layout
var fRect = glyph.layoutFeature(
args,
layout,
feature
);
if( fRect === null ) {
// could not lay out, would exceed our configured maxHeight
// mark the block as exceeding the max height
block.maxHeightExceeded = true;
}
else {
// laid out successfully
if( !( fRect.l >= blockWidthPx || fRect.l+fRect.w < 0 ) )
fRects[rectNumber] = fRect;
}
// this might happen after all the features have been sent from the store
if( ! --featuresInProgress && allFeaturesRead ) {
featuresLaidOut.resolve();
}
},
errorCallback
)
}
this.store.getFeatures(
region,
featCallback,
// callback when all features sent
() => {
if( this.destroyed )
return
allFeaturesRead = true
if( ! featuresInProgress && ! featuresLaidOut.isFulfilled() ) {
featuresLaidOut.resolve()
}
featuresLaidOut.then( () => {
const totalHeight = layout.getTotalHeight()
const c = block.featureCanvas =
domConstruct.create(
'canvas',
{ height: totalHeight,
width: block.domNode.offsetWidth+1,
style: {
cursor: 'default',
height: totalHeight+'px',
position: 'absolute'
},
innerHTML: 'Your web browser cannot display this type of track.',
className: 'canvas-track'
},
block.domNode
)
const ctx = c.getContext('2d')
// scale the canvas to work well with the various device pixel ratios
this._scaleCanvas(c)
if (block.maxHeightExceeded)
this.markBlockHeightOverflow(block)
this.heightUpdate(totalHeight, blockIndex)
this.renderFeatures(args, fRects)
this.renderClickMap(args, fRects)
finishCallback()
})
},
errorCallback
)
},
startZoom: function() {
this.zooming = true
this.inherited( arguments );
array.forEach( this.blocks, function(b) {
try {
b.featureCanvas.style.width = '100%';
} catch(e) {};
});
},
endZoom: function() {
array.forEach( this.blocks, function(b) {
try {
delete b.featureCanvas.style.width;
} catch(e) {};
});
this.clear();
this.inherited( arguments );
this.zooming = false
},
renderClickMap: function( args, fRects ) {
var block = args.block;
// make an index of the fRects by ID, and by coordinate, and
// store it in the block
var index = new FRectIndex({ h: block.featureCanvas.height, w: block.featureCanvas.width });
block.fRectIndex = index;
index.addAll( fRects );
if( ! block.featureCanvas || ! block.featureCanvas.getContext('2d') ) {
console.warn( "No 2d context available from canvas" );
return;
}
this._attachMouseOverEvents( );
if( this.displayMode != 'collapsed' || !this.config.disableCollapsedClick ) {
// connect up the event handlers
this._connectEventHandlers( block );
}
this.updateStaticElements( { x: this.browser.view.getX() } );
},
_attachMouseOverEvents: function( ) {
var gv = this.browser.view;
var thisB = this;
if( this.displayMode == 'collapsed' && !this.config.enableCollapsedMouseover) {
if( this._mouseoverEvent ) {
this._mouseoverEvent.remove();
delete this._mouseoverEvent;
}
if( this._mouseoutEvent ) {
this._mouseoutEvent.remove();
delete this._mouseoutEvent;
}
} else if( this.displayMode != 'collapsed' || this.config.enableCollapsedMouseover ) {
if( !this._mouseoverEvent ) {
this._mouseoverEvent = this.own( on( this.staticCanvas, 'mousemove', function( evt ) {
evt = domEvent.fix( evt );
var bpX = gv.absXtoBp( evt.clientX );
var feature = thisB.layout.getByCoord( bpX, ( evt.offsetY === undefined ? evt.layerY : evt.offsetY ) );
thisB.mouseoverFeature( feature, evt );
}))[0];
}
if( !this._mouseoutEvent ) {
this._mouseoutEvent = this.own( on( this.staticCanvas, 'mouseout', function( evt) {
thisB.mouseoverFeature( undefined );
}))[0];
}
}
},
_makeLabelTooltip: function( ) {
if( !this.showTooltips || this.labelTooltip )
return;
var labelTooltip = this.labelTooltip = domConstruct.create(
'div', {
className: 'featureTooltip',
style: {
position: 'fixed',
display: 'none',
zIndex: 19
}
}, this.browser.container );
domConstruct.create(
'span', {
className: 'tooltipLabel',
style: {
display: 'block'
}
}, labelTooltip);
domConstruct.create(
'span', {
className: 'tooltipDescription',
style: {
display: 'block'
}
}, labelTooltip);
},
_connectEventHandlers: function( block ) {
for( var event in this.eventHandlers ) {
var handler = this.eventHandlers[event];
(function( event, handler ) {
var thisB = this;
block.own(
on( this.staticCanvas, event, function( evt ) {
evt = domEvent.fix( evt );
var bpX = thisB.browser.view.absXtoBp( evt.clientX );
if( block.containsBp( bpX ) ) {
var feature = thisB.layout.getByCoord( bpX, ( evt.offsetY === undefined ? evt.layerY : evt.offsetY ) );
if( feature ) {
var fRect = block.fRectIndex.getByID( feature.id() );
handler.call({
track: thisB,
feature: feature,
fRect: fRect,
block: block,
callbackArgs: [ thisB, feature, fRect ]
},
feature,
fRect,
block,
thisB,
evt
);
}
}
})
);
}).call( this, event, handler );
}
},
getRenderingContext: function( viewArgs ) {
if( ! viewArgs.block || ! viewArgs.block.featureCanvas )
return null;
try {
var ctx = viewArgs.block.featureCanvas.getContext('2d');
// ctx.translate( viewArgs.block.offsetLeft - this.featureCanvas.offsetLeft, 0 );
// console.log( viewArgs.blockIndex, 'block offset', viewArgs.block.offsetLeft - this.featureCanvas.offsetLeft );
return ctx;
} catch(e) {
console.error(e, e.stack);
return null;
}
},
// draw the features on the canvas
renderFeatures: function( args, fRects ) {
var context = this.getRenderingContext( args );
if( context ) {
var thisB = this;
array.forEach( fRects, function( fRect ) {
if( fRect )
thisB.renderFeature( context, fRect );
});
}
},
// given viewargs and a feature object, highlight that feature in
// all blocks. if feature is undefined or null, unhighlight any currently
// highlighted feature
mouseoverFeature: function( feature, evt ) {
if( this.lastMouseover == feature )
return;
if( evt )
var bpX = this.browser.view.absXtoBp( evt.clientX );
if( this.labelTooltip)
this.labelTooltip.style.display = 'none';
array.forEach( this.blocks, function( block, i ) {
if( ! block )
return;
var context = this.getRenderingContext({ block: block, leftBase: block.startBase, scale: block.scale });
if( ! context )
return;
if( this.lastMouseover && block.fRectIndex ) {
var r = block.fRectIndex.getByID( this.lastMouseover.id() );
if( r )
this.renderFeature( context, r );
}
if( block.tooltipTimeout )
window.clearTimeout( block.tooltipTimeout );
if( feature ) {
var fRect = block.fRectIndex && block.fRectIndex.getByID( feature.id() );
if( ! fRect )
return;
if( block.containsBp( bpX ) ) {
var renderTooltip = dojo.hitch( this, function() {
if( !this.labelTooltip )
return;
var label = fRect.label || fRect.glyph.makeFeatureLabel( feature );
var description = fRect.description || fRect.glyph.makeFeatureDescriptionLabel( feature );
if( ( !label && !description ) )
return;
if( !this.ignoreTooltipTimeout ) {
this.labelTooltip.style.left = evt.clientX + "px";
this.labelTooltip.style.top = (evt.clientY + 15) + "px";
}
this.ignoreTooltipTimeout = true;
this.labelTooltip.style.display = 'block';
var labelSpan = this.labelTooltip.childNodes[0],
descriptionSpan = this.labelTooltip.childNodes[1];
if( this.config.onClick&&this.config.onClick.label ) {
var context = lang.mixin( { track: this, feature: feature, callbackArgs: [ this, feature ] } );
labelSpan.style.display = 'block';
labelSpan.style.font = label.font;
labelSpan.style.color = label.fill;
labelSpan.innerHTML = this.template( feature, this._evalConf( context, this.config.onClick.label, "label" ) );
return;
}
if( label ) {
labelSpan.style.display = 'block';
labelSpan.style.font = label.font;
labelSpan.style.color = label.fill;
labelSpan.innerHTML = label.text;
} else {
labelSpan.style.display = 'none';
labelSpan.innerHTML = '(no label)';
}
if( description ) {
descriptionSpan.style.display = 'block';
descriptionSpan.style.font = description.font;
descriptionSpan.style.color = description.fill;
descriptionSpan.innerHTML = description.text;
}
else {
descriptionSpan.style.display = 'none';
descriptionSpan.innerHTML = '(no description)';
}
});
if( this.ignoreTooltipTimeout )
renderTooltip();
else
block.tooltipTimeout = window.setTimeout( renderTooltip, 600);
}
fRect.glyph.mouseoverFeature( context, fRect );
this._refreshContextMenu( fRect );
} else {
block.tooltipTimeout = window.setTimeout( dojo.hitch(this, function() { this.ignoreTooltipTimeout = false; }), 200);
}
}, this );
this.lastMouseover = feature;
},
cleanupBlock: function(block) {
this.inherited( arguments );
// garbage collect the layout
if ( block && this.layout )
this.layout.discardRange( block.startBase, block.endBase );
},
// draw each feature
renderFeature: function( context, fRect ) {
fRect.glyph.renderFeature( context, fRect );
},
_trackMenuOptions: function () {
var opts = this.inherited(arguments);
var thisB = this;
var displayModeList = ["normal", "compact", "collapsed"];
this.displayModeMenuItems = displayModeList.map(function(displayMode) {
return {
label: displayMode,
type: 'dijit/CheckedMenuItem',
title: "Render this track in " + displayMode + " mode",
checked: thisB.displayMode == displayMode,
onClick: function() {
thisB.displayMode = displayMode;
thisB._clearLayout();
thisB.hideAll();
thisB.genomeView.showVisibleBlocks(true);
thisB.makeTrackMenu();
// set cookie for displayMode
thisB.browser.cookie('track-' + thisB.name, thisB.displayMode);
}
};
});
var updateMenuItems = dojo.hitch(this, function() {
for(var index in this.displayModeMenuItems) {
this.displayModeMenuItems[index].checked = (this.displayMode == this.displayModeMenuItems[index].label);
}
});
opts.push.apply(
opts,
[
{ type: 'dijit/MenuSeparator' },
{
label: "Display mode",
iconClass: "dijitIconPackage",
title: "Make features take up more or less space",
children: this.displayModeMenuItems
},
{ label: 'Show labels',
type: 'dijit/CheckedMenuItem',
checked: !!( 'showLabels' in this ? this.showLabels : this.config.style.showLabels ),
onClick: function(event) {
thisB.showLabels = this.checked;
thisB.changed();
}
}
]
);
return opts;
},
_exportFormats: function() {
return [ {name: 'GFF3', label: 'GFF3', fileExt: 'gff3'}, {name: 'BED', label: 'BED', fileExt: 'bed'}, { name: 'SequinTable', label: 'Sequin Table', fileExt: 'sqn' } ];
},
updateStaticElements: function( coords ) {
this.inherited( arguments );
this.updateYScaleFromViewDimensions( coords );
if( coords.hasOwnProperty("x") ) {
var context = this.staticCanvas.getContext('2d');
let ratio = Util.getResolution( context, this.browser.config.highResolutionMode );
this.staticCanvas.width = this.browser.view.elem.clientWidth*ratio;
this.staticCanvas.style.width = this.browser.view.elem.clientWidth + "px";
this.staticCanvas.style.left = coords.x + "px";
context.setTransform(1,0,0,1,0,0)
context.scale(ratio,ratio)
context.clearRect(0, 0, this.staticCanvas.width, this.staticCanvas.height);
var minVisible = this.browser.view.minVisible();
var maxVisible = this.browser.view.maxVisible();
var viewArgs = {
minVisible: minVisible,
maxVisible: maxVisible,
bpToPx: dojo.hitch(this.browser.view, "bpToPx"),
lWidth: this.label.offsetWidth
};
this.blocks.forEach(block => {
if( !block || !block.fRectIndex || this.zooming )
return;
var idx = block.fRectIndex.byID;
for( var id in idx ) {
var fRect = idx[id];
fRect.glyph.updateStaticElements( context, fRect, viewArgs );
}
});
}
},
heightUpdate: function( height, blockIndex ) {
this.inherited( arguments );
if( this.staticCanvas ) {
let ratio = Util.getResolution( this.staticCanvas.getContext('2d'), this.browser.config.highResolutionMode );
this.staticCanvas.height = this.staticCanvas.offsetHeight*ratio;
}
},
destroy: function() {
this.destroyed = true;
domConstruct.destroy( this.staticCanvas );
delete this.staticCanvas;
delete this.layout;
delete this.glyphsLoaded;
this.inherited( arguments );
}
});
});