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.

494 lines
14 KiB

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