Commit 301ee804 authored by Eric Duminil's avatar Eric Duminil
Browse files

Merge branch 'develop'

parents d818e19f 6bd788bb
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
......
......@@ -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>
......
......@@ -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();
}
......
......@@ -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;
......
......@@ -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>
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;
......
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({