1
0
Fork 0
arangodb/html/admin/js/graphViewer/graph/communityNode.js

551 lines
14 KiB
JavaScript

/*jslint indent: 2, nomen: true, maxlen: 100, white: true plusplus: true */
/*global _, document, ForceLayouter, DomObserverFactory*/
////////////////////////////////////////////////////////////////////////////////
/// @brief Graph functionality
///
/// @file
///
/// DISCLAIMER
///
/// Copyright 2010-2012 triagens GmbH, Cologne, Germany
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
/// http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///
/// Copyright holder is triAGENS GmbH, Cologne, Germany
///
/// @author Michael Hackstein
/// @author Copyright 2011-2013, triAGENS GmbH, Cologne, Germany
////////////////////////////////////////////////////////////////////////////////
function CommunityNode(parent, initial) {
"use strict";
if (_.isUndefined(parent)
|| !_.isFunction(parent.dissolveCommunity)
|| !_.isFunction(parent.checkNodeLimit)) {
throw "A parent element has to be given.";
}
initial = initial || [];
var
////////////////////////////////////
// Private variables //
////////////////////////////////////
self = this,
bBox,
bBoxBorder,
bBoxTitle,
nodes = {},
observer,
nodeArray = [],
intEdgeArray = [],
internal = {},
inbound = {},
outbound = {},
outReferences = {},
layouter,
////////////////////////////////////
// Private functions //
////////////////////////////////////
getDistance = function(def) {
if (self._expanded) {
return 2 * def * Math.sqrt(nodeArray.length);
}
return def;
},
getCharge = function(def) {
if (self._expanded) {
return 4 * def * Math.sqrt(nodeArray.length);
}
return def;
},
compPosi = function(p) {
var d = self.position,
x = p.x * d.z + d.x,
y = p.y * d.z + d.y,
z = p.z * d.z;
return {
x: x,
y: y,
z: z
};
},
getSourcePosition = function(e) {
if (self._expanded) {
return compPosi(e._source.position);
}
return self.position;
},
getTargetPosition = function(e) {
if (self._expanded) {
return compPosi(e._target.position);
}
return self.position;
},
updateBoundingBox = function() {
var boundingBox = document.getElementById(self._id).getBBox();
bBox.attr("transform", "translate(" + (boundingBox.x - 5) + "," + (boundingBox.y - 25) + ")");
bBoxBorder.attr("width", boundingBox.width + 10)
.attr("height", boundingBox.height + 30);
bBoxTitle.attr("width", boundingBox.width + 10);
},
getObserver = function() {
if (!observer) {
var factory = new DomObserverFactory();
observer = factory.createObserver(function(e){
if (_.any(e, function(obj) {
return obj.attributeName === "transform";
})) {
updateBoundingBox();
observer.disconnect();
}
});
}
return observer;
},
updateNodeArray = function() {
layouter.stop();
nodeArray.length = 0;
_.each(nodes, function(v) {
nodeArray.push(v);
});
layouter.start();
},
updateEdgeArray = function() {
layouter.stop();
intEdgeArray.length = 0;
_.each(internal, function(e) {
intEdgeArray.push(e);
});
layouter.start();
},
toArray = function(obj) {
var res = [];
_.each(obj, function(v) {
res.push(v);
});
return res;
},
hasNode = function(id) {
return !!nodes[id];
},
getNodes = function() {
return nodeArray;
},
getNode = function(id) {
return nodes[id];
},
insertNode = function(n) {
nodes[n._id] = n;
updateNodeArray();
self._size++;
},
insertInitialNodes = function(ns) {
_.each(ns, function(n) {
nodes[n._id] = n;
self._size++;
});
updateNodeArray();
},
removeNode = function(n) {
var id = n._id || n;
delete nodes[id];
updateNodeArray();
self._size--;
},
removeInboundEdge = function(e) {
var id;
if (!_.has(e, "_id")) {
id = e;
e = internal[id] || inbound[id];
} else {
id = e._id;
}
e.target = e._target;
delete e._target;
if (internal[id]) {
delete internal[id];
self._outboundCounter++;
outbound[id] = e;
updateEdgeArray();
return;
}
delete inbound[id];
self._inboundCounter--;
return;
},
removeOutboundEdge = function(e) {
var id;
if (!_.has(e, "_id")) {
id = e;
e = internal[id] || outbound[id];
} else {
id = e._id;
}
e.source = e._source;
delete e._source;
delete outReferences[e.source._id][id];
if (internal[id]) {
delete internal[id];
self._inboundCounter++;
inbound[id] = e;
updateEdgeArray();
return;
}
delete outbound[id];
self._outboundCounter--;
return;
},
removeOutboundEdgesFromNode = function(n) {
var id = n._id || n,
res = [];
_.each(outReferences[id], function(e) {
removeOutboundEdge(e);
res.push(e);
});
delete outReferences[id];
return res;
},
insertInboundEdge = function(e) {
e._target = e.target;
e.target = self;
if (outbound[e._id]) {
delete outbound[e._id];
self._outboundCounter--;
internal[e._id] = e;
updateEdgeArray();
return true;
}
inbound[e._id] = e;
self._inboundCounter++;
return false;
},
insertOutboundEdge = function(e) {
var sId = e.source._id;
e._source = e.source;
e.source = self;
outReferences[sId] = outReferences[sId] || {};
outReferences[sId][e._id] = e;
if (inbound[e._id]) {
delete inbound[e._id];
self._inboundCounter--;
internal[e._id] = e;
updateEdgeArray();
return true;
}
self._outboundCounter++;
outbound[e._id] = e;
return false;
},
getDissolveInfo = function() {
return {
nodes: nodeArray,
edges: {
both: intEdgeArray,
inbound: toArray(inbound),
outbound: toArray(outbound)
}
};
},
expand = function() {
this._expanded = true;
},
dissolve = function() {
parent.dissolveCommunity(self);
},
collapse = function() {
this._expanded = false;
},
addCollapsedLabel = function(g, colourMapper) {
var width = g.select("rect").attr("width"),
textN = g.append("text") // Append a label for the node
.attr("text-anchor", "middle") // Define text-anchor
.attr("fill", colourMapper.getForegroundCommunityColour())
.attr("stroke", "none"); // Make it readable
width *= 2;
width /= 3;
if (self._reason && self._reason.key) {
textN.append("tspan")
.attr("x", "0")
.attr("dy", "-4")
.text(self._reason.key + ":");
textN.append("tspan")
.attr("x", "0")
.attr("dy", "16")
.text(self._reason.value);
}
textN.append("tspan")
.attr("x", width)
.attr("y", "0")
.attr("fill", colourMapper.getCommunityColour())
.text(self._size);
},
addCollapsedShape = function(g, shapeFunc, start, colourMapper) {
var inner = g.append("g")
.attr("stroke", colourMapper.getForegroundCommunityColour())
.attr("fill", colourMapper.getCommunityColour());
shapeFunc(inner, 9);
shapeFunc(inner, 6);
shapeFunc(inner, 3);
shapeFunc(inner);
inner.on("click", function() {
self.expand();
parent.checkNodeLimit(self);
start();
});
addCollapsedLabel(inner, colourMapper);
},
addNodeShapes = function(g, shapeQue) {
var interior = g.selectAll(".node")
.data(nodeArray, function(d) {
return d._id;
});
interior.enter()
.append("g")
.attr("class", "node")
.attr("id", function(d) {
return d._id;
});
// Remove all old
interior.exit().remove();
interior.selectAll("* > *").remove();
shapeQue(interior);
},
addBoundingBox = function(g, start) {
bBox = g.append("g");
bBoxBorder = bBox.append("rect")
.attr("rx", "8")
.attr("ry", "8")
.attr("fill", "none")
.attr("stroke", "black");
bBoxTitle = bBox.append("rect")
.attr("rx", "8")
.attr("ry", "8")
.attr("height", "20")
.attr("fill", "#686766")
.attr("stroke", "none");
var dissolveBtn = bBox.append("image")
.attr("id", self._id + "_dissolve")
.attr("xlink:href", "img/icon_delete.png")
.attr("width", "16")
.attr("height", "16")
.attr("x", "5")
.attr("y", "2")
.attr("style", "cursor:pointer")
.on("click", function() {
self.dissolve();
start();
}),
collapseBtn = bBox.append("image")
.attr("id", self._id + "_collapse")
.attr("xlink:href", "img/gv_collapse.png")
.attr("width", "16")
.attr("height", "16")
.attr("x", "25")
.attr("y", "2")
.attr("style", "cursor:pointer")
.on("click", function() {
self.collapse();
start();
}),
title = bBox.append("text")
.attr("x", "45")
.attr("y", "15")
.attr("fill", "white")
.attr("stroke", "none")
.attr("text-anchor", "left");
if (self._reason) {
title.text(self._reason.text);
}
getObserver().observe(document.getElementById(self._id), {
subtree:true,
attributes:true
});
},
addDistortion = function(distFunc) {
if (self._expanded) {
var oldFocus = distFunc.focus(),
newFocus = [
oldFocus[0] - self.position.x,
oldFocus[1] - self.position.y
];
distFunc.focus(newFocus);
_.each(nodeArray, function(n) {
n.position = distFunc(n);
n.position.x /= self.position.z;
n.position.y /= self.position.z;
n.position.z /= self.position.z;
});
distFunc.focus(oldFocus);
}
},
shapeAll = function(g, shapeFunc, shapeQue, start, colourMapper) {
// First unbind all click events that are proably still bound
g.on("click", null);
if (self._expanded) {
addBoundingBox(g, start);
addNodeShapes(g, shapeQue, start, colourMapper);
return;
}
addCollapsedShape(g, shapeFunc, start, colourMapper);
},
updateEdges = function(g, addPosition, addUpdate) {
if (self._expanded) {
var interior = g.selectAll(".link"),
line = interior.select("line");
addPosition(line, interior);
addUpdate(interior);
}
},
shapeEdges = function(g, addQue) {
var idFunction = function(d) {
return d._id;
},
line,
interior;
if (self._expanded) {
interior = g
.selectAll(".link")
.data(intEdgeArray, idFunction);
// Append the group and class to all new
interior.enter()
.append("g")
.attr("class", "link") // link is CSS class that might be edited
.attr("id", idFunction);
// Remove all old
interior.exit().remove();
// Remove all elements that are still included.
interior.selectAll("* > *").remove();
line = interior.append("line");
addQue(line, interior);
}
},
collapseNode = function(n) {
removeOutboundEdgesFromNode(n);
};
////////////////////////////////////
// Setup //
////////////////////////////////////
layouter = new ForceLayouter({
distance: 100,
gravity: 0.1,
charge: -500,
width: 1,
height: 1,
nodes: nodeArray,
links: intEdgeArray
});
////////////////////////////////////
// Values required for shaping //
////////////////////////////////////
this._id = "*community_" + Math.floor(Math.random()* 1000000);
if (initial.length > 0) {
this.x = initial[0].x;
this.y = initial[0].y;
} else {
this.x = 0;
this.y = 0;
}
this._size = 0;
this._inboundCounter = 0;
this._outboundCounter = 0;
this._expanded = false;
// Easy check for the other classes,
// no need for a regex on the _id any more.
this._isCommunity = true;
insertInitialNodes(initial);
////////////////////////////////////
// Public functions //
////////////////////////////////////
this.hasNode = hasNode;
this.getNodes = getNodes;
this.getNode = getNode;
this.getDistance = getDistance;
this.getCharge = getCharge;
this.insertNode = insertNode;
this.insertInboundEdge = insertInboundEdge;
this.insertOutboundEdge = insertOutboundEdge;
this.removeNode = removeNode;
this.removeInboundEdge = removeInboundEdge;
this.removeOutboundEdge = removeOutboundEdge;
this.removeOutboundEdgesFromNode = removeOutboundEdgesFromNode;
this.collapseNode = collapseNode;
this.dissolve = dissolve;
this.getDissolveInfo = getDissolveInfo;
this.collapse = collapse;
this.expand = expand;
this.shapeNodes = shapeAll;
this.shapeInnerEdges = shapeEdges;
this.updateInnerEdges = updateEdges;
this.addDistortion = addDistortion;
this.getSourcePosition = getSourcePosition;
this.getTargetPosition = getTargetPosition;
}