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.

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