/**
|
|
* 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};
|