pax_global_header00006660000000000000000000000064122142361430014510gustar00rootroot0000000000000052 comment=1bceed381bccbeb55dd8fe2f20ecd71df926b697 tilelive-mapnik-0.6.1/000077500000000000000000000000001221423614300146065ustar00rootroot00000000000000tilelive-mapnik-0.6.1/.travis.yml000066400000000000000000000004351221423614300167210ustar00rootroot00000000000000language: node_js node_js: - "0.10" - "0.8" - "0.6" before_install: - sudo apt-add-repository --yes ppa:mapnik/v2.2.0 - sudo apt-get update - sudo apt-get install libprotobuf7 libprotobuf-dev protobuf-compiler libmapnik libmapnik-dev before_script: - npm install mocha tilelive-mapnik-0.6.1/CHANGELOG.md000066400000000000000000000024671221423614300164300ustar00rootroot00000000000000# tilelive-mapnik changelog ## 0.6.1 * Fixed package.json `engines` declaration * Upgraded to node-mapnik 1.2.0 ## 0.6.0 * Upgraded to node-mapnik-1.1.2 (#68) * Moved cache of solid tiles to tilesource (rather than global) #64 * Ensured `scale` value passed to Mapnik is a number * Exposed mapnik binding as MapnikSource.mapnik ## 0.5.0 * Removed cached _xml property (#25) * Fixes to behavior of source.close() and handling of draining map pool * Improvements to setting of content-type for various output formats * Changed to async isSolid interface in node-mapnik, which requires at least node-mapnik@0.7.17 * Converted tests to use mocha, instead of expresso (#35) * Added ability to disable internal cache of source objects by pass `uri.query.internal_cache` (#59) * Changed map loading to be synchronous to avoid possibility of race condition in Mapnik (#58) ## 0.4.4 * Fixed scoping typo in close() ## 0.4.3 * Fixed formatting error when reporting out of bounds Tile coords * Properly drain pool by leveraging new `generic-pool` (#43) * Tests output image diff for any failing image comparisions ## 0.4.2 * Supports node v8 ## 0.4.1 * Supports `scale-factor` for high-dpi displays ## 0.4.0 * Merges `parameter` branch - interactivity information is now managed in `Parameter` XML elements in Mapnik XML source tilelive-mapnik-0.6.1/LICENSE000066400000000000000000000027141221423614300156170ustar00rootroot00000000000000Copyright (c), Development Seed All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name "Development Seed" nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. tilelive-mapnik-0.6.1/Makefile000066400000000000000000000005111221423614300162430ustar00rootroot00000000000000all: npm install clean: rm -rf node_modules/* ifndef only test: @PATH="./node_modules/mocha/bin:${PATH}" && NODE_PATH="./lib:$(NODE_PATH)" mocha -R spec test/ else test: @PATH="./node_modules/mocha/bin:${PATH}" && NODE_PATH="./lib:$(NODE_PATH)" mocha -R spec test/${only}.test.js endif check: npm test .PHONY: test checktilelive-mapnik-0.6.1/README.md000066400000000000000000000034511221423614300160700ustar00rootroot00000000000000# tilelive-mapnik Renderer backend for [tilelive.js](http://github.com/mapbox/tilelive.js) that uses [node-mapnik](http://github.com/mapnik/node-mapnik) to render tiles and grids from a Mapnik XML file. `tilelive-mapnik` implements the [Tilesource API](https://github.com/mapbox/tilelive.js/blob/master/API.md). [![Build Status](https://secure.travis-ci.org/mapbox/tilelive-mapnik.png)](http://travis-ci.org/mapbox/tilelive-mapnik) ## Installation npm install tilelive-mapnik Though `tilelive` is not a dependency of `tilelive-mapnik` you will want to install it to actually make use of `tilelive-mapnik` through a reasonable API. ## Usage ```javascript var tilelive = require('tilelive'); require('tilelive-mapnik').registerProtocols(tilelive); tilelive.load('mapnik:///path/to/file.xml', function(err, source) { if (err) throw err; // Interface is in XYZ/Google coordinates. // Use `y = (1 << z) - 1 - y` to flip TMS coordinates. source.getTile(0, 0, 0, function(err, tile, headers) { // `err` is an error object when generation failed, otherwise null. // `tile` contains the compressed image file as a Buffer // `headers` is a hash with HTTP headers for the image. }); // The `.getGrid` is implemented accordingly. }); ``` Note that grid generation will only work when there's metadata inside a `` object in the Mapnik XML. The key fields are `interactivity_layer` and `interactivity_fields`. See an [example in the tests](https://github.com/mapbox/tilelive-mapnik/blob/4e9cbf8347eba7c3c2b7e8fd4270ea39f9cc7af5/test/data/test.xml#L6-L7). These `Parameters` are normally added by the application that creates the XML, in this case [CartoCSS](https://github.com/mapbox/carto/blob/55fbafe0d0e8ec00515c5782a3664c15502f0437/lib/carto/renderer.js#L152-L189) tilelive-mapnik-0.6.1/lib/000077500000000000000000000000001221423614300153545ustar00rootroot00000000000000tilelive-mapnik-0.6.1/lib/lockingcache.js000066400000000000000000000043611221423614300203300ustar00rootroot00000000000000module.exports = LockingCache; function LockingCache(generate, timeout) { this.callbacks = {}; this.timeouts = {}; this.results = {}; // When there's no generator function, you this.generate = generate || function() {}; // Timeout cached objects after 1 minute by default. // A value of 0 will cause it to not cache at all beyond, just return the result to // all locked requests. this.timeout = (typeof timeout === "undefined") ? 60000 : timeout; } LockingCache.prototype.get = function(id, callback) { if (!this.callbacks[id]) this.callbacks[id] = []; this.callbacks[id].push(callback); if (this.results[id]) { this.trigger(id); } else { var ids = this.generate.call(this, id); if (!ids || ids.indexOf(id) < 0) { this.put(id, new Error("Generator didn't generate this item")); } else ids.forEach(function(id) { this.results[id] = this.results[id] || true; }, this); } }; LockingCache.prototype.del = function(id) { delete this.results[id]; delete this.callbacks[id]; if (this.timeouts[id]) { clearTimeout(this.timeouts[id]); delete this.timeouts[id]; } }; LockingCache.prototype.put = function(id) { if (this.timeout > 0) { this.timeouts[id] = setTimeout(this.del.bind(this, id), this.timeout); } this.results[id] = Array.prototype.slice.call(arguments, 1); if (this.callbacks[id] && this.callbacks[id].length) { this.trigger(id); } }; LockingCache.prototype.clear = function() { for (var id in this.timeouts) { this.del(id); } }; LockingCache.prototype.trigger = function(id) { if (this.results[id] && this.results[id] !== true) { process.nextTick(function() { var data = this.results[id]; var callbacks = this.callbacks[id] || []; if (this.timeout === 0) { // instant purge with the first put() for a key // clears timeouts and results this.del(id); } else { delete this.callbacks[id]; } callbacks.forEach(function(callback) { callback.apply(callback, data); }); }.bind(this)); } }; tilelive-mapnik-0.6.1/lib/mapnik_backend.js000066400000000000000000000307761221423614300206550ustar00rootroot00000000000000var fs = require('fs'); var crypto = require('crypto'); var Step = require('step'); var path = require('path'); var url = require('url'); var mapnik = require('mapnik'); var qs = require('querystring'); var Pool = require('generic-pool').Pool; var LockingCache = require('./lockingcache'); var cache = {}; if (process.platform !== 'win32') { var major_version = parseInt(process.versions.node.split('.')[0],10); var minor_version = parseInt(process.versions.node.split('.')[1],10); // older node versions support eio, newer need UV_THREADPOOL_SIZE set if (major_version == 0 && minor_version < 9) { // Increase number of threads to 1.5x the number of logical CPUs. var threads = Math.ceil(Math.max(4, require('os').cpus().length * 1.5)); require('eio').setMinParallel(threads); } } exports = module.exports = MapnikSource; require('util').inherits(MapnikSource, require('events').EventEmitter); function MapnikSource(uri, callback) { uri = this._normalizeURI(uri); var key = url.format(uri); if (uri.protocol && uri.protocol !== 'mapnik:') { throw new Error('Only the mapnik protocol is supported'); } // by default we use an internal self-caching mechanism but // calling applications can pass `internal_cache:false` to disable // TODO - consider removing completely once https://github.com/mapbox/tilemill/issues/1893 // is in place and a solid reference implementation of external caching if (uri.query.internal_cache === false) { this.once('open', callback); this._open(uri); } else { // https://github.com/mapbox/tilelive-mapnik/issues/47 if (!cache[key]) { cache[key] = this; this._self_cache_key = key; this._open(uri); } var source = cache[key]; source.setMaxListeners(0); if (!source.open) { source.once('open', function(err, source) { if (err) cache[key] = false; callback(err, source); }); } else { callback(null, source); } } // cache used to skip encoding of solid images this.solidCache = {}; return undefined; } MapnikSource.mapnik = mapnik; MapnikSource.prototype.toJSON = function() { return url.format(this._uri); }; MapnikSource.prototype._normalizeURI = function(uri) { if (typeof uri === 'string') uri = url.parse(uri, true); if (uri.hostname === '.' || uri.hostname == '..') { uri.pathname = uri.hostname + uri.pathname; delete uri.hostname; delete uri.host; } uri.pathname = path.resolve(uri.pathname); uri.query = uri.query || {}; if (typeof uri.query === 'string') uri.query = qs.parse(uri.query); if (typeof uri.query.internal_cache === "undefined") uri.query.internal_cache = true; if (!uri.query.base) uri.query.base = ''; if (!uri.query.metatile) uri.query.metatile = 2; if (!uri.query.resolution) uri.query.resolution = 4; if (!uri.query.bufferSize) uri.query.bufferSize = 128; if (!uri.query.tileSize) uri.query.tileSize = 256; if (!uri.query.scale) uri.query.scale = 1; return uri; }; // Finds all XML files in the filepath and returns their tilesource URI. MapnikSource.list = function(filepath, callback) { filepath = path.resolve(filepath); fs.readdir(filepath, function(err, files) { if (err) return callback(err); for (var result = {}, i = 0; i < files.length; i++) { var name = files[i].match(/^([\w-]+)\.xml$/); if (name) result[name[1]] = 'mapnik://' + path.join(filepath, name[0]); } return callback(null, result); }); }; // Finds an XML file with the given ID in the filepath and returns a // tilesource URI. MapnikSource.findID = function(filepath, id, callback) { filepath = path.resolve(filepath); var file = path.join(filepath, id + '.xml'); fs.stat(file, function(err, stats) { if (err) return callback(err); else return callback(null, 'mapnik://' + file); }); }; MapnikSource.prototype._open = function(uri) { var source = this; function error(err) { process.nextTick(function() { source.emit('open', err); }); } this._stats = { render: 0, // # of times a render is requested from mapnik total: 0, // # of tiles returned from source encoded: 0, // # of tiles encoded solid: 0, // # of tiles isSolid solidPainted: 0 // # of tiles isSolid && painted }; this._internal_cache = uri.query.internal_cache; this._base = uri.query.base; uri.query.metatile = +uri.query.metatile; uri.query.resolution = +uri.query.resolution; uri.query.bufferSize = +uri.query.bufferSize; uri.query.tileSize = +uri.query.tileSize; this._uri = uri; // Public API to announce how we're metatiling. this.metatile = uri.query.metatile; this.bufferSize = uri.query.bufferSize; // Initialize this map. This wraps `localize()` and calls `create()` // afterwards to actually create a new Mapnik map object. Step(function() { source._loadXML(this); }, function(err, xml) { if (err) return error(err); // https://github.com/mapbox/tilelive-mapnik/issues/25 // there seems to be no value to assinging xml to a property //source._xml = xml; source._createMetatileCache(); source._createPool(xml, this); }, function(err) { if (err) return error(err); source._populateInfo(this); }, function(err) { if (err) return error(err); source.open = true; source.emit('open', null, source); }); }; MapnikSource.prototype.close = function(callback) { this._close(function() { return callback(); }); }; MapnikSource.prototype._cache = cache; MapnikSource.prototype._close = function(callback) { if (cache[this._self_cache_key]) delete cache[this._self_cache_key]; if (this._tileCache) this._tileCache.clear(); // https://github.com/coopernurse/node-pool/issues/17#issuecomment-6565795 if (this._pool) { var pool = this._pool; // NOTE: .drain() and .destoryAllNow() do // not pass error args pool.drain(function() { pool.destroyAllNow(function() { return callback(); }); }); } }; MapnikSource.registerProtocols = function(tilelive) { tilelive.protocols['mapnik:'] = MapnikSource; }; // Loads the XML file from the specified path. Calls `callback` when the mapfile // can be expected to be in `mapfile`. If this isn't successful, `callback` gets // an error as its first argument. MapnikSource.prototype._loadXML = function(callback) { var source = this; this._base = path.resolve(path.dirname(this._uri.pathname)); // This is a string-based map file. Pass it on literally. if (this._uri.xml) return callback(null, this._uri.xml); // Load XML from file. fs.readFile(path.resolve(source._uri.pathname), 'utf8', function(err, xml) { if (err) return callback(err); callback(null, xml); }); }; // Create a new mapnik map object at `this.mapnik`. Requires that the mapfile // be localized with `this.localize()`. This can be called in repetition because // it won't recreate `this.mapnik`. MapnikSource.prototype._createPool = function(xml, callback) { var source = this; if (!this._pool) this._pool = Pool({ create: function(callback) { var map = new mapnik.Map(source._uri.query.tileSize, source._uri.query.tileSize); map.bufferSize = source._uri.query.bufferSize; var opts = {strict: false, base: source._base + '/'}; //https://github.com/mapbox/tilelive-mapnik/issues/58 try { map.fromStringSync(xml, opts); return callback(null, map); } catch (err) { return callback(err); } }, destroy: function(map) { delete map; }, // @TODO: need a smarter way to scale this. More // maps in pool seems better for PostGIS. max: require('os').cpus().length }); callback(null); }; MapnikSource.prototype._populateInfo = function(callback) { var source = this; var id = path.basename(this._uri.pathname, path.extname(this._uri.pathname)); this._pool.acquire(function(err, map) { if (err) return callback(err); var info = { id: id, name: id, minzoom: 0, maxzoom: 22 }; var p = map.parameters; for (var key in p) info[key] = p[key]; if (p.bounds) info.bounds = p.bounds.split(',').map(parseFloat); if (p.center) info.center = p.center.split(',').map(parseFloat); if (p.minzoom) info.minzoom = parseInt(p.minzoom, 10); if (p.maxzoom) info.maxzoom = parseInt(p.maxzoom, 10); if (p.interactivity_fields) info.interactivity_fields = p.interactivity_fields.split(','); if (!info.bounds || info.bounds.length !== 4) info.bounds = [ -180, -85.05112877980659, 180, 85.05112877980659 ]; if (!info.center || info.center.length !== 3) info.center = [ (info.bounds[2] - info.bounds[0]) / 2 + info.bounds[0], (info.bounds[3] - info.bounds[1]) / 2 + info.bounds[1], 2 ]; source._info = info; source._pool.release(map); callback(null) }); }; // Creates a locking cache that generates tiles. When requesting the same tile // multiple times, they'll be grouped to one request. MapnikSource.prototype._createMetatileCache = function() { var source = this; this._tileCache = new LockingCache(function(key) { var cache = this; var coords = key.split(','); var keys = source._renderMetatile({ metatile: source._uri.query.metatile, tileSize: source._uri.query.tileSize, format: coords[0], z: +coords[1], x: +coords[2], y: +coords[3] }, function(err, tiles) { if (err) { // Push error objects to all entries that were supposed to // be generated. keys.forEach(function(key) { cache.put(key, err); }); } else { // Put all the generated tiles into the locking cache. for (var key in tiles) { cache.put(key, null, tiles[key].image, tiles[key].headers); } } }); // Return a list of all the tile coordinates that are being rendered // as part of this metatile. return keys; }, 0); // purge immediately after callbacks }; // Render handler for a given tile request. MapnikSource.prototype.getTile = function(z, x, y, callback) { z = +z; x = +x; y = +y; if (isNaN(z) || isNaN(x) || isNaN(y)) { return callback(new Error('Invalid coordinates: '+z+'/'+x+'/'+y)); } var max = (1 << z); if (x >= max || x < 0 || y >= max || y < 0) { return callback(new Error('Coordinates out of range: '+z+'/'+x+'/'+y)); } var format = (this._info && this._info.format) || 'png'; var key = [format, z, x, y].join(','); this._tileCache.get(key, function(err, tile, headers) { if (err) return callback(err); callback(null, tile, headers); }); }; MapnikSource.prototype.getGrid = function(z, x, y, callback) { z = +z; x = +x; y = +y; if (isNaN(z) || isNaN(x) || isNaN(y)) { return callback(new Error('Invalid coordinates: '+z+'/'+x+'/'+y)); } var max = (1 << z); if (x >= max || x < 0 || y >= max || y < 0) { return callback(new Error('Coordinates out of range: '+z+'/'+x+'/'+y)); } else if (!this._info || !this._info.interactivity_fields || !this._info.interactivity_layer) { if (!this._info) { return callback(new Error('Tilesource info is missing, cannot rendering interactivity')); } else { return callback(new Error('Tileset has no interactivity')); } } else if (!mapnik.supports.grid) { return callback(new Error('Mapnik is missing grid support')); } var key = ['utf', z, x, y].join(','); this._tileCache.get(key, function(err, grid, headers) { if (err) return callback(err); delete grid.solid; callback(null, grid, headers); }); }; MapnikSource.prototype.getInfo = function(callback) { if (this._info) callback(null, this._info); else callback(new Error('Info is unavailable')); }; // Add other functions. require('./render'); tilelive-mapnik-0.6.1/lib/render.js000066400000000000000000000203621221423614300171740ustar00rootroot00000000000000var mapnik = require('mapnik'); var _ = require('underscore'); 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 = 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 { 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': 'text/javascript; charset=utf-8' }; 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.lookup(options.format) }; } 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; map.resize(meta.width, meta.height); map.extent = meta.bbox; // todo - handle per tile bufferSize: https://github.com/mapnik/node-mapnik/issues/175 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(','); }); }; tilelive-mapnik-0.6.1/package.json000066400000000000000000000021021221423614300170670ustar00rootroot00000000000000{ "name" : "tilelive-mapnik", "version" : "0.6.1", "main" : "./lib/mapnik_backend.js", "description" : "Mapnik backend for tilelive", "url" : "http://github.com/mapbox/tilelive-mapnik", "keywords" : ["map", "server", "mapnik", "tms"], "licenses" : [{ "type": "BSD" }], "repository": { "type": "git", "url": "http://github.com/mapbox/tilelive-mapnik.git" }, "contributors": [ "Tom MacWright ", "Will White ", "Dane Springmeyer ", "Young Hahn " ], "dependencies": { "step" : "~0.0.5", "underscore" : "~1.3.3", "generic-pool": "~2.0.0", "mapnik": "~1.2.0", "eio" : "~0.2.0", "mime" : "~1.2.9", "sphericalmercator": "~1.0.1" }, "devDependencies": { "mocha": "*" }, "engines": { "node": ">= 0.6.13 < 0.11.0" }, "scripts": { "test": "mocha -R spec --timeout 5000" } } tilelive-mapnik-0.6.1/test/000077500000000000000000000000001221423614300155655ustar00rootroot00000000000000tilelive-mapnik-0.6.1/test/close.test.js000066400000000000000000000056141221423614300202140ustar00rootroot00000000000000var fs = require('fs'); var assert = require('assert'); var mapnik_backend = require('..'); describe('Closing behavior ', function() { it('should close cleanly 1', function(done) { new mapnik_backend('mapnik://./test/data/world.xml', function(err, source) { if (err) throw err; var cache_len = Object.keys(source._cache).length; assert.ok(source._cache[source._self_cache_key]); // now close the source source.close(function(err){ assert.equal(err,undefined); var new_cache_length = Object.keys(source._cache).length; assert.equal(cache_len,new_cache_length+1); assert.ok(!source._cache[source._self_cache_key]); done(); }); }); }); it('should close cleanly 2', function(done) { new mapnik_backend('mapnik://./test/data/world.xml', function(err, source) { if (err) throw err; source.getTile(0,0,0, function(err, info, headers) { if (err) throw err; var cache_len = Object.keys(source._cache).length; // now close the source source.close(function(err){ assert.equal(err,undefined); var new_cache_length = Object.keys(source._cache).length; assert.equal(cache_len,new_cache_length+1); done(); }); }); }); }); it('should throw with invalid usage (close before getTile)', function(done) { new mapnik_backend('mapnik://./test/data/world.xml', function(err, source) { if (err) throw err; // now close the source // now that the pool is draining further // access to the source is invalid and should throw source.close(function(err){ // pool will be draining... }); source.getTile(0,0,0, function(err, info, headers) { assert.equal(err.message,'pool is draining and cannot accept work'); done(); }); }); }); it('should throw with invalid usage (close after getTile)', function(done) { new mapnik_backend('mapnik://./test/data/world.xml', function(err, source) { if (err) throw err; source.getTile(0,0,0, function(err, info, headers) { // now close the source source.close(function(err){ // pool will be draining... }); // now that the pool is draining further // access to the source is invalid and should throw source.getTile(0,0,0, function(err, info, headers) { assert.equal(err.message,'pool is draining and cannot accept work'); done(); }); }); }); }); });tilelive-mapnik-0.6.1/test/data/000077500000000000000000000000001221423614300164765ustar00rootroot00000000000000tilelive-mapnik-0.6.1/test/data/invalid_interactivity_1.xml000066400000000000000000000015451221423614300240510ustar00rootroot00000000000000 blah NAME world world_merc/world_merc.shp shape tilelive-mapnik-0.6.1/test/data/invalid_interactivity_2.xml000066400000000000000000000013041221423614300240430ustar00rootroot00000000000000 world world_merc/world_merc.shp shape tilelive-mapnik-0.6.1/test/data/invalid_style.xml000066400000000000000000000000271221423614300220650ustar00rootroot00000000000000this is not an xml filetilelive-mapnik-0.6.1/test/data/stylesheet.xml000066400000000000000000000012641221423614300214140ustar00rootroot00000000000000 style world_merc/world_merc shape tilelive-mapnik-0.6.1/test/data/test-jpeg.xml000066400000000000000000000022021221423614300211160ustar00rootroot00000000000000 1.054687500000007,29.53522956294847,2 world NAME {{NAME}}]]> jpeg45 world world_merc/world_merc shape tilelive-mapnik-0.6.1/test/data/test.xml000066400000000000000000000021161221423614300201770ustar00rootroot00000000000000 1.054687500000007,29.53522956294847,2 world NAME {{NAME}}]]> world world_merc/world_merc shape tilelive-mapnik-0.6.1/test/data/world.xml000066400000000000000000000016041221423614300203500ustar00rootroot00000000000000 world world_merc/world_merc.shp shape tilelive-mapnik-0.6.1/test/data/world_bad.xml000066400000000000000000000011531221423614300211550ustar00rootroot00000000000000