Vector tiles, PostGIS and OpenLayers.
Mapbox Vector Tiles was the new kid in town until a couple of years ago. Since its first draft, on April 13, 2014, it has come a long way and today it’s adoption is widening, thanks to more libraries and software supporting it. There are a lot of resources on the web about it, first of all the page from Mapbox, so I won’t delve into the details. It will suffice to say that it’s a vector data encoding in the form of binary data tiles, geared towards reduced data size, bandwidth reduction and (potentially) improved rendering performance.
Vector tiling is not new in the GIS world, one of the first use cases was from Canada Geographic Information System in 1966. But it’s been the huge increase of (spatial)data intensive web applications, coupled with ever more browser power, that demanded a more efficient data scheme and format that could make web mapping engines take advantage of vector data for processing and styling on the client side, instead of relying on pre-rendered images served statically or through dynamic services (like OGC/WMS).
Simply put a Mapbox Vector Tile is the result of splitting a vector layer by a grid, which can be as XYZ tiles or TMS, and encoding each tile like this:
- feature geometries are transformed to a single tile, local, pixel coordinate system, which by default goes from upper-left(0,0) to lower-right (4096,4096), i.e. an affine transformation with integer rounding.
- features attributes are encoded as a unique set of keys (something like a layer fields schema) and the list of their values.
- geometries and attributes are encoded as Google Protobuf (PBF) binary data.
Here you can read the full specification with all the technical details
Several tools and libraries are available to create and consume MVT tiles, most of them designed as libraries to be employed inside higher level machinery, but with the help of PostGIS and OpenLayers we can play with them easily. Let’s see how we can make the most of them with the least effort.
Generating MVT tiles with PostGIS
Since PostGIS 2.4.0, two new functions are available: ST_AsMVTGeom and ST_AsMVT. The first one transforms a geometry into the coordinate space of an MVT tile:
ST_AsMVTGeom(geometry geom, box2d bounds, integer extent=4096, integer buffer=256, boolean clip_geom=true);
It needs at least one geometry object and the geographical tile bounds to perform the coordinates transformation. The default is a 4096 wide tile, with a 256 px buffer used for geometry clipping, and the geometry clipping flag itself. In my tests I’ve set the latter two to 0 and false, because I didn’t need to perform clipping.
ST_AsMVT needs a set of rows (vector features) to generate the MVT tile, and we can use the geometry from the previous function to feed it:
ST_AsMVT(tile) FROM (SELECT attribute1, attribute2, ..., ST_AsMVTGeom(geom, <bounds>, 4096, 0, false)) AS tile;
Any selected attribute from the original table (vector layer) will be included in the tile. The <bounds> parameter must be calculated for each tile, in accordance to the chosen grid.
The result of this function will be the tile PBF binary data.
In my test I’m using Python and Flask, but obviously any web programming language and framework can be used.
Here is a minimal webapp to accept XYZ tiles requests and respond with the encoded tiles:
It essentially does the following:
- tile bounds are calculated from its x, y and z coordinates
- The box2d <bounds> parameter for ST_AsMVT is created from the EPSG:3857 tile bounds; here I turn off buffering and the clipping.
- the tile is saved to a file, following the typical z/x/y hierarchical tiles cache scheme and then it’s returned to the client as binary data (octect-stream)
The relevant row is the query to PostGIS, where the actual MVT encoding happens.
SELECT ST_AsMVT(tile) FROM (SELECT id, name, ST_AsMVTGeom(geom, ST_Makebox2d(ST_transform(ST_SetSrid(ST_MakePoint(%s,%s),4326),3857),ST_transform(ST_SetSrid(ST_MakePoint(%s,%s),4326),3857)), 4096, 0, false) AS geom FROM admin_areas) AS tile
Viewing the tiles with OpenLayers
OpenLayers can ingest MVT tiles since version 3.18 through its ol.format.MVT class. This makes using MVT as a vector layer source possible.
The HTML page contains just a basic OpenLayers map with a base OSM layer and the MVT vector layer:
As you can see, all that we need is a vector tile layer and a vector tile source, pointing to our tiny server app and configured to use the MVT format. Being vector data, you can define whatever styling you wish. In this case I’m applying a simple stroke without fill.
This is just a simple example to test the new PostGIS MVT functions and to show how easily vector tiles can be introduced in a web mapping application. Here the tiles are always generated at full resolution, independent from the zoom level. The next step could employ multiresolution geometry simplification like ST_Simplify, ST_SimplyPreserveTopology or the new ST_SimplifyVW and ST_SetEffectiveArea functions implementing the Visvalingam-Whyatt algorithm and effective area calculations. Maybe thiscan be the subject for a future post!