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 - libgif-dev
before_install: before_install:
- npm install -g npm@2
- createdb template_postgis - createdb template_postgis
- psql -c "CREATE EXTENSION postgis" template_postgis - psql -c "CREATE EXTENSION postgis" template_postgis

26
NEWS.md
View File

@ -1,10 +1,34 @@
# Changelog # Changelog
## 2.20.1 ## 2.22.1
Released 2016-mm-dd 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 ## 2.20.0
Released 2016-01-20 Released 2016-01-20

View File

@ -147,7 +147,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache. // internal cache.
cacheOnTimeout: true 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: { http: {
timeout: 2000, // the timeout in ms for a http tile request timeout: 2000, // the timeout in ms for a http tile request
@ -250,7 +267,10 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default. // whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true, onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API // 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

@ -141,7 +141,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache. // internal cache.
cacheOnTimeout: true 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: { http: {
timeout: 2000, // the timeout in ms for a http tile request timeout: 2000, // the timeout in ms for a http tile request
@ -250,7 +267,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default. // whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true, onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API // 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

@ -141,7 +141,24 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache. // internal cache.
cacheOnTimeout: true 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: { http: {
timeout: 2000, // the timeout in ms for a http tile request timeout: 2000, // the timeout in ms for a http tile request
@ -250,7 +267,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default. // whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true, onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API // 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

@ -141,6 +141,22 @@ var config = {
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the // memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
// internal cache. // internal cache.
cacheOnTimeout: true 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: { http: {
@ -246,7 +262,9 @@ var config = {
// whether it should intercept tile render errors an act based on them, enabled by default. // whether it should intercept tile render errors an act based on them, enabled by default.
onTileErrorStrategy: true, onTileErrorStrategy: true,
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API // 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 # 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: 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 #### Response

View File

@ -1,6 +1,6 @@
# Named Maps # 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: 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). 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 ## Create
@ -91,7 +91,7 @@ auth |
|_ method | `"token"` or `"open"` (the default if no `"method"` is given). |_ 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. |_ 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. 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. 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. 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 ```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 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 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** **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 ```javascript
{ {

View File

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

1217
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,13 @@
{ {
"private": true, "private": true,
"name": "windshaft-cartodb", "name": "windshaft-cartodb",
"version": "2.20.1", "version": "2.22.1",
"description": "A map tile server for CartoDB", "description": "A map tile server for CartoDB",
"keywords": [ "keywords": [
"cartodb" "cartodb"
], ],
"url": "https://github.com/CartoDB/Windshaft-cartodb", "url": "https://github.com/CartoDB/Windshaft-cartodb",
"licenses": [{ "license": "BSD-3-Clause",
"type": "BSD",
"url": "https://github.com/CartoDB/Windshaft-cartodb/blob/master/LICENCE"
}],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/CartoDB/Windshaft-cartodb.git" "url": "git://github.com/CartoDB/Windshaft-cartodb.git"
@ -29,7 +26,7 @@
"node-statsd": "~0.0.7", "node-statsd": "~0.0.7",
"underscore" : "~1.6.0", "underscore" : "~1.6.0",
"dot": "~1.0.2", "dot": "~1.0.2",
"windshaft": "1.7.0", "windshaft": "1.8.3",
"step": "~0.0.6", "step": "~0.0.6",
"queue-async": "~1.0.7", "queue-async": "~1.0.7",
"request": "~2.62.0", "request": "~2.62.0",
@ -56,6 +53,6 @@
}, },
"engines": { "engines": {
"node": ">=0.8 <0.11", "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) { function jsonp_test(body) {
assert.ok(body.layergroupid); assert.ok(body.layergroupid);
expected_token = LayergroupToken.parse(body.layergroupid).token; expected_token = LayergroupToken.parse(body.layergroupid).token;
assert.deepEqual(body.metadata, { assert.ok(body.metadata.layers.length === 2);
layers: [ assert.ok(body.metadata.layers[0].type === 'mapnik');
{ type: "mapnik", "meta":{} }, assert.ok(body.metadata.layers[0].meta);
{ type: "mapnik", "meta":{} } assert.ok(body.metadata.layers[1].type === 'mapnik');
] assert.ok(body.metadata.layers[1].meta);
});
didRunJsonCallback = true; didRunJsonCallback = true;
} }
eval(res.body); 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); 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);
};