Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce8c21261f | ||
|
|
dd8340b400 | ||
|
|
b93d33e065 | ||
|
|
4c06c9ade4 | ||
|
|
2393a611a8 | ||
|
|
495fdaf8ec | ||
|
|
da680ec2a8 | ||
|
|
3cadf7f2a2 | ||
|
|
7c7bec6f31 | ||
|
|
0683f638ce | ||
|
|
ae9daed43f | ||
|
|
5301e748de | ||
|
|
37ae6b4fa0 | ||
|
|
6695e1128c | ||
|
|
fb146f164c | ||
|
|
877425267e | ||
|
|
36b7377662 | ||
|
|
f78c6fbc63 | ||
|
|
62e8868e4b | ||
|
|
aed0e03f7d | ||
|
|
3b67efeab1 | ||
|
|
dcfa38e29c | ||
|
|
1c567ec455 | ||
|
|
842fa4dfd2 | ||
|
|
3161939de9 | ||
|
|
4d8b341b6f | ||
|
|
b7fff960a2 | ||
|
|
b6273cfef3 | ||
|
|
b3f62e1631 | ||
|
|
65e539d4c8 | ||
|
|
76653a4417 | ||
|
|
cd81a59418 | ||
|
|
121c146ef9 | ||
|
|
3ab94c7c91 | ||
|
|
a44ed65a0b | ||
|
|
d0f84b2440 | ||
|
|
fe5c6faff1 | ||
|
|
39cb463fbd | ||
|
|
f71d38fb79 | ||
|
|
ba0cf1eddf | ||
|
|
b381e8bad7 | ||
|
|
700335062e | ||
|
|
cd2bc319d8 | ||
|
|
4f8534afb3 | ||
|
|
c5b7d400f5 | ||
|
|
ef58d7bcbd | ||
|
|
bbb8841f5a | ||
|
|
5b50e784cd | ||
|
|
f0af107ffa | ||
|
|
0606fca484 | ||
|
|
e6812ef6c1 | ||
|
|
260e5ec25f | ||
|
|
097f68f98c | ||
|
|
45d72b2bc6 | ||
|
|
82d4c20586 | ||
|
|
03e3f7f13c | ||
|
|
b571b39b38 | ||
|
|
f42d20f2c3 | ||
|
|
74cb876771 | ||
|
|
d78e01b7a4 | ||
|
|
73478ed0e9 | ||
|
|
887d71a9ad | ||
|
|
56095926e0 | ||
|
|
13c3fbae70 | ||
|
|
0b6845235a | ||
|
|
d2558197d2 | ||
|
|
d005521aa4 | ||
|
|
336aaa3840 | ||
|
|
edbdd95f79 | ||
|
|
bb5a8fd0bf | ||
|
|
3284e709c3 | ||
|
|
d33ae29211 | ||
|
|
da51a173d7 | ||
|
|
425ec83209 | ||
|
|
971b77451d | ||
|
|
6407101709 | ||
|
|
0a218da835 | ||
|
|
89033d2cd4 | ||
|
|
870688309a | ||
|
|
a5070162c2 | ||
|
|
8348f74513 | ||
|
|
e1babd05c1 | ||
|
|
08d43a8620 | ||
|
|
8601a67e97 | ||
|
|
640500a0e3 | ||
|
|
6ee1f1a8bf | ||
|
|
a4041524a3 | ||
|
|
c3b17df3e7 | ||
|
|
5e77c50102 | ||
|
|
1620cbc8df | ||
|
|
912b8f6ff4 | ||
|
|
486a55ed7f | ||
|
|
af50af325d | ||
|
|
feef31d1bf | ||
|
|
f5b12d81ed | ||
|
|
81200b72b4 | ||
|
|
d6ecb8c793 | ||
|
|
5038ae6b1b | ||
|
|
37a4aaeeb4 | ||
|
|
6dbb0cb1c1 | ||
|
|
3b6abb5c9f | ||
|
|
ef9e9f8c78 | ||
|
|
2a819e559b | ||
|
|
8d691b2048 | ||
|
|
1f6d5cfd6d | ||
|
|
81cb75f821 | ||
|
|
8592136683 | ||
|
|
18246418a0 | ||
|
|
a6e3b07439 | ||
|
|
c8033700c3 | ||
|
|
77f529d519 | ||
|
|
6532024330 | ||
|
|
62cc53228c | ||
|
|
532654eea8 | ||
|
|
87bffb9657 | ||
|
|
094c9076be | ||
|
|
cc0385d614 | ||
|
|
ed9b3e1230 | ||
|
|
ffd89edaa7 | ||
|
|
5543fcb736 | ||
|
|
7c897a40bf | ||
|
|
528574c550 | ||
|
|
b9f8812c98 | ||
|
|
09568050d6 | ||
|
|
3dad225568 | ||
|
|
4ca8ecf64c | ||
|
|
2f2f6114e8 | ||
|
|
e4e7d6c840 | ||
|
|
d266d9a590 | ||
|
|
1e8525162a | ||
|
|
5fed5afe1f | ||
|
|
8a49e46626 | ||
|
|
9feae66173 | ||
|
|
f8e4deb4b9 | ||
|
|
9c2fe9eac4 | ||
|
|
6aa9515fd1 | ||
|
|
54854f0984 | ||
|
|
89590d32df | ||
|
|
d53a293f28 | ||
|
|
1dea84f9bf | ||
|
|
e72bd77265 | ||
|
|
8b50097a12 | ||
|
|
fbc1bccb48 | ||
|
|
b5b881e662 | ||
|
|
30e479094f | ||
|
|
2b3244440f | ||
|
|
ba4db870e4 | ||
|
|
d3f5b03f13 | ||
|
|
e00661aa34 | ||
|
|
3afb7a0eb3 | ||
|
|
1eb69ae3d1 | ||
|
|
5eeaa0272c | ||
|
|
3e601cc2e6 | ||
|
|
3e9f2a1319 | ||
|
|
45acdd9f39 | ||
|
|
62b1df970a | ||
|
|
1aac4316ab | ||
|
|
97d3bed1d2 | ||
|
|
19216eaa88 | ||
|
|
3780aed1b7 | ||
|
|
04a2d1d33c | ||
|
|
18278da4bb | ||
|
|
553c64bd8b | ||
|
|
74abee2700 | ||
|
|
8ea159e0a1 | ||
|
|
353919239d | ||
|
|
9e74e8633a | ||
|
|
98611be544 | ||
|
|
3b7ff2285c | ||
|
|
8203c878f4 | ||
|
|
216e7b7f1d | ||
|
|
4f4480dc9b | ||
|
|
16e1abe376 | ||
|
|
2d0ebc821f | ||
|
|
67e921017c | ||
|
|
7ed96ef0bb | ||
|
|
6f7bbe4ff5 | ||
|
|
74898e4261 | ||
|
|
c664d5392c | ||
|
|
76cbc2f863 | ||
|
|
69fdaca41f | ||
|
|
5ac327272f | ||
|
|
53a2d52523 | ||
|
|
cd847adfb3 | ||
|
|
e65dc2d790 | ||
|
|
e0d8f5afac | ||
|
|
7d2f543284 | ||
|
|
ad566d8ff0 | ||
|
|
67ba517a0e | ||
|
|
9a01c8b26e | ||
|
|
b40fd29228 | ||
|
|
152440e611 | ||
|
|
ea2a94be88 | ||
|
|
0a38549c55 | ||
|
|
e3b25f3080 | ||
|
|
1de4753daa | ||
|
|
d9614cc1c5 | ||
|
|
37ff13493c | ||
|
|
9264f4a668 | ||
|
|
e254379244 | ||
|
|
4c851a0d09 | ||
|
|
a84dd7cd29 | ||
|
|
86abc392a1 | ||
|
|
9e00cb3309 | ||
|
|
6d55544d70 | ||
|
|
d8e38e7e81 | ||
|
|
3dbcf5f293 | ||
|
|
7d230cc15d | ||
|
|
2320b0b852 | ||
|
|
837f99f31c | ||
|
|
663703abae | ||
|
|
5844c031c9 | ||
|
|
c9e95ccc9d | ||
|
|
98b14de02e | ||
|
|
4bc8c57729 | ||
|
|
479861e612 | ||
|
|
a746445b88 | ||
|
|
2c4cb6d42e | ||
|
|
b2cd421e2e | ||
|
|
8a81828a3d | ||
|
|
8e568d0f20 | ||
|
|
3b9759d5e4 | ||
|
|
a20a1f692b | ||
|
|
628b1921b4 | ||
|
|
fd8131201d | ||
|
|
ee39450864 | ||
|
|
34e866007e | ||
|
|
277c0ee818 | ||
|
|
a3c7b3fc35 | ||
|
|
034b492788 | ||
|
|
cd00680c80 | ||
|
|
cc0ebf70a7 | ||
|
|
8087f838ef | ||
|
|
3fa4f8573e | ||
|
|
3eda1750cc | ||
|
|
2c35e27095 | ||
|
|
90487819bf | ||
|
|
50a943c131 | ||
|
|
36b91180e4 | ||
|
|
df44a84bfc | ||
|
|
e5afdb1e04 | ||
|
|
cf2774c852 | ||
|
|
36369068e1 | ||
|
|
af0812e990 | ||
|
|
0285f015e2 | ||
|
|
473b20596a | ||
|
|
fda405f35d | ||
|
|
4047cb82b9 | ||
|
|
7b57d22444 | ||
|
|
50e63bf83d | ||
|
|
8a9e257cdb | ||
|
|
3a05d8c2e8 | ||
|
|
8b222914c5 | ||
|
|
6d9182aba8 | ||
|
|
f9d3e419a0 | ||
|
|
4b0ecb1251 | ||
|
|
36a6af3266 | ||
|
|
2f592e6f62 | ||
|
|
5e0c9377f2 | ||
|
|
8db1ad6f19 | ||
|
|
52dbe14af2 | ||
|
|
9bc95a6071 | ||
|
|
b80e80bd61 | ||
|
|
59fb1dea54 |
15
.travis.yml
15
.travis.yml
@@ -1,10 +1,17 @@
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
postgresql: "9.4"
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-plpython-9.4
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- sudo apt-get install postgresql-plpython-9.3
|
||||
- npm install -g npm@2
|
||||
- createdb template_postgis
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
|
||||
53
INSTALL.md
Normal file
53
INSTALL.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Installing Windshaft-CartoDB #
|
||||
|
||||
## Requirements ##
|
||||
Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1 <2.0.0
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
On Ubuntu 14.04 the dependencies can be installed with
|
||||
|
||||
```shell
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y make g++ pkg-config git-core \
|
||||
libgif-dev libjpeg-dev libcairo2-dev \
|
||||
libhiredis-dev redis-server \
|
||||
nodejs nodejs-legacy npm \
|
||||
postgresql-9.3-postgis-2.1 postgresql-plpython-9.3 postgresql-server-dev-9.3
|
||||
```
|
||||
|
||||
On Ubuntu 12.04 the [cartodb/cairo PPA](https://launchpad.net/~cartodb/+archive/ubuntu/cairo) may be useful.
|
||||
|
||||
## PostGIS setup ##
|
||||
|
||||
A `template_postgis` database is expected. One can be set up with
|
||||
|
||||
```shell
|
||||
createdb --owner postgres --template template0 template_postgis
|
||||
psql -d template_postgis -c 'CREATE EXTENSION postgis;'
|
||||
```
|
||||
|
||||
## Build/install ##
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Note that the ```npm install``` step will populate the node_modules/
|
||||
directory with modules, some of which being compiled on demand. If you
|
||||
happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
126
NEWS.md
126
NEWS.md
@@ -1,5 +1,117 @@
|
||||
# Changelog
|
||||
|
||||
## 2.25.2
|
||||
|
||||
Released 2016-02-22
|
||||
|
||||
Bug fixes:
|
||||
- Correct URLs for widgets in named maps #381
|
||||
|
||||
|
||||
## 2.25.1
|
||||
|
||||
Released 2016-02-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.11.1](https://github.com/CartoDB/Windshaft/releases/tag/1.11.1)
|
||||
|
||||
|
||||
## 2.25.0
|
||||
|
||||
Released 2016-02-18
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.11.0](https://github.com/CartoDB/Windshaft/releases/tag/1.11.0)
|
||||
|
||||
|
||||
## 2.24.0
|
||||
|
||||
Released 2016-02-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.10.1](https://github.com/CartoDB/Windshaft/releases/tag/1.10.1)
|
||||
|
||||
|
||||
## 2.23.0
|
||||
|
||||
Released 2016-02-10
|
||||
|
||||
Improvements:
|
||||
- Support for overviews
|
||||
|
||||
|
||||
## 2.22.0
|
||||
|
||||
Released 2016-02-08
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.8.3](https://github.com/CartoDB/Windshaft/releases/tag/1.8.3)
|
||||
|
||||
|
||||
## 2.21.1
|
||||
|
||||
Released 2016-02-05
|
||||
|
||||
Bug fixes:
|
||||
- Added default config for geojson renderer
|
||||
|
||||
|
||||
## 2.21.0
|
||||
|
||||
Released 2016-02-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.8.2](https://github.com/CartoDB/Windshaft/releases/tag/1.8.2)
|
||||
|
||||
|
||||
## 2.20.0
|
||||
|
||||
Released 2016-01-20
|
||||
|
||||
Bug fixes:
|
||||
- Change redis pool name to report with a valid statsd key #363
|
||||
|
||||
Improvements:
|
||||
- Query runner improvements #359
|
||||
|
||||
Unsupported:
|
||||
- Widgets endpoints
|
||||
- Layer filters
|
||||
|
||||
Note: API for unsupported list might change in the future, use at your own peril.
|
||||
|
||||
|
||||
## 2.19.1
|
||||
|
||||
Released 2015-11-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.6.1](https://github.com/CartoDB/Windshaft/releases/tag/1.6.1)
|
||||
|
||||
|
||||
## 2.19.0
|
||||
|
||||
Released 2015-11-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.6.0](https://github.com/CartoDB/Windshaft/releases/tag/1.6.0)
|
||||
|
||||
## 2.18.0
|
||||
|
||||
Released 2015-11-02
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.5.0](https://github.com/CartoDB/Windshaft/releases/tag/1.5.0)
|
||||
|
||||
|
||||
## 2.17.0
|
||||
|
||||
Released 2015-10-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.4.0](https://github.com/CartoDB/Windshaft/releases/tag/1.4.0)
|
||||
|
||||
|
||||
## 2.16.0
|
||||
|
||||
Released 2015-10-22
|
||||
@@ -771,7 +883,7 @@ Released 2014-03-10
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Set statsd prefix for all endpoints
|
||||
- Set statsd prefix for all endpoints
|
||||
- Respond with a permission denied on attempt to access map tiles waiving
|
||||
signature of someone who had not left any (#170)
|
||||
- Do not log an error on GET / (#177)
|
||||
@@ -811,7 +923,7 @@ Released 2014-02-27
|
||||
Enhancements:
|
||||
|
||||
- Upgrades windshaft to 0.19.1 with many performance improvements,
|
||||
See node_modules/windshaft/NEWS
|
||||
See node_modules/windshaft/NEWS
|
||||
- Improve speed of instanciating a map (#147, #159, #165)
|
||||
- Give meaningful error on attempts to use map tokens
|
||||
with attribute service (#156)
|
||||
@@ -900,7 +1012,7 @@ Bug fixes:
|
||||
|
||||
Released 2014-01-30
|
||||
|
||||
Bug fixes:
|
||||
Bug fixes:
|
||||
|
||||
* layergroup accept both map_key and api_key (#91)
|
||||
* Fix public instanciation of signed template accessing private data (#114)
|
||||
@@ -1023,7 +1135,7 @@ Released 2013-10-03
|
||||
"[ zoom > 3]" CartoCSS snippets (note the space)
|
||||
* Fix backward compatibility handling of sqlapi.host configuration (#82)
|
||||
* Fix error for invalid text-name in CartoCSS (#81)
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
|
||||
## 1.3.4
|
||||
|
||||
@@ -1070,7 +1182,7 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
* Multilayer API changes
|
||||
* Layers passed by index in grid fetching url
|
||||
* Interactivity only specified in layergroup config
|
||||
* Embed cache_buster within token
|
||||
* Embed cache_buster within token
|
||||
* Use ISO format for last_modified timestamp
|
||||
* Expected LZMA encoding changed to base64
|
||||
|
||||
@@ -1124,7 +1236,7 @@ Released DD//MM//YY
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
* Reduce default extent to allow for consistent proj4 round-tripping
|
||||
* Reduce default extent to allow for consistent proj4 round-tripping
|
||||
* Enhance reset_styles script to use full configuration (#62)
|
||||
* Have reset_styles script also drop extended keys (#58)
|
||||
* Fix example postgis parameter for simplifying input geoms (#63)
|
||||
@@ -1174,7 +1286,7 @@ Released (30/10/12)
|
||||
* Autodetect target mapnik version and let config override it
|
||||
* Add tools/reset_styles script to batch-reset (and optionally convert) styles
|
||||
* Configurable logging format (#4)
|
||||
* Detailed error on missing user metadata
|
||||
* Detailed error on missing user metadata
|
||||
* Properly handle unauthenticated requests for metadata
|
||||
* Accept "api_key" in addition to "map_key",
|
||||
both in query_string and POST body (#38)
|
||||
|
||||
50
README.md
50
README.md
@@ -13,23 +13,9 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
|
||||
* provides a link to varnish high speed cache
|
||||
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See Installing Mapnik.
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
- For running the testsuite
|
||||
- ImageMagick (http://www.imagemagick.org)
|
||||
Install
|
||||
-------
|
||||
See [INSTALL.md](INSTALL.md) for detailed installation instructions.
|
||||
|
||||
Configure
|
||||
---------
|
||||
@@ -40,22 +26,6 @@ see ```./configure --help``` to see available options.
|
||||
|
||||
Look at lib/cartodb/server_options.js for more on config
|
||||
|
||||
Build/install
|
||||
-------------
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```
|
||||
git clone
|
||||
npm install
|
||||
```
|
||||
|
||||
Note that the ```npm install``` step will populate the node_modules/
|
||||
directory with modules, some of which being compiled on demand. If you
|
||||
happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
@@ -93,8 +63,20 @@ Examples
|
||||
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
|
||||
|
||||
Contributing
|
||||
---
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
### Developing with a custom windshaft version
|
||||
|
||||
If you plan or want to use a custom / not released yet version of windshaft (or any other dependency) the best option is
|
||||
to use `npm link`. You can read more about it at [npm-link: Symlink a package folder](https://docs.npmjs.com/cli/link).
|
||||
|
||||
**Quick start**:
|
||||
|
||||
```shell
|
||||
~/windshaft-directory $ npm install
|
||||
~/windshaft-directory $ npm link
|
||||
~/windshaft-cartodb-directory $ npm link windshaft
|
||||
```
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -33,7 +33,7 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
@@ -147,7 +147,24 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -232,7 +249,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
@@ -250,7 +267,10 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
@@ -141,7 +141,24 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -226,7 +243,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:false
|
||||
,serverMetadata: {
|
||||
@@ -250,7 +267,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/maps/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
@@ -141,7 +141,24 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -226,7 +243,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
@@ -250,7 +267,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
@@ -141,6 +141,22 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false // this requires postgis >=2.2 and geos >=3.5
|
||||
}
|
||||
},
|
||||
http: {
|
||||
@@ -228,7 +244,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
@@ -246,7 +262,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
978
docs/Map-API.md
978
docs/Map-API.md
@@ -1,977 +1,19 @@
|
||||
## Maps API
|
||||
# Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles, using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
|
||||
You can create two types of maps with the Maps API:
|
||||
|
||||
- **Anonymous maps**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example](/cartodb-platform/cartodb-js/getting-started/).
|
||||
|
||||
- **Named maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
## Quickstart
|
||||
## Documentation
|
||||
|
||||
### Anonymous maps
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.3.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Named maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type ``man curl`` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl -X POST 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
## General Concepts
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
|
||||
### Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
|
||||
### Errors
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
### CORS support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
|
||||
## Anonymous Maps
|
||||
|
||||
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
|
||||
### Instantiate
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md).
|
||||
|
||||
#### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
- **layergroupid**
|
||||
The ID for that map, used to compose the URL for the tiles. The final URL is:
|
||||
|
||||
```html
|
||||
https://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- **updated_at**
|
||||
The ISO date of the last time the data involved in the query was updated.
|
||||
|
||||
- **metadata**
|
||||
Includes information about the layers.
|
||||
-
|
||||
|
||||
- **cdn_url**
|
||||
URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Retrieve resources from the layergroup
|
||||
|
||||
###### Mapnik tiles can be accessed using:
|
||||
|
||||
These tiles will get just the mapnik layers. To get individual layers see next section.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
###### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `:layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
###### Attributes defined in `attributes` section:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
###### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer_filter/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
|
||||
`:layer_filter` can be used to select some layers to be rendered together. `:layer_filter` supports two formats:
|
||||
|
||||
- `all` alias
|
||||
|
||||
Using `all` as `:layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/0,3,4/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Once a mapnik layer is selected, all mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all mapnik layers if you want to select at least one so you will get a consistent
|
||||
behavior in the future.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
### Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **config**
|
||||
Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
|
||||
- **lmza**
|
||||
This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
|
||||
- **callback**
|
||||
JSON callback name.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## Named Maps
|
||||
|
||||
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
- **auth layer**
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the MapConfig is static it can contain some variables so the client can modify the map's appearance using those variables.
|
||||
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
|
||||
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
<div class="code-title">template.json</div>
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"view": {
|
||||
"zoom": 4,
|
||||
"center": {
|
||||
"lng": 0,
|
||||
"lat": 0
|
||||
},
|
||||
"bounds": {
|
||||
"west": -45,
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-) or underscores (_).
|
||||
- **auth**:
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
- **placeholders**: Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md) for more info.
|
||||
- **view** (optional): extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
- **zoom** The zoom level to use
|
||||
- **center**
|
||||
- **lng** The longitude to use for the center
|
||||
- **lat** The latitude to use for the center
|
||||
- **bounds**
|
||||
- **west**: LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
- **south**: LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
- **east**: UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
|
||||
- **north**: UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
#### Template Format
|
||||
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
|
||||
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
#### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
- **sql_literal** internal single-quotes will be sql-escaped
|
||||
- **sql_ident** internal double-quotes will be sql-escaped
|
||||
- **number** can only contain numerical representation
|
||||
- **css_color** can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with new options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name",
|
||||
}
|
||||
```
|
||||
|
||||
### Instantiate
|
||||
|
||||
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400)
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
#### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
|
||||
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
<div class="code-title">Response</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">Error</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
### Using JSONP
|
||||
|
||||
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
- **callback:** JSON callback name
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
#### Other Info
|
||||
|
||||
Updating a named map removes all the named map instances so they need to be initialized again.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Delete the specified template map from the server and it disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request">REQUEST</div>
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Getting a Specific Template
|
||||
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Use with CartoDB.js
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) has methods for accessing your named maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
## Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
### Maps API endpoints
|
||||
|
||||
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
#### Zoom + center
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:z**: the zoom level of the map
|
||||
* **:lat**: the latitude for the center of the map
|
||||
* **:lng**: the longitude for the center of the map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
#### Bounding Box
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:bbox/:width/:height.:format`
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:bbox**: the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
- LowerCorner longitude, in decimal degrees (aka most western)
|
||||
- LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
- UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
- UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as:
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
|
||||
```
|
||||
|
||||
#### Named map
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/named/:name/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:name**: the name of the named map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
A named maps static image will get its constraints from the [view in the template](#Arguments), if `view` is not present it will estimate the extent based on the involved tables otherwise it fallback to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
####Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
As described in the [Mapconfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
#### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
#### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
#### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
* [Quickstart](quickstart.md)
|
||||
* [General Concepts](general_concepts.md)
|
||||
* [Anonymous Maps](anonymous_maps.md)
|
||||
* [Named Maps](named_maps.md)
|
||||
* [Static Maps API](static_maps_api.md)
|
||||
|
||||
@@ -17,8 +17,8 @@ Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
```json
|
||||
{
|
||||
http: 'http://cdn_url.com/'
|
||||
https: 'https://secure.cdn_url.com/'
|
||||
"http": "http://cdn_url.com/",
|
||||
"https": "https://secure.cdn_url.com/"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
192
docs/anonymous_maps.md
Normal file
192
docs/anonymous_maps.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Anonymous Maps
|
||||
|
||||
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
|
||||
|
||||
## Instantiate
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
See [MapConfig File Formats](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for details.
|
||||
|
||||
#### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
Attributes | Description
|
||||
--- | ---
|
||||
layergroupid | The ID for that map, used to compose the URL for the tiles. The final URL is: `https://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png`
|
||||
updated_at | The ISO date of the last time the data involved in the query was updated.
|
||||
metadata | Includes information about the layers.
|
||||
cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieve resources from the layergroup
|
||||
|
||||
#### Mapnik tiles can be accessed using
|
||||
|
||||
These tiles will get just the mapnik layers. To get individual layers see next section.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
#### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `:layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
#### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer_filter/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
|
||||
`:layer_filter` can be used to select some layers to be rendered together. `:layer_filter` supports two formats:
|
||||
|
||||
- `all` alias
|
||||
|
||||
Using `all` as `:layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/0,3,4/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Once a mapnik layer is selected, all mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all mapnik layers if you want to select at least one so you will get a consistent
|
||||
behavior in the future.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
|
||||
## Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
config | Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
lmza | This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
callback | JSON callback name.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
27
docs/general_concepts.md
Normal file
27
docs/general_concepts.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# General Concepts
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
|
||||
## Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
|
||||
## Errors
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
## CORS support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
475
docs/named_maps.md
Normal file
475
docs/named_maps.md
Normal file
@@ -0,0 +1,475 @@
|
||||
# Named Maps
|
||||
|
||||
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server, and the map is given a unique name. You can create named maps from private data, and users without an API Key can view your Named Map (while keeping your data private). The Named map workflow consists of making a call to your database, referencing a table, inserting your variables into the template where placeholders are defined, and creating custom queries.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
- **auth layer**
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the MapConfig is static it can contain some variables so the client can modify the map's appearance using those variables.
|
||||
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
|
||||
|
||||
**Note:** There is a limit of 4,096 named maps allowed per account. If you need to create more Named maps, it is recommended to use templates.
|
||||
|
||||
## Create
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
#### template.json
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"view": {
|
||||
"zoom": 4,
|
||||
"center": {
|
||||
"lng": 0,
|
||||
"lat": 0
|
||||
},
|
||||
"bounds": {
|
||||
"west": -45,
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
name | There can be at most _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-) or underscores (_).
|
||||
|
||||
auth |
|
||||
--- | ---
|
||||
|_ method | `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
|_ valid_tokens | when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
placeholders | Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
layergroup | the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for more info.
|
||||
|
||||
view (optional) | extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
--- | ---
|
||||
|_ zoom | The zoom level to use
|
||||
|
||||
|_ center |
|
||||
--- | ---
|
||||
|_ |_ lng | The longitude to use for the center
|
||||
|_ |_ lat | The latitude to use for the center
|
||||
|
||||
|_ bounds |
|
||||
--- | ---
|
||||
|_ |_ west | LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
|_ |_ south | LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
|_ |_ east | UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
|
||||
|_ |_ north | UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
### Template Format
|
||||
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
Types | Description
|
||||
--- | ---
|
||||
sql_literal | internal single-quotes will be sql-escaped
|
||||
sql_ident | internal double-quotes will be sql-escaped
|
||||
number | can only contain numerical representation
|
||||
css_color | can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with new options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
#### Call
|
||||
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name",
|
||||
}
|
||||
```
|
||||
|
||||
## Instantiate
|
||||
|
||||
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
auth_token | optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400)
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
|
||||
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
## Using JSONP
|
||||
|
||||
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
auth_token | optional, but required when `"method"` is set to `"token"`
|
||||
config | Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
lmza | This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
callback | JSON callback name
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
## Update
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
### Other Info
|
||||
|
||||
Updating a named map removes all the named map instances so they need to be initialized again.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete
|
||||
|
||||
Delete the specified template map from the server and it disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
## Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## Getting a Specific Template
|
||||
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## Use CartoDB.js to Create Named Maps
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
[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.
|
||||
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
### Complete Examples of Named Maps created with CartoDB.js
|
||||
|
||||
- [Named map selectors with interaction](http://bl.ocks.org/ohasselblad/515a8af1f99d5e690484)
|
||||
|
||||
- [Named map with interactivity and config file used to create it](http://bl.ocks.org/ohasselblad/d1a45b8ff5e7bd90cd68)
|
||||
|
||||
- [Toggling sublayers in a Named Map](http://bl.ocks.org/ohasselblad/c1a0f4913610eec53cd3)
|
||||
100
docs/quickstart.md
Normal file
100
docs/quickstart.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Quickstart
|
||||
|
||||
## Anonymous maps
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.3.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Named maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type `man curl` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
224
docs/static_maps_api.md
Normal file
224
docs/static_maps_api.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
## Maps API endpoints
|
||||
|
||||
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
### Zoom + center
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
:token | the layergroupid token from the map instantiation
|
||||
:z | the zoom level of the map
|
||||
:lat | the latitude for the center of the map
|
||||
|
||||
:format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
### Bounding Box
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:bbox/:width/:height.:format`
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
:token | the layergroupid token from the map instantiation
|
||||
|
||||
:bbox | the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
--- | ---
|
||||
| LowerCorner longitude, in decimal degrees (aka most western)
|
||||
| LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
| UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
| UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
:width | the width in pixels for the output image
|
||||
:height | the height in pixels for the output image
|
||||
:format | the format for the image, supported types: `png`, `jpg`
|
||||
|
||||
:format | the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
|
||||
```
|
||||
|
||||
### Named map
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/named/:name/:width/:height.:format
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
:name | the name of the named map
|
||||
:width | the width in pixels for the output image
|
||||
:height | the height in pixels for the output image
|
||||
:height | the height in pixels for the output image
|
||||
|
||||
:format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
A named maps static image will get its constraints from the [view in the template](#Arguments), if `view` is not present it will estimate the extent based on the involved tables otherwise it fallback to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
#### Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
```javascript
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
```
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
As described in the [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
## Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
44
lib/cartodb/api/overviews_metadata_api.js
Normal file
44
lib/cartodb/api/overviews_metadata_api.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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')
|
||||
;
|
||||
}
|
||||
|
||||
OverviewsMetadataApi.prototype.getOverviewsMetadata = function (username, sql, callback) {
|
||||
var query = 'SELECT * FROM CDB_Overviews(CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$))';
|
||||
this.pgQueryRunner.run(username, query, function handleOverviewsRows(err, rows) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not get overviews metadata: ' + msg));
|
||||
return;
|
||||
}
|
||||
var metadata = {};
|
||||
rows.forEach(function(row) {
|
||||
var table = row.base_table;
|
||||
var table_metadata = metadata[table];
|
||||
if ( !table_metadata ) {
|
||||
table_metadata = metadata[table] = {};
|
||||
}
|
||||
table_metadata[row.z] = { table: row.overview_table };
|
||||
});
|
||||
return callback(null, metadata);
|
||||
});
|
||||
|
||||
};
|
||||
@@ -13,26 +13,22 @@ module.exports = QueryTablesApi;
|
||||
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback);
|
||||
this.pgQueryRunner.run(username, query, function handleAffectedTablesInQueryRows (err, rows) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = rows[0].cdb_querytablestext;
|
||||
return callback(null, tableNames);
|
||||
});
|
||||
};
|
||||
|
||||
function handleAffectedTablesInQueryRows(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = rows[0].cdb_querytablestext;
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, sql, callback) {
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
@@ -42,28 +38,26 @@ QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (usernam
|
||||
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
|
||||
};
|
||||
this.pgQueryRunner.run(username, query, function handleAffectedTablesAndLastUpdatedTimeRows (err, rows) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
var result = rows[0];
|
||||
|
||||
var result = rows[0];
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = result.tablenames;
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = result.tablenames;
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 1000
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 1000
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, callback) {
|
||||
if (!Array.isArray(tableNames) || tableNames.length === 0) {
|
||||
@@ -77,23 +71,21 @@ QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, ca
|
||||
'])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleLastUpdatedTimeRows, callback);
|
||||
this.pgQueryRunner.run(username, query, function handleLastUpdatedTimeRows (err, rows) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var lastUpdated = 0;
|
||||
if (rows.length !== 0) {
|
||||
lastUpdated = rows[0].max || 0;
|
||||
}
|
||||
|
||||
return callback(null, lastUpdated*1000);
|
||||
});
|
||||
};
|
||||
|
||||
function handleLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var lastUpdated = 0;
|
||||
if (rows.length !== 0) {
|
||||
lastUpdated = rows[0].max || 0;
|
||||
}
|
||||
|
||||
return callback(null, lastUpdated*1000);
|
||||
}
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
|
||||
@@ -35,19 +35,17 @@ TablesExtentApi.prototype.getBounds = function (username, tableNames, callback)
|
||||
"FROM ext"
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleBoundsResult, callback);
|
||||
this.pgQueryRunner.run(username, query, function handleBoundsResult (err, rows) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch source tables: ' + msg));
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
function handleBoundsResult(err, rows, callback) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch source tables: ' + msg));
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,14 @@ function PgQueryRunner(pgConnection) {
|
||||
|
||||
module.exports = PgQueryRunner;
|
||||
|
||||
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
/**
|
||||
* Runs `query` with `username`'s PostgreSQL role, callback receives error and rows array.
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} query
|
||||
* @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array
|
||||
*/
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
@@ -33,8 +39,7 @@ PgQueryRunner.prototype.run = function(username, query, queryHandler, callback)
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
queryHandler(err, rows, callback);
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ var util = require('util');
|
||||
// @param opts TemplateMap options. Supported elements:
|
||||
// 'max_user_templates' limit on the number of per-user
|
||||
//
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) {
|
||||
return new TemplateMaps();
|
||||
@@ -41,7 +41,7 @@ function TemplateMaps(redis_pool, opts) {
|
||||
//
|
||||
// Map templates are owned by a user that specifies access permissions
|
||||
// for their instances.
|
||||
//
|
||||
//
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User templates: set of per-user map templates
|
||||
@@ -197,7 +197,7 @@ function templateDefaults(template) {
|
||||
// @param template layergroup template, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err, tpl_id)
|
||||
// @param callback function(err, tpl_id)
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
@@ -224,8 +224,10 @@ o.addTemplate = function(owner, template, callback) {
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
assert.ifError(err);
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
var limitReachedError = new Error("User '" + owner + "' reached limit on number of templates (" +
|
||||
numberOfTemplates + "/" + limit + ")");
|
||||
limitReachedError.http_status = 409;
|
||||
throw limitReachedError;
|
||||
}
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
@@ -294,7 +296,7 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
var self = this;
|
||||
|
||||
@@ -353,7 +355,7 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
//
|
||||
// @param callback function(err, tpl_id_list)
|
||||
// Returns a list of template identifiers
|
||||
//
|
||||
//
|
||||
o.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
};
|
||||
@@ -420,7 +422,7 @@ o.isAuthorized = function(template, authTokens) {
|
||||
// Only the ones found in the template's placeholders object
|
||||
// will be used, with missing ones taking default values.
|
||||
//
|
||||
// @returns a layergroup configuration
|
||||
// @returns a layergroup configuration
|
||||
//
|
||||
// @throws Error on malformed template or parameter
|
||||
//
|
||||
@@ -429,7 +431,7 @@ var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
function _replaceVars (str, params) {
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// Construct regular expressions for each param
|
||||
Object.keys(params).forEach(function(k) {
|
||||
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
|
||||
|
||||
@@ -13,7 +13,17 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
'callback',
|
||||
// widgets & filters
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'bins', // number
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
// widgets search
|
||||
'q'
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
@@ -191,7 +201,7 @@ BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
debug('[%s ERROR] -- %d: %s', label, statusCode, err);
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
@@ -229,6 +239,7 @@ module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
@@ -242,6 +253,8 @@ function statusFromErrorMessage(errMsg) {
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {WidgetBackend} widgetBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
@@ -24,13 +25,14 @@ var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
|
||||
widgetBackend, surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.widgetBackend = widgetBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
@@ -66,6 +68,72 @@ LayergroupController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
|
||||
this.bbox.bind(this));
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:widgetName', cors(), userMiddleware,
|
||||
this.widget.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
|
||||
this.widgetSearch.bind(this));
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widget = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.getWidget(mapConfigProvider, req.params, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widgetSearch = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.search(mapConfigProvider, req.params, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res) {
|
||||
|
||||
@@ -18,6 +18,7 @@ var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider');
|
||||
var MapConfigOverviewsAdapter = require('../models/mapconfig_overviews_adapter');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -26,12 +27,14 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_laye
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
* @param {OverviewsMetadataApi} overviewsMetadataApi
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi,
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
queryTablesApi, overviewsMetadataApi,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
@@ -41,11 +44,13 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.overviewsMetadataApi = overviewsMetadataApi;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
this.overviewsAdapter = new MapConfigOverviewsAdapter(this.overviewsMetadataApi);
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
@@ -148,6 +153,22 @@ 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);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
requestMapConfig.layers = layers;
|
||||
}
|
||||
return next(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
},
|
||||
function createLayergroup(err, requestMapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource || Datasource.EmptyDatasource());
|
||||
@@ -165,6 +186,8 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
addWidgetsUrl(req.context.user, layergroup);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
@@ -203,7 +226,23 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams/*, context*/) {
|
||||
function addOverviewsInformation(err, requestMapConfig, rendererParams/*, context*/) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.overviewsAdapter.getLayers(req.context.user, requestMapConfig.layers,
|
||||
function(err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
requestMapConfig.layers = layers;
|
||||
}
|
||||
return next(null, requestMapConfig, rendererParams);
|
||||
}
|
||||
);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
@@ -223,6 +262,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
addWidgetsUrl(req.context.user, layergroup);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
|
||||
@@ -324,3 +365,34 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addWidgetsUrl(username, layergroup) {
|
||||
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (layer.widgets) {
|
||||
Object.keys(layer.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName].url = getUrls(username, resource);
|
||||
});
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getUrls(username, resource) {
|
||||
var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url;
|
||||
if (cdnUrl) {
|
||||
return {
|
||||
http: 'http://' + cdnUrl.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnUrl.https + '/' + username + '/api/v1/map/' + resource
|
||||
};
|
||||
} else {
|
||||
var port = global.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
53
lib/cartodb/models/mapconfig_overviews_adapter.js
Normal file
53
lib/cartodb/models/mapconfig_overviews_adapter.js
Normal file
@@ -0,0 +1,53 @@
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
|
||||
function MapConfigOverviewsAdapter(overviewsMetadataApi) {
|
||||
this.overviewsMetadataApi = overviewsMetadataApi;
|
||||
}
|
||||
|
||||
module.exports = MapConfigOverviewsAdapter;
|
||||
|
||||
MapConfigOverviewsAdapter.prototype.getLayers = function(username, layers, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, layers);
|
||||
}
|
||||
|
||||
var augmentLayersQueue = queue(layers.length);
|
||||
|
||||
function augmentLayer(layer, done) {
|
||||
if ( layer.type !== 'mapnik' && layer.type !== 'cartodb' ) {
|
||||
return done(null, layer);
|
||||
}
|
||||
self.overviewsMetadataApi.getOverviewsMetadata(username, layer.options.sql, function(err, metadata){
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function layersAugmentQueueFinish(err, layers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(new Error('Missing layers array from layergroup config'));
|
||||
}
|
||||
|
||||
return callback(null, layers);
|
||||
}
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
augmentLayersQueue.defer(augmentLayer, layer);
|
||||
});
|
||||
augmentLayersQueue.awaitAll(layersAugmentQueueFinish);
|
||||
|
||||
};
|
||||
@@ -20,6 +20,7 @@ var mapnik = windshaft.mapnik;
|
||||
|
||||
var TemplateMaps = require('./backends/template_maps.js');
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var OverviewsMetadataApi = require('./api/overviews_metadata_api');
|
||||
var UserLimitsApi = require('./api/user_limits_api');
|
||||
var AuthApi = require('./api/auth_api');
|
||||
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
|
||||
@@ -36,7 +37,7 @@ module.exports = function(serverOptions) {
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
|
||||
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
|
||||
name: 'windshaft:server',
|
||||
name: 'windshaft-server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}));
|
||||
@@ -52,6 +53,7 @@ module.exports = function(serverOptions) {
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
@@ -131,7 +133,7 @@ module.exports = function(serverOptions) {
|
||||
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
var attributesBackend = new windshaft.backend.Attributes(mapStore);
|
||||
var attributesBackend = new windshaft.backend.Attributes();
|
||||
var previewBackend = new windshaft.backend.Preview(rendererCache);
|
||||
var tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
@@ -161,6 +163,7 @@ module.exports = function(serverOptions) {
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
new windshaft.backend.Widget(),
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
queryTablesApi,
|
||||
@@ -174,6 +177,7 @@ module.exports = function(serverOptions) {
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
queryTablesApi,
|
||||
overviewsMetadataApi,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
var os = require('os');
|
||||
var _ = require('underscore');
|
||||
var OverviewsQueryRewriter = require('./utils/overviews_query_rewriter');
|
||||
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
@@ -15,6 +20,8 @@ var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
http: {}
|
||||
});
|
||||
|
||||
rendererConfig.mapnik.queryRewriter = overviewsQueryRewriter;
|
||||
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
@@ -61,7 +68,16 @@ module.exports = {
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mapnik: rendererConfig.mapnik,
|
||||
mapnik: _.defaults(rendererConfig.mapnik, {
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
size: 16,
|
||||
idleTimeout: 3000,
|
||||
reapInterval: 1000
|
||||
},
|
||||
clipByBox2d: false,
|
||||
}
|
||||
}),
|
||||
torque: rendererConfig.torque,
|
||||
http: rendererConfig.http
|
||||
},
|
||||
|
||||
182
lib/cartodb/utils/overviews_query_rewriter.js
Normal file
182
lib/cartodb/utils/overviews_query_rewriter.js
Normal file
@@ -0,0 +1,182 @@
|
||||
var TableNameParser = require('./table_name_parser');
|
||||
|
||||
function OverviewsQueryRewriter(options) {
|
||||
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
module.exports = OverviewsQueryRewriter;
|
||||
|
||||
// TODO: some names are introudced in the queries, and the
|
||||
// '_vovw_' (for vector overviews) is used in them, but no check
|
||||
// is performed for conflicts with existing identifiers in the query.
|
||||
|
||||
// Build UNION expression to replace table, using overviews metadata
|
||||
// overviews metadata: { 1: 'table_ov1', ... }
|
||||
// assume table and overview names include schema if necessary and are quoted as needed
|
||||
function overviews_view_for_table(table, overviews_metadata, indent) {
|
||||
var condition, i, len, ov_table, overview_layers, selects, z_hi, z_lo;
|
||||
var parsed_table = TableNameParser.parse(table);
|
||||
|
||||
var sorted_overviews = []; // [[1, 'table_ov1'], ...]
|
||||
|
||||
indent = indent || ' ';
|
||||
for (var z in overviews_metadata) {
|
||||
if (overviews_metadata.hasOwnProperty(z)) {
|
||||
sorted_overviews.push([z, overviews_metadata[z].table]);
|
||||
}
|
||||
}
|
||||
sorted_overviews.sort(function(a, b){ return a[0]-b[0]; });
|
||||
|
||||
overview_layers = [];
|
||||
z_lo = null;
|
||||
for (i = 0, len = sorted_overviews.length; i < len; i++) {
|
||||
z_hi = parseInt(sorted_overviews[i][0]);
|
||||
ov_table = sorted_overviews[i][1];
|
||||
overview_layers.push([overview_z_condition(z_lo, z_hi), ov_table]);
|
||||
z_lo = z_hi;
|
||||
}
|
||||
overview_layers.push(["_vovw_z > " + z_lo, table]);
|
||||
|
||||
selects = overview_layers.map(function(condition_table) {
|
||||
condition = condition_table[0];
|
||||
ov_table = TableNameParser.parse(condition_table[1]);
|
||||
ov_table.schema = ov_table.schema || parsed_table.schema;
|
||||
var ov_identifier = TableNameParser.table_identifier(ov_table);
|
||||
return indent + "SELECT * FROM " + ov_identifier + ", _vovw_scale WHERE " + condition;
|
||||
});
|
||||
|
||||
return selects.join("\n"+indent+"UNION ALL\n");
|
||||
}
|
||||
|
||||
function overview_z_condition(z_lo, z_hi) {
|
||||
if (z_lo !== null) {
|
||||
if (z_lo === z_hi - 1) {
|
||||
return "_vovw_z = " + z_hi;
|
||||
} else {
|
||||
return "_vovw_z > " + z_lo + " AND _vovw_z <= " + z_hi;
|
||||
}
|
||||
} else {
|
||||
if (z_hi === 0) {
|
||||
return "_vovw_z = " + z_hi;
|
||||
} else {
|
||||
return "_vovw_z <= " + z_hi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// name to be used for the view of the table using overviews
|
||||
function overviews_view_name(table) {
|
||||
var parsed_table = TableNameParser.parse(table);
|
||||
parsed_table.table = '_vovw_' + parsed_table.table;
|
||||
parsed_table.schema = null;
|
||||
return TableNameParser.table_identifier(parsed_table);
|
||||
}
|
||||
|
||||
// replace a table name in a query by anoter name
|
||||
function replace_table_in_query(sql, old_table_name, new_table_name) {
|
||||
var old_table = TableNameParser.parse(old_table_name);
|
||||
var new_table = TableNameParser.parse(new_table_name);
|
||||
var old_table_ident = TableNameParser.table_identifier(old_table);
|
||||
var new_table_ident = TableNameParser.table_identifier(new_table);
|
||||
|
||||
// text that will be substituted by the table name pattern
|
||||
var replacement = new_table_ident;
|
||||
|
||||
// regular expression prefix (beginning) to match a table name
|
||||
function pattern_prefix(schema, identifier) {
|
||||
if ( schema ) {
|
||||
// to match a table name including schema prefix
|
||||
// name should not be part of another name, so we require
|
||||
// to start a at a word boundary
|
||||
if ( identifier[0] !== '"' ) {
|
||||
return '\\b';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// to match a table name without schema
|
||||
// name should not begin right after a dot (i.e. have a explicit schema)
|
||||
// nor be part of another name
|
||||
// since the pattern matches the first character of the table
|
||||
// it must be put back in the replacement text
|
||||
replacement = '$01'+replacement;
|
||||
return '([^\.a-z0-9_]|^)';
|
||||
}
|
||||
}
|
||||
|
||||
// regular expression suffix (ending) to match a table name
|
||||
function pattern_suffix(identifier) {
|
||||
// name shouldn't be the prefix of a longer name
|
||||
if ( identifier[identifier.length-1] !== '"' ) {
|
||||
return '\\b';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// regular expression to match a table name
|
||||
var regexp = pattern_prefix(old_table.schema, old_table_ident) +
|
||||
old_table_ident +
|
||||
pattern_suffix(old_table_ident);
|
||||
|
||||
// replace all occurrences of the table pattern
|
||||
return sql.replace(new RegExp(regexp, 'g'), replacement);
|
||||
}
|
||||
|
||||
function overviews_query(query, overviews, zoom_level_expression) {
|
||||
var replaced_query = query;
|
||||
var sql = "WITH\n _vovw_scale AS ( SELECT " + zoom_level_expression + " AS _vovw_z )";
|
||||
for ( var table in overviews ) {
|
||||
if (overviews.hasOwnProperty(table)) {
|
||||
var table_overviews = overviews[table];
|
||||
var table_view = overviews_view_name(table);
|
||||
replaced_query = replace_table_in_query(replaced_query, table, table_view);
|
||||
sql += ",\n " + table_view + " AS (\n" + overviews_view_for_table(table, table_overviews) + "\n )";
|
||||
}
|
||||
}
|
||||
if ( replaced_query !== query ) {
|
||||
sql += "\n";
|
||||
sql += replaced_query;
|
||||
} else {
|
||||
sql = query;
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
// 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
|
||||
//
|
||||
// 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)) {
|
||||
return query;
|
||||
}
|
||||
var zoom_level_expression = this.options.zoom_level || '0';
|
||||
return overviews_query(query, overviews, zoom_level_expression);
|
||||
};
|
||||
|
||||
OverviewsQueryRewriter.prototype.is_supported_query = function(sql) {
|
||||
return !!sql.match(
|
||||
/^\s*SELECT\s+[\*\.a-z0-9_,\s]+?\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*$/i
|
||||
);
|
||||
};
|
||||
|
||||
OverviewsQueryRewriter.prototype.overviews_metadata = function(data) {
|
||||
return data && data.overviews;
|
||||
};
|
||||
106
lib/cartodb/utils/table_name_parser.js
Normal file
106
lib/cartodb/utils/table_name_parser.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// Quote an PostgreSQL identifier if ncecessary
|
||||
function quote_identifier_if_needed(txt) {
|
||||
if ( txt && !txt.match(/^[a-z_][a-z_0-9]*$/)) {
|
||||
return '"' + txt.replace(/\"/g, '""') + '"';
|
||||
} else {
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PostgreSQL table name (possibly quoted and with optional schema).+
|
||||
// Returns { schema: 'schema_name', table: 'table_name' }
|
||||
function parse_table_name(table) {
|
||||
|
||||
function split_as_quoted_parts(table_name) {
|
||||
// parse table into 'parts' that may be quoted, each part
|
||||
// in the parts array being an object { part: 'text', quoted: false/true }
|
||||
var parts = [];
|
||||
var splitted = table_name.split(/\"/);
|
||||
for (var i=0; i<splitted.length; i++ ) {
|
||||
if ( splitted[i] === '' ) {
|
||||
if ( parts.length > 0 && i < splitted.length-1 ) {
|
||||
i++;
|
||||
parts[parts.length - 1].part += '"' + splitted[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
var is_quoted = (i > 0 && splitted[i-1] === '') ||
|
||||
(i < splitted.length - 1 && splitted[i+1] === '');
|
||||
parts.push({ part: splitted[i], quoted: is_quoted });
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
var parts = split_as_quoted_parts(table);
|
||||
|
||||
function split_single_part(part) {
|
||||
var schema_part = null;
|
||||
var table_part = null;
|
||||
if ( part.quoted ) {
|
||||
table_part = part.part;
|
||||
} else {
|
||||
var parts = part.part.split('.');
|
||||
if ( parts.length === 1 ) {
|
||||
schema_part = null;
|
||||
table_part = parts[0];
|
||||
} else if ( parts.length === 2 ) {
|
||||
schema_part = parts[0];
|
||||
table_part = parts[1];
|
||||
} // else invalid table name
|
||||
}
|
||||
return {
|
||||
schema: schema_part,
|
||||
table: table_part
|
||||
};
|
||||
}
|
||||
|
||||
function split_two_parts(part1, part2) {
|
||||
var schema_part = null;
|
||||
var table_part = null;
|
||||
if ( part1.quoted && !part2.quoted ) {
|
||||
if ( part2.part[0] === '.' ) {
|
||||
schema_part = part1.part;
|
||||
table_part = part2.part.slice(1);
|
||||
} // else invalid table name (missing dot)
|
||||
} else if ( !part1.quoted && part2.quoted ) {
|
||||
if ( part1.part[part1.part.length - 1] === '.' ) {
|
||||
schema_part = part1.part.slice(0, -1);
|
||||
table_part = part2.part;
|
||||
} // else invalid table name (missing dot)
|
||||
} // else invalid table name (missing dot)
|
||||
return {
|
||||
schema: schema_part,
|
||||
table: table_part
|
||||
};
|
||||
}
|
||||
|
||||
if ( parts.length === 1 ) {
|
||||
return split_single_part(parts[0]);
|
||||
} else if ( parts.length === 2 ) {
|
||||
return split_two_parts(parts[0], parts[1]);
|
||||
} else if ( parts.length === 3 && parts[1].part === '.' ) {
|
||||
return {
|
||||
schema: parts[0].part,
|
||||
table: parts[2].part
|
||||
};
|
||||
} // else invalid table name
|
||||
}
|
||||
|
||||
function table_identifier(parsed_name) {
|
||||
if ( parsed_name && parsed_name.table ) {
|
||||
if ( parsed_name.schema ) {
|
||||
return quote_identifier_if_needed(parsed_name.schema) + '.' + quote_identifier_if_needed(parsed_name.table);
|
||||
} else {
|
||||
return quote_identifier_if_needed(parsed_name.table);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parse: parse_table_name,
|
||||
quote: quote_identifier_if_needed,
|
||||
table_identifier: table_identifier
|
||||
};
|
||||
2455
npm-shrinkwrap.json
generated
2455
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,16 +1,13 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.16.0",
|
||||
"version": "2.25.2",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"licenses": [{
|
||||
"type": "BSD",
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb/blob/master/LICENCE"
|
||||
}],
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
|
||||
@@ -29,7 +26,7 @@
|
||||
"node-statsd": "~0.0.7",
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "~1.2.0",
|
||||
"windshaft": "1.11.1",
|
||||
"step": "~0.0.6",
|
||||
"queue-async": "~1.0.7",
|
||||
"request": "~2.62.0",
|
||||
@@ -56,6 +53,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=1.2.1"
|
||||
"npm": ">=2.14.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,6 @@ describe('health checks', function () {
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -292,7 +292,8 @@ describe('render limits', function() {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/render-timeout-fallback.png', 25,
|
||||
var referenceImagePath = './test/fixtures/render-timeout-fallback.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
|
||||
@@ -52,14 +52,14 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2 offset 2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -104,7 +104,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
|
||||
@@ -122,7 +122,7 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
}
|
||||
@@ -190,38 +190,50 @@ describe(suiteName, function() {
|
||||
});
|
||||
|
||||
|
||||
it("should include serverMedata in the response", function(done) {
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
describe('server-metadata', function() {
|
||||
var serverMetadata;
|
||||
beforeEach(function() {
|
||||
serverMetadata = global.environment.serverMetadata;
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.environment.serverMetadata = serverMetadata;
|
||||
});
|
||||
|
||||
it("should include serverMedata in the response", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -245,7 +257,7 @@ describe(suiteName, function() {
|
||||
]
|
||||
};
|
||||
|
||||
var expected_token;
|
||||
var expected_token;
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
@@ -253,7 +265,7 @@ describe(suiteName, function() {
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
@@ -329,7 +341,7 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!))' +
|
||||
' as the_geom_webmercator from test_table limit 1',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -392,7 +404,8 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = 'test/fixtures/test_multilayer_bbox.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err/*, similarity*/) {
|
||||
next(err);
|
||||
});
|
||||
@@ -413,7 +426,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
|
||||
@@ -431,7 +444,8 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = 'test/fixtures/test_multilayer_bbox.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err/*, similarity*/) {
|
||||
next(err);
|
||||
});
|
||||
@@ -485,8 +499,8 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
' ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -576,10 +590,10 @@ describe(suiteName, function() {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h' +
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fit:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fit:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -606,8 +620,8 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select bogus(0,0) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -617,7 +631,7 @@ describe(suiteName, function() {
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ": " + res.body);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ": " + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/bogus.*exist/), msg);
|
||||
@@ -633,13 +647,13 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table_private_1 where cartodb_id=1',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select * from test_table_private_1 where cartodb_id=2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -684,7 +698,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
next(err);
|
||||
@@ -780,7 +794,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table where cartodb_id=1',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -830,7 +844,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc, "Missing X-Cache-Channel");
|
||||
assert.ok(cc, "Missing X-Cache-Channel");
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
return null;
|
||||
@@ -965,7 +979,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator",
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1007,7 +1021,7 @@ describe(suiteName, function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
}
|
||||
@@ -1031,7 +1045,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1105,7 +1119,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: sql,
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1194,7 +1208,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select *, 'SQLAPINOANSWER' from test_table",
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0'
|
||||
} }
|
||||
]
|
||||
@@ -1318,7 +1332,40 @@ describe(suiteName, function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should response to empty layers mapconfig', function(done) {
|
||||
var layergroup = {
|
||||
layers: []
|
||||
};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -310,8 +310,8 @@ describe('tests from old api translated to multilayer', function() {
|
||||
it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('fake error message'), [], callback);
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
return callback(new Error('fake error message'), []);
|
||||
};
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
@@ -332,13 +332,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
// TODO when affected tables query makes the request to fail layergroup should be removed
|
||||
keysToDelete['map_cfg|4fb7bd7008322ce66f22d20aebba1ab0'] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["Error: could not fetch affected tables or last updated time: fake error message"]
|
||||
errors: ["could not get overviews metadata: fake error message"]
|
||||
});
|
||||
|
||||
done();
|
||||
@@ -365,8 +361,8 @@ describe('tests from old api translated to multilayer', function() {
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
return callback(new Error('failed to query database for affected tables'), []);
|
||||
};
|
||||
|
||||
// reset internal cacheChannel cache
|
||||
|
||||
110
test/acceptance/overviews_metadata.js
Normal file
110
test/acceptance/overviews_metadata.js
Normal file
@@ -0,0 +1,110 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
|
||||
|
||||
describe('overviews metadata', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table_overviews',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var non_overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
test_helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it("layers with and without overviews", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [overviews_layer, non_overviews_layer]
|
||||
};
|
||||
|
||||
var layergroup_url = '/api/v1/map';
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url,
|
||||
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.deepEqual(non_overviews_layer, mapConfig._cfg.layers[1]);
|
||||
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: {
|
||||
1: { table: '_vovw_1_test_table_overviews' },
|
||||
2: { table: '_vovw_2_test_table_overviews' }
|
||||
}
|
||||
}
|
||||
};
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
73
test/acceptance/overviews_queries.js
Normal file
73
test/acceptance/overviews_queries.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
var assert = require('../support/assert');
|
||||
|
||||
var cartodbServer = require('../../lib/cartodb/server');
|
||||
var ServerOptions = require('./ported/support/ported_server_options');
|
||||
var testClient = require('./ported/support/test_client');
|
||||
var BaseController = require('../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('overviews_queries', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
function imageCompareFn(fixture, done) {
|
||||
return function(err, tile) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var referenceImagePath = './test/fixtures/' + fixture;
|
||||
assert.imageBufferIsSimilarToFile(tile.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
it("should not use overview for tables without overviews", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 1, 0, 0,
|
||||
imageCompareFn('test_table_1_0_0.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for tables without overviews at z=2", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 2, 1, 1,
|
||||
imageCompareFn('test_table_2_1_1.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for tables without overviews at z=2", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 3, 3, 3,
|
||||
imageCompareFn('test_table_3_3_3.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should use overview for zoom level 1", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 1, 0, 0,
|
||||
imageCompareFn('_vovw_1_test_table_1_0_0.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should use overview for zoom level 1", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 2, 1, 1,
|
||||
imageCompareFn('_vovw_2_test_table_2_1_1.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for zoom level 3", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 3, 3, 3,
|
||||
imageCompareFn('test_table_3_3_3.png', done)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -95,10 +95,12 @@ describe('blend png renderer', function() {
|
||||
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,10 +156,12 @@ describe('blend layer filtering', function() {
|
||||
|
||||
it('should filter on ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('blend http fallback', function() {
|
||||
};
|
||||
|
||||
var filteredLayersSuite = [
|
||||
['all'], // layers displayed: 2 + 4, skipping 3 as it fails
|
||||
//['all'], // layers displayed: 2 + 4, skipping 3 as it fails
|
||||
[0, 4],
|
||||
[0, 3], // skips layer 3 as it fails
|
||||
[1, 2],
|
||||
@@ -111,10 +111,12 @@ describe('blend http fallback', function() {
|
||||
|
||||
it('should fallback on http error while blending layers ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,8 @@ describe('external resources', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
var referenceImagePath = './test/fixtures/' + fixture;
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +84,13 @@ describe.skip('render limits', function() {
|
||||
testClient.withLayergroup(slowQueryMapConfig, options, function(err, requestTile, finish) {
|
||||
var tileUrl = '/0/0/0.png';
|
||||
requestTile(tileUrl, options, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +117,8 @@ describe('multilayer', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
checkCORSHeaders(res);
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_bigpoint_red.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = './test/fixtures/test_bigpoint_red.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -191,7 +192,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -302,7 +304,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -400,12 +403,11 @@ describe('multilayer', function() {
|
||||
function jsonp_test(body) {
|
||||
assert.ok(body.layergroupid);
|
||||
expected_token = LayergroupToken.parse(body.layergroupid).token;
|
||||
assert.deepEqual(body.metadata, {
|
||||
layers: [
|
||||
{ type: "mapnik", "meta":{} },
|
||||
{ type: "mapnik", "meta":{} }
|
||||
]
|
||||
});
|
||||
assert.ok(body.metadata.layers.length === 2);
|
||||
assert.ok(body.metadata.layers[0].type === 'mapnik');
|
||||
assert.ok(body.metadata.layers[0].meta);
|
||||
assert.ok(body.metadata.layers[1].type === 'mapnik');
|
||||
assert.ok(body.metadata.layers[1].meta);
|
||||
didRunJsonCallback = true;
|
||||
}
|
||||
eval(res.body);
|
||||
@@ -426,7 +428,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -542,7 +545,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -728,7 +732,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -762,7 +767,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -857,7 +863,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -1260,7 +1267,7 @@ describe('multilayer', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
checkCORSHeaders(res);
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_bigpoint_red.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, './test/fixtures/test_bigpoint_red.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -1278,4 +1285,3 @@ describe('multilayer', function() {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -64,46 +64,6 @@ describe('multilayer error cases', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft/issues/154
|
||||
it("mapnik tokens cannot be used with attributes service", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, 1 as n, the_geom, !bbox! as b from test_table limit 1',
|
||||
cartocss: '#layer { marker-fill:red }',
|
||||
cartocss_version: '2.0.1',
|
||||
attributes: { id:'cartodb_id', columns:['n'] },
|
||||
geom_column: 'the_geom'
|
||||
} }
|
||||
]
|
||||
};
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check(err, res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/Attribute service cannot be activated/), msg);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("layergroup with no cartocss_version", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -374,4 +374,3 @@ describe('multilayer interactivity and layers order', function() {
|
||||
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('raster', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(res.headers['content-type'], "image/png");
|
||||
var next = this;
|
||||
assert.imageEqualsFile(res.body,
|
||||
assert.imageBufferIsSimilarToFile(res.body,
|
||||
'./test/fixtures/raster_gray_rect.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,9 @@ describe('server_gettile', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
assert.imageBufferIsSimilarToFile(
|
||||
res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,12 +115,13 @@ describe('server_gettile', function() {
|
||||
assert.ok(res.headers.hasOwnProperty('x-windshaft-cache'), "Did not hit renderer cache on second time");
|
||||
assert.ok(res.headers['x-windshaft-cache'] >= 0);
|
||||
|
||||
assert.imageEqualsFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('server_png8_format', function() {
|
||||
assert.equal(responsePng8.headers['content-type'], "image/png");
|
||||
bufferPng8 = responsePng8.body;
|
||||
assert.ok(bufferPng8.length < bufferPng32.length);
|
||||
assert.imageBuffersAreEqual(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
assert.imageBuffersAreSimilar(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, imagePaths, similarity) {
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
|
||||
@@ -2,6 +2,10 @@ var _ = require('underscore');
|
||||
var serverOptions = require('../../../../lib/cartodb/server_options');
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var OverviewsQueryRewriter = require('../../../../lib/cartodb/utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
module.exports = _.extend({}, serverOptions, {
|
||||
base_url: '/database/:dbname/table/:table',
|
||||
@@ -26,7 +30,8 @@ module.exports = _.extend({}, serverOptions, {
|
||||
limits: {
|
||||
render: 0,
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
queryRewriter: overviewsQueryRewriter
|
||||
},
|
||||
http: {
|
||||
timeout: 5000,
|
||||
|
||||
@@ -216,8 +216,10 @@ describe('torque', function() {
|
||||
assert.ok(tm0,
|
||||
'No layer 0 in "torque" in metadata:' + JSON.stringify(tm));
|
||||
var expectedTorqueMetadata = {"start":0,"end":86400000,"data_steps":2,"column_type":"date"};
|
||||
assert.deepEqual(tm0, expectedTorqueMetadata);
|
||||
assert.deepEqual(meta.layers[0].meta, expectedTorqueMetadata);
|
||||
Object.keys(expectedTorqueMetadata).forEach(function(k) {
|
||||
assert.equal(tm0[k], expectedTorqueMetadata[k]);
|
||||
assert.equal(meta.layers[0].meta[k], expectedTorqueMetadata[k]);
|
||||
});
|
||||
return null;
|
||||
},
|
||||
function do_get_tile(err)
|
||||
@@ -416,7 +418,7 @@ describe('torque', function() {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
|
||||
@@ -83,10 +83,12 @@ describe('torque png renderer', function() {
|
||||
var zxy = [z, x, y];
|
||||
it('tile ' + zxy.join('/') + '.torque.png', function (done) {
|
||||
testClient.getTileLayer(torquePngPointsMapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,10 +107,12 @@ describe('wrap x coordinate', function() {
|
||||
var fixtureZxy = [testScenario.fixture.z, testScenario.fixture.x, testScenario.fixture.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ describe('template_api', function() {
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2 offset 2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -313,51 +313,63 @@ describe('template_api', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("instance endpoint should return server metadata", function(done){
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
var tmpl = _.clone(template_acceptance1);
|
||||
tmpl.name = "rambotemplate2";
|
||||
describe('server-metadata', function() {
|
||||
var serverMetadata;
|
||||
beforeEach(function() {
|
||||
serverMetadata = global.environment.serverMetadata;
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
});
|
||||
|
||||
step(function postTemplate1() {
|
||||
var next = this;
|
||||
var post_request = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(tmpl)
|
||||
};
|
||||
assert.response(server, post_request, {}, function(res) {
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function testCORS() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + tmpl.name,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
},{
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
next(null);
|
||||
});
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
var del_request = {
|
||||
url: '/api/v1/map/named/' + tmpl.name + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
};
|
||||
assert.response(server, del_request, {}, function() {
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
afterEach(function() {
|
||||
global.environment.serverMetadata = serverMetadata;
|
||||
});
|
||||
|
||||
|
||||
it("instance endpoint should return server metadata", function(done){
|
||||
var tmpl = _.clone(template_acceptance1);
|
||||
tmpl.name = "rambotemplate2";
|
||||
|
||||
step(function postTemplate1() {
|
||||
var next = this;
|
||||
var post_request = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(tmpl)
|
||||
};
|
||||
assert.response(server, post_request, {}, function(res) {
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function testCORS() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + tmpl.name,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
},{
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
next(null);
|
||||
});
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
var del_request = {
|
||||
url: '/api/v1/map/named/' + tmpl.name + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
};
|
||||
assert.response(server, del_request, {}, function() {
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -720,7 +732,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -848,7 +860,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkTile(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
return null;
|
||||
@@ -869,7 +881,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkForeignSignerError(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 403,
|
||||
assert.equal(res.statusCode, 403,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('errors'),
|
||||
@@ -1036,7 +1048,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkTile_fetchOnRestart(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
@@ -1056,7 +1068,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkCacheChannel(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
@@ -1112,7 +1124,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 where cartodb_id in ( 5,6 )",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
attributes: { id:'cartodb_id', columns: ['name', 'address'] }
|
||||
} }
|
||||
@@ -1224,7 +1236,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkAttribute(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized getAttributes: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
return null;
|
||||
@@ -1277,7 +1289,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1347,7 +1359,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1430,7 +1442,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1503,8 +1515,8 @@ describe('template_api', function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
' ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -1516,7 +1528,7 @@ describe('template_api', function() {
|
||||
};
|
||||
var statskey = "user:localhost:mapviews";
|
||||
var redis_stats_client = redis.createClient(global.environment.redis.port);
|
||||
var template_id; // will be set on template post
|
||||
var template_id; // will be set on template post
|
||||
var now = strftime("%Y%m%d", new Date());
|
||||
var errors = [];
|
||||
step(
|
||||
@@ -1621,7 +1633,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1947,4 +1959,140 @@ describe('template_api', function() {
|
||||
|
||||
});
|
||||
|
||||
var torqueParamsScenarios = [
|
||||
{
|
||||
templateParams: {},
|
||||
expectedTile: [{"x__uint8":125,"y__uint8":159,"vals__uint8":[2],"dates__uint16":[0]}]
|
||||
},
|
||||
{
|
||||
templateParams: { namesFilter: "'Hawai'" },
|
||||
expectedTile: [{"x__uint8":125,"y__uint8":159,"vals__uint8":[1],"dates__uint16":[0]}]
|
||||
}
|
||||
];
|
||||
torqueParamsScenarios.forEach(function(scenario) {
|
||||
it("can instantiate with torque layer and params=" + JSON.stringify(scenario.templateParams), function(done) {
|
||||
|
||||
var torqueParamsTemplate = {
|
||||
version: '0.0.1',
|
||||
name: 'acceptance_torque_params',
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
placeholders: {
|
||||
namesFilter: {
|
||||
"type": "sql_ident",
|
||||
"default": "'Hawai', 'El Estocolmo'"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "select * from test_table_private_1 where name in (<%= namesFilter %>)",
|
||||
cartocss: "Map { -torque-frame-count:1; -torque-resolution:1; " +
|
||||
"-torque-aggregation-function:'count(*)'; -torque-time-attribute:'cartodb_id'; }",
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroupIdToDelete;
|
||||
step(
|
||||
function createTemplate() {
|
||||
var next = this;
|
||||
var createTemplateRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(torqueParamsTemplate)
|
||||
};
|
||||
assert.response(
|
||||
server,
|
||||
createTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function instantiateTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('template_id'), "Missing 'template_id' from response: " + res.body);
|
||||
var templateId = parsed.template_id;
|
||||
var instantiatePostRequest = {
|
||||
url: '/api/v1/map/named/' + templateId,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(scenario.templateParams)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(
|
||||
server,
|
||||
instantiatePostRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function requestTile(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var layergroupId = JSON.parse(res.body).layergroupid;
|
||||
layergroupIdToDelete = LayergroupToken.parse(layergroupId).token;
|
||||
|
||||
var torqueTileRequest = {
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0/0.torque.json',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
var next = this;
|
||||
assert.response(
|
||||
server,
|
||||
torqueTileRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function validateTileAndFinish(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
keysToDelete['map_cfg|' + layergroupIdToDelete] = 0;
|
||||
keysToDelete['map_tpl|localhost'] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
scenario.expectedTile
|
||||
);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
74
test/acceptance/widgets/aggregation.js
Normal file
74
test/acceptance/widgets/aggregation.js
Normal file
@@ -0,0 +1,74 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('aggregation widgets', function() {
|
||||
|
||||
var aggregationMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
country_places_count: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0_a3',
|
||||
aggregation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it("should expose an aggregation", function(done) {
|
||||
var testClient = new TestClient(aggregationMapConfig);
|
||||
testClient.getWidget('country_places_count', { own_filter: 0 }, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
assert.equal(aggregation.categories.length, 6);
|
||||
assert.deepEqual(aggregation.categories[0], { value: 769, category: 'USA', agg: false });
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
|
||||
describe('category', function () {
|
||||
|
||||
it("should expose a filtered aggregation", function (done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{country_places_count: {accept: ['CAN']}}
|
||||
]
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(aggregationMapConfig);
|
||||
testClient.getWidget('country_places_count', params, function (err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
assert.equal(aggregation.categories.length, 1);
|
||||
assert.deepEqual(aggregation.categories[0], { value: 256, category: 'CAN', agg: false });
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
120
test/acceptance/widgets/histogram.js
Normal file
120
test/acceptance/widgets/histogram.js
Normal file
@@ -0,0 +1,120 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('histogram widgets', function() {
|
||||
|
||||
it("should expose layer histogram", function(done) {
|
||||
var histogramMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
pop_max: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(histogramMapConfig);
|
||||
|
||||
testClient.getWidget('pop_max', function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var histogram = JSON.parse(res.body);
|
||||
assert.ok(histogram.bins.length);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
|
||||
describe('range', function() {
|
||||
var histogramMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
country_places_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it("should expose an histogram", function(done) {
|
||||
var testClient = new TestClient(histogramMapConfig);
|
||||
testClient.getWidget('country_places_histogram', { own_filter: 0 }, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var histogram = JSON.parse(res.body);
|
||||
// notice min value
|
||||
assert.deepEqual(
|
||||
histogram.bins[0],
|
||||
{ bin: 0, freq: 6497, min: 0, max: 742572, avg: 113511.16823149147 }
|
||||
);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered histogram", function(done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{
|
||||
country_places_histogram: { min: 4000000 }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(histogramMapConfig);
|
||||
testClient.getWidget('country_places_histogram', params, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var histogram = JSON.parse(res.body);
|
||||
// notice min value
|
||||
assert.deepEqual(histogram.bins[0], {
|
||||
bin: 0,
|
||||
freq: 62,
|
||||
min: 4000000,
|
||||
max: 9276403,
|
||||
avg: 5815009.596774193
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
51
test/acceptance/widgets/list.js
Normal file
51
test/acceptance/widgets/list.js
Normal file
@@ -0,0 +1,51 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('list widgets', function() {
|
||||
|
||||
it("should expose layer list", function(done) {
|
||||
|
||||
var listWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
names: {
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(listWidgetMapConfig);
|
||||
|
||||
testClient.getWidget('names', function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var expectedList = [
|
||||
{name:"Hawai"},
|
||||
{name:"El Estocolmo"},
|
||||
{name:"El Rey del Tallarín"},
|
||||
{name:"El Lacón"},
|
||||
{name:"El Pico"}
|
||||
];
|
||||
assert.deepEqual(JSON.parse(res.body).rows, expectedList);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
250
test/acceptance/widgets/named-maps.js
Normal file
250
test/acceptance/widgets/named-maps.js
Normal file
@@ -0,0 +1,250 @@
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
|
||||
var url = require('url');
|
||||
var queue = require('queue-async');
|
||||
|
||||
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 LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
describe('named-maps widgets', 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: {
|
||||
sql: "select * from populated_places_simple_reduced_private",
|
||||
cartocss: '#layer { marker-fill: blue; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
pop_max_formula_sum: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
country_places_count: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0_a3',
|
||||
aggregation: 'count'
|
||||
}
|
||||
},
|
||||
pop_max: {
|
||||
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'
|
||||
},
|
||||
data: JSON.stringify(widgetsTemplate)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
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 fetchTile(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
layergroup = JSON.parse(res.body);
|
||||
assert.ok(layergroup.hasOwnProperty('layergroupid'), "Missing 'layergroupid' from: " + res.body);
|
||||
layergroupid = layergroup.layergroupid;
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function getWidget(widgetName, callback) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupid + '/0/widget/' + widgetName,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
return callback(err, res, parsedBody);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('should be able to retrieve widgets from all URLs', function(done) {
|
||||
var widgetsPaths = layergroup.metadata.layers.reduce(function(paths, layer) {
|
||||
var widgets = layer.widgets || {};
|
||||
Object.keys(widgets).forEach(function(widget) {
|
||||
paths.push(url.parse(widgets[widget].url.http).path);
|
||||
});
|
||||
|
||||
return paths;
|
||||
}, []);
|
||||
|
||||
var widgetsQueue = queue(widgetsPaths.length);
|
||||
|
||||
widgetsPaths.forEach(function(path) {
|
||||
widgetsQueue.defer(function(path, done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: path,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
return done(null, parsedBody);
|
||||
}
|
||||
);
|
||||
}, path);
|
||||
});
|
||||
|
||||
widgetsQueue.awaitAll(function(err, results) {
|
||||
assert.equal(results.length, 3);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should retrieve aggregation", function(done) {
|
||||
getWidget('country_places_count', function(err, response, aggregation) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(aggregation.type, 'aggregation');
|
||||
assert.equal(aggregation.max, 769);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should retrieve histogram", function(done) {
|
||||
getWidget('pop_max', function(err, response, histogram) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(histogram.type, 'histogram');
|
||||
assert.equal(histogram.bin_width, 743250);
|
||||
|
||||
return done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
162
test/acceptance/widgets/widgets.js
Normal file
162
test/acceptance/widgets/widgets.js
Normal file
@@ -0,0 +1,162 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('widget filters', function() {
|
||||
|
||||
describe('combine widget filters', function() {
|
||||
var combinedWidgetsMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
country_places_count: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0_a3',
|
||||
aggregation: 'count'
|
||||
}
|
||||
},
|
||||
country_places_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it("should expose a filtered aggregation", function(done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{
|
||||
country_places_count: { reject: ['CHN'] }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(combinedWidgetsMapConfig);
|
||||
testClient.getWidget('country_places_count', params, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
|
||||
// first one would be CHN if reject filter wasn't applied
|
||||
assert.deepEqual(aggregation.categories[0], { value: 769, category: "USA", agg: false });
|
||||
|
||||
// confirm 'CHN' was filtered out (reject)
|
||||
assert.equal(aggregation.categories.reduce(function(sum, row) {
|
||||
return sum + (row.category === 'CHN' ? 1 : 0);
|
||||
}, 0), 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered aggregation", function(done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{
|
||||
country_places_count: { reject: ['CHN'] },
|
||||
country_places_histogram: { min: 7000000 }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(combinedWidgetsMapConfig);
|
||||
testClient.getWidget('country_places_count', params, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
|
||||
// first one would be CHN if reject filter wasn't applied
|
||||
assert.deepEqual(aggregation.categories[0], { value: 4, category: 'IND', agg: false });
|
||||
|
||||
// confirm 'CHN' was filtered out (reject)
|
||||
assert.equal(aggregation.categories.reduce(function(sum, row) {
|
||||
return sum + (row.category === 'CHN' ? 1 : 0);
|
||||
}, 0), 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to filter by bounding box a filtered aggregation", function(done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{
|
||||
country_places_histogram: { min: 50000 }
|
||||
}
|
||||
]
|
||||
},
|
||||
bbox: '-20,0,45,60'
|
||||
};
|
||||
var testClient = new TestClient(combinedWidgetsMapConfig);
|
||||
testClient.getWidget('country_places_count', params, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
|
||||
// first one would be CHN if reject filter wasn't applied
|
||||
assert.deepEqual(aggregation.categories[0], { value: 96, category: "RUS", agg: false });
|
||||
|
||||
// confirm 'CHN' was filtered out (reject)
|
||||
assert.equal(aggregation.categories.reduce(function(sum, row) {
|
||||
return sum + (row.category === 'CHN' ? 1 : 0);
|
||||
}, 0), 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should allow to filter by bounding box a filtered aggregation, with reject", function(done) {
|
||||
var params = {
|
||||
filters: {
|
||||
layers: [
|
||||
{
|
||||
country_places_count: { reject: ['RUS'] },
|
||||
country_places_histogram: { min: 50000 }
|
||||
}
|
||||
]
|
||||
},
|
||||
bbox: '-20,0,45,60'
|
||||
};
|
||||
var testClient = new TestClient(combinedWidgetsMapConfig);
|
||||
testClient.getWidget('country_places_count', params, function(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var aggregation = JSON.parse(res.body);
|
||||
|
||||
// first one would be CHN if reject filter wasn't applied
|
||||
assert.deepEqual(aggregation.categories[0], { value: 77, category: "TUR", agg: false });
|
||||
|
||||
// confirm 'CHN' was filtered out (reject)
|
||||
assert.equal(aggregation.categories.reduce(function(sum, row) {
|
||||
return sum + (row.category === 'CHN' ? 1 : 0);
|
||||
}, 0), 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
BIN
test/fixtures/_vovw_1_test_table_1_0_0.png
vendored
Normal file
BIN
test/fixtures/_vovw_1_test_table_1_0_0.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
test/fixtures/_vovw_2_test_table_2_1_1.png
vendored
Normal file
BIN
test/fixtures/_vovw_2_test_table_2_1_1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
test/fixtures/test_table_1_0_0.png
vendored
Normal file
BIN
test/fixtures/test_table_1_0_0.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 888 B |
BIN
test/fixtures/test_table_2_1_1.png
vendored
Normal file
BIN
test/fixtures/test_table_2_1_1.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
test/fixtures/test_table_3_3_3.png
vendored
Normal file
BIN
test/fixtures/test_table_3_3_3.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 834 B |
87
test/integration/mapconfig_overviews_adapter.js
Normal file
87
test/integration/mapconfig_overviews_adapter.js
Normal file
@@ -0,0 +1,87 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
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 MapConfigOverviewsAdapter = require('../../lib/cartodb/models/mapconfig_overviews_adapter');
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool }));
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
|
||||
|
||||
var mapConfigOverviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi);
|
||||
|
||||
describe('MapConfigOverviewsAdapter', function() {
|
||||
|
||||
it('should not modify layers for which no overviews are available', function(done) {
|
||||
var sql = 'SELECT * FROM test_table';
|
||||
var cartocss = '#layer { marker-fill: black; }';
|
||||
var cartocss_version = '2.3.0';
|
||||
var layer_without_overviews = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: cartocss_version
|
||||
}
|
||||
};
|
||||
|
||||
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], function(err, layers) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, sql);
|
||||
assert.equal(layers[0].options.cartocss, cartocss);
|
||||
assert.equal(layers[0].options.cartocss_version, cartocss_version);
|
||||
assert.equal(layers[0].options.overviews, undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MapConfigOverviewsAdapter', function() {
|
||||
|
||||
it('should add overviews metadata for layers using tables with overviews', function(done) {
|
||||
var sql = 'SELECT * FROM test_table_overviews';
|
||||
var cartocss = '#layer { marker-fill: black; }';
|
||||
var cartocss_version = '2.3.0';
|
||||
var layer_without_overviews = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: cartocss_version
|
||||
}
|
||||
};
|
||||
|
||||
mapConfigOverviewsAdapter.getLayers('localhost', [layer_without_overviews], function(err, layers) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, sql);
|
||||
assert.equal(layers[0].options.cartocss, cartocss);
|
||||
assert.equal(layers[0].options.cartocss_version, cartocss_version);
|
||||
assert.ok(layers[0].options.query_rewrite_data);
|
||||
var expected_data = {
|
||||
overviews: {
|
||||
test_table_overviews: {
|
||||
1: { table: '_vovw_1_test_table_overviews' },
|
||||
2: { table: '_vovw_2_test_table_overviews' }
|
||||
}
|
||||
}
|
||||
};
|
||||
assert.deepEqual(layers[0].options.query_rewrite_data, expected_data);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
54
test/integration/overviews-metadata-api.js
Normal file
54
test/integration/overviews-metadata-api.js
Normal file
@@ -0,0 +1,54 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
|
||||
var PgConnection = require('../../lib/cartodb/backends/pg_connection');
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTablesApi = require('../../lib/cartodb/api/query_tables_api');
|
||||
var OverviewsMetadataApi = require('../../lib/cartodb/api/overviews_metadata_api');
|
||||
|
||||
|
||||
describe('OverviewsMetadataApi', function() {
|
||||
|
||||
var queryTablesApi, overviewsMetadataApi;
|
||||
|
||||
before(function() {
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
});
|
||||
|
||||
it('should return an empty relation for tables that have no overviews', function(done) {
|
||||
var query = 'select * from test_table';
|
||||
overviewsMetadataApi.getOverviewsMetadata('localhost', query, function(err, result) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(result, {});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return overviews metadata', function(done) {
|
||||
var query = 'select * from test_table_overviews';
|
||||
overviewsMetadataApi.getOverviewsMetadata('localhost', query, function(err, result) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
'test_table_overviews': {
|
||||
1: { table: '_vovw_1_test_table_overviews' },
|
||||
2: { table: '_vovw_2_test_table_overviews' }
|
||||
}
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
46
test/integration/pg-query-runner.js
Normal file
46
test/integration/pg-query-runner.js
Normal file
@@ -0,0 +1,46 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
|
||||
var PgConnection = require('../../lib/cartodb/backends/pg_connection');
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
|
||||
|
||||
describe('PgQueryRunner', function() {
|
||||
|
||||
var queryRunner;
|
||||
|
||||
before(function() {
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
queryRunner = new PgQueryRunner(pgConnection);
|
||||
});
|
||||
|
||||
it('should work for happy case', function(done) {
|
||||
var query = 'select cartodb_id from test_table limit 3';
|
||||
queryRunner.run('localhost', query, function(err, result) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(result));
|
||||
assert.equal(result.length, 3);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should receive rows array even on error', function(done) {
|
||||
var query = 'select __error___ from test_table';
|
||||
queryRunner.run('localhost', query, function(err, result) {
|
||||
assert.ok(err);
|
||||
|
||||
assert.ok(Array.isArray(result));
|
||||
assert.equal(result.length, 0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
55
test/integration/query-tables-api.js
Normal file
55
test/integration/query-tables-api.js
Normal file
@@ -0,0 +1,55 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
|
||||
var PgConnection = require('../../lib/cartodb/backends/pg_connection');
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTablesApi = require('../../lib/cartodb/api/query_tables_api');
|
||||
|
||||
|
||||
describe('QueryTablesApi', function() {
|
||||
|
||||
var queryTablesApi;
|
||||
|
||||
before(function() {
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
});
|
||||
|
||||
// Check test/support/sql/windshaft.test.sql to understand where the values come from.
|
||||
|
||||
it('should return an object with affected tables array and last updated time', function(done) {
|
||||
var query = 'select * from test_table';
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime('localhost', query, function(err, result) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
affectedTables: [ 'public.test_table' ],
|
||||
lastUpdatedTime: 1234567890123
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with private tables', function(done) {
|
||||
var query = 'select * from test_table_private_1';
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime('localhost', query, function(err, result) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(result, {
|
||||
affectedTables: [ 'public.test_table_private_1' ],
|
||||
lastUpdatedTime: 1234567890123
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
78
test/integration/template-maps-limits.js
Normal file
78
test/integration/template-maps-limits.js
Normal file
@@ -0,0 +1,78 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var redis = require('redis');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps');
|
||||
|
||||
|
||||
describe('TemplateMaps limits', function() {
|
||||
|
||||
var OWNER = 'username';
|
||||
var templateCounter = 0;
|
||||
function templateUniqueName() {
|
||||
return 'tpl_' + templateCounter++;
|
||||
}
|
||||
function createTemplate() {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: templateUniqueName(),
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: 'blue'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
afterEach(function(done) {
|
||||
redisClient.del('map_tpl|' + OWNER, done);
|
||||
});
|
||||
|
||||
it('should allow to create templates when there is no limit in options', function(done) {
|
||||
var templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
templateMaps.addTemplate(OWNER, createTemplate(), function(err, templateName, template) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(template);
|
||||
|
||||
templateMaps.addTemplate(OWNER, createTemplate(), function(err, templateName, template) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(template);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to create templates with limit in options', function(done) {
|
||||
var templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1});
|
||||
|
||||
templateMaps.addTemplate(OWNER, createTemplate(), function(err, templateName, template) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(template);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail to create more templates than allowed by options', function(done) {
|
||||
var templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1});
|
||||
|
||||
templateMaps.addTemplate(OWNER, createTemplate(), function(err, templateName, template) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(template);
|
||||
templateMaps.addTemplate(OWNER, createTemplate(), function(err) {
|
||||
assert.ok(err);
|
||||
assert.equal(err.http_status, 409);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// Cribbed from the ever prolific Konstantin Kaefer
|
||||
// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js
|
||||
|
||||
var exec = require('child_process').exec;
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var util = require('util');
|
||||
@@ -13,7 +12,7 @@ var request = require('request');
|
||||
var assert = module.exports = exports = require('assert');
|
||||
|
||||
/**
|
||||
* Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the
|
||||
* Takes an image data as an input and an image path and compare them using mapnik.Image.compare mechanism, in case the
|
||||
* similarity is not within the tolerance limit it will callback with an error.
|
||||
*
|
||||
* @param buffer The image data to compare from
|
||||
@@ -22,70 +21,39 @@ var assert = module.exports = exports = require('assert');
|
||||
* @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself
|
||||
* @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric
|
||||
*/
|
||||
assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
assert.imageBufferIsSimilarToFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
callback = callback || function(err) { assert.ifError(err); };
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath),
|
||||
testImageFilePath = createImageFromBuffer(buffer, 'test');
|
||||
|
||||
imageFilesAreEqual(testImageFilePath, referenceImageFilePath, tolerance, function(err) {
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
|
||||
var referenceImageBuffer = fs.readFileSync(referenceImageFilePath, { encoding: null });
|
||||
|
||||
assert.imageBuffersAreSimilar(buffer, referenceImageBuffer, tolerance, callback);
|
||||
};
|
||||
|
||||
assert.imageBuffersAreSimilar = function(bufferA, bufferB, tolerance, callback) {
|
||||
var testImage = mapnik.Image.fromBytes(Buffer.isBuffer(bufferA) ? bufferA : new Buffer(bufferA, 'binary'));
|
||||
var referenceImage = mapnik.Image.fromBytes(Buffer.isBuffer(bufferB) ? bufferB : new Buffer(bufferB, 'binary'));
|
||||
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, callback);
|
||||
};
|
||||
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
callback = callback || function(err) { assert.ifError(err); };
|
||||
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
|
||||
|
||||
var referenceImage = mapnik.Image.fromBytes(fs.readFileSync(referenceImageFilePath, { encoding: null }));
|
||||
|
||||
imagesAreSimilar(testImage, referenceImage, tolerance, function(err) {
|
||||
if (err) {
|
||||
var testImageFilePath = randomImagePath();
|
||||
testImage.save(testImageFilePath);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
assert.imageBuffersAreEqual = function(bufferA, bufferB, tolerance, callback) {
|
||||
var randStr = (Math.random() * 1e16).toString().substring(0, 8);
|
||||
var imageFilePathA = createImageFromBuffer(bufferA, randStr + '-a'),
|
||||
imageFilePathB = createImageFromBuffer(bufferB, randStr + '-b');
|
||||
|
||||
imageFilesAreEqual(imageFilePathA, imageFilePathB, tolerance, function(err, similarity) {
|
||||
callback(err, [imageFilePathA, imageFilePathB], similarity);
|
||||
});
|
||||
};
|
||||
|
||||
function createImageFromBuffer(buffer, nameHint) {
|
||||
var imageFilePath = path.resolve('test/results/png/image-' + nameHint + '-' + Date.now() + '.png');
|
||||
var err = fs.writeFileSync(imageFilePath, buffer, 'binary');
|
||||
assert.ifError(err);
|
||||
return imageFilePath;
|
||||
}
|
||||
|
||||
function imageFilesAreEqual(testImageFilePath, referenceImageFilePath, tolerance, callback) {
|
||||
var resultFilePath = path.resolve(util.format('/tmp/windshaft-result-%s-diff.png', Date.now()));
|
||||
var imageMagickCmd = util.format(
|
||||
'compare -metric fuzz "%s" "%s" "%s"',
|
||||
testImageFilePath, referenceImageFilePath, resultFilePath
|
||||
);
|
||||
|
||||
exec(imageMagickCmd, function(err, stdout, stderr) {
|
||||
if (err) {
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
callback(err);
|
||||
} else {
|
||||
stderr = stderr.trim();
|
||||
var metrics = stderr.match(/([0-9]*) \((.*)\)/);
|
||||
if ( ! metrics ) {
|
||||
callback(new Error("No match for " + stderr));
|
||||
return;
|
||||
}
|
||||
var similarity = parseFloat(metrics[2]),
|
||||
tolerancePerMil = (tolerance / 1000);
|
||||
if (similarity > tolerancePerMil) {
|
||||
err = new Error(util.format(
|
||||
'Images %s and %s are not equal (got %d similarity, expected %d). Result %s',
|
||||
testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil, resultFilePath)
|
||||
);
|
||||
err.similarity = similarity;
|
||||
callback(err, similarity);
|
||||
} else {
|
||||
fs.unlinkSync(resultFilePath);
|
||||
callback(null, similarity);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assert.imagesAreSimilar = function(testImage, referenceImage, tolerance, callback) {
|
||||
function imagesAreSimilar(testImage, referenceImage, tolerance, callback) {
|
||||
if (testImage.width() !== referenceImage.width() || testImage.height() !== referenceImage.height()) {
|
||||
return callback(new Error('Images are not the same size'));
|
||||
}
|
||||
@@ -103,27 +71,10 @@ assert.imagesAreSimilar = function(testImage, referenceImage, tolerance, callbac
|
||||
} else {
|
||||
callback(null, similarity);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
assert.imageIsSimilarToFile = function(testImage, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
callback = callback || function(err) { assert.ifError(err); };
|
||||
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath);
|
||||
|
||||
var referenceImage = mapnik.Image.fromBytes(fs.readFileSync(referenceImageFilePath, { encoding: null }));
|
||||
|
||||
assert.imagesAreSimilar(testImage, referenceImage, tolerance, function(err) {
|
||||
if (err) {
|
||||
var testImageFilePath = randomImagePath();
|
||||
testImage.save(testImageFilePath);
|
||||
}
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
function randomImagePath(nameHint) {
|
||||
nameHint = nameHint || 'test';
|
||||
return path.resolve('test/results/png/image-' + nameHint + '-' + Date.now() + '.png');
|
||||
function randomImagePath() {
|
||||
return path.resolve('test/results/png/image-test-' + Date.now() + '.png');
|
||||
}
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
|
||||
@@ -7,8 +7,8 @@ module.exports = function(opts) {
|
||||
|
||||
var config = {
|
||||
redis_pool: {
|
||||
max: 10,
|
||||
idleTimeoutMillis: 1,
|
||||
max: 10,
|
||||
idleTimeoutMillis: 1,
|
||||
reapIntervalMillis: 1,
|
||||
port: global.environment.redis.port
|
||||
}
|
||||
|
||||
@@ -72,16 +72,16 @@ if test x"$PREPARE_PGSQL" = xyes; then
|
||||
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
|
||||
|
||||
cat sql/windshaft.test.sql sql/gadm4.sql |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
sed "s/:PUBLICPASS/${PUBLICPASS}/" |
|
||||
sed "s/:TESTUSER/${TESTUSER}/" |
|
||||
sed "s/:TESTPASS/${TESTPASS}/" |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
sed "s/:PUBLICPASS/${PUBLICPASS}/" |
|
||||
sed "s/:TESTUSER/${TESTUSER}/" |
|
||||
sed "s/:TESTPASS/${TESTPASS}/" |
|
||||
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
|
||||
|
||||
psql -c "CREATE EXTENSION plpythonu;" ${TEST_DB}
|
||||
# curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql
|
||||
# curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql
|
||||
cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql |
|
||||
psql -c "CREATE LANGUAGE plpythonu;" ${TEST_DB}
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql
|
||||
cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql sql/CDB_Overviews.sql |
|
||||
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
|
||||
|
||||
fi
|
||||
@@ -98,7 +98,7 @@ HMSET rails:users:localhost id ${TESTUSERID} \
|
||||
SADD rails:users:localhost:map_key 1235
|
||||
EOF
|
||||
|
||||
# A user configured as with cartodb-2.5.0+
|
||||
# A user configured as with cartodb-2.5.0+
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET rails:users:cartodb250user id ${TESTUSERID} \
|
||||
database_name "${TEST_DB}" \
|
||||
|
||||
1
test/support/sql/.gitignore
vendored
Normal file
1
test/support/sql/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
CDB_*.sql
|
||||
46
test/support/sql/CDB_Overviews.sql
Normal file
46
test/support/sql/CDB_Overviews.sql
Normal file
@@ -0,0 +1,46 @@
|
||||
-- Mockup for CDB_Overviews
|
||||
CREATE OR REPLACE FUNCTION CDB_Overviews(table_names regclass[])
|
||||
RETURNS TABLE(base_table regclass, z integer, overview_table regclass)
|
||||
AS $$
|
||||
BEGIN
|
||||
IF (SELECT 'test_table_overviews'::regclass = ANY (table_names)) THEN
|
||||
RETURN QUERY
|
||||
SELECT 'test_table_overviews'::regclass AS base_table, 1 AS z, '_vovw_1_test_table_overviews'::regclass AS overview_table
|
||||
UNION ALL
|
||||
SELECT 'test_table_overviews'::regclass AS base_table, 2 AS z, '_vovw_2_test_table_overviews'::regclass AS overview_table;
|
||||
ELSE
|
||||
RETURN;
|
||||
END IF;
|
||||
END
|
||||
$$ LANGUAGE PLPGSQL;
|
||||
|
||||
CREATE OR REPLACE FUNCTION CDB_ZoomFromScale(scaleDenominator numeric) RETURNS int AS $$
|
||||
BEGIN
|
||||
CASE
|
||||
WHEN scaleDenominator > 500000000 THEN RETURN 0;
|
||||
WHEN scaleDenominator <= 500000000 AND scaleDenominator > 200000000 THEN RETURN 1;
|
||||
WHEN scaleDenominator <= 200000000 AND scaleDenominator > 100000000 THEN RETURN 2;
|
||||
WHEN scaleDenominator <= 100000000 AND scaleDenominator > 50000000 THEN RETURN 3;
|
||||
WHEN scaleDenominator <= 50000000 AND scaleDenominator > 25000000 THEN RETURN 4;
|
||||
WHEN scaleDenominator <= 25000000 AND scaleDenominator > 12500000 THEN RETURN 5;
|
||||
WHEN scaleDenominator <= 12500000 AND scaleDenominator > 6500000 THEN RETURN 6;
|
||||
WHEN scaleDenominator <= 6500000 AND scaleDenominator > 3000000 THEN RETURN 7;
|
||||
WHEN scaleDenominator <= 3000000 AND scaleDenominator > 1500000 THEN RETURN 8;
|
||||
WHEN scaleDenominator <= 1500000 AND scaleDenominator > 750000 THEN RETURN 9;
|
||||
WHEN scaleDenominator <= 750000 AND scaleDenominator > 400000 THEN RETURN 10;
|
||||
WHEN scaleDenominator <= 400000 AND scaleDenominator > 200000 THEN RETURN 11;
|
||||
WHEN scaleDenominator <= 200000 AND scaleDenominator > 100000 THEN RETURN 12;
|
||||
WHEN scaleDenominator <= 100000 AND scaleDenominator > 50000 THEN RETURN 13;
|
||||
WHEN scaleDenominator <= 50000 AND scaleDenominator > 25000 THEN RETURN 14;
|
||||
WHEN scaleDenominator <= 25000 AND scaleDenominator > 12500 THEN RETURN 15;
|
||||
WHEN scaleDenominator <= 12500 AND scaleDenominator > 5000 THEN RETURN 16;
|
||||
WHEN scaleDenominator <= 5000 AND scaleDenominator > 2500 THEN RETURN 17;
|
||||
WHEN scaleDenominator <= 2500 AND scaleDenominator > 1500 THEN RETURN 18;
|
||||
WHEN scaleDenominator <= 1500 AND scaleDenominator > 750 THEN RETURN 19;
|
||||
WHEN scaleDenominator <= 750 AND scaleDenominator > 500 THEN RETURN 20;
|
||||
WHEN scaleDenominator <= 500 AND scaleDenominator > 250 THEN RETURN 21;
|
||||
WHEN scaleDenominator <= 250 AND scaleDenominator > 100 THEN RETURN 22;
|
||||
WHEN scaleDenominator <= 100 THEN RETURN 23;
|
||||
END CASE;
|
||||
END
|
||||
$$ LANGUAGE plpgsql IMMUTABLE;
|
||||
@@ -7385,3 +7385,8 @@ GRANT ALL ON TABLE populated_places_simple_reduced TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE populated_places_simple_reduced TO :PUBLICUSER;
|
||||
|
||||
VACUUM ANALYZE populated_places_simple_reduced;
|
||||
|
||||
create table populated_places_simple_reduced_private AS
|
||||
select * from populated_places_simple_reduced;
|
||||
|
||||
GRANT ALL ON TABLE populated_places_simple_reduced_private TO :TESTUSER;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
--
|
||||
-- Windshaft test database
|
||||
--
|
||||
--
|
||||
-- To use run ../prepare_db.sh
|
||||
-- NOTE: requires a postgis template called template_postgis
|
||||
--
|
||||
@@ -238,3 +238,97 @@ CREATE INDEX test_big_poly_the_geom_webmercator_idx ON test_big_poly USING gist
|
||||
|
||||
GRANT ALL ON TABLE test_big_poly TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_big_poly TO :PUBLICUSER;
|
||||
|
||||
-- table with overviews
|
||||
|
||||
CREATE TABLE test_table_overviews (
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
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))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE test_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_table_overviews TO :PUBLICUSER;
|
||||
|
||||
CREATE SEQUENCE test_table_overviews_cartodb_id_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
|
||||
ALTER SEQUENCE test_table_overviews_cartodb_id_seq OWNED BY test_table_overviews.cartodb_id;
|
||||
|
||||
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');
|
||||
|
||||
ALTER TABLE ONLY test_table_overviews ADD CONSTRAINT test_table_overviews_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
CREATE INDEX test_table_overviews_the_geom_idx ON test_table_overviews USING gist (the_geom);
|
||||
CREATE INDEX test_table_overviews_the_geom_webmercator_idx ON test_table_overviews USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_table_overviews TO :PUBLICUSER;
|
||||
|
||||
CREATE TABLE _vovw_1_test_table_overviews (
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
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))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE _vovw_1_test_table_overviews TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE _vovw_1_test_table_overviews TO :PUBLICUSER;
|
||||
|
||||
CREATE TABLE _vovw_2_test_table_overviews (
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry,
|
||||
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))),
|
||||
CONSTRAINT enforce_geotype_the_geom_webmercator CHECK (((geometrytype(the_geom_webmercator) = 'POINT'::text) OR (the_geom_webmercator IS NULL))),
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
|
||||
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');
|
||||
|
||||
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');
|
||||
|
||||
119
test/support/test-client.js
Normal file
119
test/support/test-client.js
Normal file
@@ -0,0 +1,119 @@
|
||||
'use strict';
|
||||
|
||||
var qs = require('querystring');
|
||||
var step = require('step');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var assert = require('./assert');
|
||||
var helper = require('./test_helper');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
|
||||
function TestClient(mapConfig) {
|
||||
this.mapConfig = mapConfig;
|
||||
this.keysToDelete = {};
|
||||
}
|
||||
|
||||
module.exports = TestClient;
|
||||
|
||||
TestClient.prototype.getWidget = function(widgetName, params, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = params;
|
||||
params = {};
|
||||
}
|
||||
|
||||
var url = '/api/v1/map';
|
||||
if (params && params.filters) {
|
||||
url += '?' + qs.stringify({ filters: JSON.stringify(params.filters) });
|
||||
}
|
||||
|
||||
var layergroupId;
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(self.mapConfig)
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var expectedWidgetURLS = {
|
||||
http: "/api/v1/map/" + parsedBody.layergroupid + "/0/widget/" + widgetName
|
||||
};
|
||||
assert.ok(parsedBody.metadata.layers[0].widgets[widgetName]);
|
||||
assert.ok(
|
||||
parsedBody.metadata.layers[0].widgets[widgetName].url.http.match(expectedWidgetURLS.http)
|
||||
);
|
||||
return next(null, parsedBody.layergroupid);
|
||||
}
|
||||
);
|
||||
},
|
||||
function getWidgetResult(err, _layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
layergroupId = _layergroupId;
|
||||
|
||||
var urlParams = {
|
||||
own_filter: params.hasOwnProperty('own_filter') ? params.own_filter : 1
|
||||
};
|
||||
if (params && params.bbox) {
|
||||
urlParams.bbox = params.bbox;
|
||||
}
|
||||
url = '/api/v1/map/' + layergroupId + '/0/widget/' + widgetName + '?' + qs.stringify(urlParams);
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function finish(err, res) {
|
||||
self.keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
self.keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
return callback(err, res);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TestClient.prototype.drain = function(callback) {
|
||||
helper.deleteRedisKeys(this.keysToDelete, callback);
|
||||
};
|
||||
@@ -66,6 +66,14 @@ function checkSurrogateKey(res, expectedKey) {
|
||||
assert.equal(res.headers['surrogate-key'], expectedKey);
|
||||
}
|
||||
|
||||
var redisClient;
|
||||
|
||||
beforeEach(function() {
|
||||
if (!redisClient) {
|
||||
redisClient = redis.createClient(global.environment.redis.port);
|
||||
}
|
||||
});
|
||||
|
||||
//global afterEach to capture test suites that leave keys in redis
|
||||
afterEach(function(done) {
|
||||
|
||||
@@ -102,7 +110,6 @@ afterEach(function(done) {
|
||||
}
|
||||
|
||||
Object.keys(databasesTasks).forEach(function(db) {
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
redisClient.select(db, function() {
|
||||
// Check that we start with an empty redis db
|
||||
redisClient.keys("*", function(err, keys) {
|
||||
@@ -129,6 +136,7 @@ function deleteRedisKeys(keysToDelete, callback) {
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
redisClient.select(keysToDelete[k], function() {
|
||||
redisClient.del(k, function(err, deletedKeysCount) {
|
||||
redisClient.quit();
|
||||
assert.notStrictEqual(deletedKeysCount, 0, 'No KEYS deleted for: [db=' + keysToDelete[k] + ']' + k);
|
||||
taskDone(k);
|
||||
});
|
||||
|
||||
429
test/unit/cartodb/overviews_query_rewriter.js
Normal file
429
test/unit/cartodb/overviews_query_rewriter.js
Normal file
@@ -0,0 +1,429 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var OverviewsQueryRewriter = require('../../../lib/cartodb/utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'ZoomLevel()'
|
||||
});
|
||||
|
||||
function normalize_whitespace(txt) {
|
||||
return txt.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
// compare SQL statements ignoring whitespace
|
||||
function assertSameSql(sql1, sql2) {
|
||||
assert.equal(normalize_whitespace(sql1), normalize_whitespace(sql2));
|
||||
}
|
||||
|
||||
describe('Overviews query rewriter', function() {
|
||||
|
||||
it('does not alter queries if no overviews data is present', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql);
|
||||
assert.equal(overviews_sql, sql);
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, {});
|
||||
assert.equal(overviews_sql, sql);
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, { overviews: {} });
|
||||
assert.equal(overviews_sql, sql);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('does not alter queries which don\'t use overviews', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table2: {
|
||||
0: { table: 'table2_ov0' },
|
||||
1: { table: 'table2_ov1' },
|
||||
4: { table: 'table2_ov4' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
done();
|
||||
});
|
||||
|
||||
// jshint multistr:true
|
||||
|
||||
it('generates query with single overview layer for level 0', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
0: { table: 'table1_ov0' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov0, _vovw_scale WHERE _vovw_z = 0\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 0\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query with single overview layer for level >0', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query with multiple overview layers for all levels up to N', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
0: { table: 'table1_ov0' },
|
||||
1: { table: 'table1_ov1' },
|
||||
2: { table: 'table1_ov2' },
|
||||
3: { table: 'table1_ov3' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
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\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query with multiple overview layers for random levels', function(done){
|
||||
var sql = "SELECT * FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
0: { table: 'table1_ov0' },
|
||||
1: { table: 'table1_ov1' },
|
||||
6: { table: 'table1_ov6' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
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_ov6, _vovw_scale WHERE _vovw_z > 1 AND _vovw_z <= 6\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 6\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query for a table with explicit schema', function(done){
|
||||
var sql = "SELECT * FROM public.table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
'public.table1': {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM public.table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM public.table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query for a table with explicit schema in the overviews info', function(done){
|
||||
var sql = "SELECT * FROM public.table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
'public.table1': {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM public.table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM public.table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query for a table that needs quoting with explicit schema', function(done){
|
||||
var sql = "SELECT * FROM public.\"table 1\"";
|
||||
var data = {
|
||||
overviews: {
|
||||
'public."table 1"': {
|
||||
2: { table: '"table 1_ov2"' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
\"_vovw_table 1\" AS (\
|
||||
SELECT * FROM public.\"table 1_ov2\", _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM public.\"table 1\", _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM \"_vovw_table 1\"\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query for a table with explicit schema that needs quoting', function(done){
|
||||
var sql = "SELECT * FROM \"user-1\".table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
'"user-1".table1': {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM \"user-1\".table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM \"user-1\".table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('generates query for a table with explicit schema both needing quoting', function(done){
|
||||
var sql = "SELECT * FROM \"user-1\".\"table 1\"";
|
||||
var data = {
|
||||
overviews: {
|
||||
'"user-1"."table 1"': {
|
||||
2: { table: '"table 1_ov2"' }
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
\"_vovw_table 1\" AS (\
|
||||
SELECT * FROM \"user-1\".\"table 1_ov2\", _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM \"user-1\".\"table 1\", _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT * FROM \"_vovw_table 1\"\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
it('generates query using overviews for queries with selected columns', function(done){
|
||||
var sql = "SELECT column1, column2, column3 FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT column1, column2, column3 FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query using overviews for queries with selected columns and all columns', function(done){
|
||||
var sql = "SELECT table1.*, column1, column2, column3 FROM table1";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT _vovw_table1.*, column1, column2, column3 FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query using overviews for queries with a semicolon', function(done){
|
||||
var sql = "SELECT table1.*, column1, column2, column3 FROM table1;";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT _vovw_table1.*, column1, column2, column3 FROM _vovw_table1;\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('generates query using overviews for queries with extra whitespace', function(done){
|
||||
var sql = " SELECT table1.* , column1,column2, column3 FROM table1 ";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
var expected_sql = "\
|
||||
WITH\
|
||||
_vovw_scale AS ( SELECT ZoomLevel() AS _vovw_z ),\
|
||||
_vovw_table1 AS (\
|
||||
SELECT * FROM table1_ov2, _vovw_scale WHERE _vovw_z <= 2\
|
||||
UNION ALL\
|
||||
SELECT * FROM table1, _vovw_scale WHERE _vovw_z > 2\
|
||||
)\
|
||||
SELECT _vovw_table1.* , column1,column2, column3 FROM _vovw_table1\
|
||||
";
|
||||
assertSameSql(overviews_sql, expected_sql);
|
||||
done();
|
||||
});
|
||||
|
||||
it('does not alter queries which have not the simple supported form', function(done){
|
||||
var sql = "SELECT * FROM table1 WHERE column1='x'";
|
||||
var data = {
|
||||
overviews: {
|
||||
table1: {
|
||||
2: { table: 'table1_ov2' }
|
||||
}
|
||||
}
|
||||
};
|
||||
var overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "SELECT * FROM table1 JOIN table2 ON (table1.col1=table2.col1)";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "SELECT a+b AS c FROM table1";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "SELECT f(a) AS b FROM table1";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "SELECT * FROM table1 AS x";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "WITH a AS (1) SELECT * FROM table1";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "SELECT * FROM table1 WHERE a=1";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
sql = "\
|
||||
SELECT table1.* FROM table1 \
|
||||
JOIN areas ON ST_Intersects(table1.the_geom, areas.the_geom) \
|
||||
WHERE areas.name='A' \
|
||||
";
|
||||
overviews_sql = overviewsQueryRewriter.query(sql, data);
|
||||
assert.equal(overviews_sql, sql);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -30,7 +30,7 @@ describe('req2params', function() {
|
||||
baseController = new BaseController(authApi, pgConnection);
|
||||
});
|
||||
|
||||
|
||||
|
||||
it('can be found in server_options', function(){
|
||||
assert.ok(_.isFunction(baseController.req2params));
|
||||
});
|
||||
|
||||
60
test/unit/cartodb/table_name_parser.js
Normal file
60
test/unit/cartodb/table_name_parser.js
Normal file
@@ -0,0 +1,60 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var TableNameParser = require('../../../lib/cartodb/utils/table_name_parser');
|
||||
|
||||
describe('TableNameParser', function() {
|
||||
|
||||
it('parses table names with scheme and quotes', function(done){
|
||||
|
||||
var test_cases = [
|
||||
['xyz', { schema: null, table: 'xyz' }],
|
||||
['"xyz"', { schema: null, table: 'xyz' }],
|
||||
['"xy z"', { schema: null, table: 'xy z' }],
|
||||
['"xy.z"', { schema: null, table: 'xy.z' }],
|
||||
['"x.y.z"', { schema: null, table: 'x.y.z' }],
|
||||
['abc.xyz', { schema: 'abc', table: 'xyz' }],
|
||||
['"abc".xyz', { schema: 'abc', table: 'xyz' }],
|
||||
['abc."xyz"', { schema: 'abc', table: 'xyz' }],
|
||||
['"abc"."xyz"', { schema: 'abc', table: 'xyz' }],
|
||||
['"a bc"."x yz"', { schema: 'a bc', table: 'x yz' }],
|
||||
['"a bc".xyz', { schema: 'a bc', table: 'xyz' }],
|
||||
['"a.bc".xyz', { schema: 'a.bc', table: 'xyz' }],
|
||||
['"a.b.c".xyz', { schema: 'a.b.c', table: 'xyz' }],
|
||||
['"a.b.c.".xyz', { schema: 'a.b.c.', table: 'xyz' }],
|
||||
['"a""bc".xyz', { schema: 'a"bc', table: 'xyz' }],
|
||||
['"a""bc"."x""yz"', { schema: 'a"bc', table: 'x"yz' }],
|
||||
];
|
||||
|
||||
test_cases.forEach(function(test_case) {
|
||||
var table_name = test_case[0];
|
||||
var expected_result = test_case[1];
|
||||
var result = TableNameParser.parse(table_name);
|
||||
assert.deepEqual(result, expected_result);
|
||||
});
|
||||
done();
|
||||
});
|
||||
|
||||
it('quotes identifiers that need quoting', function(done){
|
||||
assert.equal(TableNameParser.quote('x yz'), '"x yz"');
|
||||
assert.equal(TableNameParser.quote('x-yz'), '"x-yz"');
|
||||
assert.equal(TableNameParser.quote('x.yz'), '"x.yz"');
|
||||
done();
|
||||
});
|
||||
|
||||
it('doubles quotes', function(done){
|
||||
assert.equal(TableNameParser.quote('x"yz'), '"x""yz"');
|
||||
assert.equal(TableNameParser.quote('x"y"z'), '"x""y""z"');
|
||||
assert.equal(TableNameParser.quote('x""y"z'), '"x""""y""z"');
|
||||
assert.equal(TableNameParser.quote('x "yz'), '"x ""yz"');
|
||||
assert.equal(TableNameParser.quote('x"y-y"z'), '"x""y-y""z"');
|
||||
done();
|
||||
});
|
||||
|
||||
it('does not quote identifiers that don\'t need to be quoted', function(done){
|
||||
assert.equal(TableNameParser.quote('xyz'), 'xyz');
|
||||
assert.equal(TableNameParser.quote('x_z123'), 'x_z123');
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
Reference in New Issue
Block a user