blob: 9620b6d83cbd474c9d96fb5bcc81cd281557f726 [file] [log] [blame]
/**
* The BandIt module provides methods to draw a workflow<br/>
* Author: Olivier Sallou <olivier.sallou@irisa.fr></br>
* License: CeCILL-B
* @module BandIt
* @requires RaphaelJS, JQuery
*
*/
CONTAINER=1;
/**
* BandIt is a library above RaphaelJS to create workflows in HTML/JS.
* @class BandIt
* @constructor
* @param diveditor Name of the div where editor will be put
* @param width Width of the div
* @param height Height of the div
*/
function BandIt(diveditor,width,height) {
// Raphael SVG
this.paper = Raphael(diveditor,width,height);
/**
* Operation mode:
* 0 : drag and drop, selection
* 1 : link
* 2 : delete
* 3 : group
*/
this.mode = 0;
this.count = 0;
this.currentnode = null;
this.nodes = {};
this.outlinks = {};
this.inlinks = {};
this.paths = {};
this.zoom = 1;
// Node properties object
this.properties = {};
this.options = {};
this.selectCallbacks = [];
this.deleteCallbacks = [];
this.addCallbacks = [];
this.name = "bandit";
this.description = "";
// selectgroup is assigned the rect node id of the selection
this.selectgroup = 0;
this.selectionx = -1;
this.selectiony = -1;
// contains the list of node ids selected
this.selectnodes = [];
// Record actions for undo/redo
this.recordactions = true;
this.action = -1;
this.actions = [];
this.maxactions = 100;
mybandit = this;
$('#'+diveditor).mousedown(function(e) {
if(mybandit.mode!=3) {
return;
}
divposition = $('#'+diveditor).position();
var match=false;
mybandit.selectionx = e.pageX - divposition.left;
mybandit.selectiony = e.pageY -divposition.top;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) { // do not select text elements
var xselect = false;
var yselect = false;
if((mybandit.selectionx>=el.attr("x"))&&(mybandit.selectionx<=(el.attr("x")+el.attr("width")))) {
xselect = true;
}
if((mybandit.selectiony>=el.attr("y"))&&(mybandit.selectiony<=(el.attr("y")+el.attr("height")))) {
yselect = true;
}
if(xselect && yselect) {
match=true;
}
}
});
if(match==true) {
return;
}
// Click on empty location
if(mybandit.selectnodes.length>0) {
// Reset group
mybandit.clearSelection();
}
else {
banditLogger.DEBUG('start selection at '+(e.pageX - divposition.left)+":"+(e.pageY -divposition.top));
mybandit.selectionx = e.pageX - divposition.left;
mybandit.selectiony = e.pageY -divposition.top;
mybandit.selectgroup = mybandit.paper.rect(mybandit.selectionx,mybandit.selectiony,1,1).id;
}
});
$('#'+diveditor).mousemove(function(e) {
if(mybandit.mode!=3) {
return;
}
if(mybandit.selectgroup == 0) {
return;
}
divposition = $('#'+diveditor).position();
destx = e.pageX - divposition.left;
desty = e.pageY -divposition.top;
selectrect = mybandit.paper.getById(mybandit.selectgroup);
if(destx>mybandit.selectionx) {
posx = mybandit.selectionx;
}
else {
posx = destx;
}
if(desty>mybandit.selectiony) {
posy = mybandit.selectiony;
}
else {
posy = desty;
}
selectrect.attr({ x: posx, y : posy, width: (Math.abs(destx - mybandit.selectionx)) , height: (Math.abs(desty - mybandit.selectiony)) });
});
$('#'+diveditor).mouseup(function(e) {
if(mybandit.mode!=3) {
return;
}
if(mybandit.selectgroup == 0) {
return;
}
mybandit.paper.getById(mybandit.selectgroup).remove();
divposition = $('#'+diveditor).position();
destx = e.pageX - divposition.left;
desty = e.pageY -divposition.top;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) { // do not select text elements
var xselect = false;
var yselect = false;
if(destx>mybandit.selectionx) {
if((mybandit.selectionx <= el.attr("x"))&&(el.attr("x") <= destx)) {
xselect = true;
}
}
else {
if((mybandit.selectionx >= el.attr("x"))&&(el.attr("x") >= destx)) {
xselect = true;
}
}
if(desty>mybandit.selectiony) {
if((mybandit.selectiony <= el.attr("y"))&&(el.attr("y") <= desty)) {
yselect = true;
}
}
else {
if((mybandit.selectiony >= el.attr("y"))&&(el.attr("y") >= desty)) {
yselect = true;
}
}
if(xselect && yselect) {
mybandit.selectnodes.push({ id : el.id, x: el.attr('x'), y: el.attr('y') });
mybandit.paper.getById(el.id).attr("opacity",0.6);
}
} // end if not text
});
mybandit.selectgroup = 0;
mybandit.selectionx = -1;
mybandit.selectiony = -1;
});
}
/**
* Sets undo/redo buffer size
* @method setUndoRedo
* @param {int} Buffer size
*/
BandIt.prototype.setUndoRedo = function(max) {
this.maxactions = max;
}
/**
* Clear group selection
* @method clearSelection
*/
BandIt.prototype.clearSelection = function() {
// Reset group
for(var i in this.selectnodes) {
groupnode = mybandit.paper.getById(this.selectnodes[i]["id"]);
if(groupnode!=null) {
if(mybandit.isContainer(this.selectnodes[i]["id"])) {
groupnode.attr("opacity",0.8);
}
else {
groupnode.attr("opacity",1);
}
}
}
this.selectnodes = [];
}
/**
* Sets workflow name and description
* @method info
* @param name {String} Name of the workflow
* @param description {String} Description of the workflow
*
*/
BandIt.prototype.info = function(name,description) {
banditLogger.DEBUG("Update info: "+name+","+description);
this.name = name;
this.description = description;
}
/**
* Adds an arrow to a path
* @method arrow
* @param obj1 {Node} Start node
* @param obj2 {Node} End node
* @param connector {Path} Path object between nodes
* @return {Path} Arrow object
*
*/
BandIt.prototype.arrow = function(obj1, obj2, connector) {
var bb1 = obj1.getBBox(),
bb2 = obj2.getBBox(),
p = [{x: bb1.x + bb1.width / 2, y: bb1.y - 1},
{x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1},
{x: bb1.x - 1, y: bb1.y + bb1.height / 2},
{x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2},
{x: bb2.x + bb2.width / 2, y: bb2.y - 1},
{x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1},
{x: bb2.x - 1, y: bb2.y + bb2.height / 2},
{x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2}],
d = {}, dis = [];
for (var i = 0; i < 4; i++) {
for (var j = 4; j < 8; j++) {
var dx = Math.abs(p[i].x - p[j].x),
dy = Math.abs(p[i].y - p[j].y);
if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x) && ((i != 2 && j != 7) || p[i].x > p[j].x) && ((i != 0 && j != 5) || p[i].y > p[j].y) && ((i != 1 && j != 4)
|| p[i].y < p[j].y))) {
dis.push(dx + dy);
d[dis[dis.length - 1]] = [i, j];
}
}
}
if (dis.length == 0) {
var res = [0, 4];
} else {
res = d[Math.min.apply(Math, dis)];
}
var x1 = p[res[0]].x,
y1 = p[res[0]].y,
x4 = p[res[1]].x,
y4 = p[res[1]].y;
dx = Math.max(Math.abs(x1 - x4) / 2, 10);
dy = Math.max(Math.abs(y1 - y4) / 2, 10);
var x2 = [x1, x1, x1 - dx, x1 + dx][res[0]].toFixed(3),
y2 = [y1 - dy, y1 + dy, y1, y1][res[0]].toFixed(3),
x3 = [0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx][res[1]].toFixed(3),
y3 = [0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4][res[1]].toFixed(3);
var angle = Math.atan2(x1-x4.toFixed(3),y4.toFixed(3)-y1);
angle = (angle / (2 * Math.PI)) * 360;
var conn = connector.getBBox();
var ax2 = conn.x+ conn.width/2;
var ay2 = conn.y + conn.height/2;
var size=10;
var arrowPath = this.paper.path("M" + ax2 + " " + ay2 + " L" + (ax2 - size) + " " + (ay2 - size) + " L" + (ax2 - size) + " " + (ay2 + size) + " L" + ax2 + " " + ay2 ).attr("fill","black").rotate((90+angle),ax2,ay2);
return arrowPath;
}
/**
* Register a callback when a node is selected
* @method registerSelect
* @param callback Function to use when a node is selected, function(nodeid, nodeproperties)
*/
BandIt.prototype.registerSelect = function(callback) {
this.selectCallbacks.push(callback);
}
/**
* Register a callback when a node is deleted
* @method registerDelete
* @param callback Function to use when a node is deleted, function(nodeid)
*/
BandIt.prototype.registerDelete = function(callback) {
this.deleteCallbacks.push(callback);
}
/**
* Register a callback when a node is added
* @method registerAdd
* @param callback Function to use when a node is added, function(nodeid)
*/
BandIt.prototype.registerAdd = function(callback) {
this.addCallbacks.push(callback);
}
/**
* Gets current edition mode
* @method getMode
* @return current {int} mode
*
*/
BandIt.prototype.getMode = function() {
return this.mode;
}
/**
* Sets current edition mode
* @method setMode
* @param newmode {int} new edition mode
*
*/
BandIt.prototype.setMode = function(newmode) {
if(this.mode==3 && this.mode!=newmode) {
// Special case, unselect any group
if(this.selectnodes.length>0) {
// Reset group
this.clearSelection();
}
}
this.mode = newmode;
banditLogger.DEBUG("Switch to mode "+this.mode);
}
/**
* Sets global workflow options
* @method setOptions
* @param options {Object} HashMap of options
*
*/
BandIt.prototype.setOptions = function(options) {
this.options = options;
}
/**
* Get node properties
* @method getProperties
* @param nodeid {int} ID of the node
* @return {Object} Array of properties for the node
*/
BandIt.prototype.getProperties = function(nodeid) {
if(this.nodes[nodeid]!=null) {
return this.nodes[nodeid]["properties"];
}
else { return null; }
}
/**
* Get a node property
* @method getProperty
* @param nodeid {int} ID of the node
* @param key {String} Property key
* @return {String} Property value
*/
BandIt.prototype.getProperty = function(nodeid,key) {
if(this.nodes[nodeid]!=null) {
return this.nodes[nodeid]["properties"][key];
}
else { return null; }
}
/**
* Update the properties of a node
* @method setProperties
* @param nodeid {int} ID of the node
* @param props {Object} Properties of the node
*
*/
BandIt.prototype.setProperties = function(nodeid,props) {
if(props["name"]!=null && this.nodes[nodeid]["properties"]["name"]!=null && props["name"]!=this.nodes[nodeid]["properties"]["name"]) {
banditLogger.DEBUG("Change name of node "+nodeid+" to "+props["name"]);
oldtext = this.paper.getById(this.nodes[nodeid]["child"]["text"]);
oldtext.remove();
var node = this.paper.getById(nodeid);
xpos = node.attr("x") + node.attr("width")/2;
ypos = node.attr("y") + node.attr("height")/2;
var nodetext = this.paper.text(xpos,ypos,props["name"]).attr("fill","#FBFBEF");
nodetext.attr("font-size",16*this.zoom);
nodetext.toFront();
this.nodes[node.id]["child"]["text"]=nodetext.id;
}
this.nodes[nodeid]["properties"] = props;
this.newaction("setproperties");
}
BandIt.prototype.setAttributes = function(nodeid,attrs) {
this.paper.getById(nodeid).attr(attrs);
this.newaction("setattributes");
}
/**
* Sets default list of properties
* @method setDefaultProperties
* @param props {Object} Key/value pairs of properties
*
*/
BandIt.prototype.setDefaultProperties = function(props) {
this.properties = props;
}
/**
* get Rapahel Paper element
* @method getPaper
* @return {Paper} paper used for the draw
*/
BandIt.prototype.getPaper = function() {
return this.paper;
}
/**
* Creates a directed link between two nodes
* @method link
* @param startnode {int} Id of start node
* @param endnode {int} Id of end node
* @return {int} Id of the graph link
*/
BandIt.prototype.link = function(startnodeid,endnodeid) {
banditLogger.DEBUG("Link nodes "+startnodeid+" <-> "+endnodeid);
startnode = this.paper.getById(startnodeid);
endnode = this.paper.getById(endnodeid);
xpos = startnode.attr("x") + startnode.attr("width")/2;
ypos = startnode.attr("y") + startnode.attr("height")/2;
xend = endnode.attr("x") + endnode.attr("width")/2 ;
yend = endnode.attr("y") + endnode.attr("height")/2 ;
path = this.paper.path(this.getConnector(startnode,endnode));
//path = this.paper.path("M"+xpos+","+ypos+"L"+xend+","+yend);
arrowpath = this.arrow(startnode,endnode,path);
//banditLogger.DEBUG("add arrow "+arrowpath.id);
this.paths[path.id] = { arrow : arrowpath.id, direction: endnode.id };
mybandit = this;
path.mousedown(function(e) {
if(mybandit.mode==2) {
path = mybandit.paper.getElementByPoint(e.x,e.y);
mybandit.deletepath(path);
}
});
path.toBack();
if (this.inlinks[endnode.id] == null) {
this.inlinks[endnode.id] = [];
}
this.inlinks[endnode.id].push({ path : path.id, node : startnode.id });
if (this.outlinks[startnode.id] == null) {
this.outlinks[startnode.id] = [];
}
this.outlinks[startnode.id].push( { path : path.id, node : endnode.id });
this.newaction("link");
return path.id;
}
/**
* Gets a node id by its name
* @method getByName
* @param name {String} name of the node
* @return {int} Id of the node
*
*/
BandIt.prototype.getByName = function(name) {
var nodeid = null;
for(var node in this.nodes) {
if(name == this.nodes[node]["properties"]["name"]) {
nodeid = node;
break;
}
}
return nodeid;
}
/**
* Check if node is a container
* @method isContainer
* @param {integer} node id
* @return {boolean} True if it is a container, else returns false
*/
BandIt.prototype.isContainer = function(nodeid) {
if(this.nodes[nodeid]["type"]!=null && this.nodes[nodeid]["type"]==CONTAINER) {
return true;
}
return false;
}
/**
* Adds a new node container on paper. A container acts as a node except it cannot be used
* for links, and nodes dropped in a container will be linked to the container (parent property of the node).
* @method addContainer
* @param name {string} Unique name of the node (optional). If undefined, a default counter is used
* @param atts {Object} Element properties (optional). For properties, look at Raphael Element documentation.
* @return {Node} Node element
*
*/
BandIt.prototype.addContainer = function(name,attrs) {
var node = this.add(name,attrs);
node.attr("width",200*this.zoom);
node.attr("height",200*this.zoom);
node.attr("opacity",0.8);
node.attr("fill","#01DF01");
node.toBack();
this.nodes[node.id]["type"]=CONTAINER;
this.action--; // Move back because has been registered as a node
this.newaction("addcontainer");
return node;
}
/**
* Adds a new node on paper
* @method add
* @param name {string} Unique name of the node (optional). If undefined, a default counter is used
* @param atts {Object} Element properties (optional). For properties, look at Raphael Element documentation.
* @return {Node} Node element
*
*/
BandIt.prototype.add = function(name,attrs) {
var node = this.paper.rect(50,50, 100*this.zoom,60*this.zoom);
nodeattr = pick(attrs,{ fill : "#f00" });
node.attr(nodeattr);
this.nodes[node.id] = {};
this.nodes[node.id]["properties"] = {};
for(var p in this.properties) {
this.nodes[node.id]["properties"][p] = this.properties[p];
}
xpos = node.attr("x") + node.attr("width")/2;
ypos = node.attr("y") + node.attr("height")/2;
nodename = pick(name, "node"+this.count);
this.count += 1;
var nodetext = this.paper.text(xpos,ypos,nodename).attr("fill","#FBFBEF");
nodetext.attr("font-size",16*this.zoom);
nodetext.toFront();
this.nodes[node.id]["child"] = {}
this.nodes[node.id]["child"]["text"]=nodetext.id;
this.nodes[node.id]["properties"]["name"]=nodename;
var mybandit = this;
node.mousedown(function(e) {
node = mybandit.paper.getElementByPoint(e.x,e.y);
currentnode = node.id;
if (mybandit.mode==1) {
// Link mode , nothing to do
return;
}
if (mybandit.mode==3) {
// Group mode , nothing to do
return;
}
if (mybandit.mode==2) {
// Delete mode, delete node
if(mybandit.selectnodes.length>0) {
for(var i in selectnodes) {
mybandit.deletenode(selectnodes[i]["id"]);
}
mybandit.clearSelection();
}
else {
mybandit.deletenode(node.id);
}
return;
}
("node "+node.id+" selected");
// callbacks
for(var i in mybandit.selectCallbacks) {
mybandit.selectCallbacks[i](node.id,mybandit.nodes[node.id]["properties"]);
}
});
node.mouseup(function(e) {
if (mybandit.mode==2) {
return;
}
if (mybandit.mode==3) {
return;
}
if (mybandit.mode==1) {
node = mybandit.paper.getElementByPoint(e.x,e.y);
if(node.id == currentnode) {
return; // Do not link same node
}
startnode = mybandit.paper.getById(currentnode);
if(!mybandit.isContainer(node.id) && !mybandit.isContainer(startnode.id)) {
mybandit.link(startnode.id, node.id);
}
else {
banditLogger.DEBUG("destination is container, drop link");
}
}
});
var start = function () {
if(mybandit.mode == 2) {
return;
}
this.ox = this.attr("x");
this.oy = this.attr("y");
this.animate({ opacity: .25}, 500, ">");
};
var move = function (dx, dy,x,y) {
if(mybandit.mode == 1 || mybandit.mode==2) {
}
else {
var isselected = false;
for(var i in mybandit.selectnodes) {
if (mybandit.selectnodes[i]["id"]==this.id) {
isselected = true;
break;
}
}
if(!isselected) {
deltax = (this.ox + dx) - this.attr("x");
deltay = (this.oy + dy) - this.attr("y");
this.attr({x: this.ox + dx, y: this.oy + dy});
xpos = this.ox + dx + this.attr("width")/2;
ypos = this.oy + dy + this.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[this.id]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(this.id);
// If container, move childs
if(mybandit.nodes[this.id]["type"]==CONTAINER) {
mybandit.moveChilds(this.id,deltax ,deltay);
}
}
else {
// move all elements
for(var i in mybandit.selectnodes) {
nodeposx=this.ox + dx;
nodeposy=this.oy + dy;
deltax = (this.ox + dx) - this.attr("x");
deltay = (this.oy + dy) - this.attr("y");
var node = mybandit.paper.getById(mybandit.selectnodes[i]["id"]);
node.attr({x: mybandit.selectnodes[i]["x"] + dx, y: mybandit.selectnodes[i]["y"] + dy});
xpos = mybandit.selectnodes[i]["x"] + dx + node.attr("width")/2;
ypos = mybandit.selectnodes[i]["y"] + dy + node.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[mybandit.selectnodes[i]["id"]]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(mybandit.selectnodes[i]["id"]);
// If container, move childs
if(mybandit.selectnodes[i]["type"]==CONTAINER) {
mybandit.moveChilds(mybandit.selectnodes[i]["id"],deltax ,deltay );
}
}
}
}
};
var up = function () {
if(mybandit.mode == 2 || mybandit.mode ==3) {
return;
}
if(mybandit.isContainer(this.id)) {
this.animate({ opacity: 0.8}, 500, ">");
}
else {
this.animate({ opacity: 1}, 500, ">");
}
var nbox = mybandit.paper.getById(this.id).getBBox();
var intersect = false;
for(var container in mybandit.nodes) {
if(container != this.id && mybandit.isContainer(container)) {
var cbox = mybandit.paper.getById(container).getBBox();
if(Raphael.isBBoxIntersect(nbox,cbox) && (mybandit.nodes[container]["parent"]==null || mybandit.nodes[container]["parent"]!=this.id)) {
banditLogger.DEBUG("attach node to container "+container);
mybandit.nodes[this.id]["parent"] = container;
mybandit.paper.getById(container).toBack();
intersect = true;
}
}
if(intersect) {
break;
}
else {
// Was in a container? remove it
if(mybandit.nodes[this.id]["parent"] != null) {
banditLogger.DEBUG("detach node from container "+mybandit.nodes[this.id]["parent"]);
mybandit.nodes[this.id]["parent"] = null;
}
}
}
if(mybandit.mode != 1) {
// Do not record a move for a link
mybandit.newaction("move");
}
};
node.drag(move,start,up);
// callbacks
for(var i in mybandit.addCallbacks) {
mybandit.addCallbacks[i](node.id);
}
this.newaction("addnode");
return node;
} // end add
/**
* Move childs of a container
* @method moveChidls
* @param {int} id of the container
* @param {int} X translation
* @param {int} Y translation
*/
BandIt.prototype.moveChilds = function(id,dx,dy) {
for(var i in this.nodes) {
if(this.nodes[i]["parent"] == id ) {
//banditLogger.DEBUG("move child "+i+" - "+dx+","+dy);
var node = this.paper.getById(i);
node.attr({x: node.attr("x")+ dx, y: node.attr("y") + dy});
xpos = this.nodes[i]["x"] + dx + node.attr("width")/2;
ypos = this.nodes[i]["y"] + dy + node.attr("height")/2;
var textnode = this.paper.getById(this.nodes[i]["child"]["text"]);
textnode.attr({x: textnode.attr("x")+ dx, y: textnode.attr("y") + dy});
//banditLogger.DEBUG("redraw links for "+i);
if(this.nodes[i]["type"]==CONTAINER) {
// Recursive move
this.moveChilds(i,dx,dy);
}
this.redrawpaths(i);
}
}
}
// Redraw all path linked to current dragged node
/**
* Redraw all paths linked to a node
* @method redrawpaths
* @param nodeid {int} Id of the Node
*
*/
BandIt.prototype.redrawpaths = function(nodeid) {
node = this.paper.getById(nodeid);
if (node == null ) {
return;
}
if (this.outlinks[nodeid]!=null) {
for(var i=0;i<this.outlinks[nodeid].length;i++) {
this.redrawpath(this.outlinks[nodeid][i], node);
}
}
if (this.inlinks[nodeid]!=null) {
for(var i=0;i<this.inlinks[nodeid].length;i++) {
this.redrawpath(this.inlinks[nodeid][i],node);
}
}
} // end redrawpaths
/**
* Delete a node
* @method deletenode
* @param nodeid {int} Node id to delete
*
*/
BandIt.prototype.deletenode = function(nodeid) {
var node = this.paper.getById(nodeid);
banditLogger.DEBUG("delete node "+node.id);
// callbacks
for(var i in this.deleteCallbacks) {
this.deleteCallbacks[i](node.id);
}
if(this.nodes[node.id]["type"] == CONTAINER) {
// Delete childs of container
for(var cnode in this.nodes) {
if(this.nodes[cnode]["parent"] == node.id) {
console.log("should delete "+cnode);
this.deletenode(cnode);
}
}
}
paths = this.outlinks[node.id];
// Delete text
if(this.nodes[node.id]!=null) {
text = this.paper.getById(this.nodes[node.id]["child"]["text"]);
if(text!=null) {
text.remove();
}
}
// Remove paths linked to node, if any
for(var i in paths) {
if(paths[i]!=null) {
var pathobject = this.paper.getById(paths[i]["path"]);
this.deletepath(pathobject);
}
}
delete this.outlinks[node.id];
paths = this.inlinks[node.id];
for(var i in paths) {
if(paths[i]!=null) {
var pathobject = this.paper.getById(paths[i]["path"]);
this.deletepath(pathobject);
}
}
delete this.inlinks[node.id];
delete this.nodes[node.id];
node.remove();
this.newaction("deletenode");
} // end deletenode
/**
* Delete a path
* @methode deletepath
* @param path {Path} Path element to delete
*
*/
BandIt.prototype.deletepath = function(path) {
banditLogger.DEBUG("Delete link "+path.id);
pathid = path.id;
for(var node in this.outlinks) {
links = this.outlinks[node];
for(var i in links) {
if(links[i]!=null && links[i]["path"]==pathid) {
banditLogger.DEBUG("remove outlinks path "+i);
links[i]=null;
}
}
}
for(var node in this.inlinks) {
links = this.inlinks[node];
for(var i in links) {
if(links[i]!=null && links[i]["path"]==pathid) {
banditLogger.DEBUG("remove inlinks path "+i);
links[i]=null;
}
}
}
arrow = this.paths[path.id]["arrow"];
this.paper.getById(arrow).remove();
path.remove();
this.newaction("deletepath");
} // end deletepath
// Redraw a path element
/**
* Redraw a path
* @methode redrawpath
* @param link {Path} Path element to delete
* @param node {int} Id of the node the path is linked as origin
*
*/
BandIt.prototype.redrawpath = function(link,node) {
if(link==null) {
// A deleted path
return;
}
path = this.paper.getById(link["path"]);
arrow = this.paths[path.id]["arrow"];
this.paper.getById(arrow).remove();
remotenode = this.paper.getById(link["node"]);
xend = remotenode.attr("x") + remotenode.attr("width")/2;
yend = remotenode.attr("y") + remotenode.attr("height")/2;
xpos = node.attr("x") + node.attr("width")/2;
ypos = node.attr("y") + node.attr("height")/2;
//path.attr("path","M"+xpos+","+ypos+"L"+xend+","+yend);
path.attr("path",this.getConnector(node,remotenode));
arrowdirection = this.paths[path.id]["direction"];
if(arrowdirection!=node.id) {
newarrow = this.arrow(node,remotenode,path);
}
else {
newarrow = this.arrow(remotenode, node,path);
}
this.paths[path.id] = { arrow: newarrow.id, direction: arrowdirection } ;
} // end redrawpath
/**
* Get SVG path to connect two objects
* @methode getConnector
* @param obj1 {Node} Start node
* @param obj2 {Node} End node
*
*/
BandIt.prototype.getConnector = function(obj1,obj2) {
var bb1 = obj1.getBBox(),
bb2 = obj2.getBBox(),
p = [{x: bb1.x + bb1.width / 2, y: bb1.y - 1},
{x: bb1.x + bb1.width / 2, y: bb1.y + bb1.height + 1},
{x: bb1.x - 1, y: bb1.y + bb1.height / 2},
{x: bb1.x + bb1.width + 1, y: bb1.y + bb1.height / 2},
{x: bb2.x + bb2.width / 2, y: bb2.y - 1},
{x: bb2.x + bb2.width / 2, y: bb2.y + bb2.height + 1},
{x: bb2.x - 1, y: bb2.y + bb2.height / 2},
{x: bb2.x + bb2.width + 1, y: bb2.y + bb2.height / 2}],
d = {}, dis = [];
for (var i = 0; i < 4; i++) {
for (var j = 4; j < 8; j++) {
var dx = Math.abs(p[i].x - p[j].x),
dy = Math.abs(p[i].y - p[j].y);
if ((i == j - 4) || (((i != 3 && j != 6) || p[i].x < p[j].x) && ((i != 2 && j != 7) || p[i].x > p[j].x) && ((i != 0 && j != 5) || p[i].y > p[j].y) && ((i != 1 && j != 4)
|| p[i].y < p[j].y))) {
dis.push(dx + dy);
d[dis[dis.length - 1]] = [i, j];
}
}
}
if (dis.length == 0) {
var res = [0, 4];
} else {
res = d[Math.min.apply(Math, dis)];
}
var x1 = p[res[0]].x,
y1 = p[res[0]].y,
x4 = p[res[1]].x,
y4 = p[res[1]].y;
dx = Math.max(Math.abs(x1 - x4) / 2, 10);
dy = Math.max(Math.abs(y1 - y4) / 2, 10);
var x2 = [x1, x1, x1 - dx, x1 + dx][res[0]].toFixed(3),
y2 = [y1 - dy, y1 + dy, y1, y1][res[0]].toFixed(3),
x3 = [0, 0, 0, 0, x4, x4, x4 - dx, x4 + dx][res[1]].toFixed(3),
y3 = [0, 0, 0, 0, y1 + dy, y1 - dy, y4, y4][res[1]].toFixed(3);
return ["M", x1.toFixed(3), y1.toFixed(3), "C", x2, y2, x3, y3, x4.toFixed(3), y4.toFixed(3)].join(",");
}
/**
* Zoom in the workflow
* @methode zoomIn
*
*/
BandIt.prototype.zoomIn = function() {
this.zoom = this.zoom * 2;
banditLogger.DEBUG("zoom with "+this.zoom);
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr("x", el.attr("x") * 2);
el.attr("y", el.attr("y") * 2);
el.attr({ width : el.attr("width") * 2, height: el.attr("height") * 2 });
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
text = mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]);
text.attr({x: xpos, y: ypos}).attr("font-size",text.attr("font-size") * 2);
mybandit.redrawpaths(el.id);
}
});
this.newaction("zoomin");
}
/**
* Zoom out the workflow
* @methode zoomOut
*
*/
BandIt.prototype.zoomOut = function() {
this.zoom = this.zoom * 0.5;
banditLogger.DEBUG("zoom with "+this.zoom);
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr({x: el.attr("x") * 0.5, y: el.attr("y") * 0.5});
el.attr({ width : el.attr("width") * 0.5, height: el.attr("height") * 0.5});
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
text = mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]);
text.attr({x: xpos, y: ypos}).attr("font-size",text.attr("font-size")*0.5);
mybandit.redrawpaths(el.id);
}
});
this.newaction("zoomout");
}
/**
* Translate on the left the workflow
* @methode moveLeft
* @param step {int} Step of the move
*
*/
BandIt.prototype.moveLeft = function(step) {
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr({ x: el.attr("x") - step });
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(el.id);
}
});
this.newaction("move");
}
/**
* Translate on the right the workflow
* @methode moveRight
* @param step {int} Step of the move
*
*/
BandIt.prototype.moveRight = function(step) {
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr({ x: el.attr("x") + step });
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(el.id);
}
});
this.newaction("move");
}
/**
* Translate to the up the workflow
* @methode moveUp
* @param step {int} Step of the move
*
*/
BandIt.prototype.moveUp = function(step) {
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr({ y: el.attr("y") - step });
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(el.id);
}
});
this.newaction("move");
}
/**
* Translate to the down the workflow
* @methode moveDown
* @param step {int} Step of the move
*
*/
BandIt.prototype.moveDown = function(step) {
mybandit = this;
mybandit.paper.forEach(function (el) {
if(mybandit.nodes[el.id]!=null) {
el.attr({ y: el.attr("y") + step });
xpos = el.attr("x") + el.attr("width")/2;
ypos = el.attr("y") + el.attr("height")/2;
mybandit.paper.getById(mybandit.nodes[el.id]["child"]["text"]).attr({x: xpos, y: ypos});
mybandit.redrawpaths(el.id);
}
});
this.newaction("move");
}
/**
* Resets the workflow
* @method clean
*
*/
BandIt.prototype.clean = function() {
this.currentnode = null;
this.nodes = {};
this.outlinks = {};
this.inlinks = {};
this.paths = {};
this.paper.clear();
this.newaction("clean");
}
/**
* Imports a workflow, resetting undo/redo actions.
* @method import
* @param data {String} Workflow data
* @param clean {boolean} Clean exiting data or append to existing workflow
* @return {Array} Name and Description of the workflow
*/
BandIt.prototype.import = function(data,clean) {
var res = this.load(data,clean);
bandit.actions=[];
bandit.action=-1;
return res;
}
/**
* Loads a workflow
* @method load
* @param data {String} Workflow data
* @param clean {boolean} Clean exiting data or append to existing workflow
* @return {Array} Name and Description of the workflow
*/
BandIt.prototype.load = function (data,clean) {
// If not clean, skip root
data = data.replace(/(\n|\r)+$/, '');
wflow = JSON.parse($.base64.decode(data));
var maxnodeid = 0;
this.zoomFit();
if(clean) {
this.clean();
}
var wlinks = {}; // list of node id / node names to link with
var wnodes = {}; // list of node name/node id pairs
var wparents = {} // parent reference for containers
this.name = wflow["workflow"]["name"];
this.description = wflow["workflow"]["description"];
for(var node in wflow["workflow"]) {
if(node=="name" || node=="description") {
continue;
}
var nexts = null;
var newnode = null;
if(!clean && node=="root") {
// This is root and we are in append, so do not add/draw the root node
var nexts = wflow["workflow"][node]["next"];
var nextnodes = nexts.split(',');
// Get root node
var rootnodeid = this.getByName("root");
banditLogger.DEBUG("Root node id:" +rootnodeid);
wlinks[rootnodeid] = nextnodes; // register future links
newnodeid = rootnodeid;
}
else {
var newnode;
if(wflow["workflow"][node]["type"]!=null && wflow["workflow"][node]["type"]==CONTAINER) {
newnode = this.addContainer(node,wflow["workflow"][node]["graph"]);
}
else {
newnode = this.add(node,wflow["workflow"][node]["graph"]);
}
banditLogger.DEBUG("Load node: "+node+","+newnode.id);
if(node.indexOf("node")==0) {
var patt=/node(\d+)/;
var nodeid = patt.exec(node);
if(nodeid.length>0) {
var newid = parseInt(nodeid[1]);
if(newid>maxnodeid) { maxnodeid = newid; }
}
}
for(var prop in this.properties) {
this.nodes[newnode.id]["properties"][prop]=wflow["workflow"][node][prop];
}
// is node contained in container?
if(wflow["workflow"][node]["parent"]!=null) {
wparents[newnode.id] = wflow["workflow"][node]["parent"];
}
wnodes[node] = newnode.id;
newnodeid = newnode.id;
}
var nexts = wflow["workflow"][node]["next"];
var nextnodes = nexts.split(',');
if(nextnodes.length>0 && nextnodes[0]!="") {
banditLogger.DEBUG("register link from "+newnodeid+" to "+nextnodes);
wlinks[newnodeid] = nextnodes; // register future links
}
} // end for wflow
// Set parents
for(var wparent in wparents) {
banditLogger.DEBUG("Set parent of node "+wparent);
for(var containernode in this.nodes) {
if(this.nodes[containernode]["properties"]["name"] == wparents[wparent]) {
this.nodes[wparent]["parent"] = containernode;
break;
}
}
}
// Add the links
for(var wlink in wlinks) {
banditLogger.DEBUG("Add links of node "+wlink);
for(var i in wlinks[wlink]) {
remotenodeid = wnodes[wlinks[wlink][i]];
originnodeid = wlink;
this.link(originnodeid,remotenodeid);
}
}
this.count = maxnodeid + 1;
return [this.name,this.workflow];
}
/**
* Zoom back to 1
* @method zoomFit
*
*/
BandIt.prototype.zoomFit = function() {
if(this.zoom>1) {
this.zoomOut();
this.zoomFit();
}
if(this.zoom<1) {
this.zoomIn();
this.zoomFit();
}
this.newaction("zoomfit");
}
/**
* Export diagram to GraphML
* @method exportGraphML
* @return {String} GraphML workflow
*/
BandIt.prototype.exportGraphML = function() {
var graph = '<?xml version="1.0" encoding="UTF-8"?><graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlnshttp://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd">\n';
i = 0;
// Define properties
for(var key in this.properties) {
graph += '<key id="prop'+i+'" for="node" attr.name="'+key+'" attr.type="string"><default>'+this.properties[key]+'</default></key>\n';
i += 1;
}
graph += '<key id="type" for="node" attr.name="type" attr.type="string"><default></default></key>';
graph += '<key id="parent" for="node" attr.name="parent" attr.type="string"><default></default></key>';
graph += '<graph id="'+this.name+'" edgedefault="directed">\n';
graph += '<desc>'+this.description+'</desc>';
// Now manage nodes
for(var i in this.nodes) {
var node = this.nodes[i];
graph += '<node id="'+node["properties"]["name"]+'">';
//var attrs = this.paper.getById(i).attr();
for(var prop in node["properties"]) {
graph += '<data key="'+prop+'">'+node["properties"][prop]+'</data>\n';
}
if(node["type"]==CONTAINER) {
graph += '<data key="type">'+CONTAINER+'</data>\n';
}
if(node["parent"]!=null) {
graph += '<data key="parent">'+this.nodes[node["parent"]]["properties"]["name"]+'</data>\n';
}
graph += '</node>\n';
// Now manage links
var nexts = this.outlinks[i];
if(node["properties"]["name"]!="root") {
if(this.inlinks[i]==undefined && node["type"]!=CONTAINER) { alert("Warning, the node "+node["properties"]["name"]+" is not linked to root node");}
}
var next = "";
var nbnext = 0;
for (var j in nexts) {
if(nexts[j]!=null) {
next = this.nodes[nexts[j]["node"]]["properties"]["name"];
graph += '<edge id="'+i+'-'+j+'" source="'+node["properties"]["name"]+'" target="'+next+'"/>\n';
nbnext += 1;
}
}
}
graph += '</graph>\n';
graph += '</graphml>';
return graph;
}
/**
* Export diagram to YAML format with all information
* @method export
* @return {String} YAML export of the workflow
*/
BandIt.prototype.export = function(silent) {
var is_silent = pick(silent,false);
var exportobject = {};
exportobject["options"] = {};
for(var option in this.options) {
exportobject["options"][option] = this.options[option];
}
exportobject["workflow"] = {};
exportobject["workflow"]["name"] = this.name;
exportobject["workflow"]["description"] = this.description;
for(var i in this.nodes) {
var node = this.nodes[i];
var attrs = this.paper.getById(i).attr();
nodeprops = {};
nodeprops["graph"] = attrs;
for(var prop in node["properties"]) {
if(prop!="name") {
nodeprops[prop] = node["properties"][prop];
}
}
// Now manage links
var nexts = this.outlinks[i];
if(node["properties"]["name"]!="root") {
//banditLogger.DEBUG(this.inlinks[i]);
if(this.inlinks[i]==undefined && node["type"]!=CONTAINER && !is_silent) { alert("Warning, the node "+node["properties"]["name"]+" is not linked to root node");}
}
var next = "";
var nbnext = 0;
for (var j in nexts) {
if(nexts[j]!=null) {
if(nbnext>0) {
next += ",";
}
next += this.nodes[nexts[j]["node"]]["properties"]["name"];
nbnext += 1;
}
}
if(next!="") {
nodeprops["next"] = next;
}
if(node["type"]==CONTAINER) {
nodeprops["type"] = CONTAINER;
}
if(node["parent"]!=null) {
nodeprops["parent"] = this.nodes[node["parent"]]["properties"]["name"];
}
exportobject["workflow"][node["properties"]["name"]] = nodeprops;
}
if(!is_silent) {
banditLogger.DEBUG(JSON.stringify(exportobject));
}
return JSON.stringify(exportobject);
}
/**
* Record a new action for undo/redo.
* Not yet implemented: move, delete
* @method newaction
* @param type {string} Optional action type (for debug)
*/
BandIt.prototype.newaction = function (type) {
if(this.actions.length>this.maxactions) {
this.actions.pop();
}
if(this.recordactions) {
this.action++;
banditLogger.DEBUG("Record new action: "+type);
this.actions[this.action]=$.base64.encode(this.export(true));
if(this.actions.length>this.action) {
// empty array after this point
this.actions[this.action+1]=null;
}
}
}
/**
* Undo
* @method undo last action
*/
BandIt.prototype.undo = function() {
if(this.action>0 && this.actions[this.action-1]!=null) {
this.action--;
var undo_action = this.actions[this.action];
banditLogger.DEBUG("undo "+this.action);
this.recordactions = false;
this.load(undo_action,true);
this.recordactions = true;
}
}
/**
* Redo
* @method redo last action
*/
BandIt.prototype.redo = function() {
if(this.action<this.actions.length-1 && this.actions[this.action+1]!=null) {
this.action++;
var redo_action = this.actions[this.action];
banditLogger.DEBUG("redo "+this.action);
this.recordactions = false;
this.load(redo_action,true);
this.recordactions = true;
}
}
/**
* Eval an argument.
* @class pick
* @param arg {Object} Value to test
* @param def {Object} Default value
* @return {Object} default is undefined
*
*/
function pick(arg, def) {
return (typeof arg == 'undefined' ? def : arg);
}
/**
* BanditLogger
* @class BanditLogger
* @constructor
* @param level {int} Level of log: 0:DEBUG, 1:INFO, 2: ERROR
*
*/
var level = 2; // Error
function BanditLogger(newlevel) {
level = newlevel;
}
/**
* Log a debug level message
* @method DEBUG
* @param msg {String} message to log
*
*/
BanditLogger.prototype.DEBUG = function(msg) {
if (level<=0) {
date = new Date
console.log("#DEBUG "+date.toString()+": "+msg);
}
}
/**
* Log an info level message
* @method INFO
* @param msg {String} message to log
*
*/
BanditLogger.prototype.INFO = function(msg) {
if (level<=1) {
date = new Date
console.log("#INFO "+date.toString()+": "+msg);
}
}
/**
* Log an error level message
* @method ERROR
* @param msg {String} message to log
*
*/
BanditLogger.prototype.ERROR = function(msg) {
if (level<=2) {
date = new Date
console.log("#ERROR "+date.toString()+": "+msg);
}
}
banditLogger = new BanditLogger(2);