azkaban.svg.graph.view.js

537 lines | 15.983 kB Blame History Raw Download
function hasClass(el, name) 
{
	var classes = el.getAttribute("class");
	if (classes == null) {
		return false;
	}
   return new RegExp('(\\s|^)'+name+'(\\s|$)').test(classes);
}

function addClass(el, name)
{
   if (!hasClass(el, name)) { 
   		var classes = el.getAttribute("class");
   		classes += classes ? ' ' + name : '' +name;
   		el.setAttribute("class", classes);
   }
}

function removeClass(el, name)
{
   if (hasClass(el, name)) {
      var classes = el.getAttribute("class");
      el.setAttribute("class", classes.replace(new RegExp('(\\s|^)'+name+'(\\s|$)'),' ').replace(/^\s+|\s+$/g, ''));
   }
}


azkaban.SvgGraphView = Backbone.View.extend({
	events: {
		"click g" : "clickGraph",
		"contextmenu" : "handleRightClick",
		"contextmenu g" : "handleRightClick",
		"contextmenu polyline": "handleRightClick"
	},
	initialize: function(settings) {
		this.model.bind('change:selected', this.changeSelected, this);
		this.model.bind('centerNode', this.centerNode, this);
		this.model.bind('change:graph', this.render, this);
		this.model.bind('resetPanZoom', this.resetPanZoom, this);
		this.model.bind('change:update', this.handleStatusUpdate, this);
		this.model.bind('change:disabled', this.handleDisabledChange, this);
		this.model.bind('change:updateAll', this.handleUpdateAllStatus, this);
		
		this.graphMargin = settings.graphMargin ? settings.graphMargin : 25;
		this.svgns = "http://www.w3.org/2000/svg";
		this.xlinksn = "http://www.w3.org/1999/xlink";
		
		var graphDiv = this.el[0];
		var svg = $(this.el).find('svg')[0];
		this.svgGraph = svg;
		
		var gNode = document.createElementNS(this.svgns, 'g');
		svg.appendChild(gNode);
		this.mainG = gNode;
		if (settings.rightClick) {
			this.rightClick = settings.rightClick;
		}

		$(svg).svgNavigate();
	},
	initializeDefs: function(self) {
		var def = document.createElementNS(svgns, 'defs');
		def.setAttribute("id", "buttonDefs");

		// ArrowHead
		var arrowHeadMarker = document.createElementNS(svgns, 'marker');
		arrowHeadMarker.setAttribute("id", "triangle");
		arrowHeadMarker.setAttribute("viewBox", "0 0 10 10");
		arrowHeadMarker.setAttribute("refX", "5");
		arrowHeadMarker.setAttribute("refY", "5");
		arrowHeadMarker.setAttribute("markerUnits", "strokeWidth");
		arrowHeadMarker.setAttribute("markerWidth", "4");
		arrowHeadMarker.setAttribute("markerHeight", "3");
		arrowHeadMarker.setAttribute("orient", "auto");
		var path = document.createElementNS(svgns, 'polyline');
		arrowHeadMarker.appendChild(path);
		path.setAttribute("points", "0,0 10,5 0,10 1,5");

		def.appendChild(arrowHeadMarker);
		
		this.svgGraph.appendChild(def);
	},
	render: function(self) {
		console.log("graph render");

		// Clean everything
		while (this.mainG.lastChild) {
			this.mainG.removeChild(this.mainG.lastChild);
		}

		var data = this.model.get("data");
		var nodes = data.nodes;
		var edges = data.edges;
		if (nodes.length == 0) {
			console.log("No results");
			return;
		};
	
		nodes.sort();
		edges.sort();
		
		var bounds = {};
		this.nodes = {};
		for (var i = 0; i < nodes.length; ++i) {
			this.nodes[nodes[i].id] = nodes[i];
			nodes[i].label = nodes[i].id;
		}
		
		this.gNodes = {};
		for (var i = 0; i < nodes.length; ++i) {
			this.drawNode(this, nodes[i]);
		}
		
		// layout
		layoutGraph(nodes, edges, 10);
		this.moveNodes(bounds);
		
		for (var i = 0; i < edges.length; ++i) {
			var inNodes = this.nodes[edges[i].target].inNodes;
			if (!inNodes) {
				inNodes = {};
				this.nodes[edges[i].target].inNodes = inNodes;
			}
			inNodes[edges[i].from] = this.nodes[edges[i].from];
			
			var outNodes = this.nodes[edges[i].from].outNodes;
			if (!outNodes) {
				outNodes = {};
				this.nodes[edges[i].from].outNodes = outNodes;
			}
			outNodes[edges[i].target] = this.nodes[edges[i].target];

			this.drawEdge(this, edges[i]);
		}
		
		this.model.set({"nodes": this.nodes, "edges": edges});
		
		var margin = this.graphMargin;
		bounds.minX = bounds.minX ? bounds.minX - margin : -margin;
		bounds.minY = bounds.minY ? bounds.minY - margin : -margin;
		bounds.maxX = bounds.maxX ? bounds.maxX + margin : margin;
		bounds.maxY = bounds.maxY ? bounds.maxY + margin : margin;
		
		this.assignInitialStatus(self);
		this.handleDisabledChange(self);
		this.graphBounds = bounds;
		this.resetPanZoom(0);
	},
	handleDisabledChange: function(evt) {
		var disabledMap = this.model.get("disabled");

		for(var id in this.nodes) {
			 var g = this.gNodes[id];
			if (disabledMap[id]) {
				this.nodes[id].disabled = true;
				addClass(g, "disabled");
			}
		    else {
		    	this.nodes[id].disabled = false;
		    	removeClass(g, "disabled");
			}
		}
	},
	assignInitialStatus: function(evt) {
		var data = this.model.get("data");
		for (var i = 0; i < data.nodes.length; ++i) {
			var updateNode = data.nodes[i];
			var g = this.gNodes[updateNode.id];
			if (updateNode.status) {
				addClass(g, updateNode.status);
			}
			else {
				addClass(g, "READY");
			}
		}
	},
	changeSelected: function(self) {
		console.log("change selected");
		var selected = this.model.get("selected");
		var previous = this.model.previous("selected");
		
		if (previous) {
			// Unset previous
			var g = this.gNodes[previous];
			removeClass(g, "selected");
		}
		
		if (selected) {
			var g = this.gNodes[selected];
			var node = this.nodes[selected];
			
			addClass(g, "selected");
			
			console.log(this.model.get("autoPanZoom"));
			if (this.model.get("autoPanZoom")) {
				var offset = 150;
				var widthHeight = offset*2;
				var x = node.x - offset;
				var y = node.y - offset;
				
				$(this.svgGraph).svgNavigate("transformToBox", {x: x, y: y, width: widthHeight, height: widthHeight});
			}
		}
	},
	handleStatusUpdate: function(evt) {
		var updateData = this.model.get("update");
		if (updateData.nodes) {
			for (var i = 0; i < updateData.nodes.length; ++i) {
				var updateNode = updateData.nodes[i];
				
				var g = this.gNodes[updateNode.id];
				this.handleRemoveAllStatus(g);
				
				addClass(g, updateNode.status);
			}
		}
	},
	handleRemoveAllStatus: function(gNode) {
		for (var j = 0; j < statusList.length; ++j) {
			var status = statusList[j];
			removeClass(gNode, status);
		}
	},
	clickGraph: function(self) {
		console.log("click");
		if (self.currentTarget.jobid) {
			this.model.set({"selected": self.currentTarget.jobid});
		}
	},
	handleRightClick: function(self) {
		if (this.rightClick) {
			var callbacks = this.rightClick;
			var currentTarget = self.currentTarget;
			if (callbacks.node && currentTarget.jobid) {
				callbacks.node(self);
			}
			else if (callbacks.edge && (currentTarget.nodeName == "polyline" || currentTarget.nodeName == "line")) {
				callbacks.edge(self);
			}
			else if (callbacks.graph) {
				callbacks.graph(self);
			}
			return false;
		}
	
		return true;
	},	
	drawEdge: function(self, edge) {
		var svg = self.svgGraph;
		var svgns = self.svgns;
		
		var startNode = this.nodes[edge.from];
		var endNode = this.nodes[edge.target];
		
		var startPointY = startNode.y + startNode.height/2 - 3;
		var endPointY = endNode.y - endNode.height/2 + 3;
		if (edge.guides) {
			var pointString = "" + startNode.x + "," + startPointY + " ";

			for (var i = 0; i < edge.guides.length; ++i ) {
				edgeGuidePoint = edge.guides[i];
				pointString += edgeGuidePoint.x + "," + edgeGuidePoint.y + " ";
			}
			
			pointString += endNode.x + "," + endPointY;
			var polyLine = document.createElementNS(svgns, "polyline");
			polyLine.setAttribute("class", "edge");
			polyLine.setAttribute("points", pointString);
			polyLine.setAttribute("style", "fill:none;");
			$(self.mainG).prepend(polyLine);
		}
		else { 
			var line = document.createElementNS(svgns, 'line');
			line.setAttribute("class", "edge");
			line.setAttribute("x1", startNode.x);
			line.setAttribute("y1", startPointY);
			line.setAttribute("x2", endNode.x);
			line.setAttribute("y2", endPointY);
			
			$(self.mainG).prepend(line);
		}
	},
	drawNode: function(self, node) {
		if (node.type == 'flow') {
			this.drawFlowNode(self, node);
		}
		else {
			this.drawBoxNode(self,node);
			//this.drawCircleNode(self,node,bounds);
		}
	},
	moveNodes: function(bounds) {
		var nodes = this.nodes;
		var gNodes = this.gNodes;
		
		for (var key in nodes) {
			var node = nodes[key];
			var gNode = gNodes[node.id];
			var centerX = node.centerX;
			var centerY = node.centerY;
			
			gNode.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
			this.addBounds(bounds, {minX:node.x - centerX, minY: node.y - centerY, maxX: node.x + centerX, maxY: node.y + centerY});
		}
	},
	drawFlowNode: function(self, node) {
		var svg = self.svgGraph;
		var svgns = self.svgns;

		var height = 26;
		var nodeG = document.createElementNS(svgns, "g");
		nodeG.setAttribute("class", "jobnode");
		nodeG.setAttribute("font-family", "helvetica");
		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
		this.gNodes[node.id] = nodeG;
		
		var innerG = document.createElementNS(svgns, "g");
		innerG.setAttribute("class", "nodebox");
		
		var rect = document.createElementNS(svgns, 'rect');
		rect.setAttribute("rx", 3);
		rect.setAttribute("ry", 5);
		rect.setAttribute("style", "width:inherit;stroke-opacity:1");
		//rect.setAttribute("class", "nodecontainer");
		
		var text = document.createElementNS(svgns, 'text');
		var textLabel = document.createTextNode(node.label);
		text.appendChild(textLabel);
		text.setAttribute("y", 1);
		text.setAttribute("height", 10);

		var flowIdText = document.createElementNS(svgns, 'text');
		var flowIdLabel = document.createTextNode(node.flowId);
		flowIdText.appendChild(flowIdLabel);
		flowIdText.setAttribute("y", 11);
		flowIdText.setAttribute("font-size", 8);
		
		var iconHeight = 20;
		var iconWidth = 21;
		var iconMargin = 4;
		var iconNode = document.createElementNS(svgns, 'image');
		iconNode.setAttribute("width", iconHeight);
		iconNode.setAttribute("height", iconWidth);
		iconNode.setAttribute("x", 0);
		iconNode.setAttribute("y", -10);
		iconNode.setAttributeNS('http://www.w3.org/1999/xlink', "xlink:href", contextURL + "/images/graph-icon.png");
		
		innerG.appendChild(rect);
		innerG.appendChild(text);
		innerG.appendChild(flowIdText);
		innerG.appendChild(iconNode);
		innerG.jobid = node.id;

		nodeG.appendChild(innerG);
		self.mainG.appendChild(nodeG);

		var horizontalMargin = 8;
		var verticalMargin = 2;
		
		// Need to get text width after attaching to SVG.
		var subLabelTextLength = flowIdText.getComputedTextLength();
		
		var computeTextLength = text.getComputedTextLength();
		var computeTextHeight = 22;
		
		var width = computeTextLength > subLabelTextLength ? computeTextLength : subLabelTextLength;
		width += iconWidth + iconMargin;
		var halfWidth = width/2;
		var halfHeight = height/2;
		
		// Margin for surrounding box.
		var boxWidth = width + horizontalMargin * 2;
		var boxHeight = height + verticalMargin * 2;
		
		node.width = boxWidth;
		node.height = boxHeight;
		node.centerX = boxWidth/2;
		node.centerY = boxHeight/2;
		
		var textXOffset = -halfWidth + iconWidth + iconMargin;
		iconNode.setAttribute("x", -halfWidth);
		text.setAttribute("x", textXOffset);
		flowIdText.setAttribute("x",textXOffset);
		rect.setAttribute("x", -node.centerX);
		rect.setAttribute("y", -node.centerY);
		rect.setAttribute("width", node.width);
		rect.setAttribute("height", node.height);
		
		nodeG.setAttribute("class", "node");
		nodeG.jobid=node.id;
	},
	drawBoxNode: function(self, node) {
		var svg = self.svgGraph;
		var svgns = self.svgns;

		var height = 18;
		var nodeG = document.createElementNS(svgns, "g");
		nodeG.setAttribute("class", "jobnode");
		nodeG.setAttribute("font-family", "helvetica");
		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
		this.gNodes[node.id] = nodeG;
		
		var innerG = document.createElementNS(svgns, "g");
		innerG.setAttribute("class", "nodebox");
		
		var rect = document.createElementNS(svgns, 'rect');
		rect.setAttribute("rx", 3);
		rect.setAttribute("ry", 5);
		rect.setAttribute("style", "width:inherit;stroke-opacity:1");
		//rect.setAttribute("class", "nodecontainer");
		
		var text = document.createElementNS(svgns, 'text');
		var textLabel = document.createTextNode(node.label);
		text.appendChild(textLabel);
		text.setAttribute("y", 6);
		text.setAttribute("height", 10); 

		//this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});

		innerG.appendChild(rect);
		innerG.appendChild(text);
		innerG.jobid = node.id;

		nodeG.appendChild(innerG);
		self.mainG.appendChild(nodeG);

		var horizontalMargin = 8;
		var verticalMargin = 2;
		
		// Need to get text width after attaching to SVG.
		var computeText = text.getComputedTextLength();
		var computeTextHeight = 22;
		var halfWidth = computeText/2;
		var halfHeight = height/2;
		
		// Margin for surrounding box.
		var boxWidth = computeText + horizontalMargin * 2;
		var boxHeight = height + verticalMargin * 2;
		
		node.width = boxWidth;
		node.height = boxHeight;
		node.centerX = boxWidth/2;
		node.centerY = boxHeight/2;
		
		text.setAttribute("x", -halfWidth);
		rect.setAttribute("x", -node.centerX);
		rect.setAttribute("y", -node.centerY);
		rect.setAttribute("width", node.width);
		rect.setAttribute("height", node.height);
		
		nodeG.setAttribute("class", "node");
		nodeG.jobid=node.id;
	},
	drawCircleNode: function(self, node, bounds) {
		var svg = self.svgGraph;
		var svgns = self.svgns;

		var xOffset = 10;
		var yOffset = 10;
		
		var nodeG = document.createElementNS(svgns, "g");
		nodeG.setAttribute("class", "jobnode");
		nodeG.setAttribute("font-family", "helvetica");
		nodeG.setAttribute("transform", "translate(" + node.x + "," + node.y + ")");
		this.gNodes[node.id] = nodeG;
		
		var innerG = document.createElementNS(svgns, "g");
		innerG.setAttribute("transform", "translate(-10,-10)");
		
		var circle = document.createElementNS(svgns, 'circle');
		circle.setAttribute("cy", 10);
		circle.setAttribute("cx", 10);
		circle.setAttribute("r", 12);
		circle.setAttribute("style", "width:inherit;stroke-opacity:1");
		//circle.setAttribute("class", "border");
		//circle.setAttribute("class", "nodecontainer");
		
		var text = document.createElementNS(svgns, 'text');
		var textLabel = document.createTextNode(node.label);
		text.appendChild(textLabel);
		text.setAttribute("x", 0);
		text.setAttribute("y", 0);
				
		this.addBounds(bounds, {minX:node.x - xOffset, minY: node.y - yOffset, maxX: node.x + xOffset, maxY: node.y + yOffset});
		
		var backRect = document.createElementNS(svgns, 'rect');
		backRect.setAttribute("x", 0);
		backRect.setAttribute("y", 2);
		backRect.setAttribute("class", "backboard");
		backRect.setAttribute("width", 10);
		backRect.setAttribute("height", 15);
		
		innerG.appendChild(circle);
		innerG.appendChild(backRect);
		innerG.appendChild(text);
		innerG.jobid = node.id;

		nodeG.appendChild(innerG);
		self.mainG.appendChild(nodeG);

		// Need to get text width after attaching to SVG.
		var computeText = text.getComputedTextLength();
		var halfWidth = computeText/2;
		text.setAttribute("x", -halfWidth + 10);
		backRect.setAttribute("x", -halfWidth);
		backRect.setAttribute("width", computeText + 20);

		nodeG.setAttribute("class", "node");
		nodeG.jobid=node.id;
	},
	addBounds: function(toBounds, addBounds) {
		toBounds.minX = toBounds.minX ? Math.min(toBounds.minX, addBounds.minX) : addBounds.minX;
		toBounds.minY = toBounds.minY ? Math.min(toBounds.minY, addBounds.minY) : addBounds.minY;
		toBounds.maxX = toBounds.maxX ? Math.max(toBounds.maxX, addBounds.maxX) : addBounds.maxX;
		toBounds.maxY = toBounds.maxY ? Math.max(toBounds.maxY, addBounds.maxY) : addBounds.maxY;
	},
	resetPanZoom : function(duration) {
		var bounds = this.graphBounds;
		var param = {x: bounds.minX, y: bounds.minY, width: (bounds.maxX - bounds.minX), height: (bounds.maxY - bounds.minY), duration: duration };

		this.panZoom(param);
	},
	centerNode: function(jobId) {
		var node = this.nodes[jobId];
		
		var offset = 150;
		var widthHeight = offset*2;
		var x = node.x - offset;
		var y = node.y - offset;
		
		this.panZoom({x: x, y: y, width: widthHeight, height: widthHeight});
	},
	panZoom: function(params) {
		params.maxScale = 2;
		$(this.svgGraph).svgNavigate("transformToBox", params);
	}
});