gtfs-to-html 2.9.11 → 2.9.13

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.11",
3
+ "version": "2.9.13",
4
4
  "private": false,
5
5
  "description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
6
6
  "keywords": [
@@ -53,7 +53,7 @@
53
53
  "cli-table": "^0.3.11",
54
54
  "csv-stringify": "^6.5.1",
55
55
  "express": "^4.21.0",
56
- "gtfs": "^4.14.4",
56
+ "gtfs": "^4.14.5",
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.3.1",
67
+ "puppeteer": "^23.4.1",
68
68
  "sanitize-filename": "^1.6.3",
69
69
  "sqlstring": "^2.3.3",
70
70
  "timer-machine": "^1.1.0",
@@ -77,7 +77,7 @@
77
77
  "@types/express": "^4.17.21",
78
78
  "@types/lodash-es": "^4.17.12",
79
79
  "@types/morgan": "^1.9.9",
80
- "@types/node": "^20.16.5",
80
+ "@types/node": "^20.16.10",
81
81
  "@types/timer-machine": "^1.1.3",
82
82
  "@types/yargs": "^17.0.33",
83
83
  "husky": "^9.1.6",
@@ -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 {
@@ -265,6 +265,7 @@ a:hover {
265
265
 
266
266
  .timetable-page .timetable .stop-code {
267
267
  font-weight: normal;
268
+ padding-top: 0.25rem;
268
269
  }
269
270
 
270
271
  .timetable-page .timetable .stop-time {
@@ -287,12 +288,12 @@ a:hover {
287
288
  }
288
289
 
289
290
  .timetable-page .timetable .city-row {
290
- font-size: 1.5em;
291
+ font-size: 1.5rem;
291
292
  color: #415d86;
292
293
  }
293
294
 
294
295
  .timetable-page .timetable th.city-column {
295
- font-size: 1.5em;
296
+ font-size: 1.5rem;
296
297
  text-align: center;
297
298
  }
298
299
 
@@ -412,7 +413,7 @@ a:hover {
412
413
  display: flex;
413
414
  align-items: center;
414
415
  justify-content: center;
415
- font-size: 12px;
416
+ font-size: 0.75rem;
416
417
  letter-spacing: -0.5px;
417
418
  padding: 0 2px;
418
419
  flex-shrink: 0;
@@ -427,7 +428,7 @@ a:hover {
427
428
  display: flex;
428
429
  align-items: center;
429
430
  justify-content: center;
430
- font-size: 20px;
431
+ font-size: 1.25rem;
431
432
  font-weight: bold;
432
433
  letter-spacing: -1px;
433
434
  padding: 0 6px;
@@ -497,6 +498,27 @@ a:hover {
497
498
  grid-template-columns: auto 1fr;
498
499
  gap: 0.5rem;
499
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;
500
522
  }
501
523
 
502
524
  .timetable-page
@@ -508,6 +530,16 @@ a:hover {
508
530
  font-weight: bold;
509
531
  }
510
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
+
511
543
  .timetable-page .map-legend {
512
544
  max-width: 30%;
513
545
  background-color: #fff;
@@ -559,6 +591,7 @@ a:hover {
559
591
  display: flex;
560
592
  align-items: center;
561
593
  justify-content: center;
594
+ cursor: pointer;
562
595
  }
563
596
 
564
597
  .timetable-page .vehicle-marker .vehicle-marker-arrow {
@@ -569,6 +602,12 @@ a:hover {
569
602
  height: 14px;
570
603
  }
571
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
+
572
611
  .timetable-page .timetable-alerts {
573
612
  margin-bottom: 1.5rem;
574
613
  }
@@ -613,7 +652,7 @@ a:hover {
613
652
  display: flex;
614
653
  align-items: center;
615
654
  justify-content: center;
616
- font-size: 12px;
655
+ font-size: 0.75rem;
617
656
  letter-spacing: -0.5px;
618
657
  padding: 0 2px;
619
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