gtfs-to-html 2.11.4 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.js +39 -30
- package/dist/app/index.js.map +1 -1
- package/dist/bin/gtfs-to-html.js +55 -9
- package/dist/bin/gtfs-to-html.js.map +1 -1
- package/dist/index.js +55 -9
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/views/default/css/overview_styles.css +4 -2
- package/views/default/css/timetable_styles.css +24 -9
- package/views/default/formatting_functions.pug +1 -1
- package/views/default/js/system-map.js +143 -92
- package/views/default/js/timetable-alerts.js +68 -50
- package/views/default/js/timetable-map.js +264 -164
- package/views/default/js/timetable-menu.js +64 -40
- package/views/default/overview.pug +1 -1
- package/views/default/overview_full.pug +4 -6
- package/views/default/timetable_continuation_as.pug +1 -1
- package/views/default/timetable_continuation_from.pug +1 -1
- package/views/default/timetable_horizontal.pug +2 -2
- package/views/default/timetable_hourly.pug +1 -1
- package/views/default/timetable_map.pug +1 -1
- package/views/default/timetable_menu.pug +2 -2
- package/views/default/timetable_note_symbol.pug +1 -1
- package/views/default/timetable_stop_name.pug +1 -1
- package/views/default/timetable_stoptime.pug +7 -7
- package/views/default/timetable_vertical.pug +3 -3
- package/views/default/timetablepage.pug +8 -8
- package/views/default/timetablepage_full.pug +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs-to-html",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
|
|
6
6
|
"keywords": [
|
|
@@ -47,23 +47,26 @@
|
|
|
47
47
|
"prepare": "husky"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@maplibre/maplibre-gl-geocoder": "^1.9.1",
|
|
50
51
|
"@turf/helpers": "^7.2.0",
|
|
51
52
|
"@turf/simplify": "^7.2.0",
|
|
52
53
|
"anchorme": "^3.0.8",
|
|
53
54
|
"archiver": "^7.0.1",
|
|
54
55
|
"cli-table": "^0.3.11",
|
|
56
|
+
"css.escape": "^1.5.1",
|
|
55
57
|
"csv-stringify": "^6.6.0",
|
|
56
58
|
"express": "^5.1.0",
|
|
57
59
|
"gtfs": "^4.18.1",
|
|
58
60
|
"gtfs-realtime-pbf-js-module": "^1.0.0",
|
|
59
61
|
"js-beautify": "^1.15.4",
|
|
60
62
|
"lodash-es": "^4.17.21",
|
|
61
|
-
"
|
|
63
|
+
"maplibre-gl": "^5.12.0",
|
|
64
|
+
"marked": "^17.0.0",
|
|
62
65
|
"moment": "^2.30.1",
|
|
63
66
|
"pbf": "^4.0.1",
|
|
64
67
|
"pretty-error": "^4.0.0",
|
|
65
68
|
"pug": "^3.0.3",
|
|
66
|
-
"puppeteer": "^24.
|
|
69
|
+
"puppeteer": "^24.29.1",
|
|
67
70
|
"sanitize-filename": "^1.6.3",
|
|
68
71
|
"sanitize-html": "^2.17.0",
|
|
69
72
|
"sqlstring": "^2.3.3",
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
"devDependencies": {
|
|
75
78
|
"@types/archiver": "^7.0.0",
|
|
76
79
|
"@types/cli-table": "^0.3.4",
|
|
77
|
-
"@types/express": "^5.0.
|
|
80
|
+
"@types/express": "^5.0.5",
|
|
78
81
|
"@types/insane": "^1.0.0",
|
|
79
82
|
"@types/js-beautify": "^1.14.3",
|
|
80
83
|
"@types/lodash-es": "^4.17.12",
|
|
@@ -98,11 +98,12 @@ a:hover {
|
|
|
98
98
|
align-items: center;
|
|
99
99
|
justify-content: center;
|
|
100
100
|
font-size: 12px;
|
|
101
|
-
letter-spacing: -0.
|
|
101
|
+
letter-spacing: -0.1px;
|
|
102
102
|
padding: 0 2px;
|
|
103
103
|
flex-shrink: 0;
|
|
104
104
|
font-weight: bold;
|
|
105
105
|
color: white;
|
|
106
|
+
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
.timetable-overview .route-color-swatch-large {
|
|
@@ -114,11 +115,12 @@ a:hover {
|
|
|
114
115
|
justify-content: center;
|
|
115
116
|
font-size: 20px;
|
|
116
117
|
font-weight: bold;
|
|
117
|
-
letter-spacing: -
|
|
118
|
+
letter-spacing: -0.2px;
|
|
118
119
|
padding: 0 6px;
|
|
119
120
|
flex-shrink: 0;
|
|
120
121
|
font-weight: bold;
|
|
121
122
|
color: white;
|
|
123
|
+
text-shadow: 0 0 4px rgba(0,0,0,0.5);
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
.timetable-overview .btn-blue {
|
|
@@ -422,11 +422,12 @@ a:hover {
|
|
|
422
422
|
align-items: center;
|
|
423
423
|
justify-content: center;
|
|
424
424
|
font-size: 0.75rem;
|
|
425
|
-
letter-spacing: -0.
|
|
425
|
+
letter-spacing: -0.1px;
|
|
426
426
|
padding: 0 2px;
|
|
427
427
|
flex-shrink: 0;
|
|
428
428
|
font-weight: bold;
|
|
429
429
|
color: white;
|
|
430
|
+
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
|
430
431
|
}
|
|
431
432
|
|
|
432
433
|
.timetable-page .route-color-swatch-large {
|
|
@@ -438,11 +439,12 @@ a:hover {
|
|
|
438
439
|
justify-content: center;
|
|
439
440
|
font-size: 1.25rem;
|
|
440
441
|
font-weight: bold;
|
|
441
|
-
letter-spacing: -
|
|
442
|
+
letter-spacing: -0.2px;
|
|
442
443
|
padding: 0 6px;
|
|
443
444
|
flex-shrink: 0;
|
|
444
445
|
font-weight: bold;
|
|
445
446
|
color: white;
|
|
447
|
+
text-shadow: 0 0 4px rgba(0,0,0,0.5);
|
|
446
448
|
}
|
|
447
449
|
|
|
448
450
|
/* Map Styles */
|
|
@@ -551,33 +553,35 @@ a:hover {
|
|
|
551
553
|
}
|
|
552
554
|
|
|
553
555
|
.timetable-page .map-legend {
|
|
554
|
-
padding: 0
|
|
556
|
+
padding: 8px 8px 0 8px;
|
|
557
|
+
display: flex;
|
|
558
|
+
flex-direction: column;
|
|
559
|
+
gap: 6px;
|
|
555
560
|
}
|
|
556
561
|
|
|
557
562
|
@media screen and (min-width: 768px) {
|
|
558
563
|
.timetable-page .map-legend {
|
|
559
|
-
padding:
|
|
564
|
+
padding: 8px 6px;
|
|
560
565
|
background-color: #fff;
|
|
561
566
|
border-radius: 3px;
|
|
562
567
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
|
563
568
|
position: absolute;
|
|
564
569
|
left: 10px;
|
|
565
|
-
bottom:
|
|
570
|
+
bottom: 10px;
|
|
566
571
|
z-index: 1;
|
|
572
|
+
gap: 8px;
|
|
567
573
|
}
|
|
568
574
|
}
|
|
569
575
|
|
|
570
576
|
.timetable-page .map-legend .legend-item {
|
|
571
|
-
padding: 4px 0;
|
|
572
577
|
display: flex;
|
|
573
578
|
flex-direction: row;
|
|
574
|
-
align-items:
|
|
579
|
+
align-items: center;
|
|
575
580
|
gap: 4px;
|
|
576
581
|
}
|
|
577
582
|
|
|
578
583
|
.timetable-page .map-legend .legend-item .legend-icon {
|
|
579
584
|
width: 22px;
|
|
580
|
-
padding-top: 3px;
|
|
581
585
|
display: flex;
|
|
582
586
|
flex-direction: row;
|
|
583
587
|
align-items: center;
|
|
@@ -587,6 +591,7 @@ a:hover {
|
|
|
587
591
|
|
|
588
592
|
.timetable-page .map-legend .legend-item .legend-text {
|
|
589
593
|
font-size: 12px;
|
|
594
|
+
line-height: 1;
|
|
590
595
|
}
|
|
591
596
|
|
|
592
597
|
.timetable-page .stop-marker {
|
|
@@ -668,11 +673,12 @@ a:hover {
|
|
|
668
673
|
align-items: center;
|
|
669
674
|
justify-content: center;
|
|
670
675
|
font-size: 0.75rem;
|
|
671
|
-
letter-spacing: -0.
|
|
676
|
+
letter-spacing: -0.1px;
|
|
672
677
|
padding: 0 2px;
|
|
673
678
|
flex-shrink: 0;
|
|
674
679
|
font-weight: bold;
|
|
675
680
|
color: white;
|
|
681
|
+
text-shadow: 0 0 2px rgba(0,0,0,0.5);
|
|
676
682
|
}
|
|
677
683
|
|
|
678
684
|
.timetable-page .timetable-alerts .alert-header .alert-title {
|
|
@@ -684,6 +690,15 @@ a:hover {
|
|
|
684
690
|
margin-top: 0.5rem;
|
|
685
691
|
}
|
|
686
692
|
|
|
693
|
+
.timetable-page .timetable-alerts .alert-body .alert-label {
|
|
694
|
+
margin-top: 0.5rem;
|
|
695
|
+
font-weight: bold;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
.timetable-page .timetable-alerts .alert-body ul {
|
|
699
|
+
margin: 0.5rem 0;
|
|
700
|
+
}
|
|
701
|
+
|
|
687
702
|
.timetable-page .timetable-alerts .alert-more-info {
|
|
688
703
|
margin-top: 0.75rem;
|
|
689
704
|
}
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
minifiedGeojson.features.push(feature)
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
geojsons[
|
|
97
|
+
geojsons[timetable.timetable_id] = optimizeGeojsonProperties(minifiedGeojson)
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const gtfsRealtimeUrls = {}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* global
|
|
1
|
+
/* global maplibregl, geojson, mapStyleUrl, MaplibreGeocoder */
|
|
2
2
|
/* eslint prefer-arrow-callback: "off", no-unused-vars: "off" */
|
|
3
3
|
|
|
4
4
|
function formatRouteColor(route) {
|
|
@@ -10,86 +10,103 @@ function formatRouteTextColor(route) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function formatRoute(route) {
|
|
13
|
-
const
|
|
14
|
-
?
|
|
15
|
-
:
|
|
13
|
+
const element = route.route_url
|
|
14
|
+
? document.createElement('a')
|
|
15
|
+
: document.createElement('div');
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
element.className = 'map-route-item';
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
if (route.route_url) {
|
|
20
|
+
element.href = route.route_url;
|
|
21
|
+
}
|
|
20
22
|
|
|
21
23
|
if (route.route_color) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
);
|
|
24
|
+
const colorSwatch = document.createElement('div');
|
|
25
|
+
colorSwatch.className = 'route-color-swatch';
|
|
26
|
+
colorSwatch.style.backgroundColor = formatRouteColor(route);
|
|
27
|
+
colorSwatch.style.color = formatRouteTextColor(route);
|
|
28
|
+
colorSwatch.textContent = route.route_short_name ?? '';
|
|
29
|
+
element.appendChild(colorSwatch);
|
|
29
30
|
}
|
|
30
|
-
routeItemDivs.push(
|
|
31
|
-
jQuery('<div>')
|
|
32
|
-
.addClass('underline-hover')
|
|
33
|
-
.text(route.route_long_name ?? `Route ${route.route_short_name}`),
|
|
34
|
-
);
|
|
35
31
|
|
|
36
|
-
|
|
32
|
+
const routeName = document.createElement('div');
|
|
33
|
+
routeName.className = 'underline-hover';
|
|
34
|
+
routeName.textContent =
|
|
35
|
+
route.route_long_name ?? `Route ${route.route_short_name}`;
|
|
36
|
+
element.appendChild(routeName);
|
|
37
37
|
|
|
38
|
-
return
|
|
38
|
+
return element.outerHTML;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function formatRoutePopup(features) {
|
|
42
|
-
const
|
|
42
|
+
const container = document.createElement('div');
|
|
43
43
|
|
|
44
44
|
if (features.length > 1) {
|
|
45
|
-
|
|
45
|
+
const title = document.createElement('div');
|
|
46
|
+
title.className = 'popup-title';
|
|
47
|
+
title.textContent = 'Routes';
|
|
48
|
+
container.appendChild(title);
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
features.forEach((feature) => {
|
|
52
|
+
const routeHTML = formatRoute(feature.properties);
|
|
53
|
+
container.insertAdjacentHTML('beforeend', routeHTML);
|
|
54
|
+
});
|
|
51
55
|
|
|
52
|
-
return
|
|
56
|
+
return container.outerHTML;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
function formatStopPopup(feature) {
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
let routes = [];
|
|
61
|
+
try {
|
|
62
|
+
routes = JSON.parse(feature.properties.routes);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Failed to parse routes JSON:', error);
|
|
65
|
+
}
|
|
66
|
+
const container = document.createElement('div');
|
|
58
67
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
const title = document.createElement('div');
|
|
69
|
+
title.className = 'popup-title';
|
|
70
|
+
title.textContent = feature.properties.stop_name;
|
|
71
|
+
container.appendChild(title);
|
|
63
72
|
|
|
64
73
|
if (feature.properties.stop_code ?? false) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
const stopCodeContainer = document.createElement('div');
|
|
75
|
+
|
|
76
|
+
const label = document.createElement('div');
|
|
77
|
+
label.className = 'popup-label';
|
|
78
|
+
label.textContent = 'Stop Code:';
|
|
79
|
+
stopCodeContainer.appendChild(label);
|
|
80
|
+
|
|
81
|
+
const code = document.createElement('strong');
|
|
82
|
+
code.textContent = feature.properties.stop_code;
|
|
83
|
+
stopCodeContainer.appendChild(code);
|
|
84
|
+
|
|
85
|
+
container.appendChild(stopCodeContainer);
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
|
|
88
|
+
const routesLabel = document.createElement('div');
|
|
89
|
+
routesLabel.className = 'popup-label';
|
|
90
|
+
routesLabel.textContent = 'Routes Served:';
|
|
91
|
+
container.appendChild(routesLabel);
|
|
74
92
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
const routeList = document.createElement('div');
|
|
94
|
+
routeList.className = 'route-list';
|
|
95
|
+
routes.forEach((route) => {
|
|
96
|
+
const routeHTML = formatRoute(route);
|
|
97
|
+
routeList.insertAdjacentHTML('beforeend', routeHTML);
|
|
98
|
+
});
|
|
99
|
+
container.appendChild(routeList);
|
|
80
100
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.appendTo(html);
|
|
91
|
-
|
|
92
|
-
return html.prop('outerHTML');
|
|
101
|
+
const streetviewLink = document.createElement('a');
|
|
102
|
+
streetviewLink.className = 'btn-blue btn-sm';
|
|
103
|
+
streetviewLink.href = `https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${feature.geometry.coordinates[1]},${feature.geometry.coordinates[0]}&heading=0&pitch=0&fov=90`;
|
|
104
|
+
streetviewLink.target = '_blank';
|
|
105
|
+
streetviewLink.rel = 'noopener noreferrer';
|
|
106
|
+
streetviewLink.textContent = 'View on Streetview';
|
|
107
|
+
container.appendChild(streetviewLink);
|
|
108
|
+
|
|
109
|
+
return container.outerHTML;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
112
|
function getBounds(geojson) {
|
|
@@ -121,7 +138,10 @@ function createSystemMap() {
|
|
|
121
138
|
};
|
|
122
139
|
|
|
123
140
|
if (!geojson || geojson.features.length === 0) {
|
|
124
|
-
|
|
141
|
+
const systemMapElement = document.getElementById('system_map');
|
|
142
|
+
if (systemMapElement) {
|
|
143
|
+
systemMapElement.style.display = 'none';
|
|
144
|
+
}
|
|
125
145
|
return false;
|
|
126
146
|
}
|
|
127
147
|
|
|
@@ -132,11 +152,6 @@ function createSystemMap() {
|
|
|
132
152
|
center: bounds.getCenter(),
|
|
133
153
|
zoom: 12,
|
|
134
154
|
});
|
|
135
|
-
const routes = {};
|
|
136
|
-
|
|
137
|
-
for (const feature of geojson.features) {
|
|
138
|
-
routes[feature.properties.route_id] = feature.properties;
|
|
139
|
-
}
|
|
140
155
|
|
|
141
156
|
map.scrollZoom.disable();
|
|
142
157
|
map.addControl(new maplibregl.NavigationControl());
|
|
@@ -148,7 +163,7 @@ function createSystemMap() {
|
|
|
148
163
|
fitMapToBounds(map, bounds);
|
|
149
164
|
disablePointsOfInterest(map);
|
|
150
165
|
addMapLayers(map, geojson, defaultRouteColor, lineLayout);
|
|
151
|
-
setupEventListeners(map
|
|
166
|
+
setupEventListeners(map);
|
|
152
167
|
});
|
|
153
168
|
}
|
|
154
169
|
|
|
@@ -159,12 +174,15 @@ function addGeocoder(map, bounds) {
|
|
|
159
174
|
forwardGeocode: async (config) => {
|
|
160
175
|
const features = [];
|
|
161
176
|
try {
|
|
162
|
-
const request = `https://nominatim.openstreetmap.org/search?q=${
|
|
163
|
-
config.query
|
|
164
|
-
}&format=geojson&polygon_geojson=1&addressdetails=1&viewbox=${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}&bounded=1`;
|
|
177
|
+
const request = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
|
|
178
|
+
config.query,
|
|
179
|
+
)}&format=geojson&polygon_geojson=1&addressdetails=1&viewbox=${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}&bounded=1`;
|
|
165
180
|
const response = await fetch(request);
|
|
166
|
-
const
|
|
167
|
-
for (const feature of
|
|
181
|
+
const geocodeResult = await response.json();
|
|
182
|
+
for (const feature of geocodeResult.features) {
|
|
183
|
+
if (!feature.bbox || feature.bbox.length < 4) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
168
186
|
const center = [
|
|
169
187
|
feature.bbox[0] + (feature.bbox[2] - feature.bbox[0]) / 2,
|
|
170
188
|
feature.bbox[1] + (feature.bbox[3] - feature.bbox[1]) / 2,
|
|
@@ -195,6 +213,7 @@ function addGeocoder(map, bounds) {
|
|
|
195
213
|
},
|
|
196
214
|
{
|
|
197
215
|
maplibregl,
|
|
216
|
+
debounceSearch: 800,
|
|
198
217
|
proximity: {
|
|
199
218
|
latitude: bounds.getCenter()[0],
|
|
200
219
|
longitude: bounds.getCenter()[1],
|
|
@@ -247,11 +266,6 @@ function addMapLayers(map, geojson, defaultRouteColor, lineLayout) {
|
|
|
247
266
|
addRouteLabels(map, geojson);
|
|
248
267
|
}
|
|
249
268
|
|
|
250
|
-
function getFirstSymbolLayerId(map) {
|
|
251
|
-
const layers = map.getStyle().layers;
|
|
252
|
-
return layers.find((layer) => layer.type === 'symbol').id;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
269
|
function addRouteLineShadow(map, geojson, lineLayout, firstSymbolId) {
|
|
256
270
|
map.addLayer(
|
|
257
271
|
{
|
|
@@ -513,22 +527,23 @@ function addRouteLabels(map, geojson) {
|
|
|
513
527
|
});
|
|
514
528
|
}
|
|
515
529
|
|
|
516
|
-
function setupEventListeners(map
|
|
517
|
-
map.on('mousemove', (event) => handleMouseMove(event, map
|
|
530
|
+
function setupEventListeners(map) {
|
|
531
|
+
map.on('mousemove', (event) => handleMouseMove(event, map));
|
|
518
532
|
map.on('click', (event) => handleClick(event, map));
|
|
519
533
|
setupTableHoverListeners(map);
|
|
520
534
|
}
|
|
521
535
|
|
|
522
|
-
function handleMouseMove(event, map
|
|
536
|
+
function handleMouseMove(event, map) {
|
|
523
537
|
const features = map.queryRenderedFeatures(event.point, {
|
|
524
538
|
layers: ['routes', 'route-outlines', 'stops-highlighted', 'stops'],
|
|
525
539
|
});
|
|
526
540
|
if (features.length > 0) {
|
|
527
541
|
map.getCanvas().style.cursor = 'pointer';
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
542
|
+
// Get unique route IDs
|
|
543
|
+
const routeIds = features
|
|
544
|
+
.map((feature) => feature.properties.route_id)
|
|
545
|
+
.filter((value, index, self) => value && self.indexOf(value) === index);
|
|
546
|
+
highlightRoutes(map, routeIds);
|
|
532
547
|
|
|
533
548
|
if (features.some((feature) => feature.layer.id === 'stops')) {
|
|
534
549
|
highlightStop(
|
|
@@ -574,10 +589,32 @@ function showStopPopup(map, feature) {
|
|
|
574
589
|
}
|
|
575
590
|
|
|
576
591
|
function showRoutePopup(map, features, lngLat) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
)
|
|
592
|
+
// Get list of unique routes, using route_short_name as the key
|
|
593
|
+
const seen = {};
|
|
594
|
+
const uniqueRoutes = [];
|
|
595
|
+
for (const feature of features) {
|
|
596
|
+
const routeShortName = feature.properties.route_short_name;
|
|
597
|
+
if (!seen[routeShortName]) {
|
|
598
|
+
seen[routeShortName] = true;
|
|
599
|
+
uniqueRoutes.push(feature);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Sort by route_short_name as number, then alphabetically
|
|
604
|
+
const routes = uniqueRoutes.sort((a, b) => {
|
|
605
|
+
const aNum = Number.parseInt(a.properties.route_short_name, 10);
|
|
606
|
+
|
|
607
|
+
if (Number.isNaN(aNum) && Number.isNaN(bNum)) {
|
|
608
|
+
return a.properties.route_short_name.localeCompare(
|
|
609
|
+
b.properties.route_short_name,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
if (Number.isNaN(aNum)) return 1;
|
|
613
|
+
if (Number.isNaN(bNum)) return -1;
|
|
614
|
+
|
|
615
|
+
const bNum = Number.parseInt(b.properties.route_short_name, 10);
|
|
616
|
+
return aNum - bNum;
|
|
617
|
+
});
|
|
581
618
|
|
|
582
619
|
new maplibregl.Popup()
|
|
583
620
|
.setLngLat(lngLat)
|
|
@@ -676,18 +713,32 @@ function unHighlightRoutes(map, zoom) {
|
|
|
676
713
|
}
|
|
677
714
|
|
|
678
715
|
function setupTableHoverListeners(map) {
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
716
|
+
if (document.readyState === 'loading') {
|
|
717
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
718
|
+
initializeTableHoverListeners(map);
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
initializeTableHoverListeners(map);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function initializeTableHoverListeners(map) {
|
|
726
|
+
const overviewLinks = document.querySelectorAll('.overview-list a');
|
|
727
|
+
|
|
728
|
+
overviewLinks.forEach((link) => {
|
|
729
|
+
link.addEventListener('mouseenter', (event) => {
|
|
730
|
+
const routeIdString = event.currentTarget.dataset.routeIds;
|
|
682
731
|
if (routeIdString) {
|
|
683
732
|
const routeIds = routeIdString.toString().split(',');
|
|
684
733
|
highlightRoutes(map, routeIds, true);
|
|
685
734
|
}
|
|
686
735
|
});
|
|
687
|
-
|
|
688
|
-
jQuery('.overview-list').hover(
|
|
689
|
-
() => {},
|
|
690
|
-
() => unHighlightRoutes(map, true),
|
|
691
|
-
);
|
|
692
736
|
});
|
|
737
|
+
|
|
738
|
+
const overviewList = document.querySelector('.overview-list');
|
|
739
|
+
if (overviewList) {
|
|
740
|
+
overviewList.addEventListener('mouseleave', () => {
|
|
741
|
+
unHighlightRoutes(map, true);
|
|
742
|
+
});
|
|
743
|
+
}
|
|
693
744
|
}
|