Conflicts: dist/vis.js lib/network/mixins/ManipulationMixin.jsflowchartTest
@ -0,0 +1,103 @@ | |||||
<!doctype html> | |||||
<html> | |||||
<head> | |||||
<title>Network | Basic usage</title> | |||||
<script type="text/javascript" src="../../dist/vis.js"></script> | |||||
<link href="../../dist/vis.css" rel="stylesheet" type="text/css" /> | |||||
<style type="text/css"> | |||||
#mynetwork { | |||||
width: 400px; | |||||
height: 400px; | |||||
border: 1px solid lightgray; | |||||
} | |||||
</style> | |||||
</head> | |||||
<body> | |||||
<div id="mynetwork"></div> | |||||
<script type="text/javascript"> | |||||
// create an array with nodes | |||||
var nodes = [ | |||||
{id: 1, label: 'Node 1'}, | |||||
{id: 2, label: 'Node 2'}, | |||||
{id: 3, label: 'Node 3'}, | |||||
{id: 4, label: 'Node 4'}, | |||||
{id: 5, label: 'Node 5'}, | |||||
{id: 6, label: 'Node 6', cid:1}, | |||||
{id: 7, label: 'Node 7', cid:1}, | |||||
{id: 8, label: 'Node 8', cid:1}, | |||||
{id: 9, label: 'Node 9', cid:1}, | |||||
{id: 10, label: 'Node 10', cid:1} | |||||
]; | |||||
// create an array with edges | |||||
var edges = [ | |||||
{from: 1, to: 2}, | |||||
{from: 1, to: 3}, | |||||
{from: 10, to: 4}, | |||||
{from: 2, to: 5}, | |||||
{from: 6, to: 2}, | |||||
{from: 7, to: 5}, | |||||
{from: 8, to: 6}, | |||||
{from: 9, to: 7}, | |||||
{from: 10, to: 9} | |||||
]; | |||||
// create a network | |||||
var container = document.getElementById('mynetwork'); | |||||
var data = { | |||||
nodes: nodes, | |||||
edges: edges | |||||
}; | |||||
var options = {}; | |||||
var network = new vis.Network(container, data, options); | |||||
var clusterOptions = { | |||||
joinCondition:function(parentOptions,childOptions) { | |||||
return true; | |||||
}, | |||||
processClusterProperties: function (properties, childNodes, childEdges) { | |||||
return properties; | |||||
}, | |||||
clusterNodeProperties: {id:'bla', borderWidth:8}, | |||||
} | |||||
var clusterOptionsByData = { | |||||
joinCondition:function(childOptions) { | |||||
return childOptions.cid == 1; | |||||
}, | |||||
processClusterProperties: function (properties, childNodes, childEdges) { | |||||
return properties; | |||||
}, | |||||
clusterNodeProperties: {id:'bla', borderWidth:8} | |||||
} | |||||
// network.clusterByNodeData(clusterOptionsByData) | |||||
network.clustering.clusterOutliers({clusterNodeProperties: {borderWidth:8}}) | |||||
// network.clusterByConnection(2, clusterOptions); | |||||
// network.clusterByConnection(9, { | |||||
// joinCondition:function(parentOptions,childOptions) {return true;}, | |||||
// processProperties:function (properties, childNodes, childEdges) { | |||||
// return properties; | |||||
// }, | |||||
// clusterNodeProperties: {id:'bla2', label:"bla2", borderWidth:8} | |||||
// }); | |||||
network.on("select", function(params) { | |||||
if (params.nodes.length == 1) { | |||||
if (network.clustering.isCluster(params.nodes[0]) == true) { | |||||
network.clustering.openCluster(params.nodes[0]) | |||||
} | |||||
} | |||||
}) | |||||
// network.openCluster('bla'); | |||||
// network.openCluster('bla2'); | |||||
</script> | |||||
</body> | |||||
</html> |
@ -1,399 +0,0 @@ | |||||
/** | |||||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
* The Barnes Hut method is used to speed up this N-body simulation. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function() { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != 0) { | |||||
var node; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
var nodeCount = nodeIndices.length; | |||||
this._formBarnesHutTree(nodes,nodeIndices); | |||||
var barnesHutTree = this.barnesHutTree; | |||||
// place the nodes one by one recursively | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
if (node.options.mass > 0) { | |||||
// starting with root is irrelevant, it never passes the BarnesHut condition | |||||
this._getForceContribution(barnesHutTree.root.children.NW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.NE,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SW,node); | |||||
this._getForceContribution(barnesHutTree.root.children.SE,node); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
exports._getForceContribution = function(parentBranch,node) { | |||||
// we get no force contribution from an empty region | |||||
if (parentBranch.childrenCount > 0) { | |||||
var dx,dy,distance; | |||||
// get the distance from the center of mass to the node. | |||||
dx = parentBranch.centerOfMass.x - node.x; | |||||
dy = parentBranch.centerOfMass.y - node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// BarnesHut condition | |||||
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed | |||||
// calcSize = 1/s --> d * 1/s > 1/theta = passed | |||||
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) { | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.1*Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
node.fx += fx; | |||||
node.fy += fy; | |||||
} | |||||
else { | |||||
// Did not pass the condition, go into children if available | |||||
if (parentBranch.childrenCount == 4) { | |||||
this._getForceContribution(parentBranch.children.NW,node); | |||||
this._getForceContribution(parentBranch.children.NE,node); | |||||
this._getForceContribution(parentBranch.children.SW,node); | |||||
this._getForceContribution(parentBranch.children.SE,node); | |||||
} | |||||
else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||||
if (parentBranch.children.data.id != node.id) { // if it is not self | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.5*Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.constants.physics.barnesHut.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
node.fx += fx; | |||||
node.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
* | |||||
* @param nodes | |||||
* @param nodeIndices | |||||
* @private | |||||
*/ | |||||
exports._formBarnesHutTree = function(nodes,nodeIndices) { | |||||
var node; | |||||
var nodeCount = nodeIndices.length; | |||||
var minX = Number.MAX_VALUE, | |||||
minY = Number.MAX_VALUE, | |||||
maxX =-Number.MAX_VALUE, | |||||
maxY =-Number.MAX_VALUE; | |||||
// get the range of the nodes | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
var x = nodes[nodeIndices[i]].x; | |||||
var y = nodes[nodeIndices[i]].y; | |||||
if (nodes[nodeIndices[i]].options.mass > 0) { | |||||
if (x < minX) { minX = x; } | |||||
if (x > maxX) { maxX = x; } | |||||
if (y < minY) { minY = y; } | |||||
if (y > maxY) { maxY = y; } | |||||
} | |||||
} | |||||
// make the range a square | |||||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize | |||||
else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize | |||||
var minimumTreeSize = 1e-5; | |||||
var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX)); | |||||
var halfRootSize = 0.5 * rootSize; | |||||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
// construct the barnesHutTree | |||||
var barnesHutTree = { | |||||
root:{ | |||||
centerOfMass: {x:0, y:0}, | |||||
mass:0, | |||||
range: { | |||||
minX: centerX-halfRootSize,maxX:centerX+halfRootSize, | |||||
minY: centerY-halfRootSize,maxY:centerY+halfRootSize | |||||
}, | |||||
size: rootSize, | |||||
calcSize: 1 / rootSize, | |||||
children: { data:null}, | |||||
maxWidth: 0, | |||||
level: 0, | |||||
childrenCount: 4 | |||||
} | |||||
}; | |||||
this._splitBranch(barnesHutTree.root); | |||||
// place the nodes one by one recursively | |||||
for (i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
if (node.options.mass > 0) { | |||||
this._placeInTree(barnesHutTree.root,node); | |||||
} | |||||
} | |||||
// make global | |||||
this.barnesHutTree = barnesHutTree | |||||
}; | |||||
/** | |||||
* this updates the mass of a branch. this is increased by adding a node. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
exports._updateBranchMass = function(parentBranch, node) { | |||||
var totalMass = parentBranch.mass + node.options.mass; | |||||
var totalMassInv = 1/totalMass; | |||||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; | |||||
parentBranch.centerOfMass.x *= totalMassInv; | |||||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; | |||||
parentBranch.centerOfMass.y *= totalMassInv; | |||||
parentBranch.mass = totalMass; | |||||
var biggestSize = Math.max(Math.max(node.height,node.radius),node.width); | |||||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
}; | |||||
/** | |||||
* determine in which branch the node will be placed. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param skipMassUpdate | |||||
* @private | |||||
*/ | |||||
exports._placeInTree = function(parentBranch,node,skipMassUpdate) { | |||||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
// update the mass of the branch. | |||||
this._updateBranchMass(parentBranch,node); | |||||
} | |||||
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||||
this._placeInRegion(parentBranch,node,"NW"); | |||||
} | |||||
else { // in SW | |||||
this._placeInRegion(parentBranch,node,"SW"); | |||||
} | |||||
} | |||||
else { // in NE or SE | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
this._placeInRegion(parentBranch,node,"NE"); | |||||
} | |||||
else { // in SE | |||||
this._placeInRegion(parentBranch,node,"SE"); | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* actually place the node in a region (or branch) | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param region | |||||
* @private | |||||
*/ | |||||
exports._placeInRegion = function(parentBranch,node,region) { | |||||
switch (parentBranch.children[region].childrenCount) { | |||||
case 0: // place node here | |||||
parentBranch.children[region].children.data = node; | |||||
parentBranch.children[region].childrenCount = 1; | |||||
this._updateBranchMass(parentBranch.children[region],node); | |||||
break; | |||||
case 1: // convert into children | |||||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
// we move one node a pixel and we do not put it in the tree. | |||||
if (parentBranch.children[region].children.data.x == node.x && | |||||
parentBranch.children[region].children.data.y == node.y) { | |||||
node.x += Math.random(); | |||||
node.y += Math.random(); | |||||
} | |||||
else { | |||||
this._splitBranch(parentBranch.children[region]); | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
} | |||||
break; | |||||
case 4: // place in branch | |||||
this._placeInTree(parentBranch.children[region],node); | |||||
break; | |||||
} | |||||
}; | |||||
/** | |||||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
* after the split is complete. | |||||
* | |||||
* @param parentBranch | |||||
* @private | |||||
*/ | |||||
exports._splitBranch = function(parentBranch) { | |||||
// if the branch is shaded with a node, replace the node in the new subset. | |||||
var containedNode = null; | |||||
if (parentBranch.childrenCount == 1) { | |||||
containedNode = parentBranch.children.data; | |||||
parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0; | |||||
} | |||||
parentBranch.childrenCount = 4; | |||||
parentBranch.children.data = null; | |||||
this._insertRegion(parentBranch,"NW"); | |||||
this._insertRegion(parentBranch,"NE"); | |||||
this._insertRegion(parentBranch,"SW"); | |||||
this._insertRegion(parentBranch,"SE"); | |||||
if (containedNode != null) { | |||||
this._placeInTree(parentBranch,containedNode); | |||||
} | |||||
}; | |||||
/** | |||||
* This function subdivides the region into four new segments. | |||||
* Specifically, this inserts a single new segment. | |||||
* It fills the children section of the parentBranch | |||||
* | |||||
* @param parentBranch | |||||
* @param region | |||||
* @param parentRange | |||||
* @private | |||||
*/ | |||||
exports._insertRegion = function(parentBranch, region) { | |||||
var minX,maxX,minY,maxY; | |||||
var childSize = 0.5 * parentBranch.size; | |||||
switch (region) { | |||||
case "NW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "NE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "SW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
case "SE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
} | |||||
parentBranch.children[region] = { | |||||
centerOfMass:{x:0,y:0}, | |||||
mass:0, | |||||
range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY}, | |||||
size: 0.5 * parentBranch.size, | |||||
calcSize: 2 * parentBranch.calcSize, | |||||
children: {data:null}, | |||||
maxWidth: 0, | |||||
level: parentBranch.level+1, | |||||
childrenCount: 0 | |||||
}; | |||||
}; | |||||
/** | |||||
* This function is for debugging purposed, it draws the tree. | |||||
* | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
exports._drawTree = function(ctx,color) { | |||||
if (this.barnesHutTree !== undefined) { | |||||
ctx.lineWidth = 1; | |||||
this._drawBranch(this.barnesHutTree.root,ctx,color); | |||||
} | |||||
}; | |||||
/** | |||||
* This function is for debugging purposes. It draws the branches recursively. | |||||
* | |||||
* @param branch | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
exports._drawBranch = function(branch,ctx,color) { | |||||
if (color === undefined) { | |||||
color = "#FF0000"; | |||||
} | |||||
if (branch.childrenCount == 4) { | |||||
this._drawBranch(branch.children.NW,ctx); | |||||
this._drawBranch(branch.children.NE,ctx); | |||||
this._drawBranch(branch.children.SE,ctx); | |||||
this._drawBranch(branch.children.SW,ctx); | |||||
} | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.minY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX,branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX,branch.range.minY); | |||||
ctx.stroke(); | |||||
/* | |||||
if (branch.mass > 0) { | |||||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
ctx.stroke(); | |||||
} | |||||
*/ | |||||
}; |
@ -1,154 +0,0 @@ | |||||
/** | |||||
* Calculate the forces the nodes apply on eachother based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function () { | |||||
var dx, dy, distance, fx, fy, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.hierarchicalRepulsion.nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
// nodes only affect nodes on their level | |||||
if (node1.level == node2.level) { | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
var steepness = 0.05; | |||||
if (distance < nodeDistance) { | |||||
repulsingForce = -Math.pow(steepness*distance,2) + Math.pow(steepness*nodeDistance,2); | |||||
} | |||||
else { | |||||
repulsingForce = 0; | |||||
} | |||||
// normalize force with | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
else { | |||||
repulsingForce = repulsingForce / distance; | |||||
} | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateHierarchicalSpringForces = function () { | |||||
var edgeLength, edge, edgeId; | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
var edges = this.edges; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
for (var i = 0; i < nodeIndices.length; i++) { | |||||
var node1 = nodes[nodeIndices[i]]; | |||||
node1.springFx = 0; | |||||
node1.springFy = 0; | |||||
} | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
edgeLength = edge.physics.springLength; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
if (edge.to.level != edge.from.level) { | |||||
edge.to.springFx -= fx; | |||||
edge.to.springFy -= fy; | |||||
edge.from.springFx += fx; | |||||
edge.from.springFy += fy; | |||||
} | |||||
else { | |||||
var factor = 0.5; | |||||
edge.to.fx -= factor*fx; | |||||
edge.to.fy -= factor*fy; | |||||
edge.from.fx += factor*fx; | |||||
edge.from.fy += factor*fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// normalize spring forces | |||||
var springForce = 1; | |||||
var springFx, springFy; | |||||
for (i = 0; i < nodeIndices.length; i++) { | |||||
var node = nodes[nodeIndices[i]]; | |||||
springFx = Math.min(springForce,Math.max(-springForce,node.springFx)); | |||||
springFy = Math.min(springForce,Math.max(-springForce,node.springFy)); | |||||
node.fx += springFx; | |||||
node.fy += springFy; | |||||
} | |||||
// retain energy balance | |||||
var totalFx = 0; | |||||
var totalFy = 0; | |||||
for (i = 0; i < nodeIndices.length; i++) { | |||||
var node = nodes[nodeIndices[i]]; | |||||
totalFx += node.fx; | |||||
totalFy += node.fy; | |||||
} | |||||
var correctionFx = totalFx / nodeIndices.length; | |||||
var correctionFy = totalFy / nodeIndices.length; | |||||
for (i = 0; i < nodeIndices.length; i++) { | |||||
var node = nodes[nodeIndices[i]]; | |||||
node.fx -= correctionFx; | |||||
node.fy -= correctionFy; | |||||
} | |||||
}; |
@ -1,724 +0,0 @@ | |||||
var util = require('../../../util'); | |||||
var RepulsionMixin = require('./RepulsionMixin'); | |||||
var HierarchialRepulsionMixin = require('./HierarchialRepulsionMixin'); | |||||
var BarnesHutMixin = require('./BarnesHutMixin'); | |||||
/** | |||||
* Toggling barnes Hut calculation on and off. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._toggleBarnesHut = function () { | |||||
this.constants.physics.barnesHut.enabled = !this.constants.physics.barnesHut.enabled; | |||||
this._loadSelectedForceSolver(); | |||||
this.moving = true; | |||||
this.start(); | |||||
}; | |||||
/** | |||||
* This loads the node force solver based on the barnes hut or repulsion algorithm | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._loadSelectedForceSolver = function () { | |||||
// this overloads the this._calculateNodeForces | |||||
if (this.constants.physics.barnesHut.enabled == true) { | |||||
this._clearMixin(RepulsionMixin); | |||||
this._clearMixin(HierarchialRepulsionMixin); | |||||
this.constants.physics.centralGravity = this.constants.physics.barnesHut.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.barnesHut.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.barnesHut.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.barnesHut.damping; | |||||
this._loadMixin(BarnesHutMixin); | |||||
} | |||||
else if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
this._clearMixin(BarnesHutMixin); | |||||
this._clearMixin(RepulsionMixin); | |||||
this.constants.physics.centralGravity = this.constants.physics.hierarchicalRepulsion.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.hierarchicalRepulsion.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.hierarchicalRepulsion.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.hierarchicalRepulsion.damping; | |||||
this._loadMixin(HierarchialRepulsionMixin); | |||||
} | |||||
else { | |||||
this._clearMixin(BarnesHutMixin); | |||||
this._clearMixin(HierarchialRepulsionMixin); | |||||
this.barnesHutTree = undefined; | |||||
this.constants.physics.centralGravity = this.constants.physics.repulsion.centralGravity; | |||||
this.constants.physics.springLength = this.constants.physics.repulsion.springLength; | |||||
this.constants.physics.springConstant = this.constants.physics.repulsion.springConstant; | |||||
this.constants.physics.damping = this.constants.physics.repulsion.damping; | |||||
this._loadMixin(RepulsionMixin); | |||||
} | |||||
}; | |||||
/** | |||||
* Before calculating the forces, we check if we need to cluster to keep up performance and we check | |||||
* if there is more than one node. If it is just one node, we dont calculate anything. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._initializeForceCalculation = function () { | |||||
// stop calculation if there is only one node | |||||
if (this.nodeIndices.length == 1) { | |||||
this.nodes[this.nodeIndices[0]]._setForce(0, 0); | |||||
} | |||||
else { | |||||
// if there are too many nodes on screen, we cluster without repositioning | |||||
if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) { | |||||
this.clusterToFit(this.constants.clustering.reduceToNodes, false); | |||||
} | |||||
// we now start the force calculation | |||||
this._calculateForces(); | |||||
} | |||||
}; | |||||
/** | |||||
* Calculate the external forces acting on the nodes | |||||
* Forces are caused by: edges, repulsing forces between nodes, gravity | |||||
* @private | |||||
*/ | |||||
exports._calculateForces = function () { | |||||
// Gravity is required to keep separated groups from floating off | |||||
// the forces are reset to zero in this loop by using _setForce instead | |||||
// of _addForce | |||||
this._calculateGravitationalForces(); | |||||
this._calculateNodeForces(); | |||||
if (this.constants.physics.springConstant > 0) { | |||||
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { | |||||
this._calculateSpringForcesWithSupport(); | |||||
} | |||||
else { | |||||
if (this.constants.physics.hierarchicalRepulsion.enabled == true) { | |||||
this._calculateHierarchicalSpringForces(); | |||||
} | |||||
else { | |||||
this._calculateSpringForces(); | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
* handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
* This function joins the datanodes and invisible (called support) nodes into one object. | |||||
* We do this so we do not contaminate this.nodes with the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._updateCalculationNodes = function () { | |||||
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) { | |||||
this.calculationNodes = {}; | |||||
this.calculationNodeIndices = []; | |||||
for (var nodeId in this.nodes) { | |||||
if (this.nodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId] = this.nodes[nodeId]; | |||||
} | |||||
} | |||||
var supportNodes = this.sectors['support']['nodes']; | |||||
for (var supportNodeId in supportNodes) { | |||||
if (supportNodes.hasOwnProperty(supportNodeId)) { | |||||
if (this.edges.hasOwnProperty(supportNodes[supportNodeId].parentEdgeId)) { | |||||
this.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
} | |||||
else { | |||||
supportNodes[supportNodeId]._setForce(0, 0); | |||||
} | |||||
} | |||||
} | |||||
for (var idx in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(idx)) { | |||||
this.calculationNodeIndices.push(idx); | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
this.calculationNodes = this.nodes; | |||||
this.calculationNodeIndices = this.nodeIndices; | |||||
} | |||||
}; | |||||
/** | |||||
* this function applies the central gravity effect to keep groups from floating off | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateGravitationalForces = function () { | |||||
var dx, dy, distance, node, i; | |||||
var nodes = this.calculationNodes; | |||||
var gravity = this.constants.physics.centralGravity; | |||||
var gravityForce = 0; | |||||
for (i = 0; i < this.calculationNodeIndices.length; i++) { | |||||
node = nodes[this.calculationNodeIndices[i]]; | |||||
node.damping = this.constants.physics.damping; // possibly add function to alter damping properties of clusters. | |||||
// gravity does not apply when we are in a pocket sector | |||||
if (this._sector() == "default" && gravity != 0) { | |||||
dx = -node.x; | |||||
dy = -node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
node.fx = dx * gravityForce; | |||||
node.fy = dy * gravityForce; | |||||
} | |||||
else { | |||||
node.fx = 0; | |||||
node.fy = 0; | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* this function calculates the effects of the springs in the case of unsmooth curves. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForces = function () { | |||||
var edgeLength, edge, edgeId; | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
edgeLength = edge.physics.springLength; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
edge.from.fx += fx; | |||||
edge.from.fy += fy; | |||||
edge.to.fx -= fx; | |||||
edge.to.fy -= fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForcesWithSupport = function () { | |||||
var edgeLength, edge, edgeId, combinedClusterSize; | |||||
var edges = this.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) { | |||||
if (edge.via != null) { | |||||
var node1 = edge.to; | |||||
var node2 = edge.via; | |||||
var node3 = edge.from; | |||||
edgeLength = edge.physics.springLength; | |||||
combinedClusterSize = node1.clusterSize + node3.clusterSize - 2; | |||||
// this implies that the edges between big clusters are longer | |||||
edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth; | |||||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
}; | |||||
/** | |||||
* This is the code actually performing the calculation for the function above. It is split out to avoid repetition. | |||||
* | |||||
* @param node1 | |||||
* @param node2 | |||||
* @param edgeLength | |||||
* @private | |||||
*/ | |||||
exports._calculateSpringForce = function (node1, node2, edgeLength) { | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
dx = (node1.x - node2.x); | |||||
dy = (node1.y - node2.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.constants.physics.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
node1.fx += fx; | |||||
node1.fy += fy; | |||||
node2.fx -= fx; | |||||
node2.fy -= fy; | |||||
}; | |||||
exports._cleanupPhysicsConfiguration = function() { | |||||
if (this.physicsConfiguration !== undefined) { | |||||
while (this.physicsConfiguration.hasChildNodes()) { | |||||
this.physicsConfiguration.removeChild(this.physicsConfiguration.firstChild); | |||||
} | |||||
this.physicsConfiguration.parentNode.removeChild(this.physicsConfiguration); | |||||
this.physicsConfiguration = undefined; | |||||
} | |||||
} | |||||
/** | |||||
* Load the HTML for the physics config and bind it | |||||
* @private | |||||
*/ | |||||
exports._loadPhysicsConfiguration = function () { | |||||
if (this.physicsConfiguration === undefined) { | |||||
this.backupConstants = {}; | |||||
util.deepExtend(this.backupConstants,this.constants); | |||||
var maxGravitational = Math.max(20000, (-1 * this.constants.physics.barnesHut.gravitationalConstant) * 10); | |||||
var maxSpring = Math.min(0.05, this.constants.physics.barnesHut.springConstant * 10) | |||||
var hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; | |||||
this.physicsConfiguration = document.createElement('div'); | |||||
this.physicsConfiguration.className = "PhysicsConfiguration"; | |||||
this.physicsConfiguration.innerHTML = '' + | |||||
'<table><tr><td><b>Simulation Mode:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod1" value="BH" checked="checked">Barnes Hut</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod2" value="R">Repulsion</td>' + | |||||
'<td width="120px"><input type="radio" name="graph_physicsMethod" id="graph_physicsMethod3" value="H">Hierarchical</td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_BH_table" style="display:none">' + | |||||
'<tr><td><b>Barnes Hut</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">gravitationalConstant</td><td>0</td><td><input type="range" min="0" max="'+maxGravitational+'" value="' + (-1 * this.constants.physics.barnesHut.gravitationalConstant) + '" step="25" style="width:300px" id="graph_BH_gc"></td><td width="50px">-'+maxGravitational+'</td><td><input value="' + (this.constants.physics.barnesHut.gravitationalConstant) + '" id="graph_BH_gc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="6" value="' + this.constants.physics.barnesHut.centralGravity + '" step="0.05" style="width:300px" id="graph_BH_cg"></td><td>3</td><td><input value="' + this.constants.physics.barnesHut.centralGravity + '" id="graph_BH_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.barnesHut.springLength + '" step="1" style="width:300px" id="graph_BH_sl"></td><td>500</td><td><input value="' + this.constants.physics.barnesHut.springLength + '" id="graph_BH_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="'+maxSpring+'" value="' + this.constants.physics.barnesHut.springConstant + '" step="0.0001" style="width:300px" id="graph_BH_sc"></td><td>'+maxSpring+'</td><td><input value="' + this.constants.physics.barnesHut.springConstant + '" id="graph_BH_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.barnesHut.damping + '" step="0.005" style="width:300px" id="graph_BH_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.barnesHut.damping + '" id="graph_BH_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_R_table" style="display:none">' + | |||||
'<tr><td><b>Repulsion</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.repulsion.nodeDistance + '" step="1" style="width:300px" id="graph_R_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.repulsion.nodeDistance + '" id="graph_R_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.repulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_R_cg"></td><td>3</td><td><input value="' + this.constants.physics.repulsion.centralGravity + '" id="graph_R_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.repulsion.springLength + '" step="1" style="width:300px" id="graph_R_sl"></td><td>500</td><td><input value="' + this.constants.physics.repulsion.springLength + '" id="graph_R_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.repulsion.springConstant + '" step="0.001" style="width:300px" id="graph_R_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.repulsion.springConstant + '" id="graph_R_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.repulsion.damping + '" step="0.005" style="width:300px" id="graph_R_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.repulsion.damping + '" id="graph_R_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table id="graph_H_table" style="display:none">' + | |||||
'<tr><td width="150"><b>Hierarchical</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeDistance</td><td>0</td><td><input type="range" min="0" max="300" value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" step="1" style="width:300px" id="graph_H_nd"></td><td width="50px">300</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.nodeDistance + '" id="graph_H_nd_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">centralGravity</td><td>0</td><td><input type="range" min="0" max="3" value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" step="0.05" style="width:300px" id="graph_H_cg"></td><td>3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.centralGravity + '" id="graph_H_cg_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springLength</td><td>0</td><td><input type="range" min="0" max="500" value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" step="1" style="width:300px" id="graph_H_sl"></td><td>500</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springLength + '" id="graph_H_sl_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">springConstant</td><td>0</td><td><input type="range" min="0" max="0.5" value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" step="0.001" style="width:300px" id="graph_H_sc"></td><td>0.5</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.springConstant + '" id="graph_H_sc_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">damping</td><td>0</td><td><input type="range" min="0" max="0.3" value="' + this.constants.physics.hierarchicalRepulsion.damping + '" step="0.005" style="width:300px" id="graph_H_damp"></td><td>0.3</td><td><input value="' + this.constants.physics.hierarchicalRepulsion.damping + '" id="graph_H_damp_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">direction</td><td>1</td><td><input type="range" min="0" max="3" value="' + hierarchicalLayoutDirections.indexOf(this.constants.hierarchicalLayout.direction) + '" step="1" style="width:300px" id="graph_H_direction"></td><td>4</td><td><input value="' + this.constants.hierarchicalLayout.direction + '" id="graph_H_direction_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">levelSeparation</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.levelSeparation + '" step="1" style="width:300px" id="graph_H_levsep"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.levelSeparation + '" id="graph_H_levsep_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'<tr>' + | |||||
'<td width="150px">nodeSpacing</td><td>1</td><td><input type="range" min="0" max="500" value="' + this.constants.hierarchicalLayout.nodeSpacing + '" step="1" style="width:300px" id="graph_H_nspac"></td><td>500</td><td><input value="' + this.constants.hierarchicalLayout.nodeSpacing + '" id="graph_H_nspac_value" style="width:60px"></td>' + | |||||
'</tr>' + | |||||
'</table>' + | |||||
'<table><tr><td><b>Options:</b></td></tr>' + | |||||
'<tr>' + | |||||
'<td width="180px"><input type="button" id="graph_toggleSmooth" value="Toggle smoothCurves" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_repositionNodes" value="Reinitialize" style="width:150px"></td>' + | |||||
'<td width="180px"><input type="button" id="graph_generateOptions" value="Generate Options" style="width:150px"></td>' + | |||||
'</tr>' + | |||||
'</table>' | |||||
this.containerElement.parentElement.insertBefore(this.physicsConfiguration, this.containerElement); | |||||
this.optionsDiv = document.createElement("div"); | |||||
this.optionsDiv.style.fontSize = "14px"; | |||||
this.optionsDiv.style.fontFamily = "verdana"; | |||||
this.containerElement.parentElement.insertBefore(this.optionsDiv, this.containerElement); | |||||
var rangeElement; | |||||
rangeElement = document.getElementById('graph_BH_gc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_gc', -1, "physics_barnesHut_gravitationalConstant"); | |||||
rangeElement = document.getElementById('graph_BH_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_BH_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_BH_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_BH_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_BH_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_R_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_nd', 1, "physics_repulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_R_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_R_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_R_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_R_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_R_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_nd'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
rangeElement = document.getElementById('graph_H_cg'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
rangeElement = document.getElementById('graph_H_sc'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
rangeElement = document.getElementById('graph_H_sl'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
rangeElement = document.getElementById('graph_H_damp'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_damp', 1, "physics_damping"); | |||||
rangeElement = document.getElementById('graph_H_direction'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_direction', hierarchicalLayoutDirections, "hierarchicalLayout_direction"); | |||||
rangeElement = document.getElementById('graph_H_levsep'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_levsep', 1, "hierarchicalLayout_levelSeparation"); | |||||
rangeElement = document.getElementById('graph_H_nspac'); | |||||
rangeElement.onchange = showValueOfRange.bind(this, 'graph_H_nspac', 1, "hierarchicalLayout_nodeSpacing"); | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
var radioButton3 = document.getElementById("graph_physicsMethod3"); | |||||
radioButton2.checked = true; | |||||
if (this.constants.physics.barnesHut.enabled) { | |||||
radioButton1.checked = true; | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled) { | |||||
radioButton3.checked = true; | |||||
} | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
var graph_repositionNodes = document.getElementById("graph_repositionNodes"); | |||||
var graph_generateOptions = document.getElementById("graph_generateOptions"); | |||||
graph_toggleSmooth.onclick = graphToggleSmoothCurves.bind(this); | |||||
graph_repositionNodes.onclick = graphRepositionNodes.bind(this); | |||||
graph_generateOptions.onclick = graphGenerateOptions.bind(this); | |||||
if (this.constants.smoothCurves == true && this.constants.dynamicSmoothCurves == false) { | |||||
graph_toggleSmooth.style.background = "#A4FF56"; | |||||
} | |||||
else { | |||||
graph_toggleSmooth.style.background = "#FF8532"; | |||||
} | |||||
switchConfigurations.apply(this); | |||||
radioButton1.onchange = switchConfigurations.bind(this); | |||||
radioButton2.onchange = switchConfigurations.bind(this); | |||||
radioButton3.onchange = switchConfigurations.bind(this); | |||||
} | |||||
}; | |||||
/** | |||||
* This overwrites the this.constants. | |||||
* | |||||
* @param constantsVariableName | |||||
* @param value | |||||
* @private | |||||
*/ | |||||
exports._overWriteGraphConstants = function (constantsVariableName, value) { | |||||
var nameArray = constantsVariableName.split("_"); | |||||
if (nameArray.length == 1) { | |||||
this.constants[nameArray[0]] = value; | |||||
} | |||||
else if (nameArray.length == 2) { | |||||
this.constants[nameArray[0]][nameArray[1]] = value; | |||||
} | |||||
else if (nameArray.length == 3) { | |||||
this.constants[nameArray[0]][nameArray[1]][nameArray[2]] = value; | |||||
} | |||||
}; | |||||
/** | |||||
* this function is bound to the toggle smooth curves button. That is also why it is not in the prototype. | |||||
*/ | |||||
function graphToggleSmoothCurves () { | |||||
this.constants.smoothCurves.enabled = !this.constants.smoothCurves.enabled; | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this._configureSmoothCurves(false); | |||||
} | |||||
/** | |||||
* this function is used to scramble the nodes | |||||
* | |||||
*/ | |||||
function graphRepositionNodes () { | |||||
for (var nodeId in this.calculationNodes) { | |||||
if (this.calculationNodes.hasOwnProperty(nodeId)) { | |||||
this.calculationNodes[nodeId].vx = 0; this.calculationNodes[nodeId].vy = 0; | |||||
this.calculationNodes[nodeId].fx = 0; this.calculationNodes[nodeId].fy = 0; | |||||
} | |||||
} | |||||
if (this.constants.hierarchicalLayout.enabled == true) { | |||||
this._setupHierarchicalLayout(); | |||||
showValueOfRange.call(this, 'graph_H_nd', 1, "physics_hierarchicalRepulsion_nodeDistance"); | |||||
showValueOfRange.call(this, 'graph_H_cg', 1, "physics_centralGravity"); | |||||
showValueOfRange.call(this, 'graph_H_sc', 1, "physics_springConstant"); | |||||
showValueOfRange.call(this, 'graph_H_sl', 1, "physics_springLength"); | |||||
showValueOfRange.call(this, 'graph_H_damp', 1, "physics_damping"); | |||||
} | |||||
else { | |||||
this.repositionNodes(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
/** | |||||
* this is used to generate an options file from the playing with physics system. | |||||
*/ | |||||
function graphGenerateOptions () { | |||||
var options = "No options are required, default values used."; | |||||
var optionsSpecific = []; | |||||
var radioButton1 = document.getElementById("graph_physicsMethod1"); | |||||
var radioButton2 = document.getElementById("graph_physicsMethod2"); | |||||
if (radioButton1.checked == true) { | |||||
if (this.constants.physics.barnesHut.gravitationalConstant != this.backupConstants.physics.barnesHut.gravitationalConstant) {optionsSpecific.push("gravitationalConstant: " + this.constants.physics.barnesHut.gravitationalConstant);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.barnesHut.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.barnesHut.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.barnesHut.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.barnesHut.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (this.constants.smoothCurves.enabled != this.backupConstants.smoothCurves.enabled) { | |||||
if (optionsSpecific.length == 0) {options = "var options = {";} | |||||
else {options += ", "} | |||||
options += "smoothCurves: " + this.constants.smoothCurves.enabled; | |||||
} | |||||
if (options != "No options are required, default values used.") { | |||||
options += '};' | |||||
} | |||||
} | |||||
else if (radioButton2.checked == true) { | |||||
options = "var options = {"; | |||||
options += "physics: {barnesHut: {enabled: false}"; | |||||
if (this.constants.physics.repulsion.nodeDistance != this.backupConstants.physics.repulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.repulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.repulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.repulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.repulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.repulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += ", repulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}}' | |||||
} | |||||
if (optionsSpecific.length == 0) {options += "}"} | |||||
if (this.constants.smoothCurves != this.backupConstants.smoothCurves) { | |||||
options += ", smoothCurves: " + this.constants.smoothCurves; | |||||
} | |||||
options += '};' | |||||
} | |||||
else { | |||||
options = "var options = {"; | |||||
if (this.constants.physics.hierarchicalRepulsion.nodeDistance != this.backupConstants.physics.hierarchicalRepulsion.nodeDistance) {optionsSpecific.push("nodeDistance: " + this.constants.physics.hierarchicalRepulsion.nodeDistance);} | |||||
if (this.constants.physics.centralGravity != this.backupConstants.physics.hierarchicalRepulsion.centralGravity) {optionsSpecific.push("centralGravity: " + this.constants.physics.centralGravity);} | |||||
if (this.constants.physics.springLength != this.backupConstants.physics.hierarchicalRepulsion.springLength) {optionsSpecific.push("springLength: " + this.constants.physics.springLength);} | |||||
if (this.constants.physics.springConstant != this.backupConstants.physics.hierarchicalRepulsion.springConstant) {optionsSpecific.push("springConstant: " + this.constants.physics.springConstant);} | |||||
if (this.constants.physics.damping != this.backupConstants.physics.hierarchicalRepulsion.damping) {optionsSpecific.push("damping: " + this.constants.physics.damping);} | |||||
if (optionsSpecific.length != 0) { | |||||
options += "physics: {hierarchicalRepulsion: {"; | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", "; | |||||
} | |||||
} | |||||
options += '}},'; | |||||
} | |||||
options += 'hierarchicalLayout: {'; | |||||
optionsSpecific = []; | |||||
if (this.constants.hierarchicalLayout.direction != this.backupConstants.hierarchicalLayout.direction) {optionsSpecific.push("direction: " + this.constants.hierarchicalLayout.direction);} | |||||
if (Math.abs(this.constants.hierarchicalLayout.levelSeparation) != this.backupConstants.hierarchicalLayout.levelSeparation) {optionsSpecific.push("levelSeparation: " + this.constants.hierarchicalLayout.levelSeparation);} | |||||
if (this.constants.hierarchicalLayout.nodeSpacing != this.backupConstants.hierarchicalLayout.nodeSpacing) {optionsSpecific.push("nodeSpacing: " + this.constants.hierarchicalLayout.nodeSpacing);} | |||||
if (optionsSpecific.length != 0) { | |||||
for (var i = 0; i < optionsSpecific.length; i++) { | |||||
options += optionsSpecific[i]; | |||||
if (i < optionsSpecific.length - 1) { | |||||
options += ", " | |||||
} | |||||
} | |||||
options += '}' | |||||
} | |||||
else { | |||||
options += "enabled:true}"; | |||||
} | |||||
options += '};' | |||||
} | |||||
this.optionsDiv.innerHTML = options; | |||||
} | |||||
/** | |||||
* this is used to switch between barnesHut, repulsion and hierarchical. | |||||
* | |||||
*/ | |||||
function switchConfigurations () { | |||||
var ids = ["graph_BH_table", "graph_R_table", "graph_H_table"]; | |||||
var radioButton = document.querySelector('input[name="graph_physicsMethod"]:checked').value; | |||||
var tableId = "graph_" + radioButton + "_table"; | |||||
var table = document.getElementById(tableId); | |||||
table.style.display = "block"; | |||||
for (var i = 0; i < ids.length; i++) { | |||||
if (ids[i] != tableId) { | |||||
table = document.getElementById(ids[i]); | |||||
table.style.display = "none"; | |||||
} | |||||
} | |||||
this._restoreNodes(); | |||||
if (radioButton == "R") { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
} | |||||
else if (radioButton == "H") { | |||||
if (this.constants.hierarchicalLayout.enabled == false) { | |||||
this.constants.hierarchicalLayout.enabled = true; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = true; | |||||
this.constants.physics.barnesHut.enabled = false; | |||||
this.constants.smoothCurves.enabled = false; | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
} | |||||
else { | |||||
this.constants.hierarchicalLayout.enabled = false; | |||||
this.constants.physics.hierarchicalRepulsion.enabled = false; | |||||
this.constants.physics.barnesHut.enabled = true; | |||||
} | |||||
this._loadSelectedForceSolver(); | |||||
var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); | |||||
if (this.constants.smoothCurves.enabled == true) {graph_toggleSmooth.style.background = "#A4FF56";} | |||||
else {graph_toggleSmooth.style.background = "#FF8532";} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
/** | |||||
* this generates the ranges depending on the iniital values. | |||||
* | |||||
* @param id | |||||
* @param map | |||||
* @param constantsVariableName | |||||
*/ | |||||
function showValueOfRange (id,map,constantsVariableName) { | |||||
var valueId = id + "_value"; | |||||
var rangeValue = document.getElementById(id).value; | |||||
if (Array.isArray(map)) { | |||||
document.getElementById(valueId).value = map[parseInt(rangeValue)]; | |||||
this._overWriteGraphConstants(constantsVariableName,map[parseInt(rangeValue)]); | |||||
} | |||||
else { | |||||
document.getElementById(valueId).value = parseInt(map) * parseFloat(rangeValue); | |||||
this._overWriteGraphConstants(constantsVariableName, parseInt(map) * parseFloat(rangeValue)); | |||||
} | |||||
if (constantsVariableName == "hierarchicalLayout_direction" || | |||||
constantsVariableName == "hierarchicalLayout_levelSeparation" || | |||||
constantsVariableName == "hierarchicalLayout_nodeSpacing") { | |||||
this._setupHierarchicalLayout(); | |||||
} | |||||
this.moving = true; | |||||
this.start(); | |||||
} | |||||
@ -1,64 +0,0 @@ | |||||
/** | |||||
* Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
exports._calculateNodeForces = function () { | |||||
var dx, dy, angle, distance, fx, fy, combinedClusterSize, | |||||
repulsingForce, node1, node2, i, j; | |||||
var nodes = this.calculationNodes; | |||||
var nodeIndices = this.calculationNodeIndices; | |||||
// approximation constants | |||||
var a_base = -2 / 3; | |||||
var b = 4 / 3; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.constants.physics.repulsion.nodeDistance; | |||||
var minimumDistance = nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
combinedClusterSize = node1.clusterSize + node2.clusterSize - 2; | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// same condition as BarnesHut, making sure nodes are never 100% overlapping. | |||||
if (distance == 0) { | |||||
distance = 0.1*Math.random(); | |||||
dx = distance; | |||||
} | |||||
minimumDistance = (combinedClusterSize == 0) ? nodeDistance : (nodeDistance * (1 + combinedClusterSize * this.constants.clustering.distanceAmplification)); | |||||
var a = a_base / minimumDistance; | |||||
if (distance < 2 * minimumDistance) { | |||||
if (distance < 0.5 * minimumDistance) { | |||||
repulsingForce = 1.0; | |||||
} | |||||
else { | |||||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)) | |||||
} | |||||
// amplify the repulsion for clusters. | |||||
repulsingForce *= (combinedClusterSize == 0) ? 1 : 1 + combinedClusterSize * this.constants.clustering.forceAmplification; | |||||
repulsingForce = repulsingForce / Math.max(distance,0.01*minimumDistance); | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
node1.fx -= fx; | |||||
node1.fy -= fy; | |||||
node2.fx += fx; | |||||
node2.fy += fy; | |||||
} | |||||
} | |||||
} | |||||
}; |
@ -0,0 +1,229 @@ | |||||
/** | |||||
* Created by Alex on 26-Feb-15. | |||||
*/ | |||||
var Hammer = require('../../module/hammer'); | |||||
class Canvas { | |||||
/** | |||||
* Create the main frame for the Network. | |||||
* This function is executed once when a Network object is created. The frame | |||||
* contains a canvas, and this canvas contains all objects like the axis and | |||||
* nodes. | |||||
* @private | |||||
*/ | |||||
constructor(body, options) { | |||||
this.body = body; | |||||
this.setOptions(options); | |||||
this.translation = {x: 0, y: 0}; | |||||
this.scale = 1.0; | |||||
this.body.emitter.on("_setScale", (scale) => {this.scale = scale}); | |||||
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||||
this.body.emitter.once("resize", (obj) => {this.translation.x = obj.width * 0.5; this.translation.y = obj.height * 0.5; this.body.emitter.emit("_setTranslation", this.translation)}); | |||||
this.pixelRatio = 1; | |||||
// remove all elements from the container element. | |||||
while (this.body.container.hasChildNodes()) { | |||||
this.body.container.removeChild(this.body.container.firstChild); | |||||
} | |||||
this.frame = document.createElement('div'); | |||||
this.frame.className = 'vis network-frame'; | |||||
this.frame.style.position = 'relative'; | |||||
this.frame.style.overflow = 'hidden'; | |||||
this.frame.tabIndex = 900; | |||||
////////////////////////////////////////////////////////////////// | |||||
this.frame.canvas = document.createElement("canvas"); | |||||
this.frame.canvas.style.position = 'relative'; | |||||
this.frame.appendChild(this.frame.canvas); | |||||
if (!this.frame.canvas.getContext) { | |||||
var noCanvas = document.createElement( 'DIV' ); | |||||
noCanvas.style.color = 'red'; | |||||
noCanvas.style.fontWeight = 'bold' ; | |||||
noCanvas.style.padding = '10px'; | |||||
noCanvas.innerHTML = 'Error: your browser does not support HTML canvas'; | |||||
this.frame.canvas.appendChild(noCanvas); | |||||
} | |||||
else { | |||||
var ctx = this.frame.canvas.getContext("2d"); | |||||
this.pixelRatio = (window.devicePixelRatio || 1) / (ctx.webkitBackingStorePixelRatio || | |||||
ctx.mozBackingStorePixelRatio || | |||||
ctx.msBackingStorePixelRatio || | |||||
ctx.oBackingStorePixelRatio || | |||||
ctx.backingStorePixelRatio || 1); | |||||
//this.pixelRatio = Math.max(1,this.pixelRatio); // this is to account for browser zooming out. The pixel ratio is ment to switch between 1 and 2 for HD screens. | |||||
this.frame.canvas.getContext("2d").setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); | |||||
} | |||||
// add the frame to the container element | |||||
this.body.container.appendChild(this.frame); | |||||
this.body.emitter.emit("_setScale", 1);; | |||||
this.body.emitter.emit("_setTranslation", {x: 0.5 * this.frame.canvas.clientWidth,y: 0.5 * this.frame.canvas.clientHeight});; | |||||
this._bindHammer(); | |||||
} | |||||
/** | |||||
* This function binds hammer, it can be repeated over and over due to the uniqueness check. | |||||
* @private | |||||
*/ | |||||
_bindHammer() { | |||||
var me = this; | |||||
if (this.hammer !== undefined) { | |||||
this.hammer.dispose(); | |||||
} | |||||
this.drag = {}; | |||||
this.pinch = {}; | |||||
this.hammer = Hammer(this.frame.canvas, { | |||||
prevent_default: true | |||||
}); | |||||
this.hammer.on('tap', me.body.eventListeners.onTap ); | |||||
this.hammer.on('doubletap', me.body.eventListeners.onDoubleTap ); | |||||
this.hammer.on('hold', me.body.eventListeners.onHold ); | |||||
this.hammer.on('touch', me.body.eventListeners.onTouch ); | |||||
this.hammer.on('dragstart', me.body.eventListeners.onDragStart ); | |||||
this.hammer.on('drag', me.body.eventListeners.onDrag ); | |||||
this.hammer.on('dragend', me.body.eventListeners.onDragEnd ); | |||||
if (this.options.zoomable == true) { | |||||
this.hammer.on('mousewheel', me.body.eventListeners.onMouseWheel.bind(me)); | |||||
this.hammer.on('DOMMouseScroll', me.body.eventListeners.onMouseWheel.bind(me)); // for FF | |||||
this.hammer.on('pinch', me.body.eventListeners.onPinch.bind(me) ); | |||||
} | |||||
this.hammer.on('mousemove', me.body.eventListeners.onMouseMove.bind(me) ); | |||||
this.hammerFrame = Hammer(this.frame, { | |||||
prevent_default: true | |||||
}); | |||||
this.hammerFrame.on('release', me.body.eventListeners.onRelease.bind(me) ); | |||||
} | |||||
setOptions(options = {}) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* Set a new size for the network | |||||
* @param {string} width Width in pixels or percentage (for example '800px' | |||||
* or '50%') | |||||
* @param {string} height Height in pixels or percentage (for example '400px' | |||||
* or '30%') | |||||
*/ | |||||
setSize(width, height) { | |||||
var emitEvent = false; | |||||
var oldWidth = this.frame.canvas.width; | |||||
var oldHeight = this.frame.canvas.height; | |||||
if (width != this.options.width || height != this.options.height || this.frame.style.width != width || this.frame.style.height != height) { | |||||
this.frame.style.width = width; | |||||
this.frame.style.height = height; | |||||
this.frame.canvas.style.width = '100%'; | |||||
this.frame.canvas.style.height = '100%'; | |||||
this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; | |||||
this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; | |||||
this.options.width = width; | |||||
this.options.height = height; | |||||
emitEvent = true; | |||||
} | |||||
else { | |||||
// this would adapt the width of the canvas to the width from 100% if and only if | |||||
// there is a change. | |||||
if (this.frame.canvas.width != this.frame.canvas.clientWidth * this.pixelRatio) { | |||||
this.frame.canvas.width = this.frame.canvas.clientWidth * this.pixelRatio; | |||||
emitEvent = true; | |||||
} | |||||
if (this.frame.canvas.height != this.frame.canvas.clientHeight * this.pixelRatio) { | |||||
this.frame.canvas.height = this.frame.canvas.clientHeight * this.pixelRatio; | |||||
emitEvent = true; | |||||
} | |||||
} | |||||
if (emitEvent === true) { | |||||
this.body.emitter.emit('resize', {width:this.frame.canvas.width * this.pixelRatio,height:this.frame.canvas.height * this.pixelRatio, oldWidth: oldWidth * this.pixelRatio, oldHeight: oldHeight * this.pixelRatio}); | |||||
} | |||||
}; | |||||
/** | |||||
* Convert the X coordinate in DOM-space (coordinate point in browser relative to the container div) to | |||||
* the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) | |||||
* @param {number} x | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_XconvertDOMtoCanvas(x) { | |||||
return (x - this.translation.x) / this.scale; | |||||
} | |||||
/** | |||||
* Convert the X coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to | |||||
* the X coordinate in DOM-space (coordinate point in browser relative to the container div) | |||||
* @param {number} x | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_XconvertCanvasToDOM(x) { | |||||
return x * this.scale + this.translation.x; | |||||
} | |||||
/** | |||||
* Convert the Y coordinate in DOM-space (coordinate point in browser relative to the container div) to | |||||
* the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) | |||||
* @param {number} y | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_YconvertDOMtoCanvas(y) { | |||||
return (y - this.translation.y) / this.scale; | |||||
} | |||||
/** | |||||
* Convert the Y coordinate in canvas-space (the simulation sandbox, which the camera looks upon) to | |||||
* the Y coordinate in DOM-space (coordinate point in browser relative to the container div) | |||||
* @param {number} y | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_YconvertCanvasToDOM(y) { | |||||
return y * this.scale + this.translation.y ; | |||||
} | |||||
/** | |||||
* | |||||
* @param {object} pos = {x: number, y: number} | |||||
* @returns {{x: number, y: number}} | |||||
* @constructor | |||||
*/ | |||||
canvasToDOM (pos) { | |||||
return {x: this._XconvertCanvasToDOM(pos.x), y: this._YconvertCanvasToDOM(pos.y)}; | |||||
} | |||||
/** | |||||
* | |||||
* @param {object} pos = {x: number, y: number} | |||||
* @returns {{x: number, y: number}} | |||||
* @constructor | |||||
*/ | |||||
DOMtoCanvas (pos) { | |||||
return {x: this._XconvertDOMtoCanvas(pos.x), y: this._YconvertDOMtoCanvas(pos.y)}; | |||||
} | |||||
} | |||||
export {Canvas}; |
@ -0,0 +1,246 @@ | |||||
/** | |||||
* Created by Alex on 26-Feb-15. | |||||
*/ | |||||
if (typeof window !== 'undefined') { | |||||
window.requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || | |||||
window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; | |||||
} | |||||
class CanvasRenderer { | |||||
constructor(body) { | |||||
this.body = body; | |||||
this.redrawRequested = false; | |||||
this.renderTimer = false; | |||||
this.requiresTimeout = true; | |||||
this.continueRendering = true; | |||||
this.renderRequests = 0; | |||||
this.translation = {x: 0, y: 0}; | |||||
this.scale = 1.0; | |||||
this.canvasTopLeft = {x: 0, y: 0}; | |||||
this.canvasBottomRight = {x: 0, y: 0}; | |||||
this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||||
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||||
this.body.emitter.on("_redraw", this._redraw.bind(this)); | |||||
this.body.emitter.on("_redrawHidden", this._redraw.bind(this, true)); | |||||
this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this)); | |||||
this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.continueRendering = true; this.startRendering();}); | |||||
this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.continueRendering = this.renderRequests > 0;}); | |||||
this._determineBrowserMethod(); | |||||
} | |||||
startRendering() { | |||||
if (this.continueRendering === true) { | |||||
if (!this.renderTimer) { | |||||
if (this.requiresTimeout == true) { | |||||
this.renderTimer = window.setTimeout(this.renderStep.bind(this), this.simulationInterval); // wait this.renderTimeStep milliseconds and perform the animation step function | |||||
} | |||||
else { | |||||
this.renderTimer = window.requestAnimationFrame(this.renderStep.bind(this)); // wait this.renderTimeStep milliseconds and perform the animation step function | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
} | |||||
} | |||||
renderStep() { | |||||
// reset the renderTimer so a new scheduled animation step can be set | |||||
this.renderTimer = undefined; | |||||
if (this.requiresTimeout == true) { | |||||
// this schedules a new simulation step | |||||
this.startRendering(); | |||||
} | |||||
this._redraw(); | |||||
if (this.requiresTimeout == false) { | |||||
// this schedules a new simulation step | |||||
this.startRendering(); | |||||
} | |||||
} | |||||
setCanvas(canvas) { | |||||
this.canvas = canvas; | |||||
} | |||||
/** | |||||
* Redraw the network with the current data | |||||
* chart will be resized too. | |||||
*/ | |||||
redraw() { | |||||
this.setSize(this.constants.width, this.constants.height); | |||||
this._redraw(); | |||||
} | |||||
/** | |||||
* Redraw the network with the current data | |||||
* @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over. | |||||
* @private | |||||
*/ | |||||
_requestRedraw(hidden) { | |||||
if (this.redrawRequested !== true) { | |||||
this.redrawRequested = true; | |||||
if (this.requiresTimeout === true) { | |||||
window.setTimeout(this._redraw.bind(this, hidden),0); | |||||
} | |||||
else { | |||||
window.requestAnimationFrame(this._redraw.bind(this, hidden, true)); | |||||
} | |||||
} | |||||
} | |||||
_redraw(hidden = false) { | |||||
this.body.emitter.emit("_beforeRender"); | |||||
this.redrawRequested = false; | |||||
var ctx = this.canvas.frame.canvas.getContext('2d'); | |||||
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0); | |||||
// clear the canvas | |||||
var w = this.canvas.frame.canvas.clientWidth; | |||||
var h = this.canvas.frame.canvas.clientHeight; | |||||
ctx.clearRect(0, 0, w, h); | |||||
// set scaling and translation | |||||
ctx.save(); | |||||
ctx.translate(this.translation.x, this.translation.y); | |||||
ctx.scale(this.scale, this.scale); | |||||
this.canvasTopLeft = this.canvas.DOMtoCanvas({x:0,y:0}); | |||||
this.canvasBottomRight = this.canvas.DOMtoCanvas({x:this.canvas.frame.canvas.clientWidth,y:this.canvas.frame.canvas.clientHeight}); | |||||
if (hidden === false) { | |||||
// todo: solve this | |||||
//if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) { | |||||
this._drawEdges(ctx); | |||||
//} | |||||
} | |||||
// todo: solve this | |||||
//if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideNodesOnDrag == false) { | |||||
this._drawNodes(ctx, this.body.nodes, hidden); | |||||
//} | |||||
if (hidden === false) { | |||||
if (this.controlNodesActive == true) { | |||||
this._drawControlNodes(ctx); | |||||
} | |||||
} | |||||
//this._drawNodes(ctx,this.body.supportNodes,true); | |||||
// this.physics.nodesSolver._debug(ctx,"#F00F0F"); | |||||
// restore original scaling and translation | |||||
ctx.restore(); | |||||
if (hidden === true) { | |||||
ctx.clearRect(0, 0, w, h); | |||||
} | |||||
} | |||||
/** | |||||
* Redraw all nodes | |||||
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||||
* @param {CanvasRenderingContext2D} ctx | |||||
* @param {Boolean} [alwaysShow] | |||||
* @private | |||||
*/ | |||||
_drawNodes(ctx,nodes,alwaysShow = false) { | |||||
// first draw the unselected nodes | |||||
var selected = []; | |||||
for (var id in nodes) { | |||||
if (nodes.hasOwnProperty(id)) { | |||||
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight); | |||||
if (nodes[id].isSelected()) { | |||||
selected.push(id); | |||||
} | |||||
else { | |||||
if (alwaysShow === true) { | |||||
nodes[id].draw(ctx); | |||||
} | |||||
else if (nodes[id].inArea() === true) { | |||||
nodes[id].draw(ctx); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// draw the selected nodes on top | |||||
for (var s = 0, sMax = selected.length; s < sMax; s++) { | |||||
if (nodes[selected[s]].inArea() || alwaysShow) { | |||||
nodes[selected[s]].draw(ctx); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Redraw all edges | |||||
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||||
* @param {CanvasRenderingContext2D} ctx | |||||
* @private | |||||
*/ | |||||
_drawEdges(ctx) { | |||||
var edges = this.body.edges; | |||||
for (var id in edges) { | |||||
if (edges.hasOwnProperty(id)) { | |||||
var edge = edges[id]; | |||||
edge.setScale(this.scale); | |||||
if (edge.connected === true) { | |||||
edges[id].draw(ctx); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Redraw all edges | |||||
* The 2d context of a HTML canvas can be retrieved by canvas.getContext('2d'); | |||||
* @param {CanvasRenderingContext2D} ctx | |||||
* @private | |||||
*/ | |||||
_drawControlNodes(ctx) { | |||||
var edges = this.body.edges; | |||||
for (var id in edges) { | |||||
if (edges.hasOwnProperty(id)) { | |||||
edges[id]._drawControlNodes(ctx); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Determine if the browser requires a setTimeout or a requestAnimationFrame. This was required because | |||||
* some implementations (safari and IE9) did not support requestAnimationFrame | |||||
* @private | |||||
*/ | |||||
_determineBrowserMethod() { | |||||
if (typeof window !== 'undefined') { | |||||
var browserType = navigator.userAgent.toLowerCase(); | |||||
this.requiresTimeout = false; | |||||
if (browserType.indexOf('msie 9.0') != -1) { // IE 9 | |||||
this.requiresTimeout = true; | |||||
} | |||||
else if (browserType.indexOf('safari') != -1) { // safari | |||||
if (browserType.indexOf('chrome') <= -1) { | |||||
this.requiresTimeout = true; | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
this.requiresTimeout = true; | |||||
} | |||||
} | |||||
} | |||||
export {CanvasRenderer}; |
@ -0,0 +1,620 @@ | |||||
/** | |||||
* Created by Alex on 24-Feb-15. | |||||
*/ | |||||
var util = require("../../util"); | |||||
class ClusterEngine { | |||||
constructor(body) { | |||||
this.body = body; | |||||
this.clusteredNodes = {}; | |||||
} | |||||
/** | |||||
* | |||||
* @param hubsize | |||||
* @param options | |||||
*/ | |||||
clusterByConnectionCount(hubsize, options) { | |||||
if (hubsize === undefined) { | |||||
hubsize = this._getHubSize(); | |||||
} | |||||
else if (tyepof(hubsize) == "object") { | |||||
options = this._checkOptions(hubsize); | |||||
hubsize = this._getHubSize(); | |||||
} | |||||
var nodesToCluster = []; | |||||
for (var i = 0; i < this.body.nodeIndices.length; i++) { | |||||
var node = this.body.nodes[this.body.nodeIndices[i]]; | |||||
if (node.edges.length >= hubsize) { | |||||
nodesToCluster.push(node.id); | |||||
} | |||||
} | |||||
for (var i = 0; i < nodesToCluster.length; i++) { | |||||
var node = this.body.nodes[nodesToCluster[i]]; | |||||
this.clusterByConnection(node,options,{},{},true); | |||||
} | |||||
this.body.emitter.emit('_dataChanged'); | |||||
} | |||||
/** | |||||
* loop over all nodes, check if they adhere to the condition and cluster if needed. | |||||
* @param options | |||||
* @param doNotUpdateCalculationNodes | |||||
*/ | |||||
clusterByNodeData(options = {}, doNotUpdateCalculationNodes = false) { | |||||
if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");} | |||||
// check if the options object is fine, append if needed | |||||
options = this._checkOptions(options); | |||||
var childNodesObj = {}; | |||||
var childEdgesObj = {} | |||||
// collect the nodes that will be in the cluster | |||||
for (var i = 0; i < this.body.nodeIndices.length; i++) { | |||||
var nodeId = this.body.nodeIndices[i]; | |||||
var clonedOptions = this._cloneOptions(nodeId); | |||||
if (options.joinCondition(clonedOptions) == true) { | |||||
childNodesObj[nodeId] = this.body.nodes[nodeId]; | |||||
} | |||||
} | |||||
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); | |||||
} | |||||
/** | |||||
* Cluster all nodes in the network that have only 1 edge | |||||
* @param options | |||||
* @param doNotUpdateCalculationNodes | |||||
*/ | |||||
clusterOutliers(options, doNotUpdateCalculationNodes) { | |||||
options = this._checkOptions(options); | |||||
var clusters = []; | |||||
// collect the nodes that will be in the cluster | |||||
for (var i = 0; i < this.body.nodeIndices.length; i++) { | |||||
var childNodesObj = {}; | |||||
var childEdgesObj = {}; | |||||
var nodeId = this.body.nodeIndices[i]; | |||||
if (this.body.nodes[nodeId].edges.length == 1) { | |||||
var edge = this.body.nodes[nodeId].edges[0]; | |||||
var childNodeId = this._getConnectedId(edge, nodeId); | |||||
if (childNodeId != nodeId) { | |||||
if (options.joinCondition === undefined) { | |||||
childNodesObj[nodeId] = this.body.nodes[nodeId]; | |||||
childNodesObj[childNodeId] = this.body.nodes[childNodeId]; | |||||
} | |||||
else { | |||||
var clonedOptions = this._cloneOptions(nodeId); | |||||
if (options.joinCondition(clonedOptions) == true) { | |||||
childNodesObj[nodeId] = this.body.nodes[nodeId]; | |||||
} | |||||
clonedOptions = this._cloneOptions(childNodeId); | |||||
if (options.joinCondition(clonedOptions) == true) { | |||||
childNodesObj[childNodeId] = this.body.nodes[childNodeId]; | |||||
} | |||||
} | |||||
clusters.push({nodes:childNodesObj, edges:childEdgesObj}) | |||||
} | |||||
} | |||||
} | |||||
for (var i = 0; i < clusters.length; i++) { | |||||
this._cluster(clusters[i].nodes, clusters[i].edges, options, true) | |||||
} | |||||
if (doNotUpdateCalculationNodes !== true) { | |||||
this.body.emitter.emit('_dataChanged'); | |||||
} | |||||
} | |||||
/** | |||||
* | |||||
* @param nodeId | |||||
* @param options | |||||
* @param doNotUpdateCalculationNodes | |||||
*/ | |||||
clusterByConnection(nodeId, options, doNotUpdateCalculationNodes) { | |||||
// kill conditions | |||||
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} | |||||
if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} | |||||
var node = this.body.nodes[nodeId]; | |||||
options = this._checkOptions(options, node); | |||||
if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;} | |||||
if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;} | |||||
var childNodesObj = {}; | |||||
var childEdgesObj = {} | |||||
var parentNodeId = node.id; | |||||
var parentClonedOptions = this._cloneOptions(parentNodeId); | |||||
childNodesObj[parentNodeId] = node; | |||||
// collect the nodes that will be in the cluster | |||||
for (var i = 0; i < node.edges.length; i++) { | |||||
var edge = node.edges[i]; | |||||
var childNodeId = this._getConnectedId(edge, parentNodeId); | |||||
if (childNodeId !== parentNodeId) { | |||||
if (options.joinCondition === undefined) { | |||||
childEdgesObj[edge.id] = edge; | |||||
childNodesObj[childNodeId] = this.body.nodes[childNodeId]; | |||||
} | |||||
else { | |||||
// clone the options and insert some additional parameters that could be interesting. | |||||
var childClonedOptions = this._cloneOptions(childNodeId); | |||||
if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) { | |||||
childEdgesObj[edge.id] = edge; | |||||
childNodesObj[childNodeId] = this.body.nodes[childNodeId]; | |||||
} | |||||
} | |||||
} | |||||
else { | |||||
childEdgesObj[edge.id] = edge; | |||||
} | |||||
} | |||||
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); | |||||
} | |||||
/** | |||||
* This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes. | |||||
* @param objId | |||||
* @param type | |||||
* @returns {{}} | |||||
* @private | |||||
*/ | |||||
_cloneOptions(objId, type) { | |||||
var clonedOptions = {}; | |||||
if (type === undefined || type == 'node') { | |||||
util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); | |||||
util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); | |||||
clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; | |||||
} | |||||
else { | |||||
util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); | |||||
} | |||||
return clonedOptions; | |||||
} | |||||
/** | |||||
* This function creates the edges that will be attached to the cluster. | |||||
* | |||||
* @param childNodesObj | |||||
* @param childEdgesObj | |||||
* @param newEdges | |||||
* @param options | |||||
* @private | |||||
*/ | |||||
_createClusterEdges (childNodesObj, childEdgesObj, newEdges, options) { | |||||
var edge, childNodeId, childNode; | |||||
var childKeys = Object.keys(childNodesObj); | |||||
for (var i = 0; i < childKeys.length; i++) { | |||||
childNodeId = childKeys[i]; | |||||
childNode = childNodesObj[childNodeId]; | |||||
// mark all edges for removal from global and construct new edges from the cluster to others | |||||
for (var j = 0; j < childNode.edges.length; j++) { | |||||
edge = childNode.edges[j]; | |||||
childEdgesObj[edge.id] = edge; | |||||
var otherNodeId = edge.toId; | |||||
var otherOnTo = true; | |||||
if (edge.toId != childNodeId) { | |||||
otherNodeId = edge.toId; | |||||
otherOnTo = true; | |||||
} | |||||
else if (edge.fromId != childNodeId) { | |||||
otherNodeId = edge.fromId; | |||||
otherOnTo = false; | |||||
} | |||||
if (childNodesObj[otherNodeId] === undefined) { | |||||
var clonedOptions = this._cloneOptions(edge.id, 'edge'); | |||||
util.deepExtend(clonedOptions, options.clusterEdgeProperties); | |||||
if (otherOnTo === true) { | |||||
clonedOptions.from = options.clusterNodeProperties.id; | |||||
clonedOptions.to = otherNodeId; | |||||
} | |||||
else { | |||||
clonedOptions.from = otherNodeId; | |||||
clonedOptions.to = options.clusterNodeProperties.id; | |||||
} | |||||
clonedOptions.id = 'clusterEdge:' + util.randomUUID(); | |||||
newEdges.push(this.body.functions.createEdge(clonedOptions)) | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* This function checks the options that can be supplied to the different cluster functions | |||||
* for certain fields and inserts defaults if needed | |||||
* @param options | |||||
* @returns {*} | |||||
* @private | |||||
*/ | |||||
_checkOptions(options = {}) { | |||||
if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};} | |||||
if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};} | |||||
return options; | |||||
} | |||||
/** | |||||
* | |||||
* @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node | |||||
* @param {Object} childEdgesObj | object with edge objects, id as keys | |||||
* @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} | |||||
* @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up | |||||
* @private | |||||
*/ | |||||
_cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes = false) { | |||||
// kill condition: no children so cant cluster | |||||
if (Object.keys(childNodesObj).length == 0) {return;} | |||||
// check if we have an unique id; | |||||
if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();} | |||||
var clusterId = options.clusterNodeProperties.id; | |||||
// create the new edges that will connect to the cluster | |||||
var newEdges = []; | |||||
this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); | |||||
// construct the clusterNodeProperties | |||||
var clusterNodeProperties = options.clusterNodeProperties; | |||||
if (options.processProperties !== undefined) { | |||||
// get the childNode options | |||||
var childNodesOptions = []; | |||||
for (var nodeId in childNodesObj) { | |||||
var clonedOptions = this._cloneOptions(nodeId); | |||||
childNodesOptions.push(clonedOptions); | |||||
} | |||||
// get clusterproperties based on childNodes | |||||
var childEdgesOptions = []; | |||||
for (var edgeId in childEdgesObj) { | |||||
var clonedOptions = this._cloneOptions(edgeId, 'edge'); | |||||
childEdgesOptions.push(clonedOptions); | |||||
} | |||||
clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); | |||||
if (!clusterNodeProperties) { | |||||
throw new Error("The processClusterProperties function does not return properties!"); | |||||
} | |||||
} | |||||
if (clusterNodeProperties.label === undefined) { | |||||
clusterNodeProperties.label = 'cluster'; | |||||
} | |||||
// give the clusterNode a postion if it does not have one. | |||||
var pos = undefined; | |||||
if (clusterNodeProperties.x === undefined) { | |||||
pos = this._getClusterPosition(childNodesObj); | |||||
clusterNodeProperties.x = pos.x; | |||||
clusterNodeProperties.allowedToMoveX = true; | |||||
} | |||||
if (clusterNodeProperties.x === undefined) { | |||||
if (pos === undefined) { | |||||
pos = this._getClusterPosition(childNodesObj); | |||||
} | |||||
clusterNodeProperties.y = pos.y; | |||||
clusterNodeProperties.allowedToMoveY = true; | |||||
} | |||||
// force the ID to remain the same | |||||
clusterNodeProperties.id = clusterId; | |||||
// create the clusterNode | |||||
var clusterNode = this.body.functions.createNode(clusterNodeProperties); | |||||
clusterNode.isCluster = true; | |||||
clusterNode.containedNodes = childNodesObj; | |||||
clusterNode.containedEdges = childEdgesObj; | |||||
// delete contained edges from global | |||||
for (var edgeId in childEdgesObj) { | |||||
if (childEdgesObj.hasOwnProperty(edgeId)) { | |||||
if (this.body.edges[edgeId] !== undefined) { | |||||
if (this.body.edges[edgeId].via !== null) { | |||||
var viaId = this.body.edges[edgeId].via.id; | |||||
if (viaId) { | |||||
this.body.edges[edgeId].via = null | |||||
delete this.body.supportNodes[viaId]; | |||||
} | |||||
} | |||||
this.body.edges[edgeId].disconnect(); | |||||
delete this.body.edges[edgeId]; | |||||
} | |||||
} | |||||
} | |||||
// remove contained nodes from global | |||||
for (var nodeId in childNodesObj) { | |||||
if (childNodesObj.hasOwnProperty(nodeId)) { | |||||
this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.body.nodes[nodeId]}; | |||||
delete this.body.nodes[nodeId]; | |||||
} | |||||
} | |||||
// finally put the cluster node into global | |||||
this.body.nodes[clusterNodeProperties.id] = clusterNode; | |||||
// push new edges to global | |||||
for (var i = 0; i < newEdges.length; i++) { | |||||
this.body.edges[newEdges[i].id] = newEdges[i]; | |||||
this.body.edges[newEdges[i].id].connect(); | |||||
} | |||||
// create bezier nodes for smooth curves if needed | |||||
this.body.emitter.emit("_newEdgesCreated"); | |||||
// set ID to undefined so no duplicates arise | |||||
clusterNodeProperties.id = undefined; | |||||
// wrap up | |||||
if (doNotUpdateCalculationNodes !== true) { | |||||
this.body.emitter.emit('_dataChanged'); | |||||
} | |||||
} | |||||
/** | |||||
* Check if a node is a cluster. | |||||
* @param nodeId | |||||
* @returns {*} | |||||
*/ | |||||
isCluster(nodeId) { | |||||
if (this.body.nodes[nodeId] !== undefined) { | |||||
return this.body.nodes[nodeId].isCluster; | |||||
} | |||||
else { | |||||
console.log("Node does not exist.") | |||||
return false; | |||||
} | |||||
} | |||||
/** | |||||
* get the position of the cluster node based on what's inside | |||||
* @param {object} childNodesObj | object with node objects, id as keys | |||||
* @returns {{x: number, y: number}} | |||||
* @private | |||||
*/ | |||||
_getClusterPosition(childNodesObj) { | |||||
var childKeys = Object.keys(childNodesObj); | |||||
var minX = childNodesObj[childKeys[0]].x; | |||||
var maxX = childNodesObj[childKeys[0]].x; | |||||
var minY = childNodesObj[childKeys[0]].y; | |||||
var maxY = childNodesObj[childKeys[0]].y; | |||||
var node; | |||||
for (var i = 0; i < childKeys.lenght; i++) { | |||||
node = childNodesObj[childKeys[0]]; | |||||
minX = node.x < minX ? node.x : minX; | |||||
maxX = node.x > maxX ? node.x : maxX; | |||||
minY = node.y < minY ? node.y : minY; | |||||
maxY = node.y > maxY ? node.y : maxY; | |||||
} | |||||
return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)}; | |||||
} | |||||
/** | |||||
* Open a cluster by calling this function. | |||||
* @param {String} clusterNodeId | the ID of the cluster node | |||||
* @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true | |||||
*/ | |||||
openCluster(clusterNodeId, doNotUpdateCalculationNodes) { | |||||
// kill conditions | |||||
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} | |||||
if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} | |||||
if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return}; | |||||
var node = this.body.nodes[clusterNodeId]; | |||||
var containedNodes = node.containedNodes; | |||||
var containedEdges = node.containedEdges; | |||||
// release nodes | |||||
for (var nodeId in containedNodes) { | |||||
if (containedNodes.hasOwnProperty(nodeId)) { | |||||
this.body.nodes[nodeId] = containedNodes[nodeId]; | |||||
// inherit position | |||||
this.body.nodes[nodeId].x = node.x; | |||||
this.body.nodes[nodeId].y = node.y; | |||||
// inherit speed | |||||
this.body.nodes[nodeId].vx = node.vx; | |||||
this.body.nodes[nodeId].vy = node.vy; | |||||
delete this.clusteredNodes[nodeId]; | |||||
} | |||||
} | |||||
// release edges | |||||
for (var edgeId in containedEdges) { | |||||
if (containedEdges.hasOwnProperty(edgeId)) { | |||||
this.body.edges[edgeId] = containedEdges[edgeId]; | |||||
this.body.edges[edgeId].connect(); | |||||
var edge = this.body.edges[edgeId]; | |||||
if (edge.connected === false) { | |||||
if (this.clusteredNodes[edge.fromId] !== undefined) { | |||||
this._connectEdge(edge, edge.fromId, true); | |||||
} | |||||
if (this.clusteredNodes[edge.toId] !== undefined) { | |||||
this._connectEdge(edge, edge.toId, false); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
this.body.emitter.emit("_newEdgesCreated",containedEdges); | |||||
var edgeIds = []; | |||||
for (var i = 0; i < node.edges.length; i++) { | |||||
edgeIds.push(node.edges[i].id); | |||||
} | |||||
// remove edges in clusterNode | |||||
for (var i = 0; i < edgeIds.length; i++) { | |||||
var edge = this.body.edges[edgeIds[i]]; | |||||
// if the edge should have been connected to a contained node | |||||
if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) { | |||||
// the node in the from array was contained in the cluster | |||||
if (this.body.nodes[edge.fromArray[0].id] !== undefined) { | |||||
this._connectEdge(edge, edge.fromArray[0].id, true); | |||||
} | |||||
} | |||||
else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) { | |||||
// the node in the to array was contained in the cluster | |||||
if (this.body.nodes[edge.toArray[0].id] !== undefined) { | |||||
this._connectEdge(edge, edge.toArray[0].id, false); | |||||
} | |||||
} | |||||
else { | |||||
var edgeId = edgeIds[i]; | |||||
var viaId = this.body.edges[edgeId].via.id; | |||||
if (viaId) { | |||||
this.body.edges[edgeId].via = null | |||||
delete this.body.supportNodes[viaId]; | |||||
} | |||||
// this removes the edge from node.edges, which is why edgeIds is formed | |||||
this.body.edges[edgeId].disconnect(); | |||||
delete this.body.edges[edgeId]; | |||||
} | |||||
} | |||||
// remove clusterNode | |||||
delete this.body.nodes[clusterNodeId]; | |||||
if (doNotUpdateCalculationNodes !== true) { | |||||
this.body.emitter.emit('_dataChanged'); | |||||
} | |||||
} | |||||
/** | |||||
* Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to | |||||
* is currently residing in cluster B | |||||
* @param edge | |||||
* @param nodeId | |||||
* @param from | |||||
* @private | |||||
*/ | |||||
_connectEdge(edge, nodeId, from) { | |||||
var clusterStack = this._getClusterStack(nodeId); | |||||
if (from == true) { | |||||
edge.from = clusterStack[clusterStack.length - 1]; | |||||
edge.fromId = clusterStack[clusterStack.length - 1].id; | |||||
clusterStack.pop() | |||||
edge.fromArray = clusterStack; | |||||
} | |||||
else { | |||||
edge.to = clusterStack[clusterStack.length - 1]; | |||||
edge.toId = clusterStack[clusterStack.length - 1].id; | |||||
clusterStack.pop(); | |||||
edge.toArray = clusterStack; | |||||
} | |||||
edge.connect(); | |||||
} | |||||
/** | |||||
* Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node | |||||
* @param nodeId | |||||
* @returns {Array} | |||||
* @private | |||||
*/ | |||||
_getClusterStack(nodeId) { | |||||
var stack = []; | |||||
var max = 100; | |||||
var counter = 0; | |||||
while (this.clusteredNodes[nodeId] !== undefined && counter < max) { | |||||
stack.push(this.clusteredNodes[nodeId].node); | |||||
nodeId = this.clusteredNodes[nodeId].clusterId; | |||||
counter++; | |||||
} | |||||
stack.push(this.body.nodes[nodeId]); | |||||
return stack; | |||||
} | |||||
/** | |||||
* Get the Id the node is connected to | |||||
* @param edge | |||||
* @param nodeId | |||||
* @returns {*} | |||||
* @private | |||||
*/ | |||||
_getConnectedId(edge, nodeId) { | |||||
if (edge.toId != nodeId) { | |||||
return edge.toId; | |||||
} | |||||
else if (edge.fromId != nodeId) { | |||||
return edge.fromId; | |||||
} | |||||
else { | |||||
return edge.fromId; | |||||
} | |||||
} | |||||
/** | |||||
* We determine how many connections denote an important hub. | |||||
* We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) | |||||
* | |||||
* @private | |||||
*/ | |||||
_getHubSize() { | |||||
var average = 0; | |||||
var averageSquared = 0; | |||||
var hubCounter = 0; | |||||
var largestHub = 0; | |||||
for (var i = 0; i < this.body.nodeIndices.length; i++) { | |||||
var node = this.body.nodes[this.body.nodeIndices[i]]; | |||||
if (node.edges.length > largestHub) { | |||||
largestHub = node.edges.length; | |||||
} | |||||
average += node.edges.length; | |||||
averageSquared += Math.pow(node.edges.length,2); | |||||
hubCounter += 1; | |||||
} | |||||
average = average / hubCounter; | |||||
averageSquared = averageSquared / hubCounter; | |||||
var variance = averageSquared - Math.pow(average,2); | |||||
var standardDeviation = Math.sqrt(variance); | |||||
var hubThreshold = Math.floor(average + 2*standardDeviation); | |||||
// always have at least one to cluster | |||||
if (hubThreshold > largestHub) { | |||||
hubThreshold = largestHub; | |||||
} | |||||
return hubThreshold; | |||||
}; | |||||
} | |||||
export { ClusterEngine }; |
@ -0,0 +1,419 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
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"; | |||||
var util = require('../../util'); | |||||
class PhysicsEngine { | |||||
constructor(body, options) { | |||||
this.body = body; | |||||
this.physicsBody = {calculationNodes: {}, calculationNodeIndices:[], forces: {}, velocities: {}}; | |||||
this.scale = 1; | |||||
this.viewFunction = undefined; | |||||
this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||||
this.simulationInterval = 1000 / 60; | |||||
this.requiresTimeout = true; | |||||
this.previousStates = {}; | |||||
this.renderTimer == undefined; | |||||
this.stabilized = false; | |||||
this.stabilizationIterations = 0; | |||||
// default options | |||||
this.options = { | |||||
barnesHut: { | |||||
thetaInverted: 1 / 0.5, // inverted to save time during calculation | |||||
gravitationalConstant: -2000, | |||||
centralGravity: 0.3, | |||||
springLength: 95, | |||||
springConstant: 0.04, | |||||
damping: 0.09 | |||||
}, | |||||
repulsion: { | |||||
centralGravity: 0.0, | |||||
springLength: 200, | |||||
springConstant: 0.05, | |||||
nodeDistance: 100, | |||||
damping: 0.09 | |||||
}, | |||||
hierarchicalRepulsion: { | |||||
centralGravity: 0.0, | |||||
springLength: 100, | |||||
springConstant: 0.01, | |||||
nodeDistance: 150, | |||||
damping: 0.09 | |||||
}, | |||||
model: 'BarnesHut', | |||||
timestep: 0.5, | |||||
maxVelocity: 50, | |||||
minVelocity: 0.1, // px/s | |||||
stabilization: { | |||||
enabled: true, | |||||
iterations: 1000, // maximum number of iteration to stabilize | |||||
updateInterval: 100, | |||||
onlyDynamicEdges: false, | |||||
zoomExtent: true | |||||
} | |||||
} | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
if (options !== undefined) { | |||||
if (typeof options.stabilization == 'boolean') { | |||||
options.stabilization = { | |||||
enabled: options.stabilization | |||||
} | |||||
} | |||||
util.deepExtend(this.options, options); | |||||
} | |||||
this.init(); | |||||
} | |||||
init() { | |||||
var options; | |||||
if (this.options.model == "repulsion") { | |||||
options = this.options.repulsion; | |||||
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options); | |||||
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options); | |||||
} | |||||
else if (this.options.model == "hierarchicalRepulsion") { | |||||
options = this.options.hierarchicalRepulsion; | |||||
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options); | |||||
this.edgesSolver = new HierarchicalSpringSolver(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; | |||||
} | |||||
startSimulation() { | |||||
this.stabilized = false; | |||||
if (this.options.stabilization.enabled === true) { | |||||
this.stabilize(); | |||||
} | |||||
else { | |||||
this.runSimulation(); | |||||
} | |||||
} | |||||
runSimulation() { | |||||
if (this.viewFunction === undefined) { | |||||
this.viewFunction = this.simulationStep.bind(this); | |||||
this.body.emitter.on("_beforeRender", this.viewFunction); | |||||
this.body.emitter.emit("_startRendering"); | |||||
} | |||||
} | |||||
simulationStep() { | |||||
// check if the physics have settled | |||||
var startTime = Date.now(); | |||||
this.physicsTick(); | |||||
var physicsTime = Date.now() - startTime; | |||||
// run double speed if it is a little graph | |||||
if ((physicsTime < 0.4 * this.simulationInterval || this.runDoubleSpeed == true) && this.stabilized === false) { | |||||
this.physicsTick(); | |||||
// this makes sure there is no jitter. The decision is taken once to run it at double speed. | |||||
this.runDoubleSpeed = true; | |||||
} | |||||
if (this.stabilized === true) { | |||||
if (this.stabilizationIterations > 1) { | |||||
// trigger the "stabilized" event. | |||||
// The event is triggered on the next tick, to prevent the case that | |||||
// it is fired while initializing the Network, in which case you would not | |||||
// be able to catch it | |||||
var me = this; | |||||
var params = { | |||||
iterations: this.stabilizationIterations | |||||
}; | |||||
this.stabilizationIterations = 0; | |||||
this.startedStabilization = false; | |||||
setTimeout(function () { | |||||
me.body.emitter.emit("stabilized", params); | |||||
}, 0); | |||||
} | |||||
else { | |||||
this.stabilizationIterations = 0; | |||||
} | |||||
this.body.emitter.emit("_stopRendering"); | |||||
} | |||||
} | |||||
/** | |||||
* A single simulation step (or "tick") in the physics simulation | |||||
* | |||||
* @private | |||||
*/ | |||||
physicsTick() { | |||||
if (this.stabilized === false) { | |||||
this.calculateForces(); | |||||
this.stabilized = this.moveNodes(); | |||||
// determine if the network has stabilzied | |||||
if (this.stabilized === true) { | |||||
this.revert(); | |||||
} | |||||
else { | |||||
// this is here to ensure that there is no start event when the network is already stable. | |||||
if (this.startedStabilization == false) { | |||||
this.body.emitter.emit("startStabilizing"); | |||||
this.startedStabilization = true; | |||||
} | |||||
} | |||||
this.stabilizationIterations++; | |||||
} | |||||
} | |||||
/** | |||||
* Smooth curves are created by adding invisible nodes in the center of the edges. These nodes are also | |||||
* handled in the calculateForces function. We then use a quadratic curve with the center node as control. | |||||
* This function joins the datanodes and invisible (called support) nodes into one object. | |||||
* We do this so we do not contaminate this.body.nodes with the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
_updateCalculationNodes() { | |||||
this.physicsBody.calculationNodes = {}; | |||||
this.physicsBody.forces = {}; | |||||
this.physicsBody.calculationNodeIndices = []; | |||||
for (let i = 0; i < this.body.nodeIndices.length; i++) { | |||||
let nodeId = this.body.nodeIndices[i]; | |||||
this.physicsBody.calculationNodes[nodeId] = this.body.nodes[nodeId]; | |||||
} | |||||
// if support nodes are used, we have them here | |||||
var supportNodes = this.body.supportNodes; | |||||
for (let i = 0; i < this.body.supportNodeIndices.length; i++) { | |||||
let supportNodeId = this.body.supportNodeIndices[i]; | |||||
if (this.body.edges[supportNodes[supportNodeId].parentEdgeId] !== undefined) { | |||||
this.physicsBody.calculationNodes[supportNodeId] = supportNodes[supportNodeId]; | |||||
} | |||||
else { | |||||
console.error("Support node detected that does not have an edge!") | |||||
} | |||||
} | |||||
this.physicsBody.calculationNodeIndices = Object.keys(this.physicsBody.calculationNodes); | |||||
for (let i = 0; i < this.physicsBody.calculationNodeIndices.length; i++) { | |||||
let nodeId = this.physicsBody.calculationNodeIndices[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 (this.physicsBody.calculationNodes[nodeId] === undefined) { | |||||
delete this.physicsBody.velocities[nodeId]; | |||||
} | |||||
} | |||||
} | |||||
revert() { | |||||
var nodeIds = Object.keys(this.previousStates); | |||||
var nodes = this.physicsBody.calculationNodes; | |||||
var velocities = this.physicsBody.velocities; | |||||
for (let i = 0; i < nodeIds.length; i++) { | |||||
let nodeId = nodeIds[i]; | |||||
if (nodes[nodeId] !== undefined) { | |||||
velocities[nodeId].x = this.previousStates[nodeId].vx; | |||||
velocities[nodeId].y = this.previousStates[nodeId].vy; | |||||
nodes[nodeId].x = this.previousStates[nodeId].x; | |||||
nodes[nodeId].y = this.previousStates[nodeId].y; | |||||
} | |||||
else { | |||||
delete this.previousStates[nodeId]; | |||||
} | |||||
} | |||||
} | |||||
moveNodes() { | |||||
var nodesPresent = false; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var maxVelocity = this.options.maxVelocity === 0 ? 1e9 : this.options.maxVelocity; | |||||
var stabilized = true; | |||||
var vminCorrected = this.options.minVelocity / Math.max(this.scale,0.05); | |||||
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 | |||||
stabilized = nodeVelocity < vminCorrected && stabilized === true; | |||||
nodesPresent = true; | |||||
} | |||||
if (nodesPresent == true) { | |||||
if (vminCorrected > 0.5*this.options.maxVelocity) { | |||||
return false; | |||||
} | |||||
else { | |||||
return stabilized; | |||||
} | |||||
} | |||||
return true; | |||||
} | |||||
_performStep(nodeId,maxVelocity) { | |||||
var node = this.physicsBody.calculationNodes[nodeId]; | |||||
var timestep = this.options.timestep; | |||||
var forces = this.physicsBody.forces; | |||||
var 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.xFixed) { | |||||
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; | |||||
} | |||||
if (!node.yFixed) { | |||||
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; | |||||
} | |||||
var totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2)); | |||||
return totalVelocity; | |||||
} | |||||
calculateForces() { | |||||
this.gravitySolver.solve(); | |||||
this.nodesSolver.solve(); | |||||
this.edgesSolver.solve(); | |||||
} | |||||
/** | |||||
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization | |||||
* because only the supportnodes for the smoothCurves have to settle. | |||||
* | |||||
* @private | |||||
*/ | |||||
_freezeNodes() { | |||||
var nodes = this.body.nodes; | |||||
for (var id in nodes) { | |||||
if (nodes.hasOwnProperty(id)) { | |||||
if (nodes[id].x != null && nodes[id].y != null) { | |||||
nodes[id].fixedData.x = nodes[id].xFixed; | |||||
nodes[id].fixedData.y = nodes[id].yFixed; | |||||
nodes[id].xFixed = true; | |||||
nodes[id].yFixed = true; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Unfreezes the nodes that have been frozen by _freezeDefinedNodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
_restoreFrozenNodes() { | |||||
var nodes = this.body.nodes; | |||||
for (var id in nodes) { | |||||
if (nodes.hasOwnProperty(id)) { | |||||
if (nodes[id].fixedData.x != null) { | |||||
nodes[id].xFixed = nodes[id].fixedData.x; | |||||
nodes[id].yFixed = nodes[id].fixedData.y; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* Find a stable position for all nodes | |||||
* @private | |||||
*/ | |||||
stabilize() { | |||||
if (this.options.stabilization.onlyDynamicEdges == true) { | |||||
this._freezeNodes(); | |||||
} | |||||
this.stabilizationSteps = 0; | |||||
setTimeout(this._stabilizationBatch.bind(this),0); | |||||
} | |||||
_stabilizationBatch() { | |||||
var count = 0; | |||||
while (this.stabilized == false && count < this.options.stabilization.updateInterval && this.stabilizationSteps < this.options.stabilization.iterations) { | |||||
this.physicsTick(); | |||||
this.stabilizationSteps++; | |||||
count++; | |||||
} | |||||
if (this.stabilized == false && this.stabilizationSteps < this.options.stabilization.iterations) { | |||||
this.body.emitter.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.options.stabilization.iterations}); | |||||
setTimeout(this._stabilizationBatch.bind(this),0); | |||||
} | |||||
else { | |||||
this._finalizeStabilization(); | |||||
} | |||||
} | |||||
_finalizeStabilization() { | |||||
if (this.options.stabilization.zoomExtent == true) { | |||||
this.body.emitter.emit("zoomExtent", {duration:0}); | |||||
} | |||||
if (this.options.stabilization.onlyDynamicEdges == true) { | |||||
this._restoreFrozenNodes(); | |||||
} | |||||
this.body.emitter.emit("stabilizationIterationsDone"); | |||||
this.body.emitter.emit("_requestRedraw"); | |||||
} | |||||
} | |||||
export {PhysicsEngine}; |
@ -0,0 +1,460 @@ | |||||
/** | |||||
* Created by Alex on 2/27/2015. | |||||
* | |||||
*/ | |||||
import {SelectionHandler} from "./components/SelectionHandler" | |||||
var util = require('../../util'); | |||||
class TouchEventHandler { | |||||
constructor(body) { | |||||
this.body = body; | |||||
this.body.eventListeners.onTap = this.onTap.bind(this); | |||||
this.body.eventListeners.onTouch = this.onTouch.bind(this); | |||||
this.body.eventListeners.onDoubleTap = this.onDoubleTap.bind(this); | |||||
this.body.eventListeners.onHold = this.onHold.bind(this); | |||||
this.body.eventListeners.onDragStart = this.onDragStart.bind(this); | |||||
this.body.eventListeners.onDrag = this.onDrag.bind(this); | |||||
this.body.eventListeners.onDragEnd = this.onDragEnd.bind(this); | |||||
this.body.eventListeners.onMouseWheel = this.onMouseWheel.bind(this); | |||||
this.body.eventListeners.onPinch = this.onPinch.bind(this); | |||||
this.body.eventListeners.onMouseMove = this.onMouseMove.bind(this); | |||||
this.body.eventListeners.onRelease = this.onRelease.bind(this); | |||||
this.touchTime = 0; | |||||
this.drag = {}; | |||||
this.pinch = {}; | |||||
this.pointerPosition = {x:0,y:0}; | |||||
this.scale = 1.0; | |||||
this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||||
this.selectionHandler = new SelectionHandler(body); | |||||
} | |||||
setCanvas(canvas) { | |||||
this.canvas = canvas; | |||||
this.selectionHandler.setCanvas(canvas); | |||||
} | |||||
/** | |||||
* Get the pointer location from a touch location | |||||
* @param {{pageX: Number, pageY: Number}} touch | |||||
* @return {{x: Number, y: Number}} pointer | |||||
* @private | |||||
*/ | |||||
getPointer(touch) { | |||||
return { | |||||
x: touch.pageX - util.getAbsoluteLeft(this.canvas.frame.canvas), | |||||
y: touch.pageY - util.getAbsoluteTop(this.canvas.frame.canvas) | |||||
}; | |||||
} | |||||
/** | |||||
* On start of a touch gesture, store the pointer | |||||
* @param event | |||||
* @private | |||||
*/ | |||||
onTouch(event) { | |||||
if (new Date().valueOf() - this.touchTime > 100) { | |||||
this.drag.pointer = this.getPointer(event.gesture.center); | |||||
this.drag.pinched = false; | |||||
this.pinch.scale = this.scale; | |||||
// to avoid double fireing of this event because we have two hammer instances. (on canvas and on frame) | |||||
this.touchTime = new Date().valueOf(); | |||||
} | |||||
} | |||||
/** | |||||
* handle tap/click event: select/unselect a node | |||||
* @private | |||||
*/ | |||||
onTap(event) { | |||||
console.log("tap",event) | |||||
var pointer = this.getPointer(event.gesture.center); | |||||
this.pointerPosition = pointer; | |||||
this.selectionHandler.selectOnPoint(pointer); | |||||
} | |||||
/** | |||||
* handle drag start event | |||||
* @private | |||||
*/ | |||||
/** | |||||
* This function is called by onDragStart. | |||||
* It is separated out because we can then overload it for the datamanipulation system. | |||||
* | |||||
* @private | |||||
*/ | |||||
onDragStart(event) { | |||||
// in case the touch event was triggered on an external div, do the initial touch now. | |||||
//if (this.drag.pointer === undefined) { | |||||
// this.onTouch(event); | |||||
//} | |||||
// | |||||
//var node = this._getNodeAt(this.drag.pointer); | |||||
//// note: drag.pointer is set in onTouch to get the initial touch location | |||||
// | |||||
//this.drag.dragging = true; | |||||
//this.drag.selection = []; | |||||
//this.drag.translation = this._getTranslation(); | |||||
//this.drag.nodeId = null; | |||||
//this.draggingNodes = false; | |||||
// | |||||
//if (node != null && this.constants.dragNodes == true) { | |||||
// this.draggingNodes = true; | |||||
// this.drag.nodeId = node.id; | |||||
// // select the clicked node if not yet selected | |||||
// if (!node.isSelected()) { | |||||
// this._selectObject(node, false); | |||||
// } | |||||
// | |||||
// this.emit("dragStart", {nodeIds: this.getSelection().nodes}); | |||||
// | |||||
// // create an array with the selected nodes and their original location and status | |||||
// for (var objectId in this.selectionObj.nodes) { | |||||
// if (this.selectionObj.nodes.hasOwnProperty(objectId)) { | |||||
// var object = this.selectionObj.nodes[objectId]; | |||||
// var s = { | |||||
// id: object.id, | |||||
// node: object, | |||||
// | |||||
// // store original x, y, xFixed and yFixed, make the node temporarily Fixed | |||||
// x: object.x, | |||||
// y: object.y, | |||||
// xFixed: object.xFixed, | |||||
// yFixed: object.yFixed | |||||
// }; | |||||
// | |||||
// object.xFixed = true; | |||||
// object.yFixed = true; | |||||
// | |||||
// this.drag.selection.push(s); | |||||
// } | |||||
// } | |||||
//} | |||||
} | |||||
/** | |||||
* handle drag event | |||||
* @private | |||||
*/ | |||||
onDrag(event) { | |||||
//if (this.drag.pinched) { | |||||
// return; | |||||
//} | |||||
// | |||||
//// remove the focus on node if it is focussed on by the focusOnNode | |||||
//this.releaseNode(); | |||||
// | |||||
//var pointer = this.getPointer(event.gesture.center); | |||||
//var me = this; | |||||
//var drag = this.drag; | |||||
//var selection = drag.selection; | |||||
//if (selection && selection.length && this.constants.dragNodes == true) { | |||||
// // calculate delta's and new location | |||||
// var deltaX = pointer.x - drag.pointer.x; | |||||
// var deltaY = pointer.y - drag.pointer.y; | |||||
// | |||||
// // update position of all selected nodes | |||||
// selection.forEach(function (s) { | |||||
// var node = s.node; | |||||
// | |||||
// if (!s.xFixed) { | |||||
// node.x = me._XconvertDOMtoCanvas(me._XconvertCanvasToDOM(s.x) + deltaX); | |||||
// } | |||||
// | |||||
// if (!s.yFixed) { | |||||
// node.y = me._YconvertDOMtoCanvas(me._YconvertCanvasToDOM(s.y) + deltaY); | |||||
// } | |||||
// }); | |||||
// | |||||
// | |||||
// // start _animationStep if not yet running | |||||
// if (!this.moving) { | |||||
// this.moving = true; | |||||
// this.start(); | |||||
// } | |||||
//} | |||||
//else { | |||||
// // move the network | |||||
// if (this.constants.dragNetwork == true) { | |||||
// // if the drag was not started properly because the click started outside the network div, start it now. | |||||
// if (this.drag.pointer === undefined) { | |||||
// this._handleDragStart(event); | |||||
// return; | |||||
// } | |||||
// var diffX = pointer.x - this.drag.pointer.x; | |||||
// var diffY = pointer.y - this.drag.pointer.y; | |||||
// | |||||
// this._setTranslation( | |||||
// this.drag.translation.x + diffX, | |||||
// this.drag.translation.y + diffY | |||||
// ); | |||||
// this._redraw(); | |||||
// } | |||||
//} | |||||
} | |||||
/** | |||||
* handle drag start event | |||||
* @private | |||||
*/ | |||||
onDragEnd(event) { | |||||
//this.drag.dragging = false; | |||||
//var selection = this.drag.selection; | |||||
//if (selection && selection.length) { | |||||
// selection.forEach(function (s) { | |||||
// // restore original xFixed and yFixed | |||||
// s.node.xFixed = s.xFixed; | |||||
// s.node.yFixed = s.yFixed; | |||||
// }); | |||||
// this.moving = true; | |||||
// this.start(); | |||||
//} | |||||
//else { | |||||
// this._redraw(); | |||||
//} | |||||
//if (this.draggingNodes == false) { | |||||
// this.emit("dragEnd", {nodeIds: []}); | |||||
//} | |||||
//else { | |||||
// this.emit("dragEnd", {nodeIds: this.getSelection().nodes}); | |||||
//} | |||||
} | |||||
/** | |||||
* handle doubletap event | |||||
* @private | |||||
*/ | |||||
onDoubleTap(event) { | |||||
//var pointer = this.getPointer(event.gesture.center); | |||||
//this._handleDoubleTap(pointer); | |||||
} | |||||
/** | |||||
* handle long tap event: multi select nodes | |||||
* @private | |||||
*/ | |||||
onHold(event) { | |||||
//var pointer = this.getPointer(event.gesture.center); | |||||
//this.pointerPosition = pointer; | |||||
//this._handleOnHold(pointer); | |||||
} | |||||
/** | |||||
* handle the release of the screen | |||||
* | |||||
* @private | |||||
*/ | |||||
onRelease(event) { | |||||
//var pointer = this.getPointer(event.gesture.center); | |||||
//this._handleOnRelease(pointer); | |||||
} | |||||
/** | |||||
* Handle pinch event | |||||
* @param event | |||||
* @private | |||||
*/ | |||||
onPinch(event) { | |||||
//var pointer = this.getPointer(event.gesture.center); | |||||
// | |||||
//this.drag.pinched = true; | |||||
//if (!('scale' in this.pinch)) { | |||||
// this.pinch.scale = 1; | |||||
//} | |||||
// | |||||
//// TODO: enabled moving while pinching? | |||||
//var scale = this.pinch.scale * event.gesture.scale; | |||||
//this._zoom(scale, pointer) | |||||
} | |||||
/** | |||||
* Zoom the network in or out | |||||
* @param {Number} scale a number around 1, and between 0.01 and 10 | |||||
* @param {{x: Number, y: Number}} pointer Position on screen | |||||
* @return {Number} appliedScale scale is limited within the boundaries | |||||
* @private | |||||
*/ | |||||
_zoom(scale, pointer) { | |||||
//if (this.constants.zoomable == true) { | |||||
// var scaleOld = this._getScale(); | |||||
// if (scale < 0.00001) { | |||||
// scale = 0.00001; | |||||
// } | |||||
// if (scale > 10) { | |||||
// scale = 10; | |||||
// } | |||||
// | |||||
// var preScaleDragPointer = null; | |||||
// if (this.drag !== undefined) { | |||||
// if (this.drag.dragging == true) { | |||||
// preScaleDragPointer = this.canvas.DOMtoCanvas(this.drag.pointer); | |||||
// } | |||||
// } | |||||
// // + this.canvas.frame.canvas.clientHeight / 2 | |||||
// var translation = this._getTranslation(); | |||||
// | |||||
// var scaleFrac = scale / scaleOld; | |||||
// var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac; | |||||
// var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac; | |||||
// | |||||
// this._setScale(scale); | |||||
// this._setTranslation(tx, ty); | |||||
// | |||||
// if (preScaleDragPointer != null) { | |||||
// var postScaleDragPointer = this.canvas.canvasToDOM(preScaleDragPointer); | |||||
// this.drag.pointer.x = postScaleDragPointer.x; | |||||
// this.drag.pointer.y = postScaleDragPointer.y; | |||||
// } | |||||
// | |||||
// this._redraw(); | |||||
// | |||||
// if (scaleOld < scale) { | |||||
// this.emit("zoom", {direction: "+"}); | |||||
// } | |||||
// else { | |||||
// this.emit("zoom", {direction: "-"}); | |||||
// } | |||||
// | |||||
// return scale; | |||||
//} | |||||
} | |||||
/** | |||||
* Event handler for mouse wheel event, used to zoom the timeline | |||||
* See http://adomas.org/javascript-mouse-wheel/ | |||||
* https://github.com/EightMedia/hammer.js/issues/256 | |||||
* @param {MouseEvent} event | |||||
* @private | |||||
*/ | |||||
onMouseWheel(event) { | |||||
//// retrieve delta | |||||
//var delta = 0; | |||||
//if (event.wheelDelta) { /* IE/Opera. */ | |||||
// delta = event.wheelDelta / 120; | |||||
//} else if (event.detail) { /* Mozilla case. */ | |||||
// // In Mozilla, sign of delta is different than in IE. | |||||
// // Also, delta is multiple of 3. | |||||
// delta = -event.detail / 3; | |||||
//} | |||||
// | |||||
//// If delta is nonzero, handle it. | |||||
//// Basically, delta is now positive if wheel was scrolled up, | |||||
//// and negative, if wheel was scrolled down. | |||||
//if (delta) { | |||||
// | |||||
// // calculate the new scale | |||||
// var scale = this._getScale(); | |||||
// var zoom = delta / 10; | |||||
// if (delta < 0) { | |||||
// zoom = zoom / (1 - zoom); | |||||
// } | |||||
// scale *= (1 + zoom); | |||||
// | |||||
// // calculate the pointer location | |||||
// var gesture = hammerUtil.fakeGesture(this, event); | |||||
// var pointer = this.getPointer(gesture.center); | |||||
// | |||||
// // apply the new scale | |||||
// this._zoom(scale, pointer); | |||||
//} | |||||
// | |||||
//// Prevent default actions caused by mouse wheel. | |||||
//event.preventDefault(); | |||||
} | |||||
/** | |||||
* Mouse move handler for checking whether the title moves over a node with a title. | |||||
* @param {Event} event | |||||
* @private | |||||
*/ | |||||
onMouseMove(event) { | |||||
//var gesture = hammerUtil.fakeGesture(this, event); | |||||
//var pointer = this.getPointer(gesture.center); | |||||
//var popupVisible = false; | |||||
// | |||||
//// check if the previously selected node is still selected | |||||
//if (this.popup !== undefined) { | |||||
// if (this.popup.hidden === false) { | |||||
// this._checkHidePopup(pointer); | |||||
// } | |||||
// | |||||
// // if the popup was not hidden above | |||||
// if (this.popup.hidden === false) { | |||||
// popupVisible = true; | |||||
// this.popup.setPosition(pointer.x + 3, pointer.y - 5) | |||||
// this.popup.show(); | |||||
// } | |||||
//} | |||||
// | |||||
//// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over | |||||
//if (this.constants.keyboard.bindToWindow == false && this.constants.keyboard.enabled == true) { | |||||
// this.canvas.frame.focus(); | |||||
//} | |||||
// | |||||
//// start a timeout that will check if the mouse is positioned above an element | |||||
//if (popupVisible === false) { | |||||
// var me = this; | |||||
// var checkShow = function() { | |||||
// me._checkShowPopup(pointer); | |||||
// }; | |||||
// | |||||
// if (this.popupTimer) { | |||||
// clearInterval(this.popupTimer); // stop any running calculationTimer | |||||
// } | |||||
// if (!this.drag.dragging) { | |||||
// this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay); | |||||
// } | |||||
//} | |||||
// | |||||
///** | |||||
// * Adding hover highlights | |||||
// */ | |||||
//if (this.constants.hover == true) { | |||||
// // removing all hover highlights | |||||
// for (var edgeId in this.hoverObj.edges) { | |||||
// if (this.hoverObj.edges.hasOwnProperty(edgeId)) { | |||||
// this.hoverObj.edges[edgeId].hover = false; | |||||
// delete this.hoverObj.edges[edgeId]; | |||||
// } | |||||
// } | |||||
// | |||||
// // adding hover highlights | |||||
// var obj = this._getNodeAt(pointer); | |||||
// if (obj == null) { | |||||
// obj = this._getEdgeAt(pointer); | |||||
// } | |||||
// if (obj != null) { | |||||
// this._hoverObject(obj); | |||||
// } | |||||
// | |||||
// // removing all node hover highlights except for the selected one. | |||||
// for (var nodeId in this.hoverObj.nodes) { | |||||
// if (this.hoverObj.nodes.hasOwnProperty(nodeId)) { | |||||
// if (obj instanceof Node && obj.id != nodeId || obj instanceof Edge || obj == null) { | |||||
// this._blurObject(this.hoverObj.nodes[nodeId]); | |||||
// delete this.hoverObj.nodes[nodeId]; | |||||
// } | |||||
// } | |||||
// } | |||||
// this.redraw(); | |||||
//} | |||||
} | |||||
} | |||||
export {TouchEventHandler}; |
@ -0,0 +1,342 @@ | |||||
/** | |||||
* Created by Alex on 26-Feb-15. | |||||
*/ | |||||
var util = require('../../util'); | |||||
class View { | |||||
constructor(body, options) { | |||||
this.body = body; | |||||
this.setOptions(options); | |||||
this.animationSpeed = 1/this.renderRefreshRate; | |||||
this.animationEasingFunction = "easeInOutQuint"; | |||||
this.easingTime = 0; | |||||
this.sourceScale = 0; | |||||
this.targetScale = 0; | |||||
this.sourceTranslation = 0; | |||||
this.targetTranslation = 0; | |||||
this.lockedOnNodeId = null; | |||||
this.lockedOnNodeOffset = null; | |||||
this.touchTime = 0; | |||||
this.translation = {x: 0, y: 0}; | |||||
this.scale = 1.0; | |||||
this.viewFunction = undefined; | |||||
this.body.emitter.on("zoomExtent", this.zoomExtent.bind(this)); | |||||
this.body.emitter.on("_setScale", (scale) => this.scale = scale); | |||||
this.body.emitter.on("_setTranslation", (translation) => {this.translation.x = translation.x; this.translation.y = translation.y;}); | |||||
this.body.emitter.on("animationFinished", () => {this.body.emitter.emit("_stopRendering");}); | |||||
this.body.emitter.on("unlockNode", this.releaseNode.bind(this)); | |||||
} | |||||
setOptions(options = {}) { | |||||
this.options = options; | |||||
} | |||||
setCanvas(canvas) { | |||||
this.canvas = canvas; | |||||
} | |||||
// zoomExtent | |||||
/** | |||||
* Find the center position of the network | |||||
* @private | |||||
*/ | |||||
_getRange(specificNodes = []) { | |||||
var minY = 1e9, maxY = -1e9, minX = 1e9, maxX = -1e9, node; | |||||
if (specificNodes.length > 0) { | |||||
for (var i = 0; i < specificNodes.length; i++) { | |||||
node = this.body.nodes[specificNodes[i]]; | |||||
if (minX > (node.boundingBox.left)) { | |||||
minX = node.boundingBox.left; | |||||
} | |||||
if (maxX < (node.boundingBox.right)) { | |||||
maxX = node.boundingBox.right; | |||||
} | |||||
if (minY > (node.boundingBox.bottom)) { | |||||
minY = node.boundingBox.top; | |||||
} // top is negative, bottom is positive | |||||
if (maxY < (node.boundingBox.top)) { | |||||
maxY = node.boundingBox.bottom; | |||||
} // top is negative, bottom is positive | |||||
} | |||||
} | |||||
else { | |||||
for (var nodeId in this.body.nodes) { | |||||
if (this.body.nodes.hasOwnProperty(nodeId)) { | |||||
node = this.body.nodes[nodeId]; | |||||
if (minX > (node.boundingBox.left)) { | |||||
minX = node.boundingBox.left; | |||||
} | |||||
if (maxX < (node.boundingBox.right)) { | |||||
maxX = node.boundingBox.right; | |||||
} | |||||
if (minY > (node.boundingBox.bottom)) { | |||||
minY = node.boundingBox.top; | |||||
} // top is negative, bottom is positive | |||||
if (maxY < (node.boundingBox.top)) { | |||||
maxY = node.boundingBox.bottom; | |||||
} // top is negative, bottom is positive | |||||
} | |||||
} | |||||
} | |||||
if (minX == 1e9 && maxX == -1e9 && minY == 1e9 && maxY == -1e9) { | |||||
minY = 0, maxY = 0, minX = 0, maxX = 0; | |||||
} | |||||
return {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; | |||||
} | |||||
/** | |||||
* @param {object} range = {minX: minX, maxX: maxX, minY: minY, maxY: maxY}; | |||||
* @returns {{x: number, y: number}} | |||||
* @private | |||||
*/ | |||||
_findCenter(range) { | |||||
return {x: (0.5 * (range.maxX + range.minX)), | |||||
y: (0.5 * (range.maxY + range.minY))}; | |||||
} | |||||
/** | |||||
* This function zooms out to fit all data on screen based on amount of nodes | |||||
* @param {Object} | |||||
* @param {Boolean} [initialZoom] | zoom based on fitted formula or range, true = fitted, default = false; | |||||
* @param {Boolean} [disableStart] | If true, start is not called. | |||||
*/ | |||||
zoomExtent(options = {nodes:[]}, initialZoom = false) { | |||||
var range; | |||||
var zoomLevel; | |||||
if (initialZoom == true) { | |||||
// check if more than half of the nodes have a predefined position. If so, we use the range, not the approximation. | |||||
var positionDefined = 0; | |||||
for (var nodeId in this.body.nodes) { | |||||
if (this.body.nodes.hasOwnProperty(nodeId)) { | |||||
var node = this.body.nodes[nodeId]; | |||||
if (node.predefinedPosition == true) { | |||||
positionDefined += 1; | |||||
} | |||||
} | |||||
} | |||||
if (positionDefined > 0.5 * this.body.nodeIndices.length) { | |||||
this.zoomExtent(options,false); | |||||
return; | |||||
} | |||||
range = this._getRange(options.nodes); | |||||
var numberOfNodes = this.body.nodeIndices.length; | |||||
if (this.options.smoothCurves == true) { | |||||
zoomLevel = 12.662 / (numberOfNodes + 7.4147) + 0.0964822; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. | |||||
} | |||||
else { | |||||
zoomLevel = 30.5062972 / (numberOfNodes + 19.93597763) + 0.08413486; // this is obtained from fitting a dataset from 5 points with scale levels that looked good. | |||||
} | |||||
// correct for larger canvasses. | |||||
var factor = Math.min(this.canvas.frame.canvas.clientWidth / 600, this.canvas.frame.canvas.clientHeight / 600); | |||||
zoomLevel *= factor; | |||||
} | |||||
else { | |||||
this.body.emitter.emit("_redrawHidden"); | |||||
range = this._getRange(options.nodes); | |||||
var xDistance = Math.abs(range.maxX - range.minX) * 1.1; | |||||
var yDistance = Math.abs(range.maxY - range.minY) * 1.1; | |||||
var xZoomLevel = this.canvas.frame.canvas.clientWidth / xDistance; | |||||
var yZoomLevel = this.canvas.frame.canvas.clientHeight / yDistance; | |||||
zoomLevel = (xZoomLevel <= yZoomLevel) ? xZoomLevel : yZoomLevel; | |||||
} | |||||
if (zoomLevel > 1.0) { | |||||
zoomLevel = 1.0; | |||||
} | |||||
var center = this._findCenter(range); | |||||
var animationOptions = {position: center, scale: zoomLevel, animation: options}; | |||||
this.moveTo(animationOptions); | |||||
} | |||||
// animation | |||||
/** | |||||
* Center a node in view. | |||||
* | |||||
* @param {Number} nodeId | |||||
* @param {Number} [options] | |||||
*/ | |||||
focusOnNode(nodeId, options = {}) { | |||||
if (this.body.nodes[nodeId] !== undefined) { | |||||
var nodePosition = {x: this.body.nodes[nodeId].x, y: this.body.nodes[nodeId].y}; | |||||
options.position = nodePosition; | |||||
options.lockedOnNode = nodeId; | |||||
this.moveTo(options) | |||||
} | |||||
else { | |||||
console.log("Node: " + nodeId + " cannot be found."); | |||||
} | |||||
} | |||||
/** | |||||
* | |||||
* @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels | |||||
* | options.scale = Number // scale to move to | |||||
* | options.position = {x:Number, y:Number} // position to move to | |||||
* | options.animation = {duration:Number, easingFunction:String} || Boolean // position to move to | |||||
*/ | |||||
moveTo(options) { | |||||
if (options === undefined) { | |||||
options = {}; | |||||
return; | |||||
} | |||||
if (options.offset === undefined) {options.offset = {x: 0, y: 0}; } | |||||
if (options.offset.x === undefined) {options.offset.x = 0; } | |||||
if (options.offset.y === undefined) {options.offset.y = 0; } | |||||
if (options.scale === undefined) {options.scale = this.scale; } | |||||
if (options.position === undefined) {options.position = this.translation;} | |||||
if (options.animation === undefined) {options.animation = {duration:0}; } | |||||
if (options.animation === false ) {options.animation = {duration:0}; } | |||||
if (options.animation === true ) {options.animation = {}; } | |||||
if (options.animation.duration === undefined) {options.animation.duration = 1000; } // default duration | |||||
if (options.animation.easingFunction === undefined) {options.animation.easingFunction = "easeInOutQuad"; } // default easing function | |||||
this.animateView(options); | |||||
} | |||||
/** | |||||
* | |||||
* @param {Object} options | options.offset = {x:Number, y:Number} // offset from the center in DOM pixels | |||||
* | options.time = Number // animation time in milliseconds | |||||
* | options.scale = Number // scale to animate to | |||||
* | options.position = {x:Number, y:Number} // position to animate to | |||||
* | options.easingFunction = String // linear, easeInQuad, easeOutQuad, easeInOutQuad, | |||||
* // easeInCubic, easeOutCubic, easeInOutCubic, | |||||
* // easeInQuart, easeOutQuart, easeInOutQuart, | |||||
* // easeInQuint, easeOutQuint, easeInOutQuint | |||||
*/ | |||||
animateView(options) { | |||||
if (options === undefined) { | |||||
return; | |||||
} | |||||
this.animationEasingFunction = options.animation.easingFunction; | |||||
// release if something focussed on the node | |||||
this.releaseNode(); | |||||
if (options.locked == true) { | |||||
this.lockedOnNodeId = options.lockedOnNode; | |||||
this.lockedOnNodeOffset = options.offset; | |||||
} | |||||
// forcefully complete the old animation if it was still running | |||||
if (this.easingTime != 0) { | |||||
this._transitionRedraw(true); // by setting easingtime to 1, we finish the animation. | |||||
} | |||||
this.sourceScale = this.scale; | |||||
this.sourceTranslation = this.translation; | |||||
this.targetScale = options.scale; | |||||
// set the scale so the viewCenter is based on the correct zoom level. This is overridden in the transitionRedraw | |||||
// but at least then we'll have the target transition | |||||
this.body.emitter.emit("_setScale",this.targetScale); | |||||
var viewCenter = this.canvas.DOMtoCanvas({x: 0.5 * this.canvas.frame.canvas.clientWidth, y: 0.5 * this.canvas.frame.canvas.clientHeight}); | |||||
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node | |||||
x: viewCenter.x - options.position.x, | |||||
y: viewCenter.y - options.position.y | |||||
}; | |||||
this.targetTranslation = { | |||||
x: this.sourceTranslation.x + distanceFromCenter.x * this.targetScale + options.offset.x, | |||||
y: this.sourceTranslation.y + distanceFromCenter.y * this.targetScale + options.offset.y | |||||
}; | |||||
// if the time is set to 0, don't do an animation | |||||
if (options.animation.duration == 0) { | |||||
if (this.lockedOnNodeId != null) { | |||||
this.viewFunction = this._lockedRedraw.bind(this); | |||||
this.body.emitter.on("_beforeRender", this.viewFunction); | |||||
} | |||||
else { | |||||
this.body.emitter.emit("_setScale", this.targetScale);; | |||||
this.body.emitter.emit("_setTranslation", this.targetTranslation); | |||||
this.body.emitter.emit("_requestRedraw"); | |||||
} | |||||
} | |||||
else { | |||||
this.animationSpeed = 1 / (60 * options.animation.duration * 0.001) || 1 / 60; // 60 for 60 seconds, 0.001 for milli's | |||||
this.animationEasingFunction = options.animation.easingFunction; | |||||
this.viewFunction = this._transitionRedraw.bind(this); | |||||
this.body.emitter.on("_beforeRender", this.viewFunction); | |||||
this.body.emitter.emit("_startRendering"); | |||||
} | |||||
} | |||||
/** | |||||
* used to animate smoothly by hijacking the redraw function. | |||||
* @private | |||||
*/ | |||||
_lockedRedraw() { | |||||
var nodePosition = {x: this.body.nodes[this.lockedOnNodeId].x, y: this.body.nodes[this.lockedOnNodeId].y}; | |||||
var viewCenter = this.DOMtoCanvas({x: 0.5 * this.frame.canvas.clientWidth, y: 0.5 * this.frame.canvas.clientHeight}); | |||||
var distanceFromCenter = { // offset from view, distance view has to change by these x and y to center the node | |||||
x: viewCenter.x - nodePosition.x, | |||||
y: viewCenter.y - nodePosition.y | |||||
}; | |||||
var sourceTranslation = this.translation; | |||||
var targetTranslation = { | |||||
x: sourceTranslation.x + distanceFromCenter.x * this.scale + this.lockedOnNodeOffset.x, | |||||
y: sourceTranslation.y + distanceFromCenter.y * this.scale + this.lockedOnNodeOffset.y | |||||
}; | |||||
this.body.emitter.emit("_setTranslation", targetTranslation); | |||||
} | |||||
releaseNode() { | |||||
if (this.lockedOnNodeId !== undefined) { | |||||
this.body.emitter.off("_beforeRender", this.viewFunction); | |||||
this.lockedOnNodeId = undefined; | |||||
this.lockedOnNodeOffset = undefined; | |||||
} | |||||
} | |||||
/** | |||||
* | |||||
* @param easingTime | |||||
* @private | |||||
*/ | |||||
_transitionRedraw(finished = false) { | |||||
this.easingTime += this.animationSpeed; | |||||
this.easingTime = finished === true ? 1.0 : this.easingTime; | |||||
var progress = util.easingFunctions[this.animationEasingFunction](this.easingTime); | |||||
this.body.emitter.emit("_setScale", this.sourceScale + (this.targetScale - this.sourceScale) * progress); | |||||
this.body.emitter.emit("_setTranslation", { | |||||
x: this.sourceTranslation.x + (this.targetTranslation.x - this.sourceTranslation.x) * progress, | |||||
y: this.sourceTranslation.y + (this.targetTranslation.y - this.sourceTranslation.y) * progress | |||||
}); | |||||
// cleanup | |||||
if (this.easingTime >= 1.0) { | |||||
this.body.emitter.off("_beforeRender", this.viewFunction); | |||||
this.easingTime = 0; | |||||
if (this.lockedOnNodeId != null) { | |||||
this.viewFunction = this._lockedRedraw.bind(this); | |||||
this.body.emitter.on("_beforeRender", this.viewFunction); | |||||
} | |||||
this.body.emitter.emit("animationFinished"); | |||||
} | |||||
}; | |||||
} | |||||
export {View}; |
@ -0,0 +1,640 @@ | |||||
/** | |||||
* Created by Alex on 2/27/2015. | |||||
*/ | |||||
var Node = require("../../Node"); | |||||
class SelectionHandler { | |||||
constructor(body) { | |||||
this.body = body; | |||||
this.selectionObj = {nodes:[], edges:[]}; | |||||
this.options = { | |||||
select: true, | |||||
selectConnectedEdges: true | |||||
} | |||||
} | |||||
setCanvas(canvas) { | |||||
this.canvas = canvas; | |||||
} | |||||
/** | |||||
* handles the selection part of the tap; | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
selectOnPoint(pointer) { | |||||
if (this.options.select === true) { | |||||
if (this._getSelectedObjectCount() > 0) {this._unselectAll();} | |||||
this.selectObject(pointer); | |||||
this._generateClickEvent(pointer); | |||||
this.body.emitter.emit("_requestRedraw"); | |||||
} | |||||
} | |||||
_generateClickEvent(pointer) { | |||||
var properties = this.getSelection(); | |||||
properties['pointer'] = { | |||||
DOM: {x: pointer.x, y: pointer.y}, | |||||
canvas: this.canvas.DOMtoCanvas(pointer) | |||||
} | |||||
this.body.emitter.emit("click", properties); | |||||
} | |||||
selectObject(pointer) { | |||||
var obj = this._getNodeAt(pointer); | |||||
if (obj != null) { | |||||
if (this.options.selectConnectedEdges === true) { | |||||
this._selectConnectedEdges(obj); | |||||
} | |||||
} | |||||
else { | |||||
obj = this._getEdgeAt(pointer); | |||||
} | |||||
if (obj !== null) { | |||||
obj.select(); | |||||
this._addToSelection(obj); | |||||
this.body.emitter.emit('selected', this.getSelection()) | |||||
} | |||||
return obj; | |||||
} | |||||
/** | |||||
* | |||||
* @param object | |||||
* @param overlappingNodes | |||||
* @private | |||||
*/ | |||||
_getNodesOverlappingWith(object, overlappingNodes) { | |||||
var nodes = this.body.nodes; | |||||
for (var nodeId in nodes) { | |||||
if (nodes.hasOwnProperty(nodeId)) { | |||||
if (nodes[nodeId].isOverlappingWith(object)) { | |||||
overlappingNodes.push(nodeId); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* retrieve all nodes overlapping with given object | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
_getAllNodesOverlappingWith(object) { | |||||
var overlappingNodes = []; | |||||
this._getNodesOverlappingWith(object,overlappingNodes); | |||||
return overlappingNodes; | |||||
} | |||||
/** | |||||
* Return a position object in canvasspace from a single point in screenspace | |||||
* | |||||
* @param pointer | |||||
* @returns {{left: number, top: number, right: number, bottom: number}} | |||||
* @private | |||||
*/ | |||||
_pointerToPositionObject(pointer) { | |||||
var canvasPos = this.canvas.DOMtoCanvas(pointer); | |||||
return { | |||||
left: canvasPos.x, | |||||
top: canvasPos.y, | |||||
right: canvasPos.x, | |||||
bottom: canvasPos.y | |||||
}; | |||||
} | |||||
/** | |||||
* Get the top node at the a specific point (like a click) | |||||
* | |||||
* @param {{x: Number, y: Number}} pointer | |||||
* @return {Node | null} node | |||||
* @private | |||||
*/ | |||||
_getNodeAt(pointer) { | |||||
// we first check if this is an navigation controls element | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingNodes = this._getAllNodesOverlappingWith(positionObject); | |||||
// if there are overlapping nodes, select the last one, this is the | |||||
// one which is drawn on top of the others | |||||
if (overlappingNodes.length > 0) { | |||||
return this.body.nodes[overlappingNodes[overlappingNodes.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
} | |||||
/** | |||||
* retrieve all edges overlapping with given object, selector is around center | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
_getEdgesOverlappingWith(object, overlappingEdges) { | |||||
var edges = this.body.edges; | |||||
for (var edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
if (edges[edgeId].isOverlappingWith(object)) { | |||||
overlappingEdges.push(edgeId); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* retrieve all nodes overlapping with given object | |||||
* @param {Object} object An object with parameters left, top, right, bottom | |||||
* @return {Number[]} An array with id's of the overlapping nodes | |||||
* @private | |||||
*/ | |||||
_getAllEdgesOverlappingWith(object) { | |||||
var overlappingEdges = []; | |||||
this._getEdgesOverlappingWith(object,overlappingEdges); | |||||
return overlappingEdges; | |||||
} | |||||
/** | |||||
* Place holder. To implement change the _getNodeAt to a _getObjectAt. Have the _getObjectAt call | |||||
* _getNodeAt and _getEdgesAt, then priortize the selection to user preferences. | |||||
* | |||||
* @param pointer | |||||
* @returns {null} | |||||
* @private | |||||
*/ | |||||
_getEdgeAt(pointer) { | |||||
var positionObject = this._pointerToPositionObject(pointer); | |||||
var overlappingEdges = this._getAllEdgesOverlappingWith(positionObject); | |||||
if (overlappingEdges.length > 0) { | |||||
return this.body.edges[overlappingEdges[overlappingEdges.length - 1]]; | |||||
} | |||||
else { | |||||
return null; | |||||
} | |||||
} | |||||
/** | |||||
* Add object to the selection array. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
_addToSelection(obj) { | |||||
if (obj instanceof Node) { | |||||
this.selectionObj.nodes[obj.id] = obj; | |||||
} | |||||
else { | |||||
this.selectionObj.edges[obj.id] = obj; | |||||
} | |||||
} | |||||
/** | |||||
* Add object to the selection array. | |||||
* | |||||
* @param obj | |||||
* @private | |||||
*/ | |||||
_addToHover(obj) { | |||||
if (obj instanceof Node) { | |||||
this.hoverObj.nodes[obj.id] = obj; | |||||
} | |||||
else { | |||||
this.hoverObj.edges[obj.id] = obj; | |||||
} | |||||
} | |||||
/** | |||||
* Remove a single option from selection. | |||||
* | |||||
* @param {Object} obj | |||||
* @private | |||||
*/ | |||||
_removeFromSelection(obj) { | |||||
if (obj instanceof Node) { | |||||
delete this.selectionObj.nodes[obj.id]; | |||||
} | |||||
else { | |||||
delete this.selectionObj.edges[obj.id]; | |||||
} | |||||
} | |||||
/** | |||||
* Unselect all. The selectionObj is useful for this. | |||||
* | |||||
* @param {Boolean} [doNotTrigger] | ignore trigger | |||||
* @private | |||||
*/ | |||||
_unselectAll(doNotTrigger = false) { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
this.selectionObj.nodes[nodeId].unselect(); | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
this.selectionObj.edges[edgeId].unselect(); | |||||
} | |||||
} | |||||
this.selectionObj = {nodes:{},edges:{}}; | |||||
if (doNotTrigger == false) { | |||||
this.body.emitter.emit('select', this.getSelection()); | |||||
} | |||||
} | |||||
/** | |||||
* return the number of selected nodes | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedNodeCount() { | |||||
var count = 0; | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
} | |||||
/** | |||||
* return the selected node | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedNode() { | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* return the selected edge | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedEdge() { | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
return this.selectionObj.edges[edgeId]; | |||||
} | |||||
} | |||||
return null; | |||||
} | |||||
/** | |||||
* return the number of selected edges | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedEdgeCount() { | |||||
var count = 0; | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
} | |||||
/** | |||||
* return the number of selected objects. | |||||
* | |||||
* @returns {number} | |||||
* @private | |||||
*/ | |||||
_getSelectedObjectCount() { | |||||
var count = 0; | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
count += 1; | |||||
} | |||||
} | |||||
return count; | |||||
} | |||||
/** | |||||
* Check if anything is selected | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
_selectionIsEmpty() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
for(var edgeId in this.selectionObj.edges) { | |||||
if(this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
return false; | |||||
} | |||||
} | |||||
return true; | |||||
} | |||||
/** | |||||
* check if one of the selected nodes is a cluster. | |||||
* | |||||
* @returns {boolean} | |||||
* @private | |||||
*/ | |||||
_clusterInSelection() { | |||||
for(var nodeId in this.selectionObj.nodes) { | |||||
if(this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (this.selectionObj.nodes[nodeId].clusterSize > 1) { | |||||
return true; | |||||
} | |||||
} | |||||
} | |||||
return false; | |||||
} | |||||
/** | |||||
* select the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
_selectConnectedEdges(node) { | |||||
for (var i = 0; i < node.edges.length; i++) { | |||||
var edge = node.edges[i]; | |||||
edge.select(); | |||||
this._addToSelection(edge); | |||||
} | |||||
} | |||||
/** | |||||
* select the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
_hoverConnectedEdges(node) { | |||||
for (var i = 0; i < node.edges.length; i++) { | |||||
var edge = node.edges[i]; | |||||
edge.hover = true; | |||||
this._addToHover(edge); | |||||
} | |||||
} | |||||
/** | |||||
* unselect the edges connected to the node that is being selected | |||||
* | |||||
* @param {Node} node | |||||
* @private | |||||
*/ | |||||
_unselectConnectedEdges(node) { | |||||
for (var i = 0; i < node.edges.length; i++) { | |||||
var edge = node.edges[i]; | |||||
edge.unselect(); | |||||
this._removeFromSelection(edge); | |||||
} | |||||
} | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @private | |||||
*/ | |||||
_blurObject(object) { | |||||
if (object.hover == true) { | |||||
object.hover = false; | |||||
this.body.emitter.emit("blurNode",{node:object.id}); | |||||
} | |||||
} | |||||
/** | |||||
* This is called when someone clicks on a node. either select or deselect it. | |||||
* If there is an existing selection and we don't want to append to it, clear the existing selection | |||||
* | |||||
* @param {Node || Edge} object | |||||
* @private | |||||
*/ | |||||
_hoverObject(object) { | |||||
if (object.hover == false) { | |||||
object.hover = true; | |||||
this._addToHover(object); | |||||
if (object instanceof Node) { | |||||
this.body.emitter.emit("hoverNode",{node:object.id}); | |||||
} | |||||
} | |||||
if (object instanceof Node) { | |||||
this._hoverConnectedEdges(object); | |||||
} | |||||
} | |||||
/** | |||||
* handles the selection part of the double tap and opens a cluster if needed | |||||
* | |||||
* @param {Object} pointer | |||||
* @private | |||||
*/ | |||||
_handleDoubleTap(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null && node !== undefined) { | |||||
// we reset the areaCenter here so the opening of the node will occur | |||||
this.areaCenter = {"x" : this._XconvertDOMtoCanvas(pointer.x), | |||||
"y" : this._YconvertDOMtoCanvas(pointer.y)}; | |||||
this.openCluster(node); | |||||
} | |||||
var properties = this.getSelection(); | |||||
properties['pointer'] = { | |||||
DOM: {x: pointer.x, y: pointer.y}, | |||||
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)} | |||||
} | |||||
this.body.emitter.emit("doubleClick", properties); | |||||
} | |||||
/** | |||||
* Handle the onHold selection part | |||||
* | |||||
* @param pointer | |||||
* @private | |||||
*/ | |||||
_handleOnHold(pointer) { | |||||
var node = this._getNodeAt(pointer); | |||||
if (node != null) { | |||||
this._selectObject(node,true); | |||||
} | |||||
else { | |||||
var edge = this._getEdgeAt(pointer); | |||||
if (edge != null) { | |||||
this._selectObject(edge,true); | |||||
} | |||||
} | |||||
this._requestRedraw(); | |||||
} | |||||
/** | |||||
* | |||||
* retrieve the currently selected objects | |||||
* @return {{nodes: Array.<String>, edges: Array.<String>}} selection | |||||
*/ | |||||
getSelection() { | |||||
var nodeIds = this.getSelectedNodes(); | |||||
var edgeIds = this.getSelectedEdges(); | |||||
return {nodes:nodeIds, edges:edgeIds}; | |||||
} | |||||
/** | |||||
* | |||||
* retrieve the currently selected nodes | |||||
* @return {String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelectedNodes() { | |||||
var idArray = []; | |||||
if (this.options.select == true) { | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
idArray.push(nodeId); | |||||
} | |||||
} | |||||
} | |||||
return idArray | |||||
} | |||||
/** | |||||
* | |||||
* retrieve the currently selected edges | |||||
* @return {Array} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
getSelectedEdges() { | |||||
var idArray = []; | |||||
if (this.options.select == true) { | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
idArray.push(edgeId); | |||||
} | |||||
} | |||||
} | |||||
return idArray; | |||||
} | |||||
/** | |||||
* select zero or more nodes with the option to highlight edges | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
* @param {boolean} [highlightEdges] | |||||
*/ | |||||
selectNodes(selection, highlightEdges) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var node = this.body.nodes[id]; | |||||
if (!node) { | |||||
throw new RangeError('Node with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(node,true,true,highlightEdges,true); | |||||
} | |||||
this.redraw(); | |||||
} | |||||
/** | |||||
* select zero or more edges | |||||
* @param {Number[] | String[]} selection An array with the ids of the | |||||
* selected nodes. | |||||
*/ | |||||
selectEdges(selection) { | |||||
var i, iMax, id; | |||||
if (!selection || (selection.length == undefined)) | |||||
throw 'Selection must be an array with ids'; | |||||
// first unselect any selected node | |||||
this._unselectAll(true); | |||||
for (i = 0, iMax = selection.length; i < iMax; i++) { | |||||
id = selection[i]; | |||||
var edge = this.body.edges[id]; | |||||
if (!edge) { | |||||
throw new RangeError('Edge with id "' + id + '" not found'); | |||||
} | |||||
this._selectObject(edge,true,true,false,true); | |||||
} | |||||
this.redraw(); | |||||
} | |||||
/** | |||||
* Validate the selection: remove ids of nodes which no longer exist | |||||
* @private | |||||
*/ | |||||
_updateSelection() { | |||||
for (var nodeId in this.selectionObj.nodes) { | |||||
if (this.selectionObj.nodes.hasOwnProperty(nodeId)) { | |||||
if (!this.body.nodes.hasOwnProperty(nodeId)) { | |||||
delete this.selectionObj.nodes[nodeId]; | |||||
} | |||||
} | |||||
} | |||||
for (var edgeId in this.selectionObj.edges) { | |||||
if (this.selectionObj.edges.hasOwnProperty(edgeId)) { | |||||
if (!this.body.edges.hasOwnProperty(edgeId)) { | |||||
delete this.selectionObj.edges[edgeId]; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
export {SelectionHandler}; |
@ -0,0 +1,446 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
class BarnesHutSolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.barnesHutTree; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* This function calculates the forces the nodes apply on eachother based on a gravitational model. | |||||
* The Barnes Hut method is used to speed up this N-body simulation. | |||||
* | |||||
* @private | |||||
*/ | |||||
solve() { | |||||
if (this.options.gravitationalConstant != 0) { | |||||
var node; | |||||
var nodes = this.physicsBody.calculationNodes; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var nodeCount = nodeIndices.length; | |||||
// create the tree | |||||
var barnesHutTree = this._formBarnesHutTree(nodes, nodeIndices); | |||||
// for debugging | |||||
this.barnesHutTree = barnesHutTree; | |||||
// place the nodes one by one recursively | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
if (node.options.mass > 0) { | |||||
// starting with root is irrelevant, it never passes the BarnesHutSolver condition | |||||
this._getForceContribution(barnesHutTree.root.children.NW, node); | |||||
this._getForceContribution(barnesHutTree.root.children.NE, node); | |||||
this._getForceContribution(barnesHutTree.root.children.SW, node); | |||||
this._getForceContribution(barnesHutTree.root.children.SE, node); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass. | |||||
* If a region contains a single node, we check if it is not itself, then we apply the force. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
_getForceContribution(parentBranch, node) { | |||||
// we get no force contribution from an empty region | |||||
if (parentBranch.childrenCount > 0) { | |||||
var dx, dy, distance; | |||||
// get the distance from the center of mass to the node. | |||||
dx = parentBranch.centerOfMass.x - node.x; | |||||
dy = parentBranch.centerOfMass.y - node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// BarnesHutSolver condition | |||||
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed | |||||
// calcSize = 1/s --> d * 1/s > 1/theta = passed | |||||
if (distance * parentBranch.calcSize > this.options.thetaInverted) { | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.1 * Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
this.physicsBody.forces[node.id].x += fx; | |||||
this.physicsBody.forces[node.id].y += fy; | |||||
} | |||||
else { | |||||
// Did not pass the condition, go into children if available | |||||
if (parentBranch.childrenCount == 4) { | |||||
this._getForceContribution(parentBranch.children.NW, node); | |||||
this._getForceContribution(parentBranch.children.NE, node); | |||||
this._getForceContribution(parentBranch.children.SW, node); | |||||
this._getForceContribution(parentBranch.children.SE, node); | |||||
} | |||||
else { // parentBranch must have only one node, if it was empty we wouldnt be here | |||||
if (parentBranch.children.data.id != node.id) { // if it is not self | |||||
// duplicate code to reduce function calls to speed up program | |||||
if (distance == 0) { | |||||
distance = 0.5 * Math.random(); | |||||
dx = distance; | |||||
} | |||||
var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance); | |||||
var fx = dx * gravityForce; | |||||
var fy = dy * gravityForce; | |||||
this.physicsBody.forces[node.id].x += fx; | |||||
this.physicsBody.forces[node.id].y += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes. | |||||
* | |||||
* @param nodes | |||||
* @param nodeIndices | |||||
* @private | |||||
*/ | |||||
_formBarnesHutTree(nodes, nodeIndices) { | |||||
var node; | |||||
var nodeCount = nodeIndices.length; | |||||
var minX = Number.MAX_VALUE, | |||||
minY = Number.MAX_VALUE, | |||||
maxX = -Number.MAX_VALUE, | |||||
maxY = -Number.MAX_VALUE; | |||||
// get the range of the nodes | |||||
for (var i = 0; i < nodeCount; i++) { | |||||
var x = nodes[nodeIndices[i]].x; | |||||
var y = nodes[nodeIndices[i]].y; | |||||
if (nodes[nodeIndices[i]].options.mass > 0) { | |||||
if (x < minX) { | |||||
minX = x; | |||||
} | |||||
if (x > maxX) { | |||||
maxX = x; | |||||
} | |||||
if (y < minY) { | |||||
minY = y; | |||||
} | |||||
if (y > maxY) { | |||||
maxY = y; | |||||
} | |||||
} | |||||
} | |||||
// make the range a square | |||||
var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y | |||||
if (sizeDiff > 0) { | |||||
minY -= 0.5 * sizeDiff; | |||||
maxY += 0.5 * sizeDiff; | |||||
} // xSize > ySize | |||||
else { | |||||
minX += 0.5 * sizeDiff; | |||||
maxX -= 0.5 * sizeDiff; | |||||
} // xSize < ySize | |||||
var minimumTreeSize = 1e-5; | |||||
var rootSize = Math.max(minimumTreeSize, Math.abs(maxX - minX)); | |||||
var halfRootSize = 0.5 * rootSize; | |||||
var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY); | |||||
// construct the barnesHutTree | |||||
var barnesHutTree = { | |||||
root: { | |||||
centerOfMass: {x: 0, y: 0}, | |||||
mass: 0, | |||||
range: { | |||||
minX: centerX - halfRootSize, maxX: centerX + halfRootSize, | |||||
minY: centerY - halfRootSize, maxY: centerY + halfRootSize | |||||
}, | |||||
size: rootSize, | |||||
calcSize: 1 / rootSize, | |||||
children: {data: null}, | |||||
maxWidth: 0, | |||||
level: 0, | |||||
childrenCount: 4 | |||||
} | |||||
}; | |||||
this._splitBranch(barnesHutTree.root); | |||||
// place the nodes one by one recursively | |||||
for (i = 0; i < nodeCount; i++) { | |||||
node = nodes[nodeIndices[i]]; | |||||
if (node.options.mass > 0) { | |||||
this._placeInTree(barnesHutTree.root, node); | |||||
} | |||||
} | |||||
// make global | |||||
return barnesHutTree | |||||
} | |||||
/** | |||||
* this updates the mass of a branch. this is increased by adding a node. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @private | |||||
*/ | |||||
_updateBranchMass(parentBranch, node) { | |||||
var totalMass = parentBranch.mass + node.options.mass; | |||||
var totalMassInv = 1 / totalMass; | |||||
parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass; | |||||
parentBranch.centerOfMass.x *= totalMassInv; | |||||
parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass; | |||||
parentBranch.centerOfMass.y *= totalMassInv; | |||||
parentBranch.mass = totalMass; | |||||
var biggestSize = Math.max(Math.max(node.height, node.radius), node.width); | |||||
parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth; | |||||
} | |||||
/** | |||||
* determine in which branch the node will be placed. | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param skipMassUpdate | |||||
* @private | |||||
*/ | |||||
_placeInTree(parentBranch, node, skipMassUpdate) { | |||||
if (skipMassUpdate != true || skipMassUpdate === undefined) { | |||||
// update the mass of the branch. | |||||
this._updateBranchMass(parentBranch, node); | |||||
} | |||||
if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NW | |||||
this._placeInRegion(parentBranch, node, "NW"); | |||||
} | |||||
else { // in SW | |||||
this._placeInRegion(parentBranch, node, "SW"); | |||||
} | |||||
} | |||||
else { // in NE or SE | |||||
if (parentBranch.children.NW.range.maxY > node.y) { // in NE | |||||
this._placeInRegion(parentBranch, node, "NE"); | |||||
} | |||||
else { // in SE | |||||
this._placeInRegion(parentBranch, node, "SE"); | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* actually place the node in a region (or branch) | |||||
* | |||||
* @param parentBranch | |||||
* @param node | |||||
* @param region | |||||
* @private | |||||
*/ | |||||
_placeInRegion(parentBranch, node, region) { | |||||
switch (parentBranch.children[region].childrenCount) { | |||||
case 0: // place node here | |||||
parentBranch.children[region].children.data = node; | |||||
parentBranch.children[region].childrenCount = 1; | |||||
this._updateBranchMass(parentBranch.children[region], node); | |||||
break; | |||||
case 1: // convert into children | |||||
// if there are two nodes exactly overlapping (on init, on opening of cluster etc.) | |||||
// we move one node a pixel and we do not put it in the tree. | |||||
if (parentBranch.children[region].children.data.x == node.x && | |||||
parentBranch.children[region].children.data.y == node.y) { | |||||
node.x += Math.random(); | |||||
node.y += Math.random(); | |||||
} | |||||
else { | |||||
this._splitBranch(parentBranch.children[region]); | |||||
this._placeInTree(parentBranch.children[region], node); | |||||
} | |||||
break; | |||||
case 4: // place in branch | |||||
this._placeInTree(parentBranch.children[region], node); | |||||
break; | |||||
} | |||||
} | |||||
/** | |||||
* this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch | |||||
* after the split is complete. | |||||
* | |||||
* @param parentBranch | |||||
* @private | |||||
*/ | |||||
_splitBranch(parentBranch) { | |||||
// if the branch is shaded with a node, replace the node in the new subset. | |||||
var containedNode = null; | |||||
if (parentBranch.childrenCount == 1) { | |||||
containedNode = parentBranch.children.data; | |||||
parentBranch.mass = 0; | |||||
parentBranch.centerOfMass.x = 0; | |||||
parentBranch.centerOfMass.y = 0; | |||||
} | |||||
parentBranch.childrenCount = 4; | |||||
parentBranch.children.data = null; | |||||
this._insertRegion(parentBranch, "NW"); | |||||
this._insertRegion(parentBranch, "NE"); | |||||
this._insertRegion(parentBranch, "SW"); | |||||
this._insertRegion(parentBranch, "SE"); | |||||
if (containedNode != null) { | |||||
this._placeInTree(parentBranch, containedNode); | |||||
} | |||||
} | |||||
/** | |||||
* This function subdivides the region into four new segments. | |||||
* Specifically, this inserts a single new segment. | |||||
* It fills the children section of the parentBranch | |||||
* | |||||
* @param parentBranch | |||||
* @param region | |||||
* @param parentRange | |||||
* @private | |||||
*/ | |||||
_insertRegion(parentBranch, region) { | |||||
var minX, maxX, minY, maxY; | |||||
var childSize = 0.5 * parentBranch.size; | |||||
switch (region) { | |||||
case "NW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "NE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY; | |||||
maxY = parentBranch.range.minY + childSize; | |||||
break; | |||||
case "SW": | |||||
minX = parentBranch.range.minX; | |||||
maxX = parentBranch.range.minX + childSize; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
case "SE": | |||||
minX = parentBranch.range.minX + childSize; | |||||
maxX = parentBranch.range.maxX; | |||||
minY = parentBranch.range.minY + childSize; | |||||
maxY = parentBranch.range.maxY; | |||||
break; | |||||
} | |||||
parentBranch.children[region] = { | |||||
centerOfMass: {x: 0, y: 0}, | |||||
mass: 0, | |||||
range: {minX: minX, maxX: maxX, minY: minY, maxY: maxY}, | |||||
size: 0.5 * parentBranch.size, | |||||
calcSize: 2 * parentBranch.calcSize, | |||||
children: {data: null}, | |||||
maxWidth: 0, | |||||
level: parentBranch.level + 1, | |||||
childrenCount: 0 | |||||
}; | |||||
} | |||||
//--------------------------- DEBUGGING BELOW ---------------------------// | |||||
/** | |||||
* This function is for debugging purposed, it draws the tree. | |||||
* | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
_debug(ctx, color) { | |||||
if (this.barnesHutTree !== undefined) { | |||||
ctx.lineWidth = 1; | |||||
this._drawBranch(this.barnesHutTree.root, ctx, color); | |||||
} | |||||
} | |||||
/** | |||||
* This function is for debugging purposes. It draws the branches recursively. | |||||
* | |||||
* @param branch | |||||
* @param ctx | |||||
* @param color | |||||
* @private | |||||
*/ | |||||
_drawBranch(branch, ctx, color) { | |||||
if (color === undefined) { | |||||
color = "#FF0000"; | |||||
} | |||||
if (branch.childrenCount == 4) { | |||||
this._drawBranch(branch.children.NW, ctx); | |||||
this._drawBranch(branch.children.NE, ctx); | |||||
this._drawBranch(branch.children.SE, ctx); | |||||
this._drawBranch(branch.children.SW, ctx); | |||||
} | |||||
ctx.strokeStyle = color; | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX, branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX, branch.range.minY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX, branch.range.minY); | |||||
ctx.lineTo(branch.range.maxX, branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.maxX, branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX, branch.range.maxY); | |||||
ctx.stroke(); | |||||
ctx.beginPath(); | |||||
ctx.moveTo(branch.range.minX, branch.range.maxY); | |||||
ctx.lineTo(branch.range.minX, branch.range.minY); | |||||
ctx.stroke(); | |||||
/* | |||||
if (branch.mass > 0) { | |||||
ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass); | |||||
ctx.stroke(); | |||||
} | |||||
*/ | |||||
} | |||||
} | |||||
export {BarnesHutSolver}; |
@ -0,0 +1,40 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
class CentralGravitySolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
solve() { | |||||
var dx, dy, distance, node, i; | |||||
var nodes = this.physicsBody.calculationNodes; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var forces = this.physicsBody.forces; | |||||
var gravity = this.options.centralGravity; | |||||
var gravityForce = 0; | |||||
for (i = 0; i < nodeIndices.length; i++) { | |||||
let nodeId = nodeIndices[i]; | |||||
node = nodes[nodeId]; | |||||
dx = -node.x; | |||||
dy = -node.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
gravityForce = (distance == 0) ? 0 : (gravity / distance); | |||||
forces[nodeId].x = dx * gravityForce; | |||||
forces[nodeId].y = dy * gravityForce; | |||||
} | |||||
} | |||||
} | |||||
export {CentralGravitySolver}; |
@ -0,0 +1,73 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
class HierarchicalRepulsionSolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
solve() { | |||||
var dx, dy, distance, fx, fy, repulsingForce, node1, node2, i, j; | |||||
var nodes = this.physicsBody.calculationNodes; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var forces = this.physicsBody.forces; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.options.nodeDistance; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
// nodes only affect nodes on their level | |||||
if (node1.level == node2.level) { | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
var steepness = 0.05; | |||||
if (distance < nodeDistance) { | |||||
repulsingForce = -Math.pow(steepness * distance, 2) + Math.pow(steepness * nodeDistance, 2); | |||||
} | |||||
else { | |||||
repulsingForce = 0; | |||||
} | |||||
// normalize force with | |||||
if (distance == 0) { | |||||
distance = 0.01; | |||||
} | |||||
else { | |||||
repulsingForce = repulsingForce / distance; | |||||
} | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
forces[node1.id].x -= fx; | |||||
forces[node1.id].y -= fy; | |||||
forces[node2.id].x += fx; | |||||
forces[node2.id].y += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
export {HierarchicalRepulsionSolver}; |
@ -0,0 +1,104 @@ | |||||
/** | |||||
* Created by Alex on 2/25/2015. | |||||
*/ | |||||
class HierarchicalSpringSolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
solve() { | |||||
var edgeLength, edge, edgeId; | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
var edges = this.body.edges; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var forces = this.physicsBody.forces; | |||||
// initialize the spring force counters | |||||
for (let i = 0; i < nodeIndices.length; i++) { | |||||
let nodeId = nodeIndices[i]; | |||||
forces[nodeId].springFx = 0; | |||||
forces[nodeId].springFy = 0; | |||||
} | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected === true) { | |||||
edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; | |||||
dx = (edge.from.x - edge.to.x); | |||||
dy = (edge.from.y - edge.to.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
distance = distance == 0 ? 0.01 : distance; | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.options.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
if (edge.to.level != edge.from.level) { | |||||
forces[edge.toId].springFx -= fx; | |||||
forces[edge.toId].springFy -= fy; | |||||
forces[edge.fromId].springFx += fx; | |||||
forces[edge.fromId].springFy += fy; | |||||
} | |||||
else { | |||||
let factor = 0.5; | |||||
forces[edge.toId].x -= factor*fx; | |||||
forces[edge.toId].y -= factor*fy; | |||||
forces[edge.fromId].x += factor*fx; | |||||
forces[edge.fromId].y += factor*fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
// normalize spring forces | |||||
var springForce = 1; | |||||
var springFx, springFy; | |||||
for (let i = 0; i < nodeIndices.length; i++) { | |||||
let nodeId = nodeIndices[i]; | |||||
springFx = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFx)); | |||||
springFy = Math.min(springForce,Math.max(-springForce,forces[nodeId].springFy)); | |||||
forces[nodeId].x += springFx; | |||||
forces[nodeId].y += springFy; | |||||
} | |||||
// retain energy balance | |||||
var totalFx = 0; | |||||
var totalFy = 0; | |||||
for (let i = 0; i < nodeIndices.length; i++) { | |||||
let nodeId = nodeIndices[i]; | |||||
totalFx += forces[nodeId].x; | |||||
totalFy += forces[nodeId].y; | |||||
} | |||||
var correctionFx = totalFx / nodeIndices.length; | |||||
var correctionFy = totalFy / nodeIndices.length; | |||||
for (let i = 0; i < nodeIndices.length; i++) { | |||||
let nodeId = nodeIndices[i]; | |||||
forces[nodeId].x -= correctionFx; | |||||
forces[nodeId].y -= correctionFy; | |||||
} | |||||
} | |||||
} | |||||
export {HierarchicalSpringSolver}; |
@ -0,0 +1,75 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
class RepulsionSolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* Calculate the forces the nodes apply on each other based on a repulsion field. | |||||
* This field is linearly approximated. | |||||
* | |||||
* @private | |||||
*/ | |||||
solve() { | |||||
var dx, dy, distance, fx, fy, repulsingForce, node1, node2; | |||||
var nodes = this.physicsBody.calculationNodes; | |||||
var nodeIndices = this.physicsBody.calculationNodeIndices; | |||||
var forces = this.physicsBody.forces; | |||||
// repulsing forces between nodes | |||||
var nodeDistance = this.options.nodeDistance; | |||||
// approximation constants | |||||
var a = (-2 / 3) / nodeDistance; | |||||
var b = 4 / 3; | |||||
// we loop from i over all but the last entree in the array | |||||
// j loops from i+1 to the last. This way we do not double count any of the indices, nor i == j | |||||
for (let i = 0; i < nodeIndices.length - 1; i++) { | |||||
node1 = nodes[nodeIndices[i]]; | |||||
for (let j = i + 1; j < nodeIndices.length; j++) { | |||||
node2 = nodes[nodeIndices[j]]; | |||||
dx = node2.x - node1.x; | |||||
dy = node2.y - node1.y; | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
// same condition as BarnesHutSolver, making sure nodes are never 100% overlapping. | |||||
if (distance == 0) { | |||||
distance = 0.1*Math.random(); | |||||
dx = distance; | |||||
} | |||||
if (distance < 2 * nodeDistance) { | |||||
if (distance < 0.5 * nodeDistance) { | |||||
repulsingForce = 1.0; | |||||
} | |||||
else { | |||||
repulsingForce = a * distance + b; // linear approx of 1 / (1 + Math.exp((distance / nodeDistance - 1) * steepness)) | |||||
} | |||||
repulsingForce = repulsingForce / distance; | |||||
fx = dx * repulsingForce; | |||||
fy = dy * repulsingForce; | |||||
forces[node1.id].x -= fx; | |||||
forces[node1.id].y -= fy; | |||||
forces[node2.id].x += fx; | |||||
forces[node2.id].y += fy; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
export {RepulsionSolver}; |
@ -0,0 +1,80 @@ | |||||
/** | |||||
* Created by Alex on 2/23/2015. | |||||
*/ | |||||
class SpringSolver { | |||||
constructor(body, physicsBody, options) { | |||||
this.body = body; | |||||
this.physicsBody = physicsBody; | |||||
this.setOptions(options); | |||||
} | |||||
setOptions(options) { | |||||
this.options = options; | |||||
} | |||||
/** | |||||
* This function calculates the springforces on the nodes, accounting for the support nodes. | |||||
* | |||||
* @private | |||||
*/ | |||||
solve() { | |||||
var edgeLength, edge, edgeId; | |||||
var edges = this.body.edges; | |||||
// forces caused by the edges, modelled as springs | |||||
for (edgeId in edges) { | |||||
if (edges.hasOwnProperty(edgeId)) { | |||||
edge = edges[edgeId]; | |||||
if (edge.connected === true) { | |||||
// only calculate forces if nodes are in the same sector | |||||
if (this.body.nodes[edge.toId] !== undefined && this.body.nodes[edge.fromId] !== undefined) { | |||||
edgeLength = edge.properties.length === undefined ? this.options.springLength : edge.properties.length; | |||||
if (edge.via != null) { | |||||
var node1 = edge.to; | |||||
var node2 = edge.via; | |||||
var node3 = edge.from; | |||||
this._calculateSpringForce(node1, node2, 0.5 * edgeLength); | |||||
this._calculateSpringForce(node2, node3, 0.5 * edgeLength); | |||||
} | |||||
else { | |||||
this._calculateSpringForce(edge.from, edge.to, edgeLength); | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
} | |||||
/** | |||||
* This is the code actually performing the calculation for the function above. | |||||
* | |||||
* @param node1 | |||||
* @param node2 | |||||
* @param edgeLength | |||||
* @private | |||||
*/ | |||||
_calculateSpringForce(node1, node2, edgeLength) { | |||||
var dx, dy, fx, fy, springForce, distance; | |||||
dx = (node1.x - node2.x); | |||||
dy = (node1.y - node2.y); | |||||
distance = Math.sqrt(dx * dx + dy * dy); | |||||
distance = distance == 0 ? 0.01 : distance; | |||||
// the 1/distance is so the fx and fy can be calculated without sine or cosine. | |||||
springForce = this.options.springConstant * (edgeLength - distance) / distance; | |||||
fx = dx * springForce; | |||||
fy = dy * springForce; | |||||
this.physicsBody.forces[node1.id].x += fx; | |||||
this.physicsBody.forces[node1.id].y += fy; | |||||
this.physicsBody.forces[node2.id].x -= fx; | |||||
this.physicsBody.forces[node2.id].y -= fy; | |||||
} | |||||
} | |||||
export {SpringSolver}; |