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