diff --git a/LICENSE b/LICENSE
index a3f25ef5aedbfc561eb545ef1ea133057e5f0c49..05e4869eff0964a65833e61a3a8029d47c0f0e9e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2019 University of Applied Science Stuttgart
+Copyright (c) 2022 University of Applied Science Stuttgart
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/pom.xml b/pom.xml
index 19f1d2170d85d9061d24f5de0bc019e1a5e7e4e1..7e2a10de5a7c588dc8db98ae18c7f8df4045ffd4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,7 +4,7 @@
 
 	<groupId>eu.simstadt</groupId>
 	<artifactId>region-chooser</artifactId>
-	<version>0.2.9-SNAPSHOT</version>
+	<version>0.3.0-SNAPSHOT</version>
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
diff --git a/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java b/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java
index 9e6b2885e8ac9a6279d0bfeeb31e6da61eaf0a5c..8e59d50aea9bdf9e0bf0810509b896b0a1cc18fa 100644
--- a/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java
+++ b/src/main/java/eu/simstadt/regionchooser/RegionChooserBrowser.java
@@ -49,12 +49,8 @@ public JavaScriptFXBridge() {
 		/**
 		 * Launches a background thread in which the hull gets extracted for every CityGML file. The hull gets sent back
 		 * to the JS app in order to be displayed.
-		 *
-		 * NOTE: To be very honest, I don't really understand concurrency in JavaFX. Eric
-		 *
 		 */
 		public void refreshHulls() {
-			//NOTE: Could add progress bar?
 			Task<Void> task = new Task<Void>() {
 				@Override
 				public Void call() throws IOException {
@@ -76,26 +72,35 @@ public Void call() throws IOException {
 		}
 
 		/**
-		 * This method is called from Javascript, with a prepared wktPolygon written in local coordinates.
+		 * This method is called from Javascript, with a prepared wktPolygon written in local coordinates. Executes it in
+		 * the background to avoid freezing the GUI
 		 */
-		public int downloadRegionFromCityGMLs(String wktPolygon, String project, String csvCitygmls, String srsName)
-				throws IOException, ParseException, XPathParseException, NavException {
+		public void downloadRegionFromCityGMLs(String wktPolygon, String project, String csvCitygmls, String srsName) {
 			// It doesn't seem possible to pass arrays or list from JS to Java. So csvCitygmls contains names separated by ;
 			Path[] paths = Stream.of(csvCitygmls.split(";")).map(s -> citygmlPath(project, s)).toArray(Path[]::new);
-
 			String proposedName = csvCitygmls.replace(";", "_").replace(".gml", "") + ".gml";
-
 			File outputFile = selectSaveFileWithDialog(project, proposedName, "part");
-
 			if (outputFile == null) {
-				return -1;
+				return;
 			}
 
-			int count;
-			try (BufferedWriter gmlWriter = Files.newBufferedWriter(outputFile.toPath())) {
-				count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, srsName, gmlWriter, paths);
-			}
-			return count;
+			Task<Integer> downloadTask = new Task<Integer>() {
+				@Override
+				public Integer call() throws IOException, XPathParseException, NavException, ParseException {
+					int count = -1;
+					try (BufferedWriter gmlWriter = Files.newBufferedWriter(outputFile.toPath())) {
+						count = RegionExtractor.selectRegionDirectlyFromCityGML(wktPolygon, srsName, gmlWriter, paths);
+					}
+					LOGGER.info(outputFile + " has been written");
+					return count;
+				}
+			};
+
+			downloadTask.setOnRunning(e -> jsApp.call("downloadStart"));
+
+			downloadTask.setOnSucceeded(e -> jsApp.call("downloadFinished", e.getSource().getValue()));
+
+			new Thread(downloadTask).start();
 		}
 
 
diff --git a/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java b/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java
index 665c32d96abddf0db163949119f214f55795b3ff..c9c460fcdae6c090ea4c920df8c475338da0e82c 100644
--- a/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java
+++ b/src/main/java/eu/simstadt/regionchooser/RegionExtractor.java
@@ -88,7 +88,7 @@ static int selectRegionDirectlyFromCityGML(String wktPolygon, String srsName, Wr
 			LOGGER.warning("No building found in the selected region.");
 		}
 
-		LOGGER.info("Buildings found in selected region " + foundBuildingsCount);
+		LOGGER.info("Buildings found in selected region : " + foundBuildingsCount);
 		//NOTE: This could be a problem if header starts with <core:CityModel> and footer ends with </CityModel>
 		sb.append(citygml.getFooter());
 		return foundBuildingsCount;
diff --git a/src/main/resources/eu/simstadt/regionchooser/website/index.html b/src/main/resources/eu/simstadt/regionchooser/website/index.html
index 141a41524beaf026e85be61ea9e093e2ee7f7729..978ba3540854e07561a8e92a38d966ad5a1f1262 100644
--- a/src/main/resources/eu/simstadt/regionchooser/website/index.html
+++ b/src/main/resources/eu/simstadt/regionchooser/website/index.html
@@ -31,6 +31,7 @@
 		<div id="side">
 			<div id="dataPanel" ></div>
 		</div>
+		<script src="script/utils.js" type="text/javascript"></script>
 		<script src="script/simstadt_openlayers.js" type="text/javascript"></script>
 	</body>
 </html>
diff --git a/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js b/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js
index 165a96bf4dce2011f935b42c439feccf6e675951..99f3b8ec6d26bf18b7eda47648fa7bd81dff42ce 100644
--- a/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js
+++ b/src/main/resources/eu/simstadt/regionchooser/website/script/simstadt_openlayers.js
@@ -1,15 +1,19 @@
-var regionChooser = (function(){
+const styles = {};
+
+styles.original = utils.polygon_style('#447744', 0.2);
+styles.highlighted = utils.polygon_style("#ff44a2", 0.7);
+styles.selected = utils.polygon_style("#ffff00", 0.8);
+
+const regionChooser = (function(){
 	//TODO: Somehow split in classes. This file is getting too big and mixed
 	var publicScope = {};
-	var fromJavaFX = navigator.userAgent.indexOf('JavaFX') !== -1;
-	var dataPanel = $('#dataPanel');
-	var wgs84Sphere = new ol.Sphere(6378137);
+	const fromJavaFX = navigator.userAgent.indexOf('JavaFX') !== -1;
+	const dataPanel = $('#dataPanel');
+	const wgs84Sphere = new ol.Sphere(6378137);
 	var features_by_project;
-	var gmlId;
 	
 	publicScope.init = function(){
 		//NOTE: Only called from JavaFX. At startup, or when Repo has been changed.
-		gmlId = 0;
 		kml_source.clear();
 		document.getElementById("select_repository").style.visibility = "visible";
 	}
@@ -22,33 +26,11 @@ var regionChooser = (function(){
 		source: new ol.source.OSM()
 	});
 
-	function read_kml(url){
-		return new ol.source.KML({
-			projection : ol.proj.get('EPSG:3857'),
-			url : url,
-			extractAttributes : false,
-			extractStyles : false
-		});
-	}
-	
-	var kml_source = read_kml(fromJavaFX ? undefined : 'data/citygml_hulls.kml');
-
-	function polygon_style(color, alpha) {
-		return new ol.style.Style({
-			fill : new ol.style.Fill({
-				color : 'rgba(255, 255, 255,' + alpha + ')'
-			}),
-			stroke : new ol.style.Stroke({
-				color : color,
-				width : 2,
-				lineDash : [ 5, 10 ]
-			}),
-		});
-	}
+	var kml_source = utils.read_kml(fromJavaFX ? undefined : 'data/citygml_hulls.kml');
 
 	var kml_layer = new ol.layer.Vector({
 		source : kml_source,
-		style : polygon_style('#447744', 0.2)
+		style : styles.original
 	});
 
 	var intersections = new ol.source.Vector();
@@ -65,7 +47,6 @@ var regionChooser = (function(){
 	publicScope.addCityGmlHull = function(kmlString) {
 		options = {featureProjection: ol.proj.get('EPSG:3857')};
 		feature = kmlFormat.readFeature(kmlString, options);
-		feature.setId(gmlId++);
 		kml_source.addFeature(feature);
 		dataPanel.append('.');
 		srsName = feature.get("srsName");
@@ -82,10 +63,10 @@ var regionChooser = (function(){
 		})
 	});
 
-	var geoJsonFormat = new ol.format.GeoJSON();
-	var kmlFormat = new ol.format.KML({extractStyles: false});
+	const geoJsonFormat = new ol.format.GeoJSON();
+	const kmlFormat = new ol.format.KML({extractStyles: false});
 
-	kml_layer.addEventListener("change", function() {
+	kml_source.addEventListener("addfeature", function() {
 		map.getView().fitExtent(kml_source.getExtent(), (map.getSize()));
 	});
 
@@ -96,13 +77,13 @@ var regionChooser = (function(){
 			feature["project"] = feature.get("project");
 			feature["name"] = feature.get("name");
 			feature["source"] = "CityGML";
-			feature["originalStyle"] = feature.getStyle();
+			feature["status"] = "original";
 		});
 		
 		var features = Array.from(kml_source.getFeatures());
 		// Sort projects
 		features.sort((a, b) => a.project.localeCompare(b.project));
-		features_by_project = groupBy(features, "project");
+		features_by_project = utils.groupBy(features, "project");
 		// Sort CityGMLs inside each project
 		Object.values(features_by_project).forEach(features => features.sort((a, b) => a.name.localeCompare(b.name)));
 	}
@@ -111,7 +92,7 @@ var regionChooser = (function(){
 	// but to a feature overlay which holds a collection of features.
 	// This collection is passed to the modify and also the draw
 	// interaction, so that both can add or modify features.
-	var featureOverlay = new ol.FeatureOverlay({
+	var drawnLayer = new ol.FeatureOverlay({
 		style : new ol.style.Style({
 			fill : new ol.style.Fill({
 				color : 'rgba(255, 155, 51, 0.5)'
@@ -128,11 +109,9 @@ var regionChooser = (function(){
 			})
 		})
 	});
-	featureOverlay.setMap(map);
+	drawnLayer.setMap(map);
 
-	//TODO: Rename to Javascript naming convention (CamelCase).
-	var selected_features = featureOverlay.getFeatures();
-	selected_features.on('add', function(event) {
+	drawnLayer.getFeatures().on('add', function(event) {
 		var feature = event.element;
 		feature.on("change", function() {
 			displayInfo();
@@ -140,7 +119,7 @@ var regionChooser = (function(){
 	});
 
 	var modify = new ol.interaction.Modify({
-		features : featureOverlay.getFeatures(),
+		features : drawnLayer.getFeatures(),
 		// the SHIFT key must be pressed to delete vertices, so
 		// that new vertices can be drawn at the same position
 		// of existing vertices
@@ -151,7 +130,7 @@ var regionChooser = (function(){
 	map.addInteraction(modify);
 
 	var draw = new ol.interaction.Draw({
-		features : featureOverlay.getFeatures(),
+		features : drawnLayer.getFeatures(),
 		type : 'Polygon'
 	});
 	map.addInteraction(draw);
@@ -169,39 +148,62 @@ var regionChooser = (function(){
 		var intersectionArea = intersection.getGeometry().getArea();
 		var citygml_percentage = Math.round(intersectionArea / feature["area"] * 100);
 		var sketch_percentage = Math.round(intersectionArea / polygonArea * 100);
-		var id = feature.getId();
 		intersections.addFeature(intersection);
-		var link = '<li onmouseover="regionChooser.highlightPolygon(' + id + ')" onmouseout="regionChooser.resetHighlight(' + id +')">';
-		link += '<input type="checkbox" id="citygml_' + feature.getId() + '" class="select_citygml" onclick="regionChooser.isDownloadPossible();">'
-		 	+ '<label for="citygml_' + feature.getId() + '">' + feature['name'] + '</label>';
-
-		link += " (" + citygml_percentage + "%";
+		
+		li = document.createElement('li');
+		li.feature = feature;
+		
+		li.onmouseover = function(){ regionChooser.highlightPolygon(this.feature) };
+		li.onmouseout = function(){ regionChooser.resetHighlight(this.feature) };
+		
+		let label = li.appendChild(document.createElement('label'));
+		var text = feature.name;
+		
+		let checkbox = document.createElement('input');
+		checkbox.type = 'checkbox'
+		checkbox.className = "select_citygml";
+		checkbox.feature = feature;
+		checkbox.setAttribute('onclick', "regionChooser.isDownloadPossible()");
+		
+		text += " (" + citygml_percentage + "%";
 		if (sketch_percentage == 100) {
-			link += ", all inside";
+			text += ", all inside";
 		}
-		dataPanel.append(link + ")\n");
+		
+		label.textContent = text + ")\n";
+		label.prepend(checkbox);
+		
+		// append to DOM element, not to jQuery object
+		dataPanel[0].appendChild(li);
+	}
+	
+	publicScope.highlightPolygon = function(feature) {
+		feature.setStyle(styles.highlighted);
 	}
 	
-	publicScope.highlightPolygon = function(i) {
-		var feature = kml_source.getFeatureById(i);
-		feature.setStyle(polygon_style("#ff44a2", 0.7));
+	publicScope.resetHighlight = function(feature) {
+		refreshStyle(feature);
 	}
 	
-	publicScope.resetHighlight = function(i) {
-		var feature = kml_source.getFeatureById(i);
-		feature.setStyle(feature.originalStyle);
+	refreshStyle = function(feature, status){
+		if (status){
+			feature.status = status;
+		}
+		feature.setStyle(styles[feature.status]);
 	}
 	
 	publicScope.isDownloadPossible = function(){
-		kml_source.getFeatures().forEach(f => f.setStyle(f.originalStyle));
+		kml_source.getFeatures().forEach(f => refreshStyle(f, "original"));
 		
-		//TODO: Dry
-		var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked);
-		var checkbox_ids = checkedBoxes.map(c => c.id);
-		var features = getCheckedPolygons(checkbox_ids);
-		features.forEach(f => f.setStyle(polygon_style("#ffff00", 0.8)));
+		selectedFeatures = getSelectedGMLs();
 		
-		document.getElementById("download_region_button").disabled = (checkedBoxes.length == 0);
+		selectedFeatures.forEach(f => refreshStyle(f, "selected"));
+		
+		document.getElementById("download_region_button").disabled = (selectedFeatures.length == 0);
+	}
+	
+	function getSelectedGMLs(){
+		return Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked).map(c => c.feature);
 	}
 	
 	function findIntersection(feature, polygon) {
@@ -241,16 +243,28 @@ var regionChooser = (function(){
 		dataPanel.append(text + "<br/>\n");
 	}
 	
-	getCheckedPolygons = function(checkbox_ids){
-		return checkbox_ids.map(checkbox_id => {
-			var i = Number(checkbox_id.replace("citygml_", ""));
-			return kml_source.getFeatureById(i);
-		})
+	publicScope.downloadStart = function(){
+		document.getElementById("download_region_button").disabled = true;
+		document.documentElement.className = 'wait';
+		dataPanel.prepend("<h2 id='download_start' class='ok'>Starting to extract region...</h2><br/>\n");
 	}
 	
-	publicScope.downloadRegionFromCityGMLs = function(checkbox_ids) {
-		var features = getCheckedPolygons(checkbox_ids);
-		
+	publicScope.downloadFinished = function(count){
+		document.documentElement.className = ''; // Stop waiting
+		document.getElementById("download_start").remove();
+		if (count > 0){
+			dataPanel.prepend("<h2 class='ok'>Done! (" + count + " buildings found) </h2><br/>\n");
+		} else {
+			dataPanel.prepend("<h2 class='error'>No building has been found in this region</h2><br/>\n");
+		}
+		var button = document.getElementById("download_region_button");
+		if (button){ // Region might have been modified since download start
+			button.disabled = false;
+		}
+	}
+	
+	publicScope.downloadFromSelectedCityGMLs = function() {
+		var features = getSelectedGMLs();
 		var project = features[0].get("project");
 		var srsName = features[0].get("srsName");
 		
@@ -263,38 +277,15 @@ var regionChooser = (function(){
 			dataPanel.prepend("<h2 class='error'>Sorry, the CityGML files should all be written with the same coordinate system.</h2><br/>\n");
 		}
 		
-		document.documentElement.className = 'wait';
-		
 		var citygmlNames = features.map(f => f.get("name"));
-		// Waiting 100ms in order to let the cursor change
-		setTimeout(function() {
-			var start = new Date().getTime();
-			if (proj4.defs(srsName)){
-				console.log("Selected region is written in " + srsName + " coordinate system.");
-				try {
-					var count = fxapp.downloadRegionFromCityGMLs(sketchAsWKT(srsName), project, citygmlNames.join(";"), srsName);
-					if (count == -1){
-						console.log("No output file has been selected.");
-					} else {
-						dataPanel.prepend("<h2 class='ok'>Done! (" + count + " buildings found) </h2><br/>\n");
-					}
-				} catch (e) {
-					console.warn("ERROR : " + e);
-					dataPanel.prepend("<h2 class='error'>Some problem occured!</h2><br/>\n");
-				}
-				var end = new Date().getTime();
-				var time = end - start;
-				console.log('Download Execution time: ' + (time / 1000).toFixed(3) + 's');
-				setTimeout(function() {
-					document.getElementById("download_region_button").disabled = false;
-					document.documentElement.className = ''; // Stop waiting
-				}, 100);
-			} else {
-				var msg = "ERROR : Unknown coordinate system : \"" + srsName + "\". Cannot extract any region";
-				console.log(msg);
-				dataPanel.append(msg + "<br/>\n");
-			}
-		}, 100);
+		if (proj4.defs(srsName)){
+			console.log("Selected region is written in " + srsName + " coordinate system.");
+			fxapp.downloadRegionFromCityGMLs(sketchAsWKT(srsName), project, citygmlNames.join(";"), srsName);
+		} else {
+			var msg = "ERROR : Unknown coordinate system : \"" + srsName + "\". Cannot extract any region";
+			console.log(msg);
+			dataPanel.append(msg + "<br/>\n");
+		}
 	}
 
 	function displayInfo() {
@@ -306,8 +297,8 @@ var regionChooser = (function(){
 		dataPanel.append("<h3 class='clean'>Area : " + (area / 10000).toFixed(1) + " ha\n");
 		dataPanel.append('<div style="visibility:hidden" id="download_region">' + 
 			'<button type="button" onclick="regionChooser.downloadFromSelectedCityGMLs()" id="download_region_button" disabled>Download Region</button><br/>\n' +
-			'<a href="#" onclick="regionChooser.checkCityGMLS(true);">(Select All)</a>\n' +
-			'<a href="#" onclick="regionChooser.checkCityGMLS(false);">(Select None)</a>\n'+
+			'<a href="#" onclick="regionChooser.selectAllOrNone(true);">(Select All)</a>\n' +
+			'<a href="#" onclick="regionChooser.selectAllOrNone(false);">(Select None)</a>\n'+
 			'</div>\n');
 		findIntersections();
 		dataPanel.append('<button type="button" onclick="regionChooser.copyCoordinatesToClipboard()" id="get_wgs84">Copy coordinates</button><br/>\n')
@@ -338,8 +329,9 @@ var regionChooser = (function(){
 		} finally {
 			displayHelp();
 			document.documentElement.className = ''; // Stop waiting
+			kml_source.getFeatures().forEach(f => refreshStyle(f, "original"));
 			draw.setActive(true);
-			featureOverlay.getFeatures().clear();
+			drawnLayer.getFeatures().clear();
 			intersections.clear();
 			focusOnMap();
 		}
@@ -370,13 +362,6 @@ var regionChooser = (function(){
 		}
 	}
 	
-	groupBy = function(xs, key) {
-		  return xs.reduce(function(rv, x) {
-			(rv[x[key]] = rv[x[key]] || []).push(x);
-			return rv;
-		  }, {});
-		};
-	
 	function displayHelp(){
 		dataPanel.empty();
 		dataPanel.append("<h2 class='info'>Welcome to Region Chooser!<br><br>\n");
@@ -400,20 +385,7 @@ var regionChooser = (function(){
 		console.log("Ready!");
 	}
 	
-	publicScope.downloadFromSelectedCityGMLs = function() {
-		document.getElementById("download_region_button").disabled = true;
-		var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked);
-		// CheckBoxes isn't empty, because otherwise the button cannot be clicked.
-		
-		var checkbox_ids = checkedBoxes.map(c => c.id);
-		var features = getCheckedPolygons(checkbox_ids);
-		
-		features.forEach(f => f.setStyle(polygon_style("#ffff00", 0.8)));
-
-		publicScope.downloadRegionFromCityGMLs(checkedBoxes.map(c => c.id));
-	}
-	
-	publicScope.checkCityGMLS = function(allOrNone) {
+	publicScope.selectAllOrNone = function(allOrNone) {
 		document.querySelectorAll("input.select_citygml").forEach(c => c.checked = allOrNone);
 		publicScope.isDownloadPossible();
 	}
@@ -427,42 +399,8 @@ var regionChooser = (function(){
 		var wgs84Coords = geom.getLinearRing(0).getCoordinates();
 		var wktPolygon = "POLYGON((";
 		wktPolygon += wgs84Coords.map(lonLat => lonLat.join(" ")).join(", ");
-		publicScope.copyToClipboard(wktPolygon + "))");
-	}
-	
-	// Copies a string to the clipboard. Must be called from within an
-	// event handler such as click. May return false if it failed, but
-	// this is not always possible. Browser support for Chrome 43+,
-	// Firefox 42+, Safari 10+, Edge and Internet Explorer 10+.
-	// Internet Explorer: The clipboard feature may be disabled by
-	// an administrator. By default a prompt is shown the first
-	// time the clipboard is used (per session).
-	// https://stackoverflow.com/a/33928558/6419007
-	publicScope.copyToClipboard = function(text) {
-	if (window.clipboardData && window.clipboardData.setData) {
-		// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
-		return window.clipboardData.setData("Text", text);
-	}
-	else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
-		var textarea = document.createElement("textarea");
-		textarea.textContent = text;
-		textarea.style.position = "fixed";  // Prevent scrolling to bottom of page in Microsoft Edge.
-		document.body.appendChild(textarea);
-		textarea.select();
-		try {
-			document.execCommand("copy");  // Security exception may be thrown by some browsers.
-			dataPanel.append("<h2 class='ok'>Coordinates copied to clipboard!</h2><br/>\n");
-			return;
-		}
-		catch (ex) {
-			console.warn("Copy to clipboard failed.", ex);
-			return prompt("Copy to clipboard: Ctrl+C, Enter", text);
-		}
-		finally {
-			document.body.removeChild(textarea);
-		}
+		utils.copyToClipboard(wktPolygon + "))", dataPanel);
 	}
-}
 	
 	publicScope.showRepositoryName = function(path) {
 		document.getElementById("repo_path").textContent = path;
diff --git a/src/main/resources/eu/simstadt/regionchooser/website/script/utils.js b/src/main/resources/eu/simstadt/regionchooser/website/script/utils.js
new file mode 100644
index 0000000000000000000000000000000000000000..2a4fa6692d7b8496f56d68979430fa49d1f70e11
--- /dev/null
+++ b/src/main/resources/eu/simstadt/regionchooser/website/script/utils.js
@@ -0,0 +1,64 @@
+var utils = {};
+
+utils.groupBy = function(xs, key) {
+	return xs.reduce(function(rv, x) {
+	(rv[x[key]] = rv[x[key]] || []).push(x);
+	return rv;
+	}, {});
+}
+	
+// Copies a string to the clipboard. Must be called from within an
+// event handler such as click. May return false if it failed, but
+// this is not always possible. Browser support for Chrome 43+,
+// Firefox 42+, Safari 10+, Edge and Internet Explorer 10+.
+// Internet Explorer: The clipboard feature may be disabled by
+// an administrator. By default a prompt is shown the first
+// time the clipboard is used (per session).
+// https://stackoverflow.com/a/33928558/6419007
+utils.copyToClipboard = function(text, log) {
+	if (window.clipboardData && window.clipboardData.setData) {
+		// Internet Explorer-specific code path to prevent textarea being shown while dialog is visible.
+		return window.clipboardData.setData("Text", text);
+	}
+	else if (document.queryCommandSupported && document.queryCommandSupported("copy")) {
+		var textarea = document.createElement("textarea");
+		textarea.textContent = text;
+		textarea.style.position = "fixed";  // Prevent scrolling to bottom of page in Microsoft Edge.
+		document.body.appendChild(textarea);
+		textarea.select();
+		try {
+			document.execCommand("copy");  // Security exception may be thrown by some browsers.
+			log.append("<h2 class='ok'>Coordinates copied to clipboard!</h2><br/>\n");
+			return;
+		}
+		catch (ex) {
+			console.warn("Copy to clipboard failed.", ex);
+			return prompt("Copy to clipboard: Ctrl+C, Enter", text);
+		}
+		finally {
+			document.body.removeChild(textarea);
+		}
+	}
+}
+	
+utils.read_kml = function(url){
+	return new ol.source.KML({
+		projection : ol.proj.get('EPSG:3857'),
+		url : url,
+		extractAttributes : false,
+		extractStyles : false
+	});
+}
+	
+utils.polygon_style = function(color, alpha) {
+	return new ol.style.Style({
+		fill : new ol.style.Fill({
+			color : 'rgba(255, 255, 255,' + alpha + ')'
+		}),
+		stroke : new ol.style.Stroke({
+			color : color,
+			width : 2,
+			lineDash : [ 5, 10 ]
+		}),
+	});
+}