gtfs-to-html 2.10.17 → 2.11.1

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