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/lib/utils.js CHANGED
@@ -1,24 +1,92 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
 
3
- import { cloneDeep, compact, every, find, findLast, first, flatMap, flattenDeep, isEqual, groupBy, last, maxBy, reduce, size, some, sortBy, uniq, uniqBy } from 'lodash-es';
4
- import { getCalendarDates, getTrips, getTimetableNotesReferences, getTimetableNotes, getRoutes, getDb, getCalendars, getTimetableStopOrders, getStops, getStopAttributes, getStoptimes, getFrequencies, getTimetables, getTimetablePages, getAgencies } from 'gtfs';
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 { formatDate, formatDays, formatDaysLong, formatFrequency, formatStops, formatTimetableId, formatTimetableLabel, formatTimetablePageLabel, formatTrip, isNullOrEmpty, mergeTimetablesWithSameId, resetStoptimesToMidnight, timeToSeconds, updateStoptimesByOffset } from './formatters.js';
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 { fromGTFSDate, toGTFSDate, calendarToCalendarCode, secondsAfterMidnight, fromGTFSTime, calendarCodeToCalendar } from './time-utils.js';
13
-
14
- const { version } = JSON.parse(readFileSync(new URL('../package.json', import.meta.url)));
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 !isNullOrEmpty(stoptime.arrival_time) && !isNullOrEmpty(stoptime.departure_time);
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 = config.showOnlyTimepoint === true ? trips.map(trip => trip.stoptimes.filter(stoptime => isTimepoint(stoptime))) : trips.map(trip => trip.stoptimes);
33
-
34
- return maxBy(filteredTripStoptimes, stoptimes => size(stoptimes));
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 => trip.stoptimes.find(tripStoptime => tripStoptime.stop_id === stoptime.stop_id && tripStoptime.arrival_time !== null));
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 ? find(trip.stoptimes, {
82
- stop_id: commonStopId
83
- }) : trip.stoptimes[0];
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(stoptime => stoptime.departure_time);
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, { ...config, sortingAlgorithm: 'beginning' });
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(trips, ['firstStoptime', 'lastStoptime'], ['asc', 'asc']);
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(trips, ['lastStoptime', 'firstStoptime'], ['asc', 'asc']);
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) => sortBy(trips, trip => {
176
- const stoptime = find(trip.stoptimes, { stop_id: stopId });
177
- return stoptime ? timeToSeconds(stoptime.departure_time) : undefined;
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
- service_id: timetable.service_ids
186
- }, [], [
187
- ['date', 'ASC']
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(formatDate(calendarDate, config.dateFormat));
296
+ filteredCalendarDates.includedDates.push(
297
+ formatDate(calendarDate, config.dateFormat)
298
+ );
200
299
  } else if (calendarDate.exception_type === 2) {
201
- filteredCalendarDates.excludedDates.push(formatDate(calendarDate, config.dateFormat));
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
- direction_id: timetable.direction_id,
239
- route_id: timetable.route_ids
240
- }, [
241
- 'trip_headsign'
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 (noteReference.stop_sequence === '' || noteReference.stop_sequence === null) {
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(`Timetable Note Reference for note_id=${noteReference.note_id} has a \`stop_sequence\` but no \`stop_id\` - ignoring`);
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(stop => stop.stop_id === noteReference.stop_id);
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(trip => trip.stop_sequence === noteReference.stop_sequence);
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 = symbolIndex < (symbols.length - 1) ? symbols[symbolIndex] : (symbolIndex - symbols.length);
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 = (route, direction, calendars, calendarDates, config) => {
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: (calendarDates && calendarDates.length > 0) ? 1 : 0,
363
- service_id: (calendarDates && calendarDates.length > 0) ? calendarDates[0].service_id : null,
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(moment.min(calendars.map(calendar => fromGTFSDate(calendar.start_date))));
381
- timetable.end_date = toGTFSDate(moment.max(calendars.map(calendar => fromGTFSDate(calendar.end_date))));
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(`SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map(serviceId => `'${serviceId}'`).join(', ')})`);
401
-
402
- const timetablePages = await Promise.all(routes.map(async route => {
403
- const trips = await getTrips({
404
- route_id: route.route_id
405
- }, [
406
- 'trip_headsign',
407
- 'direction_id'
408
- ]);
409
- const directions = uniqBy(trips, trip => trip.direction_id);
410
- const dayGroups = groupBy(calendars, calendarToCalendarCode);
411
- const calendarDateGroups = groupBy(calendarDates, 'service_id');
412
-
413
- return Promise.all(directions.map(direction => Promise.all([
414
- Promise.all(Object.values(dayGroups).map(calendars => convertRouteToTimetablePage(route, direction, calendars, null, config))),
415
- Promise.all(Object.values(calendarDateGroups).map(calendarDates => convertRouteToTimetablePage(route, direction, null, calendarDates, config)))
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 => formatFrequency(frequency, config));
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 (let offset = startSeconds; offset < endSeconds; offset += frequency.headway_secs) {
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 = (stopIds, timetable, config) => {
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(fromGTFSTime(stoptime.arrival_time), 'minutes');
455
-
456
- if (config.showArrivalOnDifference === null || timepointDifference < config.showArrivalOnDifference) {
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 (stoptime.stop_id === stopIds[index + 1] || stoptime.stop_id === stopIds[index - 1]) {
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
- timetable_id: timetable.timetable_id
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(timetableStopOrder => timetableStopOrder.stop_id);
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 = config.showOnlyTimepoint === true ? trip.stoptimes.filter(stoptime => isTimepoint(stoptime)).map(stoptime => stoptime.stop_id) : trip.stoptimes.map(stoptime => stoptime.stop_id);
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(stopIds, timetable, config);
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(timetable.orderedTrips, config);
521
- const stopIds = longestTripStoptimes.map(stoptime => stoptime.stop_id);
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(orderedStopIds.map(async (stopId, index) => {
536
- const stops = await getStops({
537
- stop_id: stopId
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
- const stop = {
545
- ...stops[0],
546
- trips: []
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
- if (index < (orderedStopIds.length - 1) && stopId === orderedStopIds[index + 1]) {
550
- stop.type = 'arrival';
551
- } else if (index > 0 && stopId === orderedStopIds[index - 1]) {
552
- stop.type = 'departure';
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
- return stop;
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(stop => stop.stop_id === stopAttribute.stop_id);
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(days, (memo, value, key) => {
595
- if (value === 1) {
596
- memo.push(`${key} = 1`);
597
- }
791
+ const dayQueries = reduce(
792
+ days,
793
+ (memo, value, key) => {
794
+ if (value === 1) {
795
+ memo.push(`${key} = 1`);
796
+ }
598
797
 
599
- return memo;
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(`SELECT DISTINCT service_id FROM calendar_dates WHERE ${whereClauses.join(' AND ')}`);
629
- return calendarDates.map(calendarDate => calendarDate.service_id);
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
- parent_station: stop.parent_station
650
- }, ['stop_id']);
853
+ const stopsInParentStation = await getStops(
854
+ {
855
+ parent_station: stop.parent_station,
856
+ },
857
+ ['stop_id']
858
+ );
651
859
 
652
- return [stop.parent_station, ...stopsInParentStation.map(stop => stop.stop_id)];
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
- block_id: trip.block_id,
661
- service_id: timetable.service_ids
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
- ['stop_sequence', 'ASC']
674
- ]
675
- );
676
-
677
- if (stopTimes.length === 0) {
678
- throw new Error(`No stoptimes found found for trip_id=${blockTrip.trip_id}`);
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
- blockTrip.firstStoptime = first(stopTimes);
682
- blockTrip.lastStoptime = last(stopTimes);
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(blockTrips, blockTrip => blockTrip.lastStoptime.arrival_timestamp <= firstStoptime.departure_timestamp);
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 (previousTrip && previousTrip.route_id !== trip.route_id && previousTrip.lastStoptime.arrival_timestamp >= firstStoptime.departure_timestamp - maxContinuesAsWaitingTimeSeconds && firstStopIds.includes(previousTrip.lastStoptime.stop_id)) {
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(blockTrips, blockTrip => blockTrip.firstStoptime.departure_timestamp >= lastStoptime.arrival_timestamp);
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 (nextTrip && nextTrip.route_id !== trip.route_id && nextTrip.firstStoptime.departure_timestamp <= lastStoptime.arrival_timestamp + maxContinuesAsWaitingTimeSeconds && lastStopIds.includes(nextTrip.firstStoptime.stop_id)) {
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 => timetableStopIds.has(stoptime.stop_id));
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(`No trips found for route_id=${timetable.route_ids.join('_')}, direction_id=${timetable.direction_id}, service_ids=${JSON.stringify(timetable.service_ids)}, timetable_id=${timetable.timetable_id}`);
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(trips.map(async trip => {
791
- const formattedTrip = formatTrip(trip, timetable, calendars, config);
792
- formattedTrip.stoptimes = await getStoptimes({
793
- trip_id: formattedTrip.trip_id
794
- },
795
- [],
796
- [
797
- ['stop_sequence', 'ASC']
798
- ]);
799
-
800
- if (formattedTrip.stoptimes.length === 0) {
801
- timetable.warnings.push(`No stoptimes found for trip_id=${formattedTrip.trip_id}, route_id=${timetable.route_ids.join('_')}, timetable_id=${timetable.timetable_id}`);
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
- // Exclude trips before timetable `start_timestamp`
805
- if (timetable.start_timestamp !== '' && timetable.start_timestamp !== null && timetable.start_timestamp !== undefined) {
806
- if (trip.stoptimes[0].arrival_timestamp < timetable.start_timestamp) {
807
- return;
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
- // Exclude trips after timetable `end_timestamp`
812
- if (timetable.end_timestamp !== '' && timetable.end_timestamp !== null && timetable.end_timestamp !== undefined) {
813
- if (trip.stoptimes[0].arrival_timestamp >= timetable.end_timestamp) {
814
- return;
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
- if (timetable.show_trip_continuation) {
819
- await addTripContinuation(formattedTrip, timetable);
1077
+ if (timetable.show_trip_continuation) {
1078
+ await addTripContinuation(formattedTrip, timetable);
820
1079
 
821
- if (formattedTrip.continues_as_route) {
822
- timetable.has_continues_as_route = true;
823
- }
1080
+ if (formattedTrip.continues_as_route) {
1081
+ timetable.has_continues_as_route = true;
1082
+ }
824
1083
 
825
- if (formattedTrip.continues_from_route) {
826
- timetable.has_continues_from_route = true;
1084
+ if (formattedTrip.continues_from_route) {
1085
+ timetable.has_continues_from_route = true;
1086
+ }
827
1087
  }
828
- }
829
1088
 
830
- const tripFrequencies = frequencies.filter(frequency => frequency.trip_id === trip.trip_id);
831
-
832
- if (tripFrequencies.length === 0) {
833
- formattedTrips.push(formattedTrip);
834
- } else {
835
- const frequencyTrips = generateTripsByFrequencies(formattedTrip, frequencies, config);
836
- formattedTrips.push(...frequencyTrips);
837
- timetable.frequencies = frequencies;
838
- timetable.frequencyExactTimes = some(frequencies, {
839
- exact_times: 1
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
- stop_id: uniq(stopIds)
855
- },
856
- [
857
- 'parent_station',
858
- 'stop_id'
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(stop => stop.stop_id === stoptime.stop_id);
864
- stoptime.stop_id = parentStationStop.parent_station || parentStationStop.stop_id;
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(timetables.map(async timetable => {
877
- timetable.warnings = [];
878
- const dayList = formatDays(timetable, config);
879
- const calendars = await getCalendarsFromTimetable(timetable);
880
- let serviceIds = calendars.map(calendar => calendar.service_id);
881
-
882
- if (timetable.include_exceptions === 1) {
883
- const calendarDatesServiceIds = await getCalendarDatesServiceIds(timetable.start_date, timetable.end_date);
884
- serviceIds = uniq([...serviceIds, ...calendarDatesServiceIds]);
885
- }
886
-
887
- Object.assign(timetable, {
888
- noServiceSymbolUsed: false,
889
- requestDropoffSymbolUsed: false,
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
- timetable.orderedTrips = await getTripsForTimetable(timetable, calendars, config);
909
- timetable.stops = await getStopsForTimetable(timetable, config);
910
- timetable.calendarDates = await getCalendarDatesForTimetable(timetable, config);
911
- timetable.timetable_label = formatTimetableLabel(timetable);
912
- timetable.notes = await getTimetableNotesForTimetable(timetable, config);
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
- if (config.showMap) {
915
- timetable.geojson = await getTimetableGeoJSON(timetable, config);
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
- // Filter trips after all timetable properties are assigned
919
- timetable.orderedTrips = filterTrips(timetable);
1197
+ // Filter trips after all timetable properties are assigned
1198
+ timetable.orderedTrips = filterTrips(timetable);
920
1199
 
921
- // Format stops after all timetable properties are assigned
922
- timetable.stops = formatStops(timetable, config);
1200
+ // Format stops after all timetable properties are assigned
1201
+ timetable.stops = formatStops(timetable, config);
923
1202
 
924
- return timetable;
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(timetable => timetable.orderedTrips.length > 0);
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
- ['timetable_page_id', 'ASC']
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(timetables.map(timetable => convertTimetableToTimetablePage(timetable, config)));
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(timetablePages.map(async timetablePage => {
959
- timetablePage.timetables = sortBy(timetables.filter(timetable => timetable.timetable_page_id === timetablePage.timetable_page_id), 'timetable_sequence');
960
-
961
- // Add routes for each timetable.
962
- for (const timetable of timetablePage.timetables) {
963
- timetable.routes = routes.filter(route => timetable.route_ids.includes(route.route_id));
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
- return timetablePage;
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(`Multiple timetable_pages found for timetable_page_id=${timetablePageId}`);
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(timetables.filter(timetable => timetable.timetable_page_id === timetablePageId), 'timetable_sequence');
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(timetablePage.timetables.map(async timetable => {
992
- timetable.routes = await getRoutes({
993
- route_id: timetable.route_ids
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(timetable => timetable.timetable_id === timetablePageId);
1309
+ const timetablePageTimetables = timetables.filter(
1310
+ (timetable) => timetable.timetable_id === timetablePageId
1311
+ );
1003
1312
 
1004
1313
  if (timetablePageTimetables.length === 0) {
1005
- throw new Error(`No timetable found for timetable_page_id=${timetablePageId}`);
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
- route_id: routeId,
1034
- direction_id: directionId
1035
- }, [
1036
- 'trip_headsign',
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(`No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`);
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(routes[0], directions[0], calendars, calendarDates, config);
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: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
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(timetablePage.timetables, config);
1448
+ timetablePage.consolidatedTimetables = await formatTimetables(
1449
+ timetablePage.timetables,
1450
+ config
1451
+ );
1122
1452
  timetablePage.timetable_page_label = formatTimetablePageLabel(timetablePage);
1123
- timetablePage.dayList = formatDays(getDaysFromCalendars(timetablePage.consolidatedTimetables), config);
1124
- timetablePage.dayLists = uniq(timetablePage.consolidatedTimetables.map(timetable => timetable.dayList));
1125
- timetablePage.route_ids = uniq(flatMap(timetablePage.consolidatedTimetables, 'route_ids'));
1126
-
1127
- const timetableRoutes = await getRoutes({
1128
- route_id: timetablePage.route_ids
1129
- }, [
1130
- 'route_color',
1131
- 'route_text_color',
1132
- 'agency_id'
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(route => route.route_text_color);
1137
- timetablePage.agency_ids = compact(timetableRoutes.map(route => route.agency_id));
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(timetablePage.consolidatedTimetables.map(async timetable => {
1146
- if (isNullOrEmpty(timetable.direction_name)) {
1147
- timetable.direction_name = await getDirectionHeadsignFromTimetable(timetable);
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
- if (!timetable.routes) {
1151
- timetable.routes = await getRoutes({
1152
- route_id: timetable.route_ids
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 async function generateHTML(timetablePage, config) {
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 (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
1223
- return Number.parseInt(timetablePage.consolidatedTimetables[0].routes[0].route_short_name, 10);
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 (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
1228
- return timetablePage.consolidatedTimetables[0].routes[0].route_short_name;
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
  }