gtfs-to-html 2.8.0 → 2.9.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/README.md +1 -1
- package/dist/app/index.d.ts +2 -0
- package/dist/app/index.js +1863 -0
- package/dist/app/index.js.map +1 -0
- package/dist/bin/gtfs-to-html.d.ts +1 -0
- package/dist/bin/gtfs-to-html.js +2240 -0
- package/dist/bin/gtfs-to-html.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +2183 -0
- package/dist/index.js.map +1 -0
- package/package.json +13 -10
|
@@ -0,0 +1,1863 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/app/index.ts
|
|
8
|
+
import path2 from "node:path";
|
|
9
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { map } from "lodash-es";
|
|
12
|
+
import yargs from "yargs";
|
|
13
|
+
import { hideBin } from "yargs/helpers";
|
|
14
|
+
import { openDb as openDb2 } from "gtfs";
|
|
15
|
+
import express from "express";
|
|
16
|
+
import logger from "morgan";
|
|
17
|
+
import untildify2 from "untildify";
|
|
18
|
+
|
|
19
|
+
// src/lib/formatters.ts
|
|
20
|
+
import {
|
|
21
|
+
clone,
|
|
22
|
+
find as find2,
|
|
23
|
+
first as first2,
|
|
24
|
+
groupBy as groupBy2,
|
|
25
|
+
last as last2,
|
|
26
|
+
omit,
|
|
27
|
+
sortBy as sortBy2,
|
|
28
|
+
zipObject
|
|
29
|
+
} from "lodash-es";
|
|
30
|
+
import moment3 from "moment";
|
|
31
|
+
|
|
32
|
+
// src/lib/time-utils.ts
|
|
33
|
+
import moment from "moment";
|
|
34
|
+
function fromGTFSTime(timeString) {
|
|
35
|
+
const duration = moment.duration(timeString);
|
|
36
|
+
return moment({
|
|
37
|
+
hour: duration.hours(),
|
|
38
|
+
minute: duration.minutes(),
|
|
39
|
+
second: duration.seconds()
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function toGTFSTime(time) {
|
|
43
|
+
return time.format("HH:mm:ss");
|
|
44
|
+
}
|
|
45
|
+
function fromGTFSDate(gtfsDate) {
|
|
46
|
+
return moment(gtfsDate, "YYYYMMDD");
|
|
47
|
+
}
|
|
48
|
+
function toGTFSDate(date) {
|
|
49
|
+
return moment(date).format("YYYYMMDD");
|
|
50
|
+
}
|
|
51
|
+
function calendarToCalendarCode(c) {
|
|
52
|
+
if (c.service_id) {
|
|
53
|
+
return c.service_id;
|
|
54
|
+
}
|
|
55
|
+
return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
|
|
56
|
+
}
|
|
57
|
+
function calendarCodeToCalendar(code) {
|
|
58
|
+
const days2 = [
|
|
59
|
+
"monday",
|
|
60
|
+
"tuesday",
|
|
61
|
+
"wednesday",
|
|
62
|
+
"thursday",
|
|
63
|
+
"friday",
|
|
64
|
+
"saturday",
|
|
65
|
+
"sunday"
|
|
66
|
+
];
|
|
67
|
+
const calendar = {};
|
|
68
|
+
for (const [index, day] of days2.entries()) {
|
|
69
|
+
calendar[day] = code[index];
|
|
70
|
+
}
|
|
71
|
+
return calendar;
|
|
72
|
+
}
|
|
73
|
+
function secondsAfterMidnight(timeString) {
|
|
74
|
+
return moment.duration(timeString).asSeconds();
|
|
75
|
+
}
|
|
76
|
+
function minutesAfterMidnight(timeString) {
|
|
77
|
+
return moment.duration(timeString).asMinutes();
|
|
78
|
+
}
|
|
79
|
+
function updateTimeByOffset(timeString, offsetSeconds) {
|
|
80
|
+
const newTime = fromGTFSTime(timeString);
|
|
81
|
+
return toGTFSTime(newTime.add(offsetSeconds, "seconds"));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/lib/utils.ts
|
|
85
|
+
import {
|
|
86
|
+
cloneDeep,
|
|
87
|
+
compact,
|
|
88
|
+
countBy,
|
|
89
|
+
entries,
|
|
90
|
+
every as every2,
|
|
91
|
+
find,
|
|
92
|
+
findLast,
|
|
93
|
+
first,
|
|
94
|
+
flatMap as flatMap2,
|
|
95
|
+
flattenDeep,
|
|
96
|
+
flow,
|
|
97
|
+
isEqual,
|
|
98
|
+
groupBy,
|
|
99
|
+
head,
|
|
100
|
+
last,
|
|
101
|
+
maxBy,
|
|
102
|
+
partialRight,
|
|
103
|
+
reduce,
|
|
104
|
+
size,
|
|
105
|
+
some,
|
|
106
|
+
sortBy,
|
|
107
|
+
uniq,
|
|
108
|
+
uniqBy,
|
|
109
|
+
zip
|
|
110
|
+
} from "lodash-es";
|
|
111
|
+
import {
|
|
112
|
+
getCalendarDates,
|
|
113
|
+
getTrips,
|
|
114
|
+
getTimetableNotesReferences,
|
|
115
|
+
getTimetableNotes,
|
|
116
|
+
getRoutes,
|
|
117
|
+
getCalendars,
|
|
118
|
+
getTimetableStopOrders,
|
|
119
|
+
getStops,
|
|
120
|
+
getStopAttributes,
|
|
121
|
+
getStoptimes,
|
|
122
|
+
getFrequencies,
|
|
123
|
+
getTimetables,
|
|
124
|
+
getTimetablePages,
|
|
125
|
+
getAgencies,
|
|
126
|
+
openDb
|
|
127
|
+
} from "gtfs";
|
|
128
|
+
import { stringify } from "csv-stringify";
|
|
129
|
+
import moment2 from "moment";
|
|
130
|
+
import sqlString from "sqlstring";
|
|
131
|
+
import toposort from "toposort";
|
|
132
|
+
|
|
133
|
+
// src/lib/file-utils.ts
|
|
134
|
+
import path from "node:path";
|
|
135
|
+
import { fileURLToPath } from "node:url";
|
|
136
|
+
import _ from "lodash-es";
|
|
137
|
+
import archiver from "archiver";
|
|
138
|
+
import beautify from "js-beautify";
|
|
139
|
+
import { renderFile } from "pug";
|
|
140
|
+
import puppeteer from "puppeteer";
|
|
141
|
+
import sanitize from "sanitize-filename";
|
|
142
|
+
import untildify from "untildify";
|
|
143
|
+
import insane from "insane";
|
|
144
|
+
import { marked } from "marked";
|
|
145
|
+
|
|
146
|
+
// src/lib/template-functions.ts
|
|
147
|
+
var template_functions_exports = {};
|
|
148
|
+
__export(template_functions_exports, {
|
|
149
|
+
formatHtmlId: () => formatHtmlId,
|
|
150
|
+
formatTripName: () => formatTripName,
|
|
151
|
+
formatTripNameForCSV: () => formatTripNameForCSV,
|
|
152
|
+
getNotesForStop: () => getNotesForStop,
|
|
153
|
+
getNotesForStoptime: () => getNotesForStoptime,
|
|
154
|
+
getNotesForTimetableLabel: () => getNotesForTimetableLabel,
|
|
155
|
+
getNotesForTrip: () => getNotesForTrip,
|
|
156
|
+
hasNotesOrNotices: () => hasNotesOrNotices,
|
|
157
|
+
timetableHasDifferentDays: () => timetableHasDifferentDays,
|
|
158
|
+
timetablePageHasDifferentDays: () => timetablePageHasDifferentDays,
|
|
159
|
+
timetablePageHasDifferentLabels: () => timetablePageHasDifferentLabels
|
|
160
|
+
});
|
|
161
|
+
import { every } from "lodash-es";
|
|
162
|
+
function formatHtmlId(id) {
|
|
163
|
+
return id.replace(/([^\w[\]{}.:-])\s?/g, "");
|
|
164
|
+
}
|
|
165
|
+
function timetableHasDifferentDays(timetable) {
|
|
166
|
+
return !every(timetable.orderedTrips, (trip, idx) => {
|
|
167
|
+
if (idx === 0) {
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return trip.dayList === timetable.orderedTrips[idx - 1].dayList;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function timetablePageHasDifferentDays(timetablePage) {
|
|
174
|
+
return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
|
|
175
|
+
if (idx === 0) {
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return timetable.dayListLong === timetablePage.consolidatedTimetables[idx - 1].dayListLong;
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function timetablePageHasDifferentLabels(timetablePage) {
|
|
182
|
+
return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
|
|
183
|
+
if (idx === 0) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
return timetable.timetable_label === timetablePage.consolidatedTimetables[idx - 1].timetable_label;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function hasNotesOrNotices(timetable) {
|
|
190
|
+
return timetable.requestPickupSymbolUsed || timetable.noPickupSymbolUsed || timetable.requestDropoffSymbolUsed || timetable.noDropoffSymbolUsed || timetable.noServiceSymbolUsed || timetable.interpolatedStopSymbolUsed || timetable.notes.length > 0;
|
|
191
|
+
}
|
|
192
|
+
function getNotesForTimetableLabel(notes) {
|
|
193
|
+
return notes.filter((note) => !note.stop_id && !note.trip_id);
|
|
194
|
+
}
|
|
195
|
+
function getNotesForStop(notes, stop) {
|
|
196
|
+
return notes.filter((note) => {
|
|
197
|
+
if (note.trip_id) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
if (note.stop_sequence && !stop.trips.some((trip) => trip.stop_sequence === note.stop_sequence)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return note.stop_id === stop.stop_id;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
function getNotesForTrip(notes, trip) {
|
|
207
|
+
return notes.filter((note) => {
|
|
208
|
+
if (note.stop_id) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
return note.trip_id === trip.trip_id;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
function getNotesForStoptime(notes, stoptime) {
|
|
215
|
+
return notes.filter((note) => {
|
|
216
|
+
if (!note.trip_id && note.stop_id === stoptime.stop_id && note.show_on_stoptime === 1) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (!note.stop_id && note.trip_id === stoptime.trip_id && note.show_on_stoptime === 1) {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
return note.trip_id === stoptime.trip_id && note.stop_id === stoptime.stop_id;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function formatTripName(trip, index, timetable) {
|
|
226
|
+
let tripName;
|
|
227
|
+
if (timetable.routes.length > 1) {
|
|
228
|
+
tripName = trip.route_short_name;
|
|
229
|
+
} else if (timetable.orientation === "horizontal") {
|
|
230
|
+
if (trip.trip_short_name) {
|
|
231
|
+
tripName = trip.trip_short_name;
|
|
232
|
+
} else {
|
|
233
|
+
tripName = `Run #${index + 1}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (timetableHasDifferentDays(timetable)) {
|
|
237
|
+
tripName += ` ${trip.dayList}`;
|
|
238
|
+
}
|
|
239
|
+
return tripName;
|
|
240
|
+
}
|
|
241
|
+
function formatTripNameForCSV(trip, timetable) {
|
|
242
|
+
let tripName = "";
|
|
243
|
+
if (timetable.routes.length > 1) {
|
|
244
|
+
tripName += `${trip.route_short_name} - `;
|
|
245
|
+
}
|
|
246
|
+
if (trip.trip_short_name) {
|
|
247
|
+
tripName += trip.trip_short_name;
|
|
248
|
+
} else {
|
|
249
|
+
tripName += trip.trip_id;
|
|
250
|
+
}
|
|
251
|
+
if (trip.trip_headsign) {
|
|
252
|
+
tripName += ` - ${trip.trip_headsign}`;
|
|
253
|
+
}
|
|
254
|
+
if (timetableHasDifferentDays(timetable)) {
|
|
255
|
+
tripName += ` - ${trip.dayList}`;
|
|
256
|
+
}
|
|
257
|
+
return tripName;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/lib/file-utils.ts
|
|
261
|
+
function getTemplatePath(templateFileName, config2) {
|
|
262
|
+
let fullTemplateFileName = templateFileName;
|
|
263
|
+
if (config2.noHead !== true) {
|
|
264
|
+
fullTemplateFileName += "_full";
|
|
265
|
+
}
|
|
266
|
+
const templatePath = config2.templatePath === void 0 ? path.join(fileURLToPath(import.meta.url), "../../../views/default") : untildify(config2.templatePath);
|
|
267
|
+
return path.join(templatePath, `${fullTemplateFileName}.pug`);
|
|
268
|
+
}
|
|
269
|
+
function generateFileName(timetable, config2, extension = "html") {
|
|
270
|
+
let filename = timetable.timetable_id;
|
|
271
|
+
for (const route of timetable.routes) {
|
|
272
|
+
filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, "-")}` : `_${route.route_short_name.replace(/\s/g, "-")}`;
|
|
273
|
+
}
|
|
274
|
+
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
275
|
+
filename += `_${timetable.direction_id}`;
|
|
276
|
+
}
|
|
277
|
+
filename += `_${formatDays(timetable, config2).replace(/\s/g, "")}.${extension}`;
|
|
278
|
+
return sanitize(filename).toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
async function renderTemplate(templateFileName, templateVars, config2) {
|
|
281
|
+
const templatePath = getTemplatePath(templateFileName, config2);
|
|
282
|
+
const html = await renderFile(templatePath, {
|
|
283
|
+
_,
|
|
284
|
+
md: (text) => insane(marked.parseInline(text)),
|
|
285
|
+
...template_functions_exports,
|
|
286
|
+
formatRouteColor,
|
|
287
|
+
formatRouteTextColor,
|
|
288
|
+
...templateVars
|
|
289
|
+
});
|
|
290
|
+
if (config2.beautify === true) {
|
|
291
|
+
return beautify.html_beautify(html, {
|
|
292
|
+
indent_size: 2
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return html;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/lib/geojson-utils.ts
|
|
299
|
+
import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
|
|
300
|
+
import { flatMap } from "lodash-es";
|
|
301
|
+
import simplify from "@turf/simplify";
|
|
302
|
+
import { featureCollection, round } from "@turf/helpers";
|
|
303
|
+
var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
|
|
304
|
+
var truncateGeoJSONDecimals = (geojson, config2) => {
|
|
305
|
+
for (const feature of geojson.features) {
|
|
306
|
+
if (feature.geometry.coordinates) {
|
|
307
|
+
if (feature.geometry.type.toLowerCase() === "point") {
|
|
308
|
+
feature.geometry.coordinates = feature.geometry.coordinates.map(
|
|
309
|
+
(number) => round(number, config2.coordinatePrecision)
|
|
310
|
+
);
|
|
311
|
+
} else if (feature.geometry.type.toLowerCase() === "linestring") {
|
|
312
|
+
feature.geometry.coordinates = feature.geometry.coordinates.map(
|
|
313
|
+
(coordinate) => coordinate.map(
|
|
314
|
+
(number) => round(number, config2.coordinatePrecision)
|
|
315
|
+
)
|
|
316
|
+
);
|
|
317
|
+
} else if (feature.geometry.type.toLowerCase() === "multilinestring") {
|
|
318
|
+
feature.geometry.coordinates = feature.geometry.coordinates.map(
|
|
319
|
+
(linestring) => linestring.map(
|
|
320
|
+
(coordinate) => coordinate.map(
|
|
321
|
+
(number) => round(number, config2.coordinatePrecision)
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return geojson;
|
|
329
|
+
};
|
|
330
|
+
var simplifyGeoJSON = (geojson, config2) => {
|
|
331
|
+
try {
|
|
332
|
+
const simplifiedGeojson = simplify(geojson, {
|
|
333
|
+
tolerance: 1 / 10 ** config2.coordinatePrecision,
|
|
334
|
+
highQuality: true
|
|
335
|
+
});
|
|
336
|
+
return truncateGeoJSONDecimals(simplifiedGeojson, config2);
|
|
337
|
+
} catch {
|
|
338
|
+
config2.logWarning("Unable to simplify geojson");
|
|
339
|
+
return truncateGeoJSONDecimals(geojson, config2);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
function getTimetableGeoJSON(timetable, config2) {
|
|
343
|
+
const shapesGeojsons = timetable.route_ids.map(
|
|
344
|
+
(routeId) => getShapesAsGeoJSON({
|
|
345
|
+
route_id: routeId,
|
|
346
|
+
direction_id: timetable.direction_id,
|
|
347
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
|
|
348
|
+
})
|
|
349
|
+
);
|
|
350
|
+
const stopsGeojsons = timetable.route_ids.map(
|
|
351
|
+
(routeId) => getStopsAsGeoJSON({
|
|
352
|
+
route_id: routeId,
|
|
353
|
+
direction_id: timetable.direction_id,
|
|
354
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
|
|
355
|
+
})
|
|
356
|
+
);
|
|
357
|
+
const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
|
|
358
|
+
return simplifyGeoJSON(geojson, config2);
|
|
359
|
+
}
|
|
360
|
+
function getAgencyGeoJSON(config2) {
|
|
361
|
+
const shapesGeojsons = getShapesAsGeoJSON();
|
|
362
|
+
const stopsGeojsons = getStopsAsGeoJSON();
|
|
363
|
+
const geojson = mergeGeojson(shapesGeojsons, stopsGeojsons);
|
|
364
|
+
return simplifyGeoJSON(geojson, config2);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// package.json
|
|
368
|
+
var version = "2.8.1";
|
|
369
|
+
|
|
370
|
+
// src/lib/utils.ts
|
|
371
|
+
var isTimepoint = (stoptime) => {
|
|
372
|
+
if (isNullOrEmpty(stoptime.timepoint)) {
|
|
373
|
+
return !isNullOrEmpty(stoptime.arrival_time) && !isNullOrEmpty(stoptime.departure_time);
|
|
374
|
+
}
|
|
375
|
+
return stoptime.timepoint === 1;
|
|
376
|
+
};
|
|
377
|
+
var getLongestTripStoptimes = (trips, config2) => {
|
|
378
|
+
const filteredTripStoptimes = trips.map(
|
|
379
|
+
(trip) => trip.stoptimes.filter((stoptime) => {
|
|
380
|
+
if (config2.showOnlyTimepoint === true) {
|
|
381
|
+
return isTimepoint(stoptime);
|
|
382
|
+
}
|
|
383
|
+
return true;
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
return maxBy(filteredTripStoptimes, (stoptimes) => size(stoptimes));
|
|
387
|
+
};
|
|
388
|
+
var findCommonStopId = (trips, config2) => {
|
|
389
|
+
const longestTripStoptimes = getLongestTripStoptimes(trips, config2);
|
|
390
|
+
if (!longestTripStoptimes) {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
|
|
394
|
+
if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
if (isNullOrEmpty(stoptime.arrival_time)) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
return every2(
|
|
401
|
+
trips,
|
|
402
|
+
(trip) => trip.stoptimes.find(
|
|
403
|
+
(tripStoptime) => tripStoptime.stop_id === stoptime.stop_id && tripStoptime.arrival_time !== null
|
|
404
|
+
)
|
|
405
|
+
);
|
|
406
|
+
});
|
|
407
|
+
return commonStoptime ? commonStoptime.stop_id : null;
|
|
408
|
+
};
|
|
409
|
+
var deduplicateTrips = (trips, commonStopId) => {
|
|
410
|
+
const deduplicatedTrips = [];
|
|
411
|
+
for (const trip of trips) {
|
|
412
|
+
if (deduplicatedTrips.length === 0 || trip.stoptimes.length === 0) {
|
|
413
|
+
deduplicatedTrips.push(trip);
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const stoptimes = trip.stoptimes.map((stoptime) => stoptime.departure_time);
|
|
417
|
+
const selectedStoptime = commonStopId ? find(trip.stoptimes, {
|
|
418
|
+
stop_id: commonStopId
|
|
419
|
+
}) : trip.stoptimes[0];
|
|
420
|
+
const similarTrips = deduplicatedTrips.filter((trip2) => {
|
|
421
|
+
const stoptime = find(trip2.stoptimes, {
|
|
422
|
+
stop_id: selectedStoptime.stop_id
|
|
423
|
+
});
|
|
424
|
+
if (!stoptime) {
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
return stoptime.departure_time === selectedStoptime.departure_time;
|
|
428
|
+
});
|
|
429
|
+
const tripIsUnique = every2(similarTrips, (similarTrip) => {
|
|
430
|
+
const similarTripStoptimes = similarTrip.stoptimes.map(
|
|
431
|
+
(stoptime) => stoptime.departure_time
|
|
432
|
+
);
|
|
433
|
+
return !isEqual(stoptimes, similarTripStoptimes);
|
|
434
|
+
});
|
|
435
|
+
if (tripIsUnique) {
|
|
436
|
+
deduplicatedTrips.push(trip);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return deduplicatedTrips;
|
|
440
|
+
};
|
|
441
|
+
var sortTrips = (trips, config2) => {
|
|
442
|
+
let sortedTrips;
|
|
443
|
+
let commonStopId;
|
|
444
|
+
if (config2.sortingAlgorithm === "common") {
|
|
445
|
+
commonStopId = findCommonStopId(trips, config2);
|
|
446
|
+
if (commonStopId) {
|
|
447
|
+
sortedTrips = sortTripsByStoptimeAtStop(trips, commonStopId);
|
|
448
|
+
} else {
|
|
449
|
+
sortedTrips = sortTrips(trips, {
|
|
450
|
+
...config2,
|
|
451
|
+
sortingAlgorithm: "beginning"
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
} else if (config2.sortingAlgorithm === "beginning") {
|
|
455
|
+
for (const trip of trips) {
|
|
456
|
+
if (trip.stoptimes.length === 0) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
trip.firstStoptime = timeToSeconds(first(trip.stoptimes).departure_time);
|
|
460
|
+
trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
|
|
461
|
+
}
|
|
462
|
+
sortedTrips = sortBy(
|
|
463
|
+
trips,
|
|
464
|
+
["firstStoptime", "lastStoptime"],
|
|
465
|
+
["asc", "asc"]
|
|
466
|
+
);
|
|
467
|
+
} else if (config2.sortingAlgorithm === "end") {
|
|
468
|
+
for (const trip of trips) {
|
|
469
|
+
if (trip.stoptimes.length === 0) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
trip.firstStoptime = timeToSeconds(first(trip.stoptimes).departure_time);
|
|
473
|
+
trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
|
|
474
|
+
}
|
|
475
|
+
sortedTrips = sortBy(
|
|
476
|
+
trips,
|
|
477
|
+
["lastStoptime", "firstStoptime"],
|
|
478
|
+
["asc", "asc"]
|
|
479
|
+
);
|
|
480
|
+
} else if (config2.sortingAlgorithm === "first") {
|
|
481
|
+
const longestTripStoptimes = getLongestTripStoptimes(trips, config2);
|
|
482
|
+
const firstStopId = first(longestTripStoptimes).stop_id;
|
|
483
|
+
sortedTrips = sortTripsByStoptimeAtStop(trips, firstStopId);
|
|
484
|
+
} else if (config2.sortingAlgorithm === "last") {
|
|
485
|
+
const longestTripStoptimes = getLongestTripStoptimes(trips, config2);
|
|
486
|
+
const lastStopId = last(longestTripStoptimes).stop_id;
|
|
487
|
+
sortedTrips = sortTripsByStoptimeAtStop(trips, lastStopId);
|
|
488
|
+
}
|
|
489
|
+
return deduplicateTrips(sortedTrips, commonStopId);
|
|
490
|
+
};
|
|
491
|
+
var sortTripsByStoptimeAtStop = (trips, stopId) => sortBy(trips, (trip) => {
|
|
492
|
+
const stoptime = find(trip.stoptimes, { stop_id: stopId });
|
|
493
|
+
return stoptime ? timeToSeconds(stoptime.departure_time) : void 0;
|
|
494
|
+
});
|
|
495
|
+
var getCalendarDatesForTimetable = (timetable, config2) => {
|
|
496
|
+
const calendarDates = getCalendarDates(
|
|
497
|
+
{
|
|
498
|
+
service_id: timetable.service_ids
|
|
499
|
+
},
|
|
500
|
+
[],
|
|
501
|
+
[["date", "ASC"]]
|
|
502
|
+
);
|
|
503
|
+
const start = fromGTFSDate(timetable.start_date);
|
|
504
|
+
const end = fromGTFSDate(timetable.end_date);
|
|
505
|
+
const excludedDates = [];
|
|
506
|
+
const includedDates = [];
|
|
507
|
+
for (const calendarDate of calendarDates) {
|
|
508
|
+
if (moment2(calendarDate.date, "YYYYMMDD").isBetween(start, end)) {
|
|
509
|
+
if (calendarDate.exception_type === 1) {
|
|
510
|
+
includedDates.push(formatDate(calendarDate, config2.dateFormat));
|
|
511
|
+
} else if (calendarDate.exception_type === 2) {
|
|
512
|
+
excludedDates.push(formatDate(calendarDate, config2.dateFormat));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const includedAndExcludedDates = excludedDates.filter(
|
|
517
|
+
(date) => includedDates.includes(date)
|
|
518
|
+
);
|
|
519
|
+
return {
|
|
520
|
+
excludedDates: excludedDates.filter(
|
|
521
|
+
(date) => !includedAndExcludedDates.includes(date)
|
|
522
|
+
),
|
|
523
|
+
includedDates: includedDates.filter(
|
|
524
|
+
(date) => !includedAndExcludedDates.includes(date)
|
|
525
|
+
)
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
var getDaysFromCalendars = (calendars) => {
|
|
529
|
+
const days2 = {
|
|
530
|
+
monday: 0,
|
|
531
|
+
tuesday: 0,
|
|
532
|
+
wednesday: 0,
|
|
533
|
+
thursday: 0,
|
|
534
|
+
friday: 0,
|
|
535
|
+
saturday: 0,
|
|
536
|
+
sunday: 0
|
|
537
|
+
};
|
|
538
|
+
for (const calendar of calendars) {
|
|
539
|
+
for (const [day, value] of Object.entries(days2)) {
|
|
540
|
+
days2[day] = value | calendar[day];
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return days2;
|
|
544
|
+
};
|
|
545
|
+
var getDirectionHeadsignFromTimetable = (timetable) => {
|
|
546
|
+
const trips = getTrips(
|
|
547
|
+
{
|
|
548
|
+
direction_id: timetable.direction_id,
|
|
549
|
+
route_id: timetable.route_ids
|
|
550
|
+
},
|
|
551
|
+
["trip_headsign"]
|
|
552
|
+
);
|
|
553
|
+
if (trips.length === 0) {
|
|
554
|
+
return "";
|
|
555
|
+
}
|
|
556
|
+
const mostCommonHeadsign = flow(
|
|
557
|
+
countBy,
|
|
558
|
+
entries,
|
|
559
|
+
partialRight(maxBy, last),
|
|
560
|
+
head
|
|
561
|
+
)(compact(trips.map((trip) => trip.trip_headsign)));
|
|
562
|
+
return mostCommonHeadsign;
|
|
563
|
+
};
|
|
564
|
+
var getTimetableNotesForTimetable = (timetable, config2) => {
|
|
565
|
+
const noteReferences = [
|
|
566
|
+
// Get all notes for this timetable.
|
|
567
|
+
...getTimetableNotesReferences({
|
|
568
|
+
timetable_id: timetable.timetable_id
|
|
569
|
+
}),
|
|
570
|
+
// Get all notes for this route.
|
|
571
|
+
...getTimetableNotesReferences({
|
|
572
|
+
route_id: timetable.routes.map((route) => route.route_id),
|
|
573
|
+
timetable_id: null
|
|
574
|
+
}),
|
|
575
|
+
// Get all notes for all trips in this timetable.
|
|
576
|
+
...getTimetableNotesReferences({
|
|
577
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
|
|
578
|
+
}),
|
|
579
|
+
// Get all notes for all stops in this timetable.
|
|
580
|
+
...getTimetableNotesReferences({
|
|
581
|
+
stop_id: timetable.stops.map((stop) => stop.stop_id),
|
|
582
|
+
trip_id: null,
|
|
583
|
+
route_id: null,
|
|
584
|
+
timetable_id: null
|
|
585
|
+
})
|
|
586
|
+
];
|
|
587
|
+
const usedNoteReferences = [];
|
|
588
|
+
for (const noteReference of noteReferences) {
|
|
589
|
+
if (noteReference.stop_sequence === "" || noteReference.stop_sequence === null) {
|
|
590
|
+
usedNoteReferences.push(noteReference);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (noteReference.stop_id === "" || noteReference.stop_id === null) {
|
|
594
|
+
config2.logWarning(
|
|
595
|
+
`Timetable Note Reference for note_id=${noteReference.note_id} has a \`stop_sequence\` but no \`stop_id\` - ignoring`
|
|
596
|
+
);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const stop = timetable.stops.find(
|
|
600
|
+
(stop2) => stop2.stop_id === noteReference.stop_id
|
|
601
|
+
);
|
|
602
|
+
if (!stop) {
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const tripWithMatchingStopSequence = stop.trips.find(
|
|
606
|
+
(trip) => trip.stop_sequence === noteReference.stop_sequence
|
|
607
|
+
);
|
|
608
|
+
if (tripWithMatchingStopSequence) {
|
|
609
|
+
usedNoteReferences.push(noteReference);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const notes = getTimetableNotes({
|
|
613
|
+
note_id: usedNoteReferences.map((noteReference) => noteReference.note_id)
|
|
614
|
+
});
|
|
615
|
+
const symbols = "abcdefghijklmnopqrstuvwxyz".split("");
|
|
616
|
+
let symbolIndex = 0;
|
|
617
|
+
for (const note of notes) {
|
|
618
|
+
if (note.symbol === "" || note.symbol === null) {
|
|
619
|
+
note.symbol = symbolIndex < symbols.length - 1 ? symbols[symbolIndex] : symbolIndex - symbols.length;
|
|
620
|
+
symbolIndex += 1;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const formattedNotes = usedNoteReferences.map((noteReference) => ({
|
|
624
|
+
...noteReference,
|
|
625
|
+
...notes.find((note) => note.note_id === noteReference.note_id)
|
|
626
|
+
}));
|
|
627
|
+
return sortBy(formattedNotes, "symbol");
|
|
628
|
+
};
|
|
629
|
+
var convertTimetableToTimetablePage = (timetable, config2) => {
|
|
630
|
+
if (!timetable.routes) {
|
|
631
|
+
timetable.routes = getRoutes({
|
|
632
|
+
route_id: timetable.route_ids
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const filename = generateFileName(timetable, config2, "html");
|
|
636
|
+
return {
|
|
637
|
+
timetable_page_id: timetable.timetable_id,
|
|
638
|
+
timetable_page_label: timetable.timetable_label,
|
|
639
|
+
timetables: [timetable],
|
|
640
|
+
filename
|
|
641
|
+
};
|
|
642
|
+
};
|
|
643
|
+
var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config2) => {
|
|
644
|
+
const timetable = {
|
|
645
|
+
route_ids: [route.route_id],
|
|
646
|
+
direction_id: direction ? direction.direction_id : void 0,
|
|
647
|
+
direction_name: direction ? direction.trip_headsign : void 0,
|
|
648
|
+
routes: [route],
|
|
649
|
+
include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
|
|
650
|
+
service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
|
|
651
|
+
service_notes: null,
|
|
652
|
+
timetable_label: null,
|
|
653
|
+
start_time: null,
|
|
654
|
+
end_time: null,
|
|
655
|
+
orientation: null,
|
|
656
|
+
timetable_sequence: null,
|
|
657
|
+
show_trip_continuation: null,
|
|
658
|
+
start_date: null,
|
|
659
|
+
end_date: null
|
|
660
|
+
};
|
|
661
|
+
if (calendars && calendars.length > 0) {
|
|
662
|
+
Object.assign(timetable, getDaysFromCalendars(calendars));
|
|
663
|
+
timetable.start_date = toGTFSDate(
|
|
664
|
+
moment2.min(
|
|
665
|
+
calendars.map((calendar) => fromGTFSDate(calendar.start_date))
|
|
666
|
+
)
|
|
667
|
+
);
|
|
668
|
+
timetable.end_date = toGTFSDate(
|
|
669
|
+
moment2.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
timetable.timetable_id = formatTimetableId(timetable);
|
|
673
|
+
return convertTimetableToTimetablePage(timetable, config2);
|
|
674
|
+
};
|
|
675
|
+
var convertRoutesToTimetablePages = (config2) => {
|
|
676
|
+
const db = openDb(config2);
|
|
677
|
+
const routes = getRoutes();
|
|
678
|
+
let whereClause = "";
|
|
679
|
+
const whereClauses = [];
|
|
680
|
+
if (config2.endDate) {
|
|
681
|
+
whereClauses.push(
|
|
682
|
+
`start_date <= ${sqlString.escape(toGTFSDate(moment2(config2.endDate)))}`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
if (config2.startDate) {
|
|
686
|
+
whereClauses.push(
|
|
687
|
+
`end_date >= ${sqlString.escape(toGTFSDate(moment2(config2.startDate)))}`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (whereClauses.length > 0) {
|
|
691
|
+
whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
692
|
+
}
|
|
693
|
+
const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
|
|
694
|
+
const serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
695
|
+
const calendarDates = db.prepare(
|
|
696
|
+
`SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
|
|
697
|
+
).all();
|
|
698
|
+
const timetablePages = routes.map((route) => {
|
|
699
|
+
const trips = getTrips(
|
|
700
|
+
{
|
|
701
|
+
route_id: route.route_id
|
|
702
|
+
},
|
|
703
|
+
["trip_headsign", "direction_id", "trip_id", "service_id"]
|
|
704
|
+
);
|
|
705
|
+
const directions = uniqBy(trips, (trip) => trip.direction_id);
|
|
706
|
+
const dayGroups = groupBy(calendars, calendarToCalendarCode);
|
|
707
|
+
const calendarDateGroups = groupBy(calendarDates, "service_id");
|
|
708
|
+
return directions.map((direction) => [
|
|
709
|
+
Object.values(dayGroups).map((calendars2) => {
|
|
710
|
+
const tripsForCalendars = trips.filter(
|
|
711
|
+
(trip) => some(calendars2, { service_id: trip.service_id })
|
|
712
|
+
);
|
|
713
|
+
if (tripsForCalendars.length > 0) {
|
|
714
|
+
return convertRouteToTimetablePage(
|
|
715
|
+
route,
|
|
716
|
+
direction,
|
|
717
|
+
calendars2,
|
|
718
|
+
null,
|
|
719
|
+
config2
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
}),
|
|
723
|
+
Object.values(calendarDateGroups).map((calendarDates2) => {
|
|
724
|
+
const tripsForCalendarDates = trips.filter(
|
|
725
|
+
(trip) => some(calendarDates2, { service_id: trip.service_id })
|
|
726
|
+
);
|
|
727
|
+
if (tripsForCalendarDates.length > 0) {
|
|
728
|
+
return convertRouteToTimetablePage(
|
|
729
|
+
route,
|
|
730
|
+
direction,
|
|
731
|
+
null,
|
|
732
|
+
calendarDates2,
|
|
733
|
+
config2
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
]);
|
|
738
|
+
});
|
|
739
|
+
return compact(flattenDeep(timetablePages));
|
|
740
|
+
};
|
|
741
|
+
var generateTripsByFrequencies = (trip, frequencies, config2) => {
|
|
742
|
+
const formattedFrequencies = frequencies.map(
|
|
743
|
+
(frequency) => formatFrequency(frequency, config2)
|
|
744
|
+
);
|
|
745
|
+
const resetTrip = resetStoptimesToMidnight(trip);
|
|
746
|
+
const trips = [];
|
|
747
|
+
for (const frequency of formattedFrequencies) {
|
|
748
|
+
const startSeconds = secondsAfterMidnight(frequency.start_time);
|
|
749
|
+
const endSeconds = secondsAfterMidnight(frequency.end_time);
|
|
750
|
+
for (let offset = startSeconds; offset < endSeconds; offset += frequency.headway_secs) {
|
|
751
|
+
const newTrip = cloneDeep(resetTrip);
|
|
752
|
+
trips.push({
|
|
753
|
+
...newTrip,
|
|
754
|
+
trip_id: `${resetTrip.trip_id}_freq_${trips.length}`,
|
|
755
|
+
stoptimes: updateStoptimesByOffset(newTrip, offset)
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return trips;
|
|
760
|
+
};
|
|
761
|
+
var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config2) => {
|
|
762
|
+
if (config2.showArrivalOnDifference === null) {
|
|
763
|
+
return stopIds;
|
|
764
|
+
}
|
|
765
|
+
for (const trip of timetable.orderedTrips) {
|
|
766
|
+
for (const stoptime of trip.stoptimes) {
|
|
767
|
+
const timepointDifference = fromGTFSTime(stoptime.departure_time).diff(
|
|
768
|
+
fromGTFSTime(stoptime.arrival_time),
|
|
769
|
+
"minutes"
|
|
770
|
+
);
|
|
771
|
+
if (timepointDifference < config2.showArrivalOnDifference) {
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
const index = stopIds.indexOf(stoptime.stop_id);
|
|
775
|
+
if (index === 0 || index === stopIds.length - 1) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if (stoptime.stop_id === stopIds[index + 1] || stoptime.stop_id === stopIds[index - 1]) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
stopIds.splice(index, 0, stoptime.stop_id);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return stopIds;
|
|
785
|
+
};
|
|
786
|
+
var getStopOrder = (timetable, config2) => {
|
|
787
|
+
const timetableStopOrders = getTimetableStopOrders(
|
|
788
|
+
{
|
|
789
|
+
timetable_id: timetable.timetable_id
|
|
790
|
+
},
|
|
791
|
+
["stop_id"],
|
|
792
|
+
[["stop_sequence", "ASC"]]
|
|
793
|
+
);
|
|
794
|
+
if (timetableStopOrders.length > 0) {
|
|
795
|
+
return timetableStopOrders.map(
|
|
796
|
+
(timetableStopOrder) => timetableStopOrder.stop_id
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
const stopGraph = [];
|
|
801
|
+
for (const trip of timetable.orderedTrips) {
|
|
802
|
+
const sortedStopIds = trip.stoptimes.filter((stoptime) => {
|
|
803
|
+
if (config2.showOnlyTimepoint === true) {
|
|
804
|
+
return isTimepoint(stoptime);
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
}).map((stoptime) => stoptime.stop_id);
|
|
808
|
+
for (const [index, stopId] of sortedStopIds.entries()) {
|
|
809
|
+
if (index === sortedStopIds.length - 1) {
|
|
810
|
+
continue;
|
|
811
|
+
}
|
|
812
|
+
stopGraph.push([stopId, sortedStopIds[index + 1]]);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
const stopIds2 = toposort(stopGraph);
|
|
816
|
+
return duplicateStopsForDifferentArrivalDeparture(
|
|
817
|
+
stopIds2,
|
|
818
|
+
timetable,
|
|
819
|
+
config2
|
|
820
|
+
);
|
|
821
|
+
} catch {
|
|
822
|
+
}
|
|
823
|
+
const longestTripStoptimes = getLongestTripStoptimes(
|
|
824
|
+
timetable.orderedTrips,
|
|
825
|
+
config2
|
|
826
|
+
);
|
|
827
|
+
const stopIds = longestTripStoptimes.map((stoptime) => stoptime.stop_id);
|
|
828
|
+
return duplicateStopsForDifferentArrivalDeparture(stopIds, timetable, config2);
|
|
829
|
+
};
|
|
830
|
+
var getStopsForTimetable = (timetable, config2) => {
|
|
831
|
+
if (timetable.orderedTrips.length === 0) {
|
|
832
|
+
return [];
|
|
833
|
+
}
|
|
834
|
+
const orderedStopIds = getStopOrder(timetable, config2);
|
|
835
|
+
const orderedStops = orderedStopIds.map((stopId, index) => {
|
|
836
|
+
const stops = getStops({
|
|
837
|
+
stop_id: stopId
|
|
838
|
+
});
|
|
839
|
+
if (stops.length === 0) {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
const stop = {
|
|
845
|
+
...stops[0],
|
|
846
|
+
trips: []
|
|
847
|
+
};
|
|
848
|
+
if (index < orderedStopIds.length - 1 && stopId === orderedStopIds[index + 1]) {
|
|
849
|
+
stop.type = "arrival";
|
|
850
|
+
} else if (index > 0 && stopId === orderedStopIds[index - 1]) {
|
|
851
|
+
stop.type = "departure";
|
|
852
|
+
}
|
|
853
|
+
return stop;
|
|
854
|
+
});
|
|
855
|
+
if (timetable.showStopCity) {
|
|
856
|
+
const stopAttributes = getStopAttributes({
|
|
857
|
+
stop_id: orderedStopIds
|
|
858
|
+
});
|
|
859
|
+
for (const stopAttribute of stopAttributes) {
|
|
860
|
+
const stop = orderedStops.find(
|
|
861
|
+
(stop2) => stop2.stop_id === stopAttribute.stop_id
|
|
862
|
+
);
|
|
863
|
+
if (stop) {
|
|
864
|
+
stop.stop_city = stopAttribute.stop_city;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return orderedStops;
|
|
869
|
+
};
|
|
870
|
+
var getCalendarsFromTimetable = (timetable) => {
|
|
871
|
+
const db = openDb();
|
|
872
|
+
let whereClause = "";
|
|
873
|
+
const whereClauses = [];
|
|
874
|
+
if (timetable.end_date) {
|
|
875
|
+
if (!moment2(timetable.end_date, "YYYYMMDD", true).isValid()) {
|
|
876
|
+
throw new Error(
|
|
877
|
+
`Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
whereClauses.push(`start_date <= ${sqlString.escape(timetable.end_date)}`);
|
|
881
|
+
}
|
|
882
|
+
if (timetable.start_date) {
|
|
883
|
+
if (!moment2(timetable.start_date, "YYYYMMDD", true).isValid()) {
|
|
884
|
+
throw new Error(
|
|
885
|
+
`Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
whereClauses.push(`end_date >= ${sqlString.escape(timetable.start_date)}`);
|
|
889
|
+
}
|
|
890
|
+
const days2 = getDaysFromCalendars([timetable]);
|
|
891
|
+
const dayQueries = reduce(
|
|
892
|
+
days2,
|
|
893
|
+
(memo, value, key) => {
|
|
894
|
+
if (value === 1) {
|
|
895
|
+
memo.push(`${key} = 1`);
|
|
896
|
+
}
|
|
897
|
+
return memo;
|
|
898
|
+
},
|
|
899
|
+
[]
|
|
900
|
+
);
|
|
901
|
+
if (dayQueries.length > 0) {
|
|
902
|
+
whereClauses.push(`(${dayQueries.join(" OR ")})`);
|
|
903
|
+
}
|
|
904
|
+
if (whereClauses.length > 0) {
|
|
905
|
+
whereClause = `WHERE ${whereClauses.join(" AND ")}`;
|
|
906
|
+
}
|
|
907
|
+
return db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
|
|
908
|
+
};
|
|
909
|
+
var getCalendarDatesServiceIds = (startDate, endDate) => {
|
|
910
|
+
const db = openDb();
|
|
911
|
+
const whereClauses = ["exception_type = 1"];
|
|
912
|
+
if (endDate) {
|
|
913
|
+
whereClauses.push(`date <= ${sqlString.escape(endDate)}`);
|
|
914
|
+
}
|
|
915
|
+
if (startDate) {
|
|
916
|
+
whereClauses.push(`date >= ${sqlString.escape(startDate)}`);
|
|
917
|
+
}
|
|
918
|
+
const calendarDates = db.prepare(
|
|
919
|
+
`SELECT DISTINCT service_id FROM calendar_dates WHERE ${whereClauses.join(
|
|
920
|
+
" AND "
|
|
921
|
+
)}`
|
|
922
|
+
).all();
|
|
923
|
+
return calendarDates.map((calendarDate) => calendarDate.service_id);
|
|
924
|
+
};
|
|
925
|
+
var getAllStationStopIds = (stopId) => {
|
|
926
|
+
const stops = getStops({
|
|
927
|
+
stop_id: stopId
|
|
928
|
+
});
|
|
929
|
+
if (stops.length === 0) {
|
|
930
|
+
throw new Error(`No stop found for stop_id=${stopId}`);
|
|
931
|
+
}
|
|
932
|
+
const stop = stops[0];
|
|
933
|
+
if (isNullOrEmpty(stop.parent_station)) {
|
|
934
|
+
return [stopId];
|
|
935
|
+
}
|
|
936
|
+
const stopsInParentStation = getStops(
|
|
937
|
+
{
|
|
938
|
+
parent_station: stop.parent_station
|
|
939
|
+
},
|
|
940
|
+
["stop_id"]
|
|
941
|
+
);
|
|
942
|
+
return [
|
|
943
|
+
stop.parent_station,
|
|
944
|
+
...stopsInParentStation.map((stop2) => stop2.stop_id)
|
|
945
|
+
];
|
|
946
|
+
};
|
|
947
|
+
var getTripsWithSameBlock = (trip, timetable) => {
|
|
948
|
+
const trips = getTrips(
|
|
949
|
+
{
|
|
950
|
+
block_id: trip.block_id,
|
|
951
|
+
service_id: timetable.service_ids
|
|
952
|
+
},
|
|
953
|
+
["trip_id", "route_id"]
|
|
954
|
+
);
|
|
955
|
+
for (const blockTrip of trips) {
|
|
956
|
+
const stopTimes = getStoptimes(
|
|
957
|
+
{
|
|
958
|
+
trip_id: blockTrip.trip_id
|
|
959
|
+
},
|
|
960
|
+
[],
|
|
961
|
+
[["stop_sequence", "ASC"]]
|
|
962
|
+
);
|
|
963
|
+
if (stopTimes.length === 0) {
|
|
964
|
+
throw new Error(
|
|
965
|
+
`No stoptimes found found for trip_id=${blockTrip.trip_id}`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
blockTrip.firstStoptime = first(stopTimes);
|
|
969
|
+
blockTrip.lastStoptime = last(stopTimes);
|
|
970
|
+
}
|
|
971
|
+
return sortBy(trips, (trip2) => trip2.firstStoptime.departure_timestamp);
|
|
972
|
+
};
|
|
973
|
+
var addTripContinuation = (trip, timetable) => {
|
|
974
|
+
if (!trip.block_id || trip.stoptimes.length === 0) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const maxContinuesAsWaitingTimeSeconds = 60 * 60;
|
|
978
|
+
const firstStoptime = first(trip.stoptimes);
|
|
979
|
+
const firstStopIds = getAllStationStopIds(firstStoptime.stop_id);
|
|
980
|
+
const lastStoptime = last(trip.stoptimes);
|
|
981
|
+
const lastStopIds = getAllStationStopIds(lastStoptime.stop_id);
|
|
982
|
+
const blockTrips = getTripsWithSameBlock(trip, timetable);
|
|
983
|
+
const previousTrip = findLast(
|
|
984
|
+
blockTrips,
|
|
985
|
+
(blockTrip) => blockTrip.lastStoptime.arrival_timestamp <= firstStoptime.departure_timestamp
|
|
986
|
+
);
|
|
987
|
+
if (previousTrip && previousTrip.route_id !== trip.route_id && previousTrip.lastStoptime.arrival_timestamp >= firstStoptime.departure_timestamp - maxContinuesAsWaitingTimeSeconds && firstStopIds.includes(previousTrip.lastStoptime.stop_id)) {
|
|
988
|
+
const routes = getRoutes({
|
|
989
|
+
route_id: previousTrip.route_id
|
|
990
|
+
});
|
|
991
|
+
previousTrip.route = routes[0];
|
|
992
|
+
trip.continues_from_route = previousTrip;
|
|
993
|
+
}
|
|
994
|
+
const nextTrip = find(
|
|
995
|
+
blockTrips,
|
|
996
|
+
(blockTrip) => blockTrip.firstStoptime.departure_timestamp >= lastStoptime.arrival_timestamp
|
|
997
|
+
);
|
|
998
|
+
if (nextTrip && nextTrip.route_id !== trip.route_id && nextTrip.firstStoptime.departure_timestamp <= lastStoptime.arrival_timestamp + maxContinuesAsWaitingTimeSeconds && lastStopIds.includes(nextTrip.firstStoptime.stop_id)) {
|
|
999
|
+
const routes = getRoutes({
|
|
1000
|
+
route_id: nextTrip.route_id
|
|
1001
|
+
});
|
|
1002
|
+
nextTrip.route = routes[0];
|
|
1003
|
+
trip.continues_as_route = nextTrip;
|
|
1004
|
+
}
|
|
1005
|
+
};
|
|
1006
|
+
var filterTrips = (timetable) => {
|
|
1007
|
+
let filteredTrips = timetable.orderedTrips;
|
|
1008
|
+
for (const trip of filteredTrips) {
|
|
1009
|
+
const combinedStoptimes = [];
|
|
1010
|
+
for (const [index, stoptime] of trip.stoptimes.entries()) {
|
|
1011
|
+
if (index === 0 || stoptime.stop_id !== trip.stoptimes[index - 1].stop_id) {
|
|
1012
|
+
combinedStoptimes.push(stoptime);
|
|
1013
|
+
} else {
|
|
1014
|
+
combinedStoptimes[combinedStoptimes.length - 1].departure_time = stoptime.departure_time;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
trip.stoptimes = combinedStoptimes;
|
|
1018
|
+
}
|
|
1019
|
+
const timetableStopIds = new Set(timetable.stops.map((stop) => stop.stop_id));
|
|
1020
|
+
for (const trip of filteredTrips) {
|
|
1021
|
+
trip.stoptimes = trip.stoptimes.filter(
|
|
1022
|
+
(stoptime) => timetableStopIds.has(stoptime.stop_id)
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
|
|
1026
|
+
return filteredTrips;
|
|
1027
|
+
};
|
|
1028
|
+
var getTripsForTimetable = (timetable, calendars, config2) => {
|
|
1029
|
+
const tripQuery = {
|
|
1030
|
+
route_id: timetable.route_ids,
|
|
1031
|
+
service_id: timetable.service_ids
|
|
1032
|
+
};
|
|
1033
|
+
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
1034
|
+
tripQuery.direction_id = timetable.direction_id;
|
|
1035
|
+
}
|
|
1036
|
+
const trips = getTrips(tripQuery);
|
|
1037
|
+
if (trips.length === 0) {
|
|
1038
|
+
timetable.warnings.push(
|
|
1039
|
+
`No trips found for route_id=${timetable.route_ids.join(
|
|
1040
|
+
"_"
|
|
1041
|
+
)}, direction_id=${timetable.direction_id}, service_ids=${JSON.stringify(
|
|
1042
|
+
timetable.service_ids
|
|
1043
|
+
)}, timetable_id=${timetable.timetable_id}`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
const frequencies = getFrequencies({
|
|
1047
|
+
trip_id: trips.map((trip) => trip.trip_id)
|
|
1048
|
+
});
|
|
1049
|
+
timetable.service_ids = uniq(trips.map((trip) => trip.service_id));
|
|
1050
|
+
const formattedTrips = [];
|
|
1051
|
+
for (const trip of trips) {
|
|
1052
|
+
const formattedTrip = formatTrip(trip, timetable, calendars, config2);
|
|
1053
|
+
formattedTrip.stoptimes = getStoptimes(
|
|
1054
|
+
{
|
|
1055
|
+
trip_id: formattedTrip.trip_id
|
|
1056
|
+
},
|
|
1057
|
+
[],
|
|
1058
|
+
[["stop_sequence", "ASC"]]
|
|
1059
|
+
);
|
|
1060
|
+
if (formattedTrip.stoptimes.length === 0) {
|
|
1061
|
+
timetable.warnings.push(
|
|
1062
|
+
`No stoptimes found for trip_id=${formattedTrip.trip_id}, route_id=${timetable.route_ids.join("_")}, timetable_id=${timetable.timetable_id}`
|
|
1063
|
+
);
|
|
1064
|
+
}
|
|
1065
|
+
if (timetable.start_timestamp !== "" && timetable.start_timestamp !== null && timetable.start_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp < timetable.start_timestamp) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
if (timetable.end_timestamp !== "" && timetable.end_timestamp !== null && timetable.end_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp >= timetable.end_timestamp) {
|
|
1069
|
+
return;
|
|
1070
|
+
}
|
|
1071
|
+
if (timetable.show_trip_continuation) {
|
|
1072
|
+
addTripContinuation(formattedTrip, timetable);
|
|
1073
|
+
if (formattedTrip.continues_as_route) {
|
|
1074
|
+
timetable.has_continues_as_route = true;
|
|
1075
|
+
}
|
|
1076
|
+
if (formattedTrip.continues_from_route) {
|
|
1077
|
+
timetable.has_continues_from_route = true;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const tripFrequencies = frequencies.filter(
|
|
1081
|
+
(frequency) => frequency.trip_id === trip.trip_id
|
|
1082
|
+
);
|
|
1083
|
+
if (tripFrequencies.length === 0) {
|
|
1084
|
+
formattedTrips.push(formattedTrip);
|
|
1085
|
+
} else {
|
|
1086
|
+
const frequencyTrips = generateTripsByFrequencies(
|
|
1087
|
+
formattedTrip,
|
|
1088
|
+
frequencies,
|
|
1089
|
+
config2
|
|
1090
|
+
);
|
|
1091
|
+
formattedTrips.push(...frequencyTrips);
|
|
1092
|
+
timetable.frequencies = frequencies;
|
|
1093
|
+
timetable.frequencyExactTimes = some(frequencies, {
|
|
1094
|
+
exact_times: 1
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (config2.useParentStation) {
|
|
1099
|
+
const stopIds = [];
|
|
1100
|
+
for (const trip of formattedTrips) {
|
|
1101
|
+
for (const stoptime of trip.stoptimes) {
|
|
1102
|
+
stopIds.push(stoptime.stop_id);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
const stops = getStops(
|
|
1106
|
+
{
|
|
1107
|
+
stop_id: uniq(stopIds)
|
|
1108
|
+
},
|
|
1109
|
+
["parent_station", "stop_id"]
|
|
1110
|
+
);
|
|
1111
|
+
for (const trip of formattedTrips) {
|
|
1112
|
+
for (const stoptime of trip.stoptimes) {
|
|
1113
|
+
const stop = stops.find((stop2) => stop2.stop_id === stoptime.stop_id);
|
|
1114
|
+
if (stop?.parent_station) {
|
|
1115
|
+
stoptime.stop_id = stop.parent_station;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return sortTrips(formattedTrips, config2);
|
|
1121
|
+
};
|
|
1122
|
+
var formatTimetables = (timetables, config2) => {
|
|
1123
|
+
const formattedTimetables = timetables.map((timetable) => {
|
|
1124
|
+
timetable.warnings = [];
|
|
1125
|
+
const dayList = formatDays(timetable, config2);
|
|
1126
|
+
const calendars = getCalendarsFromTimetable(timetable);
|
|
1127
|
+
let serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
1128
|
+
if (timetable.include_exceptions === 1) {
|
|
1129
|
+
const calendarDatesServiceIds = getCalendarDatesServiceIds(
|
|
1130
|
+
timetable.start_date,
|
|
1131
|
+
timetable.end_date
|
|
1132
|
+
);
|
|
1133
|
+
serviceIds = uniq([...serviceIds, ...calendarDatesServiceIds]);
|
|
1134
|
+
}
|
|
1135
|
+
Object.assign(timetable, {
|
|
1136
|
+
noServiceSymbolUsed: false,
|
|
1137
|
+
requestDropoffSymbolUsed: false,
|
|
1138
|
+
noDropoffSymbolUsed: false,
|
|
1139
|
+
requestPickupSymbolUsed: false,
|
|
1140
|
+
noPickupSymbolUsed: false,
|
|
1141
|
+
interpolatedStopSymbolUsed: false,
|
|
1142
|
+
showStopCity: config2.showStopCity,
|
|
1143
|
+
showStopDescription: config2.showStopDescription,
|
|
1144
|
+
noServiceSymbol: config2.noServiceSymbol,
|
|
1145
|
+
requestDropoffSymbol: config2.requestDropoffSymbol,
|
|
1146
|
+
noDropoffSymbol: config2.noDropoffSymbol,
|
|
1147
|
+
requestPickupSymbol: config2.requestPickupSymbol,
|
|
1148
|
+
noPickupSymbol: config2.noPickupSymbol,
|
|
1149
|
+
interpolatedStopSymbol: config2.interpolatedStopSymbol,
|
|
1150
|
+
orientation: timetable.orientation || config2.defaultOrientation,
|
|
1151
|
+
service_ids: serviceIds,
|
|
1152
|
+
dayList,
|
|
1153
|
+
dayListLong: formatDaysLong(dayList, config2)
|
|
1154
|
+
});
|
|
1155
|
+
timetable.orderedTrips = getTripsForTimetable(timetable, calendars, config2);
|
|
1156
|
+
timetable.stops = getStopsForTimetable(timetable, config2);
|
|
1157
|
+
timetable.calendarDates = getCalendarDatesForTimetable(timetable, config2);
|
|
1158
|
+
timetable.timetable_label = formatTimetableLabel(timetable);
|
|
1159
|
+
timetable.notes = getTimetableNotesForTimetable(timetable, config2);
|
|
1160
|
+
if (config2.showMap) {
|
|
1161
|
+
timetable.geojson = getTimetableGeoJSON(timetable, config2);
|
|
1162
|
+
}
|
|
1163
|
+
timetable.orderedTrips = filterTrips(timetable);
|
|
1164
|
+
timetable.stops = formatStops(timetable, config2);
|
|
1165
|
+
return timetable;
|
|
1166
|
+
});
|
|
1167
|
+
if (config2.allowEmptyTimetables) {
|
|
1168
|
+
return formattedTimetables;
|
|
1169
|
+
}
|
|
1170
|
+
return formattedTimetables.filter(
|
|
1171
|
+
(timetable) => timetable.orderedTrips.length > 0
|
|
1172
|
+
);
|
|
1173
|
+
};
|
|
1174
|
+
function getTimetablePagesForAgency(config2) {
|
|
1175
|
+
const timetables = mergeTimetablesWithSameId(getTimetables());
|
|
1176
|
+
if (timetables.length === 0) {
|
|
1177
|
+
return convertRoutesToTimetablePages(config2);
|
|
1178
|
+
}
|
|
1179
|
+
const timetablePages = getTimetablePages(
|
|
1180
|
+
{},
|
|
1181
|
+
[],
|
|
1182
|
+
[["timetable_page_id", "ASC"]]
|
|
1183
|
+
);
|
|
1184
|
+
if (timetablePages.length === 0) {
|
|
1185
|
+
return timetables.map(
|
|
1186
|
+
(timetable) => convertTimetableToTimetablePage(timetable, config2)
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
const routes = getRoutes();
|
|
1190
|
+
return timetablePages.map((timetablePage) => {
|
|
1191
|
+
timetablePage.timetables = sortBy(
|
|
1192
|
+
timetables.filter(
|
|
1193
|
+
(timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
|
|
1194
|
+
),
|
|
1195
|
+
"timetable_sequence"
|
|
1196
|
+
);
|
|
1197
|
+
for (const timetable of timetablePage.timetables) {
|
|
1198
|
+
timetable.routes = routes.filter(
|
|
1199
|
+
(route) => timetable.route_ids.includes(route.route_id)
|
|
1200
|
+
);
|
|
1201
|
+
}
|
|
1202
|
+
return timetablePage;
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
var getTimetablePageById = (timetablePageId, config2) => {
|
|
1206
|
+
const timetablePages = getTimetablePages({
|
|
1207
|
+
timetable_page_id: timetablePageId
|
|
1208
|
+
});
|
|
1209
|
+
const timetables = mergeTimetablesWithSameId(getTimetables());
|
|
1210
|
+
if (timetablePages.length > 1) {
|
|
1211
|
+
throw new Error(
|
|
1212
|
+
`Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
if (timetablePages.length === 1) {
|
|
1216
|
+
const timetablePage = timetablePages[0];
|
|
1217
|
+
timetablePage.timetables = sortBy(
|
|
1218
|
+
timetables.filter(
|
|
1219
|
+
(timetable) => timetable.timetable_page_id === timetablePageId
|
|
1220
|
+
),
|
|
1221
|
+
"timetable_sequence"
|
|
1222
|
+
);
|
|
1223
|
+
for (const timetable of timetablePage.timetables) {
|
|
1224
|
+
timetable.routes = getRoutes({
|
|
1225
|
+
route_id: timetable.route_ids
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
return timetablePage;
|
|
1229
|
+
}
|
|
1230
|
+
if (timetables.length > 0) {
|
|
1231
|
+
const timetablePageTimetables = timetables.filter(
|
|
1232
|
+
(timetable) => timetable.timetable_id === timetablePageId
|
|
1233
|
+
);
|
|
1234
|
+
if (timetablePageTimetables.length === 0) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`No timetable found for timetable_page_id=${timetablePageId}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
return convertTimetableToTimetablePage(timetablePageTimetables[0], config2);
|
|
1240
|
+
}
|
|
1241
|
+
let calendarCode;
|
|
1242
|
+
let calendars;
|
|
1243
|
+
let calendarDates;
|
|
1244
|
+
let serviceId;
|
|
1245
|
+
let directionId = "";
|
|
1246
|
+
const parts = timetablePageId.split("|");
|
|
1247
|
+
if (parts.length > 2) {
|
|
1248
|
+
directionId = Number.parseInt(parts.pop(), 10);
|
|
1249
|
+
calendarCode = parts.pop();
|
|
1250
|
+
} else if (parts.length > 1) {
|
|
1251
|
+
directionId = null;
|
|
1252
|
+
calendarCode = parts.pop();
|
|
1253
|
+
}
|
|
1254
|
+
const routeId = parts.join("|");
|
|
1255
|
+
const routes = getRoutes({
|
|
1256
|
+
route_id: routeId
|
|
1257
|
+
});
|
|
1258
|
+
const trips = getTrips(
|
|
1259
|
+
{
|
|
1260
|
+
route_id: routeId,
|
|
1261
|
+
direction_id: directionId
|
|
1262
|
+
},
|
|
1263
|
+
["trip_headsign", "direction_id"]
|
|
1264
|
+
);
|
|
1265
|
+
const directions = uniqBy(trips, (trip) => trip.direction_id);
|
|
1266
|
+
if (directions.length === 0) {
|
|
1267
|
+
throw new Error(
|
|
1268
|
+
`No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (/^[01]*$/.test(calendarCode)) {
|
|
1272
|
+
calendars = getCalendars({
|
|
1273
|
+
...calendarCodeToCalendar(calendarCode)
|
|
1274
|
+
});
|
|
1275
|
+
} else {
|
|
1276
|
+
serviceId = calendarCode;
|
|
1277
|
+
calendarDates = getCalendarDates({
|
|
1278
|
+
exception_type: 1,
|
|
1279
|
+
service_id: serviceId
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return convertRouteToTimetablePage(
|
|
1283
|
+
routes[0],
|
|
1284
|
+
directions[0],
|
|
1285
|
+
calendars,
|
|
1286
|
+
calendarDates,
|
|
1287
|
+
config2
|
|
1288
|
+
);
|
|
1289
|
+
};
|
|
1290
|
+
function setDefaultConfig(initialConfig) {
|
|
1291
|
+
const defaults = {
|
|
1292
|
+
allowEmptyTimetables: false,
|
|
1293
|
+
beautify: false,
|
|
1294
|
+
coordinatePrecision: 5,
|
|
1295
|
+
dateFormat: "MMM D, YYYY",
|
|
1296
|
+
daysShortStrings: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
|
|
1297
|
+
daysStrings: [
|
|
1298
|
+
"Monday",
|
|
1299
|
+
"Tuesday",
|
|
1300
|
+
"Wednesday",
|
|
1301
|
+
"Thursday",
|
|
1302
|
+
"Friday",
|
|
1303
|
+
"Saturday",
|
|
1304
|
+
"Sunday"
|
|
1305
|
+
],
|
|
1306
|
+
defaultOrientation: "vertical",
|
|
1307
|
+
interpolatedStopSymbol: "\u2022",
|
|
1308
|
+
interpolatedStopText: "Estimated time of arrival",
|
|
1309
|
+
gtfsToHtmlVersion: version,
|
|
1310
|
+
linkStopUrls: false,
|
|
1311
|
+
menuType: "jump",
|
|
1312
|
+
noDropoffSymbol: "\u2021",
|
|
1313
|
+
noDropoffText: "No drop off available",
|
|
1314
|
+
noHead: false,
|
|
1315
|
+
noPickupSymbol: "**",
|
|
1316
|
+
noPickupText: "No pickup available",
|
|
1317
|
+
noServiceSymbol: "-",
|
|
1318
|
+
noServiceText: "No service at this stop",
|
|
1319
|
+
outputFormat: "html",
|
|
1320
|
+
requestDropoffSymbol: "\u2020",
|
|
1321
|
+
requestDropoffText: "Must request drop off",
|
|
1322
|
+
requestPickupSymbol: "***",
|
|
1323
|
+
requestPickupText: "Request stop - call for pickup",
|
|
1324
|
+
serviceNotProvidedOnText: "Service not provided on",
|
|
1325
|
+
serviceProvidedOnText: "Service provided on",
|
|
1326
|
+
showArrivalOnDifference: 0.2,
|
|
1327
|
+
showCalendarExceptions: true,
|
|
1328
|
+
showMap: false,
|
|
1329
|
+
showOnlyTimepoint: false,
|
|
1330
|
+
showRouteTitle: true,
|
|
1331
|
+
showStopCity: false,
|
|
1332
|
+
showStopDescription: false,
|
|
1333
|
+
showStoptimesForRequestStops: true,
|
|
1334
|
+
skipImport: false,
|
|
1335
|
+
sortingAlgorithm: "common",
|
|
1336
|
+
timeFormat: "h:mma",
|
|
1337
|
+
useParentStation: true,
|
|
1338
|
+
verbose: true,
|
|
1339
|
+
zipOutput: false
|
|
1340
|
+
};
|
|
1341
|
+
const config2 = Object.assign(defaults, initialConfig);
|
|
1342
|
+
if (config2.outputFormat === "pdf") {
|
|
1343
|
+
config2.noHead = false;
|
|
1344
|
+
}
|
|
1345
|
+
config2.hasGtfsRealtime = config2.agencies.some(
|
|
1346
|
+
(agency) => agency.realtimeTripUpdates?.url || agency.realtimeVehiclePositions?.url
|
|
1347
|
+
);
|
|
1348
|
+
return config2;
|
|
1349
|
+
}
|
|
1350
|
+
function getFormattedTimetablePage(timetablePageId, config2) {
|
|
1351
|
+
const timetablePage = getTimetablePageById(
|
|
1352
|
+
timetablePageId,
|
|
1353
|
+
config2
|
|
1354
|
+
);
|
|
1355
|
+
const timetableRoutes = getRoutes(
|
|
1356
|
+
{
|
|
1357
|
+
route_id: timetablePage.route_ids
|
|
1358
|
+
},
|
|
1359
|
+
["agency_id"]
|
|
1360
|
+
);
|
|
1361
|
+
const consolidatedTimetables = formatTimetables(
|
|
1362
|
+
timetablePage.timetables,
|
|
1363
|
+
config2
|
|
1364
|
+
);
|
|
1365
|
+
for (const timetable of consolidatedTimetables) {
|
|
1366
|
+
if (isNullOrEmpty(timetable.direction_name)) {
|
|
1367
|
+
timetable.direction_name = getDirectionHeadsignFromTimetable(timetable);
|
|
1368
|
+
}
|
|
1369
|
+
if (!timetable.routes) {
|
|
1370
|
+
timetable.routes = getRoutes({
|
|
1371
|
+
route_id: timetable.route_ids
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
const formattedTimetablePage = {
|
|
1376
|
+
...timetablePage,
|
|
1377
|
+
consolidatedTimetables,
|
|
1378
|
+
dayList: formatDays(getDaysFromCalendars(consolidatedTimetables), config2),
|
|
1379
|
+
dayLists: uniq(
|
|
1380
|
+
consolidatedTimetables.map((timetable) => timetable.dayList)
|
|
1381
|
+
),
|
|
1382
|
+
route_ids: uniq(flatMap2(consolidatedTimetables, "route_ids")),
|
|
1383
|
+
agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
|
|
1384
|
+
filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`
|
|
1385
|
+
};
|
|
1386
|
+
return formattedTimetablePage;
|
|
1387
|
+
}
|
|
1388
|
+
function generateTimetableHTML(timetablePage, config2) {
|
|
1389
|
+
const templateVars = {
|
|
1390
|
+
timetablePage,
|
|
1391
|
+
config: config2
|
|
1392
|
+
};
|
|
1393
|
+
return renderTemplate("timetablepage", templateVars, config2);
|
|
1394
|
+
}
|
|
1395
|
+
function generateOverviewHTML(timetablePages, config2) {
|
|
1396
|
+
const agencies = getAgencies();
|
|
1397
|
+
if (agencies.length === 0) {
|
|
1398
|
+
throw new Error("No agencies found");
|
|
1399
|
+
}
|
|
1400
|
+
let geojson;
|
|
1401
|
+
if (config2.showMap) {
|
|
1402
|
+
geojson = getAgencyGeoJSON(config2);
|
|
1403
|
+
}
|
|
1404
|
+
const sortedTimetablePages = sortBy(timetablePages, [
|
|
1405
|
+
(timetablePage) => {
|
|
1406
|
+
if (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
|
|
1407
|
+
return Number.parseInt(
|
|
1408
|
+
timetablePage.consolidatedTimetables[0].routes[0].route_short_name?.replace(
|
|
1409
|
+
/^\D+/g,
|
|
1410
|
+
""
|
|
1411
|
+
),
|
|
1412
|
+
10
|
|
1413
|
+
) || 0;
|
|
1414
|
+
}
|
|
1415
|
+
},
|
|
1416
|
+
(timetablePage) => {
|
|
1417
|
+
if (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
|
|
1418
|
+
return timetablePage.consolidatedTimetables[0].routes[0].route_short_name;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
]);
|
|
1422
|
+
const templateVars = {
|
|
1423
|
+
agency: {
|
|
1424
|
+
...first(agencies),
|
|
1425
|
+
geojson
|
|
1426
|
+
},
|
|
1427
|
+
agencies,
|
|
1428
|
+
geojson,
|
|
1429
|
+
config: config2,
|
|
1430
|
+
timetablePages: sortedTimetablePages
|
|
1431
|
+
};
|
|
1432
|
+
return renderTemplate("overview", templateVars, config2);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// src/lib/formatters.ts
|
|
1436
|
+
function replaceAll(string, mapObject) {
|
|
1437
|
+
const re = new RegExp(Object.keys(mapObject).join("|"), "gi");
|
|
1438
|
+
return string.replace(re, (matched) => mapObject[matched]);
|
|
1439
|
+
}
|
|
1440
|
+
function isNullOrEmpty(value) {
|
|
1441
|
+
return value === null || value === "";
|
|
1442
|
+
}
|
|
1443
|
+
function formatDate(date, dateFormat) {
|
|
1444
|
+
if (date.holiday_name) {
|
|
1445
|
+
return date.holiday_name;
|
|
1446
|
+
}
|
|
1447
|
+
return moment3(date.date, "YYYYMMDD").format(dateFormat);
|
|
1448
|
+
}
|
|
1449
|
+
function timeToSeconds(time) {
|
|
1450
|
+
return moment3.duration(time).asSeconds();
|
|
1451
|
+
}
|
|
1452
|
+
function formatStopTime(stoptime, timetable, config2) {
|
|
1453
|
+
stoptime.classes = [];
|
|
1454
|
+
if (stoptime.type === "arrival" && stoptime.arrival_time) {
|
|
1455
|
+
const arrivalTime = fromGTFSTime(stoptime.arrival_time);
|
|
1456
|
+
stoptime.formatted_time = arrivalTime.format(config2.timeFormat);
|
|
1457
|
+
stoptime.classes.push(arrivalTime.format("a"));
|
|
1458
|
+
} else if (stoptime.type === "departure" && stoptime.departure_time) {
|
|
1459
|
+
const departureTime = fromGTFSTime(stoptime.departure_time);
|
|
1460
|
+
stoptime.formatted_time = departureTime.format(config2.timeFormat);
|
|
1461
|
+
stoptime.classes.push(departureTime.format("a"));
|
|
1462
|
+
}
|
|
1463
|
+
if (stoptime.pickup_type === 1) {
|
|
1464
|
+
stoptime.noPickup = true;
|
|
1465
|
+
stoptime.classes.push("no-pickup");
|
|
1466
|
+
if (timetable.noPickupSymbol !== null) {
|
|
1467
|
+
timetable.noPickupSymbolUsed = true;
|
|
1468
|
+
}
|
|
1469
|
+
} else if (stoptime.pickup_type === 2 || stoptime.pickup_type === 3) {
|
|
1470
|
+
stoptime.requestPickup = true;
|
|
1471
|
+
stoptime.classes.push("request-pickup");
|
|
1472
|
+
if (timetable.requestPickupSymbol !== null) {
|
|
1473
|
+
timetable.requestPickupSymbolUsed = true;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (stoptime.drop_off_type === 1) {
|
|
1477
|
+
stoptime.noDropoff = true;
|
|
1478
|
+
stoptime.classes.push("no-drop-off");
|
|
1479
|
+
if (timetable.noDropoffSymbol !== null) {
|
|
1480
|
+
timetable.noDropoffSymbolUsed = true;
|
|
1481
|
+
}
|
|
1482
|
+
} else if (stoptime.drop_off_type === 2 || stoptime.drop_off_type === 3) {
|
|
1483
|
+
stoptime.requestDropoff = true;
|
|
1484
|
+
stoptime.classes.push("request-drop-off");
|
|
1485
|
+
if (timetable.requestDropoffSymbol !== null) {
|
|
1486
|
+
timetable.requestDropoffSymbolUsed = true;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (stoptime.timepoint === 0 || stoptime.departure_time === "") {
|
|
1490
|
+
stoptime.interpolated = true;
|
|
1491
|
+
stoptime.classes.push("interpolated");
|
|
1492
|
+
if (timetable.interpolatedStopSymbol !== null) {
|
|
1493
|
+
timetable.interpolatedStopSymbolUsed = true;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (stoptime.timepoint === null && stoptime.departure_time === null) {
|
|
1497
|
+
stoptime.skipped = true;
|
|
1498
|
+
stoptime.classes.push("skipped");
|
|
1499
|
+
if (timetable.noServiceSymbol !== null) {
|
|
1500
|
+
timetable.noServiceSymbolUsed = true;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
if (stoptime.timepoint === 1) {
|
|
1504
|
+
stoptime.classes.push("timepoint");
|
|
1505
|
+
}
|
|
1506
|
+
return stoptime;
|
|
1507
|
+
}
|
|
1508
|
+
function filterHourlyTimes(stops) {
|
|
1509
|
+
const firstStopTimes = [];
|
|
1510
|
+
const firstTripMinutes = minutesAfterMidnight(stops[0].trips[0].arrival_time);
|
|
1511
|
+
for (const trip of stops[0].trips) {
|
|
1512
|
+
const minutes = minutesAfterMidnight(trip.arrival_time);
|
|
1513
|
+
if (minutes >= firstTripMinutes + 60) {
|
|
1514
|
+
break;
|
|
1515
|
+
}
|
|
1516
|
+
firstStopTimes.push(fromGTFSTime(trip.arrival_time));
|
|
1517
|
+
}
|
|
1518
|
+
const firstStopTimesAndIndex = firstStopTimes.map((time, idx) => ({
|
|
1519
|
+
idx,
|
|
1520
|
+
time
|
|
1521
|
+
}));
|
|
1522
|
+
const sortedFirstStopTimesAndIndex = sortBy2(
|
|
1523
|
+
firstStopTimesAndIndex,
|
|
1524
|
+
(item) => Number.parseInt(item.time.format("m"), 10)
|
|
1525
|
+
);
|
|
1526
|
+
return stops.map((stop) => {
|
|
1527
|
+
stop.hourlyTimes = sortedFirstStopTimesAndIndex.map(
|
|
1528
|
+
(item) => fromGTFSTime(stop.trips[item.idx].arrival_time).format(":mm")
|
|
1529
|
+
);
|
|
1530
|
+
return stop;
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
var days = [
|
|
1534
|
+
"monday",
|
|
1535
|
+
"tuesday",
|
|
1536
|
+
"wednesday",
|
|
1537
|
+
"thursday",
|
|
1538
|
+
"friday",
|
|
1539
|
+
"saturday",
|
|
1540
|
+
"sunday"
|
|
1541
|
+
];
|
|
1542
|
+
function formatDays(calendar, config2) {
|
|
1543
|
+
const daysShort = config2.daysShortStrings;
|
|
1544
|
+
let daysInARow = 0;
|
|
1545
|
+
let dayString = "";
|
|
1546
|
+
if (!calendar) {
|
|
1547
|
+
return "";
|
|
1548
|
+
}
|
|
1549
|
+
for (let i = 0; i <= 6; i += 1) {
|
|
1550
|
+
const currentDayOperating = calendar[days[i]] === 1;
|
|
1551
|
+
const previousDayOperating = i > 0 ? calendar[days[i - 1]] === 1 : false;
|
|
1552
|
+
const nextDayOperating = i < 6 ? calendar[days[i + 1]] === 1 : false;
|
|
1553
|
+
if (currentDayOperating) {
|
|
1554
|
+
if (dayString.length > 0) {
|
|
1555
|
+
if (!previousDayOperating) {
|
|
1556
|
+
dayString += ", ";
|
|
1557
|
+
} else if (daysInARow === 1) {
|
|
1558
|
+
dayString += "-";
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
daysInARow += 1;
|
|
1562
|
+
if (dayString.length === 0 || !nextDayOperating || i === 6 || !previousDayOperating) {
|
|
1563
|
+
dayString += daysShort[i];
|
|
1564
|
+
}
|
|
1565
|
+
} else {
|
|
1566
|
+
daysInARow = 0;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (dayString.length === 0) {
|
|
1570
|
+
dayString = "No regular service days";
|
|
1571
|
+
}
|
|
1572
|
+
return dayString;
|
|
1573
|
+
}
|
|
1574
|
+
function formatDaysLong(dayList, config2) {
|
|
1575
|
+
const mapObject = zipObject(config2.daysShortStrings, config2.daysStrings);
|
|
1576
|
+
return replaceAll(dayList, mapObject);
|
|
1577
|
+
}
|
|
1578
|
+
function formatTrip(trip, timetable, calendars, config2) {
|
|
1579
|
+
trip.calendar = find2(calendars, {
|
|
1580
|
+
service_id: trip.service_id
|
|
1581
|
+
});
|
|
1582
|
+
trip.dayList = formatDays(trip.calendar, config2);
|
|
1583
|
+
trip.dayListLong = formatDaysLong(trip.dayList, config2);
|
|
1584
|
+
if (timetable.routes.length === 1) {
|
|
1585
|
+
trip.route_short_name = timetable.routes[0].route_short_name;
|
|
1586
|
+
} else {
|
|
1587
|
+
const route = timetable.routes.find(
|
|
1588
|
+
(route2) => route2.route_id === trip.route_id
|
|
1589
|
+
);
|
|
1590
|
+
trip.route_short_name = route.route_short_name;
|
|
1591
|
+
}
|
|
1592
|
+
return trip;
|
|
1593
|
+
}
|
|
1594
|
+
function formatFrequency(frequency, config2) {
|
|
1595
|
+
const startTime = fromGTFSTime(frequency.start_time);
|
|
1596
|
+
const endTime = fromGTFSTime(frequency.end_time);
|
|
1597
|
+
const headway = moment3.duration(frequency.headway_secs, "seconds");
|
|
1598
|
+
frequency.start_formatted_time = startTime.format(config2.timeFormat);
|
|
1599
|
+
frequency.end_formatted_time = endTime.format(config2.timeFormat);
|
|
1600
|
+
frequency.headway_min = Math.round(headway.asMinutes());
|
|
1601
|
+
return frequency;
|
|
1602
|
+
}
|
|
1603
|
+
function formatTimetableId(timetable) {
|
|
1604
|
+
let timetableId = `${timetable.route_ids.join("_")}|${calendarToCalendarCode(
|
|
1605
|
+
timetable
|
|
1606
|
+
)}`;
|
|
1607
|
+
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
1608
|
+
timetableId += `|${timetable.direction_id}`;
|
|
1609
|
+
}
|
|
1610
|
+
return timetableId;
|
|
1611
|
+
}
|
|
1612
|
+
function createEmptyStoptime(stopId, tripId) {
|
|
1613
|
+
return {
|
|
1614
|
+
id: null,
|
|
1615
|
+
trip_id: tripId,
|
|
1616
|
+
arrival_time: null,
|
|
1617
|
+
departure_time: null,
|
|
1618
|
+
stop_id: stopId,
|
|
1619
|
+
stop_sequence: null,
|
|
1620
|
+
stop_headsign: null,
|
|
1621
|
+
pickup_type: null,
|
|
1622
|
+
drop_off_type: null,
|
|
1623
|
+
continuous_pickup: null,
|
|
1624
|
+
continuous_drop_off: null,
|
|
1625
|
+
shape_dist_traveled: null,
|
|
1626
|
+
timepoint: null
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
function formatStops(timetable, config2) {
|
|
1630
|
+
for (const trip of timetable.orderedTrips) {
|
|
1631
|
+
let stopIndex = -1;
|
|
1632
|
+
for (const [idx, stoptime] of trip.stoptimes.entries()) {
|
|
1633
|
+
const stop = find2(timetable.stops, (st, idx2) => {
|
|
1634
|
+
if (st.stop_id === stoptime.stop_id && idx2 > stopIndex) {
|
|
1635
|
+
stopIndex = idx2;
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
return false;
|
|
1639
|
+
});
|
|
1640
|
+
if (!stop) {
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (idx === 0) {
|
|
1644
|
+
stoptime.drop_off_type = 0;
|
|
1645
|
+
}
|
|
1646
|
+
if (idx === trip.stoptimes.length - 1) {
|
|
1647
|
+
stoptime.pickup_type = 0;
|
|
1648
|
+
}
|
|
1649
|
+
if (stop.type === "arrival" && idx < trip.stoptimes.length - 1) {
|
|
1650
|
+
const departureStoptime = clone(stoptime);
|
|
1651
|
+
departureStoptime.type = "departure";
|
|
1652
|
+
timetable.stops[stopIndex + 1].trips.push(
|
|
1653
|
+
formatStopTime(departureStoptime, timetable, config2)
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
if (!(stop.type === "arrival" && idx === 0)) {
|
|
1657
|
+
stoptime.type = "arrival";
|
|
1658
|
+
stop.trips.push(formatStopTime(stoptime, timetable, config2));
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
for (const stop of timetable.stops) {
|
|
1662
|
+
const lastStopTime = last2(stop.trips);
|
|
1663
|
+
if (!lastStopTime || lastStopTime.trip_id !== trip.trip_id) {
|
|
1664
|
+
stop.trips.push(
|
|
1665
|
+
formatStopTime(
|
|
1666
|
+
createEmptyStoptime(stop.stop_id, trip.trip_id),
|
|
1667
|
+
timetable,
|
|
1668
|
+
config2
|
|
1669
|
+
)
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (timetable.orientation === "hourly") {
|
|
1675
|
+
timetable.stops = filterHourlyTimes(timetable.stops);
|
|
1676
|
+
}
|
|
1677
|
+
for (const stop of timetable.stops) {
|
|
1678
|
+
stop.is_timepoint = stop.trips.some((stoptime) => isTimepoint(stoptime));
|
|
1679
|
+
}
|
|
1680
|
+
return timetable.stops;
|
|
1681
|
+
}
|
|
1682
|
+
function resetStoptimesToMidnight(trip) {
|
|
1683
|
+
const offsetSeconds = secondsAfterMidnight(
|
|
1684
|
+
first2(trip.stoptimes).departure_time
|
|
1685
|
+
);
|
|
1686
|
+
if (offsetSeconds > 0) {
|
|
1687
|
+
for (const stoptime of trip.stoptimes) {
|
|
1688
|
+
stoptime.departure_time = toGTFSTime(
|
|
1689
|
+
fromGTFSTime(stoptime.departure_time).subtract(
|
|
1690
|
+
offsetSeconds,
|
|
1691
|
+
"seconds"
|
|
1692
|
+
)
|
|
1693
|
+
);
|
|
1694
|
+
stoptime.arrival_time = toGTFSTime(
|
|
1695
|
+
fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, "seconds")
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
return trip;
|
|
1700
|
+
}
|
|
1701
|
+
function updateStoptimesByOffset(trip, offsetSeconds) {
|
|
1702
|
+
return trip.stoptimes.map((stoptime) => {
|
|
1703
|
+
delete stoptime._id;
|
|
1704
|
+
stoptime.departure_time = updateTimeByOffset(
|
|
1705
|
+
stoptime.departure_time,
|
|
1706
|
+
offsetSeconds
|
|
1707
|
+
);
|
|
1708
|
+
stoptime.arrival_time = updateTimeByOffset(
|
|
1709
|
+
stoptime.arrival_time,
|
|
1710
|
+
offsetSeconds
|
|
1711
|
+
);
|
|
1712
|
+
stoptime.trip_id = trip.trip_id;
|
|
1713
|
+
return stoptime;
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
function formatRouteColor(route) {
|
|
1717
|
+
return route.route_color ? `#${route.route_color}` : "#000000";
|
|
1718
|
+
}
|
|
1719
|
+
function formatRouteTextColor(route) {
|
|
1720
|
+
return route.route_text_color ? `#${route.route_text_color}` : "#FFFFFF";
|
|
1721
|
+
}
|
|
1722
|
+
function formatTimetableLabel(timetable) {
|
|
1723
|
+
if (!isNullOrEmpty(timetable.timetable_label)) {
|
|
1724
|
+
return timetable.timetable_label;
|
|
1725
|
+
}
|
|
1726
|
+
let timetableLabel = "";
|
|
1727
|
+
if (timetable.routes && timetable.routes.length > 0) {
|
|
1728
|
+
timetableLabel += "Route ";
|
|
1729
|
+
if (!isNullOrEmpty(timetable.routes[0].route_short_name)) {
|
|
1730
|
+
timetableLabel += timetable.routes[0].route_short_name;
|
|
1731
|
+
} else if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
|
|
1732
|
+
timetableLabel += timetable.routes[0].route_long_name;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (timetable.stops && timetable.stops.length > 0) {
|
|
1736
|
+
const firstStop = timetable.stops[0].stop_name;
|
|
1737
|
+
const lastStop = timetable.stops[timetable.stops.length - 1].stop_name;
|
|
1738
|
+
if (firstStop === lastStop) {
|
|
1739
|
+
if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
|
|
1740
|
+
timetableLabel += ` - ${timetable.routes[0].route_long_name}`;
|
|
1741
|
+
}
|
|
1742
|
+
timetableLabel += " - Loop";
|
|
1743
|
+
} else {
|
|
1744
|
+
timetableLabel += ` - ${firstStop} to ${lastStop}`;
|
|
1745
|
+
}
|
|
1746
|
+
} else if (timetable.direction_name !== null) {
|
|
1747
|
+
timetableLabel += ` to ${timetable.direction_name}`;
|
|
1748
|
+
}
|
|
1749
|
+
return timetableLabel;
|
|
1750
|
+
}
|
|
1751
|
+
function mergeTimetablesWithSameId(timetables) {
|
|
1752
|
+
if (timetables.length === 0) {
|
|
1753
|
+
return [];
|
|
1754
|
+
}
|
|
1755
|
+
const mergedTimetables = groupBy2(timetables, "timetable_id");
|
|
1756
|
+
return Object.values(mergedTimetables).map((timetableGroup) => {
|
|
1757
|
+
const mergedTimetable = omit(timetableGroup[0], "route_id");
|
|
1758
|
+
mergedTimetable.route_ids = timetableGroup.map(
|
|
1759
|
+
(timetable) => timetable.route_id
|
|
1760
|
+
);
|
|
1761
|
+
return mergedTimetable;
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// src/app/index.ts
|
|
1766
|
+
var argv = yargs(hideBin(process.argv)).option("c", {
|
|
1767
|
+
alias: "configPath",
|
|
1768
|
+
describe: "Path to config file",
|
|
1769
|
+
default: "./config.json",
|
|
1770
|
+
type: "string"
|
|
1771
|
+
}).parseSync();
|
|
1772
|
+
var app = express();
|
|
1773
|
+
var router = express.Router();
|
|
1774
|
+
var configPath = argv.configPath || new URL("../../config.json", import.meta.url);
|
|
1775
|
+
var selectedConfig = JSON.parse(readFileSync(configPath, "utf8"));
|
|
1776
|
+
var config = setDefaultConfig(selectedConfig);
|
|
1777
|
+
config.noHead = false;
|
|
1778
|
+
config.assetPath = "/";
|
|
1779
|
+
config.log = console.log;
|
|
1780
|
+
config.logWarning = console.warn;
|
|
1781
|
+
config.logError = console.error;
|
|
1782
|
+
try {
|
|
1783
|
+
openDb2(config);
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
if (error?.code === "SQLITE_CANTOPEN") {
|
|
1786
|
+
config.logError(
|
|
1787
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
throw error;
|
|
1791
|
+
}
|
|
1792
|
+
router.get("/", async (request, response, next) => {
|
|
1793
|
+
try {
|
|
1794
|
+
const timetablePages = [];
|
|
1795
|
+
const timetablePageIds = map(
|
|
1796
|
+
getTimetablePagesForAgency(config),
|
|
1797
|
+
"timetable_page_id"
|
|
1798
|
+
);
|
|
1799
|
+
for (const timetablePageId of timetablePageIds) {
|
|
1800
|
+
const timetablePage = await getFormattedTimetablePage(
|
|
1801
|
+
timetablePageId,
|
|
1802
|
+
config
|
|
1803
|
+
);
|
|
1804
|
+
if (!timetablePage.consolidatedTimetables || timetablePage.consolidatedTimetables.length === 0) {
|
|
1805
|
+
console.error(
|
|
1806
|
+
`No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
timetablePage.relativePath = `/timetables/${timetablePage.timetable_page_id}`;
|
|
1810
|
+
for (const timetable of timetablePage.consolidatedTimetables) {
|
|
1811
|
+
timetable.timetable_label = formatTimetableLabel(timetable);
|
|
1812
|
+
}
|
|
1813
|
+
timetablePages.push(timetablePage);
|
|
1814
|
+
}
|
|
1815
|
+
const html = await generateOverviewHTML(timetablePages, config);
|
|
1816
|
+
response.send(html);
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
console.error(error);
|
|
1819
|
+
next(error);
|
|
1820
|
+
}
|
|
1821
|
+
});
|
|
1822
|
+
router.get("/timetables/:timetablePageId", async (request, response, next) => {
|
|
1823
|
+
const { timetablePageId } = request.params;
|
|
1824
|
+
if (!timetablePageId) {
|
|
1825
|
+
return next(new Error("No timetablePageId provided"));
|
|
1826
|
+
}
|
|
1827
|
+
try {
|
|
1828
|
+
const timetablePage = await getFormattedTimetablePage(
|
|
1829
|
+
timetablePageId,
|
|
1830
|
+
config
|
|
1831
|
+
);
|
|
1832
|
+
const html = await generateTimetableHTML(timetablePage, config);
|
|
1833
|
+
response.send(html);
|
|
1834
|
+
} catch (error) {
|
|
1835
|
+
next(error);
|
|
1836
|
+
}
|
|
1837
|
+
});
|
|
1838
|
+
app.set("views", path2.join(fileURLToPath2(import.meta.url), "../../../views"));
|
|
1839
|
+
app.set("view engine", "pug");
|
|
1840
|
+
app.use(logger("dev"));
|
|
1841
|
+
var staticAssetPath = config.templatePath === void 0 ? path2.join(fileURLToPath2(import.meta.url), "../../../views/default") : untildify2(config.templatePath);
|
|
1842
|
+
app.use(express.static(staticAssetPath));
|
|
1843
|
+
app.use(
|
|
1844
|
+
"/js",
|
|
1845
|
+
express.static(
|
|
1846
|
+
path2.join(fileURLToPath2(import.meta.url), "../../../node_modules/pbf/dist")
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
app.use(
|
|
1850
|
+
"/js",
|
|
1851
|
+
express.static(
|
|
1852
|
+
path2.join(
|
|
1853
|
+
fileURLToPath2(import.meta.url),
|
|
1854
|
+
"../../../node_modules/gtfs-realtime-pbf-js-module"
|
|
1855
|
+
)
|
|
1856
|
+
)
|
|
1857
|
+
);
|
|
1858
|
+
app.use("/", router);
|
|
1859
|
+
app.set("port", process.env.PORT || 3e3);
|
|
1860
|
+
var server = app.listen(app.get("port"), () => {
|
|
1861
|
+
console.log(`Express server listening on port ${app.get("port")}`);
|
|
1862
|
+
});
|
|
1863
|
+
//# sourceMappingURL=index.js.map
|