simstadt_openlayers.js 14.4 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);
7
8
9
10
11
12
	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
13
		document.getElementById("select_repository").style.visibility = "visible";
14
	}
15
16
	
	if (fromJavaFX){
Eric Duminil's avatar
Eric Duminil committed
17
		document.documentElement.className = 'wait';
18
	}
19
20
21

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

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

35
36
37
38
39
40
41
42
43
44
45
46
	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
47

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

	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
61
62
		})
	});
63

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

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

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

	function updateGMLPolygons() {
		kml_source.forEachFeature(function(feature) {
93
			feature["geoJSON"] = geoJsonFormat.writeFeatureObject(feature);
94
			feature["area"] = feature.getGeometry().getArea();
Eric Duminil's avatar
Eric Duminil committed
95
96
			feature["project"] = feature.get("project");
			feature["name"] = feature.get("name");
97
98
99
100
101
102
103
104
105
106
			feature["source"] = "CityGML";
		});
	}

	// 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
107
			fill : new ol.style.Fill({
108
109
110
111
112
113
114
115
116
117
118
				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
119
120
121
			})
		})
	});
122
123
	featureOverlay.setMap(map);

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

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

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

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

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

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

	draw.on('drawend', function() {
		displayInfo();
		draw.setActive(false);
	});
287
288
289
290
291
292
293
294
	
	// Pressing ESCAPE or DELETE resets the drawing.
	// With OpenLayers 3.9, draw_interaction.removeLastPoint(); might be better.
	document.addEventListener('keydown', function(e) {
		if (e.which == 27 || e.which == 46){
			resetDrawing();
		}
	});
295

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

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

427
	focusOnMap();
Eric Duminil's avatar
Eric Duminil committed
428
429
	//var regionChooser = publicScope; //NOTE: In order to open closure. For debugging
	return publicScope;
430
})();