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 +367 -201
- package/dist/app/index.js.map +1 -1
- package/dist/bin/gtfs-to-html.js +399 -211
- package/dist/bin/gtfs-to-html.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +399 -211
- package/dist/index.js.map +1 -1
- package/package.json +9 -10
- 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,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
|
|
289
|
-
|
|
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 +=
|
|
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, "")}
|
|
297
|
-
return sanitize(filename
|
|
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(
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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.
|
|
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: ">=
|
|
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)
|
|
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 =
|
|
663
|
-
const end =
|
|
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
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
797
|
-
timetable_page_label: timetable.timetable_label,
|
|
798
|
-
timetables: [timetable],
|
|
802
|
+
...timetablePage,
|
|
799
803
|
filename
|
|
800
804
|
};
|
|
801
805
|
};
|
|
802
|
-
var
|
|
803
|
-
|
|
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:
|
|
806
|
-
direction_name:
|
|
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
|
-
|
|
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:
|
|
818
|
-
end_date:
|
|
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
|
-
|
|
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) => {
|
|
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
|
|
865
|
-
|
|
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
|
-
|
|
868
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
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)
|
|
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
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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 (
|
|
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(
|
|
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(
|
|
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
|
|
1374
|
-
(timetable) =>
|
|
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
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
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
|
|
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
|
|
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
|
|
1454
|
-
if (
|
|
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
|
|
1471
|
-
routes[0],
|
|
1472
|
-
directions[0],
|
|
1512
|
+
return {
|
|
1473
1513
|
calendars,
|
|
1474
1514
|
calendarDates,
|
|
1475
|
-
|
|
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 =
|
|
1574
|
-
|
|
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(
|
|
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(
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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
|
|
1939
|
-
return `Route ${route.
|
|
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
|
|
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) :
|
|
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
|