By: Raj Singh
Raj is a Developer Advocate for Watson and Cloud at IBM. He specializes in data science and all things geospatial. Raj pioneered Web mapping-as-a-service in the late 1990s with Syncline, a startup he co-founded and worked on geospatial data interoperability challenges for the Open Geospatial Consortium. He has a PhD in Urban Planning from MIT.
At the Consumer Electronics Show (CES) 2018 in January, IBM and Local Motors debuted #AccessibleOlli — the future of public transit that adapts to your particular accessibility needs. Whether hampered by a sight or hearing impairment, or a physical or cognitive disability, we showed how autonomous vehicles and the bus stops of the future can make your journey better.
Olli is an all-electric, partially 3D-printed, self-driving bus from Local Motors with a cognitive rider experience delivered by Watson and the IBM Cloud. Local Motors partnered with IBM to make their autonomous vehicle smarter, and IBM chose Mapbox’s location data platform to deliver maps and directions because of the ease with which it integrated into our systems.
In this post, I’ll share how we used the GL JS to show bus movement throughout the CES conference and how you can build it yourself. While this is just a use case, it shows the extent of how Cloudant + Mapbox can be used for real-time mapping of any kind of moving geographic features. This could be a bus or any IoT architecture where data is involved.
Let’s build a web app using GL JS to display a moving bus icon the map. We’ll then continuously update the location of the bus by “listening” for changes to a CouchDB database, which is in turn being updated by a separate bus movement simulation program that we’ll walk through next (in real-life the GPS on the bus would be sending location update messages to the database).
Setting up
Before digging into the code, let’s set up our database. We’ll use Apache CouchDB™ for this project for two reasons: First, it is hands-down the best database for propagating incremental changes to client apps. Second, its native data format is JSON, allowing us to store the exact format we want for mapping — GeoJSON — in the database. No transformations are needed. We just “listen” for change documents (new feature locations) in the database, and put them on the map.
NOTE: IBM Cloudant is the managed, hosted version of CouchDB. When you go to production, or if you simply don’t want or need to run locally, log into the IBM Cloud and provision a Cloudant NoSQL DB Lite Plan instance, which is free.
Install Apache CouchDB
- Install CouchDB 2.1.
- Configure CouchDB for a single-node setup, as opposed to a cluster setup. Once you have finished setting up CouchDB, you should be able to access CouchDB at http://127.0.0.1:5984/.
- Ensure that CouchDB is running and take note of your admin username and password
Setup the database
- Create the database
- In the Cloudant or CouchDB dashboard, select the Databases tab on the left
- Use the `Create Database` button to create a database called “ollilocation”.
2. Enable Cross-Origin Resource Sharing (CORS)
- Select the Account Settings (Cloudant) or Config (CouchDB) tab and then select the CORS tab.
- Enable CORS and restrict the domain as needed for security.
Setup NodeJS
- Install NodeJS
- There is no step 2
Getting the code
Head to GitHub to download the code for this project:
- Go to https://github.com/ibm-watson-data-lab/olli-simple-map.
- Clone the repository using GitHub Desktop app or from the command line by typing `git clone git@github.com:ibm-watson-data-lab/olli-simple-map.git.`
- Install required node modules from the command line by cd-ing into olli-simple-map and typing `npm install`.
Simulating moving objects
The first thing we need to do is simulate a moving object. In a real-life IoT scenario, you will have things out in the world (vehicles, drones, people, etc.) recording their location and sending a stream of data to your back-end infrastructure. In the #AccessibleOlli booth at CES, we had various items being tracked:
- Visitors moving in and out of the bus stop and in and out of the bus itself.
- Status of hardware: lights, audio equipment, signage, etc.
- The “virtual” location of three buses.
All these data points were constantly written to CouchDB for use by various applications that needed to act on particular changes of state that they had an interest in. For the purposes of this article, however, we will only simulate a single bus moving along a fixed route in Rochester, Minnesota — our study area.
The data is stored in a single GeoJSON file called route.json. It serves as our “virtual bus”. It contains an array of Point features specifying the bus location at a particular point in time.
What we’re going to do in this code is connect to the database and write a feature at a time, pausing every 500 milliseconds, or a user-specified time if the simulator_event_interval variable is set. We will run through the file once, unless the simulator_number_of_runs variable is set, which specifies how many times to cycle through the simulation. Some features have a property “is_stop” = true. In this case, the bus has arrived at a bus stop and stays there for awhile. This is 2500 milliseconds, or a user-specified time if the simulator_stop_duration variable is set.
NOTE: In the demo we used the timestamp property to sync up the locations of multiple buses with other events that needed to know where they were, and we used the heading property to face the bus the right way in another app that drove a first-person point of view virtual reality visualization. We’ll ignore those data properties for the purposes of this demo.
Remember the CouchDB (or Cloudant) database you set up earlier? Now we’ll take a feature out of the route.json file periodically and write it to the database. This is done in the `simulator.js` code. Let’s take a quick look at that NodeJS application.
First we bring in in configuration file reading utility, ‘dotenv’ a CouchDB/Cloudant client helper library, ‘cloudant’, and the route data, route.json. Then we initialize a configuration object called config with parameters we’ll need later.
Go to the end of the file and take a close look at this line, which is actually the first command to be executed at runtime:
initCloudant().then( () => simulate(0) )
This starts off the simulation by setting up the database connection by calling initCloudant. Communication with the database is asynchronous, and we don’t want to try to do anything else until we’re sure that connection has been made, so we make initCloudant return a Promise. When that Promise returns successfully we get dropped into “then”, where we can do the next thing. Now it’s safe to start the simulation, so we call simulate with a value of 0.
Back up at the top of the file, the simulate function uses its input parameter, step, as an index into the array of features in route.json (starting over from 0 if the value surpasses the number of features in the array). It grabs this feature from the array, writes it to the database in insertCloudant, then once again waits for the Promise to return before continuing.
After the write is successful, we call a timer function sleepTimer to pause before doing anything else. All that function does is call the JavaScript function setTimeout. But that function is also asynchronous so we wrap it in a Promise so we can signal when it’s done. After that returns, we increment the step value and recursively call ourselves to grab the next feature from the route.
You can run this application now if you’d like. It will write data to the “ollilocation” database, but you won’t see anything graphical until we make a map to show it on. To run the simulator:
- Copy .env.templateto .env and change the simulator_target_cloudant variable to be a user that can write to the database. In an out-of-the-box CouchDB setup both your username and password are admin.
- Did you run npm install when you checked out the repo? If not do that now.
- Run node simulator.js
When the simulator is finished, in the dashboard you should see 6822 documents in the “ollilocation” database, exactly the number of Features in the route.json document.
Running a local web server
We’re going to build a very simple Mapbox GL map that runs as a web page. If you have a web server set up on your computer, you probably know well how to put the `public` directory in this repo in a place that’s visible to your web server. If you don’t, or if you don’t want to mess with it, no worries. We’ve included a basic little NodeJS Express web server in this repo that requires zero configuration. Just run node webserver.js and you’ll be up and running, serving any file in the public directory. Upon execution, you’ll see command line output that looks something like this:
To view your app, open this link in your browser: http://localhost:6001
Pop that URL in your browser and you will see the web page we are building.
Build the map
Doesn’t setup always take about 90% of your time before you get to the fun stuff? Well we’re finally there. I hope you learned a lot of helpful lessons from the earlier steps, but you’re really here for maps. So let’s get to it.
In the web app we’re going to do two things.
- Connect to the database and wait for new documents to be written to it
- Take the new document and use it to update a GeoJSON layer in a Mapbox GL map.
For simplicity, all the code for the web app is in public/index.html. All we have in terms of actual HTML in here is a single <div> that is used to hold the map:
<div id=’map’></div>
Everything else is JavaScript.
The first thing we do is bring in two libraries in the <head> of our <html>. PouchDB for managing the communication with our database, and Mapbox GL for mapping. Now let’s look at the <script> tag, where all the magic is made.
PouchDB is basically a 100% JavaScript implementation of most of CouchDB. It’s often used to synchronize data with a remote database, bringing everything over to the client so your app can run with little to no dependency on the network. We could do that here, but to keep things really simple, we’ll use PouchDB in what I call “mirror mode”. Instead of using PouchDB as an in-browser database we’ll just use it as a helper client library to talk to our remote database. The first two lines of our script sets this up. We specify the REMOTE_DB and initialize an instance of PouchDB. Update the REMOTE_DB constant to match your setup. including username and password and also host and port if you are not running local (i.e., https://username:password@hostname:port/ollilocation). Remember that these credentials are visible to anyone looking at your source code, so if you require a username and password for read-access, those credentials will not be private. CouchDB databases are world-readable by default, but if or you’re using Cloudant, you will have to change the permissions on ollilocation to allow for reading the database without authentication.
Next, specify your Mapbox access token and you’re done with configuration. The rest of the code should work as-is.
Now we can do a standard initialization of a Mapbox GL map, specifying our access token, setting the basemap, zoom and center. Then we use the mapboxgl.Map.loadImage command to get a custom Olli bus icon into the map’s symbol library and give it an ID of ‘olli-icon’. And that’s it for map setup.
Now the exciting stuff happens in just a few lines of code. Since map initialization is an asynchronous process, we can’t do anything to the map until we’re sure it’s ready to go. So the rest of the action is in the map.on(‘load’) callback function. Here we add a GeoJSON layer to the map with an ID of ‘olli-location’. We specify our ‘olli-icon’ as the image to use for symbolization, but we don’t initialize it with any data. The layer is on the map, and it has a symbol to draw point with, but there are no features to draw.
The PouchDB “changes” function is where the data updates happen. Here’s what those configuration parameters mean:
- since: ‘now’: don’t worry about any changes that occurred before you started listening
- live: true: keep listening until you are cancelled
- include_docs: true: return the entire changed document, not just the ID and revision number
When a change occurs, PouchDB calls the on(‘change’) function. That’s where we take the new document, change.doc, and set that as our olli-location GeoJSON layer data.
map.getSource(‘olli-location’).setData(change.doc)
The way we used Mapbox for #AccessibleOlli project was greatly simplified here to more clearly illustrate the core techniques at work. In practice, we did some very sophisticated things with the simulator and the web app to suit our needs. The “real” simulator has an additional option to skip the database writing step and communicate directly with the client’s PouchDB via web sockets, and the client can initiate the simulation instead of requiring a human operator to jump in and hit a “start” button. This is a great setup for impromptu demos as IBM-ers evangelize the work on accessible transit throughout the world.
We built the client app in the React JavaScript framework and used Redux for sending event update messages between components in the app. This was critical for us as the app had a lot going on. It was “reacting” to bus stop entry events, changing the UI based on whether the user was blind, deaf, or in a wheelchair. It was handling sign language display and translation for the deaf. And for those who aren’t deaf, it was talking to visitors using IBM’s Watson Conversation. It did all of this in addition to drawing the real-time location of three virtual Olli buses on the map at all times.
In an industrial IoT setting, more often than not mapping and geospatial visualization are secondary activities. They take a back seat to functions such as data processing for operations management, predictive analytics. Therefore, mapping systems must integrate easily with existing technology infrastructures and not require additional middleware or changes to data structures. Mapbox technology fit into our architecture seamlessly and reliably.
If you’d like to get involved in the #AcessibleOlli project — whether it be from a coding, planning or policy perspective — the process is open to all. Join the conversation at https://launchforth.io/localmotors/accessibleolli/. Or if you’re interested in learning how we integrated Mapbox into our React framework, follow me at the IBM Watson Data Lab Blog.
Mapping moving objects with Mapbox, , and a self-driving bus named Olli was originally published in Points of interest on Medium, where people are continuing the conversation by highlighting and responding to this story.