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