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 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
- const emptyStoptime = createEmptyStoptime(stop.stop_id, trip.trip_id);
347
- stop.trips.push(formatStopTime(emptyStoptime, timetable, config));
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(offsetSeconds, 'seconds')
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;
@@ -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.logWarning('');
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
- // If `showOnlyTimepoint` is true, then filter out all non-timepoints.
105
- const filteredTripStoptimes =
106
- config.showOnlyTimepoint === true
107
- ? trips.map((trip) =>
108
- trip.stoptimes.filter((stoptime) => isTimepoint(stoptime)),
109
- )
110
- : trips.map((trip) => trip.stoptimes);
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 filteredCalendarDates = {
294
- excludedDates: [],
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
- filteredCalendarDates.includedDates.push(
302
- formatDate(calendarDate, config.dateFormat),
303
- );
301
+ includedDates.push(formatDate(calendarDate, config.dateFormat));
304
302
  } else if (calendarDate.exception_type === 2) {
305
- filteredCalendarDates.excludedDates.push(
306
- formatDate(calendarDate, config.dateFormat),
307
- );
303
+ excludedDates.push(formatDate(calendarDate, config.dateFormat));
308
304
  }
309
305
  }
310
306
  }
311
307
 
312
- return filteredCalendarDates;
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
- // If `showOnlyTimepoint` is true, then filter out all non-timepoints.
678
- const sortedStopIds =
679
- config.showOnlyTimepoint === true
680
- ? trip.stoptimes
681
- .filter((stoptime) => isTimepoint(stoptime))
682
- .map((stoptime) => stoptime.stop_id)
683
- : trip.stoptimes.map((stoptime) => stoptime.stop_id);
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.6",
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.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.0",
43
+ "csv-stringify": "^6.4.5",
44
44
  "express": "^4.18.2",
45
- "gtfs": "^4.5.0",
46
- "js-beautify": "^1.14.9",
45
+ "gtfs": "^4.5.1",
46
+ "js-beautify": "^1.14.11",
47
47
  "lodash-es": "^4.17.21",
48
- "moment": "^2.29.4",
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.0",
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": "^14.0.1",
64
- "prettier": "^3.0.2"
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,