gtfs-to-html 2.10.17 → 2.11.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.
@@ -23,14 +23,15 @@ import {
23
23
  readFile,
24
24
  rm
25
25
  } from "fs/promises";
26
+ import { homedir } from "os";
26
27
  import * as _ from "lodash-es";
28
+ import { uniqBy as uniqBy2 } from "lodash-es";
27
29
  import archiver from "archiver";
28
30
  import beautify from "js-beautify";
29
31
  import sanitizeHtml from "sanitize-html";
30
32
  import { renderFile } from "pug";
31
33
  import puppeteer from "puppeteer";
32
34
  import sanitize from "sanitize-filename";
33
- import untildify from "untildify";
34
35
  import { marked } from "marked";
35
36
 
36
37
  // src/lib/formatters.ts
@@ -59,16 +60,7 @@ function fromGTFSTime(timeString) {
59
60
  function toGTFSTime(time) {
60
61
  return time.format("HH:mm:ss");
61
62
  }
62
- function fromGTFSDate(gtfsDate) {
63
- return moment(gtfsDate, "YYYYMMDD");
64
- }
65
- function toGTFSDate(date) {
66
- return moment(date).format("YYYYMMDD");
67
- }
68
63
  function calendarToCalendarCode(c) {
69
- if (c.service_id) {
70
- return c.service_id;
71
- }
72
64
  return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
73
65
  }
74
66
  function calendarCodeToCalendar(code) {
@@ -109,13 +101,13 @@ import {
109
101
  find,
110
102
  findLast,
111
103
  first,
112
- flatMap as flatMap2,
113
- flattenDeep,
104
+ flatMap,
114
105
  flow,
115
106
  groupBy,
116
107
  head,
117
108
  last,
118
109
  maxBy,
110
+ orderBy,
119
111
  partialRight,
120
112
  reduce,
121
113
  size,
@@ -149,7 +141,6 @@ import toposort from "toposort";
149
141
 
150
142
  // src/lib/geojson-utils.ts
151
143
  import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
152
- import { flatMap } from "lodash-es";
153
144
  import simplify from "@turf/simplify";
154
145
  import { featureCollection, round } from "@turf/helpers";
155
146
 
@@ -311,7 +302,7 @@ function progressBar(formatString, barTotal, config) {
311
302
  }
312
303
 
313
304
  // src/lib/geojson-utils.ts
314
- var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
305
+ var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
315
306
  var truncateGeoJSONDecimals = (geojson, config) => {
316
307
  for (const feature of geojson.features) {
317
308
  if (feature.geometry.coordinates) {
@@ -502,7 +493,7 @@ function formatTripNameForCSV(trip, timetable) {
502
493
  // package.json
503
494
  var package_default = {
504
495
  name: "gtfs-to-html",
505
- version: "2.10.17",
496
+ version: "2.11.0",
506
497
  private: false,
507
498
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
508
499
  keywords: [
@@ -555,24 +546,22 @@ var package_default = {
555
546
  "cli-table": "^0.3.11",
556
547
  "csv-stringify": "^6.6.0",
557
548
  express: "^5.1.0",
558
- gtfs: "^4.17.7",
549
+ gtfs: "^4.18.0",
559
550
  "gtfs-realtime-pbf-js-module": "^1.0.0",
560
551
  "js-beautify": "^1.15.4",
561
552
  "lodash-es": "^4.17.21",
562
- marked: "^16.1.2",
553
+ marked: "^16.3.0",
563
554
  moment: "^2.30.1",
564
555
  pbf: "^4.0.1",
565
556
  "pretty-error": "^4.0.0",
566
557
  pug: "^3.0.3",
567
- puppeteer: "^24.16.2",
558
+ puppeteer: "^24.21.0",
568
559
  "sanitize-filename": "^1.6.3",
569
560
  "sanitize-html": "^2.17.0",
570
561
  sqlstring: "^2.3.3",
571
- "timer-machine": "^1.1.0",
572
562
  toposort: "^2.0.2",
573
- untildify: "^5.0.0",
574
563
  yargs: "^18.0.0",
575
- yoctocolors: "^2.1.1"
564
+ yoctocolors: "^2.1.2"
576
565
  },
577
566
  devDependencies: {
578
567
  "@types/archiver": "^6.0.3",
@@ -584,10 +573,11 @@ var package_default = {
584
573
  "@types/node": "^22",
585
574
  "@types/pug": "^2.0.10",
586
575
  "@types/sanitize-html": "^2.16.0",
587
- "@types/timer-machine": "^1.1.3",
576
+ "@types/sqlstring": "^2.3.2",
577
+ "@types/toposort": "^2.0.7",
588
578
  "@types/yargs": "^17.0.33",
589
579
  husky: "^9.1.7",
590
- "lint-staged": "^16.1.5",
580
+ "lint-staged": "^16.1.6",
591
581
  prettier: "^3.6.2",
592
582
  tsup: "^8.5.0",
593
583
  typescript: "^5.9.2"
@@ -643,7 +633,7 @@ var findCommonStopId = (trips, config) => {
643
633
  return null;
644
634
  }
645
635
  const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
646
- if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
636
+ if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes)?.stop_id) {
647
637
  return false;
648
638
  }
649
639
  if (isNullOrEmpty(stoptime.arrival_time)) {
@@ -696,11 +686,7 @@ var sortTrips = (trips, config) => {
696
686
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
697
687
  );
698
688
  }
699
- sortedTrips = sortBy(
700
- trips,
701
- ["firstStoptime", "lastStoptime"],
702
- ["asc", "asc"]
703
- );
689
+ sortedTrips = sortBy(trips, ["firstStoptime", "lastStoptime"]);
704
690
  } else if (config.sortingAlgorithm === "end") {
705
691
  for (const trip of trips) {
706
692
  if (trip.stoptimes.length === 0) {
@@ -711,11 +697,7 @@ var sortTrips = (trips, config) => {
711
697
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
712
698
  );
713
699
  }
714
- sortedTrips = sortBy(
715
- trips,
716
- ["lastStoptime", "firstStoptime"],
717
- ["asc", "asc"]
718
- );
700
+ sortedTrips = sortBy(trips, ["lastStoptime", "firstStoptime"]);
719
701
  } else if (config.sortingAlgorithm === "first") {
720
702
  const longestTripStoptimes = getLongestTripStoptimes(trips, config);
721
703
  const firstStopId = first(longestTripStoptimes).stop_id;
@@ -742,8 +724,8 @@ var getCalendarDatesForTimetable = (timetable, config) => {
742
724
  [],
743
725
  [["date", "ASC"]]
744
726
  );
745
- const start = fromGTFSDate(timetable.start_date);
746
- const end = fromGTFSDate(timetable.end_date);
727
+ const start = moment2(timetable.start_date, "YYYYMMDD");
728
+ const end = moment2(timetable.end_date, "YYYYMMDD");
747
729
  const excludedDates = /* @__PURE__ */ new Set();
748
730
  const includedDates = /* @__PURE__ */ new Set();
749
731
  for (const calendarDate of calendarDates) {
@@ -868,28 +850,70 @@ var getTimetableNotesForTimetable = (timetable, config) => {
868
850
  }));
869
851
  return sortBy(formattedNotes, "symbol");
870
852
  };
871
- var convertTimetableToTimetablePage = (timetable, config) => {
872
- if (!timetable.routes) {
873
- timetable.routes = getRoutes({
874
- route_id: timetable.route_ids
875
- });
876
- }
877
- const filename = generateFileName(timetable, config, "html");
853
+ var createTimetablePage = ({
854
+ timetablePageId,
855
+ timetables,
856
+ config
857
+ }) => {
858
+ const updatedTimetables = timetables.map((timetable) => {
859
+ if (!timetable.routes) {
860
+ timetable.routes = getRoutes({
861
+ route_id: timetable.route_ids
862
+ });
863
+ }
864
+ return timetable;
865
+ });
866
+ const timetablePage = {
867
+ timetable_page_id: timetablePageId,
868
+ timetables: updatedTimetables,
869
+ routes: updatedTimetables.flatMap((timetable) => timetable.routes)
870
+ };
871
+ const filename = generateTimetablePageFileName(timetablePage, config);
878
872
  return {
879
- timetable_page_id: timetable.timetable_id,
880
- timetable_page_label: timetable.timetable_label,
881
- timetables: [timetable],
873
+ ...timetablePage,
882
874
  filename
883
875
  };
884
876
  };
885
- var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config) => {
886
- const timetable = {
877
+ var createTimetable = ({
878
+ route,
879
+ directionId,
880
+ tripHeadsign,
881
+ calendars,
882
+ calendarDates
883
+ }) => {
884
+ const serviceIds = uniq([
885
+ ...calendars?.map((calendar) => calendar.service_id) ?? [],
886
+ ...calendarDates?.map((calendarDate) => calendarDate.service_id) ?? []
887
+ ]);
888
+ const days2 = {};
889
+ let startDate = null;
890
+ let endDate = null;
891
+ if (calendars && calendars.length > 0) {
892
+ Object.assign(days2, getDaysFromCalendars(calendars));
893
+ startDate = parseInt(
894
+ moment2.min(
895
+ calendars.map((calendar) => moment2(calendar.start_date, "YYYYMMDD"))
896
+ ).format("YYYYMMDD"),
897
+ 10
898
+ );
899
+ endDate = parseInt(
900
+ moment2.max(calendars.map((calendar) => moment2(calendar.end_date, "YYYYMMDD"))).format("YYYYMMDD"),
901
+ 10
902
+ );
903
+ }
904
+ const timetableId = formatTimetableId({
905
+ routeIds: [route.route_id],
906
+ directionId,
907
+ days: days2
908
+ });
909
+ return {
910
+ timetable_id: timetableId,
887
911
  route_ids: [route.route_id],
888
- direction_id: direction ? direction.direction_id : void 0,
889
- direction_name: direction ? direction.trip_headsign : void 0,
912
+ direction_id: directionId === null ? null : directionId,
913
+ direction_name: tripHeadsign === null ? null : tripHeadsign,
890
914
  routes: [route],
891
915
  include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
892
- service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
916
+ service_ids: serviceIds,
893
917
  service_notes: null,
894
918
  timetable_label: null,
895
919
  start_time: null,
@@ -897,88 +921,83 @@ var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, c
897
921
  orientation: null,
898
922
  timetable_sequence: null,
899
923
  show_trip_continuation: null,
900
- start_date: null,
901
- end_date: null
924
+ start_date: startDate,
925
+ end_date: endDate,
926
+ ...days2
902
927
  };
903
- if (calendars && calendars.length > 0) {
904
- Object.assign(timetable, getDaysFromCalendars(calendars));
905
- timetable.start_date = toGTFSDate(
906
- moment2.min(
907
- calendars.map((calendar) => fromGTFSDate(calendar.start_date))
908
- )
909
- );
910
- timetable.end_date = toGTFSDate(
911
- moment2.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
912
- );
913
- }
914
- timetable.timetable_id = formatTimetableId(timetable);
915
- return convertTimetableToTimetablePage(timetable, config);
916
928
  };
917
929
  var convertRoutesToTimetablePages = (config) => {
918
- const db = openDb(config);
919
930
  const routes = getRoutes();
920
- let whereClause = "";
921
- const whereClauses = [];
922
- if (config.endDate) {
923
- whereClauses.push(
924
- `start_date <= ${sqlString.escape(toGTFSDate(moment2(config.endDate)))}`
925
- );
926
- }
927
- if (config.startDate) {
928
- whereClauses.push(
929
- `end_date >= ${sqlString.escape(toGTFSDate(moment2(config.startDate)))}`
930
- );
931
- }
932
- if (whereClauses.length > 0) {
933
- whereClause = `WHERE ${whereClauses.join(" AND ")}`;
934
- }
935
- const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
936
- const serviceIds = calendars.map((calendar) => calendar.service_id);
937
- const calendarDates = db.prepare(
938
- `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
939
- ).all();
940
- const timetablePages = routes.map((route) => {
931
+ const timetablePages = [];
932
+ const { calendars, calendarDates } = getCalendarsFromConfig(config);
933
+ for (const route of routes) {
941
934
  const trips = getTrips(
942
935
  {
943
936
  route_id: route.route_id
944
937
  },
945
938
  ["trip_headsign", "direction_id", "trip_id", "service_id"]
946
939
  );
947
- const directions = uniqBy(trips, (trip) => trip.direction_id);
948
- const dayGroups = groupBy(calendars, calendarToCalendarCode);
940
+ const uniqueTripDirections = orderBy(
941
+ uniqBy(trips, (trip) => trip.direction_id),
942
+ "direction_id"
943
+ );
944
+ const sortedCalendars = orderBy(calendars, calendarToCalendarCode, "desc");
945
+ const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
949
946
  const calendarDateGroups = groupBy(calendarDates, "service_id");
950
- return directions.map((direction) => [
951
- Object.values(dayGroups).map((calendars2) => {
947
+ const timetables = [];
948
+ for (const uniqueTripDirection of uniqueTripDirections) {
949
+ for (const calendars2 of Object.values(calendarGroups)) {
952
950
  const tripsForCalendars = trips.filter(
953
951
  (trip) => some(calendars2, { service_id: trip.service_id })
954
952
  );
955
953
  if (tripsForCalendars.length > 0) {
956
- return convertRouteToTimetablePage(
957
- route,
958
- direction,
959
- calendars2,
960
- null,
961
- config
954
+ timetables.push(
955
+ createTimetable({
956
+ route,
957
+ directionId: uniqueTripDirection.direction_id,
958
+ tripHeadsign: uniqueTripDirection.trip_headsign,
959
+ calendars: calendars2
960
+ })
962
961
  );
963
962
  }
964
- }),
965
- Object.values(calendarDateGroups).map((calendarDates2) => {
963
+ }
964
+ for (const calendarDates2 of Object.values(calendarDateGroups)) {
966
965
  const tripsForCalendarDates = trips.filter(
967
966
  (trip) => some(calendarDates2, { service_id: trip.service_id })
968
967
  );
969
968
  if (tripsForCalendarDates.length > 0) {
970
- return convertRouteToTimetablePage(
971
- route,
972
- direction,
973
- null,
974
- calendarDates2,
975
- config
969
+ timetables.push(
970
+ createTimetable({
971
+ route,
972
+ directionId: uniqueTripDirection.direction_id,
973
+ tripHeadsign: uniqueTripDirection.trip_headsign,
974
+ calendarDates: calendarDates2
975
+ })
976
976
  );
977
977
  }
978
- })
979
- ]);
980
- });
981
- return compact(flattenDeep(timetablePages));
978
+ }
979
+ }
980
+ if (config.groupTimetablesIntoPages === true) {
981
+ timetablePages.push(
982
+ createTimetablePage({
983
+ timetablePageId: `route_${route.route_short_name ?? route.route_long_name}`,
984
+ timetables,
985
+ config
986
+ })
987
+ );
988
+ } else {
989
+ for (const timetable of timetables) {
990
+ timetablePages.push(
991
+ createTimetablePage({
992
+ timetablePageId: timetable.timetable_id,
993
+ timetables: [timetable],
994
+ config
995
+ })
996
+ );
997
+ }
998
+ }
999
+ }
1000
+ return timetablePages;
982
1001
  };
983
1002
  var generateTripsByFrequencies = (trip, frequencies, config) => {
984
1003
  const formattedFrequencies = frequencies.map(
@@ -1001,7 +1020,7 @@ var generateTripsByFrequencies = (trip, frequencies, config) => {
1001
1020
  return trips;
1002
1021
  };
1003
1022
  var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config) => {
1004
- if (config.showArrivalOnDifference === null) {
1023
+ if (config.showArrivalOnDifference === null || config.showArrivalOnDifference === void 0) {
1005
1024
  return stopIds;
1006
1025
  }
1007
1026
  for (const trip of timetable.orderedTrips) {
@@ -1123,7 +1142,7 @@ var getStopsForTimetable = (timetable, config) => {
1123
1142
  }
1124
1143
  return stop;
1125
1144
  });
1126
- if (timetable.showStopCity) {
1145
+ if (config.showStopCity) {
1127
1146
  const stopAttributes = getStopAttributes({
1128
1147
  stop_id: orderedStopIds
1129
1148
  });
@@ -1138,6 +1157,39 @@ var getStopsForTimetable = (timetable, config) => {
1138
1157
  }
1139
1158
  return orderedStops;
1140
1159
  };
1160
+ var getCalendarsFromConfig = (config) => {
1161
+ const db = openDb();
1162
+ let whereClause = "";
1163
+ const whereClauses = [];
1164
+ if (config.endDate) {
1165
+ if (!moment2(config.endDate).isValid()) {
1166
+ throw new Error(`Invalid endDate=${config.endDate} in config.json`);
1167
+ }
1168
+ whereClauses.push(
1169
+ `start_date <= ${sqlString.escape(moment2(config.endDate).format("YYYYMMDD"))}`
1170
+ );
1171
+ }
1172
+ if (config.startDate) {
1173
+ if (!moment2(config.startDate).isValid()) {
1174
+ throw new Error(`Invalid startDate=${config.startDate} in config.json`);
1175
+ }
1176
+ whereClauses.push(
1177
+ `end_date >= ${sqlString.escape(moment2(config.startDate).format("YYYYMMDD"))}`
1178
+ );
1179
+ }
1180
+ if (whereClauses.length > 0) {
1181
+ whereClause = `WHERE ${whereClauses.join(" AND ")}`;
1182
+ }
1183
+ const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
1184
+ const serviceIds = calendars.map((calendar) => calendar.service_id);
1185
+ const calendarDates = db.prepare(
1186
+ `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
1187
+ ).all();
1188
+ return {
1189
+ calendars,
1190
+ calendarDates
1191
+ };
1192
+ };
1141
1193
  var getCalendarsFromTimetable = (timetable) => {
1142
1194
  const db = openDb();
1143
1195
  let whereClause = "";
@@ -1287,13 +1339,17 @@ var filterTrips = (timetable) => {
1287
1339
  }
1288
1340
  trip.stoptimes = combinedStoptimes;
1289
1341
  }
1290
- const timetableStopIds = new Set(timetable.stops.map((stop) => stop.stop_id));
1342
+ const timetableStopIds = new Set(
1343
+ timetable.stops.map((stop) => stop.stop_id)
1344
+ );
1291
1345
  for (const trip of filteredTrips) {
1292
1346
  trip.stoptimes = trip.stoptimes.filter(
1293
1347
  (stoptime) => timetableStopIds.has(stoptime.stop_id)
1294
1348
  );
1295
1349
  }
1296
- filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
1350
+ filteredTrips = filteredTrips.filter(
1351
+ (trip) => trip.stoptimes.length > 1
1352
+ );
1297
1353
  return filteredTrips;
1298
1354
  };
1299
1355
  var getTripsForTimetable = (timetable, calendars, config) => {
@@ -1444,6 +1500,15 @@ var formatTimetables = (timetables, config) => {
1444
1500
  };
1445
1501
  function getTimetablePagesForAgency(config) {
1446
1502
  const timetables = mergeTimetablesWithSameId(getTimetables());
1503
+ const routes = getRoutes();
1504
+ const formattedTimetables = timetables.map((timetable) => {
1505
+ return {
1506
+ ...timetable,
1507
+ routes: routes.filter(
1508
+ (route) => timetable.route_ids.includes(route.route_id)
1509
+ )
1510
+ };
1511
+ });
1447
1512
  if (timetables.length === 0) {
1448
1513
  return convertRoutesToTimetablePages(config);
1449
1514
  }
@@ -1453,68 +1518,33 @@ function getTimetablePagesForAgency(config) {
1453
1518
  [["timetable_page_id", "ASC"]]
1454
1519
  );
1455
1520
  if (timetablePages.length === 0) {
1456
- return timetables.map(
1457
- (timetable) => convertTimetableToTimetablePage(timetable, config)
1521
+ return formattedTimetables.map(
1522
+ (timetable) => createTimetablePage({
1523
+ timetablePageId: timetable.timetable_id,
1524
+ timetables: [timetable],
1525
+ config
1526
+ })
1458
1527
  );
1459
1528
  }
1460
- const routes = getRoutes();
1461
1529
  return timetablePages.map((timetablePage) => {
1462
- timetablePage.timetables = sortBy(
1463
- timetables.filter(
1464
- (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1465
- ),
1466
- "timetable_sequence"
1467
- );
1468
- for (const timetable of timetablePage.timetables) {
1469
- timetable.routes = routes.filter(
1470
- (route) => timetable.route_ids.includes(route.route_id)
1471
- );
1472
- }
1473
- return timetablePage;
1530
+ return {
1531
+ ...timetablePage,
1532
+ timetables: sortBy(
1533
+ formattedTimetables.filter(
1534
+ (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1535
+ ),
1536
+ "timetable_sequence"
1537
+ )
1538
+ };
1474
1539
  });
1475
1540
  }
1476
- var getTimetablePageById = (timetablePageId, config) => {
1477
- const timetablePages = getTimetablePages({
1478
- timetable_page_id: timetablePageId
1479
- });
1480
- const timetables = mergeTimetablesWithSameId(getTimetables());
1481
- if (timetablePages.length > 1) {
1482
- throw new Error(
1483
- `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1484
- );
1485
- }
1486
- if (timetablePages.length === 1) {
1487
- const timetablePage = timetablePages[0];
1488
- timetablePage.timetables = sortBy(
1489
- timetables.filter(
1490
- (timetable) => timetable.timetable_page_id === timetablePageId
1491
- ),
1492
- "timetable_sequence"
1493
- );
1494
- for (const timetable of timetablePage.timetables) {
1495
- timetable.routes = getRoutes({
1496
- route_id: timetable.route_ids
1497
- });
1498
- }
1499
- return timetablePage;
1500
- }
1501
- if (timetables.length > 0) {
1502
- const timetablePageTimetables = timetables.filter(
1503
- (timetable) => timetable.timetable_id === timetablePageId
1504
- );
1505
- if (timetablePageTimetables.length === 0) {
1506
- throw new Error(
1507
- `No timetable found for timetable_page_id=${timetablePageId}`
1508
- );
1509
- }
1510
- return convertTimetableToTimetablePage(timetablePageTimetables[0], config);
1511
- }
1541
+ var getDataForTimetablePageById = (timetablePageId) => {
1512
1542
  let calendarCode;
1513
1543
  let calendars;
1514
1544
  let calendarDates;
1515
1545
  let serviceId;
1516
1546
  let directionId = "";
1517
- const parts = timetablePageId.split("|");
1547
+ const parts = timetablePageId?.split("|") ?? [];
1518
1548
  if (parts.length > 2) {
1519
1549
  directionId = Number.parseInt(parts.pop(), 10);
1520
1550
  calendarCode = parts.pop();
@@ -1533,13 +1563,13 @@ var getTimetablePageById = (timetablePageId, config) => {
1533
1563
  },
1534
1564
  ["trip_headsign", "direction_id"]
1535
1565
  );
1536
- const directions = uniqBy(trips, (trip) => trip.direction_id);
1537
- if (directions.length === 0) {
1566
+ const uniqueTripDirections = uniqBy(trips, (trip) => trip.direction_id);
1567
+ if (uniqueTripDirections.length === 0) {
1538
1568
  throw new Error(
1539
1569
  `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1540
1570
  );
1541
1571
  }
1542
- if (/^[01]*$/.test(calendarCode)) {
1572
+ if (/^[01]*$/.test(calendarCode ?? "")) {
1543
1573
  calendars = getCalendars({
1544
1574
  ...calendarCodeToCalendar(calendarCode)
1545
1575
  });
@@ -1550,13 +1580,129 @@ var getTimetablePageById = (timetablePageId, config) => {
1550
1580
  service_id: serviceId
1551
1581
  });
1552
1582
  }
1553
- return convertRouteToTimetablePage(
1554
- routes[0],
1555
- directions[0],
1583
+ return {
1556
1584
  calendars,
1557
1585
  calendarDates,
1586
+ route: routes[0],
1587
+ directionId: uniqueTripDirections[0].direction_id,
1588
+ tripHeadsign: uniqueTripDirections[0].trip_headsign
1589
+ };
1590
+ };
1591
+ var getTimetablePageById = (timetablePageId, config) => {
1592
+ const timetablePages = getTimetablePages({
1593
+ timetable_page_id: timetablePageId
1594
+ });
1595
+ const timetables = mergeTimetablesWithSameId(getTimetables());
1596
+ if (timetablePages.length > 1) {
1597
+ throw new Error(
1598
+ `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1599
+ );
1600
+ }
1601
+ if (timetablePages.length === 1) {
1602
+ const timetablePage = timetablePages[0];
1603
+ timetablePage.timetables = sortBy(
1604
+ timetables.filter(
1605
+ (timetable2) => timetable2.timetable_page_id === timetablePageId
1606
+ ),
1607
+ "timetable_sequence"
1608
+ );
1609
+ for (const timetable2 of timetablePage.timetables) {
1610
+ timetable2.routes = getRoutes({
1611
+ route_id: timetable2.route_ids
1612
+ });
1613
+ }
1614
+ return timetablePage;
1615
+ }
1616
+ if (timetables.length > 0) {
1617
+ const timetablePageTimetables = timetables.filter(
1618
+ (timetable2) => timetable2.timetable_id === timetablePageId
1619
+ );
1620
+ if (timetablePageTimetables.length === 0) {
1621
+ throw new Error(
1622
+ `No timetable found for timetable_page_id=${timetablePageId}`
1623
+ );
1624
+ }
1625
+ return createTimetablePage({
1626
+ timetablePageId,
1627
+ timetables: [timetablePageTimetables[0]],
1628
+ config
1629
+ });
1630
+ }
1631
+ if (timetablePageId.startsWith("route_")) {
1632
+ const routes = getRoutes({
1633
+ route_short_name: timetablePageId.split("_")[1]
1634
+ });
1635
+ if (routes.length === 0) {
1636
+ throw new Error(
1637
+ `No route found for timetable_page_id=${timetablePageId}`
1638
+ );
1639
+ }
1640
+ const { calendars: calendars2, calendarDates: calendarDates2 } = getCalendarsFromConfig(config);
1641
+ const trips = getTrips(
1642
+ {
1643
+ route_id: routes[0].route_id
1644
+ },
1645
+ ["trip_headsign", "direction_id", "trip_id", "service_id"]
1646
+ );
1647
+ const uniqueTripDirections = orderBy(
1648
+ uniqBy(trips, (trip) => trip.direction_id),
1649
+ "direction_id"
1650
+ );
1651
+ const sortedCalendars = orderBy(calendars2, calendarToCalendarCode, "desc");
1652
+ const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
1653
+ const calendarDateGroups = groupBy(calendarDates2, "service_id");
1654
+ const timetables2 = [];
1655
+ for (const uniqueTripDirection of uniqueTripDirections) {
1656
+ for (const calendars3 of Object.values(calendarGroups)) {
1657
+ const tripsForCalendars = trips.filter(
1658
+ (trip) => some(calendars3, { service_id: trip.service_id })
1659
+ );
1660
+ if (tripsForCalendars.length > 0) {
1661
+ timetables2.push(
1662
+ createTimetable({
1663
+ route: routes[0],
1664
+ directionId: uniqueTripDirection.direction_id,
1665
+ tripHeadsign: uniqueTripDirection.trip_headsign,
1666
+ calendars: calendars3
1667
+ })
1668
+ );
1669
+ }
1670
+ }
1671
+ for (const calendarDates3 of Object.values(calendarDateGroups)) {
1672
+ const tripsForCalendarDates = trips.filter(
1673
+ (trip) => some(calendarDates3, { service_id: trip.service_id })
1674
+ );
1675
+ if (tripsForCalendarDates.length > 0) {
1676
+ timetables2.push(
1677
+ createTimetable({
1678
+ route: routes[0],
1679
+ directionId: uniqueTripDirection.direction_id,
1680
+ tripHeadsign: uniqueTripDirection.trip_headsign,
1681
+ calendarDates: calendarDates3
1682
+ })
1683
+ );
1684
+ }
1685
+ }
1686
+ }
1687
+ return createTimetablePage({
1688
+ timetablePageId,
1689
+ timetables: timetables2,
1690
+ config
1691
+ });
1692
+ }
1693
+ const { calendars, calendarDates, route, directionId, tripHeadsign } = getDataForTimetablePageById(timetablePageId);
1694
+ const timetable = createTimetable({
1695
+ route,
1696
+ directionId,
1697
+ tripHeadsign,
1698
+ calendars,
1699
+ calendarDates
1700
+ });
1701
+ return createTimetablePage({
1702
+ timetablePageId,
1703
+ timetables: [timetable],
1558
1704
  config
1559
- );
1705
+ });
1560
1706
  };
1561
1707
  function setDefaultConfig(initialConfig) {
1562
1708
  const defaults = {
@@ -1577,6 +1723,7 @@ function setDefaultConfig(initialConfig) {
1577
1723
  defaultOrientation: "vertical",
1578
1724
  interpolatedStopSymbol: "\u2022",
1579
1725
  interpolatedStopText: "Estimated time of arrival",
1726
+ groupTimetablesIntoPages: true,
1580
1727
  gtfsToHtmlVersion: version,
1581
1728
  linkStopUrls: false,
1582
1729
  mapStyleUrl: "https://tiles.openfreemap.org/styles/positron",
@@ -1633,12 +1780,6 @@ function getFormattedTimetablePage(timetablePageId, config) {
1633
1780
  timetablePageId,
1634
1781
  config
1635
1782
  );
1636
- const timetableRoutes = getRoutes(
1637
- {
1638
- route_id: timetablePage.route_ids
1639
- },
1640
- ["agency_id"]
1641
- );
1642
1783
  const consolidatedTimetables = formatTimetables(
1643
1784
  timetablePage.timetables,
1644
1785
  config
@@ -1654,7 +1795,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1654
1795
  }
1655
1796
  }
1656
1797
  const uniqueRoutes = uniqBy(
1657
- flatMap2(consolidatedTimetables, (timetable) => timetable.routes),
1798
+ flatMap(consolidatedTimetables, (timetable) => timetable.routes),
1658
1799
  "route_id"
1659
1800
  );
1660
1801
  const formattedTimetablePage = {
@@ -1665,7 +1806,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1665
1806
  consolidatedTimetables.map((timetable) => timetable.dayList)
1666
1807
  ),
1667
1808
  route_ids: uniqueRoutes.map((route) => route.route_id),
1668
- agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
1809
+ agency_ids: uniq(compact(uniqueRoutes.map((route) => route.agency_id))),
1669
1810
  filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`,
1670
1811
  timetable_page_label: timetablePage.timetable_page_label ?? formatListForDisplay(uniqueRoutes.map((route) => formatRouteName(route)))
1671
1812
  };
@@ -1923,12 +2064,14 @@ function formatFrequency(frequency, config) {
1923
2064
  frequency.headway_min = Math.round(headway.asMinutes());
1924
2065
  return frequency;
1925
2066
  }
1926
- function formatTimetableId(timetable) {
1927
- let timetableId = `${timetable.route_ids.join("_")}|${calendarToCalendarCode(
1928
- timetable
1929
- )}`;
1930
- if (!isNullOrEmpty(timetable.direction_id)) {
1931
- timetableId += `|${timetable.direction_id}`;
2067
+ function formatTimetableId({
2068
+ routeIds,
2069
+ directionId,
2070
+ days: days2
2071
+ }) {
2072
+ let timetableId = `${routeIds.join("_")}|${calendarToCalendarCode(days2)}`;
2073
+ if (!isNullOrEmpty(directionId)) {
2074
+ timetableId += `|${directionId}`;
1932
2075
  }
1933
2076
  return timetableId;
1934
2077
  }
@@ -2081,10 +2224,18 @@ function formatTimetableLabel(timetable) {
2081
2224
  return timetableLabel;
2082
2225
  }
2083
2226
  var formatRouteName = (route) => {
2084
- if (route.route_long_name === null || route.route_long_name === "") {
2085
- return `Route ${route.route_short_name}`;
2227
+ if (route.route_long_name) {
2228
+ return `Route ${route.route_long_name}`;
2086
2229
  }
2087
- return route.route_long_name ?? "Unknown";
2230
+ return route.route_short_name ?? "Unknown";
2231
+ };
2232
+ var formatRouteNameForFilename = (route) => {
2233
+ if (route.route_short_name) {
2234
+ return route.route_short_name.replace(/\s/g, "-");
2235
+ } else if (route.route_long_name) {
2236
+ return route.route_long_name.replace(/\s/g, "-");
2237
+ }
2238
+ return "Unknown";
2088
2239
  };
2089
2240
  var formatListForDisplay = (list) => {
2090
2241
  return new Intl.ListFormat("en-US", {
@@ -2107,6 +2258,7 @@ function mergeTimetablesWithSameId(timetables) {
2107
2258
  }
2108
2259
 
2109
2260
  // src/lib/file-utils.ts
2261
+ var homeDirectory = homedir();
2110
2262
  async function getConfig(argv2) {
2111
2263
  let data;
2112
2264
  let config;
@@ -2216,15 +2368,34 @@ function zipFolder(outputPath) {
2216
2368
  archive.finalize();
2217
2369
  });
2218
2370
  }
2219
- function generateFileName(timetable, config, extension = "html") {
2220
- let filename = timetable.timetable_id;
2371
+ function generateTimetablePageFileName(timetablePage, config) {
2372
+ if (timetablePage.filename) {
2373
+ return sanitize(timetablePage.filename);
2374
+ }
2375
+ if (config.groupTimetablesIntoPages === true && uniqBy2(timetablePage.timetables, "route_id").length === 1) {
2376
+ const route = timetablePage.timetables[0].routes[0];
2377
+ return sanitize(`${formatRouteNameForFilename(route).toLowerCase()}.html`);
2378
+ }
2379
+ const timetable = timetablePage.timetables[0];
2380
+ let filename = timetable.timetable_id ?? "";
2381
+ for (const route of timetable.routes) {
2382
+ filename += `_${formatRouteNameForFilename(route)}`;
2383
+ }
2384
+ if (!isNullOrEmpty(timetable.direction_id)) {
2385
+ filename += `_${timetable.direction_id}`;
2386
+ }
2387
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.html`;
2388
+ return sanitize(filename.toLowerCase());
2389
+ }
2390
+ function generateCSVFileName(timetable, config) {
2391
+ let filename = timetable.timetable_id ?? "";
2221
2392
  for (const route of timetable.routes) {
2222
- filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, "-")}` : `_${route.route_short_name.replace(/\s/g, "-")}`;
2393
+ filename += `_${formatRouteNameForFilename(route)}`;
2223
2394
  }
2224
2395
  if (!isNullOrEmpty(timetable.direction_id)) {
2225
2396
  filename += `_${timetable.direction_id}`;
2226
2397
  }
2227
- filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.${extension}`;
2398
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.csv`;
2228
2399
  return sanitize(filename).toLowerCase();
2229
2400
  }
2230
2401
  function generateFolderName(timetablePage) {
@@ -2264,22 +2435,22 @@ async function renderPdf(htmlPath) {
2264
2435
  });
2265
2436
  await browser.close();
2266
2437
  }
2438
+ function untildify(pathWithTilde) {
2439
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
2440
+ }
2267
2441
 
2268
2442
  // src/lib/gtfs-to-html.ts
2269
2443
  import path from "path";
2270
2444
  import { mkdir as mkdir2, writeFile } from "fs/promises";
2271
2445
  import { openDb as openDb2, importGtfs } from "gtfs";
2272
2446
  import sanitize2 from "sanitize-filename";
2273
- import Timer from "timer-machine";
2274
- import untildify2 from "untildify";
2275
2447
  var gtfsToHtml = async (initialConfig) => {
2276
2448
  const config = setDefaultConfig(initialConfig);
2277
- const timer = new Timer();
2449
+ const startTime = process.hrtime.bigint();
2278
2450
  const agencyKey = config.agencies.map(
2279
2451
  (agency) => agency.agencyKey ?? agency.agency_key ?? "unknown"
2280
2452
  ).join("-");
2281
- const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2282
- timer.start();
2453
+ const outputPath = config.outputPath ? untildify(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2283
2454
  await prepDirectory(outputPath, config);
2284
2455
  try {
2285
2456
  openDb2(config);
@@ -2325,9 +2496,11 @@ var gtfsToHtml = async (initialConfig) => {
2325
2496
  config
2326
2497
  );
2327
2498
  for (const timetable of timetablePage.timetables) {
2328
- for (const warning of timetable.warnings) {
2329
- stats.warnings.push(warning);
2330
- bar?.interrupt(warning);
2499
+ if (timetable.warnings) {
2500
+ for (const warning of timetable.warnings) {
2501
+ stats.warnings.push(warning);
2502
+ bar?.interrupt(warning);
2503
+ }
2331
2504
  }
2332
2505
  }
2333
2506
  if (timetablePage.consolidatedTimetables.length === 0) {
@@ -2350,7 +2523,7 @@ var gtfsToHtml = async (initialConfig) => {
2350
2523
  const csvPath = path.join(
2351
2524
  outputPath,
2352
2525
  datePath,
2353
- generateFileName(timetable, config, "csv")
2526
+ generateCSVFileName(timetable, config)
2354
2527
  );
2355
2528
  await writeFile(csvPath, csv);
2356
2529
  }
@@ -2396,11 +2569,11 @@ var gtfsToHtml = async (initialConfig) => {
2396
2569
  `${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullOutputPath}`
2397
2570
  );
2398
2571
  logStats(config)(stats);
2399
- const seconds = Math.round(timer.time() / 1e3);
2572
+ const endTime = process.hrtime.bigint();
2573
+ const elapsedSeconds = Number(endTime - startTime) / 1e9;
2400
2574
  log(config)(
2401
- `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`
2575
+ `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${elapsedSeconds.toFixed(1)} seconds`
2402
2576
  );
2403
- timer.stop();
2404
2577
  return fullOutputPath;
2405
2578
  };
2406
2579
  var gtfs_to_html_default = gtfsToHtml;