From c556cc29ae3e7189e9e595833c65f93dc2c41d5b Mon Sep 17 00:00:00 2001
From: kcantrel
Date: Thu, 12 Aug 2021 14:12:30 -0700
Subject: [PATCH 1/9] basic version of lca
---
.../js/abstract-observer-pattern.js | 60 ++++++
empress/support_files/js/bp-tree.js | 75 ++++++-
empress/support_files/js/canvas-events.js | 13 +-
empress/support_files/js/drawer.js | 33 +++
empress/support_files/js/empress.js | 192 ++++++++++++++++-
empress/support_files/js/path-selector.js | 58 +++++
.../support_files/js/side-panel-handler.js | 31 +--
empress/support_files/js/tree-controller.js | 49 +++++
empress/support_files/js/util.js | 24 +++
.../templates/empress-template.html | 2 +
.../support_files/templates/side-panel.html | 26 +++
tests/index.html | 9 +-
tests/test-bp-tree.js | 200 +++++++++++++++++-
tests/test-empress.js | 20 ++
tests/test-tree-controller.js | 62 +++++-
tests/utilities-for-testing.js | 8 +-
16 files changed, 819 insertions(+), 43 deletions(-)
create mode 100644 empress/support_files/js/abstract-observer-pattern.js
create mode 100644 empress/support_files/js/path-selector.js
diff --git a/empress/support_files/js/abstract-observer-pattern.js b/empress/support_files/js/abstract-observer-pattern.js
new file mode 100644
index 00000000..5f6739b9
--- /dev/null
+++ b/empress/support_files/js/abstract-observer-pattern.js
@@ -0,0 +1,60 @@
+define(["underscore"], function (_) {
+ /**
+ * @abstract
+ * @class AbstractObserverPattern
+ *
+ * From Wikipedia: https://en.wikipedia.org/wiki/Observer_pattern
+ * The observer pattern is a software design pattern in which an object,
+ * named the 'Subject', maintains a list of its dependents, called
+ * 'Observers',and notifies them automatically of any state changes, usually
+ * by calling one of their methods.
+ *
+ * Any class that inherits AbstractObserverPattern will become a 'Subject'.
+ *
+ * @param{String} notifyFunction The function that Subject will call on all
+ * of its observers. observers must implement
+ * this function in order to register as an
+ * observer.
+ */
+ class AbstractObserverPattern {
+ constructor(notifyFunction) {
+ if (this.constructor === AbstractObserverPattern) {
+ throw new Error(
+ "Abstract class: " +
+ "AbstractObserverPattern cannot be instantiated."
+ );
+ }
+
+ this.observers = [];
+ this.notifyFunction = notifyFunction;
+ }
+
+ /*
+ * Adds an observer to the observer list.
+ */
+ registerObserver(observer) {
+ if (!this.hasNotifyFunction(observer)) {
+ throw new Error(
+ "Cannot register observer: missing " + this.notifyFunction
+ );
+ }
+ this.observers.push(observer);
+ }
+
+ /**
+ * Notify all observers.
+ */
+ notify(data) {
+ var scope = this;
+ _.each(this.observers, function (obs) {
+ obs[scope.notifyFunction](data);
+ });
+ }
+
+ hasNotifyFunction(observer) {
+ return typeof observer[this.notifyFunction] === "function";
+ }
+ }
+
+ return AbstractObserverPattern;
+});
diff --git a/empress/support_files/js/bp-tree.js b/empress/support_files/js/bp-tree.js
index a5de5135..11baa4b8 100644
--- a/empress/support_files/js/bp-tree.js
+++ b/empress/support_files/js/bp-tree.js
@@ -301,10 +301,17 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
* @return{Number} The excess at position i
* @private
*/
- BPTree.prototype.excess_ = function (i) {
+ BPTree.prototype.excess = function (i) {
// need to subtract 1 since i starts at 0
// Note: rank(1,i) - rank(0,i) = (2*(rank(1,i)) - i
- return 2 * this.r1Cache_[i] - i - 1;
+ // if (i < 0) {
+ // return 0;
+ // }
+ // return 2 * this.r1Cache_[i] - i - 1;
+
+ // this method is used frequently and thus has been updated
+ // to use a cached version
+ return this.eCache_[i];
};
/**
@@ -1103,5 +1110,69 @@ define(["ByteArray", "underscore"], function (ByteArray, _) {
};
};
+ /*
+ * Finds the minimum excess between i and j.
+ *
+ * @param {Number} i The ith index of the bp array.
+ * @param {Number} j The jth index of the bp array.
+ *
+ * @return The miniumn excess between i and j.
+ */
+ BPTree.prototype.rmq = function (i, j) {
+ if (j < i) {
+ var temp = j;
+ j = i;
+ i = temp;
+ }
+ var minK = i;
+ var minV = this.excess(i);
+ for (var k = i; k < j + 1; k++) {
+ var obsV = this.excess(k);
+ if (obsV < minV) {
+ minK = k;
+ minV = obsV;
+ }
+ }
+ return minK;
+ };
+
+ /*
+ * Test to see if i if the ancestor of j.
+ *
+ * @param {Number} i The ith index of the bp array.
+ * @param {Number} j The jth index of the bp array.
+ *
+ * @return true if i is the ancestor of j, false otherwise.
+ */
+ BPTree.prototype.isAncestor = function (i, j) {
+ if (i === j) {
+ return false;
+ }
+
+ if (!this.b_[i]) {
+ i = this.open(i);
+ }
+
+ return i <= j && j < this.close(i);
+ };
+
+ /*
+ * Finds the lowest common ancestor of i and j
+ *
+ * @param {Number} i The ith index of the bp array.
+ * @param {Number} j The jth index of the bp array.
+ *
+ * @return The index of the lca of i and j.
+ */
+ BPTree.prototype.lca = function (i, j) {
+ if (this.isAncestor(i, j)) {
+ return i;
+ } else if (this.isAncestor(j, i)) {
+ return j;
+ } else {
+ return this.parent(this.rmq(i, j) + 1);
+ }
+ };
+
return BPTree;
});
diff --git a/empress/support_files/js/canvas-events.js b/empress/support_files/js/canvas-events.js
index ecf50ed7..ec9ca382 100644
--- a/empress/support_files/js/canvas-events.js
+++ b/empress/support_files/js/canvas-events.js
@@ -137,6 +137,7 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function (
// removes the selected node menu if the mouseMove flag is not set
var mouseClick = function (e) {
+ var shiftPressed = e.shiftKey;
if (!scope.mouseMove) {
// clear old select menu
selectedNodeMenu.clearSelectedNode();
@@ -201,7 +202,8 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function (
scope.placeNodeSelectionMenu(
empress.getNodeInfo(closeNode, "name"),
false,
- closeNode
+ closeNode,
+ shiftPressed
);
}
}
@@ -419,8 +421,15 @@ define(["underscore", "glMatrix", "SelectedNodeMenu"], function (
CanvasEvents.prototype.placeNodeSelectionMenu = function (
nodeName,
moveTree,
- nodeKey
+ nodeKey,
+ shiftPressed = false
) {
+ if (shiftPressed) {
+ this.empress.setSelectedNode(nodeKey);
+ this.selectedNodeMenu.setSelectedNodes([nodeKey]);
+ this.empress.drawTree();
+ return;
+ }
var scope = this;
var node;
/**
diff --git a/empress/support_files/js/drawer.js b/empress/support_files/js/drawer.js
index 8b304f99..64ed092c 100644
--- a/empress/support_files/js/drawer.js
+++ b/empress/support_files/js/drawer.js
@@ -138,6 +138,14 @@ define(["underscore", "glMatrix", "Camera", "Colorer"], function (
s.treeColorBuff = c.createBuffer();
this.treeColorSize = 0;
+ // buffer object for path coordinates
+ s.pathCoordBuff = c.createBuffer();
+ this.pathCoordSize = 0;
+
+ // buffer object to store path color
+ s.pathColorBuff = c.createBuffer();
+ this.pathColorSize = 0;
+
// buffer object used to thicken node lines
s.thickNodeBuff = c.createBuffer();
this.thickNodeSize = 0;
@@ -315,6 +323,27 @@ define(["underscore", "glMatrix", "Camera", "Colorer"], function (
this.fillBufferData_(this.sProg_.treeColorBuff, data);
};
+ /**
+ * Fills the buffer used to draw the selected path.
+ *
+ * @param {Array} data The coordinates [x, y, ...] to fill pathCoordBuff
+ */
+ Drawer.prototype.loadPathCoordsBuff = function (data) {
+ data = new Float32Array(data);
+ this.pathCoordSize = data.length / 2;
+ this.fillBufferData_(this.sProg_.pathCoordBuff, data);
+ };
+
+ /**
+ * Fills the buffer used to draw the selected path.
+ *
+ * @param {Array} data The color data to fill pathColorBuff
+ */
+ Drawer.prototype.loadPathColorBuff = function (data) {
+ data = new Float32Array(data);
+ this.pathColorSize = data.length;
+ this.fillBufferData_(this.sProg_.pathColorBuff, data);
+ };
/**
* Fills the buffer used to thicken node lines
*
@@ -414,6 +443,10 @@ define(["underscore", "glMatrix", "Camera", "Colorer"], function (
this.bindBuffer(s.treeColorBuff, 3, 1);
c.drawArrays(c.LINES, 0, this.treeCoordSize);
+ this.bindBuffer(s.pathCoordBuff, 2, 2);
+ this.bindBuffer(s.pathColorBuff, 3, 1);
+ c.drawArrays(c.LINES, 0, this.pathCoordSize);
+
this.bindBuffer(s.thickNodeBuff, 1, 3);
c.drawArrays(c.TRIANGLES, 0, this.thickNodeSize);
diff --git a/empress/support_files/js/empress.js b/empress/support_files/js/empress.js
index fc803003..c891a180 100644
--- a/empress/support_files/js/empress.js
+++ b/empress/support_files/js/empress.js
@@ -12,6 +12,7 @@ define([
"LayoutsUtil",
"ExportUtil",
"TreeController",
+ "PathSelector",
], function (
_,
Camera,
@@ -25,7 +26,8 @@ define([
chroma,
LayoutsUtil,
ExportUtil,
- TreeController
+ TreeController,
+ PathSelector
) {
/**
* @class EmpressTree
@@ -387,6 +389,9 @@ define([
* for clades in this array that share the same group membership.
*/
this._group = new Array(this._tree.size + 1).fill(-1);
+
+ this.pathSelector = new PathSelector();
+ this.pathSelector.registerObserver(this);
}
/**
@@ -3792,5 +3797,190 @@ define([
this.redrawBarPlotsToMatchLayout();
};
+ /*
+ * Finds the lowest comman ancestor of the input nodes.
+ *
+ * @param{Array} nodes An array of node keys.
+ *
+ * @return The node key of the lca to nodes.
+ */
+ Empress.prototype.findLCA = function (nodes) {
+ var scope = this;
+ // 1: find least common ancestor.
+ var nodeBPIndices = _.map(nodes, function (node) {
+ return scope._tree.postorderselect(node);
+ });
+ var lcaBPIndx = this._tree.lca(nodeBPIndices[0], nodeBPIndices[1]);
+ nodeBPIndices.shift(); // removes first nodes
+ nodeBPIndices.shift(); // removes second node
+
+ while (nodeBPIndices.length > 0) {
+ lcaBPIndx = this._tree.lca(lcaBPIndx, nodeBPIndices.shift());
+ }
+ var lcaNode = this._tree.postorder(lcaBPIndx);
+ return lcaNode;
+ };
+
+ /*
+ * Colors the tree by highlighting the patch of nodes to the lca.
+ *
+ * @param{Array} nodes An array of node keys.
+ * @param{Array} lca The node key of the lca.
+ */
+ Empress.prototype.colorLCAPath = function (nodes, lca) {
+ var lcaBPIndx = this._tree.postorderselect(lca);
+
+ var coordsBuff = [];
+ var colorsBuff = [];
+ var scope = this;
+ var color = Colorer.rgbToFloat([64, 200, 64]);
+ addColorToBuff = () => {
+ colorsBuff.push(color, color);
+ colorsBuff.push(color, color);
+ };
+ var addCoordsToBuff = (node, parent) => {
+ if (this._currentLayout === "Unrooted") {
+ coordsBuff.push(this.getX(parent), this.getY(parent));
+ coordsBuff.push(this.getX(node), this.getY(node));
+ addColorToBuff();
+ } else if (this._currentLayout === "Rectangular") {
+ // draw horizontal line from child x, y to parent x, child y
+ coordsBuff.push(this.getX(node), this.getY(node));
+ coordsBuff.push(this.getX(parent), this.getY(node));
+ addColorToBuff();
+
+ // draw vertical line from parent x, y to parent x child y
+ coordsBuff.push(this.getX(parent), this.getY(parent));
+ coordsBuff.push(this.getX(parent), this.getY(node));
+ addColorToBuff();
+ } else if (this._currentLayout === "Circular") {
+ // draw arc from parent to child
+ var pAngle = this.getNodeInfo(parent, "angle");
+ var nAngle = this.getNodeInfo(node, "angle");
+ var arcDelta = nAngle - pAngle;
+
+ var numSamples = this._numSampToApproximate(arcDelta);
+ var sampleAngle = arcDelta / numSamples;
+ var sX = this.getX(parent);
+ var sY = this.getY(parent);
+ var x, y;
+ for (var line = 0; line < numSamples; line++) {
+ x =
+ sX * Math.cos(line * sampleAngle) -
+ sY * Math.sin(line * sampleAngle);
+ y =
+ sX * Math.sin(line * sampleAngle) +
+ sY * Math.cos(line * sampleAngle);
+ coordsBuff.push(x, y);
+
+ x =
+ sX * Math.cos((line + 1) * sampleAngle) -
+ sY * Math.sin((line + 1) * sampleAngle);
+ y =
+ sX * Math.sin((line + 1) * sampleAngle) +
+ sY * Math.cos((line + 1) * sampleAngle);
+ coordsBuff.push(x, y);
+ addColorToBuff();
+ }
+
+ // draw line connect node x,y to parent arc
+ coordsBuff.push(this.getX(node), this.getY(node));
+ coordsBuff.push(x, y);
+ addColorToBuff();
+ }
+ };
+ for (var node of nodes) {
+ // this.setNodeInfo(node, "color", Colorer.rgbToFloat([64, 200, 64]));
+ var parentBPIndx = this._tree.parent(
+ this._tree.postorderselect(node)
+ );
+ var parent = this._tree.postorder(parentBPIndx);
+ while (parentBPIndx !== lcaBPIndx) {
+ // && parentBPIndx !== this._tree.root()) {
+ // add buff info
+ addCoordsToBuff(node, parent);
+
+ // progress nodes
+ node = parent;
+ parentBPIndx = this._tree.parent(parentBPIndx);
+ parent = this._tree.postorder(parentBPIndx);
+ // this.setNodeInfo(parent, "color", Colorer.rgbToFloat([64, 200, 64]));
+ }
+ addCoordsToBuff(node, parent);
+ }
+ this._drawer.loadPathCoordsBuff(coordsBuff);
+ this._drawer.loadPathColorBuff(colorsBuff);
+ this.drawTree();
+ };
+
+ /*
+ * Calculates the distance of the path connecting nodes to the lca.
+ *
+ * @param{Array} nodes An array of node keys.
+ * @param{Array} lca The node key of the lca.
+ * @return The distance.
+ */
+ Empress.prototype.calcLCADistance = function (nodes, lca) {
+ var dist = 0;
+ var lcaBPIndx = this._tree.postorderselect(lca);
+
+ // find unique nodes
+ var uniqueNodeBPIndices = new Set();
+ for (var node of nodes) {
+ var nodeBPIndx = this._tree.postorderselect(node);
+ while (nodeBPIndx !== lcaBPIndx) {
+ uniqueNodeBPIndices.add(nodeBPIndx);
+ nodeBPIndx = this._tree.parent(nodeBPIndx);
+ }
+ }
+
+ // sum distance
+ for (var nodeIdx of uniqueNodeBPIndices) {
+ dist += this._tree.length(nodeIdx);
+ }
+
+ return dist;
+ };
+
+ /*
+ * Attempts to a node to the selected path. If the selected path is maxed
+ * out, the a warning message will appear.
+ *
+ * @param{Number} node A node key
+ */
+ Empress.prototype.setSelectedNode = function (node) {
+ var name = this.getName(node);
+ this.pathSelector.addNode(node, name);
+ };
+
+ /*
+ * notify function for PathSelector. If nodes contains 2 or more nodes
+ * then empress will highlight the selected path. Otherwire, empress
+ * will reset the selected path.
+ *
+ * @param{Array} nodes An array of node keys.
+ */
+ Empress.prototype.pathSelectorUpdate = function (nodes) {
+ if (nodes.length > 1) {
+ // 1: find lowest common ancestor.
+ var lcaNode = this.findLCA(nodes);
+ var lcaBPIndx = this._tree.postorderselect(lcaNode);
+
+ // 2: color branches leading from nodes to lca
+ this.colorLCAPath(nodes, lcaNode);
+
+ // 3: draw tree
+ this.drawTree();
+
+ // return distances
+ var dist = this.calcLCADistance(nodes, lcaNode);
+ this.pathSelector.addDistance(dist);
+ } else {
+ this._drawer.loadPathCoordsBuff([]);
+ this._drawer.loadPathColorBuff([]);
+ this.drawTree();
+ }
+ };
+
return Empress;
});
diff --git a/empress/support_files/js/path-selector.js b/empress/support_files/js/path-selector.js
new file mode 100644
index 00000000..3e07c1ed
--- /dev/null
+++ b/empress/support_files/js/path-selector.js
@@ -0,0 +1,58 @@
+define(["util", "AbstractObserverPattern"], function (
+ util,
+ AbstractObserverPattern
+) {
+ class PathSelector extends AbstractObserverPattern {
+ constructor() {
+ super("pathSelectorUpdate");
+ this.selectedNodeIDs = [];
+ this.selectedNodeNames = [];
+ this.n1Container = document.getElementById("shortest-path-n1-div");
+ this.n2Container = document.getElementById("shortest-path-n2-div");
+ this.distContainer = document.getElementById(
+ "shortest-path-distance-div"
+ );
+ this.resetBtn = document.getElementById("reset-shorest-path");
+ this.resetBtn.onclick = () => {
+ this.reset();
+ };
+ }
+
+ addNode(nodeId, nodeName) {
+ if (this.selectedNodeIDs.length >= 2) {
+ util.toastMsg(
+ "test",
+ "Max nodes selected. Press the reset to clear the selected" +
+ "nodes.",
+ 3000,
+ "warning"
+ );
+ return;
+ }
+
+ this.selectedNodeIDs.push(nodeId);
+ this.selectedNodeNames.push(nodeName);
+ if (this.selectedNodeIDs.length === 1) {
+ this.n1Container.innerText = nodeName;
+ } else {
+ this.n2Container.innerText = nodeName;
+ this.notify(this.selectedNodeIDs);
+ }
+ }
+
+ addDistance(distance) {
+ util.populateNum("shortest-path-distance-div", distance);
+ }
+
+ reset() {
+ this.selectedNodeIDs = [];
+ this.selectedNodeNames = [];
+ this.n1Container.innerText = "";
+ this.n2Container.innerText = "";
+ this.distContainer.innerText = "";
+ this.notify([]);
+ }
+ }
+
+ return PathSelector;
+});
diff --git a/empress/support_files/js/side-panel-handler.js b/empress/support_files/js/side-panel-handler.js
index 5a50128e..25787537 100644
--- a/empress/support_files/js/side-panel-handler.js
+++ b/empress/support_files/js/side-panel-handler.js
@@ -668,32 +668,13 @@ define(["underscore", "Colorer", "util"], function (_, Colorer, util) {
* https://stackoverflow.com/a/31581206/10730311.
*/
SidePanel.prototype.populateTreeStats = function () {
- // Formats a number with just toLocaleString() (leaving the locale
- // unspecified should mean the user's settings are respected).
- // For English, at least, this should mean that numbers are formatted
- // with commas as thousands separators (e.g. 12,345).
- var populateNum = function (htmlID, val, localeOptions) {
- document.getElementById(htmlID).textContent = val.toLocaleString(
- undefined,
- localeOptions
- );
- };
-
- // Formats a number with toLocaleString(), and also limits
- // the number to 4 digits after the decimal point.
- var populateFloat = function (htmlID, val) {
- populateNum(htmlID, val, {
- minimumFractionDigits: 0,
- maximumFractionDigits: 4,
- });
- };
var stats = this.empress.getTreeStats();
- populateNum("stats-tip-count", stats.tipCt);
- populateNum("stats-int-count", stats.intCt);
- populateNum("stats-total-count", stats.allCt);
- populateFloat("stats-min-length", stats.min);
- populateFloat("stats-max-length", stats.max);
- populateFloat("stats-avg-length", stats.avg);
+ util.populateNum("stats-tip-count", stats.tipCt);
+ util.populateNum("stats-int-count", stats.intCt);
+ util.populateNum("stats-total-count", stats.allCt);
+ util.populateFloat("stats-min-length", stats.min);
+ util.populateFloat("stats-max-length", stats.max);
+ util.populateFloat("stats-avg-length", stats.avg);
};
return SidePanel;
diff --git a/empress/support_files/js/tree-controller.js b/empress/support_files/js/tree-controller.js
index 42a42d48..c513433a 100644
--- a/empress/support_files/js/tree-controller.js
+++ b/empress/support_files/js/tree-controller.js
@@ -531,5 +531,54 @@ define([], function () {
return this.model.getCladeNodes(cladeRoot);
};
+ /*
+ * Test to see if i if the ancestor of j.
+ *
+ * @param {Number} i The ith index of the bp array.
+ * @param {Number} j The jth index of the bp array.
+ *
+ * @return true if i is the ancestor of j, false otherwise.
+ */
+ TreeController.prototype.isAncestor = function (i, j) {
+ var shearedTree = this.model.shearedTree;
+ var fullTree = this.model.fullTree;
+
+ var nodeI = shearedTree.postorderselect(
+ this.model.fullToSheared.get(fullTree.postorder(i))
+ );
+ var nodeJ = shearedTree.postorderselect(
+ this.model.fullToSheared.get(fullTree.postorder(j))
+ );
+
+ result = shearedTree.isAncestor(nodeI, nodeJ);
+ return result;
+ };
+
+ /*
+ * Finds the lowest common ancestor of i and j
+ *
+ * @param {Number} i The ith index of the bp array.
+ * @param {Number} j The jth index of the bp array.
+ *
+ * @return The index of the lca of i and j.
+ */
+ TreeController.prototype.lca = function (i, j) {
+ var shearedTree = this.model.shearedTree;
+ var fullTree = this.model.fullTree;
+
+ var nodeI = shearedTree.postorderselect(
+ this.model.fullToSheared.get(fullTree.postorder(i))
+ );
+ var nodeJ = shearedTree.postorderselect(
+ this.model.fullToSheared.get(fullTree.postorder(j))
+ );
+
+ resultNode = shearedTree.postorder(shearedTree.lca(nodeI, nodeJ));
+ resultNode = fullTree.postorderselect(
+ this.model.shearedToFull.get(resultNode)
+ );
+ return resultNode;
+ };
+
return TreeController;
});
diff --git a/empress/support_files/js/util.js b/empress/support_files/js/util.js
index 9ada67bc..d3ce96c8 100644
--- a/empress/support_files/js/util.js
+++ b/empress/support_files/js/util.js
@@ -298,6 +298,28 @@ define(["underscore", "toastr"], function (_, toastr) {
}
}
+ /* Formats a number with just toLocaleString() (leaving the locale
+ * unspecified should mean the user's settings are respected).
+ * For English, at least, this should mean that numbers are formatted
+ * with commas as thousands separators (e.g. 12,345).
+ */
+ function populateNum(htmlID, val, localeOptions) {
+ document.getElementById(htmlID).textContent = val.toLocaleString(
+ undefined,
+ localeOptions
+ );
+ }
+
+ /* Formats a number with toLocaleString(), and also limits
+ * the number to 4 digits after the decimal point.
+ */
+ function populateFloat(htmlID, val) {
+ populateNum(htmlID, val, {
+ minimumFractionDigits: 0,
+ maximumFractionDigits: 4,
+ });
+ }
+
return {
keepUniqueKeys: keepUniqueKeys,
naturalSort: naturalSort,
@@ -307,5 +329,7 @@ define(["underscore", "toastr"], function (_, toastr) {
toastMsg: toastMsg,
assignBarplotLengths: assignBarplotLengths,
removeEmptyArrayKeys: removeEmptyArrayKeys,
+ populateNum: populateNum,
+ populateFloat: populateFloat,
};
});
diff --git a/empress/support_files/templates/empress-template.html b/empress/support_files/templates/empress-template.html
index f7172fc9..c1688ea6 100644
--- a/empress/support_files/templates/empress-template.html
+++ b/empress/support_files/templates/empress-template.html
@@ -119,6 +119,8 @@
'EnableDisableTab': './js/enable-disable-tab',
'EnableDisableSidePanelTab': './js/enable-disable-side-panel-tab',
'EnableDisableAnimationTab': './js/enable-disable-animation-tab',
+ 'AbstractObserverPattern': './js/abstract-observer-pattern',
+ 'PathSelector': './js/path-selector',
}
});
diff --git a/empress/support_files/templates/side-panel.html b/empress/support_files/templates/side-panel.html
index 92bc82a2..37ade480 100644
--- a/empress/support_files/templates/side-panel.html
+++ b/empress/support_files/templates/side-panel.html
@@ -450,6 +450,32 @@
+
+
+
+ Display the shortest path between two nodes.
+
+
+ To select node, hold down the `Shift'
+ and select the nodes on the canvas.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/index.html b/tests/index.html
index b5e8aa1d..993ef10f 100644
--- a/tests/index.html
+++ b/tests/index.html
@@ -165,7 +165,12 @@
-
+
+
+
+
+
+
{{ emperor_base_dependencies }}
@@ -78,6 +80,10 @@
+
+
+
+