If you ask a bunch of people to upload GPS traces when they walk/drive and you combine those traces, you can get a rudimentary map. In fact, this is one of the primary data sources of OpenStreetMap. The data for those is freely available, so we can use it in a small project.
To draw simple outlines, iterating over the GPS track points and putting them on an image should be enough. It will give us the main roads and the general city structure, and will be possible to recognize when compared to an actual map.
To begin, let’s get the coordinates of the place we’ll be mapping. In my case, this will be Sheffield. If you go to OpenStreetMap and hit Export, it will let you select and area with a bounding box and get the coordinates of it. We’ll get the coordinates and write it to to our script.
# Area format is left, bottom, right, top
AREA = [-1.4853, 53.3730, -1.4557, 53.3893]
The other thing we should get out of the way is the output size. We should go with a nice 720p picture.
WIDTH = 1280
HEIGHT = 720
Getting the GPS data
OpenStreetMap provides an API that we can use in order to fetch GPS track data. It gives us the data for a given region in the XML format.
A small disclaimer about the API. It’s normally meant for editing, which means you should try to keep your usage very light. While making this project and iterating on the code, I kept all my API calls in a local cache. I’ll write about this in the future.
You can find the documentation for the API here. Here’s the gist of it.
bbox = ','.join(AREA)
url = 'https://api.openstreetmap.org/api/0.6/trackpoints'
xml = requests.get(url, params={'bbox': bbox}).text
This should get an XML document with the GPS trackpoints. Let’s parse it and get the latitude/longitude pairs. The coordinates are held in <trkpt>
tags.
root = ET.fromstring(xml)
selector = './/{http://www.topografix.com/GPX/1/0}trkpt'
for trkpt in root.findall(selector):
print(trkpt.attrib['lat'], trkpt.attrib['lon'])
As you can see, this is relatively straightforward. The XML selector might look weird. It just means get all the trkpt elements that belong to the namespace of that URL.
Plotting the data
Let’s start with a small example, creating an empty image and drawing a pixel in it.
img = Image.new('L', (WIDTH, HEIGHT), color='white')
img.putpixel((5, 5), 1)
This code will draw a single black pixel on an empty image. The rest of the way should be looking pretty clear now, go through the lat/lon pairs and plot them as pixels. But before we get to that step, there is one more hurdle to get through. And that is mapping the GPS coordinates to image coordinates.
Mapping coordinates for our map
The problem is, we have a 1280x720 image. And we can’t ask Python to put a pixel on (52.6447, -8.6337)
. We already know the exact area of the map we’re drawing, and the size of our output. What we need to do is get those two ranges and interpolate where a given coordinate falls on our image. For this, we can use the interp function from numpy.
y, x = point
x = int(interp(x, [AREA[0], AREA[2]], [0, WIDTH]))
y = int(interp(y, [AREA[1], AREA[3]], [HEIGHT, 0]))
try:
img.putpixel((x, y), 1)
except:
# In case math goes wrong
pass
Drawing the map
We are know able to get GPS trackpoints and know how to map them to image coordinates. So let’s loop through everything and put the pixels on our map. Since each page has a limit of 5000 points, we should also iterate through the pages.
for page in range(15):
for point in get_points(AREA, page):
y, x = point
x = int(interp(x, [AREA[0], AREA[2]], [0, WIDTH]))
y = int(interp(y, [AREA[1], AREA[3]], [HEIGHT, 0]))
try:
img.putpixel((x, y), 1)
except:
pass
Getting points with pagination
Here’s a generator function to return OpenStreetMap trackpoints with pagination.
def get_points(area, page=0):
bbox = ','.join(map(str, area))
xml = sess.get('https://api.openstreetmap.org/api/0.6/trackpoints',
params={'bbox': bbox, 'page': page}).text
root = ET.fromstring(xml)
for trkpt in root.findall('.//{http://www.topografix.com/GPX/1/0}trkpt'):
yield trkpt.attrib['lat'], trkpt.attrib['lon']