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.

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