@gmod/jbrowse
Version:
JBrowse - client-side genome browser
1,288 lines (1,126 loc) • 46.9 kB
JavaScript
define( [
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/_base/array',
'dojo/json',
'dojo/aspect',
'dojo/dom-construct',
'dojo/dom-geometry',
'dojo/dom-class',
'dojo/dom-style',
'dojo/query',
'dojo/on',
'dojo/when',
'dijit/Destroyable',
'JBrowse/View/InfoDialog',
'dijit/Dialog',
'dijit/Menu',
'dijit/PopupMenuItem',
'dijit/MenuItem',
'dijit/CheckedMenuItem',
'dijit/MenuSeparator',
'dijit/RadioMenuItem',
'JBrowse/Util',
'JBrowse/Component',
'JBrowse/FeatureFiltererMixin',
'JBrowse/Errors',
'JBrowse/Model/Location',
'JBrowse/View/TrackConfigEditor',
'JBrowse/View/ConfirmDialog',
'JBrowse/View/Track/BlockBased/Block',
'JBrowse/View/DetailsMixin'
],
function( declare,
lang,
array,
JSON,
aspect,
domConstruct,
domGeom,
domClass,
domStyle,
query,
on,
when,
Destroyable,
InfoDialog,
Dialog,
dijitMenu,
dijitPopupMenuItem,
dijitMenuItem,
dijitCheckedMenuItem,
dijitMenuSeparator,
dijitRadioMenuItem,
Util,
Component,
FeatureFiltererMixin,
Errors,
Location,
TrackConfigEditor,
ConfirmDialog,
Block,
DetailsMixin
) {
// we get `own` and `destroy` from Destroyable, see dijit/Destroyable docs
return declare( [Component,DetailsMixin,FeatureFiltererMixin,Destroyable],
/**
* @lends JBrowse.View.Track.BlockBased.prototype
*/
{
/**
* Base class for all JBrowse tracks.
* @constructs
*/
constructor: function( args ) {
args = args || {};
this.refSeq = args.refSeq;
this.name = args.label || this.config.label;
this.key = args.key || this.config.key || this.name;
this._changedCallback = args.changeCallback || function(){};
this.height = 0;
this.shown = true;
this.empty = false;
this.browser = args.browser;
this.setFeatureFilterParentComponent( this.browser.view );
this.store = args.store;
// retrieve any user-set style info
lang.mixin( this.config.style, this.getUserStyles() );
},
// get/set persistent per-user style information for this track
updateUserStyles: function( settings ) {
// set in this object
lang.mixin( this.config.style, settings );
// set in the saved style
var saved = JSON.parse( this.browser.cookie("track-style-" + this.name ) || '{}' );
lang.mixin( saved, settings );
this.browser.cookie( "track-style-" + this.name, saved );
// redraw this track
this.redraw();
},
getUserStyles: function() {
return JSON.parse( this.browser.cookie("track-style-" + this.name ) || '{}' );
},
/**
* Returns object holding the default configuration for this track
* type. Might want to override in subclasses.
* @private
*/
_defaultConfig: function() {
return {
maxFeatureSizeForUnderlyingRefSeq: 250000,
subfeatureDetailLevel: 2
};
},
heightUpdate: function(height, blockIndex) {
if (!this.shown) {
this.heightUpdateCallback(0);
return;
}
if (blockIndex !== undefined)
this.blockHeights[blockIndex] = height;
this.height = Math.max( this.height, height );
if ( ! this.inShowRange ) {
this.heightUpdateCallback( Math.max( this.labelHeight, this.height ) );
// reposition any height-overflow markers in our blocks
query( '.height_overflow_message', this.div )
.style( 'top', this.height - 16 + 'px' );
}
},
setViewInfo: function( genomeView, heightUpdate, numBlocks,
trackDiv,
widthPct, widthPx, scale) {
this.genomeView = genomeView;
this.heightUpdateCallback = heightUpdate;
this.div = trackDiv;
this.widthPct = widthPct;
this.widthPx = widthPx;
this.leftBlank = document.createElement("div");
this.leftBlank.className = "blank-block";
this.rightBlank = document.createElement("div");
this.rightBlank.className = "blank-block";
this.div.appendChild(this.rightBlank);
this.div.appendChild(this.leftBlank);
this.sizeInit(numBlocks, widthPct);
this.labelHTML = "";
this.labelHeight = 0;
if( this.config.pinned )
this.setPinned( true );
if( ! this.label ) {
this.makeTrackLabel();
}
this.setLabel( this.key );
},
makeTrackLabel: function() {
var params = {
className: "track-label dojoDndHandle",
id: "label_" + this.name,
style: {
position: 'absolute'
}
};
if (typeof this.browser.config.trackLabels !== 'undefined' && this.browser.config.trackLabels==='no-block') {
params.style.top = "-30px";
}
var labelDiv = dojo.create(
'div', params ,this.div);
this.label = labelDiv;
if ( ( this.config.style || {} ).trackLabelCss){
labelDiv.style.cssText += ";" + this.config.style.trackLabelCss;
}
var closeButton = dojo.create('div',{
className: 'track-close-button'
},labelDiv);
this.own( on( closeButton, 'click', dojo.hitch(this,function(evt){
this.browser.view.suppressDoubleClick( 100 );
this.browser.publish( '/jbrowse/v1/v/tracks/hide', [this.config]);
evt.stopPropagation();
})));
var labelText = dojo.create('span', { className: 'track-label-text' }, labelDiv );
var menuButton = dojo.create('div',{
className: 'track-menu-button'
},labelDiv);
dojo.create('div', {}, menuButton ); // will be styled with an icon by CSS
this.labelMenuButton = menuButton;
// make the track menu with things like 'save as'
this.makeTrackMenu();
},
hide: function() {
if (this.shown) {
this.div.style.display = "none";
this.shown = false;
}
},
show: function() {
if (!this.shown) {
this.div.style.display = "block";
this.shown = true;
}
},
initBlocks: function() {
this.blocks = new Array(this.numBlocks);
this.blockHeights = new Array(this.numBlocks);
for (var i = 0; i < this.numBlocks; i++) this.blockHeights[i] = 0;
this.firstAttached = null;
this.lastAttached = null;
this._adjustBlanks();
},
clear: function() {
if (this.blocks) {
for (var i = 0; i < this.numBlocks; i++)
this._hideBlock(i);
}
this.initBlocks();
this.makeTrackMenu();
},
setLabel: function(newHTML) {
if (this.label === undefined || this.labelHTML == newHTML )
return;
this.labelHTML = newHTML;
query('.track-label-text',this.label)
.forEach(function(n){ n.innerHTML = newHTML; });
this.labelHeight = this.label.offsetHeight;
},
/**
* Stub.
*/
transfer: function() {},
/**
* Stub.
*/
startZoom: function(destScale, destStart, destEnd) {},
/**
* Stub.
*/
endZoom: function(destScale, destBlockBases) {
},
showRange: function(first, last, startBase, bpPerBlock, scale,
containerStart, containerEnd, finishCallback) {
if( this.fatalError ) {
this.showFatalError( this.fatalError );
return;
}
if ( this.blocks === undefined || ! this.blocks.length )
return;
// this might make more sense in setViewInfo, but the label element
// isn't in the DOM tree yet at that point
if ((this.labelHeight == 0) && this.label)
this.labelHeight = this.label.offsetHeight;
this.inShowRange = true;
this.height = this.labelHeight;
var firstAttached = (null == this.firstAttached ? last + 1 : this.firstAttached);
var lastAttached = (null == this.lastAttached ? first - 1 : this.lastAttached);
var i, leftBase;
var maxHeight = 0;
var blockShowingPromises = [];
//fill left, including existing blocks (to get their heights)
for (i = lastAttached; i >= first; i--) {
leftBase = startBase + (bpPerBlock * (i - first));
blockShowingPromises.push( new Promise((resolve,reject) => {
this._showBlock(i, leftBase, leftBase + bpPerBlock, scale,
containerStart, containerEnd, resolve);
}))
}
//fill right
for (i = lastAttached + 1; i <= last; i++) {
leftBase = startBase + (bpPerBlock * (i - first));
blockShowingPromises.push( new Promise((resolve,reject) => {
this._showBlock(i, leftBase, leftBase + bpPerBlock, scale,
containerStart, containerEnd, resolve);
}))
}
// if we have a finishing callback, call it when we have finished all our _showBlock calls
if( finishCallback )
Promise.all(blockShowingPromises)
.then(finishCallback, finishCallback)
//detach left blocks
var destBlock = this.blocks[first];
for (i = firstAttached; i < first; i++) {
this.transfer(this.blocks[i], destBlock, scale,
containerStart, containerEnd);
this.cleanupBlock(this.blocks[i]);
this._hideBlock(i);
}
//detach right blocks
destBlock = this.blocks[last];
for (i = lastAttached; i > last; i--) {
this.transfer(this.blocks[i], destBlock, scale,
containerStart, containerEnd);
this.cleanupBlock(this.blocks[i]);
this._hideBlock(i);
}
this.firstAttached = first;
this.lastAttached = last;
this._adjustBlanks();
this.inShowRange = false;
this.heightUpdate(this.height);
this.updateStaticElements( this.genomeView.getPosition() );
},
cleanupBlock: function( block ) {
if( block )
block.destroy();
},
/**
* Called when this track object is destroyed. Cleans up things
* to avoid memory leaks.
*/
destroy: function() {
array.forEach( this.blocks || [], function( block ) {
this.cleanupBlock( block );
}, this);
delete this.blocks;
delete this.div;
this.inherited( arguments );
},
_hideBlock: function(blockIndex) {
if (this.blocks[blockIndex]) {
this.div.removeChild( this.blocks[blockIndex].domNode );
this.cleanupBlock( this.blocks[blockIndex] );
this.blocks[blockIndex] = undefined;
this.blockHeights[blockIndex] = 0;
}
},
_adjustBlanks: function() {
if ((this.firstAttached === null)
|| (this.lastAttached === null)) {
this.leftBlank.style.left = "0px";
this.leftBlank.style.width = "50%";
this.rightBlank.style.left = "50%";
this.rightBlank.style.width = "50%";
} else {
this.leftBlank.style.width = (this.firstAttached * this.widthPct) + "%";
this.rightBlank.style.left = ((this.lastAttached + 1)
* this.widthPct) + "%";
this.rightBlank.style.width = ((this.numBlocks - this.lastAttached - 1)
* this.widthPct) + "%";
}
},
hideAll: function() {
if (null == this.firstAttached) return;
for (var i = this.firstAttached; i <= this.lastAttached; i++)
this._hideBlock(i);
this.firstAttached = null;
this.lastAttached = null;
this._adjustBlanks();
},
// hides all blocks that overlap the given region/location
hideRegion: function( location ) {
if (null == this.firstAttached) return;
// hide all blocks that overlap the given region
for (var i = this.firstAttached; i <= this.lastAttached; i++)
if( this.blocks[i] && location.ref == this.refSeq.name && !( this.blocks[i].leftBase > location.end || this.blocks[i].rightBase < location.start ) )
this._hideBlock(i);
this._adjustBlanks();
},
/**
* _changeCallback invoked here is passed in constructor,
* and typically is GenomeView.showVisibleBlocks()
*/
changed: function() {
this.hideAll();
if( this._changedCallback )
this._changedCallback();
},
_makeLoadingMessage: function() {
var msgDiv = dojo.create(
'div', {
className: 'loading',
innerHTML: '<div class="text">Loading</span>',
title: 'Loading data...',
style: { visibility: 'hidden' }
});
window.setTimeout(function() { msgDiv.style.visibility = 'visible'; }, 200);
return msgDiv;
},
showFatalError: function( error ) {
query( '.block', this.div )
.concat( query( '.blank-block', this.div ) )
.concat( query( '.error', this.div ) )
.orphan();
this.blocks = [];
this.blockHeights = [];
this.fatalErrorMessageElement = this._renderErrorMessage( error || this.fatalError, this.div );
this.heightUpdate( domGeom.position( this.fatalErrorMessageElement ).h );
this.updateStaticElements( this.genomeView.getPosition() );
},
// generic handler for all types of errors
_handleError: function( error, viewArgs ) {
var errorContext = dojo.mixin( {}, error );
dojo.mixin( errorContext, viewArgs );
var isObject = typeof error == 'object';
if( isObject && error instanceof Errors.TimeOut && errorContext.block )
this.fillBlockTimeout( errorContext.blockIndex, errorContext.block, error );
else if( isObject && error instanceof Errors.DataOverflow ) {
if( errorContext.block )
this.fillTooManyFeaturesMessage( errorContext.blockIndex, errorContext.block, viewArgs.scale, error );
else
array.forEach( this.blocks, function( block, blockIndex ) {
if( block )
this.fillTooManyFeaturesMessage( blockIndex, block, viewArgs.scale, error );
},this);
}
else {
console.error( error.stack || ''+error, error );
this.fatalError = error;
this.showFatalError( error );
}
},
fillBlockError: function( blockIndex, block, error ) {
error = error || this.fatalError || this.error;
domConstruct.empty( block.domNode );
var msgDiv = this._renderErrorMessage( error, block.domNode );
this.heightUpdate( dojo.position(msgDiv).h, blockIndex );
},
_renderErrorMessage: function( message, parent ) {
return domConstruct.create(
'div', {
className: 'error',
innerHTML: '<h2>Error</h2><div class="text">An error was encountered when displaying this track.</div>'
+( message ? '<div class="codecaption">Diagnostic message</div><code>'+message+'</code>' : '' ),
title: 'An error occurred'
}, parent );
},
fillTooManyFeaturesMessage: function( blockIndex, block, scale, error ) {
var message = (error && error.message || 'Too much data to show').replace(/\.$/,'');
this.fillMessage(
blockIndex,
block,
message
+ (scale >= this.browser.view.maxPxPerBp ? '': '; zoom in to see detail')
+ '.'
);
},
redraw: function() {
this.clear();
this.genomeView.showVisibleBlocks(true);
},
markBlockHeightOverflow: function( block ) {
if( block.heightOverflowed )
return;
block.heightOverflowed = true;
domClass.add( block.domNode, 'height_overflow' );
domConstruct.create( 'div', {
className: 'height_overflow_message',
innerHTML: 'Max height reached',
style: {
top: (this.height-16) + 'px',
height: '16px'
}
}, block.domNode );
},
_showBlock: function(blockIndex, startBase, endBase, scale,
containerStart, containerEnd, finishCallback) {
if ( this.empty || this.fatalError ) {
this.heightUpdate( this.labelHeight );
if (finishCallback) finishCallback();
return;
}
if (this.blocks[blockIndex]) {
this.heightUpdate(this.blockHeights[blockIndex], blockIndex);
if (finishCallback) finishCallback();
return;
}
var block = new Block({
startBase: startBase,
endBase: endBase,
scale: scale,
node: {
className: 'block',
style: {
left: (blockIndex * this.widthPct) + "%",
width: this.widthPct + "%"
}
}
});
this.blocks[blockIndex] = block;
this.div.appendChild( block.domNode );
var args = [blockIndex,
block,
this.blocks[blockIndex - 1],
this.blocks[blockIndex + 1],
startBase,
endBase,
scale,
this.widthPx,
containerStart,
containerEnd];
if( this.fatalError ) {
this.fillBlockError( blockIndex, block );
if (finishCallback) finishCallback();
return;
}
// loadMessage is an opaque mask div that we place over the
// block until the fillBlock finishes
var loadMessage = this._makeLoadingMessage();
block.domNode.appendChild( loadMessage );
var finish = function() {
if( block && loadMessage.parentNode )
block.domNode.removeChild( loadMessage );
if( finishCallback )
finishCallback()
};
var viewargs = {
blockIndex: blockIndex,
block: block,
leftBlock: this.blocks[blockIndex - 1],
rightBlock: this.blocks[blockIndex + 1],
leftBase: startBase,
rightBase: endBase,
scale: scale,
stripeWidth: this.widthPx,
containerStart: containerStart,
containerEnd: containerEnd,
finishCallback: finish
};
try {
this.fillBlock( viewargs );
} catch( e ) {
this._handleError( e, viewargs );
finish();
}
},
moveBlocks: function(delta) {
var newBlocks = new Array(this.numBlocks);
var newHeights = new Array(this.numBlocks);
var i;
for (i = 0; i < this.numBlocks; i++)
newHeights[i] = 0;
var destBlock;
if ((this.lastAttached + delta < 0)
|| (this.firstAttached + delta >= this.numBlocks)) {
this.firstAttached = null;
this.lastAttached = null;
} else {
this.firstAttached = Math.max(0, Math.min(this.numBlocks - 1,
this.firstAttached + delta));
this.lastAttached = Math.max(0, Math.min(this.numBlocks - 1,
this.lastAttached + delta));
if (delta < 0)
destBlock = this.blocks[this.firstAttached - delta];
else
destBlock = this.blocks[this.lastAttached - delta];
}
for (i = 0; i < this.blocks.length; i++) {
var newIndex = i + delta;
if ((newIndex < 0) || (newIndex >= this.numBlocks)) {
//We're not keeping this block around, so delete
//the old one.
if (destBlock && this.blocks[i])
this.transfer(this.blocks[i], destBlock);
this._hideBlock(i);
} else {
//move block
newBlocks[newIndex] = this.blocks[i];
if (newBlocks[newIndex])
newBlocks[newIndex].domNode.style.left =
((newIndex) * this.widthPct) + "%";
newHeights[newIndex] = this.blockHeights[i];
}
}
this.blocks = newBlocks;
this.blockHeights = newHeights;
this._adjustBlanks();
},
sizeInit: function(numBlocks, widthPct, blockDelta) {
var i, oldLast;
this.numBlocks = numBlocks;
this.widthPct = widthPct;
if (blockDelta) this.moveBlocks(-blockDelta);
if (this.blocks && (this.blocks.length > 0)) {
//if we're shrinking, clear out the end blocks
var destBlock = this.blocks[numBlocks - 1];
for (i = numBlocks; i < this.blocks.length; i++) {
if (destBlock && this.blocks[i])
this.transfer(this.blocks[i], destBlock);
this._hideBlock(i);
}
oldLast = this.blocks.length;
this.blocks.length = numBlocks;
this.blockHeights.length = numBlocks;
//if we're expanding, set new blocks to be not there
for (i = oldLast; i < numBlocks; i++) {
this.blocks[i] = undefined;
this.blockHeights[i] = 0;
}
this.lastAttached = Math.min(this.lastAttached, numBlocks - 1);
if (this.firstAttached > this.lastAttached) {
//not sure if this can happen
this.firstAttached = null;
this.lastAttached = null;
}
if( this.blocks.length != numBlocks )
throw new Error(
"block number mismatch: should be "
+ numBlocks + "; blocks.length: "
+ this.blocks.length
);
for (i = 0; i < numBlocks; i++) {
if (this.blocks[i]) {
//if (!this.blocks[i].style) console.log(this.blocks);
this.blocks[i].domNode.style.left = (i * widthPct) + "%";
this.blocks[i].domNode.style.width = widthPct + "%";
}
}
} else {
this.initBlocks();
}
this.makeTrackMenu();
},
fillMessage: function( blockIndex, block, message, class_ ) {
domConstruct.empty( block.domNode );
var msgDiv = dojo.create(
'div', {
className: class_ || 'message',
innerHTML: message
}, block.domNode );
this.heightUpdate( domGeom.getMarginBox(msgDiv, domStyle.getComputedStyle(msgDiv)).h, blockIndex );
},
/**
* Called by GenomeView when the view is scrolled: communicates the
* new x, y, width, and height of the view. This is needed by tracks
* for positioning stationary things like axis labels.
*/
updateStaticElements: function( /**Object*/ coords ) {
this.window_info = dojo.mixin( this.window_info || {}, coords );
if( this.fatalErrorMessageElement ) {
this.fatalErrorMessageElement.style.width = this.window_info.width * 0.6 + 'px';
if( 'x' in coords )
this.fatalErrorMessageElement.style.left = coords.x+this.window_info.width * 0.2 +'px';
}
if( this.label && 'x' in coords )
this.label.style.left = coords.x+'px';
},
/**
* Render a dijit menu from a specification object.
*
* @param menuTemplate definition of the menu's structure
* @param context {Object} optional object containing the context
* in which any click handlers defined in the menu should be
* invoked, containing thing like what feature is being operated
* upon, the track object that is involved, etc.
* @param parent {dijit.Menu|...} parent menu, if this is a submenu
*/
_renderContextMenu: function( /**Object*/ menuStructure, /** Object */ context, /** dijit.Menu */ parent ) {
if ( !parent ) {
parent = new dijitMenu();
this.own( parent );
}
for ( var key in menuStructure ) {
var spec = menuStructure [ key ];
try {
if ( spec.children ) {
var child = new dijitMenu();
parent.addChild( child );
parent.addChild( new dijitPopupMenuItem(
{
popup : child,
label : spec.label
}));
this._renderContextMenu( spec.children, context, child );
}
else {
var menuConf = dojo.clone( spec );
if( menuConf.action || menuConf.url || menuConf.href ) {
menuConf.onClick = this._makeClickHandler( spec, context );
}
// only draw other menu items if they do something when clicked.
// drawing menu items that do nothing when clicked
// would frustrate users.
if( menuConf.label && !menuConf.onClick )
menuConf.disabled = true;
// currently can only use preloaded types
var class_ = {
'dijit/MenuItem': dijitMenuItem,
'dijit/CheckedMenuItem': dijitCheckedMenuItem,
'dijit/RadioMenuItem': dijitRadioMenuItem,
'dijit/MenuSeparator': dijitMenuSeparator
}[spec.type] || dijitMenuItem;
parent.addChild( new class_( menuConf ) );
}
} catch(e) {
console.error('failed to render menu item '+key,e);
}
}
return parent;
},
_makeClickHandler: function( inputSpec, context ) {
var track = this;
if( typeof inputSpec == 'function' ) {
inputSpec = { action: inputSpec };
}
else if( typeof inputSpec == 'undefined' ) {
console.error("Undefined click specification, cannot make click handler");
return function() {};
}
else if( inputSpec.action == 'defaultDialog' ) {
inputSpec.action = 'contentDialog';
inputSpec.content = dojo.hitch(this,'defaultFeatureDetail');
}
var handler = function ( evt ) {
if( track.genomeView.dragging )
return;
var ctx = context || this;
var spec = track._processMenuSpec( dojo.clone( inputSpec ), ctx );
var url = spec.url || spec.href;
spec.url = url;
var style = dojo.clone( spec.style || {} );
// try to understand the `action` setting
spec.action = spec.action ||
( url ? 'iframeDialog' :
spec.content ? 'contentDialog' :
false
);
spec.title = spec.title || spec.label;
if( typeof spec.action == 'string' ) {
// treat `action` case-insensitively
spec.action = {
iframedialog: 'iframeDialog',
iframe: 'iframeDialog',
contentdialog: 'contentDialog',
content: 'contentDialog',
baredialog: 'bareDialog',
bare: 'bareDialog',
xhrdialog: 'xhrDialog',
xhr: 'xhrDialog',
newwindow: 'newWindow',
"_blank": 'newWindow',
thiswindow: 'navigateTo',
navigateto: 'navigateTo'
}[(''+spec.action).toLowerCase()];
if( spec.action == 'newWindow' )
window.open( url, '_blank' );
else if( spec.action == 'navigateTo' )
window.location = url;
else if( spec.action in { iframeDialog:1, contentDialog:1, xhrDialog:1, bareDialog: 1} )
track._openDialog( spec, evt, ctx );
}
else if( typeof spec.action == 'function' ) {
spec.action.call( ctx, evt );
}
else {
return;
}
};
// if there is a label, set it on the handler so that it's
// accessible for tooltips or whatever.
if( inputSpec.label )
handler.label = inputSpec.label;
return handler;
},
/**
* @returns {Object} DOM element containing a rendering of the
* detailed metadata about this track
*/
_trackDetailsContent: function( additional ) {
var details = domConstruct.create('div', { className: 'detail' });
var fmt = lang.hitch(this, 'renderDetailField', details );
fmt( 'Name', this.key || this.name );
var metadata = lang.clone( this.getMetadata() );
delete metadata.key;
delete metadata.label;
if( typeof metadata.conf == 'object' )
delete metadata.conf;
if (this.browser && this.browser.config && this.browser.config.trackSelector && this.browser.config.trackSelector.renameFacets){
var metadataCopy = {};
for (var k in metadata){
var key = this.browser.config.trackSelector.renameFacets[k] || k;
metadataCopy[key] = metadata[k];
}
metadata = metadataCopy;
}
var md_keys = [];
for( var k in metadata ){
md_keys.push(k);
}
md_keys.sort(function (a, b) {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
for (var i = 0 ; i < md_keys.length ; i++ ){
var k = md_keys[i];
fmt(this.camelToTitleCase(k), metadata[k]);
}
for(var k in additional){
fmt(k, additional[k]);
}
return details;
},
camelToTitleCase: function(str){
if (str === str.toLowerCase()){
return Util.ucFirst(str.replace(/_/g, " "));
} else {
return str;
}
},
getMetadata: function() {
return this.config.metadata || this.browser && this.browser.trackMetaDataStore && this.browser.trackMetaDataStore.getItem(this.name) || {};
},
setPinned: function( p ) {
this.config.pinned = !!p;
if( this.config.pinned )
domClass.add( this.div, 'pinned' );
else
domClass.remove( this.div, 'pinned' );
return this.config.pinned;
},
isPinned: function() {
return !! this.config.pinned;
},
/**
* @returns {Array} menu options for this track's menu (usually contains save as, etc)
*/
_trackMenuOptions: function() {
var that = this;
return [
{ label: 'About this track',
title: 'About track: '+(this.key||this.name),
iconClass: 'jbrowseIconHelp',
action: 'contentDialog',
content: dojo.hitch(this,'_trackDetailsContent')
},
{ label: 'Pin to top',
type: 'dijit/CheckedMenuItem',
title: "make this track always visible at the top of the view",
checked: that.isPinned(),
//iconClass: 'dijitIconDelete',
onClick: function() {
that.browser.publish( '/jbrowse/v1/v/tracks/'+( this.checked ? 'pin' : 'unpin' ), [ that.name ] );
}
},
{ label: 'Edit config',
title: "edit this track's configuration",
iconClass: 'dijitIconConfigure',
action: function() {
new TrackConfigEditor( that.config )
.show( function( result ) {
// replace this track's configuration
that.browser.publish( '/jbrowse/v1/v/tracks/replace', [result.conf] );
});
}
},
{ label: 'Delete track',
title: "delete this track",
iconClass: 'dijitIconDelete',
action: function() {
new ConfirmDialog({ title: 'Delete track?', message: 'Really delete this track?' })
.show( function( confirmed ) {
if( confirmed )
that.browser.publish( '/jbrowse/v1/v/tracks/delete', [that.config] );
});
}
}
];
},
_processMenuSpec: function( spec, context ) {
for( var x in spec ) {
if( spec.hasOwnProperty(x) ) {
if( typeof spec[x] == 'object' )
spec[x] = this._processMenuSpec( spec[x], context );
else
spec[x] = this.template( context.feature, this._evalConf( context, spec[x], x ) );
}
}
return spec;
},
/**
* Get the value of a conf variable, evaluating it if it is a
* function. Note: does not template it, that is a separate step.
*
* @private
*/
_evalConf: function( context, confVal, confKey ) {
// list of conf vals that should not be run immediately on the
// feature data if they are functions
var dontRunImmediately = {
action: 1,
click: 1,
content: 1
};
return typeof confVal == 'function' && !dontRunImmediately[confKey]
? confVal.apply( context, context.callbackArgs || [] )
: confVal;
},
/**
* Like getConf, but get a conf value that explicitly can vary
* feature by feature. Provides a uniform function signature for
* user-defined callbacks.
*/
getConfForFeature: function( path, feature ) {
return this.getConf( path, [feature, path, null, null, this ] );
},
isFeatureHighlighted: function( feature, name ) {
var highlight = this.browser.getHighlight();
return highlight
&& ( highlight.objectName && highlight.objectName == name )
&& highlight.ref == this.refSeq.name
&& !( feature.get('start') > highlight.end || feature.get('end') < highlight.start );
},
_openDialog: function( spec, evt, context ) {
context = context || {};
var type = spec.action;
type = type.replace(/Dialog/,'');
var featureName = context.feature && (context.feature.get('name')||context.feature.get('id'));
var dialogOpts = {
"class": "popup-dialog popup-dialog-"+type,
title: spec.title || spec.label || ( featureName ? featureName +' details' : "Details"),
style: dojo.clone( spec.style || {} )
};
if( spec.dialog )
declare.safeMixin( dialogOpts, spec.dialog );
var dialog;
function setContent( dialog, content ) {
// content can be a promise or Deferred
if( typeof content.then == 'function' )
content.then( function( c ) { dialog.set( 'content', c ); } );
// or maybe it's just a regular object
else
dialog.set( 'content', content );
}
// if dialog == xhr, open the link in a dialog
// with the html from the URL just shoved in it
if( type == 'xhr' || type == 'content' ) {
if( type == 'xhr' )
dialogOpts.href = spec.url;
dialog = new InfoDialog( dialogOpts );
context.dialog = dialog;
if( type == 'content' )
setContent( dialog, this._evalConf( context, spec.content, null ) );
Util.removeAttribute( context, 'dialog' );
}
else if( type == 'bare' ) {
dialog = new Dialog( dialogOpts );
context.dialog = dialog;
setContent( dialog, this._evalConf( context, spec.content, null ) );
Util.removeAttribute( context, 'dialog' );
}
// open the link in a dialog with an iframe
else if( type == 'iframe' ) {
var iframeDims = function() {
var d = domGeom.position( this.browser.container );
return { h: Math.round(d.h * 0.8), w: Math.round( d.w * 0.8 ) };
}.call(this);
dialog = new Dialog( dialogOpts );
var iframe = dojo.create(
'iframe', {
tabindex: "0",
width: iframeDims.w,
height: iframeDims.h,
style: { border: 'none' },
src: spec.url
});
dialog.set( 'content', iframe );
if(!spec.hideIframeDialogUrl) {
dojo.create( 'a', {
href: spec.url,
target: '_blank',
className: 'dialog-new-window',
title: 'open in new window',
onclick: dojo.hitch(dialog,'hide'),
innerHTML: spec.url
}, dialog.titleBar );
}
var updateIframeSize = function() {
// hitch a ride on the dialog box's
// layout function, which is called on
// initial display, and when the window
// is resized, to keep the iframe
// sized to fit exactly in it.
var cDims = domGeom.position( dialog.containerNode );
var width = cDims.w;
var height = cDims.h - domGeom.position(dialog.titleBar).h;
iframe.width = width;
iframe.height = height;
};
aspect.after( dialog, 'layout', updateIframeSize );
aspect.after( dialog, 'show', updateIframeSize );
}
// destroy the dialog after it is hidden
aspect.after( dialog, 'hide', function() {
setTimeout(function() {
dialog.destroyRecursive();
}, 500 );
});
// show the dialog
dialog.show();
},
/**
* Given a string with template callouts, interpolate them with
* data from the given object. For example, "{foo}" is replaced
* with whatever is returned by obj.get('foo')
*/
template: function( /** Object */ obj, /** String */ template ) {
if( typeof template != 'string' || !obj )
return template;
var valid = true;
if ( template ) {
return template.replace(
/\{([^}]+)\}/g,
function(match, group) {
var val = obj ? obj.get( group.toLowerCase() ) : undefined;
if (val !== undefined)
return val;
else {
return '';
}
});
}
return undefined;
},
/**
* Makes and installs the dropdown menu showing operations available for this track.
* @private
*/
makeTrackMenu: function() {
var thisB = this;
when( this._trackMenuOptions() )
.then( function( options ) {
if( options && options.length && thisB.label && thisB.labelMenuButton ) {
// remove our old track menu if we have one
if( thisB.trackMenu )
thisB.trackMenu.destroyRecursive();
// render and bind our track menu
var menu = thisB._renderContextMenu( options, { menuButton: thisB.labelMenuButton, track: thisB, browser: thisB.browser, refSeq: thisB.refSeq } );
menu.startup();
menu.set('leftClickToOpen', true );
menu.bindDomNode( thisB.labelMenuButton );
menu.set('leftClickToOpen', false);
menu.bindDomNode( thisB.label );
thisB.trackMenu = menu;
thisB.own( thisB.trackMenu );
}
});
},
// display a rendering-timeout message
fillBlockTimeout: function( blockIndex, block ) {
domConstruct.empty( block.domNode );
domClass.add( block.domNode, 'timed_out' );
this.fillMessage( blockIndex, block,
'This region took too long'
+ ' to display, possibly because'
+ ' it contains too much data.'
+ ' Try zooming in to show a smaller region.'
);
},
renderRegionBookmark: function( args, bookmarks, renderLabels ) {
var thisB=this;
if( bookmarks.then ) {
bookmarks.then(
function( books ) {
array.forEach( books.features, function( bookmark ) {
if( bookmark.ref != this.refSeq.name ) return;
var loc = new Location( bookmark.refseq+":"+bookmark.start+".."+bookmark.end );
this.renderRegionHighlight( args, loc, bookmark.color, renderLabels?bookmark.label:null, renderLabels?bookmark.rlabel:null );
}, thisB);
},
function(error) {
console.log("Couldn't get bookmarks");
}
);
}
else {
array.forEach( bookmarks.features, function( bookmark ) {
if( bookmark.ref != this.refSeq.name ) return;
var loc = new Location( bookmark.refseq+":"+bookmark.start+".."+bookmark.end );
this.renderRegionHighlight( args, loc, bookmark.color, renderLabels?bookmark.label:null, renderLabels?bookmark.rlabel:null );
}, this);
}
},
renderRegionHighlight: function( args, highlight, color, label, rlabel ) {
// do nothing if the highlight does not overlap this region
if( highlight.start > args.rightBase || highlight.end < args.leftBase )
return;
var block_span = args.rightBase - args.leftBase;
var left = highlight.start;
var right = highlight.end;
// trim left and right to avoid making a huge element that can cause problems
var trimLeft = args.leftBase - left;
if( trimLeft > 0 ) {
left += trimLeft;
}
var trimRight = right - args.rightBase;
if( trimRight > 0 ) {
right -= trimRight;
}
var width = (right-left)*100/block_span;
left = (left - args.leftBase)*100/block_span;
var highlight=domConstruct.create('div', {
className: (color?'global_highlight_mod':'global_highlight')
+ (trimLeft <= 0 ? ' left' : '')
+ (trimRight <= 0 ? ' right' : '' ),
style: {
left: left+'%',
width: width+'%',
height: '100%',
background: color
}
}, args.block.domNode );
this.postRenderHighlight(highlight);
if( label ) {
/*
// vertical text, has bugs
if( trimLeft <= 0 ) {
domConstruct.create('div', { className:'verticaltext', style: { top: '50px', left: left+'%',transformOrigin: left+'%'+' top' }, innerHTML: label }, args.block.domNode);
}
if( trimRight <= 0 ) {
domConstruct.create('div', { className:'verticaltext', style: { top: '50px', left: left+width+'%',transformOrigin: left+width+'%'+' top' }, innerHTML: rlabel }, args.block.domNode);
}*/
if( trimLeft <= 0 ) {
var d1=domConstruct.create('div', { className:'horizontaltext', style: { background: 'white', zIndex: 1000, left: left+'%' }, innerHTML: label }, args.block.domNode);
}
if( trimRight <= 0 ) {
var d2=domConstruct.create('div', { className:'horizontaltext', style: { background: 'white', zIndex: 1000, left: left+width+'%' }, innerHTML: rlabel }, args.block.domNode);
}
var textWidth = (d1.clientWidth + 1) + "px";
d1.style.left='calc('+left+'% - '+textWidth+')';
}
},
postRenderHighlight: function(node) {
}
});
});
/*
Copyright (c) 2007-2009 The Evolutionary Software Foundation
Created by Mitchell Skinner <mitch_skinner@berkeley.edu>
This package and its accompanying libraries are free software; you can
redistribute it and/or modify it under the terms of the LGPL (either
version 2.1, or at your option, any later version) or the Artistic
License 2.0. Refer to LICENSE for the full license text.
*/