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