Rendering a map using Go, Mapbox and OpenStreetMap
Fantasy books or novels have a lot of success all around the world, such as the famous The Lord of the Rings or Magician. In those books, in order to help you feel the atmosphere of the world your heroes are evolving in, you often find a map of the world. Most of the times they look hand-drawn and they also have a fantasy design. A well-known example would be the map of the Land of Oz.
In the past, I had a project of hand-drawing such a map of the place where I’ve grown. I guess that it was an inspiration when I started this one.
As a first step, this article will cover how to retrieve map data and how to draw it, but without any fantasy style. The second step of rendering a fantasy map will be for another article if I actually succeed to do so!
How to retrieve the map data
Locate the place to render
First, I want to textually look for the zone I want to draw. But what actually happens when you type your address on Google Maps? How does it locate the map on the address you typed? Turning an address into latitude/longitude coordinates is called « forward geocoding ». The reverse, turning latitude/longitude coordinates into text, is called « reverse geocoding ».
One would need a lot of information and data to actually implement a geocoding feature and this is where the Mapbox Geocoding API will be great: they provide a geocoding API that will help us retrieve latitude/longitude coordinates from text inputs.
Get a bounding box
A bounding box, also called an extent, is basically the rectangle containing all the data we’re interested in.
The Mapbox API documentation explains that I retrieve « Feature » objects. There is many feature types in the Mapbox API but those interesting me are the type « place » and the type « poi ».
The good thing with the type « place » is that it ships a bounding box. However it could be smart to check whether or not this bounding box is very large: if it is, too much objects will be returned and drawn. Let’s not bother too much about this for now.
The other type « poi » means Point Of Interest, it is a simple position with no bounding box, for example the position of a fountain in the middle of a place. But it’s enough because, with approximate math, I’m able to compute a bounding box.
Get the data from OpenStreetMap
OpenStreetMap is an open “database” « built by a community of mappers that contribute and maintain data about roads, trails, cafés, railway stations, and much more, all over the world. »
It is a tremendous source of data and this is in this huge database that I will retrieve everything I need to start rendering the maps.
However, I will host this project on a small VPS with only 10 Gb of storage. At the time of writing, the XML file containing all the data for the whole planet is 69GB compressed. I need to fetch the data from an external source, moreover, I need to fetch only the data inside the bounding box I’ve previously retrieved or computed.
OpenStreetMap offers such a solution: the Overpass API. Very powerful, it offers a complete language of query in order to ask for specific data in a given zone. Thus, getting the data only consist of an HTTP call to this service. We must be cautious to not spam it as, again, it seems to be runned by volunteers. Example of a call on the API to retrieve objects inside a given bounding box:
This API route returns XML data using the OpenStreetMap format: in order to turn it into useful Go objects, I’ve used the library go-osm.
This done, using the OpenStreetMap objects definition in their documentation and their website which gives all information on each object in the map, I was able to characterize most of the objects that I want to draw.
Example of the description of the OpenStreetMap object type « Relation » for the « Lac d’Annecy »:
Now that I’ve gathered all the objects contained in the zone I want to draw, we can start the rendering process to a PNG file.
I chose the great gg library library in order to draw primitives such as areas and lines which actually represents the roads, the buildings, the rivers, etc.
Using this library, the rendering is the easiest part as the gg API fits this use-case very well.
Rendering a road
A road will simply be drawn as a line, not much to say here. OpenStreetMap does the difference between major roads and minor roads, paths, etc. I’ve just changed the thinness depending on this information.
First rendering I’ve done, drawing only roads around « Lyon, France »:
Rendering a river
A river is not a simple line: it can be large or it can be very thin but it will be better to render it as an area. For example: when you get the object « Lac d’Annecy » from OpenStreetMap, you actually get an object with many inner and outer lines. In order to draw it we will want to use the « outer » line as the borders of a polygon, and fill its content with a blue-ish color.
Example of rendering only the water areas around Lyon, France:
I won’t detail the rendering of each type of objects but you got the main idea.
Finally, here are some rendering of different zones of the world:
A street of Montreal, Quebec
There is still many things wrong with this map rendering, for exemple, I didn’t apply any projection on the data while drawing it on a 2D plan, thus, it is not really accurate and even badly render some parts of the world. It is a complex topic and I didn’t want to deal with it for now.
Moreover, there is still some OpenStreetMap objects that I don’t process and that I don’t draw.
Obviously, the next step will be to render all these objects using a hand-drawn fantasy style, and this is where the real challenge will appear!