gtfs-to-html 2.2.0 → 2.3.2
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/.eslintrc.json +15 -20
- package/.husky/pre-commit +4 -0
- package/CHANGELOG.md +275 -2
- package/README.md +59 -41
- package/app/index.js +46 -24
- package/bin/gtfs-to-html.js +5 -7
- package/lib/file-utils.js +52 -15
- package/lib/formatters.js +123 -28
- package/lib/geojson-utils.js +32 -17
- package/lib/gtfs-to-html.js +96 -34
- package/lib/log-utils.js +23 -15
- package/lib/template-functions.js +80 -17
- package/lib/time-utils.js +10 -2
- package/lib/utils.js +762 -371
- package/package.json +29 -11
- package/public/css/timetable_styles.css +55 -49
- package/public/js/system-map.js +73 -60
- package/public/js/timetable-map.js +103 -96
- package/public/js/timetable-menu.js +32 -8
- package/views/default/formatting_functions.pug +0 -17
- package/views/default/overview_full.pug +2 -2
- package/views/default/timetablepage_full.pug +2 -2
- package/www/blog/2021-11-06-CSV-Export.md +26 -0
- package/www/docs/configuration.md +87 -85
- package/www/docs/current-usage.md +31 -30
- package/www/docs/introduction.md +8 -5
- package/www/docs/timetables.md +35 -27
- package/www/package.json +2 -5
- package/www/static/img/gtfs-to-html-logo.svg +15 -61
- package/www/yarn.lock +2160 -3398
package/lib/utils.js
CHANGED
|
@@ -1,24 +1,92 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
cloneDeep,
|
|
5
|
+
compact,
|
|
6
|
+
every,
|
|
7
|
+
find,
|
|
8
|
+
findLast,
|
|
9
|
+
first,
|
|
10
|
+
flatMap,
|
|
11
|
+
flattenDeep,
|
|
12
|
+
isEqual,
|
|
13
|
+
groupBy,
|
|
14
|
+
last,
|
|
15
|
+
maxBy,
|
|
16
|
+
reduce,
|
|
17
|
+
size,
|
|
18
|
+
some,
|
|
19
|
+
sortBy,
|
|
20
|
+
uniq,
|
|
21
|
+
uniqBy,
|
|
22
|
+
zip,
|
|
23
|
+
} from 'lodash-es';
|
|
24
|
+
import {
|
|
25
|
+
getCalendarDates,
|
|
26
|
+
getTrips,
|
|
27
|
+
getTimetableNotesReferences,
|
|
28
|
+
getTimetableNotes,
|
|
29
|
+
getRoutes,
|
|
30
|
+
getDb,
|
|
31
|
+
getCalendars,
|
|
32
|
+
getTimetableStopOrders,
|
|
33
|
+
getStops,
|
|
34
|
+
getStopAttributes,
|
|
35
|
+
getStoptimes,
|
|
36
|
+
getFrequencies,
|
|
37
|
+
getTimetables,
|
|
38
|
+
getTimetablePages,
|
|
39
|
+
getAgencies,
|
|
40
|
+
} from 'gtfs';
|
|
41
|
+
import { stringify } from 'csv-stringify';
|
|
5
42
|
import moment from 'moment';
|
|
6
43
|
import sqlString from 'sqlstring';
|
|
7
44
|
import toposort from 'toposort';
|
|
8
45
|
|
|
9
46
|
import { generateFileName, renderTemplate } from './file-utils.js';
|
|
10
|
-
import {
|
|
47
|
+
import {
|
|
48
|
+
formatDate,
|
|
49
|
+
formatDays,
|
|
50
|
+
formatDaysLong,
|
|
51
|
+
formatFrequency,
|
|
52
|
+
formatStopName,
|
|
53
|
+
formatStops,
|
|
54
|
+
formatTimetableId,
|
|
55
|
+
formatTimetableLabel,
|
|
56
|
+
formatTimetablePageLabel,
|
|
57
|
+
formatTrip,
|
|
58
|
+
formatTripContinuesAs,
|
|
59
|
+
formatTripContinuesFrom,
|
|
60
|
+
isNullOrEmpty,
|
|
61
|
+
mergeTimetablesWithSameId,
|
|
62
|
+
resetStoptimesToMidnight,
|
|
63
|
+
timeToSeconds,
|
|
64
|
+
updateStoptimesByOffset,
|
|
65
|
+
} from './formatters.js';
|
|
11
66
|
import { getTimetableGeoJSON, getAgencyGeoJSON } from './geojson-utils.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
67
|
+
import {
|
|
68
|
+
fromGTFSDate,
|
|
69
|
+
toGTFSDate,
|
|
70
|
+
calendarToCalendarCode,
|
|
71
|
+
secondsAfterMidnight,
|
|
72
|
+
fromGTFSTime,
|
|
73
|
+
calendarCodeToCalendar,
|
|
74
|
+
} from './time-utils.js';
|
|
75
|
+
import { formatTripNameForCSV } from './template-functions.js';
|
|
76
|
+
|
|
77
|
+
const { version } = JSON.parse(
|
|
78
|
+
readFileSync(new URL('../package.json', import.meta.url))
|
|
79
|
+
);
|
|
15
80
|
|
|
16
81
|
/*
|
|
17
82
|
* Determine if a stoptime is a timepoint.
|
|
18
83
|
*/
|
|
19
|
-
const isTimepoint = stoptime => {
|
|
84
|
+
const isTimepoint = (stoptime) => {
|
|
20
85
|
if (isNullOrEmpty(stoptime.timepoint)) {
|
|
21
|
-
return
|
|
86
|
+
return (
|
|
87
|
+
!isNullOrEmpty(stoptime.arrival_time) &&
|
|
88
|
+
!isNullOrEmpty(stoptime.departure_time)
|
|
89
|
+
);
|
|
22
90
|
}
|
|
23
91
|
|
|
24
92
|
return stoptime.timepoint === 1;
|
|
@@ -29,9 +97,14 @@ const isTimepoint = stoptime => {
|
|
|
29
97
|
*/
|
|
30
98
|
const getLongestTripStoptimes = (trips, config) => {
|
|
31
99
|
// If `showOnlyTimepoint` is true, then filter out all non-timepoints.
|
|
32
|
-
const filteredTripStoptimes =
|
|
33
|
-
|
|
34
|
-
|
|
100
|
+
const filteredTripStoptimes =
|
|
101
|
+
config.showOnlyTimepoint === true
|
|
102
|
+
? trips.map((trip) =>
|
|
103
|
+
trip.stoptimes.filter((stoptime) => isTimepoint(stoptime))
|
|
104
|
+
)
|
|
105
|
+
: trips.map((trip) => trip.stoptimes);
|
|
106
|
+
|
|
107
|
+
return maxBy(filteredTripStoptimes, (stoptimes) => size(stoptimes));
|
|
35
108
|
};
|
|
36
109
|
|
|
37
110
|
/*
|
|
@@ -57,7 +130,13 @@ const findCommonStopId = (trips, config) => {
|
|
|
57
130
|
}
|
|
58
131
|
|
|
59
132
|
// Check if all trips have this stoptime and that they have a time.
|
|
60
|
-
return every(trips, trip =>
|
|
133
|
+
return every(trips, (trip) =>
|
|
134
|
+
trip.stoptimes.find(
|
|
135
|
+
(tripStoptime) =>
|
|
136
|
+
tripStoptime.stop_id === stoptime.stop_id &&
|
|
137
|
+
tripStoptime.arrival_time !== null
|
|
138
|
+
)
|
|
139
|
+
);
|
|
61
140
|
});
|
|
62
141
|
|
|
63
142
|
return commonStoptime ? commonStoptime.stop_id : null;
|
|
@@ -77,15 +156,17 @@ const deduplicateTrips = (trips, commonStopId) => {
|
|
|
77
156
|
continue;
|
|
78
157
|
}
|
|
79
158
|
|
|
80
|
-
const stoptimes = trip.stoptimes.map(stoptime => stoptime.departure_time);
|
|
81
|
-
const selectedStoptime = commonStopId
|
|
82
|
-
|
|
83
|
-
|
|
159
|
+
const stoptimes = trip.stoptimes.map((stoptime) => stoptime.departure_time);
|
|
160
|
+
const selectedStoptime = commonStopId
|
|
161
|
+
? find(trip.stoptimes, {
|
|
162
|
+
stop_id: commonStopId,
|
|
163
|
+
})
|
|
164
|
+
: trip.stoptimes[0];
|
|
84
165
|
|
|
85
166
|
// Find all other trips where the common stop has the same departure time.
|
|
86
|
-
const similarTrips = deduplicatedTrips.filter(trip => {
|
|
167
|
+
const similarTrips = deduplicatedTrips.filter((trip) => {
|
|
87
168
|
const stoptime = find(trip.stoptimes, {
|
|
88
|
-
stop_id: selectedStoptime.stop_id
|
|
169
|
+
stop_id: selectedStoptime.stop_id,
|
|
89
170
|
});
|
|
90
171
|
if (!stoptime) {
|
|
91
172
|
return false;
|
|
@@ -95,8 +176,10 @@ const deduplicateTrips = (trips, commonStopId) => {
|
|
|
95
176
|
});
|
|
96
177
|
|
|
97
178
|
// Only add trip if no existing trip with the same set of timepoints has already been added.
|
|
98
|
-
const tripIsUnique = every(similarTrips, similarTrip => {
|
|
99
|
-
const similarTripStoptimes = similarTrip.stoptimes.map(
|
|
179
|
+
const tripIsUnique = every(similarTrips, (similarTrip) => {
|
|
180
|
+
const similarTripStoptimes = similarTrip.stoptimes.map(
|
|
181
|
+
(stoptime) => stoptime.departure_time
|
|
182
|
+
);
|
|
100
183
|
return !isEqual(stoptimes, similarTripStoptimes);
|
|
101
184
|
});
|
|
102
185
|
|
|
@@ -124,7 +207,10 @@ const sortTrips = (trips, config) => {
|
|
|
124
207
|
sortedTrips = sortTripsByStoptimeAtStop(trips, commonStopId);
|
|
125
208
|
} else {
|
|
126
209
|
// Default to 'beginning' if no common stop is found.
|
|
127
|
-
sortedTrips = sortTrips(trips, {
|
|
210
|
+
sortedTrips = sortTrips(trips, {
|
|
211
|
+
...config,
|
|
212
|
+
sortingAlgorithm: 'beginning',
|
|
213
|
+
});
|
|
128
214
|
}
|
|
129
215
|
} else if (config.sortingAlgorithm === 'beginning') {
|
|
130
216
|
// Sort trips chronologically using first stoptime of each trip, which can be at different stops.
|
|
@@ -138,7 +224,11 @@ const sortTrips = (trips, config) => {
|
|
|
138
224
|
trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
|
|
139
225
|
}
|
|
140
226
|
|
|
141
|
-
sortedTrips = sortBy(
|
|
227
|
+
sortedTrips = sortBy(
|
|
228
|
+
trips,
|
|
229
|
+
['firstStoptime', 'lastStoptime'],
|
|
230
|
+
['asc', 'asc']
|
|
231
|
+
);
|
|
142
232
|
} else if (config.sortingAlgorithm === 'end') {
|
|
143
233
|
// Sort trips chronologically using last stoptime of each trip, which can be at different stops.
|
|
144
234
|
|
|
@@ -151,7 +241,11 @@ const sortTrips = (trips, config) => {
|
|
|
151
241
|
trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
|
|
152
242
|
}
|
|
153
243
|
|
|
154
|
-
sortedTrips = sortBy(
|
|
244
|
+
sortedTrips = sortBy(
|
|
245
|
+
trips,
|
|
246
|
+
['lastStoptime', 'firstStoptime'],
|
|
247
|
+
['asc', 'asc']
|
|
248
|
+
);
|
|
155
249
|
} else if (config.sortingAlgorithm === 'first') {
|
|
156
250
|
// Sort trips chronologically using the stoptime of a the first stop of the longest trip.
|
|
157
251
|
|
|
@@ -172,33 +266,40 @@ const sortTrips = (trips, config) => {
|
|
|
172
266
|
/*
|
|
173
267
|
* Sort trips by stoptime at a specific stop
|
|
174
268
|
*/
|
|
175
|
-
const sortTripsByStoptimeAtStop = (trips, stopId) =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
269
|
+
const sortTripsByStoptimeAtStop = (trips, stopId) =>
|
|
270
|
+
sortBy(trips, (trip) => {
|
|
271
|
+
const stoptime = find(trip.stoptimes, { stop_id: stopId });
|
|
272
|
+
return stoptime ? timeToSeconds(stoptime.departure_time) : undefined;
|
|
273
|
+
});
|
|
179
274
|
|
|
180
275
|
/*
|
|
181
276
|
* Get all calendar dates for a specific timetable.
|
|
182
277
|
*/
|
|
183
278
|
const getCalendarDatesForTimetable = async (timetable, config) => {
|
|
184
|
-
const calendarDates = await getCalendarDates(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
279
|
+
const calendarDates = await getCalendarDates(
|
|
280
|
+
{
|
|
281
|
+
service_id: timetable.service_ids,
|
|
282
|
+
},
|
|
283
|
+
[],
|
|
284
|
+
[['date', 'ASC']]
|
|
285
|
+
);
|
|
189
286
|
const start = fromGTFSDate(timetable.start_date);
|
|
190
287
|
const end = fromGTFSDate(timetable.end_date);
|
|
191
288
|
const filteredCalendarDates = {
|
|
192
289
|
excludedDates: [],
|
|
193
|
-
includedDates: []
|
|
290
|
+
includedDates: [],
|
|
194
291
|
};
|
|
195
292
|
|
|
196
293
|
for (const calendarDate of calendarDates) {
|
|
197
294
|
if (moment(calendarDate.date, 'YYYYMMDD').isBetween(start, end)) {
|
|
198
295
|
if (calendarDate.exception_type === 1) {
|
|
199
|
-
filteredCalendarDates.includedDates.push(
|
|
296
|
+
filteredCalendarDates.includedDates.push(
|
|
297
|
+
formatDate(calendarDate, config.dateFormat)
|
|
298
|
+
);
|
|
200
299
|
} else if (calendarDate.exception_type === 2) {
|
|
201
|
-
filteredCalendarDates.excludedDates.push(
|
|
300
|
+
filteredCalendarDates.excludedDates.push(
|
|
301
|
+
formatDate(calendarDate, config.dateFormat)
|
|
302
|
+
);
|
|
202
303
|
}
|
|
203
304
|
}
|
|
204
305
|
}
|
|
@@ -209,7 +310,7 @@ const getCalendarDatesForTimetable = async (timetable, config) => {
|
|
|
209
310
|
/*
|
|
210
311
|
* Get days of the week from calendars.
|
|
211
312
|
*/
|
|
212
|
-
const getDaysFromCalendars = calendars => {
|
|
313
|
+
const getDaysFromCalendars = (calendars) => {
|
|
213
314
|
const days = {
|
|
214
315
|
monday: 0,
|
|
215
316
|
tuesday: 0,
|
|
@@ -217,7 +318,7 @@ const getDaysFromCalendars = calendars => {
|
|
|
217
318
|
thursday: 0,
|
|
218
319
|
friday: 0,
|
|
219
320
|
saturday: 0,
|
|
220
|
-
sunday: 0
|
|
321
|
+
sunday: 0,
|
|
221
322
|
};
|
|
222
323
|
|
|
223
324
|
for (const calendar of calendars) {
|
|
@@ -233,13 +334,14 @@ const getDaysFromCalendars = calendars => {
|
|
|
233
334
|
/*
|
|
234
335
|
* Get the `trip_headsign` for a specific timetable.
|
|
235
336
|
*/
|
|
236
|
-
const getDirectionHeadsignFromTimetable = async timetable => {
|
|
237
|
-
const trips = await getTrips(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
337
|
+
const getDirectionHeadsignFromTimetable = async (timetable) => {
|
|
338
|
+
const trips = await getTrips(
|
|
339
|
+
{
|
|
340
|
+
direction_id: timetable.direction_id,
|
|
341
|
+
route_id: timetable.route_ids,
|
|
342
|
+
},
|
|
343
|
+
['trip_headsign']
|
|
344
|
+
);
|
|
243
345
|
|
|
244
346
|
if (trips.length === 0) {
|
|
245
347
|
return '';
|
|
@@ -254,51 +356,60 @@ const getDirectionHeadsignFromTimetable = async timetable => {
|
|
|
254
356
|
const getTimetableNotesForTimetable = async (timetable, config) => {
|
|
255
357
|
const noteReferences = [
|
|
256
358
|
// Get all notes for this timetable.
|
|
257
|
-
...await getTimetableNotesReferences({
|
|
258
|
-
timetable_id: timetable.timetable_id
|
|
259
|
-
}),
|
|
359
|
+
...(await getTimetableNotesReferences({
|
|
360
|
+
timetable_id: timetable.timetable_id,
|
|
361
|
+
})),
|
|
260
362
|
|
|
261
363
|
// Get all notes for this route.
|
|
262
|
-
...await getTimetableNotesReferences({
|
|
263
|
-
route_id: timetable.routes.map(route => route.route_id),
|
|
264
|
-
timetable_id: null
|
|
265
|
-
}),
|
|
364
|
+
...(await getTimetableNotesReferences({
|
|
365
|
+
route_id: timetable.routes.map((route) => route.route_id),
|
|
366
|
+
timetable_id: null,
|
|
367
|
+
})),
|
|
266
368
|
|
|
267
369
|
// Get all notes for all trips in this timetable.
|
|
268
|
-
...await getTimetableNotesReferences({
|
|
269
|
-
trip_id: timetable.orderedTrips.map(trip => trip.trip_id)
|
|
270
|
-
}),
|
|
370
|
+
...(await getTimetableNotesReferences({
|
|
371
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id),
|
|
372
|
+
})),
|
|
271
373
|
|
|
272
374
|
// Get all notes for all stops in this timetable.
|
|
273
|
-
...await getTimetableNotesReferences({
|
|
274
|
-
stop_id: timetable.stops.map(stop => stop.stop_id),
|
|
375
|
+
...(await getTimetableNotesReferences({
|
|
376
|
+
stop_id: timetable.stops.map((stop) => stop.stop_id),
|
|
275
377
|
trip_id: null,
|
|
276
378
|
route_id: null,
|
|
277
|
-
timetable_id: null
|
|
278
|
-
})
|
|
379
|
+
timetable_id: null,
|
|
380
|
+
})),
|
|
279
381
|
];
|
|
280
382
|
|
|
281
383
|
const usedNoteReferences = [];
|
|
282
384
|
// Check if stop_sequence matches any trip.
|
|
283
385
|
for (const noteReference of noteReferences) {
|
|
284
|
-
if (
|
|
386
|
+
if (
|
|
387
|
+
noteReference.stop_sequence === '' ||
|
|
388
|
+
noteReference.stop_sequence === null
|
|
389
|
+
) {
|
|
285
390
|
usedNoteReferences.push(noteReference);
|
|
286
391
|
continue;
|
|
287
392
|
}
|
|
288
393
|
|
|
289
394
|
// Note references with stop_sequence must also have stop_id.
|
|
290
395
|
if (noteReference.stop_id === '' || noteReference.stop_id === null) {
|
|
291
|
-
config.logWarning(
|
|
396
|
+
config.logWarning(
|
|
397
|
+
`Timetable Note Reference for note_id=${noteReference.note_id} has a \`stop_sequence\` but no \`stop_id\` - ignoring`
|
|
398
|
+
);
|
|
292
399
|
continue;
|
|
293
400
|
}
|
|
294
401
|
|
|
295
|
-
const stop = timetable.stops.find(
|
|
402
|
+
const stop = timetable.stops.find(
|
|
403
|
+
(stop) => stop.stop_id === noteReference.stop_id
|
|
404
|
+
);
|
|
296
405
|
|
|
297
406
|
if (!stop) {
|
|
298
407
|
continue;
|
|
299
408
|
}
|
|
300
409
|
|
|
301
|
-
const tripWithMatchingStopSequence = stop.trips.find(
|
|
410
|
+
const tripWithMatchingStopSequence = stop.trips.find(
|
|
411
|
+
(trip) => trip.stop_sequence === noteReference.stop_sequence
|
|
412
|
+
);
|
|
302
413
|
|
|
303
414
|
if (tripWithMatchingStopSequence) {
|
|
304
415
|
usedNoteReferences.push(noteReference);
|
|
@@ -306,7 +417,7 @@ const getTimetableNotesForTimetable = async (timetable, config) => {
|
|
|
306
417
|
}
|
|
307
418
|
|
|
308
419
|
const notes = await getTimetableNotes({
|
|
309
|
-
note_id: usedNoteReferences.map(noteReference => noteReference.note_id)
|
|
420
|
+
note_id: usedNoteReferences.map((noteReference) => noteReference.note_id),
|
|
310
421
|
});
|
|
311
422
|
|
|
312
423
|
// Assign symbols to each note if unassigned. Use a-z then default to integers.
|
|
@@ -314,14 +425,17 @@ const getTimetableNotesForTimetable = async (timetable, config) => {
|
|
|
314
425
|
let symbolIndex = 0;
|
|
315
426
|
for (const note of notes) {
|
|
316
427
|
if (note.symbol === '' || note.symbol === null) {
|
|
317
|
-
note.symbol =
|
|
428
|
+
note.symbol =
|
|
429
|
+
symbolIndex < symbols.length - 1
|
|
430
|
+
? symbols[symbolIndex]
|
|
431
|
+
: symbolIndex - symbols.length;
|
|
318
432
|
symbolIndex += 1;
|
|
319
433
|
}
|
|
320
434
|
}
|
|
321
435
|
|
|
322
|
-
const formattedNotes = usedNoteReferences.map(noteReference => ({
|
|
436
|
+
const formattedNotes = usedNoteReferences.map((noteReference) => ({
|
|
323
437
|
...noteReference,
|
|
324
|
-
...notes.find(note => note.note_id === noteReference.note_id)
|
|
438
|
+
...notes.find((note) => note.note_id === noteReference.note_id),
|
|
325
439
|
}));
|
|
326
440
|
|
|
327
441
|
return sortBy(formattedNotes, 'symbol');
|
|
@@ -334,7 +448,7 @@ const getTimetableNotesForTimetable = async (timetable, config) => {
|
|
|
334
448
|
const convertTimetableToTimetablePage = async (timetable, config) => {
|
|
335
449
|
if (!timetable.routes) {
|
|
336
450
|
timetable.routes = await getRoutes({
|
|
337
|
-
route_id: timetable.route_ids
|
|
451
|
+
route_id: timetable.route_ids,
|
|
338
452
|
});
|
|
339
453
|
}
|
|
340
454
|
|
|
@@ -344,7 +458,7 @@ const convertTimetableToTimetablePage = async (timetable, config) => {
|
|
|
344
458
|
timetable_page_id: timetable.timetable_id,
|
|
345
459
|
timetable_page_label: timetable.timetable_label,
|
|
346
460
|
timetables: [timetable],
|
|
347
|
-
filename
|
|
461
|
+
filename,
|
|
348
462
|
};
|
|
349
463
|
};
|
|
350
464
|
|
|
@@ -353,14 +467,23 @@ const convertTimetableToTimetablePage = async (timetable, config) => {
|
|
|
353
467
|
* is present.
|
|
354
468
|
*/
|
|
355
469
|
/* eslint-disable max-params */
|
|
356
|
-
const convertRouteToTimetablePage = (
|
|
470
|
+
const convertRouteToTimetablePage = (
|
|
471
|
+
route,
|
|
472
|
+
direction,
|
|
473
|
+
calendars,
|
|
474
|
+
calendarDates,
|
|
475
|
+
config
|
|
476
|
+
) => {
|
|
357
477
|
const timetable = {
|
|
358
478
|
route_ids: [route.route_id],
|
|
359
479
|
direction_id: direction ? direction.direction_id : undefined,
|
|
360
480
|
direction_name: direction ? direction.trip_headsign : undefined,
|
|
361
481
|
routes: [route],
|
|
362
|
-
include_exceptions:
|
|
363
|
-
service_id:
|
|
482
|
+
include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
|
|
483
|
+
service_id:
|
|
484
|
+
calendarDates && calendarDates.length > 0
|
|
485
|
+
? calendarDates[0].service_id
|
|
486
|
+
: null,
|
|
364
487
|
service_notes: null,
|
|
365
488
|
timetable_label: null,
|
|
366
489
|
start_time: null,
|
|
@@ -369,7 +492,7 @@ const convertRouteToTimetablePage = (route, direction, calendars, calendarDates,
|
|
|
369
492
|
timetable_sequence: null,
|
|
370
493
|
show_trip_continuation: null,
|
|
371
494
|
start_date: null,
|
|
372
|
-
end_date: null
|
|
495
|
+
end_date: null,
|
|
373
496
|
};
|
|
374
497
|
/* eslint-enable max-params */
|
|
375
498
|
|
|
@@ -377,8 +500,12 @@ const convertRouteToTimetablePage = (route, direction, calendars, calendarDates,
|
|
|
377
500
|
// Get days of week from calendars and assign to timetable.
|
|
378
501
|
Object.assign(timetable, getDaysFromCalendars(calendars));
|
|
379
502
|
|
|
380
|
-
timetable.start_date = toGTFSDate(
|
|
381
|
-
|
|
503
|
+
timetable.start_date = toGTFSDate(
|
|
504
|
+
moment.min(calendars.map((calendar) => fromGTFSDate(calendar.start_date)))
|
|
505
|
+
);
|
|
506
|
+
timetable.end_date = toGTFSDate(
|
|
507
|
+
moment.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
|
|
508
|
+
);
|
|
382
509
|
}
|
|
383
510
|
|
|
384
511
|
timetable.timetable_id = formatTimetableId(timetable);
|
|
@@ -390,31 +517,61 @@ const convertRouteToTimetablePage = (route, direction, calendars, calendarDates,
|
|
|
390
517
|
* Create timetable pages for all routes in an agency. Used if no
|
|
391
518
|
* `timetables.txt` is present.
|
|
392
519
|
*/
|
|
393
|
-
const convertRoutesToTimetablePages = async config => {
|
|
520
|
+
const convertRoutesToTimetablePages = async (config) => {
|
|
394
521
|
const db = getDb();
|
|
395
522
|
const routes = await getRoutes();
|
|
396
523
|
const calendars = await getCalendars();
|
|
397
524
|
|
|
398
525
|
// Find all calendar dates with service_ids not present in `calendar.txt`.
|
|
399
|
-
const serviceIds = calendars.map(calendar => calendar.service_id);
|
|
400
|
-
const calendarDates = await db.all(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
526
|
+
const serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
527
|
+
const calendarDates = await db.all(
|
|
528
|
+
`SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds
|
|
529
|
+
.map((serviceId) => `'${serviceId}'`)
|
|
530
|
+
.join(', ')})`
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const timetablePages = await Promise.all(
|
|
534
|
+
routes.map(async (route) => {
|
|
535
|
+
const trips = await getTrips(
|
|
536
|
+
{
|
|
537
|
+
route_id: route.route_id,
|
|
538
|
+
},
|
|
539
|
+
['trip_headsign', 'direction_id']
|
|
540
|
+
);
|
|
541
|
+
const directions = uniqBy(trips, (trip) => trip.direction_id);
|
|
542
|
+
const dayGroups = groupBy(calendars, calendarToCalendarCode);
|
|
543
|
+
const calendarDateGroups = groupBy(calendarDates, 'service_id');
|
|
544
|
+
|
|
545
|
+
return Promise.all(
|
|
546
|
+
directions.map((direction) =>
|
|
547
|
+
Promise.all([
|
|
548
|
+
Promise.all(
|
|
549
|
+
Object.values(dayGroups).map((calendars) =>
|
|
550
|
+
convertRouteToTimetablePage(
|
|
551
|
+
route,
|
|
552
|
+
direction,
|
|
553
|
+
calendars,
|
|
554
|
+
null,
|
|
555
|
+
config
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
),
|
|
559
|
+
Promise.all(
|
|
560
|
+
Object.values(calendarDateGroups).map((calendarDates) =>
|
|
561
|
+
convertRouteToTimetablePage(
|
|
562
|
+
route,
|
|
563
|
+
direction,
|
|
564
|
+
null,
|
|
565
|
+
calendarDates,
|
|
566
|
+
config
|
|
567
|
+
)
|
|
568
|
+
)
|
|
569
|
+
),
|
|
570
|
+
])
|
|
571
|
+
)
|
|
572
|
+
);
|
|
573
|
+
})
|
|
574
|
+
);
|
|
418
575
|
|
|
419
576
|
return compact(flattenDeep(timetablePages));
|
|
420
577
|
};
|
|
@@ -423,7 +580,9 @@ const convertRoutesToTimetablePages = async config => {
|
|
|
423
580
|
* Generate all trips based on a start trip and an array of frequencies.
|
|
424
581
|
*/
|
|
425
582
|
const generateTripsByFrequencies = (trip, frequencies, config) => {
|
|
426
|
-
const formattedFrequencies = frequencies.map(frequency =>
|
|
583
|
+
const formattedFrequencies = frequencies.map((frequency) =>
|
|
584
|
+
formatFrequency(frequency, config)
|
|
585
|
+
);
|
|
427
586
|
const resetTrip = resetStoptimesToMidnight(trip);
|
|
428
587
|
const trips = [];
|
|
429
588
|
|
|
@@ -431,12 +590,16 @@ const generateTripsByFrequencies = (trip, frequencies, config) => {
|
|
|
431
590
|
const startSeconds = secondsAfterMidnight(frequency.start_time);
|
|
432
591
|
const endSeconds = secondsAfterMidnight(frequency.end_time);
|
|
433
592
|
|
|
434
|
-
for (
|
|
593
|
+
for (
|
|
594
|
+
let offset = startSeconds;
|
|
595
|
+
offset < endSeconds;
|
|
596
|
+
offset += frequency.headway_secs
|
|
597
|
+
) {
|
|
435
598
|
const newTrip = cloneDeep(resetTrip);
|
|
436
599
|
trips.push({
|
|
437
600
|
...newTrip,
|
|
438
601
|
trip_id: `${resetTrip.trip_id}_freq_${trips.length}`,
|
|
439
|
-
stoptimes: updateStoptimesByOffset(newTrip, offset)
|
|
602
|
+
stoptimes: updateStoptimesByOffset(newTrip, offset),
|
|
440
603
|
});
|
|
441
604
|
}
|
|
442
605
|
}
|
|
@@ -448,12 +611,22 @@ const generateTripsByFrequencies = (trip, frequencies, config) => {
|
|
|
448
611
|
* Check if any stoptimes have different arrival and departure times and
|
|
449
612
|
* if they do, duplicate the stop id unless it is the first or last stop.
|
|
450
613
|
*/
|
|
451
|
-
const duplicateStopsForDifferentArrivalDeparture = (
|
|
614
|
+
const duplicateStopsForDifferentArrivalDeparture = (
|
|
615
|
+
stopIds,
|
|
616
|
+
timetable,
|
|
617
|
+
config
|
|
618
|
+
) => {
|
|
452
619
|
for (const trip of timetable.orderedTrips) {
|
|
453
620
|
for (const stoptime of trip.stoptimes) {
|
|
454
|
-
const timepointDifference = fromGTFSTime(stoptime.departure_time).diff(
|
|
455
|
-
|
|
456
|
-
|
|
621
|
+
const timepointDifference = fromGTFSTime(stoptime.departure_time).diff(
|
|
622
|
+
fromGTFSTime(stoptime.arrival_time),
|
|
623
|
+
'minutes'
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
if (
|
|
627
|
+
config.showArrivalOnDifference === null ||
|
|
628
|
+
timepointDifference < config.showArrivalOnDifference
|
|
629
|
+
) {
|
|
457
630
|
continue;
|
|
458
631
|
}
|
|
459
632
|
|
|
@@ -462,7 +635,10 @@ const duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config)
|
|
|
462
635
|
continue;
|
|
463
636
|
}
|
|
464
637
|
|
|
465
|
-
if (
|
|
638
|
+
if (
|
|
639
|
+
stoptime.stop_id === stopIds[index + 1] ||
|
|
640
|
+
stoptime.stop_id === stopIds[index - 1]
|
|
641
|
+
) {
|
|
466
642
|
continue;
|
|
467
643
|
}
|
|
468
644
|
|
|
@@ -478,18 +654,18 @@ const duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config)
|
|
|
478
654
|
*/
|
|
479
655
|
const getStopOrder = async (timetable, config) => {
|
|
480
656
|
// First, check if `timetable_stop_order.txt` for route exists
|
|
481
|
-
const timetableStopOrders = await getTimetableStopOrders(
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
'stop_id'
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
['stop_sequence', 'ASC']
|
|
489
|
-
]);
|
|
657
|
+
const timetableStopOrders = await getTimetableStopOrders(
|
|
658
|
+
{
|
|
659
|
+
timetable_id: timetable.timetable_id,
|
|
660
|
+
},
|
|
661
|
+
['stop_id'],
|
|
662
|
+
[['stop_sequence', 'ASC']]
|
|
663
|
+
);
|
|
490
664
|
|
|
491
665
|
if (timetableStopOrders.length > 0) {
|
|
492
|
-
return timetableStopOrders.map(
|
|
666
|
+
return timetableStopOrders.map(
|
|
667
|
+
(timetableStopOrder) => timetableStopOrder.stop_id
|
|
668
|
+
);
|
|
493
669
|
}
|
|
494
670
|
|
|
495
671
|
// Next, try using a directed graph to determine stop order.
|
|
@@ -498,7 +674,12 @@ const getStopOrder = async (timetable, config) => {
|
|
|
498
674
|
|
|
499
675
|
for (const trip of timetable.orderedTrips) {
|
|
500
676
|
// If `showOnlyTimepoint` is true, then filter out all non-timepoints.
|
|
501
|
-
const sortedStopIds =
|
|
677
|
+
const sortedStopIds =
|
|
678
|
+
config.showOnlyTimepoint === true
|
|
679
|
+
? trip.stoptimes
|
|
680
|
+
.filter((stoptime) => isTimepoint(stoptime))
|
|
681
|
+
.map((stoptime) => stoptime.stop_id)
|
|
682
|
+
: trip.stoptimes.map((stoptime) => stoptime.stop_id);
|
|
502
683
|
|
|
503
684
|
for (const [index, stopId] of sortedStopIds.entries()) {
|
|
504
685
|
if (index === sortedStopIds.length - 1) {
|
|
@@ -511,14 +692,21 @@ const getStopOrder = async (timetable, config) => {
|
|
|
511
692
|
|
|
512
693
|
const stopIds = toposort(stopGraph);
|
|
513
694
|
|
|
514
|
-
return duplicateStopsForDifferentArrivalDeparture(
|
|
695
|
+
return duplicateStopsForDifferentArrivalDeparture(
|
|
696
|
+
stopIds,
|
|
697
|
+
timetable,
|
|
698
|
+
config
|
|
699
|
+
);
|
|
515
700
|
} catch {
|
|
516
701
|
// Ignore errors and move to next strategy.
|
|
517
702
|
}
|
|
518
703
|
|
|
519
704
|
// Finally, fall back to using the stop order from the trip with the most stoptimes.
|
|
520
|
-
const longestTripStoptimes = getLongestTripStoptimes(
|
|
521
|
-
|
|
705
|
+
const longestTripStoptimes = getLongestTripStoptimes(
|
|
706
|
+
timetable.orderedTrips,
|
|
707
|
+
config
|
|
708
|
+
);
|
|
709
|
+
const stopIds = longestTripStoptimes.map((stoptime) => stoptime.stop_id);
|
|
522
710
|
|
|
523
711
|
return duplicateStopsForDifferentArrivalDeparture(stopIds, timetable, config);
|
|
524
712
|
};
|
|
@@ -532,37 +720,46 @@ const getStopsForTimetable = async (timetable, config) => {
|
|
|
532
720
|
}
|
|
533
721
|
|
|
534
722
|
const orderedStopIds = await getStopOrder(timetable, config);
|
|
535
|
-
const orderedStops = await Promise.all(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
if (stops.length === 0) {
|
|
541
|
-
throw new Error(`No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`);
|
|
542
|
-
}
|
|
723
|
+
const orderedStops = await Promise.all(
|
|
724
|
+
orderedStopIds.map(async (stopId, index) => {
|
|
725
|
+
const stops = await getStops({
|
|
726
|
+
stop_id: stopId,
|
|
727
|
+
});
|
|
543
728
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
729
|
+
if (stops.length === 0) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
`No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`
|
|
732
|
+
);
|
|
733
|
+
}
|
|
548
734
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
735
|
+
const stop = {
|
|
736
|
+
...stops[0],
|
|
737
|
+
trips: [],
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
if (
|
|
741
|
+
index < orderedStopIds.length - 1 &&
|
|
742
|
+
stopId === orderedStopIds[index + 1]
|
|
743
|
+
) {
|
|
744
|
+
stop.type = 'arrival';
|
|
745
|
+
} else if (index > 0 && stopId === orderedStopIds[index - 1]) {
|
|
746
|
+
stop.type = 'departure';
|
|
747
|
+
}
|
|
554
748
|
|
|
555
|
-
|
|
556
|
-
|
|
749
|
+
return stop;
|
|
750
|
+
})
|
|
751
|
+
);
|
|
557
752
|
|
|
558
753
|
// If `showStopCity` is true, look up stop attributes.
|
|
559
754
|
if (timetable.showStopCity) {
|
|
560
755
|
const stopAttributes = await getStopAttributes({
|
|
561
|
-
stop_id: orderedStopIds
|
|
756
|
+
stop_id: orderedStopIds,
|
|
562
757
|
});
|
|
563
758
|
|
|
564
759
|
for (const stopAttribute of stopAttributes) {
|
|
565
|
-
const stop = orderedStops.find(
|
|
760
|
+
const stop = orderedStops.find(
|
|
761
|
+
(stop) => stop.stop_id === stopAttribute.stop_id
|
|
762
|
+
);
|
|
566
763
|
|
|
567
764
|
if (stop) {
|
|
568
765
|
stop.stop_city = stopAttribute.stop_city;
|
|
@@ -576,7 +773,7 @@ const getStopsForTimetable = async (timetable, config) => {
|
|
|
576
773
|
/*
|
|
577
774
|
* Get all calendars from a specific timetable.
|
|
578
775
|
*/
|
|
579
|
-
const getCalendarsFromTimetable = async timetable => {
|
|
776
|
+
const getCalendarsFromTimetable = async (timetable) => {
|
|
580
777
|
const db = getDb();
|
|
581
778
|
let whereClause = '';
|
|
582
779
|
const whereClauses = [];
|
|
@@ -591,13 +788,17 @@ const getCalendarsFromTimetable = async timetable => {
|
|
|
591
788
|
|
|
592
789
|
const days = getDaysFromCalendars([timetable]);
|
|
593
790
|
// Create an 'OR' query array of days based on calendars.
|
|
594
|
-
const dayQueries = reduce(
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
791
|
+
const dayQueries = reduce(
|
|
792
|
+
days,
|
|
793
|
+
(memo, value, key) => {
|
|
794
|
+
if (value === 1) {
|
|
795
|
+
memo.push(`${key} = 1`);
|
|
796
|
+
}
|
|
598
797
|
|
|
599
|
-
|
|
600
|
-
|
|
798
|
+
return memo;
|
|
799
|
+
},
|
|
800
|
+
[]
|
|
801
|
+
);
|
|
601
802
|
|
|
602
803
|
if (dayQueries.length > 0) {
|
|
603
804
|
whereClauses.push(`(${dayQueries.join(' OR ')})`);
|
|
@@ -625,8 +826,12 @@ const getCalendarDatesServiceIds = async (startDate, endDate) => {
|
|
|
625
826
|
whereClauses.push(`date >= ${sqlString.escape(startDate)}`);
|
|
626
827
|
}
|
|
627
828
|
|
|
628
|
-
const calendarDates = await db.all(
|
|
629
|
-
|
|
829
|
+
const calendarDates = await db.all(
|
|
830
|
+
`SELECT DISTINCT service_id FROM calendar_dates WHERE ${whereClauses.join(
|
|
831
|
+
' AND '
|
|
832
|
+
)}`
|
|
833
|
+
);
|
|
834
|
+
return calendarDates.map((calendarDate) => calendarDate.service_id);
|
|
630
835
|
};
|
|
631
836
|
|
|
632
837
|
/*
|
|
@@ -634,9 +839,9 @@ const getCalendarDatesServiceIds = async (startDate, endDate) => {
|
|
|
634
839
|
* and the stop_id of parent station itself. If no parent station, it returns the
|
|
635
840
|
* stop_id.
|
|
636
841
|
*/
|
|
637
|
-
const getAllStationStopIds = async stopId => {
|
|
842
|
+
const getAllStationStopIds = async (stopId) => {
|
|
638
843
|
const stops = await getStops({
|
|
639
|
-
stop_id: stopId
|
|
844
|
+
stop_id: stopId,
|
|
640
845
|
});
|
|
641
846
|
|
|
642
847
|
const stop = stops[0];
|
|
@@ -645,44 +850,53 @@ const getAllStationStopIds = async stopId => {
|
|
|
645
850
|
return [stopId];
|
|
646
851
|
}
|
|
647
852
|
|
|
648
|
-
const stopsInParentStation = await getStops(
|
|
649
|
-
|
|
650
|
-
|
|
853
|
+
const stopsInParentStation = await getStops(
|
|
854
|
+
{
|
|
855
|
+
parent_station: stop.parent_station,
|
|
856
|
+
},
|
|
857
|
+
['stop_id']
|
|
858
|
+
);
|
|
651
859
|
|
|
652
|
-
return [
|
|
860
|
+
return [
|
|
861
|
+
stop.parent_station,
|
|
862
|
+
...stopsInParentStation.map((stop) => stop.stop_id),
|
|
863
|
+
];
|
|
653
864
|
};
|
|
654
865
|
|
|
655
866
|
/*
|
|
656
867
|
* Get trips with the same `block_id`.
|
|
657
868
|
*/
|
|
658
869
|
const getTripsWithSameBlock = async (trip, timetable) => {
|
|
659
|
-
const trips = await getTrips(
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
'trip_id',
|
|
664
|
-
'route_id'
|
|
665
|
-
]);
|
|
666
|
-
|
|
667
|
-
await Promise.all(trips.map(async blockTrip => {
|
|
668
|
-
const stopTimes = await getStoptimes({
|
|
669
|
-
trip_id: blockTrip.trip_id
|
|
870
|
+
const trips = await getTrips(
|
|
871
|
+
{
|
|
872
|
+
block_id: trip.block_id,
|
|
873
|
+
service_id: timetable.service_ids,
|
|
670
874
|
},
|
|
671
|
-
[]
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
875
|
+
['trip_id', 'route_id']
|
|
876
|
+
);
|
|
877
|
+
|
|
878
|
+
await Promise.all(
|
|
879
|
+
trips.map(async (blockTrip) => {
|
|
880
|
+
const stopTimes = await getStoptimes(
|
|
881
|
+
{
|
|
882
|
+
trip_id: blockTrip.trip_id,
|
|
883
|
+
},
|
|
884
|
+
[],
|
|
885
|
+
[['stop_sequence', 'ASC']]
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
if (stopTimes.length === 0) {
|
|
889
|
+
throw new Error(
|
|
890
|
+
`No stoptimes found found for trip_id=${blockTrip.trip_id}`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
680
893
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
894
|
+
blockTrip.firstStoptime = first(stopTimes);
|
|
895
|
+
blockTrip.lastStoptime = last(stopTimes);
|
|
896
|
+
})
|
|
897
|
+
);
|
|
684
898
|
|
|
685
|
-
return sortBy(trips, trip => trip.firstStoptime.departure_timestamp);
|
|
899
|
+
return sortBy(trips, (trip) => trip.firstStoptime.departure_timestamp);
|
|
686
900
|
};
|
|
687
901
|
|
|
688
902
|
/*
|
|
@@ -703,7 +917,12 @@ const addTripContinuation = async (trip, timetable) => {
|
|
|
703
917
|
const blockTrips = await getTripsWithSameBlock(trip, timetable);
|
|
704
918
|
|
|
705
919
|
// "Continues From" trips must be the previous trip chronologically.
|
|
706
|
-
const previousTrip = findLast(
|
|
920
|
+
const previousTrip = findLast(
|
|
921
|
+
blockTrips,
|
|
922
|
+
(blockTrip) =>
|
|
923
|
+
blockTrip.lastStoptime.arrival_timestamp <=
|
|
924
|
+
firstStoptime.departure_timestamp
|
|
925
|
+
);
|
|
707
926
|
|
|
708
927
|
/*
|
|
709
928
|
* "Continues From" trips
|
|
@@ -711,9 +930,15 @@ const addTripContinuation = async (trip, timetable) => {
|
|
|
711
930
|
* * must not be more than 60 minutes before
|
|
712
931
|
* * must have their last stop_id be the same as the next trip's first stop_id
|
|
713
932
|
*/
|
|
714
|
-
if (
|
|
933
|
+
if (
|
|
934
|
+
previousTrip &&
|
|
935
|
+
previousTrip.route_id !== trip.route_id &&
|
|
936
|
+
previousTrip.lastStoptime.arrival_timestamp >=
|
|
937
|
+
firstStoptime.departure_timestamp - maxContinuesAsWaitingTimeSeconds &&
|
|
938
|
+
firstStopIds.includes(previousTrip.lastStoptime.stop_id)
|
|
939
|
+
) {
|
|
715
940
|
const routes = await getRoutes({
|
|
716
|
-
route_id: previousTrip.route_id
|
|
941
|
+
route_id: previousTrip.route_id,
|
|
717
942
|
});
|
|
718
943
|
|
|
719
944
|
previousTrip.route = routes[0];
|
|
@@ -722,7 +947,12 @@ const addTripContinuation = async (trip, timetable) => {
|
|
|
722
947
|
}
|
|
723
948
|
|
|
724
949
|
// "Continues As" trips must be the next trip chronologically.
|
|
725
|
-
const nextTrip = find(
|
|
950
|
+
const nextTrip = find(
|
|
951
|
+
blockTrips,
|
|
952
|
+
(blockTrip) =>
|
|
953
|
+
blockTrip.firstStoptime.departure_timestamp >=
|
|
954
|
+
lastStoptime.arrival_timestamp
|
|
955
|
+
);
|
|
726
956
|
|
|
727
957
|
// "Continues As" trips must be a different route_id.
|
|
728
958
|
/*
|
|
@@ -731,9 +961,15 @@ const addTripContinuation = async (trip, timetable) => {
|
|
|
731
961
|
* * must not be more than 60 minutes later
|
|
732
962
|
* * must have their first stop_id be the same as the previous trip's last stop_id
|
|
733
963
|
*/
|
|
734
|
-
if (
|
|
964
|
+
if (
|
|
965
|
+
nextTrip &&
|
|
966
|
+
nextTrip.route_id !== trip.route_id &&
|
|
967
|
+
nextTrip.firstStoptime.departure_timestamp <=
|
|
968
|
+
lastStoptime.arrival_timestamp + maxContinuesAsWaitingTimeSeconds &&
|
|
969
|
+
lastStopIds.includes(nextTrip.firstStoptime.stop_id)
|
|
970
|
+
) {
|
|
735
971
|
const routes = await getRoutes({
|
|
736
|
-
route_id: nextTrip.route_id
|
|
972
|
+
route_id: nextTrip.route_id,
|
|
737
973
|
});
|
|
738
974
|
|
|
739
975
|
nextTrip.route = routes[0];
|
|
@@ -745,17 +981,19 @@ const addTripContinuation = async (trip, timetable) => {
|
|
|
745
981
|
* Apply time range filters to trips and remove trips with less than two stoptimes for stops used in this timetable.
|
|
746
982
|
* Stops can be excluded by using `timetable_stop_order.txt`. Additionally, remove trip stoptimes for unused stops.
|
|
747
983
|
*/
|
|
748
|
-
const filterTrips = timetable => {
|
|
984
|
+
const filterTrips = (timetable) => {
|
|
749
985
|
let filteredTrips = timetable.orderedTrips;
|
|
750
986
|
|
|
751
987
|
// Remove stoptimes for stops not used in timetable
|
|
752
|
-
const timetableStopIds = new Set(timetable.stops.map(stop => stop.stop_id));
|
|
988
|
+
const timetableStopIds = new Set(timetable.stops.map((stop) => stop.stop_id));
|
|
753
989
|
for (const trip of filteredTrips) {
|
|
754
|
-
trip.stoptimes = trip.stoptimes.filter(stoptime =>
|
|
990
|
+
trip.stoptimes = trip.stoptimes.filter((stoptime) =>
|
|
991
|
+
timetableStopIds.has(stoptime.stop_id)
|
|
992
|
+
);
|
|
755
993
|
}
|
|
756
994
|
|
|
757
995
|
// Exclude trips with less than two stops
|
|
758
|
-
filteredTrips = filteredTrips.filter(trip => trip.stoptimes.length > 1);
|
|
996
|
+
filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
|
|
759
997
|
|
|
760
998
|
return filteredTrips;
|
|
761
999
|
};
|
|
@@ -766,7 +1004,7 @@ const filterTrips = timetable => {
|
|
|
766
1004
|
const getTripsForTimetable = async (timetable, calendars, config) => {
|
|
767
1005
|
const tripQuery = {
|
|
768
1006
|
route_id: timetable.route_ids,
|
|
769
|
-
service_id: timetable.service_ids
|
|
1007
|
+
service_id: timetable.service_ids,
|
|
770
1008
|
};
|
|
771
1009
|
|
|
772
1010
|
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
@@ -776,70 +1014,98 @@ const getTripsForTimetable = async (timetable, calendars, config) => {
|
|
|
776
1014
|
const trips = await getTrips(tripQuery);
|
|
777
1015
|
|
|
778
1016
|
if (trips.length === 0) {
|
|
779
|
-
timetable.warnings.push(
|
|
1017
|
+
timetable.warnings.push(
|
|
1018
|
+
`No trips found for route_id=${timetable.route_ids.join(
|
|
1019
|
+
'_'
|
|
1020
|
+
)}, direction_id=${timetable.direction_id}, service_ids=${JSON.stringify(
|
|
1021
|
+
timetable.service_ids
|
|
1022
|
+
)}, timetable_id=${timetable.timetable_id}`
|
|
1023
|
+
);
|
|
780
1024
|
}
|
|
781
1025
|
|
|
782
1026
|
const frequencies = await getFrequencies({
|
|
783
|
-
trip_id: trips.map(trip => trip.trip_id)
|
|
1027
|
+
trip_id: trips.map((trip) => trip.trip_id),
|
|
784
1028
|
});
|
|
785
1029
|
|
|
786
1030
|
// Updated timetable.serviceIds with only the service IDs actually used in one or more trip.
|
|
787
|
-
timetable.service_ids = uniq(trips.map(trip => trip.service_id));
|
|
1031
|
+
timetable.service_ids = uniq(trips.map((trip) => trip.service_id));
|
|
788
1032
|
|
|
789
1033
|
const formattedTrips = [];
|
|
790
|
-
await Promise.all(
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
1034
|
+
await Promise.all(
|
|
1035
|
+
trips.map(async (trip) => {
|
|
1036
|
+
const formattedTrip = formatTrip(trip, timetable, calendars, config);
|
|
1037
|
+
formattedTrip.stoptimes = await getStoptimes(
|
|
1038
|
+
{
|
|
1039
|
+
trip_id: formattedTrip.trip_id,
|
|
1040
|
+
},
|
|
1041
|
+
[],
|
|
1042
|
+
[['stop_sequence', 'ASC']]
|
|
1043
|
+
);
|
|
1044
|
+
|
|
1045
|
+
if (formattedTrip.stoptimes.length === 0) {
|
|
1046
|
+
timetable.warnings.push(
|
|
1047
|
+
`No stoptimes found for trip_id=${
|
|
1048
|
+
formattedTrip.trip_id
|
|
1049
|
+
}, route_id=${timetable.route_ids.join('_')}, timetable_id=${
|
|
1050
|
+
timetable.timetable_id
|
|
1051
|
+
}`
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
803
1054
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
1055
|
+
// Exclude trips before timetable `start_timestamp`
|
|
1056
|
+
if (
|
|
1057
|
+
timetable.start_timestamp !== '' &&
|
|
1058
|
+
timetable.start_timestamp !== null &&
|
|
1059
|
+
timetable.start_timestamp !== undefined
|
|
1060
|
+
) {
|
|
1061
|
+
if (trip.stoptimes[0].arrival_timestamp < timetable.start_timestamp) {
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
808
1064
|
}
|
|
809
|
-
}
|
|
810
1065
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
1066
|
+
// Exclude trips after timetable `end_timestamp`
|
|
1067
|
+
if (
|
|
1068
|
+
timetable.end_timestamp !== '' &&
|
|
1069
|
+
timetable.end_timestamp !== null &&
|
|
1070
|
+
timetable.end_timestamp !== undefined
|
|
1071
|
+
) {
|
|
1072
|
+
if (trip.stoptimes[0].arrival_timestamp >= timetable.end_timestamp) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
815
1075
|
}
|
|
816
|
-
}
|
|
817
1076
|
|
|
818
|
-
|
|
819
|
-
|
|
1077
|
+
if (timetable.show_trip_continuation) {
|
|
1078
|
+
await addTripContinuation(formattedTrip, timetable);
|
|
820
1079
|
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1080
|
+
if (formattedTrip.continues_as_route) {
|
|
1081
|
+
timetable.has_continues_as_route = true;
|
|
1082
|
+
}
|
|
824
1083
|
|
|
825
|
-
|
|
826
|
-
|
|
1084
|
+
if (formattedTrip.continues_from_route) {
|
|
1085
|
+
timetable.has_continues_from_route = true;
|
|
1086
|
+
}
|
|
827
1087
|
}
|
|
828
|
-
}
|
|
829
1088
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
1089
|
+
const tripFrequencies = frequencies.filter(
|
|
1090
|
+
(frequency) => frequency.trip_id === trip.trip_id
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
if (tripFrequencies.length === 0) {
|
|
1094
|
+
formattedTrips.push(formattedTrip);
|
|
1095
|
+
} else {
|
|
1096
|
+
const frequencyTrips = generateTripsByFrequencies(
|
|
1097
|
+
formattedTrip,
|
|
1098
|
+
frequencies,
|
|
1099
|
+
config
|
|
1100
|
+
);
|
|
1101
|
+
formattedTrips.push(...frequencyTrips);
|
|
1102
|
+
timetable.frequencies = frequencies;
|
|
1103
|
+
timetable.frequencyExactTimes = some(frequencies, {
|
|
1104
|
+
exact_times: 1,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
})
|
|
1108
|
+
);
|
|
843
1109
|
|
|
844
1110
|
if (config.useParentStation) {
|
|
845
1111
|
const stopIds = [];
|
|
@@ -850,18 +1116,20 @@ const getTripsForTimetable = async (timetable, calendars, config) => {
|
|
|
850
1116
|
}
|
|
851
1117
|
}
|
|
852
1118
|
|
|
853
|
-
const stops = await getStops(
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
'parent_station',
|
|
858
|
-
|
|
859
|
-
]);
|
|
1119
|
+
const stops = await getStops(
|
|
1120
|
+
{
|
|
1121
|
+
stop_id: uniq(stopIds),
|
|
1122
|
+
},
|
|
1123
|
+
['parent_station', 'stop_id']
|
|
1124
|
+
);
|
|
860
1125
|
|
|
861
1126
|
for (const trip of formattedTrips) {
|
|
862
1127
|
for (const stoptime of trip.stoptimes) {
|
|
863
|
-
const parentStationStop = stops.find(
|
|
864
|
-
|
|
1128
|
+
const parentStationStop = stops.find(
|
|
1129
|
+
(stop) => stop.stop_id === stoptime.stop_id
|
|
1130
|
+
);
|
|
1131
|
+
stoptime.stop_id =
|
|
1132
|
+
parentStationStop.parent_station || parentStationStop.stop_id;
|
|
865
1133
|
}
|
|
866
1134
|
}
|
|
867
1135
|
}
|
|
@@ -873,62 +1141,76 @@ const getTripsForTimetable = async (timetable, calendars, config) => {
|
|
|
873
1141
|
* Format timetables for display.
|
|
874
1142
|
*/
|
|
875
1143
|
const formatTimetables = async (timetables, config) => {
|
|
876
|
-
const formattedTimetables = await Promise.all(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
noDropoffSymbolUsed: false,
|
|
891
|
-
requestPickupSymbolUsed: false,
|
|
892
|
-
noPickupSymbolUsed: false,
|
|
893
|
-
interpolatedStopSymbolUsed: false,
|
|
894
|
-
showStopCity: config.showStopCity,
|
|
895
|
-
showStopDescription: config.showStopDescription,
|
|
896
|
-
noServiceSymbol: config.noServiceSymbol,
|
|
897
|
-
requestDropoffSymbol: config.requestDropoffSymbol,
|
|
898
|
-
noDropoffSymbol: config.noDropoffSymbol,
|
|
899
|
-
requestPickupSymbol: config.requestPickupSymbol,
|
|
900
|
-
noPickupSymbol: config.noPickupSymbol,
|
|
901
|
-
interpolatedStopSymbol: config.interpolatedStopSymbol,
|
|
902
|
-
orientation: timetable.orientation || config.defaultOrientation,
|
|
903
|
-
service_ids: serviceIds,
|
|
904
|
-
dayList,
|
|
905
|
-
dayListLong: formatDaysLong(dayList, config)
|
|
906
|
-
});
|
|
1144
|
+
const formattedTimetables = await Promise.all(
|
|
1145
|
+
timetables.map(async (timetable) => {
|
|
1146
|
+
timetable.warnings = [];
|
|
1147
|
+
const dayList = formatDays(timetable, config);
|
|
1148
|
+
const calendars = await getCalendarsFromTimetable(timetable);
|
|
1149
|
+
let serviceIds = calendars.map((calendar) => calendar.service_id);
|
|
1150
|
+
|
|
1151
|
+
if (timetable.include_exceptions === 1) {
|
|
1152
|
+
const calendarDatesServiceIds = await getCalendarDatesServiceIds(
|
|
1153
|
+
timetable.start_date,
|
|
1154
|
+
timetable.end_date
|
|
1155
|
+
);
|
|
1156
|
+
serviceIds = uniq([...serviceIds, ...calendarDatesServiceIds]);
|
|
1157
|
+
}
|
|
907
1158
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1159
|
+
Object.assign(timetable, {
|
|
1160
|
+
noServiceSymbolUsed: false,
|
|
1161
|
+
requestDropoffSymbolUsed: false,
|
|
1162
|
+
noDropoffSymbolUsed: false,
|
|
1163
|
+
requestPickupSymbolUsed: false,
|
|
1164
|
+
noPickupSymbolUsed: false,
|
|
1165
|
+
interpolatedStopSymbolUsed: false,
|
|
1166
|
+
showStopCity: config.showStopCity,
|
|
1167
|
+
showStopDescription: config.showStopDescription,
|
|
1168
|
+
noServiceSymbol: config.noServiceSymbol,
|
|
1169
|
+
requestDropoffSymbol: config.requestDropoffSymbol,
|
|
1170
|
+
noDropoffSymbol: config.noDropoffSymbol,
|
|
1171
|
+
requestPickupSymbol: config.requestPickupSymbol,
|
|
1172
|
+
noPickupSymbol: config.noPickupSymbol,
|
|
1173
|
+
interpolatedStopSymbol: config.interpolatedStopSymbol,
|
|
1174
|
+
orientation: timetable.orientation || config.defaultOrientation,
|
|
1175
|
+
service_ids: serviceIds,
|
|
1176
|
+
dayList,
|
|
1177
|
+
dayListLong: formatDaysLong(dayList, config),
|
|
1178
|
+
});
|
|
913
1179
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1180
|
+
timetable.orderedTrips = await getTripsForTimetable(
|
|
1181
|
+
timetable,
|
|
1182
|
+
calendars,
|
|
1183
|
+
config
|
|
1184
|
+
);
|
|
1185
|
+
timetable.stops = await getStopsForTimetable(timetable, config);
|
|
1186
|
+
timetable.calendarDates = await getCalendarDatesForTimetable(
|
|
1187
|
+
timetable,
|
|
1188
|
+
config
|
|
1189
|
+
);
|
|
1190
|
+
timetable.timetable_label = formatTimetableLabel(timetable);
|
|
1191
|
+
timetable.notes = await getTimetableNotesForTimetable(timetable, config);
|
|
1192
|
+
|
|
1193
|
+
if (config.showMap) {
|
|
1194
|
+
timetable.geojson = await getTimetableGeoJSON(timetable, config);
|
|
1195
|
+
}
|
|
917
1196
|
|
|
918
|
-
|
|
919
|
-
|
|
1197
|
+
// Filter trips after all timetable properties are assigned
|
|
1198
|
+
timetable.orderedTrips = filterTrips(timetable);
|
|
920
1199
|
|
|
921
|
-
|
|
922
|
-
|
|
1200
|
+
// Format stops after all timetable properties are assigned
|
|
1201
|
+
timetable.stops = formatStops(timetable, config);
|
|
923
1202
|
|
|
924
|
-
|
|
925
|
-
|
|
1203
|
+
return timetable;
|
|
1204
|
+
})
|
|
1205
|
+
);
|
|
926
1206
|
|
|
927
1207
|
if (config.allowEmptyTimetables) {
|
|
928
1208
|
return formattedTimetables;
|
|
929
1209
|
}
|
|
930
1210
|
|
|
931
|
-
return formattedTimetables.filter(
|
|
1211
|
+
return formattedTimetables.filter(
|
|
1212
|
+
(timetable) => timetable.orderedTrips.length > 0
|
|
1213
|
+
);
|
|
932
1214
|
};
|
|
933
1215
|
|
|
934
1216
|
/*
|
|
@@ -942,29 +1224,45 @@ export async function getTimetablePagesForAgency(config) {
|
|
|
942
1224
|
return convertRoutesToTimetablePages(config);
|
|
943
1225
|
}
|
|
944
1226
|
|
|
945
|
-
const timetablePages = await getTimetablePages(
|
|
946
|
-
|
|
947
|
-
|
|
1227
|
+
const timetablePages = await getTimetablePages(
|
|
1228
|
+
{},
|
|
1229
|
+
[],
|
|
1230
|
+
[['timetable_page_id', 'ASC']]
|
|
1231
|
+
);
|
|
948
1232
|
|
|
949
1233
|
// Check if there are any timetable pages defined in `timetable_pages.txt`.
|
|
950
1234
|
if (timetablePages.length === 0) {
|
|
951
1235
|
// If no timetablepages, use timetables
|
|
952
|
-
return Promise.all(
|
|
1236
|
+
return Promise.all(
|
|
1237
|
+
timetables.map((timetable) =>
|
|
1238
|
+
convertTimetableToTimetablePage(timetable, config)
|
|
1239
|
+
)
|
|
1240
|
+
);
|
|
953
1241
|
}
|
|
954
1242
|
|
|
955
1243
|
const routes = await getRoutes();
|
|
956
1244
|
|
|
957
1245
|
// Otherwise, use timetable pages defined in `timetable_pages.txt`.
|
|
958
|
-
return Promise.all(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1246
|
+
return Promise.all(
|
|
1247
|
+
timetablePages.map(async (timetablePage) => {
|
|
1248
|
+
timetablePage.timetables = sortBy(
|
|
1249
|
+
timetables.filter(
|
|
1250
|
+
(timetable) =>
|
|
1251
|
+
timetable.timetable_page_id === timetablePage.timetable_page_id
|
|
1252
|
+
),
|
|
1253
|
+
'timetable_sequence'
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
// Add routes for each timetable.
|
|
1257
|
+
for (const timetable of timetablePage.timetables) {
|
|
1258
|
+
timetable.routes = routes.filter((route) =>
|
|
1259
|
+
timetable.route_ids.includes(route.route_id)
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
965
1262
|
|
|
966
|
-
|
|
967
|
-
|
|
1263
|
+
return timetablePage;
|
|
1264
|
+
})
|
|
1265
|
+
);
|
|
968
1266
|
}
|
|
969
1267
|
|
|
970
1268
|
/*
|
|
@@ -973,36 +1271,49 @@ export async function getTimetablePagesForAgency(config) {
|
|
|
973
1271
|
const getTimetablePageById = async (timetablePageId, config) => {
|
|
974
1272
|
// Check if there are any timetable pages defined in `timetable_pages.txt`.
|
|
975
1273
|
const timetablePages = await getTimetablePages({
|
|
976
|
-
timetable_page_id: timetablePageId
|
|
1274
|
+
timetable_page_id: timetablePageId,
|
|
977
1275
|
});
|
|
978
1276
|
|
|
979
1277
|
const timetables = mergeTimetablesWithSameId(await getTimetables());
|
|
980
1278
|
|
|
981
1279
|
if (timetablePages.length > 1) {
|
|
982
|
-
throw new Error(
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
|
|
1282
|
+
);
|
|
983
1283
|
}
|
|
984
1284
|
|
|
985
1285
|
if (timetablePages.length === 1) {
|
|
986
1286
|
// Use timetablePage defined in `timetable_pages.txt`.
|
|
987
1287
|
const timetablePage = timetablePages[0];
|
|
988
|
-
timetablePage.timetables = sortBy(
|
|
1288
|
+
timetablePage.timetables = sortBy(
|
|
1289
|
+
timetables.filter(
|
|
1290
|
+
(timetable) => timetable.timetable_page_id === timetablePageId
|
|
1291
|
+
),
|
|
1292
|
+
'timetable_sequence'
|
|
1293
|
+
);
|
|
989
1294
|
|
|
990
1295
|
// Add routes for each timetable
|
|
991
|
-
await Promise.all(
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1296
|
+
await Promise.all(
|
|
1297
|
+
timetablePage.timetables.map(async (timetable) => {
|
|
1298
|
+
timetable.routes = await getRoutes({
|
|
1299
|
+
route_id: timetable.route_ids,
|
|
1300
|
+
});
|
|
1301
|
+
})
|
|
1302
|
+
);
|
|
996
1303
|
|
|
997
1304
|
return timetablePage;
|
|
998
1305
|
}
|
|
999
1306
|
|
|
1000
1307
|
if (timetables.length > 0) {
|
|
1001
1308
|
// If no timetable_page, use timetable defined in `timetables.txt`.
|
|
1002
|
-
const timetablePageTimetables = timetables.filter(
|
|
1309
|
+
const timetablePageTimetables = timetables.filter(
|
|
1310
|
+
(timetable) => timetable.timetable_id === timetablePageId
|
|
1311
|
+
);
|
|
1003
1312
|
|
|
1004
1313
|
if (timetablePageTimetables.length === 0) {
|
|
1005
|
-
throw new Error(
|
|
1314
|
+
throw new Error(
|
|
1315
|
+
`No timetable found for timetable_page_id=${timetablePageId}`
|
|
1316
|
+
);
|
|
1006
1317
|
}
|
|
1007
1318
|
|
|
1008
1319
|
return convertTimetableToTimetablePage(timetablePageTimetables[0], config);
|
|
@@ -1026,35 +1337,43 @@ const getTimetablePageById = async (timetablePageId, config) => {
|
|
|
1026
1337
|
const routeId = parts.join('|');
|
|
1027
1338
|
|
|
1028
1339
|
const routes = await getRoutes({
|
|
1029
|
-
route_id: routeId
|
|
1340
|
+
route_id: routeId,
|
|
1030
1341
|
});
|
|
1031
1342
|
|
|
1032
|
-
const trips = await getTrips(
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
'direction_id'
|
|
1038
|
-
|
|
1039
|
-
const directions = uniqBy(trips, trip => trip.direction_id);
|
|
1343
|
+
const trips = await getTrips(
|
|
1344
|
+
{
|
|
1345
|
+
route_id: routeId,
|
|
1346
|
+
direction_id: directionId,
|
|
1347
|
+
},
|
|
1348
|
+
['trip_headsign', 'direction_id']
|
|
1349
|
+
);
|
|
1350
|
+
const directions = uniqBy(trips, (trip) => trip.direction_id);
|
|
1040
1351
|
|
|
1041
1352
|
if (directions.length === 0) {
|
|
1042
|
-
throw new Error(
|
|
1353
|
+
throw new Error(
|
|
1354
|
+
`No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
|
|
1355
|
+
);
|
|
1043
1356
|
}
|
|
1044
1357
|
|
|
1045
1358
|
if (/^[01]*$/.test(calendarCode)) {
|
|
1046
1359
|
calendars = await getCalendars({
|
|
1047
|
-
...calendarCodeToCalendar(calendarCode)
|
|
1360
|
+
...calendarCodeToCalendar(calendarCode),
|
|
1048
1361
|
});
|
|
1049
1362
|
} else {
|
|
1050
1363
|
serviceId = calendarCode;
|
|
1051
1364
|
calendarDates = await getCalendarDates({
|
|
1052
1365
|
exception_type: 1,
|
|
1053
|
-
service_id: serviceId
|
|
1366
|
+
service_id: serviceId,
|
|
1054
1367
|
});
|
|
1055
1368
|
}
|
|
1056
1369
|
|
|
1057
|
-
return convertRouteToTimetablePage(
|
|
1370
|
+
return convertRouteToTimetablePage(
|
|
1371
|
+
routes[0],
|
|
1372
|
+
directions[0],
|
|
1373
|
+
calendars,
|
|
1374
|
+
calendarDates,
|
|
1375
|
+
config
|
|
1376
|
+
);
|
|
1058
1377
|
};
|
|
1059
1378
|
|
|
1060
1379
|
/*
|
|
@@ -1067,7 +1386,15 @@ export function setDefaultConfig(initialConfig) {
|
|
|
1067
1386
|
coordinatePrecision: 5,
|
|
1068
1387
|
dateFormat: 'MMM D, YYYY',
|
|
1069
1388
|
daysShortStrings: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
|
|
1070
|
-
daysStrings: [
|
|
1389
|
+
daysStrings: [
|
|
1390
|
+
'Monday',
|
|
1391
|
+
'Tuesday',
|
|
1392
|
+
'Wednesday',
|
|
1393
|
+
'Thursday',
|
|
1394
|
+
'Friday',
|
|
1395
|
+
'Saturday',
|
|
1396
|
+
'Sunday',
|
|
1397
|
+
],
|
|
1071
1398
|
defaultOrientation: 'vertical',
|
|
1072
1399
|
interpolatedStopSymbol: '•',
|
|
1073
1400
|
interpolatedStopText: 'Estimated time of arrival',
|
|
@@ -1099,7 +1426,7 @@ export function setDefaultConfig(initialConfig) {
|
|
|
1099
1426
|
timeFormat: 'h:mma',
|
|
1100
1427
|
useParentStation: true,
|
|
1101
1428
|
verbose: true,
|
|
1102
|
-
zipOutput: false
|
|
1429
|
+
zipOutput: false,
|
|
1103
1430
|
};
|
|
1104
1431
|
|
|
1105
1432
|
const config = Object.assign(defaults, initialConfig);
|
|
@@ -1118,23 +1445,36 @@ export function setDefaultConfig(initialConfig) {
|
|
|
1118
1445
|
export async function getFormattedTimetablePage(timetablePageId, config) {
|
|
1119
1446
|
const timetablePage = await getTimetablePageById(timetablePageId, config);
|
|
1120
1447
|
|
|
1121
|
-
timetablePage.consolidatedTimetables = await formatTimetables(
|
|
1448
|
+
timetablePage.consolidatedTimetables = await formatTimetables(
|
|
1449
|
+
timetablePage.timetables,
|
|
1450
|
+
config
|
|
1451
|
+
);
|
|
1122
1452
|
timetablePage.timetable_page_label = formatTimetablePageLabel(timetablePage);
|
|
1123
|
-
timetablePage.dayList = formatDays(
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
'
|
|
1132
|
-
|
|
1133
|
-
|
|
1453
|
+
timetablePage.dayList = formatDays(
|
|
1454
|
+
getDaysFromCalendars(timetablePage.consolidatedTimetables),
|
|
1455
|
+
config
|
|
1456
|
+
);
|
|
1457
|
+
timetablePage.dayLists = uniq(
|
|
1458
|
+
timetablePage.consolidatedTimetables.map((timetable) => timetable.dayList)
|
|
1459
|
+
);
|
|
1460
|
+
timetablePage.route_ids = uniq(
|
|
1461
|
+
flatMap(timetablePage.consolidatedTimetables, 'route_ids')
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
const timetableRoutes = await getRoutes(
|
|
1465
|
+
{
|
|
1466
|
+
route_id: timetablePage.route_ids,
|
|
1467
|
+
},
|
|
1468
|
+
['route_color', 'route_text_color', 'agency_id']
|
|
1469
|
+
);
|
|
1134
1470
|
|
|
1135
|
-
timetablePage.routeColors = timetableRoutes.map(route => route.route_color);
|
|
1136
|
-
timetablePage.routeTextColors = timetableRoutes.map(
|
|
1137
|
-
|
|
1471
|
+
timetablePage.routeColors = timetableRoutes.map((route) => route.route_color);
|
|
1472
|
+
timetablePage.routeTextColors = timetableRoutes.map(
|
|
1473
|
+
(route) => route.route_text_color
|
|
1474
|
+
);
|
|
1475
|
+
timetablePage.agency_ids = compact(
|
|
1476
|
+
timetableRoutes.map((route) => route.agency_id)
|
|
1477
|
+
);
|
|
1138
1478
|
|
|
1139
1479
|
// Set default filename.
|
|
1140
1480
|
if (!timetablePage.filename) {
|
|
@@ -1142,17 +1482,21 @@ export async function getFormattedTimetablePage(timetablePageId, config) {
|
|
|
1142
1482
|
}
|
|
1143
1483
|
|
|
1144
1484
|
// Get `direction_name` for each timetable.
|
|
1145
|
-
await Promise.all(
|
|
1146
|
-
|
|
1147
|
-
timetable.direction_name
|
|
1148
|
-
|
|
1485
|
+
await Promise.all(
|
|
1486
|
+
timetablePage.consolidatedTimetables.map(async (timetable) => {
|
|
1487
|
+
if (isNullOrEmpty(timetable.direction_name)) {
|
|
1488
|
+
timetable.direction_name = await getDirectionHeadsignFromTimetable(
|
|
1489
|
+
timetable
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1149
1492
|
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1493
|
+
if (!timetable.routes) {
|
|
1494
|
+
timetable.routes = await getRoutes({
|
|
1495
|
+
route_id: timetable.route_ids,
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
})
|
|
1499
|
+
);
|
|
1156
1500
|
|
|
1157
1501
|
return timetablePage;
|
|
1158
1502
|
}
|
|
@@ -1160,12 +1504,12 @@ export async function getFormattedTimetablePage(timetablePageId, config) {
|
|
|
1160
1504
|
/*
|
|
1161
1505
|
* Generate stats about timetable page.
|
|
1162
1506
|
*/
|
|
1163
|
-
const generateStats = timetablePage => {
|
|
1507
|
+
export const generateStats = (timetablePage) => {
|
|
1164
1508
|
const stats = {
|
|
1165
1509
|
stops: 0,
|
|
1166
1510
|
trips: 0,
|
|
1167
1511
|
route_ids: {},
|
|
1168
|
-
service_ids: {}
|
|
1512
|
+
service_ids: {},
|
|
1169
1513
|
};
|
|
1170
1514
|
|
|
1171
1515
|
for (const timetable of timetablePage.consolidatedTimetables) {
|
|
@@ -1189,17 +1533,54 @@ const generateStats = timetablePage => {
|
|
|
1189
1533
|
/*
|
|
1190
1534
|
* Generate the HTML timetable for a timetable page.
|
|
1191
1535
|
*/
|
|
1192
|
-
export
|
|
1536
|
+
export function generateTimetableHTML(timetablePage, config) {
|
|
1193
1537
|
const templateVars = {
|
|
1194
1538
|
timetablePage,
|
|
1195
|
-
config
|
|
1196
|
-
};
|
|
1197
|
-
const html = await renderTemplate('timetablepage', templateVars, config);
|
|
1198
|
-
const stats = generateStats(timetablePage);
|
|
1199
|
-
return {
|
|
1200
|
-
html,
|
|
1201
|
-
stats
|
|
1539
|
+
config,
|
|
1202
1540
|
};
|
|
1541
|
+
return renderTemplate('timetablepage', templateVars, config);
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/*
|
|
1545
|
+
* Generate the CSV timetable for a timetable page.
|
|
1546
|
+
*/
|
|
1547
|
+
export async function generateTimetableCSV(timetable) {
|
|
1548
|
+
// Generate horizontal orientation, then transpose if vertical is needed.
|
|
1549
|
+
const lines = [];
|
|
1550
|
+
|
|
1551
|
+
lines.push([
|
|
1552
|
+
'',
|
|
1553
|
+
...timetable.orderedTrips.map((trip) =>
|
|
1554
|
+
formatTripNameForCSV(trip, timetable)
|
|
1555
|
+
),
|
|
1556
|
+
]);
|
|
1557
|
+
|
|
1558
|
+
if (timetable.has_continues_from_route) {
|
|
1559
|
+
lines.push([
|
|
1560
|
+
'Continues from route',
|
|
1561
|
+
...timetable.orderedTrips.map((trip) => formatTripContinuesFrom(trip)),
|
|
1562
|
+
]);
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
for (const stop of timetable.stops) {
|
|
1566
|
+
lines.push([
|
|
1567
|
+
formatStopName(stop),
|
|
1568
|
+
...stop.trips.map((stoptime) => stoptime.formatted_time),
|
|
1569
|
+
]);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
if (timetable.has_continues_as_route) {
|
|
1573
|
+
lines.push([
|
|
1574
|
+
'Continues as route',
|
|
1575
|
+
...timetable.orderedTrips.map((trip) => formatTripContinuesAs(trip)),
|
|
1576
|
+
]);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
if (timetable.orientation === 'vertical') {
|
|
1580
|
+
return stringify(zip(...lines));
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
return stringify(lines);
|
|
1203
1584
|
}
|
|
1204
1585
|
|
|
1205
1586
|
/*
|
|
@@ -1218,27 +1599,37 @@ export async function generateOverviewHTML(timetablePages, config) {
|
|
|
1218
1599
|
|
|
1219
1600
|
// Sort timetables for display, first numerically then alphabetically.
|
|
1220
1601
|
const sortedTimetablePages = sortBy(timetablePages, [
|
|
1221
|
-
timetablePage => {
|
|
1222
|
-
if (
|
|
1223
|
-
|
|
1602
|
+
(timetablePage) => {
|
|
1603
|
+
if (
|
|
1604
|
+
timetablePage.consolidatedTimetables.length > 0 &&
|
|
1605
|
+
timetablePage.consolidatedTimetables[0].routes.length > 0
|
|
1606
|
+
) {
|
|
1607
|
+
return Number.parseInt(
|
|
1608
|
+
timetablePage.consolidatedTimetables[0].routes[0].route_short_name,
|
|
1609
|
+
10
|
|
1610
|
+
);
|
|
1224
1611
|
}
|
|
1225
1612
|
},
|
|
1226
|
-
timetablePage => {
|
|
1227
|
-
if (
|
|
1228
|
-
|
|
1613
|
+
(timetablePage) => {
|
|
1614
|
+
if (
|
|
1615
|
+
timetablePage.consolidatedTimetables.length > 0 &&
|
|
1616
|
+
timetablePage.consolidatedTimetables[0].routes.length > 0
|
|
1617
|
+
) {
|
|
1618
|
+
return timetablePage.consolidatedTimetables[0].routes[0]
|
|
1619
|
+
.route_short_name;
|
|
1229
1620
|
}
|
|
1230
|
-
}
|
|
1621
|
+
},
|
|
1231
1622
|
]);
|
|
1232
1623
|
|
|
1233
1624
|
const templateVars = {
|
|
1234
1625
|
agency: {
|
|
1235
1626
|
...first(agencies),
|
|
1236
|
-
geojson
|
|
1627
|
+
geojson,
|
|
1237
1628
|
},
|
|
1238
1629
|
agencies,
|
|
1239
1630
|
geojson,
|
|
1240
1631
|
config,
|
|
1241
|
-
timetablePages: sortedTimetablePages
|
|
1632
|
+
timetablePages: sortedTimetablePages,
|
|
1242
1633
|
};
|
|
1243
1634
|
return renderTemplate('overview', templateVars, config);
|
|
1244
1635
|
}
|