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