class BarnesHutSolver { constructor(body, physicsBody, options) { this.body = body; this.physicsBody = physicsBody; this.barnesHutTree; this.setOptions(options); } setOptions(options) { this.options = options; this.thetaInversed = 1/ this.options.theta; } /** * 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 && this.physicsBody.physicsNodeIndices.length > 0) { let node; let nodes = this.body.nodes; let nodeIndices = this.physicsBody.physicsNodeIndices; let nodeCount = nodeIndices.length; // create the tree let barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); // for debugging this.barnesHutTree = barnesHutTree; // place the nodes one by one recursively for (let 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) { let 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 < theta = passed === d/s > 1/theta = passed // calcSize = 1/s --> d * 1/s > 1/theta = passed if (distance * parentBranch.calcSize > this.thetaInversed) { this._calculateForces(distance, dx, dy, node, parentBranch); } 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 this._calculateForces(distance, dx, dy, node, parentBranch); } } } } } /** * Calculate the forces based on the distance. * * @param distance * @param dx * @param dy * @param node * @param parentBranch * @private */ _calculateForces(distance, dx, dy, node, parentBranch) { if (distance === 0) { distance = 0.1 * Math.random(); dx = distance; } // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce let gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance,3); let fx = dx * gravityForce; let fy = dy * gravityForce; this.physicsBody.forces[node.id].x += fx; this.physicsBody.forces[node.id].y += 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) { let node; let nodeCount = nodeIndices.length; let minX = nodes[nodeIndices[0]].x; let minY = nodes[nodeIndices[0]].y; let maxX = nodes[nodeIndices[0]].x; let maxY = nodes[nodeIndices[0]].y; // get the range of the nodes for (let i = 1; i < nodeCount; i++) { let x = nodes[nodeIndices[i]].x; let 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 let 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 let minimumTreeSize = 1e-5; let rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); let halfRootSize = 0.5 * rootSize; let centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); // construct the barnesHutTree let 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 (let 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) { let totalMass = parentBranch.mass + node.options.mass; let 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; let 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. let 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) { let minX, maxX, minY, maxY; let 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 */ _debug(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 default BarnesHutSolver;