diff --git a/lib/cartodb/controllers/index.js b/lib/cartodb/controllers/index.js index 8a00dad8..79f686ab 100644 --- a/lib/cartodb/controllers/index.js +++ b/lib/cartodb/controllers/index.js @@ -1,6 +1,7 @@ module.exports = { + Layergroup: require('./layergroup'), Map: require('./map'), NamedMaps: require('./named_maps'), NamedMapsAdmin: require('./named_maps_admin'), ServerInfo: require('./server_info') -}; \ No newline at end of file +}; diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js new file mode 100644 index 00000000..f46bcecf --- /dev/null +++ b/lib/cartodb/controllers/layergroup.js @@ -0,0 +1,205 @@ +var assert = require('assert'); +var step = require('step'); + +var cors = require('../middleware/cors'); + +var windshaft = require('windshaft'); +var MapStoreMapConfigProvider = windshaft.model.provider.MapStoreMapConfig; + +/** + * @param app + * @param {MapStore} mapStore + * @param {TileBackend} tileBackend + * @param {PreviewBackend} previewBackend + * @param {AttributesBackend} attributesBackend + * @constructor + */ +function LayergroupController(app, mapStore, tileBackend, previewBackend, attributesBackend) { + this.app = app; + this.mapStore = mapStore; + this.tileBackend = tileBackend; + this.previewBackend = previewBackend; + this.attributesBackend = attributesBackend; +} + +module.exports = LayergroupController; + + +LayergroupController.prototype.register = function(app) { + app.get(app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), this.tile.bind(this)); + app.get(app.base_url_mapconfig + '/:token/:z/:x/:y.:format', cors(), this.tile.bind(this)); + app.get(app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', cors(), this.layer.bind(this)); + app.get(app.base_url_mapconfig + '/:token/:layer/attributes/:fid', cors(), this.attributes.bind(this)); + app.get(app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), + this.center.bind(this)); + app.get(app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), + this.bbox.bind(this)); +}; + +LayergroupController.prototype.attributes = function(req, res) { + var self = this; + + req.profiler.start('windshaft.maplayer_attribute'); + + step( + function setupParams() { + self.app.req2params(req, this); + }, + function retrieveFeatureAttributes(err) { + req.profiler.done('req2params'); + + assert.ifError(err); + + self.attributesBackend.getFeatureAttributes(req.params, false, this); + }, + function finish(err, tile, stats) { + req.profiler.add(stats || {}); + + if (err) { + // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 + var errMsg = err.message ? ( '' + err.message ) : ( '' + err ); + var statusCode = self.app.findStatusCode(err); + self.app.sendError(res, { errors: [errMsg] }, statusCode, 'GET ATTRIBUTES', err); + } else { + self.app.sendResponse(res, [tile, 200]); + } + } + ); + +}; + +// Gets a tile for a given token and set of tile ZXY coords. (OSM style) +LayergroupController.prototype.tile = function(req, res) { + req.profiler.start('windshaft.map_tile'); + this.tileOrLayer(req, res); +}; + +// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style) +LayergroupController.prototype.layer = function(req, res, next) { + if (req.params.token === 'static') { + return next(); + } + req.profiler.start('windshaft.maplayer_tile'); + this.tileOrLayer(req, res); +}; + +LayergroupController.prototype.tileOrLayer = function (req, res) { + var self = this; + + step( + function mapController$prepareParams() { + self.app.req2params(req, this); + }, + function mapController$getTileOrGrid(err) { + req.profiler.done('req2params'); + if ( err ) { + throw err; + } + self.tileBackend.getTile(new MapStoreMapConfigProvider(self.mapStore, req.params), req.params, this); + }, + function mapController$finalize(err, tile, headers, stats) { + req.profiler.add(stats); + self.finalizeGetTileOrGrid(err, req, res, tile, headers); + return null; + }, + function finish(err) { + if ( err ) { + console.error("windshaft.tiles: " + err); + } + } + ); +}; + +// This function is meant for being called as the very last +// step by all endpoints serving tiles or grids +LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) { + var supportedFormats = { + grid_json: true, + json_torque: true, + torque_json: true, + png: true + }; + + var formatStat = 'invalid'; + if (req.params.format) { + var format = req.params.format.replace('.', '_'); + if (supportedFormats[format]) { + formatStat = format; + } + } + + if (err){ + // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 + var errMsg = err.message ? ( '' + err.message ) : ( '' + err ); + var statusCode = this.app.findStatusCode(err); + + // Rewrite mapnik parsing errors to start with layer number + var matches = errMsg.match("(.*) in style 'layer([0-9]+)'"); + if (matches) { + errMsg = 'style'+matches[2]+': ' + matches[1]; + } + + this.app.sendError(res, { errors: ['' + errMsg] }, statusCode, 'TILE RENDER', err); + global.statsClient.increment('windshaft.tiles.error'); + global.statsClient.increment('windshaft.tiles.' + formatStat + '.error'); + } else { + this.app.sendWithHeaders(res, tile, 200, headers); + global.statsClient.increment('windshaft.tiles.success'); + global.statsClient.increment('windshaft.tiles.' + formatStat + '.success'); + } +}; + +LayergroupController.prototype.bbox = function(req, res) { + this.staticMap(req, res, +req.params.width, +req.params.height, { + west: +req.params.west, + north: +req.params.north, + east: +req.params.east, + south: +req.params.south + }); +}; + +LayergroupController.prototype.center = function(req, res) { + this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, { + lng: +req.params.lng, + lat: +req.params.lat + }); +}; + +LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) { + var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; + req.params.layer = 'all'; + req.params.format = 'png'; + + var self = this; + + step( + function() { + self.app.req2params(req, this); + }, + function(err) { + req.profiler.done('req2params'); + assert.ifError(err); + if (center) { + self.previewBackend.getImage(new MapStoreMapConfigProvider(self.mapStore, req.params), + format, width, height, zoom, center, this); + } else { + self.previewBackend.getImage(new MapStoreMapConfigProvider(self.mapStore, req.params), + format, width, height, zoom /* bounds */, this); + } + }, + function handleImage(err, image, headers, stats) { + req.profiler.done('render-' + format); + req.profiler.add(stats || {}); + + if (err) { + if (!err.error) { + err.error = err.message; + } + self.app.sendError(res, {errors: ['' + err] }, self.app.findStatusCode(err), 'STATIC_MAP', err); + } else { + res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format); + self.app.sendResponse(res, [image, 200]); + } + } + ); +}; diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index 4e3ef210..1ad2f3c9 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -1,37 +1,37 @@ +var _ = require('underscore'); var assert = require('assert'); - var step = require('step'); +var windshaft = require('windshaft'); var cors = require('../middleware/cors'); -var windshaft = require('windshaft'); -var MapStoreMapConfigProvider = windshaft.model.provider.MapStoreMapConfig; var MapConfig = windshaft.model.MapConfig; var Datasource = windshaft.model.Datasource; +var NamedMapsCacheEntry = require('../cache/model/named_maps_entry'); + var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter'); +var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider'); /** * @param app * @param {PgConnection} pgConnection - * @param {MapStore} mapStore * @param {TemplateMaps} templateMaps * @param {MapBackend} mapBackend - * @param {TileBackend} tileBackend - * @param {PreviewBackend} previewBackend - * @param {AttributesBackend} attributesBackend + * @param metadataBackend + * @param {QueryTablesApi} queryTablesApi + * @param {SurrogateKeysCache} surrogateKeysCache * @constructor */ -function MapController(app, pgConnection, mapStore, templateMaps, mapBackend, tileBackend, previewBackend, - attributesBackend) { - this._app = app; +function MapController(app, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi, + surrogateKeysCache) { + this.app = app; this.pgConnection = pgConnection; - this.mapStore = mapStore; this.templateMaps = templateMaps; this.mapBackend = mapBackend; - this.tileBackend = tileBackend; - this.previewBackend = previewBackend; - this.attributesBackend = attributesBackend; + this.metadataBackend = metadataBackend; + this.queryTablesApi = queryTablesApi; + this.surrogateKeysCache = surrogateKeysCache; this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps); } @@ -39,57 +39,82 @@ module.exports = MapController; MapController.prototype.register = function(app) { - app.get(app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format', cors(), this.tile.bind(this)); - app.get(app.base_url_mapconfig + '/:token/:z/:x/:y.:format', cors(), this.tile.bind(this)); - app.get(app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)', cors(), this.layer.bind(this)); - app.options(app.base_url_mapconfig, cors('Content-Type')); app.get(app.base_url_mapconfig, cors(), this.createGet.bind(this)); app.post(app.base_url_mapconfig, cors(), this.createPost.bind(this)); - app.get(app.base_url_mapconfig + '/:token/:layer/attributes/:fid', cors(), this.attributes.bind(this)); - app.get(app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), - this.center.bind(this)); - app.get(app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), - this.bbox.bind(this)); + app.get(app.base_url_templated + '/:template_id/jsonp', cors(), this.jsonp.bind(this)); + app.post(app.base_url_templated + '/:template_id', cors(), this.instantiate.bind(this)); + app.options(app.base_url_mapconfig, cors('Content-Type')); }; -MapController.prototype.attributes = function(req, res) { - var self = this; +MapController.prototype.createGet = function(req, res){ + req.profiler.start('windshaft.createmap_get'); - req.profiler.start('windshaft.maplayer_attribute'); + this.create(req, res, function createGet$prepareConfig(err, req) { + assert.ifError(err); + if ( ! req.params.config ) { + throw new Error('layergroup GET needs a "config" parameter'); + } + return JSON.parse(req.params.config); + }); +}; - step( - function setupParams() { - self._app.req2params(req, this); - }, - function retrieveFeatureAttributes(err) { - req.profiler.done('req2params'); +MapController.prototype.createPost = function(req, res) { + req.profiler.start('windshaft.createmap_post'); - assert.ifError(err); + this.create(req, res, function createPost$prepareConfig(err, req) { + assert.ifError(err); + if (!req.is('application/json')) { + throw new Error('layergroup POST data must be of type application/json'); + } + return req.body; + }); +}; - self.attributesBackend.getFeatureAttributes(req.params, false, this); - }, - function finish(err, tile, stats) { - req.profiler.add(stats || {}); +MapController.prototype.instantiate = function(req, res) { + if (req.profiler) { + req.profiler.start('windshaft-cartodb.instance_template_post'); + } - if (err) { - // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 - var errMsg = err.message ? ( '' + err.message ) : ( '' + err ); - var statusCode = self._app.findStatusCode(err); - self._app.sendError(res, { errors: [errMsg] }, statusCode, 'GET ATTRIBUTES', err); - } else { - self._app.sendResponse(res, [tile, 200]); + this.instantiateTemplate(req, res, function prepareTemplateParams(callback) { + if (!req.is('application/json')) { + return callback(new Error('Template POST data must be of type application/json')); + } + return callback(null, req.body); + }); +}; + +MapController.prototype.jsonp = function(req, res) { + if (req.profiler) { + req.profiler.start('windshaft-cartodb.instance_template_get'); + } + + this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) { + var err = null; + if ( req.query.callback === undefined || req.query.callback.length === 0) { + err = new Error('callback parameter should be present and be a function name'); + } + + var templateParams = {}; + if (req.query.config) { + try { + templateParams = JSON.parse(req.query.config); + } catch(e) { + err = new Error('Invalid config parameter, should be a valid JSON'); } } - ); + return callback(err, templateParams); + }); }; MapController.prototype.create = function(req, res, prepareConfigFn) { var self = this; + var mapConfig; + step( function setupParams(){ - self._app.req2params(req, this); + self.app.req2params(req, this); }, prepareConfigFn, function beforeLayergroupCreate(err, requestMapConfig) { @@ -110,178 +135,151 @@ MapController.prototype.create = function(req, res, prepareConfigFn) { }, function createLayergroup(err, requestMapConfig, datasource) { assert.ifError(err); - self.mapBackend.createLayergroup( - new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource()), req.params, this - ); + mapConfig = new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource()); + self.mapBackend.createLayergroup(mapConfig, req.params, this); + }, + function afterLayergroupCreate(err, layergroup) { + assert.ifError(err); + self.afterLayergroupCreate(req, mapConfig, layergroup, this); }, function finish(err, layergroup) { if (err) { - var statusCode = self._app.findStatusCode(err); - self._app.sendError(res, { errors: [ err.message ] }, statusCode, 'ANONYMOUS LAYERGROUP', err); + var statusCode = self.app.findStatusCode(err); + self.app.sendError(res, { errors: [ err.message ] }, statusCode, 'ANONYMOUS LAYERGROUP', err); } else { - self._app.sendResponse(res, [layergroup, 200]); + self.app.sendResponse(res, [layergroup, 200]); } } ); }; -MapController.prototype.createGet = function(req, res){ - req.profiler.start('windshaft.createmap_get'); - - this.create(req, res, function createGet$prepareConfig(err, req) { - assert.ifError(err); - if ( ! req.params.config ) { - throw new Error('layergroup GET needs a "config" parameter'); - } - return JSON.parse(req.params.config); - }); -}; - -// TODO rewrite this so it is possible to share code with `MapController::create` method -MapController.prototype.createPost = function(req, res) { - req.profiler.start('windshaft.createmap_post'); - - this.create(req, res, function createPost$prepareConfig(err, req) { - assert.ifError(err); - if (!req.is('application/json')) { - throw new Error('layergroup POST data must be of type application/json'); - } - return req.body; - }); -}; - -// Gets a tile for a given token and set of tile ZXY coords. (OSM style) -MapController.prototype.tile = function(req, res) { - req.profiler.start('windshaft.map_tile'); - this.tileOrLayer(req, res); -}; - -// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style) -MapController.prototype.layer = function(req, res, next) { - if (req.params.token === 'static') { - return next(); - } - req.profiler.start('windshaft.maplayer_tile'); - this.tileOrLayer(req, res); -}; - -MapController.prototype.tileOrLayer = function (req, res) { +MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) { var self = this; + var cdbuser = req.context.user; + + var mapConfigProvider; + var mapConfig; + step( - function mapController$prepareParams() { - self._app.req2params(req, this); + function getTemplateParams() { + prepareParamsFn(this); }, - function mapController$getTileOrGrid(err) { - req.profiler.done('req2params'); - if ( err ) { - throw err; + function getTemplate(err, templateParams) { + assert.ifError(err); + mapConfigProvider = new NamedMapMapConfigProvider( + self.templateMaps, + self.pgConnection, + cdbuser, + req.params.template_id, + templateParams, + req.query.auth_token, + req.params + ); + mapConfigProvider.getMapConfig(this); + }, + function createLayergroup(err, mapConfig_, rendererParams/*, context*/) { + assert.ifError(err); + mapConfig = mapConfig_; + self.mapBackend.createLayergroup(mapConfig, rendererParams, this); + }, + function afterLayergroupCreate(err, layergroup) { + assert.ifError(err); + self.afterLayergroupCreate(req, mapConfig, layergroup, this); + }, + function finishTemplateInstantiation(err, layergroup) { + if (err) { + var statusCode = self.app.findStatusCode(err); + self.app.sendError(res, { errors: [ err.message ] }, statusCode, 'NAMED MAP LAYERGROUP', err); + } else { + var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8); + layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid; + + res.header('X-Layergroup-Id', layergroup.layergroupid); + self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName())); + + self.app.sendResponse(res, [layergroup, 200]); } - self.tileBackend.getTile(new MapStoreMapConfigProvider(self.mapStore, req.params), req.params, this); + } + ); +}; + + +MapController.prototype.afterLayergroupCreate = function(req, mapconfig, layergroup, callback) { + var self = this; + + var username = req.context.user; + + var tasksleft = 2; // redis key and affectedTables + var errors = []; + + var done = function(err) { + if ( err ) { + errors.push('' + err); + } + if ( ! --tasksleft ) { + err = errors.length ? new Error(errors.join('\n')) : null; + callback(err, layergroup); + } + }; + + // include in layergroup response the variables in serverMedata + // those variables are useful to send to the client information + // about how to reach this server or information about it + _.extend(layergroup, global.environment.serverMetadata); + + // Don't wait for the mapview count increment to + // take place before proceeding. Error will be logged + // asynchronously + this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) { + if (req.profiler) { + req.profiler.done('incMapviewCount'); + } + if ( err ) { + console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err); + } + done(); + }); + + var sql = mapconfig.getLayers().map(function(layer) { + return layer.options.sql; + }).join(';'); + + var dbName = req.params.dbname; + var cacheKey = dbName + ':' + layergroup.layergroupid; + + step( + function getAffectedTablesAndLastUpdatedTime() { + self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this); }, - function mapController$finalize(err, tile, headers, stats) { - req.profiler.add(stats); - self.finalizeGetTileOrGrid(err, req, res, tile, headers); + function handleAffectedTablesAndLastUpdatedTime(err, result) { + if (req.profiler) { + req.profiler.done('queryTablesAndLastUpdated'); + } + assert.ifError(err); + var cacheChannel = self.app.buildCacheChannel(dbName, result.affectedTables); + self.app.channelCache[cacheKey] = cacheChannel; + + // last update for layergroup cache buster + layergroup.layergroupid = layergroup.layergroupid + ':' + result.lastUpdatedTime; + layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString(); + + var res = req.res; + if (res) { + if (req.method === 'GET') { + var ttl = global.environment.varnish.layergroupTtl || 86400; + res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); + res.header('Last-Modified', (new Date()).toUTCString()); + res.header('X-Cache-Channel', cacheChannel); + } + + res.header('X-Layergroup-Id', layergroup.layergroupid); + } + return null; }, function finish(err) { - if ( err ) { - console.error("windshaft.tiles: " + err); - } - } - ); -}; - -// This function is meant for being called as the very last -// step by all endpoints serving tiles or grids -MapController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers) { - var supportedFormats = { - grid_json: true, - json_torque: true, - torque_json: true, - png: true - }; - - var formatStat = 'invalid'; - if (req.params.format) { - var format = req.params.format.replace('.', '_'); - if (supportedFormats[format]) { - formatStat = format; - } - } - - if (err){ - // See https://github.com/Vizzuality/Windshaft-cartodb/issues/68 - var errMsg = err.message ? ( '' + err.message ) : ( '' + err ); - var statusCode = this._app.findStatusCode(err); - - // Rewrite mapnik parsing errors to start with layer number - var matches = errMsg.match("(.*) in style 'layer([0-9]+)'"); - if (matches) { - errMsg = 'style'+matches[2]+': ' + matches[1]; - } - - this._app.sendError(res, { errors: ['' + errMsg] }, statusCode, 'TILE RENDER', err); - global.statsClient.increment('windshaft.tiles.error'); - global.statsClient.increment('windshaft.tiles.' + formatStat + '.error'); - } else { - this._app.sendWithHeaders(res, tile, 200, headers); - global.statsClient.increment('windshaft.tiles.success'); - global.statsClient.increment('windshaft.tiles.' + formatStat + '.success'); - } -}; - -MapController.prototype.bbox = function(req, res) { - this.staticMap(req, res, +req.params.width, +req.params.height, { - west: +req.params.west, - north: +req.params.north, - east: +req.params.east, - south: +req.params.south - }); -}; - -MapController.prototype.center = function(req, res) { - this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, { - lng: +req.params.lng, - lat: +req.params.lat - }); -}; - -MapController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center) { - var format = req.params.format === 'jpg' ? 'jpeg' : 'png'; - req.params.layer = 'all'; - req.params.format = 'png'; - - var self = this; - - step( - function() { - self._app.req2params(req, this); - }, - function(err) { - req.profiler.done('req2params'); - assert.ifError(err); - if (center) { - self.previewBackend.getImage(new MapStoreMapConfigProvider(self.mapStore, req.params), - format, width, height, zoom, center, this); - } else { - self.previewBackend.getImage(new MapStoreMapConfigProvider(self.mapStore, req.params), - format, width, height, zoom /* bounds */, this); - } - }, - function handleImage(err, image, headers, stats) { - req.profiler.done('render-' + format); - req.profiler.add(stats || {}); - - if (err) { - if (!err.error) { - err.error = err.message; - } - self._app.sendError(res, {errors: ['' + err] }, self._app.findStatusCode(err), 'STATIC_MAP', err); - } else { - res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format); - self._app.sendResponse(res, [image, 200]); - } + done(err); } ); }; diff --git a/lib/cartodb/controllers/named_maps.js b/lib/cartodb/controllers/named_maps.js index e75100de..2ef6766c 100644 --- a/lib/cartodb/controllers/named_maps.js +++ b/lib/cartodb/controllers/named_maps.js @@ -6,17 +6,13 @@ var cors = require('../middleware/cors'); var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider'); -function NamedMapsController(app, pgConnection, mapStore, templateMaps, metadataBackend, mapBackend, tileBackend, - previewBackend, templateBaseUrl, surrogateKeysCache, tablesExtentApi) { +function NamedMapsController(app, pgConnection, templateMaps, tileBackend, previewBackend, surrogateKeysCache, + tablesExtentApi) { this.app = app; - this.mapStore = mapStore; this.pgConnection = pgConnection; this.templateMaps = templateMaps; - this.metadataBackend = metadataBackend; - this.mapBackend = mapBackend; this.tileBackend = tileBackend; this.previewBackend = previewBackend; - this.templateBaseUrl = templateBaseUrl; this.surrogateKeysCache = surrogateKeysCache; this.tablesExtentApi = tablesExtentApi; } @@ -24,49 +20,10 @@ function NamedMapsController(app, pgConnection, mapStore, templateMaps, metadata module.exports = NamedMapsController; NamedMapsController.prototype.register = function(app) { - app.get(this.templateBaseUrl + '/:template_id/:layer/:z/:x/:y.(:format)', cors(), this.tile.bind(this)); - app.get(this.templateBaseUrl + '/:template_id/jsonp', cors(), this.jsonp.bind(this)); + app.get(app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)', cors(), this.tile.bind(this)); app.get( app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', cors(), this.staticMap.bind(this) ); - app.post(this.templateBaseUrl + '/:template_id', cors(), this.instantiate.bind(this)); -}; - -NamedMapsController.prototype.instantiate = function(req, res) { - if (req.profiler) { - req.profiler.start('windshaft-cartodb.instance_template_post'); - } - - this.instantiateTemplate(req, res, function prepareTemplateParams(callback) { - if (!req.is('application/json')) { - return callback(new Error('Template POST data must be of type application/json')); - } - return callback(null, req.body); - }); -}; - -NamedMapsController.prototype.jsonp = function(req, res) { - if (req.profiler) { - req.profiler.start('windshaft-cartodb.instance_template_get'); - } - - this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) { - var err = null; - if ( req.query.callback === undefined || req.query.callback.length === 0) { - err = new Error('callback parameter should be present and be a function name'); - } - - var templateParams = {}; - if (req.query.config) { - try { - templateParams = JSON.parse(req.query.config); - } catch(e) { - err = new Error('Invalid config parameter, should be a valid JSON'); - } - } - - return callback(err, templateParams); - }); }; NamedMapsController.prototype.tile = function(req, res) { @@ -207,53 +164,6 @@ NamedMapsController.prototype.staticMap = function(req, res) { ); }; - -// Instantiate a template -NamedMapsController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) { - var self = this; - - var cdbuser = req.context.user; - - var mapConfigProvider; - - step( - function getTemplateParams() { - prepareParamsFn(this); - }, - function getTemplate(err, templateParams) { - assert.ifError(err); - mapConfigProvider = new NamedMapMapConfigProvider( - this.templateMaps, - this.pgConnection, - cdbuser, - req.params.template_id, - templateParams, - req.query.auth_token, - req.params - ); - mapConfigProvider.getMapConfig(this); - }, - function createLayergroup(err, mapConfig, rendererParams/*, context*/) { - assert.ifError(err); - self.mapBackend.createLayergroup(mapConfig, rendererParams, this); - }, - function finishTemplateInstantiation(err, layergroup) { - if (err) { - var statusCode = this._app.findStatusCode(err); - this.app.sendError(res, { errors: [ err.message ] }, statusCode, 'NAMED MAP LAYERGROUP', err); - } else { - var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8); - layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid; - - res.header('X-Layergroup-Id', layergroup.layergroupid); - self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName())); - - this.app.sendResponse(res, [layergroup, 200]); - } - } - ); -}; - function getStaticImageOptions(template, callback) { if (template.view) { var zoomCenter = templateZoomCenter(template.view); diff --git a/lib/cartodb/controllers/named_maps_admin.js b/lib/cartodb/controllers/named_maps_admin.js index 6e8ca013..db92f3ae 100644 --- a/lib/cartodb/controllers/named_maps_admin.js +++ b/lib/cartodb/controllers/named_maps_admin.js @@ -5,21 +5,20 @@ var templateName = require('../backends/template_maps').templateName; var cors = require('../middleware/cors'); -function NamedMapsAdminController(app, templateMaps, templateBaseUrl) { +function NamedMapsAdminController(app, templateMaps) { this.app = app; this.templateMaps = templateMaps; - this.templateBaseUrl = templateBaseUrl; } module.exports = NamedMapsAdminController; NamedMapsAdminController.prototype.register = function(app) { - app.post(this.templateBaseUrl, cors(), this.create.bind(this)); - app.put(this.templateBaseUrl + '/:template_id', cors(), this.update.bind(this)); - app.get(this.templateBaseUrl + '/:template_id', cors(), this.retrieve.bind(this)); - app.del(this.templateBaseUrl + '/:template_id', cors(), this.destroy.bind(this)); - app.get(this.templateBaseUrl, cors(), this.list.bind(this)); - app.options(this.templateBaseUrl + '/:template_id', cors('Content-Type')); + app.post(app.base_url_templated, cors(), this.create.bind(this)); + app.put(app.base_url_templated + '/:template_id', cors(), this.update.bind(this)); + app.get(app.base_url_templated + '/:template_id', cors(), this.retrieve.bind(this)); + app.del(app.base_url_templated + '/:template_id', cors(), this.destroy.bind(this)); + app.get(app.base_url_templated, cors(), this.list.bind(this)); + app.options(app.base_url_templated + '/:template_id', cors('Content-Type')); }; NamedMapsAdminController.prototype.create = function(req, res) { diff --git a/lib/cartodb/server.js b/lib/cartodb/server.js index 725c1483..46f5aeab 100644 --- a/lib/cartodb/server.js +++ b/lib/cartodb/server.js @@ -195,90 +195,6 @@ module.exports = function(serverOptions) { return statusCode; }; -// var layergroupRequestDecorator = { -// afterLayergroupCreate: function(req, mapconfig, layergroup, callback) { -// var token = layergroup.layergroupid; -// -// var username = req.context.user; -// -// var tasksleft = 2; // redis key and affectedTables -// var errors = []; -// -// var done = function(err) { -// if ( err ) { -// errors.push('' + err); -// } -// if ( ! --tasksleft ) { -// err = errors.length ? new Error(errors.join('\n')) : null; -// callback(err); -// } -// }; -// -// // include in layergroup response the variables in serverMedata -// // those variables are useful to send to the client information -// // about how to reach this server or information about it -// var serverMetadata = global.environment.serverMetadata; -// if (serverMetadata) { -// _.extend(layergroup, serverMetadata); -// } -// -// // Don't wait for the mapview count increment to -// // take place before proceeding. Error will be logged -// // asyncronously -// metadataBackend.incMapviewCount(username, mapconfig.stat_tag, function(err) { -// if (req.profiler) { -// req.profiler.done('incMapviewCount'); -// } -// if ( err ) { -// console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err); -// } -// done(); -// }); -// -// var sql = mapconfig.layers.map(function(layer) { -// return layer.options.sql; -// }).join(';'); -// -// var dbName = req.params.dbname; -// var cacheKey = dbName + ':' + token; -// -// step( -// function getAffectedTablesAndLastUpdatedTime() { -// queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this); -// }, -// function handleAffectedTablesAndLastUpdatedTime(err, result) { -// if (req.profiler) { -// req.profiler.done('queryTablesAndLastUpdated'); -// } -// assert.ifError(err); -// var cacheChannel = app.buildCacheChannel(dbName, result.affectedTables); -// app.channelCache[cacheKey] = cacheChannel; -// -// // last update for layergroup cache buster -// layergroup.layergroupid = layergroup.layergroupid + ':' + result.lastUpdatedTime; -// layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString(); -// -// var res = req.res; -// if (res) { -// if (req.method === 'GET') { -// var ttl = global.environment.varnish.layergroupTtl || 86400; -// res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate'); -// res.header('Last-Modified', (new Date()).toUTCString()); -// res.header('X-Cache-Channel', cacheChannel); -// } -// -// res.header('X-Layergroup-Id', layergroup.layergroupid); -// } -// -// return null; -// }, -// function finish(err) { -// done(err); -// } -// ); -// } -// }; - var TablesExtentApi = require('./api/tables_extent_api'); var tablesExtentApi = new TablesExtentApi(pgQueryRunner); @@ -291,32 +207,35 @@ module.exports = function(serverOptions) { next(); }); - new controller.Map( + new controller.Layergroup( app, - pgConnection, mapStore, - templateMaps, - mapBackend, tileBackend, previewBackend, attributesBackend ).register(app); + new controller.Map( + app, + pgConnection, + templateMaps, + mapBackend, + metadataBackend, + queryTablesApi, + surrogateKeysCache + ).register(app); + new controller.NamedMaps( app, pgConnection, - mapStore, templateMaps, - metadataBackend, - mapBackend, tileBackend, previewBackend, - template_baseurl, surrogateKeysCache, tablesExtentApi ).register(app); - new controller.NamedMapsAdmin(app, templateMaps, template_baseurl).register(app); + new controller.NamedMapsAdmin(app, templateMaps).register(app); new controller.ServerInfo().register(app); @@ -857,8 +776,8 @@ module.exports = function(serverOptions) { }; function validateOptions(opts) { - if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig)) { - throw new Error("Must initialise Windshaft with: 'base_url'/'base_url_mapconfig' URLs"); + if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) { + throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs"); } // Be nice and warn if configured mapnik version is != instaled mapnik version diff --git a/lib/cartodb/server_options.js b/lib/cartodb/server_options.js index 551a9c6b..c0c60270 100644 --- a/lib/cartodb/server_options.js +++ b/lib/cartodb/server_options.js @@ -42,6 +42,8 @@ module.exports = { // base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)', + base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)', + grainstore: { map: { // TODO: allow to specify in configuration diff --git a/test/unit/cartodb/ported/tile_stats.test.js b/test/unit/cartodb/ported/tile_stats.test.js index d1515a3a..0f570829 100644 --- a/test/unit/cartodb/ported/tile_stats.test.js +++ b/test/unit/cartodb/ported/tile_stats.test.js @@ -5,7 +5,7 @@ var cartodbServer = require('../../../../lib/cartodb/server'); var serverOptions = require('../../../../lib/cartodb/server_options'); var StatsClient = require('windshaft').stats.Client; -var MapController = require('../../../../lib/cartodb/controllers/map'); +var LayergroupController = require('../../../../lib/cartodb/controllers/layergroup'); describe('tile stats', function() { @@ -31,14 +31,14 @@ describe('tile stats', function() { var ws = cartodbServer(serverOptions); ws.sendError = function(){}; - var mapController = new MapController(ws, null); + var layergroupController = new LayergroupController(ws, null); var reqMock = { params: { format: invalidFormat } }; - mapController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, {}, null, null); + layergroupController.finalizeGetTileOrGrid('Unsupported format png2', reqMock, {}, null, null); assert.ok(formatMatched, 'Format was never matched in increment method'); assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method'); @@ -64,9 +64,9 @@ describe('tile stats', function() { var ws = cartodbServer(serverOptions); ws.sendError = function(){}; - var mapController = new MapController(ws, null); + var layergroupController = new LayergroupController(ws, null); - mapController.finalizeGetTileOrGrid('Another error happened', reqMock, {}, null, null); + layergroupController.finalizeGetTileOrGrid('Another error happened', reqMock, {}, null, null); assert.ok(formatMatched, 'Format was never matched in increment method'); assert.equal(expectedCalls, 0, 'Unexpected number of calls to increment method');