commit 0354f04045fe936012cf3644d449fd03eaa621a5
Author: Pavel Fidranský <fidransky@outlook.com>
Date:   Fri Aug 24 11:41:23 2018 +0200

    Viewport minimap
    
    (WIP) minimap may be dragged to move Viewport
    
    viewport Minimap

diff --git a/sources/src/main/webapp/css/main.css b/sources/src/main/webapp/css/main.css
index e9b53f0..2e69167 100644
--- a/sources/src/main/webapp/css/main.css
+++ b/sources/src/main/webapp/css/main.css
@@ -468,7 +468,6 @@ body {
 	flex-basis: 350px;
 	flex-direction: column;
 	justify-content: space-between;
-	width: 350px;
 	background-color: #f0f9ff;
 }
 
@@ -790,10 +789,24 @@ body {
 	content: "▼";
 }
 
+.minimap {
+	border-top: 1px solid #7db9e8;
+	background-color: #fff;
+}
+
+.minimap-viewport {
+	stroke: #999;
+	stroke-width: 1px;
+	fill-opacity: 0;
+	shape-rendering: crispedges;
+	transition: stroke .4s;
+}
+
+.minimap-viewport:hover {
+	stroke: #000;
+}
+
 .status-bar {
-    /* position: fixed; */
-    bottom: 0;
-    flex-basis: 22px;
     font-size: 11px;
     z-index: 1;
     color: #cceefc;
@@ -806,6 +819,11 @@ body {
 	padding: 5px;
 }
 
+.status-bar .link {
+	text-decoration: underline;
+	cursor: pointer;
+}
+
 /**
  * MODAL WINDOW
  **/
diff --git a/sources/src/main/webapp/js/app.js b/sources/src/main/webapp/js/app.js
index 6c4f365..d84307e 100644
--- a/sources/src/main/webapp/js/app.js
+++ b/sources/src/main/webapp/js/app.js
@@ -133,6 +133,7 @@ function App() {
 
 		self.sidebarComponent = new Sidebar;
 		content.appendChild(self.sidebarComponent.render());
+		self.sidebarComponent.minimapComponent.setViewportSize(self.viewportComponent.getSize());
 
 		// context menu
 		document.body.addEventListener('mousedown', function() {
@@ -279,6 +280,8 @@ function App() {
 		window.addEventListener('resize', function(e) {
 			self.headerHeight = getHeaderHeight();
 			self.redrawEdges();
+
+			self.sidebarComponent.minimapComponent.setViewportSize(self.viewportComponent.getSize());
 		});
 	}
 
diff --git a/sources/src/main/webapp/js/components/minimap.js b/sources/src/main/webapp/js/components/minimap.js
new file mode 100644
index 0000000..fab59ce
--- /dev/null
+++ b/sources/src/main/webapp/js/components/minimap.js
@@ -0,0 +1,93 @@
+/**
+ * @constructor
+ */
+function Minimap() {
+	var rootElement;
+	var viewportElement;
+
+	var zoom = 0.1;
+	var useElement = '#graph';
+
+	/**
+	 * 
+	 * @param {object} size Object holding current dimensions of the viewport component.
+	 */
+	this.setViewportSize = function(size) {
+		viewportElement.setAttribute('width', size.width * zoom);
+		viewportElement.setAttribute('height', size.height * zoom);
+	};
+
+	/**
+	 * @returns {Coordinates} Current coordinates of the viewport.
+	 */
+	this.getViewportPosition = function() {
+		return new Coordinates(
+			+viewportElement.getAttribute('x'),
+			+viewportElement.getAttribute('y'),
+		);
+	};
+
+	/**
+	 * TODO: jsDoc
+	 * @param {Coordinates} coords Coordinates of the viewport.
+	 */
+	this.setViewportPosition = function(coords) {
+		viewportElement.setAttribute('x', coords.x * -1 * zoom);
+		viewportElement.setAttribute('y', coords.y * -1 * zoom);
+	};
+
+	/**
+	 * Creates a new DOM element representing the minimap in memory.
+	 * @returns {Element} HTML DOM element.
+	 */
+	this.render = function() {
+		rootElement = app.utils.createSvgElement('svg', {
+			'class': 'minimap',
+			'id': 'minimapComponent',
+			'viewBox': '-100 -50 350 200',
+		});
+
+		var graphElement = app.dom.createSvgElement('use', {
+			'transform': `scale(${zoom})`,
+		});
+		graphElement.setAttributeNS('http://www.w3.org/1999/xlink', 'href', useElement);
+		rootElement.appendChild(graphElement);
+
+		viewportElement = app.dom.createSvgElement('rect', {
+			'class': 'minimap-viewport',
+		});
+		viewportElement.addEventListener('mousedown', onMouseDown.bind(this));
+		rootElement.appendChild(viewportElement);
+
+		return rootElement;
+	};
+
+	function onMouseDown(e) {
+		var start = new Coordinates(e.clientX, e.clientY);
+		var minimapViewportPosition = this.getViewportPosition();
+		var viewportPosition = app.viewportComponent.getPosition();
+
+		viewportElement.addEventListener('mousemove', mouseMove);
+		viewportElement.addEventListener('mouseup', mouseUp);
+		
+		function mouseMove(e) {
+			e.preventDefault();
+
+			var offset = new Coordinates(start.x - e.clientX, start.y - e.clientY);
+
+			viewportElement.setAttribute('x', minimapViewportPosition.x - offset.x);
+			viewportElement.setAttribute('y', minimapViewportPosition.y - offset.y);
+
+			app.viewportComponent.setPosition(new Coordinates(viewportPosition.x + (1 / zoom) * offset.x, viewportPosition.y + (1 / zoom) * offset.y));
+		}
+
+		function mouseUp(e) {
+			start = null;
+			minimapViewportPosition = null;
+			viewportPosition = null;
+
+			viewportElement.removeEventListener('mousemove', mouseMove);
+			viewportElement.removeEventListener('mouseup', mouseUp);
+		}
+	}
+}
diff --git a/sources/src/main/webapp/js/components/sidebar.js b/sources/src/main/webapp/js/components/sidebar.js
index ac1103c..8bc49ef 100644
--- a/sources/src/main/webapp/js/components/sidebar.js
+++ b/sources/src/main/webapp/js/components/sidebar.js
@@ -9,6 +9,8 @@ function Sidebar() {
 	this.unconnectedNodeListComponent = null;
 	/** @prop {SidebarExcludedNodeList} excludedNodeListComponent */
 	this.excludedNodeListComponent = null;
+	/** @prop {Minimap} minimapComponent */
+	this.minimapComponent = null;
 	/** @prop {StatusBar} statusBarComponent */
 	this.statusBarComponent = null;
 
@@ -38,7 +40,6 @@ function Sidebar() {
 			'id': 'sidebar',
 		});
 
-
 		var sidebarNav = app.utils.createHtmlElement('nav', {
 			'class': 'sidebar-navbar',
 			'id': 'uploadMenu',
@@ -75,7 +76,6 @@ function Sidebar() {
 		});
 		sidebarContainer.appendChild(this.unconnectedNodeListComponent.render());
 
-
 		// excluded nodes
 		this.excludedNodeListComponent = new SidebarExcludedNodeList({
 			'id': 'excludedNodeListComponent',
@@ -83,10 +83,18 @@ function Sidebar() {
 		rootElement.appendChild(this.excludedNodeListComponent.render());
 
 
+		var sidebarBottom = app.utils.createHtmlElement('div', {
+			'class': 'sidebar-bottom',
+		});
+		rootElement.appendChild(sidebarBottom);
+
+		// minimap
+		this.minimapComponent = new Minimap;
+		sidebarBottom.appendChild(this.minimapComponent.render());
+
 		// status bar
 		this.statusBarComponent = new StatusBar;
-		rootElement.appendChild(this.statusBarComponent.render());
-
+		sidebarBottom.appendChild(this.statusBarComponent.render());
 
 		return rootElement;
 	};
diff --git a/sources/src/main/webapp/js/components/statusBar.js b/sources/src/main/webapp/js/components/statusBar.js
index bb8f879..e909614 100644
--- a/sources/src/main/webapp/js/components/statusBar.js
+++ b/sources/src/main/webapp/js/components/statusBar.js
@@ -29,6 +29,13 @@ function StatusBar() {
 		});
 		rootElement.appendChild(componentCounterElement);
 
+		minimapToggleElement = app.dom.createHtmlElement('span', {
+			'class': 'link',
+		});
+		minimapToggleElement.appendChild(app.dom.createTextElement('toggle minimap'));
+		minimapToggleElement.addEventListener('click', toggleMinimap.bind(this));
+		rootElement.appendChild(minimapToggleElement);
+
 		return rootElement;
 	};
 
@@ -38,4 +45,10 @@ function StatusBar() {
 	this.reset = function() {
 		componentCounterElement.innerHTML = '';
 	};
+
+	function toggleMinimap(e) {
+		e.preventDefault();
+
+		document.getElementById('minimapComponent').classList.toggle('hidden');
+	}
 }
\ No newline at end of file
diff --git a/sources/src/main/webapp/js/components/viewport.js b/sources/src/main/webapp/js/components/viewport.js
index ebd591c..f3608fa 100644
--- a/sources/src/main/webapp/js/components/viewport.js
+++ b/sources/src/main/webapp/js/components/viewport.js
@@ -94,6 +94,13 @@ function Viewport() {
 		return groupList;
 	};
 
+	this.getSize = function() {
+		return {
+			'width': rootElement.offsetWidth,
+			'height': rootElement.offsetHeight,
+		};
+	};
+
 	this.getPosition = function() {
 		return new Coordinates(
 			+innerSvgElement.getAttribute('x'),
@@ -101,6 +108,11 @@ function Viewport() {
 		);
 	};
 
+	this.setPosition = function(coords) {
+		innerSvgElement.setAttribute('x', coords.x);
+		innerSvgElement.setAttribute('y', coords.y);
+	};
+
 	this.center = function() {
 		var sumOfCenters = new Coordinates(0, 0);
 		var bbox = rootElement.getBoundingClientRect();
@@ -222,11 +234,15 @@ function Viewport() {
 		
 		function mouseMove(e) {
 			if (!pan) return;
-			
+
 			e.preventDefault();
-			
-			innerSvgElement.setAttribute('x', position.x - (start.x - e.clientX));
-			innerSvgElement.setAttribute('y', position.y - (start.y - e.clientY));
+
+			var coords = new Coordinates(position.x - (start.x - e.clientX), position.y - (start.y - e.clientY));
+
+			innerSvgElement.setAttribute('x', coords.x);
+			innerSvgElement.setAttribute('y', coords.y);
+
+			app.sidebarComponent.minimapComponent.setViewportPosition(coords);
 		}
 
 		function mouseUp(e) {
diff --git a/sources/src/main/webapp/showGraph.jsp b/sources/src/main/webapp/showGraph.jsp
index cc3aa2f..347a475 100644
--- a/sources/src/main/webapp/showGraph.jsp
+++ b/sources/src/main/webapp/showGraph.jsp
@@ -26,6 +26,7 @@
 		<script src="js/components/floatingPoint.js"></script>
 		<script src="js/components/group.js"></script>
 		<script src="js/components/groupVertexList.js"></script>
+		<script src="js/components/minimap.js"></script>
 		<script src="js/components/sidebar.js"></script>
 		<script src="js/components/sidebarExcludedNodeList.js"></script>
 		<script src="js/components/sidebarUnconnectedNodeList.js"></script>
