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.

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