@gmod/jbrowse
Version:
JBrowse - client-side genome browser
348 lines (297 loc) • 12.9 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/lang',
'JBrowse/Util/FastPromise',
'JBrowse/View/FeatureGlyph',
'./_FeatureLabelMixin',
'JBrowse/Util',
],
function(
declare,
lang,
FastPromise,
FeatureGlyph,
FeatureLabelMixin,
Util,
) {
return declare([ FeatureGlyph, FeatureLabelMixin], {
constructor: function() {
this._embeddedImagePromises = {};
},
_defaultConfig: function() {
return this._mergeConfigs(
this.inherited(arguments),
{
style: {
maxDescriptionLength: 70,
color: 'goldenrod',
mouseovercolor: 'rgba(0,0,0,0.3)',
borderColor: null,
borderWidth: 0.5,
height: 11,
marginBottom: 2,
strandArrow: true,
label: 'name, id',
textFont: 'normal 12px Univers,Helvetica,Arial,sans-serif',
textColor: 'black',
text2Color: 'blue',
text2Font: 'normal 12px Univers,Helvetica,Arial,sans-serif',
description: 'note, description'
}
});
},
_getFeatureHeight: function( viewArgs, feature ) {
var h = this.getStyle( feature, 'height');
if( viewArgs.displayMode == 'compact' )
h = Math.round( 0.45 * h );
if( this.getStyle( feature, 'strandArrow' ) ) {
var strand = feature.get('strand');
if( strand == 1 )
h = Math.max( this._embeddedImages.plusArrow.height, h );
else if( strand == -1 )
h = Math.max( this._embeddedImages.minusArrow.height, h );
}
return h;
},
_getFeatureRectangle: function( viewArgs, feature ) {
var block = viewArgs.block;
var fRect = {
l: block.bpToX( feature.get('start') ),
h: this._getFeatureHeight(viewArgs, feature),
viewInfo: viewArgs,
f: feature,
glyph: this
};
fRect.w = block.bpToX( feature.get('end') ) - fRect.l;
// save the original rect in `rect` as the dimensions
// we'll use for the rectangle itself
fRect.rect = { l: fRect.l, h: fRect.h, w: Math.max( fRect.w, 2 ), t: 0 };
fRect.w = fRect.rect.w; // in case it was increased
if( viewArgs.displayMode != 'compact' )
fRect.h += this.getStyle( feature, 'marginBottom' ) || 0
;
// if we are showing strand arrowheads, expand the frect a little
if( this.getStyle( feature, 'strandArrow') ) {
var strand = fRect.strandArrow = feature.get('strand');
if( strand == -1 ) {
var i = this._embeddedImages.minusArrow;
fRect.w += i.width;
fRect.l -= i.width;
}
else {
var i = this._embeddedImages.plusArrow;
fRect.w += i.width;
}
}
// no labels or descriptions if displayMode is collapsed, so stop here
if( viewArgs.displayMode == "collapsed")
return fRect;
this._expandRectangleWithLabels( viewArgs, feature, fRect );
this._addMasksToRect( viewArgs, feature, fRect );
return fRect;
},
layoutFeature: function( viewArgs, layout, feature ) {
var rect = this.inherited( arguments );
if( ! rect ) return rect;
// need to set the top of the inner rect
rect.rect.t = rect.t;
return rect;
},
// given an under-construction feature layout rectangle, expand it
// to accomodate a label and/or a description
_expandRectangleWithLabels: function( viewArgs, feature, fRect ) {
// maybe get the feature's name, and update the layout box
// accordingly
if( viewArgs.showLabels ) {
var label = this.makeFeatureLabel( feature, fRect );
if( label ) {
fRect.h += label.h;
fRect.w = Math.max( label.w, fRect.w );
fRect.label = label;
label.yOffset = fRect.rect.h + label.h;
}
}
// maybe get the feature's description if available, and
// update the layout box accordingly
if( viewArgs.showDescriptions ) {
var description = this.makeFeatureDescriptionLabel( feature, fRect );
if( description ) {
fRect.description = description;
fRect.h += description.h;
fRect.w = Math.max( description.w, fRect.w );
description.yOffset = fRect.h-(this.getStyle( feature, 'marginBottom' ) || 0);
}
}
},
_embeddedImages: {
plusArrow: {
data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAATUlEQVQIW2NkwATGQKFYIG4A4g8gacb///+7AWlBmNq+vj6V4uLiJiD/FRBXA/F8xu7u7kcVFRWyMEVATQz//v0Dcf9CxaYRZxIxbgIARiAhmifVe8UAAAAASUVORK5CYII=",
width: 9,
height: 5
},
minusArrow: {
data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAFCAYAAACXU8ZrAAAASklEQVQIW2NkQAABILMBiBcD8VkkcQZGIAeEE4G4FYjFent764qKiu4gKXoPUjAJiLOggsxMTEwMjIwgYQjo6Oh4TLRJME043QQA+W8UD/sdk9IAAAAASUVORK5CYII=",
width: 9,
height: 5
}
},
/**
* Returns a promise for an Image object for the image with the
* given name. Image data comes from a data URL embedded in this
* source code.
*/
getEmbeddedImage: function( name ) {
return (this._embeddedImagePromises[name] || function() {
var p = new FastPromise();
var imgRec = this._embeddedImages[ name ];
if( ! imgRec ) {
p.resolve( null );
}
else {
var i = new Image();
var thisB = this;
i.onload = function() {
p.resolve( this );
};
i.src = imgRec.data;
}
return this._embeddedImagePromises[name] = p;
}.call(this));
},
renderFeature: function( context, fRect ) {
if( this.track.displayMode != 'collapsed' )
context.clearRect( Math.floor(fRect.l), fRect.t, Math.ceil(fRect.w-Math.floor(fRect.l)+fRect.l), fRect.h );
this.renderBox( context, fRect.viewInfo, fRect.f, fRect.t, fRect.rect.h, fRect.f );
this.renderLabel( context, fRect );
this.renderDescription( context, fRect );
this.renderArrowhead( context, fRect );
},
// top and height are in px
renderBox: function( context, viewInfo, feature, top, overallHeight, parentFeature, style ) {
var left = viewInfo.block.bpToX( feature.get('start') );
var width = viewInfo.block.bpToX( feature.get('end') ) - left;
//left = Math.round( left );
//width = Math.round( width );
style = style || lang.hitch( this, 'getStyle' );
var height = this._getFeatureHeight( viewInfo, feature );
if( ! height )
return;
if( height != overallHeight )
top += Math.round( (overallHeight - height)/2 );
// background
var bgcolor = style( feature, 'color' );
if( bgcolor ) {
context.fillStyle = bgcolor;
context.fillRect( left, top, Math.max(1,width), height );
}
else {
context.clearRect( left, top, Math.max(1,width), height );
}
// foreground border
var borderColor, lineWidth;
if( (borderColor = style( feature, 'borderColor' )) && ( lineWidth = style( feature, 'borderWidth')) ) {
if( width > 3 ) {
context.lineWidth = lineWidth;
context.strokeStyle = borderColor;
// need to stroke a smaller rectangle to remain within
// the bounds of the feature's overall height and
// width, because of the way stroking is done in
// canvas. thus the +0.5 and -1 business.
context.strokeRect( left+lineWidth/2, top+lineWidth/2, width-lineWidth, height-lineWidth );
}
else {
context.globalAlpha = lineWidth*2/width;
context.fillStyle = borderColor;
context.fillRect( left, top, Math.max(1,width), height );
context.globalAlpha = 1;
}
}
},
// feature label is handled by updateStaticElements
renderLabel: function( context, fRect ) {
},
// feature description is handled by updateStaticElements
renderDescription: function( context, fRect ) {
},
// strand arrowhead is sometimes drawn normally, sometimes *also* as a static element
renderArrowhead: function( context, fRect ) {
if( fRect.strandArrow ) {
if( fRect.strandArrow == 1 && fRect.rect.l+fRect.rect.w <= context.canvas.width ) {
this.getEmbeddedImage( 'plusArrow' )
.then( function( img ) {
context.imageSmoothingEnabled = false;
context.drawImage( img, fRect.rect.l + fRect.rect.w, fRect.t + (fRect.rect.h-img.height)/2 );
});
}
else if( fRect.strandArrow == -1 && fRect.rect.l >= 0 ) {
this.getEmbeddedImage( 'minusArrow' )
.then( function( img ) {
context.imageSmoothingEnabled = false;
context.drawImage( img, fRect.rect.l-9, fRect.t + (fRect.rect.h-img.height)/2 );
});
}
}
},
updateStaticElements( context, fRect, viewArgs ) {
const vMin = viewArgs.minVisible
const vMax = viewArgs.maxVisible
const block = fRect.viewInfo.block
const bpToPx = viewArgs.bpToPx
const feature = fRect.f
const fMin = feature.get('start')
const fMax = feature.get('end')
const bMin = block.startBase
const bMax = block.endBase
if( fRect.strandArrow ) {
if( fRect.strandArrow == 1 && fMax >= vMax && fMin <= vMax ) {
this.getEmbeddedImage( 'plusArrow' )
.then( function( img ) {
context.imageSmoothingEnabled = false;
context.drawImage( img, bpToPx(vMax) - bpToPx(vMin) - 9, fRect.t + (fRect.rect.h-img.height)/2 );
});
}
else if( fRect.strandArrow == -1 && fMin <= vMin && fMax >= vMin ) {
this.getEmbeddedImage( 'minusArrow' )
.then( function( img ) {
context.imageSmoothingEnabled = false;
context.drawImage( img, 0, fRect.t + (fRect.rect.h-img.height)/2 );
});
}
}
// if the feature is within the view and within this block
if (fMin < vMax && fMax > vMin && fMin < bMax && fMax > bMin) {
let fRectLeft = fRect.l+bpToPx(block.startBase-vMin+1)
let clamp = (val,min,max) => Math.min(Math.max(val,min),max)
let renderText = fLabelRecord => {
let maxLabelLeft = fRectLeft+fRect.w-fLabelRecord.w
let labelTop = fRect.t+(fLabelRecord.yOffset||0)
let labelLeft = fRectLeft+(fLabelRecord.xOffset||0)
labelLeft = clamp(labelLeft,0,maxLabelLeft)
context.font = fLabelRecord.font
context.fillStyle = fLabelRecord.fill
context.textBaseline = fLabelRecord.baseline
let clearTop;
if (fLabelRecord.baseline === 'bottom') {
clearTop = labelTop-fLabelRecord.h
} else if (fLabelRecord.baseline === 'top') {
clearTop = labelTop
} else if (fLabelRecord.baseline === 'middle') {
clearTop = labelTop-fLabelRecord.h/2
}
if (clearTop) context.clearRect(labelLeft,clearTop,fLabelRecord.w,fLabelRecord.h)
context.fillText(
fLabelRecord.text,
labelLeft,
labelTop
)
}
if (fRect.label) {
renderText(fRect.label)
}
if (fRect.description) {
renderText(fRect.description)
}
}
}
});
});