simstadt_openlayers.js 15.3 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
	
	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);
171
		var id = feature.getId();
172
		intersections.addFeature(intersection);
173
174
175
		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>';
176
177
178
179
180
181
182
183

		link += " (" + citygml_percentage + "%";
		if (sketch_percentage == 100) {
			link += ", all inside";
		}
		dataPanel.append(link + ")\n");
	}
	
184
185
186
187
188
189
190
191
192
193
194
	publicScope.highlightPolygon = function(i) {
		var feature = kml_source.getFeatureById(i);
		feature.oldStyle = feature.getStyle();
		feature.setStyle(polygon_style("#ff44a2", 0.7));
	}
	
	publicScope.resetHighlight = function(i) {
		var feature = kml_source.getFeatureById(i);
		feature.setStyle(feature.oldStyle);
	}
	
195
	publicScope.isDownloadPossible = function(){
196
197
		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
198
199
	}
	
200
	function findIntersection(feature, polygon) {
201
		try {
202
			return turf.intersect(polygon, feature["geoJSON"]);
203
204
205
206
		} catch (err) {
			console.log(feature.get('name') + " - " + err);
		}
	}
207
208

	function findIntersections() {
209
210
		var polygon = geoJsonFormat.writeFeatureObject(sketch);
		var polygonArea = sketch.getGeometry().getArea();
211
212
		var intersection_found = false;
		intersections.clear();
213
		
214
		Object.keys(features_by_project).forEach(function(project) {
215
216
217
218
			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;
219
				dataPanel.append("<h2 class='info'>" + project);
220
221
				features_and_intersections.forEach(l => showLinkToDownload(l[0], l[1], polygonArea));
			}
222
		});
223
		
224
		if (intersection_found) {
225
			document.getElementById("download_region").style.visibility = 'visible';
226
227
		}
		else {
228
			document.getElementById("download_region").style.visibility = 'hidden';
229
			dataPanel.append("No intersection found with any CityGML file.<br/>\n");
eric.duminil's avatar
eric.duminil committed
230
231
		}
	}
232

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

289
290
	function displayInfo() {
		dataPanel.empty();
291
		var geom = sketch.getGeometry().clone().transform(sourceProj, 'EPSG:4326');
292
293
		var coordinates = geom.getLinearRing(0).getCoordinates();
		var area = Math.abs(wgs84Sphere.geodesicArea(coordinates));
294
		//NOTE: Could show m², ha or km² depending on magnitude
295
		dataPanel.append("<h3 class='clean'>Area : " + (area / 10000).toFixed(1) + " ha\n");
296
297
298
299
		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
300
			'</div>\n');
301
		findIntersections();
Eric Duminil's avatar
Eric Duminil committed
302
		dataPanel.append('<button type="button" onclick="regionChooser.copyCoordinatesToClipboard()" id="get_wgs84">Copy coordinates</button><br/>\n')
eric.duminil's avatar
eric.duminil committed
303
	}
304
305
306
307
308

	draw.on('drawend', function() {
		displayInfo();
		draw.setActive(false);
	});
309
310
311
	
	// With OpenLayers 3.9, draw_interaction.removeLastPoint(); might be better.
	document.addEventListener('keydown', function(e) {
Eric Duminil's avatar
Eric Duminil committed
312
313
		//NOTE: e.key isn't defined in JavaFX Browser
		if (e.which == 27 || e.which == 46){ // ESCAPE or DELETE.
314
315
			resetDrawing();
		}
Eric Duminil's avatar
Eric Duminil committed
316
317
318
319
320
		if (e.which == 116 && fromJavaFX){ // F5 for refresh
			dataPanel.prepend("<h2 class='ok'>Refreshing repository...</h2><br/>\n");
			document.documentElement.className = 'wait';
			fxapp.refreshHulls();
		}
321
	});
322

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

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

454
	focusOnMap();
Eric Duminil's avatar
Eric Duminil committed
455
456
	//var regionChooser = publicScope; //NOTE: In order to open closure. For debugging
	return publicScope;
457
})();