Browse Source

removed sectors, physics is almost stand-alone. Edges need better options for physics.

flowchartTest
Alex de Mulder 9 years ago
parent
commit
2edd8b82ce
11 changed files with 1084 additions and 1005 deletions
  1. +899
    -150
      dist/vis.js
  2. +72
    -122
      lib/network/Network.js
  3. +0
    -1
      lib/network/Node.js
  4. +0
    -26
      lib/network/mixins/MixinLoader.js
  5. +2
    -3
      lib/network/mixins/SelectionMixin.js
  6. +0
    -647
      lib/network/modules/ClusterEngine.js
  7. +2
    -2
      lib/network/modules/Clustering.js
  8. +81
    -10
      lib/network/modules/PhysicsEngine.js
  9. +10
    -4
      lib/network/modules/components/physics/BarnesHutSolver.js
  10. +9
    -3
      lib/network/modules/components/physics/CentralGravitySolver.js
  11. +9
    -37
      lib/network/modules/components/physics/SpringSolver.js

+ 899
- 150
dist/vis.js
File diff suppressed because it is too large
View File


+ 72
- 122
lib/network/Network.js View File

@ -19,7 +19,7 @@ var locales = require('./locales');
// Load custom shapes into CanvasRenderingContext2D
require('./shapes');
import { PhysicsEngine } from './modules/PhysicsEngine'
import { ClusterEngine } from './modules/Clustering'
/**
@ -65,8 +65,6 @@ function Network (container, data, options) {
}
};
// set constant values
this.defaultOptions = {
nodes: {
@ -142,7 +140,6 @@ function Network (container, data, options) {
configurePhysics:false,
physics: {
barnesHut: {
enabled: true,
thetaInverted: 1 / 0.5, // inverted to save time during calculation
gravitationalConstant: -2000,
centralGravity: 0.3,
@ -158,20 +155,13 @@ function Network (container, data, options) {
damping: 0.09
},
hierarchicalRepulsion: {
enabled: false,
centralGravity: 0.0,
springLength: 100,
springConstant: 0.01,
nodeDistance: 150,
damping: 0.09
},
damping: null,
centralGravity: null,
springLength: null,
springConstant: null
},
clustering: { // Per Node in Cluster = PNiC
enabled: false // (Boolean) | global on/off switch for clustering.
model:'BarnesHut'
},
navigation: {
enabled: false
@ -232,9 +222,10 @@ function Network (container, data, options) {
// containers for nodes and edges
this.body = {
sectors: {},
nodeIndices: [],
nodes: {},
nodeIndices: [],
supportNodes: {},
supportNodeIndices: [],
edges: {},
data: {
nodes: null, // A DataSet or DataView
@ -251,6 +242,10 @@ function Network (container, data, options) {
}
};
// modules
this.clustering = new ClusterEngine(this.body);
this.physics = new PhysicsEngine(this.body);
this.pixelRatio = 1;
@ -288,11 +283,9 @@ function Network (container, data, options) {
// loading all the mixins:
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
//this._loadPhysicsSystem();
// create a frame and canvas
this._create();
// load the sector system. (mandatory, fully integrated with Network)
this._loadSectorSystem();
// load the cluster system. (mandatory, even when not using the cluster system, there are function calls to it)
this._loadClusterSystem();
// load the selection system. (mandatory, required by Network)
@ -313,8 +306,6 @@ function Network (container, data, options) {
this.stabilizationIterations = null;
this.draggingNodes = false;
this.clustering = new ClusterEngine(this.body);
// position and scale variables and objects
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
@ -351,6 +342,7 @@ function Network (container, data, options) {
}
};
// properties for the animation
this.moving = true;
this.timer = undefined; // Scheduling function. Is definded in this.start();
@ -377,7 +369,7 @@ function Network (container, data, options) {
// this event will trigger a rebuilding of the cache of colors, nodes etc.
this.on("_dataChanged", function () {
me._updateNodeIndexList();
me._updateCalculationNodes();
me.physics._updateCalculationNodes();
me._markAllEdgesAsDirty();
if (me.initializing !== true) {
me.moving = true;
@ -605,7 +597,7 @@ Network.prototype.zoomExtent = function(options, initialZoom, disableStart) {
* @private
*/
Network.prototype._updateNodeIndexList = function() {
this._clearNodeIndexList();
this.body.supportNodeIndices = Object.keys(this.body.supportNodes)
this.body.nodeIndices = Object.keys(this.body.nodes);
};
@ -665,7 +657,6 @@ Network.prototype.setData = function(data, disableStart) {
this._setNodes(data && data.nodes);
this._setEdges(data && data.edges);
}
this._putDataInSector();
if (disableStart == false) {
if (this.constants.hierarchicalLayout.enabled == true) {
@ -695,7 +686,7 @@ Network.prototype.setData = function(data, disableStart) {
Network.prototype.setOptions = function (options) {
if (options) {
var prop;
var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','clustering','navigation',
var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
];
// extend all but the values in fields
@ -707,24 +698,14 @@ Network.prototype.setOptions = function (options) {
if (options.physics) {
util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
util.mergeOptions(this.constants.physics, options.physics,'repulsion');
if (options.physics.hierarchicalRepulsion) {
this.constants.hierarchicalLayout.enabled = true;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this.constants.physics.barnesHut.enabled = false;
for (prop in options.physics.hierarchicalRepulsion) {
if (options.physics.hierarchicalRepulsion.hasOwnProperty(prop)) {
this.constants.physics.hierarchicalRepulsion[prop] = options.physics.hierarchicalRepulsion[prop];
}
}
}
util.mergeOptions(this.constants.physics, options.physics,'hierarchicalRepulsion');
}
if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
if (options.onAdd) {this.triggerFunctions.add = options.onAdd;}
if (options.onEdit) {this.triggerFunctions.edit = options.onEdit;}
if (options.onEditEdge) {this.triggerFunctions.editEdge = options.onEditEdge;}
if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
if (options.onConnect) {this.triggerFunctions.connect = options.onConnect;}
if (options.onDelete) {this.triggerFunctions.del = options.onDelete;}
util.mergeOptions(this.constants, options,'smoothCurves');
util.mergeOptions(this.constants, options,'hierarchicalLayout');
@ -816,7 +797,7 @@ Network.prototype.setOptions = function (options) {
// (Re)loading the mixins that can be enabled or disabled in the options.
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
this.physics.setOptions(this.constants.physics);
// load the navigation system.
this._loadNavigationControls();
// load the data manipulation system
@ -1700,7 +1681,7 @@ Network.prototype._addNodes = function(ids) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this._updateCalculationNodes();
this.physics._updateCalculationNodes();
this._reconnectEdges();
this._updateValueRange(this.body.nodes);
};
@ -1771,7 +1752,7 @@ Network.prototype._removeNodes = function(ids) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this._updateCalculationNodes();
this.physics._updateCalculationNodes();
this._reconnectEdges();
this._updateSelection();
this._updateValueRange(nodes);
@ -1848,7 +1829,7 @@ Network.prototype._addEdges = function (ids) {
this.moving = true;
this._updateValueRange(edges);
this._createBezierNodes();
this._updateCalculationNodes();
this.physics._updateCalculationNodes();
if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
this._resetLevels();
this._setupHierarchicalLayout();
@ -1911,7 +1892,7 @@ Network.prototype._removeEdges = function (ids) {
var edge = edges[id];
if (edge) {
if (edge.via != null) {
delete this.body.sectors['support']['nodes'][edge.via.id];
delete this.body.supportNodes[edge.via.id];
}
edge.disconnect();
delete edges[id];
@ -1924,7 +1905,7 @@ Network.prototype._removeEdges = function (ids) {
this._resetLevels();
this._setupHierarchicalLayout();
}
this._updateCalculationNodes();
this.physics._updateCalculationNodes();
};
/**
@ -2013,10 +1994,7 @@ Network.prototype._requestRedraw = function(hidden) {
}
};
Network.prototype._redraw = function(hidden, requested) {
if (hidden === undefined) {
hidden = false;
}
Network.prototype._redraw = function(hidden = false) {
this.redrawRequested = false;
var ctx = this.frame.canvas.getContext('2d');
@ -2042,24 +2020,23 @@ Network.prototype._redraw = function(hidden, requested) {
};
if (hidden === false) {
this._doInAllSectors("_drawAllSectorNodes", ctx);
if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
this._doInAllSectors("_drawEdges", ctx);
this._drawEdges(ctx);
}
}
if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) {
this._doInAllSectors("_drawNodes",ctx,false);
this._drawNodes(ctx, this.body.nodes, hidden);
}
if (hidden === false) {
if (this.controlNodesActive == true) {
this._doInAllSectors("_drawControlNodes", ctx);
this._drawControlNodes(ctx);
}
}
//this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
this._drawNodes(ctx,this.body.supportNodes,true);
// this.physics.nodesSolver._debug(ctx,"#F00F0F");
// restore original scaling and translation
ctx.restore();
@ -2195,13 +2172,8 @@ Network.prototype.DOMtoCanvas = function (pos) {
* @param {Boolean} [alwaysShow]
* @private
*/
Network.prototype._drawNodes = function(ctx,alwaysShow) {
if (alwaysShow === undefined) {
alwaysShow = false;
}
Network.prototype._drawNodes = function(ctx,nodes,alwaysShow = false) {
// first draw the unselected nodes
var nodes = this.body.nodes;
var selected = [];
for (var id in nodes) {
@ -2211,7 +2183,10 @@ Network.prototype._drawNodes = function(ctx,alwaysShow) {
selected.push(id);
}
else {
if (nodes[id].inArea() || alwaysShow) {
if (alwaysShow === true) {
nodes[id].draw(ctx);
}
else if (nodes[id].inArea() === true) {
nodes[id].draw(ctx);
}
}
@ -2346,13 +2321,11 @@ Network.prototype._restoreFrozenNodes = function() {
* @return {boolean} true if moving, false if non of the nodes is moving
* @private
*/
Network.prototype._isMoving = function(vmin) {
var nodes = this.body.nodes;
for (var id in nodes) {
if (nodes[id] !== undefined) {
if (nodes[id].isMoving(vmin) == true) {
return true;
}
Network.prototype._isMoving = function(nodes, nodeIndices, vmin) {
for (let i = 0; i < nodeIndices.length; i++) {
let node = nodes[nodeIndices[i]];
if (node.isMoving(vmin) == true) {
return true;
}
}
return false;
@ -2365,44 +2338,40 @@ Network.prototype._isMoving = function(vmin) {
*
* @private
*/
Network.prototype._discreteStepNodes = function() {
Network.prototype._discreteStepNodes = function(nodes, nodeIndices) {
var interval = this.physicsDiscreteStepsize;
var nodes = this.body.nodes;
var nodeId;
var nodesPresent = false;
if (this.constants.maxVelocity > 0) {
for (nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
nodes[nodeId].discreteStepLimited(interval, this.constants.maxVelocity);
nodesPresent = true;
}
for (let i = 0; i < nodeIndices.length; i++) {
let node = nodes[nodeIndices[i]];
node.discreteStepLimited(interval, this.constants.maxVelocity);
nodesPresent = true;
}
}
else {
for (nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
nodes[nodeId].discreteStep(interval);
nodesPresent = true;
}
for (let i = 0; i < nodeIndices.length; i++) {
let node = nodes[nodeIndices[i]];
node.discreteStep(interval);
nodesPresent = true;
}
}
if (nodesPresent == true) {
var vminCorrected = this.constants.minVelocity / Math.max(this.scale,0.05);
if (vminCorrected > 0.5*this.constants.maxVelocity) {
return true;
}
else {
return this._isMoving(vminCorrected);
return this._isMoving(nodes, nodeIndices, vminCorrected);
}
}
return false;
};
Network.prototype._revertPhysicsState = function() {
var nodes = this.body.nodes;
Network.prototype._revertPhysicsTick = function(nodes) {
for (var nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
nodes[nodeId].revertPosition();
@ -2410,13 +2379,6 @@ Network.prototype._revertPhysicsState = function() {
}
}
Network.prototype._revertPhysicsTick = function() {
this._doInAllActiveSectors("_revertPhysicsState");
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
this._doInSupportSector("_revertPhysicsState");
}
}
/**
* A single simulation step (or "tick") in the physics simulation
*
@ -2425,24 +2387,16 @@ Network.prototype._revertPhysicsTick = function() {
Network.prototype._physicsTick = function() {
if (!this.freezeSimulationEnabled) {
if (this.moving == true) {
var mainMovingStatus = false;
var supportMovingStatus = false;
this.physics.step();
this._doInAllActiveSectors("_initializeForceCalculation");
var mainMoving = this._doInAllActiveSectors("_discreteStepNodes");
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
supportMovingStatus = this._doInSupportSector("_discreteStepNodes");
}
// gather movement data from all sectors, if one moves, we are NOT stabilzied
for (var i = 0; i < mainMoving.length; i++) {
mainMovingStatus = mainMoving[i] || mainMovingStatus;
}
var mainMovingStatus = this._discreteStepNodes(this.body.nodes, this.body.nodeIndices);
var supportMovingStatus = this._discreteStepNodes(this.body.supportNodes, this.body.supportNodeIndices);
// determine if the network has stabilzied
this.moving = mainMovingStatus || supportMovingStatus;
if (this.moving == false) {
this._revertPhysicsTick();
this._revertPhysicsTick(this.body.nodes);
this._revertPhysicsTick(this.body.supportNodes);
}
else {
// this is here to ensure that there is no start event when the network is already stable.
@ -2593,24 +2547,21 @@ Network.prototype.freezeSimulation = function(freeze) {
* @param {boolean} [disableStart]
* @private
*/
Network.prototype._configureSmoothCurves = function(disableStart) {
if (disableStart === undefined) {
disableStart = true;
}
Network.prototype._configureSmoothCurves = function(disableStart = true) {
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
this._createBezierNodes();
// cleanup unused support nodes
for (var nodeId in this.body.sectors['support']['nodes']) {
if (this.body.sectors['support']['nodes'].hasOwnProperty(nodeId)) {
if (this.body.edges[this.body.sectors['support']['nodes'][nodeId].parentEdgeId] === undefined) {
delete this.body.sectors['support']['nodes'][nodeId];
}
for (let i = 0; i < this.body.supportNodeIndices.length; i++) {
let nodeId = this.body.supportNodeIndices[i];
// delete support nodes for edges that have been deleted
if (this.body.edges[this.body.supportNodes[nodeId].parentEdgeId] === undefined) {
delete this.body.supportNodes[nodeId];
}
}
}
else {
// delete the support nodes
this.body.sectors['support']['nodes'] = {};
this.body.supportNodes = {};
for (var edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
this.body.edges[edgeId].via = null;
@ -2619,7 +2570,8 @@ Network.prototype._configureSmoothCurves = function(disableStart) {
}
this._updateCalculationNodes();
this._updateNodeIndexList();
this.physics._updateCalculationNodes();
if (!disableStart) {
this.moving = true;
this.start();
@ -2633,30 +2585,28 @@ Network.prototype._configureSmoothCurves = function(disableStart) {
*
* @private
*/
Network.prototype._createBezierNodes = function(specificEdges) {
console.log('specifics', specificEdges)
if (specificEdges === undefined) {
specificEdges = this.body.edges;
}
Network.prototype._createBezierNodes = function(specificEdges = this.body.edges) {
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
for (var edgeId in specificEdges) {
if (specificEdges.hasOwnProperty(edgeId)) {
var edge = specificEdges[edgeId];
if (edge.via == null) {
var nodeId = "edgeId:".concat(edge.id);
this.body.sectors['support']['nodes'][nodeId] = new Node(
var node = new Node(
{id:nodeId,
mass:1,
shape:'circle',
image:"",
internalMultiplier:1
},{},{},this.constants);
edge.via = this.body.sectors['support']['nodes'][nodeId];
this.body.supportNodes[nodeId] = node;
edge.via = node;
edge.via.parentEdgeId = edge.id;
edge.positionBezierNode();
}
}
}
this._updateNodeIndexList();
}
};

+ 0
- 1
lib/network/Node.js View File

@ -396,7 +396,6 @@ Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
this.fy = 0;
this.vy = 0;
}
};
/**

+ 0
- 26
lib/network/mixins/MixinLoader.js View File

@ -64,32 +64,6 @@ exports._loadClusterSystem = function () {
};
/**
* Mixin the sector system and initialize the parameters required
*
* @private
*/
exports._loadSectorSystem = function () {
this.body.sectors = {};
this.activeSector = ["default"];
this.body.sectors["active"] = {};
this.body.sectors["active"]["default"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.body.sectors["frozen"] = {};
this.body.sectors["support"] = {"nodes": {},
"edges": {},
"nodeIndices": [],
"formationScale": 1.0,
"drawingNode": undefined };
this.body.nodeIndices = this.body.sectors["active"]["default"]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
this._loadMixin(SectorsMixin);
};
/**
* Mixin the selection system and initialize the parameters required

+ 2
- 3
lib/network/mixins/SelectionMixin.js View File

@ -1,7 +1,6 @@
var Node = require('../Node');
/**
* This function can be called from the _doInAllSectors function
*
* @param object
* @param overlappingNodes
@ -26,7 +25,7 @@ exports._getNodesOverlappingWith = function(object, overlappingNodes) {
*/
exports._getAllNodesOverlappingWith = function (object) {
var overlappingNodes = [];
this._doInAllActiveSectors("_getNodesOverlappingWith",object,overlappingNodes);
this._getNodesOverlappingWith(object,overlappingNodes);
return overlappingNodes;
};
@ -100,7 +99,7 @@ exports._getEdgesOverlappingWith = function (object, overlappingEdges) {
*/
exports._getAllEdgesOverlappingWith = function (object) {
var overlappingEdges = [];
this._doInAllActiveSectors("_getEdgesOverlappingWith",object,overlappingEdges);
this._getEdgesOverlappingWith(object,overlappingEdges);
return overlappingEdges;
};

+ 0
- 647
lib/network/modules/ClusterEngine.js View File

@ -1,647 +0,0 @@
/**
* Created by Alex on 2/20/2015.
*/
var Node = require('../Node');
var Edge = require('../Edge');
var util = require('../../util');
function ClusterEngine(data,options) {
this.nodes = data.nodes;
this.edges = data.edges;
this.nodeIndices = data.nodeIndices;
this.emitter = data.emitter;
this.clusteredNodes = {};
}
/**
*
* @param hubsize
* @param options
*/
ClusterEngine.prototype.clusterByConnectionCount = function(hubsize, options) {
if (hubsize === undefined) {
hubsize = this._getHubSize();
}
else if (tyepof(hubsize) == "object") {
options = this._checkOptions(hubsize);
hubsize = this._getHubSize();
}
var nodesToCluster = [];
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.edges.length >= hubsize) {
nodesToCluster.push(node.id);
}
}
for (var i = 0; i < nodesToCluster.length; i++) {
var node = this.nodes[nodesToCluster[i]];
this.clusterByConnection(node,options,{},{},true);
}
this.emitter.emit('dataChanged');
}
/**
* loop over all nodes, check if they adhere to the condition and cluster if needed.
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
// check if the options object is fine, append if needed
options = this._checkOptions(options);
var childNodesObj = {};
var childEdgesObj = {}
// collect the nodes that will be in the cluster
for (var i = 0; i < this.nodeIndices.length; i++) {
var nodeId = this.nodeIndices[i];
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.nodes[nodeId];
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
options = this._checkOptions(options);
var clusters = []
// collect the nodes that will be in the cluster
for (var i = 0; i < this.nodeIndices.length; i++) {
var childNodesObj = {};
var childEdgesObj = {};
var nodeId = this.nodeIndices[i];
if (this.nodes[nodeId].edges.length == 1) {
var edge = this.nodes[nodeId].edges[0];
var childNodeId = this._getConnectedId(edge, nodeId);
if (childNodeId != nodeId) {
if (options.joinCondition === undefined) {
childNodesObj[nodeId] = this.nodes[nodeId];
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
else {
var clonedOptions = this._cloneOptions(nodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[nodeId] = this.nodes[nodeId];
}
clonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(clonedOptions) == true) {
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
}
clusters.push({nodes:childNodesObj, edges:childEdgesObj})
}
}
}
for (var i = 0; i < clusters.length; i++) {
this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
}
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
*
* @param nodeId
* @param options
* @param doNotUpdateCalculationNodes
*/
ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
// kill conditions
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
var node = this.nodes[nodeId];
options = this._checkOptions(options, node);
if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
var childNodesObj = {};
var childEdgesObj = {}
var parentNodeId = node.id;
var parentClonedOptions = this._cloneOptions(parentNodeId);
childNodesObj[parentNodeId] = node;
// collect the nodes that will be in the cluster
for (var i = 0; i < node.edges.length; i++) {
var edge = node.edges[i];
var childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
var childClonedOptions = this._cloneOptions(childNodeId);
if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.nodes[childNodeId];
}
}
}
else {
childEdgesObj[edge.id] = edge;
}
}
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
}
/**
* This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
* @param objId
* @param type
* @returns {{}}
* @private
*/
ClusterEngine.prototype._cloneOptions = function(objId, type) {
var clonedOptions = {};
if (type === undefined || type == 'node') {
util.deepExtend(clonedOptions, this.nodes[objId].options, true);
util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
}
else {
util.deepExtend(clonedOptions, this.edges[objId].properties, true);
}
return clonedOptions;
}
/**
* This function creates the edges that will be attached to the cluster.
*
* @param childNodesObj
* @param childEdgesObj
* @param newEdges
* @param options
* @private
*/
ClusterEngine.prototype._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
var edge, childNodeId, childNode;
var childKeys = Object.keys(childNodesObj);
for (var i = 0; i < childKeys.length; i++) {
childNodeId = childKeys[i];
childNode = childNodesObj[childNodeId];
// mark all edges for removal from global and construct new edges from the cluster to others
for (var j = 0; j < childNode.edges.length; j++) {
edge = childNode.edges[j];
childEdgesObj[edge.id] = edge;
var otherNodeId = edge.toId;
var otherOnTo = true;
if (edge.toId != childNodeId) {
otherNodeId = edge.toId;
otherOnTo = true;
}
else if (edge.fromId != childNodeId) {
otherNodeId = edge.fromId;
otherOnTo = false;
}
if (childNodesObj[otherNodeId] === undefined) {
var clonedOptions = this._cloneOptions(edge.id, 'edge');
util.deepExtend(clonedOptions, options.clusterEdgeProperties);
// avoid forcing the default color on edges that inherit color
if (edge.properties.color === undefined) {
delete clonedOptions.color;
}
if (otherOnTo === true) {
clonedOptions.from = options.clusterNodeProperties.id;
clonedOptions.to = otherNodeId;
}
else {
clonedOptions.from = otherNodeId;
clonedOptions.to = options.clusterNodeProperties.id;
}
clonedOptions.id = 'clusterEdge:' + util.randomUUID();
newEdges.push(new Edge(clonedOptions,this,this.constants))
}
}
}
}
/**
* This function checks the options that can be supplied to the different cluster functions
* for certain fields and inserts defaults if needed
* @param options
* @returns {*}
* @private
*/
ClusterEngine.prototype._checkOptions = function(options) {
if (options === undefined) {options = {};}
if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
return options;
}
/**
*
* @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
* @param {Object} childEdgesObj | object with edge objects, id as keys
* @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
* @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
* @private
*/
ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
// kill condition: no children so cant cluster
if (Object.keys(childNodesObj).length == 0) {return;}
// check if we have an unique id;
if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
var clusterId = options.clusterNodeProperties.id;
// create the new edges that will connect to the cluster
var newEdges = [];
this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
// construct the clusterNodeProperties
var clusterNodeProperties = options.clusterNodeProperties;
if (options.processProperties !== undefined) {
// get the childNode options
var childNodesOptions = [];
for (var nodeId in childNodesObj) {
var clonedOptions = this._cloneOptions(nodeId);
childNodesOptions.push(clonedOptions);
}
// get clusterproperties based on childNodes
var childEdgesOptions = [];
for (var edgeId in childEdgesObj) {
var clonedOptions = this._cloneOptions(edgeId, 'edge');
childEdgesOptions.push(clonedOptions);
}
clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
if (!clusterNodeProperties) {
throw new Error("The processClusterProperties function does not return properties!");
}
}
if (clusterNodeProperties.label === undefined) {
clusterNodeProperties.label = 'cluster';
}
// give the clusterNode a postion if it does not have one.
var pos = undefined
if (clusterNodeProperties.x === undefined) {
pos = this._getClusterPosition(childNodesObj);
clusterNodeProperties.x = pos.x;
clusterNodeProperties.allowedToMoveX = true;
}
if (clusterNodeProperties.x === undefined) {
if (pos === undefined) {
pos = this._getClusterPosition(childNodesObj);
}
clusterNodeProperties.y = pos.y;
clusterNodeProperties.allowedToMoveY = true;
}
// force the ID to remain the same
clusterNodeProperties.id = clusterId;
// create the clusterNode
var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
clusterNode.isCluster = true;
clusterNode.containedNodes = childNodesObj;
clusterNode.containedEdges = childEdgesObj;
// delete contained edges from global
for (var edgeId in childEdgesObj) {
if (childEdgesObj.hasOwnProperty(edgeId)) {
if (this.edges[edgeId] !== undefined) {
if (this.edges[edgeId].via !== null) {
var viaId = this.edges[edgeId].via.id;
if (viaId) {
this.edges[edgeId].via = null
delete this.sectors['support']['nodes'][viaId];
}
}
this.edges[edgeId].disconnect();
delete this.edges[edgeId];
}
}
}
// remove contained nodes from global
for (var nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
delete this.nodes[nodeId];
}
}
// finally put the cluster node into global
this.nodes[clusterNodeProperties.id] = clusterNode;
// push new edges to global
for (var i = 0; i < newEdges.length; i++) {
this.edges[newEdges[i].id] = newEdges[i];
this.edges[newEdges[i].id].connect();
}
// create bezier nodes for smooth curves if needed
this._createBezierNodes(newEdges);
// set ID to undefined so no duplicates arise
clusterNodeProperties.id = undefined;
// wrap up
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
* Check if a node is a cluster.
* @param nodeId
* @returns {*}
*/
ClusterEngine.prototype.isCluster = function(nodeId) {
if (this.nodes[nodeId] !== undefined) {
return this.nodes[nodeId].isCluster;
}
else {
console.log("Node does not exist.")
return false;
}
}
/**
* get the position of the cluster node based on what's inside
* @param {object} childNodesObj | object with node objects, id as keys
* @returns {{x: number, y: number}}
* @private
*/
ClusterEngine.prototype._getClusterPosition = function(childNodesObj) {
var childKeys = Object.keys(childNodesObj);
var minX = childNodesObj[childKeys[0]].x;
var maxX = childNodesObj[childKeys[0]].x;
var minY = childNodesObj[childKeys[0]].y;
var maxY = childNodesObj[childKeys[0]].y;
var node;
for (var i = 0; i < childKeys.lenght; i++) {
node = childNodesObj[childKeys[0]];
minX = node.x < minX ? node.x : minX;
maxX = node.x > maxX ? node.x : maxX;
minY = node.y < minY ? node.y : minY;
maxY = node.y > maxY ? node.y : maxY;
}
return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
}
/**
* Open a cluster by calling this function.
* @param {String} clusterNodeId | the ID of the cluster node
* @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
*/
ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
// kill conditions
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
var node = this.nodes[clusterNodeId];
var containedNodes = node.containedNodes;
var containedEdges = node.containedEdges;
// release nodes
for (var nodeId in containedNodes) {
if (containedNodes.hasOwnProperty(nodeId)) {
this.nodes[nodeId] = containedNodes[nodeId];
// inherit position
this.nodes[nodeId].x = node.x;
this.nodes[nodeId].y = node.y;
// inherit speed
this.nodes[nodeId].vx = node.vx;
this.nodes[nodeId].vy = node.vy;
delete this.clusteredNodes[nodeId];
}
}
// release edges
for (var edgeId in containedEdges) {
if (containedEdges.hasOwnProperty(edgeId)) {
this.edges[edgeId] = containedEdges[edgeId];
this.edges[edgeId].connect();
var edge = this.edges[edgeId];
if (edge.connected === false) {
if (this.clusteredNodes[edge.fromId] !== undefined) {
this._connectEdge(edge, edge.fromId, true);
}
if (this.clusteredNodes[edge.toId] !== undefined) {
this._connectEdge(edge, edge.toId, false);
}
}
}
}
this._createBezierNodes(containedEdges);
var edgeIds = [];
for (var i = 0; i < node.edges.length; i++) {
edgeIds.push(node.edges[i].id);
}
// remove edges in clusterNode
for (var i = 0; i < edgeIds.length; i++) {
var edge = this.edges[edgeIds[i]];
// if the edge should have been connected to a contained node
if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
// the node in the from array was contained in the cluster
if (this.nodes[edge.fromArray[0].id] !== undefined) {
this._connectEdge(edge, edge.fromArray[0].id, true);
}
}
else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
// the node in the to array was contained in the cluster
if (this.nodes[edge.toArray[0].id] !== undefined) {
this._connectEdge(edge, edge.toArray[0].id, false);
}
}
else {
var edgeId = edgeIds[i];
var viaId = this.edges[edgeId].via.id;
if (viaId) {
this.edges[edgeId].via = null
delete this.sectors['support']['nodes'][viaId];
}
// this removes the edge from node.edges, which is why edgeIds is formed
this.edges[edgeId].disconnect();
delete this.edges[edgeId];
}
}
// remove clusterNode
delete this.nodes[clusterNodeId];
if (doNotUpdateCalculationNodes !== true) {
this.emitter.emit('dataChanged');
}
}
/**
* Recalculate navigation nodes, color edges dirty, update nodes list etc.
* @private
*/
ClusterEngine.prototype._wrapUp = function() {
this._updateNodeIndexList();
this._updateCalculationNodes();
this._markAllEdgesAsDirty();
if (this.initializing !== true) {
this.moving = true;
this.start();
}
}
/**
* Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
* is currently residing in cluster B
* @param edge
* @param nodeId
* @param from
* @private
*/
ClusterEngine.prototype._connectEdge = function(edge, nodeId, from) {
var clusterStack = this._getClusterStack(nodeId);
if (from == true) {
edge.from = clusterStack[clusterStack.length - 1];
edge.fromId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop()
edge.fromArray = clusterStack;
}
else {
edge.to = clusterStack[clusterStack.length - 1];
edge.toId = clusterStack[clusterStack.length - 1].id;
clusterStack.pop();
edge.toArray = clusterStack;
}
edge.connect();
}
/**
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
* @param nodeId
* @returns {Array}
* @private
*/
ClusterEngine.prototype._getClusterStack = function(nodeId) {
var stack = [];
var max = 100;
var counter = 0;
while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
stack.push(this.clusteredNodes[nodeId].node);
nodeId = this.clusteredNodes[nodeId].clusterId;
counter++;
}
stack.push(this.nodes[nodeId]);
return stack;
}
/**
* Get the Id the node is connected to
* @param edge
* @param nodeId
* @returns {*}
* @private
*/
ClusterEngine.prototype._getConnectedId = function(edge, nodeId) {
if (edge.toId != nodeId) {
return edge.toId;
}
else if (edge.fromId != nodeId) {
return edge.fromId;
}
else {
return edge.fromId;
}
}
/**
* We determine how many connections denote an important hub.
* We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
*
* @private
*/
ClusterEngine.prototype._getHubSize = function() {
var average = 0;
var averageSquared = 0;
var hubCounter = 0;
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
if (node.edges.length > largestHub) {
largestHub = node.edges.length;
}
average += node.edges.length;
averageSquared += Math.pow(node.edges.length,2);
hubCounter += 1;
}
average = average / hubCounter;
averageSquared = averageSquared / hubCounter;
var variance = averageSquared - Math.pow(average,2);
var standardDeviation = Math.sqrt(variance);
var hubThreshold = Math.floor(average + 2*standardDeviation);
// always have at least one to cluster
if (hubThreshold > largestHub) {
hubThreshold = largestHub;
}
return hubThreshold;
};
module.exports = clusterEngine

+ 2
- 2
lib/network/modules/Clustering.js View File

@ -333,7 +333,7 @@ class ClusterEngine {
var viaId = this.body.edges[edgeId].via.id;
if (viaId) {
this.body.edges[edgeId].via = null
delete this.body.sectors['support']['nodes'][viaId];
delete this.body.supportNodes[viaId];
}
}
this.body.edges[edgeId].disconnect();
@ -494,7 +494,7 @@ class ClusterEngine {
var viaId = this.body.edges[edgeId].via.id;
if (viaId) {
this.body.edges[edgeId].via = null
delete this.body.sectors['support']['nodes'][viaId];
delete this.body.supportNodes[viaId];
}
// this removes the edge from node.edges, which is why edgeIds is formed
this.body.edges[edgeId].disconnect();

+ 81
- 10
lib/network/modules/PhysicsEngine.js View File

@ -2,34 +2,105 @@
* Created by Alex on 2/23/2015.
*/
import {BarnesHut} from "./components/physics/BarnesHutSolver";
import {BarnesHutSolver} from "./components/physics/BarnesHutSolver";
// TODO Create
//import {Repulsion} from "./components/physics/Repulsion";
//import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsion";
import {SpringSolver} from "./components/physics/SpringSolver";
// TODO Create
//import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
import {CentralGravitySolver} from "./components/physics/CentralGravitySolver";
class PhysicsEngine {
constructor(body, options) {
this.body = body;
this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[]};
this.setOptions(options);
}
this.nodesSolver = new BarnesHut(body, options);
this.edgesSolver = new SpringSolver(body, options);
this.gravitySolver = new CentralGravitySolver(body, options);
setOptions(options) {
if (options !== undefined) {
this.options = options;
this.init();
}
}
init() {
var options;
if (this.options.model == "repulsion") {
options = this.options.repulsion;
// TODO uncomment when created
//this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
//this.edgesSolver = new SpringSolver(this.body, options);
}
else if (this.options.model == "hierarchicalRepulsion") {
options = this.options.hierarchicalRepulsion;
// TODO uncomment when created
//this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
//this.edgesSolver = new HierarchicalSpringSolver(this.body, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, options);
}
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
/**
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
* handled in the calculateForces function. We then use a quadratic curve with the center node as control.
* This function joins the datanodes and invisible (called support) nodes into one object.
* We do this so we do not contaminate this.body.nodes with the support nodes.
*
* @private
*/
_updateCalculationNodes() {
this.physicsBody.calculationNodes = {};
this.physicsBody.calculationNodeIndices = [];
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let nodeId = this.body.nodeIndices[i];
this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId];
}
// if support nodes are used, we have them here
var supportNodes = this.body.supportNodes;
for (let i = 0; i < this.body.supportNodeIndices.length; i++) {
let supportNodeId = this.body.supportNodeIndices[i];
if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) {
this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId];
}
else {
console.error("Support node detected that does not have an edge!")
}
}
console.log('here', this.body)
this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes);
}
calculateField() {
this.nodesSolver.solve();
};
}
calculateSprings() {
this.edgesSolver.solve();
};
}
calculateCentralGravity() {
this.gravitySolver.solve();
};
}
calculate() {
step() {
this.calculateCentralGravity();
this.calculateField();
this.calculateSprings();
};
}
}
}
export {PhysicsEngine};

+ 10
- 4
lib/network/modules/components/physics/BarnesHutSolver.js View File

@ -3,9 +3,11 @@
*/
class BarnesHutSolver {
constructor(body, options) {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.options = options;
this.barnesHutTree;
}
@ -18,12 +20,16 @@ class BarnesHutSolver {
solve() {
if (this.options.gravitationalConstant != 0) {
var node;
var nodes = this.body.calculationNodes;
var nodeIndices = this.body.calculationNodeIndices;
var nodes = this.physicsBody.calculationNodes;
var nodeIndices = this.physicsBody.calculationNodeIndices;
var nodeCount = nodeIndices.length;
// create the tree
var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
// for debugging
this.barnesHutTree = barnesHutTree;
// place the nodes one by one recursively
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
@ -371,7 +377,7 @@ class BarnesHutSolver {
* @param color
* @private
*/
_drawTree(ctx, color) {
_debug(ctx, color) {
if (this.barnesHutTree !== undefined) {
ctx.lineWidth = 1;

+ 9
- 3
lib/network/modules/components/physics/CentralGravitySolver.js View File

@ -3,20 +3,26 @@
*/
class CentralGravitySolver {
constructor(body, options) {
constructor(body, physicsBody, options) {
this.body = body;
this.physicsBody = physicsBody;
this.setOptions(options);
}
setOptions(options) {
this.options = options;
}
solve() {
var dx, dy, distance, node, i;
var nodes = this.body.calculationNodes;
var nodes = this.physicsBody.calculationNodes;
var calculationNodeIndices = this.physicsBody.calculationNodeIndices;
var gravity = this.options.centralGravity;
var gravityForce = 0;
var calculationNodeIndices = this.body.calculationNodeIndices;
for (i = 0; i < calculationNodeIndices.length; i++) {
node = nodes[calculationNodeIndices[i]];
node.damping = this.options.damping;
dx = -node.x;
dy = -node.y;
distance = Math.sqrt(dx * dx + dy * dy);

+ 9
- 37
lib/network/modules/components/physics/SpringSolver.js View File

@ -9,49 +9,19 @@ class SpringSolver {
}
solve() {
if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) {
this._calculateSpringForcesWithSupport();
}
else {
this._calculateSpringForces();
}
this._calculateSpringForces();
}
/**
* this function calculates the effects of the springs in the case of unsmooth curves.
*
* @private
*/
_calculateSpringForces() {
var edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.physics.springLength;
this._calculateSpringForce(edge.from, edge.to, edgeLength);
}
}
}
}
}
/**
* This function calculates the springforces on the nodes, accounting for the support nodes.
*
* @private
*/
_calculateSpringForcesWithSupport() {
_calculateSpringForces() {
var edgeLength, edge, edgeId;
var edges = this.edges;
var edges = this.body.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
@ -59,17 +29,19 @@ class SpringSolver {
edge = edges[edgeId];
if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) {
edgeLength = edge.physics.springLength;
if (edge.via != null) {
var node1 = edge.to;
var node2 = edge.via;
var node3 = edge.from;
edgeLength = edge.physics.springLength;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
else {
this._calculateSpringForce(edge.from, edge.to, edgeLength);
}
}
}
}
@ -78,7 +50,7 @@ class SpringSolver {
/**
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
* This is the code actually performing the calculation for the function above.
*
* @param node1
* @param node2

Loading…
Cancel
Save