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