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.

293 lines
10 KiB

  1. import BarnesHutSolver from './components/physics/BarnesHutSolver';
  2. import Repulsion from './components/physics/RepulsionSolver';
  3. import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver';
  4. import SpringSolver from './components/physics/SpringSolver';
  5. import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver';
  6. import CentralGravitySolver from './components/physics/CentralGravitySolver';
  7. import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver';
  8. import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
  9. class PhysicsWorker {
  10. constructor(postMessage) {
  11. this.body = {
  12. nodes: {},
  13. edges: {}
  14. };
  15. this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
  16. this.postMessage = postMessage;
  17. this.options = {};
  18. this.stabilized = false;
  19. this.previousStates = {};
  20. this.positions = {};
  21. this.timestep = 0.5;
  22. this.toRemove = {
  23. nodes: [],
  24. edges: []
  25. };
  26. }
  27. handleMessage(event) {
  28. var msg = event.data;
  29. switch (msg.type) {
  30. case 'physicsTick':
  31. this.physicsTick();
  32. break;
  33. case 'updatePositions':
  34. let updatedNode = this.body.nodes[msg.data.id];
  35. if (updatedNode) {
  36. updatedNode.x = msg.data.x;
  37. updatedNode.y = msg.data.y;
  38. this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0};
  39. this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0};
  40. }
  41. break;
  42. case 'updateProperties':
  43. this.updateProperties(msg.data);
  44. break;
  45. case 'addElements':
  46. this.addElements(msg.data);
  47. break;
  48. case 'removeElements':
  49. // schedule removal of elements on the next physicsTick
  50. // avoids having to defensively check every node read in each physics implementation
  51. this.toRemove.nodes.push.apply(this.toRemove.nodes, msg.data.nodes);
  52. this.toRemove.edges.push.apply(this.toRemove.edges, msg.data.edges);
  53. break;
  54. case 'initPhysicsData':
  55. this.initPhysicsData(msg.data);
  56. break;
  57. case 'options':
  58. this.options = msg.data;
  59. this.timestep = this.options.timestep;
  60. this.init();
  61. break;
  62. default:
  63. console.warn('unknown message from PhysicsEngine', msg);
  64. }
  65. }
  66. /**
  67. * configure the engine.
  68. */
  69. init() {
  70. var options;
  71. if (this.options.solver === 'forceAtlas2Based') {
  72. options = this.options.forceAtlas2Based;
  73. this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
  74. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  75. this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
  76. }
  77. else if (this.options.solver === 'repulsion') {
  78. options = this.options.repulsion;
  79. this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
  80. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  81. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  82. }
  83. else if (this.options.solver === 'hierarchicalRepulsion') {
  84. options = this.options.hierarchicalRepulsion;
  85. this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
  86. this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
  87. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  88. }
  89. else { // barnesHut
  90. options = this.options.barnesHut;
  91. this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
  92. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  93. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  94. }
  95. this.modelOptions = options;
  96. }
  97. physicsTick() {
  98. this.processRemovals();
  99. this.calculateForces();
  100. this.moveNodes();
  101. for (let i = 0; i < this.toRemove.nodes.length; i++) {
  102. delete this.positions[this.toRemove.nodes[i]];
  103. }
  104. this.postMessage({
  105. type: 'positions',
  106. data: {
  107. positions: this.positions,
  108. stabilized: this.stabilized
  109. }
  110. });
  111. }
  112. updateProperties(data) {
  113. let optionsNode = this.body.nodes[data.id];
  114. if (optionsNode) {
  115. let opts = data.options;
  116. if (opts.fixed) {
  117. if (opts.fixed.x !== undefined) {
  118. optionsNode.options.fixed.x = opts.fixed.x;
  119. }
  120. if (opts.fixed.y !== undefined) {
  121. optionsNode.options.fixed.y = opts.fixed.y;
  122. }
  123. }
  124. if (opts.mass !== undefined) {
  125. optionsNode.options.mass = opts.mass;
  126. }
  127. } else {
  128. console.log('sending property to unknown node');
  129. }
  130. }
  131. addElements(data, replaceElements = true) {
  132. let nodeIds = Object.keys(data.nodes);
  133. for (let i = 0; i < nodeIds.length; i++) {
  134. let nodeId = nodeIds[i];
  135. let newNode = data.nodes[nodeId];
  136. if (replaceElements) {
  137. this.body.nodes[nodeId] = newNode;
  138. }
  139. this.positions[nodeId] = {
  140. x: newNode.x,
  141. y: newNode.y
  142. };
  143. this.physicsBody.forces[nodeId] = {x: 0, y: 0};
  144. // forces can be reset because they are recalculated. Velocities have to persist.
  145. if (this.physicsBody.velocities[nodeId] === undefined) {
  146. this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
  147. }
  148. if (this.physicsBody.physicsNodeIndices.indexOf(nodeId) === -1) {
  149. this.physicsBody.physicsNodeIndices.push(nodeId);
  150. }
  151. }
  152. let edgeIds = Object.keys(data.edges);
  153. for (let i = 0; i < edgeIds.length; i++) {
  154. let edgeId = edgeIds[i];
  155. if (replaceElements) {
  156. this.body.edges[edgeId] = data.edges[edgeId];
  157. }
  158. if (this.physicsBody.physicsEdgeIndices.indexOf(edgeId) === -1) {
  159. this.physicsBody.physicsEdgeIndices.push(edgeId);
  160. }
  161. }
  162. }
  163. processRemovals() {
  164. while (this.toRemove.nodes.length > 0) {
  165. let nodeId = this.toRemove.nodes.pop();
  166. let index = this.physicsBody.physicsNodeIndices.indexOf(nodeId);
  167. if (index > -1) {
  168. this.physicsBody.physicsNodeIndices.splice(index,1);
  169. }
  170. delete this.physicsBody.forces[nodeId];
  171. delete this.physicsBody.velocities[nodeId];
  172. delete this.positions[nodeId];
  173. delete this.body.nodes[nodeId];
  174. }
  175. while (this.toRemove.edges.length > 0) {
  176. let edgeId = this.toRemove.edges.pop();
  177. let index = this.physicsBody.physicsEdgeIndices.indexOf(edgeId);
  178. if (index > -1) {
  179. this.physicsBody.physicsEdgeIndices.splice(index,1);
  180. }
  181. delete this.body.edges[edgeId];
  182. }
  183. }
  184. /**
  185. * Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
  186. *
  187. * @private
  188. */
  189. initPhysicsData(data) {
  190. this.physicsBody.forces = {};
  191. this.physicsBody.physicsNodeIndices = [];
  192. this.physicsBody.physicsEdgeIndices = [];
  193. this.positions = {};
  194. this.body.nodes = data.nodes;
  195. this.body.edges = data.edges;
  196. this.addElements(data, false);
  197. // clean deleted nodes from the velocity vector
  198. for (let nodeId in this.physicsBody.velocities) {
  199. if (this.body.nodes[nodeId] === undefined) {
  200. delete this.physicsBody.velocities[nodeId];
  201. }
  202. }
  203. }
  204. /**
  205. * move the nodes one timestap and check if they are stabilized
  206. * @returns {boolean}
  207. */
  208. moveNodes() {
  209. var nodeIndices = this.physicsBody.physicsNodeIndices;
  210. var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
  211. var maxNodeVelocity = 0;
  212. for (let i = 0; i < nodeIndices.length; i++) {
  213. let nodeId = nodeIndices[i];
  214. let nodeVelocity = this._performStep(nodeId, maxVelocity);
  215. // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
  216. maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
  217. }
  218. // evaluating the stabilized and adaptiveTimestepEnabled conditions
  219. this.stabilized = maxNodeVelocity < this.options.minVelocity;
  220. }
  221. /**
  222. * Perform the actual step
  223. *
  224. * @param nodeId
  225. * @param maxVelocity
  226. * @returns {number}
  227. * @private
  228. */
  229. _performStep(nodeId,maxVelocity) {
  230. let node = this.body.nodes[nodeId];
  231. let timestep = this.timestep;
  232. let forces = this.physicsBody.forces;
  233. let velocities = this.physicsBody.velocities;
  234. // store the state so we can revert
  235. this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
  236. if (node.options.fixed.x === false) {
  237. let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
  238. let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
  239. velocities[nodeId].x += ax * timestep; // velocity
  240. velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
  241. node.x += velocities[nodeId].x * timestep; // position
  242. }
  243. else {
  244. forces[nodeId].x = 0;
  245. velocities[nodeId].x = 0;
  246. }
  247. this.positions[nodeId].x = node.x;
  248. if (node.options.fixed.y === false) {
  249. let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
  250. let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
  251. velocities[nodeId].y += ay * timestep; // velocity
  252. velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
  253. node.y += velocities[nodeId].y * timestep; // position
  254. }
  255. else {
  256. forces[nodeId].y = 0;
  257. velocities[nodeId].y = 0;
  258. }
  259. this.positions[nodeId].y = node.y;
  260. let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
  261. return totalVelocity;
  262. }
  263. /**
  264. * calculate the forces for one physics iteration.
  265. */
  266. calculateForces() {
  267. this.gravitySolver.solve();
  268. this.nodesSolver.solve();
  269. this.edgesSolver.solve();
  270. }
  271. }
  272. export default PhysicsWorker;