665 lines
20 KiB
JavaScript
665 lines
20 KiB
JavaScript
const cors = require('../middleware/cors');
|
|
const user = require('../middleware/user');
|
|
const vectorError = require('../middleware/vector-error');
|
|
const locals = require('../middleware/locals');
|
|
const cleanUpQueryParams = require('../middleware/clean-up-query-params');
|
|
const layergroupToken = require('../middleware/layergroup-token');
|
|
const credentials = require('../middleware/credentials');
|
|
const dbConnSetup = require('../middleware/db-conn-setup');
|
|
const authorize = require('../middleware/authorize');
|
|
const rateLimit = require('../middleware/rate-limit');
|
|
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
|
const cacheControlHeader = require('../middleware/cache-control-header');
|
|
const cacheChannelHeader = require('../middleware/cache-channel-header');
|
|
const surrogateKeyHeader = require('../middleware/surrogate-key-header');
|
|
const lastModifiedHeader = require('../middleware/last-modified-header');
|
|
const sendResponse = require('../middleware/send-response');
|
|
const DataviewBackend = require('../backends/dataview');
|
|
const AnalysisStatusBackend = require('../backends/analysis-status');
|
|
const MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
|
|
const dbParamsFromResLocals = require('../utils/database-params');
|
|
|
|
const SUPPORTED_FORMATS = {
|
|
grid_json: true,
|
|
json_torque: true,
|
|
torque_json: true,
|
|
png: true,
|
|
png32: true,
|
|
mvt: true
|
|
};
|
|
|
|
const ALLOWED_DATAVIEW_QUERY_PARAMS = [
|
|
'filters', // json
|
|
'own_filter', // 0, 1
|
|
'no_filters', // 0, 1
|
|
'bbox', // w,s,e,n
|
|
'start', // number
|
|
'end', // number
|
|
'column_type', // string
|
|
'bins', // number
|
|
'aggregation', //string
|
|
'offset', // number
|
|
'q', // widgets search
|
|
'categories', // number
|
|
];
|
|
|
|
/**
|
|
* @param {prepareContext} prepareContext
|
|
* @param {PgConnection} pgConnection
|
|
* @param {MapStore} mapStore
|
|
* @param {TileBackend} tileBackend
|
|
* @param {PreviewBackend} previewBackend
|
|
* @param {AttributesBackend} attributesBackend
|
|
* @param {SurrogateKeysCache} surrogateKeysCache
|
|
* @param {UserLimitsApi} userLimitsApi
|
|
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
|
* @param {AnalysisBackend} analysisBackend
|
|
* @constructor
|
|
*/
|
|
function LayergroupController(
|
|
pgConnection,
|
|
mapStore,
|
|
tileBackend,
|
|
previewBackend,
|
|
attributesBackend,
|
|
surrogateKeysCache,
|
|
userLimitsApi,
|
|
layergroupAffectedTablesCache,
|
|
analysisBackend,
|
|
authApi
|
|
) {
|
|
this.pgConnection = pgConnection;
|
|
this.mapStore = mapStore;
|
|
this.tileBackend = tileBackend;
|
|
this.previewBackend = previewBackend;
|
|
this.attributesBackend = attributesBackend;
|
|
this.surrogateKeysCache = surrogateKeysCache;
|
|
this.userLimitsApi = userLimitsApi;
|
|
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
|
|
|
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
|
this.analysisStatusBackend = new AnalysisStatusBackend();
|
|
this.authApi = authApi;
|
|
}
|
|
|
|
module.exports = LayergroupController;
|
|
|
|
LayergroupController.prototype.register = function(app) {
|
|
const { base_url_mapconfig: mapConfigBasePath } = app;
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:z/:x/:y@:scale_factor?x.:format`,
|
|
cors(),
|
|
cleanUpQueryParams(),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getTile(this.tileBackend, 'map_tile'),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
incrementSuccessMetrics(global.statsClient),
|
|
sendResponse(),
|
|
incrementErrorMetrics(global.statsClient),
|
|
tileError(),
|
|
vectorError()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:z/:x/:y.:format`,
|
|
cors(),
|
|
cleanUpQueryParams(),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getTile(this.tileBackend, 'map_tile'),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
incrementSuccessMetrics(global.statsClient),
|
|
sendResponse(),
|
|
incrementErrorMetrics(global.statsClient),
|
|
tileError(),
|
|
vectorError()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:layer/:z/:x/:y.(:format)`,
|
|
distinguishLayergroupFromStaticRoute(),
|
|
cors(),
|
|
cleanUpQueryParams(),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getTile(this.tileBackend, 'maplayer_tile'),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
incrementSuccessMetrics(global.statsClient),
|
|
sendResponse(),
|
|
incrementErrorMetrics(global.statsClient),
|
|
tileError(),
|
|
vectorError()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:layer/attributes/:fid`,
|
|
cors(),
|
|
cleanUpQueryParams(),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getFeatureAttributes(this.attributesBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
const forcedFormat = 'png';
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/static/center/:token/:z/:lat/:lng/:width/:height.:format`,
|
|
cors(),
|
|
cleanUpQueryParams(['layer']),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache,
|
|
forcedFormat
|
|
),
|
|
getPreviewImageByCenter(this.previewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`,
|
|
cors(),
|
|
cleanUpQueryParams(['layer']),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache,
|
|
forcedFormat
|
|
),
|
|
getPreviewImageByBoundingBox(this.previewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
// Undocumented/non-supported API endpoint methods.
|
|
// Use at your own peril.
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/dataview/:dataviewName`,
|
|
cors(),
|
|
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getDataview(this.dataviewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:layer/widget/:dataviewName`,
|
|
cors(),
|
|
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
getDataview(this.dataviewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/dataview/:dataviewName/search`,
|
|
cors(),
|
|
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
dataviewSearch(this.dataviewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/:layer/widget/:dataviewName/search`,
|
|
cors(),
|
|
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
createMapStoreMapConfigProvider(
|
|
this.mapStore,
|
|
this.userLimitsApi,
|
|
this.pgConnection,
|
|
this.layergroupAffectedTablesCache
|
|
),
|
|
dataviewSearch(this.dataviewBackend),
|
|
cacheControlHeader(),
|
|
cacheChannelHeader(),
|
|
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
|
lastModifiedHeader(),
|
|
sendResponse()
|
|
);
|
|
|
|
app.get(
|
|
`${mapConfigBasePath}/:token/analysis/node/:nodeId`,
|
|
cors(),
|
|
cleanUpQueryParams(),
|
|
locals(),
|
|
user(),
|
|
rateLimit(this.userLimitsApi, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS),
|
|
layergroupToken(),
|
|
credentials(),
|
|
authorize(this.authApi),
|
|
dbConnSetup(this.pgConnection),
|
|
analysisNodeStatus(this.analysisStatusBackend),
|
|
sendResponse()
|
|
);
|
|
};
|
|
|
|
function distinguishLayergroupFromStaticRoute () {
|
|
return function distinguishLayergroupFromStaticRouteMiddleware(req, res, next) {
|
|
if (req.params.token === 'static') {
|
|
return next('route');
|
|
}
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
function analysisNodeStatus (analysisStatusBackend) {
|
|
return function analysisNodeStatusMiddleware(req, res, next) {
|
|
const { nodeId } = req.params;
|
|
const dbParams = dbParamsFromResLocals(res.locals);
|
|
|
|
analysisStatusBackend.getNodeStatus(nodeId, dbParams, (err, nodeStatus, stats = {}) => {
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'GET NODE STATUS';
|
|
return next(err);
|
|
}
|
|
|
|
res.set({
|
|
'Cache-Control': 'public,max-age=5',
|
|
'Last-Modified': new Date().toUTCString()
|
|
});
|
|
|
|
res.body = nodeStatus;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function getRequestParams(locals) {
|
|
const params = Object.assign({}, locals);
|
|
|
|
delete params.mapConfigProvider;
|
|
delete params.allowedQueryParams;
|
|
|
|
return params;
|
|
}
|
|
|
|
function createMapStoreMapConfigProvider (
|
|
mapStore,
|
|
userLimitsApi,
|
|
pgConnection,
|
|
affectedTablesCache,
|
|
forcedFormat = null
|
|
) {
|
|
return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
|
|
const { user, token, cache_buster, api_key } = res.locals;
|
|
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
|
const { layer, z, x, y, scale_factor, format } = req.params;
|
|
|
|
const params = {
|
|
user, token, cache_buster, api_key,
|
|
dbuser, dbname, dbpassword, dbhost, dbport,
|
|
layer, z, x, y, scale_factor, format
|
|
};
|
|
|
|
if (forcedFormat) {
|
|
params.format = forcedFormat;
|
|
params.layer = params.layer || 'all';
|
|
}
|
|
|
|
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(
|
|
mapStore,
|
|
user,
|
|
userLimitsApi,
|
|
pgConnection,
|
|
affectedTablesCache,
|
|
params
|
|
);
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
function getDataview (dataviewBackend) {
|
|
return function getDataviewMiddleware (req, res, next) {
|
|
const { user, mapConfigProvider } = res.locals;
|
|
const params = getRequestParams(res.locals);
|
|
|
|
dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => {
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'GET DATAVIEW';
|
|
return next(err);
|
|
}
|
|
|
|
res.body = dataview;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function dataviewSearch (dataviewBackend) {
|
|
return function dataviewSearchMiddleware (req, res, next) {
|
|
const { user, dataviewName, mapConfigProvider } = res.locals;
|
|
const params = getRequestParams(res.locals);
|
|
|
|
dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => {
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'GET DATAVIEW SEARCH';
|
|
return next(err);
|
|
}
|
|
|
|
res.body = searchResult;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function getFeatureAttributes (attributesBackend) {
|
|
return function getFeatureAttributesMiddleware (req, res, next) {
|
|
req.profiler.start('windshaft.maplayer_attribute');
|
|
|
|
const { mapConfigProvider } = res.locals;
|
|
const params = getRequestParams(res.locals);
|
|
|
|
attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => {
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'GET ATTRIBUTES';
|
|
return next(err);
|
|
}
|
|
|
|
res.body = tile;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function getStatusCode(tile, format){
|
|
return tile.length === 0 && format === 'mvt'? 204 : 200;
|
|
}
|
|
|
|
function parseFormat (format = '') {
|
|
const prettyFormat = format.replace('.', '_');
|
|
return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid';
|
|
}
|
|
|
|
function getTile (tileBackend, profileLabel = 'tile') {
|
|
return function getTileMiddleware (req, res, next) {
|
|
req.profiler.start(`windshaft.${profileLabel}`);
|
|
|
|
const { mapConfigProvider } = res.locals;
|
|
const params = getRequestParams(res.locals);
|
|
|
|
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (headers) {
|
|
res.set(headers);
|
|
}
|
|
|
|
const formatStat = parseFormat(req.params.format);
|
|
|
|
res.statusCode = getStatusCode(tile, formatStat);
|
|
res.body = tile;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function getPreviewImageByCenter (previewBackend) {
|
|
return function getPreviewImageByCenterMiddleware (req, res, next) {
|
|
const width = +req.params.width;
|
|
const height = +req.params.height;
|
|
const zoom = +req.params.z;
|
|
const center = {
|
|
lng: +req.params.lng,
|
|
lat: +req.params.lat
|
|
};
|
|
|
|
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
|
const { mapConfigProvider: provider } = res.locals;
|
|
|
|
previewBackend.getImage(provider, format, width, height, zoom, center, (err, image, headers, stats = {}) => {
|
|
req.profiler.done(`render-${format}`);
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'STATIC_MAP';
|
|
return next(err);
|
|
}
|
|
|
|
if (headers) {
|
|
res.set(headers);
|
|
}
|
|
|
|
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
|
|
|
res.body = image;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function getPreviewImageByBoundingBox (previewBackend) {
|
|
return function getPreviewImageByBoundingBoxMiddleware (req, res, next) {
|
|
const width = +req.params.width;
|
|
const height = +req.params.height;
|
|
const bounds = {
|
|
west: +req.params.west,
|
|
north: +req.params.north,
|
|
east: +req.params.east,
|
|
south: +req.params.south
|
|
};
|
|
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
|
const { mapConfigProvider: provider } = res.locals;
|
|
|
|
previewBackend.getImage(provider, format, width, height, bounds, (err, image, headers, stats = {}) => {
|
|
req.profiler.done(`render-${format}`);
|
|
req.profiler.add(stats);
|
|
|
|
if (err) {
|
|
err.label = 'STATIC_MAP';
|
|
return next(err);
|
|
}
|
|
|
|
if (headers) {
|
|
res.set(headers);
|
|
}
|
|
|
|
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
|
|
|
res.body = image;
|
|
|
|
next();
|
|
});
|
|
};
|
|
}
|
|
|
|
function incrementSuccessMetrics (statsClient) {
|
|
return function incrementSuccessMetricsMiddleware (req, res, next) {
|
|
const formatStat = parseFormat(req.params.format);
|
|
|
|
statsClient.increment('windshaft.tiles.success');
|
|
statsClient.increment(`windshaft.tiles.${formatStat}.success`);
|
|
|
|
next();
|
|
};
|
|
}
|
|
|
|
function incrementErrorMetrics (statsClient) {
|
|
return function incrementErrorMetricsMiddleware (err, req, res, next) {
|
|
const formatStat = parseFormat(req.params.format);
|
|
|
|
statsClient.increment('windshaft.tiles.error');
|
|
statsClient.increment(`windshaft.tiles.${formatStat}.error`);
|
|
|
|
next(err);
|
|
};
|
|
}
|
|
|
|
function tileError () {
|
|
return function tileErrorMiddleware (err, req, res, next) {
|
|
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
|
let errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
|
|
|
// Rewrite mapnik parsing errors to start with layer number
|
|
const matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
|
|
|
if (matches) {
|
|
errMsg = `style${matches[2]}: ${matches[1]}`;
|
|
}
|
|
|
|
err.message = errMsg;
|
|
err.label = 'TILE RENDER';
|
|
|
|
next(err);
|
|
};
|
|
}
|