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 @@