diff --git a/lib/network/modules/LayoutEngine.js b/lib/network/modules/LayoutEngine.js index 09468955..e2466792 100644 --- a/lib/network/modules/LayoutEngine.js +++ b/lib/network/modules/LayoutEngine.js @@ -3,6 +3,171 @@ let util = require('../../util'); import NetworkUtil from '../NetworkUtil'; + +/** + * Container for derived data on current network, relating to hierarchy. + * + * Local, private class. + * + * TODO: Perhaps move more code for hierarchy state handling to this class. + * Till now, only the required and most obvious has been done. + */ +class HierarchicalStatus { + + constructor() { + this.childrenReference = {}; + this.parentReference = {}; + this.levels = {}; + this.trees = {}; + + this.isTree = false; + } + + + /** + * Add the relation between given nodes to the current state. + */ + addRelation(parentNodeId, childNodeId) { + if (this.childrenReference[parentNodeId] === undefined) { + this.childrenReference[parentNodeId] = []; + } + this.childrenReference[parentNodeId].push(childNodeId); + + if (this.parentReference[childNodeId] === undefined) { + this.parentReference[childNodeId] = []; + } + this.parentReference[childNodeId].push(parentNodeId); + } + + + /** + * Check if the current state is for a tree or forest network. + * + * This is the case if every node has at most one parent. + * + * Pre: parentReference init'ed properly for current network + */ + checkIfTree() { + for (let i in this.parentReference) { + if (this.parentReference[i].length > 1) { + this.isTree = false; + return; + } + } + + this.isTree = true; + } + + + /** + * Ensure level for given id is defined. + * + * Sets level to zero for given node id if not already present + */ + ensureLevel(nodeId) { + if (this.levels[nodeId] === undefined) { + this.levels[nodeId] = 0; + } + } + + + /** + * get the maximum level of a branch. + * + * TODO: Never entered; find a test case to test this! + */ + getMaxLevel(nodeId) { + let accumulator = {}; + + let _getMaxLevel = (nodeId) => { + if (accumulator[nodeId] !== undefined) { + return accumulator[nodeId]; + } + let level = this.levels[nodeId]; + if (this.childrenReference[nodeId]) { + let children = this.childrenReference[nodeId]; + if (children.length > 0) { + for (let i = 0; i < children.length; i++) { + level = Math.max(level,_getMaxLevel(children[i])); + } + } + } + accumulator[nodeId] = level; + return level; + }; + + return _getMaxLevel(nodeId); + } + + + levelDownstream(nodeA, nodeB) { + if (this.levels[nodeB.id] === undefined) { + // set initial level + if (this.levels[nodeA.id] === undefined) { + this.levels[nodeA.id] = 0; + } + // set level + this.levels[nodeB.id] = this.levels[nodeA.id] + 1; + } + } + + + /** + * Small util method to set the minimum levels of the nodes to zero. + */ + setMinLevelToZero(nodes) { + let minLevel = 1e9; + // get the minimum level + for (let nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (this.levels[nodeId] !== undefined) { + minLevel = Math.min(this.levels[nodeId], minLevel); + } + } + } + + // subtract the minimum from the set so we have a range starting from 0 + for (let nodeId in nodes) { + if (nodes.hasOwnProperty(nodeId)) { + if (this.levels[nodeId] !== undefined) { + this.levels[nodeId] -= minLevel; + } + } + } + } + + + /** + * Get the min and max xy-coordinates of a given tree + */ + getTreeSize(nodes, index) { + let min_x = 1e9; + let max_x = -1e9; + let min_y = 1e9; + let max_y = -1e9; + + for (let nodeId in this.trees) { + if (this.trees.hasOwnProperty(nodeId)) { + if (this.trees[nodeId] === index) { + let node = nodes[nodeId]; + min_x = Math.min(node.x, min_x); + max_x = Math.max(node.x, max_x); + min_y = Math.min(node.y, min_y); + max_y = Math.max(node.y, max_y); + } + } + } + + return { + min_x: min_x, + max_x: max_x, + min_y: min_y, + max_y: max_y + }; + } +} + + class LayoutEngine { constructor(body) { this.body = body; @@ -308,11 +473,8 @@ class LayoutEngine { let definedLevel = false; let definedPositions = true; let undefinedLevel = false; - this.hierarchicalLevels = {}; this.lastNodeOnLevel = {}; - this.hierarchicalChildrenReference = {}; - this.hierarchicalParentReference = {}; - this.hierarchicalTrees = {}; + this.hierarchical = new HierarchicalStatus(); this.treeIndex = -1; this.distributionOrdering = {}; @@ -328,7 +490,7 @@ class LayoutEngine { } if (node.options.level !== undefined) { definedLevel = true; - this.hierarchicalLevels[nodeId] = node.options.level; + this.hierarchical.levels[nodeId] = node.options.level; } else { undefinedLevel = true; @@ -358,9 +520,7 @@ class LayoutEngine { // fallback for cases where there are nodes but no edges for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] === undefined) { - this.hierarchicalLevels[nodeId] = 0; - } + this.hierarchical.ensureLevel(nodeId); } } // check the distribution of the nodes per level. @@ -402,9 +562,9 @@ class LayoutEngine { // shift a single tree by an offset let shiftTree = (index, offset) => { - for (let nodeId in this.hierarchicalTrees) { - if (this.hierarchicalTrees.hasOwnProperty(nodeId)) { - if (this.hierarchicalTrees[nodeId] === index) { + for (let nodeId in this.hierarchical.trees) { + if (this.hierarchical.trees.hasOwnProperty(nodeId)) { + if (this.hierarchical.trees[nodeId] === index) { let node = this.body.nodes[nodeId]; let pos = this._getPositionForHierarchy(node); this._setPositionForHierarchy(node, pos + offset, undefined, true); @@ -415,18 +575,12 @@ class LayoutEngine { // get the width of a tree let getTreeSize = (index) => { - let min = 1e9; - let max = -1e9; - for (let nodeId in this.hierarchicalTrees) { - if (this.hierarchicalTrees.hasOwnProperty(nodeId)) { - if (this.hierarchicalTrees[nodeId] === index) { - let pos = this._getPositionForHierarchy(this.body.nodes[nodeId]); - min = Math.min(pos, min); - max = Math.max(pos, max); - } - } + let res = this.hierarchical.getTreeSize(this.body.nodes, index); + if (this._isVertical()) { + return {min: res.min_x, max: res.max_x}; + } else { + return {min: res.min_y, max: res.max_y}; } - return {min:min, max:max}; }; // get the width of all trees @@ -445,8 +599,8 @@ class LayoutEngine { return; } map[source.id] = true; - if (this.hierarchicalChildrenReference[source.id]) { - let children = this.hierarchicalChildrenReference[source.id]; + if (this.hierarchical.childrenReference[source.id]) { + let children = this.hierarchical.childrenReference[source.id]; if (children.length > 0) { for (let i = 0; i < children.length; i++) { getBranchNodes(this.body.nodes[children[i]], map); @@ -465,7 +619,7 @@ class LayoutEngine { for (let branchNode in branchMap) { if (branchMap.hasOwnProperty(branchNode)) { let node = this.body.nodes[branchNode]; - let level = this.hierarchicalLevels[node.id]; + let level = this.hierarchical.levels[node.id]; let position = this._getPositionForHierarchy(node); // get the space around the node. @@ -484,39 +638,18 @@ class LayoutEngine { return [min, max, minSpace, maxSpace]; }; - // get the maximum level of a branch. - let getMaxLevel = (nodeId) => { - let accumulator = {}; - let _getMaxLevel = (nodeId) => { - if (accumulator[nodeId] !== undefined) { - return accumulator[nodeId]; - } - let level = this.hierarchicalLevels[nodeId]; - if (this.hierarchicalChildrenReference[nodeId]) { - let children = this.hierarchicalChildrenReference[nodeId]; - if (children.length > 0) { - for (let i = 0; i < children.length; i++) { - level = Math.max(level,_getMaxLevel(children[i])); - } - } - } - accumulator[nodeId] = level; - return level; - }; - return _getMaxLevel(nodeId); - }; // check what the maximum level is these nodes have in common. let getCollisionLevel = (node1, node2) => { - let maxLevel1 = getMaxLevel(node1.id); - let maxLevel2 = getMaxLevel(node2.id); + let maxLevel1 = this.hierarchical.getMaxLevel(node1.id); + let maxLevel2 = this.hierarchical.getMaxLevel(node2.id); return Math.min(maxLevel1, maxLevel2); }; // check if two nodes have the same parent(s) let hasSameParent = (node1, node2) => { - let parents1 = this.hierarchicalParentReference[node1.id]; - let parents2 = this.hierarchicalParentReference[node2.id]; + let parents1 = this.hierarchical.parentReference[node1.id]; + let parents2 = this.hierarchical.parentReference[node2.id]; if (parents1 === undefined || parents2 === undefined) { return false; } @@ -539,7 +672,7 @@ class LayoutEngine { if (levelNodes.length > 1) { for (let j = 0; j < levelNodes.length - 1; j++) { if (hasSameParent(levelNodes[j],levelNodes[j+1]) === true) { - if (this.hierarchicalTrees[levelNodes[j].id] === this.hierarchicalTrees[levelNodes[j+1].id]) { + if (this.hierarchical.trees[levelNodes[j].id] === this.hierarchical.trees[levelNodes[j+1].id]) { callback(levelNodes[j],levelNodes[j+1], centerParents); } }} @@ -593,7 +726,7 @@ class LayoutEngine { // console.log("ts",node.id); let nodeId = node.id; let allEdges = node.edges; - let nodeLevel = this.hierarchicalLevels[node.id]; + let nodeLevel = this.hierarchical.levels[node.id]; // gather constants let C2 = this.options.hierarchical.levelSeparation * this.options.hierarchical.levelSeparation; @@ -604,7 +737,7 @@ class LayoutEngine { if (edge.toId != edge.fromId) { let otherNode = edge.toId == nodeId ? edge.from : edge.to; referenceNodes[allEdges[i].id] = otherNode; - if (this.hierarchicalLevels[otherNode.id] < nodeLevel) { + if (this.hierarchical.levels[otherNode.id] < nodeLevel) { aboveEdges.push(edge); } } @@ -802,7 +935,7 @@ class LayoutEngine { if (map === undefined) { useMap = false; } - let level = this.hierarchicalLevels[node.id]; + let level = this.hierarchical.levels[node.id]; if (level !== undefined) { let index = this.distributionIndex[node.id]; let position = this._getPositionForHierarchy(node); @@ -837,16 +970,16 @@ class LayoutEngine { * @private */ _centerParent(node) { - if (this.hierarchicalParentReference[node.id]) { - let parents = this.hierarchicalParentReference[node.id]; + if (this.hierarchical.parentReference[node.id]) { + let parents = this.hierarchical.parentReference[node.id]; for (var i = 0; i < parents.length; i++) { let parentId = parents[i]; let parentNode = this.body.nodes[parentId]; - if (this.hierarchicalChildrenReference[parentId]) { + if (this.hierarchical.childrenReference[parentId]) { // get the range of the children let minPos = 1e9; let maxPos = -1e9; - let children = this.hierarchicalChildrenReference[parentId]; + let children = this.hierarchical.childrenReference[parentId]; if (children.length > 0) { for (let i = 0; i < children.length; i++) { let childNode = this.body.nodes[children[i]]; @@ -893,7 +1026,7 @@ class LayoutEngine { // we get the X or Y values we need and store them in pos and previousPos. The get and set make sure we get X or Y if (handledNodeCount > 0) {pos = this._getPositionForHierarchy(nodeArray[i-1]) + this.options.hierarchical.nodeSpacing;} this._setPositionForHierarchy(node, pos, level); - this._validataPositionAndContinue(node, level, pos); + this._validatePositionAndContinue(node, level, pos); handledNodeCount++; } @@ -913,14 +1046,14 @@ class LayoutEngine { */ _placeBranchNodes(parentId, parentLevel) { // if this is not a parent, cancel the placing. This can happen with multiple parents to one child. - if (this.hierarchicalChildrenReference[parentId] === undefined) { + if (this.hierarchical.childrenReference[parentId] === undefined) { return; } // get a list of childNodes let childNodes = []; - for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) { - childNodes.push(this.body.nodes[this.hierarchicalChildrenReference[parentId][i]]); + for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { + childNodes.push(this.body.nodes[this.hierarchical.childrenReference[parentId][i]]); } // use the positions to order the nodes. @@ -929,7 +1062,7 @@ class LayoutEngine { // position the childNodes for (let i = 0; i < childNodes.length; i++) { let childNode = childNodes[i]; - let childNodeLevel = this.hierarchicalLevels[childNode.id]; + let childNodeLevel = this.hierarchical.levels[childNode.id]; // check if the child node is below the parent node and if it has already been positioned. if (childNodeLevel > parentLevel && this.positionedNodes[childNode.id] === undefined) { // get the amount of space required for this node. If parent the width is based on the amount of children. @@ -939,7 +1072,7 @@ class LayoutEngine { if (i === 0) {pos = this._getPositionForHierarchy(this.body.nodes[parentId]);} else {pos = this._getPositionForHierarchy(childNodes[i-1]) + this.options.hierarchical.nodeSpacing;} this._setPositionForHierarchy(childNode, pos, childNodeLevel); - this._validataPositionAndContinue(childNode, childNodeLevel, pos); + this._validatePositionAndContinue(childNode, childNodeLevel, pos); } else { return; @@ -966,7 +1099,11 @@ class LayoutEngine { * @param pos * @private */ - _validataPositionAndContinue(node, level, pos) { + _validatePositionAndContinue(node, level, pos) { + // This only works for strict hierarchical networks, i.e. trees and forests + // Early exit if this is not the case + if (!this.hierarchical.isTree) return; + // if overlap has been detected, we shift the branch if (this.lastNodeOnLevel[level] !== undefined) { let previousPos = this._getPositionForHierarchy(this.body.nodes[this.lastNodeOnLevel[level]]); @@ -1013,7 +1150,7 @@ class LayoutEngine { for (nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { node = this.body.nodes[nodeId]; - let level = this.hierarchicalLevels[nodeId] === undefined ? 0 : this.hierarchicalLevels[nodeId]; + let level = this.hierarchical.levels[nodeId] === undefined ? 0 : this.hierarchical.levels[nodeId]; if (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU') { node.y = this.options.hierarchical.levelSeparation * level; node.options.fixed.y = true; @@ -1043,7 +1180,7 @@ class LayoutEngine { for (let nodeId in this.body.nodes) { if (this.body.nodes.hasOwnProperty(nodeId)) { let node = this.body.nodes[nodeId]; - if (this.hierarchicalLevels[nodeId] === undefined) { + if (this.hierarchical.levels[nodeId] === undefined) { hubSize = node.edges.length < hubSize ? hubSize : node.edges.length; } } @@ -1062,15 +1199,8 @@ class LayoutEngine { let hubSize = 1; let levelDownstream = (nodeA, nodeB) => { - if (this.hierarchicalLevels[nodeB.id] === undefined) { - // set initial level - if (this.hierarchicalLevels[nodeA.id] === undefined) { - this.hierarchicalLevels[nodeA.id] = 0; - } - // set level - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1; - } - }; + this.hierarchical.levelDownstream(nodeA, nodeB); + } while (hubSize > 0) { // determine hubs @@ -1089,8 +1219,11 @@ class LayoutEngine { } } + /** * TODO: release feature + * TODO: Determine if this feature is needed at all + * * @private */ _determineLevelsCustomCallback() { @@ -1101,10 +1234,12 @@ class LayoutEngine { }; + // TODO: perhaps move to HierarchicalStatus. + // But I currently don't see the point, this method is not used. let levelByDirection = (nodeA, nodeB, edge) => { - let levelA = this.hierarchicalLevels[nodeA.id]; + let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;} + if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} let diff = customCallback( NetworkUtil.cloneOptions(nodeA,'node'), @@ -1112,11 +1247,11 @@ class LayoutEngine { NetworkUtil.cloneOptions(edge,'edge') ); - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + diff; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + diff; }; this._crawlNetwork(levelByDirection); - this._setMinLevelToZero(); + this.hierarchical.setMinLevelToZero(this.body.nodes); } /** @@ -1127,45 +1262,21 @@ class LayoutEngine { */ _determineLevelsDirected() { let minLevel = 10000; + let levelByDirection = (nodeA, nodeB, edge) => { - let levelA = this.hierarchicalLevels[nodeA.id]; + let levelA = this.hierarchical.levels[nodeA.id]; // set initial level - if (levelA === undefined) {this.hierarchicalLevels[nodeA.id] = minLevel;} + if (levelA === undefined) {this.hierarchical.levels[nodeA.id] = minLevel;} if (edge.toId == nodeB.id) { - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] + 1; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] + 1; } else { - this.hierarchicalLevels[nodeB.id] = this.hierarchicalLevels[nodeA.id] - 1; + this.hierarchical.levels[nodeB.id] = this.hierarchical.levels[nodeA.id] - 1; } }; - this._crawlNetwork(levelByDirection); - this._setMinLevelToZero(); - } - - /** - * Small util method to set the minimum levels of the nodes to zero. - * @private - */ - _setMinLevelToZero() { - let minLevel = 1e9; - // get the minimum level - for (let nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] !== undefined) { - minLevel = Math.min(this.hierarchicalLevels[nodeId], minLevel); - } - } - } - - // subtract the minimum from the set so we have a range starting from 0 - for (let nodeId in this.body.nodes) { - if (this.body.nodes.hasOwnProperty(nodeId)) { - if (this.hierarchicalLevels[nodeId] !== undefined) { - this.hierarchicalLevels[nodeId] -= minLevel; - } - } - } + this._crawlNetwork(levelByDirection); + this.hierarchical.setMinLevelToZero(this.body.nodes); } @@ -1175,21 +1286,13 @@ class LayoutEngine { */ _generateMap() { let fillInRelations = (parentNode, childNode) => { - if (this.hierarchicalLevels[childNode.id] > this.hierarchicalLevels[parentNode.id]) { - let parentNodeId = parentNode.id; - let childNodeId = childNode.id; - if (this.hierarchicalChildrenReference[parentNodeId] === undefined) { - this.hierarchicalChildrenReference[parentNodeId] = []; - } - this.hierarchicalChildrenReference[parentNodeId].push(childNodeId); - if (this.hierarchicalParentReference[childNodeId] === undefined) { - this.hierarchicalParentReference[childNodeId] = []; - } - this.hierarchicalParentReference[childNodeId].push(parentNodeId); + if (this.hierarchical.levels[childNode.id] > this.hierarchical.levels[parentNode.id]) { + this.hierarchical.addRelation(parentNode.id, childNode.id); } }; this._crawlNetwork(fillInRelations); + this.hierarchical.checkIfTree(); } @@ -1206,8 +1309,8 @@ class LayoutEngine { let crawler = (node, tree) => { if (progress[node.id] === undefined) { - if (this.hierarchicalTrees[node.id] === undefined) { - this.hierarchicalTrees[node.id] = tree; + if (this.hierarchical.trees[node.id] === undefined) { + this.hierarchical.trees[node.id] = tree; this.treeIndex = Math.max(tree, this.treeIndex); } @@ -1272,9 +1375,9 @@ class LayoutEngine { else { this.body.nodes[parentId].y += diff; } - if (this.hierarchicalChildrenReference[parentId] !== undefined) { - for (let i = 0; i < this.hierarchicalChildrenReference[parentId].length; i++) { - shifter(this.hierarchicalChildrenReference[parentId][i]); + if (this.hierarchical.childrenReference[parentId] !== undefined) { + for (let i = 0; i < this.hierarchical.childrenReference[parentId].length; i++) { + shifter(this.hierarchical.childrenReference[parentId][i]); } } }; @@ -1292,18 +1395,18 @@ class LayoutEngine { _findCommonParent(childA,childB) { let parents = {}; let iterateParents = (parents,child) => { - if (this.hierarchicalParentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) { - let parent = this.hierarchicalParentReference[child][i]; + if (this.hierarchical.parentReference[child] !== undefined) { + for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { + let parent = this.hierarchical.parentReference[child][i]; parents[parent] = true; iterateParents(parents, parent) } } }; let findParent = (parents, child) => { - if (this.hierarchicalParentReference[child] !== undefined) { - for (let i = 0; i < this.hierarchicalParentReference[child].length; i++) { - let parent = this.hierarchicalParentReference[child][i]; + if (this.hierarchical.parentReference[child] !== undefined) { + for (let i = 0; i < this.hierarchical.parentReference[child].length; i++) { + let parent = this.hierarchical.parentReference[child][i]; if (parents[parent] !== undefined) { return {foundParent:parent, withChild:child}; } @@ -1350,6 +1453,18 @@ class LayoutEngine { } } + + /** + * Utility function to cut down on typing this all the time. + * + * TODO: use this in all applicable situations in this class. + * + * @private + */ + _isVertical() { + return (this.options.hierarchical.direction === 'UD' || this.options.hierarchical.direction === 'DU'); + } + /** * Abstract the getting of the position of a node so we do not have to repeat the direction check all the time. * @param node @@ -1384,9 +1499,6 @@ class LayoutEngine { } } } - - - } export default LayoutEngine;