Convert Google Maps Directions to geoJSON with Node
When Freya (my wife) and I gave up a permanent home in early 2013 to live nomadically for a while (which we did until September of this year), one of our goals was to keep a blog on the trip of all the things we did, the sights we saw, the roads we traveled.
We were very unsuccessful in that goal. So now that we’ve settled down I’ve made it a personal goal to build a site to record the trip in hindsight. We didn’t fulfill the goal as we traveled, but we can do it now!
With that in mind, yesterday I sat down at my computer to start working through this tutorial to make some maps of the routes we undertook on our trip using D3.js. A few paragraphs into the tutorial, it became clear that I needed some geoJSON to work with. I ended up spending the day building the geoJSON files of all our routes, and I thought I’d document the file I ended up with that takes a list of cities and turns them into a geoJSON dataset of road paths.
geoJSON & Google Maps
GeoJSON is a format that helpfully allows you to encode geographic data as JSON, which can then be used in a variety of means. There are quite a few tools that will help you export an annotated map as geoJSON and such, but I spent awhile searching for a way to take a driving route and turn it into a geoJSON, and I couldn’t find it. Thankfully, there were a few node packages that helped me out.
The format is pretty standard, but it’s not used by Google Maps. I potentially could have used other mapping tools to get the road routes, but the Google Maps API is pretty convenient to use, so I decided to go with the path of least resistance.
To achieve my goal, I relied on 3 node modules:
- googlemaps: a handy Node wrapper for the GMaps API
- polyline: a tool to decode GMaps polyline data into an array of Lat/Lng data made by Mapbox
- geojson: which takes regular JSON and converts it into geoJSON
I also used the es6-promise package because I love Promises when dealing with async code.
Step 1: Get the Google maps Route
Here’s the code:
The first thing you’ll notice is that I’m passing in an apiKey, you can get yours from Google. Next, I’m creating a function to get the route information from Google, which takes 3 arguments:
Destination are required, thus the if statement that logs an error if they are missing.
getGoogleRouteInformation() returns a Promise, which allows me to string together all the steps into a very neat, human readable format. If you don’t know much about Promises, I collected a list of great reading material that can help you get up to speed. In this script, I’m using the Promises
thenable functionality to pass the data along, which you’ll see in the next section.
After confirming the required arguments are present, I build a small function to resolve or reject the promise once the data is loaded, so that I can have a little help in debugging if things go awry.
The last line of the function is the actual call to Google, and it uses the
googlemaps node module, which itself has a slightly confusing API. If you dig into the module, you’ll see that
directions() takes an argument for each possible attribute of the Google Directions API, but the documentation doesn’t make it clear how to format the args. After a bit of playing around and Googling, I was able to get things correctly. Just so you know, the function call for
directions() looks like this in the Module:
And each argument is just expecting a preformatted string. For the waypoints, a single string is passed in with each waypoint separated by a pipe:
Nashville, TN|Dallas, TX|Amarillo, TX.
Step 2: Error Handling
I’ve been working on Ember apps with one company for a few months now and I learned from a coworker recently that sometimes when dealing with Promises you’ll get silent failures if you aren’t always catching errors. Because of that, I wanted to make sure I was aware of anything that went wrong in our chain. A promise’s
.then() function takes two arguments, the first a success handler, the second an error handler. So
handleError() is a compact function to be passed into every
then() in case something goes wrong.
Step 3: Decode Google Polyline Data
getGoogleRouteInformation(), I can start building out the chain that will process the resulting data.
Google Maps returns a large JSON object with all the necessary information for displaying a map on a page, but for my purpose, I only care about the
overview_polyline object in the JSON. This is an encoded polyline object that represents the road route - the blue line you’re used to seeing if you’ve ever looked up driving directions on Google Maps.
Polyline is a small tool by Mapbox that decodes the polyline data and returns an array of arrays, with each child array containing a latitude and longitude point. This array is what I’ll manipulate next.
Step 4: Normalize the decoded Polyline Data
Before I can take the geo data and convert it to geoJSON, it needs to be standardized into an object that the
geojson object can understand. To do that, I just loop through the arrays, build a new object, and push that object into a new array.
The decoded polyline data is an array where the first value is the latitudinal point, and the second is the longitudintal point.
Geojson expects an object with standardized keys, so I create that inside this loop. After looping through all the data, I can return the new array of objects.
Step 5: Create the geoJSON
Geojson is an elegant module that exposes one function:
parse(). I use it to take our new array and convert it to a geoJSON object like so:
normalizedPoints, the array of objects, is passed to
parse() with the instructions to create each Point from the keys
lng. The variable
geoData is the data I’m after.
Step 6: Write to a file
Node has a core api called
FS that allows you to work with the FileSystem. This last step uses it to write out the
geoData file so I can store it in a repo. This step was made easier by this post on Stackoverflow.
output is a variable set at the top of the file, and it is used to define the filename. The data is written out using
JSON.stringify() so that I get the actual object data, and not just
[Object object]. If everything goes according to plan, Node will print a success message, otherwise
.catch() will handle the error.
I also found it very helpful to test the geoJSON output with this viewer while I was debugging the Google Maps Waypoints, a simple copy/paste showed the points on a map so I could see if my route was correct.
Hopefully this article was helpful, please let me know if you have feedback, optimizations, or alternatives that I should have considered!