If you want to take a bunch of GIS data and rasterize it as a tiled image map for public consumption, the folks at ESRI would be happy to sell you an expensive solution. Of course, as with oh-so-many projects, you can accomplish the same thing for free with open-source software. In this case, we'll use Python and a library called Mapnik to render beautiful map layers, then display them on Google Maps, just like this demo rendering of my home county!
Ready to get started? Dust off your Python skills, and let's go!
Mapnik - the basics
Mapnik is a high-powered rendering library that can take GIS data from a number of sources (ESRI shapefiles, PostGIS databases, etc.) and use them to render beautiful 2-dimensional maps. It's used as the underlying rendering solution for a lot of online mapping services, most notably including MapQuest and the OpenStreetMap project, so it's a truly production-quality framework. And, despite being written in C++, it comes with bindings for Python and Node, so you can leverage it in the language of your choice. Cool!
For the purposes of this post, we'll be using Mapnik's Python bindings. After a couple of false starts, I was able to install Mapnik 2.2.0 on Ubuntu via Aptitude and a little help from this Mapnik/Ubuntu install guide, but your mileage may vary. Mapnik has a (seemingly well-deserved) reputation for being challenging to install. Good luck out there.
A first map
Once you get Mapnik installed, it's time to try creating a simple rendering. For testing, I grabbed a freely-available shapefile of the boundaries of Wake County, NC to play around with, but you could use any data source you wanted. For the basic demo, we'll style this shapefile and draw it in a PNG image.
The easiest way to draw a shapefile in Mapnik is to use an XML style document (though you can build styles programmatically too). The one I used is shown below.
Most of the tags in here are super self-explanatory, so I won't bore you with the details. But I will explain SRS. The SRS tag specifies the spatial reference system of your GIS data. You'll need to set the SRS for your world, and for each layer you add to it. If you have layers in different reference systems, Mapnik will automatically transform them into the correct coordinate system for drawing the final map.
So how do you know which SRS to use? Well, in this case, we're using what's know as "Web Mercator" for the map itself, 'cause that's what Google Maps uses, and we're going to put this on a Google Map later. For each individual layer, you need to specify the coordinate system of the layer data itself. You can figure this out pretty easily using the .PRJ meta-data that comes with an ESRI shapefile and spatialreference.org. In the Wake County sample data linked above, the .PRJ file says the coordinate system is "NAD 1983 StatePlane North Carolina FIPS 3200 Feet." If you search for that on spatialreference.org, it will actually provide a ready-to-go Mapnik XML template that includes the appropriate SRS! You can also simply copy the "proj4" formatted SRS string off the site. Couldn't be easier.
So, now we just need to get this thing rendered! The Python code for this is trivial.
And here's our beautiful visualization of Wake County!
Rendering Google Maps tiles
Of course, rendering a picture of a county is one thing. But I promised that we'd get this displayed on Google Maps! That takes a little bit more doing.
Under the hood, Google Maps has divided the world into 256x256 pixel "tiles" and serves up a bunch of PNG files containing images of what's in those tiles (if you don't believe me, go to Google Maps and whip out your Chrome developer tools). They do this at a bunch of different zoom levels - level 1 is a single tile showing the entire extent of the globe, and each subsequent level divides its predecessor into 4x as many tiles. Conveniently, Google makes it really easy to add additional tile layers to their maps... as long as you chop up your images into the exact same tile structure as they use.
Thankfully, others have already done the hard work of generating some Python helper functions for chopping up a Mapnik map into the appropriate tiles for Google Maps. The code we'll use (shown below) is borrowed (with very light modification) from this Google Code post. Check it out.
Leveraging the helper functions here is really easy too! You just need to provide a lat/long bounding box with the maximum extent of the tiles you want to render, a directory to put the output in, plus a minimum and maximum zoom level. Everything else happens under the hood. For simplicity (and to keep from flogging my web server for the example), we'll just render zoom level 9 here - but you could render everything if you wanted.
On my PC, this runs virtually instantly and pumps out a good set of PNGs. Awesome!
Putting the tiles on a Google Map
So, how do you put the tiles on a Google Map? Well, the Google Maps API makes it pretty simple. Using a slight modification of Google's code, I came up with the following for the demo you saw above...
Note that the bounds object specifies the minimum and maximum X and Y indexes for our tile images at each possible zoom level (and, as I've said before, I locked this down to zoom level 9 only for simplicity). You can figure out what these minimum and maximum indexes are simply by looking at the directory structure of your Mapnik output. Also note that I've specified a black/white style for the map to make the red shape file stand out a bit more clearly. Finally, as you've surely guessed, that getTileUrl function returns the URL on my web server to the tiles in the map, or null if the tile is out of bounds. I'll let you figure out the rest.
Obviously, this is a super-basic example of what you can accomplish here. But there are an amazing list of things you can do with this. It's a great way to render GIS data in an app that people are already used to consuming (Google Maps), that looks pretty (even on mobile) and that doesn't cost you a single penny. Hard to beat.
If you're working on implementing this, consider the following tips to improve your mapping:
- Store your GIS data in a PostGIS database, so you can take advantage of spatial indexes, etc.
- Re-project all layers into your final coordinate system before running Mapnik. Mapnik is notoriously slow at re-projecting.
- Make your Python script multi-threaded.
As always, I'd love to hear about some cool ways you're using Mapnik and Google Maps!