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.

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