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