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