tilestrata-mapnik
Version:
A TileStrata plugin for rendering tiles with mapnik
234 lines (214 loc) • 7.45 kB
JavaScript
var mapnik = require('mapnik');
var Step = require('step');
var mime = require('mime')
var MapnikSource = require('./mapnik_backend');
var EARTH_RADIUS = 6378137;
var EARTH_DIAMETER = EARTH_RADIUS * 2;
var EARTH_CIRCUMFERENCE = EARTH_DIAMETER * Math.PI;
var MAX_RES = EARTH_CIRCUMFERENCE / 256;
var ORIGIN_SHIFT = EARTH_CIRCUMFERENCE/2;
exports['calculateMetatile'] = calculateMetatile;
function calculateMetatile(options) {
var z = +options.z, x = +options.x, y = +options.y;
var total = 1 << z;
var resolution = MAX_RES / total;
// Make sure we start at a metatile boundary.
x -= x % options.metatile;
y -= y % options.metatile;
// Make sure we don't calculcate a metatile that is larger than the bounds.
var metaWidth = Math.min(options.metatile, total, total - x);
var metaHeight = Math.min(options.metatile, total, total - y);
// Generate all tile coordinates that are within the metatile.
var tiles = [];
for (var dx = 0; dx < metaWidth; dx++) {
for (var dy = 0; dy < metaHeight; dy++) {
tiles.push([ z, x + dx, y + dy ]);
}
}
var minx = (x * 256) * resolution - ORIGIN_SHIFT;
var miny = -((y + metaHeight) * 256) * resolution + ORIGIN_SHIFT;
var maxx = ((x + metaWidth) * 256) * resolution - ORIGIN_SHIFT;
var maxy = -((y * 256) * resolution - ORIGIN_SHIFT);
return {
width: metaWidth * options.tileSize,
height: metaHeight * options.tileSize,
x: x, y: y,
tiles: tiles,
bbox: [ minx, miny, maxx, maxy ]
};
}
exports['sliceMetatile'] = sliceMetatile;
function sliceMetatile(source, image, options, meta, callback) {
var tiles = {};
Step(function() {
var group = this.group();
meta.tiles.forEach(function(c) {
var next = group();
var key = [options.format, c[0], c[1], c[2]].join(',');
getImage(source,
image,
options,
(c[1] - meta.x) * options.tileSize,
(c[2] - meta.y) * options.tileSize,
function(err, image) {
tiles[key] = {
image: image,
headers: options.headers
};
next();
});
});
}, function(err) {
if (err) return callback(err);
callback(null, tiles);
});
}
exports['encodeSingleTile'] = encodeSingleTile;
function encodeSingleTile(source, image, options, meta, callback) {
var tiles = {};
var key = [options.format, options.z, options.x, options.y].join(',');
getImage(source, image, options, 0, 0, function(err, image) {
if (err) return callback(err);
tiles[key] = { image: image, headers: options.headers };
callback(null, tiles);
});
}
function getImage(source, image, options, x, y, callback) {
var view = image.view(x, y, options.tileSize, options.tileSize);
view.isSolid(function(err, solid, pixel) {
if (err) return callback(err);
var pixel_key = '';
if (solid) {
if (options.format === 'utf') {
// TODO https://github.com/mapbox/tilelive-mapnik/issues/56
pixel_key = pixel.toString();
} else {
// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Operators/Bitwise_Operators
var a = (pixel>>>24) & 0xff;
var r = pixel & 0xff;
var g = (pixel>>>8) & 0xff;
var b = (pixel>>>16) & 0xff;
pixel_key = options.format + r +','+ g + ',' + b + ',' + a;
}
}
// Add stats.
options.source._stats.total++;
if (solid !== false) options.source._stats.solid++;
if (solid !== false && image.painted()) options.source._stats.solidPainted++;
// If solid and image buffer is cached skip image encoding.
if (solid && source.solidCache[pixel_key]) return callback(null, source.solidCache[pixel_key]);
// Note: the second parameter is needed for grid encoding.
options.source._stats.encoded++;
try {
if (options.format == 'utf') {
view.encode(options, function(err, buffer) {
if (err) {
return callback(err);
}
if (solid !== false) {
// @TODO for 'utf' this attaches an extra, bogus 'solid' key to
// to the grid as it is not a buffer but an actual JS object.
// Fix is to propagate a third parameter through callbacks all
// the way back to tilelive source #getGrid.
buffer.solid = pixel_key;
source.solidCache[pixel_key] = buffer;
}
return callback(null, buffer);
});
} else {
view.encode(options.format, options, function(err, buffer) {
if (err) {
return callback(err);
}
if (solid !== false) {
// @TODO for 'utf' this attaches an extra, bogus 'solid' key to
// to the grid as it is not a buffer but an actual JS object.
// Fix is to propagate a third parameter through callbacks all
// the way back to tilelive source #getGrid.
buffer.solid = pixel_key;
source.solidCache[pixel_key] = buffer;
}
return callback(null, buffer);
});
}
} catch (err) {
return callback(err);
}
});
}
// Render png/jpg/tif image or a utf grid and return an encoded buffer
MapnikSource.prototype._renderMetatile = function(options, callback) {
var source = this;
// Calculate bbox from xyz, respecting metatile settings.
var meta = calculateMetatile(options);
// Set default options.
if (options.format === 'utf') {
options.layer = source._info.interactivity_layer;
options.fields = source._info.interactivity_fields;
options.resolution = source._uri.query.resolution;
options.headers = { 'Content-Type': 'application/json' };
var image = new mapnik.Grid(meta.width, meta.height);
} else {
// NOTE: formats use mapnik syntax like `png8:m=h` or `jpeg80`
// so we need custom handling for png/jpeg
if (options.format.indexOf('png') != -1) {
options.headers = { 'Content-Type': 'image/png' };
} else if (options.format.indexOf('jpeg') != -1 ||
options.format.indexOf('jpg') != -1) {
options.headers = { 'Content-Type': 'image/jpeg' };
} else {
// will default to 'application/octet-stream' if unable to detect
options.headers = { 'Content-Type': mime.getType(options.format.split(':')[0]) };
}
var image = new mapnik.Image(meta.width, meta.height);
}
options.scale = +source._uri.query.scale;
// Add reference to the source allowing debug/stat reporting to be compiled.
options.source = source;
process.nextTick(function() {
// acquire can throw if pool is draining
try {
source._pool.acquire(function(err, map) {
if (err) {
return callback(err);
}
// Begin at metatile boundary.
options.x = meta.x;
options.y = meta.y;
options.variables = { zoom: options.z };
map.resize(meta.width, meta.height);
map.extent = meta.bbox;
try {
source._stats.render++;
map.render(image, options, function(err, image) {
process.nextTick(function() {
// Release after the .render() callback returned
// to avoid mapnik errors.
source._pool.release(map);
});
if (err) return callback(err);
if (meta.tiles.length > 1) {
sliceMetatile(source, image, options, meta, callback);
} else {
encodeSingleTile(source, image, options, meta, callback);
}
});
} catch(err) {
process.nextTick(function() {
// Release after the .render() callback returned
// to avoid mapnik errors.
source._pool.release(map);
});
return callback(err);
}
});
} catch (err) {
return callback(err);
}
});
// Return a list of all the tile coordinates that are being rendered
// as part of this metatile.
return meta.tiles.map(function(tile) {
return options.format + ',' + tile.join(',');
});
};