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.

511 lines
15 KiB

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