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.

439 lines
13 KiB

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