diff --git a/NEWS.md b/NEWS.md index 70ba9e7c0..0785c311d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,6 +1,9 @@ # Version 6.0.1 2020-mm-dd +Announcements: +- Apply ES2018 syntax uniformly (#724) +- Better error messages for TorqueRenderer. # Version 6.0.0 2020-04-05 diff --git a/lib/backends/README.md b/lib/backends/README.md deleted file mode 100644 index 0b1e44cbc..000000000 --- a/lib/backends/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# About backend - -Backend implementations found here should not know anything about request/response objects, instead basic types should -be the parameters when working with any backend. Collaborators will be provided to the backend. - -The current state is not like that as for know we are moving code to where we think it fits better, removing that kind -of dependencies should be addressed in the future. \ No newline at end of file diff --git a/lib/backends/attributes.js b/lib/backends/attributes.js index c54ae0a2e..0098a2bea 100644 --- a/lib/backends/attributes.js +++ b/lib/backends/attributes.js @@ -1,94 +1,89 @@ 'use strict'; const PSQL = require('cartodb-psql'); - -const RendererParams = require('../renderers/renderer-params'); +const parseDbParams = require('../renderers/renderer-params'); const Timer = require('../stats/timer'); const SubstitutionTokens = require('cartodb-query-tables').utils.substitutionTokens; -function AttributesBackend () {} - -module.exports = AttributesBackend; - -/// Gets attributes for a given layer feature -// -/// Calls req2params, then expects parameters: -/// -/// * token - MapConfig identifier -/// * layer - Layer number -/// * fid - Feature identifier -/// -/// The referenced layer must have been configured -/// to allow for attributes fetching. -/// See https://github.com/CartoDB/Windshaft/wiki/MapConfig-1.1.0 -/// -/// @param testMode if true generates a call returning requested -/// columns plus the fid column of the first record -/// it is only meant to check validity of configuration -/// -AttributesBackend.prototype.getFeatureAttributes = function (mapConfigProvider, params, testMode, callback) { - const timer = new Timer(); - - mapConfigProvider.getMapConfig((err, mapConfig) => { - if (err) { - return callback(err); - } - - const layer = mapConfig.getLayer(params.layer); - if (!layer) { - const error = new Error(`Map ${params.token} has no layer number ${params.layer}`); - return callback(error); - } +module.exports = class AttributesBackend { + // Gets attributes for a given layer feature + // + // * token - MapConfig identifier + // * layer - Layer number + // * fid - Feature identifier + // + // The referenced layer must have been configured + // to allow for attributes fetching. + // See https://github.com/CartoDB/Windshaft/wiki/MapConfig-1.1.0 + // + // @param testMode if true generates a call returning requested + // columns plus the fid column of the first record + // it is only meant to check validity of configuration + // + getFeatureAttributes (mapConfigProvider, params, testMode, callback) { + const timer = new Timer(); + + mapConfigProvider.getMapConfig((err, mapConfig) => { + if (err) { + return callback(err); + } - timer.start('getAttributes'); - const attributes = layer.options.attributes; - if (!attributes) { - const error = new Error(`Layer ${params.layer} has no exposed attributes`); - return callback(error); - } + const layer = mapConfig.getLayer(params.layer); + if (!layer) { + const error = new Error(`Map ${params.token} has no layer number ${params.layer}`); + return callback(error); + } - const dbParams = Object.assign( - {}, - RendererParams.dbParamsFromReqParams(params), - mapConfig.getLayerDatasource(params.layer) - ); - - let pg; - try { - pg = new PSQL(dbParams); - } catch (error) { - return callback(error); - } + timer.start('getAttributes'); + const attributes = layer.options.attributes; + if (!attributes) { + const error = new Error(`Layer ${params.layer} has no exposed attributes`); + return callback(error); + } - const sql = getSQL(attributes, pg, layer, params, testMode); + const dbParams = Object.assign( + {}, + parseDbParams(params), + mapConfig.getLayerDatasource(params.layer) + ); + + let pg; + try { + pg = new PSQL(dbParams); + } catch (error) { + return callback(error); + } - pg.query(sql, (err, data) => { - timer.end('getAttributes'); + const sql = getSQL(attributes, pg, layer, params, testMode); - if (err) { - return callback(err); - } + pg.query(sql, (err, data) => { + timer.end('getAttributes'); - if (testMode) { - return callback(null, null, timer.getTimes()); - } + if (err) { + return callback(err); + } - const featureAttributes = extractFeatureAttributes(data.rows); + if (testMode) { + return callback(null, null, timer.getTimes()); + } - if (!featureAttributes) { - const rowsLengthError = new Error( - `Multiple features (${data.rows.length}) identified by ` + - `'${attributes.id}' = ${params.fid} in layer ${params.layer}` - ); - if (!data.rows.length) { - rowsLengthError.http_status = 404; + const featureAttributes = extractFeatureAttributes(data.rows); + + if (!featureAttributes) { + const rowsLengthError = new Error( + `Multiple features (${data.rows.length}) identified by ` + + `'${attributes.id}' = ${params.fid} in layer ${params.layer}` + ); + if (!data.rows.length) { + rowsLengthError.http_status = 404; + } + return callback(rowsLengthError); } - return callback(rowsLengthError); - } - return callback(null, featureAttributes, timer.getTimes()); - }, true); // use read-only transaction - }); + return callback(null, featureAttributes, timer.getTimes()); + }, true); // use read-only transaction + }); + } }; function getSQL (attributes, pg, layer, params, testMode) { diff --git a/lib/backends/index.js b/lib/backends/index.js deleted file mode 100644 index ddbd0d7e0..000000000 --- a/lib/backends/index.js +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -module.exports = { - Attributes: require('./attributes'), - Map: require('./map'), - MapValidator: require('./map-validator'), - Preview: require('./preview'), - Tile: require('./tile') -}; diff --git a/lib/backends/map-validator.js b/lib/backends/map-validator.js index bb5293dae..a8cb67709 100644 --- a/lib/backends/map-validator.js +++ b/lib/backends/map-validator.js @@ -5,106 +5,153 @@ const INCOMPATIBLE_LAYERS_ERROR = 'The `mapnik` or `cartodb` layers must be cons const MapConfigProviderProxy = require('../models/providers/mapconfig-provider-proxy'); -/** - * @param {TileBackend} tileBackend - * @param {AttributesBackend} attributesBackend - * @constructor - * @type {MapValidatorBackend} - */ -function MapValidatorBackend (tileBackend, attributesBackend) { - this.tileBackend = tileBackend; - this.attributesBackend = attributesBackend; -} +module.exports = class MapValidatorBackend { + constructor (tileBackend, attributesBackend) { + this.tileBackend = tileBackend; + this.attributesBackend = attributesBackend; + } -module.exports = MapValidatorBackend; + validate (mapConfigProvider, callback) { + mapConfigProvider.getMapConfig((err, mapConfig, params) => { + if (err) { + return callback(err, false); + } -MapValidatorBackend.prototype.validate = function (mapConfigProvider, callback) { - mapConfigProvider.getMapConfig((err, mapConfig, params) => { - if (err) { - return callback(err, false); - } + const token = mapConfig.id(); - var token = mapConfig.id(); + if (mapConfig.isVectorOnlyMapConfig()) { + return this.validateVectorLayergroup(mapConfigProvider, params, token) + .then(() => callback(null, true)) + .catch(err => callback(err, false)); + } - if (mapConfig.isVectorOnlyMapConfig()) { - return this.validateVectorLayergroup(mapConfigProvider, params, token) - .then(() => callback(null, true)) - .catch(err => callback(err, false)); - } + if (mapConfig.hasIncompatibleLayers()) { + const error = new Error(INCOMPATIBLE_LAYERS_ERROR); + error.type = 'mapconfig'; + error.http_status = 400; + return callback(error); + } - if (mapConfig.hasIncompatibleLayers()) { - const error = new Error(INCOMPATIBLE_LAYERS_ERROR); - error.type = 'mapconfig'; - error.http_status = 400; - return callback(error); - } + const mapnikLayersIds = getMapnikLayerIds(mapConfig); - var mapnikLayersIds = getMapnikLayerIds(mapConfig); + if (!mapnikLayersIds.length) { + return this._validateRemaningFormatLayers(mapConfigProvider, mapConfig, params) + .then(() => callback(null, true)) + .catch(err => callback(err, false)); + } - if (!mapnikLayersIds.length) { - return this._validateRemaningFormatLayers(mapConfigProvider, mapConfig, params) + let allLayers; // if layer is undefined then it fetchs all layers + this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'png', allLayers) + .then(() => this._validateRemaningFormatLayers(mapConfigProvider, mapConfig, params)) .then(() => callback(null, true)) - .catch(err => callback(err, false)); - } - - let allLayers; // if layer is undefined then it fetchs all layers - this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'png', allLayers) - .then(() => this._validateRemaningFormatLayers(mapConfigProvider, mapConfig, params)) - .then(() => callback(null, true)) - .catch(errAllLayers => { - const checkFailingMapnikTileLayerFns = mapnikLayersIds.map(layerId => - this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'png', layerId) - ); - - if (!checkFailingMapnikTileLayerFns.length) { - return callback(errAllLayers, !errAllLayers); - } + .catch(errAllLayers => { + const checkFailingMapnikTileLayerFns = mapnikLayersIds.map(layerId => + this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'png', layerId) + ); + + if (!checkFailingMapnikTileLayerFns.length) { + return callback(errAllLayers, !errAllLayers); + } + + return Promise.all(checkFailingMapnikTileLayerFns) + .then(() => callback(errAllLayers, false)) + .catch(err => callback(err, false)); + }); + }); + } - return Promise.all(checkFailingMapnikTileLayerFns) - .then(() => callback(errAllLayers, false)) - .catch(err => callback(err, false)); - }); - }); -}; + _validateRemaningFormatLayers (mapConfigProvider, mapConfig, params) { + const token = mapConfig.id(); + const validateRemainingFormatLayerPromises = []; -MapValidatorBackend.prototype._validateRemaningFormatLayers = function (mapConfigProvider, mapConfig, params) { - const token = mapConfig.id(); - const validateRemainingFormatLayerPromises = []; + mapConfig.getLayers().forEach((layer, layerId) => { + const layerOptions = layer.options; + const layerType = mapConfig.layerType(layerId); - mapConfig.getLayers().forEach((layer, layerId) => { - var lyropt = layer.options; - var layerType = mapConfig.layerType(layerId); + if (layerType === 'mapnik') { + if (layerOptions.interactivity) { + validateRemainingFormatLayerPromises.push( + this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'grid.json', layerId) + ); + } + } else if (layerType === 'torque') { + validateRemainingFormatLayerPromises.push( + this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'json.torque', layerId) + ); + } - if (layerType === 'mapnik') { - if (lyropt.interactivity) { + // both 'cartodb' or 'torque' types can have attributes + // attribute validation is usually very inefficient when sql_raw if present so we skip it + if (layerOptions.attributes && !layerOptions.sql_raw) { validateRemainingFormatLayerPromises.push( - this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'grid.json', layerId) + this.tryFetchFeatureAttributes(mapConfigProvider, params, token, layerId) ); } - } else if (layerType === 'torque') { - validateRemainingFormatLayerPromises.push( - this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'json.torque', layerId) - ); - } + }); - // both 'cartodb' or 'torque' types can have attributes - // attribute validation is usually very inefficient when sql_raw if present so we skip it - if (lyropt.attributes && !lyropt.sql_raw) { - validateRemainingFormatLayerPromises.push( - this.tryFetchFeatureAttributes(mapConfigProvider, params, token, layerId) - ); + if (!validateRemainingFormatLayerPromises.length) { + return Promise.resolve(); } - }); - if (!validateRemainingFormatLayerPromises.length) { - return Promise.resolve(); + return Promise.all(validateRemainingFormatLayerPromises); } - return Promise.all(validateRemainingFormatLayerPromises); + tryFetchTileOrGrid (mapConfigProvider, _params, token, format, layerId) { + const params = Object.assign({}, _params); + params.token = token; + params.format = format; + params.layer = layerId; + params.x = 0; + params.y = 0; + params.z = 30; + + return new Promise((resolve, reject) => { + const mapConfigProviderProxy = new MapConfigProviderProxy(mapConfigProvider, params); + + this.tileBackend.getTile(mapConfigProviderProxy, params, (err) => { + if (err) { + // Grainstore returns styles error indicating layer index, since validation is performed + // one by one instead of all blended layers of MapConfig, this fixes the error message to + // show the right layer index. + if (err.message.match(/^style0: CartoCSS is empty/) && layerId > 0) { + err.message = err.message.replace(/style0/i, `style${layerId}`); + } + err.layerIndex = layerId; + + return reject(err); + } + + return resolve(); + }); + }); + } + + tryFetchFeatureAttributes (mapConfigProvider, _params, token, layernum) { + const params = Object.assign({}, _params); + params.token = token; + params.layer = layernum; + + const proxyProvider = new MapConfigProviderProxy(mapConfigProvider, params); + + return new Promise((resolve, reject) => { + this.attributesBackend.getFeatureAttributes(proxyProvider, params, true, err => { + if (err) { + err.layerIndex = layernum; + return reject(err); + } + resolve(); + }); + }); + } + + validateVectorLayergroup (mapConfigProvider, params, token) { + let allLayers; // if layer is undefined then it fetchs all layers + return this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'mvt', allLayers); + } }; function getMapnikLayerIds (mapConfig) { - var mapnikLayerIds = []; + const mapnikLayerIds = []; mapConfig.getLayers().forEach(function (layer, layerId) { if (mapConfig.layerType(layerId) === 'mapnik') { @@ -114,53 +161,3 @@ function getMapnikLayerIds (mapConfig) { return mapnikLayerIds; } - -MapValidatorBackend.prototype.tryFetchTileOrGrid = function (mapConfigProvider, _params, token, format, layerId) { - const params = Object.assign({}, _params); - params.token = token; - params.format = format; - params.layer = layerId; - params.x = 0; - params.y = 0; - params.z = 30; - - return new Promise((resolve, reject) => { - this.tileBackend.getTile(new MapConfigProviderProxy(mapConfigProvider, params), params, function (err) { - if (err) { - // Grainstore returns styles error indicating layer index, since validation is performed - // one by one instead of all blended layers of MapConfig, this fixes the error message to - // show the right layer index. - if (err.message.match(/^style0: CartoCSS is empty/) && layerId > 0) { - err.message = err.message.replace(/style0/i, 'style' + layerId); - } - err.layerIndex = layerId; - - return reject(err); - } - return resolve(); - }); - }); -}; - -MapValidatorBackend.prototype.tryFetchFeatureAttributes = function (mapConfigProvider, _params, token, layernum) { - const params = Object.assign({}, _params); - params.token = token; - params.layer = layernum; - - const proxyProvider = new MapConfigProviderProxy(mapConfigProvider, params); - - return new Promise((resolve, reject) => { - this.attributesBackend.getFeatureAttributes(proxyProvider, params, true, err => { - if (err) { - err.layerIndex = layernum; - return reject(err); - } - resolve(); - }); - }); -}; - -MapValidatorBackend.prototype.validateVectorLayergroup = function (mapConfigProvider, params, token) { - let allLayers; // if layer is undefined then it fetchs all layers - return this.tryFetchTileOrGrid(mapConfigProvider, params, token, 'mvt', allLayers); -}; diff --git a/lib/backends/map.js b/lib/backends/map.js index ff9e7683f..3c177c344 100644 --- a/lib/backends/map.js +++ b/lib/backends/map.js @@ -4,113 +4,107 @@ const debug = require('debug')('windshaft:backend:map'); const Timer = require('../stats/timer'); const layerMetadataFactory = require('../metadata'); -/** - * @param {RendererCache} rendererCache - * @param {MapStore} mapStore - * @param {MapValidator} mapValidator - * @constructor - */ -function MapBackend (rendererCache, mapStore, mapValidator) { - this._rendererCache = rendererCache; - this._mapStore = mapStore; - this._mapValidator = mapValidator; - this._layerMetadata = layerMetadataFactory(); -} +module.exports = class MapBackend { + constructor (rendererCache, mapStore, mapValidator) { + this._rendererCache = rendererCache; + this._mapStore = mapStore; + this._mapValidator = mapValidator; + this._layerMetadata = layerMetadataFactory(); + } -module.exports = MapBackend; + createLayergroup (mapConfig, params, validatorMapConfigProvider, callback) { + const timer = new Timer(); + timer.start('createLayergroup'); + + // Inject db parameters into the configuration + // to ensure getting different identifiers for + // maps created against different databases + // or users. See + // https://github.com/CartoDB/Windshaft/issues/163 + mapConfig.setDbParams({ + name: params.dbname, + user: params.dbuser + }); -MapBackend.prototype.createLayergroup = function (mapConfig, params, validatorMapConfigProvider, callback) { - const timer = new Timer(); - timer.start('createLayergroup'); + timer.start('mapSave'); + // will save only if successful + this._mapStore.save(mapConfig, (err, mapConfigId, known) => { + timer.end('mapSave'); - // Inject db parameters into the configuration - // to ensure getting different identifiers for - // maps created against different databases - // or users. See - // https://github.com/CartoDB/Windshaft/issues/163 - mapConfig.setDbParams({ - name: params.dbname, - user: params.dbuser - }); + if (err) { + return callback(err); + } - timer.start('mapSave'); - // will save only if successful - this._mapStore.save(mapConfig, (err, mapConfigId, known) => { - timer.end('mapSave'); + if (known) { + return this._getLayergroupMetadata(params, mapConfig, timer, callback); + } - if (err) { - return callback(err); - } + timer.start('validate'); + this._mapValidator.validate(validatorMapConfigProvider, (err, isValid) => { + timer.end('validate'); - if (known) { - return this._getLayergroupMetadata(params, mapConfig, timer, callback); - } + if (err || !isValid) { + return this._deleteMapConfigAfterError(err, mapConfig.id(), callback); + } - timer.start('validate'); - this._mapValidator.validate(validatorMapConfigProvider, (err, isValid) => { - timer.end('validate'); + return this._getLayergroupMetadata(params, mapConfig, timer, callback); + }); + }); + } - if (err || !isValid) { + _getLayergroupMetadata (params, mapConfig, timer, callback) { + timer.start('layer-metadata'); + return this._getLayersMetadata(params, mapConfig, (err, layergroupData) => { + timer.end('layer-metadata'); + + if (err) { return this._deleteMapConfigAfterError(err, mapConfig.id(), callback); } - return this._getLayergroupMetadata(params, mapConfig, timer, callback); + timer.end('createLayergroup'); + return callback(null, layergroupData, timer.getTimes()); }); - }); -}; - -MapBackend.prototype._getLayergroupMetadata = function (params, mapConfig, timer, callback) { - timer.start('layer-metadata'); - return this._getLayersMetadata(params, mapConfig, (err, layergroupData) => { - timer.end('layer-metadata'); - - if (err) { - return this._deleteMapConfigAfterError(err, mapConfig.id(), callback); - } - - timer.end('createLayergroup'); - return callback(null, layergroupData, timer.getTimes()); - }); -}; + } -MapBackend.prototype._getLayersMetadata = function (params, mapConfig, callback) { - const layergroupData = { - layergroupid: mapConfig.id() - }; + _getLayersMetadata (params, mapConfig, callback) { + const layergroupData = { + layergroupid: mapConfig.id() + }; - this._layerMetadata.getMetadata(this._rendererCache, params, mapConfig, (err, layersMetadata) => { - if (err) { - return callback(err); - } + this._layerMetadata.getMetadata(this._rendererCache, params, mapConfig, (err, layersMetadata) => { + if (err) { + return callback(err); + } - if (layersMetadata) { - layergroupData.metadata = { - layers: layersMetadata - }; + if (layersMetadata) { + layergroupData.metadata = { + layers: layersMetadata + }; - // backwards compatibility for torque - const torqueMetadata = getTorqueMetadata(layersMetadata); - if (torqueMetadata) { - layergroupData.metadata.torque = torqueMetadata; + // backwards compatibility for torque + const torqueMetadata = getTorqueMetadata(layersMetadata); + if (torqueMetadata) { + layergroupData.metadata.torque = torqueMetadata; + } } - } - return callback(null, layergroupData); - }); -}; + return callback(null, layergroupData); + }); + } -MapBackend.prototype._deleteMapConfigAfterError = function (err, mapConfigId, callback) { - this._mapStore.del(mapConfigId, function (delErr) { - if (delErr) { - debug(`Failed to delete MapConfig '${mapConfigId}' after: ${err.message}`); - } + _deleteMapConfigAfterError (err, mapConfigId, callback) { + this._mapStore.del(mapConfigId, function (delErr) { + if (delErr) { + debug(`Failed to delete MapConfig '${mapConfigId}' after: ${err.message}`); + } - return callback(err); - }); + return callback(err); + }); + } }; function getTorqueMetadata (layersMetadata) { - const torqueMetadata = layersMetadata.reduce(function (acc, layer, layerId) { + const torqueMetadata = layersMetadata.reduce((acc, layer, layerId) => { if (layer.type === 'torque') { acc[layerId] = layer.meta; } diff --git a/lib/backends/preview.js b/lib/backends/preview.js index 5d6de1a37..3dd4fdd14 100644 --- a/lib/backends/preview.js +++ b/lib/backends/preview.js @@ -2,40 +2,40 @@ const { preview } = require('@carto/cartonik'); -function PreviewBackend (rendererCache, options) { - this._rendererCache = rendererCache; - this._options = options || {}; -} - -module.exports = PreviewBackend; - -PreviewBackend.prototype.getImage = function (options, callback) { - const { mapConfigProvider, format, width, height, zoom, center, bbox } = options; - - this._rendererCache.getRenderer(mapConfigProvider, (err, renderer) => { - if (err) { - if (renderer) { - renderer.release(); +module.exports = class PreviewBackend { + constructor (rendererCache, options = {}) { + this._rendererCache = rendererCache; + this._options = options; + } + + getImage (options, callback) { + const { mapConfigProvider, format, width, height, zoom, center, bbox } = options; + + this._rendererCache.getRenderer(mapConfigProvider, (err, renderer) => { + if (err) { + if (renderer) { + renderer.release(); + } + + return callback(err); } - return callback(err); - } - - const options = { - zoom: zoom, - scale: 1, - center: center, - dimensions: { width, height }, - bbox: bbox, - format: format, - getTile: renderer.getTile.bind(renderer), - limit: (this._options.imageSizeLimit || 8192) + 1, - concurrency: this._options.concurrency - }; - - preview(options) - .then(({ image, stats }) => callback(null, image, stats)) - .catch((err) => callback(err)) - .finally(() => renderer.release()); - }); + const options = { + zoom: zoom, + scale: 1, + center: center, + dimensions: { width, height }, + bbox: bbox, + format: format, + getTile: renderer.getTile.bind(renderer), + limit: (this._options.imageSizeLimit || 8192) + 1, + concurrency: this._options.concurrency + }; + + preview(options) + .then(({ image, stats }) => callback(null, image, stats)) + .catch((err) => callback(err)) + .finally(() => renderer.release()); + }); + } }; diff --git a/lib/backends/tile.js b/lib/backends/tile.js index f6e6f0a4a..c5113b9c1 100644 --- a/lib/backends/tile.js +++ b/lib/backends/tile.js @@ -2,60 +2,63 @@ const Timer = require('../stats/timer'); -function TileBackend (rendererCache) { - this._rendererCache = rendererCache; -} - -module.exports = TileBackend; - -// Gets a tile for a given set of tile ZXY coords. (OSM style) -// Call with .png for images, or .grid.json for UTFGrid tiles -// -// query string arguments: -// -// * sql - use SQL to filter displayed results or perform operations pre-render -// * style - assign a per tile style using carto -// * interactivity - specify which columns to represent in the UTFGrid -// * cache_buster - specify to ensure a new renderer is used -// * geom_type - specify default style to use if no style present -TileBackend.prototype.getTile = function (mapConfigProvider, params, callback) { - if (params.format === 'grid.json' && !params.interactivity) { - if (!params.token) { // token embeds interactivity - return callback(new Error('Missing interactivity parameter')); - } +module.exports = class TileBackend { + constructor (rendererCache) { + this._rendererCache = rendererCache; } - const timer = new Timer(); - const extraHeaders = {}; - timer.start('getTileOrGrid'); - timer.start('getRenderer'); - this._rendererCache.getRenderer(mapConfigProvider, function (err, renderer, isCached) { - timer.end('getRenderer'); - if (err) { - if (renderer) { - renderer.release(); - } - - return callback(err); - } + // Gets a tile for a given set of tile ZXY coords. (OSM style) + // Call with .png for images, or .grid.json for UTFGrid tiles + // + // query string arguments: + // + // * sql - use SQL to filter displayed results or perform operations pre-render + // * style - assign a per tile style using carto + // * interactivity - specify which columns to represent in the UTFGrid + // * cache_buster - specify to ensure a new renderer is used + // * geom_type - specify default style to use if no style present + getTile (mapConfigProvider, params, callback) { + const { format, interactivity, token, z, x, y } = params; - if (isCached) { - extraHeaders['X-Windshaft-Cache'] = Date.now() - renderer.ctime; + // TODO: review if needed see cartonik's validations + if (format === 'grid.json' && !interactivity) { + if (!token) { // token embeds interactivity + return callback(new Error('Missing interactivity parameter')); + } } - timer.start('render-' + params.format.replace('.', '-')); - renderer.getTile(params.format, +params.z, +params.x, +params.y) - .then(({ buffer, headers, stats }) => { - timer.end('render-' + params.format.replace('.', '-')); - timer.end('getTileOrGrid'); - - return callback(null, buffer, Object.assign(extraHeaders, headers), Object.assign(timer.getTimes(), stats)); - }) - .catch((err) => callback(err)) - .finally(() => { + const timer = new Timer(); + const extraHeaders = {}; + timer.start('getTileOrGrid'); + timer.start('getRenderer'); + this._rendererCache.getRenderer(mapConfigProvider, (err, renderer, isCached) => { + timer.end('getRenderer'); + if (err) { if (renderer) { renderer.release(); } - }); - }); + + return callback(err); + } + + if (isCached) { + extraHeaders['X-Windshaft-Cache'] = Date.now() - renderer.ctime; + } + + timer.start('render-' + format.replace('.', '-')); + renderer.getTile(format, +z, +x, +y) + .then(({ buffer, headers, stats }) => { + timer.end('render-' + format.replace('.', '-')); + timer.end('getTileOrGrid'); + + return callback(null, buffer, Object.assign(extraHeaders, headers), Object.assign(timer.getTimes(), stats)); + }) + .catch((err) => callback(err)) + .finally(() => { + if (renderer) { + renderer.release(); + } + }); + }); + } }; diff --git a/lib/cache/cache-entry.js b/lib/cache/cache-entry.js index c51e949ea..7a7ffd998 100644 --- a/lib/cache/cache-entry.js +++ b/lib/cache/cache-entry.js @@ -1,131 +1,131 @@ 'use strict'; const debug = require('debug')('windshaft:cache-entry'); - -function CacheEntry (cacheBuster, key = 'key-not-provided') { - this.ready = false; - this.err = null; - this.renderer = null; - this.ctime = this.atime = Date.now(); // last access time - this.cb = []; - this._refcount = 0; - this.cacheBuster = cacheBuster; - this.key = key; -} - -CacheEntry.prototype = Object.create(require('events').EventEmitter.prototype); - -CacheEntry.prototype.pushCallback = function (cb) { - this._addRef(); - if (this.ready) { - cb(this.err, this, true); - this.setLastUsed(Date.now()); // update last access time; - } else { - this.cb.push(cb); +const { EventEmitter } = require('events'); + +module.exports = class CacheEntry extends EventEmitter { + constructor (cacheBuster, key = 'key-not-provided') { + super(); + this.ready = false; + this.err = null; + this.renderer = null; + this.ctime = this.atime = Date.now(); // last access time + this.cb = []; + this._refcount = 0; + this.cacheBuster = cacheBuster; + this.key = key; } -}; - -// Get one tile from the service -CacheEntry.prototype.getTile = async function (format, z, x, y) { - return this.renderer.getTile(format, z, x, y); -}; - -CacheEntry.prototype.getMetadata = async function () { - return this.renderer.getMetadata(); -}; -// Get the contained entry -CacheEntry.prototype.get = function () { - return this.renderer.get(); -}; - -CacheEntry.prototype.setReady = function (err, renderer) { - // consistency check - if (this.ready) { - debug('Invalid call to CacheEntry.setReady on an ready entry'); - return; + pushCallback (callback) { + this._addRef(); + if (this.ready) { + callback(this.err, this, true); + this.setLastUsed(Date.now()); // update last access time; + } else { + this.cb.push(callback); + } } - this.ready = true; - this.err = err; - this.renderer = renderer; - - var cached = false; - var cb = this.cb.shift(); - while (cb) { - cb(err, this, cached); - cached = true; - cb = this.cb.shift(); + // Get one tile from the service + async getTile (format, z, x, y) { + return this.renderer.getTile(format, z, x, y); } - // TODO: update last access time here ? - this.emit('ready', this); - - if (err) { - this.emit('error', err); + async getMetadata () { + return this.renderer.getMetadata(); } -}; -// Call this as soon as you get a reference to the object -// if you plan to use it for longer than the current tick. -// Call dropRef when finished with it. -CacheEntry.prototype._addRef = function () { - ++this._refcount; -}; - -CacheEntry.prototype.release = function () { - this._dropRef(); -}; - -CacheEntry.prototype._dropRef = function () { - if (!--this._refcount) { - this._destroy(); + // Get the contained entry + get () { + return this.renderer.get(); } -}; -CacheEntry.prototype.setLastUsed = function (t) { - this.atime = t; -}; + setReady (err, renderer) { + // consistency check + if (this.ready) { + debug('Invalid call to CacheEntry.setReady on an ready entry'); + return; + } + + this.ready = true; + this.err = err; + this.renderer = renderer; + + let cached = false; + let callback = this.cb.shift(); + while (callback) { + callback(err, this, cached); + cached = true; + callback = this.cb.shift(); + } + + // TODO: update last access time here ? + this.emit('ready', this); + + if (err) { + this.emit('error', err); + } + } -CacheEntry.prototype.timeSinceLastAccess = function (now) { - return now - this.atime; -}; + // Call this as soon as you get a reference to the object + // if you plan to use it for longer than the current tick. + // Call dropRef when finished with it. + _addRef () { + ++this._refcount; + } -// Destroy (now or ASAP) a CacheEntry -// -// TODO: accept a callback ? -// -CacheEntry.prototype._destroy = function () { - if (this.cb.length) { - debug(`CacheEntry._destroy was called while still having ${this.cb.length} callbacks pending`); - return; + release () { + this._dropRef(); } - if (this._refcount) { - debug(`CacheEntry._destroy was called while still having ${this._refcount} references`); - return; + _dropRef () { + if (!--this._refcount) { + this._destroy(); + } } - if (!this.ready) { - // not ready yet, try later - debug(`cache_entry ${this.key} isn't ready yet, scheduling destruction on 'ready' event`); - this.on('ready', this._destroy.bind(this)); - return; + setLastUsed (t) { + this.atime = t; } - if (!this.renderer) { - debug(`Cache entry ${this.key} is ready but has no renderer, so nothing to do`); - return; + timeSinceLastAccess (now) { + return now - this.atime; } - // TODO: fixme: we can't modify the renderer here because - // tilelive-mapnik is keeping an internal cache and - // so modifying an object we'd be modifying something - // which will possibly be reused later. - // See https://github.com/mapbox/tilelive-mapnik/issues/47 - this.renderer.close(() => { - debug(`Mapnik Renderer ${this.key} closed due to its cache entry was released and it doesn't have references`); - }); + // Destroy (now or ASAP) a CacheEntry + // + // TODO: accept a callback ? + // + _destroy () { + if (this.cb.length) { + debug(`CacheEntry._destroy was called while still having ${this.cb.length} callbacks pending`); + return; + } + + if (this._refcount) { + debug(`CacheEntry._destroy was called while still having ${this._refcount} references`); + return; + } + + if (!this.ready) { + // not ready yet, try later + debug(`cache_entry ${this.key} isn't ready yet, scheduling destruction on 'ready' event`); + this.on('ready', this._destroy.bind(this)); + return; + } + + if (!this.renderer) { + debug(`Cache entry ${this.key} is ready but has no renderer, so nothing to do`); + return; + } + + // TODO: fixme: we can't modify the renderer here because + // tilelive-mapnik is keeping an internal cache and + // so modifying an object we'd be modifying something + // which will possibly be reused later. + // See https://github.com/mapbox/tilelive-mapnik/issues/47 + this.renderer.close(() => { + debug(`Mapnik Renderer ${this.key} closed due to its cache entry was released and it doesn't have references`); + }); + } }; - -module.exports = CacheEntry; diff --git a/lib/cache/renderer-cache.js b/lib/cache/renderer-cache.js index 0829bbed9..9a9b6c844 100644 --- a/lib/cache/renderer-cache.js +++ b/lib/cache/renderer-cache.js @@ -5,172 +5,164 @@ // - Purges the renderer objects after `{Number} options.timeout` ms of inactivity since the last cache entry access // Renderer objects are encapsulated inside a {CacheEntry} that tracks the last access time for each renderer -var EventEmitter = require('events').EventEmitter; -var util = require('util'); -var CacheEntry = require('./cache-entry'); -var debug = require('debug')('windshaft:renderercache'); - -function RendererCache (rendererFactory, options) { - if (!(this instanceof RendererCache)) { - return new RendererCache(rendererFactory, options); +const { EventEmitter } = require('events'); +const CacheEntry = require('./cache-entry'); +const debug = require('debug')('windshaft:renderercache'); + +module.exports = class RendererCache extends EventEmitter { + constructor (rendererFactory, options = {}) { + super(); + + this.renderers = {}; + this.timeout = options.timeout || options.ttl || 60000; + this.gcRun = 0; + this.rendererFactory = rendererFactory; + + setInterval(() => { + const now = Date.now(); + for (const [key, cacheEntry] of Object.entries(this.renderers)) { + if (cacheEntry.timeSinceLastAccess(now) > this.timeout) { + this.del(key); + } + } + }, this.timeout); } - EventEmitter.call(this); - - options = options || {}; + // If renderer cache entry exists at req-derived key, return it, + // else generate a new one and save at key. + // + // Caches lifetime is driven by the timeout passed at RendererCache + // construction time. + // + // @param callback will be called with (err, renderer) + // If `err` is null the renderer should be + // ready for you to use (calling getTile or getGrid). + // Note that the object is a proxy to the actual TileStore + // so you won't get the whole TileLive interface available. + // If you need that, use the .get() function. + // In order to reduce memory usage call renderer.release() + // when you're sure you won't need it anymore. + getRenderer (mapConfigProvider, callback) { + const cacheBuster = this.getCacheBusterValue(mapConfigProvider.getCacheBuster()); + const key = mapConfigProvider.getKey(); + let cacheEntry = this.renderers[key]; + + if (this.shouldRecreateRenderer(cacheEntry, cacheBuster)) { + cacheEntry = this.renderers[key] = new CacheEntry(cacheBuster, key); + cacheEntry._addRef(); // we add another ref for this.renderers[key] + + cacheEntry.on('error', (err) => { + debug('Removing RendererCache ' + key + ' on error ' + err); + this.emit('err', err); + this.del(key); + }); - this.renderers = {}; - this.timeout = options.timeout || options.ttl || 60000; + mapConfigProvider.getMapConfig((err, mapConfig, params, context) => { + if (err) { + this.del(key); + return callback(err); + } - this.gcRun = 0; + this.rendererFactory.getRenderer(mapConfig, params, context, cacheEntry.setReady.bind(cacheEntry)); + }); + } - this.rendererFactory = rendererFactory; + cacheEntry.pushCallback(callback); + }; - setInterval(function () { - var now = Date.now(); - Object.keys(this.renderers).forEach(function (key) { - var cacheEntry = this.renderers[key]; - if (cacheEntry.timeSinceLastAccess(now) > this.timeout) { - this.del(key); - } - }.bind(this)); - }.bind(this), this.timeout); -} - -util.inherits(RendererCache, EventEmitter); - -module.exports = RendererCache; - -// If renderer cache entry exists at req-derived key, return it, -// else generate a new one and save at key. -// -// Caches lifetime is driven by the timeout passed at RendererCache -// construction time. -// -// -// @param callback will be called with (err, renderer) -// If `err` is null the renderer should be -// ready for you to use (calling getTile or getGrid). -// Note that the object is a proxy to the actual TileStore -// so you won't get the whole TileLive interface available. -// If you need that, use the .get() function. -// In order to reduce memory usage call renderer.release() -// when you're sure you won't need it anymore. -RendererCache.prototype.getRenderer = function (mapConfigProvider, callback) { - var cacheBuster = this.getCacheBusterValue(mapConfigProvider.getCacheBuster()); - - // setup - var key = mapConfigProvider.getKey(); - - var cacheEntry = this.renderers[key]; - - if (this.shouldRecreateRenderer(cacheEntry, cacheBuster)) { - cacheEntry = this.renderers[key] = new CacheEntry(cacheBuster, key); - cacheEntry._addRef(); // we add another ref for this.renderers[key] - - var self = this; - - cacheEntry.on('error', function (err) { - debug('Removing RendererCache ' + key + ' on error ' + err); - self.emit('err', err); - self.del(key); - }); - - mapConfigProvider.getMapConfig(function makeRenderer (err, mapConfig, params, context) { - if (err) { - self.del(key); - return callback(err); - } - self.rendererFactory.getRenderer(mapConfig, params, context, cacheEntry.setReady.bind(cacheEntry)); - }); - } + getCacheBusterValue (cacheBuster) { + if (cacheBuster === undefined) { + return 0; + } - cacheEntry.pushCallback(callback); -}; + if (Number.isFinite(cacheBuster)) { + return Math.min(this._getMaxCacheBusterValue(), cacheBuster); + } -RendererCache.prototype.getCacheBusterValue = function (cacheBuster) { - if (cacheBuster === undefined) { - return 0; + return cacheBuster; } - if (Number.isFinite(cacheBuster)) { - return Math.min(this._getMaxCacheBusterValue(), cacheBuster); + + _getMaxCacheBusterValue () { + return Date.now(); } - return cacheBuster; -}; -RendererCache.prototype._getMaxCacheBusterValue = function () { - return Date.now(); -}; + shouldRecreateRenderer (cacheEntry, cacheBuster) { + if (cacheEntry) { + const entryCacheBuster = parseFloat(cacheEntry.cacheBuster); + const requestCacheBuster = parseFloat(cacheBuster); -RendererCache.prototype.shouldRecreateRenderer = function (cacheEntry, cacheBuster) { - if (cacheEntry) { - var entryCacheBuster = parseFloat(cacheEntry.cacheBuster); - var requestCacheBuster = parseFloat(cacheBuster); + if (isNaN(entryCacheBuster) || isNaN(requestCacheBuster)) { + return cacheEntry.cacheBuster !== cacheBuster; + } - if (isNaN(entryCacheBuster) || isNaN(requestCacheBuster)) { - return cacheEntry.cacheBuster !== cacheBuster; + return requestCacheBuster > entryCacheBuster; } - return requestCacheBuster > entryCacheBuster; + + return true; } - return true; -}; -// delete all renderers in cache -RendererCache.prototype.purge = function () { - Object.keys(this.renderers).forEach(this.del.bind(this)); -}; + // delete all renderers in cache + purge () { + for (const key of Object.keys(this.renderers)) { + this.del(key); + } + } -// Clears out all renderers related to a given database+token, regardless of other arguments -RendererCache.prototype.reset = function (mapConfigProvider) { - Object.keys(this.renderers) - .filter(mapConfigProvider.filter.bind(mapConfigProvider)) - .forEach(this.del.bind(this)); -}; + // Clears out all renderers related to a given database+token, regardless of other arguments + reset (mapConfigProvider) { + for (const key of Object.keys(this.renderers)) { + if (mapConfigProvider.filter(key)) { + this.del(key); + } + } + } + + // drain render pools, remove renderer and associated timeout calls + del (id) { + const cacheEntry = this.renderers[id]; -// drain render pools, remove renderer and associated timeout calls -RendererCache.prototype.del = function (id) { - var cacheEntry = this.renderers[id]; - if (cacheEntry) { - delete this.renderers[id]; - cacheEntry.release(); + if (cacheEntry) { + delete this.renderers[id]; + cacheEntry.release(); + } } -}; -RendererCache.prototype.getStats = function () { - const stats = new Map(); - const rendererCacheEntries = Object.entries(this.renderers); + getStats () { + const stats = new Map(); + const rendererCacheEntries = Object.entries(this.renderers); - stats.set('rendercache.count', rendererCacheEntries.length); + stats.set('rendercache.count', rendererCacheEntries.length); - return rendererCacheEntries.reduce((accumulatedStats, [cacheKey, cacheEntry]) => { - let format = cacheKey.split(':')[3]; // [dbname, token, dbuser, format, layer, scale] + return rendererCacheEntries.reduce((accumulatedStats, [cacheKey, cacheEntry]) => { + let format = cacheKey.split(':')[3]; // [dbname, token, dbuser, format, layer, scale] - format = format === '' ? 'no-format' : format; - format = format === 'grid.json' ? 'grid' : format.replace('.', '-'); + format = format === '' ? 'no-format' : format; + format = format === 'grid.json' ? 'grid' : format.replace('.', '-'); - const key = `rendercache.format.${format}`; + const key = `rendercache.format.${format}`; - if (accumulatedStats.has(key)) { - accumulatedStats.set(key, accumulatedStats.get(key) + 1); - } else { - accumulatedStats.set(key, 1); - } + if (accumulatedStats.has(key)) { + accumulatedStats.set(key, accumulatedStats.get(key) + 1); + } else { + accumulatedStats.set(key, 1); + } - // it might a cacheEntry has been released & removed from the cache during this process - const rendererStats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats(); + // it might a cacheEntry has been released & removed from the cache during this process + const rendererStats = cacheEntry.renderer && cacheEntry.renderer.getStats && cacheEntry.renderer.getStats(); - if (!(rendererStats instanceof Map)) { - return accumulatedStats; - } + if (!(rendererStats instanceof Map)) { + return accumulatedStats; + } - for (const [stat, value] of rendererStats) { - if (accumulatedStats.has(stat)) { - accumulatedStats.set(stat, accumulatedStats.get(stat) + value); - } else { - accumulatedStats.set(stat, value); + for (const [stat, value] of rendererStats) { + if (accumulatedStats.has(stat)) { + accumulatedStats.set(stat, accumulatedStats.get(stat) + value); + } else { + accumulatedStats.set(stat, value); + } } - } - return accumulatedStats; - }, stats); + return accumulatedStats; + }, stats); + } }; diff --git a/lib/index.js b/lib/index.js index 7805858c0..c26474be8 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,8 +14,7 @@ const RendererFactory = require('./renderers/renderer-factory'); function windshaftFactory ({ rendererOptions, redisPool, onTileErrorStrategy, logger }) { const mapStore = new MapStore({ pool: redisPool, - expire_time: rendererOptions.grainstore.default_layergroup_ttl, - logger + expire_time: rendererOptions.grainstore.default_layergroup_ttl }); const rendererFactory = new RendererFactory({ diff --git a/lib/metadata/empty-layer-metadata.js b/lib/metadata/empty-layer-metadata.js index 1f7b874a3..f5eb32924 100644 --- a/lib/metadata/empty-layer-metadata.js +++ b/lib/metadata/empty-layer-metadata.js @@ -1,17 +1,15 @@ 'use strict'; -function EmptyLayerMetadata (types) { - this._types = types || {}; -} - -EmptyLayerMetadata.prototype.is = function (type) { - return this._types[type] ? this._types[type] : false; -}; - -EmptyLayerMetadata.prototype.getMetadata = function (mapConfig, layer, layerId, params, rendererCache, callback) { - process.nextTick(function () { - callback(null, {}); - }); +module.exports = class EmptyLayerMetadata { + constructor (types) { + this._types = types || {}; + } + + is (type) { + return this._types[type] ? this._types[type] : false; + } + + getMetadata (mapConfig, layer, layerId, params, rendererCache, callback) { + process.nextTick(() => callback(null, {})); + } }; - -module.exports = EmptyLayerMetadata; diff --git a/lib/metadata/index.js b/lib/metadata/index.js index 969888657..f4f45fd49 100644 --- a/lib/metadata/index.js +++ b/lib/metadata/index.js @@ -1,14 +1,16 @@ 'use strict'; -var LayerMetadata = require('./layer-metadata'); -var EmptyLayerMetadata = require('./empty-layer-metadata'); -var MapnikLayerMetadata = require('./mapnik-layer-metadata'); -var TorqueLayerMetadata = require('./torque-layer-metadata'); +const LayerMetadata = require('./layer-metadata'); +const EmptyLayerMetadata = require('./empty-layer-metadata'); +const MapnikLayerMetadata = require('./mapnik-layer-metadata'); +const TorqueLayerMetadata = require('./torque-layer-metadata'); + +module.exports = function layerMetadataFactory () { + const layerMetadataIterator = []; -module.exports = function LayerMetadataFactory () { - var layerMetadataIterator = []; layerMetadataIterator.push(new EmptyLayerMetadata({ http: true, plain: true })); layerMetadataIterator.push(new MapnikLayerMetadata()); layerMetadataIterator.push(new TorqueLayerMetadata()); + return new LayerMetadata(layerMetadataIterator); }; diff --git a/lib/metadata/layer-metadata.js b/lib/metadata/layer-metadata.js index 077fa8257..f76527d14 100644 --- a/lib/metadata/layer-metadata.js +++ b/lib/metadata/layer-metadata.js @@ -1,70 +1,70 @@ 'use strict'; -function LayerMetadata (layerMetadataIterator) { - this.layerMetadataIterator = layerMetadataIterator; -} +module.exports = class LayerMetadata { + constructor (layerMetadataIterator) { + this.layerMetadataIterator = layerMetadataIterator; + } -LayerMetadata.prototype.getLayerMetadataFn = function (mapConfig, layerId) { - const layerType = mapConfig.layerType(layerId); - let getMetadadaFn; + getLayerMetadataFn (mapConfig, layerId) { + const layerType = mapConfig.layerType(layerId); + let getMetadadaFn; - for (const layerMetadata of this.layerMetadataIterator) { - if (layerMetadata.is(layerType)) { - getMetadadaFn = layerMetadata.getMetadata.bind(layerMetadata); - break; + for (const layerMetadata of this.layerMetadataIterator) { + if (layerMetadata.is(layerType)) { + getMetadadaFn = layerMetadata.getMetadata.bind(layerMetadata); + break; + } } - } - return getMetadadaFn; -}; + return getMetadadaFn; + } -LayerMetadata.prototype.getMetadata = function (rendererCache, params, mapConfig, callback) { - const metadataParams = mapConfig.getLayers() - .map((layer, layerId) => { - const getMetadata = this.getLayerMetadataFn(mapConfig, layerId); - return getMetadata ? { getMetadata, mapConfig, layer, layerId, params, rendererCache } : null; - }) - .filter(metadataParam => metadataParam !== null); + getMetadata (rendererCache, params, mapConfig, callback) { + const metadataParams = mapConfig.getLayers() + .map((layer, layerId) => { + const getMetadata = this.getLayerMetadataFn(mapConfig, layerId); + return getMetadata ? { getMetadata, mapConfig, layer, layerId, params, rendererCache } : null; + }) + .filter(metadataParam => metadataParam !== null); - if (!metadataParams.length) { - return callback(null, []); - } + if (!metadataParams.length) { + return callback(null, []); + } - return Promise.all(metadataParams.map(({ getMetadata, mapConfig, layer, layerId, params, rendererCache }) => { - return new Promise((resolve, reject) => { - getMetadata(mapConfig, layer, layerId, params, rendererCache, (err, metadata) => { - if (err) { - return reject(err); - } + return Promise.all(metadataParams.map(({ getMetadata, mapConfig, layer, layerId, params, rendererCache }) => { + return new Promise((resolve, reject) => { + getMetadata(mapConfig, layer, layerId, params, rendererCache, (err, metadata) => { + if (err) { + return reject(err); + } - return resolve(metadata); + return resolve(metadata); + }); }); - }); - })) - .then(results => { - if (!results.length) { - return callback(null, null); - } + })) + .then(results => { + if (!results.length) { + return callback(null, null); + } - const metadata = []; + const metadata = []; - mapConfig.getLayers().forEach(function (layer, layerIndex) { - var layerType = mapConfig.layerType(layerIndex); + mapConfig.getLayers().forEach(function (layer, layerIndex) { + const layerType = mapConfig.layerType(layerIndex); - metadata[layerIndex] = { - type: layerType, - id: mapConfig.getLayerId(layerIndex), - meta: results[layerIndex] - }; + metadata[layerIndex] = { + type: layerType, + id: mapConfig.getLayerId(layerIndex), + meta: results[layerIndex] + }; - if (layer.options.cartocss && metadata[layerIndex].meta) { - metadata[layerIndex].meta.cartocss = layer.options.cartocss; - } - }); + if (layer.options.cartocss && metadata[layerIndex].meta) { + metadata[layerIndex].meta.cartocss = layer.options.cartocss; + } + }); - return callback(null, metadata); - }) - .catch(err => callback(err)); + return callback(null, metadata); + }) + .catch(err => callback(err)); + } }; - -module.exports = LayerMetadata; diff --git a/lib/metadata/mapnik-layer-metadata.js b/lib/metadata/mapnik-layer-metadata.js index 2e885d30e..57812fafe 100644 --- a/lib/metadata/mapnik-layer-metadata.js +++ b/lib/metadata/mapnik-layer-metadata.js @@ -1,18 +1,18 @@ 'use strict'; -function MapnikLayerMetadata () { - this._types = { - mapnik: true, - cartodb: true - }; -} +module.exports = class MapnikLayerMetadata { + constructor () { + this._types = { + mapnik: true, + cartodb: true + }; + } -MapnikLayerMetadata.prototype.is = function (type) { - return this._types[type] ? this._types[type] : false; -}; + is (type) { + return this._types[type] ? this._types[type] : false; + } -MapnikLayerMetadata.prototype.getMetadata = function (mapConfig, layer, layerId, params, rendererCache, callback) { - return callback(null, { cartocss: layer.options.cartocss }); + getMetadata (mapConfig, layer, layerId, params, rendererCache, callback) { + return callback(null, { cartocss: layer.options.cartocss }); + } }; - -module.exports = MapnikLayerMetadata; diff --git a/lib/metadata/torque-layer-metadata.js b/lib/metadata/torque-layer-metadata.js index 073a47005..75b2c41e2 100644 --- a/lib/metadata/torque-layer-metadata.js +++ b/lib/metadata/torque-layer-metadata.js @@ -1,43 +1,40 @@ 'use strict'; -var DummyMapConfigProvider = require('../models/providers/dummy-mapconfig-provider'); - -function TorqueLayerMetadata () { - this._types = { - torque: true - }; -} - -TorqueLayerMetadata.prototype.is = function (type) { - return this._types[type] ? this._types[type] : false; -}; - -TorqueLayerMetadata.prototype.getMetadata = function (mapConfig, layer, layerId, params, rendererCache, callback) { - params = Object.assign({}, params, { - token: mapConfig.id(), - format: 'json.torque', - layer: layerId - }); - - const dummyMapConfigProvider = new DummyMapConfigProvider(mapConfig, params); - rendererCache.getRenderer(dummyMapConfigProvider, function (err, renderer) { - if (err) { - if (renderer) { - renderer.release(); - } - - return callback(err); - } - - renderer.getMetadata() - .then(meta => callback(null, meta)) - .catch(err => callback(err)) - .finally(() => { +const DummyMapConfigProvider = require('../models/providers/dummy-mapconfig-provider'); + +module.exports = class TorqueLayerMetadata { + constructor () { + this._types = { + torque: true + }; + } + + is (type) { + return this._types[type] ? this._types[type] : false; + } + + getMetadata (mapConfig, layer, layerId, params, rendererCache, callback) { + params = Object.assign({}, params, { + token: mapConfig.id(), + format: 'json.torque', + layer: layerId + }); + + const dummyMapConfigProvider = new DummyMapConfigProvider(mapConfig, params); + + rendererCache.getRenderer(dummyMapConfigProvider, (err, renderer) => { + if (err) { if (renderer) { renderer.release(); } - }); - }); -}; -module.exports = TorqueLayerMetadata; + return callback(err); + } + + renderer.getMetadata() + .then(meta => callback(null, meta)) + .catch(err => callback(err)) + .finally(() => renderer ? renderer.release() : undefined); + }); + } +}; diff --git a/lib/models/datasource.js b/lib/models/datasource.js index e69b2402b..a75d8dc2f 100644 --- a/lib/models/datasource.js +++ b/lib/models/datasource.js @@ -1,58 +1,50 @@ 'use strict'; -/** - * @param {Array} layersDbParams - * @constructor - */ -function Datasource (layersDbParams) { - layersDbParams = layersDbParams || []; - if (!Array.isArray(layersDbParams)) { - throw new Error('layersDbParams must be an Array'); - } - this.layersDbParams = layersDbParams; -} - -module.exports = Datasource; +class Datasource { + constructor (layersDbParams = []) { + if (!Array.isArray(layersDbParams)) { + throw new Error('layersDbParams must be an Array'); + } -Datasource.prototype.getLayerDatasource = function (layerIndex) { - return this.layersDbParams[layerIndex] || {}; -}; - -Datasource.prototype.obj = function () { - return this.layersDbParams; -}; + this.layersDbParams = layersDbParams; + } -Datasource.prototype.isEmpty = function () { - return this.layersDbParams.filter(function (layerDbParams) { - return !!layerDbParams; - }).length === 0; -}; + getLayerDatasource (layerIndex) { + return this.layersDbParams[layerIndex] || {}; + } -Datasource.prototype.clone = function () { - return new Datasource(this.layersDbParams.slice()); -}; + obj () { + return this.layersDbParams; + } -// ------------------ EmptyDatasource ------------------ + isEmpty () { + return this.layersDbParams.filter(layerDbParams => !!layerDbParams).length === 0; + } -function emptyDatasource () { - return new Datasource([]); + clone () { + return new Datasource(this.layersDbParams.slice()); + } } -module.exports.EmptyDatasource = emptyDatasource; +class Builder { + constructor () { + this.layersDbParams = []; + } -// ------------------ Builder ------------------ + withLayerDatasource (layerIndex, dbParams) { + this.layersDbParams[layerIndex] = dbParams; + return this; + } -function Builder () { - this.layersDbParams = []; + build () { + return new Datasource(this.layersDbParams); + } } -module.exports.Builder = Builder; - -Builder.prototype.withLayerDatasource = function (layerIndex, dbParams) { - this.layersDbParams[layerIndex] = dbParams; - return this; -}; +function emptyDatasource () { + return new Datasource([]); +} -Builder.prototype.build = function () { - return new Datasource(this.layersDbParams); -}; +module.exports = Datasource; +module.exports.Builder = Builder; +module.exports.EmptyDatasource = emptyDatasource; diff --git a/lib/models/mapconfig.js b/lib/models/mapconfig.js index 36ba5a742..92013be6d 100644 --- a/lib/models/mapconfig.js +++ b/lib/models/mapconfig.js @@ -1,241 +1,260 @@ 'use strict'; -var Crypto = require('crypto'); -var semver = require('semver'); +const crypto = require('crypto'); +const semver = require('semver'); +const Datasource = require('./datasource'); + +// see: http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification +module.exports = class MapConfig { + // Factory like method to create MapConfig objects when you are unsure about being + // able to provide all the MapConfig collaborators or you have to create a MapConfig + // object from a serialized version + static create (rawConfig, datasource) { + if (rawConfig.ds) { + return new MapConfig(rawConfig.cfg, new Datasource(rawConfig.ds)); + } + datasource = datasource || Datasource.EmptyDatasource(); + return new MapConfig(rawConfig, datasource); + } -var Datasource = require('./datasource'); + static getLayerId (rawMapConfig, layerIndex) { + const layer = rawMapConfig.layers[layerIndex]; -// Map configuration object + if (layer.id) { + return layer.id; + } -/// API: Create MapConfig from configuration object -// -/// @param obj js MapConfiguration object, see -/// http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification -/// -function MapConfig (config, datasource) { - // TODO: inject defaults ? - this._cfg = config; + const layerType = getType(layer.type); - if (!semver.satisfies(this.version(), '>= 1.0.0 <= 1.8.0')) { - throw new Error('Unsupported layergroup configuration version ' + this.version()); - } + let layerId = `layer${getLayerIndexByType(rawMapConfig, layerType, layerIndex)}`; + if (layerType !== 'mapnik') { + layerId = `${layerType}-${layerId}`; + } - if (!Object.prototype.hasOwnProperty.call(this._cfg, 'layers')) { - throw new Error('Missing layers array from layergroup config'); + return layerId; } - this._cfg.layers.forEach(function (layer, i) { - if (!Object.prototype.hasOwnProperty.call(layer, 'options')) { - throw new Error('Missing options from layer ' + i + ' of layergroup config'); + constructor (config, datasource) { + // TODO: inject defaults ? + this._id = null; + this._cfg = config; + this._datasource = datasource; + + if (!semver.satisfies(this.version(), '>= 1.0.0 <= 1.8.0')) { + throw new Error(`Unsupported layergroup configuration version ${this.version()}`); } - // NOTE: interactivity used to be a string as of version 1.0.0 - if (Array.isArray(layer.options.interactivity)) { - layer.options.interactivity = layer.options.interactivity.join(','); + + if (!Object.prototype.hasOwnProperty.call(this._cfg, 'layers')) { + throw new Error('Missing layers array from layergroup config'); } - }); - if (this._cfg.buffersize) { - Object.keys(this._cfg.buffersize).forEach(format => { - if (this._cfg.buffersize[format] !== undefined && !Number.isFinite(this._cfg.buffersize[format])) { - throw new Error(`Buffer size of format "${format}" must be a number`); + this._cfg.layers.forEach((layer, index) => { + if (!Object.prototype.hasOwnProperty.call(layer, 'options')) { + throw new Error(`Missing options from layer ${index} of layergroup config`); + } + + // NOTE: interactivity used to be a string as of version 1.0.0 + if (Array.isArray(layer.options.interactivity)) { + layer.options.interactivity = layer.options.interactivity.join(','); } }); + + if (this._cfg.buffersize) { + Object.keys(this._cfg.buffersize).forEach(format => { + if (this._cfg.buffersize[format] !== undefined && !Number.isFinite(this._cfg.buffersize[format])) { + throw new Error(`Buffer size of format "${format}" must be a number`); + } + }); + } } - /** - * @type {Datasource} - */ - this._datasource = datasource; + serialize () { + if (this._datasource.isEmpty()) { + return JSON.stringify(this._cfg); + } - this._id = null; -} + return JSON.stringify({ + cfg: this._cfg, + ds: this._datasource.obj() + }); + } -function md5Hash (s) { - return Crypto.createHash('md5').update(s, 'binary').digest('hex'); -} + id () { + if (this._id === null) { + this._id = md5Hash(JSON.stringify(this._cfg)); + } -/// API: Get serialized version of this MapConfig -MapConfig.prototype.serialize = function () { - if (this._datasource.isEmpty()) { - return JSON.stringify(this._cfg); + return this._id; } - return JSON.stringify({ - cfg: this._cfg, - ds: this._datasource.obj() - }); -}; -/// API: Get identifier for this MapConfig -MapConfig.prototype.id = function () { - if (this._id === null) { - this._id = md5Hash(JSON.stringify(this._cfg)); + obj () { + return this._cfg; } - // debug('MapConfig.id=%s', this._id); - return this._id; -}; - -/// API: Get configuration object of this MapConfig -MapConfig.prototype.obj = function () { - return this._cfg; -}; -MapConfig.prototype.version = function () { - return this._cfg.version || '1.0.0'; -}; + version () { + return this._cfg.version || '1.0.0'; + } -MapConfig.prototype.setDbParams = function (dbParams) { - this._cfg.dbparams = dbParams; - this.flush(); -}; + setDbParams (dbParams) { + this._cfg.dbparams = dbParams; + this.flush(); + } -MapConfig.prototype.flush = function () { // flush id so it gets recalculated - this._id = null; -}; + flush () { + this._id = null; + } -/// API: Get type string of given layer -// -/// @param num layer index (0-based) -/// @returns a type string, as read from the layer -/// -MapConfig.prototype.layerType = function (num) { - var lyr = this.getLayer(num); - if (!lyr) { - return undefined; + layerType (layerIndex) { + const layer = this.getLayer(layerIndex); + + if (!layer) { + return undefined; + } + + return this.getType(layer.type); } - return this.getType(lyr.type); -}; -MapConfig.prototype.getType = function (type) { - return getType(type); -}; + getType (type) { + return getType(type); + } -function getType (type) { - // TODO: check validity of other types ? - return (!type || type === 'cartodb') ? 'mapnik' : type; -} + setBufferSize (bufferSize) { + this._cfg.buffersize = bufferSize; + this.flush(); -MapConfig.prototype.setBufferSize = function (bufferSize) { - this._cfg.buffersize = bufferSize; - this.flush(); - return this; -}; + return this; + } + + getBufferSize (format) { + if (this._cfg.buffersize && isValidBufferSize(this._cfg.buffersize[format])) { + return parseInt(this._cfg.buffersize[format], 10); + } -MapConfig.prototype.getBufferSize = function (format) { - if (this._cfg.buffersize && isValidBufferSize(this._cfg.buffersize[format])) { - return parseInt(this._cfg.buffersize[format], 10); + return undefined; } - return undefined; -}; + hasIncompatibleLayers () { + return !this.isVectorOnlyMapConfig() && this.hasVectorLayer(); + } -function isValidBufferSize (value) { - return Number.isFinite(parseInt(value, 10)); -} + isVectorOnlyMapConfig () { + const layers = this.getLayers(); + let isVectorOnlyMapConfig = false; -MapConfig.prototype.hasIncompatibleLayers = function () { - return !this.isVectorOnlyMapConfig() && this.hasVectorLayer(); -}; + if (!layers.length) { + return isVectorOnlyMapConfig; + } -MapConfig.prototype.isVectorOnlyMapConfig = function () { - const layers = this.getLayers(); - let isVectorOnlyMapConfig = false; + isVectorOnlyMapConfig = true; + + for (let index = 0; index < layers.length; index++) { + if (!this.isVectorLayer(index)) { + isVectorOnlyMapConfig = false; + break; + } + } - if (!layers.length) { return isVectorOnlyMapConfig; } - isVectorOnlyMapConfig = true; + hasVectorLayer () { + const layers = this.getLayers(); + let hasVectorLayer = false; - for (let index = 0; index < layers.length; index++) { - if (!this.isVectorLayer(index)) { - isVectorOnlyMapConfig = false; - break; + for (let index = 0; index < layers.length; index++) { + if (this.isVectorLayer(index)) { + hasVectorLayer = true; + break; + } } - } - return isVectorOnlyMapConfig; -}; + return hasVectorLayer; + } -MapConfig.prototype.hasVectorLayer = function () { - const layers = this.getLayers(); - let hasVectorLayer = false; + isVectorLayer (index) { + const layer = this.getLayer(index); + const type = getType(layer.type); + const sql = this.getLayerOption(index, 'sql'); + const cartocss = this.getLayerOption(index, 'cartocss'); + const cartocssVersion = this.getLayerOption(index, 'cartocss_version'); - for (let index = 0; index < layers.length; index++) { - if (this.isVectorLayer(index)) { - hasVectorLayer = true; - break; - } + return type === 'mapnik' && typeof sql === 'string' && cartocss === undefined && cartocssVersion === undefined; } - return hasVectorLayer; -}; + getLayerId (layerIndex) { + return MapConfig.getLayerId(this._cfg, layerIndex); + } -MapConfig.prototype.isVectorLayer = function (index) { - const layer = this.getLayer(index); - const type = getType(layer.type); - const sql = this.getLayerOption(index, 'sql'); - const cartocss = this.getLayerOption(index, 'cartocss'); - const cartocssVersion = this.getLayerOption(index, 'cartocss_version'); + getIndexByLayerId (layerId) { + for (const [index, layer] of this.getLayers().entries()) { + if (layer.id === layerId) { + return index; + } + } - return type === 'mapnik' && typeof sql === 'string' && cartocss === undefined && cartocssVersion === undefined; -}; + return -1; + } -/***************************************************************************** - * Layers - ****************************************************************************/ + getLayer (layerIndex) { + return this._cfg.layers[layerIndex]; + } -MapConfig.prototype.getLayerId = function (layerIndex) { - return getLayerId(this._cfg, layerIndex); -}; + getLayers () { + return this._cfg.layers.map((_layer, layerIndex) => this.getLayer(layerIndex)); + } -function getLayerId (rawMapConfig, layerIndex) { - var layer = rawMapConfig.layers[layerIndex]; - if (layer.id) { - return layer.id; + getLayerIndexByType (type, mapConfigLayerIndex) { + return getLayerIndexByType(this._cfg, type, mapConfigLayerIndex); } - var layerType = getType(layer.type); - var layerId = 'layer' + getLayerIndexByType(rawMapConfig, layerType, layerIndex); - if (layerType !== 'mapnik') { - layerId = layerType + '-' + layerId; + getLayerOption (layerIndex, optionName, defaultValue) { + const layer = this.getLayer(layerIndex); + let layerOption = defaultValue; + + if (layer && Object.prototype.hasOwnProperty.call(layer.options, optionName)) { + layerOption = layer.options[optionName]; + } + + return layerOption; } - return layerId; -} -MapConfig.prototype.getIndexByLayerId = function (layerId) { - var layers = this.getLayers(); + getLayerDatasource (layerIndex) { + const datasource = this._datasource.getLayerDatasource(layerIndex) || {}; + const layerSrid = this.getLayerOption(layerIndex, 'srid'); - for (var i = 0; i < layers.length; i++) { - if (layers[i].id === layerId) { - return i; + if (layerSrid) { + datasource.srid = layerSrid; } + + return datasource; } - return -1; -}; + getMVTExtents () { + const layers = this.getLayers(); + const extent = getTileExtent(layers); + const simplifyExtent = getSimplifyExtent(layers, extent); -/// API: Get layer by index -// -/// @returns undefined on invalid index -/// -MapConfig.prototype.getLayer = function (layerIndex) { - return this._cfg.layers[layerIndex]; + return { extent: extent || DEFAULT_EXTENT, simplify_extent: simplifyExtent || DEFAULT_SIMPLIFY_EXTENT }; + } }; -MapConfig.prototype.getLayers = function () { - return this._cfg.layers.map(function (_layer, layerIndex) { - return this.getLayer(layerIndex); - }.bind(this)); -}; +function md5Hash (s) { + return crypto.createHash('md5').update(s, 'binary').digest('hex'); +} -MapConfig.prototype.getLayerIndexByType = function (type, mapConfigLayerIdx) { - return getLayerIndexByType(this._cfg, type, mapConfigLayerIdx); -}; +function getType (type) { + // TODO: check validity of other types ? + return (!type || type === 'cartodb') ? 'mapnik' : type; +} + +function isValidBufferSize (value) { + return Number.isFinite(parseInt(value, 10)); +} function getLayerIndexByType (rawMapConfig, type, mapConfigLayerIdx) { - var typeLayerIndex = 0; - var mapConfigToTypeLayers = {}; + let typeLayerIndex = 0; + const mapConfigToTypeLayers = {}; rawMapConfig.layers.forEach(function (layer, layerIdx) { if (getType(layer.type) === type) { @@ -246,49 +265,6 @@ function getLayerIndexByType (rawMapConfig, type, mapConfigLayerIdx) { return mapConfigToTypeLayers[mapConfigLayerIdx]; } -MapConfig.prototype.getLayerOption = function (layerIndex, optionName, defaultValue) { - var layerOption = defaultValue; - var layer = this.getLayer(layerIndex); - if (layer && Object.prototype.hasOwnProperty.call(layer.options, optionName)) { - layerOption = layer.options[optionName]; - } - return layerOption; -}; - -/***************************************************************************** - * Datasource - ****************************************************************************/ - -MapConfig.prototype.getLayerDatasource = function (layerIndex) { - var datasource = this._datasource.getLayerDatasource(layerIndex) || {}; - - var layerSrid = this.getLayerOption(layerIndex, 'srid'); - if (layerSrid) { - datasource.srid = layerSrid; - } - - return datasource; -}; - -/** - * À la Factory method - * - * @param {Object} rawConfig - * @param {Datasource} [datasource=Datasource.EmptyDatasource()] - * @returns {MapConfig} - */ -function create (rawConfig, datasource) { - if (rawConfig.ds) { - return new MapConfig(rawConfig.cfg, new Datasource(rawConfig.ds)); - } - datasource = datasource || Datasource.EmptyDatasource(); - return new MapConfig(rawConfig, datasource); -} - -/***************************************************************************** - * MVT - ****************************************************************************/ - const DEFAULT_EXTENT = 4096; const DEFAULT_SIMPLIFY_EXTENT = 256; // Accepted values between 1 and 2^31 -1 (DEFAULT_MAX_EXTENT) @@ -313,7 +289,7 @@ function getSimplifyExtent (layers, vectorExtent) { }))]; if (extents.length > 1) { - throw new Error('Multiple simplify extent values in mapConfig (' + extents + ')'); + throw new Error(`Multiple simplify extent values in mapConfig (${extents})`); } if (undef === layers.length) { @@ -325,8 +301,7 @@ function getSimplifyExtent (layers, vectorExtent) { // Accepted values between 1 and max_extent const simplifyExtent = parseInt(extents[0]); if (!checkRange(simplifyExtent, DEFAULT_MIN_EXTENT, maxExtent)) { - throw new Error('Invalid vector_simplify_extent (' + simplifyExtent + '). ' + - 'Must be between 1 and vector_extent [' + maxExtent + ']'); + throw new Error(`Invalid vector_simplify_extent (${simplifyExtent}). Must be between 1 and vector_extent [${maxExtent}]`); } return simplifyExtent; @@ -346,7 +321,7 @@ function getTileExtent (layers) { }))]; if (layerExtents.length > 1) { - throw new Error('Multiple extent values in mapConfig (' + layerExtents + ')'); + throw new Error(`Multiple extent values in mapConfig (${layerExtents})`); } if (undef === layers.length) { @@ -355,25 +330,8 @@ function getTileExtent (layers) { const extent = parseInt(layerExtents[0]); if (!checkRange(extent, DEFAULT_MIN_EXTENT, DEFAULT_MAX_EXTENT)) { - throw new Error('Invalid vector_extent. Must be between 1 and ' + DEFAULT_MAX_EXTENT); + throw new Error(`Invalid vector_extent. Must be between 1 and ${DEFAULT_MAX_EXTENT}`); } return extent; } - -// Returns an object with the extents needed for MVTs. Throws on error -MapConfig.prototype.getMVTExtents = function () { - const layers = this.getLayers(); - const extent = getTileExtent(layers); - const simplifyExtent = getSimplifyExtent(layers, extent); - - return { extent: extent || DEFAULT_EXTENT, simplify_extent: simplifyExtent || DEFAULT_SIMPLIFY_EXTENT }; -}; - -module.exports = MapConfig; -// Factory like method to create MapConfig objects when you are unsure about being -// able to provide all the MapConfig collaborators or you have to create a MapConfig -// object from a serialized version -module.exports.create = create; - -module.exports.getLayerId = getLayerId; diff --git a/lib/models/providers/README.md b/lib/models/providers/README.md index 24f826df8..ff50134a6 100644 --- a/lib/models/providers/README.md +++ b/lib/models/providers/README.md @@ -39,6 +39,6 @@ Returns a number representing the last modification time of the MapConfig so it' must be recreated or not. ```javascript -getKey() +getCacheBuster() ``` - @return `{Number}` the last modified time for the MapConfig, aka buster diff --git a/lib/models/providers/dummy-mapconfig-provider.js b/lib/models/providers/dummy-mapconfig-provider.js index c089ddf62..b8cb8d1b6 100644 --- a/lib/models/providers/dummy-mapconfig-provider.js +++ b/lib/models/providers/dummy-mapconfig-provider.js @@ -1,18 +1,14 @@ 'use strict'; -var util = require('util'); -var MapStoreMapConfigProvider = require('./mapstore-mapconfig-provider'); +const MapStoreMapConfigProvider = require('./mapstore-mapconfig-provider'); -function DummyMapConfigProvider (mapConfig, params) { - MapStoreMapConfigProvider.call(this, undefined, params); +module.exports = class DummyMapConfigProvider extends MapStoreMapConfigProvider { + constructor (mapConfig, params) { + super(undefined, params); + this.mapConfig = mapConfig; + } - this.mapConfig = mapConfig; -} - -util.inherits(DummyMapConfigProvider, MapStoreMapConfigProvider); - -module.exports = DummyMapConfigProvider; - -DummyMapConfigProvider.prototype.getMapConfig = function (callback) { - return callback(null, this.mapConfig, this.params, {}); + getMapConfig (callback) { + return callback(null, this.mapConfig, this.params, {}); + } }; diff --git a/lib/models/providers/mapconfig-provider-proxy.js b/lib/models/providers/mapconfig-provider-proxy.js index fe20af8a8..8fc8b71d5 100644 --- a/lib/models/providers/mapconfig-provider-proxy.js +++ b/lib/models/providers/mapconfig-provider-proxy.js @@ -1,31 +1,34 @@ 'use strict'; -function MapConfigProviderProxy (mapConfigProvider, params) { - this.mapConfigProvider = mapConfigProvider; - this.params = params; -} +module.exports = class MapConfigProviderProxy { + constructor (mapConfigProvider, params) { + this.mapConfigProvider = mapConfigProvider; + this.params = params; + } -module.exports = MapConfigProviderProxy; + getMapConfig (callback) { + this.mapConfigProvider.getMapConfig((err, mapConfig, params, context) => { + if (err) { + return callback(err); + } -MapConfigProviderProxy.prototype.getMapConfig = function (callback) { - var self = this; - this.mapConfigProvider.getMapConfig(function (err, mapConfig, params, context) { - return callback(err, mapConfig, Object.assign({}, params, self.params), context); - }); -}; + return callback(null, mapConfig, Object.assign({}, params, this.params), context); + }); + } -MapConfigProviderProxy.prototype.getKey = function () { - return this.mapConfigProvider.getKey.apply(this); -}; + getKey () { + return this.mapConfigProvider.getKey.apply(this); + } -MapConfigProviderProxy.prototype.getCacheBuster = function () { - return this.mapConfigProvider.getCacheBuster.apply(this); -}; + getCacheBuster () { + return this.mapConfigProvider.getCacheBuster.apply(this); + } -MapConfigProviderProxy.prototype.filter = function () { - return this.mapConfigProvider.filter.apply(this, arguments); -}; + filter () { + return this.mapConfigProvider.filter.apply(this, arguments); + } -MapConfigProviderProxy.prototype.createKey = function () { - return this.mapConfigProvider.createKey.apply(this, arguments); + createKey () { + return this.mapConfigProvider.createKey.apply(this, arguments); + } }; diff --git a/lib/models/providers/mapstore-mapconfig-provider.js b/lib/models/providers/mapstore-mapconfig-provider.js index 40f9a1777..4d5b8f040 100644 --- a/lib/models/providers/mapstore-mapconfig-provider.js +++ b/lib/models/providers/mapstore-mapconfig-provider.js @@ -1,49 +1,55 @@ 'use strict'; -function MapStoreMapConfigProvider (mapStore, params) { - this.mapStore = mapStore; - this.params = params; - this.token = params.token; - this.cacheBuster = params.cache_buster || 0; -} - -module.exports = MapStoreMapConfigProvider; - -MapStoreMapConfigProvider.prototype.getMapConfig = function (callback) { - var self = this; - this.mapStore.load(this.token, function (err, mapConfig) { - return callback(err, mapConfig, self.params, {}); - }); -}; - -MapStoreMapConfigProvider.prototype.getKey = function () { - return createKey(this.params); -}; - -MapStoreMapConfigProvider.prototype.getCacheBuster = function () { - return this.cacheBuster; -}; - -MapStoreMapConfigProvider.prototype.filter = function (key) { - var regex = new RegExp('^' + createKey(this.params, true) + '.*'); - return key && key.match(regex); +module.exports = class MapStoreMapConfigProvider { + static createKey (...args) { + return createKey(...args); + } + + constructor (mapStore, params) { + this.mapStore = mapStore; + this.params = params; + this.token = params.token; + this.cacheBuster = params.cache_buster || 0; + } + + getMapConfig (callback) { + this.mapStore.load(this.token, (err, mapConfig) => { + if (err) { + return callback(err); + } + + return callback(null, mapConfig, this.params, {}); + }); + } + + getKey () { + return createKey(this.params); + } + + getCacheBuster () { + return this.cacheBuster; + } + + filter (key) { + const regex = new RegExp(`^${createKey(this.params, true)}.*`); + return key && key.match(regex); + } }; // Configure bases for cache keys suitable for string interpolation const baseKey = ctx => `${ctx.dbname}:${ctx.token}`; const renderKey = ctx => `${baseKey(ctx)}:${ctx.dbuser}:${ctx.format}:${ctx.layer}:${ctx.scale_factor}`; // Create a string ID/key from a set of params -function createKey (params, base) { - const ctx = Object.assign({}, { - dbname: '', - token: '', - dbuser: '', - format: '', - layer: '', - scale_factor: 1 - }, params); - - return base ? baseKey(ctx) : renderKey(ctx); -} +const defaultParams = { + dbname: '', + token: '', + dbuser: '', + format: '', + layer: '', + scale_factor: 1 +}; -module.exports.createKey = createKey; +function createKey (params, useBaseKey) { + const ctx = Object.assign({}, defaultParams, params); + return useBaseKey ? baseKey(ctx) : renderKey(ctx); +} diff --git a/lib/renderers/base-adaptor.js b/lib/renderers/base-adaptor.js index a7c9d6635..46ef00679 100644 --- a/lib/renderers/base-adaptor.js +++ b/lib/renderers/base-adaptor.js @@ -1,7 +1,7 @@ 'use strict'; function wrap (x, z) { - var limit = (1 << z); + const limit = (1 << z); return ((x % limit) + limit) % limit; } diff --git a/lib/renderers/blend/factory.js b/lib/renderers/blend/factory.js index 632592e06..bdc86d05b 100644 --- a/lib/renderers/blend/factory.js +++ b/lib/renderers/blend/factory.js @@ -3,18 +3,21 @@ const mapnik = require('@carto/mapnik'); const Renderer = require('./renderer'); const BaseAdaptor = require('../base-adaptor'); -const layersFilter = require('../../utils/layer-filter'); +const layersFilter = require('../../utils/layers-filter'); const EMPTY_IMAGE_BUFFER = new mapnik.Image(256, 256).encodeSync('png'); -const NAME = 'blend'; -class BlendFactory { +module.exports = class BlendFactory { + static get NAME () { + return 'blend'; + } + constructor (rendererFactory) { this.rendererFactory = rendererFactory; } getName () { - return NAME; + return BlendFactory.NAME; } supportsFormat (format) { @@ -89,10 +92,7 @@ class BlendFactory { }) .catch(err => callback(err)); } -} - -module.exports = BlendFactory; -module.exports.NAME = NAME; +}; async function onTileErrorStrategyHttpRenderer (err, format) { if (err.code === 'ETIMEDOUT') { diff --git a/lib/renderers/blend/index.js b/lib/renderers/blend/index.js deleted file mode 100644 index 20cd310a8..000000000 --- a/lib/renderers/blend/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.factory = require('./factory'); -module.exports.adaptor = require('../base-adaptor'); diff --git a/lib/renderers/http/factory.js b/lib/renderers/http/factory.js index 743fc55b7..0d8a794b4 100644 --- a/lib/renderers/http/factory.js +++ b/lib/renderers/http/factory.js @@ -1,68 +1,73 @@ 'use strict'; -var Renderer = require('./renderer'); -var FallbackRenderer = require('./fallback-renderer'); -var BaseAdaptor = require('../base-adaptor'); +const Renderer = require('./renderer'); +const FallbackRenderer = require('./fallback-renderer'); +const BaseAdaptor = require('../base-adaptor'); -function HttpFactory (whitelist, timeout, proxy, fallbackImage) { - this.whitelist = whitelist || []; - - this.timeout = timeout; - this.proxy = proxy; - - this.fallbackImage = fallbackImage; -} +module.exports = class HttpFactory { + static get NAME () { + return 'http'; + } -module.exports = HttpFactory; -const NAME = 'http'; -module.exports.NAME = NAME; + static isValidUrlTemplate (urlTemplate, whitelist) { + return whitelist.some((currentValue) => { + return urlTemplate === currentValue || urlTemplate.match(currentValue); + }); + } -HttpFactory.prototype.getName = function () { - return NAME; -}; + constructor (options = {}) { + this.whitelist = options.whitelist || []; + this.timeout = options.timeout; + this.proxy = options.proxy; + this.fallbackImage = options.fallbackImage; + } -HttpFactory.prototype.supportsFormat = function (format) { - return format === 'png'; -}; + getName () { + return HttpFactory.NAME; + } -HttpFactory.prototype.getAdaptor = function (renderer, onTileErrorStrategy) { - return new BaseAdaptor(renderer, onTileErrorStrategy); -}; + supportsFormat (format) { + return format === 'png'; + } -HttpFactory.prototype.getRenderer = function (mapConfig, format, options, callback) { - var layerNumber = options.layer; + getAdaptor (renderer, onTileErrorStrategy) { + return new BaseAdaptor(renderer, onTileErrorStrategy); + } - var layer = mapConfig.getLayer(layerNumber); - var urlTemplate = layer.options.urlTemplate; + getRenderer (mapConfig, format, options, callback) { + const layerNumber = options.layer; + const layer = mapConfig.getLayer(layerNumber); + const urlTemplate = layer.options.urlTemplate; - if (layer.type !== this.getName()) { - return callback(new Error('Layer is not an http layer')); - } + if (layer.type !== this.getName()) { + return callback(new Error('Layer is not an http layer')); + } - if (!urlTemplate) { - return callback(new Error('Missing mandatory "urlTemplate" option')); - } + if (!urlTemplate) { + return callback(new Error('Missing mandatory "urlTemplate" option')); + } - if (!isValidUrlTemplate(urlTemplate, this.whitelist)) { - if (this.fallbackImage) { - return callback(null, new FallbackRenderer(this.fallbackImage)); - } else { - return callback(new Error('Invalid "urlTemplate" for http layer')); + if (!HttpFactory.isValidUrlTemplate(urlTemplate, this.whitelist)) { + if (this.fallbackImage) { + return callback(null, new FallbackRenderer(this.fallbackImage)); + } else { + return callback(new Error('Invalid "urlTemplate" for http layer')); + } } - } - var subdomains = getSubdomains(urlTemplate, layer.options); + const subdomains = getSubdomains(urlTemplate, layer.options); + const rendererOptions = { + tms: layer.options.tms || false, + timeout: this.timeout, + proxy: this.proxy + }; - var rendererOptions = { - tms: layer.options.tms || false, - timeout: this.timeout, - proxy: this.proxy - }; - return callback(null, new Renderer(urlTemplate, subdomains, rendererOptions)); + return callback(null, new Renderer(urlTemplate, subdomains, rendererOptions)); + } }; function getSubdomains (urlTemplate, options) { - var subdomains = options.subdomains; + let subdomains = options.subdomains; if (!subdomains) { subdomains = urlTemplate.match(/\{ *([s]+) *\}/g) ? ['a', 'b', 'c'] : []; @@ -70,11 +75,3 @@ function getSubdomains (urlTemplate, options) { return subdomains; } - -function isValidUrlTemplate (urlTemplate, whitelist) { - return whitelist.some(function (currentValue) { - return urlTemplate === currentValue || urlTemplate.match(currentValue); - }); -} - -module.exports.isValidUrlTemplate = isValidUrlTemplate; diff --git a/lib/renderers/http/index.js b/lib/renderers/http/index.js deleted file mode 100644 index 20cd310a8..000000000 --- a/lib/renderers/http/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.factory = require('./factory'); -module.exports.adaptor = require('../base-adaptor'); diff --git a/lib/renderers/http/renderer.js b/lib/renderers/http/renderer.js index d823ecfcf..0405e22dc 100644 --- a/lib/renderers/http/renderer.js +++ b/lib/renderers/http/renderer.js @@ -92,7 +92,7 @@ const templateRe = /\{ *([\w_]+) *\}/g; // super-simple templating facility, used for TileLayer URLs function template (str, data) { return str.replace(templateRe, function (str, key) { - var value = data[key]; + let value = data[key]; if (value === undefined) { throw new Error('No value provided for variable ' + str); diff --git a/lib/renderers/mapnik/adaptor.js b/lib/renderers/mapnik/adaptor.js index acdbcf3e3..7d48261f9 100644 --- a/lib/renderers/mapnik/adaptor.js +++ b/lib/renderers/mapnik/adaptor.js @@ -7,7 +7,7 @@ const METRIC_PREFIX = 'Mk_'; function parseMapnikMetrics (stats) { if (stats && Object.prototype.hasOwnProperty.call(stats, 'Mapnik')) { Object.keys(stats.Mapnik).forEach(function (key) { - var metric = stats.Mapnik[key]; + const metric = stats.Mapnik[key]; if (metric.constructor === Object) { if (Object.prototype.hasOwnProperty.call(metric, 'Time (us)')) { stats[METRIC_PREFIX + key] = Math.floor(metric['Time (us)'] / 1000); diff --git a/lib/renderers/mapnik/factory.js b/lib/renderers/mapnik/factory.js index 05eadf982..9089dac18 100644 --- a/lib/renderers/mapnik/factory.js +++ b/lib/renderers/mapnik/factory.js @@ -3,8 +3,7 @@ const { rendererFactory: mapnikRendererFactory } = require('@carto/cartonik'); const mapnik = require('@carto/mapnik'); const grainstore = require('grainstore'); - -const layersFilter = require('../../utils/layer-filter'); +const layersFilter = require('../../utils/layers-filter'); const MapnikAdaptor = require('./adaptor'); const DefaultQueryRewriter = require('../../utils/default-query-rewriter'); @@ -13,259 +12,232 @@ const COLUMN_TYPE_RASTER = 'raster'; const COLUMN_TYPE_DEFAULT = COLUMN_TYPE_GEOMETRY; const DEFAULT_TILE_SIZE = 256; const FORMAT_MVT = 'mvt'; +const SUPPORTED_FORMATS = ['png', 'png32', 'grid.json', FORMAT_MVT]; -function MapnikFactory (options) { - this.supportedFormats = { - png: true, - png32: true, - 'grid.json': true, - mvt: true - }; - - this._options = options; - - // Set default mapnik options - this._mapnik_opts = Object.assign({}, { - geometry_field: 'the_geom_webmercator', - - // Metatile is the number of tiles-per-side that are going - // to be rendered at once. If all of them will be requested - // we'd have saved time. If only one will be used, we'd have - // wasted time. - // - // Defaults to 2 as of tilelive-mapnik@0.3.2 - // - // We'll assume an average of a 4x4 viewport - metatile: 4, - - // tilelive-mapnik uses an internal cache to store tiles/grids - // generated when using metatile. This options allow to tune - // the behaviour for that internal cache. - metatileCache: { - // Time an object must stay in the cache until is removed - ttl: 0, - // Whether an object must be removed after the first hit - // Usually you want to use `true` here when ttl>0. - deleteOnHit: false - }, - - // Override metatile behaviour depending on the format - formatMetatile: {}, - - // Buffer size is the tickness in pixel of a buffer - // around the rendered (meta?)tile. - // - // This is important for labels and other marker that overlap tile boundaries. - // Setting to 128 ensures no render artifacts. - // 64 may have artifacts but is faster. - // Less important if we can turn metatiling on. - // - // defaults to 128 as of tilelive-mapnik@0.3.2 - // - bufferSize: 64, - - // Buffer size behaviour depending on the format - formatBufferSize: {}, - - // retina support, which scale factors are supported - scale_factors: [1, 2], - - limits: { - // Time in milliseconds a render request can take before it fails, some notes: - // - 0 means no render limit - // - it considers metatiling, it naive implementation: (render timeout) * (number of tiles in metatile) - render: 0, - // As the render request will finish even if timed out, whether it should be placed in the internal - // cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve - // the same tile will result in an immediate response, however that will use a lot of more application - // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the - // internal cache. - cacheOnTimeout: true - }, - - // A query-rewriter can be passed to preprocess SQL queries - // before passing them to Mapnik. - // The query rewriter should contain one function: - // - `query(sql, data)` to transform queries using the optional data provided - // The data passed to this function can be provided for eaach layer - // through a `query_rewrite_data` entry in the layer options. - // By default a dummy query rewriter which doesn't alter queries is used. - queryRewriter: new DefaultQueryRewriter(), - - // If enabled Mapnik will reuse the features retrieved from the database - // instead of requesting them once per style inside a layer - 'cache-features': true, - - // Require stats per query to the renderer - metrics: false, - - // Options for markers attributes, ellipses and images caches - markers_symbolizer_caches: { - disabled: false - }, - - // INTERNAL: Render time variables - variables: {} - }, options.mapnik || {}); - - this._options.grainstore = this._options.grainstore ? this._options.grainstore : {}; - this._options.grainstore.mapnik_version = this._options.grainstore.mapnik_version - ? this._options.grainstore.mapnik_version - : mapnik.versions.mapnik; - - bootstrapFonts(this._options.grainstore); - - this.tile_scale_factors = this._mapnik_opts.scale_factors.reduce(function (previousValue, currentValue) { - previousValue[currentValue] = DEFAULT_TILE_SIZE * currentValue; - return previousValue; - }, {}); -} +module.exports = class MapnikFactory { + static get NAME () { + return 'mapnik'; + } -function bootstrapFonts (grainstoreOptions) { - // Set carto renderer configuration for MMLStore - grainstoreOptions.carto_env = grainstoreOptions.carto_env || {}; - const cenv = grainstoreOptions.carto_env; + constructor (options) { + this._mapnikOptions = Object.assign({}, { + geometry_field: 'the_geom_webmercator', + + // Metatile is the number of tiles-per-side that are going + // to be rendered at once. If all of them will be requested + // we'd have saved time. If only one will be used, we'd have + // wasted time. + // + // Defaults to 2 as of tilelive-mapnik@0.3.2 + // + // We'll assume an average of a 4x4 viewport + metatile: 4, + + // tilelive-mapnik uses an internal cache to store tiles/grids + // generated when using metatile. This options allow to tune + // the behaviour for that internal cache. + metatileCache: { + // Time an object must stay in the cache until is removed + ttl: 0, + // Whether an object must be removed after the first hit + // Usually you want to use `true` here when ttl>0. + deleteOnHit: false + }, + + // Override metatile behaviour depending on the format + formatMetatile: {}, + + // Buffer size is the tickness in pixel of a buffer + // around the rendered (meta?)tile. + // + // This is important for labels and other marker that overlap tile boundaries. + // Setting to 128 ensures no render artifacts. + // 64 may have artifacts but is faster. + // Less important if we can turn metatiling on. + // + // defaults to 128 as of tilelive-mapnik@0.3.2 + // + bufferSize: 64, + + // Buffer size behaviour depending on the format + formatBufferSize: {}, + + // retina support, which scale factors are supported + scale_factors: [1, 2], + + limits: { + // Time in milliseconds a render request can take before it fails, some notes: + // - 0 means no render limit + // - it considers metatiling, it naive implementation: (render timeout) * (number of tiles in metatile) + render: 0, + // As the render request will finish even if timed out, whether it should be placed in the internal + // cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve + // the same tile will result in an immediate response, however that will use a lot of more application + // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the + // internal cache. + cacheOnTimeout: true + }, + + // A query-rewriter can be passed to preprocess SQL queries + // before passing them to Mapnik. + // The query rewriter should contain one function: + // - `query(sql, data)` to transform queries using the optional data provided + // The data passed to this function can be provided for eaach layer + // through a `query_rewrite_data` entry in the layer options. + // By default a dummy query rewriter which doesn't alter queries is used. + queryRewriter: new DefaultQueryRewriter(), + + // If enabled Mapnik will reuse the features retrieved from the database + // instead of requesting them once per style inside a layer + 'cache-features': true, + + // Require stats per query to the renderer + metrics: false, + + // Options for markers attributes, ellipses and images caches + markers_symbolizer_caches: { + disabled: false + }, + + // INTERNAL: Render time variables + variables: {} + }, options.mapnik || {}); + + this._mapnikOptions.scale_factors = this._mapnikOptions.scale_factors.reduce((previousValue, currentValue) => { + previousValue[currentValue] = DEFAULT_TILE_SIZE * currentValue; + return previousValue; + }, {}); + + this._grainstoreOptions = options.grainstore || {}; + this._grainstoreOptions.mapnik_version = this._grainstoreOptions.mapnik_version || mapnik.versions.mapnik; + + bootstrapFonts(this._grainstoreOptions); + } - cenv.validation_data = cenv.validation_data || {}; - if (!cenv.validation_data.fonts) { - mapnik.register_system_fonts(); - mapnik.register_default_fonts(); - cenv.validation_data.fonts = Object.keys(mapnik.fontFiles()); + getName () { + return MapnikFactory.NAME; } -} -module.exports = MapnikFactory; -const NAME = 'mapnik'; -module.exports.NAME = NAME; + supportsFormat (format) { + return SUPPORTED_FORMATS.includes(format); + } -MapnikFactory.prototype.getName = function () { - return NAME; -}; + getAdaptor (renderer, onTileErrorStrategy) { + return new MapnikAdaptor(renderer, onTileErrorStrategy); + } -MapnikFactory.prototype.supportsFormat = function (format) { - return !!this.supportedFormats[format]; -}; + getRenderer (mapConfig, format, options, callback) { + const isMvt = format === FORMAT_MVT; -MapnikFactory.prototype.getAdaptor = function (renderer, onTileErrorStrategy) { - return new MapnikAdaptor(renderer, onTileErrorStrategy); -}; + if (mapConfig.isVectorOnlyMapConfig() && !isMvt) { + const error = new Error(`Unsupported format: 'cartocss' option is missing for ${format}`); + error.http_status = 400; + error.type = 'tile'; + return callback(error); + } -MapnikFactory.prototype.defineExpectedParams = function (params) { - if (params['cache-features'] === undefined) { - params['cache-features'] = this._mapnik_opts['cache-features']; - } + defineExpectedParams(options.params, this._mapnikOptions); - if (params.metrics === undefined) { - params.metrics = this._mapnik_opts.metrics; - } + let mmlBuilderConfig; + try { + mmlBuilderConfig = mapConfigToMMLBuilderConfig(mapConfig, options, this._mapnikOptions); + } catch (error) { + return callback(error); + } - if (params.markers_symbolizer_caches === undefined) { - params.markers_symbolizer_caches = this._mapnik_opts.markers_symbolizer_caches; - } -}; + const params = Object.assign({}, options.params, mmlBuilderConfig); + const limits = Object.assign({}, this._mapnikOptions.limits, options.limits); + const variables = Object.assign({}, this._mapnikOptions.variables, options.variables); -MapnikFactory.prototype.getRenderer = function (mapConfig, format, options, callback) { - const isMvt = format === FORMAT_MVT; + // fix layer index + // see https://github.com/CartoDB/Windshaft/blob/0.43.0/lib/backends/map_validator.js#L69-L81 + if (params.layer) { + params.layer = mapConfig.getLayerIndexByType('mapnik', params.layer); + } - if (mapConfig.isVectorOnlyMapConfig() && !isMvt) { - const error = new Error(`Unsupported format: 'cartocss' option is missing for ${format}`); - error.http_status = 400; - error.type = 'tile'; - return callback(error); - } + const scaleFactor = params.scale_factor === undefined ? 1 : +params.scale_factor; + const tileSize = this._mapnikOptions.scale_factors[scaleFactor]; - this.defineExpectedParams(options.params); + if (!tileSize) { + const err = new Error('Tile with specified resolution not found'); + err.http_status = 404; + return callback(err); + } - let mmlBuilderConfig; - try { - mmlBuilderConfig = this.mapConfigToMMLBuilderConfig(mapConfig, this._mapnik_opts.queryRewriter, options); - } catch (error) { - return callback(error); - } + let mmlBuilder; + try { + mmlBuilder = getMMLBuilder(mapConfig, this._grainstoreOptions, params, format); + } catch (error) { + return callback(error); + } - const params = Object.assign({}, options.params, mmlBuilderConfig); - const limits = Object.assign({}, this._mapnik_opts.limits, options.limits); - const variables = Object.assign({}, this._mapnik_opts.variables, options.variables); + mmlBuilder.toXML((err, xml) => { + if (err) { + return callback(err); + } - // fix layer index - // see https://github.com/CartoDB/Windshaft/blob/0.43.0/lib/backends/map_validator.js#L69-L81 - if (params.layer) { - params.layer = mapConfig.getLayerIndexByType('mapnik', params.layer); + try { + const renderer = mapnikRendererFactory({ + type: isMvt ? 'vector' : 'raster', + xml: xml, + strict: !!params.strict, + bufferSize: this._getBufferSize(mapConfig, format), + poolSize: this._mapnikOptions.poolSize, + poolMaxWaitingClients: this._mapnikOptions.poolMaxWaitingClients, + tileSize: tileSize, + limits: limits, + metrics: options.params.metrics, + variables: variables, + metatile: this._getMetatile(format), // when raster renderer + metatileCache: this._mapnikOptions.metatileCache, // when raster renderer + scale: scaleFactor, // when raster renderer + gzip: false // when vector renderer + }); + + return callback(null, renderer); + } catch (err) { + return callback(err); + } + }); } - const scaleFactor = params.scale_factor === undefined ? 1 : +params.scale_factor; - const tileSize = this.tile_scale_factors[scaleFactor]; + _getMetatile (format) { + let metatile = this._mapnikOptions.metatile; - if (!tileSize) { - var err = new Error('Tile with specified resolution not found'); - err.http_status = 404; - return callback(err); - } + if (Number.isFinite(this._mapnikOptions.formatMetatile[format])) { + metatile = this._mapnikOptions.formatMetatile[format]; + } - let mmlBuilder; - try { - mmlBuilder = getMMLBuilder(mapConfig, this._options.grainstore, params, format); - } catch (error) { - return callback(error); + return metatile; } - mmlBuilder.toXML((err, xml) => { - if (err) { - return callback(err); - } + _getBufferSize (mapConfig, format, options) { + options = options || this._mapnikOptions; - try { - const renderer = mapnikRendererFactory({ - type: isMvt ? 'vector' : 'raster', - xml: xml, - strict: !!params.strict, - bufferSize: this.getBufferSize(mapConfig, format), - poolSize: this._mapnik_opts.poolSize, - poolMaxWaitingClients: this._mapnik_opts.poolMaxWaitingClients, - tileSize: tileSize, - limits: limits, - metrics: options.params.metrics, - variables: variables, - metatile: this.getMetatile(format), // when raster renderer - metatileCache: this._mapnik_opts.metatileCache, // when raster renderer - scale: scaleFactor, // when raster renderer - gzip: false // when vector renderer - }); - - return callback(null, renderer); - } catch (err) { - return callback(err); + if (Number.isFinite(mapConfig.getBufferSize(format))) { + return mapConfig.getBufferSize(format); } - }); -}; -MapnikFactory.prototype.getMetatile = function (format) { - var metatile = this._mapnik_opts.metatile; - if (Number.isFinite(this._mapnik_opts.formatMetatile[format])) { - metatile = this._mapnik_opts.formatMetatile[format]; + return getBufferSizeFromOptions(format, options); } - return metatile; -}; - -MapnikFactory.prototype.getBufferSizeFromOptions = function (format, options) { - return options && options.formatBufferSize && Number.isFinite(options.formatBufferSize[format]) - ? options.formatBufferSize[format] - : options.bufferSize; }; -MapnikFactory.prototype.getBufferSize = function (mapConfig, format, options) { - options = options || this._mapnik_opts; +function defineExpectedParams (params, mapnikOptions) { + if (params['cache-features'] === undefined) { + params['cache-features'] = mapnikOptions['cache-features']; + } - if (Number.isFinite(mapConfig.getBufferSize(format))) { - return mapConfig.getBufferSize(format); + if (params.metrics === undefined) { + params.metrics = mapnikOptions.metrics; } - return this.getBufferSizeFromOptions(format, options); -}; + if (params.markers_symbolizer_caches === undefined) { + params.markers_symbolizer_caches = mapnikOptions.markers_symbolizer_caches; + } +} -MapnikFactory.prototype.mapConfigToMMLBuilderConfig = function (mapConfig, queryRewriter, rendererOptions) { - var self = this; - var options = { +function mapConfigToMMLBuilderConfig (mapConfig, rendererOptions, mapnikOptions) { + const options = { ids: [], sql: [], originalSql: [], @@ -280,34 +252,35 @@ MapnikFactory.prototype.mapConfigToMMLBuilderConfig = function (mapConfig, query 'cache-features': rendererOptions.params['cache-features'] }; - var layerFilter = rendererOptions.layer; + const layerFilter = rendererOptions.layer; - var filteredLayerIndexes = layersFilter(mapConfig, layerFilter); - filteredLayerIndexes.reduce(function (options, layerIndex) { - var layer = mapConfig.getLayer(layerIndex); + const filteredLayerIndexes = layersFilter(mapConfig, layerFilter); + filteredLayerIndexes.reduce((options, layerIndex) => { + const layer = mapConfig.getLayer(layerIndex); validateLayer(mapConfig, layerIndex); options.ids.push(mapConfig.getLayerId(layerIndex)); - var lyropt = layer.options; + const layerOptions = layer.options; - if (lyropt.cartocss !== undefined && lyropt.cartocss_version !== undefined) { - options.style.push(lyropt.cartocss); - options.style_version.push(lyropt.cartocss_version); + if (layerOptions.cartocss !== undefined && layerOptions.cartocss_version !== undefined) { + options.style.push(layerOptions.cartocss); + options.style_version.push(layerOptions.cartocss_version); } - var query = queryRewriter.query(lyropt.sql, lyropt.query_rewrite_data); - var queryOptions = prepareQuery(query, lyropt.geom_column, lyropt.geom_type, self._mapnik_opts); + const { queryRewriter } = mapnikOptions; + const query = queryRewriter.query(layerOptions.sql, layerOptions.query_rewrite_data); + const queryOptions = prepareQuery(query, layerOptions.geom_column, layerOptions.geom_type, mapnikOptions); options.sql.push(queryOptions.sql); options.originalSql.push(query); - var zoom = {}; - if (Number.isFinite(lyropt.minzoom)) { - zoom.minzoom = lyropt.minzoom; + const zoom = {}; + if (Number.isFinite(layerOptions.minzoom)) { + zoom.minzoom = layerOptions.minzoom; } - if (Number.isFinite(lyropt.maxzoom)) { - zoom.maxzoom = lyropt.maxzoom; + if (Number.isFinite(layerOptions.maxzoom)) { + zoom.maxzoom = layerOptions.maxzoom; } options.zooms.push(zoom); @@ -316,9 +289,9 @@ MapnikFactory.prototype.mapConfigToMMLBuilderConfig = function (mapConfig, query name: queryOptions.geomColumnName }); - var extraOpt = {}; - if (Object.prototype.hasOwnProperty.call(lyropt, 'raster_band')) { - extraOpt.band = lyropt.raster_band; + const extraOpt = {}; + if (Object.prototype.hasOwnProperty.call(layerOptions, 'raster_band')) { + extraOpt.band = layerOptions.raster_band; } options.datasource_extend.push(mapConfig.getLayerDatasource(layerIndex)); options.extra_ds_opts.push(extraOpt); @@ -327,7 +300,7 @@ MapnikFactory.prototype.mapConfigToMMLBuilderConfig = function (mapConfig, query }, options); if (!options.sql.length) { - throw new Error("No 'mapnik' layers in MapConfig"); + throw new Error('No \'mapnik\' layers in MapConfig'); } if (!options.gcols.length) { @@ -337,27 +310,40 @@ MapnikFactory.prototype.mapConfigToMMLBuilderConfig = function (mapConfig, query // Grainstore limits interactivity to one layer. If there are more than one layer in layer filter then interactivity // won't be passed to grainstore (due to format requested is png, only one layer is allowed for grid.json format) if (filteredLayerIndexes.length === 1) { - var lyrInteractivity = mapConfig.getLayer(filteredLayerIndexes[0]); - var lyropt = lyrInteractivity.options; + const lyrInteractivity = mapConfig.getLayer(filteredLayerIndexes[0]); + const layerOptions = lyrInteractivity.options; - if (lyropt.interactivity) { + if (layerOptions.interactivity) { // NOTE: interactivity used to be a string as of version 1.0.0 - if (Array.isArray(lyropt.interactivity)) { - lyropt.interactivity = lyropt.interactivity.join(','); + if (Array.isArray(layerOptions.interactivity)) { + layerOptions.interactivity = layerOptions.interactivity.join(','); } - options.interactivity.push(lyropt.interactivity); + options.interactivity.push(layerOptions.interactivity); // grainstore needs to know the layer index to take the interactivity, forced to be 0. options.layer = 0; } } return options; -}; +} + +function bootstrapFonts (grainstoreOptions) { + // Set carto renderer configuration for MMLStore + grainstoreOptions.carto_env = grainstoreOptions.carto_env || {}; + const cenv = grainstoreOptions.carto_env; + + cenv.validation_data = cenv.validation_data || {}; + if (!cenv.validation_data.fonts) { + mapnik.register_system_fonts(); + mapnik.register_default_fonts(); + cenv.validation_data.fonts = Object.keys(mapnik.fontFiles()); + } +} function validateLayer (mapConfig, layerIndex) { const layer = mapConfig.getLayer(layerIndex); - var layerOptions = layer.options; + const layerOptions = layer.options; if (!mapConfig.isVectorOnlyMapConfig()) { if (!Object.prototype.hasOwnProperty.call(layerOptions, 'cartocss')) { @@ -380,12 +366,18 @@ function validateLayer (mapConfig, layerIndex) { } } +function getBufferSizeFromOptions (format, options) { + return options && options.formatBufferSize && Number.isFinite(options.formatBufferSize[format]) + ? options.formatBufferSize[format] + : options.bufferSize; +} + // Wrap SQL requests in mapnik format if sent -function prepareQuery (userSql, geomColumnName, geomColumnType, options) { +function prepareQuery (userSql, geomColumnName, geomColumnType, mapnikOptions) { // remove trailing ';' userSql = userSql.replace(/;\s*$/, ''); - geomColumnName = geomColumnName || options.geometry_field; + geomColumnName = geomColumnName || mapnikOptions.geometry_field; geomColumnType = geomColumnType || COLUMN_TYPE_DEFAULT; return { diff --git a/lib/renderers/mapnik/index.js b/lib/renderers/mapnik/index.js deleted file mode 100644 index 2c02b3696..000000000 --- a/lib/renderers/mapnik/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.adaptor = require('./adaptor'); -module.exports.factory = require('./factory'); diff --git a/lib/renderers/pg-mvt/factory.js b/lib/renderers/pg-mvt/factory.js index e090e0472..ac58a525b 100644 --- a/lib/renderers/pg-mvt/factory.js +++ b/lib/renderers/pg-mvt/factory.js @@ -1,49 +1,37 @@ 'use strict'; -const layersFilter = require('../../utils/layer-filter'); -const RendererParams = require('../renderer-params'); +const layersFilter = require('../../utils/layers-filter'); +const parseDbParams = require('../renderer-params'); const Renderer = require('./renderer'); const BaseAdaptor = require('../base-adaptor'); -/** - * API: initializes the renderer, it should be called once - * - * @param {Object} options - * - dbPoolParams: database connection pool params - * - size: maximum number of resources to create at any given time - * - idleTimeout: max milliseconds a resource can go unused before it should be destroyed - * - reapInterval: frequency to check for idle resources - */ -function PgMvtFactory (options) { - this.options = options || {}; -} - -module.exports = PgMvtFactory; -const NAME = 'pg-mvt'; -const MVT_FORMAT = 'mvt'; -module.exports.NAME = NAME; +module.exports = class PgMvtFactory { + static get NAME () { + return 'pg-mvt'; + } -PgMvtFactory.prototype = { - /// API: renderer name, use for information purposes - name: NAME, + static get MVT_FORMAT () { + return 'mvt'; + } - /// API: tile formats this module is able to render - supported_formats: [MVT_FORMAT], + constructor (options = {}) { + this.options = options; + } - getName: function () { - return this.name; - }, + getName () { + return PgMvtFactory.NAME; + } - supportsFormat: function (format) { - return format === MVT_FORMAT; - }, + supportsFormat (format) { + return format === PgMvtFactory.MVT_FORMAT; + } - getAdaptor: function (renderer, onTileErrorStrategy) { + getAdaptor (renderer, onTileErrorStrategy) { return new BaseAdaptor(renderer, onTileErrorStrategy); - }, + } - getRenderer: function (mapConfig, format, options, callback) { - if (mapConfig.isVectorOnlyMapConfig() && format !== MVT_FORMAT) { + getRenderer (mapConfig, format, options, callback) { + if (mapConfig.isVectorOnlyMapConfig() && format !== PgMvtFactory.MVT_FORMAT) { const error = new Error(`Unsupported format: 'cartocss' option is missing for ${format}`); error.http_status = 400; error.type = 'tile'; @@ -51,7 +39,7 @@ PgMvtFactory.prototype = { } if (!this.supportsFormat(format)) { - return callback(new Error('format not supported: ' + format)); + return callback(new Error(`format not supported: ${format}`)); } const mapLayers = mapConfig.getLayers(); @@ -65,11 +53,11 @@ PgMvtFactory.prototype = { } const layers = filteredLayers.map(layerIndex => mapConfig.getLayer(layerIndex)); - const dbParams = RendererParams.dbParamsFromReqParams(options.params); + const dbParams = parseDbParams(options.params); Object.assign(dbParams, mapConfig.getLayerDatasource(options.layer)); - if (Number.isFinite(mapConfig.getBufferSize(MVT_FORMAT))) { - this.options.bufferSize = mapConfig.getBufferSize(MVT_FORMAT); + if (Number.isFinite(mapConfig.getBufferSize(PgMvtFactory.MVT_FORMAT))) { + this.options.bufferSize = mapConfig.getBufferSize(PgMvtFactory.MVT_FORMAT); } try { diff --git a/lib/renderers/pg-mvt/index.js b/lib/renderers/pg-mvt/index.js deleted file mode 100644 index 20cd310a8..000000000 --- a/lib/renderers/pg-mvt/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.factory = require('./factory'); -module.exports.adaptor = require('../base-adaptor'); diff --git a/lib/renderers/plain/factory.js b/lib/renderers/plain/factory.js index e5a56d361..bfecc15da 100644 --- a/lib/renderers/plain/factory.js +++ b/lib/renderers/plain/factory.js @@ -1,58 +1,56 @@ 'use strict'; -var mapnik = require('@carto/mapnik'); -var ColorRenderer = require('./color-renderer'); -var ImageRenderer = require('./image-renderer'); -var BaseAdaptor = require('../base-adaptor'); - -var requestImage = require('../http/fetch-image'); - -function PlainFactory () { -} - -module.exports = PlainFactory; -const NAME = 'plain'; -module.exports.NAME = NAME; - -PlainFactory.prototype.getName = function () { - return NAME; -}; +const mapnik = require('@carto/mapnik'); +const ColorRenderer = require('./color-renderer'); +const ImageRenderer = require('./image-renderer'); +const BaseAdaptor = require('../base-adaptor'); +const requestImage = require('../http/fetch-image'); + +module.exports = class PlainFactory { + static get NAME () { + return 'plain'; + } -PlainFactory.prototype.supportsFormat = function (format) { - return format === 'png'; -}; + getName () { + return PlainFactory.NAME; + } -PlainFactory.prototype.getAdaptor = function (renderer, onTileErrorStrategy) { - return new BaseAdaptor(renderer, onTileErrorStrategy); -}; + supportsFormat (format) { + return format === 'png'; + } -PlainFactory.prototype.getRenderer = function (mapConfig, format, options, callback) { - var layerNumber = options.layer; + getAdaptor (renderer, onTileErrorStrategy) { + return new BaseAdaptor(renderer, onTileErrorStrategy); + } - var layer = mapConfig.getLayer(layerNumber); + getRenderer (mapConfig, format, options, callback) { + const layerNumber = options.layer; + const layer = mapConfig.getLayer(layerNumber); - if (layer.type !== this.getName()) { - return callback(new Error('Layer is not a \'plain\' layer')); - } + if (layer.type !== this.getName()) { + return callback(new Error('Layer is not a \'plain\' layer')); + } - var color = layer.options.color; - var imageUrl = layer.options.imageUrl; + const color = layer.options.color; + const imageUrl = layer.options.imageUrl; - if (!color && !imageUrl) { - return callback(new Error('Plain layer: at least one of the options, `color` or `imageUrl`, must be provided.')); - } + if (!color && !imageUrl) { + return callback(new Error('Plain layer: at least one of the options, `color` or `imageUrl`, must be provided.')); + } - if (color) { - colorRenderer(color, layer.options, callback); - } else if (imageUrl) { - imageUrlRenderer(imageUrl, layer.options, callback); - } else { - return callback(new Error('Plain layer unknown error')); + if (color) { + colorRenderer(color, layer.options, callback); + } else if (imageUrl) { + imageUrlRenderer(imageUrl, layer.options, callback); + } else { + return callback(new Error('Plain layer unknown error')); + } } }; function colorRenderer (color, options, callback) { - var mapnikColor; + let mapnikColor; + try { if (Array.isArray(color)) { if (color.length === 3) { @@ -60,14 +58,15 @@ function colorRenderer (color, options, callback) { } else if (color.length === 4) { mapnikColor = new mapnik.Color(color[0], color[1], color[2], color[3]); } else { - return callback(new Error("Invalid color for 'plain' layer: invalid integer array")); + return callback(new Error('Invalid color for \'plain\' layer: invalid integer array')); } } else { mapnikColor = new mapnik.Color(color); } } catch (err) { - return callback(new Error("Invalid color for 'plain' layer: " + err.message)); + return callback(new Error(`Invalid color for 'plain' layer: ${err.message}`)); } + return callback(null, new ColorRenderer(mapnikColor, options)); } diff --git a/lib/renderers/plain/index.js b/lib/renderers/plain/index.js deleted file mode 100644 index 20cd310a8..000000000 --- a/lib/renderers/plain/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.factory = require('./factory'); -module.exports.adaptor = require('../base-adaptor'); diff --git a/lib/renderers/renderer-factory.js b/lib/renderers/renderer-factory.js index fa764d86a..1112b7110 100644 --- a/lib/renderers/renderer-factory.js +++ b/lib/renderers/renderer-factory.js @@ -1,123 +1,93 @@ 'use strict'; -var HttpRenderer = require('./http'); -var BlendRenderer = require('./blend'); -var TorqueRenderer = require('./torque'); -var MapnikRenderer = require('./mapnik'); -var PlainRenderer = require('./plain'); -var PgMvtRenderer = require('./pg-mvt'); -var layersFilter = require('../utils/layer-filter'); - -function RendererFactory (opts) { - opts.http = opts.http || {}; - opts.mapnik = opts.mapnik || {}; - opts.torque = opts.torque || {}; - opts.mvt = opts.mvt || {}; - if (opts.mapnik.mapnik) { - opts.mvt.limits = opts.mapnik.mapnik.limits; +const HttpRendererFactory = require('./http/factory'); +const BlendRendererFactory = require('./blend/factory'); +const TorqueRendererFactory = require('./torque/factory'); +const MapnikRendererFactory = require('./mapnik/factory'); +const PlainRendererFactory = require('./plain/factory'); +const PgMvtRendererFactory = require('./pg-mvt/factory'); +const layersFilter = require('../utils/layers-filter'); + +module.exports = class RendererFactory { + constructor ({ mapnik = {}, mvt = {}, torque = {}, http = {}, onTileErrorStrategy } = {}) { + this.onTileErrorStrategy = onTileErrorStrategy; + this.usePgMvt = mvt && mvt.usePostGIS; + mvt.limits = mapnik && mapnik.mapnik && mapnik.mapnik.limits; + this.factories = { + [MapnikRendererFactory.NAME]: new MapnikRendererFactory(mapnik), + [TorqueRendererFactory.NAME]: new TorqueRendererFactory(torque), + [PlainRendererFactory.NAME]: new PlainRendererFactory(), + [BlendRendererFactory.NAME]: new BlendRendererFactory(this), + [HttpRendererFactory.NAME]: new HttpRendererFactory(http), + [PgMvtRendererFactory.NAME]: new PgMvtRendererFactory(mvt) + }; } - this.opts = opts; - - this.mapnikRendererFactory = new MapnikRenderer.factory(opts.mapnik); // eslint-disable-line new-cap - this.blendRendererFactory = new BlendRenderer.factory(this); // eslint-disable-line new-cap - - var availableFactories = [ - this.mapnikRendererFactory, - new TorqueRenderer.factory(opts.torque), // eslint-disable-line new-cap - new PlainRenderer.factory(), // eslint-disable-line new-cap - this.blendRendererFactory, - new HttpRenderer.factory( // eslint-disable-line new-cap - opts.http.whitelist, - opts.http.timeout, - opts.http.proxy, - opts.http.fallbackImage - ), - new PgMvtRenderer.factory(opts.mvt) // eslint-disable-line new-cap - ]; - this.factories = availableFactories.reduce(function (factories, factory) { - factories[factory.getName()] = factory; - return factories; - }, {}); - - this.onTileErrorStrategy = opts.onTileErrorStrategy; -} - -module.exports = RendererFactory; - -RendererFactory.prototype.getFactory = function (mapConfig, layer, format) { - var factoryName = this.getFactoryName(mapConfig, layer, format); - return this.factories[factoryName]; -}; -RendererFactory.prototype.getRenderer = function (mapConfig, params, context, callback) { - if (Number.isFinite(+params.layer) && !mapConfig.getLayer(params.layer)) { - return callback(new Error("Layer '" + params.layer + "' not found in layergroup")); + getFactory (mapConfig, layer, format) { + const factoryName = this.getFactoryName(mapConfig, layer, format); + return this.factories[factoryName]; } - var factory; - try { - factory = this.getFactory(mapConfig, params.layer, params.format); - } catch (err) { - return callback(err); - } + getRenderer (mapConfig, params, context, callback) { + const { format, layer } = params; - if (!factory) { - return callback(new Error("Type for layer '" + params.layer + "' not supported")); - } + if (Number.isFinite(+layer) && !mapConfig.getLayer(layer)) { + return callback(new Error(`Layer '${layer}' not found in layergroup`)); + } - if (!factory.supportsFormat(params.format)) { - return callback(new Error('Unsupported format ' + params.format)); - } + const factory = this.getFactory(mapConfig, layer, format); - return this.genericMakeRenderer(factory, mapConfig, params, context, callback); -}; + if (!factory) { + return callback(new Error(`Type for layer '${layer}' not supported`)); + } -RendererFactory.prototype.genericMakeRenderer = function (factory, mapConfig, params, context, callback) { - var format = params.format; - var options = { - params: params, - layer: params.layer, - limits: context.limits || {} - }; - var onTileErrorStrategy = context.onTileErrorStrategy || this.onTileErrorStrategy; + if (!factory.supportsFormat(format)) { + return callback(new Error(`Unsupported format ${format}`)); + } - // waiting for async/await to refactor - try { - factory.getRenderer(mapConfig, format, options, function (err, renderer) { + const { limits = {} } = context; + const options = { params, layer, limits }; + + factory.getRenderer(mapConfig, format, options, (err, renderer) => { if (err) { return callback(err); } - try { - const rendererAdaptor = factory.getAdaptor(renderer, onTileErrorStrategy); - return callback(null, rendererAdaptor); - } catch (error) { - return callback(error); - } + const { onTileErrorStrategy = this.onTileErrorStrategy } = context; + const rendererAdaptor = factory.getAdaptor(renderer, onTileErrorStrategy); + + return callback(null, rendererAdaptor); }); - } catch (error) { - return callback(error); } -}; -RendererFactory.prototype.getFactoryName = function (mapConfig, layer, format) { - if (isMapnikFactory(mapConfig, layer, format)) { - if (this.opts.mvt.usePostGIS && format === 'mvt') { - return PgMvtRenderer.factory.NAME; + getFactoryName (mapConfig, layer, format) { + if (isMapnikFactory(mapConfig, layer, format)) { + if (this.usePgMvt && format === 'mvt') { + return PgMvtRendererFactory.NAME; + } + + return MapnikRendererFactory.NAME; } - return MapnikRenderer.factory.NAME; - } - if (layersFilter.isSingleLayer(layer)) { - return mapConfig.layerType(layer); - } + if (isSingleLayer(layer)) { + return mapConfig.layerType(layer); + } - return BlendRenderer.factory.NAME; + return BlendRendererFactory.NAME; + } }; function isMapnikFactory (mapConfig, layer) { - var filteredLayers = layersFilter(mapConfig, layer); - return filteredLayers - .map(index => mapConfig.layerType(index)) - .every(type => type === 'mapnik'); + try { + const filteredLayers = layersFilter(mapConfig, layer); + return filteredLayers + .map(index => mapConfig.layerType(index)) + .every(type => type === 'mapnik'); + } catch (err) { + return false; + } +} + +function isSingleLayer (layerFilter) { + return Number.isFinite(+layerFilter); } diff --git a/lib/renderers/renderer-params.js b/lib/renderers/renderer-params.js index 5fcf814ae..d873a9058 100644 --- a/lib/renderers/renderer-params.js +++ b/lib/renderers/renderer-params.js @@ -1,25 +1,27 @@ 'use strict'; -var RendererParams = { - dbParamsFromReqParams: function (params) { - var dbParams = {}; - if (params.dbuser) { - dbParams.user = params.dbuser; - } - if (params.dbpassword) { - dbParams.pass = params.dbpassword; - } - if (params.dbhost) { - dbParams.host = params.dbhost; - } - if (params.dbport) { - dbParams.port = params.dbport; - } - if (params.dbname) { - dbParams.dbname = params.dbname; - } - return dbParams; +module.exports = function parseDbParams (params) { + const dbParams = {}; + + if (params.dbuser) { + dbParams.user = params.dbuser; + } + + if (params.dbpassword) { + dbParams.pass = params.dbpassword; + } + + if (params.dbhost) { + dbParams.host = params.dbhost; } -}; -module.exports = RendererParams; + if (params.dbport) { + dbParams.port = params.dbport; + } + + if (params.dbname) { + dbParams.dbname = params.dbname; + } + + return dbParams; +}; diff --git a/lib/renderers/torque/factory.js b/lib/renderers/torque/factory.js index 9bbc1527d..351f305bd 100644 --- a/lib/renderers/torque/factory.js +++ b/lib/renderers/torque/factory.js @@ -1,57 +1,16 @@ 'use strict'; -var carto = require('carto'); -var debug = require('debug')('windshaft:renderer:torque_factory'); -var PSQL = require('cartodb-psql'); -var torqueReference = require('torque.js').cartocss_reference; - -var RendererParams = require('../renderer-params'); - -var sql = require('../../utils/sql'); -var Renderer = require('./renderer'); -var PSQLAdaptor = require('./psql-adaptor'); -var PngRenderer = require('./png-renderer'); -var BaseAdaptor = require('../base-adaptor'); - +const carto = require('carto'); +const debug = require('debug')('windshaft:renderer:torque_factory'); +const torqueReference = require('torque.js').cartocss_reference; +const parseDbParams = require('../renderer-params'); +const Renderer = require('./renderer'); +const PsqlAdaptor = require('./psql-adaptor'); +const PngRenderer = require('./png-renderer'); +const BaseAdaptor = require('../base-adaptor'); const SubstitutionTokens = require('cartodb-query-tables').utils.substitutionTokens; -/// API: initializes the renderer, it should be called once -// -/// @param options initialization options. -/// - sqlClass: class used to access postgres, by default is PSQL -/// the class should provide the following interface -/// - constructor(params) where params should contain: -/// host, port, database, user, password. -/// the class is always constructed with dbParams passed to -/// getRender as-is -/// - query(sql, callback(err, data), readonly) -/// gets an SQL query and return a javascript object with -/// the same structure of a JSON format response from -/// CartoDB-SQL-API, for reference see -/// http://github.com/CartoDB/CartoDB-SQL-API/blob/1.8.2/doc/API.md#json -/// The 'readonly' parameter (false by default) requests -/// that running the query should not allowed to change the database. -/// - dbPoolParams: database connection pool params -/// - size: maximum number of resources to create at any given time -/// - idleTimeout: max milliseconds a resource can go unused before it should be destroyed -/// - reapInterval: frequency to check for idle resources -/// -function TorqueFactory (options) { - this.options = options || {}; - this.options = Object.assign({ - sqlClass: PSQLAdaptor(PSQL, options.dbPoolParams) - }, this.options); - - if (this.options.sqlClass) { - this.sqlClass = this.options.sqlClass; - } -} - -module.exports = TorqueFactory; -const NAME = 'torque'; -module.exports.NAME = NAME; - -var formatToRenderer = { +const formatToRenderer = { 'torque.json': Renderer, 'torque.bin': Renderer, 'json.torque': Renderer, @@ -60,68 +19,137 @@ var formatToRenderer = { 'torque.png': PngRenderer }; -TorqueFactory.prototype = { - /// API: renderer name, use for information purposes - name: NAME, +module.exports = class TorqueFactory { + static get NAME () { + return 'torque'; + } - /// API: tile formats this module is able to render - // TODO: deprecate 'json.torque' and 'bin.torque' with 1.18.0 - supported_formats: Object.keys(formatToRenderer), + // API: initializes the renderer, it should be called once + // + // @param options initialization options. + // - dbPoolParams: database connection pool params + // - size: maximum number of resources to create at any given time + // - idleTimeout: max milliseconds a resource can go unused before it should be destroyed + // - reapInterval: frequency to check for idle resources + constructor (options = {}) { + this.options = options; + } - getName: function () { - return this.name; - }, + getName () { + return TorqueFactory.NAME; + } - supportsFormat: function (format) { + supportsFormat (format) { return !!formatToRenderer[format]; - }, + } - getAdaptor: function (renderer, onTileErrorStrategy) { + getAdaptor (renderer, onTileErrorStrategy) { return new BaseAdaptor(renderer, onTileErrorStrategy); - }, + } - getRenderer: function (mapConfig, format, options, callback) { - var dbParams = RendererParams.dbParamsFromReqParams(options.params); - var layer = options.layer; + getRenderer (mapConfig, format, options, callback) { + const layer = options.layer; if (layer === undefined) { return callback(new Error('torque renderer only supports a single layer')); } + if (!formatToRenderer[format]) { return callback(new Error('format not supported: ' + format)); } - var layerObj = mapConfig.getLayer(layer); + + const layerObj = mapConfig.getLayer(layer); + if (!layerObj) { return callback(new Error('layer index is greater than number of layers')); } + if (layerObj.type !== 'torque') { return callback(new Error('layer ' + layer + ' is not a torque layer')); } - dbParams = Object.assign(dbParams, mapConfig.getLayerDatasource(layer)); - - var pgSQL = sql(dbParams, this.sqlClass); - fetchMapConfigAttributes(pgSQL, layerObj, function (err, params) { - if (err) { - return callback(err); - } + const dbParams = Object.assign({}, parseDbParams(options.params), mapConfig.getLayerDatasource(layer)); + const psql = new PsqlAdaptor({ connectionParams: dbParams, poolParams: this.options.dbPoolParams }); - var RendererClass = formatToRenderer[format]; + fetchLayerAttributes(psql, layerObj) + .then((params) => { + const RendererClass = formatToRenderer[format]; - callback(null, new RendererClass(layerObj, pgSQL, params, layer)); - }); + callback(null, new RendererClass(layerObj, psql, params, layer)); + }) + .catch((err) => callback(err)); } }; +// returns an array of errors if the mapconfig is not supported or contains errors +// errors is empty is the configuration is ok +// dbParams: host, dbname, user and pass +// layer: optional, if is specified only a layer is checked +async function fetchLayerAttributes (psql, layer) { + try { + const attrs = checkLayerAttributes(layer); + const layerSql = SubstitutionTokens.replaceXYZ(layer.options.sql, { x: 0, y: 0, z: 0 }); + + // fetch the schema to know if torque colum is time column + const columnsQuery = `select * from (${layerSql}) __torque_wrap_sql limit 0`; + const columns = await psql.query(columnsQuery); + + if (!columns) { + debug(`ERROR: layer query '${layerSql}' returned no data`); + throw new Error('Layer query returned no data'); + } + + if (!Object.prototype.hasOwnProperty.call(columns.fields, attrs.column)) { + debug(`ERROR: layer query ${layerSql} does not include time-attribute column '${attrs.column}'`); + throw new Error(`Layer query did not return the requested time-attribute column '${attrs.column}'`); + } + + // get time bounds to calculate step + const isTime = columns.fields[attrs.column].type === 'date'; + const stepQuery = getAttributesStepQuery(layerSql, attrs.column, isTime); + + const { rows } = await psql.query(stepQuery); + const data = rows[0]; + + let attributeStep = (data.max_date - data.min_date + 1) / Math.min(attrs.steps, data.num_steps >> 0); + attributeStep = Math.abs(attributeStep) === Infinity ? 0 : attributeStep; + + const attributes = { + start: data.min_date, + end: data.max_date, + step: attributeStep || 1, + data_steps: data.num_steps >> 0, + is_time: isTime + }; + + return Object.assign(attrs, attributes); + } catch (err) { + err.message = `TorqueRenderer: ${err.message}`; + throw err; + } +} + +// check layer and raise an exception is some error is found // +// @throw Error if missing or malformed CartoCSS +function checkLayerAttributes (layerConfig) { + const checks = ['steps', 'resolution', 'column', 'countby']; + const cartoCSS = layerConfig.options.cartocss; + + if (!cartoCSS) { + throw new Error('cartocss can\'t be undefined'); + } + + return attrsFromCartoCSS(cartoCSS, checks); +} + // given cartocss return javascript properties // // @param required optional array of required properties // // @throw Error if required properties are not found -// function attrsFromCartoCSS (cartocss, required) { - var attrsKeys = { + const attrsKeys = { '-torque-frame-count': 'steps', '-torque-resolution': 'resolution', '-torque-animation-duration': 'animationDuration', @@ -129,8 +157,10 @@ function attrsFromCartoCSS (cartocss, required) { '-torque-time-attribute': 'column', '-torque-data-aggregation': 'data_aggregation' }; + carto.tree.Reference.setData(torqueReference.version.latest); - var env = { + + const env = { benchmark: false, validation_data: false, effects: [], @@ -139,23 +169,25 @@ function attrsFromCartoCSS (cartocss, required) { this.errors.push(e); } }; - var symbolizers = torqueReference.version.latest.layer; - var root = (carto.Parser(env)).parse(cartocss); - var definitions = root.toList(env); - var rules = getMapProperties(definitions, env); - var attrs = {}; - for (var k in attrsKeys) { + const symbolizers = torqueReference.version.latest.layer; + const root = (carto.Parser(env)).parse(cartocss); + const definitions = root.toList(env); + const rules = getMapProperties(definitions, env); + const attrs = {}; + + for (const k in attrsKeys) { if (rules[k]) { attrs[attrsKeys[k]] = rules[k].value.toString(); - var element = rules[k].value.value[0]; - var type = symbolizers[k].type; + const element = rules[k].value.value[0]; + const type = symbolizers[k].type; if (!checkValidType(element, type)) { - throw new Error("Unexpected type for property '" + k + "', expected " + type); + throw new Error(`Unexpected type for property '${k}', expected ${type}`); } } else if (required && required.indexOf(attrsKeys[k]) !== -1) { - throw new Error("Missing required property '" + k + "' in torque layer CartoCSS"); + throw new Error(`Missing required property '${k}' in torque layer CartoCSS`); } } + return attrs; } @@ -169,131 +201,36 @@ function checkValidType (e, type) { } else if (type === 'color') { return checkValidColor(e); } + return true; } function checkValidColor (e) { - var expectedArguments = { rgb: 3, hsl: 3, rgba: 4, hsla: 4 }; + const expectedArguments = { rgb: 3, hsl: 3, rgba: 4, hsla: 4 }; return typeof e.rgb !== 'undefined' || expectedArguments[e.name] === e.args; } -// // get rules from Map definition // stores errors in env.error -// function getMapProperties (definitions, env) { - var rules = {}; - definitions.filter(function (r) { - return r.elements.join('') === 'Map'; - }).forEach(function (r) { - for (var i = 0; i < r.rules.length; i++) { - var key = r.rules[i].name; - rules[key] = r.rules[i].ev(env); - } - }); - return rules; -} - -// -// check layer and raise an exception is some error is found -// -// @throw Error if missing or malformed CartoCSS -// -function _checkLayerAttributes (layerConfig) { - var cartoCSS = layerConfig.options.cartocss; - if (!cartoCSS) { - throw new Error("cartocss can't be undefined"); - } - var checks = ['steps', 'resolution', 'column', 'countby']; - return attrsFromCartoCSS(cartoCSS, checks); -} - -// returns an array of errors if the mapconfig is not supported or contains errors -// errors is empty is the configuration is ok -// dbParams: host, dbname, user and pass -// layer: optional, if is specified only a layer is checked -function fetchMapConfigAttributes (sql, layer, callback) { - // check attrs - var attrs; - try { - attrs = _checkLayerAttributes(layer); - } catch (e) { - return callback(e); - } - - // fetch layer attributes to check the query and so on is ok - var layerSql = layer.options.sql; - fetchLayerAttributes(sql, layerSql, attrs, function (err, layerAttrs) { - if (err) { - return callback(err); - } - callback(null, Object.assign(attrs, layerAttrs)); - }); -} - -function fetchLayerAttributes (sql, layerSql, attrs, callback) { - layerSql = SubstitutionTokens.replaceXYZ(layerSql, { x: 0, y: 0, z: 0 }); - - // first step, fetch the schema to know if torque colum is time column - const columnsQuery = `select * from (${layerSql}) __torque_wrap_sql limit 0`; - sql(columnsQuery, function (err, data) { - // second step, get time bounds to calculate step - if (err) { - err.message = 'TorqueRenderer: ' + err.message; - return callback(err); - } - - if (!data) { - debug(`ERROR: layer query '${layerSql}' returned no data`); - const noDataError = new Error('TorqueRenderer: Layer query returned no data'); - return callback(noDataError); - } - - if (!Object.prototype.hasOwnProperty.call(data.fields, attrs.column)) { - debug(`ERROR: layer query ${layerSql} does not include time-attribute column '${attrs.column}'`); - const columnError = new Error( - `TorqueRenderer: Layer query did not return the requested time-attribute column '${attrs.column}'` - ); - return callback(columnError); - } - - const isTime = data.fields[attrs.column].type === 'date'; - const stepQuery = getAttributesStepQuery(layerSql, attrs.column, isTime); - sql(stepQuery, function generateMetadata (err, data) { - // prepare metadata needed to render the tiles - if (err) { - err.message = 'TorqueRenderer: ' + err.message; - return callback(err); - } - - data = data.rows[0]; - - let attributeStep = (data.max_date - data.min_date + 1) / Math.min(attrs.steps, data.num_steps >> 0); - attributeStep = Math.abs(attributeStep) === Infinity ? 0 : attributeStep; - - const attributes = { - start: data.min_date, - end: data.max_date, - step: attributeStep || 1, - data_steps: data.num_steps >> 0, - is_time: isTime - }; - - callback(null, attributes); - }); - }); + return definitions + .filter((definition) => definition.elements.join('') === 'Map') + .map((definition) => definition.rules) + .reduce((rules, rule) => rules.concat(rule), []) // flat array 1 level + .reduce((properties, rule) => { + properties[rule.name] = rule.ev(env); + return properties; + }, {}); } function getAttributesStepQuery (layerSql, column, isTime) { let maxCol = `max(${column})`; let minCol = `min(${column})`; + if (isTime) { maxCol = `date_part('epoch', ${maxCol})`; minCol = `date_part('epoch', ${minCol})`; } - return ` - SELECT count(*) as num_steps, ${maxCol} max_date, ${minCol} min_date - FROM (${layerSql}) __torque_wrap_sql - `; + return `SELECT count(*) as num_steps, ${maxCol} max_date, ${minCol} min_date FROM (${layerSql}) __torque_wrap_sql`; } diff --git a/lib/renderers/torque/index.js b/lib/renderers/torque/index.js deleted file mode 100644 index 20cd310a8..000000000 --- a/lib/renderers/torque/index.js +++ /dev/null @@ -1,4 +0,0 @@ -'use strict'; - -module.exports.factory = require('./factory'); -module.exports.adaptor = require('../base-adaptor'); diff --git a/lib/renderers/torque/png-renderer.js b/lib/renderers/torque/png-renderer.js index bd5bd5b05..2544c78e0 100644 --- a/lib/renderers/torque/png-renderer.js +++ b/lib/renderers/torque/png-renderer.js @@ -9,13 +9,13 @@ const Timer = require('../../stats/timer'); const { promisify } = require('util'); module.exports = class TorquePngRenderer extends TorqueRenderer { - constructor (layer, sql, attrs) { + constructor (layer, psql, attrs) { const cartoCssOptions = torque.common.TorqueLayer.optionsFromCartoCSS(layer.options.cartocss); const rendererOptions = { bufferSize: cartoCssOptions['buffer-size'] !== undefined ? cartoCssOptions['buffer-size'] : 32 }; - super(layer, sql, attrs, rendererOptions); + super(layer, psql, attrs, rendererOptions); this.canvasImages = []; const self = this; @@ -77,7 +77,7 @@ module.exports = class TorquePngRenderer extends TorqueRenderer { const timer = new Timer(); const attrs = Object.assign({ stepSelect: this.step, stepOffset: this.stepOffset }, this.attrs); - const { buffer: rows, stats } = await this.getTileData(this.sql, { x: x, y: y }, z, this.layer.options.sql, attrs); + const { buffer: rows, stats } = await this.getTileData(this.psql, { x: x, y: y }, z, this.layer.options.sql, attrs); timer.start('render'); const canvas = createCanvas(this.tile_size, this.tile_size); diff --git a/lib/renderers/torque/psql-adaptor.js b/lib/renderers/torque/psql-adaptor.js index e4ad583e4..84e127c68 100644 --- a/lib/renderers/torque/psql-adaptor.js +++ b/lib/renderers/torque/psql-adaptor.js @@ -1,42 +1,34 @@ 'use strict'; -function PSQLAdaptor (PSQLClass, poolParams) { - var ctor = function (params) { - this._psql = new PSQLClass(params, poolParams); - }; - ctor.prototype.query = function (sql, callback, readonly) { - this._psql.query(sql, this._handleResult.bind(this, callback), readonly); - }; - ctor.prototype._handleResult = function (callback, err, result) { - if (err) { callback(err); return; } - var formatted = { - fields: this._formatResultFields(result.fields), - rows: result.rows - }; - callback(null, formatted); - }; - ctor.prototype._formatResultFields = function (flds) { - var nfields = {}; - for (var i = 0; i < flds.length; ++i) { - var f = flds[i]; - var cname = this._psql.typeName(f.dataTypeID); - var tname; - if (!cname) { - tname = 'unknown(' + f.dataTypeID + ')'; - } else { - tname = typeName(cname); - } - // console.log('cname:'+cname+' tname:'+tname); - nfields[f.name] = { type: tname }; - } - return nfields; - }; - - return ctor; -} +const Psql = require('cartodb-psql'); + +module.exports = class PSQLAdaptor { + constructor ({ connectionParams, poolParams }) { + this._psql = new Psql(connectionParams, poolParams); + } + + query (sql, readonly = true) { + return new Promise((resolve, reject) => { + this._psql.query(sql, (err, result) => { + if (err) { + return reject(err); + } + + const fields = result.fields.reduce((fields, field) => { + const cname = this._psql.typeName(field.dataTypeID); + const type = cname ? typeName(cname) : `unknown(${field.dataTypeID})`; + fields[field.name] = { type }; + return fields; + }, {}); + + return resolve({ fields, rows: result.rows }); + }, readonly); + }); + } +}; function typeName (cname) { - var tname = cname; + let tname = cname; if (cname.match('bool')) { tname = 'boolean'; @@ -54,5 +46,3 @@ function typeName (cname) { return tname; } - -module.exports = PSQLAdaptor; diff --git a/lib/renderers/torque/renderer.js b/lib/renderers/torque/renderer.js index 5b043aa3f..d396b6aa5 100644 --- a/lib/renderers/torque/renderer.js +++ b/lib/renderers/torque/renderer.js @@ -4,13 +4,12 @@ const format = require('../../utils/format'); const Timer = require('../../stats/timer'); const debug = require('debug')('windshaft:renderer:torque'); const SubstitutionTokens = require('cartodb-query-tables').utils.substitutionTokens; -const { promisify } = require('util'); module.exports = class TorqueRenderer { - constructor (layer, sql, attrs, options) { + constructor (layer, psql, attrs, options) { options = options || {}; - this.sql = promisify(sql); + this.psql = psql; this.attrs = attrs; this.layer = layer; @@ -21,7 +20,7 @@ module.exports = class TorqueRenderer { } async getTile (format, z, x, y) { - const { buffer, headers, stats } = await this.getTileData(this.sql, { x: x, y: y }, z, this.layer.options.sql, this.attrs); + const { buffer, headers, stats } = await this.getTileData(this.psql, { x: x, y: y }, z, this.layer.options.sql, this.attrs); return { buffer, headers, stats }; } @@ -47,7 +46,7 @@ module.exports = class TorqueRenderer { return meta; } - async getTileData (sql, coord, zoom, layerSql, attrs) { + async getTileData (psql, coord, zoom, layerSql, attrs) { let columnConv = attrs.column; if (attrs.is_time) { @@ -119,7 +118,7 @@ module.exports = class TorqueRenderer { const timer = new Timer(); timer.start('query'); - const data = await sql(query); + const data = await psql.query(query); timer.end('query'); return { buffer: data.rows, headers: { 'Content-Type': 'application/json' }, stats: timer.getTimes() }; diff --git a/lib/stats/timer.js b/lib/stats/timer.js index 6001cae93..e9b37efc5 100644 --- a/lib/stats/timer.js +++ b/lib/stats/timer.js @@ -1,37 +1,35 @@ 'use strict'; -function Timer () { - this.times = {}; -} - -module.exports = Timer; - -Timer.prototype.start = function (label) { - this.timeIt(label, 'start'); -}; - -Timer.prototype.end = function (label) { - this.timeIt(label, 'end'); -}; - -Timer.prototype.timeIt = function (label, what) { - this.times[label] = this.times[label] || {}; - this.times[label][what] = Date.now(); -}; - -Timer.prototype.getTimes = function () { - var self = this; - var times = {}; - - Object.keys(this.times).forEach(function (label) { - var stat = self.times[label]; - if (stat.start && stat.end) { - var elapsed = stat.end - stat.start; - if (elapsed > 0) { - times[label] = elapsed; +module.exports = class Timer { + constructor () { + this.times = {}; + } + + start (label) { + this.timeIt(label, 'start'); + } + + end (label) { + this.timeIt(label, 'end'); + } + + timeIt (label, what) { + this.times[label] = this.times[label] || {}; + this.times[label][what] = Date.now(); + } + + getTimes () { + const times = {}; + + for (const [label, stat] of Object.entries(this.times)) { + if (stat.start && stat.end) { + const elapsed = stat.end - stat.start; + if (elapsed > 0) { + times[label] = elapsed; + } } } - }); - return times; + return times; + } }; diff --git a/lib/storages/mapstore.js b/lib/storages/mapstore.js index d68b405a9..58d2472b9 100644 --- a/lib/storages/mapstore.js +++ b/lib/storages/mapstore.js @@ -1,166 +1,115 @@ 'use strict'; -// Map configuration storage - -var RedisPool = require('redis-mpool'); -var MapConfig = require('../models/mapconfig'); - -/** - * @constructor - * @type {MapStore} - */ -function MapStore (opts) { - opts = opts || {}; - this._config = Object.assign({ - redis_host: '127.0.0.1', - redis_port: 6379, - redis_db: 0, - redis_key_mapcfg_prefix: 'map_cfg|', - expire_time: 300, // in seconds (7200 is 5 hours; 300 is 5 minutes) - pool_max: 50, - pool_idleTimeout: 10000, // in milliseconds - pool_reapInterval: 1000, // in milliseconds - pool_log: false - }, opts); - this.redis_pool = this._get('pool'); - if (!this.redis_pool) { - var redisOpts = { - host: this._get('redis_host'), - port: this._get('redis_port'), - max: this._get('pool_max'), - idleTimeoutMillis: this._get('pool_idleTimeout'), - reapIntervalMillis: this._get('pool_reapInterval'), - log: this._get('pool_log') - }; - this.redis_pool = new RedisPool(redisOpts); - } - this.logger = opts.logger; -} - -var o = MapStore.prototype; - -/// Internal method: get configuration item -o._get = function (key) { - return this._config[key]; -}; - -/// Internal method: get redis pool -o._redisPool = function () { - return this.redis_pool; +const RedisPool = require('redis-mpool'); +const MapConfig = require('../models/mapconfig'); + +const defaultOptions = { + redis_host: '127.0.0.1', + redis_port: 6379, + redis_db: 0, + redis_key_mapcfg_prefix: 'map_cfg|', + expire_time: 300, // in seconds + pool: undefined, // redis pool client + pool_max: 50, + pool_idleTimeout: 10000, // in milliseconds + pool_reapInterval: 1000, // in milliseconds + pool_log: false }; -/// Internal method: run redis command -// -/// @param func - the redis function to execute (uppercase required!) -/// @param args - the arguments for the redis function in an array -/// NOTE: the array will be modified -/// @param callback - function(err,val) function to pass results too. -/// -o._redisCmd = function (func, args, callback) { - const pool = this._redisPool(); - const db = this._get('redis_db'); - - pool.acquire(db, (err, client) => { - if (err) { - this.log(err, 'adquiring client'); - return callback(err); +module.exports = class MapStore { + constructor (options = {}) { + Object.assign(this, defaultOptions, options); + + if (!this.pool) { + this.pool = new RedisPool({ + host: this.redis_host, + port: this.redis_port, + max: this.pool_max, + idleTimeoutMillis: this.pool_idleTimeout, + reapIntervalMillis: this.pool_reapInterval, + log: this.pool_log + }); } + } + + _key (id) { + return `${this.redis_key_mapcfg_prefix}${id}`; + } - client[func](...args, (err, data) => { - pool.release(db, client); + _redisCmd (func, args, callback) { + const db = this.redis_db; + this.pool.acquire(db, (err, client) => { if (err) { - this.log(err, func, args); + return callback(err); } - if (callback) { + client[func](...args, (err, data) => { + this.pool.release(db, client); + if (err) { return callback(err); } return callback(null, data); - } + }); }); - }); -}; + } -/// API: Load a saved MapStore object, renewing expiration time -// -/// Static method -/// -/// @param id the MapStore identifier -/// @param callback function(err, mapConfig) callback function -/// -o.load = function (id, callback) { - const key = this._get('redis_key_mapcfg_prefix') + id; - const exp = this._get('expire_time'); - - this._redisCmd('GET', [key], (err, json) => { - if (err) { - return callback(err); - } + // API: Load a saved MapStore object, renewing expiration time + load (id, callback) { + const key = this._key(id); - if (!json) { - const mapConfigError = new Error(`Invalid or nonexistent map configuration token '${id}'`); - return callback(mapConfigError); - } + this._redisCmd('GET', [key], (err, json) => { + if (err) { + return callback(err); + } - let mapConfig; - try { - const serializedMapConfig = JSON.parse(json); - mapConfig = MapConfig.create(serializedMapConfig); - } catch (error) { - return callback(error); - } + if (!json) { + return callback(new Error(`Invalid or nonexistent map configuration token '${id}'`)); + } - // Postpone expiration for the key - // not waiting for response - this._redisCmd('EXPIRE', [key, exp]); + let mapConfig; + try { + const serializedMapConfig = JSON.parse(json); + mapConfig = MapConfig.create(serializedMapConfig); + } catch (error) { + return callback(error); + } - return callback(null, mapConfig); - }); -}; + this._redisCmd('EXPIRE', [key, this.expire_time], (err) => { + if (err) { + return callback(err); + } -/// API: store map to redis -// -/// @param map MapConfig to store -/// @param callback function(err, id, known) called when save is completed -/// -o.save = function (map, callback) { - const id = map.id(); - const key = this._get('redis_key_mapcfg_prefix') + id; - const exp = this._get('expire_time'); - - this._redisCmd('SETNX', [key, map.serialize()], (err, wasNew) => { - if (err) { - return callback(err); - } + return callback(null, mapConfig); + }); + }); + } - this._redisCmd('EXPIRE', [key, exp]); // not waiting for response + // API: store map to redis + save (map, callback) { + const id = map.id(); + const key = this._key(id); - return callback(null, id, !wasNew); - }); -}; + this._redisCmd('SETNX', [key, map.serialize()], (err, wasNew) => { + if (err) { + return callback(err); + } -/// API: delete map from store -// -/// @param map MapConfig to delete from store -/// @param callback function(err, id, known) called when save is completed -/// -o.del = function (id, callback) { - const key = this._get('redis_key_mapcfg_prefix') + id; - this._redisCmd('DEL', [key], callback); -}; + this._redisCmd('EXPIRE', [key, this.expire_time], (err) => { + if (err) { + return callback(err); + } -o.log = function (err, command, args = []) { - if (this.logger) { - const log = { - error: err.message, - command, - args - }; - this.logger.error(JSON.stringify(log)); + return callback(null, id, !wasNew); + }); + }); } -}; -module.exports = MapStore; + // API: delete map from store + del (id, callback) { + const key = this._key(id); + this._redisCmd('DEL', [key], callback); + } +}; diff --git a/lib/utils/cartocss-utils.js b/lib/utils/cartocss-utils.js index 74b38973b..3147340c7 100644 --- a/lib/utils/cartocss-utils.js +++ b/lib/utils/cartocss-utils.js @@ -1,27 +1,28 @@ 'use strict'; -var carto = require('carto'); -var torqueReference = require('torque.js').cartocss_reference; +const carto = require('carto'); +const torqueReference = require('torque.js').cartocss_reference; +const MAP_ATTRIBUTES = ['buffer-size']; +carto.tree.Reference.setData(torqueReference.version.latest); -module.exports.getColumnNamesFromCartoCSS = function (cartocss) { +module.exports.getColumnNamesFromCartoCSS = function getColumnNamesFromCartoCSS (cartocss) { return selectors(cartocss); }; -carto.tree.Reference.setData(torqueReference.version.latest); - -var MAP_ATTRIBUTES = ['buffer-size']; -module.exports.optionsFromCartoCSS = function (cartocss) { - var shader = new carto.RendererJS().render(cartocss); - var mapConfig = shader.findLayer({ name: 'Map' }); +module.exports.optionsFromCartoCSS = function optionsFromCartoCSS (cartocss) { + const shader = new carto.RendererJS().render(cartocss); + const mapConfig = shader.findLayer({ name: 'Map' }); - var options = {}; + const options = {}; if (mapConfig) { - MAP_ATTRIBUTES.reduce(function (opts, attributeName) { - var v = mapConfig.eval(attributeName); + MAP_ATTRIBUTES.reduce((opts, attributeName) => { + const v = mapConfig.eval(attributeName); + if (v !== undefined) { opts[attributeName] = v; } + return opts; }, options); } @@ -30,7 +31,7 @@ module.exports.optionsFromCartoCSS = function (cartocss) { }; function selectors (cartocss) { - var env = { + const env = { benchmark: false, validation_data: false, effects: [], @@ -39,11 +40,11 @@ function selectors (cartocss) { this.errors.push(e); } }; - var parser = carto.Parser(env); - var tree = parser.parse(cartocss); - var definitions = tree.toList(env); + const parser = carto.Parser(env); + const tree = parser.parse(cartocss); + const definitions = tree.toList(env); - var allSelectors = {}; + const allSelectors = {}; appendFiltersKeys(definitions, allSelectors); appendRulesFields(definitions, allSelectors); @@ -52,9 +53,9 @@ function selectors (cartocss) { function appendFiltersKeys (definitions, allSelectors) { definitions - .reduce(function (filters, r) { + .reduce((filters, r) => { if (r.filters && r.filters.filters) { - Object.keys(r.filters.filters).forEach(function (filterId) { + Object.keys(r.filters.filters).forEach((filterId) => { allSelectors[r.filters.filters[filterId].key.value] = true; }); } @@ -66,22 +67,12 @@ function appendFiltersKeys (definitions, allSelectors) { function appendRulesFields (definitions, allSelectors) { definitions - .map(function (r) { - return r.rules; - }) - .reduce(function (allRules, rules) { - return allRules.concat(rules); - }, []) - .reduce(function (allValues, rule) { - return values(rule.value, allValues); - }, []) - .filter(function (values) { - return values.is === 'field'; - }) - .map(function (rule) { - return rule.value; - }) - .reduce(function (keys, field) { + .map((r) => r.rules) + .reduce((allRules, rules) => allRules.concat(rules), []) + .reduce((allValues, rule) => values(rule.value, allValues), []) + .filter((values) => values.is === 'field') + .map((rule) => rule.value) + .reduce((keys, field) => { keys[field] = true; return keys; }, allSelectors); @@ -91,16 +82,16 @@ function appendRulesFields (definitions, allSelectors) { function values (value, allValues) { allValues = allValues || []; + if (value.is === 'value' || value.is === 'expression') { if (Array.isArray(value.value)) { - value.value.forEach(function (_value) { - values(_value, allValues); - }); + value.value.forEach((_value) => values(_value, allValues)); } else { values(value.value, allValues); } } else { allValues.push(value); } + return allValues; } diff --git a/lib/utils/default-query-rewriter.js b/lib/utils/default-query-rewriter.js index 8bae41d6d..4bf5e250a 100644 --- a/lib/utils/default-query-rewriter.js +++ b/lib/utils/default-query-rewriter.js @@ -1,17 +1,7 @@ 'use strict'; -// Dummy query-rewriter which doesn't alter queries - -// This class implements the Windshaft query rewriting API: -// -// * query(sql, data) // transform SQL query, with additional data -// -function DefaultQueryRewriter () { -} - -module.exports = DefaultQueryRewriter; - -DefaultQueryRewriter.prototype.query = function (query, data) { - // Not using data parameter, but declared here for documentation purposes - return query; +module.exports = class DefaultQueryRewriter { + query (query) { + return query; + } }; diff --git a/lib/utils/format.js b/lib/utils/format.js index 0d6acfa78..37d368470 100644 --- a/lib/utils/format.js +++ b/lib/utils/format.js @@ -1,15 +1,13 @@ 'use strict'; -function format (str) { - var replacements = Array.prototype.slice.call(arguments, 1); +module.exports = function format (str) { + const replacements = Array.prototype.slice.call(arguments, 1); - replacements.forEach(function (attrs) { - Object.keys(attrs).forEach(function (attr) { - str = str.replace(new RegExp('\\{' + attr + '\\}', 'g'), attrs[attr]); - }); - }); + for (const attrs of replacements) { + for (const [key, attr] of Object.entries(attrs)) { + str = str.replace(new RegExp(`\\{${key}\\}`, 'g'), attr); + } + } return str; -} - -module.exports = format; +}; diff --git a/lib/utils/layer-columns.js b/lib/utils/layer-columns.js deleted file mode 100644 index 50f3e9e77..000000000 --- a/lib/utils/layer-columns.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -var cartocssUtils = require('./cartocss-utils'); - -var EXCLUDE_PROPERTIES = { - 'mapnik::geometry_type': true, - 'mapnik-geometry-type': true -}; - -module.exports.getColumns = function (layerOptions) { - var columns = []; - - if (Array.isArray(layerOptions.columns)) { - columns = layerOptions.columns; - } - - if (typeof layerOptions.cartocss === 'string') { - columns = columns.concat(cartocssUtils.getColumnNamesFromCartoCSS(layerOptions.cartocss)); - } - - columns = columns.concat(getColumnNamesFromInteractivity(layerOptions.interactivity)); - - // filter out repeated ones and non string values - columns = columns - .filter(function (item) { - return typeof item === 'string' && item.length > 0; - }) - .filter(function (item) { - return !Object.prototype.hasOwnProperty.call(EXCLUDE_PROPERTIES, item); - }) - .filter(function (item, pos, self) { - return self.indexOf(item) === pos; - }); - - return columns; -}; - -function getColumnNamesFromInteractivity (interactivity) { - interactivity = interactivity || []; - - var columnNameFromInteractivity = []; - - if (Array.isArray(interactivity)) { - columnNameFromInteractivity = columnNameFromInteractivity.concat(interactivity); - } else { - columnNameFromInteractivity = columnNameFromInteractivity.concat(interactivity.split(',')); - } - - return columnNameFromInteractivity; -} diff --git a/lib/utils/layer-filter.js b/lib/utils/layers-filter.js similarity index 84% rename from lib/utils/layer-filter.js rename to lib/utils/layers-filter.js index 6da253da6..735dd5275 100644 --- a/lib/utils/layer-filter.js +++ b/lib/utils/layers-filter.js @@ -1,5 +1,29 @@ 'use strict'; +module.exports = function layersFilter (mapConfig, layerFilter) { + layerFilter = defaultLayerFilter(layerFilter); + + let filteredLayers = []; + + if (typeof layerFilter === 'string') { + filteredLayers = resolveStringFilter(mapConfig, layerFilter); + } else if (!Array.isArray(layerFilter)) { + filteredLayers = [layerFilter]; + } else { + filteredLayers = layerFilter; + } + + if (!filteredLayers.every(Number.isFinite)) { + throw new Error('Invalid layer filtering'); + } + + filteredLayers = filteredLayers.sort((a, b) => a - b); + + checkLayerBounds(mapConfig, filteredLayers); + + return filteredLayers; +}; + const layerAliases = { all: true, mapnik: true, @@ -8,10 +32,6 @@ const layerAliases = { plain: true }; -function isAlias (layerFilter) { - return Object.prototype.hasOwnProperty.call(layerAliases, layerFilter); -} - function defaultLayerFilter (layerFilter) { if (layerFilter === undefined) { return 'mapnik'; @@ -23,16 +43,20 @@ function resolveAlias (mapConfig, alias) { if (Number.isFinite(+alias)) { return [+alias]; } + if (alias === 'all') { return mapConfig.getLayers().map((_, i) => i); } + if (!Object.prototype.hasOwnProperty.call(layerAliases, alias)) { throw new Error('Invalid layer filtering'); } + return mapConfig.getLayers().reduce((filteredLayers, _, index) => { if (mapConfig.layerType(index) === alias) { filteredLayers.push(index); } + return filteredLayers; }, []); } @@ -42,6 +66,7 @@ function resolveIds (mapConfig, layerIds) { if (layerIdsToNumber.every(Number.isFinite)) { return layerIdsToNumber; } + if (layerIdsToNumber.every(Number.isNaN)) { return layerIds.map(mapConfig.getIndexByLayerId.bind(mapConfig)); } @@ -50,8 +75,8 @@ function resolveIds (mapConfig, layerIds) { } function checkLayerBounds (mapConfig, filteredLayers) { - var uppermostLayerIdx = filteredLayers[filteredLayers.length - 1]; - var lowestLayerIdx = filteredLayers[0]; + const uppermostLayerIdx = filteredLayers[filteredLayers.length - 1]; + const lowestLayerIdx = filteredLayers[0]; if (lowestLayerIdx < 0 || uppermostLayerIdx >= mapConfig.getLayers().length) { throw new Error('Invalid layer filtering'); @@ -62,33 +87,10 @@ function resolveStringFilter (mapConfig, layerFilter) { if (isAlias(layerFilter)) { return resolveAlias(mapConfig, layerFilter); } + return resolveIds(mapConfig, layerFilter.split(',')); } -module.exports = function filterLayer (mapConfig, layerFilter) { - layerFilter = defaultLayerFilter(layerFilter); - - var filteredLayers = []; - - if (typeof layerFilter === 'string') { - filteredLayers = resolveStringFilter(mapConfig, layerFilter); - } else if (!Array.isArray(layerFilter)) { - filteredLayers = [layerFilter]; - } else { - filteredLayers = layerFilter; - } - - if (!filteredLayers.every(Number.isFinite)) { - throw new Error('Invalid layer filtering'); - } - - filteredLayers = filteredLayers.sort(function (a, b) { return a - b; }); - - checkLayerBounds(mapConfig, filteredLayers); - - return filteredLayers; -}; - -module.exports.isSingleLayer = function isSingleLayer (layerFilter) { - return Number.isFinite(+layerFilter); -}; +function isAlias (layerFilter) { + return Object.prototype.hasOwnProperty.call(layerAliases, layerFilter); +} diff --git a/lib/utils/sql.js b/lib/utils/sql.js deleted file mode 100644 index 718826250..000000000 --- a/lib/utils/sql.js +++ /dev/null @@ -1,19 +0,0 @@ -'use strict'; - -var debug = require('debug')('windshaft:utils'); - -function sql (dbParams, SQLClass) { - // TODO: cache class instances by dbParams/sqlClass - return function (query, callback) { - var pg; - try { - debug('Running query %s with params %s', query, dbParams); - pg = new SQLClass(dbParams); - pg.query(query, callback, true); // ensure read-only transaction - } catch (err) { - callback(err); - } - }; -} - -module.exports = sql; diff --git a/test/acceptance/torque-test.js b/test/acceptance/torque-test.js index e024d3cf5..b35452405 100644 --- a/test/acceptance/torque-test.js +++ b/test/acceptance/torque-test.js @@ -27,7 +27,7 @@ describe('torque', function () { var testClient = new TestClient(createTorqueMapConfig('Map { marker-fill:blue; }')); testClient.createLayergroup(function (err) { assert.ok(err); - assert.equal(err.message, "Missing required property '-torque-frame-count' in torque layer CartoCSS"); + assert.equal(err.message, "TorqueRenderer: Missing required property '-torque-frame-count' in torque layer CartoCSS"); done(); }); }); @@ -36,7 +36,7 @@ describe('torque', function () { var testClient = new TestClient(createTorqueMapConfig('Map { -torque-frame-count: 2; }')); testClient.createLayergroup(function (err) { assert.ok(err); - assert.equal(err.message, "Missing required property '-torque-resolution' in torque layer CartoCSS"); + assert.equal(err.message, "TorqueRenderer: Missing required property '-torque-resolution' in torque layer CartoCSS"); done(); }); }); @@ -49,7 +49,7 @@ describe('torque', function () { assert.ok(err); assert.equal( err.message, - "Missing required property '-torque-aggregation-function' in torque layer CartoCSS" + "TorqueRenderer: Missing required property '-torque-aggregation-function' in torque layer CartoCSS" ); done(); }); @@ -75,7 +75,7 @@ describe('torque', function () { var testClient = new TestClient(layergroup); testClient.createLayergroup(function (err) { assert.ok(err); - assert.equal(err.message, "Unexpected type for property '-torque-aggregation-function', expected string"); + assert.equal(err.message, "TorqueRenderer: Unexpected type for property '-torque-aggregation-function', expected string"); done(); }); }); @@ -250,7 +250,7 @@ describe('torque', function () { var testClient = new TestClient(layergroup); testClient.createLayergroup(function (err) { assert.ok(err); - assert.equal(err.message, "Unexpected type for property '-torque-aggregation-function', expected string"); + assert.equal(err.message, "TorqueRenderer: Unexpected type for property '-torque-aggregation-function', expected string"); done(); }); }); diff --git a/test/integration/renderers/http-factory-test.js b/test/integration/renderers/http-factory-test.js index 96d6be72b..932b2b4e1 100644 --- a/test/integration/renderers/http-factory-test.js +++ b/test/integration/renderers/http-factory-test.js @@ -63,9 +63,12 @@ describe('renderer_http_factory_getRenderer', function () { }); it('getRenderer returns a fallback image renderer for invalid urlTemplate', function (done) { - var factoryWithFallbackImage = new HttpRendererFactory( - whitelistSample, 2000, undefined, 'http://example.com/fallback.png' - ); + var factoryWithFallbackImage = new HttpRendererFactory({ + whitelist: whitelistSample, + timeout: 2000, + proxy: undefined, + fallbackImage: 'http://example.com/fallback.png' + }); var mapConfig = MapConfig.create({ layers: [ { @@ -87,9 +90,12 @@ describe('renderer_http_factory_getRenderer', function () { it('returns a renderer for invalid urlTemplate if whitelist is _open-minded_', function (done) { var whitelistAnyUrl = ['.*']; - var factoryWithFallbackImage = new HttpRendererFactory( - whitelistAnyUrl, 2000, undefined, 'http://example.com/fallback.png' - ); + var factoryWithFallbackImage = new HttpRendererFactory({ + whitelist: whitelistAnyUrl, + timeout: 2000, + proxy: undefined, + fallbackImage: 'http://example.com/fallback.png' + }); var mapConfig = MapConfig.create({ layers: [ { @@ -115,7 +121,10 @@ describe('renderer_http_factory_getRenderer', function () { it('returns a renderer with valid subdomains for a subdomain template', function (done) { var whitelistAnyUrl = ['.*']; - var factoryWithFallbackImage = new HttpRendererFactory(whitelistAnyUrl, 2000); + var factoryWithFallbackImage = new HttpRendererFactory({ + whitelist: whitelistAnyUrl, + timeout: 2000 + }); var mapConfig = MapConfig.create({ layers: [ { @@ -138,7 +147,10 @@ describe('renderer_http_factory_getRenderer', function () { it('returns a renderer with valid subdomains for a NON subdomain template', function (done) { var whitelistAnyUrl = ['.*']; - var factoryWithFallbackImage = new HttpRendererFactory(whitelistAnyUrl, 2000); + var factoryWithFallbackImage = new HttpRendererFactory({ + whitelist: whitelistAnyUrl, + timeout: 2000 + }); var mapConfig = MapConfig.create({ layers: [ { diff --git a/test/unit/renderers/mapnik-factory-test.js b/test/unit/renderers/mapnik-factory-test.js index 8d6275138..62264ed1d 100644 --- a/test/unit/renderers/mapnik-factory-test.js +++ b/test/unit/renderers/mapnik-factory-test.js @@ -9,7 +9,7 @@ var MapConfig = require('../../../lib/models/mapconfig'); describe('renderer-mapnik-factory metatile', function () { it('should use default metatile value', function () { var factory = new MapnikRendererFactory({}); - assert.equal(factory.getMetatile('png'), 4); + assert.equal(factory._getMetatile('png'), 4); }); it('should use provided metatile value', function () { @@ -18,7 +18,7 @@ describe('renderer-mapnik-factory metatile', function () { metatile: 1 } }); - assert.equal(factory.getMetatile('png'), 1); + assert.equal(factory._getMetatile('png'), 1); }); it('should use provided formatMetatile value', function () { @@ -30,7 +30,7 @@ describe('renderer-mapnik-factory metatile', function () { } } }); - assert.equal(factory.getMetatile('png'), 4); + assert.equal(factory._getMetatile('png'), 4); }); }); @@ -60,7 +60,7 @@ describe('renderer-mapnik-factory buffer-size', function () { it('should use default buffer-size value', function () { var factory = new MapnikRendererFactory({}); - assert.equal(factory.getBufferSize(mapConfig, 'png'), 64); + assert.equal(factory._getBufferSize(mapConfig, 'png'), 64); }); it('should use provided buffer-size value', function () { @@ -69,7 +69,7 @@ describe('renderer-mapnik-factory buffer-size', function () { bufferSize: 128 } }); - assert.equal(factory.getBufferSize(mapConfig, 'png'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'png'), 128); }); it('should use provided formatBufferSize value', function () { @@ -81,7 +81,7 @@ describe('renderer-mapnik-factory buffer-size', function () { } } }); - assert.equal(factory.getBufferSize(mapConfig, 'png'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'png'), 128); }); it('should use provided buffer-size value', function () { @@ -93,7 +93,7 @@ describe('renderer-mapnik-factory buffer-size', function () { } } }); - assert.equal(factory.getBufferSize(mapConfig, 'mvt'), 64); + assert.equal(factory._getBufferSize(mapConfig, 'mvt'), 64); }); it('should use value provided by mapConfig for png and mvt', function () { @@ -121,9 +121,9 @@ describe('renderer-mapnik-factory buffer-size', function () { } } }); - assert.equal(factory.getBufferSize(mapConfig, 'png'), 128); - assert.equal(factory.getBufferSize(mapConfig, 'mvt'), 0); - assert.equal(factory.getBufferSize(mapConfig, 'grid.json'), 64); + assert.equal(factory._getBufferSize(mapConfig, 'png'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'mvt'), 0); + assert.equal(factory._getBufferSize(mapConfig, 'grid.json'), 64); }); it('should use value provided by mapConfig for png and mvt', function () { @@ -151,8 +151,8 @@ describe('renderer-mapnik-factory buffer-size', function () { } } }); - assert.equal(factory.getBufferSize(mapConfig, 'png'), 128); - assert.equal(factory.getBufferSize(mapConfig, 'mvt'), 128); - assert.equal(factory.getBufferSize(mapConfig, 'grid.json'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'png'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'mvt'), 128); + assert.equal(factory._getBufferSize(mapConfig, 'grid.json'), 128); }); }); diff --git a/test/unit/torque-test.js b/test/unit/torque-test.js index 551dff89b..ac7a5f88f 100644 --- a/test/unit/torque-test.js +++ b/test/unit/torque-test.js @@ -3,22 +3,20 @@ require('../support/test-helper'); var assert = require('assert'); -var TorqueFactory = require('../../lib/renderers/torque').factory; +var TorqueFactory = require('../../lib/renderers/torque/factory'); var MapConfig = require('../../lib/models/mapconfig'); +const PSQLAdaptor = require('../../lib/renderers/torque/psql-adaptor'); -function dummyPSQL () { - PSQLDummy.n = Date.now(); - function PSQLDummy () { - this.query = function (sql, callback) { - var res = PSQLDummy.responses[PSQLDummy.queries.length]; - // console.log("* ", PSQLDummy.n, sql, " => ", res); - PSQLDummy.queries.push(sql); - callback.apply(module, res); - }; - } - PSQLDummy.queries = []; - PSQLDummy.responses = []; - return PSQLDummy; +function mockPSQLAdaptorQuery ({ columnsQueryResult, stepQueryResult, tileQueryResult = {} }) { + PSQLAdaptor.prototype.query = async function (sql) { + if (sql.endsWith('__torque_wrap_sql limit 0')) { + return columnsQueryResult; + } else if (sql.startsWith('SELECT count(*) as num_steps')) { + return stepQueryResult; + } else { + return tileQueryResult; + } + }; } describe('torque', function () { @@ -72,33 +70,38 @@ describe('torque', function () { var mapConfig = MapConfig.create(layergroupConfig()); - var sqlApi = null; var torque = null; + beforeEach(function () { - sqlApi = dummyPSQL(); - torque = new TorqueFactory({ - sqlClass: sqlApi - }); + torque = new TorqueFactory(); + this.originalPSQLAdaptorQueryMethod = PSQLAdaptor.prototype.query; + }); + + afterEach(function () { + PSQLAdaptor.prototype.query = this.originalPSQLAdaptorQueryMethod; }); function rendererOptions (layer) { return { - params: {}, + params: { + dbname: 'windshaft_test' + }, layer: layer }; } + var layerZeroOptions = rendererOptions(0); describe('getRenderer', function () { it('should create a renderer with right parmas', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { date: { type: 'date' } } }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }] - ]; + } + }); torque.getRenderer(mapConfig, 'json.torque', layerZeroOptions, function (err, renderer) { assert.ifError(err); assert.ok(!!renderer); @@ -108,14 +111,14 @@ describe('torque', function () { }); it('should raise an error on missing -torque-frame-count', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { date: { type: 'date' } } }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }] - ]; + } + }); var brokenConfig = MapConfig.create(layergroupConfig(makeCartoCss( [ '-torque-time-attribute: "date";', @@ -127,20 +130,20 @@ describe('torque', function () { torque.getRenderer(brokenConfig, 'json.torque', layerZeroOptions, function (err/*, renderer */) { assert.ok(err !== null); assert.ok(err instanceof Error); - assert.equal(err.message, "Missing required property '-torque-frame-count' in torque layer CartoCSS"); + assert.equal(err.message, "TorqueRenderer: Missing required property '-torque-frame-count' in torque layer CartoCSS"); done(); }); }); it('should raise an error on missing -torque-resolution', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { date: { type: 'date' } } }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }] - ]; + } + }); var brokenConfig = MapConfig.create(layergroupConfig(makeCartoCss( [ '-torque-time-attribute: "date";', @@ -152,20 +155,20 @@ describe('torque', function () { torque.getRenderer(brokenConfig, 'json.torque', layerZeroOptions, function (err/*, renderer */) { assert.ok(err !== null); assert.ok(err instanceof Error); - assert.equal(err.message, "Missing required property '-torque-resolution' in torque layer CartoCSS"); + assert.equal(err.message, "TorqueRenderer: Missing required property '-torque-resolution' in torque layer CartoCSS"); done(); }); }); it('should raise an error on missing -torque-time-attribute', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { date: { type: 'date' } } }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }] - ]; + } + }); var brokenConfig = MapConfig.create(layergroupConfig(makeCartoCss( [ '-torque-aggregation-function: "count(cartodb_id)";', @@ -177,7 +180,7 @@ describe('torque', function () { torque.getRenderer(brokenConfig, 'json.torque', layerZeroOptions, function (err/*, renderer */) { assert.ok(err !== null); assert.ok(err instanceof Error); - assert.equal(err.message, "Missing required property '-torque-time-attribute' in torque layer CartoCSS"); + assert.equal(err.message, "TorqueRenderer: Missing required property '-torque-time-attribute' in torque layer CartoCSS"); done(); }); }); @@ -211,14 +214,14 @@ describe('torque', function () { describe('Renderer', function () { it('should get metadata', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { date: { type: 'date' } } }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }] - ]; + } + }); torque.getRenderer(mapConfig, 'json.torque', layerZeroOptions, function (err, renderer) { assert.ok(err === null); renderer.getMetadata() @@ -233,19 +236,22 @@ describe('torque', function () { }); }); it('should get a tile', function (done) { - sqlApi.responses = [ - [null, { fields: { date: { type: 'date' } } }], - [null, { + mockPSQLAdaptorQuery({ + columnsQueryResult: { + fields: { date: { type: 'date' } } + }, + stepQueryResult: { rows: [ { min_date: 0, max_date: 10, num_steps: 1, xmin: 0, xmax: 10, ymin: 0, ymax: 10 } ] - }], - [null, { + }, + tileQueryResult: { rows: [ { x__uint8: 0, y__uint8: 0, vals__uint8: [0, 1, 2], dates__uint16: [4, 5, 6] } ] - }] - ]; + } + }); + torque.getRenderer(mapConfig, 'json.torque', layerZeroOptions, function (err, renderer) { assert.ifError(err); renderer.getTile('json.torque', 0, 0, 0) @@ -284,10 +290,11 @@ describe('torque', function () { }; var mapConfig = MapConfig.create(layergroup); - sqlApi.responses = [ - [null, { fields: { updated_at: { type: 'date' } } }], - [null, { rows: [{ num_steps: 0, max_date: null, min_date: null }] }] - ]; + mockPSQLAdaptorQuery({ + columnsQueryResult: { fields: { updated_at: { type: 'date' } } }, + stepQueryResult: { rows: [{ num_steps: 0, max_date: null, min_date: null }] } + }); + torque.getRenderer(mapConfig, 'json.torque', layerZeroOptions, function (err, renderer) { assert.ifError(err); assert.equal(renderer.attrs.step, 1, 'Number of steps cannot be Infinity'); diff --git a/test/unit/utils/layer-columns-test.js b/test/unit/utils/layer-columns-test.js deleted file mode 100644 index 14fb981c6..000000000 --- a/test/unit/utils/layer-columns-test.js +++ /dev/null @@ -1,75 +0,0 @@ -'use strict'; - -require('../../support/test-helper'); -var LayerColumns = require('../../../lib/utils/layer-columns'); -var assert = require('assert'); - -describe('mvt-utils', function () { - function createOptions (interactivity, columns) { - var options = { - sql: 'select * from populated_places_simple_reduced', - cartocss: ['#layer0 {', - 'marker-fill: red;', - 'marker-width: 10;', - 'text-name: [name];', - '[pop_max>100000] { marker-fill: black; } ', - '}'].join('\n'), - cartocss_version: '2.3.0' - }; - - if (interactivity) { - options.interactivity = interactivity; - } - - if (columns) { - options.columns = columns; - } - - return options; - } - - it('should not duplicate column names', function () { - var columns = LayerColumns.getColumns(createOptions()); - assert.deepEqual(columns, ['pop_max', 'name']); - }); - - it('should handle interactivity strings', function () { - var columns = LayerColumns.getColumns(createOptions('cartodb_id,pop_max')); - assert.deepEqual(columns, ['pop_max', 'name', 'cartodb_id']); - }); - - it('should handle interactivity array', function () { - var columns = LayerColumns.getColumns(createOptions(['cartodb_id', 'pop_max'])); - assert.deepEqual(columns, ['pop_max', 'name', 'cartodb_id']); - }); - - it('should handle columns array', function () { - var columns = LayerColumns.getColumns(createOptions(null, ['cartodb_id', 'pop_min'])); - assert.deepEqual(columns, ['cartodb_id', 'pop_min', 'pop_max', 'name']); - }); - - it('should handle empty columns array', function () { - var columns = LayerColumns.getColumns(createOptions(null, [])); - assert.deepEqual(columns, ['pop_max', 'name']); - }); - - it('should ignore no-string values', function () { - var columns = LayerColumns.getColumns(createOptions(null, ['cartodb_id', 'pop_min', 1])); - assert.deepEqual(columns, ['cartodb_id', 'pop_min', 'pop_max', 'name']); - }); - - it('should ignore null values', function () { - var columns = LayerColumns.getColumns(createOptions(null, ['cartodb_id', 'pop_min', null])); - assert.deepEqual(columns, ['cartodb_id', 'pop_min', 'pop_max', 'name']); - }); - - it('should ignore undefined values', function () { - var columns = LayerColumns.getColumns(createOptions(null, ['cartodb_id', undefined, 'pop_min'])); - assert.deepEqual(columns, ['cartodb_id', 'pop_min', 'pop_max', 'name']); - }); - - it('should ignore empty values', function () { - var columns = LayerColumns.getColumns(createOptions(null, ['cartodb_id', '', 'pop_min'])); - assert.deepEqual(columns, ['cartodb_id', 'pop_min', 'pop_max', 'name']); - }); -});