PostGIS to Protobuf (with vector tiles)

2015-03-17

I have seen a lot of talk about protobol buffers and vector tiles recently, which promises improved performance loading and rendering layers than just plain geojson. I've seen a lot of demos of the tech, but had trouble putting it all together. This is my first attempt at creating a server that will query PostGIS, serve vector tiles encoded into protobuf, and render them on a Leaflet map.

Create a layer in Leaflet, using the excellent Leaflet.MapboxVectorTile plugin.

var vectorTileLayer = new L.TileLayer.MVTSource({
  url: '/vector-tiles/layername/{z}/{x}/{y}.pbf',
  clickableLayers: ['layername'],
  getIDForLayerFeature: function(feature) {
    return feature._id
  }
})
map.addLayer(vectorTileLayer)

Create a route to match the above URL. Create a node server with Express, and add a route similar to a standard tile layer:

router.get('/vector-tiles/:layername/:z/:x/:y.pbf', function(req, res) {

})

Get the bounding box of our data, using the sphericalmercator library

var SphericalMercator = require('sphericalmercator')
var mercator = new SphericalMercator({
  size: 256 //tile size
})
var bbox = mercator.bbox(
  +req.params.x,
  +req.params.y,
  +req.params.z,
  false,
  '4326'
)

Use this bounding box to query PostGIS for the data we need for this tile. Convert the resulting rows into a single geojson feature collection.

select st_asgeojson(geom) as feature from table where st_intersects(geom, bbox)

Now that we have the geojson for the tile, we can create a vector tile using node-mapnik

var mapnik = require('mapnik')
var vtile = new mapnik.VectorTile(+req.params.z, +req.params.x, +req.params.y)
vtile.addGeoJSON(JSON.stringify(geojson), 'layername')

Finally, use vtile.getData() to get the protobuf-encoded buffer from the vector tile, pass it through zlib.deflate to compress, set the correct headers, and serve it.

res.setHeader('Content-Encoding', 'deflate')
res.setHeader('Content-Type', 'application/x-protobuf')
zlib.deflate(vtile.getData(), function(err, pbf) {
  res.send(pbf)
})

Your vectorTileLayer in Leaflet should now be working.

So far, this seems to load complex layers much faster than querying and returning the entire geojson for a bounding box, and has the advantage of sending actual vector data to the client, unlike a traditional tile layer.

Discuss with me on twitter.