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}; |