commit bda189f985009c3187ba09b1b081a3d325d85b1a
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Mon Oct 22 22:52:50 2018 +0200

    organized JS files into directories

diff --git a/sources/src/main/webapp/js/coordinates.js b/sources/src/main/webapp/js/coordinates.js
deleted file mode 100644
index 4583692..0000000
--- a/sources/src/main/webapp/js/coordinates.js
+++ /dev/null
@@ -1,12 +0,0 @@
-/**
- * Class representing a coordinates pair.
- * @constructor
- * @param {float} x X coordinate.
- * @param {float} y Y coordinate.
- */
-function Coordinates(x, y) {
-	/** @prop {float} x X coordinate. */
-	this.x = x;
-	/** @prop {float} y Y coordinate. */
-	this.y = y;
-}
diff --git a/sources/src/main/webapp/js/diagram.js b/sources/src/main/webapp/js/diagram.js
deleted file mode 100644
index bcd70d9..0000000
--- a/sources/src/main/webapp/js/diagram.js
+++ /dev/null
@@ -1,13 +0,0 @@
-/**
- * Class representing a saved diagram.
- * @constructor
- * @param {object} props Properties of the diagram.
- */
-function Diagram(props) {
-	/** @prop {int} id Identifier of the diagram. */
-	this.id = parseInt(props.id);
-	/** @prop {string} name Name of the diagram. */
-	this.name = props.name;
-	/** @prop {bool} public True if the diagram is public, otherwise false. */
-	this.public = props.public === '1';
-}
diff --git a/sources/src/main/webapp/js/forceDirected.js b/sources/src/main/webapp/js/forceDirected.js
deleted file mode 100644
index 0d2499e..0000000
--- a/sources/src/main/webapp/js/forceDirected.js
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * @constructor
- */
-function ForceDirected() {
-
-	var forceField = {};
-	var canvas = 0;
-
-	// parametry layoutu
-	var inumber = 30; // pocet iteraci; default 300
-	var inumberClick = 20; // pocet iteraci u tlacitka
-	var repulsiveStrength = 400; // odpudiva sila (prima umera); default 450
-	var attractiveStrength = 510; // pritazliva sila (neprima umera, nesmi byt 0); default 110
-	var dampeningEffect = 1000; // tlumeni sily (nesmi byt 0); default 200
-	var borderRatio = 1; // hranice layoutu (cislo kterym se deli velikost canvasu)
-	// tahle funkce se mi nelibi, je treba to vyresit jinak, nici to layout (nechat na 1)
-
-	/**
-	 * Force directed layout for visible components.
-	 */
-	this.run = function() {
-		var border = canvas / borderRatio;
-
-		var visibleNodes = app.viewportComponent.getNodeList(),
-			otherVisibleNodes = [];
-
-		var i = 0,
-			j = 0;
-
-		for (i = 0; i < visibleNodes.length; i++) {
-			forceField[Utils.getUniqueId(visibleNodes[i])] = [0, 0];
-		}
-
-		// calculate repulsive force
-		for (i = 0; i < visibleNodes.length; i++) {
-			var currNode = visibleNodes[i];
-
-			// other nodes
-			for (var j = 0; j < visibleNodes.length; j++){
-				otherVisibleNodes[j] = visibleNodes[j];
-			}
-			otherVisibleNodes.splice(i, 1);
-
-			// iterate over other nodes
-			for (j = 0; j < otherVisibleNodes.length; j++) {
-				var otherNode = otherVisibleNodes[j];
-
-				var currPosition = currNode.getPosition();
-				var otherPosition = otherNode.getPosition();
-
-				// calculate force
-				var x = currPosition.x - otherPosition.x;
-				var y = currPosition.y - otherPosition.y;
-				
-				var sum = Math.pow(x, 2) + Math.pow(y, 2);
-				var distance = Math.sqrt(sum);
-
-				if (distance !== 0) {
-					forceField[Utils.getUniqueId(currNode)][0] += Math.floor(x * (repulsiveStrength / distance));
-					forceField[Utils.getUniqueId(currNode)][1] += Math.floor(y * (repulsiveStrength / distance));
-				}
-			}
-		}
-
-		// calculate attractive forces
-		for (i = 0; i < visibleNodes.length; i++){
-			var currNode = visibleNodes[i];
-
-			for (j = 0; j < currNode.getInEdgeList().length; j++) {
-				var otherNode = currNode.getInEdgeList()[j].getFrom();
-
-				var currPosition = currNode.getPosition();
-				var otherPosition = otherNode.getPosition();
-
-				// calculate force
-				var x = currPosition.x - otherPosition.x;
-				var y = currPosition.y - otherPosition.y;
-
-				var sum = Math.pow(x, 2) + Math.pow(y, 2);
-				var distance = Math.sqrt(sum);
-
-				forceField[Utils.getUniqueId(currNode)][0] += Math.round(-1 * x * (distance / attractiveStrength));
-				forceField[Utils.getUniqueId(currNode)][1] += Math.round(-1 * y * (distance / attractiveStrength));
-			}
-
-			for (j = 0; j < currNode.getOutEdgeList().length; j++) {
-				var otherNode = currNode.getOutEdgeList()[j].getTo();
-
-				var currPosition = currNode.getPosition();
-				var otherPosition = otherNode.getPosition();
-
-				// calculate force
-				var x = currPosition.x - otherPosition.x;
-				var y = currPosition.y - otherPosition.y;
-
-				var sum = Math.pow(x, 2) + Math.pow(y, 2);
-				var distance = Math.sqrt(sum);
-
-				forceField[Utils.getUniqueId(currNode)][0] += Math.round(-1 * x * (distance / attractiveStrength));
-				forceField[Utils.getUniqueId(currNode)][1] += Math.round(-1 * y * (distance / attractiveStrength));
-			}
-		}
-
-		// applying the force
-		for (i = 0; i < visibleNodes.length; i++){
-			var currNode = visibleNodes[i],
-
-				halfCan = canvas / 2,
-
-				deltaX = currNode.getPosition().x - halfCan,
-				deltaY = currNode.getPosition().y - halfCan;
-
-			// tohle drzi layout uprostred, chtelo by to vymyslet nejak lip, docela ho to kurvi
-			/*
-			 if (deltaX > 0) {
-			 currNode.x = Math.min(currNode.getPosition().x, (canvas/2)+border);
-			 } else {
-			 currNode.x = Math.max(currNode.getPosition().x, (canvas/2)-border);
-			 }
-			 if (deltaY > 0) {
-			 currNode.y = Math.min(currNode.getPosition().y, (canvas/2)+border);
-			 } else {
-			 currNode.y = Math.max(currNode.getPosition().y, (canvas/2)-border);
-			 }
-			 */
-
-			// kolecko
-			var dist = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, +2)),
-				maxDist = Math.sqrt(Math.pow(border, 2) + Math.pow(border, +2));
-
-			if (dist > maxDist){
-				var ratio = maxDist / dist,
-
-					newX = deltaX * ratio,
-					newY = deltaY * ratio;
-
-				currNode.x += newX - deltaX;
-				currNode.y += newY - deltaY;
-			}
-
-			// force dampening
-			var forceX = Math.floor(forceField[Utils.getUniqueId(currNode)][0] / dampeningEffect),
-				forceY = Math.floor(forceField[Utils.getUniqueId(currNode)][1] / dampeningEffect);
-
-			// adding a random effect
-			/*
-			 forceX += -3+Math.floor((Math.random()*6)+1);
-			 forceY += -3+Math.floor((Math.random()*6)+1);
-			 */
-
-
-			// moving a component
-			if (Math.abs(forceX) > 1 || Math.abs(forceY) > 1) {
-				var coords = new Coordinates(
-					currNode.getPosition().x + forceX,
-					currNode.getPosition().y + forceY,
-				);
-
-				currNode.setPosition(coords);
-				currNode.move(coords);
-			}
-		}
-	};
-}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/graphExporter.js b/sources/src/main/webapp/js/graphExporter.js
deleted file mode 100644
index bb0a3ae..0000000
--- a/sources/src/main/webapp/js/graphExporter.js
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * @constructor
- */
-function GraphExporter() {
-
-	/**
-	 * Exports graph to JSON.
-	 */
-	this.run = function() {
-		// vertices
-		var vertices = app.vertexList.map(function(vertex) {
-			return vertex.export();
-		});
-
-		// edges
-		var edges = app.edgeList.map(function(edge) {
-			return edge.export();
-		});
-
-		// groups
-		var groups = app.groupList.map(function(group) {
-			return group.export();
-		});
-
-		// nodes excluded to the sidebar
-		var excludedNodeList = app.sidebarComponent.excludedNodeListComponent.getNodeList();
-		var sideBar = excludedNodeList.map(function(node) {
-			return {
-				id: Utils.getUniqueId(node),
-                isIconsDisplayed: node.isIconsDisplayed(),
-			};
-		});
-
-        var highlightedEdge = app.edgeList.find(function (edge) {
-            return edge.isHighlighted()
-        });
-
-        // when edge is highlighted another vertex can not be highlighted
-        if (Utils.isUndefined(highlightedEdge)) {
-            var highlightedVertex = app.nodeList.find(function (vertex) {
-                return vertex.isHighlighted()
-            });
-        }
-
-		return {
-			attributeTypes: app.attributeTypeList,
-			edgeArchetypes: app.archetype.edge,
-			vertexArchetypes: app.archetype.vertex,
-			vertices: vertices,
-			edges: edges,
-			possibleEnumValues: app.possibleEnumValues,
-			groups: groups,
-			sideBar: sideBar,
-            highlightedVertex: Utils.getUniqueId(highlightedVertex),
-			highlightedEdge: Utils.isUndefined(highlightedEdge) ? '' : highlightedEdge.id,
-		};
-	}
-
-}
diff --git a/sources/src/main/webapp/js/graphLoader.js b/sources/src/main/webapp/js/graphLoader.js
deleted file mode 100644
index f843f47..0000000
--- a/sources/src/main/webapp/js/graphLoader.js
+++ /dev/null
@@ -1,196 +0,0 @@
-/**
- * @constructor
- */
-function GraphLoader() {
-
-	/**
-	 * Loads a new graph using graph data passed as parameters.
-	 * @param {object} data Data of the graph.
-	 * @throws {InvalidArgumentError} Thrown when either graph data are incomplete.
-	 */
-	this.run  = function(data) {
-		if (Utils.isUndefined(data.vertices) || Utils.isUndefined(data.edges)) {
-			throw new InvalidArgumentError('Invalid data.');
-		}
-
-		var canvasSize = ((data.vertices.length * 75) / Math.round(Math.sqrt(data.vertices.length))) + 1000;
-
-		// store archetypes
-		app.archetype.vertex = data.vertexArchetypes;
-		app.archetype.edge = data.edgeArchetypes;
-
-		app.attributeTypeList = data.attributeTypes;
-		app.possibleEnumValues = data.possibleEnumValues;
-
-		app.archetype.vertex.filter(function(vertexArchetype) {
-			return Utils.isDefined(vertexArchetype.icon);
-		}).forEach(function(vertexArchetype) {
-			app.viewportComponent.addSvgDefinition('vertexArchetypeIcon-' + vertexArchetype.name, vertexArchetype.icon);
-		});
-
-        var highlightedNodeId;
-        var highlightedNodeType;
-        if (Utils.isDefined(data.highlightedVertex) && data.highlightedVertex.length > 0) {
-            var highlightedNodeAttr = data.highlightedVertex.split("-");
-            if (highlightedNodeAttr.length === 2) {
-                highlightedNodeType = highlightedNodeAttr[0];
-                highlightedNodeId = parseInt(highlightedNodeAttr[1], 10);
-            }
-        }
-        if (Utils.isDefined(data.highlightedEdge) && data.highlightedEdge.length > 0) {
-            var highlightedEdgeId = parseInt(data.highlightedEdge, 10);
-        }
-
-        var highlightedNode = undefined;
-        var highlightedEdge = undefined;
-
-        // construct vertices
-		var vertexMap = {};
-		data.vertices.forEach(function(component) {
-			var vertex = new Vertex(component);
-
-			if (highlightedNodeType === 'vertex' && highlightedNodeId === vertex.id ){
-                highlightedNode = vertex;
-			}
-
-			var position = component.position;
-
-			if (position === null || Utils.isUndefined(position)) {
-                // set random
-                vertex.setPosition(new Coordinates(
-                    Math.floor(Math.random() * canvasSize),
-                    Math.floor(Math.random() * canvasSize),
-                ));
-
-            } else {
-                vertex.setPosition(new Coordinates(position.x, position.y));
-			}
-
-			app.nodeList.push(vertex);
-			app.vertexList.push(vertex);
-
-			vertexMap[component.id] = vertex;
-		});
-
-		// construct edges
-		data.edges.forEach(function(component) {
-			var edge = new Edge(component);
-
-            if (highlightedEdgeId === edge.id ){
-                highlightedEdge = edge;
-            }
-
-			var fromNode = vertexMap[component.from];
-			if (fromNode) {
-				fromNode.addOutEdge(edge);
-			}
-
-			var toNode = vertexMap[component.to];
-			if (toNode) {
-				toNode.addInEdge(edge);
-			}
-
-			if (fromNode && toNode) {
-				fromNode.incrementRelatedArchetype(toNode.archetype);
-				toNode.incrementRelatedArchetype(fromNode.archetype);
-			}
-
-			app.edgeList.push(edge);
-		});
-
-		delete vertexMap;
-
-		// render components
-		app.vertexList.forEach(function(vertex) {
-			app.viewportComponent.addVertex(vertex);
-		});
-
-		app.edgeList.forEach(function(edge) {
-			app.viewportComponent.addEdge(edge);
-		});
-
-		// find unconnected vertices
-		app.vertexList.filter(function(vertex) {
-			return vertex.isUnconnected();
-		}).forEach(function(vertex) {
-			vertex.exclude();
-			app.sidebarComponent.unconnectedNodeListComponent.add(vertex);
-		});
-
-		// construct groups
-		data.groups.forEach(function(component) {
-			var group = new Group(component);
-
-			if (highlightedNodeType === 'group' && highlightedNodeId === group.id ){
-                highlightedNode = group;
-            }
-
-			// position
-			var position = component.position;
-			if (position === null || Utils.isUndefined(position)) {
-                // set random
-                group.setPosition(new Coordinates(
-                    Math.floor(Math.random() * canvasSize),
-                    Math.floor(Math.random() * canvasSize),
-                ));
-
-            } else {
-                group.setPosition(new Coordinates(position.x, position.y));
-            }
-
-			// vertices
-			app.vertexList.filter(function(vertex) {
-				return component.verticesId.indexOf(vertex.id) > -1;
-			}).forEach(function(vertex) {
-				group.addVertex(vertex);
-			});
-
-			app.nodeList.push(group);
-			app.groupList.push(group);
-
-			app.viewportComponent.addGroup(group);
-		});
-
-		// exclude nodes
-		data.sideBar.forEach(function(excludedNode) {
-            if(typeof excludedNode.id !== 'string' && !(excludedNode.id instanceof String)) {
-            	return;
-            }
-            var idArr = excludedNode.id.split("-");
-            if(idArr.length !== 2){
-				return;
-            }
-            idArr[1] = parseInt(idArr[1], 10);
-
-			var node = app.nodeList.find(function(node) {
-				var prefix = '';
-				if (node instanceof Vertex) {
-					prefix = 'vertex';
-				} else if (node instanceof Group) {
-					prefix = 'group';
-				}
-				return idArr[0] === prefix && node.id === idArr[1];
-			});
-
-			if (Utils.isDefined(node)) {
-				node.exclude(excludedNode.isIconsDisplayed);
-
-				app.sidebarComponent.excludedNodeListComponent.add(node);
-			}
-		});
-
-		// center viewport
-		app.viewportComponent.center();
-
-		// update status bar
-		app.sidebarComponent.statusBarComponent.setComponentCount(data.vertices.length);
-
-		if (Utils.isDefined(highlightedEdge)) {
-            highlightedEdge.setHighlighted(true);
-            highlightedEdge.getFrom().setHighlighted(true);
-            highlightedEdge.getTo().setHighlighted(true);
-        }
-		if (Utils.isDefined(highlightedNode)) highlightedNode.setHighlightedWithNeighbours(true);
-	};
-
-}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/loader.js b/sources/src/main/webapp/js/loader.js
deleted file mode 100644
index 01b9f8d..0000000
--- a/sources/src/main/webapp/js/loader.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Loader animation displayed when running some expensive operation.
- * 
- * @constructor
- * @param {object} options 
- */
-function Loader(options) {
-	var defaultOptions = {
-		lines: 16, // The number of lines to draw
-		length: 21, // The length of each line
-		width: 8, // The line thickness
-		radius: 40, // The radius of the inner circle
-		rotate: 0, // The rotation offset
-		color: '#000', // #rgb or #rrggbb
-		speed: 1.2, // Rounds per second
-		trail: 58, // Afterglow percentage
-		shadow: true, // Whether to render a shadow
-		hwaccel: true, // Whether to use hardware acceleration
-		className: 'loader-spinner', // The CSS class to assign to the spinner
-		zIndex: 2, // The z-index (defaults to 2000000000)
-	};
-
-	var opts = $.extend(defaultOptions, options);
-
-	/**
-	 * Spin jQuery plugin.
-	 */
-	$.fn.spin = function(opts) {
-		this.each(function() {
-			var $this = $(this),
-			data = $this.data();
-			
-			if (data.spinner) {
-				data.spinner.stop();
-				delete data.spinner;
-			}
-
-			if (opts !== false) {
-				data.spinner = new Spinner($.extend({
-					color: $this.css('color')
-				}, opts)).spin(this);
-			}
-		});
-		return this;
-	};
-
-	/**
-	* Enables loader.
-	*/
-	this.enable = function() {
-		$('#loader').show();
-		$('#spinLoader').spin(opts);
-	};
-
-	/**
-	* Disables loader.
-	*/
-	this.disable = function() {
-		$('#loader').hide();
-		$('#spinLoader').spin(false);
-	};
-}
diff --git a/sources/src/main/webapp/js/services/forceDirected.js b/sources/src/main/webapp/js/services/forceDirected.js
new file mode 100644
index 0000000..0d2499e
--- /dev/null
+++ b/sources/src/main/webapp/js/services/forceDirected.js
@@ -0,0 +1,164 @@
+/**
+ * @constructor
+ */
+function ForceDirected() {
+
+	var forceField = {};
+	var canvas = 0;
+
+	// parametry layoutu
+	var inumber = 30; // pocet iteraci; default 300
+	var inumberClick = 20; // pocet iteraci u tlacitka
+	var repulsiveStrength = 400; // odpudiva sila (prima umera); default 450
+	var attractiveStrength = 510; // pritazliva sila (neprima umera, nesmi byt 0); default 110
+	var dampeningEffect = 1000; // tlumeni sily (nesmi byt 0); default 200
+	var borderRatio = 1; // hranice layoutu (cislo kterym se deli velikost canvasu)
+	// tahle funkce se mi nelibi, je treba to vyresit jinak, nici to layout (nechat na 1)
+
+	/**
+	 * Force directed layout for visible components.
+	 */
+	this.run = function() {
+		var border = canvas / borderRatio;
+
+		var visibleNodes = app.viewportComponent.getNodeList(),
+			otherVisibleNodes = [];
+
+		var i = 0,
+			j = 0;
+
+		for (i = 0; i < visibleNodes.length; i++) {
+			forceField[Utils.getUniqueId(visibleNodes[i])] = [0, 0];
+		}
+
+		// calculate repulsive force
+		for (i = 0; i < visibleNodes.length; i++) {
+			var currNode = visibleNodes[i];
+
+			// other nodes
+			for (var j = 0; j < visibleNodes.length; j++){
+				otherVisibleNodes[j] = visibleNodes[j];
+			}
+			otherVisibleNodes.splice(i, 1);
+
+			// iterate over other nodes
+			for (j = 0; j < otherVisibleNodes.length; j++) {
+				var otherNode = otherVisibleNodes[j];
+
+				var currPosition = currNode.getPosition();
+				var otherPosition = otherNode.getPosition();
+
+				// calculate force
+				var x = currPosition.x - otherPosition.x;
+				var y = currPosition.y - otherPosition.y;
+				
+				var sum = Math.pow(x, 2) + Math.pow(y, 2);
+				var distance = Math.sqrt(sum);
+
+				if (distance !== 0) {
+					forceField[Utils.getUniqueId(currNode)][0] += Math.floor(x * (repulsiveStrength / distance));
+					forceField[Utils.getUniqueId(currNode)][1] += Math.floor(y * (repulsiveStrength / distance));
+				}
+			}
+		}
+
+		// calculate attractive forces
+		for (i = 0; i < visibleNodes.length; i++){
+			var currNode = visibleNodes[i];
+
+			for (j = 0; j < currNode.getInEdgeList().length; j++) {
+				var otherNode = currNode.getInEdgeList()[j].getFrom();
+
+				var currPosition = currNode.getPosition();
+				var otherPosition = otherNode.getPosition();
+
+				// calculate force
+				var x = currPosition.x - otherPosition.x;
+				var y = currPosition.y - otherPosition.y;
+
+				var sum = Math.pow(x, 2) + Math.pow(y, 2);
+				var distance = Math.sqrt(sum);
+
+				forceField[Utils.getUniqueId(currNode)][0] += Math.round(-1 * x * (distance / attractiveStrength));
+				forceField[Utils.getUniqueId(currNode)][1] += Math.round(-1 * y * (distance / attractiveStrength));
+			}
+
+			for (j = 0; j < currNode.getOutEdgeList().length; j++) {
+				var otherNode = currNode.getOutEdgeList()[j].getTo();
+
+				var currPosition = currNode.getPosition();
+				var otherPosition = otherNode.getPosition();
+
+				// calculate force
+				var x = currPosition.x - otherPosition.x;
+				var y = currPosition.y - otherPosition.y;
+
+				var sum = Math.pow(x, 2) + Math.pow(y, 2);
+				var distance = Math.sqrt(sum);
+
+				forceField[Utils.getUniqueId(currNode)][0] += Math.round(-1 * x * (distance / attractiveStrength));
+				forceField[Utils.getUniqueId(currNode)][1] += Math.round(-1 * y * (distance / attractiveStrength));
+			}
+		}
+
+		// applying the force
+		for (i = 0; i < visibleNodes.length; i++){
+			var currNode = visibleNodes[i],
+
+				halfCan = canvas / 2,
+
+				deltaX = currNode.getPosition().x - halfCan,
+				deltaY = currNode.getPosition().y - halfCan;
+
+			// tohle drzi layout uprostred, chtelo by to vymyslet nejak lip, docela ho to kurvi
+			/*
+			 if (deltaX > 0) {
+			 currNode.x = Math.min(currNode.getPosition().x, (canvas/2)+border);
+			 } else {
+			 currNode.x = Math.max(currNode.getPosition().x, (canvas/2)-border);
+			 }
+			 if (deltaY > 0) {
+			 currNode.y = Math.min(currNode.getPosition().y, (canvas/2)+border);
+			 } else {
+			 currNode.y = Math.max(currNode.getPosition().y, (canvas/2)-border);
+			 }
+			 */
+
+			// kolecko
+			var dist = Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, +2)),
+				maxDist = Math.sqrt(Math.pow(border, 2) + Math.pow(border, +2));
+
+			if (dist > maxDist){
+				var ratio = maxDist / dist,
+
+					newX = deltaX * ratio,
+					newY = deltaY * ratio;
+
+				currNode.x += newX - deltaX;
+				currNode.y += newY - deltaY;
+			}
+
+			// force dampening
+			var forceX = Math.floor(forceField[Utils.getUniqueId(currNode)][0] / dampeningEffect),
+				forceY = Math.floor(forceField[Utils.getUniqueId(currNode)][1] / dampeningEffect);
+
+			// adding a random effect
+			/*
+			 forceX += -3+Math.floor((Math.random()*6)+1);
+			 forceY += -3+Math.floor((Math.random()*6)+1);
+			 */
+
+
+			// moving a component
+			if (Math.abs(forceX) > 1 || Math.abs(forceY) > 1) {
+				var coords = new Coordinates(
+					currNode.getPosition().x + forceX,
+					currNode.getPosition().y + forceY,
+				);
+
+				currNode.setPosition(coords);
+				currNode.move(coords);
+			}
+		}
+	};
+}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/services/graphExporter.js b/sources/src/main/webapp/js/services/graphExporter.js
new file mode 100644
index 0000000..bb0a3ae
--- /dev/null
+++ b/sources/src/main/webapp/js/services/graphExporter.js
@@ -0,0 +1,59 @@
+/**
+ * @constructor
+ */
+function GraphExporter() {
+
+	/**
+	 * Exports graph to JSON.
+	 */
+	this.run = function() {
+		// vertices
+		var vertices = app.vertexList.map(function(vertex) {
+			return vertex.export();
+		});
+
+		// edges
+		var edges = app.edgeList.map(function(edge) {
+			return edge.export();
+		});
+
+		// groups
+		var groups = app.groupList.map(function(group) {
+			return group.export();
+		});
+
+		// nodes excluded to the sidebar
+		var excludedNodeList = app.sidebarComponent.excludedNodeListComponent.getNodeList();
+		var sideBar = excludedNodeList.map(function(node) {
+			return {
+				id: Utils.getUniqueId(node),
+                isIconsDisplayed: node.isIconsDisplayed(),
+			};
+		});
+
+        var highlightedEdge = app.edgeList.find(function (edge) {
+            return edge.isHighlighted()
+        });
+
+        // when edge is highlighted another vertex can not be highlighted
+        if (Utils.isUndefined(highlightedEdge)) {
+            var highlightedVertex = app.nodeList.find(function (vertex) {
+                return vertex.isHighlighted()
+            });
+        }
+
+		return {
+			attributeTypes: app.attributeTypeList,
+			edgeArchetypes: app.archetype.edge,
+			vertexArchetypes: app.archetype.vertex,
+			vertices: vertices,
+			edges: edges,
+			possibleEnumValues: app.possibleEnumValues,
+			groups: groups,
+			sideBar: sideBar,
+            highlightedVertex: Utils.getUniqueId(highlightedVertex),
+			highlightedEdge: Utils.isUndefined(highlightedEdge) ? '' : highlightedEdge.id,
+		};
+	}
+
+}
diff --git a/sources/src/main/webapp/js/services/graphLoader.js b/sources/src/main/webapp/js/services/graphLoader.js
new file mode 100644
index 0000000..f843f47
--- /dev/null
+++ b/sources/src/main/webapp/js/services/graphLoader.js
@@ -0,0 +1,196 @@
+/**
+ * @constructor
+ */
+function GraphLoader() {
+
+	/**
+	 * Loads a new graph using graph data passed as parameters.
+	 * @param {object} data Data of the graph.
+	 * @throws {InvalidArgumentError} Thrown when either graph data are incomplete.
+	 */
+	this.run  = function(data) {
+		if (Utils.isUndefined(data.vertices) || Utils.isUndefined(data.edges)) {
+			throw new InvalidArgumentError('Invalid data.');
+		}
+
+		var canvasSize = ((data.vertices.length * 75) / Math.round(Math.sqrt(data.vertices.length))) + 1000;
+
+		// store archetypes
+		app.archetype.vertex = data.vertexArchetypes;
+		app.archetype.edge = data.edgeArchetypes;
+
+		app.attributeTypeList = data.attributeTypes;
+		app.possibleEnumValues = data.possibleEnumValues;
+
+		app.archetype.vertex.filter(function(vertexArchetype) {
+			return Utils.isDefined(vertexArchetype.icon);
+		}).forEach(function(vertexArchetype) {
+			app.viewportComponent.addSvgDefinition('vertexArchetypeIcon-' + vertexArchetype.name, vertexArchetype.icon);
+		});
+
+        var highlightedNodeId;
+        var highlightedNodeType;
+        if (Utils.isDefined(data.highlightedVertex) && data.highlightedVertex.length > 0) {
+            var highlightedNodeAttr = data.highlightedVertex.split("-");
+            if (highlightedNodeAttr.length === 2) {
+                highlightedNodeType = highlightedNodeAttr[0];
+                highlightedNodeId = parseInt(highlightedNodeAttr[1], 10);
+            }
+        }
+        if (Utils.isDefined(data.highlightedEdge) && data.highlightedEdge.length > 0) {
+            var highlightedEdgeId = parseInt(data.highlightedEdge, 10);
+        }
+
+        var highlightedNode = undefined;
+        var highlightedEdge = undefined;
+
+        // construct vertices
+		var vertexMap = {};
+		data.vertices.forEach(function(component) {
+			var vertex = new Vertex(component);
+
+			if (highlightedNodeType === 'vertex' && highlightedNodeId === vertex.id ){
+                highlightedNode = vertex;
+			}
+
+			var position = component.position;
+
+			if (position === null || Utils.isUndefined(position)) {
+                // set random
+                vertex.setPosition(new Coordinates(
+                    Math.floor(Math.random() * canvasSize),
+                    Math.floor(Math.random() * canvasSize),
+                ));
+
+            } else {
+                vertex.setPosition(new Coordinates(position.x, position.y));
+			}
+
+			app.nodeList.push(vertex);
+			app.vertexList.push(vertex);
+
+			vertexMap[component.id] = vertex;
+		});
+
+		// construct edges
+		data.edges.forEach(function(component) {
+			var edge = new Edge(component);
+
+            if (highlightedEdgeId === edge.id ){
+                highlightedEdge = edge;
+            }
+
+			var fromNode = vertexMap[component.from];
+			if (fromNode) {
+				fromNode.addOutEdge(edge);
+			}
+
+			var toNode = vertexMap[component.to];
+			if (toNode) {
+				toNode.addInEdge(edge);
+			}
+
+			if (fromNode && toNode) {
+				fromNode.incrementRelatedArchetype(toNode.archetype);
+				toNode.incrementRelatedArchetype(fromNode.archetype);
+			}
+
+			app.edgeList.push(edge);
+		});
+
+		delete vertexMap;
+
+		// render components
+		app.vertexList.forEach(function(vertex) {
+			app.viewportComponent.addVertex(vertex);
+		});
+
+		app.edgeList.forEach(function(edge) {
+			app.viewportComponent.addEdge(edge);
+		});
+
+		// find unconnected vertices
+		app.vertexList.filter(function(vertex) {
+			return vertex.isUnconnected();
+		}).forEach(function(vertex) {
+			vertex.exclude();
+			app.sidebarComponent.unconnectedNodeListComponent.add(vertex);
+		});
+
+		// construct groups
+		data.groups.forEach(function(component) {
+			var group = new Group(component);
+
+			if (highlightedNodeType === 'group' && highlightedNodeId === group.id ){
+                highlightedNode = group;
+            }
+
+			// position
+			var position = component.position;
+			if (position === null || Utils.isUndefined(position)) {
+                // set random
+                group.setPosition(new Coordinates(
+                    Math.floor(Math.random() * canvasSize),
+                    Math.floor(Math.random() * canvasSize),
+                ));
+
+            } else {
+                group.setPosition(new Coordinates(position.x, position.y));
+            }
+
+			// vertices
+			app.vertexList.filter(function(vertex) {
+				return component.verticesId.indexOf(vertex.id) > -1;
+			}).forEach(function(vertex) {
+				group.addVertex(vertex);
+			});
+
+			app.nodeList.push(group);
+			app.groupList.push(group);
+
+			app.viewportComponent.addGroup(group);
+		});
+
+		// exclude nodes
+		data.sideBar.forEach(function(excludedNode) {
+            if(typeof excludedNode.id !== 'string' && !(excludedNode.id instanceof String)) {
+            	return;
+            }
+            var idArr = excludedNode.id.split("-");
+            if(idArr.length !== 2){
+				return;
+            }
+            idArr[1] = parseInt(idArr[1], 10);
+
+			var node = app.nodeList.find(function(node) {
+				var prefix = '';
+				if (node instanceof Vertex) {
+					prefix = 'vertex';
+				} else if (node instanceof Group) {
+					prefix = 'group';
+				}
+				return idArr[0] === prefix && node.id === idArr[1];
+			});
+
+			if (Utils.isDefined(node)) {
+				node.exclude(excludedNode.isIconsDisplayed);
+
+				app.sidebarComponent.excludedNodeListComponent.add(node);
+			}
+		});
+
+		// center viewport
+		app.viewportComponent.center();
+
+		// update status bar
+		app.sidebarComponent.statusBarComponent.setComponentCount(data.vertices.length);
+
+		if (Utils.isDefined(highlightedEdge)) {
+            highlightedEdge.setHighlighted(true);
+            highlightedEdge.getFrom().setHighlighted(true);
+            highlightedEdge.getTo().setHighlighted(true);
+        }
+		if (Utils.isDefined(highlightedNode)) highlightedNode.setHighlightedWithNeighbours(true);
+	};
+
+}
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/services/loader.js b/sources/src/main/webapp/js/services/loader.js
new file mode 100644
index 0000000..01b9f8d
--- /dev/null
+++ b/sources/src/main/webapp/js/services/loader.js
@@ -0,0 +1,62 @@
+/**
+ * Loader animation displayed when running some expensive operation.
+ * 
+ * @constructor
+ * @param {object} options 
+ */
+function Loader(options) {
+	var defaultOptions = {
+		lines: 16, // The number of lines to draw
+		length: 21, // The length of each line
+		width: 8, // The line thickness
+		radius: 40, // The radius of the inner circle
+		rotate: 0, // The rotation offset
+		color: '#000', // #rgb or #rrggbb
+		speed: 1.2, // Rounds per second
+		trail: 58, // Afterglow percentage
+		shadow: true, // Whether to render a shadow
+		hwaccel: true, // Whether to use hardware acceleration
+		className: 'loader-spinner', // The CSS class to assign to the spinner
+		zIndex: 2, // The z-index (defaults to 2000000000)
+	};
+
+	var opts = $.extend(defaultOptions, options);
+
+	/**
+	 * Spin jQuery plugin.
+	 */
+	$.fn.spin = function(opts) {
+		this.each(function() {
+			var $this = $(this),
+			data = $this.data();
+			
+			if (data.spinner) {
+				data.spinner.stop();
+				delete data.spinner;
+			}
+
+			if (opts !== false) {
+				data.spinner = new Spinner($.extend({
+					color: $this.css('color')
+				}, opts)).spin(this);
+			}
+		});
+		return this;
+	};
+
+	/**
+	* Enables loader.
+	*/
+	this.enable = function() {
+		$('#loader').show();
+		$('#spinLoader').spin(opts);
+	};
+
+	/**
+	* Disables loader.
+	*/
+	this.disable = function() {
+		$('#loader').hide();
+		$('#spinLoader').spin(false);
+	};
+}
diff --git a/sources/src/main/webapp/js/services/zoom.js b/sources/src/main/webapp/js/services/zoom.js
new file mode 100644
index 0000000..7f1b8ac
--- /dev/null
+++ b/sources/src/main/webapp/js/services/zoom.js
@@ -0,0 +1,65 @@
+/**
+ * @constructor
+ * @param {float} defaultScale Default zoom scale.
+ */
+function Zoom(defaultScale) {
+	/** @prop {float} scale Current zoom scale. */
+	this.scale = defaultScale || 1;
+	
+	var steps = [ 0.1, 0.25, 0.4, 0.55, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.45, 1.6, 1.75, 1.9, 2.5, 3.5, 5 ];
+	var step = steps.indexOf(this.scale);
+	
+	/**
+	 * Zoom in. If the current scale is maximal, it does nothing.
+	 */
+	this.zoomIn = function() {
+		if (step === steps.length - 1) return;
+
+		if (step === 0) {
+			document.querySelector('#zoomOut').classList.remove('disabled');
+			document.querySelector('#zoomOut img').src = 'images/zoom_out.png';
+		}
+
+		step++;
+		zoomChanged.call(this);
+
+		if (step === steps.length - 1) {
+			document.querySelector('#zoomIn').classList.add('disabled');
+			document.querySelector('#zoomIn img').src = 'images/zoom_in_disabled.png';
+		}
+	};
+	
+	/**
+	 * Zoom out. If the current scale is minimal, it does nothing.
+	 */
+	this.zoomOut = function() {
+		if (step === 0) return;
+
+		if (step === steps.length - 1) {
+			document.querySelector('#zoomIn').classList.remove('disabled');
+			document.querySelector('#zoomIn img').src = 'images/zoom_in.png';
+		}
+
+		step--;
+		zoomChanged.call(this);
+
+		if (step === 0) {
+			document.querySelector('#zoomOut').classList.add('disabled');
+			document.querySelector('#zoomOut img').src = 'images/zoom_out_disabled.png';
+		}
+	};
+	
+	/**
+	 * Change current zoom scale according to currently selected step.
+	 */
+	function zoomChanged() {
+		this.scale = steps[step];
+		
+		document.getElementById('zoomValue').innerText = Math.round(this.scale * 100) + '%';
+		document.getElementById('graph').setAttribute('transform', 'scale(' + this.scale + ')');
+
+		// TODO: zoom to mouse position / center
+		
+		app.redrawEdges();
+	}
+}
diff --git a/sources/src/main/webapp/js/valueObjects/coordinates.js b/sources/src/main/webapp/js/valueObjects/coordinates.js
new file mode 100644
index 0000000..4583692
--- /dev/null
+++ b/sources/src/main/webapp/js/valueObjects/coordinates.js
@@ -0,0 +1,12 @@
+/**
+ * Class representing a coordinates pair.
+ * @constructor
+ * @param {float} x X coordinate.
+ * @param {float} y Y coordinate.
+ */
+function Coordinates(x, y) {
+	/** @prop {float} x X coordinate. */
+	this.x = x;
+	/** @prop {float} y Y coordinate. */
+	this.y = y;
+}
diff --git a/sources/src/main/webapp/js/valueObjects/diagram.js b/sources/src/main/webapp/js/valueObjects/diagram.js
new file mode 100644
index 0000000..bcd70d9
--- /dev/null
+++ b/sources/src/main/webapp/js/valueObjects/diagram.js
@@ -0,0 +1,13 @@
+/**
+ * Class representing a saved diagram.
+ * @constructor
+ * @param {object} props Properties of the diagram.
+ */
+function Diagram(props) {
+	/** @prop {int} id Identifier of the diagram. */
+	this.id = parseInt(props.id);
+	/** @prop {string} name Name of the diagram. */
+	this.name = props.name;
+	/** @prop {bool} public True if the diagram is public, otherwise false. */
+	this.public = props.public === '1';
+}
diff --git a/sources/src/main/webapp/js/zoom.js b/sources/src/main/webapp/js/zoom.js
deleted file mode 100644
index 7f1b8ac..0000000
--- a/sources/src/main/webapp/js/zoom.js
+++ /dev/null
@@ -1,65 +0,0 @@
-/**
- * @constructor
- * @param {float} defaultScale Default zoom scale.
- */
-function Zoom(defaultScale) {
-	/** @prop {float} scale Current zoom scale. */
-	this.scale = defaultScale || 1;
-	
-	var steps = [ 0.1, 0.25, 0.4, 0.55, 0.7, 0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.45, 1.6, 1.75, 1.9, 2.5, 3.5, 5 ];
-	var step = steps.indexOf(this.scale);
-	
-	/**
-	 * Zoom in. If the current scale is maximal, it does nothing.
-	 */
-	this.zoomIn = function() {
-		if (step === steps.length - 1) return;
-
-		if (step === 0) {
-			document.querySelector('#zoomOut').classList.remove('disabled');
-			document.querySelector('#zoomOut img').src = 'images/zoom_out.png';
-		}
-
-		step++;
-		zoomChanged.call(this);
-
-		if (step === steps.length - 1) {
-			document.querySelector('#zoomIn').classList.add('disabled');
-			document.querySelector('#zoomIn img').src = 'images/zoom_in_disabled.png';
-		}
-	};
-	
-	/**
-	 * Zoom out. If the current scale is minimal, it does nothing.
-	 */
-	this.zoomOut = function() {
-		if (step === 0) return;
-
-		if (step === steps.length - 1) {
-			document.querySelector('#zoomIn').classList.remove('disabled');
-			document.querySelector('#zoomIn img').src = 'images/zoom_in.png';
-		}
-
-		step--;
-		zoomChanged.call(this);
-
-		if (step === 0) {
-			document.querySelector('#zoomOut').classList.add('disabled');
-			document.querySelector('#zoomOut img').src = 'images/zoom_out_disabled.png';
-		}
-	};
-	
-	/**
-	 * Change current zoom scale according to currently selected step.
-	 */
-	function zoomChanged() {
-		this.scale = steps[step];
-		
-		document.getElementById('zoomValue').innerText = Math.round(this.scale * 100) + '%';
-		document.getElementById('graph').setAttribute('transform', 'scale(' + this.scale + ')');
-
-		// TODO: zoom to mouse position / center
-		
-		app.redrawEdges();
-	}
-}
diff --git a/sources/src/main/webapp/showGraph.jsp b/sources/src/main/webapp/showGraph.jsp
index 51b50e1..965dd6a 100644
--- a/sources/src/main/webapp/showGraph.jsp
+++ b/sources/src/main/webapp/showGraph.jsp
@@ -41,20 +41,23 @@
 
 		<script src="js/errors/invalidArgumentError.js"></script>
 
-		<script src="js/constants.js"></script>
-		<script src="js/coordinates.js"></script>
-		<script src="js/diagram.js"></script>
-		<script src="js/forceDirected.js"></script>
-		<script src="js/graphLoader.js"></script>
-		<script src="js/graphExporter.js"></script>
-		<script src="js/loader.js"></script>
-		<script src="js/markSymbol.js"></script>
+		<script src="js/services/forceDirected.js"></script>
+		<script src="js/services/graphLoader.js"></script>
+		<script src="js/services/graphExporter.js"></script>
+		<script src="js/services/loader.js"></script>
+		<script src="js/services/zoom.js"></script>
+
 		<script src="js/utils/cookies.js"></script>
 		<script src="js/utils/dom.js"></script>
 		<script src="js/utils/utils.js"></script>
-		<script src="js/zoom.js"></script>
-		<script src="js/app.js"></script>
+
+		<script src="js/valueObjects/coordinates.js"></script>
+		<script src="js/valueObjects/diagram.js"></script>
+
 		<script src="js/userMenu.js"></script>
+		<script src="js/markSymbol.js"></script>
+		<script src="js/constants.js"></script>
+		<script src="js/app.js"></script>
 
 		<title>IMiGEr</title>
 	</head>
