X-Git-Url: https://gerrit.simantics.org/r/gitweb?a=blobdiff_plain;f=org.simantics.maps.server%2Fserver%2Ftileserver-mapnik%2Flib%2Fapp.js;fp=org.simantics.maps.server%2Fserver%2Ftileserver-mapnik%2Flib%2Fapp.js;h=f9a3bbed94dcfc4ca354a2bf02599fd3fa4f405c;hb=2529be6d456deeb07c128603ce4971f1dc29b695;hp=0000000000000000000000000000000000000000;hpb=2636fc31c16c23711cf2b06a4ae8537bba9c1d35;p=simantics%2Fdistrict.git diff --git a/org.simantics.maps.server/server/tileserver-mapnik/lib/app.js b/org.simantics.maps.server/server/tileserver-mapnik/lib/app.js new file mode 100644 index 00000000..f9a3bbed --- /dev/null +++ b/org.simantics.maps.server/server/tileserver-mapnik/lib/app.js @@ -0,0 +1,346 @@ +"use strict"; + +var crypto = require("crypto"), + path = require("path"), + url = require("url"), + util = require("util"); + +var abaculus = require("abaculus"), + cachecache = require("cachecache"), + clone = require("clone"), + debug = require("debug"), + express = require("express"), + handlebars = require("handlebars"), + mercator = new (require("sphericalmercator"))(); + +var tessera = require("./index"); + +debug = debug("tessera"); + +var FLOAT_PATTERN = "[+-]?(?:\\d+|\\d+\.?\\d+)"; +var SCALE_PATTERN = "@[23]x"; + +// TODO a more complete implementation of this exists...somewhere +var getExtension = function(format) { + // trim PNG variant info + switch ((format || "").replace(/^(png).*/, "$1")) { + case "png": + return "png"; + + default: + return format; + } +}; + +var getScale = function(scale) { + return (scale || "@1x").slice(1, 2) | 0; +}; + +var getTileUrls = function(domains, host, path, tilePath, format, key, protocol) { + domains = domains && domains.length > 0 ? domains : [host]; + var query = (key && key.length > 0) ? ("?key=" + key) : ""; + if (path == "/") { + path = ""; + } + + if (protocol != "https") { + protocol = "http"; + } + + var uris = []; + domains.forEach(function(domain) { + uris.push(protocol + "://" + domain + path + + tilePath.replace("{format}", getExtension(format)).replace(/\/+/g, "/") + + query); + }); + + return uris; +}; + +var normalizeHeaders = function(headers) { + var _headers = {}; + + Object.keys(headers).forEach(function(x) { + _headers[x.toLowerCase()] = headers[x]; + }); + + return _headers; +}; + +var md5sum = function(data) { + var hash = crypto.createHash("md5"); + hash.update(data); + return hash.digest(); +}; + +module.exports = function(tilelive, options) { + var app = express().disable("x-powered-by"), + templates = {}, + uri = options, + domains = [], + tilePath = "/{z}/{x}/{y}.{format}", + tilePattern; + + app.use(cachecache()); + + if (typeof options === "object") { + uri = options.source; + tilePath = options.tilePath || tilePath; + + if (options.domains && options.domains.length > 0) { + domains = options.domains.split(','); + } + + Object.keys(options.headers || {}).forEach(function(name) { + templates[name] = handlebars.compile(options.headers[name]); + + // attempt to parse so we can fail fast + try { + templates[name](); + } catch (e) { + console.error("'%s' header is invalid:", name); + console.error(e.message); + process.exit(1); + } + }); + } + + if (typeof uri === "string") { + uri = url.parse(uri, true); + } else { + uri = clone(uri); + } + + tilePattern = tilePath + .replace(/\.(?!.*\.)/, ":scale(" + SCALE_PATTERN + ")?.") + .replace(/\./g, "\.") + .replace("{z}", ":z(\\d+)") + .replace("{x}", ":x(\\d+)") + .replace("{y}", ":y(\\d+)") + .replace("{format}", ":format([\\w\\.]+)"); + + var populateHeaders = function(headers, params, extras) { + Object.keys(extras || {}).forEach(function(k) { + params[k] = extras[k]; + }); + + Object.keys(templates).forEach(function(name) { + var val = templates[name](params); + + if (val) { + headers[name.toLowerCase()] = val; + } + }); + + return headers; + }; + + // warm the cache + tilelive.load(uri); + + var sourceURIs = { + 1: uri + }; + + [2, 3].forEach(function(scale) { + var retinaURI = clone(uri); + + retinaURI.query.scale = scale; + // explicitly tell tilelive-mapnik to use larger tiles + retinaURI.query.tileSize = scale * 256; + + sourceURIs[scale] = retinaURI; + }); + + var getTile = function(z, x, y, scale, format, callback) { + var sourceURI = sourceURIs[scale], + params = { + tile: { + zoom: z, + x: x, + y: y, + format: format, + retina: scale > 1, + scale: scale + } + }; + + return tilelive.load(sourceURI, function(err, source) { + if (err) { + return callback(err); + } + + return tessera.getInfo(source, function(err, info) { + if (err) { + return callback(err); + } + + // validate format / extension + var ext = getExtension(info.format); + + if (ext !== format) { + debug("Invalid format '%s', expected '%s'", format, ext); + return callback(null, null, populateHeaders({}, params, { 404: true, invalidFormat: true })); + } + + // validate zoom + if (z < info.minzoom || z > info.maxzoom) { + debug("Invalid zoom:", z); + return callback(null, null, populateHeaders({}, params, { 404: true, invalidZoom: true })); + } + + // validate coords against bounds + var xyz = mercator.xyz(info.bounds, z); + + if (x < xyz.minX || + x > xyz.maxX || + y < xyz.minY || + y > xyz.maxY) { + debug("Invalid coordinates: %d,%d relative to bounds:", x, y, xyz); + return callback(null, null, populateHeaders({}, params, { 404: true, invalidCoordinates: true })); + } + + return source.getTile(z, x, y, function(err, data, headers) { + headers = normalizeHeaders(headers || {}); + + if (err) { + if (err.message.match(/Tile|Grid does not exist/)) { + return callback(null, null, populateHeaders(headers, params, { 404: true })); + } + + return callback(err); + } + + if (data === null || data === undefined) { + return callback(null, null, populateHeaders(headers, params, { 404: true })); + } + + if (!headers["content-md5"]) { + headers["content-md5"] = md5sum(data).toString("base64"); + } + + // work-around for PBF MBTiles that don't contain appropriate headers + if (ext === "pbf") { + headers["content-type"] = headers["content-type"] || "application/x-protobuf"; + headers["content-encoding"] = headers["content-encoding"] || "gzip"; + } + + return callback(null, data, populateHeaders(headers, params, { 200: true })); + }); + }); + }); + }; + + app.get(tilePattern, function(req, res, next) { + 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; + return getTile(z, x, y, scale, format, function(err, data, headers) { + if (err) { + return next(err); + } + if (data == null) { + return res.status(404).send('Not found'); + } else { + res.set(headers); + return res.status(200).send(data); + } + }, res, next); + }); + + var processStaticMap = function(areaParams, req, res, next) { + var scale = getScale(req.params.scale), + format = req.params.format, + params = { + zoom: req.params.z | 0, + scale: scale, + bbox: areaParams.bbox, + center: areaParams.center, + format: format, + getTile: function(z, x, y, callback) { + return getTile(z, x, y, scale, format, function(err, data, headers) { + if (!err && data == null) { + err = new Error('Not found'); + err.status = 404; + } + callback(err, data, headers); + }); + } + }; + return abaculus(params, function(err, data, headers) { + if (err && !err.status) { + return next(err); + } + res.set(headers); + res.status((err && err.status) || 200); + return res.send((err && err.message) || data); + }); + }; + + var staticPattern = "/static/%s:scale(" + SCALE_PATTERN + ")?\.:format([\\w\\.]+)"; + + var centerPattern = util.format(':lon(%s),:lat(%s),:z(\\d+)/:width(\\d+)x:height(\\d+)', + FLOAT_PATTERN, FLOAT_PATTERN); + + app.get(util.format(staticPattern, centerPattern), function(req, res, next) { + return processStaticMap({ + center: { + x: +req.params.lon, + y: +req.params.lat, + w: req.params.width | 0, + h: req.params.height | 0 + } + }, req, res, next); + }); + + var boundsPattern = util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)/:z(\\d+)', + FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN); + + app.get(util.format(staticPattern, boundsPattern), function(req, res, next) { + return processStaticMap({ + bbox: [ + +req.params.minx, + +req.params.miny, + +req.params.maxx, + +req.params.maxy + ] + }, req, res, next); + }); + + app.get("/index.json", function(req, res, next) { + var params = { + tileJSON: true + }; + + return tilelive.load(uri, function(err, source) { + if (err) { + return next(err); + } + + return tessera.getInfo(source, function(err, info) { + if (err) { + return next(err); + } + + var prefix = path.dirname(req.originalUrl); + if (prefix.length > 1) { + info.basename = prefix.substr(1); + } + + info.tiles = getTileUrls(domains, req.headers.host, prefix, + tilePath, info.format, + req.query.key, req.protocol); + info.tilejson = "2.0.0"; + + res.set(populateHeaders({}, params, { 200: true })); + return res.send(info); + }); + }); + }); + + return app; +}; + +module.exports.getTileUrls = getTileUrls;