3 var crypto = require("crypto"),
4 path = require("path"),
6 util = require("util");
8 var abaculus = require("abaculus"),
9 cachecache = require("cachecache"),
10 clone = require("clone"),
11 debug = require("debug"),
12 express = require("express"),
13 handlebars = require("handlebars"),
14 mercator = new (require("sphericalmercator"))();
16 var tessera = require("./index");
18 debug = debug("tessera");
20 var FLOAT_PATTERN = "[+-]?(?:\\d+|\\d+\.?\\d+)";
21 var SCALE_PATTERN = "@[23]x";
23 // TODO a more complete implementation of this exists...somewhere
24 var getExtension = function(format) {
25 // trim PNG variant info
26 switch ((format || "").replace(/^(png).*/, "$1")) {
35 var getScale = function(scale) {
36 return (scale || "@1x").slice(1, 2) | 0;
39 var getTileUrls = function(domains, host, path, tilePath, format, key, protocol) {
40 domains = domains && domains.length > 0 ? domains : [host];
41 var query = (key && key.length > 0) ? ("?key=" + key) : "";
46 if (protocol != "https") {
51 domains.forEach(function(domain) {
52 uris.push(protocol + "://" + domain + path +
53 tilePath.replace("{format}", getExtension(format)).replace(/\/+/g, "/") +
60 var normalizeHeaders = function(headers) {
63 Object.keys(headers).forEach(function(x) {
64 _headers[x.toLowerCase()] = headers[x];
70 var md5sum = function(data) {
71 var hash = crypto.createHash("md5");
76 module.exports = function(tilelive, options) {
77 var app = express().disable("x-powered-by"),
81 tilePath = "/{z}/{x}/{y}.{format}",
84 app.use(cachecache());
86 if (typeof options === "object") {
88 tilePath = options.tilePath || tilePath;
90 if (options.domains && options.domains.length > 0) {
91 domains = options.domains.split(',');
94 Object.keys(options.headers || {}).forEach(function(name) {
95 templates[name] = handlebars.compile(options.headers[name]);
97 // attempt to parse so we can fail fast
101 console.error("'%s' header is invalid:", name);
102 console.error(e.message);
108 if (typeof uri === "string") {
109 uri = url.parse(uri, true);
114 tilePattern = tilePath
115 .replace(/\.(?!.*\.)/, ":scale(" + SCALE_PATTERN + ")?.")
116 .replace(/\./g, "\.")
117 .replace("{z}", ":z(\\d+)")
118 .replace("{x}", ":x(\\d+)")
119 .replace("{y}", ":y(\\d+)")
120 .replace("{format}", ":format([\\w\\.]+)");
122 var populateHeaders = function(headers, params, extras) {
123 Object.keys(extras || {}).forEach(function(k) {
124 params[k] = extras[k];
127 Object.keys(templates).forEach(function(name) {
128 var val = templates[name](params);
131 headers[name.toLowerCase()] = val;
145 [2, 3].forEach(function(scale) {
146 var retinaURI = clone(uri);
148 retinaURI.query.scale = scale;
149 // explicitly tell tilelive-mapnik to use larger tiles
150 retinaURI.query.tileSize = scale * 256;
152 sourceURIs[scale] = retinaURI;
155 var getTile = function(z, x, y, scale, format, callback) {
156 var sourceURI = sourceURIs[scale],
168 return tilelive.load(sourceURI, function(err, source) {
170 return callback(err);
173 return tessera.getInfo(source, function(err, info) {
175 return callback(err);
178 // validate format / extension
179 var ext = getExtension(info.format);
181 if (ext !== format) {
182 debug("Invalid format '%s', expected '%s'", format, ext);
183 return callback(null, null, populateHeaders({}, params, { 404: true, invalidFormat: true }));
187 if (z < info.minzoom || z > info.maxzoom) {
188 debug("Invalid zoom:", z);
189 return callback(null, null, populateHeaders({}, params, { 404: true, invalidZoom: true }));
192 // validate coords against bounds
193 var xyz = mercator.xyz(info.bounds, z);
199 debug("Invalid coordinates: %d,%d relative to bounds:", x, y, xyz);
200 return callback(null, null, populateHeaders({}, params, { 404: true, invalidCoordinates: true }));
203 return source.getTile(z, x, y, function(err, data, headers) {
204 headers = normalizeHeaders(headers || {});
207 if (err.message.match(/Tile|Grid does not exist/)) {
208 return callback(null, null, populateHeaders(headers, params, { 404: true }));
211 return callback(err);
214 if (data === null || data === undefined) {
215 return callback(null, null, populateHeaders(headers, params, { 404: true }));
218 if (!headers["content-md5"]) {
219 headers["content-md5"] = md5sum(data).toString("base64");
222 // work-around for PBF MBTiles that don't contain appropriate headers
224 headers["content-type"] = headers["content-type"] || "application/x-protobuf";
225 headers["content-encoding"] = headers["content-encoding"] || "gzip";
228 return callback(null, data, populateHeaders(headers, params, { 200: true }));
234 app.get(tilePattern, function(req, res, next) {
235 var z = req.params.z | 0,
236 x = req.params.x | 0,
237 y = req.params.y | 0,
238 scale = getScale(req.params.scale),
239 format = req.params.format;
240 return getTile(z, x, y, scale, format, function(err, data, headers) {
245 return res.status(404).send('Not found');
248 return res.status(200).send(data);
253 var processStaticMap = function(areaParams, req, res, next) {
254 var scale = getScale(req.params.scale),
255 format = req.params.format,
257 zoom: req.params.z | 0,
259 bbox: areaParams.bbox,
260 center: areaParams.center,
262 getTile: function(z, x, y, callback) {
263 return getTile(z, x, y, scale, format, function(err, data, headers) {
264 if (!err && data == null) {
265 err = new Error('Not found');
268 callback(err, data, headers);
272 return abaculus(params, function(err, data, headers) {
273 if (err && !err.status) {
277 res.status((err && err.status) || 200);
278 return res.send((err && err.message) || data);
282 var staticPattern = "/static/%s:scale(" + SCALE_PATTERN + ")?\.:format([\\w\\.]+)";
284 var centerPattern = util.format(':lon(%s),:lat(%s),:z(\\d+)/:width(\\d+)x:height(\\d+)',
285 FLOAT_PATTERN, FLOAT_PATTERN);
287 app.get(util.format(staticPattern, centerPattern), function(req, res, next) {
288 return processStaticMap({
292 w: req.params.width | 0,
293 h: req.params.height | 0
298 var boundsPattern = util.format(':minx(%s),:miny(%s),:maxx(%s),:maxy(%s)/:z(\\d+)',
299 FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN, FLOAT_PATTERN);
301 app.get(util.format(staticPattern, boundsPattern), function(req, res, next) {
302 return processStaticMap({
312 app.get("/index.json", function(req, res, next) {
317 return tilelive.load(uri, function(err, source) {
322 return tessera.getInfo(source, function(err, info) {
327 var prefix = path.dirname(req.originalUrl);
328 if (prefix.length > 1) {
329 info.basename = prefix.substr(1);
332 info.tiles = getTileUrls(domains, req.headers.host, prefix,
333 tilePath, info.format,
334 req.query.key, req.protocol);
335 info.tilejson = "2.0.0";
337 res.set(populateHeaders({}, params, { 200: true }));
338 return res.send(info);
346 module.exports.getTileUrls = getTileUrls;