From b91757fabfc91059a2a3e8855f17298bc41f2867 Mon Sep 17 00:00:00 2001 From: Alex de Mulder Date: Tue, 24 Feb 2015 18:32:44 +0100 Subject: [PATCH] first step to modularize the physics engine --- dist/vis.js | 3 +- lib/network/Network.js | 2 +- lib/network/modules/Clustering.js | 2 +- lib/network/modules/PhysicsEngine.js | 48 +- .../modules/components/BarnesHutSolver.js | 409 ----------------- .../components/CentralGravitySolver.js | 32 -- .../modules/components/SpringSolver.js | 101 ---- .../components/physics/BarnesHutSolver.js | 434 ++++++++++++++++++ .../physics/CentralGravitySolver.js | 32 ++ .../components/physics/SpringSolver.js | 109 +++++ 10 files changed, 603 insertions(+), 569 deletions(-) delete mode 100644 lib/network/modules/components/BarnesHutSolver.js delete mode 100644 lib/network/modules/components/CentralGravitySolver.js delete mode 100644 lib/network/modules/components/SpringSolver.js create mode 100644 lib/network/modules/components/physics/BarnesHutSolver.js create mode 100644 lib/network/modules/components/physics/CentralGravitySolver.js create mode 100644 lib/network/modules/components/physics/SpringSolver.js diff --git a/dist/vis.js b/dist/vis.js index 4d35a5a1..88bc219d 100644 --- a/dist/vis.js +++ b/dist/vis.js @@ -23120,8 +23120,8 @@ return /******/ (function(modules) { // webpackBootstrap } var me = this; + // this event will trigger a rebuilding of the cache of colors, nodes etc. this.on("_dataChanged", function () { - console.log("here"); me._updateNodeIndexList(); me._updateCalculationNodes(); me._markAllEdgesAsDirty(); @@ -34441,7 +34441,6 @@ return /******/ (function(modules) { // webpackBootstrap var ClusterEngine = (function () { function ClusterEngine(body) { - var options = arguments[1] === undefined ? {} : arguments[1]; _classCallCheck(this, ClusterEngine); this.body = body; diff --git a/lib/network/Network.js b/lib/network/Network.js index d81b2ea5..0d635409 100644 --- a/lib/network/Network.js +++ b/lib/network/Network.js @@ -374,8 +374,8 @@ function Network (container, data, options) { } var me = this; + // this event will trigger a rebuilding of the cache of colors, nodes etc. this.on("_dataChanged", function () { - console.log("here") me._updateNodeIndexList(); me._updateCalculationNodes(); me._markAllEdgesAsDirty(); diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 289d00b7..e88c88f0 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -5,7 +5,7 @@ var util = require("../../util"); class ClusterEngine { - constructor(body, options={}) { + constructor(body) { this.body = body; this.clusteredNodes = {}; } diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js index 5de2daae..1a868b3f 100644 --- a/lib/network/modules/PhysicsEngine.js +++ b/lib/network/modules/PhysicsEngine.js @@ -2,32 +2,34 @@ * Created by Alex on 2/23/2015. */ -var BarnesHut = require("./compontents/BarnesHutSolver") -var SpringSolver = require("./compontents/SpringSolver") -var CentralGravitySolver = require("./compontents/CentralGravitySolver") +import {BarnesHut} from "./components/physics/BarnesHutSolver"; +import {SpringSolver} from "./components/physics/SpringSolver"; +import {CentralGravitySolver} from "./components/physics/CentralGravitySolver"; -function PhysicsEngine(body, options) { - this.body = body; +class PhysicsEngine { + constructor(body, options) { + this.body = body; - this.nodesSolver = new BarnesHut(body, options); - this.edgesSolver = new SpringSolver(body, options); - this.gravitySolver = new CentralGravitySolver(body, options); -} + this.nodesSolver = new BarnesHut(body, options); + this.edgesSolver = new SpringSolver(body, options); + this.gravitySolver = new CentralGravitySolver(body, options); + } -PhysicsEngine.prototype.calculateField = function () { - this.nodesSolver.solve(); -}; + calculateField() { + this.nodesSolver.solve(); + }; -PhysicsEngine.prototype.calculateSprings = function () { - this.edgesSolver.solve(); -}; + calculateSprings() { + this.edgesSolver.solve(); + }; -PhysicsEngine.prototype.calculateCentralGravity = function () { - this.gravitySolver.solve(); -}; + calculateCentralGravity() { + this.gravitySolver.solve(); + }; -PhysicsEngine.prototype.calculate = function () { - this.calculateCentralGravity(); - this.calculateField(); - this.calculateSprings(); -}; \ No newline at end of file + calculate() { + this.calculateCentralGravity(); + this.calculateField(); + this.calculateSprings(); + }; +} \ No newline at end of file diff --git a/lib/network/modules/components/BarnesHutSolver.js b/lib/network/modules/components/BarnesHutSolver.js deleted file mode 100644 index 0ba9237f..00000000 --- a/lib/network/modules/components/BarnesHutSolver.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Created by Alex on 2/23/2015. - */ - -function BarnesHutSolver(body, options) { - this.body = body; - this.options = options; -} - -/** - * This function calculates the forces the nodes apply on eachother based on a gravitational model. - * The Barnes Hut method is used to speed up this N-body simulation. - * - * @private - */ -BarnesHutSolver.prototype.solve = function() { - if (this.options.gravitationalConstant != 0) { - var node; - var nodes = this.body.calculationNodes; - var nodeIndices = this.body.calculationNodeIndices; - var nodeCount = nodeIndices.length; - - var barnesHutTree = this._formBarnesHutTree(nodes,nodeIndices); - - // place the nodes one by one recursively - for (var i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - // starting with root is irrelevant, it never passes the BarnesHutSolver condition - this._getForceContribution(barnesHutTree.root.children.NW,node); - this._getForceContribution(barnesHutTree.root.children.NE,node); - this._getForceContribution(barnesHutTree.root.children.SW,node); - this._getForceContribution(barnesHutTree.root.children.SE,node); - } - } - } -}; - - -/** - * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. - * If a region contains a single node, we check if it is not itself, then we apply the force. - * - * @param parentBranch - * @param node - * @private - */ -BarnesHutSolver.prototype._getForceContribution = function(parentBranch,node) { - // we get no force contribution from an empty region - if (parentBranch.childrenCount > 0) { - var dx,dy,distance; - - // get the distance from the center of mass to the node. - dx = parentBranch.centerOfMass.x - node.x; - dy = parentBranch.centerOfMass.y - node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - // BarnesHutSolver condition - // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed - // calcSize = 1/s --> d * 1/s > 1/theta = passed - if (distance * parentBranch.calcSize > this.options.thetaInverted) { - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.1*Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - else { - // Did not pass the condition, go into children if available - if (parentBranch.childrenCount == 4) { - this._getForceContribution(parentBranch.children.NW,node); - this._getForceContribution(parentBranch.children.NE,node); - this._getForceContribution(parentBranch.children.SW,node); - this._getForceContribution(parentBranch.children.SE,node); - } - else { // parentBranch must have only one node, if it was empty we wouldnt be here - if (parentBranch.children.data.id != node.id) { // if it is not self - // duplicate code to reduce function calls to speed up program - if (distance == 0) { - distance = 0.5*Math.random(); - dx = distance; - } - var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); - var fx = dx * gravityForce; - var fy = dy * gravityForce; - node.fx += fx; - node.fy += fy; - } - } - } - } -}; - -/** - * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. - * - * @param nodes - * @param nodeIndices - * @private - */ -BarnesHutSolver.prototype._formBarnesHutTree = function(nodes,nodeIndices) { - var node; - var nodeCount = nodeIndices.length; - - var minX = Number.MAX_VALUE, - minY = Number.MAX_VALUE, - maxX =-Number.MAX_VALUE, - maxY =-Number.MAX_VALUE; - - // get the range of the nodes - for (var i = 0; i < nodeCount; i++) { - var x = nodes[nodeIndices[i]].x; - var y = nodes[nodeIndices[i]].y; - if (nodes[nodeIndices[i]].options.mass > 0) { - if (x < minX) { minX = x; } - if (x > maxX) { maxX = x; } - if (y < minY) { minY = y; } - if (y > maxY) { maxY = y; } - } - } - // make the range a square - var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y - if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize - else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize - - - var minimumTreeSize = 1e-5; - var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); - var halfRootSize = 0.5 * rootSize; - var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); - - // construct the barnesHutTree - var barnesHutTree = { - root:{ - centerOfMass: {x:0, y:0}, - mass:0, - range: { - minX: centerX-halfRootSize,maxX:centerX+halfRootSize, - minY: centerY-halfRootSize,maxY:centerY+halfRootSize - }, - size: rootSize, - calcSize: 1 / rootSize, - children: { data:null}, - maxWidth: 0, - level: 0, - childrenCount: 4 - } - }; - this._splitBranch(barnesHutTree.root); - - // place the nodes one by one recursively - for (i = 0; i < nodeCount; i++) { - node = nodes[nodeIndices[i]]; - if (node.options.mass > 0) { - this._placeInTree(barnesHutTree.root,node); - } - } - - // make global - return barnesHutTree -}; - - -/** - * this updates the mass of a branch. this is increased by adding a node. - * - * @param parentBranch - * @param node - * @private - */ -BarnesHutSolver.prototype._updateBranchMass = function(parentBranch, node) { - var totalMass = parentBranch.mass + node.options.mass; - var totalMassInv = 1/totalMass; - - parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; - parentBranch.centerOfMass.x *= totalMassInv; - - parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; - parentBranch.centerOfMass.y *= totalMassInv; - - parentBranch.mass = totalMass; - var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); - parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; - -}; - - -/** - * determine in which branch the node will be placed. - * - * @param parentBranch - * @param node - * @param skipMassUpdate - * @private - */ -BarnesHutSolver.prototype._placeInTree = function(parentBranch,node,skipMassUpdate) { - if (skipMassUpdate != true || skipMassUpdate === undefined) { - // update the mass of the branch. - this._updateBranchMass(parentBranch,node); - } - - if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW - if (parentBranch.children.NW.range.maxY > node.y) { // in NW - this._placeInRegion(parentBranch,node,"NW"); - } - else { // in SW - this._placeInRegion(parentBranch,node,"SW"); - } - } - else { // in NE or SE - if (parentBranch.children.NW.range.maxY > node.y) { // in NE - this._placeInRegion(parentBranch,node,"NE"); - } - else { // in SE - this._placeInRegion(parentBranch,node,"SE"); - } - } -}; - - -/** - * actually place the node in a region (or branch) - * - * @param parentBranch - * @param node - * @param region - * @private - */ -BarnesHutSolver.prototype._placeInRegion = function(parentBranch,node,region) { - switch (parentBranch.children[region].childrenCount) { - case 0: // place node here - parentBranch.children[region].children.data = node; - parentBranch.children[region].childrenCount = 1; - this._updateBranchMass(parentBranch.children[region],node); - break; - case 1: // convert into children - // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) - // we move one node a pixel and we do not put it in the tree. - if (parentBranch.children[region].children.data.x == node.x && - parentBranch.children[region].children.data.y == node.y) { - node.x += Math.random(); - node.y += Math.random(); - } - else { - this._splitBranch(parentBranch.children[region]); - this._placeInTree(parentBranch.children[region],node); - } - break; - case 4: // place in branch - this._placeInTree(parentBranch.children[region],node); - break; - } -}; - - -/** - * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch - * after the split is complete. - * - * @param parentBranch - * @private - */ -BarnesHutSolver.prototype._splitBranch = function(parentBranch) { - // if the branch is shaded with a node, replace the node in the new subset. - var containedNode = null; - if (parentBranch.childrenCount == 1) { - containedNode = parentBranch.children.data; - parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; - } - parentBranch.childrenCount = 4; - parentBranch.children.data = null; - this._insertRegion(parentBranch,"NW"); - this._insertRegion(parentBranch,"NE"); - this._insertRegion(parentBranch,"SW"); - this._insertRegion(parentBranch,"SE"); - - if (containedNode != null) { - this._placeInTree(parentBranch,containedNode); - } -}; - - -/** - * This function subdivides the region into four new segments. - * Specifically, this inserts a single new segment. - * It fills the children section of the parentBranch - * - * @param parentBranch - * @param region - * @param parentRange - * @private - */ -BarnesHutSolver.prototype._insertRegion = function(parentBranch, region) { - var minX,maxX,minY,maxY; - var childSize = 0.5 * parentBranch.size; - switch (region) { - case "NW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "NE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY; - maxY = parentBranch.range.minY + childSize; - break; - case "SW": - minX = parentBranch.range.minX; - maxX = parentBranch.range.minX + childSize; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - case "SE": - minX = parentBranch.range.minX + childSize; - maxX = parentBranch.range.maxX; - minY = parentBranch.range.minY + childSize; - maxY = parentBranch.range.maxY; - break; - } - - - parentBranch.children[region] = { - centerOfMass:{x:0,y:0}, - mass:0, - range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, - size: 0.5 * parentBranch.size, - calcSize: 2 * parentBranch.calcSize, - children: {data:null}, - maxWidth: 0, - level: parentBranch.level+1, - childrenCount: 0 - }; -}; - - -/** - * This function is for debugging purposed, it draws the tree. - * - * @param ctx - * @param color - * @private - */ -BarnesHutSolver.prototype._drawTree = function(ctx,color) { - if (this.barnesHutTree !== undefined) { - - ctx.lineWidth = 1; - - this._drawBranch(this.barnesHutTree.root,ctx,color); - } -}; - - -/** - * This function is for debugging purposes. It draws the branches recursively. - * - * @param branch - * @param ctx - * @param color - * @private - */ -BarnesHutSolver.prototype._drawBranch = function(branch,ctx,color) { - if (color === undefined) { - color = "#FF0000"; - } - - if (branch.childrenCount == 4) { - this._drawBranch(branch.children.NW,ctx); - this._drawBranch(branch.children.NE,ctx); - this._drawBranch(branch.children.SE,ctx); - this._drawBranch(branch.children.SW,ctx); - } - ctx.strokeStyle = color; - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.minY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.minY); - ctx.lineTo(branch.range.maxX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.maxX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.maxY); - ctx.stroke(); - - ctx.beginPath(); - ctx.moveTo(branch.range.minX,branch.range.maxY); - ctx.lineTo(branch.range.minX,branch.range.minY); - ctx.stroke(); - - /* - if (branch.mass > 0) { - ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); - ctx.stroke(); - } - */ -}; - - -module.exports = BarnesHutSolver; \ No newline at end of file diff --git a/lib/network/modules/components/CentralGravitySolver.js b/lib/network/modules/components/CentralGravitySolver.js deleted file mode 100644 index 154450f8..00000000 --- a/lib/network/modules/components/CentralGravitySolver.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Created by Alex on 2/23/2015. - */ - -function CentralGravitySolver(body, options) { - this.body = body; - this.options = options; -} - - -CentralGravitySolver.prototype.solve = function () { - var dx, dy, distance, node, i; - var nodes = this.body.calculationNodes; - var gravity = this.options.centralGravity; - var gravityForce = 0; - - for (i = 0; i < this.body.calculationNodeIndices.length; i++) { - node = nodes[this.body.calculationNodeIndices[i]]; - node.damping = this.options.damping; // possibly add function to alter damping properties of clusters. - - dx = -node.x; - dy = -node.y; - distance = Math.sqrt(dx * dx + dy * dy); - - gravityForce = (distance == 0) ? 0 : (gravity / distance); - node.fx = dx * gravityForce; - node.fy = dy * gravityForce; - } -}; - - -module.exports = CentralGravitySolver; \ No newline at end of file diff --git a/lib/network/modules/components/SpringSolver.js b/lib/network/modules/components/SpringSolver.js deleted file mode 100644 index c22abc68..00000000 --- a/lib/network/modules/components/SpringSolver.js +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Created by Alex on 2/23/2015. - */ - -function SpringSolver(body, options) { - this.body = body; - this.options = options; -} - - - -/** - * this function calculates the effects of the springs in the case of unsmooth curves. - * - * @private - */ -SpringSolver.prototype._calculateSpringForces = function () { - 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 - */ -SpringSolver.prototype._calculateSpringForcesWithSupport = function () { - 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)) { - 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); - } - } - } - } - } -}; - - -/** - * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. - * - * @param node1 - * @param node2 - * @param edgeLength - * @private - */ -SpringSolver.prototype._calculateSpringForce = function (node1, node2, edgeLength) { - var dx, dy, fx, fy, springForce, distance; - - dx = (node1.x - node2.x); - dy = (node1.y - node2.y); - distance = Math.sqrt(dx * dx + dy * dy); - distance = distance == 0 ? 0.01 : distance; - - // the 1/distance is so the fx and fy can be calculated without sine or cosine. - springForce = this.options.springConstant * (edgeLength - distance) / distance; - - fx = dx * springForce; - fy = dy * springForce; - - node1.fx += fx; - node1.fy += fy; - node2.fx -= fx; - node2.fy -= fy; -}; - -module.exports = SpringSolver; \ No newline at end of file diff --git a/lib/network/modules/components/physics/BarnesHutSolver.js b/lib/network/modules/components/physics/BarnesHutSolver.js new file mode 100644 index 00000000..a263f2aa --- /dev/null +++ b/lib/network/modules/components/physics/BarnesHutSolver.js @@ -0,0 +1,434 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class BarnesHutSolver { + constructor(body, options) { + this.body = body; + this.options = options; + } + + + /** + * This function calculates the forces the nodes apply on eachother based on a gravitational model. + * The Barnes Hut method is used to speed up this N-body simulation. + * + * @private + */ + solve() { + if (this.options.gravitationalConstant != 0) { + var node; + var nodes = this.body.calculationNodes; + var nodeIndices = this.body.calculationNodeIndices; + var nodeCount = nodeIndices.length; + + var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); + + // place the nodes one by one recursively + for (var i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + // starting with root is irrelevant, it never passes the BarnesHutSolver condition + this._getForceContribution(barnesHutTree.root.children.NW, node); + this._getForceContribution(barnesHutTree.root.children.NE, node); + this._getForceContribution(barnesHutTree.root.children.SW, node); + this._getForceContribution(barnesHutTree.root.children.SE, node); + } + } + } + } + + + /** + * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. + * If a region contains a single node, we check if it is not itself, then we apply the force. + * + * @param parentBranch + * @param node + * @private + */ + _getForceContribution(parentBranch, node) { + // we get no force contribution from an empty region + if (parentBranch.childrenCount > 0) { + var dx, dy, distance; + + // get the distance from the center of mass to the node. + dx = parentBranch.centerOfMass.x - node.x; + dy = parentBranch.centerOfMass.y - node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + // BarnesHutSolver condition + // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed + // calcSize = 1/s --> d * 1/s > 1/theta = passed + if (distance * parentBranch.calcSize > this.options.thetaInverted) { + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.1 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; + } + else { + // Did not pass the condition, go into children if available + if (parentBranch.childrenCount == 4) { + this._getForceContribution(parentBranch.children.NW, node); + this._getForceContribution(parentBranch.children.NE, node); + this._getForceContribution(parentBranch.children.SW, node); + this._getForceContribution(parentBranch.children.SE, node); + } + else { // parentBranch must have only one node, if it was empty we wouldnt be here + if (parentBranch.children.data.id != node.id) { // if it is not self + // duplicate code to reduce function calls to speed up program + if (distance == 0) { + distance = 0.5 * Math.random(); + dx = distance; + } + var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); + var fx = dx * gravityForce; + var fy = dy * gravityForce; + node.fx += fx; + node.fy += fy; + } + } + } + } + } + + + /** + * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. + * + * @param nodes + * @param nodeIndices + * @private + */ + _formBarnesHutTree(nodes, nodeIndices) { + var node; + var nodeCount = nodeIndices.length; + + var minX = Number.MAX_VALUE, + minY = Number.MAX_VALUE, + maxX = -Number.MAX_VALUE, + maxY = -Number.MAX_VALUE; + + // get the range of the nodes + for (var i = 0; i < nodeCount; i++) { + var x = nodes[nodeIndices[i]].x; + var y = nodes[nodeIndices[i]].y; + if (nodes[nodeIndices[i]].options.mass > 0) { + if (x < minX) { + minX = x; + } + if (x > maxX) { + maxX = x; + } + if (y < minY) { + minY = y; + } + if (y > maxY) { + maxY = y; + } + } + } + // make the range a square + var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y + if (sizeDiff > 0) { + minY -= 0.5 * sizeDiff; + maxY += 0.5 * sizeDiff; + } // xSize > ySize + else { + minX += 0.5 * sizeDiff; + maxX -= 0.5 * sizeDiff; + } // xSize < ySize + + + var minimumTreeSize = 1e-5; + var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); + var halfRootSize = 0.5 * rootSize; + var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); + + // construct the barnesHutTree + var barnesHutTree = { + root: { + centerOfMass: {x: 0, y: 0}, + mass: 0, + range: { + minX: centerX - halfRootSize, maxX: centerX + halfRootSize, + minY: centerY - halfRootSize, maxY: centerY + halfRootSize + }, + size: rootSize, + calcSize: 1 / rootSize, + children: {data: null}, + maxWidth: 0, + level: 0, + childrenCount: 4 + } + }; + this._splitBranch(barnesHutTree.root); + + // place the nodes one by one recursively + for (i = 0; i < nodeCount; i++) { + node = nodes[nodeIndices[i]]; + if (node.options.mass > 0) { + this._placeInTree(barnesHutTree.root, node); + } + } + + // make global + return barnesHutTree + } + + + /** + * this updates the mass of a branch. this is increased by adding a node. + * + * @param parentBranch + * @param node + * @private + */ + _updateBranchMass(parentBranch, node) { + var totalMass = parentBranch.mass + node.options.mass; + var totalMassInv = 1 / totalMass; + + parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; + parentBranch.centerOfMass.x *= totalMassInv; + + parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; + parentBranch.centerOfMass.y *= totalMassInv; + + parentBranch.mass = totalMass; + var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); + parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; + + } + + + /** + * determine in which branch the node will be placed. + * + * @param parentBranch + * @param node + * @param skipMassUpdate + * @private + */ + _placeInTree(parentBranch, node, skipMassUpdate) { + if (skipMassUpdate != true || skipMassUpdate === undefined) { + // update the mass of the branch. + this._updateBranchMass(parentBranch, node); + } + + if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW + if (parentBranch.children.NW.range.maxY > node.y) { // in NW + this._placeInRegion(parentBranch, node, "NW"); + } + else { // in SW + this._placeInRegion(parentBranch, node, "SW"); + } + } + else { // in NE or SE + if (parentBranch.children.NW.range.maxY > node.y) { // in NE + this._placeInRegion(parentBranch, node, "NE"); + } + else { // in SE + this._placeInRegion(parentBranch, node, "SE"); + } + } + } + + + /** + * actually place the node in a region (or branch) + * + * @param parentBranch + * @param node + * @param region + * @private + */ + _placeInRegion(parentBranch, node, region) { + switch (parentBranch.children[region].childrenCount) { + case 0: // place node here + parentBranch.children[region].children.data = node; + parentBranch.children[region].childrenCount = 1; + this._updateBranchMass(parentBranch.children[region], node); + break; + case 1: // convert into children + // if there are two nodes exactly overlapping (on init, on opening of cluster etc.) + // we move one node a pixel and we do not put it in the tree. + if (parentBranch.children[region].children.data.x == node.x && + parentBranch.children[region].children.data.y == node.y) { + node.x += Math.random(); + node.y += Math.random(); + } + else { + this._splitBranch(parentBranch.children[region]); + this._placeInTree(parentBranch.children[region], node); + } + break; + case 4: // place in branch + this._placeInTree(parentBranch.children[region], node); + break; + } + } + + + /** + * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch + * after the split is complete. + * + * @param parentBranch + * @private + */ + _splitBranch(parentBranch) { + // if the branch is shaded with a node, replace the node in the new subset. + var containedNode = null; + if (parentBranch.childrenCount == 1) { + containedNode = parentBranch.children.data; + parentBranch.mass = 0; + parentBranch.centerOfMass.x = 0; + parentBranch.centerOfMass.y = 0; + } + parentBranch.childrenCount = 4; + parentBranch.children.data = null; + this._insertRegion(parentBranch, "NW"); + this._insertRegion(parentBranch, "NE"); + this._insertRegion(parentBranch, "SW"); + this._insertRegion(parentBranch, "SE"); + + if (containedNode != null) { + this._placeInTree(parentBranch, containedNode); + } + } + + + /** + * This function subdivides the region into four new segments. + * Specifically, this inserts a single new segment. + * It fills the children section of the parentBranch + * + * @param parentBranch + * @param region + * @param parentRange + * @private + */ + _insertRegion(parentBranch, region) { + var minX, maxX, minY, maxY; + var childSize = 0.5 * parentBranch.size; + switch (region) { + case "NW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "NE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY; + maxY = parentBranch.range.minY + childSize; + break; + case "SW": + minX = parentBranch.range.minX; + maxX = parentBranch.range.minX + childSize; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + case "SE": + minX = parentBranch.range.minX + childSize; + maxX = parentBranch.range.maxX; + minY = parentBranch.range.minY + childSize; + maxY = parentBranch.range.maxY; + break; + } + + + parentBranch.children[region] = { + centerOfMass: {x: 0, y: 0}, + mass: 0, + range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY}, + size: 0.5 * parentBranch.size, + calcSize: 2 * parentBranch.calcSize, + children: {data: null}, + maxWidth: 0, + level: parentBranch.level + 1, + childrenCount: 0 + }; + } + + + + + //--------------------------- DEBUGGING BELOW ---------------------------// + + + /** + * This function is for debugging purposed, it draws the tree. + * + * @param ctx + * @param color + * @private + */ + _drawTree(ctx, color) { + if (this.barnesHutTree !== undefined) { + + ctx.lineWidth = 1; + + this._drawBranch(this.barnesHutTree.root, ctx, color); + } + } + + + /** + * This function is for debugging purposes. It draws the branches recursively. + * + * @param branch + * @param ctx + * @param color + * @private + */ + _drawBranch(branch, ctx, color) { + if (color === undefined) { + color = "#FF0000"; + } + + if (branch.childrenCount == 4) { + this._drawBranch(branch.children.NW, ctx); + this._drawBranch(branch.children.NE, ctx); + this._drawBranch(branch.children.SE, ctx); + this._drawBranch(branch.children.SW, ctx); + } + ctx.strokeStyle = color; + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.minY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.minY); + ctx.lineTo(branch.range.maxX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.maxX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.maxY); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(branch.range.minX, branch.range.maxY); + ctx.lineTo(branch.range.minX, branch.range.minY); + ctx.stroke(); + + /* + if (branch.mass > 0) { + ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); + ctx.stroke(); + } + */ + } +} + + +export {BarnesHutSolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/CentralGravitySolver.js b/lib/network/modules/components/physics/CentralGravitySolver.js new file mode 100644 index 00000000..911fa562 --- /dev/null +++ b/lib/network/modules/components/physics/CentralGravitySolver.js @@ -0,0 +1,32 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class CentralGravitySolver { + constructor(body, options) { + this.body = body; + this.options = options; + } + + solve() { + var dx, dy, distance, node, i; + var nodes = this.body.calculationNodes; + var gravity = this.options.centralGravity; + var gravityForce = 0; + var calculationNodeIndices = this.body.calculationNodeIndices; + + for (i = 0; i < calculationNodeIndices.length; i++) { + node = nodes[calculationNodeIndices[i]]; + dx = -node.x; + dy = -node.y; + distance = Math.sqrt(dx * dx + dy * dy); + + gravityForce = (distance == 0) ? 0 : (gravity / distance); + node.fx = dx * gravityForce; + node.fy = dy * gravityForce; + } + } +} + + +export {CentralGravitySolver}; \ No newline at end of file diff --git a/lib/network/modules/components/physics/SpringSolver.js b/lib/network/modules/components/physics/SpringSolver.js new file mode 100644 index 00000000..d33f8d66 --- /dev/null +++ b/lib/network/modules/components/physics/SpringSolver.js @@ -0,0 +1,109 @@ +/** + * Created by Alex on 2/23/2015. + */ + +class SpringSolver { + constructor(body, options) { + this.body = body; + this.options = options; + } + + solve() { + if (this.options.smoothCurves.enabled == true && this.options.smoothCurves.dynamic == true) { + this._calculateSpringForcesWithSupport(); + } + else { + 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() { + 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)) { + 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); + } + } + } + } + } + } + + + /** + * This is the code actually performing the calculation for the function above. It is split out to avoid repetition. + * + * @param node1 + * @param node2 + * @param edgeLength + * @private + */ + _calculateSpringForce(node1, node2, edgeLength) { + var dx, dy, fx, fy, springForce, distance; + + dx = (node1.x - node2.x); + dy = (node1.y - node2.y); + distance = Math.sqrt(dx * dx + dy * dy); + distance = distance == 0 ? 0.01 : distance; + + // the 1/distance is so the fx and fy can be calculated without sine or cosine. + springForce = this.options.springConstant * (edgeLength - distance) / distance; + + fx = dx * springForce; + fy = dy * springForce; + + node1.fx += fx; + node1.fy += fy; + node2.fx -= fx; + node2.fy -= fy; + } +} + +export {SpringSolver}; \ No newline at end of file