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.

307 lines
11 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. nodeIds: [],
  24. edgeIds: []
  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.nodeIds.push.apply(this.toRemove.nodeIds, msg.data.nodeIds);
  52. this.toRemove.edgeIds.push.apply(this.toRemove.edgeIds, msg.data.edgeIds);
  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.nodeIds.length; i++) {
  102. delete this.positions[this.toRemove.nodeIds[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. if (data.type === 'node') {
  114. let optionsNode = this.body.nodes[data.id];
  115. if (optionsNode) {
  116. let opts = data.options;
  117. if (opts.fixed) {
  118. if (opts.fixed.x !== undefined) {
  119. optionsNode.options.fixed.x = opts.fixed.x;
  120. }
  121. if (opts.fixed.y !== undefined) {
  122. optionsNode.options.fixed.y = opts.fixed.y;
  123. }
  124. }
  125. if (opts.mass !== undefined) {
  126. optionsNode.options.mass = opts.mass;
  127. }
  128. } else {
  129. console.warn('sending properties to unknown node', data.id, data.options);
  130. }
  131. } else if (data.type === 'edge') {
  132. let edge = this.body.edges[data.id];
  133. if (edge) {
  134. let opts = data.options;
  135. if (opts.connected) {
  136. edge.connected = opts.connected;
  137. }
  138. } else {
  139. console.warn('sending properties to unknown edge', data.id, data.options);
  140. }
  141. } else {
  142. console.warn('sending properties to unknown element', data.id, data.options);
  143. }
  144. }
  145. addElements(data, replaceElements = true) {
  146. let nodeIds = Object.keys(data.nodes);
  147. for (let i = 0; i < nodeIds.length; i++) {
  148. let nodeId = nodeIds[i];
  149. let newNode = data.nodes[nodeId];
  150. if (replaceElements) {
  151. this.body.nodes[nodeId] = newNode;
  152. }
  153. this.positions[nodeId] = {
  154. x: newNode.x,
  155. y: newNode.y
  156. };
  157. this.physicsBody.forces[nodeId] = {x: 0, y: 0};
  158. // forces can be reset because they are recalculated. Velocities have to persist.
  159. if (this.physicsBody.velocities[nodeId] === undefined) {
  160. this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
  161. }
  162. if (this.physicsBody.physicsNodeIndices.indexOf(nodeId) === -1) {
  163. this.physicsBody.physicsNodeIndices.push(nodeId);
  164. }
  165. }
  166. let edgeIds = Object.keys(data.edges);
  167. for (let i = 0; i < edgeIds.length; i++) {
  168. let edgeId = edgeIds[i];
  169. if (replaceElements) {
  170. this.body.edges[edgeId] = data.edges[edgeId];
  171. }
  172. if (this.physicsBody.physicsEdgeIndices.indexOf(edgeId) === -1) {
  173. this.physicsBody.physicsEdgeIndices.push(edgeId);
  174. }
  175. }
  176. }
  177. processRemovals() {
  178. while (this.toRemove.nodeIds.length > 0) {
  179. let nodeId = this.toRemove.nodeIds.pop();
  180. let index = this.physicsBody.physicsNodeIndices.indexOf(nodeId);
  181. if (index > -1) {
  182. this.physicsBody.physicsNodeIndices.splice(index,1);
  183. }
  184. delete this.physicsBody.forces[nodeId];
  185. delete this.physicsBody.velocities[nodeId];
  186. delete this.positions[nodeId];
  187. delete this.body.nodes[nodeId];
  188. }
  189. while (this.toRemove.edgeIds.length > 0) {
  190. let edgeId = this.toRemove.edgeIds.pop();
  191. let index = this.physicsBody.physicsEdgeIndices.indexOf(edgeId);
  192. if (index > -1) {
  193. this.physicsBody.physicsEdgeIndices.splice(index,1);
  194. }
  195. delete this.body.edges[edgeId];
  196. }
  197. }
  198. /**
  199. * 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.
  200. *
  201. * @private
  202. */
  203. initPhysicsData(data) {
  204. this.physicsBody.forces = {};
  205. this.physicsBody.physicsNodeIndices = [];
  206. this.physicsBody.physicsEdgeIndices = [];
  207. this.positions = {};
  208. this.body.nodes = data.nodes;
  209. this.body.edges = data.edges;
  210. this.addElements(data, false);
  211. // clean deleted nodes from the velocity vector
  212. for (let nodeId in this.physicsBody.velocities) {
  213. if (this.body.nodes[nodeId] === undefined) {
  214. delete this.physicsBody.velocities[nodeId];
  215. }
  216. }
  217. }
  218. /**
  219. * move the nodes one timestap and check if they are stabilized
  220. * @returns {boolean}
  221. */
  222. moveNodes() {
  223. var nodeIndices = this.physicsBody.physicsNodeIndices;
  224. var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
  225. var maxNodeVelocity = 0;
  226. for (let i = 0; i < nodeIndices.length; i++) {
  227. let nodeId = nodeIndices[i];
  228. let nodeVelocity = this._performStep(nodeId, maxVelocity);
  229. // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
  230. maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
  231. }
  232. // evaluating the stabilized and adaptiveTimestepEnabled conditions
  233. this.stabilized = maxNodeVelocity < this.options.minVelocity;
  234. }
  235. /**
  236. * Perform the actual step
  237. *
  238. * @param nodeId
  239. * @param maxVelocity
  240. * @returns {number}
  241. * @private
  242. */
  243. _performStep(nodeId,maxVelocity) {
  244. let node = this.body.nodes[nodeId];
  245. let timestep = this.timestep;
  246. let forces = this.physicsBody.forces;
  247. let velocities = this.physicsBody.velocities;
  248. // store the state so we can revert
  249. this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
  250. if (node.options.fixed.x === false) {
  251. let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
  252. let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
  253. velocities[nodeId].x += ax * timestep; // velocity
  254. velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
  255. node.x += velocities[nodeId].x * timestep; // position
  256. }
  257. else {
  258. forces[nodeId].x = 0;
  259. velocities[nodeId].x = 0;
  260. }
  261. this.positions[nodeId].x = node.x;
  262. if (node.options.fixed.y === false) {
  263. let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
  264. let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
  265. velocities[nodeId].y += ay * timestep; // velocity
  266. velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
  267. node.y += velocities[nodeId].y * timestep; // position
  268. }
  269. else {
  270. forces[nodeId].y = 0;
  271. velocities[nodeId].y = 0;
  272. }
  273. this.positions[nodeId].y = node.y;
  274. let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
  275. return totalVelocity;
  276. }
  277. /**
  278. * calculate the forces for one physics iteration.
  279. */
  280. calculateForces() {
  281. this.gravitySolver.solve();
  282. this.nodesSolver.solve();
  283. this.edgesSolver.solve();
  284. }
  285. }
  286. export default PhysicsWorker;