|
@ -2,15 +2,645 @@ |
|
|
* Created by Alex on 2/20/2015. |
|
|
* Created by Alex on 2/20/2015. |
|
|
*/ |
|
|
*/ |
|
|
|
|
|
|
|
|
var public = require("./clustering/public"); |
|
|
|
|
|
var support = require("./clustering/support"); |
|
|
|
|
|
var backend = require("./clustering/backend"); |
|
|
|
|
|
|
|
|
var Node = require('../Node'); |
|
|
|
|
|
var Edge = require('../Edge'); |
|
|
|
|
|
var util = require('../../util'); |
|
|
|
|
|
|
|
|
function ClusterEngine(network) { |
|
|
|
|
|
this.network = network; |
|
|
|
|
|
|
|
|
function ClusterEngine(data,options) { |
|
|
|
|
|
this.nodes = data.nodes; |
|
|
|
|
|
this.edges = data.edges; |
|
|
|
|
|
this.nodeIndices = data.nodeIndices; |
|
|
|
|
|
this.emitter = data.emitter; |
|
|
|
|
|
this.clusteredNodes = {}; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* |
|
|
|
|
|
* @param hubsize |
|
|
|
|
|
* @param options |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.clusterByConnectionCount = function(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.nodeIndices.length; i++) { |
|
|
|
|
|
var node = this.nodes[this.nodeIndices[i]]; |
|
|
|
|
|
if (node.edges.length >= hubsize) { |
|
|
|
|
|
nodesToCluster.push(node.id); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < nodesToCluster.length; i++) { |
|
|
|
|
|
var node = this.nodes[nodesToCluster[i]]; |
|
|
|
|
|
this.clusterByConnection(node,options,{},{},true); |
|
|
|
|
|
} |
|
|
|
|
|
this.emitter.emit('dataChanged'); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* loop over all nodes, check if they adhere to the condition and cluster if needed. |
|
|
|
|
|
* @param options |
|
|
|
|
|
* @param doNotUpdateCalculationNodes |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) { |
|
|
|
|
|
if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");} |
|
|
|
|
|
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.nodeIndices.length; i++) { |
|
|
|
|
|
var nodeId = this.nodeIndices[i]; |
|
|
|
|
|
var clonedOptions = this._cloneOptions(nodeId); |
|
|
|
|
|
if (options.joinCondition(clonedOptions) == true) { |
|
|
|
|
|
childNodesObj[nodeId] = this.nodes[nodeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Cluster all nodes in the network that have only 1 edge |
|
|
|
|
|
* @param options |
|
|
|
|
|
* @param doNotUpdateCalculationNodes |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) { |
|
|
|
|
|
options = this._checkOptions(options); |
|
|
|
|
|
|
|
|
|
|
|
var clusters = [] |
|
|
|
|
|
|
|
|
|
|
|
// collect the nodes that will be in the cluster
|
|
|
|
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
|
|
|
var childNodesObj = {}; |
|
|
|
|
|
var childEdgesObj = {}; |
|
|
|
|
|
var nodeId = this.nodeIndices[i]; |
|
|
|
|
|
if (this.nodes[nodeId].edges.length == 1) { |
|
|
|
|
|
var edge = this.nodes[nodeId].edges[0]; |
|
|
|
|
|
var childNodeId = this._getConnectedId(edge, nodeId); |
|
|
|
|
|
if (childNodeId != nodeId) { |
|
|
|
|
|
if (options.joinCondition === undefined) { |
|
|
|
|
|
childNodesObj[nodeId] = this.nodes[nodeId]; |
|
|
|
|
|
childNodesObj[childNodeId] = this.nodes[childNodeId]; |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
var clonedOptions = this._cloneOptions(nodeId); |
|
|
|
|
|
if (options.joinCondition(clonedOptions) == true) { |
|
|
|
|
|
childNodesObj[nodeId] = this.nodes[nodeId]; |
|
|
|
|
|
} |
|
|
|
|
|
clonedOptions = this._cloneOptions(childNodeId); |
|
|
|
|
|
if (options.joinCondition(clonedOptions) == true) { |
|
|
|
|
|
childNodesObj[childNodeId] = this.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.emitter.emit('dataChanged'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* |
|
|
|
|
|
* @param nodeId |
|
|
|
|
|
* @param options |
|
|
|
|
|
* @param doNotUpdateCalculationNodes |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) { |
|
|
|
|
|
// kill conditions
|
|
|
|
|
|
if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} |
|
|
|
|
|
if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} |
|
|
|
|
|
|
|
|
|
|
|
var node = this.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.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.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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._cloneOptions = function(objId, type) { |
|
|
|
|
|
var clonedOptions = {}; |
|
|
|
|
|
if (type === undefined || type == 'node') { |
|
|
|
|
|
util.deepExtend(clonedOptions, this.nodes[objId].options, true); |
|
|
|
|
|
util.deepExtend(clonedOptions, this.nodes[objId].properties, true); |
|
|
|
|
|
clonedOptions.amountOfConnections = this.nodes[objId].edges.length; |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
util.deepExtend(clonedOptions, this.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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._createClusterEdges = function (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); |
|
|
|
|
|
// avoid forcing the default color on edges that inherit color
|
|
|
|
|
|
if (edge.properties.color === undefined) { |
|
|
|
|
|
delete clonedOptions.color; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
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(new Edge(clonedOptions,this,this.constants)) |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._checkOptions = function(options) { |
|
|
|
|
|
if (options === undefined) {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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) { |
|
|
|
|
|
// 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 = new Node(clusterNodeProperties, this.images, this.groups, this.constants); |
|
|
|
|
|
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.edges[edgeId] !== undefined) { |
|
|
|
|
|
if (this.edges[edgeId].via !== null) { |
|
|
|
|
|
var viaId = this.edges[edgeId].via.id; |
|
|
|
|
|
if (viaId) { |
|
|
|
|
|
this.edges[edgeId].via = null |
|
|
|
|
|
delete this.sectors['support']['nodes'][viaId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
this.edges[edgeId].disconnect(); |
|
|
|
|
|
delete this.edges[edgeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// remove contained nodes from global
|
|
|
|
|
|
for (var nodeId in childNodesObj) { |
|
|
|
|
|
if (childNodesObj.hasOwnProperty(nodeId)) { |
|
|
|
|
|
this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]}; |
|
|
|
|
|
delete this.nodes[nodeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// finally put the cluster node into global
|
|
|
|
|
|
this.nodes[clusterNodeProperties.id] = clusterNode; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// push new edges to global
|
|
|
|
|
|
for (var i = 0; i < newEdges.length; i++) { |
|
|
|
|
|
this.edges[newEdges[i].id] = newEdges[i]; |
|
|
|
|
|
this.edges[newEdges[i].id].connect(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// create bezier nodes for smooth curves if needed
|
|
|
|
|
|
this._createBezierNodes(newEdges); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// set ID to undefined so no duplicates arise
|
|
|
|
|
|
clusterNodeProperties.id = undefined; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// wrap up
|
|
|
|
|
|
if (doNotUpdateCalculationNodes !== true) { |
|
|
|
|
|
this.emitter.emit('dataChanged'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Check if a node is a cluster. |
|
|
|
|
|
* @param nodeId |
|
|
|
|
|
* @returns {*} |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.isCluster = function(nodeId) { |
|
|
|
|
|
if (this.nodes[nodeId] !== undefined) { |
|
|
|
|
|
return this.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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._getClusterPosition = function(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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) { |
|
|
|
|
|
// kill conditions
|
|
|
|
|
|
if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} |
|
|
|
|
|
if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} |
|
|
|
|
|
if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return}; |
|
|
|
|
|
|
|
|
|
|
|
var node = this.nodes[clusterNodeId]; |
|
|
|
|
|
var containedNodes = node.containedNodes; |
|
|
|
|
|
var containedEdges = node.containedEdges; |
|
|
|
|
|
|
|
|
|
|
|
// release nodes
|
|
|
|
|
|
for (var nodeId in containedNodes) { |
|
|
|
|
|
if (containedNodes.hasOwnProperty(nodeId)) { |
|
|
|
|
|
this.nodes[nodeId] = containedNodes[nodeId]; |
|
|
|
|
|
// inherit position
|
|
|
|
|
|
this.nodes[nodeId].x = node.x; |
|
|
|
|
|
this.nodes[nodeId].y = node.y; |
|
|
|
|
|
|
|
|
|
|
|
// inherit speed
|
|
|
|
|
|
this.nodes[nodeId].vx = node.vx; |
|
|
|
|
|
this.nodes[nodeId].vy = node.vy; |
|
|
|
|
|
|
|
|
|
|
|
delete this.clusteredNodes[nodeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// release edges
|
|
|
|
|
|
for (var edgeId in containedEdges) { |
|
|
|
|
|
if (containedEdges.hasOwnProperty(edgeId)) { |
|
|
|
|
|
this.edges[edgeId] = containedEdges[edgeId]; |
|
|
|
|
|
this.edges[edgeId].connect(); |
|
|
|
|
|
var edge = this.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._createBezierNodes(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.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.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.nodes[edge.toArray[0].id] !== undefined) { |
|
|
|
|
|
this._connectEdge(edge, edge.toArray[0].id, false); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
else { |
|
|
|
|
|
var edgeId = edgeIds[i]; |
|
|
|
|
|
var viaId = this.edges[edgeId].via.id; |
|
|
|
|
|
if (viaId) { |
|
|
|
|
|
this.edges[edgeId].via = null |
|
|
|
|
|
delete this.sectors['support']['nodes'][viaId]; |
|
|
|
|
|
} |
|
|
|
|
|
// this removes the edge from node.edges, which is why edgeIds is formed
|
|
|
|
|
|
this.edges[edgeId].disconnect(); |
|
|
|
|
|
delete this.edges[edgeId]; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// remove clusterNode
|
|
|
|
|
|
delete this.nodes[clusterNodeId]; |
|
|
|
|
|
|
|
|
|
|
|
if (doNotUpdateCalculationNodes !== true) { |
|
|
|
|
|
this.emitter.emit('dataChanged'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Recalculate navigation nodes, color edges dirty, update nodes list etc. |
|
|
|
|
|
* @private |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._wrapUp = function() { |
|
|
|
|
|
|
|
|
|
|
|
this._updateNodeIndexList(); |
|
|
|
|
|
this._updateCalculationNodes(); |
|
|
|
|
|
this._markAllEdgesAsDirty(); |
|
|
|
|
|
if (this.initializing !== true) { |
|
|
|
|
|
this.moving = true; |
|
|
|
|
|
this.start(); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* 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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._connectEdge = function(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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._getClusterStack = function(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.nodes[nodeId]); |
|
|
|
|
|
return stack; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* Get the Id the node is connected to |
|
|
|
|
|
* @param edge |
|
|
|
|
|
* @param nodeId |
|
|
|
|
|
* @returns {*} |
|
|
|
|
|
* @private |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._getConnectedId = function(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 |
|
|
|
|
|
*/ |
|
|
|
|
|
ClusterEngine.prototype._getHubSize = function() { |
|
|
|
|
|
var average = 0; |
|
|
|
|
|
var averageSquared = 0; |
|
|
|
|
|
var hubCounter = 0; |
|
|
|
|
|
var largestHub = 0; |
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
|
|
|
var node = this.nodes[this.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; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|