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.

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