Compare commits
120 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d949d1c27f | ||
|
|
aa1d411fb8 | ||
|
|
f297374449 | ||
|
|
060b93c314 | ||
|
|
3ceeaedf02 | ||
|
|
c6ba9e6102 | ||
|
|
bf40b240d3 | ||
|
|
5d4d2bddd6 | ||
|
|
95dfd87c96 | ||
|
|
eab9e8846e | ||
|
|
788bc302a0 | ||
|
|
1ba240d099 | ||
|
|
ee0405da1e | ||
|
|
5e9b326d03 | ||
|
|
1f30367e59 | ||
|
|
26a2f73c2a | ||
|
|
60005e2f7f | ||
|
|
1c7da2c4b3 | ||
|
|
3799dd2574 | ||
|
|
7efb2a2344 | ||
|
|
88777abc2c | ||
|
|
4d9a6f8fbe | ||
|
|
3d9c2e66c5 | ||
|
|
6bbe715aa6 | ||
|
|
ba002fdb2c | ||
|
|
10d1381e51 | ||
|
|
dfef7ff3c0 | ||
|
|
83d0ce4040 | ||
|
|
75f72c4d07 | ||
|
|
adb9e55fb2 | ||
|
|
5d3726de44 | ||
|
|
f186e4736b | ||
|
|
a00c2b1eef | ||
|
|
64d601179d | ||
|
|
cf2b73e473 | ||
|
|
70932c23df | ||
|
|
519d49bd10 | ||
|
|
bf814c4442 | ||
|
|
f136993c50 | ||
|
|
ba008ab518 | ||
|
|
e4ed6ee1cc | ||
|
|
fda7661dad | ||
|
|
79233471c6 | ||
|
|
a75beefe6e | ||
|
|
e43ccf4f12 | ||
|
|
cd8e320534 | ||
|
|
d7f4d39aa2 | ||
|
|
89333185a9 | ||
|
|
99b95cf839 | ||
|
|
9fbc56b82c | ||
|
|
9a1bc51fdb | ||
|
|
d42257127b | ||
|
|
5a730c6df1 | ||
|
|
418c8691d1 | ||
|
|
9885045b41 | ||
|
|
062e6f9594 | ||
|
|
d8428938ae | ||
|
|
ca5f280cb3 | ||
|
|
524d5a5597 | ||
|
|
a43779b050 | ||
|
|
ef3917fa6f | ||
|
|
031e1253ca | ||
|
|
8012d76b68 | ||
|
|
d726c9ad01 | ||
|
|
1ce8076699 | ||
|
|
54f32113f3 | ||
|
|
19bf079f2d | ||
|
|
b7ecde5c9d | ||
|
|
a2f804d79f | ||
|
|
efdfabf3e9 | ||
|
|
e9a4fc4b2c | ||
|
|
a1d536642e | ||
|
|
3c00266666 | ||
|
|
7f64d15944 | ||
|
|
8259271184 | ||
|
|
20366cedb4 | ||
|
|
a102d1d366 | ||
|
|
4b97b4fd26 | ||
|
|
b94debf10e | ||
|
|
60030784c1 | ||
|
|
cc9b190e5d | ||
|
|
4946ca688c | ||
|
|
d2828ecaff | ||
|
|
5a3dd6a914 | ||
|
|
bcd2fd8f88 | ||
|
|
94a5e66881 | ||
|
|
d55b78f76b | ||
|
|
42149f9ae7 | ||
|
|
1e08d946b1 | ||
|
|
f22216e6d2 | ||
|
|
d9cf830fb4 | ||
|
|
326cad2f2c | ||
|
|
34808d6147 | ||
|
|
79b04bbdfd | ||
|
|
45a663d5ae | ||
|
|
cace6169c0 | ||
|
|
bdce2f95f2 | ||
|
|
506e16fc87 | ||
|
|
c367743d76 | ||
|
|
fa7140e736 | ||
|
|
c63226cd26 | ||
|
|
777df6337b | ||
|
|
2dda0a80da | ||
|
|
e2bd97eea6 | ||
|
|
fb03cd3424 | ||
|
|
8a48b96c53 | ||
|
|
76b0c94835 | ||
|
|
6a36aa1f13 | ||
|
|
800870e783 | ||
|
|
6638ba91c3 | ||
|
|
47e4b9da0d | ||
|
|
81e0c3a098 | ||
|
|
2068861988 | ||
|
|
878f3bd627 | ||
|
|
170fcc1973 | ||
|
|
d0c88ce21d | ||
|
|
e81a16ce0d | ||
|
|
153a792fcb | ||
|
|
5c1b1e3214 | ||
|
|
0bca3d6f33 |
20
NEWS.md
20
NEWS.md
@@ -1,5 +1,25 @@
|
||||
# Changelog
|
||||
|
||||
## 4.7.0
|
||||
Released 2018-01-03
|
||||
|
||||
New features:
|
||||
- Return tilejson in metadata #837.
|
||||
|
||||
Bug fixes:
|
||||
- Allow to create vector map-config for layers that doesn't have points. Layers with lines or polygons won't be aggregated by default.
|
||||
|
||||
|
||||
## 4.6.0
|
||||
Released 2018-01-02
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [4.2.0](https://github.com/CartoDB/windshaft/releases/tag/4.2.0).
|
||||
- Validate aggregation input params.
|
||||
- Fix column names collisions in histograms [#828](https://github.com/CartoDB/Windshaft-cartodb/pull/828).
|
||||
- Add full-sample aggregation support for vector map-config.
|
||||
|
||||
|
||||
## 4.5.0
|
||||
Released 2017-12-19
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Anonymous Maps
|
||||
|
||||
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec).
|
||||
Alternatively, you can get the data for the map (geometry and attributes for each layer) using vector tiles (in which case CartoCSS is not required).
|
||||
|
||||
|
||||
## Instantiate
|
||||
@@ -72,19 +73,19 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
|
||||
## Map Tile Rendering
|
||||
|
||||
Map tiles create the graphical representation of your map in a web browser. The performance rendering of map tiles is dependent on the type of geospatial data model (raster or vector) that you are using.
|
||||
Map tiles are used to create the graphic representation of your map in a web browser. Tiles can be requested either as pre-rendered *raster* tiles (images) or as *vector* map data to be rendered by the client (browser).
|
||||
|
||||
- **Raster**: Generates map tiles based on a grid of pixels to represent your data. Each cell is a fixed size and contains values for particular map features. On the server-side, each request queries a dataset to retrieve data for each map tile. The grid size of map tiles can often lead to graphic quality issues.
|
||||
- **Raster**: If a tile is requested as a raster image format, like PNG, the map will be rendered on the server, using the CartoCSS styles defined in the layers of the map. It is necessary that all the layers of a map define CartoCSS styles in order to obtain raster tiles. Raster tiles are made up of 256x256 pixels; to avoid graphic quality issues tiles should be used unscaled to represent the zoom level (Z) for which they are requested. In order to render tiles, data will be retrieved from the database (in vector format) on the server-side.
|
||||
|
||||
- **Vector**: Generates map tiles based on pre-defined coordinates to represent your data, similar to how basemap image tiles are rendered. On the client-side, map tiles represent real-world geometries of a map. Depending on the coordinates, vertices are used to connect the data and display points, lines, or polygons for the map tiles.
|
||||
- **Vector**: Tiles can also be requested as MVT (Mapbox Vector Tiles). In this case, only the geospatial vector data, without any styling, is returned. These tiles should be processed in the client-side to render the map. In this case layers do not need to define CartoCSS, as any rendering and styling will be performed on the client side. The vector data of a tile represents real-world geometries by defining the vertices of points, lines or polygons in a tile-specific coordinate system.
|
||||
|
||||
## Retrieve resources from the layergroup
|
||||
|
||||
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
|
||||
|
||||
### Mapnik tiles
|
||||
### Raster tiles
|
||||
|
||||
These raster tiles retrieve just the Mapnik layers. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
These raster tiles are PNG images that represent only the Mapnik layers of a map. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
@@ -92,9 +93,9 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that store geographic vector data on the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that transfer geographic vector data to the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
|
||||
CARTO uses a Web Graphics Library (WebGL) to process MVT files. This is useful since WebGL's are compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
|
||||
CARTO uses Web Graphics Library (WebGL) to process MVT files on the browser. This is useful since WebGL is compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
@@ -245,7 +246,7 @@ center: [30, 0]
|
||||
map.setStyle({
|
||||
"version": 7,
|
||||
"glyphs": "...",
|
||||
"constants": {...},
|
||||
"constants": {...},
|
||||
"sources": {
|
||||
"cartodb": {
|
||||
"type": "vector",
|
||||
|
||||
@@ -90,6 +90,7 @@ MapController.prototype.composeCreateMapMiddleware = function (useTemplate = fal
|
||||
this.setAnalysesMetadataToLayergroup(includeQuery),
|
||||
this.setTurboCartoMetadataToLayergroup(),
|
||||
this.setAggregationMetadataToLayergroup(),
|
||||
this.setTilejsonMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
@@ -313,6 +314,74 @@ MapController.prototype.augmentLayergroupData = function () {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function getTilejson(tiles, grids) {
|
||||
const tilejson = {
|
||||
tilejson: '2.2.0',
|
||||
tiles: tiles.https || tiles.http
|
||||
};
|
||||
|
||||
if (grids) {
|
||||
tilejson.grids = grids.https || grids.http;
|
||||
}
|
||||
|
||||
return tilejson;
|
||||
}
|
||||
|
||||
MapController.prototype.setTilejsonMetadataToLayergroup = function () {
|
||||
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
|
||||
let hasMapnikLayers = false;
|
||||
layergroup.metadata.layers.forEach((layerMetadata, index) => {
|
||||
const layerId = mapconfig.getLayerId(index);
|
||||
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
|
||||
if (mapconfig.layerType(index) === 'mapnik') {
|
||||
hasMapnikLayers = true;
|
||||
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
|
||||
const layerTilejson = {
|
||||
vector: getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
|
||||
};
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
let grids = null;
|
||||
const layer = mapconfig.getLayer(index);
|
||||
if (layer.options.interactivity) {
|
||||
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
|
||||
grids = this.resourceLocator.getTileUrls(user, gridResource);
|
||||
}
|
||||
layerTilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource),
|
||||
grids
|
||||
);
|
||||
}
|
||||
layerMetadata.tilejson = layerTilejson;
|
||||
} else {
|
||||
layerMetadata.tilejson = {
|
||||
raster: getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tilejson = {};
|
||||
|
||||
if (hasMapnikLayers) {
|
||||
tilejson.vector = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`)
|
||||
);
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
tilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, `${layergroup.layergroupid}/{z}/{x}/{y}.png`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
layergroup.metadata.tilejson = tilejson;
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
const { templateName } = require('../backends/template_maps');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -20,211 +16,189 @@ function NamedMapsAdminController(authApi, templateMaps) {
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
const { base_url_templated } = app;
|
||||
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
this.checkContentType('POST', 'POST TEMPLATE'),
|
||||
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
|
||||
this.create()
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
this.checkContentType('PUT', 'PUT TEMPLATE'),
|
||||
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
|
||||
this.update()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
|
||||
this.retrieve()
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
|
||||
this.destroy()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
|
||||
this.list()
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated user can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
NamedMapsAdminController.prototype.checkContentType = function (action, label) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if (!req.is('application/json')) {
|
||||
const error = new Error(`template ${action} data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.create = function () {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
this.templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function getTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
res.status(200);
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val) {
|
||||
assert.ifError(err);
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function () {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function () {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
delete template.auth_id;
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
NamedMapsAdminController.prototype.destroy = function () {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function deleteTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (!authenticated) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
res.status(204);
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]('');
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function () {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
this.templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_ids: templateIds });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
module.exports = class AggregationMapConfig extends MapConfig {
|
||||
constructor (config, datasource) {
|
||||
super(config, datasource);
|
||||
}
|
||||
|
||||
isAggregationMapConfig () {
|
||||
return this.isVectorOnlyMapConfig() || this.hasAnyLayerAggregation();
|
||||
}
|
||||
|
||||
isAggregationLayer (index) {
|
||||
return this.isVectorOnlyMapConfig() || this.hasLayerAggregation(index);
|
||||
}
|
||||
|
||||
hasAnyLayerAggregation () {
|
||||
const layers = this.getLayers();
|
||||
|
||||
for (let index = 0; index < layers.length; index++) {
|
||||
if (this.hasLayerAggregation(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasLayerAggregation (index) {
|
||||
const layer = this.getLayer(index);
|
||||
const { aggregation } = layer.options;
|
||||
|
||||
return aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean');
|
||||
}
|
||||
};
|
||||
229
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
229
lib/cartodb/models/aggregation/aggregation-mapconfig.js
Normal file
@@ -0,0 +1,229 @@
|
||||
const MapConfig = require('windshaft').model.MapConfig;
|
||||
const aggregationQuery = require('./aggregation-query');
|
||||
const aggregationValidator = require('./aggregation-validator');
|
||||
const {
|
||||
createPositiveNumberValidator,
|
||||
createIncludesValueValidator,
|
||||
createAggregationColumnsValidator
|
||||
} = aggregationValidator;
|
||||
|
||||
const SubstitutionTokens = require('../../utils/substitution-tokens');
|
||||
|
||||
const removeDuplicates = arr => [...new Set(arr)];
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = class AggregationMapConfig extends MapConfig {
|
||||
static get AGGREGATIONS () {
|
||||
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
|
||||
}
|
||||
|
||||
static get PLACEMENTS () {
|
||||
return aggregationQuery.SUPPORTED_PLACEMENTS;
|
||||
}
|
||||
|
||||
static get THRESHOLD () {
|
||||
return 1e5; // 100K
|
||||
}
|
||||
|
||||
static get RESOLUTION () {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static get SUPPORTED_GEOMETRY_TYPES () {
|
||||
return [
|
||||
'ST_Point'
|
||||
];
|
||||
}
|
||||
|
||||
static supportsGeometryType(geometryType) {
|
||||
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
|
||||
}
|
||||
|
||||
constructor (user, config, connection, datasource) {
|
||||
super(config, datasource);
|
||||
|
||||
const validate = aggregationValidator(this);
|
||||
const positiveNumberValidator = createPositiveNumberValidator(this);
|
||||
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
|
||||
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
|
||||
|
||||
validate('resolution', positiveNumberValidator);
|
||||
validate('placement', includesValidPlacementsValidator);
|
||||
validate('threshold', positiveNumberValidator);
|
||||
validate('columns', aggregationColumnsValidator);
|
||||
|
||||
this.user = user;
|
||||
this.pgConnection = connection;
|
||||
}
|
||||
|
||||
getAggregatedQuery (index) {
|
||||
const { sql_raw, sql } = this.getLayer(index).options;
|
||||
const {
|
||||
// The default aggregation has no placement, columns or dimensions;
|
||||
// this enables the special "full-sample" aggregation.
|
||||
resolution = AggregationMapConfig.RESOLUTION,
|
||||
threshold = AggregationMapConfig.THRESHOLD,
|
||||
placement,
|
||||
columns = {},
|
||||
dimensions = {}
|
||||
} = this.getAggregation(index);
|
||||
|
||||
return aggregationQuery({
|
||||
query: sql_raw || sql,
|
||||
resolution,
|
||||
threshold,
|
||||
placement,
|
||||
columns,
|
||||
dimensions,
|
||||
isDefaultAggregation: this._isDefaultLayerAggregation(index)
|
||||
});
|
||||
}
|
||||
|
||||
isAggregationMapConfig () {
|
||||
return this.isVectorOnlyMapConfig() || this.hasAnyLayerAggregation();
|
||||
}
|
||||
|
||||
isAggregationLayer (index) {
|
||||
return this.isVectorOnlyMapConfig() || this.hasLayerAggregation(index);
|
||||
}
|
||||
|
||||
hasAnyLayerAggregation () {
|
||||
const layers = this.getLayers();
|
||||
|
||||
for (let index = 0; index < layers.length; index++) {
|
||||
if (this.hasLayerAggregation(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
hasLayerAggregation (index) {
|
||||
const layer = this.getLayer(index);
|
||||
const { aggregation } = layer.options;
|
||||
|
||||
return aggregation !== undefined && (typeof aggregation === 'object' || typeof aggregation === 'boolean');
|
||||
}
|
||||
|
||||
getAggregation (index) {
|
||||
if (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { aggregation } = this.getLayer(index).options;
|
||||
|
||||
if (typeof aggregation === 'boolean') {
|
||||
return {};
|
||||
}
|
||||
|
||||
return aggregation;
|
||||
}
|
||||
|
||||
getLayerAggregationColumns (index, callback) {
|
||||
if (this._isDefaultLayerAggregation(index)) {
|
||||
const skipGeoms = true;
|
||||
return this.getLayerColumns(index, skipGeoms, (err, columns) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, columns);
|
||||
});
|
||||
}
|
||||
|
||||
const columns = this._getLayerAggregationRequiredColumns(index);
|
||||
|
||||
return callback(null, columns);
|
||||
}
|
||||
|
||||
_getLayerAggregationRequiredColumns (index) {
|
||||
const { columns, dimensions } = this.getAggregation(index);
|
||||
|
||||
let aggregatedColumns = [];
|
||||
if (columns) {
|
||||
aggregatedColumns = Object.keys(columns)
|
||||
.map(key => columns[key].aggregated_column)
|
||||
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
|
||||
}
|
||||
|
||||
let dimensionsColumns = [];
|
||||
if (dimensions) {
|
||||
dimensionsColumns = Object.keys(dimensions)
|
||||
.map(key => dimensions[key])
|
||||
.filter(dimension => typeof dimension === 'string');
|
||||
}
|
||||
|
||||
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
|
||||
}
|
||||
|
||||
doesLayerReachThreshold(index, featureCount) {
|
||||
const threshold = this.getAggregation(index) && this.getAggregation(index).threshold ?
|
||||
this.getAggregation(index).threshold :
|
||||
AggregationMapConfig.THRESHOLD;
|
||||
|
||||
return featureCount >= threshold;
|
||||
}
|
||||
|
||||
getLayerColumns (index, skipGeoms, callback) {
|
||||
const geomColumns = ['the_geom', 'the_geom_webmercator'];
|
||||
const limitedQuery = ctx => `SELECT * FROM (${ctx.query}) __cdb_schema LIMIT 0`;
|
||||
const layer = this.getLayer(index);
|
||||
|
||||
this.pgConnection.getConnection(this.user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const sql = limitedQuery({
|
||||
query: prepareSql(layer.options.sql)
|
||||
});
|
||||
|
||||
connection.query(sql, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
let columns = result.fields || [];
|
||||
|
||||
columns = columns.map(({ name }) => name);
|
||||
|
||||
if (skipGeoms) {
|
||||
columns = columns.filter((column) => !geomColumns.includes(column));
|
||||
}
|
||||
|
||||
return callback(err, columns);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isDefaultLayerAggregation (index) {
|
||||
const aggregation = this.getAggregation(index);
|
||||
|
||||
return (this.isVectorOnlyMapConfig() && !this.hasLayerAggregation(index)) ||
|
||||
aggregation === true ||
|
||||
this._isDefaultAggregation(aggregation);
|
||||
}
|
||||
|
||||
_isDefaultAggregation (aggregation) {
|
||||
return aggregation.placement === undefined &&
|
||||
aggregation.columns === undefined &&
|
||||
this._isEmptyParameter(aggregation.dimensions);
|
||||
}
|
||||
|
||||
_isEmptyParameter(parameter) {
|
||||
return parameter === undefined || parameter === null || this._isEmptyObject(parameter);
|
||||
}
|
||||
|
||||
_isEmptyObject (parameter) {
|
||||
return typeof parameter === 'object' && Object.keys(parameter).length === 0;
|
||||
}
|
||||
};
|
||||
@@ -1,8 +1,12 @@
|
||||
const DEFAULT_PLACEMENT = 'point-sample';
|
||||
|
||||
/**
|
||||
* Returns a template function (function that accepts template parameters and returns a string)
|
||||
* to generate an aggregation query.
|
||||
* Valid options to define the query template are:
|
||||
* - placement
|
||||
* - columns
|
||||
* - dimensions*
|
||||
* The query template parameters taken by the result template function are:
|
||||
* - sourceQuery
|
||||
* - res
|
||||
@@ -10,9 +14,12 @@
|
||||
* - dimensions
|
||||
*/
|
||||
const templateForOptions = (options) => {
|
||||
let templateFn = aggregationQueryTemplates[options.placement];
|
||||
if (!templateFn) {
|
||||
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
|
||||
let templateFn = defaultAggregationQueryTemplate;
|
||||
if (!options.isDefaultAggregation) {
|
||||
templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
|
||||
if (!templateFn) {
|
||||
throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
|
||||
}
|
||||
}
|
||||
return templateFn;
|
||||
};
|
||||
@@ -25,6 +32,11 @@ const templateForOptions = (options) => {
|
||||
* - columns
|
||||
* - placement
|
||||
* - dimensions
|
||||
*
|
||||
* The default aggregation (when no explicit placement, columns or dimensions are present) returns
|
||||
* a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
|
||||
* When placement, columns or dimensions are specified, columns are aggregated as requested
|
||||
* (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
|
||||
*/
|
||||
const queryForOptions = (options) => templateForOptions(options)({
|
||||
sourceQuery: options.query,
|
||||
@@ -56,6 +68,8 @@ const SUPPORTED_AGGREGATE_FUNCTIONS = {
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);
|
||||
|
||||
const sep = (list) => {
|
||||
let expr = list.join(', ');
|
||||
return expr ? ', ' + expr : expr;
|
||||
@@ -69,8 +83,13 @@ const aggregateColumns = ctx => {
|
||||
}, ctx.columns || {});
|
||||
};
|
||||
|
||||
const aggregateColumnNames = ctx => {
|
||||
const aggregateColumnNames = (ctx, table) => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(columns).map(
|
||||
column_name => `${table}.${column_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(columns));
|
||||
};
|
||||
|
||||
@@ -90,8 +109,14 @@ const aggregateColumnDefs = ctx => {
|
||||
|
||||
const aggregateDimensions = ctx => ctx.dimensions || {};
|
||||
|
||||
const dimensionNames = ctx => {
|
||||
return sep(Object.keys(aggregateDimensions(ctx)));
|
||||
const dimensionNames = (ctx, table) => {
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
if (table) {
|
||||
return sep(Object.keys(dimensions).map(
|
||||
dimension_name => `${table}.${dimension_name}`
|
||||
));
|
||||
}
|
||||
return sep(Object.keys(dimensions));
|
||||
};
|
||||
|
||||
const dimensionDefs = ctx => {
|
||||
@@ -114,9 +139,38 @@ const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::dou
|
||||
// is only applied after the aggregation.
|
||||
// * This queries are used for rendering and the_geom is omitted in the results for better performance
|
||||
|
||||
// The special default aggregation includes all the columns of a sample row per grid cell and
|
||||
// the count (_cdb_feature_count) of the aggregated rows.
|
||||
const defaultAggregationQueryTemplate = ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
) SELECT
|
||||
_cdb_query.*
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`;
|
||||
|
||||
const aggregationQueryTemplates = {
|
||||
'centroid': ctx => `
|
||||
WITH _cdb_params AS (
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
@@ -140,34 +194,37 @@ const aggregationQueryTemplates = {
|
||||
`,
|
||||
|
||||
'point-grid': ctx => `
|
||||
WITH _cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM _cdb_clusters, _cdb_params
|
||||
`,
|
||||
|
||||
'point-sample': ctx => `
|
||||
WITH _cdb_params AS (
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
), _cdb_clusters AS (
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE the_geom_webmercator && _cdb_params.bbox
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
)
|
||||
SELECT
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
FROM _cdb_clusters, _cdb_params
|
||||
`,
|
||||
|
||||
'point-sample': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
@@ -178,13 +235,17 @@ const aggregationQueryTemplates = {
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
) SELECT
|
||||
)
|
||||
SELECT
|
||||
_cdb_clusters.cartodb_id,
|
||||
the_geom, the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
${dimensionNames(ctx, '_cdb_query')}
|
||||
${aggregateColumnNames(ctx, '_cdb_clusters')}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
|
||||
`
|
||||
|
||||
};
|
||||
|
||||
module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
|
||||
|
||||
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
93
lib/cartodb/models/aggregation/aggregation-validator.js
Normal file
@@ -0,0 +1,93 @@
|
||||
module.exports = function aggregationValidator (mapconfig) {
|
||||
return function validateProperty (key, validator) {
|
||||
for (let index = 0; index < mapconfig.getLayers().length; index++) {
|
||||
const aggregation = mapconfig.getAggregation(index);
|
||||
|
||||
if (aggregation === undefined || aggregation[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
validator(aggregation[key], key, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createIncludesValueValidator = function (mapconfig, validValues) {
|
||||
return function validateIncludesValue (value, key, index) {
|
||||
if (!validValues.includes(value)) {
|
||||
const message = `Invalid ${key}. Valid values: ${validValues.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createPositiveNumberValidator = function (mapconfig) {
|
||||
return function validatePositiveNumber (value, key, index) {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
const message = `Invalid ${key}, should be a number greather than 0`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createAggregationColumnsValidator = function (mapconfig, validAggregatedFunctions) {
|
||||
const validateAggregationColumnNames = createAggregationColumnNamesValidator(mapconfig);
|
||||
const validateAggregateFunction = createAggregateFunctionValidator(mapconfig, validAggregatedFunctions);
|
||||
const validateAggregatedColumn = createAggregatedColumnValidator(mapconfig);
|
||||
|
||||
return function validateAggregationColumns (value, key, index) {
|
||||
validateAggregationColumnNames(value, key, index);
|
||||
validateAggregateFunction(value, key, index);
|
||||
validateAggregatedColumn(value, key, index);
|
||||
};
|
||||
};
|
||||
|
||||
function createAggregationColumnNamesValidator(mapconfig) {
|
||||
return function validateAggregationColumnNames (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
if (columnName.length <= 0) {
|
||||
const message = `Invalid column name, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregateFunctionValidator (mapconfig, validAggregatedFunctions) {
|
||||
return function validateAggregateFunction (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregate_function } = value[columnName];
|
||||
|
||||
if (!validAggregatedFunctions.includes(aggregate_function)) {
|
||||
const message = `Unsupported aggregation function ${aggregate_function},` +
|
||||
` valid ones: ${validAggregatedFunctions.join(', ')}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createAggregatedColumnValidator (mapconfig) {
|
||||
return function validateAggregatedColumn (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
const { aggregated_column } = value[columnName];
|
||||
|
||||
if (typeof aggregated_column !== 'string' || aggregated_column <= 0) {
|
||||
const message = `Invalid aggregated column, should be a non empty string`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createLayerError(message, mapconfig, index) {
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapconfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapconfig.layerType(index)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
const aggregationQuery = require('./aggregation-query');
|
||||
|
||||
module.exports = class Aggregation {
|
||||
static get THRESHOLD() {
|
||||
return 1e5; // 100K
|
||||
}
|
||||
|
||||
constructor (mapconfig, query, {
|
||||
resolution = 1,
|
||||
threshold = Aggregation.THRESHOLD,
|
||||
placement = 'centroid',
|
||||
columns = {},
|
||||
dimensions = {}
|
||||
} = {}) {
|
||||
this.mapconfig = mapconfig;
|
||||
this.query = query;
|
||||
this.resolution = resolution;
|
||||
this.threshold = threshold;
|
||||
this.placement = placement;
|
||||
this.columns = columns;
|
||||
this.dimensions = dimensions;
|
||||
}
|
||||
sql () {
|
||||
return aggregationQuery(this);
|
||||
}
|
||||
};
|
||||
@@ -56,7 +56,7 @@ __wd_buckets AS
|
||||
${ctx.query}
|
||||
) __source, __wd_tz
|
||||
${condition_str}
|
||||
GROUP BY timestamp, __wd_tz.name
|
||||
GROUP BY 1, __wd_tz.name
|
||||
),`;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const binsQueryTpl = ctx => `
|
||||
) AS quartile
|
||||
FROM __cdb_filtered_source) _cdb_quartiles
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
GROUP BY 1
|
||||
) __cdb_iqr
|
||||
),
|
||||
__cdb_bins AS (
|
||||
@@ -137,8 +137,8 @@ FROM
|
||||
(
|
||||
${ctx.query}
|
||||
) __cdb_filtered_source_query${extra_tables}
|
||||
GROUP BY bin${extra_groupby}
|
||||
ORDER BY bin;`;
|
||||
GROUP BY 10${extra_groupby}
|
||||
ORDER BY 10;`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
const Aggregation = require('../../aggregation/aggregation');
|
||||
const AggregationMapConfig = require('../../aggregation/aggregation-map-config');
|
||||
const AggregationMapConfig = require('../../aggregation/aggregation-mapconfig');
|
||||
const queryUtils = require('../../../utils/query-utils');
|
||||
|
||||
const unsupportedGeometryTypeErrorMessage = ctx =>
|
||||
`Unsupported geometry type: ${ctx.geometryType}. Aggregation is available only for geometry type: ST_Point`;
|
||||
`Unsupported geometry type: ${ctx.geometryType}. ` +
|
||||
`Aggregation is available only for geometry type: ${AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES}`;
|
||||
|
||||
const invalidAggregationParamValueErrorMessage = ctx =>
|
||||
`Invalid value for 'aggregation' query param: ${ctx.value}. Valid ones are 'true' or 'false'`;
|
||||
@@ -14,11 +14,17 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
}
|
||||
|
||||
getMapConfig (user, requestMapConfig, params, context, callback) {
|
||||
if (!this._isValidAggregationParam(params)) {
|
||||
if (!this._isValidAggregationQueryParam(params)) {
|
||||
return callback(new Error(invalidAggregationParamValueErrorMessage({ value: params.aggregation })));
|
||||
}
|
||||
|
||||
const mapConfig = new AggregationMapConfig(requestMapConfig);
|
||||
let mapConfig;
|
||||
try {
|
||||
mapConfig = new AggregationMapConfig(user, requestMapConfig, this.pgConnection);
|
||||
} catch (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
|
||||
if (!this._shouldAdapt(mapConfig, params)) {
|
||||
return callback(null, requestMapConfig);
|
||||
@@ -33,7 +39,7 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
});
|
||||
}
|
||||
|
||||
_isValidAggregationParam (params) {
|
||||
_isValidAggregationQueryParam (params) {
|
||||
const { aggregation } = params;
|
||||
return aggregation === undefined || aggregation === 'true' || aggregation === 'false';
|
||||
}
|
||||
@@ -83,30 +89,36 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
if (shouldAdapt) {
|
||||
const sql = layer.options.sql_raw ? layer.options.sql_raw : layer.options.sql;
|
||||
const aggregation = new Aggregation(mapConfig, sql, layer.options.aggregation);
|
||||
const sqlQueryWrap = layer.options.sql_wrap;
|
||||
|
||||
let aggregationSql = aggregation.sql();
|
||||
|
||||
if (sqlQueryWrap) {
|
||||
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
|
||||
}
|
||||
|
||||
layer.options.sql = aggregationSql;
|
||||
if (!shouldAdapt) {
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
}
|
||||
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
const sqlQueryWrap = layer.options.sql_wrap;
|
||||
|
||||
let aggregationSql = mapConfig.getAggregatedQuery(index);
|
||||
|
||||
if (sqlQueryWrap) {
|
||||
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
|
||||
}
|
||||
|
||||
layer.options.sql = aggregationSql;
|
||||
|
||||
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
layer.options.columns = columns;
|
||||
|
||||
return resolve({ layer, index, adapted: shouldAdapt });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_shouldAdaptLayer (connection, mapConfig, layer, index, callback) {
|
||||
let shouldAdapt = false;
|
||||
|
||||
if (!mapConfig.isAggregationLayer(index)) {
|
||||
return callback(null, shouldAdapt);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const aggregationMetadata = queryUtils.getAggregationMetadata({
|
||||
@@ -115,29 +127,33 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
|
||||
connection.query(aggregationMetadata, (err, res) => {
|
||||
if (err) {
|
||||
return callback(null, shouldAdapt);
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
const result = res.rows[0] || {};
|
||||
const estimatedFeatureCount = result.count;
|
||||
|
||||
const threshold = layer.options.aggregation && layer.options.aggregation.threshold ?
|
||||
layer.options.aggregation.threshold :
|
||||
Aggregation.THRESHOLD;
|
||||
if (!mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
const message = unsupportedGeometryTypeErrorMessage({ geometryType: result.type });
|
||||
const error = new Error(message);
|
||||
error.type = 'layer';
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(index),
|
||||
index: index,
|
||||
type: mapConfig.layerType(index)
|
||||
};
|
||||
|
||||
if (estimatedFeatureCount < threshold) {
|
||||
return callback(null, shouldAdapt);
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
const geometryType = result.type;
|
||||
|
||||
if (geometryType !== 'ST_Point') {
|
||||
return callback(new Error(unsupportedGeometryTypeErrorMessage({ geometryType })));
|
||||
if (mapConfig.isVectorOnlyMapConfig() && !AggregationMapConfig.supportsGeometryType(result.type)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
shouldAdapt = true;
|
||||
if (!mapConfig.doesLayerReachThreshold(index, result.count)) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
callback(null, shouldAdapt);
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,20 +21,36 @@ function ResourceLocator(environment) {
|
||||
|
||||
module.exports = ResourceLocator;
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resource) {
|
||||
ResourceLocator.prototype.getTileUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, resource);
|
||||
}
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
|
||||
if (cdnDomain) {
|
||||
const urls = this.getUrlsFromTemplate(username, new TileResource(resourcePath));
|
||||
return {
|
||||
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
|
||||
http: Array.isArray(urls.http) ? urls.http : [urls.http],
|
||||
https: Array.isArray(urls.https) ? urls.https : [urls.https]
|
||||
};
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new TileResource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
http: [`http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`]
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resourcePath) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, new Resource(resourcePath));
|
||||
}
|
||||
var cdnUrls = getCdnUrls(this.environment.serverMetadata, username, new Resource(resourcePath));
|
||||
if (cdnUrls) {
|
||||
return cdnUrls;
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: `http://${username}.localhost.lan:${port}/api/v1/map/${resourcePath}`
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -45,45 +61,125 @@ ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
if (Array.isArray(cdnDomain.http)) {
|
||||
urls.http = cdnDomain.http.map(d => this.resourcesUrlTemplates.http({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
if (Array.isArray(cdnDomain.https)) {
|
||||
urls.https = cdnDomain.https.map(d => this.resourcesUrlTemplates.https({
|
||||
cdn_url: d,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
}));
|
||||
} else {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource.getPath()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
|
||||
class Resource {
|
||||
constructor (resourcePath) {
|
||||
this.resourcePath = resourcePath;
|
||||
}
|
||||
|
||||
getPath () {
|
||||
return this.resourcePath;
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return domain.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
let urls = getUrl(baseUrl, username, this.resourcePath);
|
||||
if (subdomains) {
|
||||
urls = urls.replace('{s}', subdomain(subdomains, this.resourcePath));
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
}
|
||||
|
||||
class TileResource extends Resource {
|
||||
constructor (resourcePath) {
|
||||
super(resourcePath);
|
||||
}
|
||||
|
||||
getDomain (domain, subdomains) {
|
||||
return subdomains.map(s => domain.replace('{s}', s));
|
||||
}
|
||||
|
||||
getUrl (baseUrl, username, subdomains) {
|
||||
if (!subdomains) {
|
||||
return [super.getUrl(baseUrl, username)];
|
||||
}
|
||||
return subdomains.map(subdomain => {
|
||||
return getUrl(baseUrl, username, this.resourcePath)
|
||||
.replace('{s}', subdomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getUrl(baseUrl, username, path) {
|
||||
return `${baseUrl}/${username}/api/v1/map/${path}`;
|
||||
}
|
||||
|
||||
function getCdnUrls(serverMetadata, username, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var httpUrls = resource.getUrl(`http://${cdnUrl.http}`, username);
|
||||
var httpsUrls = resource.getUrl(`https://${cdnUrl.https}`, username);
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
httpUrls = resource.getUrl(templates.http.url, username, templates.http.subdomains);
|
||||
httpsUrls = resource.getUrl(templates.https.url, username, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: httpUrls,
|
||||
https: httpsUrls,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getCdnDomain(serverMetadata, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var http = cdnUrl.http;
|
||||
var https = cdnUrl.https;
|
||||
var httpDomain = cdnUrl.http;
|
||||
var httpsDomain = cdnUrl.https;
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
var httpUrlTemplate = templates.http.url;
|
||||
var httpsUrlTemplate = templates.https.url;
|
||||
http = httpUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.http.subdomains, resource));
|
||||
https = httpsUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.https.subdomains, resource));
|
||||
httpDomain = httpUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpDomain = resource.getDomain(httpDomain, templates.http.subdomains);
|
||||
httpsDomain = httpsUrlTemplate.replace(/^(http[s]*:\/\/)/, '');
|
||||
httpsDomain = resource.getDomain(httpsDomain, templates.https.subdomains);
|
||||
}
|
||||
return {
|
||||
http: http,
|
||||
https: https,
|
||||
http: httpDomain,
|
||||
https: httpsDomain,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "4.5.0",
|
||||
"version": "4.7.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -46,7 +46,7 @@
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "4.1.0",
|
||||
"windshaft": "4.2.0",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -65,7 +65,7 @@
|
||||
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
|
||||
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
|
||||
"docker-pull": "docker pull cartoimages/windshaft-testing",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh && docker ps --filter status=dead --filter status=exited -aq | xargs -r docker rm -v",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
},
|
||||
|
||||
@@ -289,7 +289,8 @@ describe('aggregation', function () {
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
threshold: 1,
|
||||
placement: 'centroid'
|
||||
},
|
||||
cartocss: '#layer { marker-width: [value]; }',
|
||||
cartocss_version: '2.3.0'
|
||||
@@ -309,6 +310,45 @@ describe('aggregation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should provide all columns in the default aggregation ',
|
||||
function (done) {
|
||||
const response = {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
},
|
||||
cartocss: '#layer { marker-width: [value]; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
this.testClient.getLayergroup({ response }, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(layer.meta.aggregation.png));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip aggregation to create a layergroup with aggregation defined already', function (done) {
|
||||
const mapConfig = createVectorMapConfig([
|
||||
{
|
||||
@@ -345,6 +385,44 @@ describe('aggregation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('skip default aggregation by setting `aggregation: false` for just one layer', function (done) {
|
||||
const mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: false
|
||||
}
|
||||
}
|
||||
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, true);
|
||||
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('when the aggregation param is not valid should respond with error', function (done) {
|
||||
const mapConfig = createVectorMapConfig([
|
||||
{
|
||||
@@ -431,6 +509,8 @@ describe('aggregation', function () {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1,
|
||||
cartocss: '#layer { marker-width: [value]; }',
|
||||
cartocss_version: '2.3.0',
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
@@ -456,9 +536,14 @@ describe('aggregation', function () {
|
||||
' Aggregation is available only for geometry type: ST_Point'
|
||||
],
|
||||
errors_with_context:[{
|
||||
type: 'unknown',
|
||||
type: 'layer',
|
||||
message: 'Unsupported geometry type: ST_Polygon.' +
|
||||
' Aggregation is available only for geometry type: ST_Point'
|
||||
' Aggregation is available only for geometry type: ST_Point',
|
||||
layer: {
|
||||
id: 'layer0',
|
||||
index: 0,
|
||||
type: 'mapnik'
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
@@ -524,6 +609,198 @@ describe('aggregation', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('when dimensions is provided should return a tile returning the column used as dimensions',
|
||||
function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 1,
|
||||
dimensions: {
|
||||
value: "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
tileJSON[0].features.forEach(feature => assert.equal(typeof feature.properties.value, 'number'));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
['centroid', 'point-sample', 'point-grid'].forEach(placement => {
|
||||
it(`dimensions should work for ${placement} placement`, function(done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
placement: placement ,
|
||||
threshold: 1,
|
||||
dimensions: {
|
||||
value: "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.value, 'number')
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it(`dimensions should trigger non-default aggregation`, function(done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: {
|
||||
threshold: 1,
|
||||
dimensions: {
|
||||
value: "value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.value, 'number')
|
||||
);
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.sqrt_value, 'undefined')
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it(`aggregation columns should trigger non-default aggregation`, function(done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_2,
|
||||
aggregation: {
|
||||
threshold: 1,
|
||||
columns: {
|
||||
value: {
|
||||
aggregate_function: 'sum',
|
||||
aggregated_column: 'value'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.value, 'number')
|
||||
);
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.sqrt_value, 'undefined')
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
['centroid', 'point-sample', 'point-grid'].forEach(placement => {
|
||||
it(`aggregations with base column names should work for ${placement} placement`, function(done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
placement: placement ,
|
||||
threshold: 1,
|
||||
columns: {
|
||||
value: {
|
||||
aggregate_function: 'sum',
|
||||
aggregated_column: 'value'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
tileJSON[0].features.forEach(
|
||||
feature => assert.equal(typeof feature.properties.value, 'number')
|
||||
);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should work when the sql has single quotes', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
@@ -531,6 +808,7 @@ describe('aggregation', function () {
|
||||
options: {
|
||||
sql: `
|
||||
SELECT
|
||||
cartodb_id,
|
||||
the_geom_webmercator,
|
||||
the_geom,
|
||||
value,
|
||||
@@ -686,6 +964,408 @@ describe('aggregation', function () {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('aggregates with full-sample placement by default', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
resolution: 256,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getTile(0, 0, 0, { format: 'mvt' }, function (err, res, mvt) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const geojsonTile = JSON.parse(mvt.toGeoJSONSync(0));
|
||||
|
||||
assert.ok(Array.isArray(geojsonTile.features));
|
||||
assert.ok(geojsonTile.features.length > 0);
|
||||
|
||||
const feature = geojsonTile.features[0];
|
||||
|
||||
assert.ok(feature.properties.hasOwnProperty('value'), 'Missing value property');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad resolution', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
id: 'wadus',
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
resolution: 'wadus',
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Invalid resolution, should be a number greather than 0' ],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Invalid resolution, should be a number greather than 0',
|
||||
layer: {
|
||||
"id": "wadus",
|
||||
"index": 0,
|
||||
"type": "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad placement', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
placement: 'wadus',
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Invalid placement. Valid values: centroid, point-grid, point-sample'],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Invalid placement. Valid values: centroid, point-grid, point-sample',
|
||||
layer: {
|
||||
id: "layer0",
|
||||
index: 0,
|
||||
type: "mapnik",
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad threshold', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 'wadus',
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Invalid threshold, should be a number greather than 0' ],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Invalid threshold, should be a number greather than 0',
|
||||
layer: {
|
||||
"id": "layer0",
|
||||
"index": 0,
|
||||
"type": "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad column name', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
columns : {
|
||||
'': {
|
||||
aggregate_function: 'count',
|
||||
aggregated_column: 'value',
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Invalid column name, should be a non empty string' ],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Invalid column name, should be a non empty string',
|
||||
layer: {
|
||||
"id": "layer0",
|
||||
"index": 0,
|
||||
"type": "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad aggregated function', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
columns : {
|
||||
'wadus_function': {
|
||||
aggregate_function: 'wadus',
|
||||
aggregated_column: 'value',
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Unsupported aggregation function wadus, ' +
|
||||
'valid ones: count, avg, sum, min, max, mode' ],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Unsupported aggregation function wadus, ' +
|
||||
'valid ones: count, avg, sum, min, max, mode',
|
||||
layer: {
|
||||
"id": "layer0",
|
||||
"index": 0,
|
||||
"type": "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail with bad aggregated columns', function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
columns : {
|
||||
'total_wadus': {
|
||||
aggregate_function: 'sum',
|
||||
aggregated_column: '',
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
const options = {
|
||||
response: {
|
||||
status: 400
|
||||
}
|
||||
};
|
||||
|
||||
this.testClient.getLayergroup(options, (err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(body, {
|
||||
errors: [ 'Invalid aggregated column, should be a non empty string' ],
|
||||
errors_with_context:[{
|
||||
type: 'layer',
|
||||
message: 'Invalid aggregated column, should be a non empty string',
|
||||
layer: {
|
||||
"id": "layer0",
|
||||
"index": 0,
|
||||
"type": "mapnik"
|
||||
}
|
||||
}]
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should skip aggregation w/o failing when is Vector Only MapConfig and layer has polygons',
|
||||
function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.mvt));
|
||||
body.metadata.layers.forEach(layer => assert.ok(!layer.meta.aggregation.png));
|
||||
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
assert.equal(tileJSON[0].features.length, 7);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip aggregation for polygons (w/o failing) and aggregate when the layer has points',
|
||||
function (done) {
|
||||
this.mapConfig = createVectorMapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POLYGONS_SQL_1
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: POINTS_SQL_1,
|
||||
aggregation: {
|
||||
threshold: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
]);
|
||||
|
||||
this.testClient = new TestClient(this.mapConfig);
|
||||
|
||||
this.testClient.getLayergroup((err, body) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.equal(typeof body.metadata, 'object');
|
||||
assert.ok(Array.isArray(body.metadata.layers));
|
||||
|
||||
assert.equal(body.metadata.layers[0].meta.aggregation.mvt, false);
|
||||
assert.equal(body.metadata.layers[1].meta.aggregation.mvt, true);
|
||||
|
||||
const options = {
|
||||
format: 'mvt'
|
||||
};
|
||||
|
||||
this.testClient.getTile(0, 0, 0, options, (err, res, tile) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
const tileJSON = tile.toJSON();
|
||||
|
||||
assert.equal(tileJSON[0].features.length, 7);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
54
test/acceptance/layers-filters.js
Normal file
54
test/acceptance/layers-filters.js
Normal file
@@ -0,0 +1,54 @@
|
||||
require('../support/test_helper');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
describe('layers filters', function() {
|
||||
const type = 'mapnik';
|
||||
const sql = 'select * from populated_places_simple_reduced';
|
||||
const cartocss = `#points {
|
||||
marker-fill-opacity: 1.0;
|
||||
marker-line-color: #FFF;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: 1.0;
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: red;
|
||||
marker-allow-overlap: true;
|
||||
}`;
|
||||
const cartocss_version = '3.0.12';
|
||||
const options = {
|
||||
sql,
|
||||
cartocss,
|
||||
cartocss_version
|
||||
};
|
||||
|
||||
const mapConfig = {
|
||||
version: '1.6.0',
|
||||
layers: [
|
||||
{
|
||||
type,
|
||||
id: 'layerA',
|
||||
options
|
||||
},
|
||||
{
|
||||
type,
|
||||
id: 'layerB',
|
||||
options
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
['layerA', 'layerB'].forEach(layer => {
|
||||
it(`should work for individual layer ids: ${layer}`, function (done) {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { layers: layer }, done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
204
test/acceptance/tilejson.js
Normal file
204
test/acceptance/tilejson.js
Normal file
@@ -0,0 +1,204 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
describe('tilejson', function() {
|
||||
|
||||
function tilejsonValidation(tilejson, shouldHaveGrid = false) {
|
||||
assert.equal(tilejson.tilejson, '2.2.0');
|
||||
|
||||
assert.ok(Array.isArray(tilejson.tiles));
|
||||
assert.ok(tilejson.tiles.length > 0);
|
||||
|
||||
if (shouldHaveGrid) {
|
||||
assert.ok(Array.isArray(tilejson.grids));
|
||||
assert.ok(tilejson.grids.length > 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const sql = 'SELECT * FROM populated_places_simple_reduced';
|
||||
const cartocss = TestClient.CARTOCSS.POINTS;
|
||||
const cartocss_version = '3.0.12';
|
||||
|
||||
const RASTER_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version
|
||||
}
|
||||
};
|
||||
const RASTER_INTERACTIVITY_LAYER = {
|
||||
options: {
|
||||
sql, cartocss, cartocss_version,
|
||||
interactivity: ['cartodb_id']
|
||||
}
|
||||
};
|
||||
const VECTOR_LAYER = {
|
||||
options: {
|
||||
sql
|
||||
}
|
||||
};
|
||||
const PLAIN_LAYER = {
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#000000'
|
||||
}
|
||||
};
|
||||
|
||||
function mapConfig(layers) {
|
||||
return {
|
||||
version: '1.7.0',
|
||||
layers: Array.isArray(layers) ? layers : [layers]
|
||||
};
|
||||
}
|
||||
|
||||
describe('per layer', function() {
|
||||
it('should expose raster + vector tilejson for raster layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the vector tilejson vector only layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the raster tilejson plain layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(PLAIN_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['raster']);
|
||||
|
||||
Object.keys(layer.tilejson).forEach(k => {
|
||||
tilejsonValidation(layer.tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose grids for the raster layer with interactivity', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_INTERACTIVITY_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 1);
|
||||
|
||||
const layer = metadata.layers[0];
|
||||
assert.deepEqual(Object.keys(layer.tilejson), ['vector', 'raster']);
|
||||
|
||||
tilejsonValidation(layer.tilejson.vector);
|
||||
tilejsonValidation(layer.tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with several layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig([RASTER_LAYER, RASTER_INTERACTIVITY_LAYER]));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
assert.equal(metadata.layers.length, 2);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[0].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[0].tilejson.raster);
|
||||
|
||||
assert.deepEqual(Object.keys(metadata.layers[1].tilejson), ['vector', 'raster']);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.vector);
|
||||
tilejsonValidation(metadata.layers[1].tilejson.raster, true);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('root tilejson', function() {
|
||||
|
||||
it('should expose just the `vector` tilejson when for vector only mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(VECTOR_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose just the `vector` and `raster` tilejson for mapnik layers', function(done) {
|
||||
var testClient = new TestClient(mapConfig(RASTER_LAYER));
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
const metadata = layergroupResult.metadata;
|
||||
assert.ok(metadata);
|
||||
|
||||
const tilejson = metadata.tilejson;
|
||||
assert.deepEqual(Object.keys(tilejson), ['vector', 'raster']);
|
||||
|
||||
Object.keys(tilejson).forEach(k => {
|
||||
tilejsonValidation(tilejson[k]);
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -75,6 +75,22 @@ function checkSurrogateKey(res, expectedKey) {
|
||||
assert.deepEqual(keys, expectedKeys);
|
||||
}
|
||||
|
||||
var uncaughtExceptions = [];
|
||||
process.on('uncaughtException', function(err) {
|
||||
uncaughtExceptions.push(err);
|
||||
});
|
||||
beforeEach(function() {
|
||||
uncaughtExceptions = [];
|
||||
});
|
||||
//global afterEach to capture uncaught exceptions
|
||||
afterEach(function() {
|
||||
assert.equal(
|
||||
uncaughtExceptions.length,
|
||||
0,
|
||||
'uncaughtException:\n\n' + uncaughtExceptions.map(err => err.stack).join('\n\n'));
|
||||
});
|
||||
|
||||
|
||||
var redisClient;
|
||||
|
||||
beforeEach(function() {
|
||||
|
||||
@@ -3,9 +3,10 @@ require('../../../support/test_helper');
|
||||
var assert = require('../../../support/assert');
|
||||
var ResourceLocator = require('../../../../lib/cartodb/models/resource-locator');
|
||||
|
||||
describe('ResourceLocator.getUrls', function() {
|
||||
describe('ResourceLocator', function() {
|
||||
var USERNAME = 'username';
|
||||
var RESOURCE = 'wadus';
|
||||
var TILE_RESOURCE = 'wadus/{z}/{x}/{y}.png';
|
||||
var HTTP_SUBDOMAINS = ['1', '2', '3', '4'];
|
||||
var HTTPS_SUBDOMAINS = ['a', 'b', 'c', 'd'];
|
||||
|
||||
@@ -15,118 +16,221 @@ describe('ResourceLocator.getUrls', function() {
|
||||
assert.ok(urls);
|
||||
});
|
||||
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('basic', function() {
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
var BASIC_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(urls.http, ['http://cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://cdn.ssl.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should return default urls when basic http and https domains are provided', function() {
|
||||
var resourceLocator = new ResourceLocator(BASIC_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://cdn.ssl.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
describe('resource templates', function() {
|
||||
|
||||
assert.equal(urls.http, ['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
assert.equal(urls.https, ['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/'));
|
||||
var RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan/api/v1/map',
|
||||
https: 'https://{{=it.user}}.ssl.localhost.lan/api/v1/map'
|
||||
}
|
||||
};
|
||||
|
||||
describe('getUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + USERNAME + '.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://' + USERNAME + '.ssl.localhost.lan', 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('resources_url_templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
[`http://${USERNAME}.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
[`https://${USERNAME}.ssl.localhost.lan/api/v1/map/${TILE_RESOURCE}`]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn templates', function() {
|
||||
|
||||
var CDN_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
describe('getUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('cdn_url templates should take precedence over http and https domains', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
describe('cdn and resource templates', function() {
|
||||
|
||||
var CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT = {
|
||||
serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'cdn.carto.com',
|
||||
https: 'cdn.ssl.carto.com',
|
||||
templates: {
|
||||
http: {
|
||||
url: "http://{s}.cdn.carto.com",
|
||||
subdomains: HTTP_SUBDOMAINS
|
||||
},
|
||||
https: {
|
||||
url: "https://cdn_{s}.ssl.cdn.carto.com",
|
||||
subdomains: HTTPS_SUBDOMAINS
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
},
|
||||
resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/u/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
};
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
};
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
describe('getUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getUrls(USERNAME, RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
var httpSubdomain = ResourceLocator.subdomain(HTTP_SUBDOMAINS, RESOURCE);
|
||||
var httpsSubdomain = ResourceLocator.subdomain(HTTPS_SUBDOMAINS, RESOURCE);
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
`https://cdn_${httpsSubdomain}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${RESOURCE}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTileUrls', function() {
|
||||
it('should mix cdn_url templates and resources_url_templates', function() {
|
||||
var resourceLocator = new ResourceLocator(CDN_URL_AND_RESOURCE_TEMPLATES_ENVIRONMENT);
|
||||
var urls = resourceLocator.getTileUrls(USERNAME, TILE_RESOURCE);
|
||||
assert.ok(urls);
|
||||
|
||||
assert.deepEqual(
|
||||
urls.http,
|
||||
HTTP_SUBDOMAINS
|
||||
.map(s => `http://${s}.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
assert.deepEqual(
|
||||
urls.https,
|
||||
HTTPS_SUBDOMAINS
|
||||
.map(s => `https://cdn_${s}.ssl.cdn.carto.com/u/${USERNAME}/api/v1/map/${TILE_RESOURCE}`)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
urls.http,
|
||||
['http://' + httpSubdomain + '.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
assert.equal(
|
||||
urls.https,
|
||||
['https://cdn_' + httpsSubdomain + '.ssl.cdn.carto.com', 'u', USERNAME, 'api/v1/map', RESOURCE].join('/')
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.statsClient = this.statsClient;
|
||||
global.statsClient = this.statsClient;
|
||||
});
|
||||
|
||||
it('finalizeGetTileOrGrid does not call statsClient when format is not supported', function() {
|
||||
@@ -84,7 +84,7 @@ describe('tile stats', function() {
|
||||
});
|
||||
|
||||
function mockStatsClientGetInstance(instance) {
|
||||
global.statsClient = instance;
|
||||
global.statsClient = Object.assign(global.statsClient, instance);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -2,7 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
abaculus@cartodb/abaculus#2.0.3-cdb1:
|
||||
"abaculus@github:cartodb/abaculus#2.0.3-cdb1":
|
||||
version "2.0.3-cdb1"
|
||||
resolved "https://codeload.github.com/cartodb/abaculus/tar.gz/f5f34e1c80cdd8d49edd1d6fe3b2220ab2e23aaf"
|
||||
dependencies:
|
||||
@@ -33,8 +33,8 @@ ajv@^4.9.1:
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.1.0:
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.1.tgz#b38bb8876d9e86bee994956a04e721e88b248eb2"
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965"
|
||||
dependencies:
|
||||
co "^4.6.0"
|
||||
fast-deep-equal "^1.0.0"
|
||||
@@ -226,7 +226,7 @@ camshaft@0.60.0:
|
||||
dot "^1.0.3"
|
||||
request "^2.69.0"
|
||||
|
||||
canvas@cartodb/node-canvas#1.6.2-cdb2:
|
||||
"canvas@github:cartodb/node-canvas#1.6.2-cdb2":
|
||||
version "1.6.2-cdb2"
|
||||
resolved "https://codeload.github.com/cartodb/node-canvas/tar.gz/8acf04557005c633f9e68524488a2657c04f3766"
|
||||
dependencies:
|
||||
@@ -244,15 +244,15 @@ carto@0.16.3:
|
||||
semver "^5.1.0"
|
||||
yargs "^4.2.0"
|
||||
|
||||
carto@CartoDB/carto#0.15.1-cdb1:
|
||||
"carto@github:cartodb/carto#0.15.1-cdb1":
|
||||
version "0.15.1-cdb1"
|
||||
resolved "https://codeload.github.com/CartoDB/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/8050ec843f1f32a6469e5d1cf49602773015d398"
|
||||
dependencies:
|
||||
mapnik-reference "~6.0.2"
|
||||
optimist "~0.6.0"
|
||||
underscore "~1.6.0"
|
||||
|
||||
carto@cartodb/carto#0.15.1-cdb3:
|
||||
"carto@github:cartodb/carto#0.15.1-cdb3":
|
||||
version "0.15.1-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/carto/tar.gz/945f5efb74fd1af1f5e1f69f409f9567f94fb5a7"
|
||||
dependencies:
|
||||
@@ -842,9 +842,9 @@ graceful-fs@^4.1.2:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
|
||||
|
||||
grainstore@1.7.0:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.7.0.tgz#28d78895c82e6201f7d0ff63af1056f3c0fda0d3"
|
||||
grainstore@~1.8.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/grainstore/-/grainstore-1.8.0.tgz#9398729df88f3aecb55ffbb415d541dcca4420af"
|
||||
dependencies:
|
||||
carto "0.16.3"
|
||||
debug "~3.1.0"
|
||||
@@ -1380,8 +1380,8 @@ mocha@~3.4.1:
|
||||
supports-color "3.1.2"
|
||||
|
||||
moment@^2.10.6:
|
||||
version "2.19.4"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.4.tgz#17e5e2c6ead8819c8ecfad83a0acccb312e94682"
|
||||
version "2.20.1"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd"
|
||||
|
||||
moment@~2.18.1:
|
||||
version "2.18.1"
|
||||
@@ -2223,7 +2223,7 @@ through@2:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
|
||||
tilelive-bridge@cartodb/tilelive-bridge#2.3.1-cdb4:
|
||||
"tilelive-bridge@github:cartodb/tilelive-bridge#2.3.1-cdb4":
|
||||
version "2.3.1-cdb4"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-bridge/tar.gz/faa2b638da2d119b78281575d40255cb523f6ca6"
|
||||
dependencies:
|
||||
@@ -2231,7 +2231,7 @@ tilelive-bridge@cartodb/tilelive-bridge#2.3.1-cdb4:
|
||||
mapnik-pool "~0.1.3"
|
||||
sphericalmercator "1.0.x"
|
||||
|
||||
tilelive-mapnik@cartodb/tilelive-mapnik#0.6.18-cdb3:
|
||||
"tilelive-mapnik@github:cartodb/tilelive-mapnik#0.6.18-cdb3":
|
||||
version "0.6.18-cdb3"
|
||||
resolved "https://codeload.github.com/cartodb/tilelive-mapnik/tar.gz/23bd1c31dd57d0b76c86b9f1eaf62462b3c17d01"
|
||||
dependencies:
|
||||
@@ -2392,9 +2392,9 @@ window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
||||
windshaft@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.1.0.tgz#dc17c8369570c305171d1ab5ca130369bba04d58"
|
||||
windshaft@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/windshaft/-/windshaft-4.2.0.tgz#6a0409832a0d3bccfa09a88a8ab8288686b6762d"
|
||||
dependencies:
|
||||
abaculus cartodb/abaculus#2.0.3-cdb1
|
||||
canvas cartodb/node-canvas#1.6.2-cdb2
|
||||
@@ -2402,7 +2402,7 @@ windshaft@4.1.0:
|
||||
cartodb-psql "^0.10.1"
|
||||
debug "^3.1.0"
|
||||
dot "~1.0.2"
|
||||
grainstore "1.7.0"
|
||||
grainstore "~1.8.0"
|
||||
mapnik "3.5.14"
|
||||
queue-async "~1.0.7"
|
||||
redis-mpool "0.4.1"
|
||||
|
||||
Reference in New Issue
Block a user