WiP. Replace image markers resolution-independent Canvas versions.

This commit is contained in:
Dimosthenis Kaponis 2018-05-01 20:00:05 +03:00
parent 8cadbef61e
commit 3e84297b30
6 changed files with 194 additions and 170 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

@ -6,7 +6,7 @@ This fork adds features used by BEAT, including:
* Marker support in the static (rendered) maps
* Prometheus compatible `/metrics` endpoint
* Improved `/health/` endpoint
* Improved `/health` endpoint
## Documentation

View file

@ -36,17 +36,24 @@
"http-shutdown": "^1.2.0",
"morgan": "1.9.0",
"nomnom": "1.8.1",
"npm": "^5.7.1",
"npm": "^5.8.0",
"pbf": "3.0.5",
"proj4": "2.4.4",
"prom-client": "11.0.0",
"request": "2.83.0",
"sharp": "^0.20.0",
"tileserver-gl-styles": "1.2.0",
"prom-client": "11.0.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"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 486 B

View file

@ -1,33 +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 markerSize = 20;
var FLOAT_PATTERN = '[+-]?(?:\\d+|\\d+\.?\\d+)';
var getScale = function(scale) {
let getScale = function(scale) {
return (scale || '@1x').slice(1, 2) | 0;
};
@ -40,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)
};
@ -63,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), {
@ -95,7 +75,7 @@ function createEmptyResponse(format, color, callback) {
if (!err) {
cachedEmptyResponses[cacheKey] = buffer;
}
callback(null, {data: buffer});
callback(null, { data: buffer });
});
}
@ -108,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;
@ -164,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('/');
@ -174,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);
@ -206,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);
});
}
}
@ -257,33 +237,33 @@ 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 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) {
markerImageNames.forEach(function(imageName) {
fs.readFile(path.join(__dirname, "../public/resources/images/") + imageName + '-marker.png', function(err, fileData) {
if (err) {
rejectCallback(err);
}
if (err) {
rejectCallback(err);
}
var mkrImage = new Canvas.Image();
mkrImage.src = fileData;
markerImages.push(mkrImage);
});
var mkrImage = new Canvas.Image();
mkrImage.src = fileData;
markerImages.push(mkrImage);
});
resolveCallback();
});
resolveCallback();
});
var tileJSON = {
@ -314,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);
@ -365,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 += '; ';
}
@ -394,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];
@ -468,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) {
@ -501,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;
@ -515,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) {
@ -539,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;
}
@ -573,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)';
@ -583,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();
@ -592,17 +606,9 @@ module.exports = function(options, repo, params, id, dataResolver) {
}
if (query.showMarkers && query.showMarkers == 1) {
// Add the markers, if requested to do so.
var markers = [
[markerImages[0], precisePx(path[0],z).map(function(loc,idx){ return (idx ==1)? loc - markerSize/2: loc - markerSize/2;})],
[markerImages[1], precisePx(path[path.length-1],z).map(function(loc,idx){ return (idx == 1)?loc - markerSize/2: loc - markerSize/2;})]
];
markers.forEach(function(imgSpec){
var coordinates = imgSpec[1];
ctx.drawImage(imgSpec[0], coordinates[0], coordinates[1], markerSize, markerSize);
});
// 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();
@ -612,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);
@ -631,25 +637,25 @@ 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(400).send('Invalid zoom');
@ -666,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 ?
@ -692,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);
@ -742,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;
@ -770,21 +776,21 @@ 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);
});