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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs-to-html",
3
- "version": "2.11.4",
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
- "marked": "^16.4.1",
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.26.1",
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.4",
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.5px;
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: -1px;
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.5px;
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: -1px;
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 10px;
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: 10px;
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: 35px;
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: start;
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.5px;
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[formatHtmlId(timetable.timetable_id)] = optimizeGeojsonProperties(minifiedGeojson)
97
+ geojsons[timetable.timetable_id] = optimizeGeojsonProperties(minifiedGeojson)
98
98
  }
99
99
 
100
100
  const gtfsRealtimeUrls = {}
@@ -1,4 +1,4 @@
1
- /* global document, jQuery, _, maplibregl, geojson, mapStyleUrl */
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 html = route.route_url
14
- ? jQuery('<a>').attr('href', route.route_url)
15
- : jQuery('<div>');
13
+ const element = route.route_url
14
+ ? document.createElement('a')
15
+ : document.createElement('div');
16
16
 
17
- html.addClass('map-route-item');
17
+ element.className = 'map-route-item';
18
18
 
19
- const routeItemDivs = [];
19
+ if (route.route_url) {
20
+ element.href = route.route_url;
21
+ }
20
22
 
21
23
  if (route.route_color) {
22
- routeItemDivs.push(
23
- jQuery('<div>')
24
- .addClass('route-color-swatch')
25
- .css('backgroundColor', formatRouteColor(route))
26
- .css('color', formatRouteTextColor(route))
27
- .text(route.route_short_name ?? ''),
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
- html.append(routeItemDivs);
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 html.prop('outerHTML');
38
+ return element.outerHTML;
39
39
  }
40
40
 
41
41
  function formatRoutePopup(features) {
42
- const html = jQuery('<div>');
42
+ const container = document.createElement('div');
43
43
 
44
44
  if (features.length > 1) {
45
- jQuery('<div>').addClass('popup-title').text('Routes').appendTo(html);
45
+ const title = document.createElement('div');
46
+ title.className = 'popup-title';
47
+ title.textContent = 'Routes';
48
+ container.appendChild(title);
46
49
  }
47
50
 
48
- jQuery(html).append(
49
- features.map((feature) => formatRoute(feature.properties)),
50
- );
51
+ features.forEach((feature) => {
52
+ const routeHTML = formatRoute(feature.properties);
53
+ container.insertAdjacentHTML('beforeend', routeHTML);
54
+ });
51
55
 
52
- return html.prop('outerHTML');
56
+ return container.outerHTML;
53
57
  }
54
58
 
55
59
  function formatStopPopup(feature) {
56
- const routes = JSON.parse(feature.properties.routes);
57
- const html = jQuery('<div>');
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
- jQuery('<div>')
60
- .addClass('popup-title')
61
- .text(feature.properties.stop_name)
62
- .appendTo(html);
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
- jQuery('<div>')
66
- .html([
67
- jQuery('<div>').addClass('popup-label').text('Stop Code:'),
68
- jQuery('<strong>').text(feature.properties.stop_code),
69
- ])
70
- .appendTo(html);
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
- jQuery('<div>').addClass('popup-label').text('Routes Served:').appendTo(html);
88
+ const routesLabel = document.createElement('div');
89
+ routesLabel.className = 'popup-label';
90
+ routesLabel.textContent = 'Routes Served:';
91
+ container.appendChild(routesLabel);
74
92
 
75
- jQuery(html).append(
76
- jQuery('<div>')
77
- .addClass('route-list')
78
- .html(routes.map((route) => formatRoute(route))),
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
- jQuery('<a>')
82
- .addClass('btn-blue btn-sm')
83
- .prop(
84
- 'href',
85
- `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`,
86
- )
87
- .prop('target', '_blank')
88
- .prop('rel', 'noopener noreferrer')
89
- .html('View on Streetview')
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
- jQuery('#system_map').hide();
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, routes);
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 geojson = await response.json();
167
- for (const feature of geojson.features) {
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, routes) {
517
- map.on('mousemove', (event) => handleMouseMove(event, map, routes));
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, routes) {
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
- highlightRoutes(
529
- map,
530
- _.compact(_.uniq(features.map((feature) => feature.properties.route_id))),
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
- const routes = _.orderBy(
578
- _.uniqBy(features, (feature) => feature.properties.route_short_name),
579
- (feature) => Number.parseInt(feature.properties.route_short_name, 10),
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
- jQuery(() => {
680
- jQuery('.overview-list a').hover((event) => {
681
- const routeIdString = jQuery(event.target).data('route-ids');
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
  }