gtfs-to-html 2.6.11 → 2.7.0

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,26 @@ 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.7.0] - 2024-07-27
9
+
10
+ ### Fixed
11
+ - Fixes for horizontal orientation labels
12
+
13
+ ### Updated
14
+ - Better date format documentation
15
+ - Better default for timetable_page_label
16
+ - Larger map on desktop
17
+ - Improved button styles
18
+ - Serve static assets from templatePath
19
+ - Move static js and css to views/default
20
+ - Dependency updates
21
+
22
+ ## [2.6.12] - 2024-07-26
23
+
24
+ ### Updated
25
+ - Dependency updates
26
+ - Improved styles for default template
27
+
8
28
  ## [2.6.11] - 2024-07-24
9
29
 
10
30
  ### Updated
package/app/index.js CHANGED
@@ -4,9 +4,9 @@ import { readFileSync } from 'node:fs';
4
4
  import { map } from 'lodash-es';
5
5
  import yargs from 'yargs';
6
6
  import { openDb } from 'gtfs';
7
-
8
7
  import express from 'express';
9
8
  import logger from 'morgan';
9
+ import untildify from 'untildify';
10
10
 
11
11
  import { formatTimetableLabel } from '../lib/formatters.js';
12
12
  import {
@@ -44,7 +44,7 @@ try {
44
44
  } catch (error) {
45
45
  if (error instanceof Error && error.code === 'SQLITE_CANTOPEN') {
46
46
  config.logError(
47
- `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`
47
+ `Unable to open sqlite database "${config.sqlitePath}" defined as \`sqlitePath\` config.json. Ensure the parent directory exists or remove \`sqlitePath\` from config.json.`,
48
48
  );
49
49
  }
50
50
 
@@ -59,14 +59,14 @@ router.get('/', async (request, response, next) => {
59
59
  const timetablePages = [];
60
60
  const timetablePageIds = map(
61
61
  getTimetablePagesForAgency(config),
62
- 'timetable_page_id'
62
+ 'timetable_page_id',
63
63
  );
64
64
 
65
65
  for (const timetablePageId of timetablePageIds) {
66
66
  // eslint-disable-next-line no-await-in-loop
67
67
  const timetablePage = await getFormattedTimetablePage(
68
68
  timetablePageId,
69
- config
69
+ config,
70
70
  );
71
71
 
72
72
  if (
@@ -74,7 +74,7 @@ router.get('/', async (request, response, next) => {
74
74
  timetablePage.consolidatedTimetables.length === 0
75
75
  ) {
76
76
  console.error(
77
- `No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`
77
+ `No timetables found for timetable_page_id=${timetablePage.timetable_page_id}`,
78
78
  );
79
79
  }
80
80
 
@@ -107,7 +107,7 @@ router.get('/timetables/:timetablePageId', async (request, response, next) => {
107
107
  try {
108
108
  const timetablePage = await getFormattedTimetablePage(
109
109
  timetablePageId,
110
- config
110
+ config,
111
111
  );
112
112
 
113
113
  const html = await generateTimetableHTML(timetablePage, config);
@@ -121,9 +121,14 @@ app.set('views', path.join(fileURLToPath(import.meta.url), '../../views'));
121
121
  app.set('view engine', 'pug');
122
122
 
123
123
  app.use(logger('dev'));
124
- app.use(
125
- express.static(path.join(fileURLToPath(import.meta.url), '../../public'))
126
- );
124
+
125
+ // Serve static assets
126
+ const staticAssetPath =
127
+ config.templatePath === undefined
128
+ ? path.join(fileURLToPath(import.meta.url), '../../views/default')
129
+ : untildify(config.templatePath);
130
+
131
+ app.use(express.static(staticAssetPath));
127
132
 
128
133
  app.use('/', router);
129
134
  app.set('port', process.env.PORT || 3000);
package/lib/file-utils.js CHANGED
@@ -66,18 +66,12 @@ function getTemplatePath(templateFileName, config) {
66
66
  fullTemplateFileName += '_full';
67
67
  }
68
68
 
69
- if (config.templatePath !== undefined) {
70
- return path.join(
71
- untildify(config.templatePath),
72
- `${fullTemplateFileName}.pug`,
73
- );
74
- }
69
+ const templatePath =
70
+ config.templatePath === undefined
71
+ ? path.join(fileURLToPath(import.meta.url), '../../views/default')
72
+ : untildify(config.templatePath);
75
73
 
76
- return path.join(
77
- fileURLToPath(import.meta.url),
78
- '../../views/default',
79
- `${fullTemplateFileName}.pug`,
80
- );
74
+ return path.join(templatePath, `${fullTemplateFileName}.pug`);
81
75
  }
82
76
 
83
77
  /*
@@ -101,11 +95,12 @@ export async function prepDirectory(exportPath) {
101
95
  /*
102
96
  * Copy needed CSS and JS to export path.
103
97
  */
104
- export function copyStaticAssets(exportPath) {
105
- const staticAssetPath = path.join(
106
- fileURLToPath(import.meta.url),
107
- '../../public',
108
- );
98
+ export function copyStaticAssets(config, exportPath) {
99
+ const staticAssetPath =
100
+ config.templatePath === undefined
101
+ ? path.join(fileURLToPath(import.meta.url), '../../views/default')
102
+ : untildify(config.templatePath);
103
+
109
104
  copydir.sync(path.join(staticAssetPath, 'css'), path.join(exportPath, 'css'));
110
105
  copydir.sync(path.join(staticAssetPath, 'js'), path.join(exportPath, 'js'));
111
106
  }
package/lib/formatters.js CHANGED
@@ -247,20 +247,6 @@ export function formatTrip(trip, timetable, calendars, config) {
247
247
  return trip;
248
248
  }
249
249
 
250
- /*
251
- * Format a route name.
252
- */
253
- export function formatRouteName(route) {
254
- let routeName = 'Route ';
255
- if (!isNullOrEmpty(route.route_short_name)) {
256
- routeName += route.route_short_name;
257
- } else if (!isNullOrEmpty(route.route_long_name)) {
258
- routeName += route.route_long_name;
259
- }
260
-
261
- return routeName;
262
- }
263
-
264
250
  /*
265
251
  * Format a frequency.
266
252
  */
@@ -384,7 +370,7 @@ export function formatStopName(stop) {
384
370
  }
385
371
 
386
372
  /*
387
- * Formats trip "Contines from".
373
+ * Formats trip "Continues from".
388
374
  */
389
375
  export function formatTripContinuesFrom(trip) {
390
376
  return trip.continues_from_route
@@ -393,7 +379,7 @@ export function formatTripContinuesFrom(trip) {
393
379
  }
394
380
 
395
381
  /*
396
- * Formats trip "Contines as".
382
+ * Formats trip "Continues as".
397
383
  */
398
384
  export function formatTripContinuesAs(trip) {
399
385
  return trip.continues_as_route
@@ -500,33 +486,6 @@ export function formatTimetableLabel(timetable) {
500
486
  return timetableLabel;
501
487
  }
502
488
 
503
- /*
504
- * Format a label for a timetable page.
505
- */
506
- export function formatTimetablePageLabel(timetablePage) {
507
- if (!isNullOrEmpty(timetablePage.timetable_page_label)) {
508
- return timetablePage.timetable_page_label;
509
- }
510
-
511
- if (
512
- timetablePage.consolidatedTimetables &&
513
- timetablePage.consolidatedTimetables.length > 0
514
- ) {
515
- // Use route names from all timetables
516
- const routes = uniqBy(
517
- flatMap(
518
- timetablePage.consolidatedTimetables,
519
- (timetable) => timetable.routes,
520
- ),
521
- 'route_id',
522
- );
523
-
524
- return routes.map((route) => formatRouteName(route)).join(' and ');
525
- }
526
-
527
- return 'Unknown';
528
- }
529
-
530
489
  /*
531
490
  * Merge timetables with same `timetable_id`.
532
491
  */
@@ -89,7 +89,7 @@ const gtfsToHtml = async (initialConfig) => {
89
89
  await prepDirectory(exportPath);
90
90
 
91
91
  if (config.noHead !== true && ['html', 'pdf'].includes(config.outputFormat)) {
92
- copyStaticAssets(exportPath);
92
+ copyStaticAssets(config, exportPath);
93
93
  }
94
94
 
95
95
  const bar = progressBar(
@@ -134,7 +134,7 @@ const gtfsToHtml = async (initialConfig) => {
134
134
  );
135
135
 
136
136
  if (config.outputFormat === 'csv') {
137
- for (const timetable of timetablePage.timetables) {
137
+ for (const timetable of timetablePage.consolidatedTimetables) {
138
138
  const csv = await generateTimetableCSV(timetable);
139
139
  const csvPath = path.join(
140
140
  exportPath,
package/lib/utils.js CHANGED
@@ -58,7 +58,6 @@ import {
58
58
  formatStops,
59
59
  formatTimetableId,
60
60
  formatTimetableLabel,
61
- formatTimetablePageLabel,
62
61
  formatTrip,
63
62
  formatTripContinuesAs,
64
63
  formatTripContinuesFrom,
@@ -1505,7 +1504,6 @@ export function getFormattedTimetablePage(timetablePageId, config) {
1505
1504
  timetablePage.timetables,
1506
1505
  config,
1507
1506
  );
1508
- timetablePage.timetable_page_label = formatTimetablePageLabel(timetablePage);
1509
1507
  timetablePage.dayList = formatDays(
1510
1508
  getDaysFromCalendars(timetablePage.consolidatedTimetables),
1511
1509
  config,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs-to-html",
3
- "version": "2.6.11",
3
+ "version": "2.7.0",
4
4
  "private": false,
5
5
  "description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
6
6
  "keywords": [
@@ -40,9 +40,9 @@
40
40
  "archiver": "^7.0.1",
41
41
  "cli-table": "^0.3.11",
42
42
  "copy-dir": "^1.3.0",
43
- "csv-stringify": "^6.5.0",
43
+ "csv-stringify": "^6.5.1",
44
44
  "express": "^4.19.2",
45
- "gtfs": "^4.13.1",
45
+ "gtfs": "^4.13.2",
46
46
  "insane": "^2.6.2",
47
47
  "js-beautify": "^1.15.1",
48
48
  "lodash-es": "^4.17.21",
@@ -51,7 +51,7 @@
51
51
  "morgan": "^1.10.0",
52
52
  "pretty-error": "^4.0.0",
53
53
  "pug": "^3.0.3",
54
- "puppeteer": "^22.13.1",
54
+ "puppeteer": "^22.14.0",
55
55
  "sanitize-filename": "^1.6.3",
56
56
  "sqlstring": "^2.3.3",
57
57
  "timer-machine": "^1.1.0",
@@ -61,7 +61,7 @@
61
61
  "yoctocolors": "^2.1.1"
62
62
  },
63
63
  "devDependencies": {
64
- "husky": "^9.1.1",
64
+ "husky": "^9.1.3",
65
65
  "lint-staged": "^15.2.7",
66
66
  "prettier": "^3.3.3"
67
67
  },
@@ -36,7 +36,59 @@ a:hover {
36
36
 
37
37
  /* Overview styles */
38
38
 
39
- .route-color-swatch {
39
+
40
+ .timetable-overview {
41
+ height: 100vh;
42
+ align-items: stretch;
43
+ }
44
+
45
+ @media (min-width: 768px) {
46
+ .timetable-overview {
47
+ display: flex;
48
+ }
49
+ }
50
+
51
+ .timetable-overview h1 {
52
+ font-size: 1.5rem;
53
+ line-height: 2rem;
54
+ margin-left: 1rem;
55
+ margin-right: 1rem;
56
+ padding-top: 0.75rem;
57
+ padding-bottom: 0.75rem;
58
+ }
59
+
60
+ .timetable-overview .overview-list {
61
+ flex: none;
62
+ }
63
+
64
+ @media (min-width: 768px) {
65
+ .timetable-overview .overview-list {
66
+ max-width: 24rem;
67
+ overflow-y: scroll;
68
+ }
69
+ }
70
+
71
+ .timetable-overview a.timetable-page-link {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.5rem;
75
+ border-bottom: 1px solid rgb(226 232 240);
76
+ text-decoration: none;
77
+ padding: 0.5rem;
78
+ }
79
+
80
+ .timetable-overview a.timetable-page-link:hover {
81
+ background-color: rgb(241 245 249);
82
+ text-decoration: none;
83
+ }
84
+
85
+ .timetable-overview a.timetable-page-link .timetable-page-label {
86
+ font-size: 1.25rem;
87
+ line-height: 1;
88
+ color: rgb(30 41 59)
89
+ }
90
+
91
+ .timetable-overview .route-color-swatch {
40
92
  min-width: 34px;
41
93
  height: 34px;
42
94
  border-radius: 17px;
@@ -45,9 +97,11 @@ a:hover {
45
97
  font-size: 14px;
46
98
  letter-spacing: -0.5px;
47
99
  padding: 0 5px;
100
+ flex: none;
101
+ flex-shrink: 0;
48
102
  }
49
103
 
50
- .route-color-swatch-large {
104
+ .timetable-overview .route-color-swatch-large {
51
105
  min-width: 46px;
52
106
  height: 46px;
53
107
  border-radius: 23px;
@@ -57,11 +111,13 @@ a:hover {
57
111
  font-weight: bold;
58
112
  letter-spacing: -1px;
59
113
  padding: 0 6px;
114
+ flex: none;
115
+ flex-shrink: 0;
60
116
  }
61
117
 
62
- .btn-blue {
118
+ .timetable-overview .btn-blue {
63
119
  color: rgb(255 255 255);
64
- padding: 0.75rem 2rem;
120
+ padding: 0.75rem 1.5rem;
65
121
  background-color: rgb(37 99 235);
66
122
  border-radius: 0.375rem;
67
123
  justify-content: center;
@@ -71,172 +127,71 @@ a:hover {
71
127
  text-decoration: none;
72
128
  }
73
129
 
74
- .btn-blue:hover {
130
+ .timetable-overview .btn-blue:hover {
75
131
  background-color: rgb(29 78 216);
76
132
  text-decoration: none;
77
133
  }
78
134
 
79
- .btn-sm {
135
+ .timetable-overview .btn-sm {
80
136
  padding: 0.25rem 1rem;
81
137
  border-radius: 0.25rem;
82
138
  }
83
139
 
84
- .mr-1 {
85
- margin-right: 0.25rem;
86
- }
87
-
88
- .mx-4 {
89
- margin-left: 1rem;
90
- margin-right: 1rem;
91
- }
92
-
93
- .mb-2 {
94
- margin-bottom: 0.5rem;
95
- }
96
-
97
- .block {
98
- display: block;
99
- }
100
-
101
- .flex {
102
- display: flex;
103
- }
104
-
105
- .inline-flex {
140
+ .timetable-overview .badge-gray {
106
141
  display: inline-flex;
107
- }
108
-
109
- .h-full {
110
- height: 100%;
111
- }
112
-
113
- .h-screen {
114
- height: 100vh;
115
- }
116
-
117
- .w-full {
118
- width: 100%;
119
- }
120
-
121
- .flex-none {
122
- flex: none;
123
- }
124
-
125
- .items-center {
126
142
  align-items: center;
127
- }
128
-
129
- .items-stretch {
130
- align-items: stretch;
131
- }
132
-
133
- .justify-center {
134
143
  justify-content: center;
135
- }
136
-
137
- .gap-2 {
138
- gap: 0.5rem;
139
- }
140
-
141
- .rounded-full {
142
- border-radius: 9999px;
143
- }
144
-
145
- .border-b {
146
- border-bottom-width: 1px;
147
- }
148
-
149
- .border-slate-200 {
150
- border-color: rgb(226 232 240);
151
- }
152
-
153
- .bg-slate-200 {
154
- background-color: rgb(226 232 240);
155
- }
156
-
157
- .p-2 {
158
- padding: 0.5rem;
159
- }
160
-
161
- .px-2 {
162
144
  padding-left: 0.5rem;
163
145
  padding-right: 0.5rem;
164
- }
165
-
166
- .py-1 {
167
146
  padding-top: 0.25rem;
168
147
  padding-bottom: 0.25rem;
169
- }
170
-
171
- .py-3 {
172
- padding-top: 0.75rem;
173
- padding-bottom: 0.75rem;
174
- }
175
-
176
- .text-2xl {
177
- font-size: 1.5rem;
178
- line-height: 2rem;
179
- }
180
-
181
- .text-xl {
182
- font-size: 1.25rem;
183
- line-height: 1.75rem;
184
- }
185
-
186
- .text-xs {
187
148
  font-size: 0.75rem;
188
- line-height: 1rem;
189
- }
190
-
191
- .font-bold {
192
- font-weight: 700;
193
- }
194
-
195
- .leading-none {
196
149
  line-height: 1;
197
- }
198
-
199
- .text-slate-800 {
150
+ font-weight: 700;
200
151
  color: rgb(30 41 59);
152
+ background-color: rgb(226 232 240);
153
+ border-radius: 9999px;
201
154
  }
202
155
 
203
- .hover\:bg-slate-100:hover {
204
- background-color: rgb(241 245 249);
205
- }
206
-
207
- .hover\:no-underline:hover {
208
- text-decoration-line: none;
209
- }
210
-
211
- @media (min-width: 768px) {
212
- .md\:flex {
213
- display: flex;
214
- }
215
-
216
- .md\:max-w-96 {
217
- max-width: 24rem;
218
- }
156
+ /* Map Styles */
219
157
 
220
- .md\:overflow-y-scroll {
221
- overflow-y: scroll;
222
- }
158
+ .overview-map {
159
+ height: 100%;
160
+ width: 100%;
223
161
  }
224
162
 
225
- /* Map Styles */
226
-
227
- .map .mapboxgl-popup-content .popup-title {
163
+ .overview-map .mapboxgl-popup-content .popup-title {
228
164
  margin: 0 20px 5px 0;
229
165
  font-size: 16px;
230
166
  font-weight: bold;
231
167
  }
232
168
 
233
- .map .mapboxgl-popup-content .route-item {
169
+ .overview-map .mapboxgl-popup-content .popup-label {
170
+ margin-right: 0.25rem;
171
+ }
172
+
173
+ .overview-map .mapboxgl-popup-content .route-list {
174
+ margin-bottom: 0.5rem;
175
+ }
176
+
177
+ .overview-map .mapboxgl-popup-content .map-route-item {
234
178
  display: flex;
235
179
  align-items: center;
180
+ font-size: 0.75rem;
236
181
  line-height: 1;
182
+ margin-bottom: 0.5rem;
183
+ gap: 0.5rem;
184
+ }
185
+
186
+ .overview-map .mapboxgl-popup-content .map-route-item:hover {
187
+ text-decoration: none;
188
+ }
189
+
190
+ .overview-map .mapboxgl-popup-content a.map-route-item .underline-hover:hover {
191
+ text-decoration: underline;
237
192
  }
238
193
 
239
- .map .mapboxgl-popup-content .mapboxgl-popup-close-button {
194
+ .overview-map .mapboxgl-popup-content .mapboxgl-popup-close-button {
240
195
  padding: 0 5px;
241
196
  }
242
197