Skip to content

Commit 0434fdd

Browse files
committed
Updates to work with node-gtfs 1.0.0
version bump
1 parent 06d9380 commit 0434fdd

File tree

8 files changed

+212
-203
lines changed

8 files changed

+212
-203
lines changed

README.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99

1010
`gtfs-to-html` converts transit data in [GTFS format](https://developers.google.com/transit/gtfs/) into user-friendly HTML schedules. Many transit agencies have schedule data in GTFS format but need to show each route's schedule to users on a website. This project aims to automate the process of creating these schedules. Automating HTML schedule generation makes it easy to keep schedules up to date when data changes and reduces the likelihood of errors.
1111

12+
Additionally, a map showing the route and all stops can be included in the HTML schedule page. See the `showMap` configuration option below.
13+
1214
`gtfs-to-html` uses the [`node-gtfs`](https://github.com/blinktaginc/node-gtfs) library to handle importing and querying GTFS data.
1315

14-
`gtfs-to-html` is currently used by [Sonoma Country Transit](http://sctransit.com/) to generate schedule pages for each route.
16+
`gtfs-to-html` is currently used by [Sonoma Country Transit](http://sctransit.com/) to generate the schedules pages used on their website.
17+
18+
<img width="1265" src="https://user-images.githubusercontent.com/96217/28296063-aed45568-6b1a-11e7-9794-94b3d915d668.png">
1519

16-
<img width="1265" src="https://cloud.githubusercontent.com/assets/96217/10262598/87674f70-6983-11e5-8150-15b6372c989c.png">
1720

1821
## Installation
1922

@@ -54,6 +57,7 @@ Copy `config-sample.json` to `config.json` and then add your projects configurat
5457
| [`beautify`](#beautify) | boolean | Whether or not to beautify the HTML output. |
5558
| [`coordinatePrecision`](#coordinatePrecision) | integer | Number of decimal places to include in geoJSON map output. |
5659
| [`effectiveDate`](#effectivedate) | string | A date to print at the top of the timetable |
60+
| [`mapboxAccessToken`](#mapboxAccessToken) | string | The Mapbox access token for generating a map of the route. |
5761
| [`menuType`](#menuType) | string | The type of menu to use for selecting timetables on a timetable page. |
5862
| [`mongoUrl`](#mongoUrl) | string | The URL of the MongoDB database to import to. |
5963
| [`noHead`](#noHead) | boolean | Whether or not to skip the header and footer of the HTML document. |
@@ -166,6 +170,14 @@ API along with your API token.
166170
"effectiveDate": "July 8, 2015"
167171
```
168172

173+
### mapboxAccessToken
174+
175+
{String} The [Mapbox access token](https://www.mapbox.com/help/define-access-token/) for generating a map of the route.
176+
177+
```
178+
"mapboxAccessToken": "pk.eyXaX5F8oCJSYedim3yCnTGsVBfnRjsoXdy4Ej7ZZZydrCn2WMDXha5bPj5.bPj5xsBo8u8N8GJqJh"
179+
```
180+
169181
### menuType
170182

171183
{String} The type of menu to use for selecting or navigating to timetables on timetable pages with multiple timetables. Valid choices are `none`, `jump` and `radio`. Defaults to `jump`.

bin/gtfs-to-html.js

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,21 +34,25 @@ function handleError(err) {
3434
process.exit(1);
3535
}
3636

37-
// Read config JSON file and merge confiruration file with command-line arguments
38-
fs.readFile(resolve(argv.configPath), 'utf8')
39-
.then(data => JSON.parse(data))
40-
.then(config => _.merge(config, argv))
37+
const getConfig = async () => {
38+
const data = await fs.readFile(resolve(argv.configPath), 'utf8');
39+
return _.merge(JSON.parse(data), argv);
40+
};
41+
42+
getConfig()
4143
.catch(err => {
4244
console.error(new Error(`Cannot find configuration file at \`${argv.configPath}\`. Use config-sample.json as a starting point, pass --configPath option`));
4345
handleError(err);
4446
})
45-
.then(config => {
47+
.then(async config => {
48+
const log = (config.verbose === false) ? _.noop : console.log;
49+
4650
mongoose.Promise = global.Promise;
4751
mongoose.connect(config.mongoUrl);
48-
return gtfsToHtml(config);
49-
})
50-
.then(() => {
51-
console.log('Completed Generating HTML schedules');
52+
53+
await gtfsToHtml(config);
54+
55+
log('Completed Generating HTML schedules');
5256
process.exit();
5357
})
5458
.catch(handleError);

lib/formatters.js

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -105,14 +105,6 @@ exports.formatDays = calendar => {
105105
return dayString;
106106
};
107107

108-
exports.formatCalendars = calendars => {
109-
return calendars.map(item => {
110-
const calendar = item.toObject();
111-
calendar.dayList = exports.formatDays(calendar);
112-
return calendar;
113-
});
114-
};
115-
116108
exports.formatRouteName = route => {
117109
if (route.route_short_name !== '' && route.route_short_name !== undefined) {
118110
return route.route_short_name;

lib/gtfs-to-html.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ module.exports = selectedConfig => {
6464

6565
// Make directory, if it doesn't exist
6666
return fs.ensureDir(path.join(exportPath, datePath))
67-
.then(() => utils.generateHTML(agencyKey, timetablePage, config))
67+
.then(() => utils.generateHTML(timetablePage, config))
6868
.then(results => {
6969
_.each(outputStats, (stat, key) => {
7070
if (results.stats[key]) {

lib/utils.js

Lines changed: 63 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ const getSpecialDates = async timetable => {
9393
};
9494
};
9595

96-
const processStops = async (agencyKey, timetable, config) => {
96+
const processStops = async (timetable, config) => {
9797
timetable.showDayList = false;
9898
timetable.noServiceSymbolUsed = false;
9999
timetable.requestStopSymbolUsed = false;
@@ -119,13 +119,14 @@ const processStops = async (agencyKey, timetable, config) => {
119119
}
120120
return false;
121121
});
122-
if (!stop) {
123-
return;
124-
}
125-
stop.trips.push(formatters.formatStopTime(stoptime, timetable, config));
126122

127-
if (stop.type === 'arrival') {
128-
timetable.stops[stopIndex + 1].trips.push(formatters.formatStopTime(stoptime, timetable, config));
123+
if (stop) {
124+
stop.trips.push(formatters.formatStopTime(stoptime, timetable, config));
125+
126+
// If showing arrival and departure times, add trip to the departure stop
127+
if (stop.type === 'arrival') {
128+
timetable.stops[stopIndex + 1].trips.push(formatters.formatStopTime(stoptime, timetable, config));
129+
}
129130
}
130131
});
131132

@@ -141,7 +142,7 @@ const processStops = async (agencyKey, timetable, config) => {
141142
const stopIds = _.map(timetable.stops, 'stop_id');
142143

143144
const stopDatas = await gtfs.getStops({
144-
agency_key: agencyKey,
145+
agency_key: timetable.agency_key,
145146
stop_id: {
146147
$in: stopIds
147148
}
@@ -155,7 +156,7 @@ const processStops = async (agencyKey, timetable, config) => {
155156
// If `showStopCity` is true, look up stop attributes.
156157
if (config.showStopCity) {
157158
const stopAttributes = await gtfs.getStopAttributes({
158-
agency_key: agencyKey,
159+
agency_key: timetable.agency_key,
159160
stop_id: {
160161
$in: stopIds
161162
}
@@ -213,8 +214,20 @@ function getDaysFromCalendars(calendars) {
213214
return days;
214215
}
215216

216-
const convertRouteToTimetablePage = async (agencyKey, route, direction) => {
217+
const convertTimetableToTimetablePage = async timetable => {
218+
const filename = await generateFileName(timetable);
219+
return {
220+
agency_key: timetable.agency_key,
221+
timetable_page_id: timetable.timetable_id,
222+
timetable_page_label: timetable.timetable_label,
223+
timetables: [timetable],
224+
filename
225+
};
226+
};
227+
228+
const convertRouteToTimetablePage = async (route, direction) => {
217229
const timetable = {
230+
agency_key: route.agency_key,
218231
timetable_id: `${route.route_id}_${direction.direction_id}`,
219232
timetable_label: `${formatters.formatRouteName(route)} to ${direction.trip_headsign}`,
220233
route_id: route.route_id,
@@ -223,20 +236,14 @@ const convertRouteToTimetablePage = async (agencyKey, route, direction) => {
223236
end_date: parseInt(formatters.toGTFSDate(moment().add(1, 'week')), 10)
224237
};
225238

226-
const trips = gtfs.getTrips({
227-
agency_key: agencyKey,
239+
const serviceIds = await gtfs.getTrips({
240+
agency_key: route.agency_key,
228241
route_id: route.route_id,
229242
direction_id: direction.direction_id
230-
});
231-
232-
if (trips.length === 0) {
233-
return;
234-
}
235-
236-
const serviceIds = _.uniq(_.map(trips, 'service_id'));
243+
}).distinct('service_id');
237244

238245
const calendars = await gtfs.getCalendars({
239-
agency_key: agencyKey,
246+
agency_key: route.agency_key,
240247
service_id: {
241248
$in: serviceIds
242249
}
@@ -248,17 +255,6 @@ const convertRouteToTimetablePage = async (agencyKey, route, direction) => {
248255
return convertTimetableToTimetablePage(timetable);
249256
};
250257

251-
const convertTimetableToTimetablePage = async timetable => {
252-
const filename = await generateFileName(timetable);
253-
return {
254-
agency_key: timetable.agency_key,
255-
timetable_page_id: timetable.timetable_id,
256-
timetable_page_label: timetable.timetable_label,
257-
timetables: [timetable],
258-
filename
259-
};
260-
};
261-
262258
const convertRoutesToTimetablePages = async agencyKey => {
263259
const routes = await gtfs.getRoutes({agency_key: agencyKey});
264260
const timetablePages = await Promise.all(routes.map(async route => {
@@ -269,7 +265,7 @@ const convertRoutesToTimetablePages = async agencyKey => {
269265
const directionGroups = _.groupBy(results, direction => direction.direction_id);
270266
return Promise.all(_.map(directionGroups, directionGroup => {
271267
const direction = directionGroup[0];
272-
return convertRouteToTimetablePage(agencyKey, route, direction);
268+
return convertRouteToTimetablePage(route, direction);
273269
}));
274270
}));
275271

@@ -327,7 +323,24 @@ const formatTimetablePage = async timetablePage => {
327323
}
328324

329325
return timetablePage;
330-
}
326+
};
327+
328+
const getTimetableGeoJSON = async (timetable, config) => {
329+
const shapesGeojson = await gtfs.getShapesAsGeoJSON({
330+
agency_key: timetable.agency_key,
331+
route_id: timetable.route_id,
332+
direction_id: timetable.direction_id
333+
});
334+
335+
const stopsGeojson = await gtfs.getStopsAsGeoJSON({
336+
agency_key: timetable.agency_key,
337+
route_id: timetable.route_id,
338+
direction_id: timetable.direction_id
339+
});
340+
341+
const geojson = await geojsonMerge.merge([shapesGeojson, stopsGeojson]);
342+
return simplifyGeoJSON(geojson, config.coordinatePrecision);
343+
};
331344

332345
function simplifyGeoJSON(geojson, coordinatePrecision) {
333346
if (coordinatePrecision === undefined) {
@@ -362,7 +375,7 @@ exports.setDefaultConfig = config => {
362375
return Object.assign(defaults, config);
363376
};
364377

365-
exports.generateHTML = async (agencyKey, timetablePage, config) => {
378+
exports.generateHTML = async (timetablePage, config) => {
366379
const stats = {
367380
stops: 0,
368381
trips: 0,
@@ -378,7 +391,7 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
378391
// Format Timetables
379392
await Promise.all(timetablePage.timetables.map(async timetable => {
380393
const query = {
381-
agency_key: agencyKey,
394+
agency_key: timetablePage.agency_key,
382395
start_date: {$lt: timetable.end_date},
383396
end_date: {$gte: timetable.start_date}
384397
};
@@ -401,20 +414,19 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
401414
}
402415
});
403416

404-
const calendars = await gtfs.getCalendars(query);
405-
if (calendars.length === 0) {
417+
timetable.calendars = await gtfs.getCalendars(query);
418+
if (timetable.calendars.length === 0) {
406419
throw new Error(`No calendars found for start_date=${timetable.start_date}, end_date=${timetable.end_date}, timetable_id=${timetable.timetable_id}`);
407420
}
408-
// Get Calendars
409-
timetable.calendars = formatters.formatCalendars(calendars);
421+
410422
timetable.serviceIds = _.map(timetable.calendars, 'service_id');
411423
timetable.serviceIds.forEach(serviceId => {
412424
stats.serviceIds[serviceId] = true;
413425
});
414426
stats.routeIds[timetable.route_id] = true;
415427
timetable.dayList = formatters.formatDays(timetable);
416428
const routes = await gtfs.getRoutes({
417-
agency_key: agencyKey,
429+
agency_key: timetablePage.agency_key,
418430
route_id: timetable.route_id
419431
});
420432

@@ -424,7 +436,7 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
424436
timetable.route = routes[0];
425437

426438
timetable.trips = await gtfs.getTrips({
427-
agency_key: agencyKey,
439+
agency_key: timetablePage.agency_key,
428440
route_id: timetable.route_id,
429441
direction_id: timetable.direction_id,
430442
service_id: {
@@ -440,7 +452,7 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
440452
stats.trips += timetable.trips.length;
441453

442454
const timetableStopOrders = await gtfs.getTimetableStopOrders({
443-
agency_key: agencyKey,
455+
agency_key: timetablePage.agency_key,
444456
timetable_id: timetable.timetable_id
445457
});
446458

@@ -452,7 +464,7 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
452464
trip.dayList = formatters.formatDays(trip.calendar);
453465

454466
const stoptimes = await gtfs.getStoptimes({
455-
agency_key: agencyKey,
467+
agency_key: timetablePage.agency_key,
456468
trip_id: trip.trip_id
457469
});
458470

@@ -465,38 +477,17 @@ exports.generateHTML = async (agencyKey, timetablePage, config) => {
465477

466478
return trip;
467479
}));
480+
timetable.orderedTrips = sortTrips(trips);
468481

469482
timetable.stops = getStops(timetableStopOrders, longestTrip.stoptimes);
470483
stats.stops += timetable.stops.length;
471484

472-
if (!trips || trips.length === 0) {
473-
throw new Error(`No trips found for timetable_id=${timetable.timetable_id}`);
474-
}
475-
476-
timetable.orderedTrips = sortTrips(trips);
477-
478-
await processStops(agencyKey, timetable, config);
485+
await processStops(timetable, config);
479486

480487
timetable.specialDates = await getSpecialDates(timetable);
481488

482489
if (config.showMap) {
483-
const shapesGeojson = await gtfs.getShapesAsGeoJSON({
484-
agency_key: agencyKey,
485-
route_id: timetable.route_id,
486-
direction_id: timetable.direction_id
487-
});
488-
489-
const stopsGeojson = await gtfs.getStopsAsGeoJSON({
490-
agency_key: agencyKey,
491-
route_id: timetable.route_id,
492-
direction_id: timetable.direction_id
493-
});
494-
495-
const geojson = await geojsonMerge.merge([shapesGeojson, stopsGeojson]);
496-
timetable.geojson = await simplifyGeoJSON(geojson, config.coordinatePrecision);
497-
timetable.hasShapes = _.some(geojson.features, feature => {
498-
return feature.geometry.type === 'LineString';
499-
});
490+
timetable.geojson = await getTimetableGeoJSON(timetable, config);
500491
}
501492
}));
502493

@@ -567,20 +558,22 @@ exports.getTimetablePage = async (agencyKey, timetablePageId) => {
567558
const directionId = parseInt(parts.pop(), 10);
568559
const routeId = parts.join('_');
569560
const routes = await gtfs.getRoutes({
570-
agenct_key: agencyKey,
561+
agency_key: agencyKey,
571562
route_id: routeId
572563
});
573564

574565
if (routes.length === 0) {
575566
throw new Error(`No route found for route_id=${routeId}`);
576567
}
568+
569+
const route = routes[0];
577570
const directions = await gtfs.getDirectionsByRoute({
578571
agency_key: agencyKey,
579-
route_id: routeId,
572+
route_id: routeId
580573
});
581574

582575
const direction = _.find(directions, direction => direction.direction_id === directionId);
583-
timetablePages = await convertRouteToTimetablePage(agencyKey, result, direction);
576+
timetablePages = [await convertRouteToTimetablePage(route, direction)];
584577
} else if (timetablePages.length === 0) {
585578
// If no timetablepage, use timetable
586579
const timetable = _.find(timetables, {timetable_id: timetablePageId});

0 commit comments

Comments
 (0)