Improved www.ttss.krakow.pl
Jacek Kowalski
2018-09-23 f7c4c3ce721aaf6dfe4f82c2043156b60ee48e37
commit | author | age
57b8d3 1 //var ttss_base = 'http://www.ttss.krakow.pl/internetservice';
8b6250 2 var ttss_base = 'proxy.php';
f36d1f 3 var ttss_refresh = 10000; // 10 seconds
57b8d3 4
JK 5 var vehicles_xhr = null;
6 var vehicles_timer = null;
7 var vehicles_last_update = 0;
8 var vehicles_source = null;
9 var vehicles_layer = null;
10
11 var stops_xhr = null;
12 var stops_source = null;
13 var stops_layer = null;
14 var stop_points_source = null;
15 var stop_points_layer = null;
16
1d4785 17 var feature_clicked = null;
JK 18 var feature_selected = [];
8b6250 19 var feature_xhr = null;
JK 20 var feature_timer = null;
1d4785 21
JK 22 var route_source = null;
23 var route_layer = null;
07c714 24
57b8d3 25 var map = null;
0e60d1 26 var map_sphere = null;
57b8d3 27 var popup_element = document.getElementById('popup');
9f0f6a 28 var popup_close_callback;
57b8d3 29 var fail_element = document.getElementById('fail');
JK 30
7ca6a1 31 var ignore_hashchange = false;
JK 32
57b8d3 33 function fail(msg) {
JK 34     setText(fail_element, msg);
35     fail_element.style.top = '0.5em';
36 }
37
8b6250 38 function fail_popup(msg) {
JK 39     addElementWithText(popup_element, 'p', msg).className = 'error';
40 }
41
42 function fail_ajax_generic(data, fnc) {
57b8d3 43     // abort() is not a failure
JK 44     if(data.readyState == 0 && data.statusText == 'abort') return;
45     
46     if(data.status == 0) {
8b6250 47         fnc(lang.error_request_failed_connectivity, data);
57b8d3 48     } else if (data.statusText) {
8b6250 49         fnc(lang.error_request_failed_status.replace('$status', data.statusText), data);
57b8d3 50     } else {
8b6250 51         fnc(lang.error_request_failed, data);
57b8d3 52     }
8b6250 53 }
JK 54
55 function fail_ajax(data) {
56     fail_ajax_generic(data, fail);
57 }
58
59 function fail_ajax_popup(data) {
60     fail_ajax_generic(data, fail_popup);
57b8d3 61 }
JK 62
63 function getGeometry(object) {
07c714 64     return new ol.geom.Point(ol.proj.fromLonLat([object.longitude / 3600000.0, object.latitude / 3600000.0]));
57b8d3 65 }
JK 66
1d4785 67 function styleVehicle(vehicle, selected) {
JK 68     var color_type = 'black';
69     if(vehicle.get('vehicle_type')) {
70         switch(vehicle.get('vehicle_type').low) {
71             case 0:
72                 color_type = 'orange';
73             break;
74             case 1:
75                 color_type = 'blue';
76             break;
77             case 2:
78                 color_type = 'green';
79             break;
80         }
81     }
82     
83     var fill = (selected ? '#a00' : '#3399ff');
84     
85     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:2" /></svg>';
86     
87     return new ol.style.Style({
88         image: new ol.style.Icon({
89             src: 'data:image/svg+xml;base64,' + btoa(image),
90             rotation: Math.PI * parseFloat(vehicle.get('heading')) / 180.0,
91         }),
92         text: new ol.style.Text({
93             font: 'bold 10px sans-serif',
94             text: vehicle.get('line'),
95             fill: new ol.style.Fill({color: 'white'}),
96         }),
97     });
98 }
99
100 function styleStop(stop, selected) {
101     var fill = 'orange';
102     var stroke = 'red';
103     var stroke_width = 1;
104     var radius = 3;
105     
106     if(selected == 2) {
107         radius = 5;
108     } else if(selected) {
109         fill = 'red';
110         stroke = '#900';
111         stroke_width = 2;
112         radius = 5;
113     }
114     
115     return new ol.style.Style({
116         image: new ol.style.Circle({
117             fill: new ol.style.Fill({color: fill}),
118             stroke: new ol.style.Stroke({color: stroke, width: stroke_width}),
119             radius: radius,
120         }),
121     });
122 }
123
124 function styleFeature(feature, selected) {
125     if(!feature) return;
126     if(!feature.getId()) return;
127     
128     var style = null;
129     
130     switch(feature.getId().substr(0, 1)) {
131         case 'v':
132             style = styleVehicle(feature, selected);
133         break;
134         
135         case 's':
136         case 'p':
137             style = styleStop(feature, selected);
138         break;
139     }
140     
141     feature.setStyle(style);
142     if(selected) {
143         feature_selected.push(feature);
144     }
145 }
146
147 function unstyleSelectedFeatures() {
148     for(var i = 0; i < feature_selected.length; i++) {
149         styleFeature(feature_selected[i]);
150     }
151     feature_selected = [];
152 }
153
57b8d3 154 function updateVehicles() {
JK 155     if(vehicles_timer) clearTimeout(vehicles_timer);
156     if(vehicles_xhr) vehicles_xhr.abort();
157     
158     vehicles_xhr = $.get(
159         ttss_base + '/geoserviceDispatcher/services/vehicleinfo/vehicles' 
160             + '?positionType=CORRECTED'
161             + '&colorType=ROUTE_BASED'
162             + '&lastUpdate=' + encodeURIComponent(vehicles_last_update)
163     ).done(function(data) {
164         vehicles_last_update = data.lastUpdate;
165         
166         for(var i = 0; i < data.vehicles.length; i++) {
167             var vehicle = data.vehicles[i];
168             
169             var vehicle_feature = vehicles_source.getFeatureById('v' + vehicle.id);
170             if(vehicle.isDeleted) {
171                 if(vehicle_feature) {
172                     vehicles_source.removeFeature(vehicle_feature);
745cfd 173                     if(feature_clicked && feature_clicked.getId() === vehicle_feature.getId()) {
07c714 174                         featureClicked();
57b8d3 175                     }
JK 176                 }
177                 continue;
178             }
179             
180             var vehicle_name_space = vehicle.name.indexOf(' ');
181             vehicle.line = vehicle.name.substr(0, vehicle_name_space);
182             vehicle.direction = vehicle.name.substr(vehicle_name_space+1);
183             if(special_directions[vehicle.direction]) {
184                 vehicle.line = special_directions[vehicle.direction];
185             }
186             
187             vehicle.geometry = getGeometry(vehicle);
188             vehicle.vehicle_type = parseVehicle(vehicle.id);
189             
190             if(!vehicle_feature) {
191                 vehicle_feature = new ol.Feature(vehicle);
192                 vehicle_feature.setId('v' + vehicle.id);
193                 
1d4785 194                 styleFeature(vehicle_feature);
57b8d3 195                 vehicles_source.addFeature(vehicle_feature);
JK 196             } else {
197                 vehicle_feature.setProperties(vehicle);
198                 vehicle_feature.getStyle().getImage().setRotation(Math.PI * parseFloat(vehicle.heading) / 180.0);
199             }
200         }
201         
202         vehicles_timer = setTimeout(function() {
203             updateVehicles();
204         }, ttss_refresh);
205     }).fail(fail_ajax);
7ca6a1 206     
JK 207     return vehicles_xhr;
57b8d3 208 }
JK 209
210 function updateStopSource(stops, prefix, source) {
211     source.clear();
212     
213     for(var i = 0; i < stops.length; i++) {
214         var stop = stops[i];
e61357 215         
JK 216         if(stop.category == 'other') continue;
217         
57b8d3 218         stop.geometry = getGeometry(stop);
JK 219         var stop_feature = new ol.Feature(stop);
220         
221         stop_feature.setId(prefix + stop.id);
1d4785 222         styleFeature(stop_feature);
57b8d3 223         
JK 224         source.addFeature(stop_feature);
225     }
226 }
227
228 function updateStops() {
7ca6a1 229     return $.get(
57b8d3 230         ttss_base + '/geoserviceDispatcher/services/stopinfo/stops'
JK 231             + '?left=-648000000'
232             + '&bottom=-324000000'
233             + '&right=648000000'
234             + '&top=324000000'
235     ).done(function(data) {
236         updateStopSource(data.stops, 's', stops_source);
237     }).fail(fail_ajax);
238 }
239
240 function updateStopPoints() {
7ca6a1 241     return $.get(
57b8d3 242         ttss_base + '/geoserviceDispatcher/services/stopinfo/stopPoints'
JK 243             + '?left=-648000000'
244             + '&bottom=-324000000'
245             + '&right=648000000'
246             + '&top=324000000'
247     ).done(function(data) {
248         updateStopSource(data.stopPoints, 'p', stop_points_source);
249     }).fail(fail_ajax);
7ca6a1 250 }
JK 251
1d4785 252 function vehicleTable(tripId, table, vehicleId) {
8b6250 253     if(feature_xhr) feature_xhr.abort();
JK 254     if(feature_timer) clearTimeout(feature_timer);
255     
256     feature_xhr = $.get(
257         ttss_base + '/services/tripInfo/tripPassages'
258             + '?tripId=' + encodeURIComponent(tripId)
259             + '&mode=departure'
260     ).done(function(data) {
98ba34 261         if(!data.routeName || !data.directionText) {
8b6250 262             return;
JK 263         }
264         
265         deleteChildren(table);
266         
267         for(var i = 0, il = data.old.length; i < il; i++) {
268             var tr = document.createElement('tr');
269             addCellWithText(tr, data.old[i].actualTime || data.old[i].plannedTime);
270             addCellWithText(tr, data.old[i].stop_seq_num + '. ' + data.old[i].stop.name);
271             
272             tr.className = 'active';
273             table.appendChild(tr);
274         }
275         
1d4785 276         unstyleSelectedFeatures();
JK 277         styleFeature(feature_clicked, true);
278         
8b6250 279         for(var i = 0, il = data.actual.length; i < il; i++) {
JK 280             var tr = document.createElement('tr');
281             addCellWithText(tr, data.actual[i].actualTime || data.actual[i].plannedTime);
282             addCellWithText(tr, data.actual[i].stop_seq_num + '. ' + data.actual[i].stop.name);
1d4785 283             
JK 284             styleFeature(stops_source.getFeatureById('s' + data.actual[i].stop.id), 2);
8b6250 285             
JK 286             if(data.actual[i].status == 'STOPPING') {
287                 tr.className = 'success';
288             }
289             table.appendChild(tr);
290         }
291         
292         feature_timer = setTimeout(function() { vehicleTable(tripId, table); }, ttss_refresh);
1d4785 293         
JK 294         if(!vehicleId) return;
295            
296         feature_xhr = $.get(
9dd3d1 297             ttss_base + '/geoserviceDispatcher/services/pathinfo/vehicle'
1d4785 298                 + '?id=' + encodeURIComponent(vehicleId)
JK 299         ).done(function(data) {
300             if(!data || !data.paths || !data.paths[0] || !data.paths[0].wayPoints) return;
301             
302             var point = null;
303             var points = [];
304             for(var i = 0; i < data.paths[0].wayPoints.length; i++) {
305                 point = data.paths[0].wayPoints[i];
306                 points.push(ol.proj.fromLonLat([
307                     point.lon / 3600000.0,
308                     point.lat / 3600000.0,
309                 ]));
310             }
311             
312             route_source.addFeature(new ol.Feature({
313                 geometry: new ol.geom.LineString(points)
314             }));
315         });
8b6250 316     }).fail(fail_ajax_popup);
JK 317 }
318
319 function stopTable(stopType, stopId, table) {
320     if(feature_xhr) feature_xhr.abort();
321     if(feature_timer) clearTimeout(feature_timer);
322     
323     feature_xhr = $.get(
324         ttss_base + '/services/passageInfo/stopPassages/' + stopType
325             + '?' + stopType + '=' + encodeURIComponent(stopId)
326             + '&mode=departure'
327     ).done(function(data) {
328         deleteChildren(table);
329         
330         for(var i = 0, il = data.old.length; i < il; i++) {
331             var tr = document.createElement('tr');
332             addCellWithText(tr, data.old[i].patternText);
333             var dir_cell = addCellWithText(tr, data.old[i].direction);
334             var vehicle = parseVehicle(data.old[i].vehicleId);
335             dir_cell.appendChild(displayVehicle(vehicle));
336             var status = parseStatus(data.old[i]);
337             addCellWithText(tr, status);
338             addCellWithText(tr, '');
339             
340             tr.className = 'active';
341             table.appendChild(tr);
342         }
343         
344         for(var i = 0, il = data.actual.length; i < il; i++) {
345             var tr = document.createElement('tr');
346             addCellWithText(tr, data.actual[i].patternText);
347             var dir_cell = addCellWithText(tr, data.actual[i].direction);
348             var vehicle = parseVehicle(data.actual[i].vehicleId);
349             dir_cell.appendChild(displayVehicle(vehicle));
350             var status = parseStatus(data.actual[i]);
351             var status_cell = addCellWithText(tr, status);
352             var delay = parseDelay(data.actual[i]);
353             var delay_cell = addCellWithText(tr, delay);
354             
355             if(status == lang.boarding_sign) {
356                 tr.className = 'success';
357                 status_cell.className = 'status-boarding';
358             } else if(parseInt(delay) > 9) {
359                 tr.className = 'danger';
360                 delay_cell.className = 'status-delayed';
361             } else if(parseInt(delay) > 3) {
362                 tr.className = 'warning';
363             }
364             
365             table.appendChild(tr);
366         }
367         
368         feature_timer = setTimeout(function() { stopTable(stopType, stopId, table); }, ttss_refresh);
369     }).fail(fail_ajax_popup);
370 }
371
9f0f6a 372 function showPanel(contents, closeCallback) {
JK 373     var old_callback = popup_close_callback;
374     popup_close_callback = null;
375     if(old_callback) old_callback();
376     popup_close_callback = closeCallback;
377     
378     deleteChildren(popup_element);
379     
380     var close = addParaWithText(popup_element, '×');
381     close.className = 'close';
382     close.addEventListener('click', function() { hidePanel(); });
383     
384     popup_element.appendChild(contents);
385     
386     $(popup_element).addClass('show');
387 }
388
389 function hidePanel() {
390     var old_callback = popup_close_callback;
391     popup_close_callback = null;
392     if(old_callback) old_callback();
393     
394     $(popup_element).removeClass('show');
395 }
396
7ca6a1 397 function featureClicked(feature) {
1d4785 398     if(feature && !feature.getId()) return;
JK 399     
400     unstyleSelectedFeatures();
401     route_source.clear();
402     
7ca6a1 403     if(!feature) {
9f0f6a 404         hidePanel();
7ca6a1 405         return;
JK 406     }
407     
408     var coordinates = feature.getGeometry().getCoordinates();
409     
9f0f6a 410     var div = document.createElement('div');
8b6250 411     
07c714 412     var type;
JK 413     var name = feature.get('name');
414     var additional;
8b6250 415     var table = document.createElement('table');
JK 416     var thead = document.createElement('thead');
417     var tbody = document.createElement('tbody');
418     table.appendChild(thead);
419     table.appendChild(tbody);
07c714 420     
7ca6a1 421     switch(feature.getId().substr(0, 1)) {
JK 422         case 'v':
07c714 423             type = lang.type_vehicle;
JK 424             
425             if(!feature.get('vehicle_type')) {
426                 break;
7ca6a1 427             }
07c714 428             
JK 429             var span = displayVehicle(feature.get('vehicle_type'));
430             
431             additional = document.createElement('p');
432             setText(additional, span.title);
433             additional.insertBefore(span, additional.firstChild);
8b6250 434             
JK 435             addElementWithText(thead, 'th', lang.header_time);
436             addElementWithText(thead, 'th', lang.header_stop);
437             
1d4785 438             vehicleTable(feature.get('tripId'), tbody, feature.get('id'));
7ca6a1 439         break;
07c714 440         case 's':
JK 441             type = lang.type_stop;
8b6250 442             
JK 443             addElementWithText(thead, 'th', lang.header_line);
444             addElementWithText(thead, 'th', lang.header_direction);
445             addElementWithText(thead, 'th', lang.header_time);
446             addElementWithText(thead, 'th', lang.header_delay);
447             
448             stopTable('stop', feature.get('shortName'), tbody);
07c714 449         break;
JK 450         case 'p':
451             type = lang.type_stoppoint;
8b6250 452             
JK 453             additional = document.createElement('p');
454             additional.className = 'small';
455             addElementWithText(additional, 'a', lang.departures_for_stop).addEventListener(
456                 'click',
457                 function() {
458                     featureClicked(stops_source.forEachFeature(function(stop_feature) {
459                         if(stop_feature.get('shortName') == feature.get('shortName')) {
460                             return stop_feature;
461                         }
462                     }));
463                 }
464             );
465             
466             addElementWithText(thead, 'th', lang.header_line);
467             addElementWithText(thead, 'th', lang.header_direction);
468             addElementWithText(thead, 'th', lang.header_time);
469             addElementWithText(thead, 'th', lang.header_delay);
470             
471             stopTable('stopPoint', feature.get('stopPoint'), tbody);
07c714 472         break;
JK 473     }
8b6250 474     
JK 475     var loader = addElementWithText(tbody, 'td', lang.loading);
476     loader.className = 'active';
ee4e7c 477     loader.colSpan = thead.childNodes.length;
07c714 478     
9f0f6a 479     addParaWithText(div, type).className = 'type';
JK 480     addParaWithText(div, name).className = 'name';
07c714 481     
JK 482     if(additional) {
9f0f6a 483         div.appendChild(additional);
7ca6a1 484     }
JK 485     
9f0f6a 486     div.appendChild(table);
7ca6a1 487     
1d4785 488     styleFeature(feature, true);
JK 489     
490     setTimeout(function () {map.getView().animate({
07c714 491         center: feature.getGeometry().getCoordinates(),
1d4785 492     }) }, 10);
07c714 493     
9f0f6a 494     ignore_hashchange = true;
JK 495     window.location.hash = '#!' + feature.getId();
496     
497     showPanel(div, function() {
498         if(!ignore_hashchange) {
499             ignore_hashchange = true;
500             window.location.hash = '';
501             
502             feature_clicked = null;
503             unstyleSelectedFeatures();
504             route_source.clear();
505             
506             if(feature_xhr) feature_xhr.abort();
507             if(feature_timer) clearTimeout(feature_timer);
508         }
509     });
07c714 510     
1d4785 511     feature_clicked = feature;
7ca6a1 512 }
JK 513
514 function hash() {
515     if(ignore_hashchange) {
516         ignore_hashchange = false;
517         return;
518     }
519     
520     var tramId = null;
521     
522     var vehicleId = null;
523     var stopId = null;
524     var stopPointId = null;
525     
526     var feature = null;
527     
528     if(window.location.hash.match(/^#!t[0-9]{3}$/)) {
529         tramId = parseInt(window.location.hash.substr(3));
530     } else if(window.location.hash.match(/^#![A-Za-z]{2}[0-9]{3}$/)) {
531         tramId = parseInt(window.location.hash.substr(4));
532     } else if(window.location.hash.match(/^#!v[0-9]+$/)) {
533         vehicleId = window.location.hash.substr(3);
534     } else if(window.location.hash.match(/^#!s[0-9]+$/)) {
535         stopId = window.location.hash.substr(3);
536     } else if(window.location.hash.match(/^#!p[0-9]+$/)) {
537         stopPointId = window.location.hash.substr(3);
538     }
539     
540     if(tramId) {
541         vehicleId = tramIdToVehicleId(tramId);
542     }
543     
544     if(vehicleId) {
545         feature = vehicles_source.getFeatureById('v' + vehicleId);
546     } else if(stopId) {
547         feature = stops_source.getFeatureById('s' + stopId);
548     } else if(stopPointId) {
549         feature = stop_points_source.getFeatureById('p' + stopPointId);
550     }
551     
552     featureClicked(feature);
57b8d3 553 }
JK 554
0e60d1 555 function getDistance(c1, c2) {
JK 556     if(c1.getGeometry) {
557         c1 = c1.getGeometry().getCoordinates();
558     }
559     if(c2.getGeometry) {
560         c2 = c2.getGeometry().getCoordinates();
561     }
562     
563     var c1 = ol.proj.transform(c1, 'EPSG:3857', 'EPSG:4326');
564     var c2 = ol.proj.transform(c2, 'EPSG:3857', 'EPSG:4326');
565     return map_sphere.haversineDistance(c1, c2);
566 }
567
568 function returnClosest(point, f1, f2) {
569     if(!f1) return f2;
570     if(!f2) return f1;
571     
572     return (getDistance(point, f1) < getDistance(point, f2)) ? f1 : f2;
573 }
574
57b8d3 575 function init() {
JK 576     if(!window.jQuery) {
577         fail(lang.jquery_not_loaded);
578         return;
579     }
580     
581     $.ajaxSetup({
582         dataType: 'json',
583         timeout: 10000,
584     });
585     
586     stops_source = new ol.source.Vector({
587         features: [],
588     });
589     stops_layer = new ol.layer.Vector({
590         source: stops_source,
591     });
592     
593     stop_points_source = new ol.source.Vector({
594         features: [],
595     });
596     stop_points_layer = new ol.layer.Vector({
597         source: stop_points_source,
598         visible: false,
599     });
600     
601     vehicles_source = new ol.source.Vector({
602         features: [],
603     });
604     vehicles_layer = new ol.layer.Vector({
605         source: vehicles_source,
606     });
607     
1d4785 608     route_source = new ol.source.Vector({
JK 609         features: [],
610     });
611     route_layer = new ol.layer.Vector({
612         source: route_source,
613         style: new ol.style.Style({
614             stroke: new ol.style.Stroke({ color: [255, 153, 0, .8], width: 5 })
615         }),
616     });
617     
57b8d3 618     map = new ol.Map({
JK 619         target: 'map',
620         layers: [
621             new ol.layer.Tile({
622                 source: new ol.source.OSM()
623             }),
1d4785 624             route_layer,
57b8d3 625             stops_layer,
JK 626             stop_points_layer,
627             vehicles_layer,
628         ],
629         view: new ol.View({
630             center: ol.proj.fromLonLat([19.94, 50.06]),
631             zoom: 13
632         }),
633         controls: ol.control.defaults({
634             attributionOptions: ({
635                 collapsible: false,
636             })
637         }).extend([
638             new ol.control.Control({
639                 element: document.getElementById('title'),
640             }),
641             new ol.control.Control({
642                 element: fail_element,
643             })
644         ]),
1d4785 645         loadTilesWhileAnimating: true,
57b8d3 646     });
0e60d1 647     map_sphere = new ol.Sphere(6378137);
57b8d3 648     
JK 649     // Display popup on click
650     map.on('singleclick', function(e) {
0e60d1 651         var point = e.coordinate;
9f0f6a 652         var features = [];
JK 653         map.forEachFeatureAtPixel(e.pixel, function(feature) { if(feature.getId()) features.push(feature); });
0e60d1 654         
9f0f6a 655         if(features.length > 1) {
JK 656             var div = document.createElement('div');
657             
658             addParaWithText(div, lang.select_feature);
659             
660             for(var i = 0; i < features.length; i++) {
661                 var feature = features[i];
662                 
663                 var p = document.createElement('p');
664                 var a = document.createElement('a');
665                 p.appendChild(a);
666                 a.addEventListener('click', function(feature) { return function() {
667                     featureClicked(feature);
668                 }}(feature));
669                 
670                 var type = '';
671                 switch(feature.getId().substr(0, 1)) {
672                     case 'v':
673                         type = lang.type_vehicle + ' ' + feature.get('vehicle_type').num;
674                     break;
675                     case 's':
676                         type = lang.type_stop;
677                     break;
678                     case 'p':
679                         type = lang.type_stoppoint;
680                     break;
681                 }
682                 
683                 addElementWithText(a, 'span', type).className = 'small';
684                 a.appendChild(document.createTextNode(' '));
685                 addElementWithText(a, 'span', feature.get('name'));
686                 
687                 div.appendChild(p);
688             }
689             
690             showPanel(div);
691             
692             return;
693         }
694         
695         var feature = features[0];
0e60d1 696         if(!feature) {
JK 697             if(stops_layer.getVisible()) {
698                 feature = returnClosest(point, feature, stops_source.getClosestFeatureToCoordinate(point));
699             }
700             if(stop_points_layer.getVisible()) {
701                 feature = returnClosest(point, feature, stop_points_source.getClosestFeatureToCoordinate(point));
702             }
703             if(vehicles_layer.getVisible()) {
704                 feature = returnClosest(point, feature, vehicles_source.getClosestFeatureToCoordinate(point));
705             }
706             
707             if(getDistance(point, feature) > 200) {
708                 feature = null;
709             }
710         }
711         
7ca6a1 712         featureClicked(feature);
57b8d3 713     });
9f0f6a 714     
JK 715     fail_element.addEventListener('click', function() {
716         fail_element.style.top = '-10em';
717     });
57b8d3 718
JK 719     // Change mouse cursor when over marker
720     map.on('pointermove', function(e) {
721         var hit = map.hasFeatureAtPixel(e.pixel);
722         var target = map.getTargetElement();
723         target.style.cursor = hit ? 'pointer' : '';
724     });
725     
726     // Change layer visibility on zoom
727     map.getView().on('change:resolution', function(e) {
1d4785 728         stop_points_layer.setVisible(map.getView().getZoom() >= 16);
57b8d3 729     });
JK 730     
7ca6a1 731     $.when(
JK 732         updateVehicles(),
733         updateStops(),
734         updateStopPoints()
735     ).done(function() {
736         hash();
737     });
738     
739     window.addEventListener('hashchange', hash);
57b8d3 740     
JK 741     setTimeout(function() {
742         if(vehicles_xhr) vehicles_xhr.abort();
743         if(vehicles_timer) clearTimeout(vehicles_timer);
744           
745         fail(lang.error_refresh);
746     }, 1800000);
747 }
748
749 init();