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 +25 -0
- package/app/index.js +14 -9
- package/lib/file-utils.js +11 -16
- package/lib/formatters.js +12 -43
- package/lib/geojson-utils.js +17 -15
- package/lib/gtfs-to-html.js +2 -2
- package/lib/utils.js +17 -5
- package/package.json +9 -9
- package/{public → views/default}/css/overview_styles.css +1 -1
- package/{public → views/default}/css/timetable_styles.css +16 -10
- package/views/default/formatting_functions.pug +22 -9
- package/{public → views/default}/js/system-map.js +8 -2
- package/{public → views/default}/js/timetable-map.js +8 -2
- package/views/default/overview.pug +3 -2
- package/views/default/timetablepage.pug +3 -2
- package/www/docs/configuration.md +2 -2
- package/www/docs/timetables.md +2 -2
- package/www/package.json +2 -2
- package/www/yarn.lock +759 -768
- /package/{public → views/default}/css/timetable_pdf_styles.css +0 -0
- /package/{public → views/default}/js/timetable-menu.js +0 -0
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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 =
|
|
106
|
-
|
|
107
|
-
|
|
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 "
|
|
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 "
|
|
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
|
*/
|
package/lib/geojson-utils.js
CHANGED
|
@@ -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 =
|
|
28
|
-
|
|
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
|
-
|
|
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);
|
package/lib/gtfs-to-html.js
CHANGED
|
@@ -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.
|
|
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
|
-
['
|
|
1536
|
+
['agency_id'],
|
|
1525
1537
|
);
|
|
1526
1538
|
|
|
1527
|
-
timetablePage.agency_ids =
|
|
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.
|
|
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.
|
|
39
|
-
"@turf/simplify": "^7.
|
|
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.
|
|
43
|
+
"csv-stringify": "^6.5.1",
|
|
44
44
|
"express": "^4.19.2",
|
|
45
|
-
"gtfs": "^4.13.
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
65
|
-
"lint-staged": "^15.2.
|
|
64
|
+
"husky": "^9.1.4",
|
|
65
|
+
"lint-staged": "^15.2.9",
|
|
66
66
|
"prettier": "^3.3.3"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
4
|
+
if (timetable.orientation === 'vertical') {
|
|
5
5
|
summary += ' Stops and their schedule times are listed in the columns.';
|
|
6
|
-
} else if (timetable.orientation
|
|
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
|
|
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 === '
|
|
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 === '
|
|
99
|
+
if (feature.geometry.type.toLowerCase() === 'point') {
|
|
100
100
|
bounds.extend(feature.geometry.coordinates);
|
|
101
|
-
} else if (feature.geometry.type === '
|
|
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 === '
|
|
87
|
+
if (feature.geometry.type.toLowerCase() === 'point') {
|
|
88
88
|
bounds.extend(feature.geometry.coordinates);
|
|
89
|
-
} else if (feature.geometry.type === '
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|