package eu.simstadt.regionchooser; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.logging.Logger; import java.util.prefs.Preferences; import java.util.stream.Stream; import org.locationtech.jts.io.ParseException; import com.ximpleware.NavException; import com.ximpleware.XPathParseException; import eu.simstadt.regionchooser.fast_xml_parser.ConvexHullCalculator; import javafx.application.Platform; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.concurrent.Worker.State; import javafx.geometry.HPos; import javafx.geometry.VPos; import javafx.scene.layout.Region; import javafx.scene.web.WebEngine; import javafx.scene.web.WebView; import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; import netscape.javascript.JSObject; public class RegionChooserBrowser extends Region { private static final Logger LOGGER = Logger.getLogger(RegionChooserBrowser.class.getName()); private static final String PREF_RECENT_REPOSITORY = "RECENT_REPOSITORY"; /** * JavaFX Backend for RegionChooser. Inside simstadt_openlayers.js frontend, this class is available as `fxapp`. */ public class JavaScriptFXBridge { private Path repo; public JavaScriptFXBridge() { Preferences userPrefs = Preferences.userRoot().node("/eu/simstadt/desktop"); String repoString = userPrefs.get(PREF_RECENT_REPOSITORY, "../TestRepository"); repo = Paths.get(repoString); } /** * 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. */ public void refreshHulls() { Task task = new Task() { @Override public Void call() throws IOException { ConvexHullCalculator.extractHullsForEveryCityGML(repo, hullKML -> Platform.runLater(() -> jsApp.call("addCityGmlHull", hullKML))); return null; } }; task.setOnRunning(e -> { jsApp.call("display", "Importing citgyml. Please wait."); jsApp.call("showRepositoryName", repo.getFileName().toString()); jsApp.call("init"); }); task.setOnSucceeded(e -> jsApp.call("ready")); new Thread(task).start(); } /** * 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 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; } Task downloadTask = new Task() { @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(); } public void selectRepository() { Preferences userPrefs = Preferences.userRoot().node("/eu/simstadt/desktop"); String currentRepo = userPrefs.get(PREF_RECENT_REPOSITORY, "../TestRepository"); DirectoryChooser fileChooser = new DirectoryChooser(); Stage mainStage = (Stage) RegionChooserBrowser.this.getScene().getWindow(); fileChooser.setTitle("Select Repository"); fileChooser.setInitialDirectory(new File(currentRepo)); File repoLocation = fileChooser.showDialog(mainStage); if (repoLocation != null) { repo = repoLocation.toPath(); userPrefs.put(PREF_RECENT_REPOSITORY, repo.toAbsolutePath().toString()); LOGGER.info("Repository was set to " + repo); refreshHulls(); } else { LOGGER.warning("No repository chosen."); } } private File selectSaveFileWithDialog(String project, String citygml, String suffix) { Stage mainStage = (Stage) RegionChooserBrowser.this.getScene().getWindow(); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Save CITYGML ids"); if (project != null) { fileChooser.setInitialDirectory(repo.resolve(project + ".proj").toFile()); } else { fileChooser.setInitialDirectory(repo.toFile()); } if (suffix.isEmpty()) { fileChooser.setInitialFileName(citygml); } else { fileChooser.setInitialFileName(citygml.replace(".", "_" + suffix + ".")); } FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("GML files (*.gml)", "*.gml"); fileChooser.getExtensionFilters().add(extFilter); return fileChooser.showSaveDialog(mainStage); } public void log(String text) { LOGGER.info(text); } public void warning(String text) { LOGGER.warning(text); } private Path citygmlPath(String project, String citygml) { return repo.resolve(project + ".proj").resolve(citygml); } } final WebView browser = new WebView(); final WebEngine webEngine = browser.getEngine(); final JavaScriptFXBridge fxapp = new JavaScriptFXBridge(); private JSObject jsApp; public RegionChooserBrowser() { //apply the styles getStyleClass().add("browser"); String url = RegionChooserFX.class.getResource("website/index.html").toExternalForm(); webEngine.load(url); // load the web page // process page loading webEngine.getLoadWorker().stateProperty().addListener( (ObservableValue ov, State oldState, State newState) -> { if (newState == State.SUCCEEDED) { jsApp = (JSObject) webEngine.executeScript("regionChooser"); jsApp.call("setFxApp", fxapp); fxapp.refreshHulls(); } }); //add the web view to the scene getChildren().add(browser); } @Override protected void layoutChildren() { double w = getWidth(); double h = getHeight(); layoutInArea(browser, 0, 0, w, h, 0, HPos.CENTER, VPos.CENTER); } @Override protected double computePrefWidth(double height) { return 1024; } @Override protected double computePrefHeight(double width) { return 720; } }