diff --git a/empress/support_files/js/commands.js b/empress/support_files/js/commands.js new file mode 100644 index 000000000..70c574ab6 --- /dev/null +++ b/empress/support_files/js/commands.js @@ -0,0 +1,313 @@ +define(["underscore"], function (_) { + /** + * + * @class CommandManager + * + * Maintains and provides operations for manipulating a FIFO queue of commands. + * + * @param {Empress} empress The empress object to execute all of the commands on. + * + * @return {CommandManager} + * @constructs CommandManager + */ + function CommandManager(empress) { + this.empress = empress; + this.executed = []; + this.toExecute = []; + } + + /** + * Adds a new command at the end of the queue + * + * @param {Command} command Command to be added to the queue. + */ + CommandManager.prototype.push = function (command) { + this.toExecute.push(command); + }; + + /** + * Removes the first Command from the queue and executes it. + */ + CommandManager.prototype.executeNext = function () { + var command = this.toExecute.shift(); + command.execute(this.empress); + this.executed.push(command); + }; + + /** + * Executes all commands that have yet to be executed. + */ + CommandManager.prototype.executeAll = function () { + while (this.toExecute.length > 0) { + this.executeNext(); + } + }; + + /** + * Adds a command to the queue then executes everything that has yet + * to be executed. + * + * @param {Command} command Command to be executed. + */ + CommandManager.prototype.pushAndExecuteAll = function (command) { + this.toExecute.push(command); + this.executeAll(); + }; + + /** + * Encapsulates the information needed to perform an action. + * @class Command + */ + class Command { + /** + * Validates props and then assigns them to this object. + * + * @param {Object} props Contains the properties needed to perform the action. + * Defaults to empty object + */ + constructor(props = {}) { + this.constructor.validateProps(props); + Object.assign(this, props); + } + + /** + * Validates the properties passed to the constructor. Should throw + * an error if there is a violation. + * + * @param props {Object} props Properties that need to be validated + */ + static validateProps(props) {} + + /** + * Executes the action specified by the command. + * @param controller An object that is needed at runtime for the command. + */ + execute(controller) {} + } + + /** + * Initializes and executes a command. + * + * @param {Class} commandClass The class of the command to instantiate + * @param {Object} props Contains the properties needed to perform the action. + * @param controller An object that is need at runtime for the command. + */ + function initAndExecute(commandClass, props, controller) { + var command = new commandClass(props); + command.execute(controller); + } + + /** + * Checks props for required keys. Throws an error if a required key is missing. + * + * @param {Object} props The properties to check. + * @param {Array} required The keys that are required in props. + */ + function checkRequired(props, required) { + required.forEach(function (name) { + if (!(name in props)) { + throw "Required: '" + name + "' not in props"; + } + }); + } + + /** + * A command that performs no validation and does nothing in its execute call. + * + * @class NullCommand + */ + class NullCommand extends Command { + /** + * Performs no validation. + * @param {Object} props + */ + static validateProps(props) {} + + /** + * Performs no action. + * @param controller + */ + execute(controller) {} + } + + /** + * Sets the color of the empress tree back to default. + */ + class ResetTreeCommand extends Command { + execute(empress) { + empress.resetTree(); + } + } + + var requiredColorProps = ["colorBy", "colorMapName", "coloringMethod"]; + + /** + * Colors the tree by feature metadata + * + * @class ColorByFeatureMetadataCommand + * + */ + class ColorByFeatureMetadataCommand extends Command { + /** + * @param {Object} props Properties to use for coloring based on feature metadata + * @param {string} props.colorBy Category to color based on. + * @param {string} props.colorMapName Name of color map to use. + * @param {string} props.coloringMethod Method to use for coloring. + * @param {Boolean} props.reverseColorMap (optional) Indicates whether the the color + * map should be reverse. + */ + constructor(props) { + super(props); + } + + /** + * Ensures props contain enough information to execute the command. + * + * @param {Object} props passed to this objects constructor. + */ + static validateProps(props) { + super.validateProps(props); + checkRequired(props, requiredColorProps); + } + + /** + * Colors the empress object by feature metadata + * + * @param {Empress} empress The empress object to color by feature metadata + */ + execute(empress) { + empress.colorByFeatureMetadata( + this.colorBy, + this.colorMapName, + this.coloringMethod, + this.reverseColorMap + ); + } + } + + /** + * Collapses the clades of an empress object. + */ + class CollapseCladesCommand extends Command { + /** + * Executes the clade collapse + * @param {Empress} empress The empress object to collapse. + */ + execute(empress) { + empress.collapseClades(); + } + } + + var requiredThickenProps = ["lineWidth"]; + + /** + * Thickens the colored branches of the empress object + */ + class ThickenColoredNodesCommand extends Command { + /** + * + * @param {Object} props Properties to use for thickening the branches + * @param {Number} props.lineWidth Amount of thickness to use. + */ + constructor(props) { + super(props); + } + + /** + * + * @param {Object} props Properties to validate. + */ + static validateProps(props) { + super.validateProps(props); + checkRequired(props, requiredThickenProps); + } + + /** + * Executes the node thickening. + * + * @param {Empress} empress The Empress object to thicken the nodes of. + */ + execute(empress) { + empress.thickenColoredNodes(this.lineWidth); + } + } + + /** + * Command for drawing the tree. + */ + class DrawTreeCommand extends Command { + /** + * Executes the tree drawing. + * @param {Empress} empress The Empress object to draw the tree of. + */ + execute(empress) { + empress.drawTree(); + } + } + + /** + * A convenience class for chaining commands, that is a Command itself. + */ + class CompositeCommand extends Command { + constructor(props) { + super(props); + this.commands = []; + } + + execute(controller) { + this.commands.forEach(function (command) { + command.execute(controller); + }); + } + } + + /** + * Composite pipeline for performing multiple steps of coloring a tree by feature metadata + */ + class ColorByFeatureMetadataPipeline extends CompositeCommand { + /** + * + * @param {Object} props Properties used to color the tree by metadata. + * @param {string} props.colorBy Category to color based on. + * @param {string} props.colorMapName Name of color map to use. + * @param {string} props.coloringMethod Method to use for coloring. + * @param {Number} props.lineWidth Amount of thickness to use. + * @param {Boolean} props.reverseColorMap (optional) Indicates whether the the color + * map should be reverse. + */ + constructor(props) { + super(props); + var reset = new ResetTreeCommand(); + var color = new ColorByFeatureMetadataCommand({ + colorBy: this.colorBy, + colorMapName: this.colorMapName, + coloringMethod: this.coloringMethod, + reverseColorMap: this.reverseColorMap, + }); + var collapse; + if (this.collapseClades) { + collapse = new CollapseCladesCommand(); + } else { + // Command is the null command + collapse = new NullCommand(); + } + var thicken = new ThickenColoredNodesCommand({ + lineWidth: this.lineWidth, + }); + var draw = new DrawTreeCommand(); + + this.commands = [reset, color, collapse, thicken, draw]; + } + } + + return { + CollapseCladesCommand: CollapseCladesCommand, + ColorByFeatureMetadataCommand: ColorByFeatureMetadataCommand, + ColorByFeatureMetadataPipeline: ColorByFeatureMetadataPipeline, + CommandManager: CommandManager, + DrawTreeCommand: DrawTreeCommand, + NullCommand: NullCommand, + ResetTreeCommand: ResetTreeCommand, + ThickenColoredNodesCommand: ThickenColoredNodesCommand, + }; +}); diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js index 9cf4ffb5c..f7440ab71 100644 --- a/empress/support_files/js/side-panel-handler.js +++ b/empress/support_files/js/side-panel-handler.js @@ -1,4 +1,9 @@ -define(["underscore", "Colorer", "util"], function (_, Colorer, util) { +define(["underscore", "Colorer", "util", "Commands"], function ( + _, + Colorer, + util, + Commands +) { /** * * @class SidePanel @@ -29,6 +34,7 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { // used to event triggers this.empress = empress; + this.commandManager = new Commands.CommandManager(this.empress); // settings components this.treeNodesChk = document.getElementById("display-nodes-chk"); @@ -564,12 +570,26 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) { }; this.fUpdateBtn.onclick = function () { - scope._updateColoring( - "_colorFeatureTree", - scope.fCollapseCladesChk, - scope.fLineWidth, - scope.fUpdateBtn - ); + // hide update button + scope.fUpdateBtn.classList.add("hidden"); + + // this came from colorMethodName + var colBy = scope.fSel.value; + var col = scope.fColor.value; + var coloringMethod = scope.fMethodChk.checked ? "tip" : "all"; + var reverse = scope.fReverseColor.checked; + var collapseChecked = scope.fCollapseCladesChk.checked; + var lw = util.parseAndValidateNum(scope.fLineWidth); + + var command = new Commands.ColorByFeatureMetadataPipeline({ + collapseClades: collapseChecked, + lineWidth: lw, + colorBy: colBy, + colorMapName: col, + coloringMethod: coloringMethod, + reverseColorMap: reverse, + }); + scope.commandManager.pushAndExecuteAll(command); }; this.fCollapseCladesChk.onclick = function () { diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 3b0627a94..947bfd016 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -90,6 +90,7 @@ 'ByteArray' : './js/byte-array', 'BPTree' : './js/bp-tree', 'Camera' : './js/camera', + 'Commands': './js/commands', 'Drawer' : './js/drawer', 'SidePanel' : './js/side-panel-handler', 'AnimationPanel' : './js/animation-panel-handler', @@ -114,12 +115,12 @@ 'SidePanel', 'AnimationPanel', 'Animator', 'BarplotLayer', 'BarplotPanel', 'BIOMTable', 'Empress', 'Legend', 'Colorer', 'VectorOps', 'CanvasEvents', 'SelectedNodeMenu', - 'util', 'LayoutsUtil', 'ExportUtil'], + 'util', 'LayoutsUtil', 'ExportUtil', 'Commands'], function($, gl, chroma, underscore, spectrum, filesaver, ByteArray, BPTree, Camera, Drawer, SidePanel, AnimationPanel, Animator, BarplotLayer, BarplotPanel, BIOMTable, Empress, Legend, Colorer, VectorOps, CanvasEvents, SelectedNodeMenu, util, - LayoutsUtil, ExportUtil) { + LayoutsUtil, ExportUtil, Commands) { // initialze the tree and model var tree = new BPTree( {{ tree }}, diff --git a/tests/index.html b/tests/index.html index 95a912563..82fa1454d 100644 --- a/tests/index.html +++ b/tests/index.html @@ -169,6 +169,7 @@ 'BPTree' : './support_files/js/bp-tree', 'Camera' : './support_files/js/camera', 'Colorer' : './support_files/js/colorer', + 'Commands': './support_files/js/commands', 'BiomTable': './support_files/js/biom-table', 'util' : './support_files/js/util', 'Empress' : './support_files/js/empress', @@ -189,6 +190,7 @@ 'testCamera' : './../tests/test-camera', 'testBIOMTable' : './../tests/test-biom-table', 'testColorer' : './../tests/test-colorer', + 'testCommands': './../tests/test-commands', 'testUtil' : './../tests/test-util', 'testCircularLayoutComputation' : './../tests/test-circular-layout-computation', 'testVectorOps' : './../tests/test-vector-ops', @@ -215,6 +217,7 @@ 'BPTree', 'Camera', 'Colorer', + 'Commands', 'BiomTable', 'util', 'Empress', @@ -227,6 +230,7 @@ 'testBIOMTable', 'testCamera', 'testColorer', + 'testCommands', 'testUtil', 'testCircularLayoutComputation', 'testVectorOps', @@ -253,6 +257,7 @@ Camera, testBIOMTable, Colorer, + Commands, BiomTable, util, Empress, @@ -264,6 +269,7 @@ testByteTree, testCamera, testColorer, + testCommands, testUtil, testCircularLayoutComputation, testVectorOps, diff --git a/tests/test-commands.js b/tests/test-commands.js new file mode 100644 index 000000000..77882b388 --- /dev/null +++ b/tests/test-commands.js @@ -0,0 +1,166 @@ +require(["jquery", "Commands"], function ($, Commands) { + $(document).ready(function () { + module("Commands", { + setup: function () { + var setupScope = this; + this.treeDefaults = { + reset: 0, + colorCategory: null, + colorMap: null, + coloringMethod: null, + reverse: false, + collapsed: false, + lineThickness: 0, + drawn: false, + }; + class MockEmpress { + constructor() { + var scope = this; + Object.keys(setupScope.treeDefaults).forEach((key) => { + var value = setupScope.treeDefaults[key]; + scope[key] = value; + }); + } + + resetTree() { + var scope = this; + this.reset = this.reset + 1; + Object.keys(setupScope.treeDefaults).forEach((key) => { + if (key !== "reset") { + var value = setupScope.treeDefaults[key]; + scope[key] = value; + } + }); + } + + colorByFeatureMetadata( + colorBy, + colorMapName, + coloringMethod, + reverse = false + ) { + this.colorCategory = colorBy; + this.colorMap = colorMapName; + this.coloringMethod = coloringMethod; + this.reverse = reverse; + } + + collapseClades() { + this.collapsed = true; + } + + thickenColoredNodes(lineWidth) { + this.lineThickness = lineWidth; + } + + drawTree() { + this.drawn = true; + } + } + + this.MockEmpress = MockEmpress; + }, + teardown: function () { + this.MockEmpress = null; + }, + }); + + test("Test ResetTreeCommand", function () { + var empress = new this.MockEmpress(); + var resetCommand = new Commands.ResetTreeCommand(); + resetCommand.execute(empress); + equal(empress.reset, 1); + resetCommand.execute(empress); + equal(empress.reset, 2); + }); + + test("Test NullCommand", function () { + var empress = new this.MockEmpress(); + var nullCommand = new Commands.NullCommand(); + nullCommand.execute(empress); + // cannot use equal function on treeDefaults and empress + Object.keys(this.treeDefaults).forEach((key) => { + var exp = this.treeDefaults[key]; + var obs = empress[key]; + equal(obs, exp, key); + }); + }); + + test("Test ColorByFeatureMetadataCommand", function () { + var empress = new this.MockEmpress(); + var colorCommand = new Commands.ColorByFeatureMetadataCommand({ + colorBy: "color-category", + colorMapName: "gimme-color-map-name", + coloringMethod: "gimme-method", + }); + colorCommand.execute(empress); + + equal(empress.colorCategory, "color-category"); + equal(empress.colorMap, "gimme-color-map-name"); + equal(empress.coloringMethod, "gimme-method"); + equal(empress.reverse, false); + }); + + test("Test ColorByFeatureMetadataCommand (reverse = true)", function () { + var empress = new this.MockEmpress(); + var colorCommand = new Commands.ColorByFeatureMetadataCommand({ + colorBy: "color-category", + colorMapName: "gimme-color-map-name", + coloringMethod: "gimme-method", + reverseColorMap: true, + }); + colorCommand.execute(empress); + + equal(empress.colorCategory, "color-category"); + equal(empress.colorMap, "gimme-color-map-name"); + equal(empress.coloringMethod, "gimme-method"); + equal(empress.reverse, true); + }); + + test("Test CollapaseCladesCommand", function () { + var empress = new this.MockEmpress(); + var collapse = new Commands.CollapseCladesCommand(); + notOk(empress.collapsed); + collapse.execute(empress); + ok(empress.collapsed); + }); + + test("Test ThickenColoredNodesCommand", function () { + var empress = new this.MockEmpress(); + var thicken = new Commands.ThickenColoredNodesCommand({ + lineWidth: 7.24, + }); + + thicken.execute(empress); + equal(empress.lineThickness, 7.24); + }); + + test("Test DrawTreCommand", function () { + var empress = new this.MockEmpress(); + var draw = new Commands.DrawTreeCommand(); + notOk(empress.drawn); + draw.execute(empress); + ok(empress.drawn); + }); + + test("Test ColorByFeatureMetadataPipeline", function () { + var empress = new this.MockEmpress(); + var pipeline = new Commands.ColorByFeatureMetadataPipeline({ + colorBy: "gimme-category", + colorMapName: "map-name", + coloringMethod: "some-method", + lineWidth: 8.25, + reverseColorMap: true, + collapseClades: true, + }); + pipeline.execute(empress); + equal(empress.reset, 1); + equal(empress.colorCategory, "gimme-category"); + equal(empress.colorMap, "map-name"); + equal(empress.coloringMethod, "some-method"); + equal(empress.lineThickness, 8.25); + ok(empress.reverse); + ok(empress.collapsed); + }); + }); +});