vis.js is a dynamic, browser-based visualization library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

461 lines
14 KiB

  1. class BarnesHutSolver {
  2. constructor(body, physicsBody, options) {
  3. this.body = body;
  4. this.physicsBody = physicsBody;
  5. this.barnesHutTree;
  6. this.setOptions(options);
  7. this.randomSeed = 5;
  8. }
  9. setOptions(options) {
  10. this.options = options;
  11. this.thetaInversed = 1 / this.options.theta;
  12. this.overlapAvoidanceFactor = 1 - Math.max(0, Math.min(1,this.options.avoidOverlap)); // if 1 then min distance = 0.5, if 0.5 then min distance = 0.5 + 0.5*node.shape.radius
  13. }
  14. seededRandom() {
  15. var x = Math.sin(this.randomSeed++) * 10000;
  16. return x - Math.floor(x);
  17. }
  18. /**
  19. * This function calculates the forces the nodes apply on eachother based on a gravitational model.
  20. * The Barnes Hut method is used to speed up this N-body simulation.
  21. *
  22. * @private
  23. */
  24. solve() {
  25. if (this.options.gravitationalConstant !== 0 && this.physicsBody.physicsNodeIndices.length > 0) {
  26. let node;
  27. let nodes = this.body.nodes;
  28. let nodeIndices = this.physicsBody.physicsNodeIndices;
  29. let nodeCount = nodeIndices.length;
  30. // create the tree
  31. let barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices);
  32. // for debugging
  33. this.barnesHutTree = barnesHutTree;
  34. // place the nodes one by one recursively
  35. for (let i = 0; i < nodeCount; i++) {
  36. node = nodes[nodeIndices[i]];
  37. if (node.options.mass > 0) {
  38. // starting with root is irrelevant, it never passes the BarnesHutSolver condition
  39. this._getForceContribution(barnesHutTree.root.children.NW, node);
  40. this._getForceContribution(barnesHutTree.root.children.NE, node);
  41. this._getForceContribution(barnesHutTree.root.children.SW, node);
  42. this._getForceContribution(barnesHutTree.root.children.SE, node);
  43. }
  44. }
  45. }
  46. }
  47. /**
  48. * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
  49. * If a region contains a single node, we check if it is not itself, then we apply the force.
  50. *
  51. * @param parentBranch
  52. * @param node
  53. * @private
  54. */
  55. _getForceContribution(parentBranch, node) {
  56. // we get no force contribution from an empty region
  57. if (parentBranch.childrenCount > 0) {
  58. let dx, dy, distance;
  59. // get the distance from the center of mass to the node.
  60. dx = parentBranch.centerOfMass.x - node.x;
  61. dy = parentBranch.centerOfMass.y - node.y;
  62. distance = Math.sqrt(dx * dx + dy * dy);
  63. // BarnesHutSolver condition
  64. // original condition : s/d < theta = passed === d/s > 1/theta = passed
  65. // calcSize = 1/s --> d * 1/s > 1/theta = passed
  66. if (distance * parentBranch.calcSize > this.thetaInversed) {
  67. this._calculateForces(distance, dx, dy, node, parentBranch);
  68. }
  69. else {
  70. // Did not pass the condition, go into children if available
  71. if (parentBranch.childrenCount === 4) {
  72. this._getForceContribution(parentBranch.children.NW, node);
  73. this._getForceContribution(parentBranch.children.NE, node);
  74. this._getForceContribution(parentBranch.children.SW, node);
  75. this._getForceContribution(parentBranch.children.SE, node);
  76. }
  77. else { // parentBranch must have only one node, if it was empty we wouldnt be here
  78. if (parentBranch.children.data.id != node.id) { // if it is not self
  79. this._calculateForces(distance, dx, dy, node, parentBranch);
  80. }
  81. }
  82. }
  83. }
  84. }
  85. /**
  86. * Calculate the forces based on the distance.
  87. *
  88. * @param distance
  89. * @param dx
  90. * @param dy
  91. * @param node
  92. * @param parentBranch
  93. * @private
  94. */
  95. _calculateForces(distance, dx, dy, node, parentBranch) {
  96. if (distance === 0) {
  97. distance = 0.1;
  98. dx = distance;
  99. }
  100. if (this.overlapAvoidanceFactor < 1) {
  101. distance = Math.max(0.1 + (this.overlapAvoidanceFactor * node.shape.radius), distance - node.shape.radius);
  102. }
  103. // the dividing by the distance cubed instead of squared allows us to get the fx and fy components without sines and cosines
  104. // it is shorthand for gravityforce with distance squared and fx = dx/distance * gravityForce
  105. let gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / Math.pow(distance,3);
  106. let fx = dx * gravityForce;
  107. let fy = dy * gravityForce;
  108. this.physicsBody.forces[node.id].x += fx;
  109. this.physicsBody.forces[node.id].y += fy;
  110. }
  111. /**
  112. * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
  113. *
  114. * @param nodes
  115. * @param nodeIndices
  116. * @private
  117. */
  118. _formBarnesHutTree(nodes, nodeIndices) {
  119. let node;
  120. let nodeCount = nodeIndices.length;
  121. let minX = nodes[nodeIndices[0]].x;
  122. let minY = nodes[nodeIndices[0]].y;
  123. let maxX = nodes[nodeIndices[0]].x;
  124. let maxY = nodes[nodeIndices[0]].y;
  125. // get the range of the nodes
  126. for (let i = 1; i < nodeCount; i++) {
  127. let x = nodes[nodeIndices[i]].x;
  128. let y = nodes[nodeIndices[i]].y;
  129. if (nodes[nodeIndices[i]].options.mass > 0) {
  130. if (x < minX) {
  131. minX = x;
  132. }
  133. if (x > maxX) {
  134. maxX = x;
  135. }
  136. if (y < minY) {
  137. minY = y;
  138. }
  139. if (y > maxY) {
  140. maxY = y;
  141. }
  142. }
  143. }
  144. // make the range a square
  145. let sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
  146. if (sizeDiff > 0) {
  147. minY -= 0.5 * sizeDiff;
  148. maxY += 0.5 * sizeDiff;
  149. } // xSize > ySize
  150. else {
  151. minX += 0.5 * sizeDiff;
  152. maxX -= 0.5 * sizeDiff;
  153. } // xSize < ySize
  154. let minimumTreeSize = 1e-5;
  155. let rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX));
  156. let halfRootSize = 0.5 * rootSize;
  157. let centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
  158. // construct the barnesHutTree
  159. let barnesHutTree = {
  160. root: {
  161. centerOfMass: {x: 0, y: 0},
  162. mass: 0,
  163. range: {
  164. minX: centerX - halfRootSize, maxX: centerX + halfRootSize,
  165. minY: centerY - halfRootSize, maxY: centerY + halfRootSize
  166. },
  167. size: rootSize,
  168. calcSize: 1 / rootSize,
  169. children: {data: null},
  170. maxWidth: 0,
  171. level: 0,
  172. childrenCount: 4
  173. }
  174. };
  175. this._splitBranch(barnesHutTree.root);
  176. // place the nodes one by one recursively
  177. for (let i = 0; i < nodeCount; i++) {
  178. node = nodes[nodeIndices[i]];
  179. if (node.options.mass > 0) {
  180. this._placeInTree(barnesHutTree.root, node);
  181. }
  182. }
  183. // make global
  184. return barnesHutTree
  185. }
  186. /**
  187. * this updates the mass of a branch. this is increased by adding a node.
  188. *
  189. * @param parentBranch
  190. * @param node
  191. * @private
  192. */
  193. _updateBranchMass(parentBranch, node) {
  194. let totalMass = parentBranch.mass + node.options.mass;
  195. let totalMassInv = 1 / totalMass;
  196. parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
  197. parentBranch.centerOfMass.x *= totalMassInv;
  198. parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
  199. parentBranch.centerOfMass.y *= totalMassInv;
  200. parentBranch.mass = totalMass;
  201. let biggestSize = Math.max(Math.max(node.height, node.radius), node.width);
  202. parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
  203. }
  204. /**
  205. * determine in which branch the node will be placed.
  206. *
  207. * @param parentBranch
  208. * @param node
  209. * @param skipMassUpdate
  210. * @private
  211. */
  212. _placeInTree(parentBranch, node, skipMassUpdate) {
  213. if (skipMassUpdate != true || skipMassUpdate === undefined) {
  214. // update the mass of the branch.
  215. this._updateBranchMass(parentBranch, node);
  216. }
  217. if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
  218. if (parentBranch.children.NW.range.maxY > node.y) { // in NW
  219. this._placeInRegion(parentBranch, node, "NW");
  220. }
  221. else { // in SW
  222. this._placeInRegion(parentBranch, node, "SW");
  223. }
  224. }
  225. else { // in NE or SE
  226. if (parentBranch.children.NW.range.maxY > node.y) { // in NE
  227. this._placeInRegion(parentBranch, node, "NE");
  228. }
  229. else { // in SE
  230. this._placeInRegion(parentBranch, node, "SE");
  231. }
  232. }
  233. }
  234. /**
  235. * actually place the node in a region (or branch)
  236. *
  237. * @param parentBranch
  238. * @param node
  239. * @param region
  240. * @private
  241. */
  242. _placeInRegion(parentBranch, node, region) {
  243. switch (parentBranch.children[region].childrenCount) {
  244. case 0: // place node here
  245. parentBranch.children[region].children.data = node;
  246. parentBranch.children[region].childrenCount = 1;
  247. this._updateBranchMass(parentBranch.children[region], node);
  248. break;
  249. case 1: // convert into children
  250. // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
  251. // we move one node a pixel and we do not put it in the tree.
  252. if (parentBranch.children[region].children.data.x === node.x &&
  253. parentBranch.children[region].children.data.y === node.y) {
  254. node.x += this.seededRandom();
  255. node.y += this.seededRandom();
  256. }
  257. else {
  258. this._splitBranch(parentBranch.children[region]);
  259. this._placeInTree(parentBranch.children[region], node);
  260. }
  261. break;
  262. case 4: // place in branch
  263. this._placeInTree(parentBranch.children[region], node);
  264. break;
  265. }
  266. }
  267. /**
  268. * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
  269. * after the split is complete.
  270. *
  271. * @param parentBranch
  272. * @private
  273. */
  274. _splitBranch(parentBranch) {
  275. // if the branch is shaded with a node, replace the node in the new subset.
  276. let containedNode = null;
  277. if (parentBranch.childrenCount === 1) {
  278. containedNode = parentBranch.children.data;
  279. parentBranch.mass = 0;
  280. parentBranch.centerOfMass.x = 0;
  281. parentBranch.centerOfMass.y = 0;
  282. }
  283. parentBranch.childrenCount = 4;
  284. parentBranch.children.data = null;
  285. this._insertRegion(parentBranch, "NW");
  286. this._insertRegion(parentBranch, "NE");
  287. this._insertRegion(parentBranch, "SW");
  288. this._insertRegion(parentBranch, "SE");
  289. if (containedNode != null) {
  290. this._placeInTree(parentBranch, containedNode);
  291. }
  292. }
  293. /**
  294. * This function subdivides the region into four new segments.
  295. * Specifically, this inserts a single new segment.
  296. * It fills the children section of the parentBranch
  297. *
  298. * @param parentBranch
  299. * @param region
  300. * @param parentRange
  301. * @private
  302. */
  303. _insertRegion(parentBranch, region) {
  304. let minX, maxX, minY, maxY;
  305. let childSize = 0.5 * parentBranch.size;
  306. switch (region) {
  307. case "NW":
  308. minX = parentBranch.range.minX;
  309. maxX = parentBranch.range.minX + childSize;
  310. minY = parentBranch.range.minY;
  311. maxY = parentBranch.range.minY + childSize;
  312. break;
  313. case "NE":
  314. minX = parentBranch.range.minX + childSize;
  315. maxX = parentBranch.range.maxX;
  316. minY = parentBranch.range.minY;
  317. maxY = parentBranch.range.minY + childSize;
  318. break;
  319. case "SW":
  320. minX = parentBranch.range.minX;
  321. maxX = parentBranch.range.minX + childSize;
  322. minY = parentBranch.range.minY + childSize;
  323. maxY = parentBranch.range.maxY;
  324. break;
  325. case "SE":
  326. minX = parentBranch.range.minX + childSize;
  327. maxX = parentBranch.range.maxX;
  328. minY = parentBranch.range.minY + childSize;
  329. maxY = parentBranch.range.maxY;
  330. break;
  331. }
  332. parentBranch.children[region] = {
  333. centerOfMass: {x: 0, y: 0},
  334. mass: 0,
  335. range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY},
  336. size: 0.5 * parentBranch.size,
  337. calcSize: 2 * parentBranch.calcSize,
  338. children: {data: null},
  339. maxWidth: 0,
  340. level: parentBranch.level + 1,
  341. childrenCount: 0
  342. };
  343. }
  344. //--------------------------- DEBUGGING BELOW ---------------------------//
  345. /**
  346. * This function is for debugging purposed, it draws the tree.
  347. *
  348. * @param ctx
  349. * @param color
  350. * @private
  351. */
  352. _debug(ctx, color) {
  353. if (this.barnesHutTree !== undefined) {
  354. ctx.lineWidth = 1;
  355. this._drawBranch(this.barnesHutTree.root, ctx, color);
  356. }
  357. }
  358. /**
  359. * This function is for debugging purposes. It draws the branches recursively.
  360. *
  361. * @param branch
  362. * @param ctx
  363. * @param color
  364. * @private
  365. */
  366. _drawBranch(branch, ctx, color) {
  367. if (color === undefined) {
  368. color = "#FF0000";
  369. }
  370. if (branch.childrenCount === 4) {
  371. this._drawBranch(branch.children.NW, ctx);
  372. this._drawBranch(branch.children.NE, ctx);
  373. this._drawBranch(branch.children.SE, ctx);
  374. this._drawBranch(branch.children.SW, ctx);
  375. }
  376. ctx.strokeStyle = color;
  377. ctx.beginPath();
  378. ctx.moveTo(branch.range.minX, branch.range.minY);
  379. ctx.lineTo(branch.range.maxX, branch.range.minY);
  380. ctx.stroke();
  381. ctx.beginPath();
  382. ctx.moveTo(branch.range.maxX, branch.range.minY);
  383. ctx.lineTo(branch.range.maxX, branch.range.maxY);
  384. ctx.stroke();
  385. ctx.beginPath();
  386. ctx.moveTo(branch.range.maxX, branch.range.maxY);
  387. ctx.lineTo(branch.range.minX, branch.range.maxY);
  388. ctx.stroke();
  389. ctx.beginPath();
  390. ctx.moveTo(branch.range.minX, branch.range.maxY);
  391. ctx.lineTo(branch.range.minX, branch.range.minY);
  392. ctx.stroke();
  393. /*
  394. if (branch.mass > 0) {
  395. ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
  396. ctx.stroke();
  397. }
  398. */
  399. }
  400. }
  401. export default BarnesHutSolver;