gtfs-to-html 2.7.2 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/dist/app/index.d.ts +2 -0
  2. package/dist/app/index.js +1846 -0
  3. package/dist/app/index.js.map +1 -0
  4. package/dist/bin/gtfs-to-html.d.ts +1 -0
  5. package/dist/bin/gtfs-to-html.js +2222 -0
  6. package/dist/bin/gtfs-to-html.js.map +1 -0
  7. package/dist/index.d.ts +23 -0
  8. package/dist/index.js +2165 -0
  9. package/dist/index.js.map +1 -0
  10. package/package.json +23 -8
  11. package/.eslintrc.json +0 -28
  12. package/.husky/pre-commit +0 -4
  13. package/CHANGELOG.md +0 -1018
  14. package/app/index.js +0 -138
  15. package/bin/gtfs-to-html.js +0 -48
  16. package/config-sample.json +0 -59
  17. package/docker/Dockerfile +0 -14
  18. package/docker/README.md +0 -5
  19. package/docker/docker-compose.yml +0 -10
  20. package/examples/stop_attributes.txt +0 -6
  21. package/examples/timetable_notes.txt +0 -8
  22. package/examples/timetable_notes_references.txt +0 -8
  23. package/examples/timetable_pages.txt +0 -3
  24. package/examples/timetable_stop_order.txt +0 -16
  25. package/examples/timetables.txt +0 -9
  26. package/index.js +0 -1
  27. package/lib/file-utils.js +0 -202
  28. package/lib/formatters.js +0 -518
  29. package/lib/geojson-utils.js +0 -96
  30. package/lib/gtfs-to-html.js +0 -214
  31. package/lib/log-utils.js +0 -215
  32. package/lib/template-functions.js +0 -192
  33. package/lib/time-utils.js +0 -90
  34. package/lib/utils.js +0 -1702
  35. package/views/default/css/overview_styles.css +0 -197
  36. package/views/default/css/timetable_pdf_styles.css +0 -7
  37. package/views/default/css/timetable_styles.css +0 -447
  38. package/views/default/formatting_functions.pug +0 -113
  39. package/views/default/js/system-map.js +0 -594
  40. package/views/default/js/timetable-map.js +0 -358
  41. package/views/default/js/timetable-menu.js +0 -63
  42. package/views/default/layout.pug +0 -11
  43. package/views/default/overview.pug +0 -27
  44. package/views/default/overview_full.pug +0 -16
  45. package/views/default/timetable_continuation_as.pug +0 -7
  46. package/views/default/timetable_continuation_from.pug +0 -7
  47. package/views/default/timetable_horizontal.pug +0 -42
  48. package/views/default/timetable_hourly.pug +0 -30
  49. package/views/default/timetable_menu.pug +0 -48
  50. package/views/default/timetable_note_symbol.pug +0 -5
  51. package/views/default/timetable_stop_name.pug +0 -13
  52. package/views/default/timetable_stoptime.pug +0 -17
  53. package/views/default/timetable_vertical.pug +0 -67
  54. package/views/default/timetablepage.pug +0 -66
  55. package/views/default/timetablepage_full.pug +0 -22
  56. package/www/README.md +0 -33
  57. package/www/babel.config.js +0 -3
  58. package/www/blog/2020-07-07-New-Documentation.md +0 -12
  59. package/www/blog/2020-08-20-Version-1.0.0.md +0 -29
  60. package/www/blog/2021-11-06-CSV-Export.md +0 -26
  61. package/www/docs/additional-files.md +0 -24
  62. package/www/docs/configuration.md +0 -568
  63. package/www/docs/current-usage.md +0 -48
  64. package/www/docs/custom-templates.md +0 -13
  65. package/www/docs/introduction.md +0 -39
  66. package/www/docs/logging-sql-queries.md +0 -12
  67. package/www/docs/previewing-html-output.md +0 -24
  68. package/www/docs/processing-large-gtfs.md +0 -10
  69. package/www/docs/quick-start.md +0 -136
  70. package/www/docs/related-libraries.md +0 -54
  71. package/www/docs/reviewing-changes.md +0 -29
  72. package/www/docs/stop-attributes.md +0 -30
  73. package/www/docs/support.md +0 -12
  74. package/www/docs/timetable-notes-references.md +0 -44
  75. package/www/docs/timetable-notes.md +0 -33
  76. package/www/docs/timetable-pages.md +0 -37
  77. package/www/docs/timetable-stop-order.md +0 -63
  78. package/www/docs/timetables.md +0 -64
  79. package/www/docusaurus.config.js +0 -104
  80. package/www/package.json +0 -21
  81. package/www/sidebars.js +0 -10
  82. package/www/src/css/custom.css +0 -25
  83. package/www/src/pages/index.js +0 -270
  84. package/www/src/pages/styles.module.css +0 -53
  85. package/www/static/.nojekyll +0 -0
  86. package/www/static/img/favicon.ico +0 -0
  87. package/www/static/img/gtfs-to-html-logo.svg +0 -18
  88. package/www/static/img/overview-example.jpg +0 -0
  89. package/www/static/img/timetable-example.jpg +0 -0
  90. package/www/static/img/undraw_happy_music.svg +0 -1
  91. package/www/static/img/undraw_proud_coder.svg +0 -1
  92. package/www/static/img/undraw_spreadsheets.svg +0 -1
  93. package/www/yarn.lock +0 -8351
@@ -0,0 +1,2222 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ // src/bin/gtfs-to-html.ts
9
+ import yargs from "yargs";
10
+ import { hideBin } from "yargs/helpers";
11
+ import PrettyError from "pretty-error";
12
+
13
+ // src/lib/file-utils.ts
14
+ import path from "node:path";
15
+ import { createWriteStream } from "node:fs";
16
+ import { fileURLToPath } from "node:url";
17
+ import { readFile, rm, mkdir } from "node:fs/promises";
18
+ import copydir from "copy-dir";
19
+ import _ from "lodash-es";
20
+ import archiver from "archiver";
21
+ import beautify from "js-beautify";
22
+ import { renderFile } from "pug";
23
+ import puppeteer from "puppeteer";
24
+ import sanitize from "sanitize-filename";
25
+ import untildify from "untildify";
26
+ import insane from "insane";
27
+ import { marked } from "marked";
28
+
29
+ // src/lib/formatters.ts
30
+ import {
31
+ clone,
32
+ find as find2,
33
+ first as first2,
34
+ groupBy as groupBy2,
35
+ last as last2,
36
+ omit,
37
+ sortBy as sortBy2,
38
+ zipObject
39
+ } from "lodash-es";
40
+ import moment3 from "moment";
41
+
42
+ // src/lib/time-utils.ts
43
+ import moment from "moment";
44
+ function fromGTFSTime(timeString) {
45
+ const duration = moment.duration(timeString);
46
+ return moment({
47
+ hour: duration.hours(),
48
+ minute: duration.minutes(),
49
+ second: duration.seconds()
50
+ });
51
+ }
52
+ function toGTFSTime(time) {
53
+ return time.format("HH:mm:ss");
54
+ }
55
+ function fromGTFSDate(gtfsDate) {
56
+ return moment(gtfsDate, "YYYYMMDD");
57
+ }
58
+ function toGTFSDate(date) {
59
+ return moment(date).format("YYYYMMDD");
60
+ }
61
+ function calendarToCalendarCode(c) {
62
+ if (c.service_id) {
63
+ return c.service_id;
64
+ }
65
+ return `${c.monday}${c.tuesday}${c.wednesday}${c.thursday}${c.friday}${c.saturday}${c.sunday}`;
66
+ }
67
+ function calendarCodeToCalendar(code) {
68
+ const days2 = [
69
+ "monday",
70
+ "tuesday",
71
+ "wednesday",
72
+ "thursday",
73
+ "friday",
74
+ "saturday",
75
+ "sunday"
76
+ ];
77
+ const calendar = {};
78
+ for (const [index, day] of days2.entries()) {
79
+ calendar[day] = code[index];
80
+ }
81
+ return calendar;
82
+ }
83
+ function secondsAfterMidnight(timeString) {
84
+ return moment.duration(timeString).asSeconds();
85
+ }
86
+ function minutesAfterMidnight(timeString) {
87
+ return moment.duration(timeString).asMinutes();
88
+ }
89
+ function updateTimeByOffset(timeString, offsetSeconds) {
90
+ const newTime = fromGTFSTime(timeString);
91
+ return toGTFSTime(newTime.add(offsetSeconds, "seconds"));
92
+ }
93
+
94
+ // src/lib/utils.ts
95
+ import {
96
+ cloneDeep,
97
+ compact,
98
+ countBy,
99
+ entries,
100
+ every as every2,
101
+ find,
102
+ findLast,
103
+ first,
104
+ flatMap as flatMap2,
105
+ flattenDeep,
106
+ flow,
107
+ isEqual,
108
+ groupBy,
109
+ head,
110
+ last,
111
+ maxBy,
112
+ partialRight,
113
+ reduce,
114
+ size,
115
+ some,
116
+ sortBy,
117
+ uniq,
118
+ uniqBy,
119
+ zip
120
+ } from "lodash-es";
121
+ import {
122
+ getCalendarDates,
123
+ getTrips,
124
+ getTimetableNotesReferences,
125
+ getTimetableNotes,
126
+ getRoutes,
127
+ getCalendars,
128
+ getTimetableStopOrders,
129
+ getStops,
130
+ getStopAttributes,
131
+ getStoptimes,
132
+ getFrequencies,
133
+ getTimetables,
134
+ getTimetablePages,
135
+ getAgencies,
136
+ openDb
137
+ } from "gtfs";
138
+ import { stringify } from "csv-stringify";
139
+ import moment2 from "moment";
140
+ import sqlString from "sqlstring";
141
+ import toposort from "toposort";
142
+
143
+ // src/lib/geojson-utils.ts
144
+ import { getShapesAsGeoJSON, getStopsAsGeoJSON } from "gtfs";
145
+ import { flatMap } from "lodash-es";
146
+ import simplify from "@turf/simplify";
147
+ import { featureCollection, round } from "@turf/helpers";
148
+ var mergeGeojson = (...geojsons) => featureCollection(flatMap(geojsons, (geojson) => geojson.features));
149
+ var truncateGeoJSONDecimals = (geojson, config) => {
150
+ for (const feature of geojson.features) {
151
+ if (feature.geometry.coordinates) {
152
+ if (feature.geometry.type.toLowerCase() === "point") {
153
+ feature.geometry.coordinates = feature.geometry.coordinates.map(
154
+ (number) => round(number, config.coordinatePrecision)
155
+ );
156
+ } else if (feature.geometry.type.toLowerCase() === "linestring") {
157
+ feature.geometry.coordinates = feature.geometry.coordinates.map(
158
+ (coordinate) => coordinate.map(
159
+ (number) => round(number, config.coordinatePrecision)
160
+ )
161
+ );
162
+ } else if (feature.geometry.type.toLowerCase() === "multilinestring") {
163
+ feature.geometry.coordinates = feature.geometry.coordinates.map(
164
+ (linestring) => linestring.map(
165
+ (coordinate) => coordinate.map(
166
+ (number) => round(number, config.coordinatePrecision)
167
+ )
168
+ )
169
+ );
170
+ }
171
+ }
172
+ }
173
+ return geojson;
174
+ };
175
+ var simplifyGeoJSON = (geojson, config) => {
176
+ try {
177
+ const simplifiedGeojson = simplify(geojson, {
178
+ tolerance: 1 / 10 ** config.coordinatePrecision,
179
+ highQuality: true
180
+ });
181
+ return truncateGeoJSONDecimals(simplifiedGeojson, config);
182
+ } catch {
183
+ config.logWarning("Unable to simplify geojson");
184
+ return truncateGeoJSONDecimals(geojson, config);
185
+ }
186
+ };
187
+ function getTimetableGeoJSON(timetable, config) {
188
+ const shapesGeojsons = timetable.route_ids.map(
189
+ (routeId) => getShapesAsGeoJSON({
190
+ route_id: routeId,
191
+ direction_id: timetable.direction_id,
192
+ trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
193
+ })
194
+ );
195
+ const stopsGeojsons = timetable.route_ids.map(
196
+ (routeId) => getStopsAsGeoJSON({
197
+ route_id: routeId,
198
+ direction_id: timetable.direction_id,
199
+ trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
200
+ })
201
+ );
202
+ const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
203
+ return simplifyGeoJSON(geojson, config);
204
+ }
205
+ function getAgencyGeoJSON(config) {
206
+ const shapesGeojsons = getShapesAsGeoJSON();
207
+ const stopsGeojsons = getStopsAsGeoJSON();
208
+ const geojson = mergeGeojson(shapesGeojsons, stopsGeojsons);
209
+ return simplifyGeoJSON(geojson, config);
210
+ }
211
+
212
+ // src/lib/template-functions.ts
213
+ var template_functions_exports = {};
214
+ __export(template_functions_exports, {
215
+ formatHtmlId: () => formatHtmlId,
216
+ formatTripName: () => formatTripName,
217
+ formatTripNameForCSV: () => formatTripNameForCSV,
218
+ getNotesForStop: () => getNotesForStop,
219
+ getNotesForStoptime: () => getNotesForStoptime,
220
+ getNotesForTimetableLabel: () => getNotesForTimetableLabel,
221
+ getNotesForTrip: () => getNotesForTrip,
222
+ hasNotesOrNotices: () => hasNotesOrNotices,
223
+ timetableHasDifferentDays: () => timetableHasDifferentDays,
224
+ timetablePageHasDifferentDays: () => timetablePageHasDifferentDays,
225
+ timetablePageHasDifferentLabels: () => timetablePageHasDifferentLabels
226
+ });
227
+ import { every } from "lodash-es";
228
+ function formatHtmlId(id) {
229
+ return id.replace(/([^\w[\]{}.:-])\s?/g, "");
230
+ }
231
+ function timetableHasDifferentDays(timetable) {
232
+ return !every(timetable.orderedTrips, (trip, idx) => {
233
+ if (idx === 0) {
234
+ return true;
235
+ }
236
+ return trip.dayList === timetable.orderedTrips[idx - 1].dayList;
237
+ });
238
+ }
239
+ function timetablePageHasDifferentDays(timetablePage) {
240
+ return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
241
+ if (idx === 0) {
242
+ return true;
243
+ }
244
+ return timetable.dayListLong === timetablePage.consolidatedTimetables[idx - 1].dayListLong;
245
+ });
246
+ }
247
+ function timetablePageHasDifferentLabels(timetablePage) {
248
+ return !every(timetablePage.consolidatedTimetables, (timetable, idx) => {
249
+ if (idx === 0) {
250
+ return true;
251
+ }
252
+ return timetable.timetable_label === timetablePage.consolidatedTimetables[idx - 1].timetable_label;
253
+ });
254
+ }
255
+ function hasNotesOrNotices(timetable) {
256
+ return timetable.requestPickupSymbolUsed || timetable.noPickupSymbolUsed || timetable.requestDropoffSymbolUsed || timetable.noDropoffSymbolUsed || timetable.noServiceSymbolUsed || timetable.interpolatedStopSymbolUsed || timetable.notes.length > 0;
257
+ }
258
+ function getNotesForTimetableLabel(notes) {
259
+ return notes.filter((note) => !note.stop_id && !note.trip_id);
260
+ }
261
+ function getNotesForStop(notes, stop) {
262
+ return notes.filter((note) => {
263
+ if (note.trip_id) {
264
+ return false;
265
+ }
266
+ if (note.stop_sequence && !stop.trips.some((trip) => trip.stop_sequence === note.stop_sequence)) {
267
+ return false;
268
+ }
269
+ return note.stop_id === stop.stop_id;
270
+ });
271
+ }
272
+ function getNotesForTrip(notes, trip) {
273
+ return notes.filter((note) => {
274
+ if (note.stop_id) {
275
+ return false;
276
+ }
277
+ return note.trip_id === trip.trip_id;
278
+ });
279
+ }
280
+ function getNotesForStoptime(notes, stoptime) {
281
+ return notes.filter((note) => {
282
+ if (!note.trip_id && note.stop_id === stoptime.stop_id && note.show_on_stoptime === 1) {
283
+ return true;
284
+ }
285
+ if (!note.stop_id && note.trip_id === stoptime.trip_id && note.show_on_stoptime === 1) {
286
+ return true;
287
+ }
288
+ return note.trip_id === stoptime.trip_id && note.stop_id === stoptime.stop_id;
289
+ });
290
+ }
291
+ function formatTripName(trip, index, timetable) {
292
+ let tripName;
293
+ if (timetable.routes.length > 1) {
294
+ tripName = trip.route_short_name;
295
+ } else if (timetable.orientation === "horizontal") {
296
+ if (trip.trip_short_name) {
297
+ tripName = trip.trip_short_name;
298
+ } else {
299
+ tripName = `Run #${index + 1}`;
300
+ }
301
+ }
302
+ if (timetableHasDifferentDays(timetable)) {
303
+ tripName += ` ${trip.dayList}`;
304
+ }
305
+ return tripName;
306
+ }
307
+ function formatTripNameForCSV(trip, timetable) {
308
+ let tripName = "";
309
+ if (timetable.routes.length > 1) {
310
+ tripName += `${trip.route_short_name} - `;
311
+ }
312
+ if (trip.trip_short_name) {
313
+ tripName += trip.trip_short_name;
314
+ } else {
315
+ tripName += trip.trip_id;
316
+ }
317
+ if (trip.trip_headsign) {
318
+ tripName += ` - ${trip.trip_headsign}`;
319
+ }
320
+ if (timetableHasDifferentDays(timetable)) {
321
+ tripName += ` - ${trip.dayList}`;
322
+ }
323
+ return tripName;
324
+ }
325
+
326
+ // package.json
327
+ var version = "2.8.0";
328
+
329
+ // src/lib/utils.ts
330
+ var isTimepoint = (stoptime) => {
331
+ if (isNullOrEmpty(stoptime.timepoint)) {
332
+ return !isNullOrEmpty(stoptime.arrival_time) && !isNullOrEmpty(stoptime.departure_time);
333
+ }
334
+ return stoptime.timepoint === 1;
335
+ };
336
+ var getLongestTripStoptimes = (trips, config) => {
337
+ const filteredTripStoptimes = trips.map(
338
+ (trip) => trip.stoptimes.filter((stoptime) => {
339
+ if (config.showOnlyTimepoint === true) {
340
+ return isTimepoint(stoptime);
341
+ }
342
+ return true;
343
+ })
344
+ );
345
+ return maxBy(filteredTripStoptimes, (stoptimes) => size(stoptimes));
346
+ };
347
+ var findCommonStopId = (trips, config) => {
348
+ const longestTripStoptimes = getLongestTripStoptimes(trips, config);
349
+ if (!longestTripStoptimes) {
350
+ return null;
351
+ }
352
+ const commonStoptime = longestTripStoptimes.find((stoptime, idx) => {
353
+ if (idx === 0 && stoptime.stop_id === last(longestTripStoptimes).stop_id) {
354
+ return false;
355
+ }
356
+ if (isNullOrEmpty(stoptime.arrival_time)) {
357
+ return false;
358
+ }
359
+ return every2(
360
+ trips,
361
+ (trip) => trip.stoptimes.find(
362
+ (tripStoptime) => tripStoptime.stop_id === stoptime.stop_id && tripStoptime.arrival_time !== null
363
+ )
364
+ );
365
+ });
366
+ return commonStoptime ? commonStoptime.stop_id : null;
367
+ };
368
+ var deduplicateTrips = (trips, commonStopId) => {
369
+ const deduplicatedTrips = [];
370
+ for (const trip of trips) {
371
+ if (deduplicatedTrips.length === 0 || trip.stoptimes.length === 0) {
372
+ deduplicatedTrips.push(trip);
373
+ continue;
374
+ }
375
+ const stoptimes = trip.stoptimes.map((stoptime) => stoptime.departure_time);
376
+ const selectedStoptime = commonStopId ? find(trip.stoptimes, {
377
+ stop_id: commonStopId
378
+ }) : trip.stoptimes[0];
379
+ const similarTrips = deduplicatedTrips.filter((trip2) => {
380
+ const stoptime = find(trip2.stoptimes, {
381
+ stop_id: selectedStoptime.stop_id
382
+ });
383
+ if (!stoptime) {
384
+ return false;
385
+ }
386
+ return stoptime.departure_time === selectedStoptime.departure_time;
387
+ });
388
+ const tripIsUnique = every2(similarTrips, (similarTrip) => {
389
+ const similarTripStoptimes = similarTrip.stoptimes.map(
390
+ (stoptime) => stoptime.departure_time
391
+ );
392
+ return !isEqual(stoptimes, similarTripStoptimes);
393
+ });
394
+ if (tripIsUnique) {
395
+ deduplicatedTrips.push(trip);
396
+ }
397
+ }
398
+ return deduplicatedTrips;
399
+ };
400
+ var sortTrips = (trips, config) => {
401
+ let sortedTrips;
402
+ let commonStopId;
403
+ if (config.sortingAlgorithm === "common") {
404
+ commonStopId = findCommonStopId(trips, config);
405
+ if (commonStopId) {
406
+ sortedTrips = sortTripsByStoptimeAtStop(trips, commonStopId);
407
+ } else {
408
+ sortedTrips = sortTrips(trips, {
409
+ ...config,
410
+ sortingAlgorithm: "beginning"
411
+ });
412
+ }
413
+ } else if (config.sortingAlgorithm === "beginning") {
414
+ for (const trip of trips) {
415
+ if (trip.stoptimes.length === 0) {
416
+ continue;
417
+ }
418
+ trip.firstStoptime = timeToSeconds(first(trip.stoptimes).departure_time);
419
+ trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
420
+ }
421
+ sortedTrips = sortBy(
422
+ trips,
423
+ ["firstStoptime", "lastStoptime"],
424
+ ["asc", "asc"]
425
+ );
426
+ } else if (config.sortingAlgorithm === "end") {
427
+ for (const trip of trips) {
428
+ if (trip.stoptimes.length === 0) {
429
+ continue;
430
+ }
431
+ trip.firstStoptime = timeToSeconds(first(trip.stoptimes).departure_time);
432
+ trip.lastStoptime = timeToSeconds(last(trip.stoptimes).departure_time);
433
+ }
434
+ sortedTrips = sortBy(
435
+ trips,
436
+ ["lastStoptime", "firstStoptime"],
437
+ ["asc", "asc"]
438
+ );
439
+ } else if (config.sortingAlgorithm === "first") {
440
+ const longestTripStoptimes = getLongestTripStoptimes(trips, config);
441
+ const firstStopId = first(longestTripStoptimes).stop_id;
442
+ sortedTrips = sortTripsByStoptimeAtStop(trips, firstStopId);
443
+ } else if (config.sortingAlgorithm === "last") {
444
+ const longestTripStoptimes = getLongestTripStoptimes(trips, config);
445
+ const lastStopId = last(longestTripStoptimes).stop_id;
446
+ sortedTrips = sortTripsByStoptimeAtStop(trips, lastStopId);
447
+ }
448
+ return deduplicateTrips(sortedTrips, commonStopId);
449
+ };
450
+ var sortTripsByStoptimeAtStop = (trips, stopId) => sortBy(trips, (trip) => {
451
+ const stoptime = find(trip.stoptimes, { stop_id: stopId });
452
+ return stoptime ? timeToSeconds(stoptime.departure_time) : void 0;
453
+ });
454
+ var getCalendarDatesForTimetable = (timetable, config) => {
455
+ const calendarDates = getCalendarDates(
456
+ {
457
+ service_id: timetable.service_ids
458
+ },
459
+ [],
460
+ [["date", "ASC"]]
461
+ );
462
+ const start = fromGTFSDate(timetable.start_date);
463
+ const end = fromGTFSDate(timetable.end_date);
464
+ const excludedDates = [];
465
+ const includedDates = [];
466
+ for (const calendarDate of calendarDates) {
467
+ if (moment2(calendarDate.date, "YYYYMMDD").isBetween(start, end)) {
468
+ if (calendarDate.exception_type === 1) {
469
+ includedDates.push(formatDate(calendarDate, config.dateFormat));
470
+ } else if (calendarDate.exception_type === 2) {
471
+ excludedDates.push(formatDate(calendarDate, config.dateFormat));
472
+ }
473
+ }
474
+ }
475
+ const includedAndExcludedDates = excludedDates.filter(
476
+ (date) => includedDates.includes(date)
477
+ );
478
+ return {
479
+ excludedDates: excludedDates.filter(
480
+ (date) => !includedAndExcludedDates.includes(date)
481
+ ),
482
+ includedDates: includedDates.filter(
483
+ (date) => !includedAndExcludedDates.includes(date)
484
+ )
485
+ };
486
+ };
487
+ var getDaysFromCalendars = (calendars) => {
488
+ const days2 = {
489
+ monday: 0,
490
+ tuesday: 0,
491
+ wednesday: 0,
492
+ thursday: 0,
493
+ friday: 0,
494
+ saturday: 0,
495
+ sunday: 0
496
+ };
497
+ for (const calendar of calendars) {
498
+ for (const [day, value] of Object.entries(days2)) {
499
+ days2[day] = value | calendar[day];
500
+ }
501
+ }
502
+ return days2;
503
+ };
504
+ var getDirectionHeadsignFromTimetable = (timetable) => {
505
+ const trips = getTrips(
506
+ {
507
+ direction_id: timetable.direction_id,
508
+ route_id: timetable.route_ids
509
+ },
510
+ ["trip_headsign"]
511
+ );
512
+ if (trips.length === 0) {
513
+ return "";
514
+ }
515
+ const mostCommonHeadsign = flow(
516
+ countBy,
517
+ entries,
518
+ partialRight(maxBy, last),
519
+ head
520
+ )(compact(trips.map((trip) => trip.trip_headsign)));
521
+ return mostCommonHeadsign;
522
+ };
523
+ var getTimetableNotesForTimetable = (timetable, config) => {
524
+ const noteReferences = [
525
+ // Get all notes for this timetable.
526
+ ...getTimetableNotesReferences({
527
+ timetable_id: timetable.timetable_id
528
+ }),
529
+ // Get all notes for this route.
530
+ ...getTimetableNotesReferences({
531
+ route_id: timetable.routes.map((route) => route.route_id),
532
+ timetable_id: null
533
+ }),
534
+ // Get all notes for all trips in this timetable.
535
+ ...getTimetableNotesReferences({
536
+ trip_id: timetable.orderedTrips.map((trip) => trip.trip_id)
537
+ }),
538
+ // Get all notes for all stops in this timetable.
539
+ ...getTimetableNotesReferences({
540
+ stop_id: timetable.stops.map((stop) => stop.stop_id),
541
+ trip_id: null,
542
+ route_id: null,
543
+ timetable_id: null
544
+ })
545
+ ];
546
+ const usedNoteReferences = [];
547
+ for (const noteReference of noteReferences) {
548
+ if (noteReference.stop_sequence === "" || noteReference.stop_sequence === null) {
549
+ usedNoteReferences.push(noteReference);
550
+ continue;
551
+ }
552
+ if (noteReference.stop_id === "" || noteReference.stop_id === null) {
553
+ config.logWarning(
554
+ `Timetable Note Reference for note_id=${noteReference.note_id} has a \`stop_sequence\` but no \`stop_id\` - ignoring`
555
+ );
556
+ continue;
557
+ }
558
+ const stop = timetable.stops.find(
559
+ (stop2) => stop2.stop_id === noteReference.stop_id
560
+ );
561
+ if (!stop) {
562
+ continue;
563
+ }
564
+ const tripWithMatchingStopSequence = stop.trips.find(
565
+ (trip) => trip.stop_sequence === noteReference.stop_sequence
566
+ );
567
+ if (tripWithMatchingStopSequence) {
568
+ usedNoteReferences.push(noteReference);
569
+ }
570
+ }
571
+ const notes = getTimetableNotes({
572
+ note_id: usedNoteReferences.map((noteReference) => noteReference.note_id)
573
+ });
574
+ const symbols = "abcdefghijklmnopqrstuvwxyz".split("");
575
+ let symbolIndex = 0;
576
+ for (const note of notes) {
577
+ if (note.symbol === "" || note.symbol === null) {
578
+ note.symbol = symbolIndex < symbols.length - 1 ? symbols[symbolIndex] : symbolIndex - symbols.length;
579
+ symbolIndex += 1;
580
+ }
581
+ }
582
+ const formattedNotes = usedNoteReferences.map((noteReference) => ({
583
+ ...noteReference,
584
+ ...notes.find((note) => note.note_id === noteReference.note_id)
585
+ }));
586
+ return sortBy(formattedNotes, "symbol");
587
+ };
588
+ var convertTimetableToTimetablePage = (timetable, config) => {
589
+ if (!timetable.routes) {
590
+ timetable.routes = getRoutes({
591
+ route_id: timetable.route_ids
592
+ });
593
+ }
594
+ const filename = generateFileName(timetable, config, "html");
595
+ return {
596
+ timetable_page_id: timetable.timetable_id,
597
+ timetable_page_label: timetable.timetable_label,
598
+ timetables: [timetable],
599
+ filename
600
+ };
601
+ };
602
+ var convertRouteToTimetablePage = (route, direction, calendars, calendarDates, config) => {
603
+ const timetable = {
604
+ route_ids: [route.route_id],
605
+ direction_id: direction ? direction.direction_id : void 0,
606
+ direction_name: direction ? direction.trip_headsign : void 0,
607
+ routes: [route],
608
+ include_exceptions: calendarDates && calendarDates.length > 0 ? 1 : 0,
609
+ service_id: calendarDates && calendarDates.length > 0 ? calendarDates[0].service_id : null,
610
+ service_notes: null,
611
+ timetable_label: null,
612
+ start_time: null,
613
+ end_time: null,
614
+ orientation: null,
615
+ timetable_sequence: null,
616
+ show_trip_continuation: null,
617
+ start_date: null,
618
+ end_date: null
619
+ };
620
+ if (calendars && calendars.length > 0) {
621
+ Object.assign(timetable, getDaysFromCalendars(calendars));
622
+ timetable.start_date = toGTFSDate(
623
+ moment2.min(
624
+ calendars.map((calendar) => fromGTFSDate(calendar.start_date))
625
+ )
626
+ );
627
+ timetable.end_date = toGTFSDate(
628
+ moment2.max(calendars.map((calendar) => fromGTFSDate(calendar.end_date)))
629
+ );
630
+ }
631
+ timetable.timetable_id = formatTimetableId(timetable);
632
+ return convertTimetableToTimetablePage(timetable, config);
633
+ };
634
+ var convertRoutesToTimetablePages = (config) => {
635
+ const db = openDb(config);
636
+ const routes = getRoutes();
637
+ let whereClause = "";
638
+ const whereClauses = [];
639
+ if (config.endDate) {
640
+ whereClauses.push(
641
+ `start_date <= ${sqlString.escape(toGTFSDate(moment2(config.endDate)))}`
642
+ );
643
+ }
644
+ if (config.startDate) {
645
+ whereClauses.push(
646
+ `end_date >= ${sqlString.escape(toGTFSDate(moment2(config.startDate)))}`
647
+ );
648
+ }
649
+ if (whereClauses.length > 0) {
650
+ whereClause = `WHERE ${whereClauses.join(" AND ")}`;
651
+ }
652
+ const calendars = db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
653
+ const serviceIds = calendars.map((calendar) => calendar.service_id);
654
+ const calendarDates = db.prepare(
655
+ `SELECT * FROM calendar_dates WHERE exception_type = 1 AND service_id NOT IN (${serviceIds.map((serviceId) => `'${serviceId}'`).join(", ")})`
656
+ ).all();
657
+ const timetablePages = routes.map((route) => {
658
+ const trips = getTrips(
659
+ {
660
+ route_id: route.route_id
661
+ },
662
+ ["trip_headsign", "direction_id", "trip_id", "service_id"]
663
+ );
664
+ const directions = uniqBy(trips, (trip) => trip.direction_id);
665
+ const dayGroups = groupBy(calendars, calendarToCalendarCode);
666
+ const calendarDateGroups = groupBy(calendarDates, "service_id");
667
+ return directions.map((direction) => [
668
+ Object.values(dayGroups).map((calendars2) => {
669
+ const tripsForCalendars = trips.filter(
670
+ (trip) => some(calendars2, { service_id: trip.service_id })
671
+ );
672
+ if (tripsForCalendars.length > 0) {
673
+ return convertRouteToTimetablePage(
674
+ route,
675
+ direction,
676
+ calendars2,
677
+ null,
678
+ config
679
+ );
680
+ }
681
+ }),
682
+ Object.values(calendarDateGroups).map((calendarDates2) => {
683
+ const tripsForCalendarDates = trips.filter(
684
+ (trip) => some(calendarDates2, { service_id: trip.service_id })
685
+ );
686
+ if (tripsForCalendarDates.length > 0) {
687
+ return convertRouteToTimetablePage(
688
+ route,
689
+ direction,
690
+ null,
691
+ calendarDates2,
692
+ config
693
+ );
694
+ }
695
+ })
696
+ ]);
697
+ });
698
+ return compact(flattenDeep(timetablePages));
699
+ };
700
+ var generateTripsByFrequencies = (trip, frequencies, config) => {
701
+ const formattedFrequencies = frequencies.map(
702
+ (frequency) => formatFrequency(frequency, config)
703
+ );
704
+ const resetTrip = resetStoptimesToMidnight(trip);
705
+ const trips = [];
706
+ for (const frequency of formattedFrequencies) {
707
+ const startSeconds = secondsAfterMidnight(frequency.start_time);
708
+ const endSeconds = secondsAfterMidnight(frequency.end_time);
709
+ for (let offset = startSeconds; offset < endSeconds; offset += frequency.headway_secs) {
710
+ const newTrip = cloneDeep(resetTrip);
711
+ trips.push({
712
+ ...newTrip,
713
+ trip_id: `${resetTrip.trip_id}_freq_${trips.length}`,
714
+ stoptimes: updateStoptimesByOffset(newTrip, offset)
715
+ });
716
+ }
717
+ }
718
+ return trips;
719
+ };
720
+ var duplicateStopsForDifferentArrivalDeparture = (stopIds, timetable, config) => {
721
+ if (config.showArrivalOnDifference === null) {
722
+ return stopIds;
723
+ }
724
+ for (const trip of timetable.orderedTrips) {
725
+ for (const stoptime of trip.stoptimes) {
726
+ const timepointDifference = fromGTFSTime(stoptime.departure_time).diff(
727
+ fromGTFSTime(stoptime.arrival_time),
728
+ "minutes"
729
+ );
730
+ if (timepointDifference < config.showArrivalOnDifference) {
731
+ continue;
732
+ }
733
+ const index = stopIds.indexOf(stoptime.stop_id);
734
+ if (index === 0 || index === stopIds.length - 1) {
735
+ continue;
736
+ }
737
+ if (stoptime.stop_id === stopIds[index + 1] || stoptime.stop_id === stopIds[index - 1]) {
738
+ continue;
739
+ }
740
+ stopIds.splice(index, 0, stoptime.stop_id);
741
+ }
742
+ }
743
+ return stopIds;
744
+ };
745
+ var getStopOrder = (timetable, config) => {
746
+ const timetableStopOrders = getTimetableStopOrders(
747
+ {
748
+ timetable_id: timetable.timetable_id
749
+ },
750
+ ["stop_id"],
751
+ [["stop_sequence", "ASC"]]
752
+ );
753
+ if (timetableStopOrders.length > 0) {
754
+ return timetableStopOrders.map(
755
+ (timetableStopOrder) => timetableStopOrder.stop_id
756
+ );
757
+ }
758
+ try {
759
+ const stopGraph = [];
760
+ for (const trip of timetable.orderedTrips) {
761
+ const sortedStopIds = trip.stoptimes.filter((stoptime) => {
762
+ if (config.showOnlyTimepoint === true) {
763
+ return isTimepoint(stoptime);
764
+ }
765
+ return true;
766
+ }).map((stoptime) => stoptime.stop_id);
767
+ for (const [index, stopId] of sortedStopIds.entries()) {
768
+ if (index === sortedStopIds.length - 1) {
769
+ continue;
770
+ }
771
+ stopGraph.push([stopId, sortedStopIds[index + 1]]);
772
+ }
773
+ }
774
+ const stopIds2 = toposort(stopGraph);
775
+ return duplicateStopsForDifferentArrivalDeparture(
776
+ stopIds2,
777
+ timetable,
778
+ config
779
+ );
780
+ } catch {
781
+ }
782
+ const longestTripStoptimes = getLongestTripStoptimes(
783
+ timetable.orderedTrips,
784
+ config
785
+ );
786
+ const stopIds = longestTripStoptimes.map((stoptime) => stoptime.stop_id);
787
+ return duplicateStopsForDifferentArrivalDeparture(stopIds, timetable, config);
788
+ };
789
+ var getStopsForTimetable = (timetable, config) => {
790
+ if (timetable.orderedTrips.length === 0) {
791
+ return [];
792
+ }
793
+ const orderedStopIds = getStopOrder(timetable, config);
794
+ const orderedStops = orderedStopIds.map((stopId, index) => {
795
+ const stops = getStops({
796
+ stop_id: stopId
797
+ });
798
+ if (stops.length === 0) {
799
+ throw new Error(
800
+ `No stop found found for stop_id=${stopId} in timetable_id=${timetable.timetable_id}`
801
+ );
802
+ }
803
+ const stop = {
804
+ ...stops[0],
805
+ trips: []
806
+ };
807
+ if (index < orderedStopIds.length - 1 && stopId === orderedStopIds[index + 1]) {
808
+ stop.type = "arrival";
809
+ } else if (index > 0 && stopId === orderedStopIds[index - 1]) {
810
+ stop.type = "departure";
811
+ }
812
+ return stop;
813
+ });
814
+ if (timetable.showStopCity) {
815
+ const stopAttributes = getStopAttributes({
816
+ stop_id: orderedStopIds
817
+ });
818
+ for (const stopAttribute of stopAttributes) {
819
+ const stop = orderedStops.find(
820
+ (stop2) => stop2.stop_id === stopAttribute.stop_id
821
+ );
822
+ if (stop) {
823
+ stop.stop_city = stopAttribute.stop_city;
824
+ }
825
+ }
826
+ }
827
+ return orderedStops;
828
+ };
829
+ var getCalendarsFromTimetable = (timetable) => {
830
+ const db = openDb();
831
+ let whereClause = "";
832
+ const whereClauses = [];
833
+ if (timetable.end_date) {
834
+ if (!moment2(timetable.end_date, "YYYYMMDD", true).isValid()) {
835
+ throw new Error(
836
+ `Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`
837
+ );
838
+ }
839
+ whereClauses.push(`start_date <= ${sqlString.escape(timetable.end_date)}`);
840
+ }
841
+ if (timetable.start_date) {
842
+ if (!moment2(timetable.start_date, "YYYYMMDD", true).isValid()) {
843
+ throw new Error(
844
+ `Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`
845
+ );
846
+ }
847
+ whereClauses.push(`end_date >= ${sqlString.escape(timetable.start_date)}`);
848
+ }
849
+ const days2 = getDaysFromCalendars([timetable]);
850
+ const dayQueries = reduce(
851
+ days2,
852
+ (memo, value, key) => {
853
+ if (value === 1) {
854
+ memo.push(`${key} = 1`);
855
+ }
856
+ return memo;
857
+ },
858
+ []
859
+ );
860
+ if (dayQueries.length > 0) {
861
+ whereClauses.push(`(${dayQueries.join(" OR ")})`);
862
+ }
863
+ if (whereClauses.length > 0) {
864
+ whereClause = `WHERE ${whereClauses.join(" AND ")}`;
865
+ }
866
+ return db.prepare(`SELECT * FROM calendar ${whereClause}`).all();
867
+ };
868
+ var getCalendarDatesServiceIds = (startDate, endDate) => {
869
+ const db = openDb();
870
+ const whereClauses = ["exception_type = 1"];
871
+ if (endDate) {
872
+ whereClauses.push(`date <= ${sqlString.escape(endDate)}`);
873
+ }
874
+ if (startDate) {
875
+ whereClauses.push(`date >= ${sqlString.escape(startDate)}`);
876
+ }
877
+ const calendarDates = db.prepare(
878
+ `SELECT DISTINCT service_id FROM calendar_dates WHERE ${whereClauses.join(
879
+ " AND "
880
+ )}`
881
+ ).all();
882
+ return calendarDates.map((calendarDate) => calendarDate.service_id);
883
+ };
884
+ var getAllStationStopIds = (stopId) => {
885
+ const stops = getStops({
886
+ stop_id: stopId
887
+ });
888
+ if (stops.length === 0) {
889
+ throw new Error(`No stop found for stop_id=${stopId}`);
890
+ }
891
+ const stop = stops[0];
892
+ if (isNullOrEmpty(stop.parent_station)) {
893
+ return [stopId];
894
+ }
895
+ const stopsInParentStation = getStops(
896
+ {
897
+ parent_station: stop.parent_station
898
+ },
899
+ ["stop_id"]
900
+ );
901
+ return [
902
+ stop.parent_station,
903
+ ...stopsInParentStation.map((stop2) => stop2.stop_id)
904
+ ];
905
+ };
906
+ var getTripsWithSameBlock = (trip, timetable) => {
907
+ const trips = getTrips(
908
+ {
909
+ block_id: trip.block_id,
910
+ service_id: timetable.service_ids
911
+ },
912
+ ["trip_id", "route_id"]
913
+ );
914
+ for (const blockTrip of trips) {
915
+ const stopTimes = getStoptimes(
916
+ {
917
+ trip_id: blockTrip.trip_id
918
+ },
919
+ [],
920
+ [["stop_sequence", "ASC"]]
921
+ );
922
+ if (stopTimes.length === 0) {
923
+ throw new Error(
924
+ `No stoptimes found found for trip_id=${blockTrip.trip_id}`
925
+ );
926
+ }
927
+ blockTrip.firstStoptime = first(stopTimes);
928
+ blockTrip.lastStoptime = last(stopTimes);
929
+ }
930
+ return sortBy(trips, (trip2) => trip2.firstStoptime.departure_timestamp);
931
+ };
932
+ var addTripContinuation = (trip, timetable) => {
933
+ if (!trip.block_id || trip.stoptimes.length === 0) {
934
+ return;
935
+ }
936
+ const maxContinuesAsWaitingTimeSeconds = 60 * 60;
937
+ const firstStoptime = first(trip.stoptimes);
938
+ const firstStopIds = getAllStationStopIds(firstStoptime.stop_id);
939
+ const lastStoptime = last(trip.stoptimes);
940
+ const lastStopIds = getAllStationStopIds(lastStoptime.stop_id);
941
+ const blockTrips = getTripsWithSameBlock(trip, timetable);
942
+ const previousTrip = findLast(
943
+ blockTrips,
944
+ (blockTrip) => blockTrip.lastStoptime.arrival_timestamp <= firstStoptime.departure_timestamp
945
+ );
946
+ if (previousTrip && previousTrip.route_id !== trip.route_id && previousTrip.lastStoptime.arrival_timestamp >= firstStoptime.departure_timestamp - maxContinuesAsWaitingTimeSeconds && firstStopIds.includes(previousTrip.lastStoptime.stop_id)) {
947
+ const routes = getRoutes({
948
+ route_id: previousTrip.route_id
949
+ });
950
+ previousTrip.route = routes[0];
951
+ trip.continues_from_route = previousTrip;
952
+ }
953
+ const nextTrip = find(
954
+ blockTrips,
955
+ (blockTrip) => blockTrip.firstStoptime.departure_timestamp >= lastStoptime.arrival_timestamp
956
+ );
957
+ if (nextTrip && nextTrip.route_id !== trip.route_id && nextTrip.firstStoptime.departure_timestamp <= lastStoptime.arrival_timestamp + maxContinuesAsWaitingTimeSeconds && lastStopIds.includes(nextTrip.firstStoptime.stop_id)) {
958
+ const routes = getRoutes({
959
+ route_id: nextTrip.route_id
960
+ });
961
+ nextTrip.route = routes[0];
962
+ trip.continues_as_route = nextTrip;
963
+ }
964
+ };
965
+ var filterTrips = (timetable) => {
966
+ let filteredTrips = timetable.orderedTrips;
967
+ for (const trip of filteredTrips) {
968
+ const combinedStoptimes = [];
969
+ for (const [index, stoptime] of trip.stoptimes.entries()) {
970
+ if (index === 0 || stoptime.stop_id !== trip.stoptimes[index - 1].stop_id) {
971
+ combinedStoptimes.push(stoptime);
972
+ } else {
973
+ combinedStoptimes[combinedStoptimes.length - 1].departure_time = stoptime.departure_time;
974
+ }
975
+ }
976
+ trip.stoptimes = combinedStoptimes;
977
+ }
978
+ const timetableStopIds = new Set(timetable.stops.map((stop) => stop.stop_id));
979
+ for (const trip of filteredTrips) {
980
+ trip.stoptimes = trip.stoptimes.filter(
981
+ (stoptime) => timetableStopIds.has(stoptime.stop_id)
982
+ );
983
+ }
984
+ filteredTrips = filteredTrips.filter((trip) => trip.stoptimes.length > 1);
985
+ return filteredTrips;
986
+ };
987
+ var getTripsForTimetable = (timetable, calendars, config) => {
988
+ const tripQuery = {
989
+ route_id: timetable.route_ids,
990
+ service_id: timetable.service_ids
991
+ };
992
+ if (!isNullOrEmpty(timetable.direction_id)) {
993
+ tripQuery.direction_id = timetable.direction_id;
994
+ }
995
+ const trips = getTrips(tripQuery);
996
+ if (trips.length === 0) {
997
+ timetable.warnings.push(
998
+ `No trips found for route_id=${timetable.route_ids.join(
999
+ "_"
1000
+ )}, direction_id=${timetable.direction_id}, service_ids=${JSON.stringify(
1001
+ timetable.service_ids
1002
+ )}, timetable_id=${timetable.timetable_id}`
1003
+ );
1004
+ }
1005
+ const frequencies = getFrequencies({
1006
+ trip_id: trips.map((trip) => trip.trip_id)
1007
+ });
1008
+ timetable.service_ids = uniq(trips.map((trip) => trip.service_id));
1009
+ const formattedTrips = [];
1010
+ for (const trip of trips) {
1011
+ const formattedTrip = formatTrip(trip, timetable, calendars, config);
1012
+ formattedTrip.stoptimes = getStoptimes(
1013
+ {
1014
+ trip_id: formattedTrip.trip_id
1015
+ },
1016
+ [],
1017
+ [["stop_sequence", "ASC"]]
1018
+ );
1019
+ if (formattedTrip.stoptimes.length === 0) {
1020
+ timetable.warnings.push(
1021
+ `No stoptimes found for trip_id=${formattedTrip.trip_id}, route_id=${timetable.route_ids.join("_")}, timetable_id=${timetable.timetable_id}`
1022
+ );
1023
+ }
1024
+ if (timetable.start_timestamp !== "" && timetable.start_timestamp !== null && timetable.start_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp < timetable.start_timestamp) {
1025
+ return;
1026
+ }
1027
+ if (timetable.end_timestamp !== "" && timetable.end_timestamp !== null && timetable.end_timestamp !== void 0 && trip.stoptimes[0].arrival_timestamp >= timetable.end_timestamp) {
1028
+ return;
1029
+ }
1030
+ if (timetable.show_trip_continuation) {
1031
+ addTripContinuation(formattedTrip, timetable);
1032
+ if (formattedTrip.continues_as_route) {
1033
+ timetable.has_continues_as_route = true;
1034
+ }
1035
+ if (formattedTrip.continues_from_route) {
1036
+ timetable.has_continues_from_route = true;
1037
+ }
1038
+ }
1039
+ const tripFrequencies = frequencies.filter(
1040
+ (frequency) => frequency.trip_id === trip.trip_id
1041
+ );
1042
+ if (tripFrequencies.length === 0) {
1043
+ formattedTrips.push(formattedTrip);
1044
+ } else {
1045
+ const frequencyTrips = generateTripsByFrequencies(
1046
+ formattedTrip,
1047
+ frequencies,
1048
+ config
1049
+ );
1050
+ formattedTrips.push(...frequencyTrips);
1051
+ timetable.frequencies = frequencies;
1052
+ timetable.frequencyExactTimes = some(frequencies, {
1053
+ exact_times: 1
1054
+ });
1055
+ }
1056
+ }
1057
+ if (config.useParentStation) {
1058
+ const stopIds = [];
1059
+ for (const trip of formattedTrips) {
1060
+ for (const stoptime of trip.stoptimes) {
1061
+ stopIds.push(stoptime.stop_id);
1062
+ }
1063
+ }
1064
+ const stops = getStops(
1065
+ {
1066
+ stop_id: uniq(stopIds)
1067
+ },
1068
+ ["parent_station", "stop_id"]
1069
+ );
1070
+ for (const trip of formattedTrips) {
1071
+ for (const stoptime of trip.stoptimes) {
1072
+ const stop = stops.find((stop2) => stop2.stop_id === stoptime.stop_id);
1073
+ if (stop?.parent_station) {
1074
+ stoptime.stop_id = stop.parent_station;
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+ return sortTrips(formattedTrips, config);
1080
+ };
1081
+ var formatTimetables = (timetables, config) => {
1082
+ const formattedTimetables = timetables.map((timetable) => {
1083
+ timetable.warnings = [];
1084
+ const dayList = formatDays(timetable, config);
1085
+ const calendars = getCalendarsFromTimetable(timetable);
1086
+ let serviceIds = calendars.map((calendar) => calendar.service_id);
1087
+ if (timetable.include_exceptions === 1) {
1088
+ const calendarDatesServiceIds = getCalendarDatesServiceIds(
1089
+ timetable.start_date,
1090
+ timetable.end_date
1091
+ );
1092
+ serviceIds = uniq([...serviceIds, ...calendarDatesServiceIds]);
1093
+ }
1094
+ Object.assign(timetable, {
1095
+ noServiceSymbolUsed: false,
1096
+ requestDropoffSymbolUsed: false,
1097
+ noDropoffSymbolUsed: false,
1098
+ requestPickupSymbolUsed: false,
1099
+ noPickupSymbolUsed: false,
1100
+ interpolatedStopSymbolUsed: false,
1101
+ showStopCity: config.showStopCity,
1102
+ showStopDescription: config.showStopDescription,
1103
+ noServiceSymbol: config.noServiceSymbol,
1104
+ requestDropoffSymbol: config.requestDropoffSymbol,
1105
+ noDropoffSymbol: config.noDropoffSymbol,
1106
+ requestPickupSymbol: config.requestPickupSymbol,
1107
+ noPickupSymbol: config.noPickupSymbol,
1108
+ interpolatedStopSymbol: config.interpolatedStopSymbol,
1109
+ orientation: timetable.orientation || config.defaultOrientation,
1110
+ service_ids: serviceIds,
1111
+ dayList,
1112
+ dayListLong: formatDaysLong(dayList, config)
1113
+ });
1114
+ timetable.orderedTrips = getTripsForTimetable(timetable, calendars, config);
1115
+ timetable.stops = getStopsForTimetable(timetable, config);
1116
+ timetable.calendarDates = getCalendarDatesForTimetable(timetable, config);
1117
+ timetable.timetable_label = formatTimetableLabel(timetable);
1118
+ timetable.notes = getTimetableNotesForTimetable(timetable, config);
1119
+ if (config.showMap) {
1120
+ timetable.geojson = getTimetableGeoJSON(timetable, config);
1121
+ }
1122
+ timetable.orderedTrips = filterTrips(timetable);
1123
+ timetable.stops = formatStops(timetable, config);
1124
+ return timetable;
1125
+ });
1126
+ if (config.allowEmptyTimetables) {
1127
+ return formattedTimetables;
1128
+ }
1129
+ return formattedTimetables.filter(
1130
+ (timetable) => timetable.orderedTrips.length > 0
1131
+ );
1132
+ };
1133
+ function getTimetablePagesForAgency(config) {
1134
+ const timetables = mergeTimetablesWithSameId(getTimetables());
1135
+ if (timetables.length === 0) {
1136
+ return convertRoutesToTimetablePages(config);
1137
+ }
1138
+ const timetablePages = getTimetablePages(
1139
+ {},
1140
+ [],
1141
+ [["timetable_page_id", "ASC"]]
1142
+ );
1143
+ if (timetablePages.length === 0) {
1144
+ return timetables.map(
1145
+ (timetable) => convertTimetableToTimetablePage(timetable, config)
1146
+ );
1147
+ }
1148
+ const routes = getRoutes();
1149
+ return timetablePages.map((timetablePage) => {
1150
+ timetablePage.timetables = sortBy(
1151
+ timetables.filter(
1152
+ (timetable) => timetable.timetable_page_id === timetablePage.timetable_page_id
1153
+ ),
1154
+ "timetable_sequence"
1155
+ );
1156
+ for (const timetable of timetablePage.timetables) {
1157
+ timetable.routes = routes.filter(
1158
+ (route) => timetable.route_ids.includes(route.route_id)
1159
+ );
1160
+ }
1161
+ return timetablePage;
1162
+ });
1163
+ }
1164
+ var getTimetablePageById = (timetablePageId, config) => {
1165
+ const timetablePages = getTimetablePages({
1166
+ timetable_page_id: timetablePageId
1167
+ });
1168
+ const timetables = mergeTimetablesWithSameId(getTimetables());
1169
+ if (timetablePages.length > 1) {
1170
+ throw new Error(
1171
+ `Multiple timetable_pages found for timetable_page_id=${timetablePageId}`
1172
+ );
1173
+ }
1174
+ if (timetablePages.length === 1) {
1175
+ const timetablePage = timetablePages[0];
1176
+ timetablePage.timetables = sortBy(
1177
+ timetables.filter(
1178
+ (timetable) => timetable.timetable_page_id === timetablePageId
1179
+ ),
1180
+ "timetable_sequence"
1181
+ );
1182
+ for (const timetable of timetablePage.timetables) {
1183
+ timetable.routes = getRoutes({
1184
+ route_id: timetable.route_ids
1185
+ });
1186
+ }
1187
+ return timetablePage;
1188
+ }
1189
+ if (timetables.length > 0) {
1190
+ const timetablePageTimetables = timetables.filter(
1191
+ (timetable) => timetable.timetable_id === timetablePageId
1192
+ );
1193
+ if (timetablePageTimetables.length === 0) {
1194
+ throw new Error(
1195
+ `No timetable found for timetable_page_id=${timetablePageId}`
1196
+ );
1197
+ }
1198
+ return convertTimetableToTimetablePage(timetablePageTimetables[0], config);
1199
+ }
1200
+ let calendarCode;
1201
+ let calendars;
1202
+ let calendarDates;
1203
+ let serviceId;
1204
+ let directionId = "";
1205
+ const parts = timetablePageId.split("|");
1206
+ if (parts.length > 2) {
1207
+ directionId = Number.parseInt(parts.pop(), 10);
1208
+ calendarCode = parts.pop();
1209
+ } else if (parts.length > 1) {
1210
+ directionId = null;
1211
+ calendarCode = parts.pop();
1212
+ }
1213
+ const routeId = parts.join("|");
1214
+ const routes = getRoutes({
1215
+ route_id: routeId
1216
+ });
1217
+ const trips = getTrips(
1218
+ {
1219
+ route_id: routeId,
1220
+ direction_id: directionId
1221
+ },
1222
+ ["trip_headsign", "direction_id"]
1223
+ );
1224
+ const directions = uniqBy(trips, (trip) => trip.direction_id);
1225
+ if (directions.length === 0) {
1226
+ throw new Error(
1227
+ `No trips found for timetable_page_id=${timetablePageId} route_id=${routeId} direction_id=${directionId}`
1228
+ );
1229
+ }
1230
+ if (/^[01]*$/.test(calendarCode)) {
1231
+ calendars = getCalendars({
1232
+ ...calendarCodeToCalendar(calendarCode)
1233
+ });
1234
+ } else {
1235
+ serviceId = calendarCode;
1236
+ calendarDates = getCalendarDates({
1237
+ exception_type: 1,
1238
+ service_id: serviceId
1239
+ });
1240
+ }
1241
+ return convertRouteToTimetablePage(
1242
+ routes[0],
1243
+ directions[0],
1244
+ calendars,
1245
+ calendarDates,
1246
+ config
1247
+ );
1248
+ };
1249
+ function setDefaultConfig(initialConfig) {
1250
+ const defaults = {
1251
+ allowEmptyTimetables: false,
1252
+ beautify: false,
1253
+ coordinatePrecision: 5,
1254
+ dateFormat: "MMM D, YYYY",
1255
+ daysShortStrings: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
1256
+ daysStrings: [
1257
+ "Monday",
1258
+ "Tuesday",
1259
+ "Wednesday",
1260
+ "Thursday",
1261
+ "Friday",
1262
+ "Saturday",
1263
+ "Sunday"
1264
+ ],
1265
+ defaultOrientation: "vertical",
1266
+ interpolatedStopSymbol: "\u2022",
1267
+ interpolatedStopText: "Estimated time of arrival",
1268
+ gtfsToHtmlVersion: version,
1269
+ linkStopUrls: false,
1270
+ menuType: "jump",
1271
+ noDropoffSymbol: "\u2021",
1272
+ noDropoffText: "No drop off available",
1273
+ noHead: false,
1274
+ noPickupSymbol: "**",
1275
+ noPickupText: "No pickup available",
1276
+ noServiceSymbol: "-",
1277
+ noServiceText: "No service at this stop",
1278
+ outputFormat: "html",
1279
+ requestDropoffSymbol: "\u2020",
1280
+ requestDropoffText: "Must request drop off",
1281
+ requestPickupSymbol: "***",
1282
+ requestPickupText: "Request stop - call for pickup",
1283
+ serviceNotProvidedOnText: "Service not provided on",
1284
+ serviceProvidedOnText: "Service provided on",
1285
+ showArrivalOnDifference: 0.2,
1286
+ showCalendarExceptions: true,
1287
+ showMap: false,
1288
+ showOnlyTimepoint: false,
1289
+ showRouteTitle: true,
1290
+ showStopCity: false,
1291
+ showStopDescription: false,
1292
+ showStoptimesForRequestStops: true,
1293
+ skipImport: false,
1294
+ sortingAlgorithm: "common",
1295
+ timeFormat: "h:mma",
1296
+ useParentStation: true,
1297
+ verbose: true,
1298
+ zipOutput: false
1299
+ };
1300
+ const config = Object.assign(defaults, initialConfig);
1301
+ if (config.outputFormat === "pdf") {
1302
+ config.noHead = false;
1303
+ }
1304
+ return config;
1305
+ }
1306
+ function getFormattedTimetablePage(timetablePageId, config) {
1307
+ const timetablePage = getTimetablePageById(
1308
+ timetablePageId,
1309
+ config
1310
+ );
1311
+ const timetableRoutes = getRoutes(
1312
+ {
1313
+ route_id: timetablePage.route_ids
1314
+ },
1315
+ ["agency_id"]
1316
+ );
1317
+ const consolidatedTimetables = formatTimetables(
1318
+ timetablePage.timetables,
1319
+ config
1320
+ );
1321
+ for (const timetable of consolidatedTimetables) {
1322
+ if (isNullOrEmpty(timetable.direction_name)) {
1323
+ timetable.direction_name = getDirectionHeadsignFromTimetable(timetable);
1324
+ }
1325
+ if (!timetable.routes) {
1326
+ timetable.routes = getRoutes({
1327
+ route_id: timetable.route_ids
1328
+ });
1329
+ }
1330
+ }
1331
+ const formattedTimetablePage = {
1332
+ ...timetablePage,
1333
+ consolidatedTimetables,
1334
+ dayList: formatDays(getDaysFromCalendars(consolidatedTimetables), config),
1335
+ dayLists: uniq(
1336
+ consolidatedTimetables.map((timetable) => timetable.dayList)
1337
+ ),
1338
+ route_ids: uniq(flatMap2(consolidatedTimetables, "route_ids")),
1339
+ agency_ids: uniq(compact(timetableRoutes.map((route) => route.agency_id))),
1340
+ filename: timetablePage.filename ?? `${timetablePage.timetable_page_id}.html`
1341
+ };
1342
+ return formattedTimetablePage;
1343
+ }
1344
+ var generateStats = (timetablePage) => {
1345
+ const routeIds = {};
1346
+ const serviceIds = {};
1347
+ const stats = {
1348
+ stops: 0,
1349
+ trips: 0,
1350
+ routes: 0,
1351
+ calendars: 0
1352
+ };
1353
+ for (const timetable of timetablePage.consolidatedTimetables) {
1354
+ stats.stops += timetable.stops.length;
1355
+ stats.trips += timetable.orderedTrips.length;
1356
+ for (const serviceId of timetable.service_ids) {
1357
+ serviceIds[serviceId] = true;
1358
+ }
1359
+ for (const routeId of timetable.route_ids) {
1360
+ routeIds[routeId] = true;
1361
+ }
1362
+ }
1363
+ stats.routes = size(routeIds);
1364
+ stats.calendars = size(serviceIds);
1365
+ return stats;
1366
+ };
1367
+ function generateTimetableHTML(timetablePage, config) {
1368
+ const templateVars = {
1369
+ timetablePage,
1370
+ config
1371
+ };
1372
+ return renderTemplate("timetablepage", templateVars, config);
1373
+ }
1374
+ function generateTimetableCSV(timetable) {
1375
+ const lines = [];
1376
+ lines.push([
1377
+ "",
1378
+ ...timetable.orderedTrips.map(
1379
+ (trip) => formatTripNameForCSV(trip, timetable)
1380
+ )
1381
+ ]);
1382
+ if (timetable.has_continues_from_route) {
1383
+ lines.push([
1384
+ "Continues from route",
1385
+ ...timetable.orderedTrips.map((trip) => formatTripContinuesFrom(trip))
1386
+ ]);
1387
+ }
1388
+ for (const stop of timetable.stops) {
1389
+ lines.push([
1390
+ formatStopName(stop),
1391
+ ...stop.trips.map((stoptime) => stoptime.formatted_time)
1392
+ ]);
1393
+ }
1394
+ if (timetable.has_continues_as_route) {
1395
+ lines.push([
1396
+ "Continues as route",
1397
+ ...timetable.orderedTrips.map((trip) => formatTripContinuesAs(trip))
1398
+ ]);
1399
+ }
1400
+ if (timetable.orientation === "vertical") {
1401
+ return stringify(zip(...lines));
1402
+ }
1403
+ return stringify(lines);
1404
+ }
1405
+ function generateOverviewHTML(timetablePages, config) {
1406
+ const agencies = getAgencies();
1407
+ if (agencies.length === 0) {
1408
+ throw new Error("No agencies found");
1409
+ }
1410
+ let geojson;
1411
+ if (config.showMap) {
1412
+ geojson = getAgencyGeoJSON(config);
1413
+ }
1414
+ const sortedTimetablePages = sortBy(timetablePages, [
1415
+ (timetablePage) => {
1416
+ if (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
1417
+ return Number.parseInt(
1418
+ timetablePage.consolidatedTimetables[0].routes[0].route_short_name?.replace(
1419
+ /^\D+/g,
1420
+ ""
1421
+ ),
1422
+ 10
1423
+ ) || 0;
1424
+ }
1425
+ },
1426
+ (timetablePage) => {
1427
+ if (timetablePage.consolidatedTimetables.length > 0 && timetablePage.consolidatedTimetables[0].routes.length > 0) {
1428
+ return timetablePage.consolidatedTimetables[0].routes[0].route_short_name;
1429
+ }
1430
+ }
1431
+ ]);
1432
+ const templateVars = {
1433
+ agency: {
1434
+ ...first(agencies),
1435
+ geojson
1436
+ },
1437
+ agencies,
1438
+ geojson,
1439
+ config,
1440
+ timetablePages: sortedTimetablePages
1441
+ };
1442
+ return renderTemplate("overview", templateVars, config);
1443
+ }
1444
+
1445
+ // src/lib/formatters.ts
1446
+ function replaceAll(string, mapObject) {
1447
+ const re = new RegExp(Object.keys(mapObject).join("|"), "gi");
1448
+ return string.replace(re, (matched) => mapObject[matched]);
1449
+ }
1450
+ function isNullOrEmpty(value) {
1451
+ return value === null || value === "";
1452
+ }
1453
+ function formatDate(date, dateFormat) {
1454
+ if (date.holiday_name) {
1455
+ return date.holiday_name;
1456
+ }
1457
+ return moment3(date.date, "YYYYMMDD").format(dateFormat);
1458
+ }
1459
+ function timeToSeconds(time) {
1460
+ return moment3.duration(time).asSeconds();
1461
+ }
1462
+ function formatStopTime(stoptime, timetable, config) {
1463
+ stoptime.classes = [];
1464
+ if (stoptime.type === "arrival" && stoptime.arrival_time) {
1465
+ const arrivalTime = fromGTFSTime(stoptime.arrival_time);
1466
+ stoptime.formatted_time = arrivalTime.format(config.timeFormat);
1467
+ stoptime.classes.push(arrivalTime.format("a"));
1468
+ } else if (stoptime.type === "departure" && stoptime.departure_time) {
1469
+ const departureTime = fromGTFSTime(stoptime.departure_time);
1470
+ stoptime.formatted_time = departureTime.format(config.timeFormat);
1471
+ stoptime.classes.push(departureTime.format("a"));
1472
+ }
1473
+ if (stoptime.pickup_type === 1) {
1474
+ stoptime.noPickup = true;
1475
+ stoptime.classes.push("no-pickup");
1476
+ if (timetable.noPickupSymbol !== null) {
1477
+ timetable.noPickupSymbolUsed = true;
1478
+ }
1479
+ } else if (stoptime.pickup_type === 2 || stoptime.pickup_type === 3) {
1480
+ stoptime.requestPickup = true;
1481
+ stoptime.classes.push("request-pickup");
1482
+ if (timetable.requestPickupSymbol !== null) {
1483
+ timetable.requestPickupSymbolUsed = true;
1484
+ }
1485
+ }
1486
+ if (stoptime.drop_off_type === 1) {
1487
+ stoptime.noDropoff = true;
1488
+ stoptime.classes.push("no-drop-off");
1489
+ if (timetable.noDropoffSymbol !== null) {
1490
+ timetable.noDropoffSymbolUsed = true;
1491
+ }
1492
+ } else if (stoptime.drop_off_type === 2 || stoptime.drop_off_type === 3) {
1493
+ stoptime.requestDropoff = true;
1494
+ stoptime.classes.push("request-drop-off");
1495
+ if (timetable.requestDropoffSymbol !== null) {
1496
+ timetable.requestDropoffSymbolUsed = true;
1497
+ }
1498
+ }
1499
+ if (stoptime.timepoint === 0 || stoptime.departure_time === "") {
1500
+ stoptime.interpolated = true;
1501
+ stoptime.classes.push("interpolated");
1502
+ if (timetable.interpolatedStopSymbol !== null) {
1503
+ timetable.interpolatedStopSymbolUsed = true;
1504
+ }
1505
+ }
1506
+ if (stoptime.timepoint === null && stoptime.departure_time === null) {
1507
+ stoptime.skipped = true;
1508
+ stoptime.classes.push("skipped");
1509
+ if (timetable.noServiceSymbol !== null) {
1510
+ timetable.noServiceSymbolUsed = true;
1511
+ }
1512
+ }
1513
+ if (stoptime.timepoint === 1) {
1514
+ stoptime.classes.push("timepoint");
1515
+ }
1516
+ return stoptime;
1517
+ }
1518
+ function filterHourlyTimes(stops) {
1519
+ const firstStopTimes = [];
1520
+ const firstTripMinutes = minutesAfterMidnight(stops[0].trips[0].arrival_time);
1521
+ for (const trip of stops[0].trips) {
1522
+ const minutes = minutesAfterMidnight(trip.arrival_time);
1523
+ if (minutes >= firstTripMinutes + 60) {
1524
+ break;
1525
+ }
1526
+ firstStopTimes.push(fromGTFSTime(trip.arrival_time));
1527
+ }
1528
+ const firstStopTimesAndIndex = firstStopTimes.map((time, idx) => ({
1529
+ idx,
1530
+ time
1531
+ }));
1532
+ const sortedFirstStopTimesAndIndex = sortBy2(
1533
+ firstStopTimesAndIndex,
1534
+ (item) => Number.parseInt(item.time.format("m"), 10)
1535
+ );
1536
+ return stops.map((stop) => {
1537
+ stop.hourlyTimes = sortedFirstStopTimesAndIndex.map(
1538
+ (item) => fromGTFSTime(stop.trips[item.idx].arrival_time).format(":mm")
1539
+ );
1540
+ return stop;
1541
+ });
1542
+ }
1543
+ var days = [
1544
+ "monday",
1545
+ "tuesday",
1546
+ "wednesday",
1547
+ "thursday",
1548
+ "friday",
1549
+ "saturday",
1550
+ "sunday"
1551
+ ];
1552
+ function formatDays(calendar, config) {
1553
+ const daysShort = config.daysShortStrings;
1554
+ let daysInARow = 0;
1555
+ let dayString = "";
1556
+ if (!calendar) {
1557
+ return "";
1558
+ }
1559
+ for (let i = 0; i <= 6; i += 1) {
1560
+ const currentDayOperating = calendar[days[i]] === 1;
1561
+ const previousDayOperating = i > 0 ? calendar[days[i - 1]] === 1 : false;
1562
+ const nextDayOperating = i < 6 ? calendar[days[i + 1]] === 1 : false;
1563
+ if (currentDayOperating) {
1564
+ if (dayString.length > 0) {
1565
+ if (!previousDayOperating) {
1566
+ dayString += ", ";
1567
+ } else if (daysInARow === 1) {
1568
+ dayString += "-";
1569
+ }
1570
+ }
1571
+ daysInARow += 1;
1572
+ if (dayString.length === 0 || !nextDayOperating || i === 6 || !previousDayOperating) {
1573
+ dayString += daysShort[i];
1574
+ }
1575
+ } else {
1576
+ daysInARow = 0;
1577
+ }
1578
+ }
1579
+ if (dayString.length === 0) {
1580
+ dayString = "No regular service days";
1581
+ }
1582
+ return dayString;
1583
+ }
1584
+ function formatDaysLong(dayList, config) {
1585
+ const mapObject = zipObject(config.daysShortStrings, config.daysStrings);
1586
+ return replaceAll(dayList, mapObject);
1587
+ }
1588
+ function formatTrip(trip, timetable, calendars, config) {
1589
+ trip.calendar = find2(calendars, {
1590
+ service_id: trip.service_id
1591
+ });
1592
+ trip.dayList = formatDays(trip.calendar, config);
1593
+ trip.dayListLong = formatDaysLong(trip.dayList, config);
1594
+ if (timetable.routes.length === 1) {
1595
+ trip.route_short_name = timetable.routes[0].route_short_name;
1596
+ } else {
1597
+ const route = timetable.routes.find(
1598
+ (route2) => route2.route_id === trip.route_id
1599
+ );
1600
+ trip.route_short_name = route.route_short_name;
1601
+ }
1602
+ return trip;
1603
+ }
1604
+ function formatFrequency(frequency, config) {
1605
+ const startTime = fromGTFSTime(frequency.start_time);
1606
+ const endTime = fromGTFSTime(frequency.end_time);
1607
+ const headway = moment3.duration(frequency.headway_secs, "seconds");
1608
+ frequency.start_formatted_time = startTime.format(config.timeFormat);
1609
+ frequency.end_formatted_time = endTime.format(config.timeFormat);
1610
+ frequency.headway_min = Math.round(headway.asMinutes());
1611
+ return frequency;
1612
+ }
1613
+ function formatTimetableId(timetable) {
1614
+ let timetableId = `${timetable.route_ids.join("_")}|${calendarToCalendarCode(
1615
+ timetable
1616
+ )}`;
1617
+ if (!isNullOrEmpty(timetable.direction_id)) {
1618
+ timetableId += `|${timetable.direction_id}`;
1619
+ }
1620
+ return timetableId;
1621
+ }
1622
+ function createEmptyStoptime(stopId, tripId) {
1623
+ return {
1624
+ id: null,
1625
+ trip_id: tripId,
1626
+ arrival_time: null,
1627
+ departure_time: null,
1628
+ stop_id: stopId,
1629
+ stop_sequence: null,
1630
+ stop_headsign: null,
1631
+ pickup_type: null,
1632
+ drop_off_type: null,
1633
+ continuous_pickup: null,
1634
+ continuous_drop_off: null,
1635
+ shape_dist_traveled: null,
1636
+ timepoint: null
1637
+ };
1638
+ }
1639
+ function formatStops(timetable, config) {
1640
+ for (const trip of timetable.orderedTrips) {
1641
+ let stopIndex = -1;
1642
+ for (const [idx, stoptime] of trip.stoptimes.entries()) {
1643
+ const stop = find2(timetable.stops, (st, idx2) => {
1644
+ if (st.stop_id === stoptime.stop_id && idx2 > stopIndex) {
1645
+ stopIndex = idx2;
1646
+ return true;
1647
+ }
1648
+ return false;
1649
+ });
1650
+ if (!stop) {
1651
+ continue;
1652
+ }
1653
+ if (idx === 0) {
1654
+ stoptime.drop_off_type = 0;
1655
+ }
1656
+ if (idx === trip.stoptimes.length - 1) {
1657
+ stoptime.pickup_type = 0;
1658
+ }
1659
+ if (stop.type === "arrival" && idx < trip.stoptimes.length - 1) {
1660
+ const departureStoptime = clone(stoptime);
1661
+ departureStoptime.type = "departure";
1662
+ timetable.stops[stopIndex + 1].trips.push(
1663
+ formatStopTime(departureStoptime, timetable, config)
1664
+ );
1665
+ }
1666
+ if (!(stop.type === "arrival" && idx === 0)) {
1667
+ stoptime.type = "arrival";
1668
+ stop.trips.push(formatStopTime(stoptime, timetable, config));
1669
+ }
1670
+ }
1671
+ for (const stop of timetable.stops) {
1672
+ const lastStopTime = last2(stop.trips);
1673
+ if (!lastStopTime || lastStopTime.trip_id !== trip.trip_id) {
1674
+ stop.trips.push(
1675
+ formatStopTime(
1676
+ createEmptyStoptime(stop.stop_id, trip.trip_id),
1677
+ timetable,
1678
+ config
1679
+ )
1680
+ );
1681
+ }
1682
+ }
1683
+ }
1684
+ if (timetable.orientation === "hourly") {
1685
+ timetable.stops = filterHourlyTimes(timetable.stops);
1686
+ }
1687
+ for (const stop of timetable.stops) {
1688
+ stop.is_timepoint = stop.trips.some((stoptime) => isTimepoint(stoptime));
1689
+ }
1690
+ return timetable.stops;
1691
+ }
1692
+ function formatStopName(stop) {
1693
+ return `${stop.stop_name}${stop.type === "arrival" ? " (Arrival)" : stop.type === "departure" ? " (Departure)" : ""}`;
1694
+ }
1695
+ function formatTripContinuesFrom(trip) {
1696
+ return trip.continues_from_route ? trip.continues_from_route.route.route_short_name : "";
1697
+ }
1698
+ function formatTripContinuesAs(trip) {
1699
+ return trip.continues_as_route ? trip.continues_as_route.route.route_short_name : "";
1700
+ }
1701
+ function resetStoptimesToMidnight(trip) {
1702
+ const offsetSeconds = secondsAfterMidnight(
1703
+ first2(trip.stoptimes).departure_time
1704
+ );
1705
+ if (offsetSeconds > 0) {
1706
+ for (const stoptime of trip.stoptimes) {
1707
+ stoptime.departure_time = toGTFSTime(
1708
+ fromGTFSTime(stoptime.departure_time).subtract(
1709
+ offsetSeconds,
1710
+ "seconds"
1711
+ )
1712
+ );
1713
+ stoptime.arrival_time = toGTFSTime(
1714
+ fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, "seconds")
1715
+ );
1716
+ }
1717
+ }
1718
+ return trip;
1719
+ }
1720
+ function updateStoptimesByOffset(trip, offsetSeconds) {
1721
+ return trip.stoptimes.map((stoptime) => {
1722
+ delete stoptime._id;
1723
+ stoptime.departure_time = updateTimeByOffset(
1724
+ stoptime.departure_time,
1725
+ offsetSeconds
1726
+ );
1727
+ stoptime.arrival_time = updateTimeByOffset(
1728
+ stoptime.arrival_time,
1729
+ offsetSeconds
1730
+ );
1731
+ stoptime.trip_id = trip.trip_id;
1732
+ return stoptime;
1733
+ });
1734
+ }
1735
+ function formatRouteColor(route) {
1736
+ return route.route_color ? `#${route.route_color}` : "#000000";
1737
+ }
1738
+ function formatRouteTextColor(route) {
1739
+ return route.route_text_color ? `#${route.route_text_color}` : "#FFFFFF";
1740
+ }
1741
+ function formatTimetableLabel(timetable) {
1742
+ if (!isNullOrEmpty(timetable.timetable_label)) {
1743
+ return timetable.timetable_label;
1744
+ }
1745
+ let timetableLabel = "";
1746
+ if (timetable.routes && timetable.routes.length > 0) {
1747
+ timetableLabel += "Route ";
1748
+ if (!isNullOrEmpty(timetable.routes[0].route_short_name)) {
1749
+ timetableLabel += timetable.routes[0].route_short_name;
1750
+ } else if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
1751
+ timetableLabel += timetable.routes[0].route_long_name;
1752
+ }
1753
+ }
1754
+ if (timetable.stops && timetable.stops.length > 0) {
1755
+ const firstStop = timetable.stops[0].stop_name;
1756
+ const lastStop = timetable.stops[timetable.stops.length - 1].stop_name;
1757
+ if (firstStop === lastStop) {
1758
+ if (!isNullOrEmpty(timetable.routes[0].route_long_name)) {
1759
+ timetableLabel += ` - ${timetable.routes[0].route_long_name}`;
1760
+ }
1761
+ timetableLabel += " - Loop";
1762
+ } else {
1763
+ timetableLabel += ` - ${firstStop} to ${lastStop}`;
1764
+ }
1765
+ } else if (timetable.direction_name !== null) {
1766
+ timetableLabel += ` to ${timetable.direction_name}`;
1767
+ }
1768
+ return timetableLabel;
1769
+ }
1770
+ function mergeTimetablesWithSameId(timetables) {
1771
+ if (timetables.length === 0) {
1772
+ return [];
1773
+ }
1774
+ const mergedTimetables = groupBy2(timetables, "timetable_id");
1775
+ return Object.values(mergedTimetables).map((timetableGroup) => {
1776
+ const mergedTimetable = omit(timetableGroup[0], "route_id");
1777
+ mergedTimetable.route_ids = timetableGroup.map(
1778
+ (timetable) => timetable.route_id
1779
+ );
1780
+ return mergedTimetable;
1781
+ });
1782
+ }
1783
+
1784
+ // src/lib/file-utils.ts
1785
+ async function getConfig(argv2) {
1786
+ let data;
1787
+ let config;
1788
+ try {
1789
+ data = await readFile(path.resolve(untildify(argv2.configPath)), "utf8");
1790
+ } catch (error) {
1791
+ throw new Error(
1792
+ `Cannot find configuration file at \`${argv2.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`
1793
+ );
1794
+ }
1795
+ try {
1796
+ config = JSON.parse(data);
1797
+ } catch (error) {
1798
+ throw new Error(
1799
+ `Cannot parse configuration file at \`${argv2.configPath}\`. Check to ensure that it is valid JSON.`
1800
+ );
1801
+ }
1802
+ if (argv2.skipImport === true) {
1803
+ config.skipImport = argv2.skipImport;
1804
+ }
1805
+ if (argv2.showOnlyTimepoint === true) {
1806
+ config.showOnlyTimepoint = argv2.showOnlyTimepoint;
1807
+ }
1808
+ return config;
1809
+ }
1810
+ function getTemplatePath(templateFileName, config) {
1811
+ let fullTemplateFileName = templateFileName;
1812
+ if (config.noHead !== true) {
1813
+ fullTemplateFileName += "_full";
1814
+ }
1815
+ const templatePath = config.templatePath === void 0 ? path.join(fileURLToPath(import.meta.url), "../../../views/default") : untildify(config.templatePath);
1816
+ return path.join(templatePath, `${fullTemplateFileName}.pug`);
1817
+ }
1818
+ async function prepDirectory(exportPath) {
1819
+ await rm(exportPath, { recursive: true, force: true });
1820
+ try {
1821
+ await mkdir(exportPath, { recursive: true });
1822
+ } catch (error) {
1823
+ if (error.code === "ENOENT") {
1824
+ throw new Error(
1825
+ `Unable to write to ${exportPath}. Try running this command from a writable directory.`
1826
+ );
1827
+ }
1828
+ throw error;
1829
+ }
1830
+ }
1831
+ function copyStaticAssets(config, exportPath) {
1832
+ const staticAssetPath = config.templatePath === void 0 ? path.join(fileURLToPath(import.meta.url), "../../../views/default") : untildify(config.templatePath);
1833
+ copydir.sync(path.join(staticAssetPath, "css"), path.join(exportPath, "css"));
1834
+ copydir.sync(path.join(staticAssetPath, "js"), path.join(exportPath, "js"));
1835
+ }
1836
+ function zipFolder(exportPath) {
1837
+ const output = createWriteStream(path.join(exportPath, "timetables.zip"));
1838
+ const archive = archiver("zip");
1839
+ return new Promise((resolve, reject) => {
1840
+ output.on("close", resolve);
1841
+ archive.on("error", reject);
1842
+ archive.pipe(output);
1843
+ archive.glob("**/*.{txt,css,js,html}", {
1844
+ cwd: exportPath
1845
+ });
1846
+ archive.finalize();
1847
+ });
1848
+ }
1849
+ function generateFileName(timetable, config, extension = "html") {
1850
+ let filename = timetable.timetable_id;
1851
+ for (const route of timetable.routes) {
1852
+ filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, "-")}` : `_${route.route_short_name.replace(/\s/g, "-")}`;
1853
+ }
1854
+ if (!isNullOrEmpty(timetable.direction_id)) {
1855
+ filename += `_${timetable.direction_id}`;
1856
+ }
1857
+ filename += `_${formatDays(timetable, config).replace(/\s/g, "")}.${extension}`;
1858
+ return sanitize(filename).toLowerCase();
1859
+ }
1860
+ function generateFolderName(timetablePage) {
1861
+ const timetable = timetablePage.consolidatedTimetables[0];
1862
+ if (!timetable.start_date || !timetable.end_date) {
1863
+ return "timetables";
1864
+ }
1865
+ return sanitize(`${timetable.start_date}-${timetable.end_date}`);
1866
+ }
1867
+ async function renderTemplate(templateFileName, templateVars, config) {
1868
+ const templatePath = getTemplatePath(templateFileName, config);
1869
+ const html = await renderFile(templatePath, {
1870
+ _,
1871
+ md: (text) => insane(marked.parseInline(text)),
1872
+ ...template_functions_exports,
1873
+ formatRouteColor,
1874
+ formatRouteTextColor,
1875
+ ...templateVars
1876
+ });
1877
+ if (config.beautify === true) {
1878
+ return beautify.html_beautify(html, {
1879
+ indent_size: 2
1880
+ });
1881
+ }
1882
+ return html;
1883
+ }
1884
+ async function renderPdf(htmlPath) {
1885
+ const pdfPath = htmlPath.replace(/html$/, "pdf");
1886
+ const browser = await puppeteer.launch();
1887
+ const page = await browser.newPage();
1888
+ await page.emulateMediaType("screen");
1889
+ await page.goto(`file://${htmlPath}`, {
1890
+ waitUntil: "networkidle0"
1891
+ });
1892
+ await page.pdf({
1893
+ path: pdfPath
1894
+ });
1895
+ await browser.close();
1896
+ }
1897
+
1898
+ // src/lib/log-utils.ts
1899
+ import { clearLine, cursorTo } from "node:readline";
1900
+ import { noop } from "lodash-es";
1901
+ import * as colors from "yoctocolors";
1902
+ import { getFeedInfo } from "gtfs";
1903
+ import Table from "cli-table";
1904
+ function generateLogText(outputStats, config) {
1905
+ const feedInfo = getFeedInfo();
1906
+ const feedVersion = feedInfo.length > 0 && feedInfo[0].feed_version ? feedInfo[0].feed_version : "Unknown";
1907
+ const logText = [
1908
+ `Feed Version: ${feedVersion}`,
1909
+ `GTFS-to-HTML Version: ${config.gtfsToHtmlVersion}`,
1910
+ `Date Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
1911
+ `Timetable Page Count: ${outputStats.timetablePages}`,
1912
+ `Timetable Count: ${outputStats.timetables}`,
1913
+ `Calendar Service ID Count: ${outputStats.calendars}`,
1914
+ `Route Count: ${outputStats.routes}`,
1915
+ `Trip Count: ${outputStats.trips}`,
1916
+ `Stop Count: ${outputStats.stops}`
1917
+ ];
1918
+ for (const agency of config.agencies) {
1919
+ if (agency.url) {
1920
+ logText.push(`Source: ${agency.url}`);
1921
+ } else if (agency.path) {
1922
+ logText.push(`Source: ${agency.path}`);
1923
+ }
1924
+ }
1925
+ if (outputStats.warnings.length > 0) {
1926
+ logText.push("", "Warnings:", ...outputStats.warnings);
1927
+ }
1928
+ return logText.join("\n");
1929
+ }
1930
+ function log(config) {
1931
+ if (config.verbose === false) {
1932
+ return noop;
1933
+ }
1934
+ if (config.logFunction) {
1935
+ return config.logFunction;
1936
+ }
1937
+ return (text, overwrite) => {
1938
+ if (overwrite === true && process.stdout.isTTY) {
1939
+ clearLine(process.stdout, 0);
1940
+ cursorTo(process.stdout, 0);
1941
+ } else {
1942
+ process.stdout.write("\n");
1943
+ }
1944
+ process.stdout.write(text);
1945
+ };
1946
+ }
1947
+ function logWarning(config) {
1948
+ if (config.logFunction) {
1949
+ return config.logFunction;
1950
+ }
1951
+ return (text) => {
1952
+ process.stdout.write(`
1953
+ ${formatWarning(text)}
1954
+ `);
1955
+ };
1956
+ }
1957
+ function logError(config) {
1958
+ if (config.logFunction) {
1959
+ return config.logFunction;
1960
+ }
1961
+ return (text) => {
1962
+ process.stdout.write(`
1963
+ ${formatError(text)}
1964
+ `);
1965
+ };
1966
+ }
1967
+ function formatWarning(text) {
1968
+ const warningMessage = `${colors.underline("Warning")}: ${text}`;
1969
+ return colors.yellow(warningMessage);
1970
+ }
1971
+ function formatError(error) {
1972
+ const messageText = error instanceof Error ? error.message : error;
1973
+ const errorMessage = `${colors.underline("Error")}: ${messageText.replace(
1974
+ "Error: ",
1975
+ ""
1976
+ )}`;
1977
+ return colors.red(errorMessage);
1978
+ }
1979
+ function logStats(stats, config) {
1980
+ if (config.logFunction) {
1981
+ return;
1982
+ }
1983
+ const table = new Table({
1984
+ colWidths: [40, 20],
1985
+ head: ["Item", "Count"]
1986
+ });
1987
+ table.push(
1988
+ ["\u{1F4C4} Timetable Pages", stats.timetablePages],
1989
+ ["\u{1F551} Timetables", stats.timetables],
1990
+ ["\u{1F4C5} Calendar Service IDs", stats.calendars],
1991
+ ["\u{1F504} Routes", stats.routes],
1992
+ ["\u{1F68D} Trips", stats.trips],
1993
+ ["\u{1F6D1} Stops", stats.stops],
1994
+ ["\u26D4\uFE0F Warnings", stats.warnings.length]
1995
+ );
1996
+ config.log(table.toString());
1997
+ }
1998
+ var generateProgressBarString = (barTotal, barProgress, size2 = 40) => {
1999
+ const line = "-";
2000
+ const slider = "=";
2001
+ if (!barTotal) {
2002
+ throw new Error("Total value is either not provided or invalid");
2003
+ }
2004
+ if (!barProgress && barProgress !== 0) {
2005
+ throw new Error("Current value is either not provided or invalid");
2006
+ }
2007
+ if (isNaN(barTotal)) {
2008
+ throw new Error("Total value is not an integer");
2009
+ }
2010
+ if (isNaN(barProgress)) {
2011
+ throw new Error("Current value is not an integer");
2012
+ }
2013
+ if (isNaN(size2)) {
2014
+ throw new Error("Size is not an integer");
2015
+ }
2016
+ if (barProgress > barTotal) {
2017
+ return slider.repeat(size2 + 2);
2018
+ }
2019
+ const percentage = barProgress / barTotal;
2020
+ const progress = Math.round(size2 * percentage);
2021
+ const emptyProgress = size2 - progress;
2022
+ const progressText = slider.repeat(progress);
2023
+ const emptyProgressText = line.repeat(emptyProgress);
2024
+ return progressText + emptyProgressText;
2025
+ };
2026
+ function progressBar(formatString, barTotal, config) {
2027
+ let barProgress = 0;
2028
+ if (config.verbose === false) {
2029
+ return {
2030
+ increment: noop,
2031
+ interrupt: noop
2032
+ };
2033
+ }
2034
+ if (barTotal === 0) {
2035
+ return null;
2036
+ }
2037
+ const renderProgressString = () => formatString.replace("{value}", barProgress).replace("{total}", barTotal).replace("{bar}", generateProgressBarString(barTotal, barProgress));
2038
+ config.log(renderProgressString(), true);
2039
+ return {
2040
+ interrupt(text) {
2041
+ config.logWarning(text);
2042
+ config.log("");
2043
+ },
2044
+ increment() {
2045
+ barProgress += 1;
2046
+ config.log(renderProgressString(), true);
2047
+ }
2048
+ };
2049
+ }
2050
+
2051
+ // src/lib/gtfs-to-html.ts
2052
+ import path2 from "node:path";
2053
+ import { mkdir as mkdir2, writeFile } from "node:fs/promises";
2054
+ import { map } from "lodash-es";
2055
+ import { openDb as openDb2, importGtfs } from "gtfs";
2056
+ import sanitize2 from "sanitize-filename";
2057
+ import Timer from "timer-machine";
2058
+ var gtfsToHtml = async (initialConfig) => {
2059
+ const config = setDefaultConfig(initialConfig);
2060
+ const timer = new Timer();
2061
+ config.log = log(config);
2062
+ config.logWarning = logWarning(config);
2063
+ config.logError = logError(config);
2064
+ timer.start();
2065
+ try {
2066
+ openDb2(config);
2067
+ } catch (error) {
2068
+ if (error?.code === "SQLITE_CANTOPEN") {
2069
+ config.logError(
2070
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
2071
+ );
2072
+ }
2073
+ throw error;
2074
+ }
2075
+ if (!config.agencies || config.agencies.length === 0) {
2076
+ throw new Error("No agencies defined in `config.json`");
2077
+ }
2078
+ if (!config.skipImport) {
2079
+ await importGtfs(config);
2080
+ }
2081
+ const agencyKey = config.agencies.map((agency) => agency.agency_key ?? "unknown").join("-");
2082
+ const exportPath = path2.join(process.cwd(), "html", sanitize2(agencyKey));
2083
+ const outputStats = {
2084
+ timetables: 0,
2085
+ timetablePages: 0,
2086
+ calendars: 0,
2087
+ routes: 0,
2088
+ trips: 0,
2089
+ stops: 0,
2090
+ warnings: []
2091
+ };
2092
+ const timetablePages = [];
2093
+ const timetablePageIds = map(
2094
+ getTimetablePagesForAgency(config),
2095
+ "timetable_page_id"
2096
+ );
2097
+ await prepDirectory(exportPath);
2098
+ if (config.noHead !== true && ["html", "pdf"].includes(config.outputFormat)) {
2099
+ copyStaticAssets(config, exportPath);
2100
+ }
2101
+ const bar = progressBar(
2102
+ `${agencyKey}: Generating ${config.outputFormat.toUpperCase()} timetables {bar} {value}/{total}`,
2103
+ timetablePageIds.length,
2104
+ config
2105
+ );
2106
+ for (const timetablePageId of timetablePageIds) {
2107
+ try {
2108
+ const timetablePage = await getFormattedTimetablePage(
2109
+ timetablePageId,
2110
+ config
2111
+ );
2112
+ for (const timetable of timetablePage.timetables) {
2113
+ for (const warning of timetable.warnings) {
2114
+ outputStats.warnings.push(warning);
2115
+ bar?.interrupt(warning);
2116
+ }
2117
+ }
2118
+ if (timetablePage.consolidatedTimetables.length === 0) {
2119
+ throw new Error(
2120
+ `No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
2121
+ );
2122
+ }
2123
+ outputStats.timetables += timetablePage.consolidatedTimetables.length;
2124
+ outputStats.timetablePages += 1;
2125
+ const datePath = generateFolderName(timetablePage);
2126
+ await mkdir2(path2.join(exportPath, datePath), { recursive: true });
2127
+ config.assetPath = "../";
2128
+ timetablePage.relativePath = path2.join(
2129
+ datePath,
2130
+ sanitize2(timetablePage.filename)
2131
+ );
2132
+ if (config.outputFormat === "csv") {
2133
+ for (const timetable of timetablePage.consolidatedTimetables) {
2134
+ const csv = await generateTimetableCSV(timetable);
2135
+ const csvPath = path2.join(
2136
+ exportPath,
2137
+ datePath,
2138
+ generateFileName(timetable, config, "csv")
2139
+ );
2140
+ await writeFile(csvPath, csv);
2141
+ }
2142
+ } else {
2143
+ const html = await generateTimetableHTML(timetablePage, config);
2144
+ const htmlPath = path2.join(
2145
+ exportPath,
2146
+ datePath,
2147
+ sanitize2(timetablePage.filename)
2148
+ );
2149
+ await writeFile(htmlPath, html);
2150
+ if (config.outputFormat === "pdf") {
2151
+ await renderPdf(htmlPath);
2152
+ }
2153
+ }
2154
+ timetablePages.push(timetablePage);
2155
+ const timetableStats = generateStats(timetablePage);
2156
+ outputStats.stops += timetableStats.stops;
2157
+ outputStats.routes += timetableStats.routes;
2158
+ outputStats.trips += timetableStats.trips;
2159
+ outputStats.calendars += timetableStats.calendars;
2160
+ } catch (error) {
2161
+ outputStats.warnings.push(error?.message);
2162
+ bar?.interrupt(error.message);
2163
+ }
2164
+ bar?.increment();
2165
+ }
2166
+ if (config.outputFormat === "html") {
2167
+ config.assetPath = "";
2168
+ const html = await generateOverviewHTML(timetablePages, config);
2169
+ await writeFile(path2.join(exportPath, "index.html"), html);
2170
+ }
2171
+ const logText = generateLogText(outputStats, config);
2172
+ await writeFile(path2.join(exportPath, "log.txt"), logText);
2173
+ if (config.zipOutput) {
2174
+ await zipFolder(exportPath);
2175
+ }
2176
+ const fullExportPath = path2.join(
2177
+ exportPath,
2178
+ config.zipOutput ? "/timetables.zip" : ""
2179
+ );
2180
+ config.log(
2181
+ `${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullExportPath}`
2182
+ );
2183
+ logStats(outputStats, config);
2184
+ const seconds = Math.round(timer.time() / 1e3);
2185
+ config.log(
2186
+ `${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`
2187
+ );
2188
+ timer.stop();
2189
+ };
2190
+ var gtfs_to_html_default = gtfsToHtml;
2191
+
2192
+ // src/bin/gtfs-to-html.ts
2193
+ var pe = new PrettyError();
2194
+ var { argv } = yargs(hideBin(process.argv)).usage("Usage: $0 --configPath ./config.json").help().option("c", {
2195
+ alias: "configPath",
2196
+ describe: "Path to config file",
2197
+ default: "./config.json",
2198
+ type: "string"
2199
+ }).option("s", {
2200
+ alias: "skipImport",
2201
+ describe: "Don\u2019t import GTFS file.",
2202
+ type: "boolean"
2203
+ }).default("skipImport", void 0).option("t", {
2204
+ alias: "showOnlyTimepoint",
2205
+ describe: "Show only stops with a `timepoint` value in `stops.txt`",
2206
+ type: "boolean"
2207
+ }).default("showOnlyTimepoint", void 0);
2208
+ var handleError = (error) => {
2209
+ const text = error || "Unknown Error";
2210
+ process.stdout.write(`
2211
+ ${formatError(text)}
2212
+ `);
2213
+ console.error(pe.render(error));
2214
+ process.exit(1);
2215
+ };
2216
+ var setupImport = async () => {
2217
+ const config = await getConfig(argv);
2218
+ await gtfs_to_html_default(config);
2219
+ process.exit();
2220
+ };
2221
+ setupImport().catch(handleError);
2222
+ //# sourceMappingURL=gtfs-to-html.js.map