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.
package/dist/index.js CHANGED
@@ -9,8 +9,6 @@ import path from "path";
9
9
  import { mkdir as mkdir2, writeFile } from "fs/promises";
10
10
  import { openDb as openDb2, importGtfs } from "gtfs";
11
11
  import sanitize2 from "sanitize-filename";
12
- import Timer from "timer-machine";
13
- import untildify2 from "untildify";
14
12
 
15
13
  // src/lib/file-utils.ts
16
14
  import { dirname, join, resolve } from "path";
@@ -25,14 +23,15 @@ import {
25
23
  readFile,
26
24
  rm
27
25
  } from "fs/promises";
26
+ import { homedir } from "os";
28
27
  import * as _ from "lodash-es";
28
+ import { uniqBy as uniqBy2 } from "lodash-es";
29
29
  import archiver from "archiver";
30
30
  import beautify from "js-beautify";
31
31
  import sanitizeHtml from "sanitize-html";
32
32
  import { renderFile } from "pug";
33
33
  import puppeteer from "puppeteer";
34
34
  import sanitize from "sanitize-filename";
35
- import untildify from "untildify";
36
35
  import { marked } from "marked";
37
36
 
38
37
  // src/lib/formatters.ts
@@ -61,16 +60,7 @@ function fromGTFSTime(timeString) {
61
60
  function toGTFSTime(time) {
62
61
  return time.format("HH:mm:ss");
63
62
  }
64
- function fromGTFSDate(gtfsDate) {
65
- return moment(gtfsDate, "YYYYMMDD");
66
- }
67
- function toGTFSDate(date) {
68
- return moment(date).format("YYYYMMDD");
69
- }
70
63
  function calendarToCalendarCode(c) {
71
- if (c.service_id) {
72
- return c.service_id;
73
- }
74
64
  return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
75
65
  }
76
66
  function calendarCodeToCalendar(code) {
@@ -111,13 +101,13 @@ import {
111
101
  find,
112
102
  findLast,
113
103
  first,
114
- flatMap as flatMap2,
115
- flattenDeep,
104
+ flatMap,
116
105
  flow,
117
106
  groupBy,
118
107
  head,
119
108
  last,
120
109
  maxBy,
110
+ orderBy,
121
111
  partialRight,
122
112
  reduce,
123
113
  size,
@@ -151,7 +141,6 @@ import toposort from "toposort";
151
141
 
152
142
  // src/lib/geojson-utils.ts
153
143
  import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
154
- import { flatMap } from "lodash-es";
155
144
  import simplify from "@turf/simplify";
156
145
  import { featureCollection, round } from "@turf/helpers";
157
146
 
@@ -313,7 +302,7 @@ function progressBar(formatString, barTotal, config) {
313
302
  }
314
303
 
315
304
  // src/lib/geojson-utils.ts
316
- var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
305
+ var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
317
306
  var truncateGeoJSONDecimals = (geojson, config) => {
318
307
  for (const feature of geojson.features) {
319
308
  if (feature.geometry.coordinates) {
@@ -504,7 +493,7 @@ function formatTripNameForCSV(trip, timetable) {
504
493
  // package.json
505
494
  var package_default = {
506
495
  name: "gtfs-to-html",
507
- version: "2.10.17",
496
+ version: "2.11.0",
508
497
  private: false,
509
498
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
510
499
  keywords: [
@@ -557,24 +546,22 @@ var package_default = {
557
546
  "cli-table": "^0.3.11",
558
547
  "csv-stringify": "^6.6.0",
559
548
  express: "^5.1.0",
560
- gtfs: "^4.17.7",
549
+ gtfs: "^4.18.0",
561
550
  "gtfs-realtime-pbf-js-module": "^1.0.0",
562
551
  "js-beautify": "^1.15.4",
563
552
  "lodash-es": "^4.17.21",
564
- marked: "^16.1.2",
553
+ marked: "^16.3.0",
565
554
  moment: "^2.30.1",
566
555
  pbf: "^4.0.1",
567
556
  "pretty-error": "^4.0.0",
568
557
  pug: "^3.0.3",
569
- puppeteer: "^24.16.2",
558
+ puppeteer: "^24.21.0",
570
559
  "sanitize-filename": "^1.6.3",
571
560
  "sanitize-html": "^2.17.0",
572
561
  sqlstring: "^2.3.3",
573
- "timer-machine": "^1.1.0",
574
562
  toposort: "^2.0.2",
575
- untildify: "^5.0.0",
576
563
  yargs: "^18.0.0",
577
- yoctocolors: "^2.1.1"
564
+ yoctocolors: "^2.1.2"
578
565
  },
579
566
  devDependencies: {
580
567
  "@types/archiver": "^6.0.3",
@@ -586,10 +573,11 @@ var package_default = {
586
573
  "@types/node": "^22",
587
574
  "@types/pug": "^2.0.10",
588
575
  "@types/sanitize-html": "^2.16.0",
589
- "@types/timer-machine": "^1.1.3",
576
+ "@types/sqlstring": "^2.3.2",
577
+ "@types/toposort": "^2.0.7",
590
578
  "@types/yargs": "^17.0.33",
591
579
  husky: "^9.1.7",
592
- "lint-staged": "^16.1.5",
580
+ "lint-staged": "^16.1.6",
593
581
  prettier: "^3.6.2",
594
582
  tsup: "^8.5.0",
595
583
  typescript: "^5.9.2"
@@ -645,7 +633,7 @@ var findCommonStopId = (trips, config) => {
645
633
  return null;
646
634
  }
647
635
  const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
648
- if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
636
+ if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes)?.stop_id) {
649
637
  return false;
650
638
  }
651
639
  if (isNullOrEmpty(stoptime.arrival_time)) {
@@ -698,11 +686,7 @@ var sortTrips = (trips, config) => {
698
686
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
699
687
  );
700
688
  }
701
- sortedTrips = sortBy(
702
- trips,
703
- ["firstStoptime", "lastStoptime"],
704
- ["asc", "asc"]
705
- );
689
+ sortedTrips = sortBy(trips, ["firstStoptime", "lastStoptime"]);
706
690
  } else if (config.sortingAlgorithm === "end") {
707
691
  for (const trip of trips) {
708
692
  if (trip.stoptimes.length === 0) {
@@ -713,11 +697,7 @@ var sortTrips = (trips, config) => {
713
697
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
714
698
  );
715
699
  }
716
- sortedTrips = sortBy(
717
- trips,
718
- ["lastStoptime", "firstStoptime"],
719
- ["asc", "asc"]
720
- );
700
+ sortedTrips = sortBy(trips, ["lastStoptime", "firstStoptime"]);
721
701
  } else if (config.sortingAlgorithm === "first") {
722
702
  const longestTripStoptimes = getLongestTripStoptimes(trips, config);
723
703
  const firstStopId = first(longestTripStoptimes).stop_id;
@@ -744,8 +724,8 @@ var getCalendarDatesForTimetable = (timetable, config) => {
744
724
  [],
745
725
  [["date", "ASC"]]
746
726
  );
747
- const start = fromGTFSDate(timetable.start_date);
748
- const end = fromGTFSDate(timetable.end_date);
727
+ const start = moment2(timetable.start_date, "YYYYMMDD");
728
+ const end = moment2(timetable.end_date, "YYYYMMDD");
749
729
  const excludedDates = /* @__PURE__ */ new Set();
750
730
  const includedDates = /* @__PURE__ */ new Set();
751
731
  for (const calendarDate of calendarDates) {
@@ -870,28 +850,70 @@ var getTimetableNotesForTimetable = (timetable, config) => {
870
850
  }));
871
851
  return sortBy(formattedNotes, "symbol");
872
852
  };
873
- var convertTimetableToTimetablePage = (timetable, config) => {
874
- if (!timetable.routes) {
875
- timetable.routes = getRoutes({
876
- route_id: timetable.route_ids
877
- });
878
- }
879
- 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);
880
872
  return {
881
- timetable_page_id: timetable.timetable_id,
882
- timetable_page_label: timetable.timetable_label,
883
- timetables: [timetable],
873
+ ...timetablePage,
884
874
  filename
885
875
  };
886
876
  };
887
- var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config) => {
888
- 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,
889
911
  route_ids: [route.route_id],
890
- direction_id: direction ? direction.direction_id : void 0,
891
- direction_name: direction ? direction.trip_headsign : void 0,
912
+ direction_id: directionId === null ? null : directionId,
913
+ direction_name: tripHeadsign === null ? null : tripHeadsign,
892
914
  routes: [route],
893
915
  include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
894
- service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
916
+ service_ids: serviceIds,
895
917
  service_notes: null,
896
918
  timetable_label: null,
897
919
  start_time: null,
@@ -899,88 +921,83 @@ var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, c
899
921
  orientation: null,
900
922
  timetable_sequence: null,
901
923
  show_trip_continuation: null,
902
- start_date: null,
903
- end_date: null
924
+ start_date: startDate,
925
+ end_date: endDate,
926
+ ...days2
904
927
  };
905
- if (calendars && calendars.length > 0) {
906
- Object.assign(timetable, getDaysFromCalendars(calendars));
907
- timetable.start_date = toGTFSDate(
908
- moment2.min(
909
- calendars.map((calendar) => fromGTFSDate(calendar.start_date))
910
- )
911
- );
912
- timetable.end_date = toGTFSDate(
913
- moment2.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
914
- );
915
- }
916
- timetable.timetable_id = formatTimetableId(timetable);
917
- return convertTimetableToTimetablePage(timetable, config);
918
928
  };
919
929
  var convertRoutesToTimetablePages = (config) => {
920
- const db = openDb(config);
921
930
  const routes = getRoutes();
922
- let whereClause = "";
923
- const whereClauses = [];
924
- if (config.endDate) {
925
- whereClauses.push(
926
- `start_date <= ${sqlString.escape(toGTFSDate(moment2(config.endDate)))}`
927
- );
928
- }
929
- if (config.startDate) {
930
- whereClauses.push(
931
- `end_date >= ${sqlString.escape(toGTFSDate(moment2(config.startDate)))}`
932
- );
933
- }
934
- if (whereClauses.length > 0) {
935
- whereClause = `WHERE ${whereClauses.join(" AND ")}`;
936
- }
937
- const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
938
- const serviceIds = calendars.map((calendar) => calendar.service_id);
939
- const calendarDates = db.prepare(
940
- `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
941
- ).all();
942
- const timetablePages = routes.map((route) => {
931
+ const timetablePages = [];
932
+ const { calendars, calendarDates } = getCalendarsFromConfig(config);
933
+ for (const route of routes) {
943
934
  const trips = getTrips(
944
935
  {
945
936
  route_id: route.route_id
946
937
  },
947
938
  ["trip_headsign", "direction_id", "trip_id", "service_id"]
948
939
  );
949
- const directions = uniqBy(trips, (trip) => trip.direction_id);
950
- 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);
951
946
  const calendarDateGroups = groupBy(calendarDates, "service_id");
952
- return directions.map((direction) => [
953
- Object.values(dayGroups).map((calendars2) => {
947
+ const timetables = [];
948
+ for (const uniqueTripDirection of uniqueTripDirections) {
949
+ for (const calendars2 of Object.values(calendarGroups)) {
954
950
  const tripsForCalendars = trips.filter(
955
951
  (trip) => some(calendars2, { service_id: trip.service_id })
956
952
  );
957
953
  if (tripsForCalendars.length > 0) {
958
- return convertRouteToTimetablePage(
959
- route,
960
- direction,
961
- calendars2,
962
- null,
963
- config
954
+ timetables.push(
955
+ createTimetable({
956
+ route,
957
+ directionId: uniqueTripDirection.direction_id,
958
+ tripHeadsign: uniqueTripDirection.trip_headsign,
959
+ calendars: calendars2
960
+ })
964
961
  );
965
962
  }
966
- }),
967
- Object.values(calendarDateGroups).map((calendarDates2) => {
963
+ }
964
+ for (const calendarDates2 of Object.values(calendarDateGroups)) {
968
965
  const tripsForCalendarDates = trips.filter(
969
966
  (trip) => some(calendarDates2, { service_id: trip.service_id })
970
967
  );
971
968
  if (tripsForCalendarDates.length > 0) {
972
- return convertRouteToTimetablePage(
973
- route,
974
- direction,
975
- null,
976
- calendarDates2,
977
- config
969
+ timetables.push(
970
+ createTimetable({
971
+ route,
972
+ directionId: uniqueTripDirection.direction_id,
973
+ tripHeadsign: uniqueTripDirection.trip_headsign,
974
+ calendarDates: calendarDates2
975
+ })
978
976
  );
979
977
  }
980
- })
981
- ]);
982
- });
983
- 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;
984
1001
  };
985
1002
  var generateTripsByFrequencies = (trip, frequencies, config) => {
986
1003
  const formattedFrequencies = frequencies.map(
@@ -1003,7 +1020,7 @@ var generateTripsByFrequencies = (trip, frequencies, config) => {
1003
1020
  return trips;
1004
1021
  };
1005
1022
  var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config) => {
1006
- if (config.showArrivalOnDifference === null) {
1023
+ if (config.showArrivalOnDifference === null || config.showArrivalOnDifference === void 0) {
1007
1024
  return stopIds;
1008
1025
  }
1009
1026
  for (const trip of timetable.orderedTrips) {
@@ -1125,7 +1142,7 @@ var getStopsForTimetable = (timetable, config) => {
1125
1142
  }
1126
1143
  return stop;
1127
1144
  });
1128
- if (timetable.showStopCity) {
1145
+ if (config.showStopCity) {
1129
1146
  const stopAttributes = getStopAttributes({
1130
1147
  stop_id: orderedStopIds
1131
1148
  });
@@ -1140,6 +1157,39 @@ var getStopsForTimetable = (timetable, config) => {
1140
1157
  }
1141
1158
  return orderedStops;
1142
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
+ };
1143
1193
  var getCalendarsFromTimetable = (timetable) => {
1144
1194
  const db = openDb();
1145
1195
  let whereClause = "";
@@ -1289,13 +1339,17 @@ var filterTrips = (timetable) => {
1289
1339
  }
1290
1340
  trip.stoptimes = combinedStoptimes;
1291
1341
  }
1292
- 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
+ );
1293
1345
  for (const trip of filteredTrips) {
1294
1346
  trip.stoptimes = trip.stoptimes.filter(
1295
1347
  (stoptime) => timetableStopIds.has(stoptime.stop_id)
1296
1348
  );
1297
1349
  }
1298
- filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
1350
+ filteredTrips = filteredTrips.filter(
1351
+ (trip) => trip.stoptimes.length > 1
1352
+ );
1299
1353
  return filteredTrips;
1300
1354
  };
1301
1355
  var getTripsForTimetable = (timetable, calendars, config) => {
@@ -1446,6 +1500,15 @@ var formatTimetables = (timetables, config) => {
1446
1500
  };
1447
1501
  function getTimetablePagesForAgency(config) {
1448
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
+ });
1449
1512
  if (timetables.length === 0) {
1450
1513
  return convertRoutesToTimetablePages(config);
1451
1514
  }
@@ -1455,68 +1518,33 @@ function getTimetablePagesForAgency(config) {
1455
1518
  [["timetable_page_id", "ASC"]]
1456
1519
  );
1457
1520
  if (timetablePages.length === 0) {
1458
- return timetables.map(
1459
- (timetable) => convertTimetableToTimetablePage(timetable, config)
1521
+ return formattedTimetables.map(
1522
+ (timetable) => createTimetablePage({
1523
+ timetablePageId: timetable.timetable_id,
1524
+ timetables: [timetable],
1525
+ config
1526
+ })
1460
1527
  );
1461
1528
  }
1462
- const routes = getRoutes();
1463
1529
  return timetablePages.map((timetablePage) => {
1464
- timetablePage.timetables = sortBy(
1465
- timetables.filter(
1466
- (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1467
- ),
1468
- "timetable_sequence"
1469
- );
1470
- for (const timetable of timetablePage.timetables) {
1471
- timetable.routes = routes.filter(
1472
- (route) => timetable.route_ids.includes(route.route_id)
1473
- );
1474
- }
1475
- 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
+ };
1476
1539
  });
1477
1540
  }
1478
- var getTimetablePageById = (timetablePageId, config) => {
1479
- const timetablePages = getTimetablePages({
1480
- timetable_page_id: timetablePageId
1481
- });
1482
- const timetables = mergeTimetablesWithSameId(getTimetables());
1483
- if (timetablePages.length > 1) {
1484
- throw new Error(
1485
- `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1486
- );
1487
- }
1488
- if (timetablePages.length === 1) {
1489
- const timetablePage = timetablePages[0];
1490
- timetablePage.timetables = sortBy(
1491
- timetables.filter(
1492
- (timetable) => timetable.timetable_page_id === timetablePageId
1493
- ),
1494
- "timetable_sequence"
1495
- );
1496
- for (const timetable of timetablePage.timetables) {
1497
- timetable.routes = getRoutes({
1498
- route_id: timetable.route_ids
1499
- });
1500
- }
1501
- return timetablePage;
1502
- }
1503
- if (timetables.length > 0) {
1504
- const timetablePageTimetables = timetables.filter(
1505
- (timetable) => timetable.timetable_id === timetablePageId
1506
- );
1507
- if (timetablePageTimetables.length === 0) {
1508
- throw new Error(
1509
- `No timetable found for timetable_page_id=${timetablePageId}`
1510
- );
1511
- }
1512
- return convertTimetableToTimetablePage(timetablePageTimetables[0], config);
1513
- }
1541
+ var getDataForTimetablePageById = (timetablePageId) => {
1514
1542
  let calendarCode;
1515
1543
  let calendars;
1516
1544
  let calendarDates;
1517
1545
  let serviceId;
1518
1546
  let directionId = "";
1519
- const parts = timetablePageId.split("|");
1547
+ const parts = timetablePageId?.split("|") ?? [];
1520
1548
  if (parts.length > 2) {
1521
1549
  directionId = Number.parseInt(parts.pop(), 10);
1522
1550
  calendarCode = parts.pop();
@@ -1535,13 +1563,13 @@ var getTimetablePageById = (timetablePageId, config) => {
1535
1563
  },
1536
1564
  ["trip_headsign", "direction_id"]
1537
1565
  );
1538
- const directions = uniqBy(trips, (trip) => trip.direction_id);
1539
- if (directions.length === 0) {
1566
+ const uniqueTripDirections = uniqBy(trips, (trip) => trip.direction_id);
1567
+ if (uniqueTripDirections.length === 0) {
1540
1568
  throw new Error(
1541
1569
  `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1542
1570
  );
1543
1571
  }
1544
- if (/^[01]*$/.test(calendarCode)) {
1572
+ if (/^[01]*$/.test(calendarCode ?? "")) {
1545
1573
  calendars = getCalendars({
1546
1574
  ...calendarCodeToCalendar(calendarCode)
1547
1575
  });
@@ -1552,13 +1580,129 @@ var getTimetablePageById = (timetablePageId, config) => {
1552
1580
  service_id: serviceId
1553
1581
  });
1554
1582
  }
1555
- return convertRouteToTimetablePage(
1556
- routes[0],
1557
- directions[0],
1583
+ return {
1558
1584
  calendars,
1559
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],
1560
1704
  config
1561
- );
1705
+ });
1562
1706
  };
1563
1707
  function setDefaultConfig(initialConfig) {
1564
1708
  const defaults = {
@@ -1579,6 +1723,7 @@ function setDefaultConfig(initialConfig) {
1579
1723
  defaultOrientation: "vertical",
1580
1724
  interpolatedStopSymbol: "\u2022",
1581
1725
  interpolatedStopText: "Estimated time of arrival",
1726
+ groupTimetablesIntoPages: true,
1582
1727
  gtfsToHtmlVersion: version,
1583
1728
  linkStopUrls: false,
1584
1729
  mapStyleUrl: "https://tiles.openfreemap.org/styles/positron",
@@ -1635,12 +1780,6 @@ function getFormattedTimetablePage(timetablePageId, config) {
1635
1780
  timetablePageId,
1636
1781
  config
1637
1782
  );
1638
- const timetableRoutes = getRoutes(
1639
- {
1640
- route_id: timetablePage.route_ids
1641
- },
1642
- ["agency_id"]
1643
- );
1644
1783
  const consolidatedTimetables = formatTimetables(
1645
1784
  timetablePage.timetables,
1646
1785
  config
@@ -1656,7 +1795,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1656
1795
  }
1657
1796
  }
1658
1797
  const uniqueRoutes = uniqBy(
1659
- flatMap2(consolidatedTimetables, (timetable) => timetable.routes),
1798
+ flatMap(consolidatedTimetables, (timetable) => timetable.routes),
1660
1799
  "route_id"
1661
1800
  );
1662
1801
  const formattedTimetablePage = {
@@ -1667,7 +1806,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1667
1806
  consolidatedTimetables.map((timetable) => timetable.dayList)
1668
1807
  ),
1669
1808
  route_ids: uniqueRoutes.map((route) => route.route_id),
1670
- agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
1809
+ agency_ids: uniq(compact(uniqueRoutes.map((route) => route.agency_id))),
1671
1810
  filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`,
1672
1811
  timetable_page_label: timetablePage.timetable_page_label ?? formatListForDisplay(uniqueRoutes.map((route) => formatRouteName(route)))
1673
1812
  };
@@ -1925,12 +2064,14 @@ function formatFrequency(frequency, config) {
1925
2064
  frequency.headway_min = Math.round(headway.asMinutes());
1926
2065
  return frequency;
1927
2066
  }
1928
- function formatTimetableId(timetable) {
1929
- let timetableId = `${timetable.route_ids.join("_")}|${calendarToCalendarCode(
1930
- timetable
1931
- )}`;
1932
- if (!isNullOrEmpty(timetable.direction_id)) {
1933
- 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}`;
1934
2075
  }
1935
2076
  return timetableId;
1936
2077
  }
@@ -2083,10 +2224,18 @@ function formatTimetableLabel(timetable) {
2083
2224
  return timetableLabel;
2084
2225
  }
2085
2226
  var formatRouteName = (route) => {
2086
- if (route.route_long_name === null || route.route_long_name === "") {
2087
- return `Route ${route.route_short_name}`;
2227
+ if (route.route_long_name) {
2228
+ return `Route ${route.route_long_name}`;
2088
2229
  }
2089
- 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";
2090
2239
  };
2091
2240
  var formatListForDisplay = (list) => {
2092
2241
  return new Intl.ListFormat("en-US", {
@@ -2109,6 +2258,7 @@ function mergeTimetablesWithSameId(timetables) {
2109
2258
  }
2110
2259
 
2111
2260
  // src/lib/file-utils.ts
2261
+ var homeDirectory = homedir();
2112
2262
  function getPathToViewsFolder(config) {
2113
2263
  if (config.templatePath) {
2114
2264
  return untildify(config.templatePath);
@@ -2193,15 +2343,34 @@ function zipFolder(outputPath) {
2193
2343
  archive.finalize();
2194
2344
  });
2195
2345
  }
2196
- function generateFileName(timetable, config, extension = "html") {
2197
- let filename = timetable.timetable_id;
2346
+ function generateTimetablePageFileName(timetablePage, config) {
2347
+ if (timetablePage.filename) {
2348
+ return sanitize(timetablePage.filename);
2349
+ }
2350
+ if (config.groupTimetablesIntoPages === true && uniqBy2(timetablePage.timetables, "route_id").length === 1) {
2351
+ const route = timetablePage.timetables[0].routes[0];
2352
+ return sanitize(`${formatRouteNameForFilename(route).toLowerCase()}.html`);
2353
+ }
2354
+ const timetable = timetablePage.timetables[0];
2355
+ let filename = timetable.timetable_id ?? "";
2356
+ for (const route of timetable.routes) {
2357
+ filename += `_${formatRouteNameForFilename(route)}`;
2358
+ }
2359
+ if (!isNullOrEmpty(timetable.direction_id)) {
2360
+ filename += `_${timetable.direction_id}`;
2361
+ }
2362
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.html`;
2363
+ return sanitize(filename.toLowerCase());
2364
+ }
2365
+ function generateCSVFileName(timetable, config) {
2366
+ let filename = timetable.timetable_id ?? "";
2198
2367
  for (const route of timetable.routes) {
2199
- filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, "-")}` : `_${route.route_short_name.replace(/\s/g, "-")}`;
2368
+ filename += `_${formatRouteNameForFilename(route)}`;
2200
2369
  }
2201
2370
  if (!isNullOrEmpty(timetable.direction_id)) {
2202
2371
  filename += `_${timetable.direction_id}`;
2203
2372
  }
2204
- filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.${extension}`;
2373
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.csv`;
2205
2374
  return sanitize(filename).toLowerCase();
2206
2375
  }
2207
2376
  function generateFolderName(timetablePage) {
@@ -2241,16 +2410,18 @@ async function renderPdf(htmlPath) {
2241
2410
  });
2242
2411
  await browser.close();
2243
2412
  }
2413
+ function untildify(pathWithTilde) {
2414
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
2415
+ }
2244
2416
 
2245
2417
  // src/lib/gtfs-to-html.ts
2246
2418
  var gtfsToHtml = async (initialConfig) => {
2247
2419
  const config = setDefaultConfig(initialConfig);
2248
- const timer = new Timer();
2420
+ const startTime = process.hrtime.bigint();
2249
2421
  const agencyKey = config.agencies.map(
2250
2422
  (agency) => agency.agencyKey ?? agency.agency_key ?? "unknown"
2251
2423
  ).join("-");
2252
- const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2253
- timer.start();
2424
+ const outputPath = config.outputPath ? untildify(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2254
2425
  await prepDirectory(outputPath, config);
2255
2426
  try {
2256
2427
  openDb2(config);
@@ -2296,9 +2467,11 @@ var gtfsToHtml = async (initialConfig) => {
2296
2467
  config
2297
2468
  );
2298
2469
  for (const timetable of timetablePage.timetables) {
2299
- for (const warning of timetable.warnings) {
2300
- stats.warnings.push(warning);
2301
- bar?.interrupt(warning);
2470
+ if (timetable.warnings) {
2471
+ for (const warning of timetable.warnings) {
2472
+ stats.warnings.push(warning);
2473
+ bar?.interrupt(warning);
2474
+ }
2302
2475
  }
2303
2476
  }
2304
2477
  if (timetablePage.consolidatedTimetables.length === 0) {
@@ -2321,7 +2494,7 @@ var gtfsToHtml = async (initialConfig) => {
2321
2494
  const csvPath = path.join(
2322
2495
  outputPath,
2323
2496
  datePath,
2324
- generateFileName(timetable, config, "csv")
2497
+ generateCSVFileName(timetable, config)
2325
2498
  );
2326
2499
  await writeFile(csvPath, csv);
2327
2500
  }
@@ -2367,11 +2540,11 @@ var gtfsToHtml = async (initialConfig) => {
2367
2540
  `${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullOutputPath}`
2368
2541
  );
2369
2542
  logStats(config)(stats);
2370
- const seconds = Math.round(timer.time() / 1e3);
2543
+ const endTime = process.hrtime.bigint();
2544
+ const elapsedSeconds = Number(endTime - startTime) / 1e9;
2371
2545
  log(config)(
2372
- `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`
2546
+ `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${elapsedSeconds.toFixed(1)} seconds`
2373
2547
  );
2374
- timer.stop();
2375
2548
  return fullOutputPath;
2376
2549
  };
2377
2550
  var gtfs_to_html_default = gtfsToHtml;