UNPKG

trailpack-mapnik

Version:

Mapnik Tile Server with Trails.js

158 lines (130 loc) 4.64 kB
'use strict' const mapnik = require('@langa/mapnik') mapnik.Logger.setSeverity(mapnik.Logger.DEBUG) /** * @module MapService * @description TODO document Service */ module.exports = class MapService extends Service { /** * @param source String mapnik source * @param map.bbox Array The bounding box of the map (w, s, e, n) * @param map.width Number The width of the output image in pixels * @param map.height Number The height of the output image in pixels */ getMap(source, { bbox: [ w, s, e, n], width, height }) { const configFile = this.app.config.mapnik.maps[source].pathname const map = new mapnik.Map(parseInt(width), parseInt(height)) return new Promise((resolve, reject) => { map.load(configFile, (err, map) => { if (err) { this.log.warn(err) return reject(err) } map.extent = [ w, s, e, n ] const image = new mapnik.Image(parseInt(width), parseInt(height)) this.log.debug(`MapService.getMap RENDERING src=${source} w=${width} h=${height}`) map.render(image, { scale: 1, buffer_size: 1 }, (err, image) => { if (err) return reject(err) this.log.debug(`MapService.getMap ENCODING IMAGE src=${source} w=${width} h=${height}`) image.encode('png', (err, buffer) => { if (err) return reject(err) resolve({ buffer, headers: { 'Content-Type': 'image/png' } }) }) }) }) }) } getTile (source, { x, y, z }) { return this.validateTileParameters(source, {x, y, z}) .then(({x, y, z}) => { return this.downloadTile(source, {x, y, z}) }) .then(tile => { if (tile) return tile return this.renderTile(source, {x, y, z}) .then(tile => { // async. do not wait for tile upload. return immediately process.nextTick(() => this.uploadTile(source, {x, y, z}, tile)) return tile }) }) } downloadTile (source, {x, y, z}) { const s3cache = this.app.config.mapnik.s3cache[source] const AWSService = this.app.services.AWSService const t0 = Date.now() return new Promise((resolve, reject) => { AWSService.S3.getObject({ Bucket: s3cache.Bucket, Key: s3cache.getKey({ x, y, z }) }, (err, data) => { if (err && err.name === 'NoSuchKey') { this.log.info(`Tile cache miss: ${source}/${z}/${x}/${y}`) resolve() } else if (err) { this.log.warn('S3 Error:', err) resolve() } else if (Buffer.isBuffer(data.Body)) { this.log.info(`Retrieved tile ${source}/${z}/${x}/${y} from s3 in ${(Date.now() - t0)}ms`) resolve({ tile: data.Body, headers: { 'Content-Type': data.ContentType } }) } else { this.log.warn(`Tile from S3 ${source}/${z}/${x}/${y} is not a buffer.`) resolve() } }) }) } uploadTile (source, {x, y, z}, { tile, headers }) { const AWSService = this.app.services.AWSService const s3cache = this.app.config.mapnik.s3cache[source] this.log.debug(`Caching tile ${source}/${z}/${x}/${y} in bucket ${s3cache.Bucket}...`) const t1 = Date.now() AWSService.S3.putObject({ Bucket: s3cache.Bucket, ContentType: headers['Content-Type'], Key: s3cache.getKey({ x, y, z }), Body: tile }, (err, data) => { if (err) { this.log.warn('S3 Cache failed on tile [', z, x, y, ']:', err) } else { this.log.info(`Cached tile ${source}/${z}/${x}/${y} in ${(Date.now() - t1)}ms`) } }) } renderTile (source, {x, y, z}) { const t0 = Date.now() return new Promise((resolve, reject) => { this.app.packs.mapnik.sources[source].getTile(z, x, y, (err, tile, headers) => { if (err) return reject(err) this.log.debug(`Tile rendered: ${source}/${z}/${x}/${y},`, `size=${Math.round(tile.length / 1024)}kb`, `elapsed=${(Date.now() - t0)}ms`) resolve({ tile, headers }) }) }) } validateTileParameters (source, {x, y, z}) { const Source = this.app.packs.mapnik.sources[source] x = parseInt(x) y = parseInt(y) z = parseInt(z) if (!Source) { return Promise.reject(new Error(`Tile source "${source}" is not available.`)) } if (isNaN(x) || isNaN(y) || isNaN(z)) { return Promise.reject(new Error(`Tile coordinates [z=${z}, x=${x}, y=${y}] invalid.`)) } return Promise.resolve({x, y, z}) } }