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.
 
 
 

311 lines
11 KiB

import BarnesHutSolver from './components/physics/BarnesHutSolver';
import Repulsion from './components/physics/RepulsionSolver';
import HierarchicalRepulsion from './components/physics/HierarchicalRepulsionSolver';
import SpringSolver from './components/physics/SpringSolver';
import HierarchicalSpringSolver from './components/physics/HierarchicalSpringSolver';
import CentralGravitySolver from './components/physics/CentralGravitySolver';
import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedRepulsionSolver';
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
class PhysicsWorker {
constructor(postMessage) {
this.body = {
nodes: {},
edges: {}
};
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.postMessage = postMessage;
this.options = {};
this.stabilized = false;
this.previousStates = {};
this.positions = {};
this.timestep = 0.5;
this.toRemove = {
nodes: [],
edges: []
};
}
handleMessage(event) {
var msg = event.data;
switch (msg.type) {
case 'physicsTick':
this.physicsTick();
break;
case 'updatePositions':
let updatedNode = this.body.nodes[msg.data.id];
if (updatedNode) {
updatedNode.x = msg.data.x;
updatedNode.y = msg.data.y;
this.physicsBody.forces[updatedNode.id] = {x: 0, y: 0};
this.physicsBody.velocities[updatedNode.id] = {x: 0, y: 0};
}
break;
case 'updateProperties':
this.updateProperties(msg.data);
break;
case 'addElements':
this.addElements(msg.data);
break;
case 'removeElements':
// schedule removal of elements on the next physicsTick
// avoids having to defensively check every node read in each physics implementation
this.toRemove.nodes.push.apply(this.toRemove.nodes, msg.data.nodes);
this.toRemove.edges.push.apply(this.toRemove.edges, msg.data.edges);
break;
case 'physicsObjects':
this.body.nodes = msg.data.nodes;
this.body.edges = msg.data.edges;
this.initPhysicsData();
break;
case 'options':
this.options = msg.data;
this.timestep = this.options.timestep;
this.init();
break;
default:
console.warn('unknown message from PhysicsEngine', msg);
}
}
/**
* configure the engine.
*/
init() {
var options;
if (this.options.solver === 'forceAtlas2Based') {
options = this.options.forceAtlas2Based;
this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'repulsion') {
options = this.options.repulsion;
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'hierarchicalRepulsion') {
options = this.options.hierarchicalRepulsion;
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
this.modelOptions = options;
}
physicsTick() {
this.processRemovals();
this.calculateForces();
this.moveNodes();
for (let i = 0; i < this.toRemove.nodes.length; i++) {
delete this.positions[this.toRemove.nodes[i]];
}
this.postMessage({
type: 'positions',
data: {
positions: this.positions,
stabilized: this.stabilized
}
});
}
updateProperties(data) {
let optionsNode = this.body.nodes[data.id];
if (optionsNode) {
let opts = data.options;
if (opts.fixed) {
if (opts.fixed.x !== undefined) {
optionsNode.options.fixed.x = opts.fixed.x;
}
if (opts.fixed.y !== undefined) {
optionsNode.options.fixed.y = opts.fixed.y;
}
}
if (opts.mass !== undefined) {
optionsNode.options.mass = opts.mass;
}
} else {
console.log('sending property to unknown node');
}
}
addElements(data) {
// TODO expand to handle multiple and edges
let newNode = data;
let nodeId = newNode.id;
this.body.nodes[nodeId] = newNode;
this.positions[nodeId] = {
x: newNode.x,
y: newNode.y
};
this.physicsBody.forces[nodeId] = {x: 0, y: 0};
this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
if (this.physicsBody.physicsNodeIndices.indexOf(nodeId) === -1) {
this.physicsBody.physicsNodeIndices.push(nodeId);
}
console.log('added node', nodeId);
}
processRemovals() {
while (this.toRemove.nodes.length > 0) {
let nodeId = this.toRemove.nodes.pop();
// TODO any optimization here?
let index = this.physicsBody.physicsNodeIndices.indexOf(nodeId);
if (index === -1 && typeof nodeId === 'number') {
index = this.physicsBody.physicsNodeIndices.indexOf(nodeId.toString());
}
if (index > -1) {
this.physicsBody.physicsNodeIndices.splice(index,1);
}
delete this.physicsBody.forces[nodeId];
delete this.physicsBody.velocities[nodeId];
delete this.positions[nodeId];
delete this.body.nodes[nodeId];
console.log('removed node', nodeId);
}
while (this.toRemove.edges.length > 0) {
let edgeId = this.toRemove.edges.pop();
let index = this.physicsBody.physicsEdgeIndices.indexOf(edgeId);
if (index > -1) {
this.physicsBody.physicsEdgeIndices.splice(index,1);
}
delete this.body.edges[edgeId];
}
}
/**
* 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.
*
* @private
*/
initPhysicsData() {
this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = [];
let nodes = this.body.nodes;
let edges = this.body.edges;
// get node indices for physics
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
this.physicsBody.physicsNodeIndices.push(nodeId);
this.positions[nodeId] = {
x: nodes[nodeId].x,
y: nodes[nodeId].y
}
}
}
// get edge indices for physics
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
this.physicsBody.physicsEdgeIndices.push(edgeId);
}
}
// get the velocity and the forces vector
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let nodeId = this.physicsBody.physicsNodeIndices[i];
this.physicsBody.forces[nodeId] = {x: 0, y: 0};
// forces can be reset because they are recalculated. Velocities have to persist.
if (this.physicsBody.velocities[nodeId] === undefined) {
this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
}
}
// clean deleted nodes from the velocity vector
for (let nodeId in this.physicsBody.velocities) {
if (nodes[nodeId] === undefined) {
delete this.physicsBody.velocities[nodeId];
}
}
// console.log(this.physicsBody);
}
/**
* move the nodes one timestap and check if they are stabilized
* @returns {boolean}
*/
moveNodes() {
var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var maxNodeVelocity = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
/**
* Perform the actual step
*
* @param nodeId
* @param maxVelocity
* @returns {number}
* @private
*/
_performStep(nodeId,maxVelocity) {
let node = this.body.nodes[nodeId];
let timestep = this.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
if (node.options.fixed.x === false) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
}
this.positions[nodeId].x = node.x;
if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
this.positions[nodeId].y = node.y;
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
}
export default PhysicsWorker;