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.

452 lines
14 KiB

  1. /**
  2. * Created by Alex on 2/23/2015.
  3. */
  4. import BarnesHutSolver from "./components/physics/BarnesHutSolver";
  5. import Repulsion from "./components/physics/RepulsionSolver";
  6. import HierarchicalRepulsion from "./components/physics/HierarchicalRepulsionSolver";
  7. import SpringSolver from "./components/physics/SpringSolver";
  8. import HierarchicalSpringSolver from "./components/physics/HierarchicalSpringSolver";
  9. import CentralGravitySolver from "./components/physics/CentralGravitySolver";
  10. var util = require('../../util');
  11. class PhysicsEngine {
  12. constructor(body) {
  13. this.body = body;
  14. this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
  15. this.physicsEnabled = true;
  16. this.simulationInterval = 1000 / 60;
  17. this.requiresTimeout = true;
  18. this.previousStates = {};
  19. this.freezeCache = {};
  20. this.renderTimer == undefined;
  21. this.stabilized = false;
  22. this.stabilizationIterations = 0;
  23. this.ready = false; // will be set to true if the stabilize
  24. // default options
  25. this.options = {};
  26. this.defaultOptions = {
  27. barnesHut: {
  28. thetaInverted: 1 / 0.5, // inverted to save time during calculation
  29. gravitationalConstant: -2000,
  30. centralGravity: 0.3,
  31. springLength: 95,
  32. springConstant: 0.04,
  33. damping: 0.09
  34. },
  35. repulsion: {
  36. centralGravity: 0.0,
  37. springLength: 200,
  38. springConstant: 0.05,
  39. nodeDistance: 100,
  40. damping: 0.09
  41. },
  42. hierarchicalRepulsion: {
  43. centralGravity: 0.0,
  44. springLength: 100,
  45. springConstant: 0.01,
  46. nodeDistance: 120,
  47. damping: 0.09
  48. },
  49. solver: 'BarnesHut',
  50. timestep: 0.5,
  51. maxVelocity: 50,
  52. minVelocity: 0.1, // px/s
  53. stabilization: {
  54. enabled: true,
  55. iterations: 1000, // maximum number of iteration to stabilize
  56. updateInterval: 100,
  57. onlyDynamicEdges: false,
  58. zoomExtent: true
  59. }
  60. }
  61. util.extend(this.options, this.defaultOptions);
  62. this.body.emitter.on("initPhysics", () => {this.initPhysics();});
  63. this.body.emitter.on("resetPhysics", () => {this.stopSimulation(); this.ready = false;});
  64. this.body.emitter.on("startSimulation", () => {
  65. if (this.ready === true) {
  66. this.stabilized = false;
  67. this.runSimulation();
  68. }
  69. })
  70. this.body.emitter.on("stopSimulation", () => {console.log(4);this.stopSimulation();});
  71. }
  72. setOptions(options) {
  73. if (options === false) {
  74. this.physicsEnabled = false;
  75. }
  76. else {
  77. if (options !== undefined) {
  78. util.selectiveNotDeepExtend(['stabilization'],this.options, options);
  79. util.mergeOptions(this.options, options, 'stabilization')
  80. }
  81. this.init();
  82. }
  83. }
  84. init() {
  85. var options;
  86. if (this.options.solver == "repulsion") {
  87. options = this.options.repulsion;
  88. this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
  89. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  90. }
  91. else if (this.options.solver == "hierarchicalRepulsion") {
  92. options = this.options.hierarchicalRepulsion;
  93. this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
  94. this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
  95. }
  96. else { // barnesHut
  97. options = this.options.barnesHut;
  98. this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
  99. this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
  100. }
  101. this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
  102. this.modelOptions = options;
  103. }
  104. initPhysics() {
  105. if (this.physicsEnabled === true) {
  106. this.stabilized = false;
  107. if (this.options.stabilization.enabled === true) {
  108. this.stabilize();
  109. }
  110. else {
  111. this.ready = true;
  112. this.body.emitter.emit("zoomExtent", {duration: 0}, true)
  113. this.runSimulation();
  114. }
  115. }
  116. else {
  117. this.ready = true;
  118. this.body.emitter.emit("_redraw");
  119. }
  120. }
  121. stopSimulation() {
  122. this.stabilized = true;
  123. if (this.viewFunction !== undefined) {
  124. this.body.emitter.off("initRedraw", this.viewFunction);
  125. this.viewFunction = undefined;
  126. this.body.emitter.emit("_stopRendering");
  127. }
  128. }
  129. runSimulation() {
  130. if (this.physicsEnabled === true) {
  131. if (this.viewFunction === undefined) {
  132. this.viewFunction = this.simulationStep.bind(this);
  133. this.body.emitter.on("initRedraw", this.viewFunction);
  134. this.body.emitter.emit("_startRendering");
  135. }
  136. }
  137. else {
  138. this.body.emitter.emit("_redraw");
  139. }
  140. }
  141. simulationStep() {
  142. // check if the physics have settled
  143. var startTime = Date.now();
  144. this.physicsTick();
  145. var physicsTime = Date.now() - startTime;
  146. // run double speed if it is a little graph
  147. if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) {
  148. this.physicsTick();
  149. // this makes sure there is no jitter. The decision is taken once to run it at double speed.
  150. this.runDoubleSpeed = true;
  151. }
  152. if (this.stabilized === true) {
  153. if (this.stabilizationIterations > 1) {
  154. // trigger the "stabilized" event.
  155. // The event is triggered on the next tick, to prevent the case that
  156. // it is fired while initializing the Network, in which case you would not
  157. // be able to catch it
  158. var me = this;
  159. var params = {
  160. iterations: this.stabilizationIterations
  161. };
  162. this.stabilizationIterations = 0;
  163. this.startedStabilization = false;
  164. setTimeout(function () {
  165. me.body.emitter.emit("stabilized", params);
  166. }, 0);
  167. }
  168. else {
  169. this.stabilizationIterations = 0;
  170. }
  171. this.stopSimulation();
  172. }
  173. }
  174. /**
  175. * A single simulation step (or "tick") in the physics simulation
  176. *
  177. * @private
  178. */
  179. physicsTick() {
  180. if (this.stabilized === false) {
  181. this.calculateForces();
  182. this.stabilized = this.moveNodes();
  183. // determine if the network has stabilzied
  184. if (this.stabilized === true) {
  185. this.revert();
  186. }
  187. else {
  188. // this is here to ensure that there is no start event when the network is already stable.
  189. if (this.startedStabilization == false) {
  190. this.body.emitter.emit("startStabilizing");
  191. this.startedStabilization = true;
  192. }
  193. }
  194. this.stabilizationIterations++;
  195. }
  196. }
  197. /**
  198. * Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also
  199. * handled in the calculateForces function. We then use a quadratic curve with the center node as control.
  200. * This function joins the datanodes and invisible (called support) nodes into one object.
  201. * We do this so we do not contaminate this.body.nodes with the support nodes.
  202. *
  203. * @private
  204. */
  205. updatePhysicsIndices() {
  206. this.physicsBody.forces = {};
  207. this.physicsBody.physicsNodeIndices = [];
  208. this.physicsBody.physicsEdgeIndices = [];
  209. let nodes = this.body.nodes;
  210. let edges = this.body.edges;
  211. // get node indices for physics
  212. for (let nodeId in nodes) {
  213. if (nodes.hasOwnProperty(nodeId)) {
  214. if (nodes[nodeId].options.physics === true) {
  215. this.physicsBody.physicsNodeIndices.push(nodeId);
  216. }
  217. }
  218. }
  219. // get edge indices for physics
  220. for (let edgeId in edges) {
  221. if (edges.hasOwnProperty(edgeId)) {
  222. if (edges[edgeId].options.physics === true) {
  223. this.physicsBody.physicsEdgeIndices.push(edgeId);
  224. }
  225. }
  226. }
  227. // get the velocity and the forces vector
  228. for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
  229. let nodeId = this.physicsBody.physicsNodeIndices[i];
  230. this.physicsBody.forces[nodeId] = {x:0,y:0};
  231. // forces can be reset because they are recalculated. Velocities have to persist.
  232. if (this.physicsBody.velocities[nodeId] === undefined) {
  233. this.physicsBody.velocities[nodeId] = {x:0,y:0};
  234. }
  235. }
  236. // clean deleted nodes from the velocity vector
  237. for (let nodeId in this.physicsBody.velocities) {
  238. if (nodes[nodeId] === undefined) {
  239. delete this.physicsBody.velocities[nodeId];
  240. }
  241. }
  242. }
  243. revert() {
  244. var nodeIds = Object.keys(this.previousStates);
  245. var nodes = this.body.nodes;
  246. var velocities = this.physicsBody.velocities;
  247. for (let i = 0; i < nodeIds.length; i++) {
  248. let nodeId = nodeIds[i];
  249. if (nodes[nodeId] !== undefined) {
  250. velocities[nodeId].x = this.previousStates[nodeId].vx;
  251. velocities[nodeId].y = this.previousStates[nodeId].vy;
  252. nodes[nodeId].x = this.previousStates[nodeId].x;
  253. nodes[nodeId].y = this.previousStates[nodeId].y;
  254. }
  255. else {
  256. delete this.previousStates[nodeId];
  257. }
  258. }
  259. }
  260. moveNodes() {
  261. var nodesPresent = false;
  262. var nodeIndices = this.physicsBody.physicsNodeIndices;
  263. var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity;
  264. var stabilized = true;
  265. var vminCorrected = this.options.minVelocity / Math.max(this.body.view.scale,0.05);
  266. for (let i = 0; i < nodeIndices.length; i++) {
  267. let nodeId = nodeIndices[i];
  268. let nodeVelocity = this._performStep(nodeId, maxVelocity);
  269. // stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
  270. stabilized = nodeVelocity < vminCorrected && stabilized === true;
  271. nodesPresent = true;
  272. }
  273. if (nodesPresent == true) {
  274. if (vminCorrected > 0.5*this.options.maxVelocity) {
  275. return false;
  276. }
  277. else {
  278. return stabilized;
  279. }
  280. }
  281. return true;
  282. }
  283. _performStep(nodeId,maxVelocity) {
  284. var node = this.body.nodes[nodeId];
  285. var timestep = this.options.timestep;
  286. var forces = this.physicsBody.forces;
  287. var velocities = this.physicsBody.velocities;
  288. // store the state so we can revert
  289. this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
  290. if (node.options.fixed.x === false) {
  291. let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
  292. let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
  293. velocities[nodeId].x += ax * timestep; // velocity
  294. velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
  295. node.x += velocities[nodeId].x * timestep; // position
  296. }
  297. else {
  298. forces[nodeId].x = 0;
  299. velocities[nodeId].x = 0;
  300. }
  301. if (node.options.fixed.y === false) {
  302. let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
  303. let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
  304. velocities[nodeId].y += ay * timestep; // velocity
  305. velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
  306. node.y += velocities[nodeId].y * timestep; // position
  307. }
  308. else {
  309. forces[nodeId].y = 0;
  310. velocities[nodeId].y = 0;
  311. }
  312. var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
  313. return totalVelocity;
  314. }
  315. calculateForces() {
  316. this.gravitySolver.solve();
  317. this.nodesSolver.solve();
  318. this.edgesSolver.solve();
  319. }
  320. /**
  321. * When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
  322. * because only the supportnodes for the smoothCurves have to settle.
  323. *
  324. * @private
  325. */
  326. _freezeNodes() {
  327. var nodes = this.body.nodes;
  328. for (var id in nodes) {
  329. if (nodes.hasOwnProperty(id)) {
  330. if (nodes[id].x && nodes[id].y) {
  331. this.freezeCache[id] = {x:nodes[id].options.fixed.x,y:nodes[id].options.fixed.y};
  332. nodes[id].options.fixed.x = true;
  333. nodes[id].options.fixed.y = true;
  334. }
  335. }
  336. }
  337. }
  338. /**
  339. * Unfreezes the nodes that have been frozen by _freezeDefinedNodes.
  340. *
  341. * @private
  342. */
  343. _restoreFrozenNodes() {
  344. var nodes = this.body.nodes;
  345. for (var id in nodes) {
  346. if (nodes.hasOwnProperty(id)) {
  347. if (this.freezeCache[id] !== undefined) {
  348. nodes[id].options.fixed.x = this.freezeCache[id].x;
  349. nodes[id].options.fixed.y = this.freezeCache[id].y;
  350. }
  351. }
  352. }
  353. this.freezeCache = {};
  354. }
  355. /**
  356. * Find a stable position for all nodes
  357. * @private
  358. */
  359. stabilize() {
  360. if (this.options.stabilization.onlyDynamicEdges == true) {
  361. this._freezeNodes();
  362. }
  363. this.stabilizationSteps = 0;
  364. setTimeout(this._stabilizationBatch.bind(this),0);
  365. }
  366. _stabilizationBatch() {
  367. var count = 0;
  368. while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) {
  369. this.physicsTick();
  370. this.stabilizationSteps++;
  371. count++;
  372. }
  373. if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) {
  374. this.body.emitter.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.options.stabilization.iterations});
  375. setTimeout(this._stabilizationBatch.bind(this),0);
  376. }
  377. else {
  378. this._finalizeStabilization();
  379. }
  380. }
  381. _finalizeStabilization() {
  382. if (this.options.stabilization.zoomExtent == true) {
  383. this.body.emitter.emit("zoomExtent", {duration:0});
  384. }
  385. if (this.options.stabilization.onlyDynamicEdges == true) {
  386. this._restoreFrozenNodes();
  387. }
  388. this.body.emitter.emit("stabilizationIterationsDone");
  389. this.body.emitter.emit("_requestRedraw");
  390. this.ready = true;
  391. }
  392. }
  393. export default PhysicsEngine;