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