diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js index 8c737eedb..a9077da94 100644 --- a/empress/support_files/js/bp-tree.js +++ b/empress/support_files/js/bp-tree.js @@ -643,7 +643,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { */ BPTree.prototype.inOrderNodes = function () { if (this._inorder !== null) { - return this._inorder; + return _.clone(this._inorder); } // the root node of the tree @@ -658,7 +658,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // append children to stack nodeStack = nodeStack.concat(this.getChildren(curNode)); } - return this._inorder; + return _.clone(this._inorder); }; /** @@ -958,7 +958,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { */ BPTree.prototype.getNodesWithName = function (name) { if (name in this._nameToNodes) { - return this._nameToNodes[name]; + return _.clone(this._nameToNodes[name]); } this._nameToNodes[name] = []; @@ -968,7 +968,7 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } } - return this._nameToNodes[name]; + return _.clone(this._nameToNodes[name]); }; /** @@ -980,7 +980,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { * * @param {Set} keepTips The set of tip names to keep. * - * @return {BPTree} The new BPTree. + * @return {Object} An object containing the new tree ("tree") and two maps that + * convert the original postorder positions to the sheared + * tree postorder positions ("newToOld") and vice-versa ("oldToNew"). */ BPTree.prototype.shear = function (keepTips) { // closure @@ -1022,6 +1024,9 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { } var newBitArray = []; + var shearedToFull = new Map(); + var fullToSheared = new Map(); + var postorderPos = 1; for (i = 0; i < mask.length; i++) { if (mask[i] !== undefined) { newBitArray.push(mask[i]); @@ -1029,12 +1034,20 @@ define(["ByteArray", "underscore"], function (ByteArray, _) { // get name and length of node // Note: names and lengths of nodes are stored in postorder + if (mask[i] === 0) { names.push(this.name(i)); lengths.push(this.length(i)); + shearedToFull.set(postorderPos, this.postorder(i)); + fullToSheared.set(this.postorder(i), postorderPos); + postorderPos += 1; } } - return new BPTree(newBitArray, names, lengths, null); + return { + shearedToFull: shearedToFull, + fullToSheared: fullToSheared, + tree: new BPTree(newBitArray, names, lengths, null), + }; }; return BPTree; diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js index 976c3faa4..a0424d231 100644 --- a/empress/support_files/js/empress.js +++ b/empress/support_files/js/empress.js @@ -11,6 +11,7 @@ define([ "chroma", "LayoutsUtil", "ExportUtil", + "TreeController", ], function ( _, Camera, @@ -23,7 +24,8 @@ define([ util, chroma, LayoutsUtil, - ExportUtil + ExportUtil, + TreeController ) { /** * @class EmpressTree @@ -86,7 +88,7 @@ define([ * The phylogenetic balance parenthesis tree * @private */ - this._tree = tree; + this._tree = new TreeController(tree); /** * Used to index into _treeData @@ -375,7 +377,9 @@ define([ * Also updates this._maxDisplacement. */ Empress.prototype.getLayoutInfo = function () { - var data, i; + var data, + i, + j = 1; // set up length getter var branchMethod = this.branchMethod; var checkLengthsChange = LayoutsUtil.shouldCheckBranchLengthsChanged( @@ -383,13 +387,12 @@ define([ ); var lengthGetter = LayoutsUtil.getLengthMethod( branchMethod, - this._tree + this._tree.getTree() ); - // Rectangular if (this._currentLayout === "Rectangular") { data = LayoutsUtil.rectangularLayout( - this._tree, + this._tree.getTree(), 4020, 4020, // since lengths for "ignoreLengths" are set by `lengthGetter`, @@ -404,21 +407,22 @@ define([ checkLengthsChange ); this._yrscf = data.yScalingFactor; - for (i = 1; i <= this._tree.size; i++) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information - this._treeData[i][this._tdToInd.xr] = data.xCoord[i]; - this._treeData[i][this._tdToInd.yr] = data.yCoord[i]; + this._treeData[i][this._tdToInd.xr] = data.xCoord[j]; + this._treeData[i][this._tdToInd.yr] = data.yCoord[j]; this._treeData[i][this._tdToInd.highestchildyr] = - data.highestChildYr[i]; + data.highestChildYr[j]; this._treeData[i][this._tdToInd.lowestchildyr] = - data.lowestChildYr[i]; + data.lowestChildYr[j]; + j += 1; } } else if (this._currentLayout === "Circular") { data = LayoutsUtil.circularLayout( - this._tree, + this._tree.getTree(), 4020, 4020, this.leafSorting, @@ -426,39 +430,41 @@ define([ lengthGetter, checkLengthsChange ); - for (i = 1; i <= this._tree.size; i++) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information - this._treeData[i][this._tdToInd.xc0] = data.x0[i]; - this._treeData[i][this._tdToInd.yc0] = data.y0[i]; - this._treeData[i][this._tdToInd.xc1] = data.x1[i]; - this._treeData[i][this._tdToInd.yc1] = data.y1[i]; - this._treeData[i][this._tdToInd.angle] = data.angle[i]; - this._treeData[i][this._tdToInd.arcx0] = data.arcx0[i]; - this._treeData[i][this._tdToInd.arcy0] = data.arcy0[i]; + this._treeData[i][this._tdToInd.xc0] = data.x0[j]; + this._treeData[i][this._tdToInd.yc0] = data.y0[j]; + this._treeData[i][this._tdToInd.xc1] = data.x1[j]; + this._treeData[i][this._tdToInd.yc1] = data.y1[j]; + this._treeData[i][this._tdToInd.angle] = data.angle[j]; + this._treeData[i][this._tdToInd.arcx0] = data.arcx0[j]; + this._treeData[i][this._tdToInd.arcy0] = data.arcy0[j]; this._treeData[i][this._tdToInd.arcstartangle] = - data.arcStartAngle[i]; + data.arcStartAngle[j]; this._treeData[i][this._tdToInd.arcendangle] = - data.arcEndAngle[i]; + data.arcEndAngle[j]; + j += 1; } } else { data = LayoutsUtil.unrootedLayout( - this._tree, + this._tree.getTree(), 4020, 4020, undefined, lengthGetter, checkLengthsChange ); - for (i = 1; i <= this._tree.size; i++) { + for (i of this._tree.postorderTraversal((includeRoot = true))) { // remove old layout information this._treeData[i].length = this._numOfNonLayoutParams; // store new layout information - this._treeData[i][this._tdToInd.x2] = data.xCoord[i]; - this._treeData[i][this._tdToInd.y2] = data.yCoord[i]; + this._treeData[i][this._tdToInd.x2] = data.xCoord[j]; + this._treeData[i][this._tdToInd.y2] = data.yCoord[j]; + j += 1; } } this._drawer.loadTreeCoordsBuff(this.getTreeCoords()); @@ -595,7 +601,7 @@ define([ ); } // iterate through the tree in postorder, skip root - for (var node = 1; node < tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { // name of current node // var node = this._treeData[node]; var parent = tree.postorder( @@ -724,7 +730,7 @@ define([ addPoint(); } // iterate through the tree in postorder, skip root - for (var node = 1; node < tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (!this.getNodeInfo(node, "visible")) { continue; } @@ -891,7 +897,7 @@ define([ throw new Error("getNodeCoords() drawNodeCircles is out of range"); } - for (var node = 1; node <= tree.size; node++) { + for (var node of this._tree.postorderTraversal((includeRoot = true))) { if (!comp(node)) { continue; } @@ -1232,7 +1238,7 @@ define([ this._addThickVerticalLineCoords(coords, tree.size, lwScaled); } // iterate through the tree in postorder, skip root - for (var node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { // name of current node var parent = tree.postorder( tree.parent(tree.postorderselect(node)) @@ -1448,7 +1454,7 @@ define([ this._maxDisplacement = null; return; } - for (var node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { maxD = this[compFunc](node, maxD); } @@ -1936,7 +1942,7 @@ define([ } else { halfAngleRange = Math.PI / this._tree.numleaves(); } - for (node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { var name = this.getNodeInfo(node, "name"); var fm; @@ -2069,7 +2075,7 @@ define([ // For the circular layout, how to speed this up is less clear -- I // suspect it should be possible using WebGL and some fancy // trigonometry somehow, but I'm not sure. - for (var node = 1; node < this._tree.size; node++) { + for (var node of this._tree.postorderTraversal()) { if (this._tree.isleaf(this._tree.postorderselect(node))) { if (this._currentLayout === "Rectangular") { var y = this.getY(node); @@ -2376,7 +2382,7 @@ define([ if (!ignoreAbsentTips) { // find "non-represented" tips // Note: the following uses postorder traversal - for (i = 1; i < tree.size; i++) { + for (i of this._tree.postorderTraversal()) { if (tree.isleaf(tree.postorderselect(i))) { var represented = false; for (j = 0; j < categories.length; j++) { @@ -2396,7 +2402,7 @@ define([ // root (at index tree.size) in this loop, we iterate over all its // descendants; so in the event that all leaves are unique, // the root can still get assigned to a group. - for (i = 1; i < tree.size; i++) { + for (i of this._tree.postorderTraversal()) { var node = i; var parent = tree.postorder(tree.parent(tree.postorderselect(i))); @@ -2676,7 +2682,7 @@ define([ var x = 0, y = 0, zoomAmount = 0; - for (var node = 1; node <= this._tree.size; node++) { + for (var node of this._tree.postorderTraversal((includeRoot = true))) { // node = this._treeData[node]; x += this.getX(node); y += this.getY(node); @@ -2767,7 +2773,7 @@ define([ this._collapsedClades = {}; // Note: currently collapseClades is the only method that set // the node visibility property. - for (var i = 1; i <= this._tree.size; i++) { + for (var i of this._tree.postorderTraversal((includeRoot = true))) { this.setNodeInfo(i, "visible", true); } @@ -2807,7 +2813,7 @@ define([ // was not called. Thus, this loop is used to guarantee that if an // internal node belongs to a group then all of its descendants belong // to the same group. - for (var i = 1; i <= this._tree.size; i++) { + for (var i of this._tree.postorderTraversal()) { var parent = this._tree.postorder( this._tree.parent(this._tree.postorderselect(i)) ); @@ -2823,10 +2829,7 @@ define([ // collaped. // Collapsing a clade will set the .visible property of members to // false and will then be skipped in the for loop. - var inorder = this._tree.inOrderNodes(); - for (var node in inorder) { - node = inorder[node]; - + for (var node of this._tree.inOrderTraversal()) { // dont collapse clade if (this._dontCollapse.has(node)) { continue; diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js new file mode 100644 index 000000000..d4f8d6006 --- /dev/null +++ b/empress/support_files/js/tree-controller.js @@ -0,0 +1,475 @@ +define(["LayoutsUtil", "Colorer"], function (LayoutsUtil, Colorer) { + function TreeModel(tree) { + this.shearedTree = tree; + this.fullTree = tree; + this.shearedToFull = new Map(); + this.fullToSheared = new Map(); + + // initialize + for (var i = 1; i <= this.shearedTree.size; i++) { + this.fullToSheared.set(i, i); + this.shearedToFull.set(i, i); + } + } + + TreeModel.prototype.getTree = function () { + return this.shearedTree; + }; + + TreeModel.prototype.shear = function (tips) { + var result = this.fullTree.shear(tips); + this.shearedTree = result.tree; + this.shearedToFull = result.shearedToFull; + this.fullToSheared = result.fullToSheared; + }; + + TreeModel.prototype.unshear = function () { + this.shearedTree = this.fullTree; + for (var i = 1; i <= this.shearedTree.size; i++) { + this.fullToSheared.set(i, i); + this.shearedToFull.set(i, i); + } + }; + + TreeModel.prototype.postorderTraversal = function* (includeRoot = false) { + var nodes = [], + i; + for (i = 1; i <= this.shearedToFull.size; i++) { + nodes.push(this.shearedToFull.get(i)); + } + if (!includeRoot) { + nodes.pop(); + } + + yield* nodes; + }; + + function TreeController(tree) { + /** + * + * @class TreeController + * + * Initialzes a new TreeController. This class is extends BPTree and allows + * EMPress to dynamically shear the tree. TreeController's UI is similar to + * BPTree. The input/output to all functions shared between TreeController + * and BPTree are in respect to the original tree. For example, + * postorderselect(5) will return the index of the 5th node in a postorder + * traversal of the original tree. However, TreeController implements a new + * function __curToOrigNodeFunction() that uses the topology of the sheared + * tree to execute fchild, lchild, nsibling, and psibling. Thus, + * fchild(5) will return the first child of node 5 in the sheared tree. + * However, the input/output of fchild, lchild, nsibling, and psibling are + * still in relation to the original tree. So, fchild(5) means the first + * child of a node in the sheared tree that corresponds to the 5th node + * found in a post order traversal of the original tree. In addition the + * traversal methods such as postorderTraversal will also use the topology + * of the sheared tree but will output the results in relation to the + * original tree. The reason for this behavior is due to the fact that + * empress uses a nodes postorder postion (in the orginal tree) as its key + * in the various metadata structures. + * + * @param {BPTree} tree This should be the original BPTree created when + * initializing empress. + * + * @return {TreeController} + * @constructs TreeController + */ + this.model = new TreeModel(tree); + this.size = this.model.fullTree.size; + } + + /** + * Returns the current (sheared) tree + * + * @return {BPTree} + */ + TreeController.prototype.getTree = function () { + return this.model.getTree(); + }; + + /** + * Removes nodes from the original tree until only the nodes found in tips + * and there ancestors remain in the tree. + * + * @param{Set} tips A set of tip names that will be kept. + */ + TreeController.prototype.shear = function (tips) { + this.model.shear(tips); + }; + + /** + * Restores the original tree. + */ + TreeController.prototype.unshear = function () { + this.model.unshear(); + }; + + /** + * Returns an iterator for nodes in a post order traversal of the sheared + * tree. + * + * Note: This method will use the topology of the currect tree but will + * return the nodes position in the original tree. + * + * @param{Boolean} includeRoot If true then the root will be included. + */ + TreeController.prototype.postorderTraversal = function* ( + includeRoot = false + ) { + yield* this.model.postorderTraversal(includeRoot); + }; + + /** + * Returns an Object describing the minimum, maximum, and average of all + * non-root node lengths in the sheared tree. + * + * @return {Object} Contains three keys: "min", "max", and "avg", mapping + * to Numbers representing the minimum, maximum, and + * average non-root node length in the tree. + */ + TreeController.prototype.getLengthStats = function () { + return this.model.shearedTree.getLengthStats(); + }; + + /** + * Return the name of the ith index in the ORIGINAL bp tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return{String} + */ + TreeController.prototype.name = function (i) { + return this.model.fullTree.name(i); + }; + + /** + * Returns an array of all node names in sheared tree. + */ + TreeController.prototype.getAllNames = function () { + return this.model.shearedTree.getAllNames(); + }; + + /** + * Returns the number of leaf nodes in sheared tree + * + * @return {Number} + */ + TreeController.prototype.numleaves = function () { + return this.model.shearedTree.numleaves(); + }; + + /** + * Returns the length of the ith index in the ORIGINAL bp tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return{Number} + */ + TreeController.prototype.length = function (i) { + return this.model.fullTree.length(i); + }; + + /** + * Return the parent index of the node that corresponds to the ith index in + * the ORIGINAL bp tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * Note: The output of this method is also in relation to the original tree. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return{Number} + */ + TreeController.prototype.parent = function (i) { + return this.model.fullTree.parent(i); + }; + + /** + * Returns the index of the opening index of the root node. + * + * Note: This will always be 0. + * + * @return {Number} + */ + TreeController.prototype.root = function () { + return this.model.fullTree.root(); + }; + + /** + * Returns true if i represents a leaf node + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Boolean} + */ + TreeController.prototype.isleaf = function (i) { + return this.model.fullTree.isleaf(i); + }; + + /** + * This method is used in fchild, lchild, nsibling, and psibling and is what + * allows TreeController to use the topology of the sheared tree but returns + * the results w.r.t the original tree. + * + * @param{Number} i The index correspond to a node in the ORIGINAL tree. + * @param{String} func The function to use. This should only be fchild, + * nchild, nsibling or psibling. + * + * @return{Number} The result of func w.r.t the ORIGINAL tree. + */ + + TreeController.prototype._shearedToFullNodeFunction = function (i, func) { + var shearedTreeTree = this.model.shearedTree; + var fullTree = this.model.fullTree; + + var node = shearedTreeTree.postorderselect( + this.model.fullToSheared.get(fullTree.postorder(i)) + ); + + node = shearedTreeTree.postorder(shearedTreeTree[func](node)); + node = fullTree.postorderselect(this.model.shearedToFull.get(node)); + return node; + }; + + /** + * Returns the opening index of first child of the node represented by i. + * This method will use the topology of the sheared (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i is a leaf node + */ + TreeController.prototype.fchild = function (i) { + return this._shearedToFullNodeFunction(i, "fchild"); + }; + + /** + * Returns the opening index of last child of the node represented by i. + * This method will use the topology of the sheared (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i is a leaf node + */ + TreeController.prototype.lchild = function (i) { + return this._shearedToFullNodeFunction(i, "lchild"); + }; + + /** + * Returns the opening index of next sibling of the node represented by i. + * This method will use the topology of the sheared (sheared) tree but its + * input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i does not have a next sibling + */ + TreeController.prototype.nsibling = function (i) { + return this._shearedToFullNodeFunction(i, "nsibling"); + }; + + /** + * Returns the opening index of previous sibling of the node represented by + * i. This method will use the topology of the sheared (sheared) tree but + * its input and output will be w.r.t the ORGINAL tree. + * + * Note: The input of this method should the result of either preorderselect + * or postorderselect. + * + * @param{Number} i The index corresponding to a node in the ORIGINAL tree + * + * @return {Number} return 0 if i does not have a previous sibling + */ + TreeController.prototype.psibling = function (i) { + return this._shearedToFullNodeFunction(i, "psibling"); + }; + + /** + * Returns the postorder rank of index i in the ORIGINAL tree. + * + * Note: The input of this method should the result of parent, fchild, + * lchild, nsibling or psibling. + * + * @param {Number} i The index to assess postorder rank + * + * @return {Number} The postorder rank of index i + */ + TreeController.prototype.postorder = function (i) { + return this.model.fullTree.postorder(i); + }; + + /** + * Find the index of the node with postorder k in the ORIGINAL tree. + * + * @param {Number} k The postorder to search for + * Note: k starts at 1 + * + * @return {Number} The index position of the node in the tree + */ + TreeController.prototype.postorderselect = function (k) { + return this.model.fullTree.postorderselect(k); + }; + + /** + * Returns the preorder rank of index i in the ORIGINAL tree. + * + * Note: The input of this method should the result of parent, fchild, + * lchild, nsibling or psibling. + * + * @param {Number} i The index to assess preorder rank + * + * @return {Number} The preorder rank of index i + */ + TreeController.prototype.preorder = function (i) { + return this.model.fullTree.preorder(i); + }; + + /** + * Find the index of the node with preorder k in the ORIGINAL tree. + * + * @param {Number} k The preorder to search for. + * Note: k starts at 1. + * + * @return {Number} The index position of the node in the tree + */ + TreeController.prototype.preorderselect = function (k) { + return this.model.fullTree.preorderselect(k); + }; + + /** + * Returns an iterator for nodes in an in-order traversal of the sheared + * tree. + * + * Note: This method will use the topology of the currect tree but will + * return the nodes position in the original tree. + * + * @param{Boolean} includeRoot If true then the root will be included. + */ + TreeController.prototype.inOrderTraversal = function* ( + includeRoot = false + ) { + var inOrderNodes = this.model.shearedTree.inOrderNodes(); + for (var i = 0; i < inOrderNodes.length; i++) { + inOrderNodes[i] = this.model.shearedToFull.get(inOrderNodes[i]); + } + if (!includeRoot) { + inOrderNodes.shift(); + } + yield* inOrderNodes; + }; + + /** + * Finds the sum of lengths from start to end. This method will use the + * topology of the sheared tree but its input must be w.r.t the ORIGINAL + * tree. + * + * Note: start must be a descendant of end. An error will be thrown if start + * is not a descendant of end. Also, this method does not take into + * account the length of end since that length would represent the + * length of end to its parent. + * + * @param {Number} start The postorder position of a node + * @param {Number} end The postorder position of a node + * @param {Boolean} ignoreLengths If truthy, treat all node lengths as 1; + * if falsy, actually consider node lengths + * + * @return {Number} the sum of length from start to end + */ + TreeController.prototype.getTotalLength = function ( + start, + end, + ignoreLengths + ) { + start = this.model.fullToSheared.get(start); + end = this.model.fullToSheared.get(end); + return this.model.shearedTree.getTotalLength(start, end, ignoreLengths); + }; + + /** + * Retrieve the tips in the subtree of a given (internal) node key. This + * method will use the topology of the sheared tree but its input/output + * will be w.r.t the ORIGINAL tree. + * + * @param {Number} nodeKey The post-order position of a node in the ORIGINAL + * tree + * + * @return {Array} tips Tips of the subtree. + */ + TreeController.prototype.findTips = function (nodeKey) { + nodeKey = this.model.fullToSheared.get(nodeKey); + var tips = this.model.shearedTree.findTips(nodeKey); + for (var i = 0; i < tips.length; i++) { + tips[i] = this.model.shearedToFull.get(tips[i]); + } + return tips; + }; + + /** + * Retrieve number of tips in the subtree of a given node. This method will + * use the topology of the sheared tree but its input must be w.r.t the + * ORIGINAL tree. + * + * @param {Integer} nodeKey The postorder position of a node in the ORIGINAL + * tree + * + * @return {Integer} The number of tips on the subtree rooted at nodeKey. + */ + TreeController.prototype.getNumTips = function (nodeKey) { + nodeKey = this.model.fullToSheared.get(nodeKey); + return this.model.shearedTree.getNumTips(nodeKey); + }; + + /** + * Checks to see if name is in the sheared tree. + * + * @param {String} name The name to search for. + * + * @return {Boolean} If the name is in the tree. + */ + TreeController.prototype.containsNode = function (name) { + return this.model.shearedTree.containsNode(name); + }; + + /** + * Returns all nodes with a given name. This method will use the topology + * of the sheared tree but its output will be w.r.t the ORIGINAL tree. + * + * @param {String} name The name of the node(s) + * + * @return {Array} An array of postorder positions of nodes with a given + * name. If no nodes have the specified name, this will be + * an empty array. + */ + TreeController.prototype.getNodesWithName = function (name) { + var nodes = this.model.shearedTree.getNodesWithName(name); + for (var i = 0; i < nodes.length; i++) { + nodes[i] = this.model.shearedToFull.get(nodes[i]); + } + return nodes; + }; + + return TreeController; +}); diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html index 9aafaa5b8..8a933ebd3 100644 --- a/empress/support_files/templates/empress-template.html +++ b/empress/support_files/templates/empress-template.html @@ -115,6 +115,7 @@ 'util' : './js/util', 'LayoutsUtil': './js/layouts-util', 'ExportUtil': './js/export-util', + 'TreeController': './js/tree-controller' } }); diff --git a/tests/index.html b/tests/index.html index b39ad8f52..78f79dc83 100644 --- a/tests/index.html +++ b/tests/index.html @@ -203,6 +203,7 @@ 'SelectedNodeMenu' : './support_files/js/select-node-menu', 'LayoutsUtil' : './support_files/js/layouts-util', 'ExportUtil' : './support_files/js/export-util', + 'TreeController' : './support_files/js/tree-controller', /* test utility code */ 'UtilitiesForTesting' : './../tests/utilities-for-testing', @@ -223,12 +224,14 @@ 'testLegend': './../tests/test-legend', 'testLayoutsUtil': './../tests/test-layouts-util', 'testSelectedNodeMenu': './../tests/test-select-node-menu', + 'testTreeController': './../tests/test-tree-controller', } }); // load tests require( - ['jquery', + [ + 'jquery', 'glMatrix', 'chroma', 'underscore', @@ -262,6 +265,7 @@ 'testLegend', 'testLayoutsUtil', 'testSelectedNodeMenu', + 'testTreeController', ], // start tests @@ -300,6 +304,7 @@ testLegend, testLayoutsUtil, testSelectedNodeMenu, + testTreeController ) { $(document).ready(function() { QUnit.start(); diff --git a/tests/test-bp-tree.js b/tests/test-bp-tree.js index 78bff215d..2d8c56eaf 100644 --- a/tests/test-bp-tree.js +++ b/tests/test-bp-tree.js @@ -929,8 +929,32 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { ); var keep = new Set(["4", "6", "7", "10", "11"]); + var shearedToFull = new Map([ + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 6], + [6, 7], + [7, 8], + [8, 9], + [9, 10], + [10, 11], + ]); + var fullToSheared = new Map([ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [6, 5], + [7, 6], + [8, 7], + [9, 8], + [10, 9], + [11, 10], + ]); var result = preShearBPTree.shear(keep); - deepEqual(result.b_, [ + deepEqual(result.tree.b_, [ 1, 1, 1, @@ -952,7 +976,7 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { 0, 0, ]); - deepEqual(result.names_, [ + deepEqual(result.tree.names_, [ null, "4", "6", @@ -965,19 +989,63 @@ require(["jquery", "ByteArray", "BPTree"], function ($, ByteArray, BPTree) { "8", "r", ]); - deepEqual(result.lengths_, [null, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); + deepEqual(result.tree.lengths_, [ + null, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + ]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); keep = new Set(["7", "10", "11"]); + shearedToFull = new Map([ + [1, 6], + [2, 7], + [3, 8], + [4, 9], + [5, 10], + [6, 11], + ]); + fullToSheared = new Map([ + [6, 1], + [7, 2], + [8, 3], + [9, 4], + [10, 5], + [11, 6], + ]); result = preShearBPTree.shear(keep); - deepEqual(result.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); - deepEqual(result.names_, [null, "7", "10", "11", "9", "8", "r"]); - deepEqual(result.lengths_, [null, 6, 7, 8, 9, 10, 11]); + deepEqual(result.tree.b_, [1, 1, 0, 1, 1, 1, 0, 1, 0, 0, 0, 0]); + deepEqual(result.tree.names_, [ + null, + "7", + "10", + "11", + "9", + "8", + "r", + ]); + deepEqual(result.tree.lengths_, [null, 6, 7, 8, 9, 10, 11]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); keep = new Set([]); + shearedToFull = new Map([[1, 11]]); + fullToSheared = new Map([[11, 1]]); result = preShearBPTree.shear(keep); - deepEqual(result.b_, [1, 0]); - deepEqual(result.names_, [null, "r"]); - deepEqual(result.lengths_, [null, 11]); + deepEqual(result.tree.b_, [1, 0]); + deepEqual(result.tree.names_, [null, "r"]); + deepEqual(result.tree.lengths_, [null, 11]); + deepEqual(result.shearedToFull, shearedToFull); + deepEqual(result.fullToSheared, fullToSheared); }); }); }); diff --git a/tests/test-tree-controller.js b/tests/test-tree-controller.js new file mode 100644 index 000000000..c511a6260 --- /dev/null +++ b/tests/test-tree-controller.js @@ -0,0 +1,379 @@ +require(["jquery", "UtilitiesForTesting", "util", "TreeController"], function ( + $, + UtilitiesForTesting, + util, + TreeController +) { + $(document).ready(function () { + // Setup test variables + // Note: This is ran for each test() so tests can modify bpArray + // without affecting other tests. + module("TreeController", { + setup: function () { + this.tree = UtilitiesForTesting.getTestData(false).tree; + this.names = ["", "t1", "t2", "t3", "i4", "i5", "t6", "r"]; + this.tree.names_ = this.names; + this.lengths = [null, 1, 2, 3, 4, 5, 6, null]; + this.tree.lengths_ = this.lengths; + this.treeController = new TreeController(this.tree); + }, + + teardown: function () { + this.tree = null; + this.treeController = null; + }, + }); + + test("Test shear", function () { + this.treeController.shear(new Set(["t2", "t3"])); + + // checks to make sure correct names are kept + var shearNames = [null, "t2", "t3", "i4", "i5", "r"]; + var resutlNames = this.treeController.model.shearedTree.names_; + deepEqual(resutlNames, shearNames); + + var shearLengths = [null, 2, 3, 4, 5, null]; + var resultLengts = this.treeController.model.shearedTree.lengths_; + deepEqual(resultLengts, shearLengths); + + // checks to make sure structre of tree is correct + var shearTree = [1, 1, 1, 1, 0, 1, 0, 0, 0, 0]; + var resultTree = this.treeController.model.shearedTree.b_; + deepEqual(resultTree, shearTree); + + // checks to make sure the mappings from orignal tree to shear tree + // is correct and vice-versa + var fullToSheared = new Map([ + [2, 1], + [3, 2], + [4, 3], + [5, 4], + [7, 5], + ]); + var shearedToFull = new Map([ + [1, 2], + [2, 3], + [3, 4], + [4, 5], + [5, 7], + ]); + var resultOrigToCur = this.treeController.model.fullToSheared; + var resultCurToOrig = this.treeController.model.shearedToFull; + deepEqual(resultOrigToCur, fullToSheared); + deepEqual(resultCurToOrig, shearedToFull); + }); + + test("Test unshear", function () { + this.treeController.shear(new Set(["t2", "t3"])); + this.treeController.unshear(); + + deepEqual(this.treeController.model.shearedTree.names_, this.names); + deepEqual( + this.treeController.model.shearedTree.lengths_, + this.lengths + ); + + var map = new Map([ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + [6, 6], + [7, 7], + ]); + deepEqual(this.treeController.model.shearedToFull, map); + deepEqual(this.treeController.model.fullToSheared, map); + }); + + test("Test postorderTraversal", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var nodes = [2, 3, 4, 5, 7]; + var result = [ + ...this.treeController.postorderTraversal((includeRoot = true)), + ]; + deepEqual(result, nodes); + + nodes.pop(); + result = [ + ...this.treeController.postorderTraversal( + (includeRoot = false) + ), + ]; + deepEqual(result, nodes); + + this.treeController.unshear(); + nodes = [1, 2, 3, 4, 5, 6, 7]; + result = [ + ...this.treeController.postorderTraversal((includeRoot = true)), + ]; + deepEqual(result, nodes); + }); + + test("Test getLengthStats", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var stats = { + avg: 3.5, + min: 2, + max: 5, + }; + var result = this.treeController.getLengthStats(); + deepEqual(result, stats); + + this.treeController.unshear(); + stats = { + avg: 3.5, + min: 1, + max: 6, + }; + result = this.treeController.getLengthStats(); + deepEqual(result, stats); + }); + + test("Test name", function () { + // name() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var name = this.treeController.name(index); + deepEqual(name, "t1"); + }); + + test("Test getAllNames", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var shearNames = ["t2", "t3", "i4", "i5", "r"]; + var resutlNames = this.treeController.getAllNames(); + deepEqual(resutlNames, shearNames); + + this.treeController.unshear(); + shearNames = ["t1", "t2", "t3", "i4", "i5", "t6", "r"]; + deepEqual(this.treeController.getAllNames(), shearNames); + }); + + test("Test numleaves", function () { + this.treeController.shear(new Set(["t2", "t3"])); + equal(this.treeController.numleaves(), 2); + + this.treeController.unshear(); + equal(this.treeController.numleaves(), 4); + }); + + test("Test length", function () { + // length() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var length = this.treeController.length(index); + equal(length, 1); + }); + + test("Test parent", function () { + // parent() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var parent = this.treeController.parent(index); + parent = this.treeController.postorder(parent); + equal(parent, 5); + }); + + test("Test root", function () { + // root() only uses the original tree and thus is not effected + // by the shear operation + equal(this.treeController.root(), 0); + }); + + test("Test isleaf", function () { + // isleaf() only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var isleaf = this.treeController.isleaf(index); + equal(isleaf, true); + + index = this.treeController.postorderselect(7); + isleaf = this.treeController.isleaf(index); + equal(isleaf, false); + }); + + test("Test fchild", function () { + // fchild's input/output is in respect to the original tree. + // However, fchild will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(5); + var fchild = this.treeController.fchild(index); + var expected = this.treeController.postorderselect(4); + equal(fchild, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(5); + fchild = this.treeController.fchild(index); + expected = this.treeController.postorderselect(1); + equal(fchild, expected); + }); + + test("Test lchild", function () { + // lchild's input/output is in respect to the original tree. + // However, lchild will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(7); + var lchild = this.treeController.lchild(index); + var expected = this.treeController.postorderselect(5); + equal(lchild, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(7); + lchild = this.treeController.lchild(index); + expected = this.treeController.postorderselect(6); + equal(lchild, expected); + }); + + test("Test nsibling", function () { + // nsibling's input/output is in respect to the original tree. + // However, nsibling will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(5); + var nsibling = this.treeController.nsibling(index); + var expected = 0; // doesn't have a next sibling + equal(nsibling, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(5); + nsibling = this.treeController.nsibling(index); + expected = this.treeController.postorderselect(6); + equal(nsibling, expected); + }); + + test("Test psibling", function () { + // psibling's input/output is in respect to the original tree. + // However, psibling will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var index = this.treeController.postorderselect(4); + var psibling = this.treeController.psibling(index); + var expected = 0; // doesn't have a next sibling + equal(psibling, expected); + + this.treeController.unshear(); + index = this.treeController.postorderselect(4); + psibling = this.treeController.psibling(index); + expected = this.treeController.postorderselect(1); + equal(psibling, expected); + }); + + test("Test postorder", function () { + // postorder only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + var postorder = this.treeController.postorder(index); + equal(postorder, 1); + }); + + test("Test postorderselect", function () { + // postorderselect only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.postorderselect(1); + equal(index, 2); + }); + + test("Test preorder", function () { + // preorder only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.preorderselect(1); + var preorder = this.treeController.preorder(index); + equal(preorder, 1); + }); + + test("Test preorderselect", function () { + // preorderselect only uses the original tree and thus is not effected + // by the shear operation + var index = this.treeController.preorderselect(1); + equal(index, 0); + }); + + test("Test inOrderTraversal", function () { + // inOrderTraversal's input/output is in respect to the original tree. + // However, inOrderTraversal will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var expected = [7, 5, 4, 2, 3]; + var result = [ + ...this.treeController.inOrderTraversal((includeRoot = true)), + ]; + deepEqual(result, expected); + expected.shift(); + result = [ + ...this.treeController.inOrderTraversal((includeRoot = false)), + ]; + deepEqual(result, expected); + + this.treeController.unshear(); + expected = [7, 5, 6, 1, 4, 2, 3]; + result = [ + ...this.treeController.inOrderTraversal((includeRoot = true)), + ]; + deepEqual(result, expected); + expected.shift(); + result = [ + ...this.treeController.inOrderTraversal((includeRoot = false)), + ]; + deepEqual(result, expected); + }); + + test("Test getTotalLength", function () { + // getTotalLength's input/output is in respect to the original tree. + // However, getTotalLength will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getTotalLength(2, 7); + equal(result, 11); + + this.treeController.unshear(); + result = this.treeController.getTotalLength(2, 7); + equal(result, 11); + }); + + test("Test findTips", function () { + // findTips's input/output is in respect to the original tree. + // However, findTips will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.findTips(5); + deepEqual(result, [2, 3]); + + this.treeController.unshear(); + result = this.treeController.findTips(5); + deepEqual(result, [1, 2, 3]); + }); + + test("Test getNumTips", function () { + // getNumTips's input/output is in respect to the original tree. + // However, getNumTips will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getNumTips(5); + deepEqual(result, 2); + + this.treeController.unshear(); + result = this.treeController.getNumTips(5); + deepEqual(result, 3); + }); + + test("Test containsNode", function () { + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.containsNode("t1"); + equal(result, false); + + this.treeController.unshear(); + result = this.treeController.containsNode("t1"); + equal(result, true); + }); + + test("Test getNodesWithName", function () { + // getNodesWithName's input/output is in respect to the original tree. + // However, getNodesWithName will use the topology of the sheared tree. + this.treeController.shear(new Set(["t2", "t3"])); + var result = this.treeController.getNodesWithName("t2"); + deepEqual(result, [2]); + result = this.treeController.getNodesWithName("t1"); + deepEqual(result, []); + + this.treeController.unshear(); + result = this.treeController.getNodesWithName("t2"); + deepEqual(result, [2]); + result = this.treeController.getNodesWithName("t1"); + deepEqual(result, [1]); + }); + }); +});