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.

481 lines
15 KiB

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