simstadt_openlayers.js 14.9 KB
Newer Older
Eric Duminil's avatar
Eric Duminil committed
1
var regionChooser = (function(){
Eric Duminil's avatar
Eric Duminil committed
2
	//TODO: Somehow split in classes. This file is getting too big and mixed
3
	var publicScope = {};
4
	var fromJavaFX = navigator.userAgent.indexOf('JavaFX') !== -1;
5
6
	var dataPanel = $('#dataPanel');
	var wgs84Sphere = new ol.Sphere(6378137);
Eric Duminil's avatar
Eric Duminil committed
7
	var features_by_project;
8
9
10
11
12
13
	var gmlId;
	
	publicScope.init = function(){
		//NOTE: Only called from JavaFX. At startup, or when Repo has been changed.
		gmlId = 0;
		kml_source.clear();
Eric Duminil's avatar
Eric Duminil committed
14
		document.getElementById("select_repository").style.visibility = "visible";
15
	}
16
17
	
	if (fromJavaFX){
Eric Duminil's avatar
Eric Duminil committed
18
		document.documentElement.className = 'wait';
19
	}
20
21
22

	var osm_layer = new ol.layer.Tile({
		source: new ol.source.OSM()
eric.duminil's avatar
eric.duminil committed
23
24
	});

25
26
27
28
29
30
31
32
33
	function read_kml(url){
		return new ol.source.KML({
			projection : ol.proj.get('EPSG:3857'),
			url : url,
			extractAttributes : false,
			extractStyles : false
		});
	}
	
34
	var kml_source = read_kml(fromJavaFX ? undefined : 'data/citygml_hulls.kml');
eric.duminil's avatar
eric.duminil committed
35

36
37
38
39
40
41
42
43
44
45
46
47
	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 ]
			}),
		});
	}
eric.duminil's avatar
eric.duminil committed
48

49
50
	var kml_layer = new ol.layer.Vector({
		source : kml_source,
51
		style : polygon_style('#447744', 0.2)
52
53
54
55
56
57
58
59
60
61
	});

	var intersections = new ol.source.Vector();

	var intersections_layer = new ol.layer.Vector({
		source : intersections,
		style : new ol.style.Style({
			fill : new ol.style.Fill({
				color : 'rgba(255, 155, 51, 0.2)'
			})
eric.duminil's avatar
eric.duminil committed
62
63
		})
	});
64

65
	publicScope.addCityGmlHull = function(kmlString) {
66
		options = {featureProjection: ol.proj.get('EPSG:3857')};
67
		feature = kmlFormat.readFeature(kmlString, options);
68
		feature.setId(gmlId++);
69
70
		kml_source.addFeature(feature);
		dataPanel.append('.');
71
		srsName = feature.get("srsName");
72
		if (proj4.defs(srsName) === undefined){
Eric Duminil's avatar
Eric Duminil committed
73
			console.warn(srsName + " isn't defined by Proj4js!")
74
		}
75
76
	};
	
77
78
	var map = new ol.Map({
		target : 'map',
79
		layers : [ osm_layer, kml_layer, intersections_layer ],
80
81
82
83
84
		interactions : ol.interaction.defaults({
			keyboard : true
		})
	});

85
	var geoJsonFormat = new ol.format.GeoJSON();
86
	var kmlFormat = new ol.format.KML({extractStyles: false});
87
88
89
90
91
92
93

	kml_layer.addEventListener("change", function() {
		map.getView().fitExtent(kml_source.getExtent(), (map.getSize()));
	});

	function updateGMLPolygons() {
		kml_source.forEachFeature(function(feature) {
94
			feature["geoJSON"] = geoJsonFormat.writeFeatureObject(feature);
95
			feature["area"] = feature.getGeometry().getArea();
Eric Duminil's avatar
Eric Duminil committed
96
97
			feature["project"] = feature.get("project");
			feature["name"] = feature.get("name");
98
99
			feature["source"] = "CityGML";
		});
Eric Duminil's avatar
Eric Duminil committed
100
		
Eric Duminil's avatar
Eric Duminil committed
101
102
103
104
		var features = Array.from(kml_source.getFeatures());
		// Sort projects
		features.sort((a, b) => a.project.localeCompare(b.project));
		features_by_project = groupBy(features, "project");
Eric Duminil's avatar
Eric Duminil committed
105
106
		// Sort CityGMLs inside each project
		Object.values(features_by_project).forEach(features => features.sort((a, b) => a.name.localeCompare(b.name)));
107
108
109
110
111
112
113
114
	}

	// The features are not added to a regular vector layer/source,
	// 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({
		style : new ol.style.Style({
eric.duminil's avatar
eric.duminil committed
115
			fill : new ol.style.Fill({
116
117
118
119
120
121
122
123
124
125
126
				color : 'rgba(255, 155, 51, 0.5)'
			}),
			stroke : new ol.style.Stroke({
				color : '#ffcc33',
				width : 4
			}),
			image : new ol.style.Circle({
				radius : 5,
				fill : new ol.style.Fill({
					color : '#ffcc33'
				})
eric.duminil's avatar
eric.duminil committed
127
128
129
			})
		})
	});
130
131
	featureOverlay.setMap(map);

Eric Duminil's avatar
Eric Duminil committed
132
	//TODO: Rename to Javascript naming convention (CamelCase).
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
	var selected_features = featureOverlay.getFeatures();
	selected_features.on('add', function(event) {
		var feature = event.element;
		feature.on("change", function() {
			displayInfo();
		});
	});

	var modify = new ol.interaction.Modify({
		features : featureOverlay.getFeatures(),
		// the SHIFT key must be pressed to delete vertices, so
		// that new vertices can be drawn at the same position
		// of existing vertices
		deleteCondition : function(event) {
			return ol.events.condition.shiftKeyOnly(event) && ol.events.condition.singleClick(event);
		}
	});
	map.addInteraction(modify);

	var draw = new ol.interaction.Draw({
		features : featureOverlay.getFeatures(),
		type : 'Polygon'
	});
	map.addInteraction(draw);

	var sketch;
eric.duminil's avatar
eric.duminil committed
159

160
161
162
163
164
	draw.on('drawstart', function(evt) {
		sketch = evt.feature;
		updateGMLPolygons();
	});
	var sourceProj = map.getView().getProjection();
165
166
167
168
169
170
171
172
	
	function showLinkToDownload(feature, jsonIntersection, polygonArea){
		var intersection = geoJsonFormat.readFeature(jsonIntersection);
		var intersectionArea = intersection.getGeometry().getArea();
		var citygml_percentage = Math.round(intersectionArea / feature["area"] * 100);
		var sketch_percentage = Math.round(intersectionArea / polygonArea * 100);
		intersections.addFeature(intersection);
		var link = '<li>'
173
		// TODO: If possible, highlight the corresponding polygon when hovering above a name.
174
		link += '<input type="checkbox" id="citygml_' + feature.getId() + '" class="select_citygml" onclick="regionChooser.isDownloadPossible();"><label for="citygml_' + feature.getId() + '">' +  feature['name'] + '</label>';
175
176
177
178
179
180
181
182

		link += " (" + citygml_percentage + "%";
		if (sketch_percentage == 100) {
			link += ", all inside";
		}
		dataPanel.append(link + ")\n");
	}
	
183
	publicScope.isDownloadPossible = function(){
184
185
		var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked);
		document.getElementById("download_region_button").disabled = (checkedBoxes.length == 0);
Eric Duminil's avatar
Eric Duminil committed
186
187
	}
	
188
	function findIntersection(feature, polygon) {
189
		try {
190
			return turf.intersect(polygon, feature["geoJSON"]);
191
192
193
194
		} catch (err) {
			console.log(feature.get('name') + " - " + err);
		}
	}
195
196

	function findIntersections() {
197
198
		var polygon = geoJsonFormat.writeFeatureObject(sketch);
		var polygonArea = sketch.getGeometry().getArea();
199
200
		var intersection_found = false;
		intersections.clear();
201
		
202
		Object.keys(features_by_project).forEach(function(project) {
203
204
205
206
			features = features_by_project[project];
			features_and_intersections = features.map(f=> [f, findIntersection(f,polygon)]).filter(l => l[1] !== undefined);
			if (features_and_intersections.length > 0){
				intersection_found = true;
207
				dataPanel.append("<h2 class='info'>" + project);
208
209
				features_and_intersections.forEach(l => showLinkToDownload(l[0], l[1], polygonArea));
			}
210
		});
211
		
212
		if (intersection_found) {
213
			document.getElementById("download_region").style.visibility = 'visible';
214
215
		}
		else {
216
			document.getElementById("download_region").style.visibility = 'hidden';
217
			dataPanel.append("No intersection found with any CityGML file.<br/>\n");
eric.duminil's avatar
eric.duminil committed
218
219
		}
	}
220

221
222
223
224
	publicScope.display = function(text){
		dataPanel.append(text + "<br/>\n");
	}
	
225
	publicScope.downloadRegionFromCityGMLs = function(checkbox_ids) {
226
227
228
229
230
		var features = checkbox_ids.map(checkbox_id => {
			var i = Number(checkbox_id.replace("citygml_", ""));
			return kml_source.getFeatureById(i);
		})
		
Eric Duminil's avatar
Eric Duminil committed
231
232
233
234
		var project = features[0].get("project");
		var srsName = features[0].get("srsName");
		
		if (!features.every( f => f.get("project") === project)){
235
			dataPanel.prepend("<h2 class='error'>Sorry, the CityGML files should all belong to the same project.</h2><br/>\n");
Eric Duminil's avatar
Eric Duminil committed
236
237
238
239
			return;
		}
		
		if (!features.every( f => f.get("srsName") === srsName)){
240
			dataPanel.prepend("<h2 class='error'>Sorry, the CityGML files should all be written with the same coordinate system.</h2><br/>\n");
Eric Duminil's avatar
Eric Duminil committed
241
242
		}
		
Eric Duminil's avatar
Eric Duminil committed
243
244
		document.documentElement.className = 'wait';
		
245
		var citygmlNames = features.map(f => f.get("name"));
246
247
248
249
250
		// 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.");
251
				try {
Eric Duminil's avatar
Eric Duminil committed
252
					var count = fxapp.downloadRegionFromCityGMLs(sketchAsWKT(srsName), project, citygmlNames.join(";"), srsName);
Eric Duminil's avatar
Eric Duminil committed
253
254
255
256
257
					if (count == -1){
						console.log("No output file has been selected.");
					} else {
						dataPanel.prepend("<h2 class='ok'>Done! (" + count + " buildings found) </h2><br/>\n");
					}
258
				} catch (e) {
Eric Duminil's avatar
Eric Duminil committed
259
					console.warn("ERROR : " + e);
260
					dataPanel.prepend("<h2 class='error'>Some problem occured!</h2><br/>\n");
261
				}
262
263
				var end = new Date().getTime();
				var time = end - start;
Eric Duminil's avatar
Eric Duminil committed
264
				console.log('Download Execution time: ' + (time / 1000).toFixed(3) + 's');
265
				setTimeout(function() {
266
					document.getElementById("download_region_button").disabled = false;
Eric Duminil's avatar
Eric Duminil committed
267
					document.documentElement.className = ''; // Stop waiting
268
269
270
271
272
273
274
				}, 100);
			} else {
				var msg = "ERROR : Unknown coordinate system : \"" + srsName + "\". Cannot extract any region";
				console.log(msg);
				dataPanel.append(msg + "<br/>\n");
			}
		}, 100);
eric.duminil's avatar
eric.duminil committed
275
276
	}

277
278
	function displayInfo() {
		dataPanel.empty();
279
		var geom = sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326');
280
281
		var coordinates = geom.getLinearRing(0).getCoordinates();
		var area = Math.abs(wgs84Sphere.geodesicArea(coordinates));
282
		//NOTE: Could show m², ha or km² depending on magnitude
283
		dataPanel.append("<h3 class='clean'>Area : " + (area / 10000).toFixed(1) + " ha\n");
284
285
286
287
		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'+
Eric Duminil's avatar
Eric Duminil committed
288
			'</div>\n');
289
		findIntersections();
Eric Duminil's avatar
Eric Duminil committed
290
		dataPanel.append('<button type="button" onclick="regionChooser.copyCoordinatesToClipboard()" id="get_wgs84">Copy coordinates</button><br/>\n')
eric.duminil's avatar
eric.duminil committed
291
	}
292
293
294
295
296

	draw.on('drawend', function() {
		displayInfo();
		draw.setActive(false);
	});
297
298
299
	
	// With OpenLayers 3.9, draw_interaction.removeLastPoint(); might be better.
	document.addEventListener('keydown', function(e) {
Eric Duminil's avatar
Eric Duminil committed
300
301
		//NOTE: e.key isn't defined in JavaFX Browser
		if (e.which == 27 || e.which == 46){ // ESCAPE or DELETE.
302
303
			resetDrawing();
		}
Eric Duminil's avatar
Eric Duminil committed
304
305
306
307
308
		if (e.which == 116 && fromJavaFX){ // F5 for refresh
			dataPanel.prepend("<h2 class='ok'>Refreshing repository...</h2><br/>\n");
			document.documentElement.className = 'wait';
			fxapp.refreshHulls();
		}
309
	});
310

311
312
	function resetDrawing(){
		console.log("Reset drawing");
313
314
315
		try {
			draw.finishDrawing();
		} finally {
Eric Duminil's avatar
Eric Duminil committed
316
			displayHelp();
Eric Duminil's avatar
Eric Duminil committed
317
			document.documentElement.className = ''; // Stop waiting
318
319
320
321
322
			draw.setActive(true);
			featureOverlay.getFeatures().clear();
			intersections.clear();
			focusOnMap();
		}
323
	}
324
	
325
326
327
328
329
330
331
	function sketchAsWKT(srsName) {
		var wktFormat = new ol.format.WKT();
		return wktFormat.writeFeature(sketch, {
			dataProjection : ol.proj.get(srsName),
			featureProjection : ol.proj.get('EPSG:3857')
		});
	}
332
	
333
	function focusOnMap() {
Eric Duminil's avatar
Eric Duminil committed
334
		document.getElementById("map").focus();
335
336
	}
	
337
338
339
340
	var fxapp = undefined;
	
	publicScope.setFxApp = function(app){
		fxapp = app;
341
342
343
		console.log = function(message){
			fxapp.log(message);
		}
344
		
Eric Duminil's avatar
Eric Duminil committed
345
		console.warn = function(message){
346
347
			fxapp.warning(message);
		}
348
	}
Eric Duminil's avatar
Eric Duminil committed
349
	
350
351
352
353
354
355
356
	groupBy = function(xs, key) {
		  return xs.reduce(function(rv, x) {
			(rv[x[key]] = rv[x[key]] || []).push(x);
			return rv;
		  }, {});
		};
	
Eric Duminil's avatar
Eric Duminil committed
357
358
	function displayHelp(){
		dataPanel.empty();
359
		dataPanel.append("<h2 class='info'>Welcome to Region Chooser!<br><br>\n");
Eric Duminil's avatar
Eric Duminil committed
360
361
362
363
		dataPanel.append("You can draw a polygon on the map by clicking.<br>\n");
		dataPanel.append("You can add a new point to an existing edge by clicking and dragging.<br>\n");
		dataPanel.append("You can remove a point with SHIFT + clicking.<br>\n");
		dataPanel.append("You can cancel drawing with ESC or DEL.<br><br>\n");
364
365
366
		dataPanel.append("After drawing a polygon which intersects with at least one GML file,<br>\n");
		dataPanel.append("you can download the corresponding part by checking the<br>\n");
		dataPanel.append("desired filenames and clicking on 'Download' button.<br>\n");
Eric Duminil's avatar
Eric Duminil committed
367
368
369
		dataPanel.append("<br>\n");
		dataPanel.append("More info is available in the ");
		dataPanel.append("<a href='http://simstadt.hft-stuttgart.de/related-softwares/region-chooser/'>SimStadt documentation</a><br>\n");
Eric Duminil's avatar
Eric Duminil committed
370
	}
371

372
373
	// Executed by JavaFX when whole page is loaded.
	publicScope.ready = function() {
374
		updateGMLPolygons();
Eric Duminil's avatar
Eric Duminil committed
375
		displayHelp();
Eric Duminil's avatar
Eric Duminil committed
376
		document.documentElement.className = ''; // Stop waiting
Eric Duminil's avatar
Eric Duminil committed
377
		console.log("Ready!");
378
	}
379
	
380
	publicScope.downloadFromSelectedCityGMLs = function() {
Eric Duminil's avatar
Eric Duminil committed
381
		document.getElementById("download_region_button").disabled = true;
382
		var checkedBoxes = Array.from(document.querySelectorAll("input.select_citygml")).filter(c => c.checked);
Eric Duminil's avatar
Eric Duminil committed
383
384
		// CheckBoxes isn't empty, because otherwise the button cannot be clicked.
		publicScope.downloadRegionFromCityGMLs(checkedBoxes.map(c => c.id));
385
	}
386
	
Eric Duminil's avatar
Eric Duminil committed
387
388
	publicScope.checkCityGMLS = function(allOrNone) {
		document.querySelectorAll("input.select_citygml").forEach(c => c.checked = allOrNone);
389
		publicScope.isDownloadPossible();
Eric Duminil's avatar
Eric Duminil committed
390
391
	}
	
392
	publicScope.selectRepository = function() {
393
		fxapp.selectRepository();
394
	}
Eric Duminil's avatar
Eric Duminil committed
395
	
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
	publicScope.copyCoordinatesToClipboard = function(){
		var geom = sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326');
		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 {
Eric Duminil's avatar
Eric Duminil committed
424
425
426
			document.execCommand("copy");  // Security exception may be thrown by some browsers.
			dataPanel.append("<h2 class='ok'>Coordinates copied to clipboard!</h2><br/>\n");
			return;
427
428
		}
		catch (ex) {
Eric Duminil's avatar
Eric Duminil committed
429
			console.warn("Copy to clipboard failed.", ex);
430
431
432
433
434
435
436
437
			return prompt("Copy to clipboard: Ctrl+C, Enter", text);
		}
		finally {
			document.body.removeChild(textarea);
		}
	}
}
	
Eric Duminil's avatar
Eric Duminil committed
438
439
440
	publicScope.showRepositoryName = function(path) {
		document.getElementById("repo_path").textContent = path;
	}
441

442
	focusOnMap();
Eric Duminil's avatar
Eric Duminil committed
443
444
	//var regionChooser = publicScope; //NOTE: In order to open closure. For debugging
	return publicScope;
445
})();