|
|
- /**
- * Created by Alex on 3/3/2015.
- */
-
- class LayoutEngine {
- constructor(body) {
- this.body = body;
- }
-
- setOptions(options) {
-
- }
-
- positionInitially(nodesArray) {
- for (var i = 0; i < nodesArray.length; i++) {
- let node = nodesArray[i];
- if ((!node.isFixed()) && (node.x === null || node.y === null)) {
- var radius = 10 * 0.1*nodesArray.length + 10;
- var angle = 2 * Math.PI * Math.random();
- if (node.options.fixed.x == false) {node.x = radius * Math.cos(angle);}
- if (node.options.fixed.x == false) {node.y = radius * Math.sin(angle);}
- }
- }
- }
-
- _resetLevels() {
- for (var nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- var node = this.body.nodes[nodeId];
- if (node.preassignedLevel == false) {
- node.level = -1;
- node.hierarchyEnumerated = false;
- }
- }
- }
- }
-
- /**
- * This is the main function to layout the nodes in a hierarchical way.
- * It checks if the node details are supplied correctly
- *
- * @private
- */
- _setupHierarchicalLayout() {
- if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
- // get the size of the largest hubs and check if the user has defined a level for a node.
- var hubsize = 0;
- var node, nodeId;
- var definedLevel = false;
- var undefinedLevel = false;
-
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- if (node.level != -1) {
- definedLevel = true;
- }
- else {
- undefinedLevel = true;
- }
- if (hubsize < node.edges.length) {
- hubsize = node.edges.length;
- }
- }
- }
-
- // if the user defined some levels but not all, alert and run without hierarchical layout
- if (undefinedLevel == true && definedLevel == true) {
- throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
- this.zoomExtent({duration:0},true,this.constants.clustering.enabled);
- if (!this.constants.clustering.enabled) {
- this.start();
- }
- }
- else {
- // setup the system to use hierarchical method.
- this._changeConstants();
-
- // define levels if undefined by the users. Based on hubsize
- if (undefinedLevel == true) {
- if (this.constants.hierarchicalLayout.layout == "hubsize") {
- this._determineLevels(hubsize);
- }
- else {
- this._determineLevelsDirected(false);
- }
-
- }
- // check the distribution of the nodes per level.
- var distribution = this._getDistribution();
-
- // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
- this._placeNodesByHierarchy(distribution);
- }
- }
- }
-
-
- /**
- * This function places the nodes on the canvas based on the hierarchial distribution.
- *
- * @param {Object} distribution | obtained by the function this._getDistribution()
- * @private
- */
- _placeNodesByHierarchy(distribution) {
- var nodeId, node;
-
- // start placing all the level 0 nodes first. Then recursively position their branches.
- for (var level in distribution) {
- if (distribution.hasOwnProperty(level)) {
-
- for (nodeId in distribution[level].nodes) {
- if (distribution[level].nodes.hasOwnProperty(nodeId)) {
- node = distribution[level].nodes[nodeId];
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (node.xFixed) {
- node.x = distribution[level].minPos;
- node.xFixed = false;
-
- distribution[level].minPos += distribution[level].nodeSpacing;
- }
- }
- else {
- if (node.yFixed) {
- node.y = distribution[level].minPos;
- node.yFixed = false;
-
- distribution[level].minPos += distribution[level].nodeSpacing;
- }
- }
- this._placeBranchNodes(node.edges,node.id,distribution,node.level);
- }
- }
- }
- }
-
- // stabilize the system after positioning. This function calls zoomExtent.
- this._stabilize();
- }
-
-
- /**
- * This function get the distribution of levels based on hubsize
- *
- * @returns {Object}
- * @private
- */
- _getDistribution() {
- var distribution = {};
- var nodeId, node, level;
-
- // we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
- // the fix of X is removed after the x value has been set.
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- node.xFixed = true;
- node.yFixed = true;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
- }
- else {
- node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
- }
- if (distribution[node.level] === undefined) {
- distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
- }
- distribution[node.level].amount += 1;
- distribution[node.level].nodes[nodeId] = node;
- }
- }
-
- // determine the largest amount of nodes of all levels
- var maxCount = 0;
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- if (maxCount < distribution[level].amount) {
- maxCount = distribution[level].amount;
- }
- }
- }
-
- // set the initial position and spacing of each nodes accordingly
- for (level in distribution) {
- if (distribution.hasOwnProperty(level)) {
- distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
- distribution[level].nodeSpacing /= (distribution[level].amount + 1);
- distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
- }
- }
-
- return distribution;
- }
-
-
- /**
- * this function allocates nodes in levels based on the recursive branching from the largest hubs.
- *
- * @param hubsize
- * @private
- */
- _determineLevels(hubsize) {
- var nodeId, node;
-
- // determine hubs
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- if (node.edges.length == hubsize) {
- node.level = 0;
- }
- }
- }
-
- // branch from hubs
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- if (node.level == 0) {
- this._setLevel(1,node.edges,node.id);
- }
- }
- }
- }
-
-
-
- /**
- * this function allocates nodes in levels based on the direction of the edges
- *
- * @param hubsize
- * @private
- */
- _determineLevelsDirected() {
- var nodeId, node, firstNode;
- var minLevel = 10000;
-
- // set first node to source
- firstNode = this.body.nodes[this.nodeIndices[0]];
- firstNode.level = minLevel;
- this._setLevelDirected(minLevel,firstNode.edges,firstNode.id);
-
- // get the minimum level
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- minLevel = node.level < minLevel ? node.level : minLevel;
- }
- }
-
- // subtract the minimum from the set so we have a range starting from 0
- for (nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- node = this.body.nodes[nodeId];
- node.level -= minLevel;
- }
- }
- }
-
-
- /**
- * Since hierarchical layout does not support:
- * - smooth curves (based on the physics),
- * - clustering (based on dynamic node counts)
- *
- * We disable both features so there will be no problems.
- *
- * @private
- */
- _changeConstants() {
- this.constants.clustering.enabled = false;
- this.constants.physics.barnesHut.enabled = false;
- this.constants.physics.hierarchicalRepulsion.enabled = true;
- this._loadSelectedForceSolver();
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.dynamic = false;
- }
- this._configureSmoothCurves();
-
- var config = this.constants.hierarchicalLayout;
- config.levelSeparation = Math.abs(config.levelSeparation);
- if (config.direction == "RL" || config.direction == "DU") {
- config.levelSeparation *= -1;
- }
-
- if (config.direction == "RL" || config.direction == "LR") {
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.type = "vertical";
- }
- }
- else {
- if (this.constants.smoothCurves.enabled == true) {
- this.constants.smoothCurves.type = "horizontal";
- }
- }
- }
-
-
- /**
- * This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
- * on a X position that ensures there will be no overlap.
- *
- * @param edges
- * @param parentId
- * @param distribution
- * @param parentLevel
- * @private
- */
- _placeBranchNodes(edges, parentId, distribution, parentLevel) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- }
- else {
- childNode = edges[i].to;
- }
-
- // if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
- var nodeMoved = false;
- if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
- if (childNode.xFixed && childNode.level > parentLevel) {
- childNode.xFixed = false;
- childNode.x = distribution[childNode.level].minPos;
- nodeMoved = true;
- }
- }
- else {
- if (childNode.yFixed && childNode.level > parentLevel) {
- childNode.yFixed = false;
- childNode.y = distribution[childNode.level].minPos;
- nodeMoved = true;
- }
- }
-
- if (nodeMoved == true) {
- distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
- if (childNode.edges.length > 1) {
- this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
- }
- }
- }
- }
-
-
- /**
- * this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
- *
- * @param level
- * @param edges
- * @param parentId
- * @private
- */
- _setLevel(level, edges, parentId) {
- for (var i = 0; i < edges.length; i++) {
- var childNode = null;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- }
- else {
- childNode = edges[i].to;
- }
- if (childNode.level == -1 || childNode.level > level) {
- childNode.level = level;
- if (childNode.edges.length > 1) {
- this._setLevel(level+1, childNode.edges, childNode.id);
- }
- }
- }
- }
-
- /**
- * this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
- *
- * @param level
- * @param edges
- * @param parentId
- * @private
- */
- _setLevelDirected(level, edges, parentId) {
- this.body.nodes[parentId].hierarchyEnumerated = true;
- var childNode, direction;
- for (var i = 0; i < edges.length; i++) {
- direction = 1;
- if (edges[i].toId == parentId) {
- childNode = edges[i].from;
- direction = -1;
- }
- else {
- childNode = edges[i].to;
- }
- if (childNode.level == -1) {
- childNode.level = level + direction;
- }
- }
-
- for (var i = 0; i < edges.length; i++) {
- if (edges[i].toId == parentId) {childNode = edges[i].from;}
- else {childNode = edges[i].to;}
-
- if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
- this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
- }
- }
- }
-
-
- /**
- * Unfix nodes
- *
- * @private
- */
- _restoreNodes() {
- for (var nodeId in this.body.nodes) {
- if (this.body.nodes.hasOwnProperty(nodeId)) {
- this.body.nodes[nodeId].xFixed = false;
- this.body.nodes[nodeId].yFixed = false;
- }
- }
- }
-
-
- }
-
- export default LayoutEngine;
|