GTFS Scheduling Website
The end goal of this project is to provide an easily deployable transit schedule explorer that will ingest any transit agency's gtfs and gtfs-rt feeds and produce a functioning website that is easy to maintain and useful for riders. TODO: This site was originally built for the Whatcom Transportation Authority and we are working to make it agency configurable by means of a config.json file. TODO: Write deployment guide to set up github actions, secrets, and AWS resources.
- Clone the repository
- Run
npm installto install dependencies
To clean content from previous builds, run ./clean.sh. This will ensure that all content is up to date when site gets built.
To build site locally, run npm run build. Statically built content will be located in /_site
To start local development environment hosted at run npm run start. This will start a server running at http://localhost:8080/
- Run cleanup script (to remove copies of files so that tests don't get run twice)
- Run
npm run test.
To test with code coverage option, instead run npm run coverage. This will print out a code coverage report, and it will also generate the /coverage directory. A report is saved to /coverage/lcov-report/index.html, which can be opened in a browser to see a breakdown of the tests.
package.jsonstores node scripts and dependenciespackage-lock.jsonstores more dependency data.eleventy.jscontains the eleventyConfig to set up the eleventy buildsrc/contains all the code for the sitecss/style.csscontains the css rules for the majority of the sitemain.cssandnewstyle.csscontain css rules for the provided WTA header and footer
data/contains code to generate data files at site build timestart.jsdefines constants and calls scripts from other data filesload_static.jsfetches static GTFS data from zip and saves itgenerate_stops_kml.jsgenerates a KML file containing every stop with its stop code and name
img/contains images for siteincludes/contains html code for sitelayouts/base.njkis the main html file defining the structure of the site, as well as loading local scripts and the Google Maps Javascript APIpartials/header.njkcontains html for headerfooter.njkcontains html for footer
scripts/contains the code for the main functionality of the sitetests/contains all the test scripts for the corresponding scriptsmain.jscontains constants and global variables, as well as handling cross-page functionalitydates.jshandles formatting and processing of date-related datamaps.jshandles setup and functionality of the Interactive Map page, including the Google Maps searchrt.jshandles the fetching, processing, and displaying of real-time data on the Route Details pageutils.jscontains utility functions for working with GTFS static dataroutes.jscontains functionality for the Routes landing pageroute-details.jscontains all the functionality for the Route Details page, except the real time datastop-details.jscontains all the functionality for the My Stop pagetrip-planner.jscontains all the functionality for the global Trip Planner
.github/workflows/contains GitHub action scripts that run when code is pushedbuild-test-deploy.ymlbuilds site, runs test suite, and pushes to the test site
When the site is built using the npm run build command, 11ty will run the start() script located in src/data/start.js. This file declares a number of constants containing links and paths to fetch from and store to. Then the start() script will generate the stops kml, and then load any GTFS static data necessary.
In order to integrate schedules from external transit authorities, the following steps must be taken in start.js:
- Declare a constant containing a url to an API from which a zip folder containing all GTFS static data can be fetched (ex: SKAGIT_STATIC_URL)
- Declare a constant containing a path to a directory to store unzipped folder from step 1 (ex: SKAGIT_STATIC_DIR)
- Import a JSON file containing the following (see
src/data/skagit.jsonas an example):- Routes to import
- Stop ID conversions (mapping external stop_id to the corresponding interal stop_id)
- Stop Code conversions (see previous step)
- In
start(), Load external GTFS Static data usingget_external_gtfs_static_data(url, dir, routes, stop_id_map, stop_code_map, name)
When the site is loaded in the browser, the function main() in src/scripts/main.js will be the first thing to be called. This function does the following:
- Run initialization script
siteInit()which will:- Initialize the interactive map
- Set up global date object
- Set up Geocoder for stop page City & Zip
- Load Skagit JSON object
- Call
initRoutes()which will:- Load the routes GTFS data
- Call
create_routes_table()to build the routes table
- Call
tripPlannerInit()to initialize trip planner - Call
stopsInit()which will:- Load stops and stop time GTFS data
- Build necessary stop data objects
- Call
tripsInit()which will:- Load trips GTFS data
- Resolve GTFS merge (if necessary)
- Call
datesInit()which will:- Load calendar and dates GTFS data
- Call
reload()to set up site based on URL
main.js also declares most global variables for use throughout the site.
The Routes page is the simplest page on the site. The functionality, contained in src/scripts/routes.js consists of a single function, create_routes_table() which takes in the div to place the routes table and a sorted array of all the routes. All this function does is loops through each route and creates a row containing the route name and number, which it puts into the div. Lastly, it adds a clickable function so that when the row is clicked, the site will navigate to the route details page for that route.
The Trip Planner is a persistent sidebar that stays visible as the user navigates on the site. The functionality, contained in src/scripts/trip-planner.js, allows the user to quickly navigate across the website if they know what they are looking for.
The function setupTProutes() focuses on populating the select element with all the routes id and the click and press functionality to allow the user to view additional information on said selected route.
The function setupTPstop() focuses on allowing the user to enter a particular stop ID or address and then taking them to the repective page at a touch of a click or key press.
The function setupTPInteractiveMap() focuses on adding functionality to the button associated with the iteractive map section of the trip planner, taking the user to the interactive map at a click of the button.
The function setupTPgoogleForm() focuses on setting up the associated inputs and buttons associated with the plan trip. The function sets up the both the start and end input elements to have Google Maps API autocomplete when the user inputs a street. The function also sets up the "use my current location" button to paste the user current location to the associated input element (if permissions are enabled). Lastly, the function sets up the plan trip button which takes both the start and end input values (if applicable) and append these values to a Google Maps URL. After the appending process the user will then be sent to a newly created tab where there start and end addresses are pre-inputed (if applicable).
Functionality for the Route Details page is located in src/scripts/route-details.js. When a user navigates to the Route Details page, the function display_route_details() will set up the page, including buttons and calendar, and then call make_route_details_for_day() to build the timetable. This function will find the corresponding trips for this route using get_trips_by_route(), and then for each trip direction, create the timetable by calling create_trips_table(), initialize the real-time functionality with update_route_details_rt(), and set up and interval to call it repeatedly after a period of time using setInterval(). Finally, make_route_details_for_day() will display the default direction timetable by calling display_current_direction().
When the Route Map tab is selected, the function display_route_map() is called which will display the map.
The workflow for the My Stop page is similar to the Route Details page. The functionality for this page is located in src/scripts/stop-details.js. When a user navigates to the My Stop page, the function display_stop_details() will set up the page, including buttons, calendar, and stop address, and then call make_stop_details_for_day() to build the timetable. This function will get all the trips for this stop with get_trips_by_stop_id(), and then build the timetable with create_stops_table().
When the Street View tab is selected, the function display_streetview() is called which will display the street view.
The functionality for the Interactive Map page is located in src/scripts/maps.js. When the site is initialized, initMap() is called. This function sets up the Google Map by calling setup_map(), centers the map to the device location (if permissions allow) with center_map(), sets up the stops kml layer with setup_stops_layer(), sets up the routes kml layer with setup_routes_layer(), and finally sets up Google Autocomplete with setup_autocomplete().
Functionality for real-time data is located in src/scripts/rt.js. The following global constants are defined at the top of the file:
- RT_API_URL: the url for the real-time API
- EARLY_COLOR: the color for the real-time text when the bus is predicted to be early
- LATE_COLOR: the color for the real-time text when the bus is predicted to be late
- ON_TIME_COLOR: the color for the real-time text when the bus is predicted to be on time
The only function that is called externally is update_route_details_rt(), which is called in src/scripts/route-details.js in the function make_route_details_for_day(). The update_route_details_rt() function will first make an API call to retrieve the real-time data using the function get_predictions_for_route(). This function will fetch the response from the API.
The get_predictions_for_route() function needs to check if the requested route contains trips from an external transit authority. Because if so, it will need to convert the stop ids from the external to the internal version of these. This is done using the external JSON file set up earlier, see above.
Checks are in place to detect when the GTFS static dataset is merged, in order to handle this special case accordingly. The merge will be detected in src/scripts/main.js in tripsInit(), which calls is_merged() in src/scripts/utils.js to detect a merge. A merge is detected by the inclusion of the key original_trip_id in the trips.txt file. If there is a merge, tripsInit() will call make_merged_trip_mapping() in src/scripts/utils.js to make a mapping from original_trip_id to trip_id. This is necessary because the real-time data will give the original_trip_id (although it will call it trip_id), so it is necessary to convert it in order to find the element in the route details timetable.
Thus, in src/scripts/rt.js, update_route_details_rt() will call getTrueTripId(), which will return the correct trip id, regardless of a merge or not. If there is no merge, getTrueTripId() simply returns the trip_id. If there is a merge, it will check the merged trip mapping.