gtfs-to-html 2.6.12 → 2.7.1

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.7.1] - 2024-08-13
9
+
10
+ ### Fixed
11
+ - Handle MultiLineString geojson
12
+
13
+ ### Updated
14
+ - Warning about invalid timetable start/end dates
15
+ - Deduplicate timetablePage agency_ids
16
+ - Don't show drop_off_type info for first stoptime of a trip, don't show pickup_type info for last stoptime of a trip.
17
+ - Dependency updates
18
+
19
+ ## [2.7.0] - 2024-07-27
20
+
21
+ ### Fixed
22
+ - Fixes for horizontal orientation labels
23
+
24
+ ### Updated
25
+ - Better date format documentation
26
+ - Better default for timetable_page_label
27
+ - Larger map on desktop
28
+ - Improved button styles
29
+ - Serve static assets from templatePath
30
+ - Move static js and css to views/default
31
+ - Dependency updates
32
+
8
33
  ## [2.6.12] - 2024-07-26
9
34
 
10
35
  ### 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
  */
@@ -327,6 +313,16 @@ export function formatStops(timetable, config) {
327
313
  continue;
328
314
  }
329
315
 
316
+ // If first stoptime of the trip, remove drop_off_type information
317
+ if (idx === 0) {
318
+ stoptime.drop_off_type = 0;
319
+ }
320
+
321
+ // If last stoptime of the trip, remove pickup_type information
322
+ if (idx === trip.stoptimes.length - 1) {
323
+ stoptime.pickup_type = 0;
324
+ }
325
+
330
326
  // If showing arrival and departure times as separate columns/rows, add
331
327
  // trip to the departure stop, unless it is the last stoptime of the trip.
332
328
  if (stop.type === 'arrival' && idx < trip.stoptimes.length - 1) {
@@ -384,7 +380,7 @@ export function formatStopName(stop) {
384
380
  }
385
381
 
386
382
  /*
387
- * Formats trip "Contines from".
383
+ * Formats trip "Continues from".
388
384
  */
389
385
  export function formatTripContinuesFrom(trip) {
390
386
  return trip.continues_from_route
@@ -393,7 +389,7 @@ export function formatTripContinuesFrom(trip) {
393
389
  }
394
390
 
395
391
  /*
396
- * Formats trip "Contines as".
392
+ * Formats trip "Continues as".
397
393
  */
398
394
  export function formatTripContinuesAs(trip) {
399
395
  return trip.continues_as_route
@@ -500,33 +496,6 @@ export function formatTimetableLabel(timetable) {
500
496
  return timetableLabel;
501
497
  }
502
498
 
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
499
  /*
531
500
  * Merge timetables with same `timetable_id`.
532
501
  */
@@ -1,7 +1,7 @@
1
1
  import { getShapesAsGeoJSON, getStopsAsGeoJSON } from 'gtfs';
2
2
  import { flatMap } from 'lodash-es';
3
3
  import simplify from '@turf/simplify';
4
- import { featureCollection } from '@turf/helpers';
4
+ import { featureCollection, round } from '@turf/helpers';
5
5
 
6
6
  /*
7
7
  * Merge any number of geojson objects into one. Only works for `FeatureCollection`.
@@ -9,14 +9,6 @@ import { featureCollection } from '@turf/helpers';
9
9
  const mergeGeojson = (...geojsons) =>
10
10
  featureCollection(flatMap(geojsons, (geojson) => geojson.features));
11
11
 
12
- /*
13
- * Truncate a coordinate to a specific number of decimal places.
14
- */
15
- const truncateCoordinate = (coordinate, precision) => [
16
- Math.round(coordinate[0] * 10 ** precision) / 10 ** precision,
17
- Math.round(coordinate[1] * 10 ** precision) / 10 ** precision,
18
- ];
19
-
20
12
  /*
21
13
  * Truncate a geojson coordinates to a specific number of decimal places.
22
14
  */
@@ -24,14 +16,24 @@ const truncateGeoJSONDecimals = (geojson, config) => {
24
16
  for (const feature of geojson.features) {
25
17
  if (feature.geometry.coordinates) {
26
18
  if (feature.geometry.type.toLowerCase() === 'point') {
27
- feature.geometry.coordinates = truncateCoordinate(
28
- feature.geometry.coordinates,
29
- config.coordinatePrecision
19
+ feature.geometry.coordinates = feature.geometry.coordinates.map(
20
+ (number) => round(number, config.coordinatePrecision),
30
21
  );
31
22
  } else if (feature.geometry.type.toLowerCase() === 'linestring') {
32
23
  feature.geometry.coordinates = feature.geometry.coordinates.map(
33
24
  (coordinate) =>
34
- truncateCoordinate(coordinate, config.coordinatePrecision)
25
+ coordinate.map((number) =>
26
+ round(number, config.coordinatePrecision),
27
+ ),
28
+ );
29
+ } else if (feature.geometry.type.toLowerCase() === 'multilinestring') {
30
+ feature.geometry.coordinates = feature.geometry.coordinates.map(
31
+ (linestring) =>
32
+ linestring.map((coordinate) =>
33
+ coordinate.map((number) =>
34
+ round(number, config.coordinatePrecision),
35
+ ),
36
+ ),
35
37
  );
36
38
  }
37
39
  }
@@ -67,7 +69,7 @@ export function getTimetableGeoJSON(timetable, config) {
67
69
  route_id: routeId,
68
70
  direction_id: timetable.direction_id,
69
71
  trip_id: timetable.orderedTrips.map((trip) => trip.trip_id),
70
- })
72
+ }),
71
73
  );
72
74
 
73
75
  const stopsGeojsons = timetable.route_ids.map((routeId) =>
@@ -75,7 +77,7 @@ export function getTimetableGeoJSON(timetable, config) {
75
77
  route_id: routeId,
76
78
  direction_id: timetable.direction_id,
77
79
  trip_id: timetable.orderedTrips.map((trip) => trip.trip_id),
78
- })
80
+ }),
79
81
  );
80
82
 
81
83
  const geojson = mergeGeojson(...shapesGeojsons, ...stopsGeojsons);
@@ -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,
@@ -824,10 +823,24 @@ const getCalendarsFromTimetable = (timetable) => {
824
823
  const whereClauses = [];
825
824
 
826
825
  if (timetable.end_date) {
826
+ // Validate timetable.end_date is a valid date
827
+ if (!moment(timetable.end_date, 'YYYYMMDD', true).isValid()) {
828
+ throw new Error(
829
+ `Invalid end_date=${timetable.end_date} for timetable_id=${timetable.timetable_id}`,
830
+ );
831
+ }
832
+
827
833
  whereClauses.push(`start_date <= ${sqlString.escape(timetable.end_date)}`);
828
834
  }
829
835
 
830
836
  if (timetable.start_date) {
837
+ // Validate timetable.start_date is a valid date
838
+ if (!moment(timetable.start_date, 'YYYYMMDD', true).isValid()) {
839
+ throw new Error(
840
+ `Invalid start_date=${timetable.start_date} for timetable_id=${timetable.timetable_id}`,
841
+ );
842
+ }
843
+
831
844
  whereClauses.push(`end_date >= ${sqlString.escape(timetable.start_date)}`);
832
845
  }
833
846
 
@@ -1505,7 +1518,6 @@ export function getFormattedTimetablePage(timetablePageId, config) {
1505
1518
  timetablePage.timetables,
1506
1519
  config,
1507
1520
  );
1508
- timetablePage.timetable_page_label = formatTimetablePageLabel(timetablePage);
1509
1521
  timetablePage.dayList = formatDays(
1510
1522
  getDaysFromCalendars(timetablePage.consolidatedTimetables),
1511
1523
  config,
@@ -1521,11 +1533,11 @@ export function getFormattedTimetablePage(timetablePageId, config) {
1521
1533
  {
1522
1534
  route_id: timetablePage.route_ids,
1523
1535
  },
1524
- ['route_color', 'route_text_color', 'agency_id'],
1536
+ ['agency_id'],
1525
1537
  );
1526
1538
 
1527
- timetablePage.agency_ids = compact(
1528
- timetableRoutes.map((route) => route.agency_id),
1539
+ timetablePage.agency_ids = uniq(
1540
+ compact(timetableRoutes.map((route) => route.agency_id)),
1529
1541
  );
1530
1542
 
1531
1543
  // Set default filename.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtfs-to-html",
3
- "version": "2.6.12",
3
+ "version": "2.7.1",
4
4
  "private": false,
5
5
  "description": "Build human readable transit timetables as HTML, PDF or CSV from GTFS",
6
6
  "keywords": [
@@ -35,23 +35,23 @@
35
35
  "start": "node ./app"
36
36
  },
37
37
  "dependencies": {
38
- "@turf/helpers": "^7.0.0",
39
- "@turf/simplify": "^7.0.0",
38
+ "@turf/helpers": "^7.1.0",
39
+ "@turf/simplify": "^7.1.0",
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.4",
46
46
  "insane": "^2.6.2",
47
47
  "js-beautify": "^1.15.1",
48
48
  "lodash-es": "^4.17.21",
49
- "marked": "^13.0.2",
49
+ "marked": "^14.0.0",
50
50
  "moment": "^2.30.1",
51
51
  "morgan": "^1.10.0",
52
52
  "pretty-error": "^4.0.0",
53
53
  "pug": "^3.0.3",
54
- "puppeteer": "^22.14.0",
54
+ "puppeteer": "^23.0.2",
55
55
  "sanitize-filename": "^1.6.3",
56
56
  "sqlstring": "^2.3.3",
57
57
  "timer-machine": "^1.1.0",
@@ -61,8 +61,8 @@
61
61
  "yoctocolors": "^2.1.1"
62
62
  },
63
63
  "devDependencies": {
64
- "husky": "^9.1.3",
65
- "lint-staged": "^15.2.7",
64
+ "husky": "^9.1.4",
65
+ "lint-staged": "^15.2.9",
66
66
  "prettier": "^3.3.3"
67
67
  },
68
68
  "engines": {
@@ -117,7 +117,7 @@ a:hover {
117
117
 
118
118
  .timetable-overview .btn-blue {
119
119
  color: rgb(255 255 255);
120
- padding: 0.75rem 2rem;
120
+ padding: 0.75rem 1.5rem;
121
121
  background-color: rgb(37 99 235);
122
122
  border-radius: 0.375rem;
123
123
  justify-content: center;
@@ -180,7 +180,7 @@ a:hover {
180
180
 
181
181
  .timetable-page .btn-blue {
182
182
  color: rgb(255 255 255);
183
- padding: 0.75rem 2rem;
183
+ padding: 0.75rem 1.5rem;
184
184
  background-color: rgb(37 99 235);
185
185
  border-radius: 0.375rem;
186
186
  justify-content: center;
@@ -197,7 +197,7 @@ a:hover {
197
197
 
198
198
  .timetable-page .btn-gray {
199
199
  color: rgb(75 85 99);
200
- padding: 0.75rem 2rem;
200
+ padding: 0.75rem 1.5rem;
201
201
  background-color: rgb(209 213 219);
202
202
  border-radius: 0.375rem;
203
203
  justify-content: center;
@@ -221,7 +221,7 @@ a:hover {
221
221
  margin-bottom: 2.5rem;
222
222
  }
223
223
 
224
- .timetable-page .timetable .stop-header {
224
+ .timetable-page .timetable .table-vertical .stop-header {
225
225
  text-align: center;
226
226
  }
227
227
 
@@ -354,7 +354,13 @@ a:hover {
354
354
  }
355
355
 
356
356
  .timetable-page .table-horizontal tbody tr th.stop-name-container {
357
- min-width: 250px;
357
+ min-width: 175px;
358
+ }
359
+
360
+ @media screen and (min-width: 768px) {
361
+ .timetable-page .table-horizontal tbody tr th.stop-name-container {
362
+ min-width: 250px;
363
+ }
358
364
  }
359
365
 
360
366
  .timetable-page .table-hourly {
@@ -392,18 +398,18 @@ a:hover {
392
398
  flex-shrink: 0;
393
399
  }
394
400
 
395
- @media screen and (max-width: 767px) {
396
- .timetable-page .table-horizontal tbody tr th.stop-name-container {
397
- min-width: 175px;
398
- }
399
- }
400
-
401
401
  /* Map Styles */
402
402
 
403
403
  .timetable-page .map {
404
404
  min-height: 350px;
405
405
  }
406
406
 
407
+ @media screen and (min-width: 768px) {
408
+ .timetable-page .map {
409
+ min-height: 450px;
410
+ }
411
+ }
412
+
407
413
  .timetable-page .map .mapboxgl-popup-content .popup-title {
408
414
  margin: 0 20px 5px 0;
409
415
  font-size: 1rem;
@@ -1,11 +1,11 @@
1
1
  -
2
2
  function getTimetableSummary(timetable) {
3
3
  let summary = `This table shows schedules for a selection of key stops on the route for ${timetable.timetable_label} ${timetable.dayList}.`;
4
- if (timetable.orientation = 'vertical') {
4
+ if (timetable.orientation === 'vertical') {
5
5
  summary += ' Stops and their schedule times are listed in the columns.';
6
- } else if (timetable.orientation = 'horizontal') {
6
+ } else if (timetable.orientation === 'horizontal') {
7
7
  summary += ' Schedule times are listed in rows, starting with the stop name in the first cell of the row.';
8
- } else if (timetable.orientation = 'hourly') {
8
+ } else if (timetable.orientation === 'hourly') {
9
9
  summary += ' Schedule times are listed in rows, starting with the stop name in the first cell of the row and the minutes after the hour in the second row.';
10
10
  }
11
11
  return summary;
@@ -68,12 +68,7 @@
68
68
  }
69
69
 
70
70
  for (const feature of timetable.geojson.features) {
71
- if (feature.geometry.type === 'LineString') {
72
- feature.properties = {
73
- route_color: feature.properties.route_color
74
- }
75
- minifiedGeojson.features.push(feature)
76
- } else if (feature.geometry.type === 'Point') {
71
+ if (feature.geometry.type.toLowerCase() === 'point') {
77
72
  for (const route of feature.properties.routes) {
78
73
  routes[route.route_id] = route
79
74
  }
@@ -81,6 +76,16 @@
81
76
  feature.properties.routes = feature.properties.routes.map(route => route.route_id)
82
77
 
83
78
  minifiedGeojson.features.push(_.omit(feature, ['location_type', 'tts_stop_name']))
79
+ } else if (feature.geometry.type.toLowerCase() === 'linestring') {
80
+ feature.properties = {
81
+ route_color: feature.properties.route_color
82
+ }
83
+ minifiedGeojson.features.push(feature)
84
+ } else if (feature.geometry.type.toLowerCase() === 'multilinestring') {
85
+ feature.properties = {
86
+ route_color: feature.properties.route_color
87
+ }
88
+ minifiedGeojson.features.push(feature)
84
89
  }
85
90
  }
86
91
 
@@ -98,3 +103,11 @@
98
103
 
99
104
  return ''
100
105
  }
106
+
107
+ function formatRouteName(route) {
108
+ if (isNullOrEmpty(route.route_long_name)) {
109
+ return `Route ${route.route_short_name}`;
110
+ }
111
+
112
+ return route.route_long_name;
113
+ }
@@ -96,12 +96,18 @@ function formatStopPopup(feature) {
96
96
  function getBounds(geojson) {
97
97
  const bounds = new mapboxgl.LngLatBounds();
98
98
  for (const feature of geojson.features) {
99
- if (feature.geometry.type === 'Point') {
99
+ if (feature.geometry.type.toLowerCase() === 'point') {
100
100
  bounds.extend(feature.geometry.coordinates);
101
- } else if (feature.geometry.type === 'LineString') {
101
+ } else if (feature.geometry.type.toLowerCase() === 'linestring') {
102
102
  for (const coordinate of feature.geometry.coordinates) {
103
103
  bounds.extend(coordinate);
104
104
  }
105
+ } else if (feature.geometry.type.toLowerCase() === 'multilinestring') {
106
+ for (const linestring of feature.geometry.coordinates) {
107
+ for (const coordinate of linestring) {
108
+ bounds.extend(coordinate);
109
+ }
110
+ }
105
111
  }
106
112
  }
107
113
 
@@ -84,12 +84,18 @@ function formatStopPopup(feature, routes) {
84
84
  function getBounds(geojson) {
85
85
  const bounds = new mapboxgl.LngLatBounds();
86
86
  for (const feature of geojson.features) {
87
- if (feature.geometry.type === 'Point') {
87
+ if (feature.geometry.type.toLowerCase() === 'point') {
88
88
  bounds.extend(feature.geometry.coordinates);
89
- } else if (feature.geometry.type === 'LineString') {
89
+ } else if (feature.geometry.type.toLowerCase() === 'linestring') {
90
90
  for (const coordinate of feature.geometry.coordinates) {
91
91
  bounds.extend(coordinate);
92
92
  }
93
+ } else if (feature.geometry.type.toLowerCase() === 'multilinestring') {
94
+ for (const linestring of feature.geometry.coordinates) {
95
+ for (const coordinate of linestring) {
96
+ bounds.extend(coordinate);
97
+ }
98
+ }
93
99
  }
94
100
  }
95
101
 
@@ -10,10 +10,11 @@ include formatting_functions.pug
10
10
  each timetablePage in timetablePageGroup.timetablePages
11
11
  if config.allowEmptyTimetables || timetablePage.consolidatedTimetables.length > 0
12
12
  a.timetable-page-link(href=`${timetablePage.relativePath}` data-route-ids=`${timetablePage.route_ids ? timetablePage.route_ids.join(',') : ''}`)
13
- each route in _.uniqBy(_.flatMap(timetablePage.consolidatedTimetables, timetable => timetable.routes), 'route_id')
13
+ - const timetableRouteList = _.uniqBy(_.flatMap(timetablePage.consolidatedTimetables, timetable => timetable.routes), 'route_id')
14
+ each route in timetableRouteList
14
15
  .route-color-swatch-large(style=`background-color: ${formatRouteColor(route)}; color: ${formatRouteTextColor(route)};`)= route.route_short_name || ''
15
16
  div
16
- .timetable-page-label= timetablePage.timetable_page_label
17
+ .timetable-page-label= timetablePage.timetable_page_label || timetableRouteList.map(route => formatRouteName(route)).join(' and ')
17
18
  .badge-gray= timetablePage.dayList
18
19
  if config.showMap
19
20
  #system_map.overview-map
@@ -3,10 +3,11 @@ include formatting_functions.pug
3
3
  <!-- Timetable generated on #{new Date().toISOString()} using GTFS-to-HTML version #{config.gtfsToHtmlVersion} -->
4
4
  .timetable-page(class=`menu-type-${config.menuType}`)
5
5
  if config.showRouteTitle
6
+ - const timetableRouteList = _.uniqBy(_.flatMap(timetablePage.consolidatedTimetables, timetable => timetable.routes), 'route_id')
6
7
  h1
7
- each route in _.uniqBy(_.flatMap(timetablePage.consolidatedTimetables, timetable => timetable.routes), 'route_id')
8
+ each route in timetableRouteList
8
9
  .route-color-swatch-large(style=`background-color: ${formatRouteColor(route)}; color: ${formatRouteTextColor(route)};`)= route.route_short_name || ''
9
- div= timetablePage.timetable_page_label
10
+ div= timetablePage.timetable_page_label || timetableRouteList.map(route => formatRouteName(route)).join(' and ')
10
11
  if config.effectiveDate
11
12
  .effective-date= `Effective ${config.effectiveDate}`
12
13
 
@@ -235,7 +235,7 @@ API along with your API token.
235
235
 
236
236
  ### endDate
237
237
 
238
- \{String\} A date in ISO 8601 format to use to control which calendars are used for the timetables. Can be used with [startDate](#startdate) configuration options. Can be formatted as `YYYY-MM-DD` or `YYYYMMDD`.
238
+ \{String\} A date in `YYYYMMDD` format to use to control which calendars are used for the timetables. Can be used with [startDate](#startdate) configuration options.
239
239
 
240
240
  Optional, defaults to using all available calendars if not defined. Overridden by `start_date` and `end_date` defined in `timetables.txt`.
241
241
 
@@ -519,7 +519,7 @@ The default trip-sorting algorithm is `common`.
519
519
 
520
520
  ### startDate
521
521
 
522
- \{String\} A date in ISO 8601 format to use to control which calendars are used for the timetables. Can be used with [endDate](#enddate) configuration options. Can be formatted as `YYYY-MM-DD` or `YYYYMMDD`.
522
+ \{String\} A date in `YYYYMMDD` format to use to control which calendars are used for the timetables. Can be used with [endDate](#enddate) configuration options.
523
523
 
524
524
  Optional, defaults to using all available calendars if not defined. Overridden by `start_date` and `end_date` defined in `timetables.txt`.
525
525