gtfs-to-html 2.9.12 → 2.9.14

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.9.12",
3
+ "version": "2.9.14",
4
4
  "private": false,
5
5
  "description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
6
6
  "keywords": [
@@ -52,8 +52,8 @@
52
52
  "archiver": "^7.0.1",
53
53
  "cli-table": "^0.3.11",
54
54
  "csv-stringify": "^6.5.1",
55
- "express": "^4.21.0",
56
- "gtfs": "^4.14.5",
55
+ "express": "^4.21.1",
56
+ "gtfs": "^4.15.1",
57
57
  "gtfs-realtime-pbf-js-module": "^1.0.0",
58
58
  "insane": "^2.6.2",
59
59
  "js-beautify": "^1.15.1",
@@ -64,7 +64,7 @@
64
64
  "pbf": "^4.0.1",
65
65
  "pretty-error": "^4.0.0",
66
66
  "pug": "^3.0.3",
67
- "puppeteer": "^23.4.0",
67
+ "puppeteer": "^23.5.3",
68
68
  "sanitize-filename": "^1.6.3",
69
69
  "sqlstring": "^2.3.3",
70
70
  "timer-machine": "^1.1.0",
@@ -74,17 +74,24 @@
74
74
  "yoctocolors": "^2.1.1"
75
75
  },
76
76
  "devDependencies": {
77
+ "@types/archiver": "^6.0.2",
77
78
  "@types/express": "^4.17.21",
79
+ "@types/insane": "^1.0.0",
80
+ "@types/js-beautify": "^1.14.3",
78
81
  "@types/lodash-es": "^4.17.12",
79
82
  "@types/morgan": "^1.9.9",
80
- "@types/node": "^20.16.6",
83
+ "@types/node": "^20.16.11",
84
+ "@types/pug": "^2.0.10",
85
+ "@types/puppeteer": "^5.4.7",
86
+ "@types/sanitize-filename": "^1.1.28",
81
87
  "@types/timer-machine": "^1.1.3",
88
+ "@types/untildify": "^3.0.0",
82
89
  "@types/yargs": "^17.0.33",
83
90
  "husky": "^9.1.6",
84
91
  "lint-staged": "^15.2.10",
85
92
  "prettier": "^3.3.3",
86
93
  "tsup": "^8.3.0",
87
- "typescript": "^5.6.2"
94
+ "typescript": "^5.6.3"
88
95
  },
89
96
  "engines": {
90
97
  "node": ">= 20.11.0"
@@ -247,7 +247,7 @@ a:hover {
247
247
  .timetable-page .timetable .table-vertical .stop-header {
248
248
  text-align: center;
249
249
  line-height: 1.15;
250
- font-size: 14px;
250
+ font-size: 0.875rem;
251
251
  }
252
252
 
253
253
  .timetable-page .timetable .run-header {
@@ -288,12 +288,12 @@ a:hover {
288
288
  }
289
289
 
290
290
  .timetable-page .timetable .city-row {
291
- font-size: 1.5em;
291
+ font-size: 1.5rem;
292
292
  color: #415d86;
293
293
  }
294
294
 
295
295
  .timetable-page .timetable th.city-column {
296
- font-size: 1.5em;
296
+ font-size: 1.5rem;
297
297
  text-align: center;
298
298
  }
299
299
 
@@ -413,7 +413,7 @@ a:hover {
413
413
  display: flex;
414
414
  align-items: center;
415
415
  justify-content: center;
416
- font-size: 12px;
416
+ font-size: 0.75rem;
417
417
  letter-spacing: -0.5px;
418
418
  padding: 0 2px;
419
419
  flex-shrink: 0;
@@ -428,7 +428,7 @@ a:hover {
428
428
  display: flex;
429
429
  align-items: center;
430
430
  justify-content: center;
431
- font-size: 20px;
431
+ font-size: 1.25rem;
432
432
  font-weight: bold;
433
433
  letter-spacing: -1px;
434
434
  padding: 0 6px;
@@ -498,6 +498,27 @@ a:hover {
498
498
  grid-template-columns: auto 1fr;
499
499
  gap: 0.5rem;
500
500
  line-height: 1;
501
+ padding-top: 5px;
502
+ }
503
+
504
+ .timetable-page
505
+ .map
506
+ .mapboxgl-popup-content
507
+ .upcoming-stops div:nth-child(1) {
508
+ font-weight: bold;
509
+ border-bottom: 1px solid #dddddd;
510
+ padding-bottom: 3px;
511
+ margin-bottom: -3px;
512
+ }
513
+
514
+ .timetable-page
515
+ .map
516
+ .mapboxgl-popup-content
517
+ .upcoming-stops div:nth-child(2) {
518
+ font-weight: bold;
519
+ border-bottom: 1px solid #dddddd;
520
+ padding-bottom: 3px;
521
+ margin-bottom: -3px;
501
522
  }
502
523
 
503
524
  .timetable-page
@@ -509,6 +530,16 @@ a:hover {
509
530
  font-weight: bold;
510
531
  }
511
532
 
533
+ .timetable-page .map .mapboxgl-popup-content .vehicle-updated {
534
+ padding-top: 5px;
535
+ font-size: 10px;
536
+ text-align: right;
537
+ }
538
+
539
+ .timetable-page .map .vehicle-popup .mapboxgl-popup-content {
540
+ padding-bottom: 5px;
541
+ }
542
+
512
543
  .timetable-page .map-legend {
513
544
  max-width: 30%;
514
545
  background-color: #fff;
@@ -560,6 +591,7 @@ a:hover {
560
591
  display: flex;
561
592
  align-items: center;
562
593
  justify-content: center;
594
+ cursor: pointer;
563
595
  }
564
596
 
565
597
  .timetable-page .vehicle-marker .vehicle-marker-arrow {
@@ -570,6 +602,12 @@ a:hover {
570
602
  height: 14px;
571
603
  }
572
604
 
605
+ .timetable-page .vehicle-marker .vehicle-marker-arrow.no-bearing {
606
+ background-image: url('data:image/svg+xml,<%3Fxml version="1.0" encoding="utf-8"%3F><svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7.8 10a2.2 2.2 0 0 0 4.4 0 2.2 2.2 0 0 0-4.4 0z"/></svg>');
607
+ width: 20px;
608
+ height: 20px;
609
+ }
610
+
573
611
  .timetable-page .timetable-alerts {
574
612
  margin-bottom: 1.5rem;
575
613
  }
@@ -614,7 +652,7 @@ a:hover {
614
652
  display: flex;
615
653
  align-items: center;
616
654
  justify-content: center;
617
- font-size: 12px;
655
+ font-size: 0.75rem;
618
656
  letter-spacing: -0.5px;
619
657
  padding: 0 2px;
620
658
  flex-shrink: 0;
@@ -53,8 +53,14 @@
53
53
  routeData[route.route_id] = route
54
54
  }
55
55
 
56
- const stop = _.pick(feature.properties, ['stop_id', 'stop_code', 'stop_name', 'parent_station'])
57
- stopData[stop.stop_id] = stop
56
+ stopData[feature.properties.stop_id] = {
57
+ stop_id: feature.properties.stop_id,
58
+ stop_code: feature.properties.stop_code,
59
+ stop_name: feature.properties.stop_name,
60
+ parent_station: feature.properties.parent_station,
61
+ stop_lat: feature.geometry.coordinates[1],
62
+ stop_lon: feature.geometry.coordinates[0],
63
+ }
58
64
 
59
65
  feature.properties = {
60
66
  route_ids: feature.properties.routes.map(route => route.route_id),
@@ -187,7 +187,9 @@ function formatMovingText(vehiclePosition) {
187
187
  }
188
188
 
189
189
  function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
190
- const html = jQuery('<div>');
190
+ const html = jQuery('<div>', {
191
+ id: `vehicle-popup-${vehiclePosition.vehicle.vehicle.id}`,
192
+ });
191
193
 
192
194
  const lastUpdated = new Date(vehiclePosition.vehicle.timestamp * 1000);
193
195
  const directionName = jQuery(
@@ -209,12 +211,7 @@ function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
209
211
  jQuery('<div>').text(movingText).appendTo(html);
210
212
  }
211
213
 
212
- jQuery('<div>')
213
- .append(
214
- jQuery('<small>').text(`Updated: ${lastUpdated.toLocaleTimeString()}`),
215
- )
216
- .appendTo(html);
217
-
214
+ const numberOfArrivalsToShow = 5;
218
215
  const nextArrivals = [];
219
216
  if (vehicleTripUpdate && vehicleTripUpdate.trip_update.stop_time_update) {
220
217
  for (const stoptimeUpdate of vehicleTripUpdate.trip_update
@@ -233,7 +230,7 @@ function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
233
230
  });
234
231
  }
235
232
 
236
- if (nextArrivals.length >= 3) {
233
+ if (nextArrivals.length >= numberOfArrivalsToShow) {
237
234
  break;
238
235
  }
239
236
  }
@@ -241,12 +238,12 @@ function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
241
238
  }
242
239
 
243
240
  if (nextArrivals.length > 0) {
244
- jQuery('<div>')
245
- .append(jQuery('<small>').text('Upcoming Stops:'))
246
- .appendTo(html);
247
-
248
241
  jQuery('<div>')
249
242
  .addClass('upcoming-stops')
243
+ .append([
244
+ jQuery('<div>').text('Time'),
245
+ jQuery('<div>').text('Upcoming Stop'),
246
+ ])
250
247
  .append(
251
248
  nextArrivals.flatMap((arrival) => {
252
249
  let delay = '';
@@ -266,9 +263,104 @@ function getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate) {
266
263
  .appendTo(html);
267
264
  }
268
265
 
266
+ jQuery('<div>')
267
+ .addClass('vehicle-updated')
268
+ .text(`Updated: ${lastUpdated.toLocaleTimeString()}`)
269
+ .appendTo(html);
270
+
269
271
  return html.prop('outerHTML');
270
272
  }
271
273
 
274
+ function getVehicleBearing(vehiclePosition, vehicleTripUpdate) {
275
+ // If vehicle position includes bearing, use that
276
+ if (
277
+ vehiclePosition.vehicle.position.bearing !== undefined &&
278
+ vehiclePosition.vehicle.position.bearing !== 0
279
+ ) {
280
+ return vehiclePosition.vehicle.position.bearing;
281
+ }
282
+
283
+ // Else try to calculate bearing from next stop
284
+ if (
285
+ vehicleTripUpdate &&
286
+ vehicleTripUpdate?.trip_update?.stop_time_update?.length > 0
287
+ ) {
288
+ const nextStopTimeUpdate =
289
+ vehicleTripUpdate.trip_update.stop_time_update[0];
290
+ const nextStop = stopData[nextStopTimeUpdate.stop_id];
291
+
292
+ if (nextStop && nextStop.stop_lat && nextStop.stop_lon) {
293
+ const vehicleLocation = vehiclePosition.vehicle.position;
294
+ const lat1 = vehicleLocation.latitude;
295
+ const lon1 = vehicleLocation.longitude;
296
+ const lat2 = nextStop.stop_lat;
297
+ const lon2 = nextStop.stop_lon;
298
+
299
+ const y = Math.sin(lon2 - lon1) * Math.cos(lat2);
300
+ const x =
301
+ Math.cos(lat1) * Math.sin(lat2) -
302
+ Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1);
303
+ let bearing = (Math.atan2(y, x) * 180) / Math.PI;
304
+ bearing = (bearing + 360) % 360;
305
+
306
+ return bearing;
307
+ }
308
+ }
309
+
310
+ return null;
311
+ }
312
+
313
+ function getVehicleDirectionArrow(vehiclePosition, vehicleTripUpdate) {
314
+ const bearing = getVehicleBearing(vehiclePosition, vehicleTripUpdate);
315
+
316
+ if (bearing !== null) {
317
+ return `<div class="vehicle-marker-arrow" aria-hidden="true" style="transform:rotate(${bearing}deg)"></div>`;
318
+ } else {
319
+ return `<div class="vehicle-marker-arrow no-bearing" aria-hidden="true"></div>`;
320
+ }
321
+ }
322
+
323
+ function attachVehicleMarkerClickHandler(
324
+ vehiclePosition,
325
+ vehicleTripUpdate,
326
+ map,
327
+ ) {
328
+ const coordinates = [
329
+ vehiclePosition.vehicle.position.longitude,
330
+ vehiclePosition.vehicle.position.latitude,
331
+ ];
332
+
333
+ const vehicleMarker = vehicleMarkers[vehiclePosition.vehicle.vehicle.id];
334
+
335
+ vehicleMarker
336
+ .getElement()
337
+ .removeEventListener(
338
+ 'click',
339
+ vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id],
340
+ );
341
+
342
+ vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id] = (
343
+ event,
344
+ ) => {
345
+ event.stopPropagation();
346
+ if (vehiclePopup.isOpen()) {
347
+ vehiclePopup.remove();
348
+ }
349
+
350
+ vehiclePopup
351
+ .setLngLat(coordinates)
352
+ .setHTML(getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate))
353
+ .addTo(map);
354
+ };
355
+
356
+ vehicleMarker
357
+ .getElement()
358
+ .addEventListener(
359
+ 'click',
360
+ vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id],
361
+ );
362
+ }
363
+
272
364
  function addVehicleMarker(vehiclePosition, vehicleTripUpdate) {
273
365
  if (!vehiclePosition.vehicle || !vehiclePosition.vehicle.position) {
274
366
  return;
@@ -276,44 +368,40 @@ function addVehicleMarker(vehiclePosition, vehicleTripUpdate) {
276
368
 
277
369
  const visibleTimetableId = jQuery('.timetable:visible').data('timetable-id');
278
370
 
371
+ const vehicleDirectionArrow = getVehicleDirectionArrow(
372
+ vehiclePosition,
373
+ vehicleTripUpdate,
374
+ );
375
+
279
376
  // Create a DOM element for each marker
280
377
  const el = document.createElement('div');
281
378
  el.className = 'vehicle-marker';
282
379
  el.style.width = '20px';
283
380
  el.style.height = '20px';
284
- el.innerHTML = `<div class="vehicle-marker-arrow" aria-hidden="true" style="transform:rotate(${vehiclePosition.vehicle.position.bearing}deg)"></div>`;
381
+
382
+ if (vehicleDirectionArrow) {
383
+ el.innerHTML = vehicleDirectionArrow;
384
+ }
285
385
 
286
386
  const coordinates = [
287
387
  vehiclePosition.vehicle.position.longitude,
288
388
  vehiclePosition.vehicle.position.latitude,
289
389
  ];
290
390
 
291
- vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id] = () => {
292
- vehiclePopup
293
- .setLngLat(coordinates)
294
- .setHTML(getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate))
295
- .addTo(maps[visibleTimetableId]);
296
- };
297
-
298
- // Vehicle marker popups
299
- el.addEventListener(
300
- 'mouseenter',
301
- vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id],
302
- );
303
-
304
- el.addEventListener('mouseleave', () => {
305
- vehiclePopup.remove();
306
- });
307
-
308
391
  // Add marker to map
309
- const marker = new mapboxgl.Marker(el)
392
+ const vehicleMarker = new mapboxgl.Marker(el)
310
393
  .setLngLat(coordinates)
311
394
  .addTo(maps[visibleTimetableId]);
312
395
 
313
- return marker;
396
+ vehicleMarkers[vehiclePosition.vehicle.vehicle.id] = vehicleMarker;
314
397
  }
315
398
 
316
- function animateVehicleMarker(vehicleMarker, newCoordinates) {
399
+ function animateVehicleMarker(vehicleMarker, vehiclePosition) {
400
+ const newCoordinates = [
401
+ vehiclePosition.vehicle.position.longitude,
402
+ vehiclePosition.vehicle.position.latitude,
403
+ ];
404
+
317
405
  let startTime;
318
406
  const duration = 5000;
319
407
  const previousCoordinates = vehicleMarker.getLngLat().toArray();
@@ -331,7 +419,18 @@ function animateVehicleMarker(vehicleMarker, newCoordinates) {
331
419
  previousCoordinates[1] + safeProgress * latitudeDifference;
332
420
 
333
421
  vehicleMarker.setLngLat([newLongitude, newLatitude]);
334
- vehiclePopup.setLngLat([newLongitude, newLatitude]);
422
+
423
+ // Check if vehiclePopup element exists and is for this vehicle
424
+ const popupElement = vehiclePopup.getElement();
425
+ const vehiclePopupContentId = `vehicle-popup-${vehiclePosition.vehicle.vehicle.id}`;
426
+ const markerPopupIsOpenForThisVehicle =
427
+ popupElement && popupElement.querySelector(`#${vehiclePopupContentId}`);
428
+
429
+ // Check if the open vehicle popup is for this vehicle
430
+ if (vehiclePopup.isOpen() && markerPopupIsOpenForThisVehicle) {
431
+ // Animate the popup along with the vehicle marker
432
+ vehiclePopup.setLngLat([newLongitude, newLatitude]);
433
+ }
335
434
 
336
435
  if (safeProgress != 1) {
337
436
  requestAnimationFrame(animation);
@@ -346,36 +445,18 @@ function updateVehicleMarkerLocation(
346
445
  vehiclePosition,
347
446
  vehicleTripUpdate,
348
447
  ) {
349
- const visibleTimetableId = jQuery('.timetable:visible').data('timetable-id');
350
-
351
- const coordinates = [
352
- vehiclePosition.vehicle.position.longitude,
353
- vehiclePosition.vehicle.position.latitude,
354
- ];
355
- vehicleMarker.getElement().innerHTML = `<div class="vehicle-marker-arrow" aria-hidden="true" style="transform:rotate(${vehiclePosition.vehicle.position.bearing}deg)"></div>`;
356
-
357
- vehicleMarker
358
- .getElement()
359
- .removeEventListener(
360
- 'mouseenter',
361
- vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id],
362
- );
363
-
364
- vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id] = () => {
365
- vehiclePopup
366
- .setLngLat(coordinates)
367
- .setHTML(getVehiclePopupHtml(vehiclePosition, vehicleTripUpdate))
368
- .addTo(maps[visibleTimetableId]);
369
- };
448
+ const vehicleDirectionArrow = getVehicleDirectionArrow(
449
+ vehiclePosition,
450
+ vehicleTripUpdate,
451
+ );
370
452
 
371
- vehicleMarker
372
- .getElement()
373
- .addEventListener(
374
- 'mouseenter',
375
- vehicleMarkersEventListeners[vehiclePosition.vehicle.vehicle.id],
376
- );
453
+ if (vehicleDirectionArrow) {
454
+ vehicleMarker.getElement().innerHTML = vehicleDirectionArrow;
455
+ } else {
456
+ vehicleMarker.getElement().innerHTML = '';
457
+ }
377
458
 
378
- animateVehicleMarker(vehicleMarker, coordinates);
459
+ animateVehicleMarker(vehicleMarker, vehiclePosition);
379
460
  }
380
461
 
381
462
  async function fetchGtfsRealtime(url, headers) {
@@ -470,12 +551,9 @@ async function updateArrivals() {
470
551
 
471
552
  let vehicleMarker = vehicleMarkers[vehicleId];
472
553
 
473
- // If not on map, add it
474
554
  if (vehicleMarker === undefined) {
475
- vehicleMarkers[vehicleId] = addVehicleMarker(
476
- vehiclePosition,
477
- vehicleTripUpdate,
478
- );
555
+ // If not on map, add it
556
+ addVehicleMarker(vehiclePosition, vehicleTripUpdate);
479
557
  } else {
480
558
  // Otherwise update location
481
559
  updateVehicleMarkerLocation(
@@ -484,6 +562,14 @@ async function updateArrivals() {
484
562
  vehicleTripUpdate,
485
563
  );
486
564
  }
565
+
566
+ const visibleTimetableId =
567
+ jQuery('.timetable:visible').data('timetable-id');
568
+ attachVehicleMarkerClickHandler(
569
+ vehiclePosition,
570
+ vehicleTripUpdate,
571
+ maps[visibleTimetableId],
572
+ );
487
573
  }
488
574
 
489
575
  // Remove vehicles not in the feed
@@ -509,39 +595,19 @@ function toggleMap(id) {
509
595
 
510
596
  // Update vehicle markers to use the current visible map
511
597
  for (const [vehicleId, vehicleMarker] of Object.entries(vehicleMarkers)) {
512
- const coordinates = vehicleMarker.getLngLat();
513
-
514
- // Remove previous event listeners
515
- vehicleMarker
516
- .getElement()
517
- .removeEventListener(
518
- 'mouseenter',
519
- vehicleMarkersEventListeners[vehicleId],
520
- );
521
-
522
598
  const vehiclePosition = vehiclePositions.find(
523
599
  (vehiclePosition) => vehiclePosition.vehicle.vehicle.id === vehicleId,
524
600
  );
525
601
 
526
- const tripUpdate = tripUpdates.find(
602
+ const vehicleTripUpdate = tripUpdates.find(
527
603
  (tripUpdate) => tripUpdate.trip_update.vehicle.id === vehicleId,
528
604
  );
529
605
 
530
- // Update event listener function to use the new map
531
- vehicleMarkersEventListeners[vehicleId] = () => {
532
- vehiclePopup
533
- .setLngLat(coordinates)
534
- .setHTML(getVehiclePopupHtml(vehiclePosition, tripUpdate))
535
- .addTo(maps[id]);
536
- };
537
-
538
- // Add updated event listener to marker
539
- vehicleMarker
540
- .getElement()
541
- .addEventListener(
542
- 'mouseenter',
543
- vehicleMarkersEventListeners[vehicleId],
544
- );
606
+ attachVehicleMarkerClickHandler(
607
+ vehiclePosition,
608
+ vehicleTripUpdate,
609
+ maps[id],
610
+ );
545
611
 
546
612
  // Move marker to the current visible map
547
613
  vehicleMarker.addTo(maps[id]);
@@ -891,10 +957,18 @@ function createMaps() {
891
957
  ) {
892
958
  // Popup for realtime vehicle locations
893
959
  vehiclePopup = new mapboxgl.Popup({
894
- closeButton: false,
895
960
  closeOnClick: false,
896
961
  className: 'vehicle-popup',
897
- offset: [0, -10],
962
+ offset: {
963
+ top: [0, 10],
964
+ bottom: [0, -10],
965
+ left: [10, 0],
966
+ right: [-10, 0],
967
+ 'top-left': [10, 10],
968
+ 'top-right': [-10, 10],
969
+ 'bottom-left': [10, -10],
970
+ 'bottom-right': [-10, -10],
971
+ },
898
972
  });
899
973
 
900
974
  const arrivalUpdateInterval = 10 * 1000; // 10 seconds