Merge branch 'master' into overviews-work

This commit is contained in:
Javier Goizueta 2016-02-09 17:14:08 +01:00
commit 5b50e784cd
21 changed files with 1396 additions and 1048 deletions

View File

@ -11,6 +11,7 @@ addons:
- libgif-dev
before_install:
- npm install -g npm@2
- createdb template_postgis
- psql -c "CREATE EXTENSION postgis" template_postgis

40
NEWS.md
View File

@ -1,10 +1,34 @@
# Changelog
## 2.20.1
## 2.22.1
Released 2016-mm-dd
## 2.22.0
Released 2016-02-08
Announcements:
- Upgrades windshaft to [1.8.3](https://github.com/CartoDB/Windshaft/releases/tag/1.8.3)
## 2.21.1
Released 2016-02-05
Bug fixes:
- Added default config for geojson renderer
## 2.21.0
Released 2016-02-04
Announcements:
- Upgrades windshaft to [1.8.2](https://github.com/CartoDB/Windshaft/releases/tag/1.8.2)
## 2.20.0
Released 2016-01-20
@ -824,7 +848,7 @@ Released 2014-03-10
Enhancements:
- Set statsd prefix for all endpoints
- Set statsd prefix for all endpoints
- Respond with a permission denied on attempt to access map tiles waiving
signature of someone who had not left any (#170)
- Do not log an error on GET / (#177)
@ -864,7 +888,7 @@ Released 2014-02-27
Enhancements:
- Upgrades windshaft to 0.19.1 with many performance improvements,
See node_modules/windshaft/NEWS
See node_modules/windshaft/NEWS
- Improve speed of instanciating a map (#147, #159, #165)
- Give meaningful error on attempts to use map tokens
with attribute service (#156)
@ -953,7 +977,7 @@ Bug fixes:
Released 2014-01-30
Bug fixes:
Bug fixes:
* layergroup accept both map_key and api_key (#91)
* Fix public instanciation of signed template accessing private data (#114)
@ -1076,7 +1100,7 @@ Released 2013-10-03
"[ zoom > 3]" CartoCSS snippets (note the space)
* Fix backward compatibility handling of sqlapi.host configuration (#82)
* Fix error for invalid text-name in CartoCSS (#81)
* Do not let anonymous requests use authorized renderer caches
* Do not let anonymous requests use authorized renderer caches
## 1.3.4
@ -1123,7 +1147,7 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
* Multilayer API changes
* Layers passed by index in grid fetching url
* Interactivity only specified in layergroup config
* Embed cache_buster within token
* Embed cache_buster within token
* Use ISO format for last_modified timestamp
* Expected LZMA encoding changed to base64
@ -1177,7 +1201,7 @@ Released DD//MM//YY
Released DD//MM//YY
* Reduce default extent to allow for consistent proj4 round-tripping
* Reduce default extent to allow for consistent proj4 round-tripping
* Enhance reset_styles script to use full configuration (#62)
* Have reset_styles script also drop extended keys (#58)
* Fix example postgis parameter for simplifying input geoms (#63)
@ -1227,7 +1251,7 @@ Released (30/10/12)
* Autodetect target mapnik version and let config override it
* Add tools/reset_styles script to batch-reset (and optionally convert) styles
* Configurable logging format (#4)
* Detailed error on missing user metadata
* Detailed error on missing user metadata
* Properly handle unauthenticated requests for metadata
* Accept "api_key" in addition to "map_key",
both in query_string and POST body (#38)

View File

@ -13,7 +13,7 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
@ -33,7 +33,7 @@ var config = {
// to be able to navigate the map without a reload ?
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
@ -147,7 +147,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache.
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@ -232,7 +249,7 @@ var config = {
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:true
// Settings for the health check available at /health
@ -250,7 +267,10 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerMetadata: true
}
};

View File

@ -13,7 +13,7 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
@ -34,7 +34,7 @@ var config = {
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
@ -141,7 +141,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache.
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@ -226,7 +243,7 @@ var config = {
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:false
,serverMetadata: {
@ -250,7 +267,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerMetadata: false
}
};

View File

@ -13,7 +13,7 @@ var config = {
// Base URLs for the APIs
//
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
//
// Base url for the Templated Maps API
// "/api/v1/maps/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
@ -34,7 +34,7 @@ var config = {
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: true
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
@ -141,7 +141,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache.
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
}
},
http: {
timeout: 2000, // the timeout in ms for a http tile request
@ -226,7 +243,7 @@ var config = {
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:true
,serverMetadata: {
@ -250,7 +267,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerMetadata: true
}
};

View File

@ -13,7 +13,7 @@ var config = {
// Base URLs for the APIs
//
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
//
//
// Base url for the Templated Maps API
// "/api/v1/map/named" is the new API,
// "/tiles/template" is for compatibility with versions up to 1.6.x
@ -34,7 +34,7 @@ var config = {
// Defaults to 7200 (2 hours)
,mapConfigTTL: 7200
// idle socket timeout, in milliseconds
,socket_timeout: 600000
,socket_timeout: 600000
,enable_cors: true
,cache_enabled: false
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
@ -141,6 +141,22 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache.
cacheOnTimeout: true
},
geojson: {
dbPoolParams: {
// maximum number of resources to create at any given time
size: 16,
// max milliseconds a resource can go unused before it should be destroyed
idleTimeout: 3000,
// frequency to check for idle resources
reapInterval: 1000
},
// SQL queries will be wrapped with ST_ClipByBox2D
// Returning the portion of a geometry falling within a rectangle
// It will only work if snapToGrid is enabled
clipByBox2d: false // this requires postgis >=2.2 and geos >=3.5
}
},
http: {
@ -228,7 +244,7 @@ var config = {
serviceId: 'wadus_service_id'
}
// If useProfiler is true every response will be served with an
// X-Tiler-Profile header containing elapsed timing for various
// X-Tiler-Profile header containing elapsed timing for various
// steps taken for producing the response.
,useProfiler:true
// Settings for the health check available at /health
@ -246,7 +262,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
cdbQueryTablesFromPostgres: true
cdbQueryTablesFromPostgres: true,
// whether in mapconfig is available stats & metadata for each layer
layerMetadata: true
}
};

View File

@ -1,6 +1,6 @@
# Maps API
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles, using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
You can create two types of maps with the Maps API:

View File

@ -28,7 +28,7 @@ POST /api/v1/map
}
```
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md).
See [MapConfig File Formats](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for details.
#### Response

View File

@ -1,6 +1,6 @@
# Named Maps
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server, and the map is given a unique name. You can create named maps from private data, and users without an API Key can view your Named Map (while keeping your data private). The Named map workflow consists of making a call to your database, referencing a table, inserting your variables into the template where placeholders are defined, and creating custom queries.
The main two differences compared to anonymous maps are:
@ -12,7 +12,7 @@ The main two differences compared to anonymous maps are:
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
**Note:** There is a limit of 4,096 named maps allowed per account. If you need to create more Named Maps, it is recommended to use templates.
**Note:** There is a limit of 4,096 named maps allowed per account. If you need to create more Named maps, it is recommended to use templates.
## Create
@ -91,7 +91,7 @@ auth |
|_ method | `"token"` or `"open"` (the default if no `"method"` is given).
|_ valid_tokens | when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
placeholders | Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
layergroup | the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md) for more info.
layergroup | the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for more info.
view (optional) | extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
--- | ---
@ -437,7 +437,7 @@ curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?a
}
```
## Use with CartoDB.js
## Use CartoDB.js to Create Named Maps
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
```js
@ -462,3 +462,11 @@ cartodb.createLayer('map_dom_id',layerSource)
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
### Complete Examples of Named Maps created with CartoDB.js
- [Named map selectors with interaction](http://bl.ocks.org/ohasselblad/515a8af1f99d5e690484)
- [Named map with interactivity and config file used to create it](http://bl.ocks.org/ohasselblad/d1a45b8ff5e7bd90cd68)
- [Toggling sublayers in a Named Map](http://bl.ocks.org/ohasselblad/c1a0f4913610eec53cd3)

View File

@ -127,7 +127,7 @@ By manipulating the `"urlTemplate"` custom basemaps can be used in generating st
**CartoDB**
As described in the [Mapconfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
As described in the [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
```javascript
{

View File

@ -68,7 +68,16 @@ module.exports = {
statsInterval: rendererConfig.statsInterval
},
renderer: {
mapnik: rendererConfig.mapnik,
mapnik: _.defaults(rendererConfig.mapnik, {
geojson: {
dbPoolParams: {
size: 16,
idleTimeout: 3000,
reapInterval: 1000
},
clipByBox2d: false,
}
}),
torque: rendererConfig.torque,
http: rendererConfig.http
},

1233
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,13 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "2.20.1",
"version": "2.22.1",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
],
"url": "https://github.com/CartoDB/Windshaft-cartodb",
"licenses": [{
"type": "BSD",
"url": "https://github.com/CartoDB/Windshaft-cartodb/blob/master/LICENCE"
}],
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
@ -29,7 +26,7 @@
"node-statsd": "~0.0.7",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "1.7.0",
"windshaft": "1.8.3",
"step": "~0.0.6",
"queue-async": "~1.0.7",
"request": "~2.62.0",
@ -56,6 +53,6 @@
},
"engines": {
"node": ">=0.8 <0.11",
"npm": ">=1.2.1"
"npm": ">=2.14.16"
}
}

View File

@ -400,12 +400,11 @@ describe('multilayer', function() {
function jsonp_test(body) {
assert.ok(body.layergroupid);
expected_token = LayergroupToken.parse(body.layergroupid).token;
assert.deepEqual(body.metadata, {
layers: [
{ type: "mapnik", "meta":{} },
{ type: "mapnik", "meta":{} }
]
});
assert.ok(body.metadata.layers.length === 2);
assert.ok(body.metadata.layers[0].type === 'mapnik');
assert.ok(body.metadata.layers[0].meta);
assert.ok(body.metadata.layers[1].type === 'mapnik');
assert.ok(body.metadata.layers[1].meta);
didRunJsonCallback = true;
}
eval(res.body);
@ -1278,4 +1277,3 @@ describe('multilayer', function() {
});
});

View File

@ -374,4 +374,3 @@ describe('multilayer interactivity and layers order', function() {
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
});

View File

@ -1,479 +0,0 @@
var assert = require('../support/assert');
var step = require('step');
var qs = require('querystring');
var helper = require(__dirname + '/../support/test_helper');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
server.setMaxListeners(0);
describe('widgets', function() {
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
helper.deleteRedisKeys(keysToDelete, done);
});
function getWidget(mapConfig, widgetName, params, callback) {
if (!callback) {
callback = params;
params = {};
}
var url = '/api/v1/map';
if (params && params.filters) {
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
}
var layergroupId;
step(
function createLayergroup() {
var next = this;
assert.response(server,
{
url: url,
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
},
data: JSON.stringify(mapConfig)
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return next(err);
}
var parsedBody = JSON.parse(res.body);
var expectedWidgetURLS = {
http: "/api/v1/map/" + parsedBody.layergroupid + "/0/widget/" + widgetName
};
assert.ok(parsedBody.metadata.layers[0].widgets[widgetName]);
assert.ok(
parsedBody.metadata.layers[0].widgets[widgetName].url.http.match(expectedWidgetURLS.http)
);
return next(null, parsedBody.layergroupid);
}
);
},
function getWidgetResult(err, _layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
var urlParams = {
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
};
if (params && params.bbox) {
urlParams.bbox = params.bbox;
}
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '?' + qs.stringify(urlParams);
assert.response(server,
{
url: url,
method: 'GET',
headers: {
host: 'localhost'
}
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return next(err);
}
next(null, res);
}
);
},
function finish(err, res) {
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return callback(err, res);
}
);
}
it("should expose layer list", function(done) {
var listWidgetMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
names: {
type: 'list',
options: {
columns: ['name']
}
}
}
}
}
]
};
getWidget(listWidgetMapConfig, 'names', function(err, res) {
if (err) {
return done(err);
}
var expectedList = [
{name:"Hawai"},
{name:"El Estocolmo"},
{name:"El Rey del Tallarín"},
{name:"El Lacón"},
{name:"El Pico"}
];
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
done();
});
});
it("should expose layer histogram", function(done) {
var histogramMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
pop_max: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
getWidget(histogramMapConfig, 'pop_max', function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
assert.ok(histogram.bins.length);
done();
});
});
describe('filters', function() {
describe('category', function() {
var aggregationMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_count: {
type: 'aggregation',
options: {
column: 'adm0_a3',
aggregation: 'count'
}
}
}
}
}
]
};
it("should expose an aggregation", function(done) {
getWidget(aggregationMapConfig, 'country_places_count', { own_filter: 0 }, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
assert.equal(aggregation.categories.length, 6);
assert.deepEqual(aggregation.categories[0], { value: 769, category: 'USA', agg: false });
done();
});
});
it("should expose a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{country_places_count: {accept: ['CAN']}}
]
}
};
getWidget(aggregationMapConfig, 'country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
assert.equal(aggregation.categories.length, 1);
assert.deepEqual(aggregation.categories[0], { value: 256, category: 'CAN', agg: false });
done();
});
});
});
describe('range', function() {
var histogramMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_histogram: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
it("should expose an histogram", function(done) {
getWidget(histogramMapConfig, 'country_places_histogram', { own_filter: 0 }, function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
// notice min value
assert.deepEqual(
histogram.bins[0],
{ bin: 0, freq: 6497, min: 0, max: 742572, avg: 113511.16823149147 }
);
done();
});
});
it("should expose a filtered histogram", function(done) {
var params = {
filters: {
layers: [
{
country_places_histogram: { min: 4000000 }
}
]
}
};
getWidget(histogramMapConfig, 'country_places_histogram', params, function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
// notice min value
assert.deepEqual(histogram.bins[0], {
bin: 0,
freq: 62,
min: 4000000,
max: 9276403,
avg: 5815009.596774193
});
done();
});
});
});
describe('combine widget filters', function() {
var combinedWidgetsMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_count: {
type: 'aggregation',
options: {
column: 'adm0_a3',
aggregation: 'count'
}
},
country_places_histogram: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
it("should expose a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['CHN'] }
}
]
}
};
getWidget(combinedWidgetsMapConfig, 'country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 769, category: "USA", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
done();
});
});
it("should expose a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['CHN'] },
country_places_histogram: { min: 7000000 }
}
]
}
};
getWidget(combinedWidgetsMapConfig, 'country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 4, category: 'IND', agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
done();
});
});
it("should allow to filter by bounding box a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_histogram: { min: 50000 }
}
]
},
bbox: '-20,0,45,60'
};
getWidget(combinedWidgetsMapConfig, 'country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 96, category: "RUS", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
done();
});
});
it("should allow to filter by bounding box a filtered aggregation, with reject", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['RUS'] },
country_places_histogram: { min: 50000 }
}
]
},
bbox: '-20,0,45,60'
};
getWidget(combinedWidgetsMapConfig, 'country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 77, category: "TUR", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
done();
});
});
});
});
});

View File

@ -0,0 +1,74 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('aggregation widgets', function() {
var aggregationMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_count: {
type: 'aggregation',
options: {
column: 'adm0_a3',
aggregation: 'count'
}
}
}
}
}
]
};
it("should expose an aggregation", function(done) {
var testClient = new TestClient(aggregationMapConfig);
testClient.getWidget('country_places_count', { own_filter: 0 }, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
assert.equal(aggregation.categories.length, 6);
assert.deepEqual(aggregation.categories[0], { value: 769, category: 'USA', agg: false });
testClient.drain(done);
});
});
describe('filters', function() {
describe('category', function () {
it("should expose a filtered aggregation", function (done) {
var params = {
filters: {
layers: [
{country_places_count: {accept: ['CAN']}}
]
}
};
var testClient = new TestClient(aggregationMapConfig);
testClient.getWidget('country_places_count', params, function (err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
assert.equal(aggregation.categories.length, 1);
assert.deepEqual(aggregation.categories[0], { value: 256, category: 'CAN', agg: false });
testClient.drain(done);
});
});
});
});
});

View File

@ -0,0 +1,120 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('histogram widgets', function() {
it("should expose layer histogram", function(done) {
var histogramMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
pop_max: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
var testClient = new TestClient(histogramMapConfig);
testClient.getWidget('pop_max', function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
assert.ok(histogram.bins.length);
testClient.drain(done);
});
});
describe('filters', function() {
describe('range', function() {
var histogramMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_histogram: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
it("should expose an histogram", function(done) {
var testClient = new TestClient(histogramMapConfig);
testClient.getWidget('country_places_histogram', { own_filter: 0 }, function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
// notice min value
assert.deepEqual(
histogram.bins[0],
{ bin: 0, freq: 6497, min: 0, max: 742572, avg: 113511.16823149147 }
);
testClient.drain(done);
});
});
it("should expose a filtered histogram", function(done) {
var params = {
filters: {
layers: [
{
country_places_histogram: { min: 4000000 }
}
]
}
};
var testClient = new TestClient(histogramMapConfig);
testClient.getWidget('country_places_histogram', params, function(err, res) {
if (err) {
return done(err);
}
var histogram = JSON.parse(res.body);
// notice min value
assert.deepEqual(histogram.bins[0], {
bin: 0,
freq: 62,
min: 4000000,
max: 9276403,
avg: 5815009.596774193
});
testClient.drain(done);
});
});
});
});
});

View File

@ -0,0 +1,51 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('list widgets', function() {
it("should expose layer list", function(done) {
var listWidgetMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
names: {
type: 'list',
options: {
columns: ['name']
}
}
}
}
}
]
};
var testClient = new TestClient(listWidgetMapConfig);
testClient.getWidget('names', function(err, res) {
if (err) {
return done(err);
}
var expectedList = [
{name:"Hawai"},
{name:"El Estocolmo"},
{name:"El Rey del Tallarín"},
{name:"El Lacón"},
{name:"El Pico"}
];
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
testClient.drain(done);
});
});
});

View File

@ -0,0 +1,162 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('widget filters', function() {
describe('combine widget filters', function() {
var combinedWidgetsMapConfig = {
version: '1.5.0',
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from populated_places_simple_reduced',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
widgets: {
country_places_count: {
type: 'aggregation',
options: {
column: 'adm0_a3',
aggregation: 'count'
}
},
country_places_histogram: {
type: 'histogram',
options: {
column: 'pop_max'
}
}
}
}
}
]
};
it("should expose a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['CHN'] }
}
]
}
};
var testClient = new TestClient(combinedWidgetsMapConfig);
testClient.getWidget('country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 769, category: "USA", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
testClient.drain(done);
});
});
it("should expose a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['CHN'] },
country_places_histogram: { min: 7000000 }
}
]
}
};
var testClient = new TestClient(combinedWidgetsMapConfig);
testClient.getWidget('country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 4, category: 'IND', agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
testClient.drain(done);
});
});
it("should allow to filter by bounding box a filtered aggregation", function(done) {
var params = {
filters: {
layers: [
{
country_places_histogram: { min: 50000 }
}
]
},
bbox: '-20,0,45,60'
};
var testClient = new TestClient(combinedWidgetsMapConfig);
testClient.getWidget('country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 96, category: "RUS", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
testClient.drain(done);
});
});
it("should allow to filter by bounding box a filtered aggregation, with reject", function(done) {
var params = {
filters: {
layers: [
{
country_places_count: { reject: ['RUS'] },
country_places_histogram: { min: 50000 }
}
]
},
bbox: '-20,0,45,60'
};
var testClient = new TestClient(combinedWidgetsMapConfig);
testClient.getWidget('country_places_count', params, function(err, res) {
if (err) {
return done(err);
}
var aggregation = JSON.parse(res.body);
// first one would be CHN if reject filter wasn't applied
assert.deepEqual(aggregation.categories[0], { value: 77, category: "TUR", agg: false });
// confirm 'CHN' was filtered out (reject)
assert.equal(aggregation.categories.reduce(function(sum, row) {
return sum + (row.category === 'CHN' ? 1 : 0);
}, 0), 0);
testClient.drain(done);
});
});
});
});

119
test/support/test-client.js Normal file
View File

@ -0,0 +1,119 @@
'use strict';
var qs = require('querystring');
var step = require('step');
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
var assert = require('./assert');
var helper = require('./test_helper');
var CartodbWindshaft = require('../../lib/cartodb/server');
var serverOptions = require('../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
function TestClient(mapConfig) {
this.mapConfig = mapConfig;
this.keysToDelete = {};
}
module.exports = TestClient;
TestClient.prototype.getWidget = function(widgetName, params, callback) {
var self = this;
if (!callback) {
callback = params;
params = {};
}
var url = '/api/v1/map';
if (params && params.filters) {
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
}
var layergroupId;
step(
function createLayergroup() {
var next = this;
assert.response(server,
{
url: url,
method: 'POST',
headers: {
host: 'localhost',
'Content-Type': 'application/json'
},
data: JSON.stringify(self.mapConfig)
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return next(err);
}
var parsedBody = JSON.parse(res.body);
var expectedWidgetURLS = {
http: "/api/v1/map/" + parsedBody.layergroupid + "/0/widget/" + widgetName
};
assert.ok(parsedBody.metadata.layers[0].widgets[widgetName]);
assert.ok(
parsedBody.metadata.layers[0].widgets[widgetName].url.http.match(expectedWidgetURLS.http)
);
return next(null, parsedBody.layergroupid);
}
);
},
function getWidgetResult(err, _layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
var urlParams = {
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
};
if (params && params.bbox) {
urlParams.bbox = params.bbox;
}
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '?' + qs.stringify(urlParams);
assert.response(server,
{
url: url,
method: 'GET',
headers: {
host: 'localhost'
}
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return next(err);
}
next(null, res);
}
);
},
function finish(err, res) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
return callback(err, res);
}
);
};
TestClient.prototype.drain = function(callback) {
helper.deleteRedisKeys(this.keysToDelete, callback);
};