diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..08e6489 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,11 @@ +{ + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "env": { + "node": true, + "es6": true + } +} \ No newline at end of file diff --git a/Dockerfile_light b/Dockerfile_light deleted file mode 100644 index b3926ee..0000000 --- a/Dockerfile_light +++ /dev/null @@ -1,12 +0,0 @@ -FROM node:6 -MAINTAINER Petr Sloup - -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 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..655f05b --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,3 @@ +@Library("jenkins-devops-scripts") _ +tileserver-gl { +} diff --git a/LICENSE.md b/LICENSE.md index 60d6118..1160c6d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,6 +2,7 @@ TileServer GL ============= Copyright (c) 2016, Klokan Technologies GmbH +Copyright (c) 2018, Beat (markers feature) All rights reserved. diff --git a/README.md b/README.md index 044b31e..3b0d048 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,14 @@ -![tileserver-gl](https://cloud.githubusercontent.com/assets/59284/18173467/fa3aa2ca-7069-11e6-86b1-0f1266befeb6.jpeg) - - # 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. -## 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 -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/. diff --git a/README_light.md b/README_light.md deleted file mode 100644 index f31745d..0000000 --- a/README_light.md +++ /dev/null @@ -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/. \ No newline at end of file diff --git a/package.json b/package.json index 05bae9b..ff83eb3 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ "test": "mocha test/**.js --timeout 10000" }, "dependencies": { - "@mapbox/mapbox-gl-native": "3.5.4", + "@mapbox/mapbox-gl-native": "3.5.8", "@mapbox/mbtiles": "0.9.0", "@mapbox/sphericalmercator": "1.0.5", "@mapbox/vector-tile": "1.3.0", "advanced-pool": "0.3.3", "base64url": "2.0.0", - "canvas": "1.6.8", + "canvas": "^1.6.8", "clone": "2.1.1", "color": "1.0.3", "commander": "2.1.0", @@ -35,15 +35,25 @@ "handlebars": "4.0.11", "http-shutdown": "^1.2.0", "morgan": "1.9.0", + "nomnom": "1.8.1", + "npm": "^5.8.0", "pbf": "3.0.5", "proj4": "2.4.4", + "prom-client": "11.0.0", "request": "2.83.0", - "sharp": "0.18.2", + "sharp": "^0.20.0", "tileserver-gl-styles": "1.2.0" }, "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", "mocha": "^3.2.0", - "supertest": "^3.0.0" + "supertest": "^3.0.0", + "eslint": "^4.19.1" } } diff --git a/src/avgresp.js b/src/avgresp.js new file mode 100644 index 0000000..7ba796d --- /dev/null +++ b/src/avgresp.js @@ -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(); + } +} diff --git a/src/serve_rendered.js b/src/serve_rendered.js index aca8295..03ce9f4 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -1,31 +1,30 @@ 'use strict'; -var advancedPool = require('advanced-pool'), - fs = require('fs'), - path = require('path'), - url = require('url'), - util = require('util'), - zlib = require('zlib'); +let advancedPool = require('advanced-pool'); +let fs = require('fs'); +let path = require('path'); +let url = require('url'); +let util = require('util'); +let zlib = require('zlib'); // sharp has to be required before node-canvas // 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'), - clone = require('clone'), - Color = require('color'), - express = require('express'), - mercator = new (require('@mapbox/sphericalmercator'))(), - mbgl = require('@mapbox/mapbox-gl-native'), - mbtiles = require('@mapbox/mbtiles'), - proj4 = require('proj4'), - request = require('request'); +let utils = require('./utils'); +let markerSize = 15; +let FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)'; -var utils = require('./utils'); - -var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)'; - -var getScale = function(scale) { +let getScale = function(scale) { return (scale || '@1x').slice(1, 2) | 0; }; @@ -38,18 +37,18 @@ mbgl.on('message', function(e) { /** * Lookup of sharp output formats by file extension. */ -var extensionToFormat = { +let extensionToFormat = { '.jpg': 'jpeg', '.jpeg': 'jpeg', '.png': 'png', - '.webp': 'webp' + '.webp': 'webp', }; /** * Cache of response data by sharp output format and color. Entry for empty * string is for unknown or unsupported formats. */ -var cachedEmptyResponses = { +let cachedEmptyResponses = { '': new Buffer(0) }; @@ -61,26 +60,9 @@ var cachedEmptyResponses = { */ function createEmptyResponse(format, color, callback) { if (!format || format === 'pbf') { - callback(null, {data: cachedEmptyResponses['']}); + callback(null, { data: cachedEmptyResponses[''] }); 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 channels = array.length == 4 && format != 'jpeg' ? 4 : 3; sharp(new Buffer(array), { @@ -93,7 +75,7 @@ function createEmptyResponse(format, color, callback) { if (!err) { 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 = '@[' + scalePattern + ']x'; - + 4 var lastModified = new Date().toUTCString(); - var rootPath = options.paths.root; + // var rootPath = options.paths.root; var watermark = params.watermark || options.watermark; @@ -162,9 +144,9 @@ module.exports = function(options, repo, params, id, dataResolver) { utils.getFontsPbf( null, options.paths[protocol], fontstack, range, existingFonts ).then(function(concated) { - callback(null, {data: concated}); + callback(null, { data: concated }); }, function(err) { - callback(err, {data: null}); + callback(err, { data: null }); }); } else if (protocol == 'mbtiles') { var parts = req.url.split('/'); @@ -172,9 +154,9 @@ module.exports = function(options, repo, params, id, dataResolver) { var source = map.sources[sourceId]; var sourceInfo = styleJSON.sources[sourceId]; var z = parts[3] | 0, - x = parts[4] | 0, - y = parts[5].split('.')[0] | 0, - format = parts[5].split('.')[1]; + x = parts[4] | 0, + y = parts[5].split('.')[0] | 0, + format = parts[5].split('.')[1]; source.getTile(z, x, y, function(err, data, headers) { if (err) { //console.log('MBTiles error, serving empty', err); @@ -204,34 +186,34 @@ module.exports = function(options, repo, params, id, dataResolver) { callback(null, response); }); - } else if (protocol == 'http' || protocol == 'https') { + } else if (protocol === 'http' || protocol === 'https') { request({ - url: req.url, - encoding: null, - gzip: true + url: req.url, + encoding: null, + gzip: true }, function(err, res, body) { - var parts = url.parse(req.url); - var extension = path.extname(parts.pathname).toLowerCase(); - var format = extensionToFormat[extension] || ''; - if (err || res.statusCode < 200 || res.statusCode >= 300) { - // console.log('HTTP error', err || res.statusCode); - createEmptyResponse(format, '', callback); - return; - } + var parts = url.parse(req.url); + var extension = path.extname(parts.pathname).toLowerCase(); + var format = extensionToFormat[extension] || ''; + if (err || res.statusCode < 200 || res.statusCode >= 300) { + // console.log('HTTP error', err || res.statusCode); + createEmptyResponse(format, '', callback); + return; + } - var response = {}; - if (res.headers.modified) { - response.modified = new Date(res.headers.modified); - } - if (res.headers.expires) { - response.expires = new Date(res.headers.expires); - } - if (res.headers.etag) { - response.etag = res.headers.etag; - } + var response = {}; + if (res.headers.modified) { + response.modified = new Date(res.headers.modified); + } + if (res.headers.expires) { + response.expires = new Date(res.headers.expires); + } + if (res.headers.etag) { + response.etag = res.headers.etag; + } - response.data = body; - callback(null, response); + response.data = body; + callback(null, response); }); } } @@ -255,14 +237,35 @@ module.exports = function(options, repo, params, id, dataResolver) { var httpTester = /^(http(s)?:)?\/\//; if (styleJSON.sprite && !httpTester.test(styleJSON.sprite)) { styleJSON.sprite = 'sprites://' + - styleJSON.sprite - .replace('{style}', path.basename(styleFile, '.json')) - .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath))); + styleJSON.sprite + .replace('{style}', path.basename(styleFile, '.json')) + .replace('{styleJsonFolder}', path.relative(options.paths.sprites, path.dirname(styleJSONPath))); } if (styleJSON.glyphs && !httpTester.test(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 = { 'tilejson': '2.0.0', 'name': styleJSON.name, @@ -291,7 +294,7 @@ module.exports = function(options, repo, params, id, dataResolver) { var mbtilesFile = url.substring('mbtiles://'.length); var fromData = mbtilesFile[0] == '{' && - mbtilesFile[mbtilesFile.length - 1] == '}'; + mbtilesFile[mbtilesFile.length - 1] == '}'; if (fromData) { mbtilesFile = mbtilesFile.substr(1, mbtilesFile.length - 2); @@ -342,7 +345,7 @@ module.exports = function(options, repo, params, id, dataResolver) { } if (!attributionOverride && - source.attribution && source.attribution.length > 0) { + source.attribution && source.attribution.length > 0) { if (tileJSON.attribution.length > 0) { tileJSON.attribution += '; '; } @@ -371,25 +374,27 @@ module.exports = function(options, repo, params, id, dataResolver) { repo[id] = tileJSON; var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+)' + - ':scale(' + scalePattern + ')?\.:format([\\w]+)'; + ':scale(' + scalePattern + ')?\.:format([\\w]+)'; var respondImage = function(z, lon, lat, bearing, pitch, - width, height, scale, format, res, next, - opt_overlay) { + width, height, scale, format, res, next, + opt_overlay) { if (Math.abs(lon) > 180 || Math.abs(lat) > 85.06 || - lon != lon || lat != lat) { + lon != lon || lat != lat) { return res.status(400).send('Invalid center'); } if (Math.min(width, height) <= 0 || - Math.max(width, height) * scale > (options.maxSize || 2048) || - width != width || height != height) { + Math.max(width, height) * scale > (options.maxSize || 2048) || + width != width || height != height) { return res.status(400).send('Invalid size'); } - if (format == 'png' || format == 'webp') { - } else if (format == 'jpg' || format == 'jpeg') { - format = 'jpeg'; - } else { + + let formatIndex = ['jpg', 'jpeg', 'png', 'webp'].indexOf(format); + + if (formatIndex == -1) { return res.status(400).send('Invalid format'); + } else if (formatIndex < 2) { + format = 'jpeg'; } var pool = map.renderers[scale]; @@ -445,14 +450,14 @@ module.exports = function(options, repo, params, id, dataResolver) { } var formatQuality = (params.formatQuality || {})[format] || - (options.formatQuality || {})[format]; + (options.formatQuality || {})[format]; if (format == 'png') { - image.png({adaptiveFiltering: false}); + image.png({ adaptiveFiltering: false }); } else if (format == 'jpeg') { - image.jpeg({quality: formatQuality || 80}); + image.jpeg({ quality: formatQuality || 80 }); } else if (format == 'webp') { - image.webp({quality: formatQuality || 90}); + image.webp({ quality: formatQuality || 90 }); } image.toBuffer(function(err, buffer, info) { if (!buffer) { @@ -478,12 +483,12 @@ module.exports = function(options, repo, params, id, dataResolver) { } var z = req.params.z | 0, - x = req.params.x | 0, - y = req.params.y | 0, - scale = getScale(req.params.scale), - format = req.params.format; + x = req.params.x | 0, + y = req.params.y | 0, + scale = getScale(req.params.scale), + format = req.params.format; if (z < 0 || x < 0 || y < 0 || - z > 20 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) { + z > 20 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) { return res.status(404).send('Out of bounds'); } var tileSize = 256; @@ -492,7 +497,7 @@ module.exports = function(options, repo, params, id, dataResolver) { ((y + 0.5) / (1 << z)) * (256 << z) ], z); return respondImage(z, tileCenter[0], tileCenter[1], 0, 0, - tileSize, tileSize, scale, format, res, next); + tileSize, tileSize, scale, format, res, next); }); var extractPathFromQuery = function(query, transformer) { @@ -516,8 +521,40 @@ module.exports = function(options, repo, params, id, dataResolver) { 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, - path, query) { + path, query) { if (!path || path.length < 2) { return null; } @@ -550,7 +587,7 @@ module.exports = function(options, repo, params, id, dataResolver) { ctx.translate(-center[0] + w / 2, -center[1] + h / 2); } var lineWidth = query.width !== undefined ? - parseFloat(query.width) : 1; + parseFloat(query.width) : 1; ctx.lineWidth = lineWidth; ctx.strokeStyle = query.stroke || 'rgba(0,64,255,0.7)'; ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)'; @@ -560,7 +597,7 @@ module.exports = function(options, repo, params, id, dataResolver) { ctx.lineTo(px[0], px[1]); }); if (path[0][0] == path[path.length - 1][0] && - path[0][1] == path[path.length - 1][1]) { + path[0][1] == path[path.length - 1][1]) { ctx.closePath(); } ctx.fill(); @@ -568,6 +605,12 @@ module.exports = function(options, repo, params, id, dataResolver) { 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(); }; @@ -575,10 +618,10 @@ module.exports = function(options, repo, params, id, dataResolver) { var z = 25; var padding = query.padding !== undefined ? - parseFloat(query.padding) : 0.1; + parseFloat(query.padding) : 0.1; var minCorner = mercator.px([bbox[0], bbox[3]], z), - maxCorner = mercator.px([bbox[2], bbox[1]], z); + maxCorner = mercator.px([bbox[2], bbox[1]], z); var w_ = w / (1 + 2 * padding); var h_ = h / (1 + 2 * padding); @@ -594,28 +637,28 @@ module.exports = function(options, repo, params, id, dataResolver) { if (options.serveStaticMaps !== false) { var staticPattern = - '/' + id + '/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' + - ':scale(' + scalePattern + ')?\.:format([\\w]+)'; + '/' + id + '/static/:raw(raw)?/%s/:width(\\d+)x:height(\\d+)' + + ':scale(' + scalePattern + ')?.:format([\\w]+)'; var centerPattern = - util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', - FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, - FLOAT_PATTERN, FLOAT_PATTERN); + util.format(':x(%s),:y(%s),:z(%s)(@:bearing(%s)(,:pitch(%s))?)?', + FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, + FLOAT_PATTERN, FLOAT_PATTERN); app.get(util.format(staticPattern, centerPattern), function(req, res, next) { var raw = req.params.raw; var z = +req.params.z, - x = +req.params.x, - y = +req.params.y, - bearing = +(req.params.bearing || '0'), - pitch = +(req.params.pitch || '0'), - w = req.params.width | 0, - h = req.params.height | 0, - scale = getScale(req.params.scale), - format = req.params.format; + x = +req.params.x, + y = +req.params.y, + bearing = +(req.params.bearing || '0'), + pitch = +(req.params.pitch || '0'), + w = req.params.width | 0, + h = req.params.height | 0, + scale = getScale(req.params.scale), + format = req.params.format; if (z < 0) { - return res.status(404).send('Invalid zoom'); + return res.status(400).send('Invalid zoom'); } var transformer = raw ? @@ -629,16 +672,16 @@ module.exports = function(options, repo, params, id, dataResolver) { var path = extractPathFromQuery(req.query, transformer); var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, - path, req.query); + path, req.query); return respondImage(z, x, y, bearing, pitch, w, h, scale, format, - res, next, overlay); + res, next, overlay); }); var serveBounds = function(req, res, next) { var raw = req.params.raw; var bbox = [+req.params.minx, +req.params.miny, - +req.params.maxx, +req.params.maxy]; + +req.params.maxx, +req.params.maxy]; var center = [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; var transformer = raw ? @@ -655,26 +698,26 @@ module.exports = function(options, repo, params, id, dataResolver) { } var w = req.params.width | 0, - h = req.params.height | 0, - scale = getScale(req.params.scale), - format = req.params.format; + h = req.params.height | 0, + scale = getScale(req.params.scale), + format = req.params.format; var z = calcZForBBox(bbox, w, h, req.query), - x = center[0], - y = center[1], - bearing = 0, - pitch = 0; + x = center[0], + y = center[1], + bearing = 0, + pitch = 0; var path = extractPathFromQuery(req.query, transformer); var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, - path, req.query); + path, req.query); return respondImage(z, x, y, bearing, pitch, w, h, scale, format, - res, next, overlay); + res, next, overlay); }; var boundsPattern = - util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', - FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); + util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)', + FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); app.get(util.format(staticPattern, boundsPattern), serveBounds); @@ -705,11 +748,11 @@ module.exports = function(options, repo, params, id, dataResolver) { app.get(util.format(staticPattern, autoPattern), function(req, res, next) { var raw = req.params.raw; var w = req.params.width | 0, - h = req.params.height | 0, - bearing = 0, - pitch = 0, - scale = getScale(req.params.scale), - format = req.params.format; + h = req.params.height | 0, + bearing = 0, + pitch = 0, + scale = getScale(req.params.scale), + format = req.params.format; var transformer = raw ? mercator.inverse.bind(mercator) : dataProjWGStoInternalWGS; @@ -733,25 +776,25 @@ module.exports = function(options, repo, params, id, dataResolver) { ); var z = calcZForBBox(bbox, w, h, req.query), - x = center[0], - y = center[1]; + x = center[0], + y = center[1]; var overlay = renderOverlay(z, x, y, bearing, pitch, w, h, scale, - path, req.query); + path, req.query); return respondImage(z, x, y, bearing, pitch, w, h, scale, format, - res, next, overlay); + res, next, overlay); }); } app.get('/' + id + '.json', function(req, res, next) { var info = clone(tileJSON); info.tiles = utils.getTileUrls(req, info.tiles, - 'styles/' + id, info.format); + 'styles/' + id, info.format); return res.send(info); }); - return Promise.all([fontListingPromise, renderersReadyPromise]).then(function() { + return Promise.all([markerLoadPromise, fontListingPromise, renderersReadyPromise]).then(function() { return app; }); diff --git a/src/server.js b/src/server.js index db67be4..64cf3dc 100644 --- a/src/server.js +++ b/src/server.js @@ -21,7 +21,11 @@ var packageJson = require('../package'), serve_rendered = null, serve_style = require('./serve_style'), 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'; if (!isLight) { @@ -52,6 +56,8 @@ function start(opts) { })); } + app.use(/\/styles\/.*\/static\/.*$/, avgresp.avgresp()); + var config = opts.config || null; var configPath = null; if (opts.configPath) { @@ -391,12 +397,30 @@ function start(opts) { console.log('Startup complete'); startupComplete = true; }); + app.get('/health', function(req, res, next) { + + var healthTemplate = { + "service": "tileserver", + "status": "200", + "message": "OK" + }; + + var statusCode = 200; + if (startupComplete) { - return res.status(200).send('OK'); + statusCode = 200; + healthTemplate.message = "OK"; } 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() { diff --git a/test/static.js b/test/static.js index fc3b59b..ccb5aef 100644 --- a/test/static.js +++ b/test/static.js @@ -49,7 +49,7 @@ describe('Static endpoints', function() { testStatic(prefix, '0,0,0/256x256', 'gif', 400); 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,/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/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() {