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/app/index.js CHANGED
@@ -12,7 +12,6 @@ import yargs from "yargs";
12
12
  import { hideBin } from "yargs/helpers";
13
13
  import { openDb as openDb2 } from "gtfs";
14
14
  import express from "express";
15
- import untildify2 from "untildify";
16
15
 
17
16
  // src/lib/formatters.ts
18
17
  import {
@@ -40,16 +39,7 @@ function fromGTFSTime(timeString) {
40
39
  function toGTFSTime(time) {
41
40
  return time.format("HH:mm:ss");
42
41
  }
43
- function fromGTFSDate(gtfsDate) {
44
- return moment(gtfsDate, "YYYYMMDD");
45
- }
46
- function toGTFSDate(date) {
47
- return moment(date).format("YYYYMMDD");
48
- }
49
42
  function calendarToCalendarCode(c) {
50
- if (c.service_id) {
51
- return c.service_id;
52
- }
53
43
  return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
54
44
  }
55
45
  function calendarCodeToCalendar(code) {
@@ -90,20 +80,20 @@ import {
90
80
  find,
91
81
  findLast,
92
82
  first,
93
- flatMap as flatMap2,
94
- flattenDeep,
83
+ flatMap,
95
84
  flow,
96
85
  groupBy,
97
86
  head,
98
87
  last,
99
88
  maxBy,
89
+ orderBy,
100
90
  partialRight,
101
91
  reduce,
102
92
  size,
103
93
  some,
104
94
  sortBy,
105
95
  uniq,
106
- uniqBy,
96
+ uniqBy as uniqBy2,
107
97
  zip
108
98
  } from "lodash-es";
109
99
  import {
@@ -141,14 +131,16 @@ import {
141
131
  readFile,
142
132
  rm
143
133
  } from "fs/promises";
134
+ import { homedir } from "os";
135
+ import { findPackageJSON } from "module";
144
136
  import * as _ from "lodash-es";
137
+ import { uniqBy } from "lodash-es";
145
138
  import archiver from "archiver";
146
139
  import beautify from "js-beautify";
147
140
  import sanitizeHtml from "sanitize-html";
148
141
  import { renderFile } from "pug";
149
142
  import puppeteer from "puppeteer";
150
143
  import sanitize from "sanitize-filename";
151
- import untildify from "untildify";
152
144
  import { marked } from "marked";
153
145
 
154
146
  // src/lib/template-functions.ts
@@ -266,6 +258,7 @@ function formatTripNameForCSV(trip, timetable) {
266
258
  }
267
259
 
268
260
  // src/lib/file-utils.ts
261
+ var homeDirectory = homedir();
269
262
  function getPathToViewsFolder(config2) {
270
263
  if (config2.templatePath) {
271
264
  return untildify(config2.templatePath);
@@ -285,16 +278,24 @@ function getPathToTemplateFile(templateFileName, config2) {
285
278
  const fullTemplateFileName = config2.noHead !== true ? `${templateFileName}_full.pug` : `${templateFileName}.pug`;
286
279
  return join(getPathToViewsFolder(config2), fullTemplateFileName);
287
280
  }
288
- function generateFileName(timetable, config2, extension = "html") {
289
- let filename = timetable.timetable_id;
281
+ function generateTimetablePageFileName(timetablePage, config2) {
282
+ if (timetablePage.filename) {
283
+ return sanitize(timetablePage.filename);
284
+ }
285
+ if (config2.groupTimetablesIntoPages === true && uniqBy(timetablePage.timetables, "route_id").length === 1) {
286
+ const route = timetablePage.timetables[0].routes[0];
287
+ return sanitize(`${formatRouteNameForFilename(route).toLowerCase()}.html`);
288
+ }
289
+ const timetable = timetablePage.timetables[0];
290
+ let filename = timetable.timetable_id ?? "";
290
291
  for (const route of timetable.routes) {
291
- filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, "-")}` : `_${route.route_short_name.replace(/\s/g, "-")}`;
292
+ filename += `_${formatRouteNameForFilename(route)}`;
292
293
  }
293
294
  if (!isNullOrEmpty(timetable.direction_id)) {
294
295
  filename += `_${timetable.direction_id}`;
295
296
  }
296
- filename += `_${formatDays(timetable, config2).replace(/\s/g, "")}.${extension}`;
297
- return sanitize(filename).toLowerCase();
297
+ filename += `_${formatDays(timetable, config2).replace(/\s/g, "")}.html`;
298
+ return sanitize(filename.toLowerCase());
298
299
  }
299
300
  async function renderTemplate(templateFileName, templateVars, config2) {
300
301
  const templatePath = getPathToTemplateFile(templateFileName, config2);
@@ -313,10 +314,12 @@ async function renderTemplate(templateFileName, templateVars, config2) {
313
314
  }
314
315
  return html;
315
316
  }
317
+ function untildify(pathWithTilde) {
318
+ return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde;
319
+ }
316
320
 
317
321
  // src/lib/geojson-utils.ts
318
322
  import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
319
- import { flatMap } from "lodash-es";
320
323
  import simplify from "@turf/simplify";
321
324
  import { featureCollection, round } from "@turf/helpers";
322
325
 
@@ -342,7 +345,7 @@ function formatWarning(text) {
342
345
  }
343
346
 
344
347
  // src/lib/geojson-utils.ts
345
- var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
348
+ var mergeGeojson = (...geojsons) => featureCollection(geojsons.flatMap((geojson) => geojson.features));
346
349
  var truncateGeoJSONDecimals = (geojson, config2) => {
347
350
  for (const feature of geojson.features) {
348
351
  if (feature.geometry.coordinates) {
@@ -419,7 +422,7 @@ function getAgencyGeoJSON(config2) {
419
422
  // package.json
420
423
  var package_default = {
421
424
  name: "gtfs-to-html",
422
- version: "2.10.17",
425
+ version: "2.11.1",
423
426
  private: false,
424
427
  description: "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
425
428
  keywords: [
@@ -472,24 +475,22 @@ var package_default = {
472
475
  "cli-table": "^0.3.11",
473
476
  "csv-stringify": "^6.6.0",
474
477
  express: "^5.1.0",
475
- gtfs: "^4.17.7",
478
+ gtfs: "^4.18.0",
476
479
  "gtfs-realtime-pbf-js-module": "^1.0.0",
477
480
  "js-beautify": "^1.15.4",
478
481
  "lodash-es": "^4.17.21",
479
- marked: "^16.1.2",
482
+ marked: "^16.3.0",
480
483
  moment: "^2.30.1",
481
484
  pbf: "^4.0.1",
482
485
  "pretty-error": "^4.0.0",
483
486
  pug: "^3.0.3",
484
- puppeteer: "^24.16.2",
487
+ puppeteer: "^24.21.0",
485
488
  "sanitize-filename": "^1.6.3",
486
489
  "sanitize-html": "^2.17.0",
487
490
  sqlstring: "^2.3.3",
488
- "timer-machine": "^1.1.0",
489
491
  toposort: "^2.0.2",
490
- untildify: "^5.0.0",
491
492
  yargs: "^18.0.0",
492
- yoctocolors: "^2.1.1"
493
+ yoctocolors: "^2.1.2"
493
494
  },
494
495
  devDependencies: {
495
496
  "@types/archiver": "^6.0.3",
@@ -501,16 +502,17 @@ var package_default = {
501
502
  "@types/node": "^22",
502
503
  "@types/pug": "^2.0.10",
503
504
  "@types/sanitize-html": "^2.16.0",
504
- "@types/timer-machine": "^1.1.3",
505
+ "@types/sqlstring": "^2.3.2",
506
+ "@types/toposort": "^2.0.7",
505
507
  "@types/yargs": "^17.0.33",
506
508
  husky: "^9.1.7",
507
- "lint-staged": "^16.1.5",
509
+ "lint-staged": "^16.1.6",
508
510
  prettier: "^3.6.2",
509
511
  tsup: "^8.5.0",
510
512
  typescript: "^5.9.2"
511
513
  },
512
514
  engines: {
513
- node: ">= 20.11.0"
515
+ node: ">= 22"
514
516
  },
515
517
  "release-it": {
516
518
  github: {
@@ -560,7 +562,7 @@ var findCommonStopId = (trips, config2) => {
560
562
  return null;
561
563
  }
562
564
  const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
563
- if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
565
+ if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes)?.stop_id) {
564
566
  return false;
565
567
  }
566
568
  if (isNullOrEmpty(stoptime.arrival_time)) {
@@ -613,11 +615,7 @@ var sortTrips = (trips, config2) => {
613
615
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
614
616
  );
615
617
  }
616
- sortedTrips = sortBy(
617
- trips,
618
- ["firstStoptime", "lastStoptime"],
619
- ["asc", "asc"]
620
- );
618
+ sortedTrips = sortBy(trips, ["firstStoptime", "lastStoptime"]);
621
619
  } else if (config2.sortingAlgorithm === "end") {
622
620
  for (const trip of trips) {
623
621
  if (trip.stoptimes.length === 0) {
@@ -628,11 +626,7 @@ var sortTrips = (trips, config2) => {
628
626
  trip.stoptimes[trip.stoptimes.length - 1].departure_time
629
627
  );
630
628
  }
631
- sortedTrips = sortBy(
632
- trips,
633
- ["lastStoptime", "firstStoptime"],
634
- ["asc", "asc"]
635
- );
629
+ sortedTrips = sortBy(trips, ["lastStoptime", "firstStoptime"]);
636
630
  } else if (config2.sortingAlgorithm === "first") {
637
631
  const longestTripStoptimes = getLongestTripStoptimes(trips, config2);
638
632
  const firstStopId = first(longestTripStoptimes).stop_id;
@@ -659,8 +653,8 @@ var getCalendarDatesForTimetable = (timetable, config2) => {
659
653
  [],
660
654
  [["date", "ASC"]]
661
655
  );
662
- const start = fromGTFSDate(timetable.start_date);
663
- const end = fromGTFSDate(timetable.end_date);
656
+ const start = moment2(timetable.start_date, "YYYYMMDD");
657
+ const end = moment2(timetable.end_date, "YYYYMMDD");
664
658
  const excludedDates = /* @__PURE__ */ new Set();
665
659
  const includedDates = /* @__PURE__ */ new Set();
666
660
  for (const calendarDate of calendarDates) {
@@ -785,28 +779,70 @@ var getTimetableNotesForTimetable = (timetable, config2) => {
785
779
  }));
786
780
  return sortBy(formattedNotes, "symbol");
787
781
  };
788
- var convertTimetableToTimetablePage = (timetable, config2) => {
789
- if (!timetable.routes) {
790
- timetable.routes = getRoutes({
791
- route_id: timetable.route_ids
792
- });
793
- }
794
- const filename = generateFileName(timetable, config2, "html");
782
+ var createTimetablePage = ({
783
+ timetablePageId,
784
+ timetables,
785
+ config: config2
786
+ }) => {
787
+ const updatedTimetables = timetables.map((timetable) => {
788
+ if (!timetable.routes) {
789
+ timetable.routes = getRoutes({
790
+ route_id: timetable.route_ids
791
+ });
792
+ }
793
+ return timetable;
794
+ });
795
+ const timetablePage = {
796
+ timetable_page_id: timetablePageId,
797
+ timetables: updatedTimetables,
798
+ routes: updatedTimetables.flatMap((timetable) => timetable.routes)
799
+ };
800
+ const filename = generateTimetablePageFileName(timetablePage, config2);
795
801
  return {
796
- timetable_page_id: timetable.timetable_id,
797
- timetable_page_label: timetable.timetable_label,
798
- timetables: [timetable],
802
+ ...timetablePage,
799
803
  filename
800
804
  };
801
805
  };
802
- var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config2) => {
803
- const timetable = {
806
+ var createTimetable = ({
807
+ route,
808
+ directionId,
809
+ tripHeadsign,
810
+ calendars,
811
+ calendarDates
812
+ }) => {
813
+ const serviceIds = uniq([
814
+ ...calendars?.map((calendar) => calendar.service_id) ?? [],
815
+ ...calendarDates?.map((calendarDate) => calendarDate.service_id) ?? []
816
+ ]);
817
+ const days2 = {};
818
+ let startDate = null;
819
+ let endDate = null;
820
+ if (calendars && calendars.length > 0) {
821
+ Object.assign(days2, getDaysFromCalendars(calendars));
822
+ startDate = parseInt(
823
+ moment2.min(
824
+ calendars.map((calendar) => moment2(calendar.start_date, "YYYYMMDD"))
825
+ ).format("YYYYMMDD"),
826
+ 10
827
+ );
828
+ endDate = parseInt(
829
+ moment2.max(calendars.map((calendar) => moment2(calendar.end_date, "YYYYMMDD"))).format("YYYYMMDD"),
830
+ 10
831
+ );
832
+ }
833
+ const timetableId = formatTimetableId({
834
+ routeIds: [route.route_id],
835
+ directionId,
836
+ days: days2
837
+ });
838
+ return {
839
+ timetable_id: timetableId,
804
840
  route_ids: [route.route_id],
805
- direction_id: direction ? direction.direction_id : void 0,
806
- direction_name: direction ? direction.trip_headsign : void 0,
841
+ direction_id: directionId === null ? null : directionId,
842
+ direction_name: tripHeadsign === null ? null : tripHeadsign,
807
843
  routes: [route],
808
844
  include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
809
- service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
845
+ service_ids: serviceIds,
810
846
  service_notes: null,
811
847
  timetable_label: null,
812
848
  start_time: null,
@@ -814,88 +850,83 @@ var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, c
814
850
  orientation: null,
815
851
  timetable_sequence: null,
816
852
  show_trip_continuation: null,
817
- start_date: null,
818
- end_date: null
853
+ start_date: startDate,
854
+ end_date: endDate,
855
+ ...days2
819
856
  };
820
- if (calendars && calendars.length > 0) {
821
- Object.assign(timetable, getDaysFromCalendars(calendars));
822
- timetable.start_date = toGTFSDate(
823
- moment2.min(
824
- calendars.map((calendar) => fromGTFSDate(calendar.start_date))
825
- )
826
- );
827
- timetable.end_date = toGTFSDate(
828
- moment2.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
829
- );
830
- }
831
- timetable.timetable_id = formatTimetableId(timetable);
832
- return convertTimetableToTimetablePage(timetable, config2);
833
857
  };
834
858
  var convertRoutesToTimetablePages = (config2) => {
835
- const db = openDb(config2);
836
859
  const routes = getRoutes();
837
- let whereClause = "";
838
- const whereClauses = [];
839
- if (config2.endDate) {
840
- whereClauses.push(
841
- `start_date <= ${sqlString.escape(toGTFSDate(moment2(config2.endDate)))}`
842
- );
843
- }
844
- if (config2.startDate) {
845
- whereClauses.push(
846
- `end_date >= ${sqlString.escape(toGTFSDate(moment2(config2.startDate)))}`
847
- );
848
- }
849
- if (whereClauses.length > 0) {
850
- whereClause = `WHERE ${whereClauses.join(" AND ")}`;
851
- }
852
- const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
853
- const serviceIds = calendars.map((calendar) => calendar.service_id);
854
- const calendarDates = db.prepare(
855
- `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
856
- ).all();
857
- const timetablePages = routes.map((route) => {
860
+ const timetablePages = [];
861
+ const { calendars, calendarDates } = getCalendarsFromConfig(config2);
862
+ for (const route of routes) {
858
863
  const trips = getTrips(
859
864
  {
860
865
  route_id: route.route_id
861
866
  },
862
867
  ["trip_headsign", "direction_id", "trip_id", "service_id"]
863
868
  );
864
- const directions = uniqBy(trips, (trip) => trip.direction_id);
865
- const dayGroups = groupBy(calendars, calendarToCalendarCode);
869
+ const uniqueTripDirections = orderBy(
870
+ uniqBy2(trips, (trip) => trip.direction_id),
871
+ "direction_id"
872
+ );
873
+ const sortedCalendars = orderBy(calendars, calendarToCalendarCode, "desc");
874
+ const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
866
875
  const calendarDateGroups = groupBy(calendarDates, "service_id");
867
- return directions.map((direction) => [
868
- Object.values(dayGroups).map((calendars2) => {
876
+ const timetables = [];
877
+ for (const uniqueTripDirection of uniqueTripDirections) {
878
+ for (const calendars2 of Object.values(calendarGroups)) {
869
879
  const tripsForCalendars = trips.filter(
870
880
  (trip) => some(calendars2, { service_id: trip.service_id })
871
881
  );
872
882
  if (tripsForCalendars.length > 0) {
873
- return convertRouteToTimetablePage(
874
- route,
875
- direction,
876
- calendars2,
877
- null,
878
- config2
883
+ timetables.push(
884
+ createTimetable({
885
+ route,
886
+ directionId: uniqueTripDirection.direction_id,
887
+ tripHeadsign: uniqueTripDirection.trip_headsign,
888
+ calendars: calendars2
889
+ })
879
890
  );
880
891
  }
881
- }),
882
- Object.values(calendarDateGroups).map((calendarDates2) => {
892
+ }
893
+ for (const calendarDates2 of Object.values(calendarDateGroups)) {
883
894
  const tripsForCalendarDates = trips.filter(
884
895
  (trip) => some(calendarDates2, { service_id: trip.service_id })
885
896
  );
886
897
  if (tripsForCalendarDates.length > 0) {
887
- return convertRouteToTimetablePage(
888
- route,
889
- direction,
890
- null,
891
- calendarDates2,
892
- config2
898
+ timetables.push(
899
+ createTimetable({
900
+ route,
901
+ directionId: uniqueTripDirection.direction_id,
902
+ tripHeadsign: uniqueTripDirection.trip_headsign,
903
+ calendarDates: calendarDates2
904
+ })
893
905
  );
894
906
  }
895
- })
896
- ]);
897
- });
898
- return compact(flattenDeep(timetablePages));
907
+ }
908
+ }
909
+ if (config2.groupTimetablesIntoPages === true) {
910
+ timetablePages.push(
911
+ createTimetablePage({
912
+ timetablePageId: `route_${route.route_short_name ?? route.route_long_name}`,
913
+ timetables,
914
+ config: config2
915
+ })
916
+ );
917
+ } else {
918
+ for (const timetable of timetables) {
919
+ timetablePages.push(
920
+ createTimetablePage({
921
+ timetablePageId: timetable.timetable_id,
922
+ timetables: [timetable],
923
+ config: config2
924
+ })
925
+ );
926
+ }
927
+ }
928
+ }
929
+ return timetablePages;
899
930
  };
900
931
  var generateTripsByFrequencies = (trip, frequencies, config2) => {
901
932
  const formattedFrequencies = frequencies.map(
@@ -918,7 +949,7 @@ var generateTripsByFrequencies = (trip, frequencies, config2) => {
918
949
  return trips;
919
950
  };
920
951
  var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config2) => {
921
- if (config2.showArrivalOnDifference === null) {
952
+ if (config2.showArrivalOnDifference === null || config2.showArrivalOnDifference === void 0) {
922
953
  return stopIds;
923
954
  }
924
955
  for (const trip of timetable.orderedTrips) {
@@ -1040,7 +1071,7 @@ var getStopsForTimetable = (timetable, config2) => {
1040
1071
  }
1041
1072
  return stop;
1042
1073
  });
1043
- if (timetable.showStopCity) {
1074
+ if (config2.showStopCity) {
1044
1075
  const stopAttributes = getStopAttributes({
1045
1076
  stop_id: orderedStopIds
1046
1077
  });
@@ -1055,6 +1086,39 @@ var getStopsForTimetable = (timetable, config2) => {
1055
1086
  }
1056
1087
  return orderedStops;
1057
1088
  };
1089
+ var getCalendarsFromConfig = (config2) => {
1090
+ const db = openDb();
1091
+ let whereClause = "";
1092
+ const whereClauses = [];
1093
+ if (config2.endDate) {
1094
+ if (!moment2(config2.endDate).isValid()) {
1095
+ throw new Error(`Invalid endDate=${config2.endDate} in config.json`);
1096
+ }
1097
+ whereClauses.push(
1098
+ `start_date <= ${sqlString.escape(moment2(config2.endDate).format("YYYYMMDD"))}`
1099
+ );
1100
+ }
1101
+ if (config2.startDate) {
1102
+ if (!moment2(config2.startDate).isValid()) {
1103
+ throw new Error(`Invalid startDate=${config2.startDate} in config.json`);
1104
+ }
1105
+ whereClauses.push(
1106
+ `end_date >= ${sqlString.escape(moment2(config2.startDate).format("YYYYMMDD"))}`
1107
+ );
1108
+ }
1109
+ if (whereClauses.length > 0) {
1110
+ whereClause = `WHERE ${whereClauses.join(" AND ")}`;
1111
+ }
1112
+ const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
1113
+ const serviceIds = calendars.map((calendar) => calendar.service_id);
1114
+ const calendarDates = db.prepare(
1115
+ `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
1116
+ ).all();
1117
+ return {
1118
+ calendars,
1119
+ calendarDates
1120
+ };
1121
+ };
1058
1122
  var getCalendarsFromTimetable = (timetable) => {
1059
1123
  const db = openDb();
1060
1124
  let whereClause = "";
@@ -1204,13 +1268,17 @@ var filterTrips = (timetable) => {
1204
1268
  }
1205
1269
  trip.stoptimes = combinedStoptimes;
1206
1270
  }
1207
- const timetableStopIds = new Set(timetable.stops.map((stop) => stop.stop_id));
1271
+ const timetableStopIds = new Set(
1272
+ timetable.stops.map((stop) => stop.stop_id)
1273
+ );
1208
1274
  for (const trip of filteredTrips) {
1209
1275
  trip.stoptimes = trip.stoptimes.filter(
1210
1276
  (stoptime) => timetableStopIds.has(stoptime.stop_id)
1211
1277
  );
1212
1278
  }
1213
- filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
1279
+ filteredTrips = filteredTrips.filter(
1280
+ (trip) => trip.stoptimes.length > 1
1281
+ );
1214
1282
  return filteredTrips;
1215
1283
  };
1216
1284
  var getTripsForTimetable = (timetable, calendars, config2) => {
@@ -1361,6 +1429,15 @@ var formatTimetables = (timetables, config2) => {
1361
1429
  };
1362
1430
  function getTimetablePagesForAgency(config2) {
1363
1431
  const timetables = mergeTimetablesWithSameId(getTimetables());
1432
+ const routes = getRoutes();
1433
+ const formattedTimetables = timetables.map((timetable) => {
1434
+ return {
1435
+ ...timetable,
1436
+ routes: routes.filter(
1437
+ (route) => timetable.route_ids.includes(route.route_id)
1438
+ )
1439
+ };
1440
+ });
1364
1441
  if (timetables.length === 0) {
1365
1442
  return convertRoutesToTimetablePages(config2);
1366
1443
  }
@@ -1370,68 +1447,33 @@ function getTimetablePagesForAgency(config2) {
1370
1447
  [["timetable_page_id", "ASC"]]
1371
1448
  );
1372
1449
  if (timetablePages.length === 0) {
1373
- return timetables.map(
1374
- (timetable) => convertTimetableToTimetablePage(timetable, config2)
1450
+ return formattedTimetables.map(
1451
+ (timetable) => createTimetablePage({
1452
+ timetablePageId: timetable.timetable_id,
1453
+ timetables: [timetable],
1454
+ config: config2
1455
+ })
1375
1456
  );
1376
1457
  }
1377
- const routes = getRoutes();
1378
1458
  return timetablePages.map((timetablePage) => {
1379
- timetablePage.timetables = sortBy(
1380
- timetables.filter(
1381
- (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1382
- ),
1383
- "timetable_sequence"
1384
- );
1385
- for (const timetable of timetablePage.timetables) {
1386
- timetable.routes = routes.filter(
1387
- (route) => timetable.route_ids.includes(route.route_id)
1388
- );
1389
- }
1390
- return timetablePage;
1459
+ return {
1460
+ ...timetablePage,
1461
+ timetables: sortBy(
1462
+ formattedTimetables.filter(
1463
+ (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1464
+ ),
1465
+ "timetable_sequence"
1466
+ )
1467
+ };
1391
1468
  });
1392
1469
  }
1393
- var getTimetablePageById = (timetablePageId, config2) => {
1394
- const timetablePages = getTimetablePages({
1395
- timetable_page_id: timetablePageId
1396
- });
1397
- const timetables = mergeTimetablesWithSameId(getTimetables());
1398
- if (timetablePages.length > 1) {
1399
- throw new Error(
1400
- `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1401
- );
1402
- }
1403
- if (timetablePages.length === 1) {
1404
- const timetablePage = timetablePages[0];
1405
- timetablePage.timetables = sortBy(
1406
- timetables.filter(
1407
- (timetable) => timetable.timetable_page_id === timetablePageId
1408
- ),
1409
- "timetable_sequence"
1410
- );
1411
- for (const timetable of timetablePage.timetables) {
1412
- timetable.routes = getRoutes({
1413
- route_id: timetable.route_ids
1414
- });
1415
- }
1416
- return timetablePage;
1417
- }
1418
- if (timetables.length > 0) {
1419
- const timetablePageTimetables = timetables.filter(
1420
- (timetable) => timetable.timetable_id === timetablePageId
1421
- );
1422
- if (timetablePageTimetables.length === 0) {
1423
- throw new Error(
1424
- `No timetable found for timetable_page_id=${timetablePageId}`
1425
- );
1426
- }
1427
- return convertTimetableToTimetablePage(timetablePageTimetables[0], config2);
1428
- }
1470
+ var getDataForTimetablePageById = (timetablePageId) => {
1429
1471
  let calendarCode;
1430
1472
  let calendars;
1431
1473
  let calendarDates;
1432
1474
  let serviceId;
1433
1475
  let directionId = "";
1434
- const parts = timetablePageId.split("|");
1476
+ const parts = timetablePageId?.split("|") ?? [];
1435
1477
  if (parts.length > 2) {
1436
1478
  directionId = Number.parseInt(parts.pop(), 10);
1437
1479
  calendarCode = parts.pop();
@@ -1450,13 +1492,13 @@ var getTimetablePageById = (timetablePageId, config2) => {
1450
1492
  },
1451
1493
  ["trip_headsign", "direction_id"]
1452
1494
  );
1453
- const directions = uniqBy(trips, (trip) => trip.direction_id);
1454
- if (directions.length === 0) {
1495
+ const uniqueTripDirections = uniqBy2(trips, (trip) => trip.direction_id);
1496
+ if (uniqueTripDirections.length === 0) {
1455
1497
  throw new Error(
1456
1498
  `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1457
1499
  );
1458
1500
  }
1459
- if (/^[01]*$/.test(calendarCode)) {
1501
+ if (/^[01]*$/.test(calendarCode ?? "")) {
1460
1502
  calendars = getCalendars({
1461
1503
  ...calendarCodeToCalendar(calendarCode)
1462
1504
  });
@@ -1467,13 +1509,129 @@ var getTimetablePageById = (timetablePageId, config2) => {
1467
1509
  service_id: serviceId
1468
1510
  });
1469
1511
  }
1470
- return convertRouteToTimetablePage(
1471
- routes[0],
1472
- directions[0],
1512
+ return {
1473
1513
  calendars,
1474
1514
  calendarDates,
1475
- config2
1476
- );
1515
+ route: routes[0],
1516
+ directionId: uniqueTripDirections[0].direction_id,
1517
+ tripHeadsign: uniqueTripDirections[0].trip_headsign
1518
+ };
1519
+ };
1520
+ var getTimetablePageById = (timetablePageId, config2) => {
1521
+ const timetablePages = getTimetablePages({
1522
+ timetable_page_id: timetablePageId
1523
+ });
1524
+ const timetables = mergeTimetablesWithSameId(getTimetables());
1525
+ if (timetablePages.length > 1) {
1526
+ throw new Error(
1527
+ `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1528
+ );
1529
+ }
1530
+ if (timetablePages.length === 1) {
1531
+ const timetablePage = timetablePages[0];
1532
+ timetablePage.timetables = sortBy(
1533
+ timetables.filter(
1534
+ (timetable2) => timetable2.timetable_page_id === timetablePageId
1535
+ ),
1536
+ "timetable_sequence"
1537
+ );
1538
+ for (const timetable2 of timetablePage.timetables) {
1539
+ timetable2.routes = getRoutes({
1540
+ route_id: timetable2.route_ids
1541
+ });
1542
+ }
1543
+ return timetablePage;
1544
+ }
1545
+ if (timetables.length > 0) {
1546
+ const timetablePageTimetables = timetables.filter(
1547
+ (timetable2) => timetable2.timetable_id === timetablePageId
1548
+ );
1549
+ if (timetablePageTimetables.length === 0) {
1550
+ throw new Error(
1551
+ `No timetable found for timetable_page_id=${timetablePageId}`
1552
+ );
1553
+ }
1554
+ return createTimetablePage({
1555
+ timetablePageId,
1556
+ timetables: [timetablePageTimetables[0]],
1557
+ config: config2
1558
+ });
1559
+ }
1560
+ if (timetablePageId.startsWith("route_")) {
1561
+ const routes = getRoutes({
1562
+ route_short_name: timetablePageId.split("_")[1]
1563
+ });
1564
+ if (routes.length === 0) {
1565
+ throw new Error(
1566
+ `No route found for timetable_page_id=${timetablePageId}`
1567
+ );
1568
+ }
1569
+ const { calendars: calendars2, calendarDates: calendarDates2 } = getCalendarsFromConfig(config2);
1570
+ const trips = getTrips(
1571
+ {
1572
+ route_id: routes[0].route_id
1573
+ },
1574
+ ["trip_headsign", "direction_id", "trip_id", "service_id"]
1575
+ );
1576
+ const uniqueTripDirections = orderBy(
1577
+ uniqBy2(trips, (trip) => trip.direction_id),
1578
+ "direction_id"
1579
+ );
1580
+ const sortedCalendars = orderBy(calendars2, calendarToCalendarCode, "desc");
1581
+ const calendarGroups = groupBy(sortedCalendars, calendarToCalendarCode);
1582
+ const calendarDateGroups = groupBy(calendarDates2, "service_id");
1583
+ const timetables2 = [];
1584
+ for (const uniqueTripDirection of uniqueTripDirections) {
1585
+ for (const calendars3 of Object.values(calendarGroups)) {
1586
+ const tripsForCalendars = trips.filter(
1587
+ (trip) => some(calendars3, { service_id: trip.service_id })
1588
+ );
1589
+ if (tripsForCalendars.length > 0) {
1590
+ timetables2.push(
1591
+ createTimetable({
1592
+ route: routes[0],
1593
+ directionId: uniqueTripDirection.direction_id,
1594
+ tripHeadsign: uniqueTripDirection.trip_headsign,
1595
+ calendars: calendars3
1596
+ })
1597
+ );
1598
+ }
1599
+ }
1600
+ for (const calendarDates3 of Object.values(calendarDateGroups)) {
1601
+ const tripsForCalendarDates = trips.filter(
1602
+ (trip) => some(calendarDates3, { service_id: trip.service_id })
1603
+ );
1604
+ if (tripsForCalendarDates.length > 0) {
1605
+ timetables2.push(
1606
+ createTimetable({
1607
+ route: routes[0],
1608
+ directionId: uniqueTripDirection.direction_id,
1609
+ tripHeadsign: uniqueTripDirection.trip_headsign,
1610
+ calendarDates: calendarDates3
1611
+ })
1612
+ );
1613
+ }
1614
+ }
1615
+ }
1616
+ return createTimetablePage({
1617
+ timetablePageId,
1618
+ timetables: timetables2,
1619
+ config: config2
1620
+ });
1621
+ }
1622
+ const { calendars, calendarDates, route, directionId, tripHeadsign } = getDataForTimetablePageById(timetablePageId);
1623
+ const timetable = createTimetable({
1624
+ route,
1625
+ directionId,
1626
+ tripHeadsign,
1627
+ calendars,
1628
+ calendarDates
1629
+ });
1630
+ return createTimetablePage({
1631
+ timetablePageId,
1632
+ timetables: [timetable],
1633
+ config: config2
1634
+ });
1477
1635
  };
1478
1636
  function setDefaultConfig(initialConfig) {
1479
1637
  const defaults = {
@@ -1494,6 +1652,7 @@ function setDefaultConfig(initialConfig) {
1494
1652
  defaultOrientation: "vertical",
1495
1653
  interpolatedStopSymbol: "\u2022",
1496
1654
  interpolatedStopText: "Estimated time of arrival",
1655
+ groupTimetablesIntoPages: true,
1497
1656
  gtfsToHtmlVersion: version,
1498
1657
  linkStopUrls: false,
1499
1658
  mapStyleUrl: "https://tiles.openfreemap.org/styles/positron",
@@ -1550,12 +1709,6 @@ function getFormattedTimetablePage(timetablePageId, config2) {
1550
1709
  timetablePageId,
1551
1710
  config2
1552
1711
  );
1553
- const timetableRoutes = getRoutes(
1554
- {
1555
- route_id: timetablePage.route_ids
1556
- },
1557
- ["agency_id"]
1558
- );
1559
1712
  const consolidatedTimetables = formatTimetables(
1560
1713
  timetablePage.timetables,
1561
1714
  config2
@@ -1570,8 +1723,8 @@ function getFormattedTimetablePage(timetablePageId, config2) {
1570
1723
  });
1571
1724
  }
1572
1725
  }
1573
- const uniqueRoutes = uniqBy(
1574
- flatMap2(consolidatedTimetables, (timetable) => timetable.routes),
1726
+ const uniqueRoutes = uniqBy2(
1727
+ flatMap(consolidatedTimetables, (timetable) => timetable.routes),
1575
1728
  "route_id"
1576
1729
  );
1577
1730
  const formattedTimetablePage = {
@@ -1582,7 +1735,7 @@ function getFormattedTimetablePage(timetablePageId, config2) {
1582
1735
  consolidatedTimetables.map((timetable) => timetable.dayList)
1583
1736
  ),
1584
1737
  route_ids: uniqueRoutes.map((route) => route.route_id),
1585
- agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
1738
+ agency_ids: uniq(compact(uniqueRoutes.map((route) => route.agency_id))),
1586
1739
  filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`,
1587
1740
  timetable_page_label: timetablePage.timetable_page_label ?? formatListForDisplay(uniqueRoutes.map((route) => formatRouteName(route)))
1588
1741
  };
@@ -1786,12 +1939,14 @@ function formatFrequency(frequency, config2) {
1786
1939
  frequency.headway_min = Math.round(headway.asMinutes());
1787
1940
  return frequency;
1788
1941
  }
1789
- function formatTimetableId(timetable) {
1790
- let timetableId = `${timetable.route_ids.join("_")}|${calendarToCalendarCode(
1791
- timetable
1792
- )}`;
1793
- if (!isNullOrEmpty(timetable.direction_id)) {
1794
- timetableId += `|${timetable.direction_id}`;
1942
+ function formatTimetableId({
1943
+ routeIds,
1944
+ directionId,
1945
+ days: days2
1946
+ }) {
1947
+ let timetableId = `${routeIds.join("_")}|${calendarToCalendarCode(days2)}`;
1948
+ if (!isNullOrEmpty(directionId)) {
1949
+ timetableId += `|${directionId}`;
1795
1950
  }
1796
1951
  return timetableId;
1797
1952
  }
@@ -1935,10 +2090,18 @@ function formatTimetableLabel(timetable) {
1935
2090
  return timetableLabel;
1936
2091
  }
1937
2092
  var formatRouteName = (route) => {
1938
- if (route.route_long_name === null || route.route_long_name === "") {
1939
- return `Route ${route.route_short_name}`;
2093
+ if (route.route_long_name) {
2094
+ return `Route ${route.route_long_name}`;
2095
+ }
2096
+ return route.route_short_name ?? "Unknown";
2097
+ };
2098
+ var formatRouteNameForFilename = (route) => {
2099
+ if (route.route_short_name) {
2100
+ return route.route_short_name.replace(/\s/g, "-");
2101
+ } else if (route.route_long_name) {
2102
+ return route.route_long_name.replace(/\s/g, "-");
1940
2103
  }
1941
- return route.route_long_name ?? "Unknown";
2104
+ return "Unknown";
1942
2105
  };
1943
2106
  var formatListForDisplay = (list) => {
1944
2107
  return new Intl.ListFormat("en-US", {
@@ -1988,7 +2151,7 @@ app.use((req, res, next) => {
1988
2151
  console.log(`${req.method} ${req.url}`);
1989
2152
  next();
1990
2153
  });
1991
- var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify2(config.templatePath);
2154
+ var staticAssetPath = config.templatePath === void 0 ? getPathToViewsFolder(config) : untildify(config.templatePath);
1992
2155
  app.use(express.static(staticAssetPath));
1993
2156
  app.use(
1994
2157
  "/js",
@@ -2018,6 +2181,9 @@ app.get("/", async (req, res, next) => {
2018
2181
  (timetablePage) => timetablePage.timetable_page_id
2019
2182
  );
2020
2183
  for (const timetablePageId of timetablePageIds) {
2184
+ if (!timetablePageId) {
2185
+ continue;
2186
+ }
2021
2187
  const timetablePage = await getFormattedTimetablePage(
2022
2188
  timetablePageId,
2023
2189
  config