I have a while using Strava for my casual runs and something that always called my attention was the main feed of the app, where you can find a nice picture or snapshot of the path run by you or some of your friends. I initially thought that the application loaded a set of Geojsons and made a render for each one of the posts on run time, but after working with mobile maps in my own project I understod that rendering a map was no easy task for any device. I then noticed that every post had a capture of the route over the map and the feed wasn't too different from any social media post, nevertheless, I still had a curiosity about how we could generate those captures.
I recently found myself with some available time so I could start experimenting with this project and I'm going to walk you through my journey.
First things first: How to generate a map snapshot?
My first step was trying to render a map and make a capture of that map. After browsing the web for a couple of minutes, I found a great article named Making Artistic Maps with Python, where the author teaches us how we can use Python to generate a high-quality map of any city.
After following the tutorial and making some tests, I noticed that the script required too much time between fetching information from Open Street Maps and processing the information. Taking into consideration the number of concurrent requests that Strava must receive I said to myself that It had to be an alternative that consumes fewer resources, and then I found Mapbox's statice Images API.
MapBox has a set of APIs that are sold as software as a service, but they allow you to generate a developer API key and even design your own map styles. The Static Images API, let you generate a completely customizable map. You provide parameters like coordinates, sizes, geographical objects (points, lines, polygons, etc), and others. In return, you get an image with the map you asked for.
The service works pretty smoothly, and after working with it I noticed that even Strava uses it (you can see MapBox mark in the footnote of every map), that definitely was a signal of being walking in the right direction.
import requests
API_URL = "https://api.mapbox.com/styles/v1/{0}/static/{1}/auto/{2}x{3}?access_token={4}"
...
request_url = API_URL.format(
style_id, # Map Style Ex. 'mapbox/darkv11'
polyline_hash, # Polyline is alghoritm for hashing list of coordinates.
width, height, # Dimenssions of the image
token # Access token provided by MapBox
)
response = requests.get(request_url)
if (response.status_code == 200):
open('map.png', 'wb').write(response.content)
else:
print(response.text)
...
Let's get serious: Creating the service
Once I got a pretty basic script for generating the maps with MapBox, I had to start thinking about how our service was going to work. I knew I had to receive a set of coordinates and maybe a couple of styling parameters but there were still two elephants in the room: asynchrony and scalability.
Since this will be using a call to an external service I couldn't rely on a synchronic execution for every HTTP call that our service is going to receive and the horizon just gets darker when you think about the failure possibilities given the number of requests and the common problems with networks. The answer was, obviously, I had to receive the request, close it immediately and then start processing the map in a new thread or background process, always taking into consideration that we have to be scalable. With that and the strategy of divide and conquer in mind, I concluded that my solution would need the components:
- A web server that receives and validates the client's requests.
- A queue manager that lists the validated requests.
- A set of workers that reads from the queue manager and executes the processing.
For mounting that architecture, I couldn't imagine an easier way than using a Docker Compose (or Docker + Kubernetes) approach. That's how I ended up with a folder tree like the one below.
├───service
│ ├───nginx
│ └───src
│ └───map_equests.py
│ └───main.py
│ └───Dokerfile
│ └───requirements.txt
├───worker
│ └───src
│ └───helpers
│ └───worker.py
│ └───Dokerfile
│ └───requirements.txt
│
├───docker-compose.yml
└───.env
Conclusions
This solution is far from optimal, and I'm sure is not the only answer to this problem, but trying to figure out how to solve it in a revere engineering exercise has resulted in a pretty illustrating activity that I definitely recommend to any developer.
I have published the project on my Github account. You can find the repository here so you can dive deep into the code. I'm also living you some resources below that can help you in case you are working on something like this or maybe just looking for information about one of these technologies.
Top comments (0)