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