It’s been almost a year since I last posted about maps, so I figure it’s time for an update! I’ve added a lot of features and fixed a lot of bugs in libshumate’s vector tile renderer.
One of the major benefits of vector tiles is that, unlike raster (image) tiles, we can scale them up without making them blurry. This means that, after a certain zoom level, it’s not really useful to keep splitting the data into smaller and smaller chunks–we can just take a lower zoom level and scale it up.
This has benefits throughout the stack. Offline maps will take a lot less space because we won’t have to store so many different levels of detail. Also, since each zoom level contains four times as many tiles as the one before it, level 18 for the whole world contains over 68 billion tiles! But level 14 contains “only” 268 million. That’s still a whole lot, but it’s small enough that, with a powerful enough computer, we can generate them up front and serve them with a much simpler server than raster tiles, which must be generated on demand. The pre-generated tiles are about 80 GB. If you’d like to know more about this process, check out Planetiler or watch out for a future blog post!
The symbol collision detection that I described in my previous post worked well, but had some drawbacks. Primarily, it wasn’t flexible enough to support all the text layout features I want to implement from the MapLibre spec.
The problem was that the R-tree was built ahead of time and persisted between frames, so it had to contain every symbol in the visible area and every way that each one could rotate or be repositioned. This would have made the code extremely complex if, say, I wanted to add the
text-variable-anchor property, which allows labels to move around to get the best chance of not colliding with another label. How would the R-tree know which placement of the label is the current one?
So I changed the R-tree to be re-built every frame. Only rectangles that are currently visible are included in the tree, so the code to check whether a rectangle collides is much simpler. Surprisingly, the new approach actually increases performance, despite constantly rebuilding the tree. Only visible rectangles have to be checked against on each lookup, and the rectangles are smaller because they don’t have to account for every possible rotation of the label, which makes the R-tree method more effective. Here’s the visualization of the new technique:
Following the symbol collision changes, I implemented icons in symbols. This includes both the icons in places’ labels and things like the one-way arrows on streets. It would have been much more complicated to do correctly with the old collision code, but now it works pretty well. Here’s a screenshot:
A huge part of the MapLibre spec deals with expressions and data-driven styling. It’s one of the things that make vector tiles so powerful: we can customize the map in new and creative ways without downloading a whole new set of tiles from the server.
Expressions allow style authors to make styling decisions based on the characteristics of individual features, such as giving tunnels a dotted line casing or choosing an icon based on a point of interest’s specific subclass.
The OpenStreetMap Americana project has been pioneering many interesting new uses of expressions. They have support for internationalization, even showing a city’s local name underneath its name in your language if it’s different. They also have highway shields, which are the “icons” that distinguish different route networks, such as the US Interstate system. These use fairly complicated chains of expressions, so to get these features in libshumate I’ll need to implement more of the expressions spec.
I’ve started with the
coalesce expressions, which let us use conditions and create fallback styles, and
var, which make it easier to change style configurations and keep the style organized. I also added some basic string functions like
downcase as well as mathematical operations.
I’ve created a documentation page cataloging the state of our MapLibre spec implementation. There’s a lot of red X’s at the moment. But many of them, including the most important ones, will be fairly straightforward to implement.
I’ve also made some improvements to performance–including Sysprof integration–but that’s a subject for another blog post. I’ve also started working on a new map style, which will also get a blog post when it’s a little further along.
But most importantly, the vector renderer is getting to a point where it’s usable with a real style–you might have noticed the screenshots in this post use OSM Liberty instead of the old testing style, and it looks pretty good! So the next step is to get GNOME Maps ready to use the vector styles. It will start as an experimental feature, but the goal is to get it to a point where it can become the default.
Thank you to everyone who’s encouraged my maps obsession over the years, given me feedback on my map style, reviewed my merge requests, and contributed on Sponsors. I’m looking forward to another exciting and hopefully eventful year of map development!