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 ] + }), + }); +}