This commit is contained in:
cosmix 2018-05-01 17:01:14 +00:00 committed by GitHub
commit fc6976c4d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 295 additions and 219 deletions

11
.eslintrc.json Normal file
View file

@ -0,0 +1,11 @@
{
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module"
},
"env": {
"node": true,
"es6": true
}
}

View file

@ -1,12 +0,0 @@
FROM node:6
MAINTAINER Petr Sloup <petr.sloup@klokantech.com>
ENV NODE_ENV="production"
EXPOSE 80
VOLUME /data
WORKDIR /data
ENTRYPOINT ["node", "/usr/src/app/", "-p", "80"]
RUN mkdir -p /usr/src/app
COPY / /usr/src/app
RUN cd /usr/src/app && npm install --production

3
Jenkinsfile vendored Normal file
View file

@ -0,0 +1,3 @@
@Library("jenkins-devops-scripts") _
tileserver-gl {
}

View file

@ -2,6 +2,7 @@ TileServer GL
============= =============
Copyright (c) 2016, Klokan Technologies GmbH Copyright (c) 2016, Klokan Technologies GmbH
Copyright (c) 2018, Beat (markers feature)
All rights reserved. All rights reserved.

View file

@ -1,48 +1,14 @@
![tileserver-gl](https://cloud.githubusercontent.com/assets/59284/18173467/fa3aa2ca-7069-11e6-86b1-0f1266befeb6.jpeg)
# TileServer GL # TileServer GL
[![Build Status](https://travis-ci.org/klokantech/tileserver-gl.svg?branch=master)](https://travis-ci.org/klokantech/tileserver-gl)
[![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/klokantech/tileserver-gl/)
Vector and raster maps with GL styles. Server side rendering by Mapbox GL Native. Map tile server for Mapbox GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc. Vector and raster maps with GL styles. Server side rendering by Mapbox GL Native. Map tile server for Mapbox GL JS, Android, iOS, Leaflet, OpenLayers, GIS via WMTS, etc.
## Get Started This fork adds features used by BEAT, including:
Make sure you have Node.js version **6** installed (running `node -v` it should output something like `v6.11.3`). * Marker support in the static (rendered) maps
* Prometheus compatible `/metrics` endpoint
* Improved `/health` endpoint
Install `tileserver-gl` with server-side raster rendering of vector tiles with npm
```bash
npm install -g tileserver-gl
```
Now download vector tiles from [OpenMapTiles](https://openmaptiles.org/downloads/).
```bash
curl -o zurich_switzerland.mbtiles https://[GET-YOUR-LINK]/extracts/zurich_switzerland.mbtiles
```
Start `tileserver-gl` with the downloaded vector tiles.
```bash
tileserver-gl zurich_switzerland.mbtiles
```
Alternatively, you can use the `tileserver-gl-light` package instead, which is pure javascript (does not have any native dependencies) and can run anywhere, but does not contain rasterization on the server side made with MapBox GL Native.
## Using Docker
An alternative to npm to start the packed software easier is to install [Docker](http://www.docker.com/) on your computer and then run in the directory with the downloaded MBTiles the command:
```bash
docker run --rm -it -v $(pwd):/data -p 8080:80 klokantech/tileserver-gl
```
This will download and start a ready to use container on your computer and the maps are going to be available in webbrowser on localhost:8080.
On laptop you can use [Docker Kitematic](https://kitematic.com/) and search "tileserver-gl" and run it, then drop in the 'data' folder the MBTiles.
## Documentation ## Documentation
You can read full documentation of this project at http://tileserver.readthedocs.io/. You can read full documentation of the upstream project at http://tileserver.readthedocs.io/.

View file

@ -1,17 +0,0 @@
# TileServer GL light
[![Build Status](https://travis-ci.org/klokantech/tileserver-gl.svg?branch=master)](https://travis-ci.org/klokantech/tileserver-gl)
[![Docker Hub](https://img.shields.io/badge/docker-hub-blue.svg)](https://hub.docker.com/r/klokantech/tileserver-gl/)
Vector maps with GL styles. Map tile server for Mapbox Android, iOS, GL JS, Leaflet, OpenLayers, etc. without server side rendering.
## Quickstart
Use `npm install -g tileserver-gl-light` to install the package from npm.
Then you can simply run `tileserver-gl-light zurich_switzerland.mbtiles` to start the server for the given mbtiles.
See also `tileserver-gl` which contains server side rendering.
Prepared vector tiles can be downloaded from [OSM2VectorTiles](http://osm2vectortiles.org/).
## Documentation
You can read full documentation of this project at http://tileserver.readthedocs.io/.

View file

@ -19,13 +19,13 @@
"test": "mocha test/**.js --timeout 10000" "test": "mocha test/**.js --timeout 10000"
}, },
"dependencies": { "dependencies": {
"@mapbox/mapbox-gl-native": "3.5.4", "@mapbox/mapbox-gl-native": "3.5.8",
"@mapbox/mbtiles": "0.9.0", "@mapbox/mbtiles": "0.9.0",
"@mapbox/sphericalmercator": "1.0.5", "@mapbox/sphericalmercator": "1.0.5",
"@mapbox/vector-tile": "1.3.0", "@mapbox/vector-tile": "1.3.0",
"advanced-pool": "0.3.3", "advanced-pool": "0.3.3",
"base64url": "2.0.0", "base64url": "2.0.0",
"canvas": "1.6.8", "canvas": "^1.6.8",
"clone": "2.1.1", "clone": "2.1.1",
"color": "1.0.3", "color": "1.0.3",
"commander": "2.1.0", "commander": "2.1.0",
@ -35,15 +35,25 @@
"handlebars": "4.0.11", "handlebars": "4.0.11",
"http-shutdown": "^1.2.0", "http-shutdown": "^1.2.0",
"morgan": "1.9.0", "morgan": "1.9.0",
"nomnom": "1.8.1",
"npm": "^5.8.0",
"pbf": "3.0.5", "pbf": "3.0.5",
"proj4": "2.4.4", "proj4": "2.4.4",
"prom-client": "11.0.0",
"request": "2.83.0", "request": "2.83.0",
"sharp": "0.18.2", "sharp": "^0.20.0",
"tileserver-gl-styles": "1.2.0" "tileserver-gl-styles": "1.2.0"
}, },
"devDependencies": { "devDependencies": {
"eslint-config-standard": "^11.0.0",
"eslint-plugin-import": "^2.11.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.7.0",
"eslint-plugin-standard": "^3.1.0",
"mocha": "^3.2.0",
"should": "^11.2.0", "should": "^11.2.0",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"supertest": "^3.0.0" "supertest": "^3.0.0",
"eslint": "^4.19.1"
} }
} }

42
src/avgresp.js Normal file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env node
'use strict';
var prometheus = require('prom-client');
module.exports = {
avgresp: avgresp
};
var history;
var samples;
var fullhistory = false;
/**
* Create middleware to record and output average response times
* @param {Object} options
* @return {function}
*/
function avgresp(options) {
var opts = options || {};
history = 50;
samples = new Array(history);
var currentIndex = 0;
const respSummary = new prometheus.Summary({
name: "tileserver_static_latency_seconds",
help: "The tileserver response time in seconds"
});
return function avgresp(req, res, next) {
var end = respSummary.startTimer();
res.on('finish', function() {
end();
})
next();
}
}

View file

@ -1,31 +1,30 @@
'use strict'; 'use strict';
var advancedPool = require('advanced-pool'), let advancedPool = require('advanced-pool');
fs = require('fs'), let fs = require('fs');
path = require('path'), let path = require('path');
url = require('url'), let url = require('url');
util = require('util'), let util = require('util');
zlib = require('zlib'); let zlib = require('zlib');
// sharp has to be required before node-canvas // sharp has to be required before node-canvas
// see https://github.com/lovell/sharp/issues/371 // see https://github.com/lovell/sharp/issues/371
var sharp = require('sharp'); let sharp = require('sharp');
let Canvas = require('canvas');
let clone = require('clone');
let Color = require('color');
let express = require('express');
let mercator = new (require('@mapbox/sphericalmercator'))();
let mbgl = require('@mapbox/mapbox-gl-native');
let mbtiles = require('@mapbox/mbtiles');
let proj4 = require('proj4');
let request = require('request');
var Canvas = require('canvas'), let utils = require('./utils');
clone = require('clone'), let markerSize = 15;
Color = require('color'), let FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
express = require('express'),
mercator = new (require('@mapbox/sphericalmercator'))(),
mbgl = require('@mapbox/mapbox-gl-native'),
mbtiles = require('@mapbox/mbtiles'),
proj4 = require('proj4'),
request = require('request');
var utils = require('./utils'); let getScale = function(scale) {
var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
var getScale = function(scale) {
return (scale || '@1x').slice(1, 2) | 0; return (scale || '@1x').slice(1, 2) | 0;
}; };
@ -38,18 +37,18 @@ mbgl.on('message', function(e) {
/** /**
* Lookup of sharp output formats by file extension. * Lookup of sharp output formats by file extension.
*/ */
var extensionToFormat = { let extensionToFormat = {
'.jpg': 'jpeg', '.jpg': 'jpeg',
'.jpeg': 'jpeg', '.jpeg': 'jpeg',
'.png': 'png', '.png': 'png',
'.webp': 'webp' '.webp': 'webp',
}; };
/** /**
* Cache of response data by sharp output format and color. Entry for empty * Cache of response data by sharp output format and color. Entry for empty
* string is for unknown or unsupported formats. * string is for unknown or unsupported formats.
*/ */
var cachedEmptyResponses = { let cachedEmptyResponses = {
'': new Buffer(0) '': new Buffer(0)
}; };
@ -61,26 +60,9 @@ var cachedEmptyResponses = {
*/ */
function createEmptyResponse(format, color, callback) { function createEmptyResponse(format, color, callback) {
if (!format || format === 'pbf') { if (!format || format === 'pbf') {
callback(null, {data: cachedEmptyResponses['']}); callback(null, { data: cachedEmptyResponses[''] });
return; return;
} }
if (format === 'jpg') {
format = 'jpeg';
}
if (!color) {
color = 'rgba(255,255,255,0)';
}
var cacheKey = format + ',' + color;
var data = cachedEmptyResponses[cacheKey];
if (data) {
callback(null, {data: data});
return;
}
// create an "empty" response image
var color = new Color(color);
var array = color.array(); var array = color.array();
var channels = array.length == 4 && format != 'jpeg' ? 4 : 3; var channels = array.length == 4 && format != 'jpeg' ? 4 : 3;
sharp(new Buffer(array), { sharp(new Buffer(array), {
@ -93,7 +75,7 @@ function createEmptyResponse(format, color, callback) {
if (!err) { if (!err) {
cachedEmptyResponses[cacheKey] = buffer; cachedEmptyResponses[cacheKey] = buffer;
} }
callback(null, {data: buffer}); callback(null, { data: buffer });
}); });
} }
@ -106,10 +88,10 @@ module.exports = function(options, repo, params, id, dataResolver) {
scalePattern += i.toFixed(); scalePattern += i.toFixed();
} }
scalePattern = '@[' + scalePattern + ']x'; scalePattern = '@[' + scalePattern + ']x';
4
var lastModified = new Date().toUTCString(); var lastModified = new Date().toUTCString();
var rootPath = options.paths.root; // var rootPath = options.paths.root;
var watermark = params.watermark || options.watermark; var watermark = params.watermark || options.watermark;
@ -162,9 +144,9 @@ module.exports = function(options, repo, params, id, dataResolver) {
utils.getFontsPbf( utils.getFontsPbf(
null, options.paths[protocol], fontstack, range, existingFonts null, options.paths[protocol], fontstack, range, existingFonts
).then(function(concated) { ).then(function(concated) {
callback(null, {data: concated}); callback(null, { data: concated });
}, function(err) { }, function(err) {
callback(err, {data: null}); callback(err, { data: null });
}); });
} else if (protocol == 'mbtiles') { } else if (protocol == 'mbtiles') {
var parts = req.url.split('/'); var parts = req.url.split('/');
@ -204,7 +186,7 @@ module.exports = function(options, repo, params, id, dataResolver) {
callback(null, response); callback(null, response);
}); });
} else if (protocol == 'http' || protocol == 'https') { } else if (protocol === 'http' || protocol === 'https') {
request({ request({
url: req.url, url: req.url,
encoding: null, encoding: null,
@ -263,6 +245,27 @@ module.exports = function(options, repo, params, id, dataResolver) {
styleJSON.glyphs = 'fonts://' + styleJSON.glyphs; styleJSON.glyphs = 'fonts://' + styleJSON.glyphs;
} }
var markerImages = [];
var markerImageNames = ['pickup', 'dropoff'];
var markerLoadPromise = new Promise(function(resolveCallback, rejectCallback) {
markerImageNames.forEach(function(imageName) {
fs.readFile(path.join(__dirname, "../public/resources/images/") + imageName + '-marker.png', function(err, fileData) {
if (err) {
rejectCallback(err);
}
var mkrImage = new Canvas.Image();
mkrImage.src = fileData;
markerImages.push(mkrImage);
});
});
resolveCallback();
});
var tileJSON = { var tileJSON = {
'tilejson': '2.0.0', 'tilejson': '2.0.0',
'name': styleJSON.name, 'name': styleJSON.name,
@ -385,11 +388,13 @@ module.exports = function(options, repo, params, id, dataResolver) {
width != width || height != height) { width != width || height != height) {
return res.status(400).send('Invalid size'); return res.status(400).send('Invalid size');
} }
if (format == 'png' || format == 'webp') {
} else if (format == 'jpg' || format == 'jpeg') { let formatIndex = ['jpg', 'jpeg', 'png', 'webp'].indexOf(format);
format = 'jpeg';
} else { if (formatIndex == -1) {
return res.status(400).send('Invalid format'); return res.status(400).send('Invalid format');
} else if (formatIndex < 2) {
format = 'jpeg';
} }
var pool = map.renderers[scale]; var pool = map.renderers[scale];
@ -448,11 +453,11 @@ module.exports = function(options, repo, params, id, dataResolver) {
(options.formatQuality || {})[format]; (options.formatQuality || {})[format];
if (format == 'png') { if (format == 'png') {
image.png({adaptiveFiltering: false}); image.png({ adaptiveFiltering: false });
} else if (format == 'jpeg') { } else if (format == 'jpeg') {
image.jpeg({quality: formatQuality || 80}); image.jpeg({ quality: formatQuality || 80 });
} else if (format == 'webp') { } else if (format == 'webp') {
image.webp({quality: formatQuality || 90}); image.webp({ quality: formatQuality || 90 });
} }
image.toBuffer(function(err, buffer, info) { image.toBuffer(function(err, buffer, info) {
if (!buffer) { if (!buffer) {
@ -516,6 +521,38 @@ module.exports = function(options, repo, params, id, dataResolver) {
return path; return path;
}; };
var drawMarker = function(ctx, coordinates, scale, outerColour = "rgb(0,0,0)", innerColour = "rgb(255,255,255)", outerRadius = markerSize, innerRadius = markerSize * 0.35) {
[outerRadius, innerRadius, coordinates[0], coordinates[1]].map(console.log);
outerRadius = parseInt(outerRadius);
innerRadius = parseInt(innerRadius);
let x = parseInt(coordinates[0]);
let y = parseInt(coordinates[1]);
let validParams = [outerRadius, innerRadius, x, y].reduce(function(acc, element) {
if (isNaN(element)) {
console.log("element: " + element + " is invalid.");
}
return acc && !isNaN(element);
}, true);
if (!validParams) {
console.log("invalid parameters!");
}
// outer circle.
ctx.beginPath();
ctx.arc(x, y, outerRadius, 0, 2 * Math.PI, false);
ctx.fillStyle = outerColour;
ctx.fill();
// inner circle.
ctx.beginPath();
ctx.arc(x, y, innerRadius, 0, 2 * Math.PI, false);
ctx.fillStyle = innerColour;
ctx.fill();
}
var renderOverlay = function(z, x, y, bearing, pitch, w, h, scale, var renderOverlay = function(z, x, y, bearing, pitch, w, h, scale,
path, query) { path, query) {
if (!path || path.length < 2) { if (!path || path.length < 2) {
@ -568,6 +605,12 @@ module.exports = function(options, repo, params, id, dataResolver) {
ctx.stroke(); ctx.stroke();
} }
if (query.showMarkers && query.showMarkers == 1) {
// Add the markers, if requested to do so.
drawMarker(ctx,precisePx(path[path.length-1],z),scale, "rgba(179, 0, 0, 0.7)");
drawMarker(ctx,precisePx(path[0],z),scale, "rgba(0, 151, 25, 0.7)");
}
return canvas.toBuffer(); return canvas.toBuffer();
}; };
@ -595,7 +638,7 @@ module.exports = function(options, repo, params, id, dataResolver) {
if (options.serveStaticMaps !== false) { if (options.serveStaticMaps !== false) {
var staticPattern = var staticPattern =
'/' + id + '/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' + '/' + id + '/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' +
':scale(' + scalePattern + ')?\.:format([\\w]+)'; ':scale(' + scalePattern + ')?.:format([\\w]+)';
var centerPattern = var centerPattern =
util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?',
@ -615,7 +658,7 @@ module.exports = function(options, repo, params, id, dataResolver) {
format = req.params.format; format = req.params.format;
if (z < 0) { if (z < 0) {
return res.status(404).send('Invalid zoom'); return res.status(400).send('Invalid zoom');
} }
var transformer = raw ? var transformer = raw ?
@ -751,7 +794,7 @@ module.exports = function(options, repo, params, id, dataResolver) {
return res.send(info); return res.send(info);
}); });
return Promise.all([fontListingPromise, renderersReadyPromise]).then(function() { return Promise.all([markerLoadPromise, fontListingPromise, renderersReadyPromise]).then(function() {
return app; return app;
}); });

View file

@ -21,7 +21,11 @@ var packageJson = require('../package'),
serve_rendered = null, serve_rendered = null,
serve_style = require('./serve_style'), serve_style = require('./serve_style'),
serve_data = require('./serve_data'), serve_data = require('./serve_data'),
utils = require('./utils'); utils = require('./utils'),
avgresp = require('./avgresp'),
prometheus = require('prom-client');
prometheus.collectDefaultMetrics({ timeout: 5000 });
var isLight = packageJson.name.slice(-6) == '-light'; var isLight = packageJson.name.slice(-6) == '-light';
if (!isLight) { if (!isLight) {
@ -52,6 +56,8 @@ function start(opts) {
})); }));
} }
app.use(/\/styles\/.*\/static\/.*$/, avgresp.avgresp());
var config = opts.config || null; var config = opts.config || null;
var configPath = null; var configPath = null;
if (opts.configPath) { if (opts.configPath) {
@ -391,12 +397,30 @@ function start(opts) {
console.log('Startup complete'); console.log('Startup complete');
startupComplete = true; startupComplete = true;
}); });
app.get('/health', function(req, res, next) { app.get('/health', function(req, res, next) {
var healthTemplate = {
"service": "tileserver",
"status": "200",
"message": "OK"
};
var statusCode = 200;
if (startupComplete) { if (startupComplete) {
return res.status(200).send('OK'); statusCode = 200;
healthTemplate.message = "OK";
} else { } else {
return res.status(503).send('Starting'); statusCode = 503;
healthTemplate.message = "Starting";
} }
healthTemplate.status = statusCode;
return res.status(statusCode).send(healthTemplate);
});
app.get('/metrics', function(req, res, next){
res.end(prometheus.register.metrics());
}); });
var server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() { var server = app.listen(process.env.PORT || opts.port, process.env.BIND || opts.bind, function() {

View file

@ -49,7 +49,7 @@ describe('Static endpoints', function() {
testStatic(prefix, '0,0,0/256x256', 'gif', 400); testStatic(prefix, '0,0,0/256x256', 'gif', 400);
testStatic(prefix, '0,0,0/256x256', 'png', 404, 1); testStatic(prefix, '0,0,0/256x256', 'png', 404, 1);
testStatic(prefix, '0,0,-1/256x256', 'png', 404); testStatic(prefix, '0,0,-1/256x256', 'png', 400);
testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404); testStatic(prefix, '0,0,0/256.5x256.5', 'png', 404);
testStatic(prefix, '0,0,0,/256x256', 'png', 404); testStatic(prefix, '0,0,0,/256x256', 'png', 404);
@ -91,6 +91,11 @@ describe('Static endpoints', function() {
testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20'); testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20');
testStatic(prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20'); testStatic(prefix, 'auto/200x200', 'png', 200, 3, /image\/png/, '?path=-10,-10|-20,-20');
}); });
describe('with markers', function() {
testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20&showMarkers=1');
testStatic(prefix, 'auto/20x20', 'png', 200, 2, /image\/png/, '?path=10,10|20,20&showMarkers=0');
})
}); });
describe('invalid requests return 4xx', function() { describe('invalid requests return 4xx', function() {