From 1c24f2b6f35da8c6a0562d682d709c396fc08e0a Mon Sep 17 00:00:00 2001
From: Nils Christian Ehmke <nie@informatik.uni-kiel.de>
Date: Thu, 3 Jan 2013 17:29:12 +0100
Subject: [PATCH] #621; Updated the FlowEditor; Resizing the layout units will
 resize the graph canvas as well

---
 .../beans/view/CurrentAnalysisEditorBean.java |  41 ++-
 .../src/main/webapp/js/flowEditor.js          | 309 ++++++++++++++----
 Kieker.WebGUI/src/main/webapp/js/jit.js       | 302 +++++++----------
 .../webapp/pages/AnalysisEditorPage.xhtml     |   4 +-
 .../main/webapp/pages/CockpitEditorPage.xhtml |   2 +-
 .../src/main/webapp/pages/CockpitPage.xhtml   |   2 +-
 .../main/webapp/pages/ControllerPage.xhtml    |   2 +-
 .../webapp/pages/ProjectOverviewPage.xhtml    |   2 +-
 .../main/webapp/templates/PagesTemplate.xhtml |   2 +-
 9 files changed, 400 insertions(+), 266 deletions(-)

diff --git a/Kieker.WebGUI/src/main/java/kieker/webgui/web/beans/view/CurrentAnalysisEditorBean.java b/Kieker.WebGUI/src/main/java/kieker/webgui/web/beans/view/CurrentAnalysisEditorBean.java
index af025164..35b266bb 100644
--- a/Kieker.WebGUI/src/main/java/kieker/webgui/web/beans/view/CurrentAnalysisEditorBean.java
+++ b/Kieker.WebGUI/src/main/java/kieker/webgui/web/beans/view/CurrentAnalysisEditorBean.java
@@ -78,6 +78,7 @@ public final class CurrentAnalysisEditorBean {
 
 	private ComponentListContainer availableComponents;
 	private MIAnalysisComponent selectedComponent;
+	private boolean unsavedModifications;
 	private MIProject project;
 	private String projectName;
 	private long timeStamp;
@@ -97,8 +98,8 @@ public final class CurrentAnalysisEditorBean {
 	 * Creates a new instance of this class. <b>Do not call this constructor manually. It will only be accessed by Spring.</b>
 	 */
 	public CurrentAnalysisEditorBean() {
-		this.availableComponents = new ComponentListContainer(Collections.<PluginContainer>emptyList(), Collections.<PluginContainer>emptyList(),
-				Collections.<RepositoryContainer>emptyList());
+		this.availableComponents = new ComponentListContainer(Collections.<PluginContainer> emptyList(), Collections.<PluginContainer> emptyList(),
+				Collections.<RepositoryContainer> emptyList());
 	}
 
 	/**
@@ -117,6 +118,8 @@ public final class CurrentAnalysisEditorBean {
 				this.initializeModelLibraries();
 				// Load the available readers, filters and repositories
 				this.reloadAvailableComponents();
+
+				this.unsavedModifications = false;
 			}
 		} catch (final ProjectLoadException ex) {
 			CurrentAnalysisEditorBean.LOG.error("An error occured while loading the project.", ex);
@@ -173,6 +176,8 @@ public final class CurrentAnalysisEditorBean {
 
 			// We have to reinitialize the tool palette completely! This is necessary as some of the already existing classes could need the newly loaded classes.
 			this.reloadAvailableComponents();
+
+			this.setModificationsFlag();
 		} catch (final IOException ex) {
 			CurrentAnalysisEditorBean.LOG.error("An error occured while uploading the library.", ex);
 			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgLibraryUploadingException());
@@ -200,6 +205,8 @@ public final class CurrentAnalysisEditorBean {
 				// We have to reinitialize the tool palette completely! This is necessary as some of the already existing classes could need the newly loaded
 				// classes.
 				this.reloadAvailableComponents();
+
+				this.setModificationsFlag();
 			}
 		} catch (final IOException ex) {
 			CurrentAnalysisEditorBean.LOG.error("An error occured while removing the library.", ex);
@@ -310,6 +317,8 @@ public final class CurrentAnalysisEditorBean {
 			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_INFO, this.globalPropertiesBean.getMsgProjectSaved());
 			// Update the time stamp!
 			this.resetTimeStamp();
+
+			this.clearModificationsFlag();
 		} catch (final IOException ex) {
 			CurrentAnalysisEditorBean.LOG.error("An error occured while saving the project.", ex);
 			GlobalPropertiesBean.showMessage(FacesMessage.SEVERITY_ERROR, this.globalPropertiesBean.getMsgProjectSavingException());
@@ -338,6 +347,8 @@ public final class CurrentAnalysisEditorBean {
 		this.project.getRepositories().add(repository);
 		this.currentAnalysisEditorGraphBean.addRepository(repository);
 		this.currentAnalysisEditorGraphBean.refreshGraph();
+
+		this.setModificationsFlag();
 	}
 
 	/**
@@ -358,6 +369,22 @@ public final class CurrentAnalysisEditorBean {
 			this.currentAnalysisEditorGraphBean.addFilter((MIFilter) plugin);
 		}
 		this.currentAnalysisEditorGraphBean.refreshGraph();
+
+		this.setModificationsFlag();
+	}
+
+	private synchronized void clearModificationsFlag() {
+		this.unsavedModifications = false;
+		RequestContext.getCurrentInstance().update("menuForm");
+	}
+
+	private synchronized void setModificationsFlag() {
+		this.unsavedModifications = true;
+		RequestContext.getCurrentInstance().update("menuForm");
+	}
+
+	public synchronized boolean isUnsavedModification() {
+		return this.unsavedModifications;
 	}
 
 	/**
@@ -563,6 +590,8 @@ public final class CurrentAnalysisEditorBean {
 		if (this.selectedComponent == node) {
 			this.selectedComponent = null; // NOPMD
 		}
+
+		this.setModificationsFlag();
 	}
 
 	/**
@@ -575,6 +604,8 @@ public final class CurrentAnalysisEditorBean {
 	 */
 	public synchronized void edgeCreated(final MIOutputPort sourcePort, final MIInputPort targetPort) {
 		sourcePort.getSubscribers().add(targetPort);
+
+		this.setModificationsFlag();
 	}
 
 	/**
@@ -587,6 +618,8 @@ public final class CurrentAnalysisEditorBean {
 	 */
 	public synchronized void edgeRemoved(final MIOutputPort sourcePort, final MIInputPort targetPort) {
 		sourcePort.getSubscribers().remove(targetPort);
+
+		this.setModificationsFlag();
 	}
 
 	/**
@@ -599,6 +632,8 @@ public final class CurrentAnalysisEditorBean {
 	 */
 	public synchronized void edgeCreated(final MIRepositoryConnector sourcePort, final MIRepository target) {
 		sourcePort.setRepository(target);
+
+		this.setModificationsFlag();
 	}
 
 	/**
@@ -611,6 +646,8 @@ public final class CurrentAnalysisEditorBean {
 	 */
 	public synchronized void edgeRemoved(final MIRepositoryConnector sourcePort, final MIRepository target) {
 		sourcePort.setRepository(null);
+
+		this.setModificationsFlag();
 	}
 
 }
diff --git a/Kieker.WebGUI/src/main/webapp/js/flowEditor.js b/Kieker.WebGUI/src/main/webapp/js/flowEditor.js
index 1bfdf40e..5d5515f8 100644
--- a/Kieker.WebGUI/src/main/webapp/js/flowEditor.js
+++ b/Kieker.WebGUI/src/main/webapp/js/flowEditor.js
@@ -49,7 +49,6 @@ var Log = {
 
 		
 function GraphFlow(){
-	
 	/////////////////////////////////////////////
 	//				VARIABLES				   //
 	/////////////////////////////////////////////
@@ -140,16 +139,87 @@ function GraphFlow(){
 		This information is needed to correctly draw the Grid and may come in handy in
 		future functions.
 		*/
-	var navi = { 'centerX' : 0,
+	var navi = { // screen position
+				 'centerX' : 0,
 				 'centerY' : 0,
+				 // these determine when the screen should move with a dragged node
+				 'borderLeft' : 0,
+				 'borderRight' : 0,
+				 'borderTop' : 0,
+				 'borderBottom' : 0,
+				 // memorize where we touch a node upon dragging
+				 'nodeOffsetX' : 0,
+				 'nodeOffsetY' : 0,
+				 // how far a node will be dragged
 				 'dragX' : null,
 				 'dragY' : null,
+				 // dragged nodefamily and ports
 				 'draggedNodes' : null};
 	
 	/////////////////////////////////////////////
 	//				FUNCTIONS				   //
 	/////////////////////////////////////////////
 
+	/**
+	 * Zooms in or out, depending on the factor
+	 * 
+	 * @param factor -  the graph zooms out if it is smaller than 1
+	 */
+	this.zoom = function(factor){
+		
+		// check argument
+		if(!validArg("zoom()", factor, "number")){
+			return;
+		}else if(factor < 0){
+			callListener("onError", ["Error in function zoom(): Argument factor must be a positive number!"]);
+		}
+		
+		// zoom
+		var canvas = fd.canvas;
+		canvas.scale(factor, factor);
+		
+		// show text only if it is big enough
+		canvas.showLabels = (canvas.scaleOffsetX > canvas.labelThreshold);
+	}
+	
+	/**
+		Checks if the GraphFlow container has changed in size and
+		updates the canvas area accordingly.
+	*/
+	this.updateCanvasSize = function(){
+		var container = document.getElementById(visContainer);
+		var widthOld = grid.data.$width,
+			heightOld = grid.data.$height,
+			widthNew = container.clientWidth,
+			heightNew = container.clientHeight;
+			
+		// check if the dimensions changed
+		if( widthOld != widthNew || heightOld != heightNew){
+		
+			// calculate translation parameters
+			var scale = fd.canvas.scaleOffsetX,
+				transX = fd.canvas.translateOffsetX + (widthOld - widthNew)/2,
+				transY = fd.canvas.translateOffsetY + (heightOld - heightNew)/2;
+			
+			// resize canvas, scale and translate content to maintain its position
+			fd.canvas.resize(container.clientWidth, container.clientHeight, true);
+			fd.canvas.scale(scale, scale);
+			fd.canvas.translate(transX / scale, transY / scale);
+			
+			// update grid dimensions
+			grid.data.$width = container.clientWidth;
+			grid.data.$height = container.clientHeight;
+			
+			// apply changes to the grid if visible
+			if(grid.visible){
+				var domGrid = fd.graph.getNode(grid.id);
+				domGrid.data.$width = widthNew;
+				domGrid.data.$height = heightNew;
+			}
+		}
+		fd.plot();
+	}
+	
 	/**
 		Concatenates nodefamily 'id's and their x- and y-position
 		to a string, separating each element with a space.
@@ -199,17 +269,17 @@ function GraphFlow(){
 					if(!value){
 						value = "Lucida Console";
 					}
-					value = 0.83 * node.data.$dim +"px " + value;
+					value = "px " + value;
 				}
 				// apply new value to data
 				node.data[field] = value;
-				domNode.data[field] = value;
+				if(!noPlot){ domNode.data[field] = value;}
 			}
 			
 			// make changes to name or id
 			else{
 				node[field] = value;
-				domNode[field] = value;
+				if(!noPlot){ domNode[field] = value;}
 			}
 		}
 		
@@ -575,12 +645,11 @@ function GraphFlow(){
 			grid.data.$color = newColor;
 			
 			// apply changes immediately
-			if(grid.visible){
+			if(!noPlot && grid.visible){
 				var domGrid = fd.graph.getNode(grid.id);
 				domGrid.data.$color = newColor;
 				
-				// apply visual changes
-				if(!noPlot){ fd.plot();}
+				fd.plot();
 			}
 		}
 	}
@@ -595,11 +664,11 @@ function GraphFlow(){
 			grid.data.$dim = newSize;
 			
 			// apply changes immediately
-			if(grid.visible){
+			if(!noPlot &&grid.visible){
 				var domGrid = fd.graph.getNode(grid.id);
 				domGrid.data.$dim = newSize;
-				// apply visual changes
-				if(!noPlot){ fd.plot();}
+				
+				fd.plot();
 			}
 		}
 	}
@@ -621,14 +690,16 @@ function GraphFlow(){
 		grid.visible = visibility;
 		
 		// apply changes immediately
-		if(visibility){
-			fd.graph.addNode(grid);
-		}
-		else{
-			fd.graph.removeNode(grid.id);
+		if(!noPlot){
+			if(visibility){
+				fd.graph.addNode(grid);
+			}
+			else{
+				fd.graph.removeNode(grid.id);
+			}
+			// apply visual changes
+			fd.plot();
 		}
-		// apply visual changes
-		if(!noPlot){ fd.plot();}
 	}
 	
 	/**
@@ -685,19 +756,26 @@ function GraphFlow(){
 	}
 	
 	// scaling values
-	var oldScale = fd.canvas.scaleOffsetX,
+	var canvas = fd.canvas,
+		oldScale = canvas.scaleOffsetX,
 		scaleX = grid.data.$width / (rightMost - leftMost + 4*vertexLabelSize),
-		scaleY = grid.data.$height / (bottomMost - topMost +4*vertexLabelSize)
+		scaleY = grid.data.$height / (bottomMost - topMost + 4*vertexLabelSize)
 		scale = Math.min(scaleX, scaleY) / oldScale;
 		
 	// translation values
 	var midX = (leftMost + rightMost) / 2,
 		midY = (topMost + bottomMost) / 2 -vertexLabelSize;
+		
 	
 	// translate to origin, scale, and translate to the center of the graph
-	fd.canvas.translate( -fd.canvas.translateOffsetX/oldScale, -fd.canvas.translateOffsetY/oldScale);
-	fd.canvas.scale(scale, scale);
-	fd.canvas.translate( -midX, -midY);
+	canvas.translate( -canvas.translateOffsetX/oldScale, -canvas.translateOffsetY/oldScale);
+	canvas.scale(scale, scale);
+	canvas.translate( -midX, -midY);
+	
+	// show text only if it is big enough
+	canvas.showLabels = (canvas.scaleOffsetX > canvas.labelThreshold);
+	
+	fd.plot();
 	}
 	
 	
@@ -724,6 +802,7 @@ function GraphFlow(){
 			var icon;
 			icon = new Image();
 			icon.src = path;
+			icon.onload = function(){};
 			
 			// this may happen on IE, when linking to images via web
 			if(!icon.complete){
@@ -1162,7 +1241,7 @@ function GraphFlow(){
 			"$nodeType": nodeType,
 			"$xPos": xPosition,
 			"$yPos": yPosition,
-			"$font": 0.83 * size +"px "+font,
+			"$font": "px "+font,
 			"$tooltip": nodeFamily.tooltip
 		  }, 
 		  "id": nodeFamily.id, 
@@ -1252,7 +1331,8 @@ function GraphFlow(){
 							"$yPos": y,
 							"$color": nodeStrokeColor[loopType],
 							"$fillColor": nodeColor,
-							"$tooltip": port.tooltip}, 
+							"$tooltip": port.tooltip,
+							"$symbol": port.symbol}, 
 						  "id": nodeFamily.id+"."+port.id, 
 						  "name": port.name};
 						  
@@ -1476,6 +1556,7 @@ function GraphFlow(){
 			nodes = navi.draggedNodes,
 			pos = node.pos.getc(true);
 		
+		
 		// set pos of family box
 		var data= json[familyIndex].data;
 		data.$xPos = pos.x;
@@ -1641,10 +1722,13 @@ function GraphFlow(){
 		adja.push(edge);
 		
 		// apply changes immediately
-		var domSource = fd.graph.getNode(sourceID),
+		
+		if(!noPlot){ 
+			var domSource = fd.graph.getNode(sourceID),
 			domTarget = fd.graph.getNode(targetID);
-		fd.graph.addAdjacence(domSource, domTarget, edge.data);
-		if(!noPlot){ fd.plot();}
+			fd.graph.addAdjacence(domSource, domTarget, edge.data);
+			fd.plot();
+		}
 		
 		return true;
 	}
@@ -1673,7 +1757,9 @@ function GraphFlow(){
 				adja[a].data.$bendPoints = bendPoints;
 				
 				// update displayed graph (no need for refresh then)
-				fd.graph.getAdjacence(sourceID, targetID).data.$bendPoints = bendPoints;
+				if(!noPlot){
+					fd.graph.getAdjacence(sourceID, targetID).data.$bendPoints = bendPoints;
+				}
 				break;
 			}
 		}
@@ -1756,8 +1842,11 @@ function GraphFlow(){
 	}
 	
 	// apply changes immediately
-	fd.graph.removeAdjacence(sourceID, targetID);
-	if(!noPlot){ fd.plot();}
+	
+	if(!noPlot){ 
+		fd.graph.removeAdjacence(sourceID, targetID);
+		fd.plot();
+	}
   }
   
   /**
@@ -1805,6 +1894,10 @@ function GraphFlow(){
 				else{
 					hover.setData('color', nodeStrokeColor[type], 'end');
 				}
+				// decrease size of ports and cross button
+				if(type != 'nodeFamily'){
+					hover.setData('dim', vertexLabelSize*2, 'end');
+				}
 				
 			}
 			hover = null;
@@ -1817,6 +1910,11 @@ function GraphFlow(){
 				node.setData('lineWidth', 2, 'end');
 			}else{
 				node.setData('color', nodeColorFocus, 'end');
+				
+				// increase size of ports and cross button
+				if(node.data.$type != 'nodeFamily'){
+					node.setData('dim', vertexLabelSize*3, 'end');
+				}
 			}
 			hover = node;
 		}else{
@@ -1830,7 +1928,7 @@ function GraphFlow(){
 		// of the edge animation will not be called
 		fd.animate({  
 				modes: ['edge-property:lineWidth:color',
-						'node-property:color'],
+						'node-property:color:dim'],
 				transition: trans,  
 				duration: dur
 		}); 
@@ -1909,22 +2007,28 @@ function GraphFlow(){
 	what happens on various mouse events is determined.
 	*/
   this.initGraph = function(){
-	json = [ null, null, null, null, null, null, null, null, null, null, 
-			{
-			  "adjacencies": [], 
-			  "data": {
-				"$dim": 0,
-				"$type": "none",
-				"&xPos": 0,
-				"&typeSelected" : null,
-				"&yPos": 0
-			  }, 
-			  "id": "#DUMMY_MOUSE_NODE", 
-			  "name": "",
-			  "alpha": 0
-		}];
+  
+	if(!json){
+		json = [ null, null, null, null, null, null, null, null, null, null, 
+				{
+				  "adjacencies": [], 
+				  "data": {
+					"$dim": 0,
+					"$type": "none",
+					"&xPos": 0,
+					"&typeSelected" : null,
+					"&yPos": 0
+				  }, 
+				  "id": "#DUMMY_MOUSE_NODE", 
+				  "name": "",
+				  "alpha": 0
+			}];
+	}
 	  
 	  fd = new $jit.FlowGraph({
+		width : grid.data.$width,
+		height : grid.data.$height,
+	  
 		//id of the visualization container
 		injectInto: visContainer,
 		//Enable zooming and panning
@@ -2063,29 +2167,44 @@ function GraphFlow(){
 		  
 		  /**
 		   *	EVENT:	OnDragStart
+		   *	Actually this Event fires whenever the mouse button is down
 		   */
 		  onDragStart: function(node, eventInfo, e) {
 		  var mousePos = eventInfo.getPos();
 		  
+		  // dragging the canvas
 			if(node == false || node.id == mouseNode.id ||
 			   node.data.$type != "nodeFamily"){
-			    navi.dragX = e.pageX;
-				navi.dragY = e.pageY;
+			    navi.dragX = e.layerX//e.pageX;
+				navi.dragY = e.layerX//e.pageY;
 				navi.isDragging = true;
 				return;
 			}
 			
-			// memorize where we drag the node instead on centering
-			// it to the mouse pointer
-			var	nodePos = node.pos.getc(true);
+			//dragging a node
+			if(!selectedNode){
+				// memorize where we drag the node instead on centering
+				// it to the mouse pointer
+				var	nodePos = node.pos.getc(true);
 				
-			navi.dragX = nodePos.x - mousePos.x;
-			navi.dragY = nodePos.y - mousePos.y;
-			
-			// we memorize all subnodes of the dragged nodefamily, to speed up
-			// dragging
-			prepareNodeMove(node);
-			
+				// memorize the node position before node movement
+				navi.dragX = nodePos.x;
+				navi.dragY = nodePos.y;
+				// the offset between the center of the node and the actual position
+				// where we grabbed the node
+				navi.nodeOffsetX = nodePos.x - mousePos.x;
+				navi.nodeOffsetY = nodePos.y - mousePos.y;
+				
+				// prepare borders. These determine when to move the canvas with the node
+				navi.borderLeft = (node.data.$width / 2 - navi.nodeOffsetX) * fd.canvas.scaleOffsetX;
+				navi.borderTop = (node.data.$height / 2 - navi.nodeOffsetY) * fd.canvas.scaleOffsetX;
+				navi.borderRight = grid.data.$width - (node.data.$width / 2 + navi.nodeOffsetX) * fd.canvas.scaleOffsetX;
+				navi.borderBottom = grid.data.$height - (node.data.$height / 2 + navi.nodeOffsetY) * fd.canvas.scaleOffsetX;
+				
+				// we memorize all subnodes of the dragged nodefamily, to speed up
+				// dragging
+				prepareNodeMove(node);
+			}
 			callListener("onDragStart", [node, eventInfo, e]);
 		  },
 		  
@@ -2097,9 +2216,30 @@ function GraphFlow(){
 				node.id == mouseNode.id || node.data.$type != "nodeFamily"){
 					return;
 			}
-			var pos = eventInfo.getPos();
-			moveNode(pos.x+navi.dragX,
-					 pos.y+navi.dragY);
+			
+			// move node
+			var pos = eventInfo.getPos(),
+				x = pos.x + navi.nodeOffsetX,
+				y = pos.y + navi.nodeOffsetY;
+			moveNode(x, y);
+			
+			// move screen if near edge
+			var screenX = e.layerX,
+				screenY = e.layerY;
+				
+			if(screenX < navi.borderLeft && x < navi.dragX){
+				fd.canvas.translate(20,0);
+			}
+			else if(screenX > navi.borderRight && x > navi.dragX){
+				fd.canvas.translate(-20,0);
+			}
+			
+			if(screenY < navi.borderTop && y < navi.dragY){
+				fd.canvas.translate(0,20);
+			}
+			else if(screenY > navi.borderBottom && y > navi.dragY){
+				fd.canvas.translate(0,-20);
+			}
 			callListener("onDragMove", [node, eventInfo, e]);
 			fd.plot();
 			
@@ -2112,6 +2252,7 @@ function GraphFlow(){
 			if(node.data.$type == "nodeFamily"){
 				saveNodeMove();
 			}
+			
 			fd.plot();
 			callListener("onDragEnd", [node, eventInfo, e]);
 		  },
@@ -2129,11 +2270,31 @@ function GraphFlow(){
 		   */
 		  onClick: function(node, eventInfo, e){
 		  
-		  if(node == false){
-			var deltaX = e.pageX - navi.dragX,
-				deltaY = e.pageY - navi.dragY;
+		  // finished dragging the screen
+		  if(!node){
+			var deltaX = e.layerX - navi.dragX,
+				deltaY = e.layerY - navi.dragY;
 			navi.centerX -= deltaX;
 			navi.centerY -= deltaY;
+			
+			/*
+			// deblur edges by moving the canvas to a half pixel
+			var scale = fd.canvas.scaleOffsetX;
+				halfX = fd.canvas.translateOffsetX, 
+				halfY = fd.canvas.translateOffsetY;
+			if(halfX < 0){
+				halfX = -(halfX - Math.ceil(halfX) + 0.0)/scale;
+			} else{
+				halfX = -(halfX - Math.floor(halfX) + 0.0)/scale;
+			}
+			if(halfY < 0){
+				halfY = -(halfY - Math.ceil(halfY) + 0.0)/scale;
+			} else{
+				halfY = -(halfY - Math.floor(halfY) + 0.0)/scale;
+			}
+			fd.canvas.translate(halfX, halfY);
+			*/
+			
 			//fd.canvas.translate(deltaX, deltaY);
 		  }
 		  else if(mouseOverNode && readOnly.deleteCreate){
@@ -2156,10 +2317,10 @@ function GraphFlow(){
 						
 						// add edge to mouseNode
 						if(selectedNode.from == "inputPort"){
-							addEdge(mouseNode.id, node.id, null, false, true);
+							addEdge(mouseNode.id, node.id);
 						}
 						else{
-							addEdge(node.id, mouseNode.id, null, false, true);
+							addEdge(node.id, mouseNode.id);
 						}
 						var pos = node.pos.getc(true);
 						
@@ -2191,10 +2352,10 @@ function GraphFlow(){
 					
 					// remove selectedEdge
 					var label = node.data.$label;
-					removeEdge(selectedNode.id, node.data.$direction[1],false, true);
+					removeEdge(selectedNode.id, node.data.$direction[1]);
 					
 					// add edge to mouseNode
-					addEdge(selectedNode.id, mouseNode.id, label, false, true);
+					addEdge(selectedNode.id, mouseNode.id, label);
 					var pos = eventInfo.getPos();
 					
 					// set mouse position
@@ -2217,6 +2378,14 @@ function GraphFlow(){
 		  }
 		  callListener("onClick", [node, eventInfo, e]);
 		  navi.isDragging = false;
+		  },
+		  
+		  /**
+		   * 	EVENT: OnMouseWheel
+		   */
+		  onMouseWheel: function(e, delta) {
+			  // update whether labels should be displayed
+			  fd.canvas.showLabels = (fd.canvas.scaleOffsetX > fd.canvas.labelThreshold);
 		  }
 		},
 		
@@ -2229,6 +2398,10 @@ function GraphFlow(){
 	  
 	  // load JSON data.
 	  refresh();
+	  
+	  // set zoom threshold for displaying text
+	  fd.canvas.labelThreshold = 1/vertexLabelSize;
+	  fd.canvas.showLabels = true;
 	}
 	
 	// initialize graph
diff --git a/Kieker.WebGUI/src/main/webapp/js/jit.js b/Kieker.WebGUI/src/main/webapp/js/jit.js
index 6029803a..98284eb0 100644
--- a/Kieker.WebGUI/src/main/webapp/js/jit.js
+++ b/Kieker.WebGUI/src/main/webapp/js/jit.js
@@ -7254,7 +7254,7 @@ Graph.Plot = {
       min = Math.min,
       opt = opt || this.viz.controller;
       //opt.clearCanvas && canvas.clear();
-        
+      
       var root = aGraph.getNode(id);
       if(!root) return;
       
@@ -17209,107 +17209,6 @@ Layouts.FlowGraph = new Class({
       nodef: function(x) { return k2 / (x || 1); },
       edgef: function(x) { return /* x * x / k; */ k * (x - l); }
     };
-  },
-  
-  compute: function(property, incremental) {
-    var prop = $.splat(property || ['current', 'start', 'end']);
-    var opt = this.getOptions();
-    NodeDim.compute(this.graph, prop, this.config);
-    this.graph.computeLevels(this.root, 0, "ignore");
-    this.graph.eachNode(function(n) {
-      $.each(prop, function(p) {
-        var pos = n.getPos(p);
-        if(pos.equals(Complex.KER)) {
-          pos.x = opt.width/5 * (Math.random() - 0.5);
-          pos.y = opt.height/5 * (Math.random() - 0.5);
-        }
-        //initialize disp vector
-        n.disp = {};
-        $.each(prop, function(p) {
-          n.disp[p] = $C(0, 0);
-        });
-      });
-    });
-    this.computePositions(prop, opt, incremental);
-  },
-  
-  computePositions: function(property, opt, incremental) {
-    var times = this.config.iterations, i = 0, that = this;
-    if(incremental) {
-      (function iter() {
-        for(var total=incremental.iter, j=0; j<total; j++) {
-          opt.t = opt.tstart * (1 - i++/(times -1));
-          that.computePositionStep(property, opt);
-          if(i >= times) {
-            incremental.onComplete();
-            return;
-          }
-        }
-        incremental.onStep(Math.round(i / (times -1) * 100));
-        setTimeout(iter, 1);
-      })();
-    } else {
-      for(; i < times; i++) {
-        opt.t = opt.tstart * (1 - i/(times -1));
-        this.computePositionStep(property, opt);
-      }
-    }
-  },
-  
-  computePositionStep: function(property, opt) {
-    var graph = this.graph;
-    var min = Math.min, max = Math.max;
-    var dpos = $C(0, 0);
-    //calculate repulsive forces
-    graph.eachNode(function(v) {
-      //initialize disp
-      $.each(property, function(p) {
-        v.disp[p].x = 0; v.disp[p].y = 0;
-      });
-      graph.eachNode(function(u) {
-        if(u.id != v.id) {
-          $.each(property, function(p) {
-            var vp = v.getPos(p), up = u.getPos(p);
-            dpos.x = vp.x - up.x;
-            dpos.y = vp.y - up.y;
-            var norm = dpos.norm() || 1;
-            v.disp[p].$add(dpos
-                .$scale(opt.nodef(norm) / norm));
-          });
-        }
-      });
-    });
-    //calculate attractive forces
-    var T = !!graph.getNode(this.root).visited;
-    graph.eachNode(function(node) {
-      node.eachAdjacency(function(adj) {
-        var nodeTo = adj.nodeTo;
-        if(!!nodeTo.visited === T) {
-          $.each(property, function(p) {
-            var vp = node.getPos(p), up = nodeTo.getPos(p);
-            dpos.x = vp.x - up.x;
-            dpos.y = vp.y - up.y;
-            var norm = dpos.norm() || 1;
-            node.disp[p].$add(dpos.$scale(-opt.edgef(norm) / norm));
-            nodeTo.disp[p].$add(dpos.$scale(-1));
-          });
-        }
-      });
-      node.visited = !T;
-    });
-    //arrange positions to fit the canvas
-    var t = opt.t, w2 = opt.width / 2, h2 = opt.height / 2;
-    graph.eachNode(function(u) {
-      $.each(property, function(p) {
-        var disp = u.disp[p];
-        var norm = disp.norm() || 1;
-        var p = u.getPos(p);
-        p.$add($C(disp.x * min(Math.abs(disp.x), t) / norm, 
-            disp.y * min(Math.abs(disp.y), t) / norm));
-        p.x = min(w2, max(-w2, p.x));
-        p.y = min(h2, max(-h2, p.y));
-      });
-    });
   }
 });
 
@@ -17367,8 +17266,8 @@ $jit.FlowGraph = new Class( {
 	
     var $FlowGraph = $jit.FlowGraph;
     var config = {
-      iterations: 50,
-      levelDistance: 50
+      iterations: 1,
+      levelDistance: 0
     };
 
     this.controller = this.config = $.merge(Options("Canvas", "Node", "Edge",
@@ -17414,12 +17313,12 @@ $jit.FlowGraph = new Class( {
     Computes positions and plots the tree.
   */
   refresh: function() {
-    this.compute();
+    //this.compute();
     this.plot();
   },
 
   reposition: function() {
-    this.compute('end');
+    //this.compute('end');
   },
 
 /*
@@ -17488,7 +17387,7 @@ $jit.FlowGraph = new Class( {
   */
   computeIncremental: function(opt) {
     opt = $.merge( {
-      iter: 20,
+      iter: 1, // 20
       property: 'end',
       onStep: $.empty,
       onComplete: $.empty
@@ -17883,33 +17782,35 @@ $jit.FlowGraph.$extend = true;
 					
 					var name = node.name;
 					var midX, midY, textWidth;
+					var textSize = 0.83 * dim;
 					
-					if(name != undefined){
-						ctx.fillStyle = node.getData('color');
-						ctx.font = node.data.$font;
-						
-						textWidth = ctx.measureText(name).width/2;
-						midX = tX - textWidth;
-						midY = posY;
-						
-						ctx.fillText(name, midX, midY);
-					}
-					
-					// paint class
-					
-					var nodeClass =  node.data.$nodeClass;
-					if(nodeClass != undefined){
-						nodeClass = "<"+nodeClass+">";
-						ctx.font = 0.83 * (dim-4) + "px Lucida Console";
-						
-						var classWidth = ctx.measureText(nodeClass).width/2;
-						midX = tX - classWidth;
-						midY += dim * 0.83;
+					if(canvas.showLabels){
+						if(name != undefined){
+							ctx.fillStyle = node.getData('color');
+							ctx.font = textSize + node.data.$font;
+							textWidth = ctx.measureText(name).width/2;
+							midX = tX - textWidth;
+							midY = posY;
+							
+							ctx.fillText(name, midX, midY);
+						}
 						
-						ctx.fillText(nodeClass, midX, midY);
+						// paint class
 						
-						if(textWidth < classWidth){
-							textWidth = classWidth;
+						var nodeClass =  node.data.$nodeClass;
+						if(nodeClass != undefined){
+							nodeClass = "<"+nodeClass+">";
+							ctx.font = (textSize - 3.22) + node.data.$font;
+							
+							var classWidth = ctx.measureText(nodeClass).width/2;
+							midX = tX - classWidth;
+							midY += textSize;
+							
+							ctx.fillText(nodeClass, midX, midY);
+							
+							if(textWidth < classWidth){
+								textWidth = classWidth;
+							}
 						}
 					}
 					
@@ -17945,15 +17846,18 @@ $jit.FlowGraph.$extend = true;
 						return;
 					}
 					
-	    	        var pos = node.pos.getc(true), 
-	    	            size = node.getData('dim'),
-						hSize = size/2,
-						bx = pos.x - hSize,
-						by = pos.y - hSize/2,
-	    	            ctx = canvas.getCtx();
-					
-						ctx.font = 0.83 * size +"px Verdana";
-						ctx.fillText("x", bx, by); 
+	    	        var size = node.getData('dim');
+	    	        
+	    	        if(canvas.showLabels){
+						var	pos = node.pos.getc(true),
+							hSize = size/2,
+							bx = pos.x - hSize,
+							by = pos.y - hSize/2,
+		    	            ctx = canvas.getCtx();
+						
+							ctx.font = 0.83 * size +"px Verdana";
+							ctx.fillText("x", bx, by); 
+	    	        }
 					
 	    	      },
 	    	      'contains': function(node, pos){
@@ -17976,7 +17880,7 @@ $jit.FlowGraph.$extend = true;
 	    	  'render': function(node, canvas){
 					
 	    	        var pos = node.pos.getc(true), 
-	    	            size = node.getData('dim'),
+	    	            size = node.data.$dim,
 						bx = pos.x,
 						by = pos.y,
 	    	            ctx = canvas.getCtx();
@@ -17984,19 +17888,24 @@ $jit.FlowGraph.$extend = true;
 					// draw close-button area
 					this.nodeHelper.rectangle.render('fill', {x: bx, y: by+size/4}, size, size/2, canvas);
 					
-					var bSize = size * 0.4;
-					bx -= size/6;
-					by += bSize;
-					
-					ctx.fillStyle = node.getData('fillColor');
-					ctx.font = bSize +"px Arial Black";
-					ctx.fillText("R", bx, by);
+					if(canvas.showLabels){
+						var bSize = size * 0.4;
+						bx -= size/6;
+						by += bSize;
+						
+						var symbol = node.data.$symbol;
+						if(!symbol){ symbol = "R";}
+						
+						ctx.fillStyle = node.data.$fillColor;
+						ctx.font = bSize +"px Arial Black";
+						ctx.fillText(symbol, bx, by);
+	    	  		}
 					
 	    	      },
 	    	      'contains': function(node, pos){
 				  
 	    	        var npos = node.pos.getc(true), 
-	    	            size = node.getData('dim')/2;
+	    	            size = node.data.$dim / 2;
 	    	        return Math.abs(pos.x - npos.x) <= size && Math.abs(pos.y - npos.y-size) <= size;
 	        }
 	},
@@ -18006,10 +17915,9 @@ $jit.FlowGraph.$extend = true;
 	*/
 	'inputPort': {
 	    	  'render': function(node, canvas){
-					//var vertexFillColor 	= '#F62929';
 					
 	    	        var pos = node.pos.getc(true), 
-	    	            size = node.getData('dim'),
+	    	            size = node.data.$dim,
 						bx = pos.x,
 						by = pos.y,
 	    	            ctx = canvas.getCtx();
@@ -18018,30 +17926,37 @@ $jit.FlowGraph.$extend = true;
 					this.nodeHelper.rectangle.render('fill', {x: bx, y: by+size/4}, size, size/2, canvas);
 					
 					
-					// draw arrow
-					/*var bSize = size * 0.4;
-					bx -= size/2;
-					by += bSize;
-					ctx.fillStyle = node.getData('fillColor');
-					ctx.font = bSize +"px Arial Black";
-					ctx.fillText(">", bx, by);*/
-					by += size/4;
-					var	octo = size/8,
-						xp = bx - size/2 + octo;
-					ctx.fillStyle = node.getData('fillColor');
-					ctx.beginPath();
-					ctx.moveTo(xp, by - octo);
-					ctx.lineTo(bx , by);
-					ctx.lineTo(xp, by + octo);
-					ctx.closePath();
-					ctx.fill();
+					ctx.fillStyle = node.data.$fillColor;
+					var symbol = node.data.$symbol;
 					
+					if(symbol && canvas.showLabels){
+						
+						// draw symbol
+						var bSize = size * 0.4;
+						bx -= size/6;
+						by += bSize;
+						ctx.font = bSize +"px Arial Black";
+						ctx.fillText(symbol, bx, by);
+					}
+					else{
+						
+						// draw arrow
+						by += size/4;
+						var	octo = size/8,
+							xp = bx - size/2 + octo;
+						ctx.beginPath();
+						ctx.moveTo(xp, by - octo);
+						ctx.lineTo(bx , by);
+						ctx.lineTo(xp, by + octo);
+						ctx.closePath();
+						ctx.fill();
+					}
 					
 	    	      },
 	    	      'contains': function(node, pos){
 				  
 	    	        var npos = node.pos.getc(true), 
-	    	            size = node.getData('dim')/2;
+	    	            size = node.data.$dim / 2;
 	    	        return Math.abs(pos.x - npos.x) <= size && Math.abs(pos.y - npos.y-size) <= size;
 	        }
 	},
@@ -18051,7 +17966,7 @@ $jit.FlowGraph.$extend = true;
 	'outputPort': {
 	    	  'render': function(node, canvas){
 	    	        var pos = node.pos.getc(true), 
-	    	            size = node.getData('dim'),
+	    	            size = node.data.$dim,
 						bx = pos.x,
 						by = pos.y,
 	    	            ctx = canvas.getCtx();
@@ -18059,29 +17974,37 @@ $jit.FlowGraph.$extend = true;
 					// draw rectangle
 					this.nodeHelper.rectangle.render('fill', {x: bx, y: by+size/4}, size, size/2, canvas);
 					
+					ctx.fillStyle = node.data.$fillColor;
+					var symbol = node.data.$symbol;
 					
-					// draw arrow
-					/*var bSize = size * 0.4;
-					bx += size/4;
-					by += bSize;
-					ctx.fillStyle = node.getData('fillColor');
-					ctx.font = bSize +"px Arial Black";
-					ctx.fillText(">", bx, by);*/
-					by += size/4;
-					var	octo = size/8;
-					ctx.fillStyle = node.getData('fillColor');
-					ctx.beginPath();
-					ctx.moveTo(bx, by - octo);
-					ctx.lineTo(bx + size/2 - octo , by);
-					ctx.lineTo(bx, by + octo);
-					ctx.closePath();
-					ctx.fill();
+					if(symbol && canvas.showLabels){
+						// draw symbol
+						
+						var bSize = size * 0.4;
+						bx -= size/6;
+						by += bSize;
+						
+						
+						ctx.font = bSize +"px Arial Black";
+						ctx.fillText(symbol, bx, by);
+					}
+					else{
+						// draw arrow
+						by += size/4;
+						var	octo = size/8;
+						ctx.beginPath();
+						ctx.moveTo(bx, by - octo);
+						ctx.lineTo(bx + size/2 - octo , by);
+						ctx.lineTo(bx, by + octo);
+						ctx.closePath();
+						ctx.fill();
+					}
 										
 	    	      },
 	    	      'contains': function(node, pos){
 				  
 	    	        var npos = node.pos.getc(true), 
-	    	            size = node.getData('dim')/2;
+	    	            size = node.data.$dim / 2;
 	    	        return Math.abs(pos.x - npos.x) <= size && Math.abs(pos.y - npos.y-size) <= size;
 	        }
 	},
@@ -18163,7 +18086,6 @@ $jit.FlowGraph.$extend = true;
 		
 		var from = {"x": posFrom.x, "y" : posFrom.y+dim/2};
 		var to = {"x": posTo.x, "y" : posTo.y+dim/2};
-		
 		// swap points if the edge direction is "wrong"
 		if(inverse){
 			var temp = to;
@@ -18189,7 +18111,7 @@ $jit.FlowGraph.$extend = true;
 		
 		// add the label
 		var label = adj.data.$label;
-		if(label != undefined){
+		if(label != undefined && canvas.showLabels){
 			var ctx = canvas.getCtx();
 			ctx.font = (1.23 * dim)+"px Arial";
 				
@@ -18256,7 +18178,7 @@ $jit.FlowGraph.$extend = true;
 		
 		// add the label
 		var label = adj.data.$label;
-		if(label != undefined){
+		if(label != undefined && canvas.showLabels){
 			var ctx = canvas.getCtx();
 			ctx.font = (1.23 * dim)+"px Arial";
 				
diff --git a/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml
index 38800bc6..b16d5161 100644
--- a/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/pages/AnalysisEditorPage.xhtml
@@ -12,7 +12,8 @@
     <h:body>
 
         <ui:composition template="/templates/PagesTemplate.xhtml">
-
+            
+            <ui:param name="unsavedModifications" value="#{currentAnalysisEditorBean.unsavedModification}"/>
             <ui:param name="projectName" value="#{currentAnalysisEditorBean.projectName}"/>
             <ui:param name="pagename" value="analysisEditor"/>
             <ui:param name="showProjectName" value="true"/>
@@ -127,6 +128,7 @@
             </ui:define>
 
             <ui:define name="furtherLayoutUnits">
+                <p:ajax event="resize" onstart="graph.updateCanvasSize();"/>
                 <!-- This is the component presenting the available properties. -->
                 <p:layoutUnit style="font-size: 12px" position="south" size="150" header="#{localizedMessages.properties}" resizable="true" collapsible="true">
                     <h:form id="propertiesForm" >
diff --git a/Kieker.WebGUI/src/main/webapp/pages/CockpitEditorPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/CockpitEditorPage.xhtml
index 2e5a6b1f..72dfeee7 100644
--- a/Kieker.WebGUI/src/main/webapp/pages/CockpitEditorPage.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/pages/CockpitEditorPage.xhtml
@@ -11,7 +11,7 @@
     <h:body>
 
         <ui:composition template="/templates/PagesTemplate.xhtml">
-
+            <ui:param name="unsavedModifications" value="false"/>
             <ui:param name="projectName" value="#{currentCockpitEditorBean.projectName}"/>
             <ui:param name="pagename" value="cockpitEditor"/>
             <ui:param name="showProjectName" value="true"/>
diff --git a/Kieker.WebGUI/src/main/webapp/pages/CockpitPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/CockpitPage.xhtml
index 95c89407..5fa439fc 100644
--- a/Kieker.WebGUI/src/main/webapp/pages/CockpitPage.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/pages/CockpitPage.xhtml
@@ -11,7 +11,7 @@
     <h:body>
 
         <ui:composition template="#{root}/templates/PagesTemplate.xhtml">
-
+            <ui:param name="unsavedModifications" value="false"/>
             <ui:param name="projectName" value="#{currentCockpitBean.projectName}"/>
             <ui:param name="pagename" value="cockpit"/>
             <ui:param name="showProjectName" value="true"/>
diff --git a/Kieker.WebGUI/src/main/webapp/pages/ControllerPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/ControllerPage.xhtml
index 89d6bede..b4adb2c5 100644
--- a/Kieker.WebGUI/src/main/webapp/pages/ControllerPage.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/pages/ControllerPage.xhtml
@@ -11,7 +11,7 @@
     <h:body>
 
         <ui:composition template="/templates/PagesTemplate.xhtml">
-
+            <ui:param name="unsavedModifications" value="false"/>
             <ui:param name="projectName" value="#{currentControllerBean.projectName}"/>
             <ui:param name="pagename" value="controller"/>
             <ui:param name="showProjectName" value="true"/>
diff --git a/Kieker.WebGUI/src/main/webapp/pages/ProjectOverviewPage.xhtml b/Kieker.WebGUI/src/main/webapp/pages/ProjectOverviewPage.xhtml
index 95d28cff..cf321bc2 100644
--- a/Kieker.WebGUI/src/main/webapp/pages/ProjectOverviewPage.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/pages/ProjectOverviewPage.xhtml
@@ -12,7 +12,7 @@
     <h:body>
 
         <ui:composition template="/templates/PagesTemplate.xhtml">
-
+            <ui:param name="unsavedModifications" value="false"/>
             <ui:param name="projectName" value="#{currentProjectOverviewBean.projectName}"/>
             <ui:param name="pagename" value="projectOverview"/>
             <ui:param name="showProjectName" value="false"/>
diff --git a/Kieker.WebGUI/src/main/webapp/templates/PagesTemplate.xhtml b/Kieker.WebGUI/src/main/webapp/templates/PagesTemplate.xhtml
index da013907..70d76aff 100644
--- a/Kieker.WebGUI/src/main/webapp/templates/PagesTemplate.xhtml
+++ b/Kieker.WebGUI/src/main/webapp/templates/PagesTemplate.xhtml
@@ -22,7 +22,7 @@
                         <h:form id="menuForm">
                             <p:toolbar>
                                 <p:toolbarGroup align="left">
-                                    <h:outputText styleClass="kieker-title" value="Kieker #{showProjectName ? '&raquo;' : ''} #{showProjectName ? stringBean.shortenLongName(projectName, 30) : ''}"/>
+                                    <h:outputText styleClass="kieker-title" value="Kieker #{showProjectName ? '&raquo;' : ''} #{showProjectName ? stringBean.shortenLongName(projectName, 30) : ''}#{unsavedModifications ? '*' : ''}"/>
                                 </p:toolbarGroup>
                                 <p:toolbarGroup align="right">
                                     <p:button styleClass="perspective-button" icon="ui-icon-home" outcome="projectOverview" disabled="#{pagename == 'projectOverview'}" />
-- 
GitLab