simstadt_openlayers.js 14.8 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
101
102
103
104
		
		features_by_project = groupBy(kml_source.getFeatures(), "project");
		
		// Sort CityGMLs inside each project
		Object.values(features_by_project).forEach(features => features.sort((a, b) => a.name.localeCompare(b.name)));
105
106
107
108
109
110
111
112
	}

	// 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
113
			fill : new ol.style.Fill({
114
115
116
117
118
119
120
121
122
123
124
				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
125
126
127
			})
		})
	});
128
129
	featureOverlay.setMap(map);

Eric Duminil's avatar
Eric Duminil committed
130
	//TODO: Rename to Javascript naming convention (CamelCase).
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
	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
157

158
159
160
161
162
	draw.on('drawstart', function(evt) {
		sketch = evt.feature;
		updateGMLPolygons();
	});
	var sourceProj = map.getView().getProjection();
163
164
165
166
167
168
169
170
	
	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>'
171
		// TODO: If possible, highlight the corresponding polygon when hovering above a name.
172
		link += '<input type="checkbox" id="citygml_' + feature.getId() + '" class="select_citygml" onclick="regionChooser.isDownloadPossible();"><label for="citygml_' + feature.getId() + '">' +  feature['name'] + '</label>';
173
174
175
176
177
178
179
180

		link += " (" + citygml_percentage + "%";
		if (sketch_percentage == 100) {
			link += ", all inside";
		}
		dataPanel.append(link + ")\n");
	}
	
181
	publicScope.isDownloadPossible = function(){
182
183
		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
184
185
	}
	
186
	function findIntersection(feature, polygon) {
187
		try {
188
			return turf.intersect(polygon, feature["geoJSON"]);
189
190
191
192
		} catch (err) {
			console.log(feature.get('name') + " - " + err);
		}
	}
193
194

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

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

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

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

309
310
	function resetDrawing(){
		console.log("Reset drawing");
311
312
313
		try {
			draw.finishDrawing();
		} finally {
Eric Duminil's avatar
Eric Duminil committed
314
			displayHelp();
Eric Duminil's avatar
Eric Duminil committed
315
			document.documentElement.className = ''; // Stop waiting
316
317
318
319
320
			draw.setActive(true);
			featureOverlay.getFeatures().clear();
			intersections.clear();
			focusOnMap();
		}
321
	}
322
	
323
324
325
326
327
328
329
	function sketchAsWKT(srsName) {
		var wktFormat = new ol.format.WKT();
		return wktFormat.writeFeature(sketch, {
			dataProjection : ol.proj.get(srsName),
			featureProjection : ol.proj.get('EPSG:3857')
		});
	}
330
	
331
	function focusOnMap() {
Eric Duminil's avatar
Eric Duminil committed
332
		document.getElementById("map").focus();
333
334
	}
	
335
336
337
338
	var fxapp = undefined;
	
	publicScope.setFxApp = function(app){
		fxapp = app;
339
340
341
		console.log = function(message){
			fxapp.log(message);
		}
342
		
Eric Duminil's avatar
Eric Duminil committed
343
		console.warn = function(message){
344
345
			fxapp.warning(message);
		}
346
	}
Eric Duminil's avatar
Eric Duminil committed
347
	
348
349
350
351
352
353
354
	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
355
356
	function displayHelp(){
		dataPanel.empty();
357
		dataPanel.append("<h2 class='info'>Welcome to Region Chooser!<br><br>\n");
Eric Duminil's avatar
Eric Duminil committed
358
359
360
361
		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");
362
363
364
		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
365
366
367
		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
368
	}
369

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

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