Compare commits

...

112 Commits

Author SHA1 Message Date
Simon Martín
b4d45da0de 2.43.1 fix pg vulnerability (#738)
* Adding a new cartodb-psql fixing a pg vulnerability
* Adding some changes in travis
2017-09-05 12:16:02 +02:00
Javier Goizueta
289ffbbedc Release 2.43.1 2016-05-19 12:33:41 +02:00
Javier Goizueta
e0f0751b28 Merge pull request #458 from CartoDB/457-bbox-queryrewrite
Fix dataview bbox bug when no query rewrite data exists
2016-05-19 12:29:17 +02:00
Javier Goizueta
2a06405a58 Move definition to the scope where it's needed 2016-05-18 18:21:17 +02:00
Javier Goizueta
9206b1a1b5 Fix dataviews/overviews tests and add some new cases 2016-05-18 18:16:32 +02:00
Javier Goizueta
5989ab344d Add test to detect problem #457 2016-05-18 18:02:08 +02:00
Javier Goizueta
a1e024e228 Fix dataview problem for bbox with no query rewrite data
Fixes #457
2016-05-18 17:49:09 +02:00
Javier Goizueta
8628d3b671 Stub next version 2016-05-18 16:15:27 +02:00
Javier Goizueta
7ce4104d2f Release version 2.43.0 2016-05-18 16:11:30 +02:00
Javier Goizueta
a26a5d6f5a Merge pull request #449 from CartoDB/overviews-widgets-2
Use overviews for widgets (dataviews, filtered queries)
2016-05-18 16:08:17 +02:00
Javier Goizueta
e98a5aeff0 Small code clean-up 2016-05-18 15:48:30 +02:00
Javier Goizueta
4c375780c7 replace underscore functions by standard (ES5) equivalents
Note: _.find(a,...) is not replaced by a.find(...)
because it is not available for all the collections
we need it for.
2016-05-18 15:43:20 +02:00
Javier Goizueta
48415fb1f3 Merge branch 'master' into overviews-widgets-2 2016-05-18 13:58:55 +02:00
Javier Goizueta
8da7cf73c1 Remove comment 2016-05-18 13:55:09 +02:00
Javier Goizueta
ba30f460ee Remove comment
Overviews will not be used for dataview search
2016-05-18 13:42:58 +02:00
Javier Goizueta
e1aa0bc7ae Use JSON format for EXPLAIN 2016-05-18 13:09:55 +02:00
Javier Goizueta
3987e83b7a Add tests for query rewriter with filters 2016-05-18 12:34:51 +02:00
Javier Goizueta
858d976637 Add tests for query rewriter using specific zoom level 2016-05-18 11:53:30 +02:00
Javier Goizueta
48d2978997 Test filters query rewrite data 2016-05-18 11:45:14 +02:00
Javier Goizueta
1872fbd021 Add test cases for dataview formulae
Check the overriden (sum,avg,count) and non-overriden (min, max) cases.
2016-05-18 10:48:13 +02:00
Javier Goizueta
bbb1b4a7b9 Add tests missing file 2016-05-18 08:11:52 +02:00
Javier Goizueta
aa0ddaae95 Remove comment 2016-05-18 08:07:48 +02:00
Javier Goizueta
cb3706e5cf Update Query Rewriter comments 2016-05-18 08:04:11 +02:00
Javier Goizueta
3d8f6576aa Implement category and range filters 2016-05-18 07:48:11 +02:00
Javier Goizueta
24f7bc6596 Add tests for dataviews with overviews 2016-05-18 07:47:30 +02:00
Javier Goizueta
7a6b1ec871 Fix tests for MapConfigOverviewsAdaptar changes 2016-05-17 16:01:10 +02:00
Raul Ochoa
cfdac1bcb0 Stubs next version 2016-05-17 15:46:21 +02:00
Javier Goizueta
42ef40282b 💄 shorten long lines 2016-05-17 15:46:13 +02:00
Raul Ochoa
25da6e779c Release 2.42.2 2016-05-17 15:45:21 +02:00
Javier Goizueta
7f7204df6c Add filter stats information to query rewriter data 2016-05-17 15:41:31 +02:00
Javier Goizueta
3c6d930434 Fix bug 2016-05-17 15:39:32 +02:00
Raul Ochoa
b5540fc63a Regenerate npm-shrinkwrap.json after a fresh npm --no-shrinkwrap 2016-05-17 15:24:55 +02:00
Raul Ochoa
f6f58a71b3 Merge remote-tracking branch 'origin/turbo-carto-substitution-tokens'
Conflicts:
	NEWS.md
2016-05-17 15:15:07 +02:00
Francisco Dans
3a7361a009 stubs next version 2016-05-17 15:05:16 +02:00
Raul Ochoa
420f4aacc9 Update news 2016-05-17 15:04:15 +02:00
Francisco Dans
163c10b66e upgrades turbo-carto 2016-05-17 15:03:48 +02:00
Raul Ochoa
8fb35571fe Adds support for mapnik substitution token at turbo-carto level
Goes green and fixes #455
2016-05-17 15:00:18 +02:00
Raul Ochoa
91f39abc69 Going red for #455 2016-05-17 14:59:21 +02:00
Javier Goizueta
df63fbbd04 Refactor filter application into own model
This also avoids storing an object in the overviews query rewriter
for the bbox filter (a plain data structure is used instead).
2016-05-17 13:55:00 +02:00
Raul Ochoa
5b96576227 Stubs next version 2016-05-16 20:08:39 +02:00
Raul Ochoa
9f18e2d27d Release 2.42.0 2016-05-16 19:58:22 +02:00
Raul Ochoa
1ac6ead4b2 Update news 2016-05-16 19:57:51 +02:00
Javier Goizueta
9d82e8c27c Use bounding box of dataviews to select overviews level 2016-05-13 20:47:36 +02:00
Javier Goizueta
224eb392ba Add overviews-dependent dataviews behaviour
Now QueryRewriter is used in dataview objects they can decide
whether overviews are applicable, have the oportunity to
adapt queries for overviews, etc.
This is done by having overviews-related behaviour in models/dataview/overviews
and falling back to the regular models/dataview.
2016-05-13 18:46:58 +02:00
Raul Ochoa
8f51418d84 Merge pull request #453 from CartoDB/issue-450
Fix named maps with analysis
2016-05-13 17:13:29 +02:00
Raul Ochoa
c12e5f7a27 Fix named maps with analysis
Named map provider was missing analysis backend dependency

Fixes #450
2016-05-13 16:57:27 +02:00
Raul Ochoa
1c2354dc49 Merge pull request #452 from CartoDB/turbo-carto-split-strategy
Use split strategy for head/tails turbo-carto quantification
2016-05-13 13:20:12 +02:00
Raul Ochoa
2e26e2e126 Use split strategy for head/tails turbo-carto quantification 2016-05-13 12:57:43 +02:00
Raul Ochoa
94639f7e0c Merge pull request #451 from CartoDB/turbo-carto-errors
Improve turbo-carto related errors
2016-05-13 12:54:17 +02:00
Raul Ochoa
f3957b4fce Fix test expectations for turbo-carto errors 2016-05-13 12:42:18 +02:00
Raul Ochoa
61765d20e1 Fail on turbo-carto specific errors
This will try to fallback on postcss errors so it still targets
carto parser in those cases.

Closes #434
2016-05-13 12:10:05 +02:00
Raul Ochoa
503636f9fb Upgrade to turbo-carto 0.9.0 2016-05-13 12:09:11 +02:00
Raul Ochoa
4abadec9c4 Use the more suitable getLayergroup to validate regression 2016-05-13 00:49:09 +02:00
Javier Goizueta
b574489950 Refactor to reduce cyclomatic complexity 2016-05-12 18:47:24 +02:00
Javier Goizueta
85788f42a6 Adapt QueryRewriter to new requirements 2016-05-12 18:30:10 +02:00
Javier Goizueta
5fb7f07498 Prevent problems with missing layers in mapconfig 2016-05-12 18:29:30 +02:00
Javier Goizueta
fd44b62f26 Fix tests for new MapConfigOverviewsAdapter interface 2016-05-12 17:52:39 +02:00
Javier Goizueta
3300c095ed Merge branch 'master' into overviews-widgets-2 2016-05-12 17:37:24 +02:00
Javier Goizueta
55cf0a8447 Fix typo 2016-05-12 16:43:09 +02:00
Javier Goizueta
64a87690ee 💄 Fix line lengths, etc. 2016-05-12 16:20:34 +02:00
Javier Goizueta
3890014250 Fix QueryRewriter use
QueryRewriter should be passed the query that would be used otherwise.
If QR cannot handle it, it will be returned unmodified.
So QR must be used when a query has been prepared and the result
of QR should be used to replace it.
2016-05-12 10:25:09 +02:00
Raul Ochoa
7c2924ae14 Merge pull request #448 from CartoDB/upgrade-turbo-carto
Upgrades turbo-carto to 0.8.0
2016-05-11 20:09:41 +02:00
Raul Ochoa
bfdaf67a9b Upgrades turbo-carto to 0.8.0 2016-05-11 19:57:05 +02:00
Javier Goizueta
65612f0109 Add filters information at map instantion time to the query rewriter data 2016-05-11 19:24:13 +02:00
Raul Ochoa
e0ade85565 Stubs next version 2016-05-11 18:53:17 +02:00
Raul Ochoa
c5afc0dc94 Release 2.41.1 2016-05-11 18:51:05 +02:00
Raul Ochoa
a7e00c5856 Merge pull request #447 from CartoDB/upgrade-camshaft
Upgrades camshaft to 0.8.0
2016-05-11 18:50:14 +02:00
Raul Ochoa
2482accb42 Upgrades camshaft to 0.8.0 2016-05-11 18:39:07 +02:00
Raul Ochoa
3e4f71d873 Nicer error message when missing sql from layer options
Fixes #446
2016-05-11 18:24:47 +02:00
Javier Goizueta
fa19f90a6a Apply overviews query rewriter to dataviews
This requires the QueryRewriter to handle a filters parametes in
its data (with Camshaft filter definitions) and a final
options parameters with a bounding_box parameter.
2016-05-11 18:18:22 +02:00
Raul Ochoa
bbadd46766 Stubs next version 2016-05-11 16:47:33 +02:00
Raul Ochoa
b1f618a98e Release 2.41.0 2016-05-11 16:46:57 +02:00
Raul Ochoa
cdf3fe3a25 Bumps version 2016-05-11 16:45:36 +02:00
Raul Ochoa
1440841ac8 Merge pull request #445 from CartoDB/upgrade-camshaft
Upgrade camshaft to 0.7.0
2016-05-11 16:44:07 +02:00
Raul Ochoa
fc57fd2638 Upgrade camshaft to 0.7.0 2016-05-11 16:25:42 +02:00
Raul Ochoa
776cb8d47a Stubs next version 2016-05-10 17:26:27 +02:00
Raul Ochoa
c36f52415e Release 2.40.0 2016-05-10 17:23:50 +02:00
Raul Ochoa
2ee2c5bb55 Update news and bump version 2016-05-10 17:22:51 +02:00
Raul Ochoa
dccb557cd7 Merge pull request #444 from CartoDB/optimize-source-type-query-usage
Use original query from source nodes
2016-05-10 17:19:04 +02:00
Raul Ochoa
4570d17ce1 Use original query from source nodes
Doing a st_transform doesn't make sense as we already should have
the_geom_webmercator column available
2016-05-10 17:09:36 +02:00
Raul Ochoa
b3b3abcdb8 Merge pull request #443 from CartoDB/override-static-previews-params
Allow override zoom+center or bbox for static named maps previews
2016-05-09 22:53:35 +02:00
Raul Ochoa
6639664b3f Allow override zoom+center or bbox for static named maps previews 2016-05-09 21:13:13 +02:00
Raul Ochoa
b99db7cb69 Merge pull request #441 from CartoDB/sql-wrap
Analysis layers can have a sql_wrap option to wrap node queries
2016-05-09 14:09:16 +02:00
Raul Ochoa
3e94e3288f Use sql as replacement variable 2016-05-06 17:09:41 +02:00
Raul Ochoa
1115f9fba2 Merge pull request #442 from CartoDB/remove-repeated-config
Added config parameter for ST_RemoveRepeatedPoints
2016-05-06 17:01:23 +02:00
Daniel García Aubert
24dde1e4d0 Added config parameter to windshaft in order to use postgres' function ST_RemoveRepeatedPoints (set false by the default)
Conflicts:
	lib/cartodb/server_options.js
2016-05-06 16:50:25 +02:00
Raul Ochoa
7d4caf6974 Analysis layers can have a sql_wrap option to wrap node queries 2016-05-06 16:37:52 +02:00
Raul Ochoa
e4ba68850c Stubs next version 2016-05-05 18:36:50 +02:00
csobier
e2154f6561 Merge pull request #435 from CartoDB/docs-add-named-map-torque-link
link to named map with a torque layer block page
2016-05-05 12:35:41 -04:00
Raul Ochoa
778860c81f Release 2.39.0 2016-05-05 18:09:44 +02:00
Raul Ochoa
ee3c56efba Update news 2016-05-05 18:09:15 +02:00
Raul Ochoa
5dc328724a Merge pull request #440 from CartoDB/node-status-cache-control-header
Use a more aggressive cache control header for node status endpoint
2016-05-05 18:07:16 +02:00
Raul Ochoa
c77ea49594 Use a more aggressive cache control header for node status endpoint 2016-05-05 17:52:37 +02:00
Raul Ochoa
73aa159b98 Upgrade istanbul 2016-05-05 16:06:52 +02:00
Raul Ochoa
b7d7cffb67 Reformat package.json with npm cli tool 2016-05-05 16:05:46 +02:00
Raul Ochoa
eba8db292c Merge pull request #439 from CartoDB/avoid-profiler-dots
Upgrades step-profiler to 0.3.0 to avoid dots in json keys
2016-05-05 15:41:30 +02:00
Raul Ochoa
d5391ef15b Merge remote-tracking branch 'origin/master' into avoid-profiler-dots
Conflicts:
	NEWS.md
2016-05-05 15:30:12 +02:00
Francisco Dans
685cbb1eec updates news 2016-05-05 14:34:19 +02:00
Francisco Dans
5842a239fd shrinkwrap 2016-05-05 14:32:57 +02:00
Francisco Dans
285363aa40 bumps current version and turbocarto's 2016-05-05 14:32:11 +02:00
Raul Ochoa
92e130d8de Upgrades step-profiler to 0.3.0 to avoid dots in json keys
Closes #438
2016-05-05 13:45:25 +02:00
Francisco Dans
f674f90eba Merge branch 'master' of github.com:CartoDB/Windshaft-cartodb 2016-05-05 12:28:01 +02:00
Francisco Dans
0542b65cbb shrinkwrap changes for 2.38.0 2016-05-05 12:26:12 +02:00
Francisco Dans
60fa94781e adds upgrade to NEWS 2016-05-05 12:25:12 +02:00
Raul Ochoa
38d57533c2 Discourage console usage, global.logger should be used when required 2016-05-05 12:18:22 +02:00
Raul Ochoa
e3d6da06a7 Remove console usage 2016-05-05 12:17:51 +02:00
Raul Ochoa
3af05bb734 Remove console usage 2016-05-05 12:17:33 +02:00
Francisco Dans
ffd58cc7ca bumps minor 2016-05-05 12:07:26 +02:00
Francisco Dans
e103c52d27 upgrades turbo carto 2016-05-05 12:04:14 +02:00
csobier
46a4defab7 added torque layer in named maps as a new section under the cartodb.js heading 2016-05-04 12:53:23 -04:00
Daniel García Aubert
00a71af95d Stubs next version 2016-05-03 17:58:47 +02:00
csobier
fd8be0352f link to named map with a torque layer block page 2016-05-03 11:38:57 -04:00
51 changed files with 4233 additions and 2356 deletions

View File

@@ -82,13 +82,14 @@
// "wsh" : false, // Windows Scripting Host
// "yui" : false, // Yahoo User Interface
// Custom Globals
"globals" : { // additional predefined global variables
"describe": true,
"before": true,
"after": true,
"beforeEach": true,
"afterEach": true,
"it": true
}
// Custom predefined global variables
"predef": [
"-console", // disallows console, use debug
"beforeEach",
"afterEach",
"before",
"after",
"describe",
"it"
]
}

View File

@@ -1,13 +1,18 @@
dist: precise
sudo: false
addons:
postgresql: "9.3"
apt:
packages:
- postgresql-9.3-postgis-2.1
- postgresql-plpython-9.3
- pkg-config
- libcairo2-dev
- libjpeg8-dev
- libgif-dev
- libpango1.0-dev
before_install:
- npm install -g npm@2

109
NEWS.md
View File

@@ -1,5 +1,114 @@
# Changelog
## 2.43.2
Released 2017-09-01
Bug fixes:
- Adding a new cartodb-psql fixing a pg vulnerability
## 2.43.1
Released 2016-05-19
Bug fixes:
- Dataview error when bbox present without query rewrite data #458
## 2.43.0
Released 2016-05-18
New features:
- Overviews now support dataviews and filtering #449
## 2.42.2
Released 2016-05-17
New features:
- turbo-carto: mapnik substitution tokens support #455
## 2.42.1
Released 2016-05-17
- Upgraded turbo-carto to fix reversed color scales
## 2.42.0
Released 2016-05-16
Bug fixes:
- Fix named maps with analysis #453
Enhancements:
- Use split strategy for head/tails turbo-carto quantification
Announcements:
- Upgrades turbo-carto to [0.9.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.9.0)
## 2.41.1
Released 2016-05-11
Announcements:
- Upgrades camshaft to [0.8.0](https://github.com/CartoDB/camshaft/releases/tag/0.8.0)
Bug fixes:
- Nicer error message when missing sql from layer options #446
## 2.41.0
Released 2016-05-11
Announcements:
- Upgrades camshaft to [0.7.0](https://github.com/CartoDB/camshaft/releases/tag/0.7.0)
## 2.40.0
Released 2016-05-10
Enhancements:
- Use original query from source nodes #444
New features:
- Allow override zoom+center or bbox for static named maps previews #443
- Analysis layers can have a sql_wrap option to wrap node queries #441
## 2.39.0
Released 2016-05-05
Announcements:
- Upgrades step-profiler to 0.3.0 to avoid dots in json keys #438
- Use a more aggressive cache control header for node status endpoint
## 2.38.1
Released 2016-05-05
Announcements:
- Fixes problem in turbo-carto dependency
- Removes console usages
## 2.38.0
Released 2016-05-05
Announcements:
- Upgrades turbo-carto to [0.7.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.7.0)
## 2.37.0
Released 2016-05-03

17
app.js
View File

@@ -14,6 +14,11 @@ if ( process.argv[2] ) {
ENVIRONMENT = 'development';
}
// jshint undef:false
var log = console.log.bind(console);
var logError = console.error.bind(console);
// jshint undef:true
var availableEnvironments = {
production: true,
staging: true,
@@ -22,8 +27,8 @@ var availableEnvironments = {
// sanity check
if (!availableEnvironments[ENVIRONMENT]){
console.error('node app.js [environment]');
console.error('environments: %s', Object.keys(availableEnvironments).join(', '));
logError('node app.js [environment]');
logError('environments: %s', Object.keys(availableEnvironments).join(', '));
process.exit(1);
}
@@ -58,10 +63,10 @@ if ( global.environment.log_filename ) {
// See cwd inlog4js.configure call below
logdir = path.resolve(__dirname, logdir);
if ( ! fs.existsSync(logdir) ) {
console.error("Log filename directory does not exist: " + logdir);
logError("Log filename directory does not exist: " + logdir);
process.exit(1);
}
console.log("Logs will be written to " + global.environment.log_filename);
log("Logs will be written to " + global.environment.log_filename);
log4js_config.appenders.push(
{ type: "file", filename: global.environment.log_filename }
);
@@ -94,7 +99,7 @@ var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, b
var version = require("./package").version;
listener.on('listening', function() {
console.log(
log(
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
);
@@ -111,7 +116,7 @@ process.on('SIGHUP', function() {
global.log4js.clearAndShutdownAppenders(function() {
global.log4js.configure(log4js_config);
global.logger = global.log4js.getLogger();
console.log('Log files reloaded');
log('Log files reloaded');
});
});

View File

@@ -163,6 +163,10 @@ var config = {
// 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
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
}
},

View File

@@ -157,6 +157,10 @@ var config = {
// 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
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
}
},

View File

@@ -157,6 +157,10 @@ var config = {
// 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
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
}
},

View File

@@ -156,7 +156,11 @@ var config = {
// 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
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
// geometries will be simplified using ST_RemoveRepeatedPoints
// which cost is no more expensive than snapping and results are
// much closer to the original geometry
removeRepeatedPoints: false // this requires postgis >=2.2
}
},
http: {

View File

@@ -451,6 +451,7 @@ callback({
```
## CartoDB.js for Named Maps
You can use a Named Map that you created (which is defined by its `name`), to create a map using CartoDB.js. This is achieved by adding the [`namedmap` type](http://docs.cartodb.com/cartodb-platform/cartodb-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
```javascript
@@ -478,16 +479,48 @@ You can use a Named Map that you created (which is defined by its `name`), to cr
}
}
```
**Note:** Instantiating a Named Map over a `createLayer` does not require an API Key and by default, does not include auth tokens. _If_ you defined auth tokens for the Named Map configuration, then you will have to include them.
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js/) has methods for accessing your Named Maps.
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
**Note:** The CartoDB.js `layer.setParams()` function is not supported when using Named Maps for Torque.
**Note:** The CartoDB.js `layer.setParams()` function is not supported when using Named Maps for Torque. Alternatively, you can create a [Torque layer in a Named Map](http://bl.ocks.org/iriberri/de37be6406f9cc7cfe5a)
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
### Torque Layer in a Named Map
If you are creating a Torque layer in a Named Map without using the Torque.js library, you can apply the Torque layer by applying the following code with CartoDBjs:
```javascript
// add cartodb layer with one sublayer
cartodb.createLayer(map, {
user_name: '{username}',
type: 'torque',
order: 1,
options: {
query: "",
table_name: "named_map_tutorial_table",
user_name: "{username}",
tile_style: 'Map { -torque-frame-count:512; -torque-animation-duration:10; -torque-time-attribute:"cartodb_id"; -torque-aggregation-function:"count(cartodb_id)"; -torque-resolution:2; -torque-data-aggregation:linear; } #named_map_tutorial_table_copy{ comp-op: lighter; marker-fill-opacity: 0.9; marker-line-color: #FFF; marker-line-width: 1.5; marker-line-opacity: 1; marker-type: ellipse; marker-width: 6; marker-fill: #FF9900; } #named_map_tutorial_table_copy[frame-offset=1] { marker-width:8; marker-fill-opacity:0.45; } #named_map_tutorial_table_copy[frame-offset=2] { marker-width:10; marker-fill-opacity:0.225; }'
},
named_map: {
name: "{namedmap_example}",
layers: [{
layer_name: "t"
}]
}
})
.addTo(map)
.done(function(layer) {
});
}
```
#### Examples of Named Maps created with CartoDB.js
- [Named Map selectors with interaction](http://bl.ocks.org/ohasselblad/515a8af1f99d5e690484)

View File

@@ -0,0 +1,59 @@
var _ = require('underscore');
var step = require('step');
var CamshaftFilter = require('../models/filter/camshaft');
function FilterStatsApi(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = FilterStatsApi;
function getEstimatedRows(pgQueryRunner, username, query, callback) {
pgQueryRunner.run(username, "EXPLAIN (FORMAT JSON)"+query, function(err, result_rows) {
if (err){
callback(err);
return;
}
var rows;
if ( result_rows[0] && result_rows[0]['QUERY PLAN'] &&
result_rows[0]['QUERY PLAN'][0] && result_rows[0]['QUERY PLAN'][0].Plan ) {
rows = result_rows[0]['QUERY PLAN'][0].Plan['Plan Rows'];
}
return callback(null, rows);
});
}
FilterStatsApi.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
var stats = {};
var self = this;
step(
function getUnfilteredRows() {
getEstimatedRows(self.pgQueryRunner, username, unfiltered_query, this);
},
function receiveUnfilteredRows(err, rows) {
if (err){
callback(err);
return;
}
stats.unfiltered_rows = rows;
this(null, rows);
},
function getFilteredRows() {
if ( filters && !_.isEmpty(filters)) {
var camshaftFilter = new CamshaftFilter(filters);
var query = camshaftFilter.sql(unfiltered_query);
getEstimatedRows(self.pgQueryRunner, username, query, this);
} else {
this(null, null);
}
},
function receiveFilteredRows(err, rows) {
if (err){
callback(err);
return;
}
stats.filtered_rows = rows;
callback(null, stats);
}
);
};

View File

@@ -1,24 +1,18 @@
var SubstitutionTokens = require('../utils/substitution-tokens');
function OverviewsMetadataApi(pgQueryRunner) {
this.pgQueryRunner = pgQueryRunner;
}
module.exports = OverviewsMetadataApi;
// TODO: share this with QueryTablesApi? ... or maintain independence?
var affectedTableRegexCache = {
bbox: /!bbox!/g,
scale_denominator: /!scale_denominator!/g,
pixel_width: /!pixel_width!/g,
pixel_height: /!pixel_height!/g
};
function prepareSql(sql) {
return sql
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
.replace(affectedTableRegexCache.scale_denominator, '0')
.replace(affectedTableRegexCache.pixel_width, '1')
.replace(affectedTableRegexCache.pixel_height, '1')
;
return sql && SubstitutionTokens.replace(sql, {
bbox: 'ST_MakeEnvelope(0,0,0,0)',
scale_denominator: '0',
pixel_width: '1',
pixel_height: '1'
});
}
OverviewsMetadataApi.prototype.getOverviewsMetadata = function (username, sql, callback) {

View File

@@ -8,7 +8,13 @@ var step = require('step');
var Timer = require('../stats/timer');
var BBoxFilter = require('../models/filter/bbox');
var DataviewFactory = require('../models/dataview/factory');
var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory');
var OverviewsQueryRewriter = require('../utils/overviews_query_rewriter');
var overviewsQueryRewriter = new OverviewsQueryRewriter({
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
});
function DataviewBackend(analysisBackend) {
this.analysisBackend = analysisBackend;
@@ -16,7 +22,6 @@ function DataviewBackend(analysisBackend) {
module.exports = DataviewBackend;
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
var self = this;
@@ -101,6 +106,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
ownFilter = !!ownFilter;
var query;
if (ownFilter) {
query = node.getQuery();
} else {
@@ -109,11 +115,48 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
query = node.getQuery(applyFilters);
}
var sourceId = dataviewDefinition.source.id; // node.id
var layer = _.find(
mapConfig.obj().layers,
function(l){ return l.options.source && (l.options.source.id === sourceId); }
);
var queryRewriteData = layer && layer.options.query_rewrite_data;
if ( queryRewriteData ) {
if ( node.type === 'source' ) {
var filters = node.filters; // TODO: node.getFilters() when available in camshaft
var filters_disabler = Object.keys(filters).reduce(
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
{}
);
var unfiltered_query = node.getQuery(filters_disabler);
queryRewriteData = _.extend(
{},
queryRewriteData, { filters: filters, unfiltered_query: unfiltered_query }
);
}
}
if (params.bbox) {
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox});
query = bboxFilter.sql(query);
if ( queryRewriteData ) {
var bbox_filter_definition = {
type: 'bbox',
options: {
column: 'the_geom',
srid: 4326,
},
params: {
bbox: params.bbox
}
};
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
}
}
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
);
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
function castNumbers(overrides, val, k) {
@@ -123,7 +166,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
{ownFilter: ownFilter}
);
var dataview = DataviewFactory.getDataview(query, dataviewDefinition);
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
dataview.getResult(pg, overrideParams, this);
},
function returnCallback(err, result) {
@@ -277,4 +320,4 @@ function dbParamsFromReqParams(params) {
dbParams.dbname = params.dbname;
}
return dbParams;
}
}

View File

@@ -8,15 +8,15 @@ var queue = require('queue-async');
var LruCache = require("lru-cache");
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, overviewsAdapter,
turboCartoAdapter) {
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, analysisBackend, userLimitsApi,
overviewsAdapter, turboCartoAdapter) {
this.templateMaps = templateMaps;
this.pgConnection = pgConnection;
this.metadataBackend = metadataBackend;
this.userLimitsApi = userLimitsApi;
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
this.analysisMapConfigAdapter = new AnalysisMapConfigAdapter();
this.analysisMapConfigAdapter = new AnalysisMapConfigAdapter(analysisBackend);
this.overviewsAdapter = overviewsAdapter;
this.turboCartoAdapter = turboCartoAdapter;

View File

@@ -14,6 +14,9 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
'api_key',
'auth_token',
'callback',
'zoom',
'lon',
'lat',
// widgets & filters
'filters', // json
'own_filter', // 0, 1

View File

@@ -116,7 +116,10 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) {
if (err) {
self.sendError(req, res, err, 'GET NODE STATUS');
} else {
self.sendResponse(req, res, nodeStatus, 200);
self.sendResponse(req, res, nodeStatus, 200, {
'Cache-Control': 'public,max-age=5',
'Last-Modified': new Date().toUTCString()
});
}
}
);

View File

@@ -185,17 +185,20 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
function addOverviewsInformation(err, requestMapConfig, datasource) {
assert.ifError(err);
var next = this;
self.overviewsAdapter.getLayers(req.context.user, requestMapConfig.layers, function(err, layers) {
if (err) {
return next(err);
}
self.overviewsAdapter.getLayers(
req.context.user, requestMapConfig.layers, analysesResults,
function(err, layers) {
if (err) {
return next(err);
}
if (layers) {
requestMapConfig.layers = layers;
}
if (layers) {
requestMapConfig.layers = layers;
}
return next(null, requestMapConfig, datasource);
});
return next(null, requestMapConfig, datasource);
}
);
},
function parseTurboCarto(err, requestMapConfig, datasource) {
assert.ifError(err);

View File

@@ -140,7 +140,7 @@ NamedMapsController.prototype.staticMap = function(req, res) {
function prepareImageOptions(err, _namedMapProvider) {
assert.ifError(err);
namedMapProvider = _namedMapProvider;
self.getStaticImageOptions(cdbUser, namedMapProvider, this);
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
},
function getImage(err, imageOpts) {
assert.ifError(err);
@@ -192,9 +192,37 @@ var DEFAULT_ZOOM_CENTER = {
}
};
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMapProvider, callback) {
function numMapper(n) {
return +n;
}
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, params, namedMapProvider, callback) {
var self = this;
if ([params.zoom, params.lon, params.lat].map(numMapper).every(Number.isFinite)) {
return callback(null, {
zoom: params.zoom,
center: {
lng: params.lon,
lat: params.lat
}
});
}
if (params.bbox) {
var bbox = params.bbox.split(',').map(numMapper);
if (bbox.length === 4 && bbox.every(Number.isFinite)) {
return callback(null, {
bounds: {
west: bbox[0],
south: bbox[1],
east: bbox[2],
north: bbox[3]
}
});
}
}
step(
function getTemplate() {
namedMapProvider.getTemplate(this);
@@ -205,6 +233,9 @@ NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMap
if (template.view) {
var zoomCenter = templateZoomCenter(template.view);
if (zoomCenter) {
if (Number.isFinite(+params.zoom)) {
zoomCenter.zoom = +params.zoom;
}
return zoomCenter;
}

View File

@@ -26,9 +26,12 @@ var layerQueryTemplate = dot.template([
'FROM ({{=it._query}}) _cdb_analysis_query'
].join('\n'));
function layerQuery(query, columnNames) {
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(columnNames));
return layerQueryTemplate({ _query: query, _columns: _columns.join(', ') });
function layerQuery(node) {
if (node.type === 'source') {
return node.getQuery();
}
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
return layerQueryTemplate({ _query: node.getQuery(), _columns: _columns.join(', ') });
}
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
@@ -139,7 +142,12 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(analysisConfiguration
var layerSourceId = getLayerSourceId(layer);
var layerNode = sourceId2Node[layerSourceId];
if (layerNode) {
layer.options.sql = layerQuery(layerNode.getQuery(), layerNode.getColumns());
var analysisSql = layerQuery(layerNode);
var sqlQueryWrap = layer.options.sql_wrap;
if (sqlQueryWrap) {
analysisSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, analysisSql);
}
layer.options.sql = analysisSql;
var layerDataviews = getLayerDataviews(layer, dataviews);
layer.options.columns = layerDataviews.reduce(function(columns, dataview) {
return columns.concat(getDataviewColumns(dataview));

View File

@@ -0,0 +1,32 @@
var parentFactory = require('../factory');
var dataviews = require('./');
function OverviewsDataviewFactory(queryRewriter, queryRewriteData, options) {
this.queryRewriter = queryRewriter;
this.queryRewriteData = queryRewriteData;
this.options = options;
}
OverviewsDataviewFactory.prototype.getDataview = function(query, dataviewDefinition) {
var type = dataviewDefinition.type;
var dataviews = OverviewsDataviewMetaFactory.dataviews;
if ( !this.queryRewriter || !this.queryRewriteData || !dataviews[type] ) {
return parentFactory.getDataview(query, dataviewDefinition);
}
return new dataviews[type](
query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options
);
};
var OverviewsDataviewMetaFactory = {
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
return allDataviews;
}, {}),
getFactory: function(queryRewriter, queryRewriteData, options) {
return new OverviewsDataviewFactory(queryRewriter, queryRewriteData, options);
},
};
module.exports = OverviewsDataviewMetaFactory;

View File

@@ -0,0 +1,104 @@
var _ = require('underscore');
var BaseWidget = require('../base');
var BaseDataview = require('../formula');
var debug = require('debug')('windshaft:widget:formula:overviews');
var dot = require('dot');
dot.templateSettings.strip = false;
var formulaQueryTpls = {
'count': dot.template([
'SELECT',
'sum(_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'FROM ({{=it._query}}) _cdb_formula'
].join('\n')),
'sum': dot.template([
'SELECT',
'sum({{=it._column}}*_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'FROM ({{=it._query}}) _cdb_formula'
].join('\n')),
'avg': dot.template([
'SELECT',
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
'FROM ({{=it._query}}) _cdb_formula'
].join('\n')),
};
function Formula(query, options, queryRewriter, queryRewriteData, params) {
this.base_dataview = new BaseDataview(query, options);
this.query = query;
this.column = options.column || '1';
this.operation = options.operation;
this.queryRewriter = queryRewriter;
this.queryRewriteData = queryRewriteData;
this.options = params;
}
Formula.prototype = new BaseWidget();
Formula.prototype.constructor = Formula;
module.exports = Formula;
var zoom_level_factor = 100.0;
// Compute zoom level so that the the resolution grid size of the
// selected overview is smaller (zoom_level_factor times smaller at least)
// than the bounding box size.
function zoom_level_for_bbox(bbox) {
var px_per_tile = 256.0;
var earth_width = 360.0;
// TODO: now we assume overviews are computed for 1-pixel tolerance;
// should use extended overviews metadata to compute this properly.
if ( bbox ) {
var bbox_values = _.map(bbox.split(','), function(v) { return +v; });
var w = Math.abs(bbox_values[2]-bbox_values[0]);
var h = Math.abs(bbox_values[3]-bbox_values[1]);
var max_dim = Math.min(w, h);
// Find minimum suitable z
// note that the QueryRewirter will use the minimum level overview
// of level >= z if it exists, and otherwise the base table
var z = Math.ceil(-Math.log(max_dim*px_per_tile/earth_width/zoom_level_factor)/Math.log(2.0));
return Math.max(z, 0);
}
return 0;
}
Formula.prototype.sql = function(psql, filters, override, callback) {
var _query = this.query;
var formulaQueryTpl = formulaQueryTpls[this.operation];
if ( formulaQueryTpl ) {
// supported formula for use with overviews
var zoom_level = zoom_level_for_bbox(this.options.bbox);
_query = this.queryRewriter.query(_query, this.queryRewriteData, { zoom_level: zoom_level });
var formulaSql = formulaQueryTpl({
_query: _query,
_operation: this.operation,
_column: this.column
});
debug(formulaSql);
callback = callback || override;
return callback(null, formulaSql);
}
// For non supported operations (min, max) we're not using overviews.
return this.base_dataview.sql(psql, filters, override, callback);
};
Formula.prototype.format = function(result) {
return this.base_dataview.format(result);
};
Formula.prototype.getType = function() {
return this.base_dataview.getType();
};
Formula.prototype.toString = function() {
return this.base_dataview.toString();
};

View File

@@ -0,0 +1,3 @@
module.exports = {
Formula: require('./formula')
};

View File

@@ -0,0 +1,35 @@
var filters = {
category: require('./camshaft/category'),
range: require('./camshaft/range')
};
function createFilter(filterDefinition) {
var filterType = filterDefinition.type.toLowerCase();
if (!filters.hasOwnProperty(filterType)) {
throw new Error('Unknown filter type: ' + filterType);
}
return new filters[filterType](filterDefinition.column, filterDefinition.params);
}
function CamshaftFilters(filters) {
this.filters = filters;
}
CamshaftFilters.prototype.sql = function(rawSql) {
var filters = this.filters || {};
var applyFilters = {};
return Object.keys(filters)
.filter(function(filterName) {
return applyFilters.hasOwnProperty(filterName) ? applyFilters[filterName] : true;
})
.map(function(filterName) {
var filterDefinition = filters[filterName];
return createFilter(filterDefinition);
})
.reduce(function(sql, filter) {
return filter.sql(sql);
}, rawSql);
};
module.exports = CamshaftFilters;

View File

@@ -0,0 +1,79 @@
'use strict';
var debug = require('debug')('windshaft:filter:category');
var dot = require('dot');
dot.templateSettings.strip = false;
var filterQueryTpl = dot.template([
'SELECT *',
'FROM ({{=it._sql}}) _camshaft_category_filter',
'WHERE {{=it._filters}}'
].join('\n'));
var escapeStringTpl = dot.template('$escape_{{=it._i}}${{=it._value}}$escape_{{=it._i}}$');
var inConditionTpl = dot.template('{{=it._column}} IN ({{=it._values}})');
var notInConditionTpl = dot.template('{{=it._column}} NOT IN ({{=it._values}})');
function Category(column, filterParams) {
this.column = column;
if (!Array.isArray(filterParams.accept) && !Array.isArray(filterParams.reject)) {
throw new Error('Category filter expects at least one array in accept or reject params');
}
if (Array.isArray(filterParams.accept) && Array.isArray(filterParams.reject)) {
if (filterParams.accept.length === 0 && filterParams.reject.length === 0) {
throw new Error(
'Category filter expects one value either in accept or reject params when both are provided'
);
}
}
this.accept = filterParams.accept;
this.reject = filterParams.reject;
}
module.exports = Category;
/*
- accept: [] => reject all
- reject: [] => accept all
*/
Category.prototype.sql = function(rawSql) {
var valueFilters = [];
if (Array.isArray(this.accept)) {
if (this.accept.length > 0) {
valueFilters.push(inConditionTpl({
_column: this.column,
_values: this.accept.map(function(value, i) {
return Number.isFinite(value) ? value : escapeStringTpl({_i: i, _value: value});
}).join(',')
}));
} else {
valueFilters.push('0 = 1');
}
}
if (Array.isArray(this.reject)) {
if (this.reject.length > 0) {
valueFilters.push(notInConditionTpl({
_column: this.column,
_values: this.reject.map(function (value, i) {
return Number.isFinite(value) ? value : escapeStringTpl({_i: i, _value: value});
}).join(',')
}));
} else {
valueFilters.push('1 = 1');
}
}
debug(filterQueryTpl({
_sql: rawSql,
_filters: valueFilters.join(' AND ')
}));
return filterQueryTpl({
_sql: rawSql,
_filters: valueFilters.join(' AND ')
});
};

View File

@@ -0,0 +1,43 @@
'use strict';
var dot = require('dot');
dot.templateSettings.strip = false;
var betweenFilterTpl = dot.template('{{=it._column}} BETWEEN {{=it._min}} AND {{=it._max}}');
var minFilterTpl = dot.template('{{=it._column}} >= {{=it._min}}');
var maxFilterTpl = dot.template('{{=it._column}} <= {{=it._max}}');
var filterQueryTpl = dot.template('SELECT * FROM ({{=it._sql}}) _camshaft_range_filter WHERE {{=it._filter}}');
function Range(column, filterParams) {
this.column = column;
if (!Number.isFinite(filterParams.min) && !Number.isFinite(filterParams.max)) {
throw new Error('Range filter expect to have at least one value in min or max numeric params');
}
this.min = filterParams.min;
this.max = filterParams.max;
this.columnType = filterParams.columnType;
}
module.exports = Range;
Range.prototype.sql = function(rawSql) {
var minMaxFilter;
if (Number.isFinite(this.min) && Number.isFinite(this.max)) {
minMaxFilter = betweenFilterTpl({
_column: this.column,
_min: this.min,
_max: this.max
});
} else if (Number.isFinite(this.min)) {
minMaxFilter = minFilterTpl({ _column: this.column, _min: this.min });
} else {
minMaxFilter = maxFilterTpl({ _column: this.column, _max: this.max });
}
return filterQueryTpl({
_sql: rawSql,
_filter: minMaxFilter
});
};

View File

@@ -142,7 +142,7 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
assert.ifError(err);
var next = this;
self.overviewsAdapter.getLayers(self.owner, _mapConfig.layers, function(err, layers) {
self.overviewsAdapter.getLayers(self.owner, _mapConfig.layers, self.analysesResults, function(err, layers) {
if (err) {
return next(err);
}

View File

@@ -1,13 +1,15 @@
var step = require('step');
var queue = require('queue-async');
var _ = require('underscore');
function MapConfigOverviewsAdapter(overviewsMetadataApi) {
function MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi) {
this.overviewsMetadataApi = overviewsMetadataApi;
this.filterStatsApi = filterStatsApi;
}
module.exports = MapConfigOverviewsAdapter;
MapConfigOverviewsAdapter.prototype.getLayers = function(username, layers, callback) {
MapConfigOverviewsAdapter.prototype.getLayers = function(username, layers, analysesResults, callback) {
var self = this;
if (!layers || layers.length === 0) {
@@ -24,11 +26,52 @@ MapConfigOverviewsAdapter.prototype.getLayers = function(username, layers, callb
if (err) {
done(err, layer);
} else {
if ( !_.isEmpty(metadata) ) {
layer = _.extend({}, layer);
layer.options = _.extend({}, layer.options, { query_rewrite_data: { overviews: metadata } });
}
done(null, layer);
var query_rewrite_data = { overviews: metadata };
step(
function collectFiltersData() {
var filters, unfiltered_query;
if ( layer.options.source && analysesResults ) {
var sourceId = layer.options.source.id;
var node = _.find(analysesResults, function(a){ return a.rootNode.params.id === sourceId; });
if ( node ) {
node = node.rootNode;
filters = node.filters; // TODO: node.getFilters() when available in camshaft
var filters_disabler = Object.keys(filters).reduce(
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
{}
);
unfiltered_query = node.getQuery(filters_disabler);
query_rewrite_data.filters = filters;
query_rewrite_data.unfiltered_query = unfiltered_query;
}
}
this(null, filters, unfiltered_query);
},
function collectStatsData(err, filters, unfiltered_query) {
var next_step = this;
if ( filters ) {
self.filterStatsApi.getFilterStats(
username,
unfiltered_query, filters,
function(err, stats) {
if ( !err ) {
query_rewrite_data.filter_stats = stats;
}
return next_step(err);
}
);
} else {
return next_step(null);
}
},
function addDataToLayer(err) {
if ( !err && !_.isEmpty(metadata) ) {
layer = _.extend({}, layer);
layer.options = _.extend({}, layer.options, { query_rewrite_data: query_rewrite_data });
}
done(err, layer);
}
);
}
});
}

View File

@@ -3,6 +3,7 @@ var bodyParser = require('body-parser');
var RedisPool = require('redis-mpool');
var cartodbRedis = require('cartodb-redis');
var _ = require('underscore');
var debug = require('debug')('windshaft:cartodb');
var controller = require('./controllers');
@@ -20,6 +21,7 @@ var mapnik = windshaft.mapnik;
var TemplateMaps = require('./backends/template_maps.js');
var OverviewsMetadataApi = require('./api/overviews_metadata_api');
var FilterStatsApi = require('./api/filter_stats_api');
var UserLimitsApi = require('./api/user_limits_api');
var AuthApi = require('./api/auth_api');
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
@@ -58,6 +60,7 @@ module.exports = function(serverOptions) {
var pgConnection = new PgConnection(metadataBackend);
var pgQueryRunner = new PgQueryRunner(pgConnection);
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
var filterStatsApi = new FilterStatsApi(pgQueryRunner);
var userLimitsApi = new UserLimitsApi(metadataBackend, {
limits: {
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
@@ -147,7 +150,7 @@ module.exports = function(serverOptions) {
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
var overviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi);
var overviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi);
var turboCartoParser = new TurboCartoParser(pgQueryRunner);
var turboCartoAdapter = new TurboCartoAdapter(turboCartoParser);
@@ -156,6 +159,7 @@ module.exports = function(serverOptions) {
templateMaps,
pgConnection,
metadataBackend,
analysisBackend,
userLimitsApi,
overviewsAdapter,
turboCartoAdapter
@@ -231,7 +235,7 @@ function validateOptions(opts) {
// Be nice and warn if configured mapnik version is != instaled mapnik version
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
debug('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
}
}

View File

@@ -1,5 +1,26 @@
var _ = require('underscore');
var TableNameParser = require('./table_name_parser');
var BBoxFilter = require('../models/filter/bbox');
var CamshaftFilter = require('../models/filter/camshaft');
// Minimim number of filtered rows to use overviews
var FILTER_MIN_ROWS = 65536;
// Maximum filtered fraction to not apply overviews
var FILTER_MAX_FRACTION = 0.2;
function apply_filters_to_query(query, filters, bbox_filter) {
if ( filters && !_.isEmpty(filters)) {
var camshaftFilter = new CamshaftFilter(filters);
query = camshaftFilter.sql(query);
}
if ( bbox_filter ) {
var bboxFilter = new BBoxFilter(bbox_filter.options, bbox_filter.params);
query = bboxFilter.sql(query);
}
return query;
}
function OverviewsQueryRewriter(options) {
this.options = options;
@@ -119,26 +140,34 @@ function replace_table_in_query(sql, old_table_name, replacement) {
return sql.replace(new RegExp(regexp, 'g'), replacement);
}
function overviews_query(query, overviews, zoom_level_expression) {
function replace_table_in_query_with_schema(query, table, schema, replacement) {
if ( replacement ) {
query = replace_table_in_query(query, table, replacement);
var parsed_table = TableNameParser.parse(table);
if (!parsed_table.schema && schema) {
// replace also the qualified table name, if the table wasn't qualified
parsed_table.schema = schema;
table = TableNameParser.table_identifier(parsed_table);
query = replace_table_in_query(query, table, replacement);
}
}
return query;
}
// Build query to use overviews for a variant zoom level (given by a expression to
// be evaluated by the database server)
function overviews_query_with_zoom_expression(query, overviews, zoom_level_expression) {
var replaced_query = query;
var sql = "WITH\n _vovw_scale AS ( SELECT " + zoom_level_expression + " AS _vovw_z )";
var replacement;
for ( var table in overviews ) {
if (overviews.hasOwnProperty(table)) {
var table_overviews = overviews[table];
var table_view = overviews_view_name(table);
var schema = table_overviews.schema;
replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view;
replaced_query = replace_table_in_query(replaced_query, table, replacement);
var parsed_table = TableNameParser.parse(table);
if (!parsed_table.schema && schema) {
// replace also the qualified table name, if the table wasn't qualified
parsed_table.schema = schema;
table = TableNameParser.table_identifier(parsed_table);
replaced_query = replace_table_in_query(replaced_query, table, replacement);
}
}
}
_.each(Object.keys(overviews), function(table) {
var table_overviews = overviews[table];
var table_view = overviews_view_name(table);
var schema = table_overviews.schema;
replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view;
replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement);
});
if ( replaced_query !== query ) {
sql += "\n";
sql += replaced_query;
@@ -148,34 +177,128 @@ function overviews_query(query, overviews, zoom_level_expression) {
return sql;
}
// Build query to use overviews for a specific zoom level value
function overviews_query_with_definite_zoom(query, overviews, zoom_level) {
var replaced_query = query;
var replacement;
_.each(Object.keys(overviews), function(table) {
var table_overviews = overviews[table];
var schema = table_overviews.schema;
replacement = overview_table_for_zoom_level(table_overviews, zoom_level);
replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement);
});
return replaced_query;
}
// Find a suitable overview table for a specific zoom_level
function overview_table_for_zoom_level(table_overviews, zoom_level) {
var overview_table;
if ( table_overviews ) {
overview_table = table_overviews[zoom_level];
if ( !overview_table ) {
_.every(Object.keys(table_overviews).sort(function(x,y){ return x-y; }), function(overview_zoom) {
if ( +overview_zoom > +zoom_level ) {
overview_table = table_overviews[overview_zoom];
return false;
} else {
return true;
}
});
}
}
if ( overview_table ) {
overview_table = overview_table.table;
}
return overview_table;
}
// Transform an SQL query so that it uses overviews.
// overviews contains metadata about the overviews to be used:
// { 'table-name': {1: { table: 'overview-table-1' }, ... }, ... }
//
// For a given query `SELECT * FROM table`, if any of tables in it
// has overviews as defined by the provided metadat, the query will
// be transform into something similar to this:
//
// WITH _vovw_scale AS ( ... ), -- define scale level
// WITH _vovw_table AS ( ... ), -- define union of overviews and base table
// SELECT * FROM _vovw_table -- query with table replaced by _vovw_table
// SELECT * FROM -- in the query the table is replaced by:
// ( ... ) AS _vovw_table -- a union of overviews and base table
//
// This transformation can in principle be applied to arbitrary queries
// (except for the case of queries that include the name of tables with
// overviews inside text literals: at the current table name substitution
// doesnn't prevent substitution inside literals).
// But the transformation will currently only be applied to simple queries
// of the form detected by the overviews_supported_query function.
OverviewsQueryRewriter.prototype.query = function(query, data) {
var overviews = this.overviews_metadata(data);
if ( !overviews || !this.is_supported_query(query)) {
// The data argument has the form:
// {
// overviews: // overview tables metadata
// { 'table-name': {1: { table: 'overview-table-1' }, ... }, ... },
// zoom_level: ..., // optional zoom level
// filters: ..., // filters definition
// unfiltered_query: ..., // query without the filters
// bbox_filter: ... // bounding-box filter
// }
OverviewsQueryRewriter.prototype.query = function(query, data, options) {
options = options || {};
data = data || {};
var overviews = data.overviews;
var unfiltered_query = data.unfiltered_query;
var filters = data.filters;
var bbox_filter = data.bbox_filter;
if ( !unfiltered_query ) {
unfiltered_query = query;
}
if ( !should_use_overviews(unfiltered_query, data) ) {
return query;
}
var zoom_level_expression = this.options.zoom_level || '0';
return overviews_query(query, overviews, zoom_level_expression);
var rewritten_query;
var zoom_level_expression = this.options.zoom_level;
var zoom_level = zoom_level_for_query(unfiltered_query, zoom_level_expression, options);
rewritten_query = overviews_query(unfiltered_query, overviews, zoom_level, zoom_level_expression);
if ( rewritten_query === unfiltered_query ) {
// could not or didn't need to alter the query
rewritten_query = query;
} else {
rewritten_query = apply_filters_to_query(rewritten_query, filters, bbox_filter);
}
return rewritten_query;
};
OverviewsQueryRewriter.prototype.is_supported_query = function(sql) {
function zoom_level_for_query(query, zoom_level_expression, options) {
var zoom_level = null;
if ( _.has(options, 'zoom_level') ) {
zoom_level = options.zoom_level || '0';
}
if ( zoom_level === null && !zoom_level_expression ) {
zoom_level = '0';
}
return zoom_level;
}
function overviews_query(query, overviews, zoom_level, zoom_level_expression) {
if ( zoom_level || zoom_level === '0' || zoom_level === 0 ) {
return overviews_query_with_definite_zoom(query, overviews, zoom_level);
} else {
return overviews_query_with_zoom_expression(query, overviews, zoom_level_expression);
}
}
function should_use_overviews(query, data) {
data = data || {};
var use_overviews = data.overviews && is_supported_query(query);
if ( use_overviews && data.filters && data.filter_stats ) {
var filtered_rows = data.filter_stats.filtered_rows;
var unfiltered_rows = data.filter_stats.unfiltered_rows;
if ( unfiltered_rows && (filtered_rows || filtered_rows === 0) ) {
use_overviews = filtered_rows >= FILTER_MIN_ROWS ||
(filtered_rows/unfiltered_rows) > FILTER_MAX_FRACTION;
}
}
return use_overviews;
}
function is_supported_query(sql) {
var basic_query =
/\s*SELECT\s+[\*a-z0-9_,\s]+?\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i;
var unwrapped_query = new RegExp("^"+basic_query.source+"$", 'i');
@@ -187,8 +310,4 @@ OverviewsQueryRewriter.prototype.is_supported_query = function(sql) {
'i'
);
return !!(sql.match(unwrapped_query) || sql.match(wrapped_query));
};
OverviewsQueryRewriter.prototype.overviews_metadata = function(data) {
return data && data.overviews;
};
}

View File

@@ -23,6 +23,10 @@ var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, meth
return methodTemplates;
}, {});
var method2strategy = {
headtails: 'split'
};
function PostgresDatasource (pgQueryRunner, username, query) {
this.pgQueryRunner = pgQueryRunner;
this.username = username;
@@ -48,7 +52,7 @@ PostgresDatasource.prototype.getRamp = function (column, buckets, method, callba
return a - b;
});
return callback(null, ramp);
return callback(null, { ramp: ramp, strategy: method2strategy[methodName] });
});
};

View File

@@ -1,6 +1,7 @@
'use strict';
var queue = require('queue-async');
var SubstitutionTokens = require('../substitution-tokens');
function TurboCartoAdapter(turboCartoParser) {
this.turboCartoParser = turboCartoParser;
@@ -37,13 +38,26 @@ TurboCartoAdapter.prototype._parseCartoCss = function (username, layer, callback
});
}
this.turboCartoParser.process(username, layer.options.cartocss, layer.options.sql, function (err, cartocss) {
// Ignore turbo-carto errors and continue
if (!err && cartocss) {
layer.options.cartocss = cartocss;
var sql = SubstitutionTokens.replace(layer.options.sql, {
bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
scale_denominator: '500000001',
pixel_width: '156412',
pixel_height: '156412'
});
this.turboCartoParser.process(username, layer.options.cartocss, sql, function (err, cartocss) {
// Only return turbo-carto errors
if (err && err.name === 'TurboCartoError') {
err = new Error('turbo-carto: ' + err.message);
err.http_status = 400;
return callback(err);
}
callback(null, layer);
// Try to continue in the rest of the cases
if (cartocss) {
layer.options.cartocss = cartocss;
}
return callback(null, layer);
});
};

View File

@@ -0,0 +1,19 @@
var SUBSTITUTION_TOKENS = {
bbox: /!bbox!/g,
scale_denominator: /!scale_denominator!/g,
pixel_width: /!pixel_width!/g,
pixel_height: /!pixel_height!/g
};
var SubstitutionTokens = {
replace: function(sql, replaceValues) {
Object.keys(replaceValues).forEach(function(token) {
if (SUBSTITUTION_TOKENS[token]) {
sql = sql.replace(SUBSTITUTION_TOKENS[token], replaceValues[token]);
}
});
return sql;
}
};
module.exports = SubstitutionTokens;

3883
npm-shrinkwrap.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,61 +1,61 @@
{
"private": true,
"name": "windshaft-cartodb",
"version": "2.37.0",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
],
"url": "https://github.com/CartoDB/Windshaft-cartodb",
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
},
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
"contributors": [
"Simon Tokumine <simon@vizzuality.com>",
"Javi Santana <jsantana@vizzuality.com>",
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"express": "~4.13.3",
"body-parser": "~1.14.0",
"debug": "~2.2.0",
"step-profiler": "~0.2.1",
"node-statsd": "~0.0.7",
"underscore" : "~1.6.0",
"dot": "~1.0.2",
"windshaft": "1.19.0",
"step": "~0.0.6",
"queue-async": "~1.0.7",
"request": "~2.62.0",
"cartodb-redis": "~0.13.0",
"cartodb-psql": "~0.6.1",
"camshaft": "0.6.0",
"fastly-purge": "~1.0.1",
"redis-mpool": "~0.4.0",
"lru-cache": "2.6.5",
"lzma": "~1.3.7",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
"cartodb-query-tables": "~0.1.0",
"turbo-carto": "0.6.0"
},
"devDependencies": {
"istanbul": "~0.3.6",
"mocha": "~1.21.4",
"nock": "~2.11.0",
"jshint": "~2.6.0",
"redis": "~0.8.6",
"strftime": "~0.8.2",
"semver": "~1.1.4"
},
"scripts": {
"preinstall": "make pre-install",
"test": "make test-all"
},
"engines": {
"node": ">=0.8 <0.11",
"npm": ">=2.14.16"
}
"private": true,
"name": "windshaft-cartodb",
"version": "2.43.2",
"description": "A map tile server for CartoDB",
"keywords": [
"cartodb"
],
"url": "https://github.com/CartoDB/Windshaft-cartodb",
"license": "BSD-3-Clause",
"repository": {
"type": "git",
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
},
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
"contributors": [
"Simon Tokumine <simon@vizzuality.com>",
"Javi Santana <jsantana@vizzuality.com>",
"Sandro Santilli <strk@vizzuality.com>"
],
"dependencies": {
"body-parser": "~1.14.0",
"camshaft": "0.8.0",
"cartodb-psql": "0.10.1",
"cartodb-query-tables": "~0.1.0",
"cartodb-redis": "~0.13.0",
"debug": "~2.2.0",
"dot": "~1.0.2",
"express": "~4.13.3",
"fastly-purge": "~1.0.1",
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
"lru-cache": "2.6.5",
"lzma": "~1.3.7",
"node-statsd": "~0.0.7",
"queue-async": "~1.0.7",
"redis-mpool": "~0.4.0",
"request": "~2.62.0",
"step": "~0.0.6",
"step-profiler": "~0.3.0",
"turbo-carto": "0.9.2",
"underscore": "~1.6.0",
"windshaft": "1.19.0"
},
"devDependencies": {
"istanbul": "~0.4.3",
"jshint": "~2.6.0",
"mocha": "~1.21.4",
"nock": "~2.11.0",
"redis": "~0.8.6",
"semver": "~1.1.4",
"strftime": "~0.8.2"
},
"scripts": {
"preinstall": "make pre-install",
"test": "make test-all"
},
"engines": {
"node": ">=0.8 <0.11",
"npm": ">=2.14.16"
}
}

View File

@@ -3,6 +3,7 @@ require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
var dot = require('dot');
var debug = require('debug')('windshaft:cartodb:test');
describe('analysis-layers use cases', function() {
@@ -649,7 +650,7 @@ describe('analysis-layers use cases', function() {
useCases.forEach(function(useCase, imageIdx) {
if (!!useCase.skip) {
console.log(JSON.stringify(useCase.mapConfig, null, 4));
debug(JSON.stringify(useCase.mapConfig, null, 4));
}
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {

View File

@@ -304,4 +304,67 @@ describe('analysis-layers', function() {
testClient.drain(done);
});
});
it('should response with custom cache headers for node status endpoints', function(done) {
var useCase = useCases[1];
// No API key here
var testClient = new TestClient(useCase.mapConfig, 1234);
testClient.getNodeStatus('HEAD', function(err, response, nodeStatus) {
assert.ok(!err, err);
assert.equal(nodeStatus.status, 'ready');
var headers = response.headers;
assert.equal(headers['cache-control'], 'public,max-age=5');
var lastModified = new Date(headers['last-modified']);
var tenSecondsInMs = 1e5;
assert.ok(Date.now() - lastModified.getTime() < tenSecondsInMs);
testClient.drain(done);
});
});
it('should wrap queries from analyses', function(done) {
var testClient = new TestClient(mapConfig(
[
{
"type": "cartodb",
"options": {
"source": {
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
},
"sql_wrap": "SELECT * FROM (<%= sql %>) __wrapped WHERE adm0cap = 1",
"cartocss": DEFAULT_MULTITYPE_STYLE,
"cartocss_version": "2.3.0"
}
}
],
{},
[
{
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
}
]
), 1234);
var tile = TILE_ANALYSIS_TABLES;
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
assert.ok(!err, err);
var fixturePath = './test/fixtures/analysis/adm0cap-source-id-mapnik-layer.png';
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
testClient.drain(done);
});
});
});
});

View File

@@ -1,11 +1,11 @@
var assert = require('../../support/assert');
var step = require('step');
var helper = require('../../support/test_helper');
var CartodbWindshaft = require('../../../lib/cartodb/server');
var serverOptions = require('../../../lib/cartodb/server_options');
var server = new CartodbWindshaft(serverOptions);
var TestClient = require('../../support/test-client');
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
@@ -16,230 +16,259 @@ describe('named-maps analysis', function() {
var username = 'localhost';
var widgetsTemplateName = 'widgets-template';
var layergroupid;
var layergroup;
var keysToDelete;
beforeEach(function(done) {
keysToDelete = {};
var widgetsTemplate = {
version: '0.0.1',
name: widgetsTemplateName,
layergroup: {
version: '1.5.0',
layers: [
{
"type": "cartodb",
"options": {
"source": {
"id": "HEAD"
},
"cartocss": '#buffer { polygon-fill: red; }',
"cartocss_version": "2.3.0"
}
}
],
dataviews: {
pop_max_histogram: {
source: {
id: 'HEAD'
var widgetsTemplate = {
version: '0.0.1',
name: widgetsTemplateName,
layergroup: {
version: '1.5.0',
layers: [
{
"type": "cartodb",
"options": {
"source": {
"id": "HEAD"
},
type: 'histogram',
options: {
column: 'pop_max'
}
"cartocss": '#buffer { polygon-fill: red; }',
"cartocss_version": "2.3.0"
}
},
analyses: [
{
"id": "HEAD",
"type": "buffer",
"params": {
"source": {
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
},
"radius": 50000
}
}
],
dataviews: {
pop_max_histogram: {
source: {
id: 'HEAD'
},
type: 'histogram',
options: {
column: 'pop_max'
}
]
}
};
var template_params = {};
step(
function createTemplate()
{
var next = this;
assert.response(
server,
{
url: '/api/v1/map/named?api_key=1234',
method: 'POST',
headers: {
host: username,
'Content-Type': 'application/json'
}
},
analyses: [
{
"id": "HEAD",
"type": "buffer",
"params": {
"source": {
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
"type": "source",
"params": {
"query": "select * from populated_places_simple_reduced"
}
},
data: JSON.stringify(widgetsTemplate)
},
{
status: 200
},
function(res, err) {
next(err, res);
"radius": 50000
}
);
},
function instantiateTemplate(err, res) {
assert.ifError(err);
}
]
}
};
assert.deepEqual(JSON.parse(res.body), { template_id: widgetsTemplateName });
var next = this;
assert.response(
server,
{
url: '/api/v1/map/named/' + widgetsTemplateName,
method: 'POST',
headers: {
host: username,
'Content-Type': 'application/json'
},
data: JSON.stringify(template_params)
},
{
status: 200
},
function(res) {
next(null, res);
}
);
},
function finish(err, res) {
assert.ifError(err);
layergroup = JSON.parse(res.body);
assert.ok(layergroup.hasOwnProperty('layergroupid'), "Missing 'layergroupid' from: " + res.body);
layergroupid = layergroup.layergroupid;
assert.ok(
Array.isArray(layergroup.metadata.analyses),
'Missing "analyses" array metadata from: ' + res.body
);
var analyses = layergroup.metadata.analyses;
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
var nodes = analyses[0].nodes;
var nodesIds = Object.keys(nodes);
assert.deepEqual(nodesIds, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
nodesIds.forEach(function(nodeId) {
var node = nodes[nodeId];
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
assert.ok(!node.hasOwnProperty('query'), 'Unexpected "query" attribute in node');
});
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroup.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return done();
}
);
});
afterEach(function(done) {
step(
function deleteTemplate(err) {
assert.ifError(err);
var next = this;
assert.response(
server,
{
url: '/api/v1/map/named/' + widgetsTemplateName + '?api_key=1234',
method: 'DELETE',
headers: {
host: username
}
},
{
status: 204
},
function(res, err) {
next(err, res);
}
);
},
function deleteRedisKeys(err) {
assert.ifError(err);
helper.deleteRedisKeys(keysToDelete, done);
}
);
});
it('should be able to retrieve images from analysis', function(done) {
beforeEach(function createTemplate(done) {
assert.response(
server,
{
url: '/api/v1/map/' + layergroupid + '/6/31/24.png',
method: 'GET',
encoding: 'binary',
url: '/api/v1/map/named?api_key=1234',
method: 'POST',
headers: {
host: username,
'Content-Type': 'application/json'
},
data: JSON.stringify(widgetsTemplate)
},
{
status: 200
},
function(res, err) {
assert.deepEqual(JSON.parse(res.body), { template_id: widgetsTemplateName });
return done(err);
}
);
});
afterEach(function deleteTemplate(done) {
assert.response(
server,
{
url: '/api/v1/map/named/' + widgetsTemplateName + '?api_key=1234',
method: 'DELETE',
headers: {
host: username
}
},
{
status: 200,
headers: {
'Content-Type': 'image/png'
}
status: 204
},
function(res, err) {
if (err) {
return done(err);
}
return done(err);
}
);
});
var fixturePath = './test/fixtures/analysis/named-map-buffer.png';
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
describe('layergroup', function() {
var layergroupid;
var layergroup;
var keysToDelete;
beforeEach(function(done) {
keysToDelete = {};
assert.response(
server,
{
url: '/api/v1/map/named/' + widgetsTemplateName,
method: 'POST',
headers: {
host: username,
'Content-Type': 'application/json'
},
data: JSON.stringify({})
},
{
status: 200
},
function(res, err) {
assert.ifError(err);
layergroup = JSON.parse(res.body);
assert.ok(layergroup.hasOwnProperty('layergroupid'), "Missing 'layergroupid' from: " + res.body);
layergroupid = layergroup.layergroupid;
assert.ok(
Array.isArray(layergroup.metadata.analyses),
'Missing "analyses" array metadata from: ' + res.body
);
var analyses = layergroup.metadata.analyses;
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
var nodes = analyses[0].nodes;
var nodesIds = Object.keys(nodes);
assert.deepEqual(nodesIds, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
nodesIds.forEach(function(nodeId) {
var node = nodes[nodeId];
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
assert.ok(!node.hasOwnProperty('query'), 'Unexpected "query" attribute in node');
});
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroup.layergroupid).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
return done();
}
);
});
afterEach(function(done) {
helper.deleteRedisKeys(keysToDelete, done);
});
it('should be able to retrieve images from analysis', function(done) {
assert.response(
server,
{
url: '/api/v1/map/' + layergroupid + '/6/31/24.png',
method: 'GET',
encoding: 'binary',
headers: {
host: username
}
},
{
status: 200,
headers: {
'Content-Type': 'image/png'
}
},
function(res, err) {
if (err) {
return done(err);
}
var fixturePath = './test/fixtures/analysis/named-map-buffer.png';
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
done();
});
}
);
});
it('should be able to retrieve dataviews from analysis', function(done) {
assert.response(
server,
{
url: '/api/v1/map/' + layergroupid + '/dataview/pop_max_histogram',
method: 'GET',
headers: {
host: username
}
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return done(err);
}
var dataview = JSON.parse(res.body);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_start, 0);
done();
}
);
});
it('should be able to retrieve static map preview via layergroup', function(done) {
assert.response(
server,
{
url: '/api/v1/map/static/center/' + layergroupid + '/4/42/-3/320/240.png',
method: 'GET',
encoding: 'binary',
headers: {
host: username
}
},
{
status: 200,
headers: {
'Content-Type': 'image/png'
}
},
function(res, err) {
if (err) {
return done(err);
}
var fixturePath = './test/fixtures/analysis/named-map-buffer-layergroup-static-preview.png';
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
done();
});
}
);
});
});
describe('auto-instantiation', function() {
it('should be able to retrieve static map preview via fixed url', function(done) {
TestClient.getStaticMap(widgetsTemplateName, function(err, image) {
assert.ok(!err, err);
var fixturePath = './test/fixtures/analysis/named-map-buffer-static-preview.png';
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
assert.ok(!err, err);
done();
});
}
);
});
it('should be able to retrieve dataviews from analysis', function(done) {
assert.response(
server,
{
url: '/api/v1/map/' + layergroupid + '/dataview/pop_max_histogram',
method: 'GET',
headers: {
host: username
}
},
{
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
},
function(res, err) {
if (err) {
return done(err);
}
var dataview = JSON.parse(res.body);
assert.equal(dataview.type, 'histogram');
assert.equal(dataview.bins_start, 0);
done();
}
);
});
});
});
});

View File

@@ -0,0 +1,358 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
describe('dataviews using tables without overviews', function() {
var nonOverviewsMapConfig = {
version: '1.5.0',
analyses: [
{ id: 'data-source',
type: 'source',
params: {
query: 'select * from populated_places_simple_reduced'
}
}
],
dataviews: {
country_places_count: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'adm0_a3',
operation: 'count'
}
},
country_categories: {
type: 'aggregation',
source: {id: 'data-source'},
options: {
column: 'adm0_a3',
aggregation: 'count'
}
}
},
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',
source: { id: 'data-source' }
}
}
]
};
it("should expose a formula", function(done) {
var testClient = new TestClient(nonOverviewsMapConfig);
testClient.getDataview('country_places_count', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, { operation: 'count', result: 7313, nulls: 0, type: 'formula' });
testClient.drain(done);
});
});
it("should admit a bbox", function(done) {
var params = {
bbox: "-170,-80,170,80"
};
var testClient = new TestClient(nonOverviewsMapConfig);
testClient.getDataview('country_places_count', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, { operation: 'count', result: 7253, nulls: 0, type: 'formula' });
testClient.drain(done);
});
});
describe('filters', function() {
describe('category', function () {
it("should expose a filtered formula", function (done) {
var params = {
filters: {
dataviews: {country_categories: {accept: ['CAN']}}
}
};
var testClient = new TestClient(nonOverviewsMapConfig);
testClient.getDataview('country_places_count', params, function (err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, { operation: 'count', result: 256, nulls: 0, type: 'formula' });
testClient.drain(done);
});
});
it("should expose a filtered formula and admit a bbox", function (done) {
var params = {
filters: {
dataviews: {country_categories: {accept: ['CAN']}}
},
bbox: "-170,-80,170,80"
};
var testClient = new TestClient(nonOverviewsMapConfig);
testClient.getDataview('country_places_count', params, function (err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, { operation: 'count', result: 254, nulls: 0, type: 'formula' });
testClient.drain(done);
});
});
});
});
});
describe('dataviews using tables with overviews', function() {
var overviewsMapConfig = {
version: '1.5.0',
analyses: [
{ id: 'data-source',
type: 'source',
params: {
query: 'select * from test_table_overviews'
}
}
],
dataviews: {
test_sum: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'value',
operation: 'sum'
}
},
test_categories: {
type: 'aggregation',
source: {id: 'data-source'},
options: {
column: 'name',
aggregation: 'count',
aggregationColumn: 'name',
}
},
test_avg: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'value',
operation: 'avg'
}
},
test_count: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'value',
operation: 'count'
}
},
test_min: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'value',
operation: 'min'
}
},
test_max: {
type: 'formula',
source: {id: 'data-source'},
options: {
column: 'value',
operation: 'max'
}
}
},
layers: [
{
type: 'mapnik',
options: {
sql: 'select * from test_table_overviews',
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
cartocss_version: '2.3.0',
source: { id: 'data-source' }
}
}
]
};
it("should expose a sum formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_sum', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose an avg formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_avg', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"avg","result":3,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a count formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_count', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"count","result":5,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a max formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_max', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"max","result":5,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a min formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_min', { own_filter: 0 }, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should admit a bbox", function(done) {
var params = {
bbox: "-170,-80,170,80"
};
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_sum', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
describe('filters', function() {
describe('category', function () {
var params = {
filters: {
dataviews: {test_categories: {accept: ['Hawai']}}
}
};
it("should expose a filtered sum formula", function (done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_sum', params, function (err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a filtered avg formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_avg', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a filtered count formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_count', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a filterd max formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_max', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a filterd min formula", function(done) {
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_min', params, function(err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
it("should expose a filtered sum formula with bbox", function (done) {
var bboxparams = {
filters: {
dataviews: {test_categories: {accept: ['Hawai']}}
},
bbox: "-170,-80,170,80"
};
var testClient = new TestClient(overviewsMapConfig);
testClient.getDataview('test_sum', bboxparams, function (err, formula_result) {
if (err) {
return done(err);
}
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
testClient.drain(done);
});
});
});
});
});

View File

@@ -1,3 +1,4 @@
var qs = require('querystring');
var testHelper = require('../support/test_helper');
var RedisPool = require('redis-mpool');
@@ -53,10 +54,18 @@ describe('named maps static view', function() {
templateMaps.delTemplate(username, templateName, done);
});
function getStaticMap(callback) {
function getStaticMap(params, callback) {
if (!callback) {
callback = params;
params = null;
}
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
if (params !== null) {
url += '?' + qs.stringify(params);
}
var requestOptions = {
url: url,
method: 'GET',
@@ -163,4 +172,30 @@ describe('named maps static view', function() {
});
});
it('should return override zoom', function (done) {
var view = {
bounds: {
west: 0,
south: 0,
east: 45,
north: 45
},
zoom: 4,
center: {
lng: 40,
lat: 20
}
};
templateMaps.addTemplate(username, createTemplate(view), function (err) {
if (err) {
return done(err);
}
getStaticMap({ zoom: 3 }, function(err, img) {
assert.ok(!err);
img.save('/tmp/static.png');
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
});
});
});
});

View File

@@ -109,3 +109,118 @@ describe('overviews metadata', function() {
);
});
});
describe('overviews metadata with filters', function() {
// configure redis pool instance to use in tests
var redisPool = new RedisPool(global.environment.redis);
var keysToDelete;
beforeEach(function() {
keysToDelete = {};
});
afterEach(function(done) {
test_helper.deleteRedisKeys(keysToDelete, done);
});
it("layers with overviews", function(done) {
var layergroup = {
version: '1.5.0',
layers: [
{
type: 'cartodb',
options: {
sql: 'SELECT * FROM test_table_overviews',
source: { id: 'with_overviews' },
cartocss: '#layer { marker-fill: black; }',
cartocss_version: '2.3.0'
}
}
],
dataviews: {
test_names: {
type: 'aggregation',
source: {id: 'with_overviews'},
options: {
column: 'name',
aggregation: 'count'
}
}
},
analyses: [
{ id: 'with_overviews',
type: 'source',
params: {
query: 'select * from test_table_overviews'
}
}
]
};
var filters = {
dataviews: {
test_names: { accept: ['Hawai'] }
}
};
var layergroup_url = '/api/v1/map';
var expected_token;
step(
function do_post()
{
var next = this;
assert.response(server, {
url: layergroup_url + '?filters=' + JSON.stringify(filters),
method: 'POST',
headers: {host: 'localhost', 'Content-Type': 'application/json' },
data: JSON.stringify(layergroup)
}, {}, function(res) {
assert.equal(res.statusCode, 200, res.body);
var parsedBody = JSON.parse(res.body);
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
expected_token = parsedBody.layergroupid;
next(null, res);
});
},
function do_get_mapconfig(err)
{
assert.ifError(err);
var next = this;
var mapStore = new windshaft.storage.MapStore({
pool: redisPool,
expire_time: 500000
});
mapStore.load(LayergroupToken.parse(expected_token).token, function(err, mapConfig) {
assert.ifError(err);
assert.equal(mapConfig._cfg.layers[0].type, 'cartodb');
assert.ok(mapConfig._cfg.layers[0].options.query_rewrite_data);
var expected_data = {
overviews: {
test_table_overviews: {
schema: 'public',
1: { table: '_vovw_1_test_table_overviews' },
2: { table: '_vovw_2_test_table_overviews' }
}
},
filters: { test_names: { type: 'category', column: 'name', params: { accept: [ 'Hawai' ] } } },
unfiltered_query: 'select * from test_table_overviews',
filter_stats: { unfiltered_rows: 5, filtered_rows: 1 }
};
assert.deepEqual(mapConfig._cfg.layers[0].options.query_rewrite_data, expected_data);
});
next(err);
},
function finish(err) {
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
keysToDelete['user:localhost:mapviews:global'] = 5;
done(err);
}
);
});
});

View File

@@ -0,0 +1,40 @@
require('../support/test_helper');
var assert = require('../support/assert');
var TestClient = require('../support/test-client');
describe('regressions', function() {
var ERROR_RESPONSE = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
it('should expose a nice error when missing sql option', function(done) {
var mapConfig = {
version: '1.5.0',
layers: [
{
"type": "cartodb",
"options": {
"cartocss": '#polygons { polygon-fill: red; }',
"cartocss_version": "2.3.0"
}
}
]
};
var testClient = new TestClient(mapConfig, 1234);
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
assert.ok(!err, err);
assert.equal(layergroupResult.errors.length, 1);
assert.equal(layergroupResult.errors[0], 'Missing sql for layer 0 options');
testClient.drain(done);
});
});
});

View File

@@ -0,0 +1,94 @@
require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
function makeMapconfig(markerWidth, markerFill) {
return {
"version": "1.4.0",
"layers": [
{
"type": 'mapnik',
"options": {
"cartocss_version": '2.3.0',
"sql": 'SELECT * FROM populated_places_simple_reduced',
"cartocss": createCartocss(markerWidth, markerFill)
}
}
]
};
}
function createCartocss(markerWidth, markerFill) {
return [
"#populated_places_simple_reduced {",
" marker-fill-opacity: 0.9;",
" marker-line-color: #FFF;",
" marker-line-width: 1;",
" marker-line-opacity: 1;",
" marker-placement: point;",
" marker-type: ellipse;",
" marker-allow-overlap: true;",
" marker-width: " + (markerWidth || '10') + ";",
" marker-fill: " + (markerFill || 'red') + ";",
"}"
].join('\n');
}
var ERROR_RESPONSE = {
status: 400,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
describe('turbo-carto error cases', function() {
afterEach(function (done) {
if (this.testClient) {
this.testClient.drain(done);
}
});
it('should return invalid number of ramp error', function(done) {
this.testClient = new TestClient(makeMapconfig('ramp([pop_max], (8,24,96), (8,24,96,128))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
assert.equal(layergroup.errors.length, 1);
assert.ok(layergroup.errors[0].match(/^turbo-carto/));
assert.ok(layergroup.errors[0].match(/invalid\sramp\slength/i));
done();
});
});
it('should return invalid column from datasource', function(done) {
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
assert.equal(layergroup.errors.length, 1);
assert.ok(layergroup.errors[0].match(/^turbo-carto/));
assert.ok(layergroup.errors[0].match(/unable\sto\scompute\sramp/i));
assert.ok(layergroup.errors[0].match(/wadus_column/));
done();
});
});
it('should fail by falling back to normal carto parser', function(done) {
this.testClient = new TestClient(makeMapconfig('ramp([price], (8,24,96), (8,24,96));//(red, green, blue))'));
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('errors'));
assert.equal(layergroup.errors.length, 1);
assert.ok(!layergroup.errors[0].match(/^turbo-carto/));
assert.ok(layergroup.errors[0].match(/invalid\scode/i));
done();
});
});
});

View File

@@ -3,7 +3,7 @@ require('../../support/test_helper');
var assert = require('../../support/assert');
var TestClient = require('../../support/test-client');
function makeMapconfig(cartocss) {
function makeMapconfig(sql, cartocss) {
return {
"version": "1.4.0",
"layers": [
@@ -11,19 +11,7 @@ function makeMapconfig(cartocss) {
"type": 'mapnik',
"options": {
"cartocss_version": '2.3.0',
"sql": [
'SELECT test_table.*, _prices.price FROM test_table JOIN (' +
' SELECT 1 AS cartodb_id, 10.00 AS price',
' UNION',
' SELECT 2, 10.50',
' UNION',
' SELECT 3, 11.00',
' UNION',
' SELECT 4, 12.00',
' UNION',
' SELECT 5, 21.00',
') _prices ON _prices.cartodb_id = test_table.cartodb_id'
].join('\n'),
"sql": sql,
"cartocss": cartocss
}
}
@@ -33,42 +21,84 @@ function makeMapconfig(cartocss) {
describe('turbo-carto regressions', function() {
var cartocss = [
"/** simple visualization */",
"",
"Map {",
" buffer-size: 256;",
"}",
"",
"#county_points_with_population{",
" marker-fill-opacity: 0.1;",
" marker-line-color:#FFFFFF;//#CF1C90;",
" marker-line-width: 0;",
" marker-line-opacity: 0.3;",
" marker-placement: point;",
" marker-type: ellipse;",
" //marker-comp-op: overlay;",
" marker-width: [price];",
" [zoom=5]{marker-width: [price]*2;}",
" [zoom=6]{marker-width: [price]*4;}",
" marker-fill: #000000;",
" marker-allow-overlap: true;",
" ",
"",
"}"
].join('\n');
beforeEach(function () {
this.testClient = new TestClient(makeMapconfig(cartocss));
});
afterEach(function (done) {
this.testClient.drain(done);
if (this.testClient) {
this.testClient.drain(done);
}
});
it('should accept // comments', function(done) {
this.testClient.getTile(0, 0, 0, function(err) {
var cartocss = [
"/** simple visualization */",
"",
"Map {",
" buffer-size: 256;",
"}",
"",
"#county_points_with_population{",
" marker-fill-opacity: 0.1;",
" marker-line-color:#FFFFFF;//#CF1C90;",
" marker-line-width: 0;",
" marker-line-opacity: 0.3;",
" marker-placement: point;",
" marker-type: ellipse;",
" //marker-comp-op: overlay;",
" marker-width: [cartodb_id];",
" [zoom=5]{marker-width: [cartodb_id]*2;}",
" [zoom=6]{marker-width: [cartodb_id]*4;}",
" marker-fill: #000000;",
" marker-allow-overlap: true;",
" ",
"",
"}"
].join('\n');
this.testClient = new TestClient(makeMapconfig('SELECT * FROM populated_places_simple_reduced', cartocss));
this.testClient.getLayergroup(function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('layergroupid'));
assert.ok(!layergroup.hasOwnProperty('errors'));
done();
});
});
it('should work with mapnik substitution tokens', function(done) {
var cartocss = [
"#layer {",
" line-width: 2;",
" line-color: #3B3B58;",
" line-opacity: 1;",
" polygon-opacity: 0.7;",
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
"}"
].join('\n');
var sql = [
'WITH hgrid AS (',
' SELECT CDB_HexagonGrid(',
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
' greatest(!pixel_width!,!pixel_height!) * 100',
' ) as cell',
')',
'SELECT',
' hgrid.cell as the_geom_webmercator,',
' count(1) as points_count,',
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
' 1 as cartodb_id',
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
'GROUP BY hgrid.cell'
].join('\n');
this.testClient = new TestClient(makeMapconfig(sql, cartocss));
this.testClient.getLayergroup(function(err, layergroup) {
assert.ok(!err, err);
assert.ok(layergroup.hasOwnProperty('layergroupid'));
assert.ok(!layergroup.hasOwnProperty('errors'));
done();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -6,6 +6,7 @@ var cartodbRedis = require('cartodb-redis');
var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection');
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
var OverviewsMetadataApi = require('../../lib/cartodb/api/overviews_metadata_api');
var FilterStatsApi = require('../../lib/cartodb/api/filter_stats_api');
var MapConfigOverviewsAdapter = require('../../lib/cartodb/models/mapconfig_overviews_adapter');
// configure redis pool instance to use in tests
@@ -17,9 +18,9 @@ var metadataBackend = cartodbRedis({pool: redisPool});
var pgConnection = new PgConnection(metadataBackend);
var pgQueryRunner = new PgQueryRunner(pgConnection);
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
var filterStatsApi = new FilterStatsApi(pgQueryRunner);
var mapConfigOverviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi);
var mapConfigOverviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi);
describe('MapConfigOverviewsAdapter', function() {
@@ -36,7 +37,7 @@ describe('MapConfigOverviewsAdapter', function() {
}
};
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], function(err, layers) {
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], [], function(err, layers) {
assert.ok(!err);
assert.equal(layers.length, 1);
assert.equal(layers[0].type, 'cartodb');
@@ -64,7 +65,7 @@ describe('MapConfigOverviewsAdapter', function() {
}
};
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], function(err, layers) {
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], [], function(err, layers) {
assert.ok(!err);
assert.equal(layers.length, 1);
assert.equal(layers[0].type, 'cartodb');

View File

@@ -83,7 +83,7 @@ if test x"$PREPARE_PGSQL" = xyes; then
cat sql/_CDB_QueryStatements.sql | psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
SQL_SCRIPTS='CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins'
SQL_SCRIPTS='CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ'
for i in ${SQL_SCRIPTS}
do
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/master/scripts-available/$i.sql -o sql/$i.sql

View File

@@ -247,8 +247,10 @@ CREATE TABLE test_table_overviews (
cartodb_id integer NOT NULL,
name character varying,
address character varying,
value float8,
the_geom geometry,
the_geom_webmercator geometry,
_feature_count integer,
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
@@ -274,11 +276,11 @@ SELECT pg_catalog.setval('test_table_overviews_cartodb_id_seq', 60, true);
ALTER TABLE test_table_overviews ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_overviews_cartodb_id_seq'::regclass);
INSERT INTO test_table_overviews VALUES
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 1.0, '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241', 1),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', 2.0, '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241', 1),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', 3.0, '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241', 1),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', 4.0, '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241', 1),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', 5.0, '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241', 1);
ALTER TABLE ONLY test_table_overviews ADD CONSTRAINT test_table_overviews_pkey PRIMARY KEY (cartodb_id);
@@ -294,8 +296,10 @@ CREATE TABLE _vovw_1_test_table_overviews (
cartodb_id integer NOT NULL,
name character varying,
address character varying,
value float8,
the_geom geometry,
the_geom_webmercator geometry,
_feature_count integer,
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
@@ -313,8 +317,10 @@ CREATE TABLE _vovw_2_test_table_overviews (
cartodb_id integer NOT NULL,
name character varying,
address character varying,
value float8,
the_geom geometry,
the_geom_webmercator geometry,
_feature_count integer,
CONSTRAINT enforce_dims_the_geom CHECK ((st_ndims(the_geom) = 2)),
CONSTRAINT enforce_dims_the_geom_webmercator CHECK ((st_ndims(the_geom_webmercator) = 2)),
CONSTRAINT enforce_geotype_the_geom CHECK (((geometrytype(the_geom) = 'POINT'::text) OR (the_geom IS NULL))),
@@ -327,11 +333,11 @@ GRANT ALL ON TABLE _vovw_2_test_table_overviews TO :TESTUSER;
GRANT SELECT ON TABLE _vovw_2_test_table_overviews TO :PUBLICUSER;
INSERT INTO _vovw_2_test_table_overviews VALUES
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E610000000000000009431C026043C75E7224340', '0101000020110F0000C4356B29423319C15DD1092DADCC5241');
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 8.0/3.0, '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241', 3),
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', 7.0/2.0, '0101000020E610000000000000009431C026043C75E7224340', '0101000020110F0000C4356B29423319C15DD1092DADCC5241', 2);
INSERT INTO _vovw_1_test_table_overviews VALUES
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241');
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', 3.0, '0101000020E610000000000000000020C00000000000004440', '0101000020110F000076491621312319C122D4663F1DCC5241', 5);
-- analysis tables -----------------------------------------------

View File

@@ -2,6 +2,7 @@
var qs = require('querystring');
var step = require('step');
var urlParser = require('url');
var mapnik = require('windshaft').mapnik;
@@ -379,6 +380,132 @@ TestClient.prototype.getLayergroup = function(expectedResponse, callback) {
);
};
TestClient.prototype.getNodeStatus = function(nodeName, callback) {
var self = this;
var url = '/api/v1/map';
if (this.apiKey) {
url += '?' + qs.stringify({api_key: this.apiKey});
}
var layergroupId;
var nodes = {};
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);
nodes = parsedBody.metadata.analyses.reduce(function(nodes, analysis) {
return Object.keys(analysis.nodes).reduce(function(nodes, nodeName) {
var node = analysis.nodes[nodeName];
nodes[nodeName] = node.url.http;
return nodes;
}, nodes);
}, nodes);
return next(null, parsedBody.layergroupid);
}
);
},
function getNodeStatusResult(err, _layergroupId) {
assert.ifError(err);
var next = this;
layergroupId = _layergroupId;
url = urlParser.parse(nodes[nodeName]).path;
if (self.apiKey) {
url += '?' + qs.stringify({api_key: self.apiKey});
}
var request = {
url: url,
method: 'GET',
headers: {
host: 'localhost'
}
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
assert.response(server, request, expectedResponse, function(res, err) {
assert.ifError(err);
next(null, res, JSON.parse(res.body));
});
},
function finish(err, res, image) {
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
self.keysToDelete['user:localhost:mapviews:global'] = 5;
return callback(err, res, image);
}
);
};
TestClient.prototype.drain = function(callback) {
helper.deleteRedisKeys(this.keysToDelete, callback);
};
module.exports.getStaticMap = function getStaticMap(templateName, params, callback) {
if (!callback) {
callback = params;
params = null;
}
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
if (params !== null) {
url += '?' + qs.stringify(params);
}
var requestOptions = {
url: url,
method: 'GET',
headers: {
host: 'localhost'
},
encoding: 'binary'
};
var expectedResponse = {
status: 200,
headers: {
'Content-Type': 'image/png'
}
};
// this could be removed once named maps are invalidated, otherwise you hits the cache
var server = new CartodbWindshaft(serverOptions);
assert.response(server, requestOptions, expectedResponse, function (res, err) {
helper.deleteRedisKeys({'user:localhost:mapviews:global': 5}, function() {
return callback(err, mapnik.Image.fromBytes(new Buffer(res.body, 'binary')));
});
});
};

View File

@@ -475,4 +475,137 @@ describe('Overviews query rewriter', function() {
";
assertSameSql(overviews_sql, expected_sql);
});
it('generates query for specific Z level', function(){
var sql = "SELECT * FROM table1";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
}
};
var overviews_sql = overviewsQueryRewriter.query(sql, data, { zoom_level: 3 });
var expected_sql = "SELECT * FROM table1_ov3";
assertSameSql(overviews_sql, expected_sql);
});
it('generates query for specific nonpresent Z level', function(){
var sql = "SELECT * FROM table1";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
}
};
var overviews_sql = overviewsQueryRewriter.query(sql, data, { zoom_level: 1 });
var expected_sql = "SELECT * FROM table1_ov2";
assertSameSql(overviews_sql, expected_sql);
});
it('does not use overviews for specific out-of-range Z level', function(){
var sql = "SELECT * FROM table1";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
}
};
var overviews_sql = overviewsQueryRewriter.query(sql, data, { zoom_level: 4 });
var expected_sql = "SELECT * FROM table1";
assertSameSql(overviews_sql, expected_sql);
});
it('generates query with filters', function(){
var sql = "SELECT ST_Transform(the_geom, 3857) the_geom_webmercator, cartodb_id, name\
FROM (SELECT *\
FROM (select * from table1) _camshaft_category_filter\
WHERE name IN ($escape_0$X$escape_0$)) _cdb_analysis_query";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
1: { table: 'table1_ov1' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
},
filters: { name_filter: { type: 'category', column: 'name', params: { accept: [ 'X' ] } } },
unfiltered_query: 'SELECT * FROM table1'
};
var overviews_sql = overviewsQueryRewriter.query(sql, data);
var expected_sql = "\
SELECT * FROM (WITH\
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z )\
SELECT * FROM (\
SELECT * FROM table1_ov0, _vovw_scale WHERE _vovw_z = 0\
UNION ALL\
SELECT * FROM table1_ov1, _vovw_scale WHERE _vovw_z = 1\
UNION ALL\
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z = 2\
UNION ALL\
SELECT * FROM table1_ov3, _vovw_scale WHERE _vovw_z = 3\
UNION ALL\
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 3\
) AS _vovw_table1) _camshaft_category_filter\
WHERE name IN ($escape_0$X$escape_0$)\
";
assertSameSql(overviews_sql, expected_sql);
});
it('generates query with filters for specific zoom level', function(){
var sql = "SELECT ST_Transform(the_geom, 3857) the_geom_webmercator, cartodb_id, name\
FROM (SELECT *\
FROM (select * from table1) _camshaft_category_filter\
WHERE name IN ($escape_0$X$escape_0$)) _cdb_analysis_query";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
1: { table: 'table1_ov1' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
},
filters: { name_filter: { type: 'category', column: 'name', params: { accept: [ 'X' ] } } },
unfiltered_query: 'SELECT * FROM table1',
filter_stats: { unfiltered_rows: 1000, filtered_rows: 900 }
};
var overviews_sql = overviewsQueryRewriter.query(sql, data, { zoom_level: 2 });
var expected_sql = "\
SELECT * FROM (SELECT * FROM table1_ov2) _camshaft_category_filter\
WHERE name IN ($escape_0$X$escape_0$)\
";
assertSameSql(overviews_sql, expected_sql);
});
it('does not generates query with aggressive filtering', function(){
var sql = "SELECT ST_Transform(the_geom, 3857) the_geom_webmercator, cartodb_id, name\
FROM (SELECT *\
FROM (select * from table1) _camshaft_category_filter\
WHERE name IN ($escape_0$X$escape_0$)) _cdb_analysis_query";
var data = {
overviews: {
table1: {
0: { table: 'table1_ov0' },
1: { table: 'table1_ov1' },
2: { table: 'table1_ov2' },
3: { table: 'table1_ov3' }
}
},
filters: { name_filter: { type: 'category', column: 'name', params: { accept: [ 'X' ] } } },
unfiltered_query: 'SELECT * FROM table1',
filter_stats: { unfiltered_rows: 1000, filtered_rows: 10 }
};
var overviews_sql = overviewsQueryRewriter.query(sql, data);
assert.equal(overviews_sql, sql);
});
});