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/README.md CHANGED
@@ -1,35 +1,51 @@
1
-
2
- # GTFS to HTML
3
-
4
- [![NPM version](https://img.shields.io/npm/v/gtfs-to-html.svg?style=flat)](https://www.npmjs.com/package/gtfs-to-html)
5
- [![David](https://img.shields.io/david/blinktaginc/gtfs-to-html.svg)]()
6
- [![npm](https://img.shields.io/npm/dm/gtfs-to-html.svg?style=flat)]()
7
- [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo)
8
-
9
- [![NPM](https://nodei.co/npm/gtfs-to-html.png?downloads=true)](https://nodei.co/npm/gtfs-to-html/)
10
-
11
- See [gtfstohtml.com](https://gtfstohtml.com) for full documentation.
12
-
13
- `gtfs-to-html` creates human-readable, user-friendly transit timetables in HTML and PDF format directly from [GTFS transit data](https://developers.google.com/transit/gtfs/). Most transit agencies have schedule data in GTFS format but need to show each route's schedule to users on a website. This project automates the process of creating nicely formatted HTML timetables for inclusion on a transit agency website. This makes it easy to keep timetables up to date and accurate when schedule changes happen and reduces the likelihood of errors.
1
+ <p align="center">
2
+ ➡️
3
+ <a href="https://gtfstohtml.com/docs/">Documentation</a> |
4
+ <a href="https://gtfstohtml.com/docs/quick-start">Quick Start</a> |
5
+ <a href="https://gtfstohtml.com/docs/configuration">Configuration</a> |
6
+ <a href="https://gtfstohtml.com/docs/contact">Questions and Support</a>
7
+ ⬅️
8
+ <br /><br />
9
+ <img src="www/static/img/gtfs-to-html-logo.svg" alt="GTFS-to-HTML" />
10
+ <br /><br />
11
+ <a href="https://www.npmjs.com/package/gtfs-to-html" rel="nofollow"><img src="https://img.shields.io/npm/v/gtfs-to-html.svg?style=flat" style="max-width: 100%;"></a>
12
+ <a href="https://www.npmjs.com/package/gtfs-to-html" rel="nofollow"><img src="https://img.shields.io/npm/dm/gtfs-to-html.svg?style=flat" style="max-width: 100%;"></a>
13
+ <img src="https://img.shields.io/badge/License-MIT-yellow.svg">
14
+ <br /><br />
15
+ Create human-readable, user-friendly transit timetables in HTML, PDF or CSV format directly from GTFS.
16
+ <br /><br />
17
+ <a href="https://nodei.co/npm/gtfs-to-html/" rel="nofollow"><img src="https://nodei.co/npm/gtfs-to-html.png?downloads=true" alt="NPM" style="max-width: 100%;"></a>
18
+ </p>
19
+
20
+ <hr>
21
+
22
+ See [gtfstohtml.com](https://gtfstohtml.com) for full documentation.
23
+
24
+ Most transit agencies have schedule data in [GTFS ](https://developers.google.com/transit/gtfs/) format but need to show each route's schedule to users on a website. GTFS-to-HTML automates the process of creating nicely formatted HTML timetables for inclusion on a transit agency website. This makes it easy to keep timetables up to date and accurate when schedule changes happen and reduces the likelihood of errors.
14
25
 
15
26
  <img width="1265" src="https://user-images.githubusercontent.com/96217/28296063-aed45568-6b1a-11e7-9794-94b3d915d668.png">
16
27
 
17
28
  ## Features
18
29
 
19
30
  ### Configurable and customizable
20
- `gtfs-to-html` has many options that configure how timetables are presented. It also allows using a completely custom template which makes it easy to build chunks of HTML that will fit perfectly into any website using any HTML structure and classes that you'd like. Or, create printable PDF versions of timetables using the `outputFormat` config option.
31
+
32
+ `gtfs-to-html` has many options that configure how timetables are presented. It also allows using a completely custom template which makes it easy to build chunks of HTML that will fit perfectly into any website using any HTML structure and classes that you'd like. Or, create printable PDF versions or CSV exports of timetables using the `outputFormat` config option.
21
33
 
22
34
  ### Accessibility for all
35
+
23
36
  `gtfs-to-html` properly formats timetables to ensure they are screen-reader accessible and WCAG 2.0 compliant.
24
37
 
25
38
  ### Mobile responsiveness built in
39
+
26
40
  Built-in styling makes `gtfs-to-html` timetables ready to size and scroll easily on mobile phones and tablets.
27
41
 
28
42
  ### Schedule changes? A cinch.
43
+
29
44
  By generating future timetables and including dates in table metadata, your timetables can appear in advance of a schedule change, and you can validate that your new timetables and GTFS are correct.
30
45
 
31
46
  ### Auto-generated maps
32
- `gtfs-to-html` can also generate a map for each route that can be included with the schedule page. The map shows all stops for the route and lists all routes that serve each stop. See the `showMap` configuration option below.
47
+
48
+ `gtfs-to-html` can also generate a map for each route that can be included with the schedule page. The map shows all stops for the route and lists all routes that serve each stop. See the `showMap` configuration option below.
33
49
 
34
50
  Note: If you only want maps of GTFS data, use the [gtfs-to-geojson](https://github.com/blinktaginc/gtfs-to-geojson) package instead and skip making timetables entirely. If offers many different formats of GeoJSON for routes and stops.
35
51
 
@@ -40,34 +56,36 @@ Note: If you only want maps of GTFS data, use the [gtfs-to-geojson](https://gith
40
56
  You can now use `gtfs-to-html` without actually downloading any code or doing any configuration. [run.gtfstohtml.com](https://run.gtfstohtml.com) provides a web based interface for finding GTFS feeds for agencies, setting configuration and then generates a previewable and downloadable set of timetables.
41
57
 
42
58
  ## Current Usage
59
+
43
60
  Many transit agencies use `gtfs-to-html` to generate the schedule pages used on their websites, including:
44
61
 
45
- * [Advance Transit](https://advancetransit.com)
46
- * [Brockton Area Transit Authority](https://ridebat.com)
47
- * [Capital Transit (Helena, Montana)](http://www.ridethecapitalt.org)
48
- * [Capital Transit (Juneau, Alaska)](https://juneaucapitaltransit.org)
49
- * [Central Transit (Ellensburg, Washington)](https://centraltransit.org)
50
- * [County Connection (Contra Costa County, California)](https://countyconnection.com)
51
- * [El Dorado Transit](http://eldoradotransit.com)
52
- * [Greater Attleboro-Taunton Regional Transit Authority](https://www.gatra.org)
53
- * [Humboldt Transit Authority](http://hta.org)
54
- * [Kings Area Rural Transit (KART)](https://www.kartbus.org)
55
- * [Madera County Connection](http://mcctransit.com)
56
- * [Marin Transit](https://marintransit.org)
57
- * [Morongo Basin Transit Authority](https://mbtabus.com)
58
- * [Mountain Transit](http://mountaintransit.org)
59
- * [MVgo (Mountain View, CA)](https://mvgo.org)
60
- * [NW Connector (Oregon)](http://www.nworegontransit.org)
61
- * [Palo Verde Valley Transit Agency](http://pvvta.com)
62
- * [Petaluma Transit](http://transit.cityofpetaluma.net)
63
- * [RTC Washoe (Reno, NV)](https://www.rtcwashoe.com)
64
- * [Santa Barbara Metropolitan Transit District](https://sbmtd.gov)
65
- * [Sonoma County Transit](http://sctransit.com)
66
- * [Tahoe Truckee Area Regional Transit](https://tahoetruckeetransit.com)
67
- * [Transcollines](https://transcollines.ca)
68
- * [Tulare County Area Transit](https://ridetcat.org)
69
- * [Victor Valley Transit](https://vvta.org)
70
- * [Worcester Regional Transit Authority](https://therta.com)
62
+ - [Advance Transit](https://advancetransit.com)
63
+ - [Brockton Area Transit Authority](https://ridebat.com)
64
+ - [Capital Transit (Helena, Montana)](http://www.ridethecapitalt.org)
65
+ - [Capital Transit (Juneau, Alaska)](https://juneaucapitaltransit.org)
66
+ - [Central Transit (Ellensburg, Washington)](https://centraltransit.org)
67
+ - [County Connection (Contra Costa County, California)](https://countyconnection.com)
68
+ - [El Dorado Transit](http://eldoradotransit.com)
69
+ - [Greater Attleboro-Taunton Regional Transit Authority](https://www.gatra.org)
70
+ - [Humboldt Transit Authority](http://hta.org)
71
+ - [Kings Area Rural Transit (KART)](https://www.kartbus.org)
72
+ - [Madera County Connection](http://mcctransit.com)
73
+ - [Marin Transit](https://marintransit.org)
74
+ - [Morongo Basin Transit Authority](https://mbtabus.com)
75
+ - [Mountain Transit](http://mountaintransit.org)
76
+ - [MVgo (Mountain View, CA)](https://mvgo.org)
77
+ - [NW Connector (Oregon)](http://www.nworegontransit.org)
78
+ - [Palo Verde Valley Transit Agency](http://pvvta.com)
79
+ - [Petaluma Transit](http://transit.cityofpetaluma.net)
80
+ - [RTC Washoe (Reno, NV)](https://www.rtcwashoe.com)
81
+ - [Santa Barbara Metropolitan Transit District](https://sbmtd.gov)
82
+ - [Sonoma County Transit](http://sctransit.com)
83
+ - [Tahoe Transportation District](https://www.tahoetransportation.org)
84
+ - [Tahoe Truckee Area Regional Transit](https://tahoetruckeetransit.com)
85
+ - [Transcollines](https://transcollines.ca)
86
+ - [Tulare County Area Transit](https://ridetcat.org)
87
+ - [Victor Valley Transit](https://vvta.org)
88
+ - [Worcester Regional Transit Authority](https://therta.com)
71
89
 
72
90
  Are you using `gtfs-to-html`? Let us know via email (brendan@blinktag.com) or via opening a github issue or pull request if your agency is using this library.
73
91
 
package/app/index.js CHANGED
@@ -9,20 +9,26 @@ import express from 'express';
9
9
  import logger from 'morgan';
10
10
 
11
11
  import { formatTimetableLabel } from '../lib/formatters.js';
12
- import { setDefaultConfig, getTimetablePagesForAgency, getFormattedTimetablePage, generateOverviewHTML, generateHTML } from '../lib/utils.js';
13
-
14
- const { argv } = yargs(process.argv)
15
- .option('c', {
16
- alias: 'configPath',
17
- describe: 'Path to config file',
18
- default: './config.json',
19
- type: 'string'
20
- });
12
+ import {
13
+ setDefaultConfig,
14
+ getTimetablePagesForAgency,
15
+ getFormattedTimetablePage,
16
+ generateOverviewHTML,
17
+ generateTimetableHTML,
18
+ } from '../lib/utils.js';
19
+
20
+ const { argv } = yargs(process.argv).option('c', {
21
+ alias: 'configPath',
22
+ describe: 'Path to config file',
23
+ default: './config.json',
24
+ type: 'string',
25
+ });
21
26
 
22
27
  const app = express();
23
28
  const router = new express.Router();
24
29
 
25
- const configPath = argv.configPath || new URL('../config.json', import.meta.url);
30
+ const configPath =
31
+ argv.configPath || new URL('../config.json', import.meta.url);
26
32
  const selectedConfig = JSON.parse(readFileSync(configPath));
27
33
 
28
34
  const config = setDefaultConfig(selectedConfig);
@@ -33,9 +39,11 @@ config.log = console.log;
33
39
  config.logWarning = console.warn;
34
40
  config.logError = console.error;
35
41
 
36
- openDb(config).catch(error => {
42
+ openDb(config).catch((error) => {
37
43
  if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
38
- config.logError(`Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`);
44
+ config.logError(
45
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
46
+ );
39
47
  }
40
48
 
41
49
  throw error;
@@ -47,14 +55,25 @@ openDb(config).catch(error => {
47
55
  router.get('/', async (request, response, next) => {
48
56
  try {
49
57
  const timetablePages = [];
50
- const timetablePageIds = map(await getTimetablePagesForAgency(config), 'timetable_page_id');
58
+ const timetablePageIds = map(
59
+ await getTimetablePagesForAgency(config),
60
+ 'timetable_page_id'
61
+ );
51
62
 
52
63
  for (const timetablePageId of timetablePageIds) {
53
64
  // eslint-disable-next-line no-await-in-loop
54
- const timetablePage = await getFormattedTimetablePage(timetablePageId, config);
55
-
56
- if (!timetablePage.consolidatedTimetables || timetablePage.consolidatedTimetables.length === 0) {
57
- console.error(`No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`);
65
+ const timetablePage = await getFormattedTimetablePage(
66
+ timetablePageId,
67
+ config
68
+ );
69
+
70
+ if (
71
+ !timetablePage.consolidatedTimetables ||
72
+ timetablePage.consolidatedTimetables.length === 0
73
+ ) {
74
+ console.error(
75
+ `No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
76
+ );
58
77
  }
59
78
 
60
79
  timetablePage.relativePath = `/timetables/${timetablePage.timetable_page_id}`;
@@ -76,19 +95,20 @@ router.get('/', async (request, response, next) => {
76
95
  * Show a specific timetable page
77
96
  */
78
97
  router.get('/timetables/:timetablePageId', async (request, response, next) => {
79
- const {
80
- timetablePageId
81
- } = request.params;
98
+ const { timetablePageId } = request.params;
82
99
 
83
100
  if (!timetablePageId) {
84
101
  return next(new Error('No timetablePageId provided'));
85
102
  }
86
103
 
87
104
  try {
88
- const timetablePage = await getFormattedTimetablePage(timetablePageId, config);
105
+ const timetablePage = await getFormattedTimetablePage(
106
+ timetablePageId,
107
+ config
108
+ );
89
109
 
90
- const results = await generateHTML(timetablePage, config);
91
- response.send(results.html);
110
+ const html = await generateTimetableHTML(timetablePage, config);
111
+ response.send(html);
92
112
  } catch (error) {
93
113
  next(error);
94
114
  }
@@ -98,7 +118,9 @@ app.set('views', path.join(fileURLToPath(import.meta.url), '../../views'));
98
118
  app.set('view engine', 'pug');
99
119
 
100
120
  app.use(logger('dev'));
101
- app.use(express.static(path.join(fileURLToPath(import.meta.url), '../../public')));
121
+ app.use(
122
+ express.static(path.join(fileURLToPath(import.meta.url), '../../public'))
123
+ );
102
124
 
103
125
  app.use('/', router);
104
126
  app.set('port', process.env.PORT || 3000);
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import yargs from 'yargs';
4
- /* eslint-disable-next-line node/file-extension-in-import */
5
4
  import { hideBin } from 'yargs/helpers';
6
5
 
7
6
  import { getConfig } from '../lib/file-utils.js';
@@ -15,22 +14,22 @@ const { argv } = yargs(hideBin(process.argv))
15
14
  alias: 'configPath',
16
15
  describe: 'Path to config file',
17
16
  default: './config.json',
18
- type: 'string'
17
+ type: 'string',
19
18
  })
20
19
  .option('s', {
21
20
  alias: 'skipImport',
22
21
  describe: 'Don’t import GTFS file.',
23
- type: 'boolean'
22
+ type: 'boolean',
24
23
  })
25
24
  .default('skipImport', undefined)
26
25
  .option('t', {
27
26
  alias: 'showOnlyTimepoint',
28
27
  describe: 'Show only stops with a `timepoint` value in `stops.txt`',
29
- type: 'boolean'
28
+ type: 'boolean',
30
29
  })
31
30
  .default('showOnlyTimepoint', undefined);
32
31
 
33
- const handleError = error => {
32
+ const handleError = (error) => {
34
33
  const text = error || 'Unknown Error';
35
34
  process.stdout.write(`\n${formatError(text)}\n`);
36
35
  console.error(error);
@@ -43,5 +42,4 @@ const setupImport = async () => {
43
42
  process.exit();
44
43
  };
45
44
 
46
- setupImport()
47
- .catch(handleError);
45
+ setupImport().catch(handleError);
package/lib/file-utils.js CHANGED
@@ -20,8 +20,15 @@ import * as templateFunctions from './template-functions.js';
20
20
  */
21
21
  export async function getConfig(argv) {
22
22
  try {
23
- const data = await readFile(path.resolve(untildify(argv.configPath)), 'utf8').catch(error => {
24
- console.error(new Error(`Cannot find configuration file at \`${argv.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`));
23
+ const data = await readFile(
24
+ path.resolve(untildify(argv.configPath)),
25
+ 'utf8'
26
+ ).catch((error) => {
27
+ console.error(
28
+ new Error(
29
+ `Cannot find configuration file at \`${argv.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`
30
+ )
31
+ );
25
32
  throw error;
26
33
  });
27
34
  const config = JSON.parse(data);
@@ -36,7 +43,11 @@ export async function getConfig(argv) {
36
43
 
37
44
  return config;
38
45
  } catch (error) {
39
- console.error(new Error(`Cannot parse configuration file at \`${argv.configPath}\`. Check to ensure that it is valid JSON.`));
46
+ console.error(
47
+ new Error(
48
+ `Cannot parse configuration file at \`${argv.configPath}\`. Check to ensure that it is valid JSON.`
49
+ )
50
+ );
40
51
  throw error;
41
52
  }
42
53
  }
@@ -52,15 +63,21 @@ function getTemplatePath(templateFileName, config) {
52
63
  }
53
64
 
54
65
  if (config.templatePath !== undefined) {
55
- return path.join(untildify(config.templatePath), `${fullTemplateFileName}.pug`);
66
+ return path.join(
67
+ untildify(config.templatePath),
68
+ `${fullTemplateFileName}.pug`
69
+ );
56
70
  }
57
71
 
58
- return path.join(fileURLToPath(import.meta.url), '../../views/default', `${fullTemplateFileName}.pug`);
72
+ return path.join(
73
+ fileURLToPath(import.meta.url),
74
+ '../../views/default',
75
+ `${fullTemplateFileName}.pug`
76
+ );
59
77
  }
60
78
 
61
79
  /*
62
- * Prepare the specified directory for saving HTML timetables by deleting
63
- * everything and creating the expected folders.
80
+ * Prepare the specified directory for saving HTML timetables by deleting everything.
64
81
  */
65
82
  export async function prepDirectory(exportPath) {
66
83
  await rm(exportPath, { recursive: true, force: true });
@@ -68,7 +85,9 @@ export async function prepDirectory(exportPath) {
68
85
  await mkdir(exportPath, { recursive: true });
69
86
  } catch (error) {
70
87
  if (error.code === 'ENOENT') {
71
- throw new Error(`Unable to write to ${exportPath}. Try running this command from a writable directory.`);
88
+ throw new Error(
89
+ `Unable to write to ${exportPath}. Try running this command from a writable directory.`
90
+ );
72
91
  }
73
92
 
74
93
  throw error;
@@ -79,7 +98,10 @@ export async function prepDirectory(exportPath) {
79
98
  * Copy needed CSS and JS to export path.
80
99
  */
81
100
  export function copyStaticAssets(exportPath) {
82
- const staticAssetPath = path.join(fileURLToPath(import.meta.url), '../../public');
101
+ const staticAssetPath = path.join(
102
+ fileURLToPath(import.meta.url),
103
+ '../../public'
104
+ );
83
105
  copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'));
84
106
  copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'));
85
107
  }
@@ -96,7 +118,7 @@ export function zipFolder(exportPath) {
96
118
  archive.on('error', reject);
97
119
  archive.pipe(output);
98
120
  archive.glob('**/*.{txt,css,js,html}', {
99
- cwd: exportPath
121
+ cwd: exportPath,
100
122
  });
101
123
  archive.finalize();
102
124
  });
@@ -109,7 +131,9 @@ export function generateFileName(timetable, config) {
109
131
  let filename = timetable.timetable_id;
110
132
 
111
133
  for (const route of timetable.routes) {
112
- filename += isNullOrEmpty(route.route_short_name) ? `_${route.route_long_name.replace(/\s/g, '-')}` : `_${route.route_short_name.replace(/\s/g, '-')}`;
134
+ filename += isNullOrEmpty(route.route_short_name)
135
+ ? `_${route.route_long_name.replace(/\s/g, '-')}`
136
+ : `_${route.route_short_name.replace(/\s/g, '-')}`;
113
137
  }
114
138
 
115
139
  if (!isNullOrEmpty(timetable.direction_id)) {
@@ -121,6 +145,19 @@ export function generateFileName(timetable, config) {
121
145
  return sanitize(filename).toLowerCase();
122
146
  }
123
147
 
148
+ /*
149
+ * Generate the filename for a CSV timetable.
150
+ */
151
+ export function generateCSVFileName(timetable, timetablePage) {
152
+ let filename = timetablePage.filename.replace(/.html$/, '');
153
+
154
+ if (timetablePage.timetables.length > 1) {
155
+ filename += `_${timetable.direction_id}`;
156
+ }
157
+
158
+ return sanitize(`${filename}.csv`);
159
+ }
160
+
124
161
  /*
125
162
  * Generates the folder name for a timetable page based on the date.
126
163
  */
@@ -144,13 +181,13 @@ export async function renderTemplate(templateFileName, templateVars, config) {
144
181
  const html = await renderFile(templatePath, {
145
182
  _,
146
183
  ...templateFunctions,
147
- ...templateVars
184
+ ...templateVars,
148
185
  });
149
186
 
150
187
  // Beautify HTML if `beautify` is set in config.
151
188
  if (config.beautify === true) {
152
189
  return beautify.html_beautify(html, {
153
- indent_size: 2
190
+ indent_size: 2,
154
191
  });
155
192
  }
156
193
 
@@ -166,10 +203,10 @@ export async function renderPdf(htmlPath) {
166
203
  const page = await browser.newPage();
167
204
  await page.emulateMediaType('screen');
168
205
  await page.goto(`file://${htmlPath}`, {
169
- waitUntil: 'networkidle0'
206
+ waitUntil: 'networkidle0',
170
207
  });
171
208
  await page.pdf({
172
- path: pdfPath
209
+ path: pdfPath,
173
210
  });
174
211
 
175
212
  await browser.close();