diff --git a/src/serve_data.js b/src/serve_data.js index 0f76f0e..393e5e0 100644 --- a/src/serve_data.js +++ b/src/serve_data.js @@ -18,6 +18,24 @@ try { var utils = require('./utils'); +function isGzipped(data) { + return data[0] === 0x1f && data[1] === 0x8b; +} + +function unzipTile(tile) { + if (tile.isGzipped) { + tile.data = zlib.gunzipSync(tile.data); + tile.isGzipped = false; + } +} + +function zipTile(tile) { + if (!tile.isGzipped) { + tile.data = zlib.gzipSync(tile.data); + tile.isGzipped = true; + } +} + module.exports = function(options, repo, params, id, styles) { var app = express().disable('x-powered-by'); @@ -28,10 +46,29 @@ module.exports = function(options, repo, params, id, styles) { var shrinkers = {}; + function lookupShrinker(style) { + if (shrinkers[style]) { + return shrinkers[style]; + } + var styleJSON = styles[style]; + if (!styleJSON) { + return; + } + var sourceName = null; + for (var sourceName_ in styleJSON.sources) { + var source = styleJSON.sources[sourceName_]; + if (source && source.type == 'vector' && source.url.endsWith('/' + id + '.json')) { + sourceName = sourceName_; + } + } + shrinkers[style] = tileshrinkGl.createPBFShrinker(styleJSON, sourceName); + return shrinkers[style]; + } + repo[id] = tileJSON; var mbtilesFileStats = fs.statSync(mbtilesFile); - if (!mbtilesFileStats.isFile() || mbtilesFileStats.size == 0) { + if (!mbtilesFileStats.isFile() || mbtilesFileStats.size === 0) { throw Error('Not valid MBTiles file: ' + mbtilesFile); } var source = new mbtiles(mbtilesFile, function(err) { @@ -53,7 +90,7 @@ module.exports = function(options, repo, params, id, styles) { var tilePattern = '/' + id + '/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w.]+)'; - app.get(tilePattern, function(req, res, next) { + function checkParams(req, res, next) { var z = req.params.z | 0, x = req.params.x | 0, y = req.params.y | 0; @@ -70,87 +107,108 @@ module.exports = function(options, repo, params, id, styles) { x >= Math.pow(2, z) || y >= Math.pow(2, z)) { return res.status(404).send('Out of bounds'); } - source.getTile(z, x, y, function(err, data, headers) { + req.params.format = format; + req.params.z = z; + req.params.x = x; + req.params.y = y; + next(); + } + + function getTile(req, res, next) { + var p = req.params; + source.getTile(p.z, p.x, p.y, function(err, data, headers) { if (err) { - if (/does not exist/.test(err.message)) { - return res.status(404).send(err.message); - } else { - return res.status(500).send(err.message); - } - } else { - if (data == null) { - return res.status(404).send('Not found'); - } else { - if (tileJSON['format'] == 'pbf') { - var isGzipped = data.slice(0,2).indexOf( - new Buffer([0x1f, 0x8b])) === 0; - var style = req.query.style; - if (style && tileshrinkGl) { - if (!shrinkers[style]) { - var styleJSON = styles[style]; - if (styleJSON) { - var sourceName = null; - for (var sourceName_ in styleJSON.sources) { - var source = styleJSON.sources[sourceName_]; - if (source && - source.type == 'vector' && - source.url.endsWith('/' + id + '.json')) { - sourceName = sourceName_; - } - } - shrinkers[style] = tileshrinkGl.createPBFShrinker(styleJSON, sourceName); - } - } - if (shrinkers[style]) { - if (isGzipped) { - data = zlib.unzipSync(data); - isGzipped = false; - } - data = shrinkers[style](data, z, tileJSON.maxzoom); - //console.log(shrinkers[style].getStats()); - } - } - } - if (format == 'pbf') { - headers['Content-Type'] = 'application/x-protobuf'; - } else if (format == 'geojson') { - headers['Content-Type'] = 'application/json'; - - if (isGzipped) { - data = zlib.unzipSync(data); - isGzipped = false; - } - - var tile = new VectorTile(new pbf(data)); - var geojson = { - "type": "FeatureCollection", - "features": [] - }; - for (var layerName in tile.layers) { - var layer = tile.layers[layerName]; - for (var i = 0; i < layer.length; i++) { - var feature = layer.feature(i); - var featureGeoJSON = feature.toGeoJSON(x, y, z); - featureGeoJSON.properties.layer = layerName; - geojson.features.push(featureGeoJSON); - } - } - data = JSON.stringify(geojson); - } - delete headers['ETag']; // do not trust the tile ETag -- regenerate - headers['Content-Encoding'] = 'gzip'; - res.set(headers); - - if (!isGzipped) { - data = zlib.gzipSync(data); - isGzipped = true; - } - - return res.status(200).send(data); - } + var status = /does not exist/.test(err.message) ? 404 : 500; + return res.status(status).send(err.message); } + if (data == null) { + return res.status(404).send('Not found'); + } + req.tile = { + data: data, + headers: headers, + contentType: 'application/x-protobuf', + isGzipped: isGzipped(data) + }; + next(); }); - }); + } + + function shrinkTile(req, res, next) { + if (!tileshrinkGl) { + return next(); + } + if (tileJSON['format'] !== 'pbf') { + return next(); + } + var style = req.query.style; + if (!style) { + return next(); + } + var tile = req.tile; + var shrinker = lookupShrinker(style); + if (shrinker) { + unzipTile(tile); + tile.data = shrinker(tile.data, req.params.z, tileJSON.maxzoom); + //console.log(shrinker.getStats()); + } + next(); + } + + function formatTile(req, res, next) { + var format = req.params.format; + if (format !== 'geojson') { + return next(); + } + var tile = req.tile; + var x = req.params.x, + y = req.params.y, + z = req.params.z; + + tile.contentType = 'application/json'; + unzipTile(tile); + + var vectorTile = new VectorTile(new pbf(tile.data)); + var geojson = { + "type": "FeatureCollection", + "features": [] + }; + + for (var layerName in vectorTile.layers) { + var layer = vectorTile.layers[layerName]; + for (var i = 0; i < layer.length; i++) { + var feature = layer.feature(i); + var featureGeoJSON = feature.toGeoJSON(x, y, z); + featureGeoJSON.properties.layer = layerName; + geojson.features.push(featureGeoJSON); + } + } + + tile.data = JSON.stringify(geojson); + + next(); + } + + function sendTile(req, res) { + var headers = req.tile.headers; + + delete headers['ETag']; // do not trust the tile ETag -- regenerate + headers['Content-Type'] = req.tile.contentType; + headers['Content-Encoding'] = 'gzip'; + res.set(headers); + + zipTile(req.tile); + + return res.status(200).send(req.tile.data); + } + + app.get(tilePattern, + checkParams, + getTile, + shrinkTile, + formatTile, + sendTile + ); app.get('/' + id + '.json', function(req, res, next) { var info = clone(tileJSON);