gtfs-to-html 2.5.6 → 2.5.8
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/CHANGELOG.md +25 -0
- package/lib/formatters.js +28 -15
- package/lib/gtfs-to-html.js +16 -16
- package/lib/log-utils.js +3 -3
- package/lib/utils.js +36 -26
- package/package.json +9 -9
- package/views/default/timetable_horizontal.pug +1 -1
- package/views/default/timetable_vertical.pug +1 -1
- package/www/blog/2021-11-06-CSV-Export.md +1 -1
- package/www/docs/configuration.md +136 -136
- package/www/docs/logging-sql-queries.md +2 -2
- package/www/docs/quick-start.md +41 -38
- package/www/package.json +5 -14
- package/www/yarn.lock +3184 -2497
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [2.5.8] - 2024-01-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Improved warning logging
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Handle case where a calendar_date is both included and excluded
|
|
17
|
+
|
|
18
|
+
### Updated
|
|
19
|
+
|
|
20
|
+
- Dependency updates
|
|
21
|
+
|
|
22
|
+
## [2.5.7] - 2023-11-07
|
|
23
|
+
|
|
24
|
+
### Updated
|
|
25
|
+
|
|
26
|
+
- Dependency updates
|
|
27
|
+
- Docusaurus 3.0 for documentation site
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- Add is_timepoint value to each stop
|
|
32
|
+
|
|
8
33
|
## [2.5.6] - 2023-08-23
|
|
9
34
|
|
|
10
35
|
### Updated
|
package/lib/formatters.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
toGTFSTime,
|
|
21
21
|
updateTimeByOffset,
|
|
22
22
|
} from './time-utils.js';
|
|
23
|
+
import { isTimepoint } from './utils.js';
|
|
23
24
|
|
|
24
25
|
/*
|
|
25
26
|
* Replace all instances in a string with items from an object.
|
|
@@ -141,13 +142,13 @@ function filterHourlyTimes(stops) {
|
|
|
141
142
|
time,
|
|
142
143
|
}));
|
|
143
144
|
const sortedFirstStopTimesAndIndex = sortBy(firstStopTimesAndIndex, (item) =>
|
|
144
|
-
Number.parseInt(item.time.format('m'), 10)
|
|
145
|
+
Number.parseInt(item.time.format('m'), 10),
|
|
145
146
|
);
|
|
146
147
|
|
|
147
148
|
// Filter and arrange stoptimes for all stops based on sort.
|
|
148
149
|
return stops.map((stop) => {
|
|
149
150
|
stop.hourlyTimes = sortedFirstStopTimesAndIndex.map((item) =>
|
|
150
|
-
fromGTFSTime(stop.trips[item.idx].arrival_time).format(':mm')
|
|
151
|
+
fromGTFSTime(stop.trips[item.idx].arrival_time).format(':mm'),
|
|
151
152
|
);
|
|
152
153
|
|
|
153
154
|
return stop;
|
|
@@ -234,7 +235,7 @@ export function formatTrip(trip, timetable, calendars, config) {
|
|
|
234
235
|
trip.route_short_name = timetable.routes[0].route_short_name;
|
|
235
236
|
} else {
|
|
236
237
|
const route = timetable.routes.find(
|
|
237
|
-
(route) => route.route_id === trip.route_id
|
|
238
|
+
(route) => route.route_id === trip.route_id,
|
|
238
239
|
);
|
|
239
240
|
trip.route_short_name = route.route_short_name;
|
|
240
241
|
}
|
|
@@ -274,7 +275,7 @@ export function formatFrequency(frequency, config) {
|
|
|
274
275
|
*/
|
|
275
276
|
export function formatTimetableId(timetable) {
|
|
276
277
|
let timetableId = `${timetable.route_ids.join('_')}|${calendarToCalendarCode(
|
|
277
|
-
timetable
|
|
278
|
+
timetable,
|
|
278
279
|
)}`;
|
|
279
280
|
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
280
281
|
timetableId += `|${timetable.direction_id}`;
|
|
@@ -328,7 +329,7 @@ export function formatStops(timetable, config) {
|
|
|
328
329
|
const departureStoptime = clone(stoptime);
|
|
329
330
|
departureStoptime.type = 'departure';
|
|
330
331
|
timetable.stops[stopIndex + 1].trips.push(
|
|
331
|
-
formatStopTime(departureStoptime, timetable, config)
|
|
332
|
+
formatStopTime(departureStoptime, timetable, config),
|
|
332
333
|
);
|
|
333
334
|
}
|
|
334
335
|
|
|
@@ -343,8 +344,13 @@ export function formatStops(timetable, config) {
|
|
|
343
344
|
for (const stop of timetable.stops) {
|
|
344
345
|
const lastStopTime = last(stop.trips);
|
|
345
346
|
if (!lastStopTime || lastStopTime.trip_id !== trip.trip_id) {
|
|
346
|
-
|
|
347
|
-
|
|
347
|
+
stop.trips.push(
|
|
348
|
+
formatStopTime(
|
|
349
|
+
createEmptyStoptime(stop.stop_id, trip.trip_id),
|
|
350
|
+
timetable,
|
|
351
|
+
config,
|
|
352
|
+
),
|
|
353
|
+
);
|
|
348
354
|
}
|
|
349
355
|
}
|
|
350
356
|
}
|
|
@@ -353,6 +359,10 @@ export function formatStops(timetable, config) {
|
|
|
353
359
|
timetable.stops = filterHourlyTimes(timetable.stops);
|
|
354
360
|
}
|
|
355
361
|
|
|
362
|
+
for (const stop of timetable.stops) {
|
|
363
|
+
stop.is_timepoint = stop.trips.some((stoptime) => isTimepoint(stoptime));
|
|
364
|
+
}
|
|
365
|
+
|
|
356
366
|
return timetable.stops;
|
|
357
367
|
}
|
|
358
368
|
|
|
@@ -393,15 +403,18 @@ export function formatTripContinuesAs(trip) {
|
|
|
393
403
|
*/
|
|
394
404
|
export function resetStoptimesToMidnight(trip) {
|
|
395
405
|
const offsetSeconds = secondsAfterMidnight(
|
|
396
|
-
first(trip.stoptimes).departure_time
|
|
406
|
+
first(trip.stoptimes).departure_time,
|
|
397
407
|
);
|
|
398
408
|
if (offsetSeconds > 0) {
|
|
399
409
|
for (const stoptime of trip.stoptimes) {
|
|
400
410
|
stoptime.departure_time = toGTFSTime(
|
|
401
|
-
fromGTFSTime(stoptime.departure_time).subtract(
|
|
411
|
+
fromGTFSTime(stoptime.departure_time).subtract(
|
|
412
|
+
offsetSeconds,
|
|
413
|
+
'seconds',
|
|
414
|
+
),
|
|
402
415
|
);
|
|
403
416
|
stoptime.arrival_time = toGTFSTime(
|
|
404
|
-
fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, 'seconds')
|
|
417
|
+
fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, 'seconds'),
|
|
405
418
|
);
|
|
406
419
|
}
|
|
407
420
|
}
|
|
@@ -418,11 +431,11 @@ export function updateStoptimesByOffset(trip, offsetSeconds) {
|
|
|
418
431
|
delete stoptime._id;
|
|
419
432
|
stoptime.departure_time = updateTimeByOffset(
|
|
420
433
|
stoptime.departure_time,
|
|
421
|
-
offsetSeconds
|
|
434
|
+
offsetSeconds,
|
|
422
435
|
);
|
|
423
436
|
stoptime.arrival_time = updateTimeByOffset(
|
|
424
437
|
stoptime.arrival_time,
|
|
425
|
-
offsetSeconds
|
|
438
|
+
offsetSeconds,
|
|
426
439
|
);
|
|
427
440
|
stoptime.trip_id = trip.trip_id;
|
|
428
441
|
return stoptime;
|
|
@@ -483,9 +496,9 @@ export function formatTimetablePageLabel(timetablePage) {
|
|
|
483
496
|
const routes = uniqBy(
|
|
484
497
|
flatMap(
|
|
485
498
|
timetablePage.consolidatedTimetables,
|
|
486
|
-
(timetable) => timetable.routes
|
|
499
|
+
(timetable) => timetable.routes,
|
|
487
500
|
),
|
|
488
|
-
'route_id'
|
|
501
|
+
'route_id',
|
|
489
502
|
);
|
|
490
503
|
const timetablePageLabel = routes.map((route) => formatRouteName(route));
|
|
491
504
|
|
|
@@ -509,7 +522,7 @@ export function mergeTimetablesWithSameId(timetables) {
|
|
|
509
522
|
const mergedTimetable = omit(timetableGroup[0], 'route_id');
|
|
510
523
|
|
|
511
524
|
mergedTimetable.route_ids = timetableGroup.map(
|
|
512
|
-
(timetable) => timetable.route_id
|
|
525
|
+
(timetable) => timetable.route_id,
|
|
513
526
|
);
|
|
514
527
|
|
|
515
528
|
return mergedTimetable;
|
package/lib/gtfs-to-html.js
CHANGED
|
@@ -51,7 +51,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
51
51
|
} catch (error) {
|
|
52
52
|
if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
|
|
53
53
|
config.logError(
|
|
54
|
-
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json
|
|
54
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -84,7 +84,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
84
84
|
const timetablePages = [];
|
|
85
85
|
const timetablePageIds = map(
|
|
86
86
|
getTimetablePagesForAgency(config),
|
|
87
|
-
'timetable_page_id'
|
|
87
|
+
'timetable_page_id',
|
|
88
88
|
);
|
|
89
89
|
await prepDirectory(exportPath);
|
|
90
90
|
|
|
@@ -95,7 +95,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
95
95
|
const bar = progressBar(
|
|
96
96
|
`${agencyKey}: Generating ${config.outputFormat.toUpperCase()} timetables {bar} {value}/{total}`,
|
|
97
97
|
timetablePageIds.length,
|
|
98
|
-
config
|
|
98
|
+
config,
|
|
99
99
|
);
|
|
100
100
|
|
|
101
101
|
/* eslint-disable no-await-in-loop */
|
|
@@ -103,15 +103,9 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
103
103
|
try {
|
|
104
104
|
const timetablePage = await getFormattedTimetablePage(
|
|
105
105
|
timetablePageId,
|
|
106
|
-
config
|
|
106
|
+
config,
|
|
107
107
|
);
|
|
108
108
|
|
|
109
|
-
if (timetablePage.consolidatedTimetables.length === 0) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
109
|
for (const timetable of timetablePage.timetables) {
|
|
116
110
|
for (const warning of timetable.warnings) {
|
|
117
111
|
outputStats.warnings.push(warning);
|
|
@@ -119,6 +113,12 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
|
|
116
|
+
if (timetablePage.consolidatedTimetables.length === 0) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
122
|
outputStats.timetables += timetablePage.consolidatedTimetables.length;
|
|
123
123
|
outputStats.timetablePages += 1;
|
|
124
124
|
|
|
@@ -130,7 +130,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
130
130
|
|
|
131
131
|
timetablePage.relativePath = path.join(
|
|
132
132
|
datePath,
|
|
133
|
-
sanitize(timetablePage.filename)
|
|
133
|
+
sanitize(timetablePage.filename),
|
|
134
134
|
);
|
|
135
135
|
|
|
136
136
|
if (config.outputFormat === 'csv') {
|
|
@@ -139,7 +139,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
139
139
|
const csvPath = path.join(
|
|
140
140
|
exportPath,
|
|
141
141
|
datePath,
|
|
142
|
-
generateCSVFileName(timetable, timetablePage)
|
|
142
|
+
generateCSVFileName(timetable, timetablePage),
|
|
143
143
|
);
|
|
144
144
|
await writeFile(csvPath, csv);
|
|
145
145
|
}
|
|
@@ -148,7 +148,7 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
148
148
|
const htmlPath = path.join(
|
|
149
149
|
exportPath,
|
|
150
150
|
datePath,
|
|
151
|
-
sanitize(timetablePage.filename)
|
|
151
|
+
sanitize(timetablePage.filename),
|
|
152
152
|
);
|
|
153
153
|
await writeFile(htmlPath, html);
|
|
154
154
|
|
|
@@ -192,19 +192,19 @@ const gtfsToHtml = async (initialConfig) => {
|
|
|
192
192
|
|
|
193
193
|
const fullExportPath = path.join(
|
|
194
194
|
exportPath,
|
|
195
|
-
config.zipOutput ? '/timetables.zip' : ''
|
|
195
|
+
config.zipOutput ? '/timetables.zip' : '',
|
|
196
196
|
);
|
|
197
197
|
|
|
198
198
|
// Print stats
|
|
199
199
|
config.log(
|
|
200
|
-
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullExportPath}
|
|
200
|
+
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullExportPath}`,
|
|
201
201
|
);
|
|
202
202
|
|
|
203
203
|
logStats(outputStats, config);
|
|
204
204
|
|
|
205
205
|
const seconds = Math.round(timer.time() / 1000);
|
|
206
206
|
config.log(
|
|
207
|
-
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds
|
|
207
|
+
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`,
|
|
208
208
|
);
|
|
209
209
|
|
|
210
210
|
timer.stop();
|
package/lib/log-utils.js
CHANGED
|
@@ -110,7 +110,7 @@ export function formatError(error) {
|
|
|
110
110
|
const messageText = error instanceof Error ? error.message : error;
|
|
111
111
|
const errorMessage = `${colors.underline('Error')}: ${messageText.replace(
|
|
112
112
|
'Error: ',
|
|
113
|
-
''
|
|
113
|
+
'',
|
|
114
114
|
)}`;
|
|
115
115
|
return colors.red(errorMessage);
|
|
116
116
|
}
|
|
@@ -136,7 +136,7 @@ export function logStats(stats, config) {
|
|
|
136
136
|
['🔄 Routes', stats.routes],
|
|
137
137
|
['🚍 Trips', stats.trips],
|
|
138
138
|
['🛑 Stops', stats.stops],
|
|
139
|
-
['⛔️ Warnings', stats.warnings.length]
|
|
139
|
+
['⛔️ Warnings', stats.warnings.length],
|
|
140
140
|
);
|
|
141
141
|
|
|
142
142
|
config.log(table.toString());
|
|
@@ -209,7 +209,7 @@ export function progressBar(formatString, barTotal, config) {
|
|
|
209
209
|
interrupt(text) {
|
|
210
210
|
// Log two lines to avoid overwrite by progress bar
|
|
211
211
|
config.logWarning(text);
|
|
212
|
-
config.
|
|
212
|
+
config.log('');
|
|
213
213
|
},
|
|
214
214
|
increment() {
|
|
215
215
|
barProgress += 1;
|
package/lib/utils.js
CHANGED
|
@@ -86,7 +86,7 @@ const { version } = JSON.parse(
|
|
|
86
86
|
/*
|
|
87
87
|
* Determine if a stoptime is a timepoint.
|
|
88
88
|
*/
|
|
89
|
-
const isTimepoint = (stoptime) => {
|
|
89
|
+
export const isTimepoint = (stoptime) => {
|
|
90
90
|
if (isNullOrEmpty(stoptime.timepoint)) {
|
|
91
91
|
return (
|
|
92
92
|
!isNullOrEmpty(stoptime.arrival_time) &&
|
|
@@ -101,13 +101,15 @@ const isTimepoint = (stoptime) => {
|
|
|
101
101
|
* Find the longest trip (most stops) in a group of trips and return stoptimes.
|
|
102
102
|
*/
|
|
103
103
|
const getLongestTripStoptimes = (trips, config) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
const filteredTripStoptimes = trips.map((trip) =>
|
|
105
|
+
trip.stoptimes.filter((stoptime) => {
|
|
106
|
+
// If `showOnlyTimepoint` is true, then filter out all non-timepoints.
|
|
107
|
+
if (config.showOnlyTimepoint === true) {
|
|
108
|
+
return isTimepoint(stoptime);
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
111
113
|
|
|
112
114
|
return maxBy(filteredTripStoptimes, (stoptimes) => size(stoptimes));
|
|
113
115
|
};
|
|
@@ -290,26 +292,32 @@ const getCalendarDatesForTimetable = (timetable, config) => {
|
|
|
290
292
|
);
|
|
291
293
|
const start = fromGTFSDate(timetable.start_date);
|
|
292
294
|
const end = fromGTFSDate(timetable.end_date);
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
includedDates: [],
|
|
296
|
-
};
|
|
295
|
+
const excludedDates = [];
|
|
296
|
+
const includedDates = [];
|
|
297
297
|
|
|
298
298
|
for (const calendarDate of calendarDates) {
|
|
299
299
|
if (moment(calendarDate.date, 'YYYYMMDD').isBetween(start, end)) {
|
|
300
300
|
if (calendarDate.exception_type === 1) {
|
|
301
|
-
|
|
302
|
-
formatDate(calendarDate, config.dateFormat),
|
|
303
|
-
);
|
|
301
|
+
includedDates.push(formatDate(calendarDate, config.dateFormat));
|
|
304
302
|
} else if (calendarDate.exception_type === 2) {
|
|
305
|
-
|
|
306
|
-
formatDate(calendarDate, config.dateFormat),
|
|
307
|
-
);
|
|
303
|
+
excludedDates.push(formatDate(calendarDate, config.dateFormat));
|
|
308
304
|
}
|
|
309
305
|
}
|
|
310
306
|
}
|
|
311
307
|
|
|
312
|
-
|
|
308
|
+
// Remove dates that are both included and excluded from both lists
|
|
309
|
+
const includedAndExcludedDates = excludedDates.filter((date) =>
|
|
310
|
+
includedDates.includes(date),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
return {
|
|
314
|
+
excludedDates: excludedDates.filter(
|
|
315
|
+
(date) => !includedAndExcludedDates.includes(date),
|
|
316
|
+
),
|
|
317
|
+
includedDates: includedDates.filter(
|
|
318
|
+
(date) => !includedAndExcludedDates.includes(date),
|
|
319
|
+
),
|
|
320
|
+
};
|
|
313
321
|
};
|
|
314
322
|
|
|
315
323
|
/*
|
|
@@ -674,13 +682,15 @@ const getStopOrder = (timetable, config) => {
|
|
|
674
682
|
const stopGraph = [];
|
|
675
683
|
|
|
676
684
|
for (const trip of timetable.orderedTrips) {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
685
|
+
const sortedStopIds = trip.stoptimes
|
|
686
|
+
.filter((stoptime) => {
|
|
687
|
+
// If `showOnlyTimepoint` is true, then filter out all non-timepoints.
|
|
688
|
+
if (config.showOnlyTimepoint === true) {
|
|
689
|
+
return isTimepoint(stoptime);
|
|
690
|
+
}
|
|
691
|
+
return true;
|
|
692
|
+
})
|
|
693
|
+
.map((stoptime) => stoptime.stop_id);
|
|
684
694
|
|
|
685
695
|
for (const [index, stopId] of sortedStopIds.entries()) {
|
|
686
696
|
if (index === sortedStopIds.length - 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtfs-to-html",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
|
|
6
6
|
"keywords": [
|
|
@@ -37,19 +37,19 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@turf/helpers": "^6.5.0",
|
|
39
39
|
"@turf/simplify": "^6.5.0",
|
|
40
|
-
"archiver": "^6.0.
|
|
40
|
+
"archiver": "^6.0.1",
|
|
41
41
|
"cli-table": "^0.3.11",
|
|
42
42
|
"copy-dir": "^1.3.0",
|
|
43
|
-
"csv-stringify": "^6.4.
|
|
43
|
+
"csv-stringify": "^6.4.5",
|
|
44
44
|
"express": "^4.18.2",
|
|
45
|
-
"gtfs": "^4.5.
|
|
46
|
-
"js-beautify": "^1.14.
|
|
45
|
+
"gtfs": "^4.5.1",
|
|
46
|
+
"js-beautify": "^1.14.11",
|
|
47
47
|
"lodash-es": "^4.17.21",
|
|
48
|
-
"moment": "^2.
|
|
48
|
+
"moment": "^2.30.1",
|
|
49
49
|
"morgan": "^1.10.0",
|
|
50
50
|
"pretty-error": "^4.0.0",
|
|
51
51
|
"pug": "^3.0.2",
|
|
52
|
-
"puppeteer": "^21.1
|
|
52
|
+
"puppeteer": "^21.6.1",
|
|
53
53
|
"sanitize-filename": "^1.6.3",
|
|
54
54
|
"sqlstring": "^2.3.3",
|
|
55
55
|
"timer-machine": "^1.1.0",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"husky": "^8.0.3",
|
|
63
|
-
"lint-staged": "^
|
|
64
|
-
"prettier": "^3.
|
|
63
|
+
"lint-staged": "^15.2.0",
|
|
64
|
+
"prettier": "^3.1.1"
|
|
65
65
|
},
|
|
66
66
|
"engines": {
|
|
67
67
|
"node": ">= 18.0.0"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
th(scope="row" colspan=`${stop.trips.length + 1}`)= stop.stop_city
|
|
29
29
|
- previousCity = stop.stop_city
|
|
30
30
|
|
|
31
|
-
tr.stop-row(id=`stop_id_${formatHtmlId(stop.stop_id)}` data-stop-id=`${stop.stop_id}`)
|
|
31
|
+
tr.stop-row(id=`stop_id_${formatHtmlId(stop.stop_id)}` data-stop-id=`${stop.stop_id}` data-is-timepoint=`${stop.is_timepoint}`)
|
|
32
32
|
th.stop-name-container(scope="row")
|
|
33
33
|
include timetable_stop_name.pug
|
|
34
34
|
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
caption.sr-only= `${timetable.timetable_label} | ${timetable.dayList}`
|
|
13
13
|
colgroup
|
|
14
14
|
each stop, idx in timetable.stops
|
|
15
|
-
col(id=`stop_id_${formatHtmlId(stop.stop_id)}` class=`stop-${idx}` data-stop-id=`${stop.stop_id}`)
|
|
15
|
+
col(id=`stop_id_${formatHtmlId(stop.stop_id)}` class=`stop-${idx}` data-stop-id=`${stop.stop_id}` data-is-timepoint=`${stop.is_timepoint}`)
|
|
16
16
|
thead
|
|
17
17
|
tr
|
|
18
18
|
if timetable.has_continues_from_route
|
|
@@ -11,7 +11,7 @@ GTFS-to-HTML Version 2.3.0 adds support for exporting timetables as CSV. Setting
|
|
|
11
11
|
|
|
12
12
|
An example of a CSV timetable:
|
|
13
13
|
|
|
14
|
-
```
|
|
14
|
+
```csv
|
|
15
15
|
,San Francisco Ferry Building,Vallejo Ferry Terminal,Mare Island Ferry Terminal
|
|
16
16
|
Run #1,10:30am,11:30am,
|
|
17
17
|
Run #2,11:30am,12:30pm,
|