///
///
///
///
///
///
var AB;
(function (AB) {
var Graph = (function () {
//
// Public functions
//
function Graph() {
var scope = this;
// Members
this.element = document.getElementById("Graph");
this.graph = Raphael("Graph", (50 * screen.width) / 100, screen.height);
this.root = this._createNode("Object name", Raphael.rgb(255, 255, 255), true);
this.utils = new AB.Utils(this);
this.contextMenu = new AB.ContextMenu(this);
this.toolbar = new AB.ToolBar(this);
this.parametersManager = new AB.ParametersManager(this);
this.mousex = 0;
this.mousey = 0;
this.objectName = "";
this.editing = false;
this.zoom = 1.0;
this.selectedNode = null;
this.element.onmouseover = function () {
scope.editing = false;
};
this.element.onmouseout = function () {
scope.editing = true;
};
document.addEventListener("click", function (event) {
if (!scope.contextMenu.showing) {
scope.onClick(event);
}
});
document.ondblclick = function (event) {
var result = scope.traverseGraph(null, scope.mousex, scope.mousey);
if (result.hit && result.element.node != scope.root) {
scope.onReduce();
}
};
document.onmousemove = function (event) {
scope.mousex = event.clientX - scope.graph.canvas.getBoundingClientRect().left;
scope.mousey = event.clientY - scope.graph.canvas.getBoundingClientRect().top;
};
// Set properties
this.element.scrollLeft = (this.element.getBoundingClientRect().width / 2) + (Graph.NODE_WIDTH / 2);
}
// Handles the click event. Called by this and this.contextmenu
// event: the event object
Graph.prototype.onClick = function (event) {
var result = this.traverseGraph(null, this.mousex, this.mousey, true);
if (!this.editing) {
if (this.selectedNode != null) {
var detached = this.isParentDetached(this.selectedNode);
this.selectedNode.node.attr(this.selectedNode.node.rect, "fill", this.getNodeColor(this.selectedNode, detached));
this.selectedNode = null;
if (!result.hit)
this.parametersManager.clearParameters();
}
if (result.hit && result.element.node != this.root) {
this.selectedNode = result.element;
this.selectedNode.node.attr(this.selectedNode.node.rect, "fill", this.getSelectedNodeColor(this.selectedNode, detached));
}
if (!result.hit)
this.selectedNode = null;
}
}
// Returns the node's color for a given type
// action : the action to test
// returns the node color
Graph.prototype.getSelectedNodeColor = function (action, detached) {
// Detached
if (action.detached || (action.node && action.node.detached) || detached)
return Raphael.rgb(96, 122, 14);
// Get proper color
var color = Raphael.rgb(255, 255, 255); // Default color for root
switch (action.type) {
case AB.ActionsBuilder.Type.TRIGGER: color = Raphael.rgb(41, 129, 255); break;
case AB.ActionsBuilder.Type.ACTION: color = Raphael.rgb(255, 220, 42); break;
case AB.ActionsBuilder.Type.FLOW_CONTROL: color = Raphael.rgb(255, 41, 53); break;
}
return color;
}
Graph.prototype.getNodeColor = function (action, detached) {
if (action.detached || (action.node && action.node.detached) || detached)
return Raphael.rgb(96, 122, 14);
// Get proper color
var color = Raphael.rgb(255, 255, 255); // Default color for root
switch (action.type) {
case AB.ActionsBuilder.Type.TRIGGER: color = Raphael.rgb(133, 154, 185); break;
case AB.ActionsBuilder.Type.ACTION: color = Raphael.rgb(182, 185, 132); break;
case AB.ActionsBuilder.Type.FLOW_CONTROL: color = Raphael.rgb(185, 132, 140); break;
}
return color;
}
Graph.prototype.isParentDetached = function (node) {
var parent = node.parent;
while (parent != null) {
if (parent.node.detached)
return true;
parent = parent.parent;
}
return false;
}
// Adds a node to the graph
// parent : the parent of the added node
// listElement : the values
Graph.prototype.addNode = function (parent, listElement) {
var color = this.getNodeColor(listElement);
var n = this._createNode(listElement.name, color, parent.combine ? true : false);
if (listElement.name == "CombineAction") {
n.action.combine = true;
var hubElement = AB.ActionsBuilder.FlowAction.Hub;
var hubType = AB.ActionsBuilder.Type.FLOW_CONTROL;
n.action.hub = this._createNode(hubElement.name, this.getNodeColor({ type: hubType }), false);
n.action.hub.action.type = hubType;
n.action.addChild(n.action.hub.action);
this._createNodeAnimation(n.action.hub);
}
n.action.name = listElement.name;
n.action.type = listElement.type;
n.detached = listElement.detached || false;
n.action.properties = listElement.properties;
for (var i = 0; i < listElement.properties.length; i++)
n.action.propertiesResults.push(listElement.properties[i].value);
if (parent && !parent.combine) {
parent.addChild(n.action);
}
else if (parent.combine) {
// Create hub
parent.combineArray.push(n.action);
n.action.parent = parent;
parent.node.attr(parent.node.text, "text", "");
}
this._createNodeAnimation(n);
return n;
}
// Updates the graph
Graph.prototype.update = function () {
var scope = this;
var rootChildID = 0;
var setNodeSize = function(node) {
if (node.minimized) {
node.attr(node.rect, "width", Graph.NODE_MINIMIZED_WIDTH * scope.zoom);
node.attr(node.rect, "height", Graph.NODE_HEIGHT * scope.zoom);
}
else {
node.attr(node.rect, "width", Graph.NODE_WIDTH * scope.zoom);
node.attr(node.rect, "height", Graph.NODE_HEIGHT * scope.zoom);
}
node.attr(node.text, "font-size", 11 * scope.zoom);
}
var onUpdate = function (action, yOffset) {
var node = action.node;
var parent = action.parent ? action.parent.node : null;
// Set size of node according to zoom
if (action.combine) {
var length = 0;
for (var i = 0; i < action.combineArray.length; i++) {
var n = action.combineArray[i].node;
setNodeSize(n);
length += n.attr(n.rect, "width");
}
}
setNodeSize(node);
// Set position
if (parent) {
scope._setNodePosition(node, parent.attr(parent.rect, "x"), yOffset);
scope._setLine(action);
}
// Calculate total width
var totalWidth = 0;
for (var i = 0; i < action.children.length; i++) {
var n = action.children[i].node;
totalWidth += n.attr(n.rect, "width");
}
var nodeWidth = node.attr(node.rect, "width");
var nodex = node.attr(node.rect, "x");
var startingXPos = nodex + (nodeWidth / 2) - (totalWidth / 2);
// Recursively set position of children
for (var i = 0; i < action.children.length; i++) {
var n = action.children[i].node;
var newx = startingXPos;
var newy = yOffset + Graph.VERTICAL_OFFSET * scope.zoom;
onUpdate(n.action, newy);
scope._setNodePosition(n, newx, newy);
scope._setLine(n.action);
startingXPos += n.attr(n.rect, "width");
}
};
var onPosition = function (action, maxWidth) {
var total = 0;
var start = action.combine && action.combineArray.length > 1 ? 0 : 1;
for (var i = start; i < action.children.length; i++) {
var n = action.children[i].node;
if (action.combine) {
for (var j = 1; j < action.combineArray.length; j++) {
var cn = action.combineArray[j].node;
total += cn.attr(cn.rect, "width");
}
}
else
total += n.attr(n.rect, "width");
}
if (total > maxWidth) {
maxWidth = total;
}
for (var i = 0; i < action.children.length; i++) {
maxWidth = onPosition(action.children[i], maxWidth);
}
return maxWidth;
};
// Set root node position / scale and recursively set position of its children
this._setNodePosition(this.root, (this.graph.width / 2) - (Graph.NODE_WIDTH / 2) * this.zoom, 10);
this.root.attr(this.root.rect, "width", Graph.NODE_WIDTH * scope.zoom);
this.root.attr(this.root.rect, "height", Graph.NODE_HEIGHT * scope.zoom);
onUpdate(this.root.action, 10 * this.zoom);
// Get total widths
var widths = new Array();
/*
object:
{
triggerWidth: number
childrenWidths: new Array()
}
*/
for (var i = 0; i < scope.root.action.children.length; i++) {
var a = scope.root.action.children[i];
var obj = {
triggerWidth: onPosition(a, 0),
childrenWidths: new Array()
};
for (var j = 0; j < a.children.length; j++) {
var cw = onPosition(a.children[j], 0);
obj.childrenWidths.push(cw);
obj.triggerWidth += cw;
}
widths.push(obj);
}
// Set new position of children
var rx = scope.root.attr(scope.root.rect, "x");
var rwidth = 0;
var cwidth = 0;
for (var i = 0; i < scope.root.action.children.length; i++) {
var a = scope.root.action.children[i];
var tx = a.node.attr(a.node.rect, "x");
for (var j = 0; j < a.children.length; j++) {
var n = a.children[j].node;
var x = n.attr(n.rect, "x");
var y = n.attr(n.rect, "y");
var inverse = x >= tx ? 1 : -1;
scope._setNodePosition(n, x + (scope.root.action.children.length > 1 ? widths[i].childrenWidths[j] / 1.8 : 0) * inverse, y);
scope._setLine(n.action);
scope._resizeCanvas(n);
cwidth += widths[i].childrenWidths[j] / 2;
}
if (scope.root.action.children.length > 1) {
var n = a.node;
var x = n.attr(n.rect, "x");
var y = n.attr(n.rect, "y");
var inverse = x >= rx && i == 0 && !n.minimized ? 1 : -1;
scope._setNodePosition(n, x + rwidth + (i > 1 ? widths[i - 1].triggerWidth / 1.8 : 0) + (widths[i].triggerWidth / 1.8) * inverse, y);
scope._setLine(n.action);
scope._resizeCanvas(n);
}
rwidth += widths[i].triggerWidth / 2;
}
}
// Creates the JSON according to the graph
// root: the root object to start with
Graph.prototype.createJSON = function (root) {
if (!root) root = this.root.action;
var action = {};
action.type = root.type;
action.name = root.name;
action.detached = root.node.detached;
action.children = new Array();
action.combine = new Array();
action.properties = new Array();
for (var i = 0; i < root.properties.length; i++) {
action.properties[i] = { name: root.properties[i].text, value: root.propertiesResults[i] };
if (root.properties[i].targetType != null)
action.properties[i].targetType = root.properties[i].targetType;
}
if (root.combine) {
for (var i = 0; i < root.combineArray.length; i++) {
var combinedAction = root.combineArray[i];
action.combine.push(this.createJSON(combinedAction, action));
}
}
if (root.combine)
root = root.children[0]; // The hub
for (var i = 0; i < root.children.length; i++) {
action.children.push(this.createJSON(root.children[i], action));
}
return action;
}
// Loads a graph from a JSON
// Graph: the root object's graph
// startNode: the start node to where begin the load
Graph.prototype.loadFromJSON = function (graph, startNode) {
var scope = this;
// If startNode is null, means it replaces all the graph
// If not, it comes from a copy/paste
if (startNode == null) {
for (var i = 0; i < this.root.action.children.length; i++)
this._removeAction(this.root.action.children[i], true);
this.root.action.clearChildren();
}
var load = function (root, parent, detached, combine) {
if (!parent) parent = scope.root.action;
if (!root) root = graph;
if (!detached) detached = false;
if (!combine) combine = false;
var n = null; // Not going to be created
if (root.type != AB.ActionsBuilder.Type.OBJECT) { // Means it is not the root (the edited object)
var e = {};
e.type = root.type;
e.name = root.name;
e.detached = root.detached;
e.combine = root.combine.length > 0;
e.properties = new Array();
e.combineArray = new Array();
for (var i = 0; i < root.properties.length; i++) {
e.properties.push({ text: root.properties[i].name, value: root.properties[i].value });
if (root.properties[i].targetType != null) {
e.properties[e.properties.length - 1].targetType = root.properties[i].targetType;
}
}
n = scope.addNode(parent, e);
if (detached)
n.attr(n.rect, "fill", scope.getNodeColor(n.action));
else
detached = n.detached;
// If combine array length > 0, it is a combine action
for (var i = 0; i < root.combine.length; i++) {
load(root.combine[i], n.action, detached, true);
}
if (!combine)
parent = parent.children[parent.children.length - 1];
else
n.action.parent = null;
}
for (var i = 0; i < root.children.length; i++) {
load(root.children[i], n && n.action.combine ? n.action.hub.action : parent, root.detached, false);
}
}
load(graph, startNode);
this.update();
}
// Traverse the graph and returns if hit a node
// start: the start node to start traverse
// x: the mouse's x position
// y: the mouse's y position
// traverseCombine: if check the combine nodes
Graph.prototype.traverseGraph = function (start, x, y, traverseCombine) {
if (!start) start = this.root.action;
if (traverseCombine == null) traverseCombine = false;
var result = {
hit: true,
element: start
};
if (start.node.isPointInside(x, y))
return result;
for (var i = 0; i < start.children.length; i++) {
if (start.children[i].node.isPointInside(x, y)) {
result.hit = true;
result.element = start.children[i];
if (start.children[i].combine && traverseCombine) {
var a = start.children[i];
for (var j = 0; j < a.combineArray.length; j++) {
if (a.combineArray[j].node.isPointInside(x, y)) {
result.element = a.combineArray[j];
break;
}
}
}
return result;
}
result = this.traverseGraph(start.children[i], x, y, traverseCombine);
if (result.hit)
return result;
}
result.hit = false;
result.element = null;
return result;
}
//
// Private functions
//
// Sets the given node's line
// If commented, the line isn't setted by hidden
Graph.prototype._setLine = function (element) {
var n = element.node;
var nodeWidth = n.attr(n.rect, "width");
var nodeHeight = n.attr(n.rect, "height");
var nodex = n.attr(n.rect, "x");
var nodey = n.attr(n.rect, "y");
if (n.detached) {
n.attr(n.line, "path", ["M", nodex, nodey, "L", nodex, nodey]);
return;
}
var linex = n.attr(n.rect, "x") + nodeWidth / 2;
var liney = n.attr(n.rect, "y");
var p = element.parent.node;
var parentWidth = p.attr(p.rect, "width");
var parentHeight = p.attr(p.rect, "height");
var parentx = p.attr(p.rect, "x");
var parenty = p.attr(p.rect, "y");
var liney2 = liney - (liney - parenty - parentHeight) / 2;
var linex3 = parentx + parentWidth / 2;
var liney4 = parenty + parentHeight;
n.attr(n.line, "path", ["M", linex, liney, "L", linex, liney2, "L", linex3, liney2, "L", linex3, liney4]);
n.attr(n.line, "stroke", this.getSelectedNodeColor(element, element.node.detached));
}
// Sets the given node's position
// Applies changements on its children
Graph.prototype._setNodePosition = function (node, x, y) {
var offsetx = node.attr(node.rect, "x") - x;
node.attr(node.rect, "x", x);
node.attr(node.rect, "y", y);
var bbox = node.text.getBBox();
var textWidth = 0;
if (bbox)
textWidth = node.text.getBBox().width;
node.attr(node.text, "x", x + node.attr(node.rect, "width") / 2 - textWidth / 2);
node.attr(node.text, "y", y + node.attr(node.rect, "height") / 2);
// Set combine nodes positions
if (node.action.combine) {
var length = 0;
for (var i = 0; i < node.action.combineArray.length; i++) {
var a = node.action.combineArray[i];
var n = a.node;
n.attr(n.rect, "x", node.attr(node.rect, "x") + length);
n.attr(n.rect, "y", node.attr(node.rect, "y"));
textWidth = n.text.getBBox().width;
n.attr(n.text, "x", n.attr(n.rect, "x") + n.attr(n.rect, "width") / 2 - textWidth / 2);
n.attr(n.text, "y", y + Graph.NODE_HEIGHT / 2);
length += n.attr(n.rect, "width");
}
node.attr(node.rect, "width", length);
}
for (var i = 0; i < node.action.children.length; i++) {
this._setNodePosition(node.action.children[i].node, node.action.children[i].node.attr(node.action.children[i].node.rect, "x") - offsetx, y + Graph.VERTICAL_OFFSET);
this._setLine(node.action.children[i]);
}
}
// Resizes the canvas if the node is outside the current canvas size
Graph.prototype._resizeCanvas = function (node) {
var x = node.attr(node.rect, "x");
var y = node.attr(node.rect, "y");
if (x > 0 + Graph.NODE_WIDTH && x < this.graph.width - Graph.NODE_WIDTH && y < this.graph.height)
return;
this.graph.setSize(this.graph.width + 500, this.graph.height + 500);
this._setNodePosition(this.root, (this.graph.width / 2) - (Graph.NODE_WIDTH / 2), 10);
this.element.scrollLeft = (this.graph.width / 2) - this.element.getBoundingClientRect().width / 2;
}
// Removes a node
// node : the node to remove
Graph.prototype._removeNode = function (node) {
node.rect.remove();
node.text.remove();
if (node.line)
node.line.remove();
}
// Remove an action from the graph
// action : the action to remove
// removeChildren : if true, it deletes the branch
Graph.prototype._removeAction = function (action, removeChildren) {
if (!removeChildren)
removeChildren = false;
this._removeNode(action.node);
if (action.combine) {
for (var i = 0; i < action.combineArray.length; i++) {
this._removeNode(action.combineArray[i].node);
}
action.combineArray = new Array();
}
if (removeChildren) {
for (var i = 0; i < action.children.length; i++)
this._removeAction(action.children[i], removeChildren);
action.clearChildren();
}
else {
for (var i = 0; i < action.children.length; i++) {
action.parent.addChild(action.children[i]);
action.children[i].parent = action.parent;
}
}
}
// Creates a node
// text : the text/name of the node
// color : the node's color
// noLine : if the node has parent then draw a line, or not
Graph.prototype._createNode = function (text, color, noLine) {
var n = new AB.Node();
n.rect = this.graph.rect(20, 20, Graph.NODE_WIDTH, Graph.NODE_HEIGHT, 0);
n.text = this.graph.text(0, 0, text);
if (!noLine) {
n.line = this.graph.path("M10 10L90 90");
n.line.attr("stroke", color);
}
n.action = new AB.Action(n);
n.rect.attr("fill", color);
n.text.attr("font-size", 11);
n.text.attr("text-anchor", "start");
n.text.attr("font-family", "Sinkin Sans Light");
return n;
}
// Creates the animations for a node
// element: the node
Graph.prototype._createNodeAnimation = function (element) {
var scope = this;
var mousex, mousey;
var finished = true;
var elementx = 0;
var elementy = 0;
var onMove = function (dx, dy, x, y, event) {
mousex = x;
mousey = y;
};
var onStart = function (x, y, event) {
if (element.minimized)
return;
mousex = x;
mousey = y;
if (finished) {
elementx = element.attr(element.rect, "x");
elementy = element.attr(element.rect, "y");
}
finished = false;
if (!element.minimized) {
element.rect.animate({
x: element.attr(element.rect, "x") - 10,
y: element.attr(element.rect, "y") - 5,
width: element.minimized ? Graph.NODE_MINIMIZED_WIDTH + 20 : Graph.NODE_WIDTH + 20,
height: Graph.NODE_HEIGHT + 10,
opacity: 0.25
}, 500, ">");
}
};
var onEnd = function (event) {
if (!element.minimized) {
element.rect.animate({
x: elementx,
y: elementy,
width: element.minimized ? Graph.NODE_MINIMIZED_WIDTH : Graph.NODE_WIDTH,
height: Graph.NODE_HEIGHT,
opacity: 1.0
}, 500, ">", function () { finished = true; });
}
var x = mousex - scope.graph.canvas.getBoundingClientRect().left;
var y = mousey - scope.graph.canvas.getBoundingClientRect().top;
var dragResult = scope.traverseGraph(null, x, y, true);
if (dragResult.hit && dragResult.element == element.action || !dragResult.hit) {
scope.parametersManager.createParameters(element);
}
else {
if (dragResult.element.children.length > 0 && dragResult.element.type != AB.ActionsBuilder.Type.TRIGGER)
return;
if (element.action.type == AB.ActionsBuilder.Type.TRIGGER && dragResult.element != scope.root.action)
return;
if (element.action.type == AB.ActionsBuilder.Type.ACTION && dragResult.element == scope.root.action)
return;
if (element.action.type == AB.ActionsBuilder.Type.FLOW_CONTROL && (dragResult.element == scope.root.action || dragResult.element.type == AB.ActionsBuilder.Type.FLOW_CONTROL))
return;
if (element.action.parent && element.action.parent.combine) // Musn't move hubs
return;
if (element.action == dragResult.element.parent)
return;
// Reset node
element.rect.stop(element.rect.animation);
element.attr(element.rect, "opacity", 1.0);
element.attr(element.rect, "width", Graph.NODE_WIDTH);
element.attr(element.rect, "height", Graph.NODE_HEIGHT);
if (element.action.parent) {
element.action.parent.removeChild(element.action);
dragResult.element.addChild(element.action);
scope.update();
scope._createNodeAnimation(element);
}
}
};
element.rect.drag(onMove, onStart, onEnd);
element.text.drag(onMove, onStart, onEnd);
}
Graph.NODE_MINIMIZED_WIDTH = 50;
Graph.NODE_WIDTH = 150;
Graph.NODE_HEIGHT = 25;
Graph.VERTICAL_OFFSET = 70;
return Graph;
})();
AB.Graph = Graph;
})(AB || (AB = {}));