Real-time BART Commute Mapping, Part 2: Building a Web Map

In a previous article we discussed the General Transit Feed Specification (GTFS), and saw how Bay Area Rapid Transit (BART) supplied information to applications through APIs available at http://api.bart.gov. We showed how to indicate the command to execute, and the arguments to be supplied to the functions. We presented the format of the station data and the Estimated Time of Departure (ETD) data.
We reviewed sample functions to retrieve the data and display it in a theoretical Express.js application. Today we’re going to make use of those functions in a real application. The application will show information about the BART system on a TomTom map with just a few lines of code.
You can use the following links to review GTFS, the BART program, and the APIs:
- https://developers.google.com/transit/gtfs
- https://www.bart.gov/about/developers
- http://api.bart.gov/docs/overview/index.aspx
You can find information about the TomTom developer program and the SDK at
https://developer.tomtom.com/.
What You’ll Need
To build the application, you’ll need Node.js and the Express builder. After the app is created, we’ll use Visual Studio Code to edit. Follow the links below and make sure you have these installed:
Set Up the App
Use the express command in a console window to set up the basic Express app:
The “--view pug” switch tells the Express builder that we want to use the pug view engine. (See https://pugjs.org/api/getting-started.html for details.)
The next step is to install all the dependencies. Change directory and use npm to do so:
This will take a minute or two. If you see a note about security vulnerabilities, you can usually fix them by running “npm audit fix”:
Now that the basic framework of the app is ready, you can open VS Code to do the editing. Just type “code .” in the window to open the current folder.
We’re going to create a JavaScript file in the javascripts folder, which is where we’ll put our API calls for BART. For simplicity, we’ll put all the other code in the index.pug file. We’ll use the layout.pug file for the links to the supporting stylesheets and JavaScript files for BART and for TomTom. If that seems like a lot, just copy the folder arrangement shown below.
Downloading the TomTom SDK is the next step in getting the app to run and actually show a map.
Go to https://developer.tomtom.com/maps-sdk-web to access the SDK. If you don’t already have a TomTom account, see https://developer.tomtom.com/user/register. You’ll need to register to obtain an API key in order for your map to display.
In the code you receive from the SDK, there will be placeholders for the API key, which you’ll simply replace with your own real key.
Get the TomTom SDK
Download the TomTom SDK from https://developer.tomtom.com/maps-sdk-web/downloads. We’re going to use the vector maps version of the SDK, so click this link:
Extract the SDK into a new folder and add “sdk” to the end:
Drag the sdk folder into the public folder in the VS Code project explorer to import it:
We’ll import the SDK and other files using the shared layout.pug file. Double-click layout.pug and make it look like this:
1doctype html2html3 head4 title= title5 link(rel='stylesheet', href='/stylesheets/style.css')6 link(rel='stylesheet', href='sdk/map.css')7 script(src='sdk/tomtom.min.js')8 script(src='javascripts/bart.js')910 body11 block content
Our next step is to create the file named “bart.js,” so close layout.pug now.
Code the API
The bart.js file will be our link to the BART APIs. Right-click the javascripts folder and create a new file named bart.js.
First, we’ll get the station details. As we learned in part 1, the API is a set of endpoints that return either xml or json. We’re going to create asynchronous functions that use the “fetch” command to make the call.
Getting Station Data
1async function getAllStations() {2 let response = await fetch('http://api.bart.gov/api/stn.aspx?cmd=stns&key=MW9S-E7SL-26DU-VV8V&json=y');3 let data = response.json();4 return data;5}
The api call is “http://api.bart.gov/api/stn.aspx”; the command that we run is “stns”; the key we provided is the public key that doesn’t require registration, “MW9S-E7SL-26DU-VV8V”; and the last argument tells the API that the return should be in json format. We “await” the return from the API call and then use the response.json function to format the data and return it to the caller.
API Returns
Let’s take a look at what comes back from the API so we know what we’ll be doing with it.
Refer to part 1 for more details on this. We’ll be using data.root.stations.station for the array of stations in the app.
Coding the UI
Now let’s return to our code and take a look at calling the API from a page. We’re going to be using index.pug for everything here, for simplicity. Double-click index.pug.
1extends layout23block content4 h1= title5 p Welcome to #{title}
The “extends layout” line means layout.pug is automatically included at the top. Pug files use indentation to create hierarchy on a page. This page includes the title as an h1 element and the ‘Welcome to #{title}’ in the content of the page.
Adding Page Elements
Let’s add some more visual elements to the page. We’ll want a button to make the call to get the station info, and a way to tell the user that it’s done. We’re also going to need the TomTom map. This is how we’ll start:
1div23 p45 button(onclick='getStations()') Get Stations67 span(id='gotStations')891011div(id='main' class='container')1213 div(id='map' style='height: 100%; width: 100%;')
Note the indentation. We created a div and a paragraph for the button and span with id=’gotStations’, and another div with class ‘container’ to hold the map. The TomTom SDK sets up the map using the element with id=’map’.
We’re going to need some variables to store information—a list for the stations and a quick-lookup dictionary. To create a script in the same .pug file we use the script tag, and everything indented under that tag will be emitted to a <script></script> element:
1script(type='text/javascript').2 var stationList;3 var stationDict = {};4 var map = tomtom.map('map', {5 key: '<YOUR_TOMTOM_API_KEY>',6 source: 'vector',7 basePath: '/sdk',8 center: {lon: -122.3000, lat: 37.8000},9 zoom: 1010});
Now, we’ll move to editing layout.pug to include the tomtom.min.js file, which gives us access to the tomtom object. This is what we’ll use to call all the TomTom-related functions. In this case, it’s ‘map’, which creates the map object using the element with id=’map’. See maps-sdk-web/documentation#L.Map for details.
We’ll need to adjust the css for the ‘container’ class and for the markers we’ll use on the map. Double-click “stylesheets/style.css” and add the following at the bottom:
1.icon {2 background-size: cover;3 height: 30px;4 width: 30px;5}67.container {8 position: relative;9 width: 95%;10 height: 600px;11}
Adding Code
It’s finally time to create the code to use the BART API. Return to the index.pug file and place this code below the variable declarations. Make sure to indent it so it’s within the code block below the variables.
1function getStations() {2 getAllStations()3 .then(function(data) {4 stationList = data.root.stations.station;5 dictStations();6 markDone('station');7 });8}
Let’s explore this…
getAllStations()
We call getAllStations in the bart.js file. It’s an async function and there’s an implicit return of a promise so we can handle it when it’s done with “then:”
1.then(function(data) {23 stationList = data.root.stations.station;
That’s where we get the array from the return and store it in our local variable. In a production app, we’d certainly add testing to this pipeline.
1dictStations();23markDone('station');
This is a call to a function that turns the list into an object (not yet written), followed by a call to markDone(‘station’), which will display a message that the stations were loaded.
Now let’s add those functions. The dictStations function goes like this:
1function dictStations() {23 if (!stationList) {45 return;67 }89 stationList.forEach(function(item) {1011 stationDict[item.abbr] = item;1213 });1415}
This simply creates references in the stationDict object keyed on the unique station.abbr. This allows us to quickly obtain any station without having to search through the list.
Next comes the “markDone” function. We’re going to add “done” to the span next to the button to indicate that the stations were loaded.
1function markDone(type) {23 if (type === 'station') {45 var marker = document.getElementById('gotStations');67 while (marker.firstChild) {89 marker.removeChild(marker.firstChild);1011 }1213 marker.appendChild(document.createTextNode(" done"))1415 }1617}
First Run
Click the Debug button, then click Add Configuration:
You should be able to select Node.js: Launch Program from a list, and when you’re done it should look like this:
1"version": "0.2.0",23"configurations": [45 {67 "type": "node",89 "request": "launch",1011 "name": "Launch Program",1213 "program": "${workspaceFolder}inwww"1415 }1617]
You can then press F5 to build and run the application. This will start up Node and load the program in the test server. Launch your browser and load http://localhost:3000/ to view the application.
After you click the “Get Stations” button, you should see the word “done” appear next to the button in your browser:
Now that we know that the stations have been loaded, let’s place a few markers on the map.
Adding Map Markers
Review the image below as a quick refresher on the data that’s returned from the BART API:
The layout shows the data we want is root.station[x].etd[y].estimate[z]. There are three estimates from the LAKE station to the DALY station. The GREEN line on platform 2, in 4 minutes, the BLUE line on platform 2 in 11 minutes, and the GREEN line again on platform 2 in 19 minutes.
Getting ETD Data
We’ll add functions to bart.js to get both the full list of stations and the list for just a particular station:
1async function getAllEtd() {23 let response = await fetch('http://api.bart.gov/api/etd.aspx?cmd=etd&orig=ALL&key=MW9S-E7SL-26DU-VV8V&json=y');45 let data = response.json();67 return data;89}1011async function getEtdForStation(station) {1213 let response = await fetch(`http://api.bart.gov/api/etd.aspx?cmd=etd&orig=${station}&key=MW9S-E7SL-26DU-VV8V&json=y`);1415 let data = response.json();1617 return data;1819}
Now go back to index.pug and place a button with a marker for the page to get the data:
1button(onclick='getEtds()') Get Estimates23span(id='gotEtds')
Add in variables, to hold the etd info and an array for keeping track of the markers we place:
1var etdList;23var markers = [];45Add in a page function to get all the etd info (place it under the code we wrote earlier):67function getEtds() {89 getAllEtd()1011 .then(function(data) {1213 etdList = data.root.station;1415 markDone('etd');1617 });1819}
Here’s a challenge… add code to the markDone function to handle the ‘etd’ case. Bonus if you refactor it so you don’t have to replicate code for each type. You can then run this to see the “done” message get set for the ETDs.
Adding Station Markers
We want to show two kinds of markers on the map: markers for stations and markers for the estimated departures. We’ll need the location, an icon, and a message to display on click. Let’s start with a function to place a marker:
1function addMarker(location, icon, message) {23 let marker = new tomtom.L.Marker()45 .setIcon(icon)67 .setLatLng(location);89 if (typeof message !== 'undefined' && message.length > 0) {1011 marker.bindPopup(message);1213 }1415 marker.addTo(map);1617 markers.push(marker);1819}
TomTom draws some functionality from the open-source Leaflet library, which is available via tomtom.L. The marker is one such item. You can find detailed information on markers at leafletjs.com/reference-1.3.0.html#marker. Marker is a fluent API so we can chain to the .setIcon(icon) and .setLatLng(location) functions.
If we don’t get a message, we don’t want an empty box — so, Marker first checks to see if anything’s there. In the last two lines we add the marker to the map and store a reference in the markers array.
Now let’s call addMarker and show the stations on the map:
1function showMarkersForStations() {23 stationList.forEach(function(station) {45 addMarker(L.latLng({lat: station.gtfs_latitude,67 lng: station.gtfs_longitude}),89 L.icon({iconUrl:'/sdk/images/ic_map_poi_005-black.png'}),1011 'Station name: ' + station.name + ', abbr:' +1213 station.abbr + ' Lat Lng: ' +1415 station.gtfs_latitude + ':' + station.gtfs_longitude);1617 });1819}
We traverse the stationList with forEach and call the addMarker function to create the location, the icon, and the message as needed. See leafletjs.com/reference-1.3.0.html#icon and leafletjs.com/reference-1.3.0.html#latlng for details.
We create LatLng using the latitude and longitude from the station. The icon uses the url to get an image in the SDK. And we pass in some information from the station for the message argument.
Now we need another button to show the markers for the station. We want this in a separate paragraph, so just under the button and the span for etd, add this:
1p23 button(onclick='showMarkersForStations()') Show Stations
Be careful to indent this inside the div and a sibling to the existing p. Rerun the application and click the Show Stations button, and you should see this:
Now we’re going to add something to clear out the markers and show the markers for the different lines. The lines are referred to by color so we’ll traverse the etd array looking for anything with a matching COLOR attribute.
Adding Train Departure Markers
We have the addMarker function, so all we need to do is pass in a different icon and a different message with the location for the station.
The first step is to create a function that will traverse the etdList. Remember, there’s an array inside an array inside an array to get to the data we want. We want to be able to reuse this for different purposes, so we’ll create a function that accepts a filter function as the only argument:
1function showMarkersFiltered(filter) {23 etdList.forEach(function(item) {45 // refer to the dictionary to get the station info67 let station = stationDict[item.abbr];89 item.etd.forEach(function(etd) {1011 etd.estimate.forEach(function(est) {1213 if (filter(est)) {1415 // get marker location from the station1617 addMarker(L.latLng({lat: station.gtfs_latitude,1819 lng: station.gtfs_longitude}),2021 L.icon({iconUrl:'/sdk/images/ic_map_poi_040-' +2223 est.color + '.png'}),2425 'Departing ' + station.abbr + ' platform ' +2627 est.platform + ' ' + est.direction + 'bound in ' +2829 est.minutes + ' minutes');3031 }3233 });3435 });3637 });3839}
Here we see the showMarkersFiltered function starting from the etdList and traversing the list with forEach. The top level of the etdList is the departure station, so we use item.abbr to get the station object from the stationDict. After that the function drills down into the etd and estimate levels to get through each of the estimated departures. Then we see it using the filter if (filter(est)) {. Note that it passes to the filter function the current “est” item, which is the lowest level in the dimensions—the estimate. We’re calling back to the function that requested this so it can use whatever criteria is needed to show or not show the marker for that estimate.
Before we move on, let's take a look at the line that creates the icon:
L.icon({iconUrl:'/sdk/images/ic_map_poi_040-' + est.color + '.png'})
Note that we use the est.color in the name of the icon. I made copies of the ic_map_poi_040-black.png file to create one for YELLOW, GREEN, BLUE, RED, ORANGE, and PURPLE.
Next, we’ll add in a function to call this and supply the necessary filter callback:
1function showMarkersForEtd(color) {23 showMarkersFiltered(function (est) {45 return (est.color === color);67 });89}
In the showMarkersForEtd function, showMarkersFiltered takes the est object and checks to see if the est.color matches the color argument passed, returning a Boolean to be used in the if statement.
We also need something to clear out the markers; here are the functions for that (paste them in under the rest of the JavaScript, remembering to indent):
1function clearMarker(marker) {23 map.removeLayer(marker);45}67function clearMarkers() {89 if (markers.length > 0) {1011 markers.forEach(function(item) {1213 clearMarker(item);1415 });1617 markers = [];1819 }2021}
Finally, we need buttons for clearing the markers and to call the showMarkersForEtd function, so we’ll add these in the group with the Show Stations button. This is what it should look like now…
1p23 button(onclick='clearMarkers()') Clear Markers45 button(onclick='showMarkersForStations()') Show Stations67 button(onclick='showMarkersForEtd("YELLOW")') Show Yellow
Our new button is going to call the function to show the markers for the YELLOW line. Try it out, and this time click on one of the flags. You should see something like this:
Irregular clip for article only, actual image will be the full map.
To finish up, we just add more buttons:
1p23 button(onclick='clearMarkers()') Clear Markers45 button(onclick='showMarkersForStations()') Show Stations67 button(onclick='showMarkersForEtd("YELLOW")') Show Yellow89 button(onclick='showMarkersForEtd("RED")') Show Red1011 button(onclick='showMarkersForEtd("BLUE")') Show Blue1213 button(onclick='showMarkersForEtd("GREEN")') Show Green1415 button(onclick='showMarkersForEtd("ORANGE")') Show Orange
This will show the stops for each color line.
Wrap-Up
In this exercise we:
- Reviewed GTFS and GTFS-RT.
- Reviewed the data provided by the BART API for stations and for ETDs.
- Built a very simple Express.js app and imported the TomTom SDK.
- Displayed the TomTom map and showed the locations of stations with just a few lines of code.
- Built a system for showing the estimated time of departure (ETD) for BART stops.
You can extend this app by adding any number of criteria you wish to the Boolean function used in the showMarkersFiltered function. The sample application includes a method for showing all items within a selected time period. Combine that with the color to see where you can catch the yellow line in the next 10 minutes. If you’d like to add advisory, schedule, or route information, you can get that information from other APIs athttp://api.bart.gov/docs/overview/index.aspx.
For more information on the TomTom APIs, use the Documentation menu on the TomTom Developer Dashboard at https://developer.tomtom.com/.