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.
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,16 @@ import {
25
23
  readFile,
26
24
  rm
27
25
  } from "fs/promises";
26
+ import { homedir } from "os";
27
+ import { findPackageJSON } from "module";
28
28
  import * as _ from "lodash-es";
29
+ import { uniqBy as uniqBy2 } from "lodash-es";
29
30
  import archiver from "archiver";
30
31
  import beautify from "js-beautify";
31
32
  import sanitizeHtml from "sanitize-html";
32
33
  import { renderFile } from "pug";
33
34
  import puppeteer from "puppeteer";
34
35
  import sanitize from "sanitize-filename";
35
- import untildify from "untildify";
36
36
  import { marked } from "marked";
37
37
 
38
38
  // src/lib/formatters.ts
@@ -61,16 +61,7 @@ function fromGTFSTime(timeString) {
61
61
  function toGTFSTime(time) {
62
62
  return time.format("HH:mm:ss");
63
63
  }
64
- function fromGTFSDate(gtfsDate) {
65
- return moment(gtfsDate, "YYYYMMDD");
66
- }
67
- function toGTFSDate(date) {
68
- return moment(date).format("YYYYMMDD");
69
- }
70
64
  function calendarToCalendarCode(c) {
71
- if (c.service_id) {
72
- return c.service_id;
73
- }
74
65
  return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
75
66
  }
76
67
  function calendarCodeToCalendar(code) {
@@ -111,13 +102,13 @@ import {
111
102
  find,
112
103
  findLast,
113
104
  first,
114
- flatMap as flatMap2,
115
- flattenDeep,
105
+ flatMap,
116
106
  flow,
117
107
  groupBy,
118
108
  head,
119
109
  last,
120
110
  maxBy,
111
+ orderBy,
121
112
  partialRight,
122
113
  reduce,
123
114
  size,
@@ -151,7 +142,6 @@ import toposort from "toposort";
151
142
 
152
143
  // src/lib/geojson-utils.ts
153
144
  import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
154
- import { flatMap } from "lodash-es";
155
145
  import simplify from "@turf/simplify";
156
146
  import { featureCollection, round } from "@turf/helpers";
157
147
 
@@ -313,7 +303,7 @@ function progressBar(formatString, barTotal, config) {
313
303
  }
314
304
 
315
305
  // src/lib/geojson-utils.ts
316
- var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
306
+ var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
317
307
  var truncateGeoJSONDecimals = (geojson, config) => {
318
308
  for (const feature of geojson.features) {
319
309
  if (feature.geometry.coordinates) {
@@ -504,7 +494,7 @@ function formatTripNameForCSV(trip, timetable) {
504
494
  // package.json
505
495
  var package_default = {
506
496
  name: "gtfs-to-html",
507
- version: "2.10.17",
497
+ version: "2.11.1",
508
498
  private: false,
509
499
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
510
500
  keywords: [
@@ -557,24 +547,22 @@ var package_default = {
557
547
  "cli-table": "^0.3.11",
558
548
  "csv-stringify": "^6.6.0",
559
549
  express: "^5.1.0",
560
- gtfs: "^4.17.7",
550
+ gtfs: "^4.18.0",
561
551
  "gtfs-realtime-pbf-js-module": "^1.0.0",
562
552
  "js-beautify": "^1.15.4",
563
553
  "lodash-es": "^4.17.21",
564
- marked: "^16.1.2",
554
+ marked: "^16.3.0",
565
555
  moment: "^2.30.1",
566
556
  pbf: "^4.0.1",
567
557
  "pretty-error": "^4.0.0",
568
558
  pug: "^3.0.3",
569
- puppeteer: "^24.16.2",
559
+ puppeteer: "^24.21.0",
570
560
  "sanitize-filename": "^1.6.3",
571
561
  "sanitize-html": "^2.17.0",
572
562
  sqlstring: "^2.3.3",
573
- "timer-machine": "^1.1.0",
574
563
  toposort: "^2.0.2",
575
- untildify: "^5.0.0",
576
564
  yargs: "^18.0.0",
577
- yoctocolors: "^2.1.1"
565
+ yoctocolors: "^2.1.2"
578
566
  },
579
567
  devDependencies: {
580
568
  "@types/archiver": "^6.0.3",
@@ -586,16 +574,17 @@ var package_default = {
586
574
  "@types/node": "^22",
587
575
  "@types/pug": "^2.0.10",
588
576
  "@types/sanitize-html": "^2.16.0",
589
- "@types/timer-machine": "^1.1.3",
577
+ "@types/sqlstring": "^2.3.2",
578
+ "@types/toposort": "^2.0.7",
590
579
  "@types/yargs": "^17.0.33",
591
580
  husky: "^9.1.7",
592
- "lint-staged": "^16.1.5",
581
+ "lint-staged": "^16.1.6",
593
582
  prettier: "^3.6.2",
594
583
  tsup: "^8.5.0",
595
584
  typescript: "^5.9.2"
596
585
  },
597
586
  engines: {
598
- node: ">= 20.11.0"
587
+ node: ">= 22"
599
588
  },
600
589
  "release-it": {
601
590
  github: {
@@ -645,7 +634,7 @@ var findCommonStopId = (trips, config) => {
645
634
  return null;
646
635
  }
647
636
  const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
648
- if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
637
+ if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes)?.stop_id) {
649
638
  return false;
650
639
  }
651
640
  if (isNullOrEmpty(stoptime.arrival_time)) {
@@ -698,11 +687,7 @@ var sortTrips = (trips, config) => {
698
687
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
699
688
  );
700
689
  }
701
- sortedTrips = sortBy(
702
- trips,
703
- ["firstStoptime", "lastStoptime"],
704
- ["asc", "asc"]
705
- );
690
+ sortedTrips = sortBy(trips, ["firstStoptime", "lastStoptime"]);
706
691
  } else if (config.sortingAlgorithm === "end") {
707
692
  for (const trip of trips) {
708
693
  if (trip.stoptimes.length === 0) {
@@ -713,11 +698,7 @@ var sortTrips = (trips, config) => {
713
698
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
714
699
  );
715
700
  }
716
- sortedTrips = sortBy(
717
- trips,
718
- ["lastStoptime", "firstStoptime"],
719
- ["asc", "asc"]
720
- );
701
+ sortedTrips = sortBy(trips, ["lastStoptime", "firstStoptime"]);
721
702
  } else if (config.sortingAlgorithm === "first") {
722
703
  const longestTripStoptimes = getLongestTripStoptimes(trips, config);
723
704
  const firstStopId = first(longestTripStoptimes).stop_id;
@@ -744,8 +725,8 @@ var getCalendarDatesForTimetable = (timetable, config) => {
744
725
  [],
745
726
  [["date", "ASC"]]
746
727
  );
747
- const start = fromGTFSDate(timetable.start_date);
748
- const end = fromGTFSDate(timetable.end_date);
728
+ const start = moment2(timetable.start_date, "YYYYMMDD");
729
+ const end = moment2(timetable.end_date, "YYYYMMDD");
749
730
  const excludedDates = /* @__PURE__ */ new Set();
750
731
  const includedDates = /* @__PURE__ */ new Set();
751
732
  for (const calendarDate of calendarDates) {
@@ -870,28 +851,70 @@ var getTimetableNotesForTimetable = (timetable, config) => {
870
851
  }));
871
852
  return sortBy(formattedNotes, "symbol");
872
853
  };
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");
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);
880
873
  return {
881
- timetable_page_id: timetable.timetable_id,
882
- timetable_page_label: timetable.timetable_label,
883
- timetables: [timetable],
874
+ ...timetablePage,
884
875
  filename
885
876
  };
886
877
  };
887
- var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config) => {
888
- 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,
889
912
  route_ids: [route.route_id],
890
- direction_id: direction ? direction.direction_id : void 0,
891
- direction_name: direction ? direction.trip_headsign : void 0,
913
+ direction_id: directionId === null ? null : directionId,
914
+ direction_name: tripHeadsign === null ? null : tripHeadsign,
892
915
  routes: [route],
893
916
  include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
894
- service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
917
+ service_ids: serviceIds,
895
918
  service_notes: null,
896
919
  timetable_label: null,
897
920
  start_time: null,
@@ -899,88 +922,83 @@ var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, c
899
922
  orientation: null,
900
923
  timetable_sequence: null,
901
924
  show_trip_continuation: null,
902
- start_date: null,
903
- end_date: null
925
+ start_date: startDate,
926
+ end_date: endDate,
927
+ ...days2
904
928
  };
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
929
  };
919
930
  var convertRoutesToTimetablePages = (config) => {
920
- const db = openDb(config);
921
931
  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) => {
932
+ const timetablePages = [];
933
+ const { calendars, calendarDates } = getCalendarsFromConfig(config);
934
+ for (const route of routes) {
943
935
  const trips = getTrips(
944
936
  {
945
937
  route_id: route.route_id
946
938
  },
947
939
  ["trip_headsign", "direction_id", "trip_id", "service_id"]
948
940
  );
949
- const directions = uniqBy(trips, (trip) => trip.direction_id);
950
- 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);
951
947
  const calendarDateGroups = groupBy(calendarDates, "service_id");
952
- return directions.map((direction) => [
953
- Object.values(dayGroups).map((calendars2) => {
948
+ const timetables = [];
949
+ for (const uniqueTripDirection of uniqueTripDirections) {
950
+ for (const calendars2 of Object.values(calendarGroups)) {
954
951
  const tripsForCalendars = trips.filter(
955
952
  (trip) => some(calendars2, { service_id: trip.service_id })
956
953
  );
957
954
  if (tripsForCalendars.length > 0) {
958
- return convertRouteToTimetablePage(
959
- route,
960
- direction,
961
- calendars2,
962
- null,
963
- config
955
+ timetables.push(
956
+ createTimetable({
957
+ route,
958
+ directionId: uniqueTripDirection.direction_id,
959
+ tripHeadsign: uniqueTripDirection.trip_headsign,
960
+ calendars: calendars2
961
+ })
964
962
  );
965
963
  }
966
- }),
967
- Object.values(calendarDateGroups).map((calendarDates2) => {
964
+ }
965
+ for (const calendarDates2 of Object.values(calendarDateGroups)) {
968
966
  const tripsForCalendarDates = trips.filter(
969
967
  (trip) => some(calendarDates2, { service_id: trip.service_id })
970
968
  );
971
969
  if (tripsForCalendarDates.length > 0) {
972
- return convertRouteToTimetablePage(
973
- route,
974
- direction,
975
- null,
976
- calendarDates2,
977
- config
970
+ timetables.push(
971
+ createTimetable({
972
+ route,
973
+ directionId: uniqueTripDirection.direction_id,
974
+ tripHeadsign: uniqueTripDirection.trip_headsign,
975
+ calendarDates: calendarDates2
976
+ })
978
977
  );
979
978
  }
980
- })
981
- ]);
982
- });
983
- 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;
984
1002
  };
985
1003
  var generateTripsByFrequencies = (trip, frequencies, config) => {
986
1004
  const formattedFrequencies = frequencies.map(
@@ -1003,7 +1021,7 @@ var generateTripsByFrequencies = (trip, frequencies, config) => {
1003
1021
  return trips;
1004
1022
  };
1005
1023
  var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config) => {
1006
- if (config.showArrivalOnDifference === null) {
1024
+ if (config.showArrivalOnDifference === null || config.showArrivalOnDifference === void 0) {
1007
1025
  return stopIds;
1008
1026
  }
1009
1027
  for (const trip of timetable.orderedTrips) {
@@ -1125,7 +1143,7 @@ var getStopsForTimetable = (timetable, config) => {
1125
1143
  }
1126
1144
  return stop;
1127
1145
  });
1128
- if (timetable.showStopCity) {
1146
+ if (config.showStopCity) {
1129
1147
  const stopAttributes = getStopAttributes({
1130
1148
  stop_id: orderedStopIds
1131
1149
  });
@@ -1140,6 +1158,39 @@ var getStopsForTimetable = (timetable, config) => {
1140
1158
  }
1141
1159
  return orderedStops;
1142
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
+ };
1143
1194
  var getCalendarsFromTimetable = (timetable) => {
1144
1195
  const db = openDb();
1145
1196
  let whereClause = "";
@@ -1289,13 +1340,17 @@ var filterTrips = (timetable) => {
1289
1340
  }
1290
1341
  trip.stoptimes = combinedStoptimes;
1291
1342
  }
1292
- 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
+ );
1293
1346
  for (const trip of filteredTrips) {
1294
1347
  trip.stoptimes = trip.stoptimes.filter(
1295
1348
  (stoptime) => timetableStopIds.has(stoptime.stop_id)
1296
1349
  );
1297
1350
  }
1298
- filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
1351
+ filteredTrips = filteredTrips.filter(
1352
+ (trip) => trip.stoptimes.length > 1
1353
+ );
1299
1354
  return filteredTrips;
1300
1355
  };
1301
1356
  var getTripsForTimetable = (timetable, calendars, config) => {
@@ -1446,6 +1501,15 @@ var formatTimetables = (timetables, config) => {
1446
1501
  };
1447
1502
  function getTimetablePagesForAgency(config) {
1448
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
+ });
1449
1513
  if (timetables.length === 0) {
1450
1514
  return convertRoutesToTimetablePages(config);
1451
1515
  }
@@ -1455,68 +1519,33 @@ function getTimetablePagesForAgency(config) {
1455
1519
  [["timetable_page_id", "ASC"]]
1456
1520
  );
1457
1521
  if (timetablePages.length === 0) {
1458
- return timetables.map(
1459
- (timetable) => convertTimetableToTimetablePage(timetable, config)
1522
+ return formattedTimetables.map(
1523
+ (timetable) => createTimetablePage({
1524
+ timetablePageId: timetable.timetable_id,
1525
+ timetables: [timetable],
1526
+ config
1527
+ })
1460
1528
  );
1461
1529
  }
1462
- const routes = getRoutes();
1463
1530
  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;
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
+ };
1476
1540
  });
1477
1541
  }
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
- }
1542
+ var getDataForTimetablePageById = (timetablePageId) => {
1514
1543
  let calendarCode;
1515
1544
  let calendars;
1516
1545
  let calendarDates;
1517
1546
  let serviceId;
1518
1547
  let directionId = "";
1519
- const parts = timetablePageId.split("|");
1548
+ const parts = timetablePageId?.split("|") ?? [];
1520
1549
  if (parts.length > 2) {
1521
1550
  directionId = Number.parseInt(parts.pop(), 10);
1522
1551
  calendarCode = parts.pop();
@@ -1535,13 +1564,13 @@ var getTimetablePageById = (timetablePageId, config) => {
1535
1564
  },
1536
1565
  ["trip_headsign", "direction_id"]
1537
1566
  );
1538
- const directions = uniqBy(trips, (trip) => trip.direction_id);
1539
- if (directions.length === 0) {
1567
+ const uniqueTripDirections = uniqBy(trips, (trip) => trip.direction_id);
1568
+ if (uniqueTripDirections.length === 0) {
1540
1569
  throw new Error(
1541
1570
  `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1542
1571
  );
1543
1572
  }
1544
- if (/^[01]*$/.test(calendarCode)) {
1573
+ if (/^[01]*$/.test(calendarCode ?? "")) {
1545
1574
  calendars = getCalendars({
1546
1575
  ...calendarCodeToCalendar(calendarCode)
1547
1576
  });
@@ -1552,13 +1581,129 @@ var getTimetablePageById = (timetablePageId, config) => {
1552
1581
  service_id: serviceId
1553
1582
  });
1554
1583
  }
1555
- return convertRouteToTimetablePage(
1556
- routes[0],
1557
- directions[0],
1584
+ return {
1558
1585
  calendars,
1559
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],
1560
1705
  config
1561
- );
1706
+ });
1562
1707
  };
1563
1708
  function setDefaultConfig(initialConfig) {
1564
1709
  const defaults = {
@@ -1579,6 +1724,7 @@ function setDefaultConfig(initialConfig) {
1579
1724
  defaultOrientation: "vertical",
1580
1725
  interpolatedStopSymbol: "\u2022",
1581
1726
  interpolatedStopText: "Estimated time of arrival",
1727
+ groupTimetablesIntoPages: true,
1582
1728
  gtfsToHtmlVersion: version,
1583
1729
  linkStopUrls: false,
1584
1730
  mapStyleUrl: "https://tiles.openfreemap.org/styles/positron",
@@ -1635,12 +1781,6 @@ function getFormattedTimetablePage(timetablePageId, config) {
1635
1781
  timetablePageId,
1636
1782
  config
1637
1783
  );
1638
- const timetableRoutes = getRoutes(
1639
- {
1640
- route_id: timetablePage.route_ids
1641
- },
1642
- ["agency_id"]
1643
- );
1644
1784
  const consolidatedTimetables = formatTimetables(
1645
1785
  timetablePage.timetables,
1646
1786
  config
@@ -1656,7 +1796,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1656
1796
  }
1657
1797
  }
1658
1798
  const uniqueRoutes = uniqBy(
1659
- flatMap2(consolidatedTimetables, (timetable) => timetable.routes),
1799
+ flatMap(consolidatedTimetables, (timetable) => timetable.routes),
1660
1800
  "route_id"
1661
1801
  );
1662
1802
  const formattedTimetablePage = {
@@ -1667,7 +1807,7 @@ function getFormattedTimetablePage(timetablePageId, config) {
1667
1807
  consolidatedTimetables.map((timetable) => timetable.dayList)
1668
1808
  ),
1669
1809
  route_ids: uniqueRoutes.map((route) => route.route_id),
1670
- agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
1810
+ agency_ids: uniq(compact(uniqueRoutes.map((route) => route.agency_id))),
1671
1811
  filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`,
1672
1812
  timetable_page_label: timetablePage.timetable_page_label ?? formatListForDisplay(uniqueRoutes.map((route) => formatRouteName(route)))
1673
1813
  };
@@ -1925,12 +2065,14 @@ function formatFrequency(frequency, config) {
1925
2065
  frequency.headway_min = Math.round(headway.asMinutes());
1926
2066
  return frequency;
1927
2067
  }
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}`;
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}`;
1934
2076
  }
1935
2077
  return timetableId;
1936
2078
  }
@@ -2083,10 +2225,18 @@ function formatTimetableLabel(timetable) {
2083
2225
  return timetableLabel;
2084
2226
  }
2085
2227
  var formatRouteName = (route) => {
2086
- if (route.route_long_name === null || route.route_long_name === "") {
2087
- return `Route ${route.route_short_name}`;
2228
+ if (route.route_long_name) {
2229
+ return `Route ${route.route_long_name}`;
2088
2230
  }
2089
- 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";
2090
2240
  };
2091
2241
  var formatListForDisplay = (list) => {
2092
2242
  return new Intl.ListFormat("en-US", {
@@ -2109,6 +2259,7 @@ function mergeTimetablesWithSameId(timetables) {
2109
2259
  }
2110
2260
 
2111
2261
  // src/lib/file-utils.ts
2262
+ var homeDirectory = homedir();
2112
2263
  function getPathToViewsFolder(config) {
2113
2264
  if (config.templatePath) {
2114
2265
  return untildify(config.templatePath);
@@ -2165,18 +2316,32 @@ async function copyStaticAssets(config, outputPath) {
2165
2316
  }
2166
2317
  if (config.hasGtfsRealtimeVehiclePositions || config.hasGtfsRealtimeTripUpdates || config.hasGtfsRealtimeAlerts) {
2167
2318
  await copyFile(
2168
- "node_modules/pbf/dist/pbf.js",
2319
+ join(
2320
+ dirname(findPackageJSON("pbf", import.meta.url)),
2321
+ "dist/pbf.js"
2322
+ ),
2169
2323
  join(outputPath, "js/pbf.js")
2170
2324
  );
2171
2325
  await copyFile(
2172
- "node_modules/gtfs-realtime-pbf-js-module/gtfs-realtime.browser.proto.js",
2326
+ join(
2327
+ dirname(
2328
+ findPackageJSON(
2329
+ "gtfs-realtime-pbf-js-module",
2330
+ import.meta.url
2331
+ )
2332
+ ),
2333
+ "gtfs-realtime.browser.proto.js"
2334
+ ),
2173
2335
  join(outputPath, "js/gtfs-realtime.browser.proto.js")
2174
2336
  );
2175
2337
  }
2176
2338
  if (config.hasGtfsRealtimeAlerts) {
2177
2339
  await copyFile(
2178
- "node_modules/anchorme/dist/browser/anchorme.min.js",
2179
- join(outputPath, "js//anchorme.min.js")
2340
+ join(
2341
+ dirname(findPackageJSON("anchorme", import.meta.url)),
2342
+ "dist/browser/anchorme.min.js"
2343
+ ),
2344
+ join(outputPath, "js/anchorme.min.js")
2180
2345
  );
2181
2346
  }
2182
2347
  }
@@ -2193,15 +2358,34 @@ function zipFolder(outputPath) {
2193
2358
  archive.finalize();
2194
2359
  });
2195
2360
  }
2196
- function generateFileName(timetable, config, extension = "html") {
2197
- let filename = timetable.timetable_id;
2361
+ function generateTimetablePageFileName(timetablePage, config) {
2362
+ if (timetablePage.filename) {
2363
+ return sanitize(timetablePage.filename);
2364
+ }
2365
+ if (config.groupTimetablesIntoPages === true && uniqBy2(timetablePage.timetables, "route_id").length === 1) {
2366
+ const route = timetablePage.timetables[0].routes[0];
2367
+ return sanitize(`${formatRouteNameForFilename(route).toLowerCase()}.html`);
2368
+ }
2369
+ const timetable = timetablePage.timetables[0];
2370
+ let filename = timetable.timetable_id ?? "";
2371
+ for (const route of timetable.routes) {
2372
+ filename += `_${formatRouteNameForFilename(route)}`;
2373
+ }
2374
+ if (!isNullOrEmpty(timetable.direction_id)) {
2375
+ filename += `_${timetable.direction_id}`;
2376
+ }
2377
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.html`;
2378
+ return sanitize(filename.toLowerCase());
2379
+ }
2380
+ function generateCSVFileName(timetable, config) {
2381
+ let filename = timetable.timetable_id ?? "";
2198
2382
  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, "-")}`;
2383
+ filename += `_${formatRouteNameForFilename(route)}`;
2200
2384
  }
2201
2385
  if (!isNullOrEmpty(timetable.direction_id)) {
2202
2386
  filename += `_${timetable.direction_id}`;
2203
2387
  }
2204
- filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.${extension}`;
2388
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.csv`;
2205
2389
  return sanitize(filename).toLowerCase();
2206
2390
  }
2207
2391
  function generateFolderName(timetablePage) {
@@ -2241,16 +2425,18 @@ async function renderPdf(htmlPath) {
2241
2425
  });
2242
2426
  await browser.close();
2243
2427
  }
2428
+ function untildify(pathWithTilde) {
2429
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
2430
+ }
2244
2431
 
2245
2432
  // src/lib/gtfs-to-html.ts
2246
2433
  var gtfsToHtml = async (initialConfig) => {
2247
2434
  const config = setDefaultConfig(initialConfig);
2248
- const timer = new Timer();
2435
+ const startTime = process.hrtime.bigint();
2249
2436
  const agencyKey = config.agencies.map(
2250
2437
  (agency) => agency.agencyKey ?? agency.agency_key ?? "unknown"
2251
2438
  ).join("-");
2252
- const outputPath = config.outputPath ? untildify2(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2253
- timer.start();
2439
+ const outputPath = config.outputPath ? untildify(config.outputPath) : path.join(process.cwd(), "html", sanitize2(agencyKey));
2254
2440
  await prepDirectory(outputPath, config);
2255
2441
  try {
2256
2442
  openDb2(config);
@@ -2296,9 +2482,11 @@ var gtfsToHtml = async (initialConfig) => {
2296
2482
  config
2297
2483
  );
2298
2484
  for (const timetable of timetablePage.timetables) {
2299
- for (const warning of timetable.warnings) {
2300
- stats.warnings.push(warning);
2301
- bar?.interrupt(warning);
2485
+ if (timetable.warnings) {
2486
+ for (const warning of timetable.warnings) {
2487
+ stats.warnings.push(warning);
2488
+ bar?.interrupt(warning);
2489
+ }
2302
2490
  }
2303
2491
  }
2304
2492
  if (timetablePage.consolidatedTimetables.length === 0) {
@@ -2321,7 +2509,7 @@ var gtfsToHtml = async (initialConfig) => {
2321
2509
  const csvPath = path.join(
2322
2510
  outputPath,
2323
2511
  datePath,
2324
- generateFileName(timetable, config, "csv")
2512
+ generateCSVFileName(timetable, config)
2325
2513
  );
2326
2514
  await writeFile(csvPath, csv);
2327
2515
  }
@@ -2367,11 +2555,11 @@ var gtfsToHtml = async (initialConfig) => {
2367
2555
  `${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullOutputPath}`
2368
2556
  );
2369
2557
  logStats(config)(stats);
2370
- const seconds = Math.round(timer.time() / 1e3);
2558
+ const endTime = process.hrtime.bigint();
2559
+ const elapsedSeconds = Number(endTime - startTime) / 1e9;
2371
2560
  log(config)(
2372
- `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`
2561
+ `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${elapsedSeconds.toFixed(1)} seconds`
2373
2562
  );
2374
- timer.stop();
2375
2563
  return fullOutputPath;
2376
2564
  };
2377
2565
  var gtfs_to_html_default = gtfsToHtml;