taxi

Winning entry to the Kaggle taxi competition
git clone https://esimon.eu/repos/taxi.git
Log | Files | Refs | README

script.js (27007B)


      1 /***************/
      2 /*** General ***/
      3 /***************/
      4 
      5 window.app = {};
      6 var app = window.app;
      7 
      8 app.mainLayer = new ol.layer.Tile({ source: new ol.source.OSM() });
      9 
     10 
     11 /****************/
     12 /*** Geometry ***/
     13 /****************/
     14 
     15 app.geometry = {}
     16 app.geometry.REarth = 6371000;
     17 app.geometry.toRadians = function(x){ return x * Math.PI / 180; };
     18 
     19 app.geometry.haversine = function(lat1, lon1, lat2, lon2){
     20 	var lat1 = app.geometry.toRadians(lat1);
     21 	var lon1 = app.geometry.toRadians(lon1);
     22 	var lat2 = app.geometry.toRadians(lat2);
     23 	var lon2 = app.geometry.toRadians(lon2);
     24 
     25     var dlat = Math.abs(lat1-lat2);
     26     var dlon = Math.abs(lon1-lon2);
     27 
     28     var alpha = Math.pow(Math.sin(dlat/2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon/2), 2);
     29     var d = Math.atan2(Math.sqrt(alpha), Math.sqrt(1-alpha));
     30 
     31     return  2 * app.geometry.REarth * d;
     32 };
     33 
     34 app.geometry.equirectangular = function(lat1, lon1, lat2, lon2){
     35 	var lat1 = app.geometry.toRadians(lat1);
     36 	var lon1 = app.geometry.toRadians(lon1);
     37 	var lat2 = app.geometry.toRadians(lat2);
     38 	var lon2 = app.geometry.toRadians(lon2);
     39 	var x = (lon2-lon1) * Math.cos((lat1+lat2)/2);
     40 	var y = (lat2-lat1);
     41 	return Math.sqrt(x*x + y*y) * app.geometry.REarth;
     42 };
     43 
     44 
     45 /***************/
     46 /*** Measure ***/
     47 /***************/
     48 
     49 app.measure = {};
     50 app.measure.tooltip_list = [];
     51 
     52 app.measure.source = new ol.source.Vector();
     53 
     54 app.measure.layer = new ol.layer.Vector({
     55 	source: app.measure.source,
     56 	style: new ol.style.Style({
     57 		fill: new ol.style.Fill({
     58 			color: 'rgba(255, 255, 255, 0.2)'
     59 		}),
     60 		stroke: new ol.style.Stroke({
     61 			color: '#FC3',
     62 			width: 2
     63 		}),
     64 		image: new ol.style.Circle({
     65 			radius: 7,
     66 			fill: new ol.style.Fill({
     67 				color: '#FC3'
     68 			})
     69 		})
     70 	})
     71 });
     72 
     73 app.measure.pointerMoveHandler = function(evt){
     74 	if(evt.dragging){ return; }
     75 	var tooltipCoord = evt.coordinate;
     76 
     77 	if(app.measure.sketch){
     78 		var output;
     79 		var geom = (app.measure.sketch.getGeometry());
     80 		if(geom instanceof ol.geom.LineString){
     81 			output = app.measure.formatLength((geom));
     82 			tooltipCoord = geom.getLastCoordinate();
     83 		}
     84 		app.measure.tooltipElement.innerHTML = output;
     85 		app.measure.tooltip.setPosition(tooltipCoord);
     86 	}
     87 };
     88 
     89 app.measure.addInteraction = function(){
     90 	app.measure.draw = new ol.interaction.Draw({
     91 		source: app.measure.source,
     92 		type: ('LineString'),
     93 		style: new ol.style.Style({
     94 			fill: new ol.style.Fill({
     95 				color: 'rgba(255, 255, 255, 0.2)'
     96 			}),
     97 			stroke: new ol.style.Stroke({
     98 				color: 'rgba(0, 0, 0, 0.5)',
     99 				lineDash: [10, 10],
    100 				width: 2
    101 			}),
    102 			image: new ol.style.Circle({
    103 				radius: 5,
    104 				stroke: new ol.style.Stroke({
    105 					color: 'rgba(0, 0, 0, 0.7)'
    106 				}),
    107 				fill: new ol.style.Fill({
    108 					color: 'rgba(255, 255, 255, 0.2)'
    109 				})
    110 			})
    111 		})
    112 	});
    113 	app.map.addInteraction(app.measure.draw);
    114 
    115 	app.measure.createTooltip();
    116 
    117 	app.measure.draw.on('drawstart',
    118 		function(evt){
    119 			app.measure.sketch = evt.feature;
    120 		}, this);
    121 
    122 	app.measure.draw.on('drawend',
    123 		function(evt){
    124 			app.measure.tooltipElement.className = 'measure-tooltip measure-tooltip-static';
    125 			app.measure.tooltip.setOffset([0, -7]);
    126 			app.measure.sketch = null;
    127 			app.measure.tooltipElement = null;
    128 			app.measure.createTooltip();
    129 		}, this);
    130 };
    131 
    132 app.measure.createTooltip = function(){
    133 	if(app.measure.tooltipElement){
    134 		app.measure.tooltipElement.parentNode.removeChild(app.measure.tooltipElement);
    135 	}
    136 	app.measure.tooltipElement = document.createElement('div');
    137 	app.measure.tooltipElement.className = 'measure-tooltip measure-tooltip-value';
    138 	app.measure.tooltip = new ol.Overlay({
    139 		element: app.measure.tooltipElement,
    140 		offset: [0, -15],
    141 		positioning: 'bottom-center'
    142 	});
    143 	app.measure.tooltip_list.push(app.measure.tooltip);
    144 	app.map.addOverlay(app.measure.tooltip);
    145 };
    146 
    147 app.measure.formatLength = function(line){
    148 	var length_euclidean = line.getLength();
    149 	var length_equirectangular = 0;
    150 	var length_haversine = 0;
    151 	var coordinates = line.getCoordinates();
    152 	var sourceProj = app.map.getView().getProjection();
    153 	for(var i = 0, ii = coordinates.length - 1; i < ii; ++i){
    154 		var c1 = ol.proj.transform(coordinates[i], sourceProj, 'EPSG:4326');
    155 		var c2 = ol.proj.transform(coordinates[i + 1], sourceProj, 'EPSG:4326');
    156 		length_equirectangular += app.geometry.equirectangular(c1[1], c1[0], c2[1], c2[0]);
    157 		length_haversine += app.geometry.haversine(c1[1], c1[0], c2[1], c2[0]);
    158 	}
    159 
    160 	var disp = function(x){
    161 		if(x > 100){
    162 			return Math.round(x / 1000 * 1000) / 1000 + 'km';
    163 		} else {
    164 			return Math.round(x * 1000) / 1000 + 'm';
    165 		}
    166 	}
    167 
    168 	var length_euclidean = disp(length_euclidean);
    169 	var length_equirectangular = disp(length_equirectangular);
    170 	var length_haversine = disp(length_haversine);
    171 
    172 	var display_euclidean = $('input#measure-euclidean').prop('checked');
    173 	var display_equirectangular = $('input#measure-equirectangular').prop('checked');
    174 	var display_haversine = $('input#measure-haversine').prop('checked');
    175 
    176 	var header = true;
    177 	if(display_euclidean + display_equirectangular + display_haversine == 1){
    178 		header = false;
    179 	}
    180 
    181 	var str = '';
    182 	if(display_euclidean){
    183 		if(header){ str += 'euclidean: '; }
    184 		str += length_euclidean;
    185 	}
    186 	if(display_equirectangular){
    187 		if(header){ if(display_euclidean){ str += '<br>'; } str += 'equirectangular: '; }
    188 		str += length_equirectangular;
    189 	}
    190 	if(display_haversine){
    191 		if(header){ str += '<br> haversine: '; }
    192 		str += length_haversine;
    193 	}
    194 	return str;
    195 };
    196 
    197 
    198 /*******************/
    199 /*** DataDisplay ***/
    200 /*******************/
    201 
    202 app.dataDisplay = {};
    203 app.dataDisplay.layers = {};
    204 app.dataDisplay.heatmapRadius = 5;
    205 app.dataDisplay.heatmapBlur = 5;
    206 app.dataDisplay.pathPointMode = 1; // endpoints
    207 app.dataDisplay.pathPointResolution = 50;
    208 
    209 app.dataDisplay.loadLayer = function(path){
    210 	$.ajax({url: path, cache: false, dataType: 'json',
    211 		success: function(result){
    212 			app.dataDisplay.layers[path] = app.dataDisplay.preprocess(result);
    213 			app.map.addLayer(app.dataDisplay.layers[path]);
    214 		}
    215 	});
    216 };
    217 
    218 app.dataDisplay.unloadLayer = function(path){
    219 	app.map.removeLayer(app.dataDisplay.layers[path]);
    220 	delete app.dataDisplay.layers[path];
    221 };
    222 
    223 app.dataDisplay.rawStyle = function(feature, resolution){
    224 	var style = [ new ol.style.Style({
    225 			stroke: new ol.style.Stroke({
    226 				color: '#00F',
    227 				width: 5
    228 			}),
    229 			image: new ol.style.Circle({
    230 				radius: 5,
    231 				fill: new ol.style.Fill({
    232 					color: '#00F'
    233 				})
    234 			})
    235 		}),
    236 		new ol.style.Style({
    237 			stroke: new ol.style.Stroke({
    238 				color: '#000',
    239 				width: 2
    240 			}),
    241 			image: new ol.style.Circle({
    242 				radius: 2,
    243 				fill: new ol.style.Fill({
    244 					color: '#FFF'
    245 				})
    246 			})
    247 		})
    248 	];
    249 
    250 	if(feature.get('display') == 'path' && resolution < app.dataDisplay.pathPointResolution){
    251 		if(app.dataDisplay.pathPointMode == 2){
    252 			var polyline = feature.getGeometry();
    253 			var points = polyline.getCoordinates();
    254 			for(var i=1; i<points.length-1; ++i){
    255 				var point = points[i];
    256 				var pos = i/points.length;
    257 				var red = 0;
    258 				var green = 0;
    259 				if(pos < 0.5){
    260 					green = 255;
    261 					red = Math.round(pos*2*255);
    262 				} else {
    263 					red = 255;
    264 					green = Math.round((1-pos)*2*255);
    265 				}
    266 				style.push(new ol.style.Style({
    267 					geometry: new ol.geom.Point(point),
    268 					image: new ol.style.Circle({
    269 						radius: 3,
    270 						fill: new ol.style.Fill({
    271 							color: 'rgb('+red+','+green+',0)'
    272 						})
    273 					})
    274 				}));
    275 			}
    276 		}
    277 		if(app.dataDisplay.pathPointMode >= 1){
    278 			var polyline = feature.getGeometry();
    279 			var first = polyline.getFirstCoordinate();
    280 			var last = polyline.getLastCoordinate();
    281 			style.push(new ol.style.Style({
    282 				geometry: new ol.geom.Point(first),
    283 				image: new ol.style.Circle({
    284 					radius: 5,
    285 					fill: new ol.style.Fill({
    286 						color: '#0F0'
    287 					})
    288 				})
    289 			}));
    290 			style.push(new ol.style.Style({
    291 				geometry: new ol.geom.Point(last),
    292 				image: new ol.style.Circle({
    293 					radius: 5,
    294 					fill: new ol.style.Fill({
    295 						color: '#F00'
    296 					})
    297 				})
    298 			}));
    299 		}
    300 	}
    301 
    302 	return style;
    303 };
    304 
    305 app.dataDisplay.clusterStyleCache = {};
    306 app.dataDisplay.clusterStyle = function(feature, resolution){
    307 	var size = feature.get('features').length;
    308 	var style = app.dataDisplay.clusterStyleCache[size];
    309 	if(!style){
    310 		style = [new ol.style.Style({
    311 			image: new ol.style.Circle({
    312 				radius: 10,
    313 				stroke: new ol.style.Stroke({
    314 					color: '#FFF'
    315 				}),
    316 				fill: new ol.style.Fill({
    317 					color: '#39C'
    318 				})
    319 			}),
    320 			text: new ol.style.Text({
    321 				text: size.toString(),
    322 				fill: new ol.style.Fill({
    323 					color: '#FFF'
    324 				})
    325 			})
    326 		})];
    327 		app.dataDisplay.clusterStyleCache[size] = style;
    328 	}
    329 	return style;
    330 };
    331 
    332 app.dataDisplay.pointDistributionStyle = function(feature, resolution){
    333 	var p = feature.get('info');
    334 	var red = 0;
    335 	var green = 0;
    336 	if(p < 0.5){
    337 		green = 255;
    338 		red = Math.round(p*2*255);
    339 	} else {
    340 		red = 255;
    341 		green = Math.round((1-p)*2*255);
    342 	}
    343 	return [ new ol.style.Style({
    344 		image: new ol.style.Circle({
    345 			radius: 5,
    346 			fill: new ol.style.Fill({
    347 				color: 'rgb('+red+','+green+',0)'
    348 			})
    349 		})
    350 	}) ];
    351 };
    352 
    353 app.dataDisplay.preprocess = function(egj){
    354 	var source = new ol.source.GeoJSON({
    355 		projection: 'EPSG:3857',
    356 		object: egj.data
    357 	});
    358 
    359 	if(egj.type == 'raw'){
    360 		return new ol.layer.Vector({
    361 			source: source,
    362 			style: app.dataDisplay.rawStyle
    363 		});
    364 
    365 	} else if(egj.type == 'cluster'){
    366 		return new ol.layer.Vector({
    367 			source: new ol.source.Cluster({
    368 				distance: 40,
    369 				source: source
    370 			}),
    371 			style: app.dataDisplay.clusterStyle
    372 		});
    373 
    374 	} else if(egj.type == 'heatmap'){
    375 		return new ol.layer.Heatmap({
    376 			source: source,
    377 			blur: app.dataDisplay.heatmapBlur,
    378 			radius: app.dataDisplay.heatmapRadius
    379 		});
    380 	} else if(egj.type == 'point distribution'){
    381 		return new ol.layer.Vector({
    382 			source: source,
    383 			style: app.dataDisplay.pointDistributionStyle
    384 		});
    385 	}
    386 };
    387 
    388 app.dataDisplay.reloadPathes = function(){
    389 	for(var layer in app.dataDisplay.layers){
    390 		if(app.dataDisplay.layers[layer].getSource().getFeatures()[0].get('display') == 'path'){
    391 			app.dataDisplay.layers[layer].changed();
    392 		}
    393 	}
    394 };
    395 
    396 app.dataDisplay.reloadHeatmaps = function(){
    397 	for(var key in app.dataDisplay.layers){
    398 		var layer = app.dataDisplay.layers[key];
    399 		if(layer instanceof ol.layer.Heatmap){
    400 			layer.setBlur(app.dataDisplay.heatmapBlur);
    401 			layer.setRadius(app.dataDisplay.heatmapRadius);
    402 		}
    403 	}
    404 };
    405 
    406 
    407 /****************/
    408 /*** DataList ***/
    409 /****************/
    410 
    411 app.dataList = {};
    412 app.dataList.current = {};
    413 app.dataList.idgen = 0;
    414 
    415 app.dataList.init = function(){
    416 	app.dataList.elementTree = {};
    417 	app.dataList.elementTree.parent = null;
    418 	app.dataList.elementTree.children = {};
    419 	app.dataList.elementTree.checkbox = null;
    420 	app.dataList.elementTree.ul = $('#datalist-tree ul');
    421 
    422 	app.dataList.updateList();
    423 	setInterval(app.dataList.updateList, 1000);
    424 };
    425 
    426 app.dataList.updateList = function(){
    427 	$.ajax({url: '/ls/', cache: false, dataType: 'json',
    428 		success: function(result){
    429 			result.forEach(function(file){
    430 				file.uri = file.path.join('/') + '/' + file.name
    431 				if(file.uri in app.dataList.current){
    432 					if(file.mtime > app.dataList.current[file.uri].mtime){
    433 						var act = app.dataList.current[file.uri];
    434 						if(act.checkbox.prop('checked')){
    435 							app.dataList.unloadLayer(file.uri);
    436 							app.dataList.loadLayer(file.uri);
    437 						}
    438 						act.mtime = file.mtime;
    439 					}
    440 				} else {
    441 					app.dataList.insert(file);
    442 				}
    443 			});
    444 		}
    445 	});
    446 };
    447 
    448 app.dataList.insert = function(file){
    449 	var cur = app.dataList.elementTree;
    450 	var prev = null;
    451 	for(var i = 1; i<file.path.length; i++){
    452 		if(!(file.path[i] in cur.children)){
    453 			var n = {};
    454 			n.uri = file.path.slice(0, i+1).join('/');
    455 			n.children = {};
    456 			n.parent = cur;
    457 			n.ul = $('<ul>')
    458 				.prop('id', 'folder-'+app.dataList.idgen)
    459 				.hide();
    460 
    461 			var hidelink = $('<a>')
    462 				.prop('href', '')
    463 				.append('hide')
    464 				.hide();
    465 			var showlink = $('<a>')
    466 				.prop('href', '')
    467 				.append('show');
    468 
    469 			var playlink = $('<a>')
    470 				.prop('href', '')
    471 				.append('play');
    472 			var stoplink = $('<a>')
    473 				.prop('href', '')
    474 				.append('stop')
    475 				.hide();
    476 
    477 			n.checkbox = $('<input>')
    478 				.prop('type', 'checkbox')
    479 				.prop('id', 'data-'+app.dataList.idgen)
    480 				.prop('name', n.uri);
    481 			n.checkbox.change(app.dataList.selectData);
    482 			var item = $('<li>')
    483 				.append(n.checkbox)
    484 				.append($('<label>')
    485 					.prop('for', 'data-'+app.dataList.idgen)
    486 					.append(file.path[i]))
    487 				.append(' ')
    488 				.append(hidelink)
    489 				.append(showlink)
    490 				.append(' ')
    491 				.append(playlink)
    492 				.append(stoplink)
    493 				.append(n.ul)
    494 			app.dataList.idgen++;
    495 			cur.ul.append(item);
    496 			cur.children[file.path[i]] = n;
    497 			app.dataList.current[n.uri] = n;
    498 
    499 			var foldertoggler = function(){
    500 				hidelink.toggle();
    501 				showlink.toggle();
    502 				n.ul.toggle();
    503 				return false;
    504 			};
    505 			hidelink.click(foldertoggler);
    506 			showlink.click(foldertoggler);
    507 
    508 			playlink.click(function(){
    509 				playlink.toggle();
    510 				stoplink.toggle();
    511 				app.dataPlayer.play(n);
    512 				return false;
    513 			});
    514 			stoplink.click(function(){
    515 				playlink.toggle();
    516 				stoplink.toggle();
    517 				app.dataPlayer.stop(n);
    518 				return false;
    519 			});
    520 		}
    521 		prev = cur;
    522 		cur = cur.children[file.path[i]];
    523 	}
    524 
    525 	file.parent = cur;
    526 	file.checkbox = $('<input>')
    527 		.prop('type', 'checkbox')
    528 		.prop('id', 'data-'+app.dataList.idgen)
    529 		.prop('name', file.uri);
    530 	file.checkbox.change(app.dataList.selectData);
    531 	var item = $('<li>')
    532 		.append(file.checkbox)
    533 		.append($('<label>')
    534 			.prop('for', 'data-'+app.dataList.idgen)
    535 			.append(file.name))
    536 	app.dataList.idgen++;
    537 	cur.ul.append(item);
    538 	cur.children[file.name] = file;
    539 	app.dataList.current[file.uri] = file;
    540 
    541 	if(cur.checkbox && cur.checkbox.prop('checked')){
    542 		file.checkbox.prop('checked', true);
    543 		app.dataList.updateData(file);
    544 	}
    545 };
    546 
    547 app.dataList.updateData = function(cur){
    548 	if(cur.checkbox.prop('checked')){
    549 		app.dataList.loadLayer(cur.uri);
    550 	} else {
    551 		app.dataList.unloadLayer(cur.uri);
    552 	}
    553 };
    554 
    555 app.dataList.updateCheckboxes = function(cur){
    556 	if(cur.checkbox.prop('checked')){
    557 		app.dataList.check(cur);
    558 	} else {
    559 		app.dataList.uncheck(cur);
    560 	}
    561 };
    562 
    563 app.dataList.selectData = function(e){
    564 	var cur = app.dataList.current[e.target.name];
    565 	if(!('children' in cur)){
    566 		app.dataList.updateData(cur);
    567 	}
    568 	app.dataList.updateCheckboxes(cur);
    569 };
    570 
    571 app.dataList.changeChildren = function rec(cur, val){
    572 	cur.checkbox.prop('checked', val);
    573 	if('children' in cur){
    574 		for(var child in cur.children){
    575 			rec(cur.children[child], val);
    576 		}
    577 	} else {
    578 		app.dataList.updateData(cur);
    579 	}
    580 };
    581 
    582 app.dataList.check = function(cur){
    583 	// Check all parents
    584 	var p = cur.parent;
    585 	while(p.checkbox != null){
    586 		p.checkbox.prop('checked', true);
    587 		p = p.parent;
    588 	}
    589 
    590 	// Check all children
    591 	for(var child in cur.children){
    592 		app.dataList.changeChildren(cur.children[child], true);
    593 	}
    594 };
    595 
    596 app.dataList.uncheck = function(cur){
    597 	// Uncheck empty parents
    598 	var p = cur.parent;
    599 	while(p.checkbox != null && p.checkbox.prop('checked')){
    600 		var cc = false;
    601 		for(var child in p.children){
    602 			cc = cc || p.children[child].checkbox.prop('checked');
    603 		}
    604 		if(cc){
    605 			break;
    606 		}
    607 		p.checkbox.prop('checked', false);
    608 		p = p.parent;
    609 	}
    610 
    611 	// Uncheck all children
    612 	for(var child in cur.children){
    613 		app.dataList.changeChildren(cur.children[child], false);
    614 	}
    615 };
    616 
    617 
    618 app.dataList.loadLayer = function(uri){
    619 	app.dataDisplay.loadLayer('/get'+uri);
    620 };
    621 
    622 app.dataList.unloadLayer = function(uri){
    623 	app.dataDisplay.unloadLayer('/get'+uri);
    624 };
    625 
    626 
    627 /*******************/
    628 /*** DataExtract ***/
    629 /*******************/
    630 
    631 app.dataExtract = {};
    632 app.dataExtract.current = null;
    633 
    634 app.dataExtract.init = function(){
    635 	$('#dataextract button:contains("Refresh")').click(app.dataExtract.display);
    636 	$('#dataextract input').keypress(function(e){
    637 		if(e.keyCode == 13){
    638 			app.dataExtract.display();
    639 		}
    640 	});
    641 	$('#dataextract button:contains("Clear")').click(app.dataExtract.clear);
    642 };
    643 
    644 app.dataExtract.display = function(){
    645 	if(app.dataExtract.current){
    646 		app.dataDisplay.unloadLayer('/extract/' + app.dataExtract.current);
    647 	}
    648 	app.dataExtract.current = $('#dataextract input').val();
    649 	app.dataDisplay.loadLayer('/extract/' + app.dataExtract.current);
    650 };
    651 
    652 app.dataExtract.clear = function(){
    653 	if(app.dataExtract.current){
    654 		app.dataDisplay.unloadLayer('/extract/' + app.dataExtract.current);
    655 		app.dataExtract.current = null;
    656 	}
    657 };
    658 
    659 
    660 
    661 /******************/
    662 /*** DataPlayer ***/
    663 /******************/
    664 
    665 app.dataPlayer = {};
    666 app.dataPlayer.current = {};
    667 app.dataPlayer.updateFrequency = 200;
    668 app.dataPlayer.time = 0;
    669 
    670 app.dataPlayer.init = function(){
    671 	app.dataPlayer.intervalId = setInterval(app.dataPlayer.update, app.dataPlayer.updateFrequency);
    672 };
    673 
    674 app.dataPlayer.updateInterval = function(){
    675 	clearInterval(app.dataPlayer.intervalId);
    676 	app.dataPlayer.intervalId = setInterval(app.dataPlayer.update, app.dataPlayer.updateFrequency);
    677 };
    678 
    679 app.dataPlayer.play = function(cur){
    680 	app.dataPlayer.updateKeys(cur);
    681 	if(cur.keys.length == 0){
    682 		alert("ERROR: No number in directory.");
    683 		return;
    684 	}
    685 	app.dataList.uncheck(cur);
    686 	cur.checkbox.prop('checked', true);
    687 	cur.playIndex = 0;
    688 	app.dataPlayer.current[cur.uri] = cur;
    689 	for(var key in cur.context){
    690 		var child = cur.children[cur.context[key]];
    691 		child.checkbox.prop('checked', true);
    692 		if(!('children' in child)){
    693 			app.dataList.updateData(child);
    694 		}
    695 		app.dataList.updateCheckboxes(child);
    696 	}
    697 };
    698 
    699 app.dataPlayer.updateKeys = function(cur){
    700 	var keys = Object.keys(cur.children);
    701 	cur.keys = keys.map(Number).filter(function(x){ return !isNaN(x); }).sort(function(l,r){return l-r;});
    702 	cur.context = keys.filter(function(x){ return isNaN(Number(x)); });
    703 };
    704 
    705 app.dataPlayer.stop = function(cur){
    706 	delete app.dataPlayer.current[cur.uri];
    707 	delete cur.keys;
    708 	delete cur.context;
    709 	delete cur.playIndex;
    710 };
    711 
    712 app.dataPlayer.update = function(){
    713 	for(var key in app.dataPlayer.current){
    714 		var cur = app.dataPlayer.current[key];
    715 		var prev = cur.children[cur.keys[cur.playIndex]];
    716 		cur.playIndex++;
    717 		if(cur.playIndex >= cur.keys.length){
    718 			app.dataPlayer.updateKeys(cur);
    719 			if(cur.playIndex >= cur.keys.length){
    720 				cur.playIndex = 0;
    721 			}
    722 		}
    723 		var next = cur.children[cur.keys[cur.playIndex]];
    724 
    725 		prev.checkbox.prop('checked', false);
    726 		app.dataList.updateData(prev);
    727 		next.checkbox.prop('checked', true);
    728 		app.dataList.updateData(next);
    729 	}
    730 };
    731 
    732 
    733 /*****************/
    734 /*** CoordInfo ***/
    735 /*****************/
    736 
    737 app.coordInfo = {};
    738 app.coordInfo.init = function(){
    739 	app.coordInfo.element = $('#coordInfo');
    740 	app.coordInfo.enable();
    741 };
    742 
    743 app.coordInfo.enable = function(){
    744 	app.map.on('pointermove', app.coordInfo.update);
    745 	app.coordInfo.element.show();
    746 };
    747 
    748 app.coordInfo.disable = function(){
    749 	app.coordInfo.element.hide();
    750 	app.map.un('pointermove', app.coordInfo.update);
    751 };
    752 
    753 app.coordInfo.update = function(evt){
    754 	var coord = ol.proj.transform(app.map.getEventCoordinate(evt.originalEvent), app.map.getView().getProjection(), 'EPSG:4326');
    755 	app.coordInfo.element.text(coord[1] + ', ' + coord[0]);
    756 };
    757 
    758 
    759 /*******************/
    760 /*** FeatureInfo ***/
    761 /*******************/
    762 
    763 app.featureInfo = {};
    764 
    765 app.featureInfo.init = function(){
    766 	app.featureInfo.element = $('#featureinfo');
    767 	app.featureInfo.static = $('#featureinfo-static');
    768 	app.featureInfo.dynamic = $('#featureinfo-dynamic');
    769 	app.featureInfo.element.hide();
    770 	app.featureInfo.enable();
    771 };
    772 
    773 app.featureInfo.enable = function(){
    774 	app.map.on('pointermove', app.featureInfo.updateMove);
    775 	app.map.on('click', app.featureInfo.updateClick);
    776 };
    777 
    778 app.featureInfo.disable = function(){
    779 	app.map.un('pointermove', app.featureInfo.updateMove);
    780 	app.map.un('click', app.featureInfo.updateClick);
    781 };
    782 
    783 app.featureInfo.updateMove = function(evt){
    784 	if(evt.dragging){
    785 		app.featureInfo.element.hide();
    786 		return;
    787 	}
    788 	app.featureInfo.display(evt);
    789 };
    790 
    791 app.featureInfo.updateClick = function(evt){
    792 	app.featureInfo.display(evt);
    793 };
    794 
    795 app.featureInfo.display = function(evt){
    796 	app.featureInfo.element.css({
    797 		left: (evt.pixel[0] + 10) + 'px',
    798 		top: evt.pixel[1] + 'px'
    799 	});
    800 	var feature = app.map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) {
    801 		return feature;
    802 	});
    803 	if(feature && feature.get('info')){
    804 		if(feature.get('display') == 'path'){
    805 			var dtime = app.featureInfo.interpolateTime(feature.getGeometry(), evt.coordinate);
    806 			var date = new Date(1000*(feature.get('timestamp') + dtime*15));
    807 			var desc = 'index: '+dtime+'<br>';
    808 			desc += 'date: '+date.toISOString()+'<br>';
    809 			app.featureInfo.dynamic.html(desc);
    810 		} else {
    811 			app.featureInfo.dynamic.html('');
    812 		}
    813 		app.featureInfo.static.html(feature.get('info'));
    814 		app.featureInfo.element.show();
    815 	} else {
    816 		app.featureInfo.element.hide();
    817 	}
    818 };
    819 
    820 app.featureInfo.interpolateTime = function(polyline, coord){
    821 	var closest = polyline.getClosestPoint(coord);
    822 	var best = 1000;
    823 	var bestStart = -1;
    824 	var bestEnd = -1;
    825 	var bestI = -1;
    826 	var i = 0;
    827 	var points = polyline.getCoordinates();
    828 	for(var i=0; i<points.length-1; i++){
    829 		var start = points[i];
    830 		var end = points[i+1];
    831 		var dist = Math.abs((end[0]-start[0])*closest[1] - (end[1]-start[1])*closest[0] + end[1]*start[0] - end[0]*start[1]) / Math.sqrt(Math.pow(end[0]-start[0], 2) + Math.pow(end[1]-start[1], 2));
    832 		if(dist<best){
    833 			best = dist;
    834 			bestStart = start;
    835 			bestEnd = end;
    836 			bestI = i;
    837 		}
    838 	}
    839 
    840 	if(bestI == -1){
    841 		return 0;
    842 	}
    843 
    844 	var distClosest = app.geometry.equirectangular(bestStart[1], bestStart[0], closest[1], closest[0]);
    845 	var distEnd = app.geometry.equirectangular(bestStart[1], bestStart[0], bestEnd[1], bestEnd[0]);
    846 	var ratio = distClosest / distEnd;
    847 	return bestI + ratio;
    848 };
    849 
    850 
    851 /***************/
    852 /*** Control ***/
    853 /***************/
    854 
    855 app.control = {};
    856 
    857 app.control.OpenConfigControl = function(opt_options){
    858 	var options = opt_options || {};
    859 	
    860 	var button = document.createElement('button');
    861 	button.innerHTML = '⚙';
    862 	
    863 	button.addEventListener('click', function(e){ $('#config').toggle() }, false);
    864 	button.addEventListener('touchstart', function(e){ $('#config').toggle() }, false);
    865 	
    866 	var element = document.createElement('div');
    867 	element.className = 'open-config ol-unselectable ol-control';
    868 	element.appendChild(button);
    869 	
    870 	ol.control.Control.call(this, {
    871 		element: element,
    872 		target: options.target
    873 	});
    874 };
    875 ol.inherits(app.control.OpenConfigControl, ol.control.Control);
    876 
    877 app.control.OpenDatalist = function(opt_options){
    878 	var options = opt_options || {};
    879 	
    880 	var button = document.createElement('button');
    881 	button.innerHTML = '«';
    882 	
    883 	var toggler = function(e){
    884 		$('#datalist').toggle();
    885 		if(button.innerHTML == '«'){
    886 			button.innerHTML = '»';
    887 			$('.open-datalist').css('right', '20.5em');
    888 		}
    889 		else{
    890 			button.innerHTML = '«';
    891 			$('.open-datalist').css('right', '.5em');
    892 		}
    893 	};
    894 	button.addEventListener('click', toggler, false);
    895 	button.addEventListener('touchstart', toggler, false);
    896 	
    897 	var element = document.createElement('div');
    898 	element.className = 'open-datalist ol-unselectable ol-control';
    899 	element.appendChild(button);
    900 	
    901 	ol.control.Control.call(this, {
    902 		element: element,
    903 		target: options.target
    904 	});
    905 };
    906 ol.inherits(app.control.OpenDatalist, ol.control.Control);
    907 
    908 
    909 /************/
    910 /*** Menu ***/
    911 /************/
    912 
    913 app.menu = {};
    914 
    915 app.menu.init = function(){
    916 	$('#config ul').menu();
    917 
    918 	$('#config ul li').click(function(e){
    919 		switch($(this).text()){
    920 			case 'Enable coord':
    921 				app.coordInfo.enable();
    922 				$('#config ul li:contains("Enable coord")').toggle();
    923 				$('#config ul li:contains("Disable coord")').toggle();
    924 				break;
    925 			case 'Disable coord':
    926 				app.coordInfo.disable();
    927 				$('#config ul li:contains("Enable coord")').toggle();
    928 				$('#config ul li:contains("Disable coord")').toggle();
    929 				break;
    930 			case 'Set player speed':
    931 				var tmp = prompt("Player update frequency (milliseconds)", app.dataPlayer.updateFrequency);
    932 				if(tmp){
    933 					app.dataPlayer.updateFrequency = parseInt(tmp);
    934 					app.dataPlayer.updateInterval();
    935 				}
    936 				break;
    937 		}
    938 	});
    939 
    940 	$('ul#config-measure li').click(function(e){
    941 		switch($(this).text()){
    942 			case 'Enable':
    943 				app.measure.addInteraction();
    944 				app.map.on('pointermove', app.measure.pointerMoveHandler);
    945 				app.featureInfo.disable();
    946 				$('ul#config-measure li:contains("Enable")').toggle();
    947 				$('ul#config-measure li:contains("Disable")').toggle();
    948 				break;
    949 			case 'Disable':
    950 				app.featureInfo.enable();
    951 				app.map.un('pointermove', app.measure.pointerMoveHandler);
    952 				app.map.removeInteraction(app.measure.draw);
    953 				app.measure.draw = null;
    954 				$('ul#config-measure li:contains("Enable")').toggle();
    955 				$('ul#config-measure li:contains("Disable")').toggle();
    956 				break;
    957 			case 'Clear':
    958 				app.measure.source.clear();
    959 				app.measure.tooltip_list.forEach(function(e){
    960 					app.map.removeOverlay(e);
    961 				});
    962 				app.measure.tooltip_list.length = 0;
    963 				if(app.measure.draw){
    964 					app.map.removeInteraction(app.measure.draw);
    965 					app.measure.addInteraction();
    966 				}
    967 				break;
    968 		}
    969 	});
    970 
    971 	$('ul#config-layer li').click(function(e){
    972 		switch($(this).text()){
    973 			case 'OSM':
    974 				app.mainLayer = new ol.layer.Tile({ source: new ol.source.OSM() });
    975 				break;
    976 			case 'Bing':
    977 				app.mainLayer = new ol.layer.Tile({ source: new ol.source.BingMaps({
    978 					key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
    979 					imagerySet: 'AerialWithLabels',
    980 					maxZoom: 19
    981 				}) });
    982 				break;
    983 			case 'Bing (no labels)':
    984 				app.mainLayer = new ol.layer.Tile({ source: new ol.source.BingMaps({
    985 					key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
    986 					imagerySet: 'Aerial',
    987 					maxZoom: 19
    988 				}) });
    989 				break;
    990 		}
    991 		app.map.getLayers().setAt(0, app.mainLayer);
    992 	});
    993 
    994 	$('ul#config-pathdraw li').click(function(e){
    995 		switch($(this).text()){
    996 			case 'Set resolution':
    997 				var tmp = prompt("Path points resolution", app.dataDisplay.pathPointResolution);
    998 				if(tmp){
    999 					app.dataDisplay.pathPointResolution = parseInt(tmp);
   1000 					app.dataDisplay.reloadPathes();
   1001 				}
   1002 				break;
   1003 			case 'No points':
   1004 				app.dataDisplay.pathPointMode = 0;
   1005 				app.dataDisplay.reloadPathes();
   1006 				break;
   1007 			case 'Endpoints':
   1008 				app.dataDisplay.pathPointMode = 1;
   1009 				app.dataDisplay.reloadPathes();
   1010 				break;
   1011 			case 'All points':
   1012 				app.dataDisplay.pathPointMode = 2;
   1013 				app.dataDisplay.reloadPathes();
   1014 				break;
   1015 		}
   1016 	});
   1017 
   1018 	$('ul#config-heatmap li').click(function(e){
   1019 		switch($(this).text()){
   1020 			case 'Set blur':
   1021 				var tmp = prompt("Heatmap blur", app.dataDisplay.heatmapBlur);
   1022 				if(tmp){
   1023 					app.dataDisplay.heatmapBlur = parseInt(tmp);
   1024 					app.dataDisplay.reloadHeatmaps();
   1025 				}
   1026 				break;
   1027 			case 'Set radius':
   1028 				var tmp = prompt("Heatmap radius", app.dataDisplay.heatmapRadius);
   1029 				if(tmp){
   1030 					app.dataDisplay.heatmapRadius = parseInt(tmp);
   1031 					app.dataDisplay.reloadHeatmaps();
   1032 				}
   1033 				break;
   1034 		}
   1035 	});
   1036 };
   1037 
   1038 
   1039 /**********************/
   1040 /*** Initialization ***/
   1041 /**********************/
   1042 
   1043 $(function(){
   1044 	app.map = new ol.Map({
   1045 		controls: ol.control.defaults().extend([
   1046 			new app.control.OpenConfigControl(),
   1047 			new app.control.OpenDatalist()
   1048 		]),
   1049 		target: 'map',
   1050 		layers: [ app.mainLayer, app.measure.layer ],
   1051 		view: new ol.View({
   1052 			center: ol.proj.transform([-8.621953, 41.162142], 'EPSG:4326', 'EPSG:3857'),
   1053 			zoom: 13
   1054 		})
   1055 	});
   1056 
   1057 	app.menu.init();
   1058 	app.coordInfo.init();
   1059 	app.featureInfo.init();
   1060 	app.dataList.init();
   1061 	app.dataExtract.init();
   1062 	app.dataPlayer.init();
   1063 });