From 2bc9da3893c8869468d20f6ec723ea3d96ca1ff1 Mon Sep 17 00:00:00 2001
From: Jacek Kowalski <Jacek@jacekk.info>
Date: Fri, 29 May 2020 17:06:27 +0000
Subject: [PATCH] Initial version of map.js designed for EventSource/XHR pull

---
 index.js        |    8 
 map.js          |  523 +++++++++++++++++++++++++--------------
 common.js       |  149 +++++++----
 lang_en.js      |    3 
 map.html        |    4 
 copyrights.html |   54 ++++
 lang_pl.js      |    3 
 7 files changed, 497 insertions(+), 247 deletions(-)

diff --git a/common.js b/common.js
index 4c7b840..af26776 100644
--- a/common.js
+++ b/common.js
@@ -33,13 +33,13 @@
 		return new Deferred(this.promise, this.request);
 	},
 	done: function(func) {
-		return new Deferred(this.promise.then(func), this.request);
+		return new Deferred(this.promise.then(func.bind(this)), this.request);
 	},
 	fail: function(func) {
-		return new Deferred(this.promise.catch(func), this.request);
+		return new Deferred(this.promise.catch(func.bind(this)), this.request);
 	},
 	always: function(func) {
-		return new Deferred(this.promise.finally(func), this.request);
+		return new Deferred(this.promise.finally(func.bind(this)), this.request);
 	},
 };
 Deferred.all = function(iterable) {
@@ -55,16 +55,23 @@
 var $ = {
 	timeout: 10000,
 	dataType: 'json',
-	get: function(url) {
+	get: function(url, headers) {
 		var self = this;
 		var request = new XMLHttpRequest();
 		var promise = new Promise(function(resolve, reject) {
 			request.open('GET', url, true);
+			if(headers) {
+				Object.keys(headers).forEach(function (header) {
+					request.setRequestHeader(header, headers[header]);
+				});
+			}
 			request.timeout = self.timeout;
 			request.onreadystatechange = function() {
 				if(this.readyState == 4) {
-					if(this.status == 200) {
-						if(self.dataType == 'json') {
+					if(this.status == 304) {
+						resolve();
+					} else if(this.status == 200) {
+						if(self.dataType === 'json') {
 							resolve(JSON.parse(this.responseText));
 						} else {
 							resolve(this.responseText);
@@ -88,8 +95,6 @@
 var script_version;
 var script_version_xhr;
 
-var vehicles_info = {};
-
 function checkVersion() {
 	if(script_version_xhr) script_version_xhr.abort();
 	
@@ -111,6 +116,23 @@
 function checkVersionInit() {
 	checkVersion();
 	setInterval(checkVersion, 3600000);
+}
+
+/**********
+ * ARRAYS *
+ **********/
+
+function deepMerge(a1, a2) {
+	if(typeof a1 !== 'object' || typeof a2 !== 'object') {
+		return a2;
+	}
+	Object.keys(a2).forEach(function (key) {
+		a1[key] = deepMerge(a1[key], a2[key]);
+		if(a1[key] === null) {
+			delete a1[key];
+		}
+	});
+	return a1;
 }
 
 
@@ -141,6 +163,69 @@
 	deleteChildren(element);
 	element.appendChild(document.createTextNode(text));
 }
+
+
+/*****************
+ * VEHICLES INFO *
+ *****************/
+
+function VehiclesInfo() {
+	this.data = {};
+	this.watchers = [];
+}
+VehiclesInfo.prototype = {
+	update: function () {
+		return $.get(
+			'https://mpk.jacekk.net/vehicles/'
+		).done(function(data) {
+			this.data = data;
+			this.watchers.forEach(function(watcher) {
+				watcher(this);
+			});
+		}.bind(this));
+	},
+	addWatcher: function(callback) {
+		this.watchers.push(callback);
+	},
+
+	get: function(vehicleId) {
+		if(!vehicleId) return false;
+		if(typeof this.data[vehicleId] === "undefined") {
+			return false;
+		}
+		return this.data[vehicleId];
+	},
+	getParsed: function (vehicleId) {
+		var vehicle = this.get(vehicleId);
+		if(!vehicle) return false;
+		return {
+			vehicleId: vehicleId,
+			prefix: vehicle['num'].substr(0, 2),
+			id: vehicle['num'].substr(2, 3),
+			num: vehicle['num'],
+			type: vehicle['type'],
+			low: vehicle['low']
+		};
+	},
+	depotIdToVehicleId: function(depotId, typeHelper) {
+		var prop;
+		depotId = depotId.toString();
+		if(typeHelper) {
+			for(prop in this.data) {
+				if(prop.substr(0,1) === typeHelper && this.data[prop]['num'].substr(2) === depotId) {
+					return prop;
+				}
+			}
+		} else {
+			for(prop in this.data) {
+				if(this.data[prop]['num'] === depotId) {
+					return prop;
+				}
+			}
+		}
+	},
+};
+var vehicles_info = new VehiclesInfo();
 
 
 /***********
@@ -194,48 +279,6 @@
 	return lang.time_minutes_prefix + ((actual.getTime() - planned.getTime()) / 1000 / 60) + lang.time_minutes_suffix;
 }
 
-function parseVehicle(vehicleId) {
-	if(!vehicleId) return false;
-	if(!vehicles_info || !vehicles_info[vehicleId]) {
-		return false;
-	} else {
-		var vehicle = vehicles_info[vehicleId];
-		return {
-			vehicleId: vehicleId,
-			prefix: vehicle['num'].substr(0, 2),
-			id: vehicle['num'].substr(2, 3),
-			num: vehicle['num'],
-			type: vehicle['type'],
-			low: vehicle['low']
-		};
-	}
-}
-
-function updateVehicleInfo() {
-	return $.get(
-		'https://mpk.jacekk.net/vehicles/'
-	).done(function(data) {
-		vehicles_info = data;
-	});
-}
-
-function depotIdToVehicleId(depotId, typeHelper) {
-	var prop;
-	if(typeHelper) {
-		for(prop in vehicles_info) {
-			if(prop.substr(0,1) == typeHelper && vehicles_info[prop]['num'].substr(2) == depotId) {
-				return prop;
-			}
-		}
-	} else {
-		for(prop in vehicles_info) {
-			if(vehicles_info[prop]['num'] == depotId) {
-				return prop;
-			}
-		}
-	}
-}
-
 function displayVehicle(vehicleInfo) {
 	if(!vehicleInfo) return document.createTextNode('');
 	
@@ -243,13 +286,13 @@
 	span.className = 'vehicleInfo';
 	
 	var floor_type = '';
-	if(vehicleInfo.low == 0) {
+	if(vehicleInfo.low === 0) {
 		setText(span, lang.high_floor_sign);
 		floor_type = lang.high_floor;
-	} else if(vehicleInfo.low == 1) {
+	} else if(vehicleInfo.low === 1) {
 		setText(span, lang.partially_low_floor_sign);
 		floor_type = lang.partially_low_floor;
-	} else if(vehicleInfo.low == 2) {
+	} else if(vehicleInfo.low === 2) {
 		setText(span, lang.low_floor_sign);
 		floor_type = lang.low_floor;
 	}
diff --git a/copyrights.html b/copyrights.html
new file mode 100644
index 0000000..c6fad94
--- /dev/null
+++ b/copyrights.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>TTSS - prawa autorskie i pochodzenie informacji</title>
+<meta charset="utf-8" />
+
+<p>Wykonanie strony: Jacek Kowalski</p>
+
+<p>
+    Kod źródłowy aplikacji można znaleźć pod adresem:
+    <a href="https://git.jacekk.net/project/ttss">https://git.jacekk.net/project/ttss</a>,
+    a także w serwisie GitHub:
+    <a href="https://github.com/search?q=user%3Ajacekkow+ttss">https://github.com/search?q=user%3Ajacekkow+ttss</a>
+</p>
+
+<p>
+    Dane na stronie pochodzą z serwerów i serwisów Zarządu Transportu Publicznego w Krakowie
+    i/lub Miejskiego Przedsiębiorstwa Komunikacyjnego SA w Krakowie:
+</p>
+
+<ul>
+    <li><a href="ftp://ztp.krakow.pl/pliki-gtfs/">ftp://ztp.krakow.pl/pliki-gtfs/</a></li>
+    <li><a href="http://www.ttss.krakow.pl/internetservice/">http://www.ttss.krakow.pl/internetservice/</a></li>
+    <li><a href="http://ttss.mpk.krakow.pl/internetservice/">http://ttss.mpk.krakow.pl/internetservice/</a> </li>
+</ul>
+
+<p>Dane są przetwarzane zgodnie z ustawą o ponownym wykorzystaniu informacji sektora publicznego
+    oraz <a href="https://www.bip.krakow.pl/?dok_id=48482">warunkami Gminy Miejskiej Kraków</a>.</p>
+
+<p>
+    Z zastrzeżeniem dostępności wskazanych wyżej usług i problemów technicznych, dane są przetwarzane na bieżąco.
+    Dokładne informacje o czasie ostatniego dostępu do i przetworzenia danych o pojazdach znajdują się pod adresami:
+</p>
+
+<ul>
+    <li><a href="https://ttss.pl/vehicles/vehicles_A.html">https://ttss.pl/vehicles/vehicles_A.html</a> - dla autobusów,</li>
+    <li><a href="https://ttss.pl/vehicles/vehicles_T.html">https://ttss.pl/vehicles/vehicles_T.html</a> - dla tramwajów.</li>
+</ul>
+
+<p>a także w nagłówku Etag bądź w polu id każdej odpowiedzi z danymi, w których znajduje się Unix timestamp oznaczający czas wytworzenia.</p>
+
+<p>Gmina Miejska Kraków nie ponosi odpowiedzialności za:</p>
+<ul>
+    <li>
+        szkody spowodowane pozyskaniem informacji sektora publicznego lub ponownym wykorzystywaniem
+        informacji sektora publicznego, zamieszczonej na stronach BIP MK lub w innym miejskim serwisie internetowym,
+        udostępnianej na wniosek lub pozyskanej w inny sposób, wykorzystywanej ponownie z naruszeniem warunków
+        udostępniania lub ponownego wykorzystywania informacji sektora publicznego;</li>
+    <li>
+        szkody spowodowane przez dalsze udostępnienie informacji sektora publicznego przez podmioty
+        ponownie ją wykorzystujące z naruszeniem przepisów prawa powszechnie obowiązującego, w tym dalsze
+        udostępnianie informacji sektora publicznego z naruszeniem przepisów regulujących ich ochronę
+        m.in. przepisów ustawy o prawie autorskim i prawach pokrewnych, ustawy o ochronie baz danych,
+        przepisów o ochronie danych osobowych, ustawy o ochronie informacji niejawnych itd.
+    </li>
+</ul>
diff --git a/index.js b/index.js
index f496856..2927c12 100644
--- a/index.js
+++ b/index.js
@@ -195,7 +195,7 @@
 	var candidate;
 	for(var i = 0; i < stop_name_autocomplete.options.length; i++) {
 		candidate = stop_name_autocomplete.options[i].value;
-		if(candidate.substr(0, 1) != prefix && candidate.substr(1) == stop) {
+		if(candidate.substr(0, 1) !== prefix && candidate.substr(1) == stop) {
 			alternative_stop = candidate;
 			break;
 		}
@@ -215,7 +215,7 @@
 		
 		if(alternative_stop !== null) {
 			var a = addParaWithText(times_alerts, '');
-			a = addElementWithText(a, 'a', (prefix == 'b' ? lang.departures_for_trams : lang.departures_for_buses));
+			a = addElementWithText(a, 'a', (prefix === 'b' ? lang.departures_for_trams : lang.departures_for_buses));
 			a.href = '';
 			a.onclick = function(e) {
 				e.preventDefault();
@@ -235,7 +235,7 @@
 			tr = document.createElement('tr');
 			addCellWithText(tr, all_departures[i].patternText);
 			dir_cell = addCellWithText(tr, all_departures[i].direction);
-			vehicle = parseVehicle(prefix + all_departures[i].vehicleId);
+			vehicle = vehicles_info.getParsed(prefix + all_departures[i].vehicleId);
 			dir_cell.appendChild(displayVehicle(vehicle));
 			addCellWithText(tr, (vehicle ? vehicle.num : '')).className = 'vehicleData';
 			status = parseStatus(all_departures[i]);
@@ -419,7 +419,7 @@
 		setText(vehicle_data_style, '.vehicleData { display: table-cell; }')
 	});
 	
-	updateVehicleInfo()
+	vehicles_info.update();
 	
 	hash();
 	
diff --git a/lang_en.js b/lang_en.js
index db691a6..bf8f333 100644
--- a/lang_en.js
+++ b/lang_en.js
@@ -91,10 +91,13 @@
 	help_hover_for_more: 'Hover the icon for more details.',
 	help_source: 'Source',
 	help_license: 'License',
+	help_data_attribution: 'Data: <a href="copyrights.html">ZTP w Krakowie</a>',
+	help_website_attribution: 'Strona: <a href="copyrights.html">Jacek Kowalski</a>',
 	
 	error_title: 'Error occured!',
 	error_request_failed: 'Internet request failed.',
 	error_request_failed_status: 'Internet request failed with error: $status.',
+	error_request_failed_no_data: 'No data on the server.',
 	error_request_failed_connectivity: 'Request failed - please check your network connectivity.',
 	error_new_version: 'Website has been updated, reloading...',
 	error_refresh: 'Error! Refresh the page to update information.',
diff --git a/lang_pl.js b/lang_pl.js
index 9682cd6..32b5aaf 100644
--- a/lang_pl.js
+++ b/lang_pl.js
@@ -91,10 +91,13 @@
 	help_hover_for_more: 'Najedź na ikonę, by uzyskać więcej informacji.',
 	help_source: 'Kod źródłowy',
 	help_license: 'Licencja',
+	help_data_attribution: 'Dane: <a href="copyrights.html">ZTP w Krakowie</a>',
+	help_website_attribution: 'Strona: <a href="copyrights.html">Jacek Kowalski</a>',
 	
 	error_title: 'Wystąpił błąd!',
 	error_request_failed: 'Wykonanie żądania internetowego nie udało się.',
 	error_request_failed_status: 'Wykonanie żądania internetowego nie udało się. Błąd: $status.',
+	error_request_failed_no_data: 'Brak danych na serwerze.',
 	error_request_failed_connectivity: 'Wykonanie żądania internetowego nie udało się - sprawdź połączenie z siecią.',
 	error_new_version: 'Strona została zaktualizowana, przeładowuję...',
 	error_refresh: 'Błąd! Odśwież stronę, by zaktualizować dane.',
diff --git a/map.html b/map.html
index 597a563..e929fe3 100644
--- a/map.html
+++ b/map.html
@@ -4,7 +4,7 @@
 <title>TTSS Kraków - Mapa</title>
 <meta charset="utf-8" />
 <meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width" />
-<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v5.3.0/css/ol.css" integrity="sha384-C7SzZySesoxngSK5V0BaD1DUap0LPZGWZpnXQGoIwvBXFc8G21y4s1QYvyr84FNa" crossorigin="anonymous">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/css/ol.css" integrity="sha384-6Ku0bdGS1If4LlZPWpe2wTx/qu/Y+YJob6lvk4Yi49uWWFqASeOF130VK3KjIPgh" crossorigin="anonymous">
 <link rel="stylesheet" href="map.css?v6" type="text/css" />
 <link rel="manifest" href="map.manifest" />
 </head>
@@ -19,7 +19,7 @@
 </div>
 <div id="panel"></div>
 <script src="https://polyfill.io/v3/polyfill.min.js?features=Array.prototype.forEach,Array.prototype.includes,Array.prototype.map,Element.prototype.classList,Promise,String.prototype.startsWith,XMLHttpRequest,requestAnimationFrame"></script>
-<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v5.3.0/build/ol.js" integrity="sha384-iQkGyyH4ioz3m+maM3s9MX1Oq67mACa4B9Z3ovUv3Sv37LJ96fx3WnZfLoiC3Wfl" crossorigin="anonymous"></script>
+<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.2.1/build/ol.js" integrity="sha384-Dg6Qd9Ovz0HgMsjQJjA+5upeLnRSrseIbJbT86LMVpuzuLfzffIleOxTCHIAUp6y" crossorigin="anonymous"></script>
 <script tyle="text/javascript" src="lang_pl.js?v10" id="lang_script"></script>
 <script tyle="text/javascript" src="common.js?v11"></script>
 <script tyle="text/javascript" src="map.js?v25"></script>
diff --git a/map.js b/map.js
index 6f628c9..4eb5861 100644
--- a/map.js
+++ b/map.js
@@ -1,7 +1,7 @@
 'use strict';
 
-var ttss_refresh = 10000; // 10 seconds
-var ttss_position_type = 'RAW';
+var api_refresh = 10000; // 10 seconds
+var api_poll_url = 'http://127.0.0.1/sub';
 
 var geolocation = null;
 var geolocation_set = 0;
@@ -11,15 +11,9 @@
 var geolocation_source = null;
 var geolocation_layer = null;
 
-var vehicles_xhr = {};
-var vehicles_timer = {};
-var vehicles_last_update = {};
-var vehicles_source = {};
-var vehicles_layer = {};
+var vehicles = {};
+var hash = null;
 
-var vehicles_info = {};
-
-var stops_xhr = null;
 var stops_ignored = ['131', '744', '1263', '3039'];
 var stops_style = {
 	'sb': new ol.style.Style({
@@ -74,8 +68,6 @@
 
 var fail_element = document.getElementById('fail');
 var fail_text = document.querySelector('#fail span');
-
-var ignore_hashchange = false;
 
 
 function Panel(element) {
@@ -212,8 +204,7 @@
 		this.timeout = setTimeout(this.find.bind(this), 100);
 	},
 	open: function(panel) {
-		ignore_hashchange = true;
-		window.location.hash = '#!f';
+		setHash('f');
 		
 		panel.show(this.div, this.close.bind(this));
 		this.input.focus();
@@ -223,6 +214,227 @@
 	},
 };
 
+function Vehicles(prefix) {
+	this.prefix = prefix;
+	this.source = new ol.source.Vector({
+		features: [],
+	});
+	this.layer = new ol.layer.Vector({
+		source: this.source,
+	});
+}
+Vehicles.prototype = {
+	prefix: '',
+	
+	layer: null,
+	source: null,
+	
+	lastUpdate: 0,
+	xhr: null,
+	es: null,
+	
+	selectedFeatureId: null,
+	deselectCallback: null,
+	
+	style: function(feature, clicked) {
+		var color_type = 'black';
+		
+		var vehicleType = vehicles_info.getParsed(feature.getId());
+		if(vehicleType) {
+			switch(vehicleType.low) {
+				case 0:
+					color_type = 'orange';
+				break;
+				case 1:
+				case 2:
+					color_type = 'green';
+				break;
+			}
+		}
+		
+		var fill = '#B70';
+		if(this.prefix === 'b') {
+			fill = '#05B';
+		}
+		if(clicked) {
+			fill = '#922';
+		}
+		
+		var image = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="30"><polygon points="10,0 20,23 0,23" style="fill:'+fill+';stroke:'+color_type+';stroke-width:3"/></svg>';
+		
+		feature.setStyle(new ol.style.Style({
+			image: new ol.style.Icon({
+				src: 'data:image/svg+xml;base64,' + btoa(image),
+				imgSize: [20,30],
+				rotation: Math.PI * feature.get('angle') / 180.0,
+			}),
+			text: new ol.style.Text({
+				font: 'bold 10px sans-serif',
+				// TODO: special directions
+				// vehicle.line = vehicle.name.substr(0, vehicle_name_space);
+				// vehicle.direction = normalizeName(vehicle.name.substr(vehicle_name_space+1));
+				// if(special_directions[vehicle.direction]) {
+				// 	vehicle.line = special_directions[vehicle.direction];
+				// }
+				text: feature.get('name').substr(0, feature.get('name').indexOf(' ')),
+				fill: new ol.style.Fill({color: 'white'}),
+			}),
+		}));
+	},
+	select: function(feature, callback) {
+		if(feature instanceof ol.Feature) {
+			feature = feature.getId();
+		}
+		feature = this.source.getFeatureById(feature);
+		if(!feature) {
+			this.deselect();
+			return;
+		}
+		this.style(feature, true);
+		
+		this.selectedFeatureId = feature.getId();
+		this.deselectCallback = callback;
+	},
+	deselect: function() {
+		if(!this.selectedFeatureId) return false;
+		var feature = this.source.getFeatureById(this.selectedFeatureId);
+		this.style(feature);
+		
+		this._internalDeselect();
+	},
+	_internalDeselect: function() {
+		var callback = this.deselectCallback;
+		this.deselectCallback = null;
+		this.selectedFeatureId = null;
+		if(callback) callback();
+	},
+
+	typesUpdated: function() {
+		this.source.forEachFeature(function (feature) {
+			this.style(feature);
+		}.bind(this));
+	},
+
+	_newFeature: function(id, data) {
+		var feature = new ol.Feature();
+		feature.setId(this.prefix + id);
+		feature.setProperties(data);
+		feature.setGeometry(getGeometryPair(feature.get('pos')));
+		this.style(feature);
+		return feature;
+	},
+	loadFullData: function(data) {
+		var features = [];
+		for(var id in data) {
+			features.push(this._newFeature(id, data[id]));
+		}
+		this.source.clear();
+		this.source.addFeatures(features);
+		
+		if(this.selectedFeatureId) {
+			this.select(this.selectedFeatureId);
+		}
+	},
+	loadDiffData: function(data) {
+		for(var id in data) {
+			var feature = this.source.getFeatureById(this.prefix + id);
+			var vehicle = data[id];
+			
+			// TODO: handle vehicleInfo updates
+			
+			if(vehicle === null) {
+				if(feature) {
+					this.source.removeFeature(feature);
+					if (this.selectedFeatureId === feature.getId()) {
+						this._internalDeselect();
+					}
+				}
+			} else if(feature) {
+				var isPosModified = false;
+				Object.keys(vehicle).forEach(function (key) {
+					feature.set(key, deepMerge(feature.get(key), vehicle[key]));
+					if(key === 'pos') {
+						feature.setGeometry(getGeometryPair(feature.get('pos')));
+					} else if (key === 'angle') {
+						feature.getStyle().getImage().setRotation(Math.PI * parseFloat(vehicle.angle ? vehicle.angle : 0) / 180.0);
+					} else if (key === 'name') {
+						// TODO: Special directions
+						feature.getStyle().getText().setText(vehicle.name.substr(0, vehicle.name.indexOf(' ')));
+					}
+				});
+			} else {
+				this.source.addFeature(this._newFeature(id, data[id]));
+			}
+		}
+	},
+	
+	fetch: function() {
+		var self = this;
+		var result = this.fetchFull();
+
+		// TODO: XHR only as fallback
+		result.done(function() {
+			setTimeout(self.fetchDiff.bind(self), 1);
+		});
+		
+		// TODO: updates (EventSource)
+		// TODO: error handling (reconnect)
+		// TODO: error handling (indicator)
+		
+		return result;
+	},
+	fetchFull: function() {
+		var self = this;
+		this.xhr = $.get(
+			api_poll_url + '?id=' + this.prefix + '-full'
+		).done(function(data) {
+			try {
+				self.lastUpdate = this.request.getResponseHeader('Etag');
+				self.loadFullData(data);
+			} catch(e) {
+				console.log(e);
+				throw e;
+			}
+		}).fail(this.failXhr.bind(this));
+		return this.xhr;
+	},
+	fetchDiff: function() {
+		var self = this;
+		this.xhr = $.get(
+			api_poll_url + '?id=' + this.prefix + '-diff',
+			{'If-None-Match': this.lastUpdate}
+		).done(function(data) {
+			try {
+				if(this.request.status == 304) {
+					setTimeout(self.fetchDiff.bind(self), 1000);
+					return;
+				}
+				self.lastUpdate = this.request.getResponseHeader('Etag');
+				self.loadDiffData(data);
+				setTimeout(self.fetchDiff.bind(self), 1);
+			} catch(e) {
+				console.log(e);
+				throw e;
+			}
+		}).fail(this.failXhr.bind(this));
+		return this.xhr;
+	},
+	
+	failXhr: function(result) {
+		// abort() is not a failure
+		if(result.readyState === 0) return;
+		
+		if(result.status === 0) {
+			fail(lang.error_request_failed_connectivity, result);
+		} else if (result.status === 304) {
+			fail(lang.error_request_failed_no_data, result);
+		} if (result.statusText) {
+			fail(lang.error_request_failed_status.replace('$status', result.statusText), result);
+		} else {
+			fail(lang.error_request_failed, result);
+		}
+	},
+};
 
 function fail(msg) {
 	setText(fail_text, msg);
@@ -250,46 +462,11 @@
 	fail_ajax_generic(data, panel.fail.bind(panel));
 }
 
-function getGeometry(object) {
-	return new ol.geom.Point(ol.proj.fromLonLat([object.longitude / 3600000.0, object.latitude / 3600000.0]));
+function getGeometryPair(pair) {
+	return new ol.geom.Point(ol.proj.fromLonLat(pair));
 }
-
-function styleVehicle(vehicle, selected) {
-	var color_type = 'black';
-	if(vehicle.get('vehicle_type')) {
-		switch(vehicle.get('vehicle_type').low) {
-			case 0:
-				color_type = 'orange';
-			break;
-			case 1:
-			case 2:
-				color_type = 'green';
-			break;
-		}
-	}
-	
-	var fill = '#B70';
-	if(vehicle.getId().startsWith('b')) {
-		fill = '#05B';
-	}
-	if(selected) {
-		fill = '#922';
-	}
-	
-	var image = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="30"><polygon points="10,0 20,23 0,23" style="fill:'+fill+';stroke:'+color_type+';stroke-width:3"/></svg>';
-	
-	vehicle.setStyle(new ol.style.Style({
-		image: new ol.style.Icon({
-			src: 'data:image/svg+xml;base64,' + btoa(image),
-			imgSize: [20,30],
-			rotation: Math.PI * parseFloat(vehicle.get('heading') ? vehicle.get('heading') : 0) / 180.0,
-		}),
-		text: new ol.style.Text({
-			font: 'bold 10px sans-serif',
-			text: vehicle.get('line'),
-			fill: new ol.style.Fill({color: 'white'}),
-		}),
-	}));
+function getGeometry(object) {
+	return getGeometryPair([object.longitude / 3600000.0, object.latitude / 3600000.0]);
 }
 
 function markStops(stops, ttss_type, routeStyle) {
@@ -326,65 +503,9 @@
 function unstyleSelectedFeatures() {
 	stop_selected_source.clear();
 	route_source.clear();
-	if(feature_clicked && ttss_types.includes(feature_clicked.getId().substr(0, 1))) {
-		styleVehicle(feature_clicked);
-	}
-}
-
-function updateVehicles(prefix) {
-	if(vehicles_timer[prefix]) clearTimeout(vehicles_timer[prefix]);
-	if(vehicles_xhr[prefix]) vehicles_xhr[prefix].abort();
-	vehicles_xhr[prefix] = $.get(
-		ttss_urls[prefix] + '/geoserviceDispatcher/services/vehicleinfo/vehicles'
-			+ '?positionType=' + ttss_position_type
-			+ '&colorType=ROUTE_BASED'
-			+ '&lastUpdate=' + encodeURIComponent(vehicles_last_update[prefix])
-	).done(function(data) {
-		vehicles_last_update[prefix] = data.lastUpdate;
-		
-		for(var i = 0; i < data.vehicles.length; i++) {
-			var vehicle = data.vehicles[i];
-			
-			var vehicle_feature = vehicles_source[prefix].getFeatureById(prefix + vehicle.id);
-			if(vehicle.isDeleted || !vehicle.latitude || !vehicle.longitude) {
-				if(vehicle_feature) {
-					vehicles_source[prefix].removeFeature(vehicle_feature);
-					if(feature_clicked && feature_clicked.getId() === vehicle_feature.getId()) {
-						panel.close();
-					}
-				}
-				continue;
-			}
-			
-			var vehicle_name_space = vehicle.name.indexOf(' ');
-			vehicle.line = vehicle.name.substr(0, vehicle_name_space);
-			vehicle.direction = normalizeName(vehicle.name.substr(vehicle_name_space+1));
-			if(special_directions[vehicle.direction]) {
-				vehicle.line = special_directions[vehicle.direction];
-			}
-			
-			vehicle.geometry = getGeometry(vehicle);
-			vehicle.vehicle_type = parseVehicle(prefix + vehicle.id);
-			
-			if(!vehicle_feature) {
-				vehicle_feature = new ol.Feature(vehicle);
-				vehicle_feature.setId(prefix + vehicle.id);
-				
-				styleVehicle(vehicle_feature);
-				vehicles_source[prefix].addFeature(vehicle_feature);
-			} else {
-				vehicle_feature.setProperties(vehicle);
-				vehicle_feature.getStyle().getImage().setRotation(Math.PI * parseFloat(vehicle.heading ? vehicle.heading : 0) / 180.0);
-				vehicle_feature.getStyle().getText().setText(vehicle.line);
-			}
-		}
-		
-		vehicles_timer[prefix] = setTimeout(function() {
-			updateVehicles(prefix);
-		}, ttss_refresh);
-	}).fail(fail_ajax);
-	
-	return vehicles_xhr[prefix];
+	ttss_types.forEach(function(type) {
+		vehicles[type].deselect();
+	});
 }
 
 function updateStopSource(stops, prefix) {
@@ -394,7 +515,7 @@
 	for(var i = 0; i < stops.length; i++) {
 		stop = stops[i];
 		
-		if(stop.category == 'other') continue;
+		if(stop.category === 'other') continue;
 		if(stops_ignored.includes(stop.shortName)) continue;
 		
 		stop.geometry = getGeometry(stop);
@@ -467,7 +588,7 @@
 	
 	feature_xhr = $.get(
 		ttss_urls[ttss_type] + '/services/tripInfo/tripPassages'
-			+ '?tripId=' + encodeURIComponent(feature.get('tripId'))
+			+ '?tripId=' + encodeURIComponent(feature.get('trip'))
 			+ '&mode=departure'
 	).done(function(data) {
 		if(typeof data.old === "undefined" || typeof data.actual === "undefined") {
@@ -506,7 +627,7 @@
 		
 		markStops(stopsToMark, ttss_type, true);
 		
-		feature_timer = setTimeout(function() { vehicleTable(feature, table); }, ttss_refresh);
+		feature_timer = setTimeout(function() { vehicleTable(feature, table); }, api_refresh);
 	}).fail(fail_ajax_popup);
 	return feature_xhr;
 }
@@ -528,7 +649,7 @@
 			tr = document.createElement('tr');
 			addCellWithText(tr, all_departures[i].patternText);
 			dir_cell = addCellWithText(tr, normalizeName(all_departures[i].direction));
-			vehicle = parseVehicle(all_departures[i].vehicleId);
+			vehicle = vehicles_info.getParsed(all_departures[i].vehicleId);
 			dir_cell.appendChild(displayVehicle(vehicle));
 			status = parseStatus(all_departures[i]);
 			status_cell = addCellWithText(tr, status);
@@ -550,7 +671,7 @@
 			table.appendChild(tr);
 		}
 		
-		feature_timer = setTimeout(function() { stopTable(stopType, stopId, table, ttss_type); }, ttss_refresh);
+		feature_timer = setTimeout(function() { stopTable(stopType, stopId, table, ttss_type); }, api_refresh);
 	}).fail(fail_ajax_popup);
 	return feature_xhr;
 }
@@ -592,7 +713,7 @@
 	}
 	// Vehicle
 	else if(ttss_types.includes(type)) {
-		styleVehicle(feature, true);
+		vehicles[type].select(feature);
 		
 		var span = displayVehicle(feature.get('vehicle_type'));
 		
@@ -684,24 +805,19 @@
 	
 	if(tabular_data) {
 		div.appendChild(table);
-		ignore_hashchange = true;
-		window.location.hash = '#!' + feature.getId();
+		hash.set(feature.getId());
 	}
 	
 	showOnMapFunction();
 	
 	panel.show(div, function() {
-		if(!ignore_hashchange) {
-			ignore_hashchange = true;
-			window.location.hash = '';
-			
-			unstyleSelectedFeatures();
-			feature_clicked = null;
-			
-			if(path_xhr) path_xhr.abort();
-			if(feature_xhr) feature_xhr.abort();
-			if(feature_timer) clearTimeout(feature_timer);
-		}
+		hash.set('');
+		
+		unstyleSelectedFeatures();
+		
+		if(path_xhr) path_xhr.abort();
+		if(feature_xhr) feature_xhr.abort();
+		if(feature_timer) clearTimeout(feature_timer);
 	});
 	
 	feature_clicked = feature;
@@ -769,8 +885,8 @@
 			}
 		});
 		ttss_types.forEach(function(type) {
-			if(vehicles_layer[type].getVisible()) {
-				feature = returnClosest(point, feature, vehicles_source[type].getClosestFeatureToCoordinate(point));
+			if(vehicles[type].layer.getVisible()) {
+				feature = returnClosest(point, feature, vehicles[type].source.getClosestFeatureToCoordinate(point));
 			}
 		});
 		
@@ -807,44 +923,76 @@
 	}
 }
 
-
-function hash() {
-	if(ignore_hashchange) {
-		ignore_hashchange = false;
-		return;
-	}
-	
-	var feature = null;
-	var vehicleId = null;
-	var stopId = null;
-	
-	if(window.location.hash.match(/^#!t[0-9]{3}$/)) {
-		vehicleId = depotIdToVehicleId(window.location.hash.substr(3), 't');
-	} else if(window.location.hash.match(/^#!b[0-9]{3}$/)) {
-		vehicleId = depotIdToVehicleId(window.location.hash.substr(3), 'b');
-	} else if(window.location.hash.match(/^#![A-Za-z]{2}[0-9]{3}$/)) {
-		vehicleId = depotIdToVehicleId(window.location.hash.substr(2));
-	} else if(window.location.hash.match(/^#!v-?[0-9]+$/)) {
-		vehicleId = 't' + window.location.hash.substr(3);
-	} else if(window.location.hash.match(/^#![tb]-?[0-9]+$/)) {
-		vehicleId = window.location.hash.substr(2);
-	} else if(window.location.hash.match(/^#![sp]-?[0-9]+$/)) {
-		stopId = window.location.hash.substr(2,1) + 't' + window.location.hash.substr(3);
-	} else if(window.location.hash.match(/^#![sp][tb]-?[0-9]+$/)) {
-		stopId = window.location.hash.substr(2);
-	} else if(window.location.hash.match(/^#!f$/)) {
-		find.open(panel);
-		return;
-	}
-	
-	if(vehicleId) {
-		feature = vehicles_source[vehicleId.substr(0, 1)].getFeatureById(vehicleId);
-	} else if(stopId) {
-		feature = stops_source[stopId.substr(0,2)].getFeatureById(stopId);
-	}
-	
-	featureClicked(feature);
+function Hash() {
 }
+Hash.prototype = {
+	_ignoreChange: false,
+	
+	_set: function(id) {
+		var value = '#!' + id;
+		if(value !== window.location.hash) {
+			window.location.hash = value;
+			return true;
+		}
+		return false;
+	},
+	_updateOld: function() {
+		if(window.location.hash.match(/^#!t[0-9]{3}$/)) {
+			this.go(depotIdToVehicleId(window.location.hash.substr(3), 't'));
+		} else if(window.location.hash.match(/^#!b[0-9]{3}$/)) {
+			this.go(depotIdToVehicleId(window.location.hash.substr(3), 'b'));
+		} else if(window.location.hash.match(/^#![A-Za-z]{2}[0-9]{3}$/)) {
+			this.go(depotIdToVehicleId(window.location.hash.substr(2)));
+		} else if(window.location.hash.match(/^#!v-?[0-9]+$/)) {
+			this.go('t' + window.location.hash.substr(3));
+		}
+	},
+	ready: function() {
+		this._updateOld();
+		this.changed();
+		window.addEventListener('hashchange', this.changed, false);
+	},
+	go: function(id) {
+		this._ignoreChange = false;
+		return this._set(id);
+	},
+	set: function(id) {
+		this._ignoreChange = true;
+		return this._set(id);
+	},
+	changed: function() {
+		if(this._ignoreChange) {
+			this._ignoreChange = false;
+			return false;
+		}
+		
+		var feature = null;
+		var vehicleId = null;
+		var stopId = null;
+		
+		if(window.location.hash.match(/^#![tb]-?[0-9]+$/)) {
+			vehicleId = window.location.hash.substr(2);
+		} else if(window.location.hash.match(/^#![sp]-?[0-9]+$/)) {
+			stopId = window.location.hash.substr(2,1) + 't' + window.location.hash.substr(3);
+		} else if(window.location.hash.match(/^#![sp][tb]-?[0-9]+$/)) {
+			stopId = window.location.hash.substr(2);
+		} else if(window.location.hash.match(/^#!f$/)) {
+			find.open(panel);
+			return;
+		}
+		
+		if(vehicleId) {
+			vehicles[vehicleId.substr(0, 1)].select(vehicleId);
+			return true;
+		} else if(stopId) {
+			feature = stops_source[stopId.substr(0,2)].getFeatureById(stopId);
+		}
+		
+		featureClicked(feature);
+		
+		return true;
+	},
+};
 
 function getDistance(c1, c2) {
 	if(c1.getGeometry) {
@@ -854,8 +1002,8 @@
 		c2 = c2.getGeometry().getCoordinates();
 	}
 	
-	var c1 = ol.proj.transform(c1, 'EPSG:3857', 'EPSG:4326');
-	var c2 = ol.proj.transform(c2, 'EPSG:3857', 'EPSG:4326');
+	c1 = ol.proj.transform(c1, 'EPSG:3857', 'EPSG:4326');
+	c2 = ol.proj.transform(c2, 'EPSG:3857', 'EPSG:4326');
 	return ol.sphere.getDistance(c1, c2);
 }
 
@@ -871,6 +1019,7 @@
 	find = new Find();
 	
 	route_source = new ol.source.Vector({
+		attributions: [lang.help_data_attribution],
 		features: [],
 	});
 	route_layer = new ol.layer.Vector({
@@ -901,14 +1050,7 @@
 	});
 	
 	ttss_types.forEach(function(type) {
-		vehicles_source[type] = new ol.source.Vector({
-			features: [],
-		});
-		vehicles_layer[type] = new ol.layer.Vector({
-			source: vehicles_source[type],
-			renderMode: 'image',
-		});
-		vehicles_last_update[type] = 0;
+		vehicles[type] = new Vehicles(type);
 	});
 	
 	geolocation_feature = new ol.Feature({
@@ -961,11 +1103,15 @@
 	geolocation_button.addEventListener('click', trackingToggle);
 	
 	document.getElementById('find').addEventListener('click', find.open.bind(find, panel));
-	
+
+	var pixelRatio = ol.has.DEVICE_PIXEL_RATIO > 1 ? 2 : 1;
 	var layers = [
 		new ol.layer.Tile({
-			source: new ol.source.OSM({
-				url: 'https://tiles.ttss.pl/{z}/{x}/{y}.png',
+			source: new ol.source.XYZ({
+				attributions: [ol.source.OSM.ATTRIBUTION],
+				url: 'https://tiles.ttss.pl/x' + pixelRatio + '/{z}/{x}/{y}.png',
+				maxZoom: 19,
+				tilePixelRatio: pixelRatio,
 			}),
 		}),
 		route_layer,
@@ -976,7 +1122,7 @@
 	});
 	layers.push(stop_selected_layer);
 	ttss_types.forEach(function(type) {
-		layers.push(vehicles_layer[type]);
+		layers.push(vehicles[type].layer);
 	});
 	map = new ol.Map({
 		target: 'map',
@@ -1031,17 +1177,18 @@
 	change_resolution();
 	
 	var future_requests = [
-		updateVehicleInfo(),
+		vehicles_info.update(),
 	];
 	ttss_types.forEach(function(type) {
-		future_requests.push(updateVehicles(type));
+		vehicles_info.addWatcher(vehicles[type].typesUpdated.bind(vehicles[type]));
+		future_requests.push(vehicles[type].fetch());
 	});
 	stops_type.forEach(function(type) {
 		future_requests.push(updateStops(type.substr(0,1), type.substr(1,1)));
 	});
-	Deferred.all(future_requests).done(hash);
 	
-	window.addEventListener('hashchange', hash);
+	hash = new Hash();
+	Deferred.all(future_requests).done(hash.ready.bind(hash));
 	
 	setTimeout(function() {
 		ttss_types.forEach(function(type) {

--
Gitblit v1.9.1