RegionChooserBrowser.java 7.03 KB
Newer Older
eric.duminil's avatar
eric.duminil committed
1
2
package eu.simstadt.regionchooser;

Eric Duminil's avatar
Eric Duminil committed
3
import java.io.BufferedWriter;
eric.duminil's avatar
eric.duminil committed
4
5
import java.io.File;
import java.io.IOException;
Eric Duminil's avatar
Eric Duminil committed
6
import java.nio.file.Files;
eric.duminil's avatar
eric.duminil committed
7
8
import java.nio.file.Path;
import java.nio.file.Paths;
Eric Duminil's avatar
Eric Duminil committed
9
import java.util.logging.Logger;
eric.duminil's avatar
eric.duminil committed
10
import java.util.prefs.Preferences;
11
import java.util.stream.Stream;
12
import org.locationtech.jts.io.ParseException;
eric.duminil's avatar
eric.duminil committed
13
14
import com.ximpleware.NavException;
import com.ximpleware.XPathParseException;
15
import eu.simstadt.regionchooser.fast_xml_parser.ConvexHullCalculator;
16
import javafx.application.Platform;
eric.duminil's avatar
eric.duminil committed
17
18
19
20
21
22
23
24
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;
25
import javafx.stage.DirectoryChooser;
eric.duminil's avatar
eric.duminil committed
26
27
28
29
30
31
32
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import netscape.javascript.JSObject;


public class RegionChooserBrowser extends Region
{
Eric Duminil's avatar
Eric Duminil committed
33
	private static final Logger LOGGER = Logger.getLogger(RegionChooserBrowser.class.getName());
34
	private static final String PREF_RECENT_REPOSITORY = "RECENT_REPOSITORY";
Eric Duminil's avatar
Eric Duminil committed
35

eric.duminil's avatar
eric.duminil committed
36
37
38
39
40
41
42
43
44
	/**
	 * 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");
45
46
			String repoString = userPrefs.get(PREF_RECENT_REPOSITORY, null);
			repo = repoString == null? null : Paths.get(repoString);
eric.duminil's avatar
eric.duminil committed
47
48
		}

49
50
51
52
53
		/**
		 * 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() {
54
55
56
			if (repo == null || !Files.exists(repo)) {
				selectRepository();
			}
57
58
59
60
			Task<Void> task = new Task<Void>() {
				@Override
				public Void call() throws IOException {
					ConvexHullCalculator.extractHullsForEveryCityGML(repo,
Eric Duminil's avatar
Eric Duminil committed
61
							hullKML -> Platform.runLater(() -> jsApp.call("addCityGmlHull", hullKML)));
62
63
64
65
					return null;
				}
			};

66
67
			task.setOnRunning(e -> {
				jsApp.call("display", "Importing citgyml. Please wait.");
68
69
70
				if (repo != null) {
					jsApp.call("showRepositoryName", repo.getFileName().toString());
				}
71
72
				jsApp.call("init");
			});
73

Eric Duminil's avatar
Eric Duminil committed
74
			task.setOnSucceeded(e -> jsApp.call("ready"));
75
76

			new Thread(task).start();
77
78
		}

Eric Duminil's avatar
Eric Duminil committed
79
		/**
80
81
		 * This method is called from Javascript, with a prepared wktPolygon written in local coordinates. Executes it in
		 * the background to avoid freezing the GUI
Eric Duminil's avatar
Eric Duminil committed
82
		 */
83
		public void downloadRegionFromCityGMLs(String wktPolygon, String project, String csvCitygmls, String srsName) {
Eric Duminil's avatar
Eric Duminil committed
84
			// It doesn't seem possible to pass arrays or list from JS to Java. So csvCitygmls contains names separated by ;
85
			Path[] paths = Stream.of(csvCitygmls.split(";")).map(s -> citygmlPath(project, s)).toArray(Path[]::new);
Eric Duminil's avatar
Eric Duminil committed
86
87
			String proposedName = csvCitygmls.replace(";", "_").replace(".gml", "") + ".gml";
			File outputFile = selectSaveFileWithDialog(project, proposedName, "part");
Eric Duminil's avatar
Eric Duminil committed
88
			if (outputFile == null) {
89
				return;
Eric Duminil's avatar
Eric Duminil committed
90
91
			}

92
93
94
95
96
97
98
99
100
101
102
103
104
105
			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"));

106
			downloadTask.setOnSucceeded(e -> jsApp.call("downloadFinished", e.getSource().getValue()));
107
108

			new Thread(downloadTask).start();
Eric Duminil's avatar
Eric Duminil committed
109
110
		}

eric.duminil's avatar
eric.duminil committed
111

112
113
114
115
116
117
		public void selectRepository() {
			Preferences userPrefs = Preferences.userRoot().node("/eu/simstadt/desktop");

			DirectoryChooser fileChooser = new DirectoryChooser();
			Stage mainStage = (Stage) RegionChooserBrowser.this.getScene().getWindow();
			fileChooser.setTitle("Select Repository");
118
119
120
			if (repo != null && Files.exists(repo)) {
				fileChooser.setInitialDirectory(repo.toFile());
			}
121
122
123
124
125
126
127
128
129
130
131
132
			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.");
			}
		}

eric.duminil's avatar
eric.duminil committed
133
134
135
136
		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");
Eric Duminil's avatar
Eric Duminil committed
137
			if (project != null && !isRepoAProject()) {
eric.duminil's avatar
eric.duminil committed
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
				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) {
Eric Duminil's avatar
Eric Duminil committed
153
			LOGGER.info(text);
eric.duminil's avatar
eric.duminil committed
154
155
		}

156
157
158
159
		public void warning(String text) {
			LOGGER.warning(text);
		}

Eric Duminil's avatar
Eric Duminil committed
160
161
162
163
164
165
166
167
168
		/*
		 * NOTE: Users sometime select some_region.proj/ as a repository.
		 * SimStadt won't show any project in this "repository", but RegionChooser shouldn't complain and
		 * still be able to extract regions.
		 */
		private boolean isRepoAProject(){
			return repo.toString().endsWith(".proj");
		}

eric.duminil's avatar
eric.duminil committed
169
		private Path citygmlPath(String project, String citygml) {
Eric Duminil's avatar
Eric Duminil committed
170
171
172
173
174
			if (isRepoAProject()){
				return repo.resolve(citygml);
			} else {
				return repo.resolve(project + ".proj").resolve(citygml);
			}
eric.duminil's avatar
eric.duminil committed
175
176
177
178
179
180
		}

	}

	final WebView browser = new WebView();
	final WebEngine webEngine = browser.getEngine();
181
	final JavaScriptFXBridge fxapp = new JavaScriptFXBridge();
182
	private JSObject jsApp;
eric.duminil's avatar
eric.duminil committed
183
184
185
186
187
188
189
190
191
192

	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<? extends State> ov, State oldState, State newState) -> {
					if (newState == State.SUCCEEDED) {
193
194
						jsApp = (JSObject) webEngine.executeScript("regionChooser");
						jsApp.call("setFxApp", fxapp);
195
						fxapp.refreshHulls();
eric.duminil's avatar
eric.duminil committed
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
					}
				});
		//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) {
211
		return 1024;
eric.duminil's avatar
eric.duminil committed
212
213
214
215
	}

	@Override
	protected double computePrefHeight(double width) {
216
		return 720;
eric.duminil's avatar
eric.duminil committed
217
218
	}
}