Improved www.ttss.krakow.pl
Jacek Kowalski
2020-06-09 f1de5f8d11b90e98d77918e839d0bb3fac5462fb
commit | author | age
7e7221 1 'use strict';
f4a54f 2
2bc9da 3 var api_refresh = 10000; // 10 seconds
JK 4 var api_poll_url = 'http://127.0.0.1/sub';
f1de5f 5 var api_poll_refresh = 1000;
57b8d3 6
a4d011 7 var geolocation = null;
JK 8 var geolocation_set = 0;
9 var geolocation_button = null;
10 var geolocation_feature = null;
11 var geolocation_accuracy = null;
12 var geolocation_source = null;
13 var geolocation_layer = null;
14
2bc9da 15 var vehicles = {};
JK 16 var hash = null;
eafc1c 17
0748d2 18 var stops_ignored = ['131', '744', '1263', '3039'];
88a24c 19 var stops_style = {
JK 20     'sb': new ol.style.Style({
21         image: new ol.style.Circle({
22             fill: new ol.style.Fill({color: '#07F'}),
23             stroke: new ol.style.Stroke({color: '#05B', width: 2}),
24             radius: 3,
25         }),
26     }),
27     'st': new ol.style.Style({
28         image: new ol.style.Circle({
29             fill: new ol.style.Fill({color: '#FA0'}),
30             stroke: new ol.style.Stroke({color: '#B70', width: 2}),
31             radius: 3,
32         }),
33     }),
34     'pb': new ol.style.Style({
35         image: new ol.style.Circle({
36             fill: new ol.style.Fill({color: '#07F'}),
37             stroke: new ol.style.Stroke({color: '#05B', width: 1}),
38             radius: 3,
39         }),
40     }),
41     'pt': new ol.style.Style({
42         image: new ol.style.Circle({
43             fill: new ol.style.Fill({color: '#FA0'}),
44             stroke: new ol.style.Stroke({color: '#B70', width: 1}),
45             radius: 3,
46         }),
47     }),
48 };
49 var stops_type = ['st', 'sb', 'pt', 'pb'];
1b7c52 50 var stops_mapping = {};
88a24c 51 var stops_source = {};
JK 52 var stops_layer = {};
f4a54f 53
JK 54 var stop_selected_source = null;
55 var stop_selected_layer = null;
57b8d3 56
1d4785 57 var feature_clicked = null;
8b6250 58 var feature_xhr = null;
JK 59 var feature_timer = null;
9dd2e1 60 var path_xhr = null;
1d4785 61
JK 62 var route_source = null;
63 var route_layer = null;
07c714 64
57b8d3 65 var map = null;
d29c06 66
JK 67 var panel = null;
5be662 68 var find = null;
d29c06 69
57b8d3 70 var fail_element = document.getElementById('fail');
a4d011 71 var fail_text = document.querySelector('#fail span');
7ca6a1 72
d29c06 73
JK 74 function Panel(element) {
75     this._element = element;
76     this._element.classList.add('panel');
77     
ae5170 78     this._hide = addElementWithText(this._element, 'a', '▶');
d29c06 79     this._hide.title = lang.action_collapse;
JK 80     this._hide.className = 'hide';
81     this._hide.addEventListener('click', this.toggleExpanded.bind(this));
82     
ae5170 83     this._close = addElementWithText(this._element, 'a', '×');
d29c06 84     this._close.title = lang.action_close;
JK 85     this._close.className = 'close';
86     this._close.addEventListener('click', this.close.bind(this));
87     
88     this._content = document.createElement('div');
89     this._element.appendChild(this._content);
d5e919 90 }
d29c06 91 Panel.prototype = {
JK 92     _element: null,
93     _hide: null,
94     _close: null,
95     _content: null,
96     
97     _closeCallback: null,
98     _runCallback: function() {
99         var callback = this.closeCallback;
100         this.closeCallback = null;
101         if(callback) callback();
102     },
103     
104     expand: function() {
105         this._element.classList.add('expanded');
106         setText(this._hide, '▶');
107         this._hide.title = lang.action_collapse;
108     },
109     collapse: function() {
110         this._element.classList.remove('expanded');
111         setText(this._hide, '◀');
112         this._hide.title = lang.action_expand;
113     },
114     toggleExpanded: function() {
115         if(this._element.classList.contains('expanded')) {
116             this.collapse();
117         } else {
118             this.expand();
119         }
120     },
121     fail: function(message) {
122         addParaWithText(this._content, message).className = 'error';
123     },
124     show: function(contents, closeCallback) {
125         this._runCallback();
126         this.closeCallback = closeCallback;
127         
128         deleteChildren(this._content);
129         
130         this._content.appendChild(contents);
131         this._element.classList.add('enabled');
132         setTimeout(this.expand.bind(this), 1);
133     },
134     close: function() {
135         this._runCallback();
136         this._element.classList.remove('expanded');
137         this._element.classList.remove('enabled');
138     },
139 };
5be662 140
JK 141
142 function Find() {
143     this.div = document.createElement('div');
144     
145     this.form = document.createElement('form');
146     this.div.appendChild(this.form);
147     
148     var para = addParaWithText(this.form, lang.enter_query);
149     para.appendChild(document.createElement('br'));
150     this.input = document.createElement('input');
151     this.input.type = 'text';
152     this.input.style.width = '80%';
153     para.appendChild(this.input);
154     para.appendChild(document.createElement('hr'));
155     
156     this.results = document.createElement('div');
157     this.div.appendChild(this.results);
158     
159     this.input.addEventListener('keyup', this.findDelay.bind(this));
160     this.form.addEventListener('submit', this.findDelay.bind(this));
161 }
162 Find.prototype = {
163     query: '',
164     timeout: null,
165     
166     div: null,
167     form: null,
168     input: null,
169     results: null,
170     
171     find: function() {
172         var query = this.input.value.toUpperCase();
173         if(query === this.query) return;
174         this.query = query;
175         
94177c 176         if(query === '') {
JK 177             deleteChildren(this.results);
178             return;
179         }
180         
5be662 181         var features = [];
JK 182         stops_type.forEach(function(stop_type) {
183             if(stop_type.substr(0,1) === 'p') return;
184             stops_source[stop_type].forEachFeature(function(feature) {
185                 if(feature.get('name').toUpperCase().indexOf(query) > -1) {
186                     features.push(feature);
187                 }
188             });
189         });
190         
191         ttss_types.forEach(function(ttss_type) {
192             vehicles_source[ttss_type].forEachFeature(function(feature) {
193                 if(feature.get('vehicle_type') && feature.get('vehicle_type').num.indexOf(query) > -1) {
194                     features.push(feature);
195                 }
196             });
197         });
198         
199         deleteChildren(this.results);
200         this.results.appendChild(listFeatures(features));
201     },
202     findDelay: function(e) {
203         e.preventDefault();
204         if(this.timeout) clearTimeout(this.timeout);
205         this.timeout = setTimeout(this.find.bind(this), 100);
206     },
207     open: function(panel) {
2bc9da 208         setHash('f');
5be662 209         
JK 210         panel.show(this.div, this.close.bind(this));
211         this.input.focus();
212     },
213     close: function() {
214         if(this.timeout) clearTimeout(this.timeout);
215     },
216 };
217
2bc9da 218 function Vehicles(prefix) {
JK 219     this.prefix = prefix;
220     this.source = new ol.source.Vector({
221         features: [],
222     });
223     this.layer = new ol.layer.Vector({
224         source: this.source,
225     });
226 }
227 Vehicles.prototype = {
228     prefix: '',
229     
230     layer: null,
231     source: null,
232     
233     lastUpdate: 0,
234     xhr: null,
235     es: null,
236     
237     selectedFeatureId: null,
238     deselectCallback: null,
239     
240     style: function(feature, clicked) {
241         var color_type = 'black';
242         
243         var vehicleType = vehicles_info.getParsed(feature.getId());
244         if(vehicleType) {
245             switch(vehicleType.low) {
246                 case 0:
247                     color_type = 'orange';
248                 break;
249                 case 1:
250                 case 2:
251                     color_type = 'green';
252                 break;
253             }
254         }
255         
256         var fill = '#B70';
257         if(this.prefix === 'b') {
258             fill = '#05B';
259         }
260         if(clicked) {
261             fill = '#922';
262         }
263         
264         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>';
265         
266         feature.setStyle(new ol.style.Style({
267             image: new ol.style.Icon({
268                 src: 'data:image/svg+xml;base64,' + btoa(image),
269                 imgSize: [20,30],
270                 rotation: Math.PI * feature.get('angle') / 180.0,
271             }),
272             text: new ol.style.Text({
273                 font: 'bold 10px sans-serif',
274                 // TODO: special directions
275                 // vehicle.line = vehicle.name.substr(0, vehicle_name_space);
276                 // vehicle.direction = normalizeName(vehicle.name.substr(vehicle_name_space+1));
277                 // if(special_directions[vehicle.direction]) {
278                 //     vehicle.line = special_directions[vehicle.direction];
279                 // }
280                 text: feature.get('name').substr(0, feature.get('name').indexOf(' ')),
281                 fill: new ol.style.Fill({color: 'white'}),
282             }),
283         }));
284     },
285     select: function(feature, callback) {
286         if(feature instanceof ol.Feature) {
287             feature = feature.getId();
288         }
289         feature = this.source.getFeatureById(feature);
290         if(!feature) {
291             this.deselect();
292             return;
293         }
294         this.style(feature, true);
295         
296         this.selectedFeatureId = feature.getId();
297         this.deselectCallback = callback;
298     },
299     deselect: function() {
300         if(!this.selectedFeatureId) return false;
301         var feature = this.source.getFeatureById(this.selectedFeatureId);
302         this.style(feature);
303         
304         this._internalDeselect();
305     },
306     _internalDeselect: function() {
307         var callback = this.deselectCallback;
308         this.deselectCallback = null;
309         this.selectedFeatureId = null;
310         if(callback) callback();
311     },
312
313     typesUpdated: function() {
314         this.source.forEachFeature(function (feature) {
315             this.style(feature);
316         }.bind(this));
317     },
318
319     _newFeature: function(id, data) {
320         var feature = new ol.Feature();
321         feature.setId(this.prefix + id);
322         feature.setProperties(data);
323         feature.setGeometry(getGeometryPair(feature.get('pos')));
324         this.style(feature);
325         return feature;
326     },
327     loadFullData: function(data) {
328         var features = [];
329         for(var id in data) {
330             features.push(this._newFeature(id, data[id]));
331         }
332         this.source.clear();
333         this.source.addFeatures(features);
334         
335         if(this.selectedFeatureId) {
336             this.select(this.selectedFeatureId);
337         }
338     },
339     loadDiffData: function(data) {
340         for(var id in data) {
341             var feature = this.source.getFeatureById(this.prefix + id);
342             var vehicle = data[id];
343             
344             // TODO: handle vehicleInfo updates
345             
346             if(vehicle === null) {
347                 if(feature) {
348                     this.source.removeFeature(feature);
349                     if (this.selectedFeatureId === feature.getId()) {
350                         this._internalDeselect();
351                     }
352                 }
353             } else if(feature) {
354                 var isPosModified = false;
355                 Object.keys(vehicle).forEach(function (key) {
356                     feature.set(key, deepMerge(feature.get(key), vehicle[key]));
357                     if(key === 'pos') {
358                         feature.setGeometry(getGeometryPair(feature.get('pos')));
359                     } else if (key === 'angle') {
360                         feature.getStyle().getImage().setRotation(Math.PI * parseFloat(vehicle.angle ? vehicle.angle : 0) / 180.0);
361                     } else if (key === 'name') {
362                         // TODO: Special directions
363                         feature.getStyle().getText().setText(vehicle.name.substr(0, vehicle.name.indexOf(' ')));
364                     }
365                 });
366             } else {
367                 this.source.addFeature(this._newFeature(id, data[id]));
368             }
369         }
370     },
371     
372     fetch: function() {
373         var self = this;
374         var result = this.fetchFull();
375
376         // TODO: XHR only as fallback
377         result.done(function() {
378             setTimeout(self.fetchDiff.bind(self), 1);
379         });
380         
381         // TODO: updates (EventSource)
382         // TODO: error handling (reconnect)
383         // TODO: error handling (indicator)
384         
385         return result;
386     },
387     fetchFull: function() {
388         var self = this;
389         this.xhr = $.get(
390             api_poll_url + '?id=' + this.prefix + '-full'
391         ).done(function(data) {
392             try {
393                 self.lastUpdate = this.request.getResponseHeader('Etag');
394                 self.loadFullData(data);
395             } catch(e) {
396                 console.log(e);
397                 throw e;
398             }
399         }).fail(this.failXhr.bind(this));
400         return this.xhr;
401     },
402     fetchDiff: function() {
403         var self = this;
404         this.xhr = $.get(
405             api_poll_url + '?id=' + this.prefix + '-diff',
406             {'If-None-Match': this.lastUpdate}
407         ).done(function(data) {
408             try {
409                 if(this.request.status == 304) {
f1de5f 410                     setTimeout(self.fetchDiff.bind(self), api_poll_refresh);
2bc9da 411                     return;
JK 412                 }
413                 self.lastUpdate = this.request.getResponseHeader('Etag');
414                 self.loadDiffData(data);
415                 setTimeout(self.fetchDiff.bind(self), 1);
416             } catch(e) {
417                 console.log(e);
418                 throw e;
419             }
420         }).fail(this.failXhr.bind(this));
421         return this.xhr;
422     },
423     
424     failXhr: function(result) {
425         // abort() is not a failure
426         if(result.readyState === 0) return;
427         
428         if(result.status === 0) {
429             fail(lang.error_request_failed_connectivity, result);
430         } else if (result.status === 304) {
431             fail(lang.error_request_failed_no_data, result);
432         } if (result.statusText) {
433             fail(lang.error_request_failed_status.replace('$status', result.statusText), result);
434         } else {
435             fail(lang.error_request_failed, result);
436         }
437     },
438 };
d29c06 439
57b8d3 440 function fail(msg) {
a4d011 441     setText(fail_text, msg);
57b8d3 442     fail_element.style.top = '0.5em';
8b6250 443 }
JK 444
445 function fail_ajax_generic(data, fnc) {
57b8d3 446     // abort() is not a failure
faad2a 447     if(data.readyState === 0) return;
57b8d3 448     
faad2a 449     if(data.status === 0) {
8b6250 450         fnc(lang.error_request_failed_connectivity, data);
57b8d3 451     } else if (data.statusText) {
8b6250 452         fnc(lang.error_request_failed_status.replace('$status', data.statusText), data);
57b8d3 453     } else {
8b6250 454         fnc(lang.error_request_failed, data);
57b8d3 455     }
8b6250 456 }
JK 457
458 function fail_ajax(data) {
459     fail_ajax_generic(data, fail);
460 }
461
462 function fail_ajax_popup(data) {
d29c06 463     fail_ajax_generic(data, panel.fail.bind(panel));
57b8d3 464 }
JK 465
2bc9da 466 function getGeometryPair(pair) {
JK 467     return new ol.geom.Point(ol.proj.fromLonLat(pair));
57b8d3 468 }
2bc9da 469 function getGeometry(object) {
JK 470     return getGeometryPair([object.longitude / 3600000.0, object.latitude / 3600000.0]);
1d4785 471 }
JK 472
4bfa36 473 function markStops(stops, ttss_type, routeStyle) {
f4a54f 474     stop_selected_source.clear();
ba6e87 475     
4bfa36 476     var style = stops_layer['s' + ttss_type].getStyle().clone();
f4a54f 477     
JK 478     if(routeStyle) {
479         style.getImage().setRadius(5);
480     } else {
481         style.getImage().getStroke().setWidth(2);
482         style.getImage().getStroke().setColor('#F00');
483         style.getImage().setRadius(5);
ba6e87 484     }
1d4785 485     
f4a54f 486     stop_selected_layer.setStyle(style);
JK 487     
db4410 488     var feature, prefix;
f4a54f 489     for(var i = 0; i < stops.length; i++) {
JK 490         if(stops[i].getId) {
491             feature = stops[i];
492         } else {
493             prefix = stops[i].substr(0,2);
88a24c 494             feature = stops_source[prefix].getFeatureById(stops[i]);
f4a54f 495         }
JK 496         if(feature) {
497             stop_selected_source.addFeature(feature);
498         }
1d4785 499     }
JK 500     
f4a54f 501     stop_selected_layer.setVisible(true);
1d4785 502 }
JK 503
504 function unstyleSelectedFeatures() {
f4a54f 505     stop_selected_source.clear();
JK 506     route_source.clear();
2bc9da 507     ttss_types.forEach(function(type) {
JK 508         vehicles[type].deselect();
509     });
57b8d3 510 }
JK 511
88a24c 512 function updateStopSource(stops, prefix) {
JK 513     var source = stops_source[prefix];
1b7c52 514     var mapping = stops_mapping[prefix];
7e7221 515     var stop;
57b8d3 516     for(var i = 0; i < stops.length; i++) {
7e7221 517         stop = stops[i];
e61357 518         
2bc9da 519         if(stop.category === 'other') continue;
2b6454 520         if(stops_ignored.includes(stop.shortName)) continue;
e61357 521         
57b8d3 522         stop.geometry = getGeometry(stop);
JK 523         var stop_feature = new ol.Feature(stop);
1b7c52 524         
JK 525         if(prefix.startsWith('p')) {
526             mapping[stop.stopPoint] = stop_feature;
527         } else {
528             mapping[stop.shortName] = stop_feature;
529         }
57b8d3 530         
JK 531         stop_feature.setId(prefix + stop.id);
532         
533         source.addFeature(stop_feature);
534     }
535 }
536
4bfa36 537 function updateStops(stop_type, ttss_type) {
JK 538     var methods = {
539         's': 'stops',
540         'p': 'stopPoints',
541     };
7ca6a1 542     return $.get(
4bfa36 543         ttss_urls[ttss_type] + '/geoserviceDispatcher/services/stopinfo/' + methods[stop_type]
57b8d3 544             + '?left=-648000000'
JK 545             + '&bottom=-324000000'
546             + '&right=648000000'
547             + '&top=324000000'
548     ).done(function(data) {
4bfa36 549         updateStopSource(data[methods[stop_type]], stop_type + ttss_type);
57b8d3 550     }).fail(fail_ajax);
7ca6a1 551 }
JK 552
7e7221 553 function vehiclePath(feature) {
9dd2e1 554     if(path_xhr) path_xhr.abort();
JK 555     
556     var featureId = feature.getId();
4bfa36 557     var ttss_type = featureId.substr(0, 1);
eafc1c 558     
9dd2e1 559     path_xhr = $.get(
4bfa36 560         ttss_urls[ttss_type] + '/geoserviceDispatcher/services/pathinfo/vehicle'
JK 561             + '?id=' + encodeURIComponent(featureId.substr(1))
9dd2e1 562     ).done(function(data) {
JK 563         if(!data || !data.paths || !data.paths[0] || !data.paths[0].wayPoints) return;
564         
db4410 565         var point;
9dd2e1 566         var points = [];
JK 567         for(var i = 0; i < data.paths[0].wayPoints.length; i++) {
568             point = data.paths[0].wayPoints[i];
569             points.push(ol.proj.fromLonLat([
570                 point.lon / 3600000.0,
571                 point.lat / 3600000.0,
572             ]));
573         }
574         
575         route_source.addFeature(new ol.Feature({
576             geometry: new ol.geom.LineString(points)
577         }));
578         route_layer.setVisible(true);
579     });
2b6454 580     return path_xhr;
9dd2e1 581 }
JK 582
583 function vehicleTable(feature, table) {
584     if(feature_xhr) feature_xhr.abort();
585     if(feature_timer) clearTimeout(feature_timer);
586     
587     var featureId = feature.getId();
4bfa36 588     var ttss_type = featureId.substr(0, 1);
eafc1c 589     
8b6250 590     feature_xhr = $.get(
4bfa36 591         ttss_urls[ttss_type] + '/services/tripInfo/tripPassages'
2bc9da 592             + '?tripId=' + encodeURIComponent(feature.get('trip'))
8b6250 593             + '&mode=departure'
JK 594     ).done(function(data) {
b6f8e3 595         if(typeof data.old === "undefined" || typeof data.actual === "undefined") {
8b6250 596             return;
JK 597         }
598         
599         deleteChildren(table);
600         
cb5a77 601         var all_departures = data.old.concat(data.actual);
db4410 602         var tr;
f4a54f 603         var stopsToMark = [];
cb5a77 604         for(var i = 0, il = all_departures.length; i < il; i++) {
db4410 605             tr = document.createElement('tr');
cb5a77 606             addCellWithText(tr, all_departures[i].actualTime || all_departures[i].plannedTime);
ca42d3 607             addCellWithText(tr, all_departures[i].stop_seq_num + '. ' + normalizeName(all_departures[i].stop.name));
1d4785 608             
cb5a77 609             if(i >= data.old.length) {
JK 610                 stopsToMark.push('s' + ttss_type + all_departures[i].stop.id);
611             }
8b6250 612             
cb5a77 613             if(i < data.old.length) {
JK 614                 tr.className = 'active';
615             } else if(all_departures[i].status === 'STOPPING') {
8b6250 616                 tr.className = 'success';
JK 617             }
618             table.appendChild(tr);
619         }
f4a54f 620         
b6f8e3 621         if(all_departures.length === 0) {
JK 622             tr = document.createElement('tr');
623             table.appendChild(tr);
624             tr = addCellWithText(tr, lang.no_data);
625             tr.colSpan = '2';
626             tr.className = 'active';
627         }
628         
4bfa36 629         markStops(stopsToMark, ttss_type, true);
8b6250 630         
2bc9da 631         feature_timer = setTimeout(function() { vehicleTable(feature, table); }, api_refresh);
8b6250 632     }).fail(fail_ajax_popup);
2b6454 633     return feature_xhr;
8b6250 634 }
JK 635
0ba749 636 function stopTable(stopType, stopId, table, ttss_type) {
8b6250 637     if(feature_xhr) feature_xhr.abort();
JK 638     if(feature_timer) clearTimeout(feature_timer);
eafc1c 639     
8b6250 640     feature_xhr = $.get(
4bfa36 641         ttss_urls[ttss_type] + '/services/passageInfo/stopPassages/' + stopType
8b6250 642             + '?' + stopType + '=' + encodeURIComponent(stopId)
JK 643             + '&mode=departure'
644     ).done(function(data) {
645         deleteChildren(table);
646         
cb5a77 647         var all_departures = data.old.concat(data.actual);
db4410 648         var tr, dir_cell, vehicle, status, status_cell, delay, delay_cell;
cb5a77 649         for(var i = 0, il = all_departures.length; i < il; i++) {
db4410 650             tr = document.createElement('tr');
cb5a77 651             addCellWithText(tr, all_departures[i].patternText);
ca42d3 652             dir_cell = addCellWithText(tr, normalizeName(all_departures[i].direction));
2bc9da 653             vehicle = vehicles_info.getParsed(all_departures[i].vehicleId);
8b6250 654             dir_cell.appendChild(displayVehicle(vehicle));
cb5a77 655             status = parseStatus(all_departures[i]);
db4410 656             status_cell = addCellWithText(tr, status);
cb5a77 657             delay = parseDelay(all_departures[i]);
db4410 658             delay_cell = addCellWithText(tr, delay);
8b6250 659             
cb5a77 660             if(i < data.old.length) {
db4410 661                 tr.className = 'active';
cb5a77 662             } else if(status === lang.boarding_sign) {
8b6250 663                 tr.className = 'success';
JK 664                 status_cell.className = 'status-boarding';
665             } else if(parseInt(delay) > 9) {
666                 tr.className = 'danger';
667                 delay_cell.className = 'status-delayed';
668             } else if(parseInt(delay) > 3) {
669                 tr.className = 'warning';
670             }
671             
672             table.appendChild(tr);
673         }
674         
2bc9da 675         feature_timer = setTimeout(function() { stopTable(stopType, stopId, table, ttss_type); }, api_refresh);
8b6250 676     }).fail(fail_ajax_popup);
2b6454 677     return feature_xhr;
8b6250 678 }
JK 679
7ca6a1 680 function featureClicked(feature) {
1d4785 681     if(feature && !feature.getId()) return;
JK 682     
683     unstyleSelectedFeatures();
684     
7ca6a1 685     if(!feature) {
d29c06 686         panel.close();
7ca6a1 687         return;
JK 688     }
689     
9f0f6a 690     var div = document.createElement('div');
8b6250 691     
ca42d3 692     var name = normalizeName(feature.get('name'));
07c714 693     var additional;
8b6250 694     var table = document.createElement('table');
JK 695     var thead = document.createElement('thead');
696     var tbody = document.createElement('tbody');
697     table.appendChild(thead);
698     table.appendChild(tbody);
07c714 699     
a4d011 700     var tabular_data = true;
JK 701     
4bfa36 702     var type = feature.getId().substr(0, 1);
76f5c4 703     var full_type = feature.getId().match(/^[a-z]+/)[0];
JK 704     var typeName = lang.types[full_type];
705     if(typeof typeName === 'undefined') {
706         typeName = '';
707     }
708     
4bfa36 709     // Location
JK 710     if(type == 'l') {
711         tabular_data = false;
76f5c4 712         name = typeName;
4bfa36 713         typeName = '';
JK 714     }
715     // Vehicle
2b6454 716     else if(ttss_types.includes(type)) {
2bc9da 717         vehicles[type].select(feature);
d5e919 718         
4bfa36 719         var span = displayVehicle(feature.get('vehicle_type'));
JK 720         
721         additional = document.createElement('p');
722         if(span.title) {
723             setText(additional, span.title);
724         } else {
725             setText(additional, feature.getId());
726         }
727         additional.insertBefore(span, additional.firstChild);
728         
729         addElementWithText(thead, 'th', lang.header_time);
730         addElementWithText(thead, 'th', lang.header_stop);
731         
732         vehicleTable(feature, tbody);
733         vehiclePath(feature);
734     }
735     // Stop or stop point
2b6454 736     else if(['s', 'p'].includes(type)) {
0ba749 737         var ttss_type = feature.getId().substr(1, 1);
4bfa36 738         if(type == 's') {
1b7c52 739             var second_type = lang.departures_for_buses;
JK 740             var mapping = stops_mapping['sb'];
4bfa36 741             
0ba749 742             if(ttss_type == 'b') {
1b7c52 743                 second_type = lang.departures_for_trams;
JK 744                 mapping = stops_mapping['st'];
745             }
0ba749 746             
JK 747             stopTable('stop', feature.get('shortName'), tbody, ttss_type);
1b7c52 748             
JK 749             if(mapping[feature.get('shortName')]) {
750                 additional = document.createElement('p');
751                 additional.className = 'small';
752                 addElementWithText(additional, 'a', second_type).addEventListener(
753                     'click',
754                     function() {
755                         featureClicked(mapping[feature.get('shortName')]);
756                     }
757                 );
a83099 758             }
4bfa36 759         } else {
0ba749 760             stopTable('stopPoint', feature.get('stopPoint'), tbody, ttss_type);
8b6250 761             
JK 762             additional = document.createElement('p');
763             additional.className = 'small';
764             addElementWithText(additional, 'a', lang.departures_for_stop).addEventListener(
765                 'click',
766                 function() {
0ba749 767                     var mapping = stops_mapping['s' + ttss_type];
1b7c52 768                     featureClicked(mapping[feature.get('shortName')]);
8b6250 769                 }
JK 770             );
4bfa36 771         }
JK 772         
773         addElementWithText(thead, 'th', lang.header_line);
774         addElementWithText(thead, 'th', lang.header_direction);
775         addElementWithText(thead, 'th', lang.header_time);
776         addElementWithText(thead, 'th', lang.header_delay);
777         
778         markStops([feature], feature.getId().substr(1,1));
779     } else {
780         panel.close();
781         return;
07c714 782     }
8b6250 783     
JK 784     var loader = addElementWithText(tbody, 'td', lang.loading);
785     loader.className = 'active';
ee4e7c 786     loader.colSpan = thead.childNodes.length;
07c714 787     
4bfa36 788     addParaWithText(div, typeName).className = 'type';
ae3207 789     
JK 790     var nameElement = addParaWithText(div, name + ' ');
791     nameElement.className = 'name';
792     
793     var showOnMapElement = addElementWithText(nameElement, 'a', lang.show_on_map);
794     var showOnMapFunction = function() {
795         setTimeout(function () {map.getView().animate({
796             center: feature.getGeometry().getCoordinates(),
797         })}, 10);
798     };
799     showOnMapElement.addEventListener('click', showOnMapFunction);
20d39d 800     showOnMapElement.className = 'icon icon-pin';
ae3207 801     showOnMapElement.title = lang.show_on_map;
07c714 802     
JK 803     if(additional) {
9f0f6a 804         div.appendChild(additional);
7ca6a1 805     }
JK 806     
a4d011 807     if(tabular_data) {
JK 808         div.appendChild(table);
2bc9da 809         hash.set(feature.getId());
a4d011 810     }
7ca6a1 811     
ae3207 812     showOnMapFunction();
9f0f6a 813     
d29c06 814     panel.show(div, function() {
2bc9da 815         hash.set('');
JK 816         
817         unstyleSelectedFeatures();
818         
819         if(path_xhr) path_xhr.abort();
820         if(feature_xhr) feature_xhr.abort();
821         if(feature_timer) clearTimeout(feature_timer);
9f0f6a 822     });
07c714 823     
1d4785 824     feature_clicked = feature;
a4d011 825 }
JK 826
5be662 827 function listFeatures(features) {
JK 828     var div = document.createElement('div');
829     
d5e919 830     if(features.length === 0) {
94177c 831         addParaWithText(div, lang.no_results);
JK 832         return div;
833     }
834     
5be662 835     addParaWithText(div, lang.select_feature);
JK 836     
837     var feature, p, a, full_type, typeName;
838     for(var i = 0; i < features.length; i++) {
839         feature = features[i];
840         
841         p = document.createElement('p');
842         a = document.createElement('a');
843         p.appendChild(a);
844         a.addEventListener('click', function(feature) { return function() {
845             featureClicked(feature);
846         }}(feature));
847         
848         full_type = feature.getId().match(/^[a-z]+/)[0];
849         typeName = lang.types[full_type];
850         if(typeof typeName === 'undefined') {
851             typeName = '';
852         }
853         if(feature.get('vehicle_type')) {
854             typeName += ' ' + feature.get('vehicle_type').num;
855         }
856         
857         addElementWithText(a, 'span', typeName).className = 'small';
858         a.appendChild(document.createTextNode(' '));
859         addElementWithText(a, 'span', normalizeName(feature.get('name')));
860         
861         div.appendChild(p);
862     }
863     
864     return div;
865 }
866
a4d011 867 function mapClicked(e) {
JK 868     var point = e.coordinate;
869     var features = [];
870     map.forEachFeatureAtPixel(e.pixel, function(feature, layer) {
871         if(layer == stop_selected_layer) return;
872         if(feature.getId()) features.push(feature);
873     });
874     
7e7221 875     var feature = features[0];
JK 876     
a4d011 877     if(features.length > 1) {
5be662 878         panel.show(listFeatures(features));
a4d011 879         return;
JK 880     }
881     
882     if(!feature) {
88a24c 883         stops_type.forEach(function(type) {
JK 884             if(stops_layer[type].getVisible()) {
885                 feature = returnClosest(point, feature, stops_source[type].getClosestFeatureToCoordinate(point));
886             }
887         });
4bfa36 888         ttss_types.forEach(function(type) {
2bc9da 889             if(vehicles[type].layer.getVisible()) {
JK 890                 feature = returnClosest(point, feature, vehicles[type].source.getClosestFeatureToCoordinate(point));
4bfa36 891             }
JK 892         });
a4d011 893         
JK 894         if(getDistance(point, feature) > map.getView().getResolution() * 20) {
895             feature = null;
896         }
897     }
898     
899     featureClicked(feature);
900 }
901
902 function trackingStop() {
d29c06 903     geolocation_button.classList.remove('clicked');
a4d011 904     geolocation.setTracking(false);
JK 905     
906     geolocation_source.clear();
907 }
908 function trackingStart() {
909     geolocation_set = 0;
d29c06 910     geolocation_button.classList.add('clicked');
a4d011 911     geolocation_feature.setGeometry(new ol.geom.Point(map.getView().getCenter()));
JK 912     geolocation_accuracy.setGeometry(new ol.geom.Circle(map.getView().getCenter(), 100000));
913     
914     geolocation_source.addFeature(geolocation_feature);
915     geolocation_source.addFeature(geolocation_accuracy);
916     
917     geolocation.setTracking(true);
918 }
919 function trackingToggle() {
920     if(geolocation.getTracking()) {
921         trackingStop();
922     } else {
923         trackingStart();
924     }
7ca6a1 925 }
JK 926
2bc9da 927 function Hash() {
57b8d3 928 }
2bc9da 929 Hash.prototype = {
JK 930     _ignoreChange: false,
931     
932     _set: function(id) {
933         var value = '#!' + id;
934         if(value !== window.location.hash) {
935             window.location.hash = value;
936             return true;
937         }
938         return false;
939     },
940     _updateOld: function() {
941         if(window.location.hash.match(/^#!t[0-9]{3}$/)) {
942             this.go(depotIdToVehicleId(window.location.hash.substr(3), 't'));
943         } else if(window.location.hash.match(/^#!b[0-9]{3}$/)) {
944             this.go(depotIdToVehicleId(window.location.hash.substr(3), 'b'));
945         } else if(window.location.hash.match(/^#![A-Za-z]{2}[0-9]{3}$/)) {
946             this.go(depotIdToVehicleId(window.location.hash.substr(2)));
947         } else if(window.location.hash.match(/^#!v-?[0-9]+$/)) {
948             this.go('t' + window.location.hash.substr(3));
949         }
950     },
951     ready: function() {
952         this._updateOld();
953         this.changed();
954         window.addEventListener('hashchange', this.changed, false);
955     },
956     go: function(id) {
957         this._ignoreChange = false;
958         return this._set(id);
959     },
960     set: function(id) {
961         this._ignoreChange = true;
962         return this._set(id);
963     },
964     changed: function() {
965         if(this._ignoreChange) {
966             this._ignoreChange = false;
967             return false;
968         }
969         
970         var feature = null;
971         var vehicleId = null;
972         var stopId = null;
973         
974         if(window.location.hash.match(/^#![tb]-?[0-9]+$/)) {
975             vehicleId = window.location.hash.substr(2);
976         } else if(window.location.hash.match(/^#![sp]-?[0-9]+$/)) {
977             stopId = window.location.hash.substr(2,1) + 't' + window.location.hash.substr(3);
978         } else if(window.location.hash.match(/^#![sp][tb]-?[0-9]+$/)) {
979             stopId = window.location.hash.substr(2);
980         } else if(window.location.hash.match(/^#!f$/)) {
981             find.open(panel);
982             return;
983         }
984         
985         if(vehicleId) {
986             vehicles[vehicleId.substr(0, 1)].select(vehicleId);
987             return true;
988         } else if(stopId) {
989             feature = stops_source[stopId.substr(0,2)].getFeatureById(stopId);
990         }
991         
992         featureClicked(feature);
993         
994         return true;
995     },
996 };
57b8d3 997
0e60d1 998 function getDistance(c1, c2) {
JK 999     if(c1.getGeometry) {
1000         c1 = c1.getGeometry().getCoordinates();
1001     }
1002     if(c2.getGeometry) {
1003         c2 = c2.getGeometry().getCoordinates();
1004     }
1005     
2bc9da 1006     c1 = ol.proj.transform(c1, 'EPSG:3857', 'EPSG:4326');
JK 1007     c2 = ol.proj.transform(c2, 'EPSG:3857', 'EPSG:4326');
a8a6d1 1008     return ol.sphere.getDistance(c1, c2);
0e60d1 1009 }
JK 1010
1011 function returnClosest(point, f1, f2) {
1012     if(!f1) return f2;
1013     if(!f2) return f1;
1014     
1b7c52 1015     return (getDistance(point, f1) <= getDistance(point, f2)) ? f1 : f2;
0e60d1 1016 }
JK 1017
57b8d3 1018 function init() {
d29c06 1019     panel = new Panel(document.getElementById('panel'));
5be662 1020     find = new Find();
57b8d3 1021     
4bfa36 1022     route_source = new ol.source.Vector({
2bc9da 1023         attributions: [lang.help_data_attribution],
4bfa36 1024         features: [],
JK 1025     });
1026     route_layer = new ol.layer.Vector({
1027         source: route_source,
1028         style: new ol.style.Style({
1029             stroke: new ol.style.Stroke({ color: [255, 153, 0, .8], width: 5 })
1030         }),
1031     });
1032     
88a24c 1033     stops_type.forEach(function(type) {
JK 1034         stops_source[type] = new ol.source.Vector({
1035             features: [],
1036         });
1037         stops_layer[type] = new ol.layer.Vector({
1038             source: stops_source[type],
1039             renderMode: 'image',
1040             style: stops_style[type],
1041         });
1b7c52 1042         stops_mapping[type] = {};
f4a54f 1043     });
JK 1044     
1045     stop_selected_source = new ol.source.Vector({
1046         features: [],
1047     });
1048     stop_selected_layer = new ol.layer.Vector({
1049         source: stop_selected_source,
57b8d3 1050         visible: false,
JK 1051     });
1052     
4bfa36 1053     ttss_types.forEach(function(type) {
2bc9da 1054         vehicles[type] = new Vehicles(type);
1d4785 1055     });
JK 1056     
a4d011 1057     geolocation_feature = new ol.Feature({
JK 1058         name: '',
1059         style: new ol.style.Style({
1060             image: new ol.style.Circle({
1061                 fill: new ol.style.Fill({color: '#39C'}),
1062                 stroke: new ol.style.Stroke({color: '#FFF', width: 2}),
1063                 radius: 5,
1064             }),
1065         }),
1066     });
1067     geolocation_feature.setId('location_point');
1068     geolocation_accuracy = new ol.Feature();
1069     geolocation_source = new ol.source.Vector({
1070         features: [],
1071     });
1072     geolocation_layer = new ol.layer.Vector({
1073         source: geolocation_source,
1074     });
19a338 1075     geolocation_button = document.querySelector('#track');
a4d011 1076     if(!navigator.geolocation) {
19a338 1077         geolocation_button.remove();
a4d011 1078     }
JK 1079     
376c6e 1080     geolocation = new ol.Geolocation({projection: 'EPSG:3857'});
a4d011 1081     geolocation.on('change:position', function() {
JK 1082         var coordinates = geolocation.getPosition();
1083         geolocation_feature.setGeometry(coordinates ? new ol.geom.Point(coordinates) : null);
1084         if(geolocation_set < 1) {
1085             geolocation_set = 1;
1086             map.getView().animate({
1087                 center: coordinates,
1088             })
1089         }
1090     });
1091     geolocation.on('change:accuracyGeometry', function() {
1092         var accuracy = geolocation.getAccuracyGeometry();
1093         geolocation_accuracy.setGeometry(accuracy);
1094         if(geolocation_set < 2) {
1095             geolocation_set = 2;
1096             map.getView().fit(accuracy);
1097         }
1098     });
1099     geolocation.on('error', function(error) {
1100         fail(lang.error_location + ' ' + error.message);
1101         trackingStop();
19a338 1102         geolocation_button.remove();
a4d011 1103     });
JK 1104     geolocation_button.addEventListener('click', trackingToggle);
1105     
5be662 1106     document.getElementById('find').addEventListener('click', find.open.bind(find, panel));
2bc9da 1107
JK 1108     var pixelRatio = ol.has.DEVICE_PIXEL_RATIO > 1 ? 2 : 1;
4bfa36 1109     var layers = [
JK 1110         new ol.layer.Tile({
2bc9da 1111             source: new ol.source.XYZ({
JK 1112                 attributions: [ol.source.OSM.ATTRIBUTION],
1113                 url: 'https://tiles.ttss.pl/x' + pixelRatio + '/{z}/{x}/{y}.png',
1114                 maxZoom: 19,
1115                 tilePixelRatio: pixelRatio,
428023 1116             }),
4bfa36 1117         }),
JK 1118         route_layer,
1119         geolocation_layer,
1120     ];
1121     stops_type.forEach(function(type) {
1122         layers.push(stops_layer[type]);
1123     });
1124     layers.push(stop_selected_layer);
1125     ttss_types.forEach(function(type) {
2bc9da 1126         layers.push(vehicles[type].layer);
4bfa36 1127     });
57b8d3 1128     map = new ol.Map({
JK 1129         target: 'map',
4bfa36 1130         layers: layers,
57b8d3 1131         view: new ol.View({
JK 1132             center: ol.proj.fromLonLat([19.94, 50.06]),
a4d011 1133             zoom: 14,
JK 1134             maxZoom: 19,
57b8d3 1135         }),
JK 1136         controls: ol.control.defaults({
1137             attributionOptions: ({
1138                 collapsible: false,
1139             })
1140         }).extend([
1141             new ol.control.Control({
1142                 element: document.getElementById('title'),
1143             }),
1144             new ol.control.Control({
1145                 element: fail_element,
a4d011 1146             }),
JK 1147             new ol.control.Control({
19a338 1148                 element: document.getElementById('menu'),
a4d011 1149             }),
57b8d3 1150         ]),
f4a54f 1151         loadTilesWhileAnimating: false,
57b8d3 1152     });
JK 1153     
1154     // Display popup on click
a4d011 1155     map.on('singleclick', mapClicked);
9f0f6a 1156     
JK 1157     fail_element.addEventListener('click', function() {
1158         fail_element.style.top = '-10em';
1159     });
f0bae0 1160     
57b8d3 1161     // Change mouse cursor when over marker
JK 1162     map.on('pointermove', function(e) {
1163         var hit = map.hasFeatureAtPixel(e.pixel);
1164         var target = map.getTargetElement();
1165         target.style.cursor = hit ? 'pointer' : '';
1166     });
1167     
1168     // Change layer visibility on zoom
7e7221 1169     var change_resolution = function() {
88a24c 1170         stops_type.forEach(function(type) {
JK 1171             if(type.startsWith('p')) {
1172                 stops_layer[type].setVisible(map.getView().getZoom() >= 16);
1173                 stops_layer[type].setVisible(map.getView().getZoom() >= 16);
1174             }
1175         });
1176     };
1177     map.getView().on('change:resolution', change_resolution);
1178     change_resolution();
57b8d3 1179     
4bfa36 1180     var future_requests = [
2bc9da 1181         vehicles_info.update(),
4bfa36 1182     ];
JK 1183     ttss_types.forEach(function(type) {
2bc9da 1184         vehicles_info.addWatcher(vehicles[type].typesUpdated.bind(vehicles[type]));
JK 1185         future_requests.push(vehicles[type].fetch());
7ca6a1 1186     });
4bfa36 1187     stops_type.forEach(function(type) {
JK 1188         future_requests.push(updateStops(type.substr(0,1), type.substr(1,1)));
1189     });
7ca6a1 1190     
2bc9da 1191     hash = new Hash();
JK 1192     Deferred.all(future_requests).done(hash.ready.bind(hash));
57b8d3 1193     
JK 1194     setTimeout(function() {
ae3207 1195         ttss_types.forEach(function(type) {
JK 1196             if(vehicles_xhr[type]) {
1197                 vehicles_xhr[type].abort();
1198             }
1199             if(vehicles_timer[type]) {
1200                 clearTimeout(vehicles_timer[type]);
1201             }
1202         });
1203         
57b8d3 1204         fail(lang.error_refresh);
JK 1205     }, 1800000);
1206 }
1207
1208 init();