Sunday, 16 December 2018

Self-hosting Mapbox vector tiles


As presented in a talk at FOSS4G Mapbox Studio allows to create Mapbox vector tiles and export them as a .mbtiles file.


The mapbox-gl.js library can be used to dynamically style and render Mapbox vector tiles on client (browser) side.


The missing part: How can I self-host Mapbox vector tiles (.mbtiles) so that I can consume them with mapbox-gl.js?


I know that Mapbox Studio can upload the vector tiles to the Mapbox server and let it host the tiles. But that's no option for me, I want to host the vector tiles on my own server.




The TileStream approach below turned out to be a dead end. See my answer for a working solution with Tilelive.




I tried TileStream which can serve image tiles out of .mbtiles files:



My webpage uses mapbox-gl v0.4.0:





and it creates a mapboxgl.Map in a JavaScript script:


  var map = new mapboxgl.Map({
container: 'map',
center: [46.8104, 8.2452],
zoom: 9,
style: 'c.json'

});

The c.json style file configures the vector tile source:


{
"version": 6,
"sprite": "https://www.mapbox.com/mapbox-gl-styles/sprites/bright",
"glyphs": "mapbox://fontstack/{fontstack}/{range}.pbf",
"constants": {
"@land": "#808080",
"@earth": "#805040",

"@water": "#a0c8f0",
"@road": "#000000"
},
"sources": {
"osm_roads": {
"type": "vector",
"url": "tile.json"
}
},
"layers": [{

"id": "background",
"type": "background",
"paint": {
"background-color": "@land"
}
}, {
"id": "roads",
"type": "line",
"source": "osm_roads",
"source-layer": "roads",

"paint": {
"line-color": "@road"
}
}]
}

... with the following TileJSON specification in tile.json:


{
"tilejson": "2.1.0",
"tiles": [

"http://localhost:8888/v2/osm_roads/{z}/{x}/{y}.png"
],
"minzoom": 0,
"maxzoom": 12
}

... which points to my TileStream server running at localhost:8888. TileStream has been started with:


node index.js start --tiles="..\tiles"

... where the ..\tiles folder contains my osm_roads.mbtiles file.



With this setup, I can open my webpage but only see the background layer. In the browser network trace I can see that tiles are indeed loaded when I zoom in, but the browser JavaScript error console contains several errors of the form


Error: Invalid UTF-8 codepoint: 160      in mapbox-gl.js:7

Since vector tiles are not .png images but rather ProtoBuf files, the tiles URL http://localhost:8888/v2/osm_roads/{z}/{x}/{y}.pbf would actually make more sense, but that doesn't work.


Any ideas?



Answer



As pointed out by @Greg, instead of TileStream (my first attempt) you should use Tilelive to host your own vector tiles.


Tilelive isn't a server itself but a backend framework that deals with tiles in different formats from different sources. But it's based on Node.js so you can turn it into a server in a pretty straight-forward way. To read tiles from a .mbtiles source as exported by Mapbox Studio, you need the node-mbtiles tilelive module.


Side note: Current Mapbox Studio has a bug under Windows and OS X that prevents an exported .mbtiles file to show up at your chosen destination. Workaround: Just grab the latest export-xxxxxxxx.mbtiles file in ~/.mapbox-studio/cache.


I found two server implementations (ten20 tile server by alexbirkett and TileServer by hanchao) who both use Express.js as a web app server.



Here is my minimalistic approach which is loosely based on these implementations:



  1. Install Node.js

  2. Grab the node packages with npm install tilelive mbtiles express


  3. Implement the server in the file server.js:


    var express = require('express');
    var http = require('http');
    var app = express();
    var tilelive = require('tilelive');

    require('mbtiles').registerProtocols(tilelive);

    //Depending on the OS the path might need to be 'mbtiles:///' on OS X and linux
    tilelive.load('mbtiles://path/to/osm_roads.mbtiles', function(err, source) {

    if (err) {
    throw err;
    }
    app.set('port', 7777);


    app.use(function(req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
    next();
    });

    app.get(/^\/v2\/tiles\/(\d+)\/(\d+)\/(\d+).pbf$/, function(req, res){

    var z = req.params[0];
    var x = req.params[1];

    var y = req.params[2];

    console.log('get tile %d, %d, %d', z, x, y);

    source.getTile(z, x, y, function(err, tile, headers) {
    if (err) {
    res.status(404)
    res.send(err.message);
    console.log(err.message);
    } else {

    res.set(headers);
    res.send(tile);
    }
    });
    });

    http.createServer(app).listen(app.get('port'), function() {
    console.log('Express server listening on port ' + app.get('port'));
    });
    });


    Note: The Access-Control-Allow-... headers enable cross-origin resource sharing (CORS) so webpages served from a different server may access the tiles.




  4. Run it with node server.js




  5. Set up the webpage using Mapbox GL JS in minimal.html:







    Mapbox GL JS rendering my own tiles













  6. Indicate the location of the tile source and style the layers with the following minimal.json:


    {
    "version": 6,
    "constants": {
    "@background": "#808080",

    "@road": "#000000"
    },
    "sources": {
    "osm_roads": {
    "type": "vector",
    "tiles": [
    "http://localhost:7777/v2/tiles/{z}/{x}/{y}.pbf"
    ],
    "minzoom": 0,
    "maxzoom": 12

    }
    },
    "layers": [{
    "id": "background",
    "type": "background",
    "paint": {
    "background-color": "@background"
    }
    }, {
    "id": "roads",

    "type": "line",
    "source": "osm_roads",
    "source-layer": "roads",
    "paint": {
    "line-color": "@road"
    }
    }]
    }



  7. Serve the webpage and rejoice.




No comments:

Post a Comment

arcpy - Changing output name when exporting data driven pages to JPG?

Is there a way to save the output JPG, changing the output file name to the page name, instead of page number? I mean changing the script fo...