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