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.smoothCurves == 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 == 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.customLength ? edge.length : this.constants.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.customLength ? edge.length : this.constants.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; }; /** * 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 hierarchicalLayoutDirections = ["LR", "RL", "UD", "DU"]; this.physicsConfiguration = document.createElement('div'); this.physicsConfiguration.className = "PhysicsConfiguration"; this.physicsConfiguration.innerHTML = '' + '' + '' + '' + '' + '' + '' + '
Simulation Mode:
Barnes HutRepulsionHierarchical
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
Options:
' 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) { 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 = !this.constants.smoothCurves; var graph_toggleSmooth = document.getElementById("graph_toggleSmooth"); if (this.constants.smoothCurves == 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(); } 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 != this.backupConstants.smoothCurves) { if (optionsSpecific.length == 0) {options = "var options = {";} else {options += ", "} options += "smoothCurves: " + this.constants.smoothCurves; } 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._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 == 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 (map instanceof Array) { 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(); }