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.

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