gtfs-to-html 2.11.5 → 2.12.1

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.5",
3
+ "version": "2.12.1",
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
- "@turf/helpers": "^7.2.0",
51
- "@turf/simplify": "^7.2.0",
50
+ "@maplibre/maplibre-gl-geocoder": "^1.9.1",
51
+ "@turf/helpers": "^7.3.0",
52
+ "@turf/simplify": "^7.3.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.13.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.29.0",
69
+ "puppeteer": "^24.30.0",
67
70
  "sanitize-filename": "^1.6.3",
68
71
  "sanitize-html": "^2.17.0",
69
72
  "sqlstring": "^2.3.3",
@@ -84,11 +87,11 @@
84
87
  "@types/sanitize-html": "^2.16.0",
85
88
  "@types/sqlstring": "^2.3.2",
86
89
  "@types/toposort": "^2.0.7",
87
- "@types/yargs": "^17.0.34",
90
+ "@types/yargs": "^17.0.35",
88
91
  "husky": "^9.1.7",
89
- "lint-staged": "^16.2.6",
92
+ "lint-staged": "^16.2.7",
90
93
  "prettier": "^3.6.2",
91
- "tsup": "^8.5.0",
94
+ "tsup": "^8.5.1",
92
95
  "typescript": "^5.9.3"
93
96
  },
94
97
  "engines": {
@@ -98,7 +98,7 @@ 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;
@@ -114,15 +114,15 @@ a:hover {
114
114
  justify-content: center;
115
115
  font-size: 20px;
116
116
  font-weight: bold;
117
- letter-spacing: -1px;
117
+ letter-spacing: -0.2px;
118
118
  padding: 0 6px;
119
119
  flex-shrink: 0;
120
120
  font-weight: bold;
121
121
  color: white;
122
122
  }
123
123
 
124
- .timetable-overview .btn-blue {
125
- color: rgb(255 255 255);
124
+ .timetable-overview .btn-active {
125
+ color: rgb(255, 255, 255);
126
126
  padding: 0.75rem 1.5rem;
127
127
  background-color: rgb(37 99 235);
128
128
  border-radius: 0.375rem;
@@ -133,7 +133,7 @@ a:hover {
133
133
  text-decoration: none;
134
134
  }
135
135
 
136
- .timetable-overview .btn-blue:hover {
136
+ .timetable-overview .btn-active:hover {
137
137
  background-color: rgb(29 78 216);
138
138
  text-decoration: none;
139
139
  }
@@ -201,8 +201,8 @@ a:hover {
201
201
  display: none;
202
202
  }
203
203
 
204
- .timetable-page .btn-blue {
205
- color: rgb(255 255 255);
204
+ .timetable-page .btn-active {
205
+ color: rgb(255, 255, 255);
206
206
  padding: 0.75rem 1.5rem;
207
207
  background-color: rgb(37 99 235);
208
208
  border-radius: 0.375rem;
@@ -213,12 +213,12 @@ a:hover {
213
213
  text-decoration: none;
214
214
  }
215
215
 
216
- .timetable-page .btn-blue:hover {
216
+ .timetable-page .btn-active:hover {
217
217
  background-color: rgb(29 78 216);
218
218
  text-decoration: none;
219
219
  }
220
220
 
221
- .timetable-page .btn-gray {
221
+ .timetable-page .btn-inactive {
222
222
  color: rgb(75 85 99);
223
223
  padding: 0.75rem 1.5rem;
224
224
  background-color: rgb(209 213 219);
@@ -230,7 +230,7 @@ a:hover {
230
230
  text-decoration: none;
231
231
  }
232
232
 
233
- .timetable-page .btn-gray:hover {
233
+ .timetable-page .btn-inactive:hover {
234
234
  background-color: rgb(201, 206, 213);
235
235
  text-decoration: none;
236
236
  }
@@ -422,7 +422,7 @@ 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;
@@ -438,7 +438,7 @@ a:hover {
438
438
  justify-content: center;
439
439
  font-size: 1.25rem;
440
440
  font-weight: bold;
441
- letter-spacing: -1px;
441
+ letter-spacing: -0.2px;
442
442
  padding: 0 6px;
443
443
  flex-shrink: 0;
444
444
  font-weight: bold;
@@ -551,33 +551,35 @@ a:hover {
551
551
  }
552
552
 
553
553
  .timetable-page .map-legend {
554
- padding: 0 10px;
554
+ padding: 8px 8px 0 8px;
555
+ display: flex;
556
+ flex-direction: column;
557
+ gap: 6px;
555
558
  }
556
559
 
557
560
  @media screen and (min-width: 768px) {
558
561
  .timetable-page .map-legend {
559
- padding: 10px;
562
+ padding: 8px 6px;
560
563
  background-color: #fff;
561
564
  border-radius: 3px;
562
565
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
563
566
  position: absolute;
564
567
  left: 10px;
565
- bottom: 35px;
568
+ bottom: 10px;
566
569
  z-index: 1;
570
+ gap: 8px;
567
571
  }
568
572
  }
569
573
 
570
574
  .timetable-page .map-legend .legend-item {
571
- padding: 4px 0;
572
575
  display: flex;
573
576
  flex-direction: row;
574
- align-items: start;
577
+ align-items: center;
575
578
  gap: 4px;
576
579
  }
577
580
 
578
581
  .timetable-page .map-legend .legend-item .legend-icon {
579
582
  width: 22px;
580
- padding-top: 3px;
581
583
  display: flex;
582
584
  flex-direction: row;
583
585
  align-items: center;
@@ -587,6 +589,7 @@ a:hover {
587
589
 
588
590
  .timetable-page .map-legend .legend-item .legend-text {
589
591
  font-size: 12px;
592
+ line-height: 1;
590
593
  }
591
594
 
592
595
  .timetable-page .stop-marker {
@@ -668,7 +671,7 @@ a:hover {
668
671
  align-items: center;
669
672
  justify-content: center;
670
673
  font-size: 0.75rem;
671
- letter-spacing: -0.5px;
674
+ letter-spacing: -0.1px;
672
675
  padding: 0 2px;
673
676
  flex-shrink: 0;
674
677
  font-weight: bold;
@@ -684,6 +687,15 @@ a:hover {
684
687
  margin-top: 0.5rem;
685
688
  }
686
689
 
690
+ .timetable-page .timetable-alerts .alert-body .alert-label {
691
+ margin-top: 0.5rem;
692
+ font-weight: bold;
693
+ }
694
+
695
+ .timetable-page .timetable-alerts .alert-body ul {
696
+ margin: 0.5rem 0;
697
+ }
698
+
687
699
  .timetable-page .timetable-alerts .alert-more-info {
688
700
  margin-top: 0.75rem;
689
701
  }
@@ -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-active 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
  }