Generating Map Tiles with Rust. How easy is it to transition from… | by João Paulo Figueira | May, 2024

0


How easy is it to transition from Python to Rust?

Photo by Diego García on Unsplash

Sometimes, you must display vast amounts of data on an interactive map while keeping it usable and responsive. Interactive online maps are implemented in HTML, and adding many visual elements to the map display generally degrades performance and usability. A possible alternative is to draw all of the elements offline and display them over the map as a transparent layer using tiles. Each square tile neatly overlaps the map’s tiles, and the interactive map control handles far fewer visual elements.

I addressed this issue a few years ago by writing a custom map tile generator using Python and data from the Vehicle Energy Dataset. This project illustrated how to display massive amounts of information on an interactive online map by using custom tile layers over the map. The process involves using a web application that generates, caches and serves the tiles.

As you know, Python is not fast, so there is a significant performance hit while the web application generates each tile. When a tile is cached, the serving process is quick and is not noticeable while interacting with the map.

Still, I was unhappy with the performance, so I wanted to solve the problem again by dramatically improving the code execution speed. At first, I thought about converting the code base to Cython, but then my attention was diverted to another candidate.

The Rust programming language has been on my radar for quite some time. With a background in C, C++ and C#, I was intrigued by the language’s promise of memory safety and C-like performance. I finally decided to have a go at it, and this problem looked like a perfect starting point to learn and exercise the language.

After reviewing many YouTube videos and diverse written material, I started using Rust to address this problem. I had three primary questions: How hard is it to create a web application, access SQLite data, and programmatically create a transparent PNG image? Fortunately, the answers to these questions were more straightforward to respond to than anticipated.

Rocket

To answer the web application question, I turned to Rocket. The Getting Started page from Rocket’s online documentation shows how easy it is to set up a basic web application. We will surely need more complexity to build our tile server, but the boilerplate seems minimal and straightforward. And, as it turned out to be, Rocket is very easy to use and adapt. It’s a keeper to me.

sqlx

After a few minutes online, I quickly realized that the most popular answer to accessing SQLite databases was through the sqlx package. It presents a different paradigm from the one I used in Python but much closer to the one I used in my former life when I developed in C#. Instead of generic data structures or Pandas DataFrames, you must use strongly typed data structures here. Although they are a bit more laborious to work with, they will bring an extra layer of sanity to your life.

Figure 1 below shows the first complete code sample I used to retrieve the data from the level range table.

Figure 1 — Sample code to retrieve the table of level ranges. (Image source: Author)

PNG

Creating, drawing, and saving PNG files using the image crate is easy. The code to create a transparent tile is quite simple:

Figure 2 — The image crate simplifies the manipulation of images. The code above shows how to create a solid color 256×256 map tile. (Image source: Author)

I also used the colorgrad package to handle the color gradient for the tiles.

Before I discuss the code in detail, let’s review the principle behind drawing the traffic density tiles.

Map tiles usually consist of square 256×256 bitmaps. We may address each tile by combining x and y coordinates, a “zoom” level, or a quadkey code. To each zoom level corresponds a square patchwork of tiles of different dimensions. The whole Earth is depicted on a single tile at the topmost level. By zooming in, the original tile is split up into four tiles. The following Figures 2 and 3 illustrate the process of zooming in.

Figure 3 — The whole world on a single tile at zoom level 0. (Image source: OpenStreetMap)
Figure 4 — Zooming into the previous tile, we get four tiles with the same individual dimension. (Image source: OpenStreetMap)

If we keep zooming in, and after eight iterations, each resulting tile corresponds to a pixel on the first tile. This observation is the insight that allows us to compute and display the traffic density information on the tiles.

As described in the previous article, the tile information is prepared and stored in a database. Please refer to that article for instructions on generating the density database from the Vehicle Energy Dataset.

We can now discuss the Rust server code to generate, cache, and serve tiles. The present solution closely follows the previous tile server design. Figure 5 below shows the main entry point that decides whether to provide a painted tile or the default transparent one after parsing and accepting the query parameters.

Figure 5 — The main entry point. (Image source: Author)

As you can see, the server replies to zoom levels ranging from one to eighteen only. This limitation was baked into the data generation process for the density database.

The web application draws each tile using the function listed in Figure 6 below.

Figure 6 — The function above generates the tile, if not already cached on disk, and returns the tile file name. (Image source: Author)

As you can see from the listing above, the tile painting process has three steps. First, on line 12, we collect the tile’s per-pixel density information. Next, we retrieve the tile’s level range, i.e., the minimum and maximum density levels for the tile’s “zoom” level. Finally, on line 14, we paint the tile’s bitmap. The function finalizes by saving the tile bitmap to the file cache.

Figure 7 — The function above paints a single tile on a bitmap. Note how the density information is transformed into an entry into the color gradient using a logarithmic-based transformation. (Image source: Author)

After correctly configuring the database file path, you start the tile server by opening a terminal window, changing to the Rust project directory, and running the following command:

cargo run --release

Next, you can open the map client and configure the density tile layer URI. Figure 8 below shows the Jupyter Notebook code cell to load the interactive map:

Figure 8 — Use the code above to display the Ann Arbor map with the density tiles overlayed. (Image source: Author)

And that’s it! Figure 9 below displays the result.

Figure 9 — The image above displays the base map with the overlayed traffic density tiles. (Image source: OpenStreetMap and author-generated tiles)

My first foray into Rust was not nearly as difficult as I expected. I started by immersing myself in the available literature and YouTube videos before giving it a go. Next, I ensured I was using a helping hand with a great IDE from JetBrains: RustRover. Although still in preview mode, I found this IDE helpful and instructive when using Rust. Still, you will also be perfectly fine if you prefer Visual Studio Code. Just make sure you get the sanctioned plugins.

I used Grammarly to review the writing and accepted several of its rewriting suggestions.

JetBrains’ AI assistant wrote some of the code, and I also used it to learn Rust. It has become a staple of my everyday work with both Rust and Python.

The Extended Vehicle Energy Dataset is licensed under Apache 2.0, like its originator, the Vehicle Energy Dataset.

Vehicle Energy Dataset (GitHub)

GitHub repository

Leave a Reply

Your email address will not be published. Required fields are marked *