node.jsmapnikmapbox-gl-jsvector-tiles

Self hosted Vector Tiles for MapboxGL Client rendered incorrectly


I am trying to set up a web server in Node.js that serves vector tiles to be displayed in a browser using MapboxGL JS. The data for the vector tiles is stored in a PostGIS database.

My current set up seems to going in the right direction, as I can see vector tiles being loaded and displayed in the browser. However the rendered result is incorrect (this is a screenshot of a section of my map):

Incorrect tiles

The top half of the green building footprints is repeated in the bottom half of the image. I also notice buildings "changing" positions when zooming in and out, indicating that somehow the tiles are rendered offset or for an incorrect extent... The imported data is in SRID 4326.

Here is my code:

var zlib = require('zlib');

var express = require('express');
var mapnik = require('mapnik');
var Promise = require('promise');
var SphericalMercator = require('sphericalmercator');

var mercator = new SphericalMercator({
    size: 256 //tile size
});

mapnik.register_default_input_plugins();

var app = express();

app.get('/vector-tiles/:layerName/:z/:x/:y.pbf', function(req, res) {
    var options = {
        x: parseInt(req.params.x),
        y: parseInt(req.params.y),
        z: parseInt(req.params.z),
        layerName: req.params.layerName
    };
    makeVectorTile(options).then(function(vectorTile) {
        zlib.deflate(vectorTile, function(err, data) {
            if (err) return res.status(500).send(err.message);
            res.setHeader('Content-Encoding', 'deflate');
            res.setHeader('Content-Type', 'application/x-protobuf');
            res.send(data);
        });
    });
});

function makeVectorTile(options) {
    var extent = mercator.bbox(options.x, options.y, options.z, false, '4326');

    var map = new mapnik.Map(256, 256);
    map.extent = extent;

    var layer = new mapnik.Layer(options.layerName);
    layer.datasource = new mapnik.Datasource({
        type: 'postgis',
        dbname: 'databasename',
        table: options.layerName,
        user: 'username',
        password: 'password'
    });
    layer.styles = ['default'];
    map.add_layer(layer);
    return new Promise(function (resolve, reject) {
        var vtile = new mapnik.VectorTile(parseInt(options.z), parseInt(options.x), parseInt(options.y));
        map.render(vtile, function (err, vtile) {
            if (err) return reject(err);
            resolve(vtile.getData());
        });
    });
}

The custom vector data source is included in the map like this:

var map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/light-v8',
    zoom: 10,
    center: [13.739910, 51.051151]
});

map.on('style.load', function () {
    map.addSource('local', {
        type: 'vector',
        tiles: ["http://localhost:3333/vector-tiles/building/{z}/{x}/{y}.pbf"]
    });
    map.addLayer({
        "id": "park",
        "source": "local",
        "type": "fill",
        "source-layer": "building",
        "paint": {
            "fill-color": "#5DC73A"
        }
    });
});

Solution

  • The map in the above example code uses an incorrect spatial reference systems. Vector tiles use Web Mercator Projection, but the mapnik Map is initialised in WGS84. When explicitly providing web mercator to both the mercator.bbox method and the mapnik.Map constructor, the vector tiles render correctly in the client:

    var extent = mercator.bbox(options.x, options.y, options.z, false, '3857');
    
    var map = new mapnik.Map(256, 256, '+init=epsg:3857');