@gmod/jbrowse
Version:
JBrowse - client-side genome browser
1,039 lines (923 loc) • 44.2 kB
JavaScript
define([
'dojo/_base/declare',
'dojo/_base/lang',
'dojo/on',
'dojo/dom-construct',
'dojo/dom-class',
'dojo/Deferred',
'dojo/promise/all',
'dojo/when',
'./Combination/CombinationDialog',
'dijit/Dialog',
'JBrowse/View/Track/BlockBased',
'JBrowse/Model/BinaryTreeNode',
'dojo/dnd/move',
'dojo/dnd/Source',
'dojo/dnd/Manager',
'JBrowse/Util',
'JBrowse/View/TrackConfigEditor',
'JBrowse/View/Track/_ExportMixin'
],
function(
declare,
lang,
on,
dom,
domClass,
Deferred,
all,
when,
CombinationDialog,
Dialog,
BlockBased,
TreeNode,
dndMove,
dndSource,
dndManager,
Util,
TrackConfigEditor,
ExportMixin
) {
return declare([BlockBased, ExportMixin],
{
/**
* Creates a track with a drag-and-drop interface allowing users to drag other tracks into it.
* Users select (using a dialog) a way to combine these tracks, and they are combined.
* Certain tracks (e.g. HTMLFeatures tracks) may be combined set-theoretically (union, intersection,etc ),
* while others (e.g. BigWig tracks) may be combined quantitatively (add scores, subtract scores, etc...).
* If one of the tracks is a set-based track and the other is not, track masking operations may be applied.
* @constructs
*/
constructor: function( args ) {
// The "default" track of each type is the one at
// index 0 of the resultsTypes array.
// Many different kinds of tracks can be added.
// Each is supported by a different store, and
// some can be rendered in several ways.
// The trackClasses object stores information about what can be done with each of these types.
this.trackClasses =
{
"set": {
resultsTypes: [{
name: "HTMLFeatures",
path: "JBrowse/View/Track/HTMLFeatures"
}
],
store: "JBrowse/Store/SeqFeature/Combination",
allowedOps: ["&", "U", "X", "S"],
defaultOp : "&"
},
"quantitative": {
resultsTypes: [{
name: "XYPlot",
path: "JBrowse/View/Track/Wiggle/XYPlot"
},
{
name: "Density",
path: "JBrowse/View/Track/Wiggle/Density"
}],
store: "JBrowse/Store/SeqFeature/QuantitativeCombination",
allowedOps: ["+", "-", "*", "/"],
defaultOp: "+"
},
"mask": {
resultsTypes: [{
name: "XYPlot",
path: "JBrowse/View/Track/Wiggle/XYPlot"
},
{
name: "Density",
path: "JBrowse/View/Track/Wiggle/Density"
}],
store: "JBrowse/Store/SeqFeature/Mask",
allowedOps: ["M", "N"],
defaultOp: "M"
},
"BAM": {
resultsTypes: [{
name: "Detail",
path: "JBrowse/View/Track/Alignments2"
},
{
name: "Summary",
path: "JBrowse/View/Track/SNPCoverage" //For now
}],
store: "JBrowse/Store/SeqFeature/BAMCombination",
allowedOps: ["U"],
defaultOp: "U"
}
};
this.errorCallback = dojo.hitch(this, function(error) {
this._handleError(error, {});
});
// inWords just stores, in words, what each possible operation does. This is helpful for dialogs and menus that
// allow selection of different operations.
this.inWords =
{
// These one-character codes symbolize operations between stores.
"+": "addition",
"-": "subtraction",
"*": "multiplication",
"/": "division",
"&": "intersection",
"U": "union",
"X": "XOR",
"S": "set subtraction",
"M": "regular mask",
"N": "inverse mask",
// These four-digit codes are used by the CombinationDialog object to differentiate different types of masking operations.
"0000": "combine without masking",
"0020": "use new track as mask",
"0002": "use old track as mask",
"1111": "merge tracks",
"1001": "add new track to old track's displayed data",
"1010": "add new track to old track's mask",
"0101": "add old track to new track's displayed data",
"0110": "add old track to new track's mask"
};
// Each store becomes associated with the name of a track that uses that store, so that users can read more easily.
if(!this.config.storeToKey)
this.config.storeToKey = {};
// Shows which track or store types qualify as set-based, quantitative, etc.
this.supportedBy =
{
"JBrowse/View/Track/HTMLFeatures": "set",
"JBrowse/View/Track/HTMLVariants": "set",
"JBrowse/View/Track/CanvasFeatures": "set",
"JBrowse/View/Track/CanvasVariants": "set",
"CanvasFeatures": "set",
"HTMLFeatures": "set",
"HTMLVariants": "set",
"CanvasVariants": "set",
"NeatCanvasFeatures/View/Track/NeatFeatures": "set",
"NeatHTMLFeatures/View/Track/NeatFeatures": "set",
"JBrowse/View/Track/Alignments2": "BAM",
"JBrowse/View/Track/SNPCoverage": "BAM",
"JBrowse/Store/BigWig": "quantitative",
"JBrowse/Store/SeqFeature/BigWig": "quantitative",
"JBrowse/Store/SeqFeature/BAM": "BAM",
"JBrowse/Store/SeqFeature/BAMCombination": "BAM",
"JBrowse/Store/SeqFeature/Combination": "set",
"JBrowse/Store/SeqFeature/QuantitativeCombination": "quantitative",
"JBrowse/Store/SeqFeature/Mask": "mask"
};
this.loaded = true;
// For CSS customization of the outer
this.divClass = args.divClass || "combination";
// Sets a bunch of variables to their initial values
this.reinitialize();
// When other tracks are dragged onto the combination, they don't disappear from their respective sources
// (in case the user wants to add the track separately, by itself). These variables will be used in the DnD
// methods to support this functionality
this.currentDndSource = undefined;
this.sourceWasCopyOnly = undefined;
// This is used to avoid creating a feedback loop in the height-updating process.
this.onlyRefreshOuter = false;
this.heightResults = 0;
this.height = args.height || 0;
// This variable (which will later be a deferred) ensures that when multiple tracks are added simultaneously,
// The dialogs for each one don't render all at once.
this.lastDialogDone = [true];
},
setViewInfo: function( genomeView, heightUpdate, numBlocks,
trackDiv,
widthPct, widthPx, scale) {
this.inherited( arguments );
domClass.add( this.div, 'combination_track empty' );
this.scale = scale;
// This track has a dnd source (to support dragging tracks into and out of it).
this.dnd = new dndSource( this.div,
{
accept: ["track"], //Accepts only tracks
isSource: false,
withHandles: true,
creator: dojo.hitch( this, function( trackConfig, hint ) {
// Renders the results track div (or avatar, depending).
// Code for ensuring that we don't have several results tracks
// is handled later in the file.
var data = trackConfig;
if(trackConfig.resultsTrack) {
data = trackConfig.resultsTrack;
data.storeToKey = trackConfig.storeToKey;
}
return {
data: data,
type: ["track"],
node: this.addTrack(data)
};
})
});
// Attach dnd events
this._attachDndEvents();
// If config contains a config for the results track, use it. (This allows reloading when the track config is edited. )
if(this.config.resultsTrack) {
this.reloadStoreNames = true;
this.dnd.insertNodes(false, [this.config.resultsTrack]);
}
},
// This function ensure that the combination track's drag-and-drop interface works correctly.
_attachDndEvents: function() {
var thisB = this;
// What to do at the beginning of dnd process
on(thisB.dnd, "DndStart", function(source, nodes, copy) {
// Stores the information about whether the source was copy-only, for future reference
thisB.currentDndSource = source;
thisB.sourceWasCopyOnly = source.copyOnly;
});
// When other tracks are dragged onto the combination, they don't disappear from their respective sources
on(thisB.dnd, "DraggingOver", function() {
if(thisB.currentDndSource) {
// Tracks being dragged onto this track are copied, not moved.
thisB.currentDndSource.copyOnly = true;
}
this.currentlyOver = true;
});
var dragEndingEvents = ["DraggingOut", "DndDrop", "DndCancel"];
for(var eventName in dragEndingEvents)
on(thisB.dnd, dragEndingEvents[eventName], function() {
if(thisB.currentDndSource) {
// Makes sure that the dndSource isn't permanently set to CopyOnly
thisB.currentDndSource.copyOnly = thisB.sourceWasCopyOnly;
}
this.currentlyOver = false;
});
// Bug fixer
dojo.subscribe("/dnd/drop/before", function(source, nodes, copy, target) {
if(target == thisB.dnd && nodes[0]) {
thisB.dnd.current = null;
}
});
on(thisB.dnd, "OutEvent", function() {
// Fixes a glitch wherein the trackContainer is disabled when the track we're dragging leaves the combination track
dndManager.manager().overSource(thisB.genomeView.trackDndWidget);
});
on(thisB.dnd, "DndSourceOver", function(source) {
// Fixes a glitch wherein tracks dragged into the combination track sometimes go to the trackContainer instead.
if(source != this && this.currentlyOver) {
dndManager.manager().overSource(this);
}
});
// Further restricts what categories of tracks may be added to this track
// Should re-examine this
var oldCheckAcceptance = this.dnd.checkAcceptance;
this.dnd.checkAcceptance = function(source, nodes) {
// If the original acceptance checker fails, this one will too.
var accept = oldCheckAcceptance.call(thisB.dnd, source, nodes);
// Additional logic to disqualify bad tracks - if one node is unacceptable, the whole group is disqualified
for(var i = 0; accept && nodes[i]; i++) {
var trackConfig = source.getItem(nodes[i].id).data;
accept = accept && (trackConfig.resultsTrack || thisB.supportedBy[trackConfig.storeClass] || thisB.supportedBy[trackConfig.type]);
}
return accept;
};
},
// Reset a bunch of variables
reinitialize: function() {
if(this.dnd) {
this.dnd.selectAll().deleteSelectedNodes();
}
// While there is no results track, we cannot export.
this.config.noExport = true;
this.exportFormats = [];
this.resultsDiv = undefined;
this.resultsTrack = undefined;
this.storeType = undefined;
this.oldType = undefined;
this.classIndex = {};
this.storeToShow = 0;
this.displayStore = undefined;
this.maskStore = undefined;
this.store = undefined;
this.opTree = undefined;
},
// Modifies the results track when a new track is added
addTrack: function(trackConfig) {
// Connect the track's name to its store for easy reading by user
if(trackConfig && trackConfig.key && trackConfig.store) {
this.config.storeToKey[trackConfig.store] = trackConfig.key;
}
if(trackConfig && trackConfig.storeToKey) {
lang.mixin(this.config.storeToKey, trackConfig.storeToKey);
}
// Creates the results div, if it hasn't already been created
if(!this.resultsDiv) {
this.resultsDiv = dom.create("div");
this.resultsDiv.className = "track";
this.resultsDiv.id = this.name + "_resultsDiv";
domClass.remove( this.div, 'empty' );
}
// Carry on the process of adding the track
this._addTrackStore(trackConfig);
// Because _addTrackStore has deferreds, the dnd node must be returned before it is filled
return this.resultsDiv;
},
// Obtains the store of the track that was just added.
_addTrackStore: function(trackConfig) {
var storeName = trackConfig.store;
var thisB = this;
var haveStore = (function() {
var d = new Deferred();
thisB.browser.getStore(storeName, function(store) {
if(store) {
d.resolve(store,true);
} else {
d.reject("store " + storeName + " not found", true);
}
});
return d.promise;
})();
// Once we have the store, it's time to open the dialog.
haveStore.then(function(store){
thisB.runDialog(trackConfig, store);
});
},
// Runs the dialog that asks the user how to combine the track.
runDialog: function(trackConfig, store) {
// If this is the first track being added, it's not being combined with anything, so we don't need to ask - just adds the track alone
if(this.storeType === undefined) {
// Figure out which type of track (set, quant, etc) the user is adding
this.currType = this.supportedBy[trackConfig.storeClass] || this.supportedBy[trackConfig.type];
this.storeType = this.currType;
// What type of Combination store corresponds to the track just added
this.storeClass = this.trackClasses[this.currType].store;
// opTree can be directly reloaded from track config. This is important (e.g.) when changing reference sequences
// to make sure that the right combinations of tracks are still included in this track.
if( store.isCombinationStore && !store.opTree && this.config.opTree ) {
this.loadTree( this.config.opTree ).then( dojo.hitch( this, function(tree){
this.opTree = tree;
this.displayType = this.config.displayType;
if( this.getClassIndex( this.displayType || this.storeType ) == undefined ) {
this.setTrackClass( trackConfig.type, this.displayType || this.storeType );
}
this._adjustStores( store, this.oldType, this.currType, this.config.store, this.config.maskStore, this.config.displayStore );
}));
return;
}
var opTree = store.isCombinationStore ? store.opTree.clone() : new TreeNode({Value: store, leaf: true});
this.displayType = (this.currType == "mask") ? this.supportedBy[store.stores.display.config.type] : undefined;
if( this.getClassIndex( this.displayType || this.storeType ) == undefined ) {
this.setTrackClass( trackConfig.type, this.displayType || this.storeType );
}
this.opTree = opTree;
if(this.reloadStoreNames) {
this.reloadStoreNames = false;
this._adjustStores( store, this.oldType, this.currType, this.config.store, this.config.maskStore, this.config.displayStore );
return;
}
this._adjustStores( store, this.oldType, this.currType );
return;
}
var d = new Deferred();
this.lastDialogDone.push(d);
// Once the last dialog has closed, opens a new one
when( this.lastDialogDone.shift(),
dojo.hitch( this, function() {
if(this.preferencesDialog)
this.preferencesDialog.destroyRecursive();
// Figure out which type of track (set, quant, etc) the user is adding
this.currType = this.supportedBy[trackConfig.storeClass] || this.supportedBy[trackConfig.type];
this.oldType = this.storeType;
// What type of Combination store corresponds to the track just added
this.storeClass = this.trackClasses[this.currType].store;
this.preferencesDialog = new CombinationDialog({
trackConfig: trackConfig,
store: store,
track: this
});
// Once the results of the dialog are back, uses them to continue the process of rendering the results track
this.preferencesDialog.run(dojo.hitch(this, function(opTree, newstore, displayType) {
this.opTree = opTree;
this.displayType = displayType;
this.storeType = ( this.oldType == "mask" || this.opTree.get() == "M" ||
this.opTree.get() == "N" ) ? "mask" : this.currType;
if( this.getClassIndex( this.displayType || this.storeType ) == undefined ) {
this.setTrackClass( trackConfig.type, this.displayType || this.storeType );
}
this._adjustStores(newstore, this.oldType, this.currType);
d.resolve(true);
}), dojo.hitch(this, function() {
d.resolve(true);
}));
}));
},
// If this track contains masked data, it uses three stores. Otherwise, it uses one.
// This function ensures that all secondary stores (one for the mask, one for the display) have been loaded.
// If not, it loads them itself. This function tries not to waste stores - if a store of a certain type already exists,
// it uses it rather than creating a new one.
_adjustStores: function ( store, oldType, currType, storeName, maskStoreName, displayStoreName ) {
var d = new Deferred();
if( oldType == "mask" ) {
this.maskStore.reload( this.opTree.leftChild );
this.displayStore.reload( this.opTree.rightChild );
this.store.reload( this.opTree, this.maskStore, this.displayStore );
d.resolve( true );
} else if( currType == "mask" || this.opTree.get( ) == "M" || this.opTree.get( ) == "N" ) {
var haveMaskStore = this._createStore( "set", maskStoreName );
haveMaskStore.then( dojo.hitch( this, function( newstore ) {
this.maskStore = newstore;
this.maskStore.reload( this.opTree.leftChild );
} ) );
var haveDisplayStore = this._createStore( this.displayType, displayStoreName );
haveDisplayStore.then( dojo.hitch( this, function( newStore ){
this.displayStore = newStore;
this.displayStore.reload( this.opTree.rightChild );
} ) );
this.store = undefined;
d = all( [haveMaskStore, haveDisplayStore] );
} else {
d.resolve( true );
}
d.then( dojo.hitch( this, function() {
this.createStore( storeName );
}));
},
// Checks if the primary store has been created yet. If it hasn't, calls "_createStore" and makes it.
createStore: function( storeName ) {
var d = new Deferred();
var thisB = this;
if( !this.store ) {
d = this._createStore( undefined, storeName );
} else {
d.resolve( this.store, true );
}
d.then( function(store) {
// All stores are now in place. Make sure the operation tree of the store matches that of this track,
// and then we can render the results track.
thisB.store = store;
thisB.store.reload( thisB.opTree, thisB.maskStore, thisB.displayStore );
thisB.renderResultsTrack();
});
},
// Creates a store config and passes it to the browser, which creates the store and returns its name.
_createStore: function( storeType, storeName ) {
var d = new Deferred();
if( !storeName ) {
var storeConf = this._storeConfig( storeType );
storeName = this.browser.addStoreConfig( undefined, storeConf );
storeConf.name = storeName;
}
this.browser.getStore( storeName, function( store ) {
d.resolve( store, true );
});
return d.promise;
},
// Uses the current settings of the combination track to create a store
_storeConfig: function( storeType ) {
if(!storeType)
storeType = this.storeType;
var storeClass = this.trackClasses[storeType].store;
this.config.storeClass = storeClass;
var op = this.trackClasses[storeType].defaultOp;
return {
browser: this.browser,
refSeq: this.browser.refSeq.name,
type: storeClass,
op: op
};
},
// This method is particularly useful when masked data is being displayed, and returns data which depends on
// which of (data, mask, masked data) is being currently displayed.
_visible: function() {
var which = [this.displayType || this.storeType, "set", this.displayType];
var allTypes = [{ store: this.store,
tree: this.opTree },
{ store: this.maskStore,
tree: this.opTree ? this.opTree.leftChild : undefined },
{ store: this.displayStore,
tree: this.opTree ? this.opTree.rightChild : undefined }];
for(var i in which) {
allTypes[i].which = which[i];
if(which[i]) {
var storeType = (i == 0 && this.storeType == "mask") ? "mask" : which[i];
allTypes[i].allowedOps = this.trackClasses[storeType].allowedOps;
allTypes[i].trackType = this.trackClasses[which[i]].resultsTypes[this.getClassIndex(which[i]) || 0].path;
}
}
if(this.storeType != "mask") return allTypes[0];
return allTypes[this.storeToShow];
},
// Time to actually render the results track.
renderResultsTrack: function() {
if(this.resultsTrack) {
// Destroys the results track currently in place if it exists. We're going to create a new one.
this.resultsTrack.clear();
this.resultsTrack.destroy();
while(this.resultsDiv.firstChild) { // Use dojo.empty instead?
this.resultsDiv.removeChild(this.resultsDiv.firstChild);
}
}
// Checks one last time to ensure we have a store before proceeding
if(this._visible().store) {
// Gets the path of the track to create
var trackClassName = this._visible().trackType;
var trackClass;
var thisB = this;
var config = this._resultsTrackConfig(trackClassName);
trackClassName = config.type;
// Once we have the object for the type of track we're creating, call this.
var makeTrack = function(){
// Construct a track with the relevant parameters
thisB.resultsTrack = new trackClass({
config: config,
browser: thisB.browser,
changeCallback: thisB._changedCallback,
refSeq: thisB.refSeq,
store: thisB._visible().store,
trackPadding: 0});
// Removes all options from the results track's context menu.
thisB.resultsTrackMenuOptions = thisB.resultsTrack._trackMenuOptions;
thisB.resultsTrack._trackMenuOptions = function(){ return []; };
// This will be what happens when the results track updates its height - makes necessary changes to
// outer track's height and then passes up to the heightUpdate callback specified as a parameter to this object
var resultsHeightUpdate = function(height) {
thisB.resultsDiv.style.height = height + "px";
thisB.heightResults = height;
thisB.height = height;
thisB.onlyRefreshOuter = true;
thisB.refresh();
thisB.onlyRefreshOuter = false;
thisB.heightUpdate(thisB.height);
thisB.div.style.height = thisB.height + "px";
};
// setViewInfo on results track
thisB.resultsTrack.setViewInfo (thisB.genomeView, resultsHeightUpdate,
thisB.numBlocks, thisB.resultsDiv, thisB.widthPct, thisB.widthPx, thisB.scale);
// Only do this when the masked data is selected
// (we don't want editing the config to suddenly remove the data or the mask)
thisB.config.opTree = thisB.flatten(thisB.opTree);
thisB.config.store = thisB.store.name;
thisB.config.maskStore = thisB.maskStore ? thisB.maskStore.name : undefined;
thisB.config.displayStore = thisB.displayStore ? thisB.displayStore.name : undefined;
if(thisB._visible().store == thisB.store) {
// Refresh results track config, so that the track can be recreated when the config is edited
thisB.config.resultsTrack = thisB.resultsTrack.config;
thisB.config.displayType = thisB.displayType;
thisB.browser.replaceTracks([ thisB.config ]);
if(typeof thisB.resultsTrack._exportFormats == 'function') {
thisB.config.noExport = false;
thisB.exportFormats = thisB.resultsTrack._exportFormats();
} else {
thisB.config.noExport = true;
}
}
thisB.refresh();
};
// Loads the track class from the specified path
dojo.global.require([trackClassName], function(tc) {
trackClass = tc;
if(trackClass) makeTrack();
});
}
},
// Generate the config of the results track
_resultsTrackConfig: function(trackClass) {
var config = {
store: this.store.name,
storeClass: this.store.config.type,
feature: ["match"],
key: "Results",
label: this.name + "_results",
metadata: { description: "This track was created from a combination track."},
type: trackClass,
autoscale: "local"
};
if(this.config.resultsTrack) {
if((this.config.resultsTrack.storeClass == config.storeClass || this.supportedBy[this.config.resultsTrack.storeClass] == this.displayType)
&& (this._visible().store != this.maskStore)) {
config = this.config.resultsTrack;
config.store = this.store.name;
config.storeClass = this.store.config.type;
return config;
}
config.key = this.config.resultsTrack.key;
config.label = this.config.resultsTrack.label;
config.metadata = this.config.resultsTrack.metadata;
}
return config;
},
// Refresh what the user sees on the screen for this track
refresh: function(track) {
if(!track) {
track = this;
}
if(this._visible().store && !this.onlyRefreshOuter) {
// Reload the store if it's not too much trouble
this._visible().store.reload(this._visible().tree, this.maskStore, this.displayStore);
}
else {
if(!this.onlyRefreshOuter) {
// Causes the resultsTrack to be removed from the config when it has been removed
delete this.config.resultsTrack;
delete this.config.opTree;
}
}
// once the store is properly reloaded, make sure the track is showing data correctly
if(this.range) {
track.clear();
track.showRange(this.range.f, this.range.l, this.range.st, this.range.b,
this.range.sc, this.range.cs, this.range.ce);
}
this.makeTrackMenu();
},
clear: function() {
this.inherited(arguments);
if(this.resultsTrack && !this.onlyRefreshOuter) {
this.resultsTrack.clear();
}
},
hideAll: function() {
this.inherited(arguments);
if(this.resultsTrack && !this.onlyRefreshOuter) {
this.resultsTrack.hideAll();
}
},
hideRegion: function( location ) {
this.inherited(arguments);
if(this.resultsTrack && !this.onlyRefreshOuter) {
this.resultsTrack.hideRegion( location );
}
},
sizeInit: function( numBlocks, widthPct, blockDelta ) {
this.inherited(arguments);
if(this.resultsTrack && !this.onlyRefreshOuter) {
this.resultsTrack.sizeInit( numBlocks, widthPct, blockDelta);
}
},
// Extends the BlockBased track's showRange function.
showRange: function(first, last, startBase, bpPerBlock, scale, containerStart, containerEnd, finishCallback) {
this.range = {f: first, l: last, st: startBase,
b: bpPerBlock, sc: scale,
cs: containerStart, ce: containerEnd};
if(this.resultsTrack && !this.onlyRefreshOuter) {
// This is a workaround to a glitch that causes an opaque white rectangle to appear sometimes when a quantitative
// track is loaded.
var needsDiv = !this.resultsDiv.parentNode;
if(needsDiv) {
this.div.appendChild(this.resultsDiv);
}
var loadedRegions = [];
var stores = [this.store, this.maskStore, this.displayStore];
for(var i in stores) {
if(stores[i] && typeof stores[i].loadRegion == 'function') {
var start = startBase;
var end = startBase + (last + 1 - first)*bpPerBlock;
var loadedRegion = stores[i].loadRegion({ref: this.refSeq.name, start: start, end: end});
loadedRegions.push(loadedRegion);
loadedRegion.then(function(){}, this.errorCallback); // Add error callbacks to all deferred rejections
}
}
when(all(loadedRegions), dojo.hitch(this, function(reloadedStores){
if(reloadedStores.length && reloadedStores.indexOf(this._visible().store) != -1) {
this.resultsTrack.clear();
}
this.resultsTrack.showRange(first, last, startBase, bpPerBlock, scale, containerStart, containerEnd, finishCallback);
}),
this.errorCallback);
if(needsDiv) {
this.div.removeChild(this.resultsDiv);
}
}
// Run the method from BlockBased.js
this.inherited(arguments);
// Make sure the height of this track is right
this.heightUpdate(this.height);
this.div.style.height = this.height + "px";
},
// If moveBlocks is called on this track, should be called on the results track as well
moveBlocks: function(delta) {
this.inherited(arguments);
if(this.resultsTrack)
this.resultsTrack.moveBlocks(delta);
},
// fillBlock in this renders all the relevant borders etc that surround the results track and let the user know
// that this is a combination track
fillBlock: function( args ) {
var blockIndex = args.blockIndex;
var block = args.block;
var leftBase = args.leftBase;
if( !this.resultsTrack ) {
this.fillMessage( blockIndex, block, 'Drag tracks here to combine them.' );
}
else {
this.heightUpdate( this.heightResults, blockIndex);
}
args.finishCallback();
},
// endZoom is passed down to resultsTrack
endZoom: function(destScale, destBlockBases) {
this.clear(); // Necessary?
if(this.resultsTrack)
this.resultsTrack.endZoom();
},
// updateStaticElements passed down to resultsTrack
updateStaticElements: function(args) {
this.inherited(arguments);
if(this.resultsTrack)
this.resultsTrack.updateStaticElements(args);
},
// When the results track can be shown in multiple different classes
// (e.g. XYPlot or Density), this allows users to choose between them
setClassIndex: function(index, type) {
if(!type)
type = this._visible().which;
if(type == "mask" && this.displayStore)
type = this.supportedBy[this.displayStore.config.type];
this.classIndex[type] = index;
},
// Like the setClassIndex function, but accepts the actual file path of the track in question
setTrackClass: function( tclass, type ) {
var allPaths = this.trackClasses[ type ].resultsTypes.map( function( item ) { return item.path; } );
var index = allPaths.indexOf( tclass );
if( index >= 0 ) {
this.setClassIndex( index, type );
}
},
// When the results track can be shown in multiple different classes
// (e.g. XYPlot or Density), this tells us which one is currently
// chosen
getClassIndex: function(type) {
if(type == "mask" && this.displayStore)
type = this.supportedBy[this.displayStore.config.type];
return this.classIndex[type];
},
// Adds options to the track context menu
_trackMenuOptions: function() {
// Allows the combination track to "mimic" the menu options of its results track
var resultsTrackOptions = ( this.resultsTrackMenuOptions || function() { return undefined; } ).call( this.resultsTrack );
resultsTrackOptions = resultsTrackOptions || [];
var inheritedOptions = this.inherited( arguments );
var inheritedLabels = inheritedOptions.map( function( menuItem ) {
return menuItem.label;
});
for( var i = 0; i < resultsTrackOptions.length; i++ ) {
if(resultsTrackOptions[i].label && inheritedLabels.indexOf( resultsTrackOptions[i].label ) != -1) {
resultsTrackOptions.splice( i--, 1);
}
}
var o = inheritedOptions.concat( resultsTrackOptions );
//var o = this.inherited(arguments);
var combTrack = this;
// If no tracks are added, we don't need to add any more options
if( !this.storeType )
return o;
if( this.storeType == "mask" ) {
// If a masking track, enables users to toggle between viewing data, mask, and masked data
var maskOrDisplay = ["masked data", "mask", "data only"];
var maskOrDisplayItems =
Object.keys(maskOrDisplay)
.map( function(i) {
return {
type: 'dijit/CheckedMenuItem',
checked: (combTrack.storeToShow == i),
label: maskOrDisplay[i],
title: "View " + maskOrDisplay[i],
action: function() {
combTrack.storeToShow = i;
combTrack.renderResultsTrack();
}
};
});
o.push.apply(
o,
[{
type: 'dijit/MenuSeparator'
},
{
children: maskOrDisplayItems,
label: "View",
title: "switch between the mask, display data and masked data for this masking track"
}]);
}
// User may choose which class to render results track (e.g. XYPlot or Density) if multiple options exist
var classes = this.trackClasses[this._visible().which].resultsTypes;
var classItems = Object.keys(classes).map(function(i){
return {
type: 'dijit/CheckedMenuItem',
label: classes[i].name,
checked: (combTrack.classIndex[combTrack._visible().which] == i),
title: "Display as " + classes[i].name + " track",
action: function()
{
combTrack.setClassIndex(i);
delete combTrack.config.resultsTrack;
combTrack.renderResultsTrack();
}
};
});
o.push.apply(
o,
[
{ type: 'dijit/MenuSeparator' },
{
children: classItems,
label: "Track display",
title: "Change what type of track is being displayed"
}
]);
// Allow user to view the current track formula.
if(this.opTree) {
o.push.apply(
o,
[{ label: 'View formula',
title: 'View the formula specifying this combination track',
action: function() {
var formulaDialog = new Dialog({title: "View Formula"});
var content = [];
var formulaDiv = dom.create("div",
{innerHTML: "No operation formula defined", className: "formulaPreview"});
content.push(formulaDiv);
if(combTrack.opTree) {
formulaDiv.innerHTML = combTrack._generateTreeFormula(combTrack.opTree);
}
formulaDialog.set("content", content);
formulaDialog.show();
}
}]);
}
// If the current view contains more than one track combined, user may change the last operation applied
if(this._visible().tree && this._visible().tree.getLeaves().length > 1) {
var operationItems = this._visible().allowedOps.map(
function(op) {
return {
type: 'dijit/CheckedMenuItem',
checked: (combTrack._visible().tree.get() == op),
label: combTrack.inWords[op],
title: "change operation of last track to " + combTrack.inWords[op],
action: function() {
if(combTrack.opTree) {
combTrack._visible().tree.set(op);
combTrack.refresh();
}
}
};
});
o.push.apply(
o,
[{ children: operationItems,
label: "Change last operation",
title: "change the operation applied to the last track added"
}]
);
}
return o;
},
// Turns an opTree into a formula to be better understood by the user.
_generateTreeFormula: function(tree) {
if(!tree || tree === undefined){
return '<span class="null">NULL</span>';
}
if(tree.isLeaf()){
return '<span class="leaf' + (tree.highlighted ? ' highlighted': '') + '">' + (tree.get().name ? (this.config.storeToKey[tree.get().name] ? this.config.storeToKey[tree.get().name] : tree.get().name)
: tree.get()) + '</span>';
}
return '<span class="tree">(' + this._generateTreeFormula(tree.left()) +' <span class="op" title="' + this.inWords[tree.get()] + '">'+ tree.get() +"</span> " + this._generateTreeFormula(tree.right()) +")</span>";
},
_exportFormats: function() {
return this.exportFormats || [];
},
// These methods are not currently in use, but they allow direct loading of the opTree into the config.
flatten: function(tree) {
var newTree = {
leaf: tree.leaf
};
if(tree.leftChild)
newTree.leftChild = this.flatten(tree.leftChild);
if(tree.rightChild)
newTree.rightChild = this.flatten(tree.rightChild);
if(tree.get().name)
newTree.store = tree.get().name;
else
newTree.op = tree.get();
return newTree;
},
loadTree: function(tree) {
var d = new Deferred();
var haveLeft;
var haveRight;
var thisB = this;
if(!tree) {
d.resolve(undefined, true);
return d.promise;
}
if(tree.leftChild) {
haveLeft = this.loadTree(tree.leftChild);
}
if(tree.rightChild) {
haveRight = this.loadTree(tree.rightChild);
}
when(all([haveLeft, haveRight]), function(results) {
var newTree = new TreeNode({ leftChild: results[0], rightChild: results[1], leaf: tree.leaf});
if(tree.store) {
thisB.browser.getStore(tree.store, function(store) {
newTree.set(store);
});
d.resolve(newTree, true);
} else {
newTree.set(tree.op);
d.resolve(newTree, true);
}
});
return d.promise;
}
});
});