From 70ef87d2eef7e2deffee78acfe86650ae2e65720 Mon Sep 17 00:00:00 2001
From: Nikolaus Krismer <niko@krismer.de>
Date: Wed, 12 Feb 2014 18:00:24 +0100
Subject: [PATCH] implemented configuration singleton (to prevent global
 variables) isochrone button on geosearch result now only shown if datastore
 is configured for clicked point (still some todos open)

---
 src/main/webapp/index.html                    | 42 +++-------
 src/main/webapp/js/isochrone/configuration.js | 32 ++++++++
 src/main/webapp/js/isochrone/initHelper.js    | 55 ++++++++++++++
 .../webapp/js/isochrone/searchExtender.js     | 76 +++++++++++++++++++
 src/main/webapp/js/map/geosearch.js           | 11 +--
 src/main/webapp/js/map/isomap.js              | 16 +++-
 .../webapp/js/service/serviceConfiguration.js | 69 +++++++++++------
 7 files changed, 234 insertions(+), 67 deletions(-)
 create mode 100644 src/main/webapp/js/isochrone/configuration.js
 create mode 100644 src/main/webapp/js/isochrone/initHelper.js
 create mode 100644 src/main/webapp/js/isochrone/searchExtender.js

diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html
index d58dc28b..a4ab9cb7 100644
--- a/src/main/webapp/index.html
+++ b/src/main/webapp/index.html
@@ -31,48 +31,23 @@
 	<script type="text/javascript" src="js/map/isomap.js"></script>
 	<script type="text/javascript" src="js/service/serviceConfiguration.js"></script>
 	<script type="text/javascript" src="js/service/websocket.js"></script>
+	<script type="text/javascript" src="js/isochrone/configuration.js"></script>
+	<script type="text/javascript" src="js/isochrone/initHelper.js"></script>
+	<script type="text/javascript" src="js/isochrone/searchExtender.js"></script>
 	<script type="text/javascript">
 		// TODO: Should we really work with global javascript variables (wouldn't listener also work)?
 		var datasetCfgs = {};
-		function init() {
-			var ws = new Websocket();
-			var config = new Configuration(ws);
-			config.getFromServer();
-
-			var isoMap = new IsoMap('map');
-			isoMap.locateAndDraw();
-		}
 
 		$(function() {
-			$('#help-dialog').dialog({
-	    		autoOpen: false,
-	    		modal: true,
-	    		resizable: false
-	    	});
-
-			$('#settings-dialog').dialog({
-	    		autoOpen: false,
-	    		modal: true,
-	    		resizable: false
-	    	});
-
-			$('#speed').spinner({
-				 step: 0.1,
-				 numberFormat: "n"
-			});
+			var h = new InitHelper();
 
-		    $('#settings-dialog select').css({'width':'100%'});
-		    $('#settings-dialog input').css({'width':'100%'});
-			$('#datetime').datetimepicker({
-				dateFormat: 'dd.mm.yy',
-				timeFormat: 'HH:mm'
-			});
-			$('#datetime .ui-datepicker-today').click();
-		})
+			h.initPreferences();
+			h.initMap();
+		});
 	</script>
 </head>
 
-<body onload="javascript:init();">
+<body>
 	<div class="page-wrapper">
 		<div id="map" class="map"></div>
 		<div id="help-dialog" title="Hilfe">
@@ -89,6 +64,7 @@
 				</select><br>
 				<label for="name">Dataset</label><br>
 				<select name="dataset" id="dataset" class="ui-widget-content ui-corner-all">
+					<!-- TODO: Remove hard coded values and read them from server configuration -->
 					<option value="BZ">Bozen</option>
 					<option value="ST">S&uuml;dtirol</option>
 					<option value="IT">Italien</option>
diff --git a/src/main/webapp/js/isochrone/configuration.js b/src/main/webapp/js/isochrone/configuration.js
new file mode 100644
index 00000000..05bd67f4
--- /dev/null
+++ b/src/main/webapp/js/isochrone/configuration.js
@@ -0,0 +1,32 @@
+/**
+ * Singleton class configuration.
+ * This is used to store the client's configuration
+ */
+function Configuration() {
+	if (arguments.callee._singletonInstance) {
+		return arguments.callee._singletonInstance;
+	}
+	arguments.callee._singletonInstance = this;
+
+	var datasetConfigMap = {};
+
+	this.addDatasetConfig = function(dSetConfig) {
+		if (!dSetConfig || !dSetConfig.datasetName) {
+			console.warn('Not adding invalid dataset configuration to client configuration singleton');
+			console.debug(' - dSetConfig given:', dSetConfig);
+			return;
+		}
+
+		datasetConfigMap[dSetConfig.datasetName] = dSetConfig;
+	};
+
+	this.getDatasetConfigMap = function() {
+		return datasetConfigMap;
+	};
+
+	this.getDatasetConfig = function(datasetName) {
+		return datasetConfigMap[datasetName];
+	};
+};
+
+new Configuration();
diff --git a/src/main/webapp/js/isochrone/initHelper.js b/src/main/webapp/js/isochrone/initHelper.js
new file mode 100644
index 00000000..a50e92d9
--- /dev/null
+++ b/src/main/webapp/js/isochrone/initHelper.js
@@ -0,0 +1,55 @@
+function InitHelper() {
+	var config = null;
+	var isoMap = null;
+	var ws = null;
+
+	// Public methods
+
+	this.initMap = function() {
+		isoMap = new IsoMap('map');
+		isoMap.locateAndDraw();
+
+		ws = new Websocket();
+		config = new ServiceConfiguration(ws);
+		config.getFromServer();
+
+		$(document).on('isomap_draw', extendSearch.bind(this));
+	};
+
+	this.initPreferences = function() {
+		$('#help-dialog').dialog({
+			autoOpen: false,
+			modal: true,
+			resizable: false
+		});
+
+		$('#settings-dialog').dialog({
+			autoOpen: false,
+			modal: true,
+			resizable: false
+		});
+
+		$('#speed').spinner({
+			 step: 0.1,
+			 numberFormat: 'n'
+		});
+
+	    $('#settings-dialog select').css({'width':'100%'});
+	    $('#settings-dialog input').css({'width':'100%'});
+		$('#datetime').datetimepicker({
+			dateFormat: 'dd.mm.yy',
+			timeFormat: 'HH:mm'
+		});
+		$('#datetime .ui-datepicker-today').click();
+	};
+
+	// Private methods
+
+	extendSearch = function() {
+		var mapElem = isoMap.getMap(),
+			searchExtender;
+
+		searchExtender = new SearchExtender(mapElem);
+		searchExtender.extendResult(isoMap.getMap());
+	};
+};
\ No newline at end of file
diff --git a/src/main/webapp/js/isochrone/searchExtender.js b/src/main/webapp/js/isochrone/searchExtender.js
new file mode 100644
index 00000000..59b95d96
--- /dev/null
+++ b/src/main/webapp/js/isochrone/searchExtender.js
@@ -0,0 +1,76 @@
+function SearchExtender(map) {
+	var geosearchEventName = 'geosearch_showresult';
+	var m = null;
+
+	// Constructors
+
+	m = map;
+
+	// Public methods
+
+	this.extendResult = function() {
+		m.on(geosearchEventName, onSearchResult.bind(this));
+	};
+
+	// Private methods
+
+	isochroneAvailable = function(point) {
+		var config = Configuration(),
+			configCandidates = [],
+			datasetConfigs = config.getDatasetConfigMap();
+
+		for (var dSetConfigName in datasetConfigs) {
+			var dSetConfig = datasetConfigs[dSetConfigName];
+			if (dSetConfig.latLngBBox.contains(point)) {
+				configCandidates[configCandidates.length] = dSetConfig;
+			}
+		}
+
+		// Check if only one datasets bbox contains the query point.
+		// If so... return the matching dataset config
+		var resultLength = configCandidates.length;
+		if (resultLength == 1) {
+			// TODO: Should we combine this with the dataset dropdown in the settings?
+			// Should we remove the combobox?
+			return configCandidates[0];
+		}
+
+		// at least two datasets are available for isochrone configuration
+
+		// FIXME: How do we handle this? Should we combine this with the dataset dropdown in the settings (if so... how)
+		// By now we use the smallest dataset containing the point
+		var tmpCandidate = configCandidates[0];
+		var tmpSize = tmpCandidate.bBox.getSize();
+		for (var i = 1; i < resultLength; ++i) {
+			var cSize = configCandidates[i].bBox.getSize();
+			if (cSize.x * cSize.y  < tmpSize.x * tmpSize.y) {
+				tmpCandidate = configCandidates[i];
+				tmpSize = cSize;
+			}
+		}
+
+		return tmpCandidate;
+	};
+
+	onSearchResult = function(data) {
+		var divIcons,
+        	iconIsochrone = null,
+        	li;
+
+		if (!data.queryPoint || !isochroneAvailable(data.queryPoint)) {
+			return;
+		}
+
+		divIcons = document.createElement('div');
+		li = $(data.element).find('li:first div');
+
+		iconIsochrone = document.createElement('div');
+		iconIsochrone.id = 'icon-isochrone';
+		iconIsochrone.className = 'icon-isochrone';
+		iconIsochrone.appendChild(document.createTextNode('Isochrone'));
+		divIcons.className = 'geosearch-result-icons';
+		divIcons.appendChild(iconIsochrone);
+
+		$(divIcons).insertBefore(li);
+	};
+};
diff --git a/src/main/webapp/js/map/geosearch.js b/src/main/webapp/js/map/geosearch.js
index 47877638..0dd3c68d 100644
--- a/src/main/webapp/js/map/geosearch.js
+++ b/src/main/webapp/js/map/geosearch.js
@@ -327,10 +327,8 @@ L.Control.GeoSearch = L.Control.extend({
     _setResult: function(result) {
         var displayName = null,
             displayDescription = null,
-            divIcons = document.createElement('div'),
             divText = document.createElement('div'),
             elem = this._result,
-            iconIsochrone = null,
             index = -1;
 	        li = document.createElement('li'),
 	        txtDescription = null,
@@ -354,20 +352,15 @@ L.Control.GeoSearch = L.Control.extend({
         txtName = document.createElement('span');
         txtName.className = 'result-name';
         txtName.innerHTML = displayName.replace(/^\s+|\s+$/g,'');
-        iconIsochrone = document.createElement('div');
-        iconIsochrone.id = 'icon-isochrone';
-        iconIsochrone.className = 'icon-isochrone';
-        iconIsochrone.appendChild(document.createTextNode('Isochrone'));
-        divIcons.className = 'geosearch-result-icons';
-        divIcons.appendChild(iconIsochrone);
         divText.className = 'geosearch-result-text';
         divText.appendChild(txtName);
         divText.appendChild(txtDescription);
-        li.appendChild(divIcons);
         li.appendChild(divText);
 
         elem.appendChild(li);
 	    elem.style.display = 'block';
+
+	    this._map.fireEvent('geosearch_showresult', {element: elem, queryPoint: L.latLng({lat: parseFloat(result.lat), lon: parseFloat(result.lon)})});
     },
 
 	_setSuggestList: function(results) {
diff --git a/src/main/webapp/js/map/isomap.js b/src/main/webapp/js/map/isomap.js
index c6feeb1f..c687d1c7 100644
--- a/src/main/webapp/js/map/isomap.js
+++ b/src/main/webapp/js/map/isomap.js
@@ -2,6 +2,7 @@ function IsoMap(divId) {
 	var INNSBRUCK = [47.265718, 11.391342];
 //	var VIENNA = [48.186312, 16.317615];
 	var mapDivId = null;
+	var map = null;
 	var mapOptions = {
 		attributionControl: false,
 		center: INNSBRUCK,
@@ -14,11 +15,22 @@ function IsoMap(divId) {
 
 	mapDivId = divId;
 
+	// Getter
+
+	/**
+	 * Gets the internal map object.
+	 * This is only useful after the map has been drawn
+	 *
+	 * @return the map object. Null if the map has not been drawn.
+	 */
+	this.getMap = function() {
+		return map;
+	};
+
 	// Public methods
 
 	this.draw = function() {
 		var layerControl = L.control.layers({position: 'topright'}),
-			map = null,
 			zoomControl = L.control.zoom({position: 'bottomright'});
 
 		console.log('Drawing map');
@@ -45,6 +57,8 @@ function IsoMap(divId) {
 			provider: new L.GeoSearch.Provider.OpenStreetMap(),
 			showMarker: true
 		}));
+
+		$(document).trigger('isomap_draw');
 	};
 
 	this.locateAndDraw = function() {
diff --git a/src/main/webapp/js/service/serviceConfiguration.js b/src/main/webapp/js/service/serviceConfiguration.js
index 7b8f3005..f2cab3f9 100644
--- a/src/main/webapp/js/service/serviceConfiguration.js
+++ b/src/main/webapp/js/service/serviceConfiguration.js
@@ -1,4 +1,5 @@
-function Configuration(/* Websocket */ ws) {
+function ServiceConfiguration(/* Websocket */ ws) {
+	var actionName = 'getConfiguration';
 	var websocket = null;
 
 	// Constructor
@@ -8,46 +9,66 @@ function Configuration(/* Websocket */ ws) {
 	// Public methods
 
 	this.getFromServer = function() {
-		var actionName = 'getConfiguration';
-		$(document).on(actionName, function(jsonData) {
-			console.debug('Handling event "' + actionName + '":', jsonData);
-			datasetCfgs = readConfiguration(jsonData);
-		});
-
+		$(document).on(actionName, onServerResponse.bind(this));
 		websocket.sendWsMessage('{"action" : "' + actionName + '"}');
 	};
 
 	// Private methods
 
-	readConfiguration = function(data) {
-		// reads the enabled datasets and stores them in a client side config
-		// hashtable
-		// each dataset contains:
-		// - pointQ the a 2D-coordinate of the query point
-		// - timeQ the arrival or departure time of the query point
-		// - bbox the spatial extent of the specified area
-		var result = {};
-		var datasets = data.defaultDatasets;
+	/**
+	 * Reads the enabled datasets and stores them in a client side configuration singleton
+	 * each dataset contains:
+	 * <ul>
+	 * <li>bbox the spatial extent of the specified area</li>
+	 * <li>queryPoint the a 2D-coordinate of the query point</li>
+	 * <li>time the arrival or departure time of the query point</li>
+	 * </ul>
+	 */
+	onServerResponse = function(data) {
+		var config = Configuration(),
+			datasets = data.defaultDatasets,
+			result = {};
+
+		console.debug('Handling event "' + actionName + '":', data);
 		for (var i = 0; i < datasets.length; i++) {
 			var dataset = datasets[i];
-			var cfgData = {
-				queryPoint: new L.LatLng(dataset.queryPoint.x, dataset.queryPoint.y),
-				bbox: new L.Bounds(dataset.bbox.minX, dataset.bbox.minY, dataset.bbox.maxX, dataset.bbox.maxY),
+			var boundLower = new L.Point(dataset.bbox.minX, dataset.bbox.minY);
+			var boundUpper = new L.Point(dataset.bbox.maxX, dataset.bbox.maxY);
+			var latlngLower = convertToLatLng(boundLower);
+			var latlngUpper = convertToLatLng(boundUpper);
+
+			var dSetConfig = {
+				bBox: new L.Bounds(boundLower, boundUpper),
+				datasetName: dataset.name,
 				date: dataset.date,
-				time: dataset.time,
 				isoEdgeLayer: dataset.isoEdgeLayer,
 				isoVertexLayer: dataset.isoVertexLayer,
 				isoCoverageLayer: dataset.isoCoverageLayer,
-				prefix: dataset.prefix
+				latLngBBox: new L.latLngBounds(latlngLower, latlngUpper),
+				prefix: dataset.prefix,
+				queryPoint: new L.LatLng(dataset.queryPoint.x, dataset.queryPoint.y),
+				time: dataset.time
 			};
+
 			if (dataset.totalInhabitants) {
-				cfgData.totalInhabitants = dataset.totalInhabitants;
+				dSetConfig.totalInhabitants = dataset.totalInhabitants;
 			}
 
-			result[dataset.name] = cfgData;
+			config.addDatasetConfig(dSetConfig);
 		}
 
-	//	config.contextPath = data.contextPath;
 		return result;
 	};
+
+	/**
+	 * Converts a point from projection EPSG:3857 to LatLng (EPSG:4326)
+	 *
+	 * @see http://developer.tomtom.com/docs/read/map_toolkit/javascript_sdk_2_0/Migration_Guide
+	 * @param L.Point the point to convert (containing EPSG:3857 x and y)
+	 * @return L.latLng the matching L.latLng object (coordinates in EPSG:4326)
+	 */
+	convertToLatLng = function(point) {
+		var earthRadius = 6378137;
+		return L.Projection.SphericalMercator.unproject(point.divideBy(earthRadius));
+	};
 };
-- 
GitLab