gtfs-to-html 2.2.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +15 -20
- package/.husky/pre-commit +4 -0
- package/CHANGELOG.md +275 -2
- package/README.md +59 -41
- package/app/index.js +46 -24
- package/bin/gtfs-to-html.js +5 -7
- package/lib/file-utils.js +52 -15
- package/lib/formatters.js +123 -28
- package/lib/geojson-utils.js +32 -17
- package/lib/gtfs-to-html.js +96 -34
- package/lib/log-utils.js +23 -15
- package/lib/template-functions.js +80 -17
- package/lib/time-utils.js +10 -2
- package/lib/utils.js +762 -371
- package/package.json +29 -11
- package/public/css/timetable_styles.css +55 -49
- package/public/js/system-map.js +73 -60
- package/public/js/timetable-map.js +103 -96
- package/public/js/timetable-menu.js +32 -8
- package/views/default/formatting_functions.pug +0 -17
- package/views/default/overview_full.pug +2 -2
- package/views/default/timetablepage_full.pug +2 -2
- package/www/blog/2021-11-06-CSV-Export.md +26 -0
- package/www/docs/configuration.md +87 -85
- package/www/docs/current-usage.md +31 -30
- package/www/docs/introduction.md +8 -5
- package/www/docs/timetables.md +35 -27
- package/www/package.json +2 -5
- package/www/static/img/gtfs-to-html-logo.svg +15 -61
- package/www/yarn.lock +2160 -3398
package/lib/formatters.js
CHANGED
|
@@ -1,14 +1,32 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
clone,
|
|
3
|
+
find,
|
|
4
|
+
first,
|
|
5
|
+
flatMap,
|
|
6
|
+
groupBy,
|
|
7
|
+
last,
|
|
8
|
+
omit,
|
|
9
|
+
sortBy,
|
|
10
|
+
uniqBy,
|
|
11
|
+
zipObject,
|
|
12
|
+
} from 'lodash-es';
|
|
2
13
|
import moment from 'moment';
|
|
3
14
|
|
|
4
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
fromGTFSTime,
|
|
17
|
+
minutesAfterMidnight,
|
|
18
|
+
calendarToCalendarCode,
|
|
19
|
+
secondsAfterMidnight,
|
|
20
|
+
toGTFSTime,
|
|
21
|
+
updateTimeByOffset,
|
|
22
|
+
} from './time-utils.js';
|
|
5
23
|
|
|
6
24
|
/*
|
|
7
25
|
* Replace all instances in a string with items from an object.
|
|
8
26
|
*/
|
|
9
27
|
function replaceAll(string, mapObject) {
|
|
10
28
|
const re = new RegExp(Object.keys(mapObject).join('|'), 'gi');
|
|
11
|
-
return string.replace(re, matched => mapObject[matched]);
|
|
29
|
+
return string.replace(re, (matched) => mapObject[matched]);
|
|
12
30
|
}
|
|
13
31
|
|
|
14
32
|
/*
|
|
@@ -120,13 +138,17 @@ function filterHourlyTimes(stops) {
|
|
|
120
138
|
// Sort stoptimes by minutes for first stop.
|
|
121
139
|
const firstStopTimesAndIndex = firstStopTimes.map((time, idx) => ({
|
|
122
140
|
idx,
|
|
123
|
-
time
|
|
141
|
+
time,
|
|
124
142
|
}));
|
|
125
|
-
const sortedFirstStopTimesAndIndex = sortBy(firstStopTimesAndIndex,
|
|
143
|
+
const sortedFirstStopTimesAndIndex = sortBy(firstStopTimesAndIndex, (item) =>
|
|
144
|
+
Number.parseInt(item.time.format('m'), 10)
|
|
145
|
+
);
|
|
126
146
|
|
|
127
147
|
// Filter and arrange stoptimes for all stops based on sort.
|
|
128
|
-
return stops.map(stop => {
|
|
129
|
-
stop.hourlyTimes = sortedFirstStopTimesAndIndex.map(item =>
|
|
148
|
+
return stops.map((stop) => {
|
|
149
|
+
stop.hourlyTimes = sortedFirstStopTimesAndIndex.map((item) =>
|
|
150
|
+
fromGTFSTime(stop.trips[item.idx].arrival_time).format(':mm')
|
|
151
|
+
);
|
|
130
152
|
|
|
131
153
|
return stop;
|
|
132
154
|
});
|
|
@@ -135,7 +157,15 @@ function filterHourlyTimes(stops) {
|
|
|
135
157
|
/*
|
|
136
158
|
* Format a calendar's list of days for display using abbreviated day names.
|
|
137
159
|
*/
|
|
138
|
-
const days = [
|
|
160
|
+
const days = [
|
|
161
|
+
'monday',
|
|
162
|
+
'tuesday',
|
|
163
|
+
'wednesday',
|
|
164
|
+
'thursday',
|
|
165
|
+
'friday',
|
|
166
|
+
'saturday',
|
|
167
|
+
'sunday',
|
|
168
|
+
];
|
|
139
169
|
export function formatDays(calendar, config) {
|
|
140
170
|
const daysShort = config.daysShortStrings;
|
|
141
171
|
let daysInARow = 0;
|
|
@@ -146,9 +176,9 @@ export function formatDays(calendar, config) {
|
|
|
146
176
|
}
|
|
147
177
|
|
|
148
178
|
for (let i = 0; i <= 6; i += 1) {
|
|
149
|
-
const currentDayOperating =
|
|
150
|
-
const previousDayOperating =
|
|
151
|
-
const nextDayOperating =
|
|
179
|
+
const currentDayOperating = calendar[days[i]] === 1;
|
|
180
|
+
const previousDayOperating = i > 0 ? calendar[days[i - 1]] === 1 : false;
|
|
181
|
+
const nextDayOperating = i < 6 ? calendar[days[i + 1]] === 1 : false;
|
|
152
182
|
|
|
153
183
|
if (currentDayOperating) {
|
|
154
184
|
if (dayString.length > 0) {
|
|
@@ -161,7 +191,12 @@ export function formatDays(calendar, config) {
|
|
|
161
191
|
|
|
162
192
|
daysInARow += 1;
|
|
163
193
|
|
|
164
|
-
if (
|
|
194
|
+
if (
|
|
195
|
+
dayString.length === 0 ||
|
|
196
|
+
!nextDayOperating ||
|
|
197
|
+
i === 6 ||
|
|
198
|
+
!previousDayOperating
|
|
199
|
+
) {
|
|
165
200
|
dayString += daysShort[i];
|
|
166
201
|
}
|
|
167
202
|
} else {
|
|
@@ -190,7 +225,7 @@ export function formatDaysLong(dayList, config) {
|
|
|
190
225
|
*/
|
|
191
226
|
export function formatTrip(trip, timetable, calendars, config) {
|
|
192
227
|
trip.calendar = find(calendars, {
|
|
193
|
-
service_id: trip.service_id
|
|
228
|
+
service_id: trip.service_id,
|
|
194
229
|
});
|
|
195
230
|
trip.dayList = formatDays(trip.calendar, config);
|
|
196
231
|
trip.dayListLong = formatDaysLong(trip.dayList, config);
|
|
@@ -198,7 +233,9 @@ export function formatTrip(trip, timetable, calendars, config) {
|
|
|
198
233
|
if (timetable.routes.length === 1) {
|
|
199
234
|
trip.route_short_name = timetable.routes[0].route_short_name;
|
|
200
235
|
} else {
|
|
201
|
-
const route = timetable.routes.find(
|
|
236
|
+
const route = timetable.routes.find(
|
|
237
|
+
(route) => route.route_id === trip.route_id
|
|
238
|
+
);
|
|
202
239
|
trip.route_short_name = route.route_short_name;
|
|
203
240
|
}
|
|
204
241
|
|
|
@@ -236,7 +273,9 @@ export function formatFrequency(frequency, config) {
|
|
|
236
273
|
* Generate a timetable id.
|
|
237
274
|
*/
|
|
238
275
|
export function formatTimetableId(timetable) {
|
|
239
|
-
let timetableId = `${timetable.route_ids.join('_')}|${calendarToCalendarCode(
|
|
276
|
+
let timetableId = `${timetable.route_ids.join('_')}|${calendarToCalendarCode(
|
|
277
|
+
timetable
|
|
278
|
+
)}`;
|
|
240
279
|
if (!isNullOrEmpty(timetable.direction_id)) {
|
|
241
280
|
timetableId += `|${timetable.direction_id}`;
|
|
242
281
|
}
|
|
@@ -258,7 +297,7 @@ function createEmptyStoptime(stopId, tripId) {
|
|
|
258
297
|
continuous_pickup: null,
|
|
259
298
|
continuous_drop_off: null,
|
|
260
299
|
shape_dist_traveled: null,
|
|
261
|
-
timepoint: null
|
|
300
|
+
timepoint: null,
|
|
262
301
|
};
|
|
263
302
|
}
|
|
264
303
|
|
|
@@ -288,7 +327,9 @@ export function formatStops(timetable, config) {
|
|
|
288
327
|
if (stop.type === 'arrival' && idx < trip.stoptimes.length - 1) {
|
|
289
328
|
const departureStoptime = clone(stoptime);
|
|
290
329
|
departureStoptime.type = 'departure';
|
|
291
|
-
timetable.stops[stopIndex + 1].trips.push(
|
|
330
|
+
timetable.stops[stopIndex + 1].trips.push(
|
|
331
|
+
formatStopTime(departureStoptime, timetable, config)
|
|
332
|
+
);
|
|
292
333
|
}
|
|
293
334
|
|
|
294
335
|
// Show times if it is an arrival stop and is the first stoptime for the trip.
|
|
@@ -315,16 +356,53 @@ export function formatStops(timetable, config) {
|
|
|
315
356
|
return timetable.stops;
|
|
316
357
|
}
|
|
317
358
|
|
|
359
|
+
/*
|
|
360
|
+
* Formats a stop name.
|
|
361
|
+
*/
|
|
362
|
+
export function formatStopName(stop) {
|
|
363
|
+
return `${stop.stop_name}${
|
|
364
|
+
stop.type === 'arrival'
|
|
365
|
+
? ' (Arrival)'
|
|
366
|
+
: stop.type === 'departure'
|
|
367
|
+
? ' (Departure)'
|
|
368
|
+
: ''
|
|
369
|
+
}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/*
|
|
373
|
+
* Formats trip "Contines from".
|
|
374
|
+
*/
|
|
375
|
+
export function formatTripContinuesFrom(trip) {
|
|
376
|
+
return trip.continues_from_route
|
|
377
|
+
? trip.continues_from_route.route.route_short_name
|
|
378
|
+
: '';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/*
|
|
382
|
+
* Formats trip "Contines as".
|
|
383
|
+
*/
|
|
384
|
+
export function formatTripContinuesAs(trip) {
|
|
385
|
+
return trip.continues_as_route
|
|
386
|
+
? trip.continues_as_route.route.route_short_name
|
|
387
|
+
: '';
|
|
388
|
+
}
|
|
389
|
+
|
|
318
390
|
/*
|
|
319
391
|
* Change all stoptimes of a trip so the first trip starts at midnight. Useful
|
|
320
392
|
* for hourly schedules.
|
|
321
393
|
*/
|
|
322
394
|
export function resetStoptimesToMidnight(trip) {
|
|
323
|
-
const offsetSeconds = secondsAfterMidnight(
|
|
395
|
+
const offsetSeconds = secondsAfterMidnight(
|
|
396
|
+
first(trip.stoptimes).departure_time
|
|
397
|
+
);
|
|
324
398
|
if (offsetSeconds > 0) {
|
|
325
399
|
for (const stoptime of trip.stoptimes) {
|
|
326
|
-
stoptime.departure_time = toGTFSTime(
|
|
327
|
-
|
|
400
|
+
stoptime.departure_time = toGTFSTime(
|
|
401
|
+
fromGTFSTime(stoptime.departure_time).subtract(offsetSeconds, 'seconds')
|
|
402
|
+
);
|
|
403
|
+
stoptime.arrival_time = toGTFSTime(
|
|
404
|
+
fromGTFSTime(stoptime.arrival_time).subtract(offsetSeconds, 'seconds')
|
|
405
|
+
);
|
|
328
406
|
}
|
|
329
407
|
}
|
|
330
408
|
|
|
@@ -336,10 +414,16 @@ export function resetStoptimesToMidnight(trip) {
|
|
|
336
414
|
* hourly schedules.
|
|
337
415
|
*/
|
|
338
416
|
export function updateStoptimesByOffset(trip, offsetSeconds) {
|
|
339
|
-
return trip.stoptimes.map(stoptime => {
|
|
417
|
+
return trip.stoptimes.map((stoptime) => {
|
|
340
418
|
delete stoptime._id;
|
|
341
|
-
stoptime.departure_time = updateTimeByOffset(
|
|
342
|
-
|
|
419
|
+
stoptime.departure_time = updateTimeByOffset(
|
|
420
|
+
stoptime.departure_time,
|
|
421
|
+
offsetSeconds
|
|
422
|
+
);
|
|
423
|
+
stoptime.arrival_time = updateTimeByOffset(
|
|
424
|
+
stoptime.arrival_time,
|
|
425
|
+
offsetSeconds
|
|
426
|
+
);
|
|
343
427
|
stoptime.trip_id = trip.trip_id;
|
|
344
428
|
return stoptime;
|
|
345
429
|
});
|
|
@@ -392,9 +476,18 @@ export function formatTimetablePageLabel(timetablePage) {
|
|
|
392
476
|
}
|
|
393
477
|
|
|
394
478
|
// Get label from first timetable.
|
|
395
|
-
if (
|
|
396
|
-
|
|
397
|
-
|
|
479
|
+
if (
|
|
480
|
+
timetablePage.consolidatedTimetables &&
|
|
481
|
+
timetablePage.consolidatedTimetables.length > 0
|
|
482
|
+
) {
|
|
483
|
+
const routes = uniqBy(
|
|
484
|
+
flatMap(
|
|
485
|
+
timetablePage.consolidatedTimetables,
|
|
486
|
+
(timetable) => timetable.routes
|
|
487
|
+
),
|
|
488
|
+
'route_id'
|
|
489
|
+
);
|
|
490
|
+
const timetablePageLabel = routes.map((route) => formatRouteName(route));
|
|
398
491
|
|
|
399
492
|
return timetablePageLabel.join(' and ');
|
|
400
493
|
}
|
|
@@ -412,10 +505,12 @@ export function mergeTimetablesWithSameId(timetables) {
|
|
|
412
505
|
|
|
413
506
|
const mergedTimetables = groupBy(timetables, 'timetable_id');
|
|
414
507
|
|
|
415
|
-
return Object.values(mergedTimetables).map(timetableGroup => {
|
|
508
|
+
return Object.values(mergedTimetables).map((timetableGroup) => {
|
|
416
509
|
const mergedTimetable = omit(timetableGroup[0], 'route_id');
|
|
417
510
|
|
|
418
|
-
mergedTimetable.route_ids = timetableGroup.map(
|
|
511
|
+
mergedTimetable.route_ids = timetableGroup.map(
|
|
512
|
+
(timetable) => timetable.route_id
|
|
513
|
+
);
|
|
419
514
|
|
|
420
515
|
return mergedTimetable;
|
|
421
516
|
});
|
package/lib/geojson-utils.js
CHANGED
|
@@ -6,14 +6,15 @@ import { featureCollection } from '@turf/helpers';
|
|
|
6
6
|
/*
|
|
7
7
|
* Merge any number of geojson objects into one. Only works for `FeatureCollection`.
|
|
8
8
|
*/
|
|
9
|
-
const mergeGeojson = (...geojsons) =>
|
|
9
|
+
const mergeGeojson = (...geojsons) =>
|
|
10
|
+
featureCollection(flatMap(geojsons, (geojson) => geojson.features));
|
|
10
11
|
|
|
11
12
|
/*
|
|
12
13
|
* Truncate a coordinate to a specific number of decimal places.
|
|
13
14
|
*/
|
|
14
15
|
const truncateCoordinate = (coordinate, precision) => [
|
|
15
|
-
Math.round(coordinate[0] *
|
|
16
|
-
Math.round(coordinate[1] *
|
|
16
|
+
Math.round(coordinate[0] * 10 ** precision) / 10 ** precision,
|
|
17
|
+
Math.round(coordinate[1] * 10 ** precision) / 10 ** precision,
|
|
17
18
|
];
|
|
18
19
|
|
|
19
20
|
/*
|
|
@@ -23,9 +24,15 @@ const truncateGeoJSONDecimals = (geojson, config) => {
|
|
|
23
24
|
for (const feature of geojson.features) {
|
|
24
25
|
if (feature.geometry.coordinates) {
|
|
25
26
|
if (feature.geometry.type.toLowerCase() === 'point') {
|
|
26
|
-
feature.geometry.coordinates = truncateCoordinate(
|
|
27
|
+
feature.geometry.coordinates = truncateCoordinate(
|
|
28
|
+
feature.geometry.coordinates,
|
|
29
|
+
config.coordinatePrecision
|
|
30
|
+
);
|
|
27
31
|
} else if (feature.geometry.type.toLowerCase() === 'linestring') {
|
|
28
|
-
feature.geometry.coordinates = feature.geometry.coordinates.map(
|
|
32
|
+
feature.geometry.coordinates = feature.geometry.coordinates.map(
|
|
33
|
+
(coordinate) =>
|
|
34
|
+
truncateCoordinate(coordinate, config.coordinatePrecision)
|
|
35
|
+
);
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
}
|
|
@@ -39,8 +46,8 @@ const truncateGeoJSONDecimals = (geojson, config) => {
|
|
|
39
46
|
const simplifyGeoJSON = (geojson, config) => {
|
|
40
47
|
try {
|
|
41
48
|
const simplifiedGeojson = simplify(geojson, {
|
|
42
|
-
tolerance: 1 /
|
|
43
|
-
highQuality: true
|
|
49
|
+
tolerance: 1 / 10 ** config.coordinatePrecision,
|
|
50
|
+
highQuality: true,
|
|
44
51
|
});
|
|
45
52
|
|
|
46
53
|
return truncateGeoJSONDecimals(simplifiedGeojson, config);
|
|
@@ -56,16 +63,24 @@ const simplifyGeoJSON = (geojson, config) => {
|
|
|
56
63
|
*/
|
|
57
64
|
export async function getTimetableGeoJSON(timetable, config) {
|
|
58
65
|
const [shapesGeojsons, stopsGeojsons] = await Promise.all([
|
|
59
|
-
await Promise.all(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
66
|
+
await Promise.all(
|
|
67
|
+
timetable.route_ids.map((routeId) =>
|
|
68
|
+
getShapesAsGeoJSON({
|
|
69
|
+
route_id: routeId,
|
|
70
|
+
direction_id: timetable.direction_id,
|
|
71
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id),
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
),
|
|
75
|
+
await Promise.all(
|
|
76
|
+
timetable.route_ids.map((routeId) =>
|
|
77
|
+
getStopsAsGeoJSON({
|
|
78
|
+
route_id: routeId,
|
|
79
|
+
direction_id: timetable.direction_id,
|
|
80
|
+
trip_id: timetable.orderedTrips.map((trip) => trip.trip_id),
|
|
81
|
+
})
|
|
82
|
+
)
|
|
83
|
+
),
|
|
69
84
|
]);
|
|
70
85
|
|
|
71
86
|
const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
|
package/lib/gtfs-to-html.js
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { map } from 'lodash-es';
|
|
5
5
|
import { openDb, getDb, importGtfs } from 'gtfs';
|
|
6
6
|
import sanitize from 'sanitize-filename';
|
|
7
7
|
import Timer from 'timer-machine';
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import {
|
|
10
|
+
prepDirectory,
|
|
11
|
+
copyStaticAssets,
|
|
12
|
+
generateFolderName,
|
|
13
|
+
renderPdf,
|
|
14
|
+
zipFolder,
|
|
15
|
+
generateCSVFileName,
|
|
16
|
+
} from './file-utils.js';
|
|
17
|
+
import {
|
|
18
|
+
log,
|
|
19
|
+
logWarning,
|
|
20
|
+
logError,
|
|
21
|
+
progressBar,
|
|
22
|
+
generateLogText,
|
|
23
|
+
logStats,
|
|
24
|
+
} from './log-utils.js';
|
|
25
|
+
import {
|
|
26
|
+
setDefaultConfig,
|
|
27
|
+
getTimetablePagesForAgency,
|
|
28
|
+
getFormattedTimetablePage,
|
|
29
|
+
generateTimetableHTML,
|
|
30
|
+
generateTimetableCSV,
|
|
31
|
+
generateOverviewHTML,
|
|
32
|
+
generateStats,
|
|
33
|
+
} from './utils.js';
|
|
12
34
|
|
|
13
35
|
/*
|
|
14
36
|
* Generate HTML timetables from GTFS.
|
|
15
37
|
*/
|
|
16
|
-
const gtfsToHtml = async initialConfig => {
|
|
38
|
+
const gtfsToHtml = async (initialConfig) => {
|
|
17
39
|
const config = setDefaultConfig(initialConfig);
|
|
18
40
|
const timer = new Timer();
|
|
19
41
|
|
|
@@ -23,9 +45,11 @@ const gtfsToHtml = async initialConfig => {
|
|
|
23
45
|
|
|
24
46
|
timer.start();
|
|
25
47
|
|
|
26
|
-
await openDb(config).catch(error => {
|
|
48
|
+
await openDb(config).catch((error) => {
|
|
27
49
|
if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
|
|
28
|
-
config.logError(
|
|
50
|
+
config.logError(
|
|
51
|
+
`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
|
|
52
|
+
);
|
|
29
53
|
}
|
|
30
54
|
|
|
31
55
|
throw error;
|
|
@@ -51,7 +75,9 @@ const gtfsToHtml = async initialConfig => {
|
|
|
51
75
|
await importGtfs(config);
|
|
52
76
|
}
|
|
53
77
|
|
|
54
|
-
const agencyKey = config.agencies
|
|
78
|
+
const agencyKey = config.agencies
|
|
79
|
+
.map((agency) => agency.agency_key)
|
|
80
|
+
.join('-');
|
|
55
81
|
const exportPath = path.join(process.cwd(), 'html', sanitize(agencyKey));
|
|
56
82
|
const outputStats = {
|
|
57
83
|
timetables: 0,
|
|
@@ -60,14 +86,17 @@ const gtfsToHtml = async initialConfig => {
|
|
|
60
86
|
trips: 0,
|
|
61
87
|
routes: 0,
|
|
62
88
|
stops: 0,
|
|
63
|
-
warnings: []
|
|
89
|
+
warnings: [],
|
|
64
90
|
};
|
|
65
91
|
|
|
66
92
|
const timetablePages = [];
|
|
67
|
-
const timetablePageIds = map(
|
|
93
|
+
const timetablePageIds = map(
|
|
94
|
+
await getTimetablePagesForAgency(config),
|
|
95
|
+
'timetable_page_id'
|
|
96
|
+
);
|
|
68
97
|
await prepDirectory(exportPath);
|
|
69
98
|
|
|
70
|
-
if (config.noHead !== true) {
|
|
99
|
+
if (config.noHead !== true && ['html', 'pdf'].includes(config.outputFormat)) {
|
|
71
100
|
copyStaticAssets(exportPath);
|
|
72
101
|
}
|
|
73
102
|
|
|
@@ -80,10 +109,15 @@ const gtfsToHtml = async initialConfig => {
|
|
|
80
109
|
/* eslint-disable no-await-in-loop */
|
|
81
110
|
for (const timetablePageId of timetablePageIds) {
|
|
82
111
|
try {
|
|
83
|
-
const timetablePage = await getFormattedTimetablePage(
|
|
112
|
+
const timetablePage = await getFormattedTimetablePage(
|
|
113
|
+
timetablePageId,
|
|
114
|
+
config
|
|
115
|
+
);
|
|
84
116
|
|
|
85
117
|
if (timetablePage.consolidatedTimetables.length === 0) {
|
|
86
|
-
throw new Error(
|
|
118
|
+
throw new Error(
|
|
119
|
+
`No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
|
|
120
|
+
);
|
|
87
121
|
}
|
|
88
122
|
|
|
89
123
|
for (const timetable of timetablePage.timetables) {
|
|
@@ -102,24 +136,43 @@ const gtfsToHtml = async initialConfig => {
|
|
|
102
136
|
await mkdir(path.join(exportPath, datePath), { recursive: true });
|
|
103
137
|
config.assetPath = '../';
|
|
104
138
|
|
|
105
|
-
timetablePage.relativePath = path.join(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
139
|
+
timetablePage.relativePath = path.join(
|
|
140
|
+
datePath,
|
|
141
|
+
sanitize(timetablePage.filename)
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (config.outputFormat === 'csv') {
|
|
145
|
+
for (const timetable of timetablePage.timetables) {
|
|
146
|
+
const csv = await generateTimetableCSV(timetable);
|
|
147
|
+
const csvPath = path.join(
|
|
148
|
+
exportPath,
|
|
149
|
+
datePath,
|
|
150
|
+
generateCSVFileName(timetable, timetablePage)
|
|
151
|
+
);
|
|
152
|
+
await writeFile(csvPath, csv);
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
const html = await generateTimetableHTML(timetablePage, config);
|
|
156
|
+
const htmlPath = path.join(
|
|
157
|
+
exportPath,
|
|
158
|
+
datePath,
|
|
159
|
+
sanitize(timetablePage.filename)
|
|
160
|
+
);
|
|
161
|
+
await writeFile(htmlPath, html);
|
|
162
|
+
|
|
163
|
+
if (config.outputFormat === 'pdf') {
|
|
164
|
+
await renderPdf(htmlPath);
|
|
112
165
|
}
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const htmlPath = path.join(exportPath, datePath, sanitize(timetablePage.filename));
|
|
116
|
-
await writeFile(htmlPath, results.html);
|
|
117
|
-
|
|
118
|
-
if (config.outputFormat === 'pdf') {
|
|
119
|
-
await renderPdf(htmlPath);
|
|
120
166
|
}
|
|
121
167
|
|
|
122
168
|
timetablePages.push(timetablePage);
|
|
169
|
+
const timetableStats = generateStats(timetablePage);
|
|
170
|
+
|
|
171
|
+
for (const key of Object.keys(outputStats)) {
|
|
172
|
+
if (timetableStats[key]) {
|
|
173
|
+
outputStats[key] += timetableStats[key];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
123
176
|
} catch (error) {
|
|
124
177
|
outputStats.warnings.push(error.message);
|
|
125
178
|
bar.interrupt(error.message);
|
|
@@ -129,10 +182,12 @@ const gtfsToHtml = async initialConfig => {
|
|
|
129
182
|
}
|
|
130
183
|
/* eslint-enable no-await-in-loop */
|
|
131
184
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
185
|
+
if (config.outputFormat === 'html') {
|
|
186
|
+
// Generate route summary index.html
|
|
187
|
+
config.assetPath = '';
|
|
188
|
+
const html = await generateOverviewHTML(timetablePages, config);
|
|
189
|
+
await writeFile(path.join(exportPath, 'index.html'), html);
|
|
190
|
+
}
|
|
136
191
|
|
|
137
192
|
// Generate output log.txt
|
|
138
193
|
const logText = await generateLogText(outputStats, config);
|
|
@@ -143,15 +198,22 @@ const gtfsToHtml = async initialConfig => {
|
|
|
143
198
|
await zipFolder(exportPath);
|
|
144
199
|
}
|
|
145
200
|
|
|
146
|
-
const fullExportPath = path.join(
|
|
201
|
+
const fullExportPath = path.join(
|
|
202
|
+
exportPath,
|
|
203
|
+
config.zipOutput ? '/timetables.zip' : ''
|
|
204
|
+
);
|
|
147
205
|
|
|
148
206
|
// Print stats
|
|
149
|
-
config.log(
|
|
207
|
+
config.log(
|
|
208
|
+
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetables created at ${fullExportPath}`
|
|
209
|
+
);
|
|
150
210
|
|
|
151
211
|
logStats(outputStats, config);
|
|
152
212
|
|
|
153
213
|
const seconds = Math.round(timer.time() / 1000);
|
|
154
|
-
config.log(
|
|
214
|
+
config.log(
|
|
215
|
+
`${agencyKey}: ${config.outputFormat.toUpperCase()} timetable generation required ${seconds} seconds`
|
|
216
|
+
);
|
|
155
217
|
|
|
156
218
|
timer.stop();
|
|
157
219
|
};
|
package/lib/log-utils.js
CHANGED
|
@@ -13,7 +13,10 @@ pe.start();
|
|
|
13
13
|
*/
|
|
14
14
|
export async function generateLogText(outputStats, config) {
|
|
15
15
|
const feedInfo = await getFeedInfo();
|
|
16
|
-
const feedVersion =
|
|
16
|
+
const feedVersion =
|
|
17
|
+
feedInfo.length > 0 && feedInfo[0].feed_version
|
|
18
|
+
? feedInfo[0].feed_version
|
|
19
|
+
: 'Unknown';
|
|
17
20
|
|
|
18
21
|
const logText = [
|
|
19
22
|
`Feed Version: ${feedVersion}`,
|
|
@@ -24,7 +27,7 @@ export async function generateLogText(outputStats, config) {
|
|
|
24
27
|
`Calendar Service ID Count: ${outputStats.calendars}`,
|
|
25
28
|
`Route Count: ${outputStats.routes}`,
|
|
26
29
|
`Trip Count: ${outputStats.trips}`,
|
|
27
|
-
`Stop Count: ${outputStats.stops}
|
|
30
|
+
`Stop Count: ${outputStats.stops}`,
|
|
28
31
|
];
|
|
29
32
|
|
|
30
33
|
for (const agency of config.agencies) {
|
|
@@ -74,7 +77,7 @@ export function logWarning(config) {
|
|
|
74
77
|
return config.logFunction;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
|
-
return text => {
|
|
80
|
+
return (text) => {
|
|
78
81
|
process.stdout.write(`\n${formatWarning(text)}\n`);
|
|
79
82
|
};
|
|
80
83
|
}
|
|
@@ -87,7 +90,7 @@ export function logError(config) {
|
|
|
87
90
|
return config.logFunction;
|
|
88
91
|
}
|
|
89
92
|
|
|
90
|
-
return text => {
|
|
93
|
+
return (text) => {
|
|
91
94
|
process.stdout.write(`\n${formatError(text)}\n`);
|
|
92
95
|
};
|
|
93
96
|
}
|
|
@@ -96,7 +99,9 @@ export function logError(config) {
|
|
|
96
99
|
* Format console warning text
|
|
97
100
|
*/
|
|
98
101
|
export function formatWarning(text) {
|
|
99
|
-
return `${chalk.yellow.underline('Warning')}${chalk.yellow(
|
|
102
|
+
return `${chalk.yellow.underline('Warning')}${chalk.yellow(
|
|
103
|
+
':'
|
|
104
|
+
)} ${chalk.yellow(text)}`;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
/*
|
|
@@ -104,7 +109,9 @@ export function formatWarning(text) {
|
|
|
104
109
|
*/
|
|
105
110
|
export function formatError(error) {
|
|
106
111
|
const message = error instanceof Error ? error.message : error;
|
|
107
|
-
return `${chalk.red.underline('Error')}${chalk.red(':')} ${chalk.red(
|
|
112
|
+
return `${chalk.red.underline('Error')}${chalk.red(':')} ${chalk.red(
|
|
113
|
+
message.replace('Error: ', '')
|
|
114
|
+
)}`;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
/*
|
|
@@ -118,7 +125,7 @@ export function logStats(stats, config) {
|
|
|
118
125
|
|
|
119
126
|
const table = new Table({
|
|
120
127
|
colWidths: [40, 20],
|
|
121
|
-
head: ['Item', 'Count']
|
|
128
|
+
head: ['Item', 'Count'],
|
|
122
129
|
});
|
|
123
130
|
|
|
124
131
|
table.push(
|
|
@@ -165,7 +172,7 @@ const generateProgressBarString = (barTotal, barProgress, size = 40) => {
|
|
|
165
172
|
}
|
|
166
173
|
|
|
167
174
|
const percentage = barProgress / barTotal;
|
|
168
|
-
const progress = Math.round(
|
|
175
|
+
const progress = Math.round(size * percentage);
|
|
169
176
|
const emptyProgress = size - progress;
|
|
170
177
|
const progressText = slider.repeat(progress);
|
|
171
178
|
const emptyProgressText = line.repeat(emptyProgress);
|
|
@@ -181,7 +188,7 @@ export function progressBar(formatString, barTotal, config) {
|
|
|
181
188
|
if (config.verbose === false) {
|
|
182
189
|
return {
|
|
183
190
|
increment: noop,
|
|
184
|
-
interrupt: noop
|
|
191
|
+
interrupt: noop,
|
|
185
192
|
};
|
|
186
193
|
}
|
|
187
194
|
|
|
@@ -189,15 +196,16 @@ export function progressBar(formatString, barTotal, config) {
|
|
|
189
196
|
return null;
|
|
190
197
|
}
|
|
191
198
|
|
|
192
|
-
const renderProgressString = () =>
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
const renderProgressString = () =>
|
|
200
|
+
formatString
|
|
201
|
+
.replace('{value}', barProgress)
|
|
202
|
+
.replace('{total}', barTotal)
|
|
203
|
+
.replace('{bar}', generateProgressBarString(barTotal, barProgress));
|
|
196
204
|
|
|
197
205
|
config.log(renderProgressString(), true);
|
|
198
206
|
|
|
199
207
|
return {
|
|
200
|
-
interrupt: text => {
|
|
208
|
+
interrupt: (text) => {
|
|
201
209
|
// Log two lines to avoid overwrite by progress bar
|
|
202
210
|
config.logWarning(text);
|
|
203
211
|
config.logWarning('');
|
|
@@ -205,6 +213,6 @@ export function progressBar(formatString, barTotal, config) {
|
|
|
205
213
|
increment: () => {
|
|
206
214
|
barProgress += 1;
|
|
207
215
|
config.log(renderProgressString(), true);
|
|
208
|
-
}
|
|
216
|
+
},
|
|
209
217
|
};
|
|
210
218
|
}
|