Compare commits
580 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a42305408 | ||
|
|
09616777e6 | ||
|
|
c2a5569b8c | ||
|
|
38bea2108b | ||
|
|
ab0777b45f | ||
|
|
962f94387b | ||
|
|
de5600b4fd | ||
|
|
188a202f02 | ||
|
|
19f39b87f5 | ||
|
|
c759f314f9 | ||
|
|
6c98f14c64 | ||
|
|
f2348e1b24 | ||
|
|
4b5a10fe61 | ||
|
|
163fa58b5a | ||
|
|
b3bbb6af01 | ||
|
|
28d711e1f4 | ||
|
|
410fdb8343 | ||
|
|
21c2f3bdd1 | ||
|
|
baae080318 | ||
|
|
81c0796056 | ||
|
|
6ea5c5f414 | ||
|
|
7e37705843 | ||
|
|
425b5e6b4a | ||
|
|
8942c72fb2 | ||
|
|
e9359fdd73 | ||
|
|
5c4308abc1 | ||
|
|
61576b671b | ||
|
|
1e4d6bb942 | ||
|
|
ff6c0addb4 | ||
|
|
98cd524c07 | ||
|
|
746d57ff42 | ||
|
|
7319822419 | ||
|
|
58bcde3818 | ||
|
|
b57d08f38e | ||
|
|
739a8cef32 | ||
|
|
8a6e31e025 | ||
|
|
616aac9771 | ||
|
|
23a1b7484e | ||
|
|
9624ee1c76 | ||
|
|
4c557be2c2 | ||
|
|
7577ee8015 | ||
|
|
8c73914da4 | ||
|
|
604ba300aa | ||
|
|
876166ab74 | ||
|
|
1bf8fda770 | ||
|
|
cd32218cea | ||
|
|
0fd0974738 | ||
|
|
ed7f95a1a7 | ||
|
|
a36c1c52ae | ||
|
|
226d948c4d | ||
|
|
84c67f977e | ||
|
|
c09cda84a3 | ||
|
|
bd36ea1829 | ||
|
|
01a47925e0 | ||
|
|
dd8a70eb95 | ||
|
|
013bdba4ff | ||
|
|
c1acc54d55 | ||
|
|
5f3fb6e5f7 | ||
|
|
e3fac9c161 | ||
|
|
accab9e78a | ||
|
|
cb53d140e3 | ||
|
|
72986d1946 | ||
|
|
2195c55518 | ||
|
|
c418ba1908 | ||
|
|
934356e5cc | ||
|
|
c40235a910 | ||
|
|
cd8338196e | ||
|
|
2143e87401 | ||
|
|
f0a536ee1e | ||
|
|
dde4b63c6b | ||
|
|
0e7bcc4b56 | ||
|
|
e4816b4322 | ||
|
|
af4f29c538 | ||
|
|
016adb64ef | ||
|
|
7cedccedcd | ||
|
|
a9c12d4534 | ||
|
|
77f71b1978 | ||
|
|
1c029fbc7b | ||
|
|
2bc0d8d145 | ||
|
|
4c2af88f92 | ||
|
|
ddd5d2a0b0 | ||
|
|
d5cb59dc84 | ||
|
|
f21581630a | ||
|
|
3fef37d06b | ||
|
|
a8b93896ed | ||
|
|
6c1e9bf0ca | ||
|
|
1d8947d404 | ||
|
|
834377b342 | ||
|
|
c2e0eb05e5 | ||
|
|
256032ca4a | ||
|
|
d80f2b9566 | ||
|
|
9e2f0371ba | ||
|
|
a2e74a3e1b | ||
|
|
f04a5a1ab9 | ||
|
|
7114311b75 | ||
|
|
1e9e092dc3 | ||
|
|
98b3a5ba23 | ||
|
|
200966e806 | ||
|
|
407430b81e | ||
|
|
c4b1fc039c | ||
|
|
d00379af6b | ||
|
|
7cee0f3ee3 | ||
|
|
2588346f1b | ||
|
|
863128013d | ||
|
|
51eb8eb67f | ||
|
|
28e01fd8ac | ||
|
|
726e153ad5 | ||
|
|
e8df09c85b | ||
|
|
2b4fb2971d | ||
|
|
933b36cca0 | ||
|
|
37a7cfb6ba | ||
|
|
8a1cda159c | ||
|
|
403dcbebcd | ||
|
|
373ad69306 | ||
|
|
b2029e09f5 | ||
|
|
4f37d2d0c2 | ||
|
|
a5b07bc2a8 | ||
|
|
1544a5622d | ||
|
|
d49a877771 | ||
|
|
0f4747743c | ||
|
|
368e4522e7 | ||
|
|
cb1d1bb115 | ||
|
|
612cc3dd41 | ||
|
|
bff082e577 | ||
|
|
ea41750a14 | ||
|
|
458376a665 | ||
|
|
f0284907c4 | ||
|
|
ad0385ccf7 | ||
|
|
4350fc3c65 | ||
|
|
fc3422b9e5 | ||
|
|
9703c19fb4 | ||
|
|
d36f2fb354 | ||
|
|
c837785314 | ||
|
|
33014a9f45 | ||
|
|
c16d0b8605 | ||
|
|
c88e4c5173 | ||
|
|
0540696c3e | ||
|
|
5f59a97a02 | ||
|
|
d3b815c3c7 | ||
|
|
d2a8dcbede | ||
|
|
4854e879a6 | ||
|
|
ddc99cebff | ||
|
|
47470d4f2b | ||
|
|
d9297d54de | ||
|
|
2d821f957e | ||
|
|
c0ce6e7a8a | ||
|
|
3f620c6cdd | ||
|
|
9160d8018d | ||
|
|
51307bcc69 | ||
|
|
d29651ee80 | ||
|
|
18640077aa | ||
|
|
59563c893b | ||
|
|
90fd1786e1 | ||
|
|
4a11115dd0 | ||
|
|
baf3e774c5 | ||
|
|
ac296411d5 | ||
|
|
09cea4d6d4 | ||
|
|
382ff2416f | ||
|
|
27036379dd | ||
|
|
f4e6e140e0 | ||
|
|
b4e5cb88d9 | ||
|
|
3269fef845 | ||
|
|
e797719b41 | ||
|
|
284a8f2465 | ||
|
|
54ea656da2 | ||
|
|
b4aaadf40b | ||
|
|
74d2e3ef75 | ||
|
|
b10e4c11d9 | ||
|
|
075e141a9c | ||
|
|
04acf895f0 | ||
|
|
21608bf2e2 | ||
|
|
653beb1952 | ||
|
|
050d33ff14 | ||
|
|
1ae86e039b | ||
|
|
f75cadf6ba | ||
|
|
93d0fe9176 | ||
|
|
6a15cd0566 | ||
|
|
614fe3f703 | ||
|
|
82d4bb3046 | ||
|
|
f49c13b1b3 | ||
|
|
828b817aca | ||
|
|
7f26f01743 | ||
|
|
50da63fc63 | ||
|
|
f8f6508449 | ||
|
|
7256eb0935 | ||
|
|
cb08b42e54 | ||
|
|
9e7caeff94 | ||
|
|
e72a1d73be | ||
|
|
aaacad81e7 | ||
|
|
55ee5b3b01 | ||
|
|
94bf2748be | ||
|
|
9a4aa7c1fa | ||
|
|
3e71365a95 | ||
|
|
018ffcea7c | ||
|
|
e24ba9f495 | ||
|
|
0e2e069503 | ||
|
|
c4bbff3802 | ||
|
|
290054ef5d | ||
|
|
4c25828540 | ||
|
|
5eda4888ed | ||
|
|
7c322d9411 | ||
|
|
bd35d4e78a | ||
|
|
6eb711e70b | ||
|
|
81ff0152c0 | ||
|
|
8a07f9f57e | ||
|
|
ca367d0fe7 | ||
|
|
cd7adbd792 | ||
|
|
5b76ec9f68 | ||
|
|
bb21270aab | ||
|
|
22f3a54fbf | ||
|
|
6644711969 | ||
|
|
989df4a8a4 | ||
|
|
d5423c88ea | ||
|
|
5838b7a455 | ||
|
|
86e8cedfab | ||
|
|
93c31c5433 | ||
|
|
4ca8fddd50 | ||
|
|
8cc46fd2a3 | ||
|
|
b2d8f53a5c | ||
|
|
8e8e59addc | ||
|
|
63e52878a1 | ||
|
|
ef276bd51e | ||
|
|
7ac3784f32 | ||
|
|
e12133e24b | ||
|
|
be01781373 | ||
|
|
65523768f9 | ||
|
|
f602ea88e2 | ||
|
|
da6870cf1e | ||
|
|
06e420aa70 | ||
|
|
c667e64d7f | ||
|
|
5c3dd8b09d | ||
|
|
f7c528277b | ||
|
|
2ff33b5010 | ||
|
|
d2f4e3ee74 | ||
|
|
730486b27b | ||
|
|
c4b4a93a0d | ||
|
|
f34213a147 | ||
|
|
862f8b4ce6 | ||
|
|
5a2afa9b89 | ||
|
|
4759d178d3 | ||
|
|
777fb78abc | ||
|
|
faa24caf5b | ||
|
|
5e6529363b | ||
|
|
a785ebef65 | ||
|
|
4137de5adf | ||
|
|
f012e6092f | ||
|
|
9ce4929d87 | ||
|
|
8efe844474 | ||
|
|
02cb80daa1 | ||
|
|
e9d1951d48 | ||
|
|
a11cc28dc7 | ||
|
|
a8fdd6726e | ||
|
|
7ad8a99373 | ||
|
|
c0a24108ba | ||
|
|
ae9b8a0380 | ||
|
|
31a0b01a27 | ||
|
|
efcb73e0d1 | ||
|
|
f008c74419 | ||
|
|
4a646d4700 | ||
|
|
657b262d92 | ||
|
|
988412fc07 | ||
|
|
70750d2c43 | ||
|
|
9c1db98f67 | ||
|
|
12c44fda6f | ||
|
|
a42756ba24 | ||
|
|
6ccdb6cefd | ||
|
|
9f6ce64a31 | ||
|
|
3e35604df0 | ||
|
|
01a69ef15c | ||
|
|
5adbc98c2b | ||
|
|
fb045f1836 | ||
|
|
ee49b8b2a2 | ||
|
|
5ba72b4894 | ||
|
|
6975db6ecf | ||
|
|
8134aca14d | ||
|
|
215bbbd29c | ||
|
|
c4b6f65404 | ||
|
|
69f40e6f6a | ||
|
|
20725900b6 | ||
|
|
ab984729f5 | ||
|
|
8553326c1b | ||
|
|
9f8551058d | ||
|
|
5895871fad | ||
|
|
c372d69e98 | ||
|
|
bacaee138a | ||
|
|
3add61ec57 | ||
|
|
02ae50eef0 | ||
|
|
b308259e6f | ||
|
|
14a0afc7c0 | ||
|
|
d74daf39c7 | ||
|
|
eb091caf4a | ||
|
|
424cc6d93b | ||
|
|
3bacfecc49 | ||
|
|
64dd033c94 | ||
|
|
caec04f63b | ||
|
|
2e79781711 | ||
|
|
289ffbbedc | ||
|
|
e0f0751b28 | ||
|
|
f30be00eb9 | ||
|
|
ee94b8a587 | ||
|
|
fd3f928d81 | ||
|
|
ba08745c23 | ||
|
|
573932efba | ||
|
|
31344a1c75 | ||
|
|
c7f37047b0 | ||
|
|
2a06405a58 | ||
|
|
9206b1a1b5 | ||
|
|
5989ab344d | ||
|
|
a1e024e228 | ||
|
|
8628d3b671 | ||
|
|
7ce4104d2f | ||
|
|
a26a5d6f5a | ||
|
|
e98a5aeff0 | ||
|
|
4c375780c7 | ||
|
|
48415fb1f3 | ||
|
|
8da7cf73c1 | ||
|
|
ba30f460ee | ||
|
|
e1aa0bc7ae | ||
|
|
3987e83b7a | ||
|
|
858d976637 | ||
|
|
48d2978997 | ||
|
|
1872fbd021 | ||
|
|
bbb1b4a7b9 | ||
|
|
aa0ddaae95 | ||
|
|
cb3706e5cf | ||
|
|
3d8f6576aa | ||
|
|
24f7bc6596 | ||
|
|
a1934c87d5 | ||
|
|
7a6b1ec871 | ||
|
|
cfdac1bcb0 | ||
|
|
42ef40282b | ||
|
|
25da6e779c | ||
|
|
7f7204df6c | ||
|
|
3c6d930434 | ||
|
|
b5540fc63a | ||
|
|
f6f58a71b3 | ||
|
|
3a7361a009 | ||
|
|
420f4aacc9 | ||
|
|
163c10b66e | ||
|
|
8fb35571fe | ||
|
|
91f39abc69 | ||
|
|
df63fbbd04 | ||
|
|
5b96576227 | ||
|
|
9f18e2d27d | ||
|
|
1ac6ead4b2 | ||
|
|
9d82e8c27c | ||
|
|
224eb392ba | ||
|
|
8f51418d84 | ||
|
|
c12e5f7a27 | ||
|
|
1c2354dc49 | ||
|
|
2e26e2e126 | ||
|
|
94639f7e0c | ||
|
|
f3957b4fce | ||
|
|
61765d20e1 | ||
|
|
503636f9fb | ||
|
|
4abadec9c4 | ||
|
|
b574489950 | ||
|
|
85788f42a6 | ||
|
|
5fb7f07498 | ||
|
|
fd44b62f26 | ||
|
|
3300c095ed | ||
|
|
55cf0a8447 | ||
|
|
64a87690ee | ||
|
|
3890014250 | ||
|
|
7c2924ae14 | ||
|
|
bfdaf67a9b | ||
|
|
65612f0109 | ||
|
|
e0ade85565 | ||
|
|
c5afc0dc94 | ||
|
|
a7e00c5856 | ||
|
|
2482accb42 | ||
|
|
3e4f71d873 | ||
|
|
fa19f90a6a | ||
|
|
bbadd46766 | ||
|
|
b1f618a98e | ||
|
|
cdf3fe3a25 | ||
|
|
1440841ac8 | ||
|
|
fc57fd2638 | ||
|
|
776cb8d47a | ||
|
|
c36f52415e | ||
|
|
2ee2c5bb55 | ||
|
|
dccb557cd7 | ||
|
|
4570d17ce1 | ||
|
|
b3b3abcdb8 | ||
|
|
6639664b3f | ||
|
|
b99db7cb69 | ||
|
|
3e94e3288f | ||
|
|
1115f9fba2 | ||
|
|
24dde1e4d0 | ||
|
|
7d4caf6974 | ||
|
|
e4ba68850c | ||
|
|
e2154f6561 | ||
|
|
778860c81f | ||
|
|
ee3c56efba | ||
|
|
5dc328724a | ||
|
|
c77ea49594 | ||
|
|
73aa159b98 | ||
|
|
b7d7cffb67 | ||
|
|
eba8db292c | ||
|
|
d5391ef15b | ||
|
|
685cbb1eec | ||
|
|
5842a239fd | ||
|
|
285363aa40 | ||
|
|
92e130d8de | ||
|
|
f674f90eba | ||
|
|
0542b65cbb | ||
|
|
60fa94781e | ||
|
|
38d57533c2 | ||
|
|
e3d6da06a7 | ||
|
|
3af05bb734 | ||
|
|
ffd58cc7ca | ||
|
|
e103c52d27 | ||
|
|
46a4defab7 | ||
|
|
00a71af95d | ||
|
|
2fb8036740 | ||
|
|
6902a8c9b3 | ||
|
|
fd8be0352f | ||
|
|
c19a345f6c | ||
|
|
bdc3ecff5a | ||
|
|
6c4ec29e18 | ||
|
|
412712e62e | ||
|
|
0edcb30f75 | ||
|
|
c0f49b3acb | ||
|
|
b926244085 | ||
|
|
d21136b475 | ||
|
|
8788aaaf25 | ||
|
|
120e800089 | ||
|
|
92dc4b148c | ||
|
|
755dfe6822 | ||
|
|
b787ee1033 | ||
|
|
9ee3612da0 | ||
|
|
fe0b0a9c50 | ||
|
|
dea19f20dd | ||
|
|
f9f70cc6e7 | ||
|
|
23ef1157cb | ||
|
|
8ca4d537ce | ||
|
|
98d5731555 | ||
|
|
5da8929a5d | ||
|
|
e198bafac1 | ||
|
|
dd731399dc | ||
|
|
66fd899ffb | ||
|
|
bca8a33417 | ||
|
|
a11c8d882e | ||
|
|
c5bed48d61 | ||
|
|
98b7d12796 | ||
|
|
93dd8a2213 | ||
|
|
4e4a223f24 | ||
|
|
4a73f3874d | ||
|
|
bc845b2e8d | ||
|
|
83eceb349c | ||
|
|
bb518f0744 | ||
|
|
08ad961123 | ||
|
|
146d494cae | ||
|
|
bb40ecf7c2 | ||
|
|
7de9f64f2a | ||
|
|
0bb6178d49 | ||
|
|
084b3e94a6 | ||
|
|
a0445b5cdd | ||
|
|
1d4ddd373b | ||
|
|
92795963ae | ||
|
|
e634ad6f7d | ||
|
|
ecbae52abe | ||
|
|
e1c5af8602 | ||
|
|
6704a94f36 | ||
|
|
a35403cd91 | ||
|
|
41ead9664b | ||
|
|
e04a9a2579 | ||
|
|
d70af7c9c1 | ||
|
|
d910f5ef3e | ||
|
|
57cba3d511 | ||
|
|
7902b276ad | ||
|
|
f932862ce4 | ||
|
|
ab55b083b4 | ||
|
|
263b3e3682 | ||
|
|
5baad96924 | ||
|
|
68b19c65fe | ||
|
|
3a81302699 | ||
|
|
6b942ecf0b | ||
|
|
1ed0c73525 | ||
|
|
7175b2b05e | ||
|
|
61137610c7 | ||
|
|
b53031972e | ||
|
|
da602eeda0 | ||
|
|
a26025b259 | ||
|
|
25a61c8479 | ||
|
|
e73b64bfed | ||
|
|
b5b8085444 | ||
|
|
388f08a277 | ||
|
|
aa36236ed2 | ||
|
|
a9ca453b17 | ||
|
|
9ab4eb5801 | ||
|
|
26149e7755 | ||
|
|
2d4fd62acf | ||
|
|
e037c8c1b2 | ||
|
|
09687b3811 | ||
|
|
28400f4544 | ||
|
|
7a87b8ebef | ||
|
|
9ff661480b | ||
|
|
ca564bfaad | ||
|
|
dd36877a20 | ||
|
|
1d860fd202 | ||
|
|
e9111ec3bb | ||
|
|
0981ccd0c4 | ||
|
|
077c4ab907 | ||
|
|
efacafaa0d | ||
|
|
1c250bf243 | ||
|
|
c974b91c6b | ||
|
|
6aebe26cc9 | ||
|
|
f93794717e | ||
|
|
0ebf482936 | ||
|
|
b5b8083acd | ||
|
|
ab6bae6a7f | ||
|
|
219658761f | ||
|
|
4d39d30f6e | ||
|
|
499178319d | ||
|
|
7b7e9ffe59 | ||
|
|
2bd3e46a4d | ||
|
|
e44b5eaccd | ||
|
|
26512f6485 | ||
|
|
90b92f0180 | ||
|
|
ebe25761d2 | ||
|
|
f928147559 | ||
|
|
d5c5c7bdbb | ||
|
|
ff147ca3bf | ||
|
|
f745e915d3 | ||
|
|
1e239658d8 | ||
|
|
52f35d74b9 | ||
|
|
57e6e49749 | ||
|
|
b3bbb9d97a | ||
|
|
697749b204 | ||
|
|
a769545e39 | ||
|
|
df40302fd0 | ||
|
|
5bd30b6b5f | ||
|
|
ed84ed8475 | ||
|
|
a444b80c96 | ||
|
|
43c56af0fc | ||
|
|
6db48a24b8 | ||
|
|
1da937d639 | ||
|
|
4924bcc298 | ||
|
|
a05f3d6ee9 | ||
|
|
eec44dd62d | ||
|
|
3f41f19ab9 | ||
|
|
1535820d4f | ||
|
|
b2378939c5 | ||
|
|
7fa8d1e0c9 | ||
|
|
d5fbcfcecb | ||
|
|
9d77aea6be | ||
|
|
634a4c2a01 | ||
|
|
f504807812 | ||
|
|
c7bdabfc65 | ||
|
|
10a602a4f3 | ||
|
|
e3a5c52ebf | ||
|
|
f9c0e29db0 | ||
|
|
e53d823b5a | ||
|
|
31dede5d06 | ||
|
|
69142964c6 | ||
|
|
2eac808e18 | ||
|
|
011b60eeab | ||
|
|
16654c016a | ||
|
|
9b9e6b13b7 | ||
|
|
3b11525cfb | ||
|
|
6823fd8b03 | ||
|
|
ce032fcc96 | ||
|
|
a44477dddc | ||
|
|
3709d1f1d5 | ||
|
|
e958f925d3 | ||
|
|
d923b343fc | ||
|
|
b9d2e297b6 | ||
|
|
30f8234bd0 | ||
|
|
ae8e3f2ef8 | ||
|
|
6d91172630 | ||
|
|
8d4ebc171b | ||
|
|
4d3c21f1bc | ||
|
|
dfcb3b6dc1 | ||
|
|
87b4a37f3f | ||
|
|
4db2c715a0 | ||
|
|
2f51ad9c3f | ||
|
|
ed1f753690 | ||
|
|
bcf3ce71ef | ||
|
|
354c982ea0 |
19
.jshintrc
19
.jshintrc
@@ -82,13 +82,14 @@
|
||||
// "wsh" : false, // Windows Scripting Host
|
||||
// "yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : { // additional predefined global variables
|
||||
"describe": true,
|
||||
"before": true,
|
||||
"after": true,
|
||||
"beforeEach": true,
|
||||
"afterEach": true,
|
||||
"it": true
|
||||
}
|
||||
// Custom predefined global variables
|
||||
"predef": [
|
||||
"-console", // disallows console, use debug
|
||||
"beforeEach",
|
||||
"afterEach",
|
||||
"before",
|
||||
"after",
|
||||
"describe",
|
||||
"it"
|
||||
]
|
||||
}
|
||||
|
||||
14
.travis.yml
14
.travis.yml
@@ -1,7 +1,4 @@
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
apt:
|
||||
packages:
|
||||
- pkg-config
|
||||
@@ -11,6 +8,17 @@ addons:
|
||||
|
||||
before_install:
|
||||
- npm install -g npm@2
|
||||
- lsb_release -a
|
||||
- sudo mv /etc/apt/sources.list.d/pgdg-source.list* /tmp
|
||||
- sudo apt-get -qq purge postgis* postgresql*
|
||||
- sudo rm -Rf /var/lib/postgresql /etc/postgresql
|
||||
- sudo apt-add-repository --yes ppa:cartodb/postgresql-9.5
|
||||
- sudo apt-add-repository --yes ppa:cartodb/gis
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -q postgresql-9.5-postgis-2.2
|
||||
- sudo apt-get install -q postgresql-plpython-9.5
|
||||
- echo -e "local\tall\tall\ttrust\nhost\tall\tall\t127.0.0.1/32\ttrust\nhost\tall\tall\t::1/128\ttrust" |sudo tee /etc/postgresql/9.5/main/pg_hba.conf
|
||||
- sudo service postgresql restart
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
2
Makefile
2
Makefile
@@ -43,7 +43,7 @@ jshint:
|
||||
@echo "***jshint***"
|
||||
@./node_modules/.bin/jshint lib/ test/ app.js
|
||||
|
||||
test-all: jshint test
|
||||
test-all: test jshint
|
||||
|
||||
coverage:
|
||||
@RUNTESTFLAGS=--with-coverage make test
|
||||
|
||||
429
NEWS.md
429
NEWS.md
@@ -1,5 +1,433 @@
|
||||
# Changelog
|
||||
|
||||
## 2.59.0
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.30.0](https://github.com/CartoDB/camshaft/releases/tag/0.30.0).
|
||||
|
||||
|
||||
## 2.58.0
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.29.2](https://github.com/CartoDB/camshaft/releases/tag/0.29.2).
|
||||
|
||||
Bug fixes:
|
||||
- Return full list of nodes in response metadata.
|
||||
|
||||
|
||||
## 2.57.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.28.1](https://github.com/CartoDB/camshaft/releases/tag/0.28.1).
|
||||
|
||||
|
||||
## 2.56.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.27.0](https://github.com/CartoDB/camshaft/releases/tag/0.27.0).
|
||||
|
||||
|
||||
## 2.55.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Enhancements:
|
||||
- Skip null values for quantification methods generating null values.
|
||||
|
||||
Announcements:
|
||||
- Uses new configuration for camshaft: analysis node has an associated user/owner.
|
||||
- Upgrades camshaft to [0.26.0](https://github.com/CartoDB/camshaft/releases/tag/0.26.0).
|
||||
|
||||
|
||||
## 2.54.0
|
||||
|
||||
Released 2016-06-30
|
||||
|
||||
Improvements:
|
||||
- Errors with context: replaced `turbo-carto` error type by `layer` type.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.23.0](https://github.com/CartoDB/camshaft/releases/tag/0.23.0)
|
||||
|
||||
|
||||
## 2.53.5
|
||||
|
||||
Released 2016-06-29
|
||||
|
||||
Bug fixes:
|
||||
- Uses node list so identical nodes are not de-duplicated and can be used with different ids #528.
|
||||
|
||||
|
||||
## 2.53.4
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.4](https://github.com/CartoDB/camshaft/releases/tag/0.22.4)
|
||||
|
||||
|
||||
## 2.53.3
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.3](https://github.com/CartoDB/camshaft/releases/tag/0.22.3)
|
||||
|
||||
|
||||
## 2.53.2
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.2](https://github.com/CartoDB/camshaft/releases/tag/0.22.2)
|
||||
|
||||
|
||||
## 2.53.1
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.1](https://github.com/CartoDB/camshaft/releases/tag/0.22.1)
|
||||
|
||||
|
||||
## 2.53.0
|
||||
|
||||
Released 2016-06-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.0](https://github.com/CartoDB/camshaft/releases/tag/0.22.0)
|
||||
|
||||
|
||||
## 2.52.0
|
||||
|
||||
Released 2016-06-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.21.0](https://github.com/CartoDB/camshaft/releases/tag/0.21.0)
|
||||
|
||||
|
||||
## 2.51.0
|
||||
|
||||
Released 2016-06-21
|
||||
|
||||
Enhancements:
|
||||
- Split turbo-carto adapter substitutions tokens query.
|
||||
- Now errors with context have the same schema. #519
|
||||
- Responses with error now return the layer-id to give more info to the user.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.20.0](https://github.com/CartoDB/camshaft/releases/tag/0.20.0)
|
||||
|
||||
|
||||
## 2.50.0
|
||||
|
||||
Released 2016-06-21
|
||||
|
||||
Bug fixes:
|
||||
- Pixel size query for turbo-carto adapter using radians and degrees instead of meters.
|
||||
|
||||
New features:
|
||||
- Add support for min, max, and avg operations in aggregation dataview #513.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.19.0](https://github.com/CartoDB/camshaft/releases/tag/0.19.0)
|
||||
|
||||
|
||||
## 2.49.1
|
||||
|
||||
Released 2016-06-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.12.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.12.1).
|
||||
|
||||
Bug fixes:
|
||||
- Use an empty array as default value for falsy ramps #512.
|
||||
- Use the_geom for intermediate dataviews #511.
|
||||
- Pick last update time for layergroupid from analyses results #510.
|
||||
|
||||
|
||||
## 2.49.0
|
||||
|
||||
Released 2016-06-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.17.1](https://github.com/CartoDB/camshaft/releases/tag/0.17.1)
|
||||
|
||||
|
||||
## 2.48.0
|
||||
|
||||
Released 2016-06-14
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.15.1](https://github.com/CartoDB/camshaft/releases/tag/0.15.1)
|
||||
- Responses with more context info if analysis or turbo-carto fails during map creation.
|
||||
|
||||
## 2.47.1
|
||||
|
||||
Released 2016-06-13
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.14.1](https://github.com/CartoDB/camshaft/releases/tag/0.14.1)
|
||||
|
||||
|
||||
## 2.47.0
|
||||
|
||||
Released 2016-06-10
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.14.0](https://github.com/CartoDB/camshaft/releases/tag/0.14.0)
|
||||
|
||||
|
||||
## 2.46.0
|
||||
|
||||
Released 2016-06-09
|
||||
|
||||
Improvements:
|
||||
- Support for substitution tokens in geojson tiles
|
||||
- Warn on application start about non-matching dependencies
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.3.0](https://github.com/CartoDB/camshaft/releases/tag/2.3.0)
|
||||
- Upgrades camshaft to [0.13.0](https://github.com/CartoDB/camshaft/releases/tag/0.13.0)
|
||||
- Upgrades turbo-carto to [0.11.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.11.0)
|
||||
|
||||
Bug fixes:
|
||||
- Column provided for geojson renderer should not be null #476
|
||||
- Dataviews/widgets adapter working with non sql, non source, and non widgets layers
|
||||
|
||||
|
||||
## 2.45.0
|
||||
|
||||
Released 2016-06-02
|
||||
|
||||
Improvements:
|
||||
- Removes Windshaft's widgets dependency.
|
||||
- Makes widgets/dataviews endpoint compatible, but all using dataviews backend instead of widgets from Windshaft.
|
||||
- Keeps adding widgets metadata in map instantiations for old clients.
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.0.1](https://github.com/CartoDB/camshaft/releases/tag/2.0.1 )
|
||||
- Upgrades camshaft to [0.12.1](https://github.com/CartoDB/camshaft/releases/tag/0.12.1)
|
||||
- Upgrades turbo-carto to [0.10.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.10.1)
|
||||
|
||||
|
||||
## 2.44.1
|
||||
|
||||
Released 2016-06-01
|
||||
|
||||
Improvements:
|
||||
- Extend overviews support to histogram and aggregation dataviews
|
||||
- Test improvements
|
||||
|
||||
|
||||
## 2.44.0
|
||||
|
||||
Released 2016-05-31
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.11.0](https://github.com/CartoDB/camshaft/releases/tag/0.11.0)
|
||||
- Upgrades turbo-carto to [0.10.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.10.0)
|
||||
|
||||
New features:
|
||||
- Adds support for sql wrap in all layers
|
||||
|
||||
Bug fixes:
|
||||
- Fail on turbo-carto invalid quantification methods
|
||||
|
||||
|
||||
## 2.43.1
|
||||
|
||||
Released 2016-05-19
|
||||
|
||||
Bug fixes:
|
||||
- Dataview error when bbox present without query rewrite data #458
|
||||
|
||||
|
||||
## 2.43.0
|
||||
|
||||
Released 2016-05-18
|
||||
|
||||
New features:
|
||||
- Overviews now support dataviews and filtering #449
|
||||
|
||||
|
||||
## 2.42.2
|
||||
|
||||
Released 2016-05-17
|
||||
|
||||
New features:
|
||||
- turbo-carto: mapnik substitution tokens support #455
|
||||
|
||||
|
||||
## 2.42.1
|
||||
|
||||
Released 2016-05-17
|
||||
- Upgraded turbo-carto to fix reversed color scales
|
||||
|
||||
|
||||
## 2.42.0
|
||||
|
||||
Released 2016-05-16
|
||||
|
||||
Bug fixes:
|
||||
- Fix named maps with analysis #453
|
||||
|
||||
Enhancements:
|
||||
- Use split strategy for head/tails turbo-carto quantification
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.9.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.9.0)
|
||||
|
||||
|
||||
## 2.41.1
|
||||
|
||||
Released 2016-05-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.8.0](https://github.com/CartoDB/camshaft/releases/tag/0.8.0)
|
||||
|
||||
Bug fixes:
|
||||
- Nicer error message when missing sql from layer options #446
|
||||
|
||||
|
||||
## 2.41.0
|
||||
|
||||
Released 2016-05-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.7.0](https://github.com/CartoDB/camshaft/releases/tag/0.7.0)
|
||||
|
||||
|
||||
## 2.40.0
|
||||
|
||||
Released 2016-05-10
|
||||
|
||||
Enhancements:
|
||||
- Use original query from source nodes #444
|
||||
|
||||
New features:
|
||||
- Allow override zoom+center or bbox for static named maps previews #443
|
||||
- Analysis layers can have a sql_wrap option to wrap node queries #441
|
||||
|
||||
|
||||
## 2.39.0
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades step-profiler to 0.3.0 to avoid dots in json keys #438
|
||||
- Use a more aggressive cache control header for node status endpoint
|
||||
|
||||
|
||||
## 2.38.1
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Fixes problem in turbo-carto dependency
|
||||
- Removes console usages
|
||||
|
||||
|
||||
## 2.38.0
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.7.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.7.0)
|
||||
|
||||
|
||||
## 2.37.0
|
||||
|
||||
Released 2016-05-03
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.6.0](https://github.com/CartoDB/camshaft/releases/tag/0.6.0)
|
||||
|
||||
|
||||
## 2.36.1
|
||||
|
||||
Released 2016-04-29
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.5.1](https://github.com/CartoDB/camshaft/releases/tag/0.5.1)
|
||||
|
||||
|
||||
## 2.36.0
|
||||
|
||||
Released 2016-04-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.19.0](https://github.com/CartoDB/Windshaft/releases/tag/1.19.0)
|
||||
|
||||
|
||||
## 2.35.0
|
||||
|
||||
Released 2016-04-27
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.18.0](https://github.com/CartoDB/Windshaft/releases/tag/1.18.0)
|
||||
- Appends columns to layers from associated dataviews
|
||||
|
||||
|
||||
## 2.34.1
|
||||
|
||||
Released 2016-04-27
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.17.3](https://github.com/CartoDB/Windshaft/releases/tag/1.17.3)
|
||||
|
||||
|
||||
## 2.34.0
|
||||
|
||||
Released 2016-04-27
|
||||
|
||||
Enhancements:
|
||||
- Adds support to return multiple errors in BaseController.sendError #423
|
||||
- Starts using turbo-carto dependency
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.17.2](https://github.com/CartoDB/Windshaft/releases/tag/1.17.2)
|
||||
|
||||
|
||||
## 2.33.1
|
||||
|
||||
Released 2016-04-20
|
||||
|
||||
Bug fixes:
|
||||
- Support unneeded schema names in overviews queries #421
|
||||
|
||||
|
||||
## 2.33.0
|
||||
|
||||
Released 2016-04-20
|
||||
|
||||
New features:
|
||||
- Adds experimental support for analysis and dataviews
|
||||
|
||||
Announcements:
|
||||
- Upgrades cartodb-psql to 0.6.1 version.
|
||||
- Upgrades windshaft to [1.17.1](https://github.com/CartoDB/Windshaft/releases/tag/1.17.1)
|
||||
|
||||
|
||||
## 2.32.0
|
||||
|
||||
Released 2016-04-06
|
||||
|
||||
New features:
|
||||
- Added support for dynamic styling for widgets in named maps
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.17.0](https://github.com/CartoDB/Windshaft/releases/tag/1.17.0)
|
||||
|
||||
|
||||
## 2.31.2
|
||||
|
||||
Released 2016-04-04
|
||||
@@ -8,6 +436,7 @@ Bug fixes:
|
||||
- Overviews integration for named layers #400
|
||||
- Support wrapped queries in named layers #405
|
||||
|
||||
|
||||
## 2.31.1
|
||||
|
||||
Released 2016-03-23
|
||||
|
||||
17
app.js
17
app.js
@@ -14,6 +14,11 @@ if ( process.argv[2] ) {
|
||||
ENVIRONMENT = 'development';
|
||||
}
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var availableEnvironments = {
|
||||
production: true,
|
||||
staging: true,
|
||||
@@ -22,8 +27,8 @@ var availableEnvironments = {
|
||||
|
||||
// sanity check
|
||||
if (!availableEnvironments[ENVIRONMENT]){
|
||||
console.error('node app.js [environment]');
|
||||
console.error('environments: %s', Object.keys(availableEnvironments).join(', '));
|
||||
logError('node app.js [environment]');
|
||||
logError('environments: %s', Object.keys(availableEnvironments).join(', '));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -58,10 +63,10 @@ if ( global.environment.log_filename ) {
|
||||
// See cwd inlog4js.configure call below
|
||||
logdir = path.resolve(__dirname, logdir);
|
||||
if ( ! fs.existsSync(logdir) ) {
|
||||
console.error("Log filename directory does not exist: " + logdir);
|
||||
logError("Log filename directory does not exist: " + logdir);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Logs will be written to " + global.environment.log_filename);
|
||||
log("Logs will be written to " + global.environment.log_filename);
|
||||
log4js_config.appenders.push(
|
||||
{ type: "file", filename: global.environment.log_filename }
|
||||
);
|
||||
@@ -94,7 +99,7 @@ var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, b
|
||||
var version = require("./package").version;
|
||||
|
||||
listener.on('listening', function() {
|
||||
console.log(
|
||||
log(
|
||||
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
|
||||
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
|
||||
);
|
||||
@@ -111,7 +116,7 @@ process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.logger = global.log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -163,6 +163,10 @@ var config = {
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
|
||||
},
|
||||
@@ -191,6 +195,22 @@ var config = {
|
||||
}
|
||||
}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
// batch configuration
|
||||
batch: {
|
||||
// Inline execution avoid the use of SQL API as batch endpoint
|
||||
// When set to true it will run all analysis queries in series, with a direct connection to the DB
|
||||
// This might be useful for:
|
||||
// - testing
|
||||
// - running an standalone server without any dependency on external services
|
||||
inlineExecution: false,
|
||||
// where the SQL API is running, it will use a custom Host header to specify the username.
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-dev/millstone-dev'
|
||||
|
||||
@@ -157,6 +157,10 @@ var config = {
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
|
||||
},
|
||||
@@ -185,6 +189,22 @@ var config = {
|
||||
}
|
||||
}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
// batch configuration
|
||||
batch: {
|
||||
// Inline execution avoid the use of SQL API as batch endpoint
|
||||
// When set to true it will run all analysis queries in series, with a direct connection to the DB
|
||||
// This might be useful for:
|
||||
// - testing
|
||||
// - running an standalone server without any dependency on external services
|
||||
inlineExecution: false,
|
||||
// where the SQL API is running, it will use a custom Host header to specify the username.
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
|
||||
@@ -157,6 +157,10 @@ var config = {
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
|
||||
},
|
||||
@@ -185,6 +189,22 @@ var config = {
|
||||
}
|
||||
}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
// batch configuration
|
||||
batch: {
|
||||
// Inline execution avoid the use of SQL API as batch endpoint
|
||||
// When set to true it will run all analysis queries in series, with a direct connection to the DB
|
||||
// This might be useful for:
|
||||
// - testing
|
||||
// - running an standalone server without any dependency on external services
|
||||
inlineExecution: false,
|
||||
// where the SQL API is running, it will use a custom Host header to specify the username.
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
|
||||
@@ -156,7 +156,11 @@ var config = {
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false // this requires postgis >=2.2 and geos >=3.5
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
}
|
||||
},
|
||||
http: {
|
||||
@@ -186,6 +190,22 @@ var config = {
|
||||
}
|
||||
}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
// batch configuration
|
||||
batch: {
|
||||
// Inline execution avoid the use of SQL API as batch endpoint
|
||||
// When set to true it will run all analysis queries in series, with a direct connection to the DB
|
||||
// This might be useful for:
|
||||
// - testing
|
||||
// - running an standalone server without any dependency on external services
|
||||
inlineExecution: true,
|
||||
// where the SQL API is running, it will use a custom Host header to specify the username.
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-test/millstone'
|
||||
|
||||
93
docs/MapConfig-Analyses-extension.md
Normal file
93
docs/MapConfig-Analyses-extension.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.4.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.4.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension targets layers with `sql` option, including layer types: `cartodb`, `mapnik`, and `torque`.
|
||||
|
||||
It extends MapConfig with a new attribute: `analyses`.
|
||||
|
||||
## 2.1 Analyses attribute
|
||||
|
||||
The new analyses attribute must be an array of analyses as per [camshaft](https://github.com/CartoDB/camshaft). Each
|
||||
analysis must adhere to the [camshaft-reference](https://github.com/CartoDB/camshaft/blob/0.8.0/reference/versions/0.7.0/reference.json) specification.
|
||||
|
||||
Each node can have an id that can be later references to consume the query from MapConfig's layers.
|
||||
|
||||
Basic analyses example:
|
||||
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `id` free identifier that can be reference from any layer
|
||||
"id": "HEAD",
|
||||
// REQUIRED
|
||||
// string, `type` camshaft's analysis type
|
||||
"type": "source",
|
||||
// REQUIRED
|
||||
// object, `params` will depend on `type`, check camshaft-reference for more information
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# 2.2. Integration with layers
|
||||
|
||||
As pointed before an analysis node id can be referenced from layers to consume its output query.
|
||||
|
||||
The layer consuming the output must reference it with the following option:
|
||||
|
||||
```
|
||||
{
|
||||
"options": {
|
||||
// REQUIRED
|
||||
// object, `source` as in the future we might want to have other source options
|
||||
"source": {
|
||||
// REQUIRED
|
||||
// string, `id` the analysis node identifier
|
||||
"id": "HEAD"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "...",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyses": [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
314
docs/MapConfig-Dataviews-extension.md
Normal file
314
docs/MapConfig-Dataviews-extension.md
Normal file
@@ -0,0 +1,314 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.4.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.4.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
|
||||
|
||||
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
|
||||
|
||||
## 2.1. Dataview types
|
||||
|
||||
### List
|
||||
|
||||
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the list type
|
||||
“type”: “list”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// array, `columns` to select for the list
|
||||
“columns”: [“name”, “description”]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "list",
|
||||
"rows": [
|
||||
{
|
||||
"{columnName1}": "val1",
|
||||
"{columnName2}": 100
|
||||
},
|
||||
{
|
||||
"{columnName1}": "val2",
|
||||
"{columnName2}": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the aggregation type
|
||||
“type”: “aggregation”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “country”,
|
||||
// REQUIRED
|
||||
// string, `aggregation` operation to perform
|
||||
“aggregation”: “count”
|
||||
// OPTIONAL
|
||||
// string, `aggregationColumn` column value to aggregate
|
||||
// This param is required when `aggregation` is different than "count"
|
||||
“aggregationColumn”: “population”
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "aggregation",
|
||||
"categories": [
|
||||
{
|
||||
"category": "foo",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"category": "bar",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Histograms
|
||||
|
||||
Histograms represent the data distribution for a column.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the histogram type
|
||||
“type”: “histogram”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “name”,
|
||||
// OPTIONAL
|
||||
// number, `bins` how many buckets the histogram should use
|
||||
“bins”: 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "histogram",
|
||||
"bins": [{"bin": 0, "start": 2, "end": 2, "min": 2, "max": 2, "freq": 1}, null, null, {"bin": 3, "min": 40, "max": 44, "freq": 2}, null],
|
||||
"width": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Formula
|
||||
|
||||
Formulas given a final value representing the whole dataset.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the formula type
|
||||
“type”: “formula”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// string, `column` column name to aggregate by
|
||||
“column”: “name”,
|
||||
// REQUIRED
|
||||
// string, `aggregation` operation to perform
|
||||
“operation”: “count”
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Operation must be: “min”, “max”, “count”, “avg”, or “sum”.
|
||||
|
||||
Result
|
||||
```
|
||||
{
|
||||
"type": "formula",
|
||||
"operation": "count",
|
||||
"result": 1000,
|
||||
"nulls": 0
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 2.2 Dataviews attribute
|
||||
|
||||
The new dataviews attribute must be a dictionary of dataviews.
|
||||
|
||||
An analysis node id can be referenced from dataviews to consume its output query.
|
||||
|
||||
|
||||
The layer consuming the output must reference it with the following option:
|
||||
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// object, `source` as in the future we might want to have other source options
|
||||
"source": {
|
||||
// REQUIRED
|
||||
// string, `id` the analysis node identifier
|
||||
"id": "HEAD"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.3. Complete example
|
||||
|
||||
```
|
||||
{
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "...",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataviews" {
|
||||
"basic_histogram": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"type": "histogram",
|
||||
"options": {
|
||||
"column": "pop_max"
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyses": [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from your_table"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Filters
|
||||
|
||||
Camshaft's analyses expose a filtering capability and `aggregation` and `histogram` dataviews get them for free with
|
||||
this extension. Filters are available with the very dataview id, so if you have a "basic_histogram" histogram dataview
|
||||
you can filter with a range filter with "basic_histogram" name.
|
||||
|
||||
|
||||
## 3.1 Filter types
|
||||
|
||||
### Category
|
||||
|
||||
Allows to remove results that are not contained within a set of elements.
|
||||
Initially this filter can be applied to a `numeric` or `text` columns.
|
||||
|
||||
Params
|
||||
|
||||
```
|
||||
{
|
||||
“accept”: [“Spain”, “Germany”]
|
||||
“reject”: [“Japan”]
|
||||
}
|
||||
```
|
||||
|
||||
### Range filter
|
||||
|
||||
Allows to remove results that don’t satisfy numeric min and max values.
|
||||
Filter is applied to a numeric column.
|
||||
|
||||
Params
|
||||
|
||||
```
|
||||
{
|
||||
“min”: 0,
|
||||
“max”: 1000
|
||||
}
|
||||
```
|
||||
|
||||
## 3.2. How to apply filters
|
||||
|
||||
Filters must be applied at map instantiation time.
|
||||
|
||||
With :mapconfig as a valid MapConfig and with :filters (a valid JSON) as:
|
||||
|
||||
### Anonymous map
|
||||
|
||||
`GET /api/v1/map?config=:mapconfig&filters=:filters`
|
||||
|
||||
`POST /api/v1/map?filters=:filters`
|
||||
with `BODY=:mapconfig`
|
||||
|
||||
If in the future we need to support a bigger filters param and it doesn’t fit in the query string,
|
||||
we might solve it by accepting:
|
||||
|
||||
`POST /api/v1/map`
|
||||
with `BODY={“config”: :mapconfig, “filters”: :filters}`
|
||||
|
||||
### Named map
|
||||
|
||||
Assume :params (a valid JSON) as named maps params, like in: `{“color”: “red”}`
|
||||
|
||||
`GET /api/v1/named/:name/jsonp?config=:params&filters=:filters&callback=cb`
|
||||
|
||||
`POST /api/v1/named/:name?filters=:filters`
|
||||
with `BODY=:params`
|
||||
|
||||
If, again, in the future we need to support a bigger filters param that doesn’t fit in the query string,
|
||||
we might solve it by accepting:
|
||||
|
||||
`POST /api/v1/named/:name`
|
||||
with `BODY={“config”: :params, “filters”: :filters}`
|
||||
|
||||
|
||||
## 3.3 Bounding box special filter
|
||||
|
||||
A bounding box filter allows to remove results that don’t satisfy a geospatial range.
|
||||
|
||||
The bounding box special filter is available per dataview and there is no need to create a bounding box definition as
|
||||
it’s always possible to apply a bbox filter per dataview.
|
||||
|
||||
A dataview can get its result filtered by bounding box by sending a bbox param in the query string,
|
||||
param must be in the form `west,south,east,north`.
|
||||
|
||||
So applying a bbox filter to a dataview looks like:
|
||||
GET /api/v1/map/:layergroupid/dataview/:dataview_name?bbox=-90,-45,90,45
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0-alpha
|
||||
|
||||
- WIP document
|
||||
120
docs/Routes.md
120
docs/Routes.md
@@ -11,9 +11,6 @@ This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/:z/:x/:y.(:format) {:user(f),:token(f),:layer(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Per :layer rendering based on :format [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/attributes/:fid {:user(f),:token(f),:layer(f),:fid(f)} (1)`
|
||||
<br/>Notes: Endpoint for info windows data, alternative for sql api when tables are private [0]
|
||||
|
||||
@@ -23,47 +20,60 @@ This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format {:user(f),:token(f),:west(f),:south(f),:east(f),:north(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static Maps API [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/widget/:widgetName {:user(f),:token(f),:layer(f),:widgetName(f)} (1)`
|
||||
<br/>Notes: By :widgetName per :layer widget [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/widget/:widgetName/search {:user(f),:token(f),:layer(f),:widgetName(f)} (1)`
|
||||
<br/>Notes: By :widgetName per :layer widget search [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `POST (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/jsonp {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named maps JSONP instantiation [1]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Instantiate named map [1]
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: CORS [0]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/:layer/:z/:x/:y.(:format) {:user(f),:template_id(f),:layer(f),:z(f),:x(f),:y(f),:0(f),:format(f)} (1)`
|
||||
<br/>Notes: Per :layer fixed URL named map tiles [1]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/named/:template_id/:width/:height.:format {:user(f),:template_id(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static map for named maps [1]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: Create named map (w/ API KEY) [1]
|
||||
|
||||
1. `PUT (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Update a named map (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named map retrieval (w/ API KEY) [1]
|
||||
|
||||
1. `DELETE (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Delete named map (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: List named maps (w/ API KEY) [1]
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: CORS [1]
|
||||
|
||||
1. `GET /health {} (1)`
|
||||
<br/>Notes: Health check
|
||||
|
||||
1. `GET / {} (1)`
|
||||
<br/>Notes: Welcome message
|
||||
|
||||
1. `GET /version {} (1)`
|
||||
<br/>Notes: Return relevant module versions: mapnik, grainstore, etc
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/jsonp {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named maps JSONP instantiation [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named map retrieval (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: List named maps (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/named/:template_id/:width/:height.:format {:user(f),:template_id(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static map for named maps
|
||||
|
||||
1. `GET /health {} (1)`
|
||||
<br/>Notes: Healt check
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: CORS [0]
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: CORS [1]
|
||||
|
||||
1. `POST (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: Create named map (w/ API KEY) [1]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Instantiate named map [1]
|
||||
|
||||
1. `PUT (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Update a named map (w/ API KEY) [1]
|
||||
|
||||
1. `DELETE (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Delete named map (w/ API KEY) [1]
|
||||
|
||||
## Optional deprecated routes
|
||||
|
||||
@@ -75,29 +85,29 @@ This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
Something like the following patch should do the trick
|
||||
|
||||
```javascript
|
||||
diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js
|
||||
index b9429a2..e6cc5f9 100644
|
||||
--- a/lib/cartodb/cartodb_windshaft.js
|
||||
+++ b/lib/cartodb/cartodb_windshaft.js
|
||||
@@ -212,6 +212,20 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
}
|
||||
});
|
||||
diff --git a/lib/cartodb/server.js b/lib/cartodb/server.js
|
||||
index 5f62850..bca377d 100644
|
||||
--- a/lib/cartodb/server.js
|
||||
+++ b/lib/cartodb/server.js
|
||||
@@ -215,6 +215,20 @@ module.exports = function(serverOptions) {
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
+ var format = require('util').format;
|
||||
+ var routesNotes = Object.keys(ws.routes.routes)
|
||||
+ .map(function(method) { return ws.routes.routes[method]; })
|
||||
+ .reduce(function(previous, current) { current.map(function(r) { previous.push(r) }); return previous;}, [])
|
||||
+ .map(function(route) {
|
||||
+ return format("\n1. `%s %s {%s} (%d)`\n<br/>Notes: [DEPRECATED]? ",
|
||||
+ route.method.toUpperCase(),
|
||||
+ route.path,
|
||||
+ route.keys.map(function(k) { return format(':%s(%s)', k.name, k.optional ? 't' : 'f'); } ).join(','),
|
||||
+ route.callbacks.length
|
||||
+ var routesNotes = app._router.stack
|
||||
+ .filter(function(handler) { return !!handler.route; })
|
||||
+ .map(function(handler) {
|
||||
+ return format("\n1. `%s %s {%s} (1)`\n<br/>Notes: [DEPRECATED]? ",
|
||||
+ Object.keys(handler.route.methods)[0].toUpperCase(),
|
||||
+ handler.route.path,
|
||||
+ handler.keys.map(function(k) {
|
||||
+ return format(':%s(%s)', k.name, k.optional ? 't' : 'f');
|
||||
+ }).join(',')
|
||||
+ );
|
||||
+ });
|
||||
+ console.log(routesNotes.join('\n'));
|
||||
+
|
||||
return ws;
|
||||
return app;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -451,6 +451,7 @@ callback({
|
||||
```
|
||||
|
||||
## CartoDB.js for Named Maps
|
||||
|
||||
You can use a Named Map that you created (which is defined by its `name`), to create a map using CartoDB.js. This is achieved by adding the [`namedmap` type](http://docs.cartodb.com/cartodb-platform/cartodb-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
|
||||
```javascript
|
||||
@@ -478,16 +479,48 @@ You can use a Named Map that you created (which is defined by its `name`), to cr
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Instantiating a Named Map over a `createLayer` does not require an API Key and by default, does not include auth tokens. _If_ you defined auth tokens for the Named Map configuration, then you will have to include them.
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js/) has methods for accessing your Named Maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
|
||||
**Note:** The CartoDB.js `layer.setParams()` function is not supported when using Named Maps for Torque.
|
||||
**Note:** The CartoDB.js `layer.setParams()` function is not supported when using Named Maps for Torque. Alternatively, you can create a [Torque layer in a Named Map](http://bl.ocks.org/iriberri/de37be6406f9cc7cfe5a)
|
||||
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
### Torque Layer in a Named Map
|
||||
|
||||
If you are creating a Torque layer in a Named Map without using the Torque.js library, you can apply the Torque layer by applying the following code with CartoDBjs:
|
||||
|
||||
```javascript
|
||||
// add cartodb layer with one sublayer
|
||||
cartodb.createLayer(map, {
|
||||
user_name: '{username}',
|
||||
type: 'torque',
|
||||
order: 1,
|
||||
options: {
|
||||
query: "",
|
||||
table_name: "named_map_tutorial_table",
|
||||
user_name: "{username}",
|
||||
tile_style: 'Map { -torque-frame-count:512; -torque-animation-duration:10; -torque-time-attribute:"cartodb_id"; -torque-aggregation-function:"count(cartodb_id)"; -torque-resolution:2; -torque-data-aggregation:linear; } #named_map_tutorial_table_copy{ comp-op: lighter; marker-fill-opacity: 0.9; marker-line-color: #FFF; marker-line-width: 1.5; marker-line-opacity: 1; marker-type: ellipse; marker-width: 6; marker-fill: #FF9900; } #named_map_tutorial_table_copy[frame-offset=1] { marker-width:8; marker-fill-opacity:0.45; } #named_map_tutorial_table_copy[frame-offset=2] { marker-width:10; marker-fill-opacity:0.225; }'
|
||||
|
||||
},
|
||||
named_map: {
|
||||
name: "{namedmap_example}",
|
||||
layers: [{
|
||||
layer_name: "t"
|
||||
}]
|
||||
}
|
||||
})
|
||||
.addTo(map)
|
||||
.done(function(layer) {
|
||||
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples of Named Maps created with CartoDB.js
|
||||
|
||||
- [Named Map selectors with interaction](http://bl.ocks.org/ohasselblad/515a8af1f99d5e690484)
|
||||
|
||||
@@ -49,8 +49,6 @@ bbox | the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
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.
|
||||
|
||||
|
||||
59
lib/cartodb/api/filter_stats_api.js
Normal file
59
lib/cartodb/api/filter_stats_api.js
Normal file
@@ -0,0 +1,59 @@
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var CamshaftFilter = require('../models/filter/camshaft');
|
||||
|
||||
function FilterStatsApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = FilterStatsApi;
|
||||
|
||||
function getEstimatedRows(pgQueryRunner, username, query, callback) {
|
||||
pgQueryRunner.run(username, "EXPLAIN (FORMAT JSON)"+query, function(err, result_rows) {
|
||||
if (err){
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
var rows;
|
||||
if ( result_rows[0] && result_rows[0]['QUERY PLAN'] &&
|
||||
result_rows[0]['QUERY PLAN'][0] && result_rows[0]['QUERY PLAN'][0].Plan ) {
|
||||
rows = result_rows[0]['QUERY PLAN'][0].Plan['Plan Rows'];
|
||||
}
|
||||
return callback(null, rows);
|
||||
});
|
||||
}
|
||||
|
||||
FilterStatsApi.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
|
||||
var stats = {};
|
||||
var self = this;
|
||||
step(
|
||||
function getUnfilteredRows() {
|
||||
getEstimatedRows(self.pgQueryRunner, username, unfiltered_query, this);
|
||||
},
|
||||
function receiveUnfilteredRows(err, rows) {
|
||||
if (err){
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
stats.unfiltered_rows = rows;
|
||||
this(null, rows);
|
||||
},
|
||||
function getFilteredRows() {
|
||||
if ( filters && !_.isEmpty(filters)) {
|
||||
var camshaftFilter = new CamshaftFilter(filters);
|
||||
var query = camshaftFilter.sql(unfiltered_query);
|
||||
getEstimatedRows(self.pgQueryRunner, username, query, this);
|
||||
} else {
|
||||
this(null, null);
|
||||
}
|
||||
},
|
||||
function receiveFilteredRows(err, rows) {
|
||||
if (err){
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
stats.filtered_rows = rows;
|
||||
callback(null, stats);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,43 +1,40 @@
|
||||
var SubstitutionTokens = require('../utils/substitution-tokens');
|
||||
|
||||
function OverviewsMetadataApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = OverviewsMetadataApi;
|
||||
|
||||
// TODO: share this with QueryTablesApi? ... or maintain independence?
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1')
|
||||
;
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
OverviewsMetadataApi.prototype.getOverviewsMetadata = function (username, sql, callback) {
|
||||
var query = 'SELECT * FROM CDB_Overviews(CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$))';
|
||||
// FIXME: Currently using internal function _cdb_schema_name
|
||||
// CDB_Overviews should provide the schema information directly.
|
||||
var query = 'SELECT *, _cdb_schema_name(base_table)' +
|
||||
' FROM CDB_Overviews(CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$))';
|
||||
this.pgQueryRunner.run(username, query, function handleOverviewsRows(err, rows) {
|
||||
if (err){
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
var metadata = {};
|
||||
rows.forEach(function(row) {
|
||||
var metadata = rows.reduce(function(metadata, row){
|
||||
var table = row.base_table;
|
||||
var table_metadata = metadata[table];
|
||||
if ( !table_metadata ) {
|
||||
table_metadata = metadata[table] = {};
|
||||
var schema = row._cdb_schema_name;
|
||||
if ( !metadata[table] ) {
|
||||
metadata[table] = {};
|
||||
}
|
||||
table_metadata[row.z] = { table: row.overview_table };
|
||||
});
|
||||
metadata[table][row.z] = { table: row.overview_table };
|
||||
metadata[table].schema = schema;
|
||||
return metadata;
|
||||
}, {});
|
||||
return callback(null, metadata);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
58
lib/cartodb/backends/analysis-status.js
Normal file
58
lib/cartodb/backends/analysis-status.js
Normal file
@@ -0,0 +1,58 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
|
||||
function AnalysisStatusBackend() {
|
||||
}
|
||||
|
||||
module.exports = AnalysisStatusBackend;
|
||||
|
||||
|
||||
AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
var nodeId = params.nodeId;
|
||||
|
||||
var statusQuery = [
|
||||
'SELECT node_id, status, updated_at, last_error_message as error_message',
|
||||
'FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\''
|
||||
].join(' ');
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
pg.query(statusQuery, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
result = result || {};
|
||||
|
||||
var rows = result.rows || [];
|
||||
|
||||
var statusResponse = rows[0] || {
|
||||
node_id: nodeId,
|
||||
status: 'unknown'
|
||||
};
|
||||
|
||||
if (statusResponse.status !== 'failed') {
|
||||
delete statusResponse.error_message;
|
||||
}
|
||||
|
||||
return callback(null, statusResponse);
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
19
lib/cartodb/backends/analysis.js
Normal file
19
lib/cartodb/backends/analysis.js
Normal file
@@ -0,0 +1,19 @@
|
||||
var camshaft = require('camshaft');
|
||||
|
||||
function AnalysisBackend(options) {
|
||||
var batchConfig = options.batch || {};
|
||||
batchConfig.endpoint = batchConfig.endpoint || 'http://127.0.0.1:8080/api/v1/sql/job';
|
||||
batchConfig.inlineExecution = batchConfig.inlineExecution || false;
|
||||
batchConfig.hostHeaderTemplate = batchConfig.hostHeaderTemplate || '{{=it.username}}.localhost.lan';
|
||||
this.batchConfig = batchConfig;
|
||||
}
|
||||
|
||||
module.exports = AnalysisBackend;
|
||||
|
||||
AnalysisBackend.prototype.create = function(analysisConfiguration, analysisDefinition, callback) {
|
||||
analysisConfiguration.batch.endpoint = this.batchConfig.endpoint;
|
||||
analysisConfiguration.batch.inlineExecution = this.batchConfig.inlineExecution;
|
||||
analysisConfiguration.batch.hostHeaderTemplate = this.batchConfig.hostHeaderTemplate;
|
||||
|
||||
camshaft.create(analysisConfiguration, analysisDefinition, callback);
|
||||
};
|
||||
341
lib/cartodb/backends/dataview.js
Normal file
341
lib/cartodb/backends/dataview.js
Normal file
@@ -0,0 +1,341 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var camshaft = require('camshaft');
|
||||
var step = require('step');
|
||||
|
||||
var Timer = require('../stats/timer');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
|
||||
var DataviewFactory = require('../models/dataview/factory');
|
||||
var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory');
|
||||
var OverviewsQueryRewriter = require('../utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function DataviewBackend(analysisBackend) {
|
||||
this.analysisBackend = analysisBackend;
|
||||
}
|
||||
|
||||
module.exports = DataviewBackend;
|
||||
|
||||
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var timer = new Timer();
|
||||
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
var mapConfig;
|
||||
var dataviewDefinition;
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function _getDataviewDefinition(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
mapConfig = _mapConfig;
|
||||
|
||||
var _dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!_dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
dataviewDefinition = _dataviewDefinition;
|
||||
|
||||
return dataviewDefinition;
|
||||
},
|
||||
function loadAnalysis(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisConfiguration = {
|
||||
user: user,
|
||||
db: {
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname,
|
||||
user: params.dbuser,
|
||||
pass: params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: user,
|
||||
apiKey: params.api_key
|
||||
}
|
||||
};
|
||||
|
||||
var sourceId = dataviewDefinition.source.id;
|
||||
var analysisDefinition = getAnalysisDefinition(mapConfig.obj().analyses, sourceId);
|
||||
|
||||
var next = this;
|
||||
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function(err, analysis) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = {};
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Node[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Node[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
var node = sourceId2Node[sourceId];
|
||||
|
||||
if (!node) {
|
||||
return next(new Error('Analysis node not found for dataview'));
|
||||
}
|
||||
|
||||
return next(null, node);
|
||||
});
|
||||
},
|
||||
function runDataviewQuery(err, node) {
|
||||
assert.ifError(err);
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = layerQuery(node, dataviewName, ownFilter);
|
||||
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(
|
||||
mapConfig.obj().layers,
|
||||
function(l){ return l.options.source && (l.options.source.id === sourceId); }
|
||||
);
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if ( queryRewriteData ) {
|
||||
if ( node.type === 'source' ) {
|
||||
var filters = node.getFilters();
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
var unfiltered_query = node.getQuery(filters_disabler);
|
||||
queryRewriteData = _.extend(
|
||||
{},
|
||||
queryRewriteData, { filters: filters, unfiltered_query: unfiltered_query }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
if ( queryRewriteData ) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
}
|
||||
|
||||
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
|
||||
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
|
||||
);
|
||||
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
overrides[k] = Number.isFinite(+val) ? +val : val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, overrideParams, this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result, timer.getTimes());
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var timer = new Timer();
|
||||
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
var mapConfig;
|
||||
var dataviewDefinition;
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function _getDataviewDefinition(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
mapConfig = _mapConfig;
|
||||
|
||||
var _dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!_dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
dataviewDefinition = _dataviewDefinition;
|
||||
|
||||
return dataviewDefinition;
|
||||
},
|
||||
function loadAnalysis(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisConfiguration = {
|
||||
user: user,
|
||||
db: {
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname,
|
||||
user: params.dbuser,
|
||||
pass: params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
// TODO load this from configuration
|
||||
endpoint: 'http://127.0.0.1:8080/api/v1/sql/job',
|
||||
username: user,
|
||||
apiKey: params.api_key
|
||||
}
|
||||
};
|
||||
|
||||
var sourceId = dataviewDefinition.source.id;
|
||||
var analysisDefinition = getAnalysisDefinition(mapConfig.obj().analyses, sourceId);
|
||||
|
||||
var next = this;
|
||||
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function(err, analysis) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = {};
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Node[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Node[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
var node = sourceId2Node[sourceId];
|
||||
|
||||
if (!node) {
|
||||
return next(new Error('Analysis node not found for dataview'));
|
||||
}
|
||||
|
||||
return next(null, node);
|
||||
});
|
||||
},
|
||||
function runDataviewQuery(err, node) {
|
||||
assert.ifError(err);
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = layerQuery(node, dataviewName, ownFilter);
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
}
|
||||
|
||||
var userQuery = params.q;
|
||||
|
||||
var dataview = DataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.search(pg, userQuery, this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result, timer.getTimes());
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getAnalysisDefinition(mapConfigAnalyses, sourceId) {
|
||||
mapConfigAnalyses = mapConfigAnalyses || [];
|
||||
for (var i = 0; i < mapConfigAnalyses.length; i++) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(mapConfigAnalyses[i]);
|
||||
var nodes = analysisGraph.getNodesWithId();
|
||||
if (nodes.hasOwnProperty(sourceId)) {
|
||||
return mapConfigAnalyses[i];
|
||||
}
|
||||
}
|
||||
throw new Error('There is no associated analysis for the dataview source id');
|
||||
}
|
||||
|
||||
function getDataviewDefinition(mapConfig, dataviewName) {
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
return dataviews[dataviewName];
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
|
||||
var SKIP_COLUMNS = {
|
||||
'the_geom': true,
|
||||
'the_geom_webmercator': true
|
||||
};
|
||||
|
||||
function skipColumns(columnNames) {
|
||||
return columnNames
|
||||
.filter(function(columnName) { return !SKIP_COLUMNS[columnName]; });
|
||||
}
|
||||
|
||||
var layerQueryTemplate = dot.template([
|
||||
'SELECT {{=it._columns}}',
|
||||
'FROM ({{=it._query}}) _cdb_analysis_query'
|
||||
].join('\n'));
|
||||
|
||||
function layerQuery(node, dataviewName, ownFilter) {
|
||||
var applyFilters = {};
|
||||
if (!ownFilter) {
|
||||
applyFilters[dataviewName] = false;
|
||||
}
|
||||
|
||||
if (node.type === 'source') {
|
||||
return node.getQuery(applyFilters);
|
||||
}
|
||||
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
|
||||
return layerQueryTemplate({ _query: node.getQuery(applyFilters), _columns: _columns.join(', ') });
|
||||
}
|
||||
@@ -477,7 +477,11 @@ o.instance = function(template, params) {
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
if ( lyropt.cartocss ) {
|
||||
|
||||
if ( params.styles && params.styles[i] ) {
|
||||
// dynamic styling for this layer
|
||||
lyropt.cartocss = params.styles[i];
|
||||
} else if ( lyropt.cartocss ) {
|
||||
lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
}
|
||||
if ( lyropt.sql) {
|
||||
|
||||
101
lib/cartodb/backends/turbo-carto-postgres-datasource.js
Normal file
101
lib/cartodb/backends/turbo-carto-postgres-datasource.js
Normal file
@@ -0,0 +1,101 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function createTemplate(method) {
|
||||
return dot.template([
|
||||
'SELECT',
|
||||
method,
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
methodTemplates[methodName] = createTemplate(methods[methodName]);
|
||||
return methodTemplates;
|
||||
}, {});
|
||||
|
||||
methodTemplates.category = dot.template([
|
||||
'WITH',
|
||||
'categories AS (',
|
||||
' SELECT {{=it._column}} AS category, count(1) AS value, row_number() OVER (ORDER BY count(1) desc) as rank',
|
||||
' FROM ({{=it._sql}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
'),',
|
||||
'agg_categories AS (',
|
||||
' SELECT \'__other\' category',
|
||||
' FROM categories',
|
||||
' WHERE rank >= {{=it._buckets}}',
|
||||
' GROUP BY 1',
|
||||
' UNION ALL',
|
||||
' SELECT CAST(category AS text)',
|
||||
' FROM categories',
|
||||
' WHERE rank < {{=it._buckets}}',
|
||||
')',
|
||||
'SELECT array_agg(category) AS category FROM agg_categories'
|
||||
].join('\n'));
|
||||
|
||||
var STRATEGY = {
|
||||
SPLIT: 'split',
|
||||
EXACT: 'exact'
|
||||
};
|
||||
|
||||
var method2strategy = {
|
||||
headtails: STRATEGY.SPLIT,
|
||||
category: STRATEGY.EXACT
|
||||
};
|
||||
|
||||
function PostgresDatasource (psql, query) {
|
||||
this.psql = psql;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
PostgresDatasource.prototype.getName = function () {
|
||||
return 'PostgresDatasource';
|
||||
};
|
||||
|
||||
PostgresDatasource.prototype.getRamp = function (column, buckets, method, callback) {
|
||||
if (method && !methodTemplates.hasOwnProperty(method)) {
|
||||
return callback(new Error(
|
||||
'Invalid method "' + method + '", valid methods: [' + Object.keys(methodTemplates).join(',') + ']'
|
||||
));
|
||||
}
|
||||
var methodName = method || 'quantiles';
|
||||
var template = methodTemplates[methodName];
|
||||
|
||||
var query = template({ _column: column, _sql: this.query, _buckets: buckets });
|
||||
|
||||
this.psql.query(query, function (err, resultSet) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
resultSet = resultSet || {};
|
||||
var result = resultSet.rows || [];
|
||||
|
||||
var strategy = method2strategy[methodName];
|
||||
var ramp = result[0][methodName] || [];
|
||||
// Skip null values from ramp
|
||||
// Generated turbo-carto won't be correct, but better to keep it working than failing
|
||||
// TODO fix cartodb-postgres extension quantification functions
|
||||
ramp = ramp.filter(function(value) { return value !== null; });
|
||||
if (strategy !== STRATEGY.EXACT) {
|
||||
ramp = ramp.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, { ramp: ramp, strategy: strategy });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
module.exports = PostgresDatasource;
|
||||
16
lib/cartodb/cache/named_map_provider_cache.js
vendored
16
lib/cartodb/cache/named_map_provider_cache.js
vendored
@@ -1,20 +1,17 @@
|
||||
var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
var queue = require('queue-async');
|
||||
|
||||
var LruCache = require("lru-cache");
|
||||
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, overviewsAdapter, turboCartocssAdapter) {
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
this.turboCartocssAdapter = turboCartocssAdapter;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
|
||||
this.providerCache = new LruCache({ max: 2000 });
|
||||
}
|
||||
@@ -30,10 +27,9 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok
|
||||
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.namedLayersAdapter,
|
||||
this.overviewsAdapter,
|
||||
this.turboCartocssAdapter,
|
||||
this.mapConfigAdapter,
|
||||
user,
|
||||
templateId,
|
||||
config,
|
||||
|
||||
@@ -14,6 +14,9 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// widgets & filters
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
@@ -197,7 +200,10 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
@@ -208,15 +214,15 @@ BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = { errors: [errorMessage(err)] };
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
@@ -224,6 +230,32 @@ function errorMessage(err) {
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
|
||||
@@ -7,7 +7,10 @@ var BaseController = require('./base');
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
|
||||
var DataviewBackend = require('../backends/dataview');
|
||||
var AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
|
||||
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
@@ -18,14 +21,14 @@ var QueryTables = require('cartodb-query-tables');
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {WidgetBackend} widgetBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
widgetBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
@@ -33,10 +36,12 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.widgetBackend = widgetBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
@@ -72,64 +77,100 @@ LayergroupController.prototype.register = function(app) {
|
||||
// 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));
|
||||
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
|
||||
this.widgetSearch.bind(this));
|
||||
'/:token/dataview/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
this.analysisNodeStatus.bind(this));
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widget = function(req, res) {
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
function retrieveNodeStatus(err) {
|
||||
assert.ifError(err);
|
||||
self.analysisStatusBackend.getNodeStatus(req.params, this);
|
||||
},
|
||||
function finish(err, nodeStatus, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET NODE STATUS');
|
||||
} else {
|
||||
self.sendResponse(req, res, nodeStatus, 200, {
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataview = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.getWidget(mapConfigProvider, req.params, this);
|
||||
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
self.sendError(req, res, err, 'GET DATAVIEW');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widgetSearch = function(req, res) {
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
function searchDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.search(mapConfigProvider, req.params, this);
|
||||
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -15,9 +15,8 @@ var Datasource = windshaft.model.Datasource;
|
||||
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_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 NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -25,15 +24,14 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_laye
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {OverviewsMetadataApi} overviewsMetadataApi
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables,
|
||||
overviewsAdapter, turboCartoCssAdapter) {
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
@@ -44,10 +42,8 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.turboCartoCssAdapter = turboCartoCssAdapter;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
@@ -129,61 +125,35 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
|
||||
var mapConfig;
|
||||
|
||||
var context = {};
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function beforeLayergroupCreate(err, requestMapConfig) {
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.namedLayersAdapter.getLayers(req.context.user, requestMapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
requestMapConfig.layers = layers;
|
||||
}
|
||||
return next(null, requestMapConfig, datasource);
|
||||
context.analysisConfiguration = {
|
||||
user: req.context.user,
|
||||
db: {
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: req.context.user,
|
||||
apiKey: req.params.api_key
|
||||
}
|
||||
);
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
|
||||
},
|
||||
function addOverviewsInformation(err, requestMapConfig, datasource) {
|
||||
function createLayergroup(err, requestMapConfig) {
|
||||
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 parseTurboCartoCss(err, requestMapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
self.turboCartoCssAdapter.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());
|
||||
var datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, req.params,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
|
||||
@@ -192,14 +162,15 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
addWidgetsUrl(req.context.user, layergroup);
|
||||
|
||||
var analysesResults = context.analysesResults || [];
|
||||
addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
|
||||
addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
@@ -227,10 +198,9 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.metadataBackend,
|
||||
self.userLimitsApi,
|
||||
self.namedLayersAdapter,
|
||||
self.overviewsAdapter,
|
||||
self.turboCartoCssAdapter,
|
||||
self.mapConfigAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
@@ -250,7 +220,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, mapConfigProvider.analysesResults, this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
@@ -259,7 +229,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);
|
||||
addDataviewsAndWidgetsUrls(cdbuser, layergroup, mapConfig.obj());
|
||||
addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
@@ -270,8 +241,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
@@ -330,12 +300,13 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result);
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + result.getLastUpdatedAt();
|
||||
layergroup.last_updated = new Date(result.getLastUpdatedAt()).toISOString();
|
||||
var lastUpdateTime = result.getLastUpdatedAt();
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
addWidgetsUrl(username, layergroup);
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
@@ -354,20 +325,80 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
);
|
||||
};
|
||||
|
||||
function addWidgetsUrl(username, layergroup) {
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
return analysesResults.reduce(function(lastUpdateTime, analysis) {
|
||||
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
|
||||
var nodeUpdatedAtDate = node.getUpdatedAt();
|
||||
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
|
||||
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
|
||||
}, lastUpdateTime);
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers)) {
|
||||
function addAnalysesMetadata(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce(function(nodesIdMap, node) {
|
||||
if (node.params.id) {
|
||||
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
|
||||
nodesIdMap[node.params.id] = {
|
||||
status: node.getStatus(),
|
||||
url: getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodesIdMap[node.params.id].query = node.getQuery();
|
||||
}
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
function addDataviewsAndWidgetsUrls(username, layergroup, mapConfig) {
|
||||
addDataviewsUrls(username, layergroup, mapConfig);
|
||||
addWidgetsUrl(username, layergroup, mapConfig);
|
||||
}
|
||||
|
||||
function addDataviewsUrls(username, layergroup, mapConfig) {
|
||||
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
|
||||
layergroup.metadata.dataviews[dataviewName] = {
|
||||
url: getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function addWidgetsUrl(username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (layer.widgets) {
|
||||
Object.keys(layer.widgets).forEach(function(widgetName) {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
|
||||
layer.widgets = layer.widgets || {};
|
||||
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName].url = getUrls(username, resource);
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getUrls(username, resource) {
|
||||
|
||||
@@ -140,7 +140,7 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
function prepareImageOptions(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
namedMapProvider = _namedMapProvider;
|
||||
self.getStaticImageOptions(cdbUser, namedMapProvider, this);
|
||||
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
@@ -192,9 +192,37 @@ var DEFAULT_ZOOM_CENTER = {
|
||||
}
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMapProvider, callback) {
|
||||
function numMapper(n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, params, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
|
||||
if ([params.zoom, params.lon, params.lat].map(numMapper).every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
zoom: params.zoom,
|
||||
center: {
|
||||
lng: params.lon,
|
||||
lat: params.lat
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bbox = params.bbox.split(',').map(numMapper);
|
||||
if (bbox.length === 4 && bbox.every(Number.isFinite)) {
|
||||
return callback(null, {
|
||||
bounds: {
|
||||
west: bbox[0],
|
||||
south: bbox[1],
|
||||
east: bbox[2],
|
||||
north: bbox[3]
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
namedMapProvider.getTemplate(this);
|
||||
@@ -205,6 +233,9 @@ NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMap
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+params.zoom)) {
|
||||
zoomCenter.zoom = +params.zoom;
|
||||
}
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
var windshaft = require('windshaft');
|
||||
var HealthCheck = require('../monitoring/health_check');
|
||||
|
||||
var WELCOME_MSG = "This is the CartoDB Maps API, " +
|
||||
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
|
||||
|
||||
|
||||
var versions = {
|
||||
windshaft: windshaft.version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
windshaft_cartodb: require('../../../package.json').version
|
||||
};
|
||||
|
||||
function ServerInfoController() {
|
||||
function ServerInfoController(versions) {
|
||||
this.healthConfig = global.environment.health || {};
|
||||
this.healthCheck = new HealthCheck(global.environment.disabled_file);
|
||||
this.versions = versions || {};
|
||||
}
|
||||
|
||||
module.exports = ServerInfoController;
|
||||
@@ -31,7 +23,7 @@ ServerInfoController.prototype.welcome = function(req, res) {
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.version = function(req, res) {
|
||||
res.status(200).send(versions);
|
||||
res.status(200).send(this.versions);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.health = function(req, res) {
|
||||
|
||||
282
lib/cartodb/models/dataview/aggregation.js
Normal file
282
lib/cartodb/models/dataview/aggregation.js
Normal file
@@ -0,0 +1,282 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:aggregation');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' count(1) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
count: [],
|
||||
sum: ['aggregationColumn'],
|
||||
avg: ['aggregationColumn'],
|
||||
min: ['aggregationColumn'],
|
||||
max: ['aggregationColumn']
|
||||
};
|
||||
|
||||
var TYPE = 'aggregation';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'count' // it could be, e.g., sum if column is numeric
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Aggregation(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Aggregation expects `column` in widget options');
|
||||
}
|
||||
|
||||
if (!_.isString(options.aggregation)) {
|
||||
throw new Error('Aggregation expects `aggregation` operation in widget options');
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
|
||||
}
|
||||
|
||||
var requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
var missingOptions = _.difference(requiredOptions, Object.keys(options));
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = new BaseWidget();
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
|
||||
Aggregation.prototype.format = function(result) {
|
||||
var categories = [];
|
||||
var count = 0;
|
||||
var nulls = 0;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var categoriesCount = 0;
|
||||
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
count = firstRow.count;
|
||||
nulls = firstRow.nulls_count;
|
||||
minValue = firstRow.min_val;
|
||||
maxValue = firstRow.max_val;
|
||||
categoriesCount = firstRow.categories_count;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val', 'max_val', 'categories_count'));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
categoriesCount: categoriesCount,
|
||||
categories: categories
|
||||
};
|
||||
};
|
||||
|
||||
var filterCategoriesQueryTpl = dot.template([
|
||||
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_search',
|
||||
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
|
||||
'GROUP BY {{=it._column}}'
|
||||
].join('\n'));
|
||||
|
||||
var searchQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'search_unfiltered AS (',
|
||||
' {{=it._searchUnfiltered}}',
|
||||
'),',
|
||||
'search_filtered AS (',
|
||||
' {{=it._searchFiltered}}',
|
||||
'),',
|
||||
'search_union AS (',
|
||||
' SELECT * FROM search_unfiltered',
|
||||
' UNION ALL',
|
||||
' SELECT * FROM search_filtered',
|
||||
')',
|
||||
'SELECT category, sum(value) AS value',
|
||||
'FROM search_union',
|
||||
'GROUP BY category',
|
||||
'ORDER BY value desc'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
var self = this;
|
||||
|
||||
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
var query = searchQueryTpl({
|
||||
_searchUnfiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: '0',
|
||||
_userQuery: _userQuery
|
||||
}),
|
||||
_searchFiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: 'count(1)',
|
||||
_userQuery: _userQuery
|
||||
})
|
||||
});
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, {type: self.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
Aggregation.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Aggregation.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
};
|
||||
26
lib/cartodb/models/dataview/base.js
Normal file
26
lib/cartodb/models/dataview/base.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function BaseDataview() {}
|
||||
|
||||
module.exports = BaseDataview;
|
||||
|
||||
BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
var self = this;
|
||||
this.sql(psql, override, function(err, query) {
|
||||
psql.query(query, function(err, result) {
|
||||
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
result = self.format(result, override);
|
||||
result.type = self.getType();
|
||||
|
||||
return callback(null, result);
|
||||
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
BaseDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
};
|
||||
18
lib/cartodb/models/dataview/factory.js
Normal file
18
lib/cartodb/models/dataview/factory.js
Normal file
@@ -0,0 +1,18 @@
|
||||
var dataviews = require('./');
|
||||
|
||||
var DataviewFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
|
||||
getDataview: function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
if (!this.dataviews[type]) {
|
||||
throw new Error('Invalid dataview type: "' + type + '"');
|
||||
}
|
||||
return new this.dataviews[type](query, dataviewDefinition.options);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataviewFactory;
|
||||
104
lib/cartodb/models/dataview/formula.js
Normal file
104
lib/cartodb/models/dataview/formula.js
Normal file
@@ -0,0 +1,104 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:formula');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
'{{=it._operation}}({{=it._column}}) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n'));
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
count: true,
|
||||
avg: true,
|
||||
sum: true,
|
||||
min: true,
|
||||
max: true
|
||||
};
|
||||
|
||||
var TYPE = 'formula';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'name',
|
||||
operation: 'count' // count, sum, avg
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Formula(query, options) {
|
||||
if (!_.isString(options.operation)) {
|
||||
throw new Error('Formula expects `operation` in widget options');
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error("Formula does not support '" + options.operation + "' operation");
|
||||
}
|
||||
|
||||
if (options.operation !== 'count' && !_.isString(options.column)) {
|
||||
throw new Error('Formula expects `column` in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: _query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
Formula.prototype.format = function(result) {
|
||||
var formattedResult = {
|
||||
operation: this.operation,
|
||||
result: 0,
|
||||
nulls: 0
|
||||
};
|
||||
|
||||
if (result.rows.length) {
|
||||
formattedResult.operation = this.operation;
|
||||
formattedResult.result = result.rows[0].result;
|
||||
formattedResult.nulls = result.rows[0].nulls_count;
|
||||
}
|
||||
|
||||
return formattedResult;
|
||||
};
|
||||
|
||||
Formula.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Formula.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
};
|
||||
307
lib/cartodb/models/dataview/histogram.js
Normal file
307
lib/cartodb/models/dataview/histogram.js
Normal file
@@ -0,0 +1,307 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:dataview:histogram');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' avg({{=it._column}})::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
var TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name',
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Histogram(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
Histogram.prototype = new BaseWidget();
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.query
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
debug('overriding with %j', override);
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override)
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype.format = function(result, override) {
|
||||
override = override || {};
|
||||
var buckets = [];
|
||||
|
||||
var binsCount = getBinsCount(override);
|
||||
var width = getWidth(override);
|
||||
var binsStart = getBinStart(override);
|
||||
var nulls = 0;
|
||||
var avg;
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
binsCount = firstRow.bins_number;
|
||||
width = firstRow.bin_width || width;
|
||||
avg = firstRow.avg_val;
|
||||
nulls = firstRow.nulls_count;
|
||||
binsStart = override.hasOwnProperty('start') ? getBinStart(override) : firstRow.min;
|
||||
|
||||
buckets = result.rows.map(function(row) {
|
||||
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: width,
|
||||
bins_count: binsCount,
|
||||
bins_start: binsStart,
|
||||
nulls: nulls,
|
||||
avg: avg,
|
||||
bins: buckets
|
||||
};
|
||||
};
|
||||
|
||||
function getBinStart(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
function getBinEnd(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
function getBinsCount(override) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
|
||||
function getWidth(override) {
|
||||
var width = 0;
|
||||
var binsCount = override.bins;
|
||||
|
||||
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
|
||||
width = (override.end - override.start) / binsCount;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
Histogram.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Histogram.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
};
|
||||
6
lib/cartodb/models/dataview/index.js
Normal file
6
lib/cartodb/models/dataview/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
66
lib/cartodb/models/dataview/list.js
Normal file
66
lib/cartodb/models/dataview/list.js
Normal file
@@ -0,0 +1,66 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var BaseWidget = require('./base');
|
||||
|
||||
var TYPE = 'list';
|
||||
|
||||
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name', 'description']
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function List(query, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!Array.isArray(options.columns)) {
|
||||
throw new Error('List expects `columns` array in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.columns = options.columns;
|
||||
}
|
||||
|
||||
List.prototype = new BaseWidget();
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
|
||||
List.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
}
|
||||
|
||||
var listSql = listSqlTpl({
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
|
||||
return callback(null, listSql);
|
||||
};
|
||||
|
||||
List.prototype.format = function(result) {
|
||||
return {
|
||||
rows: result.rows
|
||||
};
|
||||
};
|
||||
|
||||
List.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
List.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
};
|
||||
141
lib/cartodb/models/dataview/overviews/aggregation.js
Normal file
141
lib/cartodb/models/dataview/overviews/aggregation.js
Normal file
@@ -0,0 +1,141 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../aggregation');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' sum(_feature_count) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, filters, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = {
|
||||
count: dot.template('sum(_feature_count)'),
|
||||
sum: dot.template('sum({{=it._aggregationColumn}}*_feature_count)')
|
||||
};
|
||||
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl[this.aggregation]({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
88
lib/cartodb/models/dataview/overviews/base.js
Normal file
88
lib/cartodb/models/dataview/overviews/base.js
Normal file
@@ -0,0 +1,88 @@
|
||||
var _ = require('underscore');
|
||||
var BaseDataview = require('../base');
|
||||
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options) {
|
||||
this.BaseDataview = BaseDataview;
|
||||
this.query = query;
|
||||
this.queryOptions = queryOptions;
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions);
|
||||
}
|
||||
|
||||
module.exports = BaseOverviewsDataview;
|
||||
|
||||
BaseOverviewsDataview.prototype = new BaseDataview();
|
||||
BaseOverviewsDataview.prototype.constructor = BaseOverviewsDataview;
|
||||
|
||||
// TODO: parameterized these settings
|
||||
var SETTINGS = {
|
||||
// use overviews as a default fallback strategy
|
||||
defaultOverviews: false,
|
||||
|
||||
// minimum ratio of bounding box size to grid size
|
||||
// (this would ideally be based on the viewport size in pixels)
|
||||
zoomLevelFactor: 1024.0
|
||||
};
|
||||
|
||||
// Compute zoom level so that the the resolution grid size of the
|
||||
// selected overview is smaller (zoomLevelFactor times smaller at least)
|
||||
// than the bounding box size.
|
||||
BaseOverviewsDataview.prototype.zoomLevelForBbox = function(bbox) {
|
||||
var pxPerTile = 256.0;
|
||||
var earthWidth = 360.0;
|
||||
// TODO: now we assume overviews are computed for 1-pixel tolerance;
|
||||
// should use extended overviews metadata to compute this properly.
|
||||
if ( bbox ) {
|
||||
var bboxValues = _.map(bbox.split(','), function(v) { return +v; });
|
||||
var w = Math.abs(bboxValues[2]-bboxValues[0]);
|
||||
var h = Math.abs(bboxValues[3]-bboxValues[1]);
|
||||
var maxDim = Math.min(w, h);
|
||||
|
||||
// Find minimum suitable z
|
||||
// note that the QueryRewirter will use the minimum level overview
|
||||
// of level >= z if it exists, and otherwise the base table
|
||||
var z = Math.ceil(-Math.log(maxDim*pxPerTile/earthWidth/SETTINGS.zoomLevelFactor)/Math.log(2.0));
|
||||
return Math.max(z, 0);
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.rewrittenQuery = function(query) {
|
||||
var zoom_level = this.zoomLevelForBbox(this.options.bbox);
|
||||
return this.queryRewriter.query(query, this.queryRewriteData, { zoom_level: zoom_level });
|
||||
};
|
||||
|
||||
// Default behaviour
|
||||
BaseOverviewsDataview.prototype.defaultSql = function(psql, filters, override, callback) {
|
||||
var query = this.query;
|
||||
var dataview = this.baseDataview;
|
||||
if ( SETTINGS.defaultOverviews ) {
|
||||
query = this.rewrittenQuery(query);
|
||||
dataview = new this.BaseDataview(query, this.queryOptions);
|
||||
}
|
||||
return dataview.sql(psql, filters, override, callback);
|
||||
};
|
||||
|
||||
// default implementation that can be override in derived classes:
|
||||
|
||||
BaseOverviewsDataview.prototype.sql = function(psql, filters, override, callback) {
|
||||
return this.defaultSql(psql, filters, override, callback);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return this.baseDataview.search(psql, userQuery, callback);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.format = function(result) {
|
||||
return this.baseDataview.format(result);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.getType = function() {
|
||||
return this.baseDataview.getType();
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.toString = function() {
|
||||
return this.baseDataview.toString();
|
||||
};
|
||||
32
lib/cartodb/models/dataview/overviews/factory.js
Normal file
32
lib/cartodb/models/dataview/overviews/factory.js
Normal file
@@ -0,0 +1,32 @@
|
||||
var parentFactory = require('../factory');
|
||||
var dataviews = require('./');
|
||||
|
||||
function OverviewsDataviewFactory(queryRewriter, queryRewriteData, options) {
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
OverviewsDataviewFactory.prototype.getDataview = function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
var dataviews = OverviewsDataviewMetaFactory.dataviews;
|
||||
if ( !this.queryRewriter || !this.queryRewriteData || !dataviews[type] ) {
|
||||
return parentFactory.getDataview(query, dataviewDefinition);
|
||||
}
|
||||
return new dataviews[type](
|
||||
query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options
|
||||
);
|
||||
};
|
||||
|
||||
var OverviewsDataviewMetaFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
|
||||
getFactory: function(queryRewriter, queryRewriteData, options) {
|
||||
return new OverviewsDataviewFactory(queryRewriter, queryRewriteData, options);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = OverviewsDataviewMetaFactory;
|
||||
56
lib/cartodb/models/dataview/overviews/formula.js
Normal file
56
lib/cartodb/models/dataview/overviews/formula.js
Normal file
@@ -0,0 +1,56 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
};
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, filters, override, callback) {
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if ( formulaQueryTpl ) {
|
||||
// supported formula for use with overviews
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
callback = callback || override;
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, filters, override, callback);
|
||||
};
|
||||
217
lib/cartodb/models/dataview/overviews/histogram.js
Normal file
217
lib/cartodb/models/dataview/overviews/histogram.js
Normal file
@@ -0,0 +1,217 @@
|
||||
var _ = require('underscore');
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../histogram');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count)::numeric AS avg,',
|
||||
' sum(_feature_count) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
function Histogram(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
Histogram.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.rewrittenQuery(this.query)
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: override.start,
|
||||
_end: override.end
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
6
lib/cartodb/models/dataview/overviews/index.js
Normal file
6
lib/cartodb/models/dataview/overviews/index.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
};
|
||||
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
11
lib/cartodb/models/dataview/overviews/list.js
Normal file
@@ -0,0 +1,11 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../list');
|
||||
|
||||
function List(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
}
|
||||
|
||||
List.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
122
lib/cartodb/models/filter/bbox.js
Normal file
122
lib/cartodb/models/filter/bbox.js
Normal file
@@ -0,0 +1,122 @@
|
||||
var debug = require('debug')('windshaft:filter:bbox');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var filterQueryTpl = dot.template([
|
||||
'SELECT * FROM ({{=it._sql}}) _cdb_bbox_filter',
|
||||
'WHERE {{=it._filters}}'
|
||||
].join('\n'));
|
||||
|
||||
var bboxFilterTpl = dot.template(
|
||||
'{{=it._column}} && ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}})'
|
||||
);
|
||||
|
||||
var LATITUDE_MAX_VALUE = 85.0511287798066;
|
||||
var LONGITUDE_LOWER_BOUND = -180;
|
||||
var LONGITUDE_UPPER_BOUND = 180;
|
||||
var LONGITUDE_RANGE = LONGITUDE_UPPER_BOUND - LONGITUDE_LOWER_BOUND;
|
||||
|
||||
/**
|
||||
Definition
|
||||
{
|
||||
"type”: "bbox",
|
||||
"options": {
|
||||
"column": "the_geom_webmercator",
|
||||
"srid": 3857
|
||||
}
|
||||
}
|
||||
|
||||
Params
|
||||
{
|
||||
“bbox”: "west,south,east,north"
|
||||
}
|
||||
*/
|
||||
function BBox(filterDefinition, filterParams) {
|
||||
var bbox = filterParams.bbox;
|
||||
|
||||
if (!bbox) {
|
||||
throw new Error('BBox filter expects to have a bbox param');
|
||||
}
|
||||
|
||||
var bboxElements = bbox.split(',').map(function(e) { return +e; });
|
||||
|
||||
validateBboxElements(bboxElements);
|
||||
|
||||
this.column = filterDefinition.column || 'the_geom_webmercator';
|
||||
this.srid = filterDefinition.srid || 3857;
|
||||
|
||||
// Latitudes must be within max extent
|
||||
var south = Math.max(bboxElements[1], -LATITUDE_MAX_VALUE);
|
||||
var north = Math.min(bboxElements[3], LATITUDE_MAX_VALUE);
|
||||
|
||||
// Longitudes crossing 180º need another approach
|
||||
var adjustedLongitudeRange = adjustLongitudeRange([bboxElements[0], bboxElements[2]]);
|
||||
var west = adjustedLongitudeRange[0];
|
||||
var east = adjustedLongitudeRange[1];
|
||||
|
||||
this.bboxes = getBoundingBoxes(west, south, east, north);
|
||||
}
|
||||
|
||||
function getBoundingBoxes(west, south, east, north) {
|
||||
var bboxes = [];
|
||||
|
||||
if (east - west >= 360) {
|
||||
bboxes.push([-180, south, 180, north]);
|
||||
} else if (west >= -180 && east <= 180) {
|
||||
bboxes.push([west, south, east, north]);
|
||||
} else {
|
||||
bboxes.push([west, south, 180, north]);
|
||||
bboxes.push([-180, south, east % 180, north]);
|
||||
}
|
||||
|
||||
return bboxes;
|
||||
}
|
||||
|
||||
function validateBboxElements(bboxElements) {
|
||||
var isNumericBbox = bboxElements
|
||||
.map(function(n) { return Number.isFinite(n); })
|
||||
.reduce(function(allFinite, isFinite) {
|
||||
if (!allFinite) {
|
||||
return false;
|
||||
}
|
||||
return isFinite;
|
||||
}, true);
|
||||
|
||||
if (bboxElements.length !== 4 || !isNumericBbox) {
|
||||
throw new Error('Invalid bbox filter, expected format="west,south,east,north"');
|
||||
}
|
||||
}
|
||||
|
||||
function adjustLongitudeRange(we) {
|
||||
var west = we[0];
|
||||
west -= LONGITUDE_LOWER_BOUND;
|
||||
west = west - (LONGITUDE_RANGE * Math.floor(west / LONGITUDE_RANGE)) + LONGITUDE_LOWER_BOUND;
|
||||
|
||||
var longitudeRange = Math.min(we[1] - we[0], 360);
|
||||
|
||||
return [west, west + longitudeRange];
|
||||
}
|
||||
|
||||
module.exports = BBox;
|
||||
|
||||
module.exports.adjustLongitudeRange = adjustLongitudeRange;
|
||||
module.exports.LATITUDE_MAX_VALUE = LATITUDE_MAX_VALUE;
|
||||
module.exports.LONGITUDE_MAX_VALUE = LONGITUDE_UPPER_BOUND;
|
||||
|
||||
|
||||
BBox.prototype.sql = function(rawSql) {
|
||||
var bboxSql = filterQueryTpl({
|
||||
_sql: rawSql,
|
||||
_filters: this.bboxes.map(function(bbox) {
|
||||
return bboxFilterTpl({
|
||||
_column: this.column,
|
||||
_bbox: bbox.join(','),
|
||||
_srid: this.srid
|
||||
});
|
||||
}.bind(this)).join(' OR ')
|
||||
});
|
||||
|
||||
debug(bboxSql);
|
||||
|
||||
return bboxSql;
|
||||
};
|
||||
35
lib/cartodb/models/filter/camshaft.js
Normal file
35
lib/cartodb/models/filter/camshaft.js
Normal file
@@ -0,0 +1,35 @@
|
||||
var filters = {
|
||||
category: require('./camshaft/category'),
|
||||
range: require('./camshaft/range')
|
||||
};
|
||||
|
||||
function createFilter(filterDefinition) {
|
||||
var filterType = filterDefinition.type.toLowerCase();
|
||||
if (!filters.hasOwnProperty(filterType)) {
|
||||
throw new Error('Unknown filter type: ' + filterType);
|
||||
}
|
||||
return new filters[filterType](filterDefinition.column, filterDefinition.params);
|
||||
}
|
||||
|
||||
function CamshaftFilters(filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
CamshaftFilters.prototype.sql = function(rawSql) {
|
||||
var filters = this.filters || {};
|
||||
var applyFilters = {};
|
||||
|
||||
return Object.keys(filters)
|
||||
.filter(function(filterName) {
|
||||
return applyFilters.hasOwnProperty(filterName) ? applyFilters[filterName] : true;
|
||||
})
|
||||
.map(function(filterName) {
|
||||
var filterDefinition = filters[filterName];
|
||||
return createFilter(filterDefinition);
|
||||
})
|
||||
.reduce(function(sql, filter) {
|
||||
return filter.sql(sql);
|
||||
}, rawSql);
|
||||
};
|
||||
|
||||
module.exports = CamshaftFilters;
|
||||
79
lib/cartodb/models/filter/camshaft/category.js
Normal file
79
lib/cartodb/models/filter/camshaft/category.js
Normal file
@@ -0,0 +1,79 @@
|
||||
'use strict';
|
||||
|
||||
var debug = require('debug')('windshaft:filter:category');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var filterQueryTpl = dot.template([
|
||||
'SELECT *',
|
||||
'FROM ({{=it._sql}}) _camshaft_category_filter',
|
||||
'WHERE {{=it._filters}}'
|
||||
].join('\n'));
|
||||
var escapeStringTpl = dot.template('$escape_{{=it._i}}${{=it._value}}$escape_{{=it._i}}$');
|
||||
var inConditionTpl = dot.template('{{=it._column}} IN ({{=it._values}})');
|
||||
var notInConditionTpl = dot.template('{{=it._column}} NOT IN ({{=it._values}})');
|
||||
|
||||
function Category(column, filterParams) {
|
||||
this.column = column;
|
||||
|
||||
if (!Array.isArray(filterParams.accept) && !Array.isArray(filterParams.reject)) {
|
||||
throw new Error('Category filter expects at least one array in accept or reject params');
|
||||
}
|
||||
|
||||
if (Array.isArray(filterParams.accept) && Array.isArray(filterParams.reject)) {
|
||||
if (filterParams.accept.length === 0 && filterParams.reject.length === 0) {
|
||||
throw new Error(
|
||||
'Category filter expects one value either in accept or reject params when both are provided'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.accept = filterParams.accept;
|
||||
this.reject = filterParams.reject;
|
||||
}
|
||||
|
||||
module.exports = Category;
|
||||
|
||||
/*
|
||||
- accept: [] => reject all
|
||||
- reject: [] => accept all
|
||||
*/
|
||||
Category.prototype.sql = function(rawSql) {
|
||||
var valueFilters = [];
|
||||
|
||||
if (Array.isArray(this.accept)) {
|
||||
if (this.accept.length > 0) {
|
||||
valueFilters.push(inConditionTpl({
|
||||
_column: this.column,
|
||||
_values: this.accept.map(function(value, i) {
|
||||
return Number.isFinite(value) ? value : escapeStringTpl({_i: i, _value: value});
|
||||
}).join(',')
|
||||
}));
|
||||
} else {
|
||||
valueFilters.push('0 = 1');
|
||||
}
|
||||
}
|
||||
|
||||
if (Array.isArray(this.reject)) {
|
||||
if (this.reject.length > 0) {
|
||||
valueFilters.push(notInConditionTpl({
|
||||
_column: this.column,
|
||||
_values: this.reject.map(function (value, i) {
|
||||
return Number.isFinite(value) ? value : escapeStringTpl({_i: i, _value: value});
|
||||
}).join(',')
|
||||
}));
|
||||
} else {
|
||||
valueFilters.push('1 = 1');
|
||||
}
|
||||
}
|
||||
|
||||
debug(filterQueryTpl({
|
||||
_sql: rawSql,
|
||||
_filters: valueFilters.join(' AND ')
|
||||
}));
|
||||
|
||||
return filterQueryTpl({
|
||||
_sql: rawSql,
|
||||
_filters: valueFilters.join(' AND ')
|
||||
});
|
||||
};
|
||||
43
lib/cartodb/models/filter/camshaft/range.js
Normal file
43
lib/cartodb/models/filter/camshaft/range.js
Normal file
@@ -0,0 +1,43 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var betweenFilterTpl = dot.template('{{=it._column}} BETWEEN {{=it._min}} AND {{=it._max}}');
|
||||
var minFilterTpl = dot.template('{{=it._column}} >= {{=it._min}}');
|
||||
var maxFilterTpl = dot.template('{{=it._column}} <= {{=it._max}}');
|
||||
var filterQueryTpl = dot.template('SELECT * FROM ({{=it._sql}}) _camshaft_range_filter WHERE {{=it._filter}}');
|
||||
|
||||
function Range(column, filterParams) {
|
||||
this.column = column;
|
||||
|
||||
if (!Number.isFinite(filterParams.min) && !Number.isFinite(filterParams.max)) {
|
||||
throw new Error('Range filter expect to have at least one value in min or max numeric params');
|
||||
}
|
||||
|
||||
this.min = filterParams.min;
|
||||
this.max = filterParams.max;
|
||||
this.columnType = filterParams.columnType;
|
||||
}
|
||||
|
||||
module.exports = Range;
|
||||
|
||||
Range.prototype.sql = function(rawSql) {
|
||||
var minMaxFilter;
|
||||
if (Number.isFinite(this.min) && Number.isFinite(this.max)) {
|
||||
minMaxFilter = betweenFilterTpl({
|
||||
_column: this.column,
|
||||
_min: this.min,
|
||||
_max: this.max
|
||||
});
|
||||
} else if (Number.isFinite(this.min)) {
|
||||
minMaxFilter = minFilterTpl({ _column: this.column, _min: this.min });
|
||||
} else {
|
||||
minMaxFilter = maxFilterTpl({ _column: this.column, _max: this.max });
|
||||
}
|
||||
|
||||
return filterQueryTpl({
|
||||
_sql: rawSql,
|
||||
_filter: minMaxFilter
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,296 @@
|
||||
var queue = require('queue-async');
|
||||
var debug = require('debug')('windshaft:analysis');
|
||||
|
||||
var camshaft = require('camshaft');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function AnalysisMapConfigAdapter(analysisBackend) {
|
||||
this.analysisBackend = analysisBackend;
|
||||
}
|
||||
|
||||
module.exports = AnalysisMapConfigAdapter;
|
||||
|
||||
AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
// jshint maxcomplexity:7
|
||||
var self = this;
|
||||
|
||||
if (!shouldAdaptLayers(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var analysisConfiguration = context.analysisConfiguration;
|
||||
|
||||
var filters = {};
|
||||
if (params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
var dataviewsFilters = filters.dataviews || {};
|
||||
debug(dataviewsFilters);
|
||||
var dataviews = requestMapConfig.dataviews || {};
|
||||
|
||||
var errors = getDataviewsErrors(dataviews);
|
||||
if (errors.length > 0) {
|
||||
return callback(errors);
|
||||
}
|
||||
|
||||
var dataviewsFiltersBySourceId = Object.keys(dataviewsFilters).reduce(function(bySourceId, dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
if (dataview) {
|
||||
var sourceId = dataview.source.id;
|
||||
if (!bySourceId.hasOwnProperty(sourceId)) {
|
||||
bySourceId[sourceId] = {};
|
||||
}
|
||||
|
||||
bySourceId[sourceId][dataviewName] = getFilter(dataview, dataviewsFilters[dataviewName]);
|
||||
}
|
||||
return bySourceId;
|
||||
}, {});
|
||||
|
||||
debug(dataviewsFiltersBySourceId);
|
||||
|
||||
debug('mapconfig input', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||
|
||||
function createAnalysis(analysisDefinition, done) {
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
|
||||
if (err) {
|
||||
var error = new Error(err.message);
|
||||
error.type = 'analysis';
|
||||
error.analysis = {
|
||||
id: analysisDefinition.id,
|
||||
type: analysisDefinition.type
|
||||
};
|
||||
return done(error);
|
||||
}
|
||||
|
||||
done(null, analysis);
|
||||
});
|
||||
}
|
||||
|
||||
var analysesQueue = queue(requestMapConfig.analyses.length);
|
||||
requestMapConfig.analyses.forEach(function(analysis) {
|
||||
analysesQueue.defer(createAnalysis, analysis);
|
||||
});
|
||||
|
||||
analysesQueue.awaitAll(function(err, analysesResults) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = analysesResults.reduce(function(sourceId2Query, analysis) {
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Query[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Query[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
return sourceId2Query;
|
||||
}, {});
|
||||
|
||||
var missingNodesErrors = [];
|
||||
|
||||
requestMapConfig.layers = requestMapConfig.layers.map(function(layer, layerIndex) {
|
||||
if (getLayerSourceId(layer)) {
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
var layerNode = sourceId2Node[layerSourceId];
|
||||
if (layerNode) {
|
||||
var analysisSql = layerQuery(layerNode);
|
||||
var sqlQueryWrap = layer.options.sql_wrap;
|
||||
if (sqlQueryWrap) {
|
||||
layer.options.sql_raw = analysisSql;
|
||||
analysisSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, analysisSql);
|
||||
}
|
||||
layer.options.sql = analysisSql;
|
||||
layer.options.columns = getDataviewsColumns(getLayerDataviews(layer, dataviews));
|
||||
} else {
|
||||
missingNodesErrors.push(
|
||||
new Error('Missing analysis node.id="' + layerSourceId +'" for layer='+layerIndex)
|
||||
);
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
|
||||
|
||||
debug('mapconfig output', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
var missingDataviewsNodesErrors = getMissingDataviewsSourceIds(dataviews, sourceId2Node);
|
||||
if (missingNodesErrors.length > 0 || missingDataviewsNodesErrors.length > 0) {
|
||||
return callback(missingNodesErrors.concat(missingDataviewsNodesErrors));
|
||||
}
|
||||
|
||||
context.analysesResults = analysesResults;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
|
||||
var SKIP_COLUMNS = {
|
||||
'the_geom': true,
|
||||
'the_geom_webmercator': true
|
||||
};
|
||||
|
||||
function skipColumns(columnNames) {
|
||||
return columnNames
|
||||
.filter(function(columnName) { return !SKIP_COLUMNS[columnName]; });
|
||||
}
|
||||
|
||||
var layerQueryTemplate = dot.template([
|
||||
'SELECT {{=it._columns}}',
|
||||
'FROM ({{=it._query}}) _cdb_analysis_query'
|
||||
].join('\n'));
|
||||
|
||||
function layerQuery(node) {
|
||||
if (node.type === 'source') {
|
||||
return node.getQuery();
|
||||
}
|
||||
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
|
||||
return layerQueryTemplate({ _query: node.getQuery(), _columns: _columns.join(', ') });
|
||||
}
|
||||
|
||||
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
var analyses = requestMapConfig.analyses || [];
|
||||
|
||||
requestMapConfig.analyses = analyses.map(function(analysisDefinition) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(analysisDefinition);
|
||||
var definition = analysisDefinition;
|
||||
Object.keys(dataviewsFiltersBySourceId).forEach(function(sourceId) {
|
||||
definition = analysisGraph.getDefinitionWith(sourceId, {filters: dataviewsFiltersBySourceId[sourceId] });
|
||||
});
|
||||
|
||||
return definition;
|
||||
});
|
||||
|
||||
return requestMapConfig;
|
||||
}
|
||||
|
||||
function shouldAdaptLayers(requestMapConfig) {
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(getLayerSourceId) ||
|
||||
(Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0) ||
|
||||
requestMapConfig.dataviews;
|
||||
}
|
||||
|
||||
var DATAVIEW_TYPE_2_FILTER_TYPE = {
|
||||
aggregation: 'category',
|
||||
histogram: 'range'
|
||||
};
|
||||
function getFilter(dataview, params) {
|
||||
var type = dataview.type;
|
||||
|
||||
return {
|
||||
type: DATAVIEW_TYPE_2_FILTER_TYPE[type],
|
||||
column: dataview.options.column,
|
||||
params: params
|
||||
};
|
||||
}
|
||||
|
||||
function getLayerSourceId(layer) {
|
||||
return layer.options.source && layer.options.source.id;
|
||||
}
|
||||
|
||||
function getDataviewSourceId(dataview) {
|
||||
return dataview.source && dataview.source.id;
|
||||
}
|
||||
|
||||
function getLayerDataviews(layer, dataviews) {
|
||||
var layerDataviews = [];
|
||||
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
if (layerSourceId) {
|
||||
var dataviewsList = getDataviewsList(dataviews);
|
||||
dataviewsList.forEach(function(dataview) {
|
||||
if (getDataviewSourceId(dataview) === layerSourceId) {
|
||||
layerDataviews.push(dataview);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return layerDataviews;
|
||||
}
|
||||
|
||||
function getDataviewsColumns(dataviews) {
|
||||
return Object.keys(dataviews.reduce(function(columnsDict, dataview) {
|
||||
getDataviewColumns(dataview).forEach(function(columnName) {
|
||||
if (!!columnName) {
|
||||
columnsDict[columnName] = true;
|
||||
}
|
||||
});
|
||||
return columnsDict;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
function getDataviewColumns(dataview) {
|
||||
var columns = [];
|
||||
var options = dataview.options;
|
||||
['column', 'aggregationColumn'].forEach(function(opt) {
|
||||
if (options.hasOwnProperty(opt) && !!options[opt]) {
|
||||
columns.push(options[opt]);
|
||||
}
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
function getDataviewsList(dataviews) {
|
||||
return Object.keys(dataviews).map(function(dataviewKey) { return dataviews[dataviewKey]; });
|
||||
}
|
||||
|
||||
function getDataviewsErrors(dataviews) {
|
||||
var dataviewType = typeof dataviews;
|
||||
if (dataviewType !== 'object') {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "' + dataviewType + '" type found')];
|
||||
}
|
||||
|
||||
if (Array.isArray(dataviews)) {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "array" type found')];
|
||||
}
|
||||
|
||||
var errors = [];
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
if (!dataview.hasOwnProperty('source') || !dataview.source.id) {
|
||||
errors.push(new Error('Dataview "' + dataviewName + '" is missing `source.id` attribute'));
|
||||
}
|
||||
|
||||
if (!dataview.type) {
|
||||
errors.push(new Error('Dataview "' + dataviewName + '" is missing `type` attribute'));
|
||||
}
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getMissingDataviewsSourceIds(dataviews, sourceId2Node) {
|
||||
var missingDataviewsSourceIds = [];
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
var dataviewSourceId = getDataviewSourceId(dataview);
|
||||
if (!sourceId2Node.hasOwnProperty(dataviewSourceId)) {
|
||||
missingDataviewsSourceIds.push(new AnalysisError('Node with `source.id="' + dataviewSourceId +'"`' +
|
||||
' not found in analyses for dataview "' + dataviewName + '"'));
|
||||
}
|
||||
});
|
||||
|
||||
return missingDataviewsSourceIds;
|
||||
}
|
||||
|
||||
function AnalysisError(message) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
this.type = 'analysis';
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
require('util').inherits(AnalysisError, Error);
|
||||
@@ -0,0 +1,98 @@
|
||||
function DataviewsWidgetsMapConfigAdapter() {
|
||||
}
|
||||
|
||||
module.exports = DataviewsWidgetsMapConfigAdapter;
|
||||
|
||||
|
||||
DataviewsWidgetsMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
if (!shouldAdapt(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
// prepare placeholders for new dataviews created from widgets
|
||||
requestMapConfig.analyses = requestMapConfig.analyses || [];
|
||||
requestMapConfig.dataviews = requestMapConfig.dataviews || {};
|
||||
|
||||
requestMapConfig.layers.forEach(function(layer, index) {
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
|
||||
if (!layer.options.widgets) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layerSourceId && !layer.options.sql) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataviewSourceId = layerSourceId || 'cdb-layer-source-' + index;
|
||||
// Append a new analysis if layer has no source id but sql.
|
||||
if (!layerSourceId) {
|
||||
requestMapConfig.analyses.push(
|
||||
{
|
||||
id: dataviewSourceId,
|
||||
type: 'source',
|
||||
params: {
|
||||
query: layer.options.sql
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
var source = { id: dataviewSourceId };
|
||||
var layerWidgets = layer.options.widgets || {};
|
||||
Object.keys(layerWidgets).forEach(function(widgetId) {
|
||||
var dataview = layerWidgets[widgetId];
|
||||
requestMapConfig.dataviews[widgetId] = {
|
||||
source: source,
|
||||
type: dataview.type,
|
||||
options: dataview.options
|
||||
};
|
||||
});
|
||||
|
||||
layer.options.source = source;
|
||||
|
||||
delete layer.options.sql;
|
||||
// don't delete widgets for now as it might be useful for old clients
|
||||
//delete layer.options.widgets;
|
||||
});
|
||||
|
||||
// filters have to be rewritten also
|
||||
var filters = getFilters(params);
|
||||
var layersFilters = filters.layers || [];
|
||||
filters.dataviews = filters.dataviews || {};
|
||||
|
||||
layersFilters.forEach(function(layerFilters) {
|
||||
Object.keys(layerFilters).forEach(function(filterName) {
|
||||
if (!filters.dataviews.hasOwnProperty(filterName)) {
|
||||
filters.dataviews[filterName] = layerFilters[filterName];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
delete filters.layers;
|
||||
|
||||
params.filters = JSON.stringify(filters);
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
};
|
||||
|
||||
function shouldAdapt(requestMapConfig) {
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(function hasWidgets(layer) {
|
||||
return layer.options && layer.options.widgets && Object.keys(layer.options.widgets).length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function getLayerSourceId(layer) {
|
||||
return layer.options.source && layer.options.source.id;
|
||||
}
|
||||
|
||||
function getFilters(params) {
|
||||
var filters = {};
|
||||
if (params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return filters;
|
||||
}
|
||||
26
lib/cartodb/models/mapconfig/adapter/index.js
Normal file
26
lib/cartodb/models/mapconfig/adapter/index.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
function MapConfigAdapter(adapters) {
|
||||
this.adapters = Array.isArray(adapters) ? adapters : Array.apply(null, arguments);
|
||||
}
|
||||
|
||||
module.exports = MapConfigAdapter;
|
||||
|
||||
MapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
var i = 0;
|
||||
var tasksLeft = this.adapters.length;
|
||||
|
||||
function next(err, _requestMapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (tasksLeft-- === 0) {
|
||||
return callback(null, _requestMapConfig);
|
||||
}
|
||||
var nextAdapter = self.adapters[i++];
|
||||
nextAdapter.getMapConfig(user, _requestMapConfig, params, context, next);
|
||||
}
|
||||
|
||||
next(null, requestMapConfig);
|
||||
};
|
||||
@@ -2,17 +2,20 @@ var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
var Datasource = require('windshaft').model.Datasource;
|
||||
|
||||
function MapConfigNamedLayersAdapter(templateMaps) {
|
||||
function MapConfigNamedLayersAdapter(templateMaps, pgConnection) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = MapConfigNamedLayersAdapter;
|
||||
|
||||
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
|
||||
MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
|
||||
if (!layers) {
|
||||
return callback(null);
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var adaptLayersQueue = queue(layers.length);
|
||||
@@ -28,9 +31,9 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
var templateConfigParams = layer.options.config || {};
|
||||
var templateAuthTokens = layer.options.auth_tokens;
|
||||
|
||||
self.templateMaps.getTemplate(username, templateName, function(err, template) {
|
||||
self.templateMaps.getTemplate(user, templateName, function(err, template) {
|
||||
if (err || !template) {
|
||||
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
|
||||
return done(new Error("Template '" + templateName + "' of user '" + user + "' not found"));
|
||||
}
|
||||
|
||||
if (self.templateMaps.isAuthorized(template, templateAuthTokens)) {
|
||||
@@ -96,7 +99,10 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
|
||||
});
|
||||
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
requestMapConfig.layers = layers;
|
||||
context.datasource = datasourceBuilder.build();
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,7 +110,7 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
dbMetadata.setDBAuth(username, dbAuth, function(err) {
|
||||
this.pgConnection.setDBAuth(user, dbAuth, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
@@ -114,7 +120,8 @@ MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbM
|
||||
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
|
||||
});
|
||||
} else {
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
context.datasource = datasourceBuilder.build();
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
};
|
||||
@@ -0,0 +1,101 @@
|
||||
var step = require('step');
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
|
||||
function MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi) {
|
||||
this.overviewsMetadataApi = overviewsMetadataApi;
|
||||
this.filterStatsApi = filterStatsApi;
|
||||
}
|
||||
|
||||
module.exports = MapConfigOverviewsAdapter;
|
||||
|
||||
MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
var analysesResults = context.analysesResults;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var augmentLayersQueue = queue(layers.length);
|
||||
|
||||
function augmentLayer(layer, done) {
|
||||
if ( layer.type !== 'mapnik' && layer.type !== 'cartodb' ) {
|
||||
return done(null, layer);
|
||||
}
|
||||
self.overviewsMetadataApi.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
|
||||
if (err) {
|
||||
done(err, layer);
|
||||
} else {
|
||||
var query_rewrite_data = { overviews: metadata };
|
||||
step(
|
||||
function collectFiltersData() {
|
||||
var filters, unfiltered_query;
|
||||
if ( layer.options.source && analysesResults ) {
|
||||
var sourceId = layer.options.source.id;
|
||||
var node = _.find(analysesResults, function(a){ return a.rootNode.params.id === sourceId; });
|
||||
if ( node ) {
|
||||
node = node.rootNode;
|
||||
filters = node.getFilters();
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
unfiltered_query = node.getQuery(filters_disabler);
|
||||
query_rewrite_data.filters = filters;
|
||||
query_rewrite_data.unfiltered_query = unfiltered_query;
|
||||
}
|
||||
}
|
||||
this(null, filters, unfiltered_query);
|
||||
},
|
||||
function collectStatsData(err, filters, unfiltered_query) {
|
||||
var next_step = this;
|
||||
if ( filters ) {
|
||||
self.filterStatsApi.getFilterStats(
|
||||
user,
|
||||
unfiltered_query, filters,
|
||||
function(err, stats) {
|
||||
if ( !err ) {
|
||||
query_rewrite_data.filter_stats = stats;
|
||||
}
|
||||
return next_step(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return next_step(null);
|
||||
}
|
||||
},
|
||||
function addDataToLayer(err) {
|
||||
if ( !err && !_.isEmpty(metadata) ) {
|
||||
layer = _.extend({}, layer);
|
||||
layer.options = _.extend({}, layer.options, { query_rewrite_data: query_rewrite_data });
|
||||
}
|
||||
done(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'));
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
augmentLayersQueue.defer(augmentLayer, layer);
|
||||
});
|
||||
augmentLayersQueue.awaitAll(layersAugmentQueueFinish);
|
||||
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
function SqlWrapMapConfigAdapter() {
|
||||
}
|
||||
|
||||
module.exports = SqlWrapMapConfigAdapter;
|
||||
|
||||
|
||||
SqlWrapMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfig, params, context, callback) {
|
||||
if (requestMapConfig && Array.isArray(requestMapConfig.layers)) {
|
||||
requestMapConfig.layers = requestMapConfig.layers.map(function(layer) {
|
||||
if (layer.options) {
|
||||
var sqlQueryWrap = layer.options.sql_wrap;
|
||||
if (sqlQueryWrap) {
|
||||
var layerSql = layer.options.sql;
|
||||
if (layerSql) {
|
||||
layer.options.sql_raw = layerSql;
|
||||
layer.options.sql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, layerSql);
|
||||
}
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
};
|
||||
167
lib/cartodb/models/mapconfig/adapter/turbo-carto-adapter.js
Normal file
167
lib/cartodb/models/mapconfig/adapter/turbo-carto-adapter.js
Normal file
@@ -0,0 +1,167 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var queue = require('queue-async');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var turboCarto = require('turbo-carto');
|
||||
|
||||
var SubstitutionTokens = require('../../../utils/substitution-tokens');
|
||||
var PostgresDatasource = require('../../../backends/turbo-carto-postgres-datasource');
|
||||
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
function TurboCartoAdapter() {
|
||||
}
|
||||
|
||||
module.exports = TurboCartoAdapter;
|
||||
|
||||
TurboCartoAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
var self = this;
|
||||
|
||||
var layers = requestMapConfig.layers;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
var parseCartoQueue = queue(layers.length);
|
||||
|
||||
layers.forEach(function(layer, index) {
|
||||
var layerId = MapConfig.getLayerId(requestMapConfig, index);
|
||||
parseCartoQueue.defer(self._parseCartoCss.bind(self), user, params, layer, index, layerId);
|
||||
});
|
||||
|
||||
parseCartoQueue.awaitAll(function (err, layers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
|
||||
var tokensQueryTpl = dot.template([
|
||||
'WITH input_query AS (',
|
||||
' {{=it._sql}}',
|
||||
'),',
|
||||
'bbox_query AS (',
|
||||
' SELECT ST_SetSRID(ST_Extent(the_geom_webmercator), 3857) as bbox from input_query',
|
||||
'),',
|
||||
'zoom_query as (',
|
||||
' SELECT GREATEST(',
|
||||
' ceil(log(40075017000 / 256 / GREATEST(ST_XMax(bbox) - ST_XMin(bbox), ST_YMax(bbox) - ST_YMin(bbox)))/log(2)),',
|
||||
' 0) as zoom',
|
||||
' FROM bbox_query',
|
||||
'),',
|
||||
'pixel_size_query as (',
|
||||
' SELECT 40075017 * cos(radians(ST_Y(ST_Transform(ST_Centroid(bbox), 4326)))) / 2 ^ ((zoom) + 8) as pixel_size',
|
||||
' FROM bbox_query, zoom_query',
|
||||
'),',
|
||||
'scale_denominator_query as (',
|
||||
' SELECT (pixel_size / 0.00028)::numeric as scale_denominator',
|
||||
' FROM pixel_size_query',
|
||||
')',
|
||||
'select ST_AsText(bbox) bbox, pixel_size, scale_denominator, zoom',
|
||||
'from bbox_query, pixel_size_query, scale_denominator_query, zoom_query'
|
||||
].join('\n'));
|
||||
|
||||
TurboCartoAdapter.prototype._parseCartoCss = function (username, params, layer, layerIndex, layerId, callback) {
|
||||
if (!shouldParseLayerCartocss(layer)) {
|
||||
return callback(null, layer);
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
function processCallback(err, cartocss) {
|
||||
// Only return turbo-carto errors
|
||||
if (err && err.name === 'TurboCartoError') {
|
||||
var error = new Error('turbo-carto: ' + err.message);
|
||||
error.http_status = 400;
|
||||
error.type = 'layer';
|
||||
error.subtype = 'turbo-carto';
|
||||
error.layer = {
|
||||
id: layerId,
|
||||
index: layerIndex,
|
||||
type: layer.type,
|
||||
context: err.context
|
||||
};
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
// Try to continue in the rest of the cases
|
||||
if (cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
return callback(null, layer);
|
||||
}
|
||||
|
||||
var layerSql = layer.options.sql;
|
||||
var layerRawSql = layer.options.sql_raw;
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
|
||||
var self = this;
|
||||
var tokensQuery = tokensQueryTpl({_sql: layerRawSql});
|
||||
return pg.query(tokensQuery, function(err, resultSet) {
|
||||
if (err) {
|
||||
var error = new Error('turbo-carto: ' + err.message);
|
||||
error.type = 'turbo-carto';
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
var result = rows[0] || {};
|
||||
|
||||
var tokens = {
|
||||
bbox: 'ST_SetSRID(ST_GeomFromText(\'' + result.bbox + '\'), 3857)',
|
||||
scale_denominator: result.scale_denominator,
|
||||
pixel_width: result.pixel_size,
|
||||
pixel_height: result.pixel_size
|
||||
};
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
self.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
}, true); // use read-only transaction
|
||||
}
|
||||
|
||||
var tokens = {
|
||||
bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
|
||||
scale_denominator: '500000001',
|
||||
pixel_width: '156412',
|
||||
pixel_height: '156412'
|
||||
};
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
this.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
};
|
||||
|
||||
TurboCartoAdapter.prototype.process = function (psql, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(psql, sql);
|
||||
turboCarto(cartocss, datasource, callback);
|
||||
};
|
||||
|
||||
function shouldParseLayerCartocss(layer) {
|
||||
return layer && layer.options && layer.options.cartocss && layer.options.sql;
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var MapStoreMapConfigProvider = require('./map_store_provider');
|
||||
var MapStoreMapConfigProvider = require('./map-store-provider');
|
||||
|
||||
/**
|
||||
* @param {MapConfig} mapConfig
|
||||
@@ -4,22 +4,20 @@ var crypto = require('crypto');
|
||||
var dot = require('dot');
|
||||
var step = require('step');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
var templateName = require('../../backends/template_maps').templateName;
|
||||
var templateName = require('../../../backends/template_maps').templateName;
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @type {NamedMapMapConfigProvider}
|
||||
*/
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi,
|
||||
namedLayersAdapter, overviewsAdapter, turboCartoCssAdapter,
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter,
|
||||
owner, templateId, config, authToken, params) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.namedLayersAdapter = namedLayersAdapter;
|
||||
this.turboCartoCssAdapter = turboCartoCssAdapter;
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
|
||||
this.owner = owner;
|
||||
this.templateName = templateName(templateId);
|
||||
@@ -39,6 +37,7 @@ function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi,
|
||||
this.mapConfig = null;
|
||||
this.rendererParams = null;
|
||||
this.context = {};
|
||||
this.analysesResults = [];
|
||||
}
|
||||
|
||||
module.exports = NamedMapMapConfigProvider;
|
||||
@@ -51,17 +50,32 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
|
||||
var mapConfig = null;
|
||||
var datasource = null;
|
||||
var rendererParams;
|
||||
var apiKey;
|
||||
|
||||
var context = {};
|
||||
|
||||
step(
|
||||
function getTemplate() {
|
||||
self.getTemplate(this);
|
||||
},
|
||||
function prepareParams(err, tpl) {
|
||||
function prepareDbParams(err, tpl) {
|
||||
assert.ifError(err);
|
||||
self.template = tpl;
|
||||
|
||||
rendererParams = _.extend({}, self.params, {
|
||||
user: self.owner
|
||||
});
|
||||
self.setDBParams(self.owner, rendererParams, this);
|
||||
},
|
||||
function getUserApiKey(err) {
|
||||
assert.ifError(err);
|
||||
self.metadataBackend.getUserMapKey(self.owner, this);
|
||||
},
|
||||
function prepareParams(err, _apiKey) {
|
||||
assert.ifError(err);
|
||||
|
||||
self.template = tpl;
|
||||
apiKey = _apiKey;
|
||||
|
||||
var templateParams = {};
|
||||
if (self.config) {
|
||||
@@ -78,70 +92,33 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, _mapConfig) {
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.namedLayersAdapter.getLayers(self.owner, _mapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
return next(null, _mapConfig, datasource);
|
||||
context.analysisConfiguration = {
|
||||
user: self.owner,
|
||||
db: {
|
||||
host: rendererParams.dbhost,
|
||||
port: rendererParams.dbport,
|
||||
dbname: rendererParams.dbname,
|
||||
user: rendererParams.dbuser,
|
||||
pass: rendererParams.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: self.owner,
|
||||
apiKey: apiKey
|
||||
}
|
||||
);
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(self.owner, requestMapConfig, rendererParams, context, this);
|
||||
},
|
||||
function addOverviewsInformation(err, _mapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
self.overviewsAdapter.getLayers(self.owner, _mapConfig.layers, function(err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
|
||||
return next(null, _mapConfig, datasource);
|
||||
});
|
||||
},
|
||||
function parseTurboCartoCss(err, _mapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
self.turboCartoCssAdapter.getLayers(self.owner, _mapConfig.layers, function (err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
|
||||
return next(null, _mapConfig, datasource);
|
||||
});
|
||||
},
|
||||
function beforeLayergroupCreate(err, _mapConfig, _datasource) {
|
||||
function prepareContextLimits(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
datasource = _datasource;
|
||||
rendererParams = _.extend({}, self.params, {
|
||||
user: self.owner
|
||||
});
|
||||
self.setDBParams(self.owner, rendererParams, this);
|
||||
},
|
||||
function prepareContextLimits(err) {
|
||||
assert.ifError(err);
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, datasource);
|
||||
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, context.datasource);
|
||||
self.analysesResults = context.analysesResults || [];
|
||||
self.rendererParams = rendererParams;
|
||||
self.context.limits = renderLimits || {};
|
||||
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
|
||||
@@ -1,53 +0,0 @@
|
||||
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 OverviewsMetadataApi = require('./api/overviews_metadata_api');
|
||||
var FilterStatsApi = require('./api/filter_stats_api');
|
||||
var UserLimitsApi = require('./api/user_limits_api');
|
||||
var AuthApi = require('./api/auth_api');
|
||||
var LayergroupAffectedTablesCache = require('./cache/layergroup_affected_tables');
|
||||
@@ -27,13 +28,18 @@ var NamedMapProviderCache = require('./cache/named_map_provider_cache');
|
||||
var PgQueryRunner = require('./backends/pg_query_runner');
|
||||
var PgConnection = require('./backends/pg_connection');
|
||||
|
||||
var AnalysisBackend = require('./backends/analysis');
|
||||
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig_overviews_adapter');
|
||||
|
||||
var TurboCartocssParser = require('./utils/style/turbo-cartocss-parser');
|
||||
var TurboCartocssAdapter = require('./utils/style/turbo-cartocss-adapter');
|
||||
var SqlWrapMapConfigAdapter = require('./models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
|
||||
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
@@ -56,6 +62,7 @@ module.exports = function(serverOptions) {
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
var filterStatsApi = new FilterStatsApi(pgQueryRunner);
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
@@ -140,21 +147,26 @@ module.exports = function(serverOptions) {
|
||||
var tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
|
||||
var analysisBackend = new AnalysisBackend(serverOptions.analysis);
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var overviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi);
|
||||
|
||||
var turboCartoCssParser = new TurboCartocssParser(pgQueryRunner);
|
||||
var turboCartocssAdapter = new TurboCartocssAdapter(turboCartoCssParser);
|
||||
var mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsApi,
|
||||
overviewsAdapter,
|
||||
turboCartocssAdapter
|
||||
mapConfigAdapter
|
||||
);
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
@@ -166,6 +178,8 @@ module.exports = function(serverOptions) {
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
|
||||
|
||||
var versions = getAndValidateVersions(serverOptions);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
@@ -177,10 +191,10 @@ module.exports = function(serverOptions) {
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
new windshaft.backend.Widget(),
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache
|
||||
layergroupAffectedTablesCache,
|
||||
analysisBackend
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
@@ -192,8 +206,7 @@ module.exports = function(serverOptions) {
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache,
|
||||
overviewsAdapter,
|
||||
turboCartocssAdapter
|
||||
mapConfigAdapter
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
@@ -209,7 +222,7 @@ module.exports = function(serverOptions) {
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
|
||||
|
||||
new controller.ServerInfo().register(app);
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
@@ -222,12 +235,45 @@ function validateOptions(opts) {
|
||||
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
|
||||
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
|
||||
}
|
||||
}
|
||||
|
||||
// Be nice and warn if configured mapnik version is != instaled mapnik version
|
||||
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
|
||||
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
|
||||
function getAndValidateVersions(options) {
|
||||
// jshint undef:false
|
||||
var warn = console.warn.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var packageDefinition = require('../../package.json');
|
||||
|
||||
var declaredDependencies = packageDefinition.dependencies || {};
|
||||
var installedDependenciesVersions = {
|
||||
camshaft: require('camshaft').version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
'turbo-carto': require('turbo-carto').version,
|
||||
windshaft: windshaft.version,
|
||||
windshaft_cartodb: packageDefinition.version
|
||||
};
|
||||
|
||||
var dependenciesToValidate = ['camshaft', 'turbo-carto', 'windshaft'];
|
||||
dependenciesToValidate.forEach(function(depName) {
|
||||
var declaredDependencyVersion = declaredDependencies[depName];
|
||||
var installedDependencyVersion = installedDependenciesVersions[depName];
|
||||
if (declaredDependencyVersion !== installedDependencyVersion) {
|
||||
warn(
|
||||
'Dependency="%s" installed version="%s" does not match declared version="%s". Check your installation.',
|
||||
depName, installedDependencyVersion, declaredDependencyVersion
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Be nice and warn if configured mapnik version is != installed mapnik version
|
||||
if (mapnik.versions.mapnik !== options.grainstore.mapnik_version) {
|
||||
warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + options.grainstore.mapnik_version + ')');
|
||||
}
|
||||
|
||||
return installedDependenciesVersions;
|
||||
}
|
||||
|
||||
function bootstrapFonts(opts) {
|
||||
|
||||
@@ -3,7 +3,7 @@ var _ = require('underscore');
|
||||
var OverviewsQueryRewriter = require('./utils/overviews_query_rewriter');
|
||||
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
@@ -24,73 +24,89 @@ rendererConfig.mapnik.queryRewriter = overviewsQueryRewriter;
|
||||
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
if ( global.environment.statsd.prefix ) {
|
||||
if (global.environment.statsd) {
|
||||
if (global.environment.statsd.prefix) {
|
||||
var host_token = os.hostname().split('.').reverse().join('.');
|
||||
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
|
||||
}
|
||||
}
|
||||
|
||||
var analysisConfig = _.defaults(global.environment.analysis || {}, {
|
||||
batch: {
|
||||
inlineExecution: false,
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
bind: {
|
||||
port: global.environment.port,
|
||||
host: global.environment.host
|
||||
},
|
||||
// This is for inline maps and table maps
|
||||
base_url: global.environment.base_url_legacy || '/tiles/:table',
|
||||
// This is for inline maps and table maps
|
||||
base_url: global.environment.base_url_legacy || '/tiles/:table',
|
||||
|
||||
/// @deprecated with Windshaft-0.17.0
|
||||
///base_url_notable: '/tiles',
|
||||
/// @deprecated with Windshaft-0.17.0
|
||||
///base_url_notable: '/tiles',
|
||||
|
||||
// This is for Detached maps
|
||||
//
|
||||
// "maps" is the official, while
|
||||
// "tiles/layergroup" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
|
||||
// This is for Detached maps
|
||||
//
|
||||
// "maps" is the official, while
|
||||
// "tiles/layergroup" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
|
||||
|
||||
base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)',
|
||||
base_url_templated: global.environment.base_url_templated || '(?:/maps/named|/tiles/template)',
|
||||
|
||||
grainstore: {
|
||||
map: {
|
||||
grainstore: {
|
||||
map: {
|
||||
// TODO: allow to specify in configuration
|
||||
srid: 3857
|
||||
},
|
||||
datasource: global.environment.postgres,
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
mapnik_version: global.environment.mapnik_version,
|
||||
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
|
||||
default_layergroup_ttl: global.environment.mapConfigTTL || 7200
|
||||
},
|
||||
statsd: global.environment.statsd,
|
||||
renderCache: {
|
||||
ttl: rendererConfig.cache_ttl,
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mapnik: _.defaults(rendererConfig.mapnik, {
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
size: 16,
|
||||
idleTimeout: 3000,
|
||||
reapInterval: 1000
|
||||
},
|
||||
clipByBox2d: false,
|
||||
}
|
||||
}),
|
||||
torque: rendererConfig.torque,
|
||||
http: rendererConfig.http
|
||||
},
|
||||
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
|
||||
enable_cors: global.environment.enable_cors,
|
||||
varnish_host: global.environment.varnish.host,
|
||||
varnish_port: global.environment.varnish.port,
|
||||
varnish_http_port: global.environment.varnish.http_port,
|
||||
varnish_secret: global.environment.varnish.secret,
|
||||
varnish_purge_enabled: global.environment.varnish.purge_enabled,
|
||||
fastly: global.environment.fastly || {},
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format,
|
||||
useProfiler: global.environment.useProfiler
|
||||
datasource: global.environment.postgres,
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
mapnik_version: global.environment.mapnik_version,
|
||||
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
|
||||
default_layergroup_ttl: global.environment.mapConfigTTL || 7200
|
||||
},
|
||||
statsd: global.environment.statsd,
|
||||
renderCache: {
|
||||
ttl: rendererConfig.cache_ttl,
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mapnik: _.defaults(rendererConfig.mapnik, {
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
size: 16,
|
||||
idleTimeout: 3000,
|
||||
reapInterval: 1000
|
||||
},
|
||||
clipByBox2d: false
|
||||
}
|
||||
}),
|
||||
torque: rendererConfig.torque,
|
||||
http: rendererConfig.http
|
||||
},
|
||||
|
||||
analysis: {
|
||||
batch: {
|
||||
inlineExecution: analysisConfig.batch.inlineExecution,
|
||||
endpoint: analysisConfig.batch.endpoint,
|
||||
hostHeaderTemplate: analysisConfig.batch.hostHeaderTemplate
|
||||
}
|
||||
},
|
||||
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
|
||||
enable_cors: global.environment.enable_cors,
|
||||
varnish_host: global.environment.varnish.host,
|
||||
varnish_port: global.environment.varnish.port,
|
||||
varnish_http_port: global.environment.varnish.http_port,
|
||||
varnish_secret: global.environment.varnish.secret,
|
||||
varnish_purge_enabled: global.environment.varnish.purge_enabled,
|
||||
fastly: global.environment.fastly || {},
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format,
|
||||
useProfiler: global.environment.useProfiler
|
||||
};
|
||||
|
||||
32
lib/cartodb/stats/timer.js
Normal file
32
lib/cartodb/stats/timer.js
Normal file
@@ -0,0 +1,32 @@
|
||||
function Timer() {
|
||||
this.times = {};
|
||||
}
|
||||
|
||||
module.exports = Timer;
|
||||
|
||||
Timer.prototype.start = function(label) {
|
||||
this.timeIt(label, 'start');
|
||||
};
|
||||
|
||||
Timer.prototype.end = function(label) {
|
||||
this.timeIt(label, 'end');
|
||||
};
|
||||
|
||||
Timer.prototype.timeIt = function(label, what) {
|
||||
this.times[label] = this.times[label] || {};
|
||||
this.times[label][what] = Date.now();
|
||||
};
|
||||
|
||||
Timer.prototype.getTimes = function() {
|
||||
var self = this;
|
||||
var times = {};
|
||||
|
||||
Object.keys(this.times).forEach(function(label) {
|
||||
var stat = self.times[label];
|
||||
if (stat.start && stat.end) {
|
||||
times[label] = Math.max(0, stat.end - stat.start);
|
||||
}
|
||||
});
|
||||
|
||||
return times;
|
||||
};
|
||||
@@ -1,5 +1,26 @@
|
||||
var _ = require('underscore');
|
||||
var TableNameParser = require('./table_name_parser');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
var CamshaftFilter = require('../models/filter/camshaft');
|
||||
|
||||
// Minimim number of filtered rows to use overviews
|
||||
var FILTER_MIN_ROWS = 65536;
|
||||
// Maximum filtered fraction to not apply overviews
|
||||
var FILTER_MAX_FRACTION = 0.2;
|
||||
|
||||
function apply_filters_to_query(query, filters, bbox_filter) {
|
||||
if ( filters && !_.isEmpty(filters)) {
|
||||
var camshaftFilter = new CamshaftFilter(filters);
|
||||
query = camshaftFilter.sql(query);
|
||||
}
|
||||
if ( bbox_filter ) {
|
||||
var bboxFilter = new BBoxFilter(bbox_filter.options, bbox_filter.params);
|
||||
query = bboxFilter.sql(query);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
function OverviewsQueryRewriter(options) {
|
||||
|
||||
this.options = options;
|
||||
@@ -22,7 +43,7 @@ function overviews_view_for_table(table, overviews_metadata, indent) {
|
||||
|
||||
indent = indent || ' ';
|
||||
for (var z in overviews_metadata) {
|
||||
if (overviews_metadata.hasOwnProperty(z)) {
|
||||
if (overviews_metadata.hasOwnProperty(z) && z !== 'schema') {
|
||||
sorted_overviews.push([z, overviews_metadata[z].table]);
|
||||
}
|
||||
}
|
||||
@@ -39,11 +60,11 @@ function overviews_view_for_table(table, overviews_metadata, indent) {
|
||||
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;
|
||||
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");
|
||||
@@ -90,13 +111,13 @@ function replace_table_in_query(sql, old_table_name, replacement) {
|
||||
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_]|^)';
|
||||
// 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_]|^)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,18 +140,34 @@ function replace_table_in_query(sql, old_table_name, replacement) {
|
||||
return sql.replace(new RegExp(regexp, 'g'), replacement);
|
||||
}
|
||||
|
||||
function overviews_query(query, overviews, zoom_level_expression) {
|
||||
|
||||
function replace_table_in_query_with_schema(query, table, schema, replacement) {
|
||||
if ( replacement ) {
|
||||
query = replace_table_in_query(query, table, replacement);
|
||||
var parsed_table = TableNameParser.parse(table);
|
||||
if (!parsed_table.schema && schema) {
|
||||
// replace also the qualified table name, if the table wasn't qualified
|
||||
parsed_table.schema = schema;
|
||||
table = TableNameParser.table_identifier(parsed_table);
|
||||
query = replace_table_in_query(query, table, replacement);
|
||||
}
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
// Build query to use overviews for a variant zoom level (given by a expression to
|
||||
// be evaluated by the database server)
|
||||
function overviews_query_with_zoom_expression(query, overviews, zoom_level_expression) {
|
||||
var replaced_query = query;
|
||||
var sql = "WITH\n _vovw_scale AS ( SELECT " + zoom_level_expression + " AS _vovw_z )";
|
||||
var replacement;
|
||||
for ( var table in overviews ) {
|
||||
if (overviews.hasOwnProperty(table)) {
|
||||
var table_overviews = overviews[table];
|
||||
var table_view = overviews_view_name(table);
|
||||
replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view;
|
||||
replaced_query = replace_table_in_query(replaced_query, table, replacement);
|
||||
}
|
||||
}
|
||||
_.each(Object.keys(overviews), function(table) {
|
||||
var table_overviews = overviews[table];
|
||||
var table_view = overviews_view_name(table);
|
||||
var schema = table_overviews.schema;
|
||||
replacement = "(\n" + overviews_view_for_table(table, table_overviews) + "\n ) AS " + table_view;
|
||||
replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement);
|
||||
});
|
||||
if ( replaced_query !== query ) {
|
||||
sql += "\n";
|
||||
sql += replaced_query;
|
||||
@@ -140,46 +177,137 @@ function overviews_query(query, overviews, zoom_level_expression) {
|
||||
return sql;
|
||||
}
|
||||
|
||||
// Build query to use overviews for a specific zoom level value
|
||||
function overviews_query_with_definite_zoom(query, overviews, zoom_level) {
|
||||
var replaced_query = query;
|
||||
var replacement;
|
||||
_.each(Object.keys(overviews), function(table) {
|
||||
var table_overviews = overviews[table];
|
||||
var schema = table_overviews.schema;
|
||||
replacement = overview_table_for_zoom_level(table_overviews, zoom_level);
|
||||
replaced_query = replace_table_in_query_with_schema(replaced_query, table, schema, replacement);
|
||||
});
|
||||
return replaced_query;
|
||||
}
|
||||
|
||||
// Find a suitable overview table for a specific zoom_level
|
||||
function overview_table_for_zoom_level(table_overviews, zoom_level) {
|
||||
var overview_table;
|
||||
if ( table_overviews ) {
|
||||
overview_table = table_overviews[zoom_level];
|
||||
if ( !overview_table ) {
|
||||
_.every(Object.keys(table_overviews).sort(function(x,y){ return x-y; }), function(overview_zoom) {
|
||||
if ( +overview_zoom > +zoom_level ) {
|
||||
overview_table = table_overviews[overview_zoom];
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if ( overview_table ) {
|
||||
overview_table = overview_table.table;
|
||||
}
|
||||
return overview_table;
|
||||
}
|
||||
|
||||
// Transform an SQL query so that it uses overviews.
|
||||
// overviews contains metadata about the overviews to be used:
|
||||
// { 'table-name': {1: { table: 'overview-table-1' }, ... }, ... }
|
||||
//
|
||||
// For a given query `SELECT * FROM table`, if any of tables in it
|
||||
// has overviews as defined by the provided metadat, the query will
|
||||
// be transform into something similar to this:
|
||||
//
|
||||
// WITH _vovw_scale AS ( ... ), -- define scale level
|
||||
// WITH _vovw_table AS ( ... ), -- define union of overviews and base table
|
||||
// SELECT * FROM _vovw_table -- query with table replaced by _vovw_table
|
||||
// SELECT * FROM -- in the query the table is replaced by:
|
||||
// ( ... ) AS _vovw_table -- a union of overviews and base table
|
||||
//
|
||||
// This transformation can in principle be applied to arbitrary queries
|
||||
// (except for the case of queries that include the name of tables with
|
||||
// overviews inside text literals: at the current table name substitution
|
||||
// doesnn't prevent substitution inside literals).
|
||||
// But the transformation will currently only be applied to simple queries
|
||||
// of the form detected by the overviews_supported_query function.
|
||||
OverviewsQueryRewriter.prototype.query = function(query, data) {
|
||||
var overviews = this.overviews_metadata(data);
|
||||
if ( !overviews || !this.is_supported_query(query)) {
|
||||
// The data argument has the form:
|
||||
// {
|
||||
// overviews: // overview tables metadata
|
||||
// { 'table-name': {1: { table: 'overview-table-1' }, ... }, ... },
|
||||
// zoom_level: ..., // optional zoom level
|
||||
// filters: ..., // filters definition
|
||||
// unfiltered_query: ..., // query without the filters
|
||||
// bbox_filter: ... // bounding-box filter
|
||||
// }
|
||||
OverviewsQueryRewriter.prototype.query = function(query, data, options) {
|
||||
options = options || {};
|
||||
data = data || {};
|
||||
|
||||
var overviews = data.overviews;
|
||||
var unfiltered_query = data.unfiltered_query;
|
||||
var filters = data.filters;
|
||||
var bbox_filter = data.bbox_filter;
|
||||
|
||||
if ( !unfiltered_query ) {
|
||||
unfiltered_query = query;
|
||||
}
|
||||
|
||||
if ( !should_use_overviews(unfiltered_query, data) ) {
|
||||
return query;
|
||||
}
|
||||
var zoom_level_expression = this.options.zoom_level || '0';
|
||||
return overviews_query(query, overviews, zoom_level_expression);
|
||||
|
||||
var rewritten_query;
|
||||
|
||||
var zoom_level_expression = this.options.zoom_level;
|
||||
var zoom_level = zoom_level_for_query(unfiltered_query, zoom_level_expression, options);
|
||||
|
||||
rewritten_query = overviews_query(unfiltered_query, overviews, zoom_level, zoom_level_expression);
|
||||
|
||||
if ( rewritten_query === unfiltered_query ) {
|
||||
// could not or didn't need to alter the query
|
||||
rewritten_query = query;
|
||||
} else {
|
||||
rewritten_query = apply_filters_to_query(rewritten_query, filters, bbox_filter);
|
||||
}
|
||||
|
||||
return rewritten_query;
|
||||
};
|
||||
|
||||
OverviewsQueryRewriter.prototype.is_supported_query = function(sql) {
|
||||
var basic_query = /\s*SELECT\s+[\*a-z0-9_,\s]+?\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i;
|
||||
var unwrapped_query = new RegExp("^"+basic_query.source+"$", 'i');
|
||||
// queries for named maps are wrapped like this:
|
||||
var wrapped_query = new RegExp(
|
||||
"^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(" +
|
||||
basic_query.source +
|
||||
"\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$",
|
||||
'i'
|
||||
);
|
||||
return !!(sql.match(unwrapped_query) || sql.match(wrapped_query));
|
||||
};
|
||||
function zoom_level_for_query(query, zoom_level_expression, options) {
|
||||
var zoom_level = null;
|
||||
if ( _.has(options, 'zoom_level') ) {
|
||||
zoom_level = options.zoom_level || '0';
|
||||
}
|
||||
if ( zoom_level === null && !zoom_level_expression ) {
|
||||
zoom_level = '0';
|
||||
}
|
||||
return zoom_level;
|
||||
}
|
||||
|
||||
OverviewsQueryRewriter.prototype.overviews_metadata = function(data) {
|
||||
return data && data.overviews;
|
||||
};
|
||||
function overviews_query(query, overviews, zoom_level, zoom_level_expression) {
|
||||
if ( zoom_level || zoom_level === '0' || zoom_level === 0 ) {
|
||||
return overviews_query_with_definite_zoom(query, overviews, zoom_level);
|
||||
} else {
|
||||
return overviews_query_with_zoom_expression(query, overviews, zoom_level_expression);
|
||||
}
|
||||
}
|
||||
|
||||
function should_use_overviews(query, data) {
|
||||
data = data || {};
|
||||
var use_overviews = data.overviews && is_supported_query(query);
|
||||
if ( use_overviews && data.filters && data.filter_stats ) {
|
||||
var filtered_rows = data.filter_stats.filtered_rows;
|
||||
var unfiltered_rows = data.filter_stats.unfiltered_rows;
|
||||
if ( unfiltered_rows && (filtered_rows || filtered_rows === 0) ) {
|
||||
use_overviews = filtered_rows >= FILTER_MIN_ROWS ||
|
||||
(filtered_rows/unfiltered_rows) > FILTER_MAX_FRACTION;
|
||||
}
|
||||
}
|
||||
return use_overviews;
|
||||
}
|
||||
|
||||
function is_supported_query(sql) {
|
||||
var basic_query =
|
||||
/\s*SELECT\s+[\*a-z0-9_,\s]+?\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i;
|
||||
var unwrapped_query = new RegExp("^"+basic_query.source+"$", 'i');
|
||||
// queries for named maps are wrapped like this:
|
||||
var wrapped_query = new RegExp(
|
||||
"^\\s*SELECT\\s+\\*\\s+FROM\\s+\\(" +
|
||||
basic_query.source +
|
||||
"\\)\\s+AS\\s+wrapped_query\\s+WHERE\\s+\\d+=1\\s*$",
|
||||
'i'
|
||||
);
|
||||
return !!(sql.match(unwrapped_query) || sql.match(wrapped_query));
|
||||
}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function createTemplate(method) {
|
||||
return dot.template([
|
||||
'SELECT',
|
||||
method,
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
methodTemplates[methodName] = createTemplate(methods[methodName]);
|
||||
return methodTemplates;
|
||||
}, {});
|
||||
|
||||
function PostgresDatasource (pgQueryRunner, username, query) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
this.username = username;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
PostgresDatasource.prototype.getName = function () {
|
||||
return 'PostgresDatasource';
|
||||
};
|
||||
|
||||
PostgresDatasource.prototype.getRamp = function (column, buckets, method, callback) {
|
||||
var methodName = methods.hasOwnProperty(method) ? method : 'quantiles';
|
||||
var template = methodTemplates[methodName];
|
||||
|
||||
var query = template({ _column: column, _sql: this.query, _buckets: buckets });
|
||||
|
||||
this.pgQueryRunner.run(this.username, query, function (err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var ramp = result[0][methodName].sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
return callback(null, ramp);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = PostgresDatasource;
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var queue = require('queue-async');
|
||||
|
||||
function TurboCartocssAdapter(turboCartocssParser) {
|
||||
this.turboCartocssParser = turboCartocssParser;
|
||||
}
|
||||
|
||||
module.exports = TurboCartocssAdapter;
|
||||
|
||||
TurboCartocssAdapter.prototype.getLayers = function (username, layers, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, layers);
|
||||
}
|
||||
|
||||
var parseCartoCssQueue = queue(layers.length);
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
parseCartoCssQueue.defer(self._parseCartoCss.bind(self), username, layer);
|
||||
});
|
||||
|
||||
parseCartoCssQueue.awaitAll(function (err, layers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, layers);
|
||||
});
|
||||
};
|
||||
|
||||
TurboCartocssAdapter.prototype._parseCartoCss = function (username, layer, callback) {
|
||||
if (isNotLayerToParseCartocss(layer)) {
|
||||
return process.nextTick(function () {
|
||||
callback(null, layer);
|
||||
});
|
||||
}
|
||||
|
||||
this.turboCartocssParser.process(username, layer.options.cartocss, layer.options.sql, function (err, cartocss) {
|
||||
// Ignore turbo-cartocss errors and continue
|
||||
if (!err && cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
|
||||
callback(null, layer);
|
||||
});
|
||||
};
|
||||
|
||||
function isNotLayerToParseCartocss(layer) {
|
||||
if (!layer || !layer.options || !layer.options.cartocss || !layer.options.sql) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var turboCartoCss = require('turbo-cartocss');
|
||||
var PostgresDatasource = require('./postgres-datasource');
|
||||
|
||||
function TurboCartocssParser (pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TurboCartocssParser;
|
||||
|
||||
TurboCartocssParser.prototype.process = function (username, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(this.pgQueryRunner, username, sql);
|
||||
turboCartoCss(cartocss, datasource, callback);
|
||||
};
|
||||
29
lib/cartodb/utils/substitution-tokens.js
Normal file
29
lib/cartodb/utils/substitution-tokens.js
Normal file
@@ -0,0 +1,29 @@
|
||||
var SUBSTITUTION_TOKENS = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
var SubstitutionTokens = {
|
||||
tokens: function(sql) {
|
||||
return Object.keys(SUBSTITUTION_TOKENS).filter(function(tokenName) {
|
||||
return !!sql.match(SUBSTITUTION_TOKENS[tokenName]);
|
||||
});
|
||||
},
|
||||
|
||||
hasTokens: function(sql) {
|
||||
return this.tokens(sql).length > 0;
|
||||
},
|
||||
|
||||
replace: function(sql, replaceValues) {
|
||||
Object.keys(replaceValues).forEach(function(token) {
|
||||
if (SUBSTITUTION_TOKENS[token]) {
|
||||
sql = sql.replace(SUBSTITUTION_TOKENS[token], replaceValues[token]);
|
||||
}
|
||||
});
|
||||
return sql;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SubstitutionTokens;
|
||||
2567
npm-shrinkwrap.json
generated
2567
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
117
package.json
117
package.json
@@ -1,60 +1,61 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.31.2",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
|
||||
},
|
||||
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"express": "~4.13.3",
|
||||
"body-parser": "~1.14.0",
|
||||
"debug": "~2.2.0",
|
||||
"step-profiler": "~0.2.1",
|
||||
"node-statsd": "~0.0.7",
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "1.16.1",
|
||||
"step": "~0.0.6",
|
||||
"queue-async": "~1.0.7",
|
||||
"request": "~2.62.0",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"cartodb-psql": "~0.4.0",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"cartodb-query-tables": "~0.1.0",
|
||||
"turbo-cartocss": "0.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.3.6",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~2.11.0",
|
||||
"jshint": "~2.6.0",
|
||||
"redis": "~0.8.6",
|
||||
"strftime": "~0.8.2",
|
||||
"semver": "~1.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=2.14.16"
|
||||
}
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.59.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
|
||||
},
|
||||
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.30.0",
|
||||
"cartodb-psql": "~0.6.1",
|
||||
"cartodb-query-tables": "~0.1.0",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"debug": "~2.2.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.13.3",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~1.3.7",
|
||||
"node-statsd": "~0.0.7",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"request": "~2.62.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.12.1",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "2.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.4.3",
|
||||
"jshint": "~2.6.0",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.8.6",
|
||||
"semver": "~1.1.4",
|
||||
"strftime": "~0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=2.14.16"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ OPT_CREATE_PGSQL=yes # create the PostgreSQL test environment
|
||||
OPT_DROP_REDIS=yes # drop the redis test environment
|
||||
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
|
||||
OPT_COVERAGE=no # run tests with coverage
|
||||
OPT_DOWNLOAD_SQL=yes # download a fresh copy of sql files
|
||||
|
||||
export PGAPPNAME=cartodb_tiler_tester
|
||||
|
||||
@@ -73,6 +74,10 @@ while [ -n "$1" ]; do
|
||||
OPT_CREATE_REDIS=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--no-sql-download"; then
|
||||
OPT_DOWNLOAD_SQL=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--with-coverage"; then
|
||||
OPT_COVERAGE=yes
|
||||
shift
|
||||
@@ -113,6 +118,9 @@ fi
|
||||
if test x"$OPT_CREATE_REDIS" != xyes; then
|
||||
PREPARE_DB_OPTS="$PREPARE_DB_OPTS --skip-redis"
|
||||
fi
|
||||
if test x"$OPT_DOWNLOAD_SQL" != xyes; then
|
||||
PREPARE_DB_OPTS="$PREPARE_DB_OPTS --no-sql-download"
|
||||
fi
|
||||
|
||||
echo "Preparing the environment"
|
||||
cd ${BASEDIR}/test/support
|
||||
|
||||
148
test/acceptance/analysis/analysis-layers-dataviews.js
Normal file
148
test/acceptance/analysis/analysis-layers-dataviews.js
Normal file
@@ -0,0 +1,148 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
var dot = require('dot');
|
||||
|
||||
describe('analysis-layers-dataviews', function() {
|
||||
|
||||
var multitypeStyleTemplate = dot.template([
|
||||
"#points['mapnik::geometry_type'=1] {",
|
||||
" marker-fill-opacity: {{=it._opacity}};",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: {{=it._opacity}};",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: {{=it._color}};",
|
||||
" marker-allow-overlap: true;",
|
||||
"}",
|
||||
"#lines['mapnik::geometry_type'=2] {",
|
||||
" line-color: {{=it._color}};",
|
||||
" line-width: 2;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}",
|
||||
"#polygons['mapnik::geometry_type'=3] {",
|
||||
" polygon-fill: {{=it._color}};",
|
||||
" polygon-opacity: {{=it._opacity}};",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function cartocss(color, opacity) {
|
||||
return multitypeStyleTemplate({
|
||||
_color: color || '#F11810',
|
||||
_opacity: Number.isFinite(opacity) ? opacity : 1
|
||||
});
|
||||
}
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get histogram dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getDataview('pop_max_histogram', function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a filtered histogram dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 2008000);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip the filter when sending own_filter=0 for histogram dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
min: 2e6
|
||||
}
|
||||
}
|
||||
},
|
||||
own_filter: 0
|
||||
};
|
||||
|
||||
testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 0);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
81
test/acceptance/analysis/analysis-layers-geojson.js
Normal file
81
test/acceptance/analysis/analysis-layers-geojson.js
Normal file
@@ -0,0 +1,81 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers-dataviews-geojson', function() {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var CARTOCSS = [
|
||||
"#points {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 1.0;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get pop_max column from dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getTile(0, 0, 0, {format: 'geojson', layers: 0}, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(geojson.features));
|
||||
assert.ok(geojson.features.length > 0);
|
||||
var feature = geojson.features[0];
|
||||
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
672
test/acceptance/analysis/analysis-layers-use-cases.js
Normal file
672
test/acceptance/analysis/analysis-layers-use-cases.js
Normal file
@@ -0,0 +1,672 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
var dot = require('dot');
|
||||
var debug = require('debug')('windshaft:cartodb:test');
|
||||
|
||||
describe('analysis-layers use cases', function() {
|
||||
|
||||
|
||||
var multitypeStyleTemplate = dot.template([
|
||||
"#points['mapnik::geometry_type'=1] {",
|
||||
" marker-fill-opacity: {{=it._opacity}};",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: {{=it._opacity}};",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: {{=it._color}};",
|
||||
" marker-allow-overlap: true;",
|
||||
"}",
|
||||
"#lines['mapnik::geometry_type'=2] {",
|
||||
" line-color: {{=it._color}};",
|
||||
" line-width: 2;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}",
|
||||
"#polygons['mapnik::geometry_type'=3] {",
|
||||
" polygon-fill: {{=it._color}};",
|
||||
" polygon-opacity: {{=it._opacity}};",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function cartocss(color, opacity) {
|
||||
return multitypeStyleTemplate({
|
||||
_color: color || '#F11810',
|
||||
_opacity: Number.isFinite(opacity) ? opacity : 1
|
||||
});
|
||||
}
|
||||
|
||||
function mapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analysis: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
function analysisDef(analysis) {
|
||||
return JSON.stringify(analysis);
|
||||
}
|
||||
|
||||
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||
|
||||
var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 };
|
||||
|
||||
var useCases = [
|
||||
{
|
||||
desc: '1 mapnik layer',
|
||||
mapConfig: {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: '2 mapnik layers',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_banks",
|
||||
cartocss: cartocss('#2167AB'),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'rent listings + buffer over atm-machines',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('black', 0.5)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'rent listings + point-in-polygon from buffer atm-machines and rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('magenta', 0.5)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b1" },
|
||||
"cartocss": cartocss('green', 1.0),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b2" },
|
||||
"cartocss": cartocss('magenta', 0.5),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
id: "b2",
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "count-in-polygon",
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"columnName": 'count_airbnb',
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_rent_listings"
|
||||
},
|
||||
dataviews: {
|
||||
price_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'price'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b1",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
},
|
||||
dataviews: {
|
||||
bank_category: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'bank'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataviews: {
|
||||
count_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'count_airbnb'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'I. Distribution centers',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b0" },
|
||||
"cartocss": [
|
||||
"#distribution_centers {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: blue;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a0" },
|
||||
"cartocss": [
|
||||
"#shops {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#routing {",
|
||||
" line-color: ramp([routing_time], colorbrewer(Reds));",
|
||||
" line-width: ramp([routing_time], 2, 8);",
|
||||
" line-opacity: 1.0;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
distribution_center_name_category: {
|
||||
source: { id: 'b0' },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'name'
|
||||
}
|
||||
},
|
||||
time_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_time'
|
||||
}
|
||||
},
|
||||
distance_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_distance'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
{
|
||||
id: 'a1',
|
||||
type: 'routing-n-to-n',
|
||||
params: {
|
||||
// distanceColumn: 'routing_distance',
|
||||
// timeColumn: 'routing_time',
|
||||
originSource: {
|
||||
id: 'b0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from distribution_centers'
|
||||
}
|
||||
},
|
||||
destinationSource: {
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from shops'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'II. Population analysis',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a2" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([estimated_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a0" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
total_population_formula: {
|
||||
"source": { id: "a3" },
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'total_population',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
people_histogram: { // this injects a range filter at `a2` node output
|
||||
"source": { id: "a2" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'estimated_people'
|
||||
}
|
||||
},
|
||||
subway_line_category: { // this injects a category filter at `a0` node output
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'subway_line'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
{
|
||||
id: 'a3',
|
||||
// this will union the polygons, produce just one polygon, and calculate the total population for it
|
||||
type: 'total-population',
|
||||
params: {
|
||||
columnName: 'total_population',
|
||||
source: {
|
||||
id: 'a2',
|
||||
type: 'estimated-population',
|
||||
params: {
|
||||
columnName: 'estimated_people',
|
||||
source: {
|
||||
id: 'a1',
|
||||
type: 'trade-area',
|
||||
params: {
|
||||
source: {
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from subway_stops"
|
||||
}
|
||||
},
|
||||
kind: 'walk',
|
||||
time: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'III. Point in polygon',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([count_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
age_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'age'
|
||||
}
|
||||
},
|
||||
income_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'income'
|
||||
}
|
||||
},
|
||||
gender_category: {
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'gender'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "count-in-polygon",
|
||||
"params": {
|
||||
"columnName": 'count_people',
|
||||
"pointsSource": {
|
||||
"id": 'a0',
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select the_geom, age, gender, income from people"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from postal_codes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
useCases.forEach(function(useCase, imageIdx) {
|
||||
if (!!useCase.skip) {
|
||||
debug(JSON.stringify(useCase.mapConfig, null, 4));
|
||||
}
|
||||
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {
|
||||
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
|
||||
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
|
||||
assert.equal(image.width(), 256);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
370
test/acceptance/analysis/analysis-layers.js
Normal file
370
test/acceptance/analysis/analysis-layers.js
Normal file
@@ -0,0 +1,370 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
var dot = require('dot');
|
||||
|
||||
describe('analysis-layers', function() {
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var multitypeStyleTemplate = dot.template([
|
||||
"#points['mapnik::geometry_type'=1] {",
|
||||
" marker-fill-opacity: {{=it._opacity}};",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: {{=it._opacity}};",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: {{=it._color}};",
|
||||
" marker-allow-overlap: true;",
|
||||
"}",
|
||||
"#lines['mapnik::geometry_type'=2] {",
|
||||
" line-color: {{=it._color}};",
|
||||
" line-width: 2;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}",
|
||||
"#polygons['mapnik::geometry_type'=3] {",
|
||||
" polygon-fill: {{=it._color}};",
|
||||
" polygon-opacity: {{=it._opacity}};",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function cartocss(color, opacity) {
|
||||
return multitypeStyleTemplate({
|
||||
_color: color || '#F11810',
|
||||
_opacity: Number.isFinite(opacity) ? opacity : 1
|
||||
});
|
||||
}
|
||||
|
||||
function mapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||
|
||||
var TILE_ANALYSIS_TABLES = { z: 0, x: 0, y: 0 };
|
||||
|
||||
var useCases = [
|
||||
{
|
||||
desc: 'basic source-id mapnik layer',
|
||||
fixture: 'basic-source-id-mapnik-layer.png',
|
||||
mapConfig: mapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'buffer over source',
|
||||
fixture: 'buffer-over-source.png',
|
||||
tile: { z: 7, x: 61, y: 47 },
|
||||
mapConfig: mapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
useCases.forEach(function(useCase) {
|
||||
it('should implement use case: "' + useCase.desc + '"', function(done) {
|
||||
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
|
||||
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
// To generate images use:
|
||||
// image.save('/tmp/' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/' + useCase.fixture;
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should NOT fail for non-authenticated requests when it is just source', function(done) {
|
||||
var useCase = useCases[0];
|
||||
|
||||
// No API key here
|
||||
var testClient = new TestClient(useCase.mapConfig);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.metadata.layers.length, 1);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for non-authenticated requests that has a node other than "source"', function(done) {
|
||||
var useCase = useCases[1];
|
||||
|
||||
// No API key here
|
||||
var testClient = new TestClient(useCase.mapConfig);
|
||||
|
||||
var PERMISSION_DENIED_RESPONSE = {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
testClient.getLayergroup(PERMISSION_DENIED_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(
|
||||
layergroupResult.errors,
|
||||
["Analysis requires authentication with API key: permission denied."]
|
||||
);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should retrieve enough metadata about analyses', function(done) {
|
||||
var useCase = useCases[1];
|
||||
|
||||
// No API key here
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(
|
||||
Array.isArray(layergroupResult.metadata.analyses),
|
||||
'Missing "analyses" array metadata from: ' + JSON.stringify(layergroupResult)
|
||||
);
|
||||
var analyses = layergroupResult.metadata.analyses;
|
||||
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
|
||||
var nodes = analyses[0].nodes;
|
||||
var nodesIds = Object.keys(nodes);
|
||||
assert.deepEqual(nodesIds, ['HEAD', '2570e105-7b37-40d2-bdf4-1af889598745']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('query'), 'Missing "status" attribute in node');
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have analysis metadata when dataviews point to source node', function(done) {
|
||||
var useCase = {
|
||||
desc: 'basic source-id mapnik layer',
|
||||
fixture: 'basic-source-id-mapnik-layer.png',
|
||||
mapConfig: mapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from populated_places_simple_reduced",
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from populated_places_simple_reduced",
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
},
|
||||
scalerank_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'scalerank'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
};
|
||||
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(
|
||||
Array.isArray(layergroupResult.metadata.analyses),
|
||||
'Missing "analyses" array metadata from: ' + JSON.stringify(layergroupResult)
|
||||
);
|
||||
var analyses = layergroupResult.metadata.analyses;
|
||||
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
|
||||
var nodes = analyses[0].nodes;
|
||||
|
||||
var nodesIds = Object.keys(nodes);
|
||||
assert.deepEqual(nodesIds, ['2570e105-7b37-40d2-bdf4-1af889598745']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('query'), 'Missing "status" attribute in node');
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should response with custom cache headers for node status endpoints', function(done) {
|
||||
var useCase = useCases[1];
|
||||
|
||||
// No API key here
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
testClient.getNodeStatus('HEAD', function(err, response, nodeStatus) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(nodeStatus.status, 'ready');
|
||||
|
||||
var headers = response.headers;
|
||||
|
||||
assert.equal(headers['cache-control'], 'public,max-age=5');
|
||||
|
||||
var lastModified = new Date(headers['last-modified']);
|
||||
var tenSecondsInMs = 1e5;
|
||||
assert.ok(Date.now() - lastModified.getTime() < tenSecondsInMs);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should wrap queries from analyses', function(done) {
|
||||
var testClient = new TestClient(mapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"sql_wrap": "SELECT * FROM (<%= sql %>) __wrapped WHERE adm0cap = 1",
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
), 1234);
|
||||
|
||||
var tile = TILE_ANALYSIS_TABLES;
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/adm0cap-source-id-mapnik-layer.png';
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
268
test/acceptance/analysis/error-cases.js
Normal file
268
test/acceptance/analysis/error-cases.js
Normal file
@@ -0,0 +1,268 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers error cases', function() {
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var ERROR_RESPONSE = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
var AUTH_ERROR_RESPONSE = {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
it('should handle missing analysis nodes for layers', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "INVALID-SOURCE-ID"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(layergroupResult.errors[0], 'Missing analysis node.id="INVALID-SOURCE-ID" for layer=0');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing analyses when layers point to nonexistent one', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png",
|
||||
"subdomains": "abcd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "ID-FOR-NONEXISTENT-ANALYSIS"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Missing analysis node.id="ID-FOR-NONEXISTENT-ANALYSIS" for layer=1'
|
||||
);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing analyses when dataviews point to nonexistent one', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png",
|
||||
"subdomains": "abcd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from populated_places_simple_reduced",
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'ID-FOR-NONEXISTENT-ANALYSIS'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(layergroupResult.errors[0], 'Node with `source.id="ID-FOR-NONEXISTENT-ANALYSIS"`' +
|
||||
' not found in analyses for dataview "pop_max_histogram"');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('camshaft: should return error missing analysis nodes for layers with some context', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 11111);
|
||||
|
||||
testClient.getLayergroup(AUTH_ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Analysis requires authentication with API key: permission denied.'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(
|
||||
layergroupResult.errors_with_context[0].message,
|
||||
'Analysis requires authentication with API key: permission denied.'
|
||||
);
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('camshaft: should return error: Missing required param "radius"; with context', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Missing required param "radius"'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(layergroupResult.errors_with_context[0].message, 'Missing required param "radius"');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
274
test/acceptance/analysis/named-maps.js
Normal file
274
test/acceptance/analysis/named-maps.js
Normal file
@@ -0,0 +1,274 @@
|
||||
var assert = require('../../support/assert');
|
||||
|
||||
var helper = require('../../support/test_helper');
|
||||
|
||||
var CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
describe('named-maps analysis', function() {
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var username = 'localhost';
|
||||
var widgetsTemplateName = 'widgets-template';
|
||||
|
||||
var widgetsTemplate = {
|
||||
version: '0.0.1',
|
||||
name: widgetsTemplateName,
|
||||
layergroup: {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#buffer { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'HEAD'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(function createTemplate(done) {
|
||||
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) {
|
||||
assert.deepEqual(JSON.parse(res.body), { template_id: widgetsTemplateName });
|
||||
return done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function deleteTemplate(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + widgetsTemplateName + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res, err) {
|
||||
return done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('layergroup', function() {
|
||||
var layergroupid;
|
||||
var layergroup;
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function(done) {
|
||||
keysToDelete = {};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + widgetsTemplateName,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: username,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify({})
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ifError(err);
|
||||
|
||||
layergroup = JSON.parse(res.body);
|
||||
assert.ok(layergroup.hasOwnProperty('layergroupid'), "Missing 'layergroupid' from: " + res.body);
|
||||
layergroupid = layergroup.layergroupid;
|
||||
|
||||
assert.ok(
|
||||
Array.isArray(layergroup.metadata.analyses),
|
||||
'Missing "analyses" array metadata from: ' + res.body
|
||||
);
|
||||
var analyses = layergroup.metadata.analyses;
|
||||
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
|
||||
var nodes = analyses[0].nodes;
|
||||
var nodesIds = Object.keys(nodes);
|
||||
assert.deepEqual(nodesIds, ['HEAD', '2570e105-7b37-40d2-bdf4-1af889598745']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
|
||||
assert.ok(!node.hasOwnProperty('query'), 'Unexpected "query" attribute in node');
|
||||
});
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroup.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return done();
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it('should be able to retrieve images from analysis', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupid + '/6/31/24.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to retrieve dataviews from analysis', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupid + '/dataview/pop_max_histogram',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var dataview = JSON.parse(res.body);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 0);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to retrieve static map preview via layergroup', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/static/center/' + layergroupid + '/4/42/-3/320/240.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer-layergroup-static-preview.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('auto-instantiation', function() {
|
||||
it('should be able to retrieve static map preview via fixed url', function(done) {
|
||||
TestClient.getStaticMap(widgetsTemplateName, function(err, image) {
|
||||
assert.ok(!err, err);
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer-static-preview.png';
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
115
test/acceptance/analysis/regressions.js
Normal file
115
test/acceptance/analysis/regressions.js
Normal file
@@ -0,0 +1,115 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers regressions', function() {
|
||||
it('should return a complete list of nodes from analysis', function(done) {
|
||||
var mapConfig = {
|
||||
"version": "1.5.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss": TestClient.CARTOCSS.POINTS,
|
||||
"cartocss_version": "2.1.1",
|
||||
"interactivity": [],
|
||||
"source": {
|
||||
"id": "a4"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss": TestClient.CARTOCSS.POINTS,
|
||||
"cartocss_version": "2.1.0",
|
||||
"interactivity": [],
|
||||
"source": {
|
||||
"id": "b1"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"74493a30-4679-4b72-a60c-b6f808b57c98": {
|
||||
"type": "histogram",
|
||||
"source": {
|
||||
"id": "b0"
|
||||
},
|
||||
"options": {
|
||||
"column": "customer_value",
|
||||
"bins": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a4",
|
||||
"type": "intersection",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "a3",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "a2",
|
||||
"type": "centroid",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b1",
|
||||
"type": "kmeans",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "SELECT * FROM populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"clusters": 5
|
||||
}
|
||||
},
|
||||
"category_column": "cluster_no"
|
||||
}
|
||||
},
|
||||
"radius": 200000
|
||||
}
|
||||
},
|
||||
"target": {
|
||||
"id": "customer_home_locations",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "SELECT * FROM populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroupResult);
|
||||
assert.ok(layergroupResult.metadata);
|
||||
var analyses = layergroupResult.metadata.analyses;
|
||||
assert.ok(analyses);
|
||||
assert.equal(analyses.length, 1);
|
||||
|
||||
var expectedIds = ['customer_home_locations', 'b0', 'b1', 'a2', 'a3', 'a4'];
|
||||
expectedIds.forEach(function(expectedId) {
|
||||
assert.ok(
|
||||
analyses[0].nodes.hasOwnProperty(expectedId),
|
||||
'Missing "' + expectedId + '" from node list.'
|
||||
);
|
||||
});
|
||||
assert.equal(Object.keys(analyses[0].nodes).length, expectedIds.length, Object.keys(analyses[0].nodes));
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
59
test/acceptance/dataviews/aggregation.js
Normal file
59
test/acceptance/dataviews/aggregation.js
Normal file
@@ -0,0 +1,59 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('aggregations', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function aggregationOperationMapConfig(operation) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: operation,
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
var operations = ['count', 'sum', 'avg', 'max', 'min'];
|
||||
|
||||
operations.forEach(function(operation) {
|
||||
it('should be able to use "' + operation + '" as aggregation operation', function(done) {
|
||||
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation));
|
||||
this.testClient.getDataview('adm0name', { own_filter: 0 }, function (err, aggregation) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(aggregation);
|
||||
|
||||
assert.equal(aggregation.type, 'aggregation');
|
||||
assert.equal(aggregation.aggregation, operation);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
87
test/acceptance/dataviews/error-cases.js
Normal file
87
test/acceptance/dataviews/error-cases.js
Normal file
@@ -0,0 +1,87 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('histogram-dataview', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var ERROR_RESPONSE = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
function createMapConfig(dataviews) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: dataviews,
|
||||
analyses: [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select null::geometry the_geom_webmercator, x from generate_series(0,1000) x"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
it('should fail when invalid dataviews object is provided, string case', function(done) {
|
||||
var mapConfig = createMapConfig("wadus-string");
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "string" type found' ]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when invalid dataviews object is provided, array case', function(done) {
|
||||
var mapConfig = createMapConfig([]);
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "array" type found' ]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with empty but valid objects', function(done) {
|
||||
var mapConfig = createMapConfig({});
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup);
|
||||
assert.ok(layergroup.layergroupid);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
80
test/acceptance/dataviews/histogram.js
Normal file
80
test/acceptance/dataviews/histogram.js
Normal file
@@ -0,0 +1,80 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('histogram-dataview', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'x'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select null::geometry the_geom_webmercator, x from generate_series(0,1000) x"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get bin_width right when max > min in filter', function(done) {
|
||||
var params = {
|
||||
bins: 10,
|
||||
start: 1e3,
|
||||
end: 0
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('pop_max_histogram', params, function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.ok(dataview.bin_width > 0, 'Unexpected bin width: ' + dataview.bin_width);
|
||||
dataview.bins.forEach(function(bin) {
|
||||
assert.ok(bin.min <= bin.max, 'bin min < bin max: ' + JSON.stringify(bin));
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
358
test/acceptance/dataviews/overviews.js
Normal file
358
test/acceptance/dataviews/overviews.js
Normal file
@@ -0,0 +1,358 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('dataviews using tables without overviews', function() {
|
||||
|
||||
var nonOverviewsMapConfig = {
|
||||
version: '1.5.0',
|
||||
analyses: [
|
||||
{ id: 'data-source',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from populated_places_simple_reduced'
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
country_places_count: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'adm0_a3',
|
||||
operation: 'count'
|
||||
}
|
||||
},
|
||||
country_categories: {
|
||||
type: 'aggregation',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'adm0_a3',
|
||||
aggregation: 'count'
|
||||
}
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
source: { id: 'data-source' }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it("should expose a formula", function(done) {
|
||||
var testClient = new TestClient(nonOverviewsMapConfig);
|
||||
testClient.getDataview('country_places_count', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, { operation: 'count', result: 7313, nulls: 0, type: 'formula' });
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should admit a bbox", function(done) {
|
||||
var params = {
|
||||
bbox: "-170,-80,170,80"
|
||||
};
|
||||
var testClient = new TestClient(nonOverviewsMapConfig);
|
||||
testClient.getDataview('country_places_count', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, { operation: 'count', result: 7253, nulls: 0, type: 'formula' });
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
|
||||
describe('category', function () {
|
||||
|
||||
it("should expose a filtered formula", function (done) {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {country_categories: {accept: ['CAN']}}
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(nonOverviewsMapConfig);
|
||||
testClient.getDataview('country_places_count', params, function (err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, { operation: 'count', result: 256, nulls: 0, type: 'formula' });
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered formula and admit a bbox", function (done) {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {country_categories: {accept: ['CAN']}}
|
||||
},
|
||||
bbox: "-170,-80,170,80"
|
||||
};
|
||||
var testClient = new TestClient(nonOverviewsMapConfig);
|
||||
testClient.getDataview('country_places_count', params, function (err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, { operation: 'count', result: 254, nulls: 0, type: 'formula' });
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('dataviews using tables with overviews', function() {
|
||||
|
||||
var overviewsMapConfig = {
|
||||
version: '1.5.0',
|
||||
analyses: [
|
||||
{ id: 'data-source',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_table_overviews'
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
test_sum: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
test_categories: {
|
||||
type: 'aggregation',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'count',
|
||||
aggregationColumn: 'name',
|
||||
}
|
||||
},
|
||||
test_avg: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'avg'
|
||||
}
|
||||
},
|
||||
test_count: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'count'
|
||||
}
|
||||
},
|
||||
test_min: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'min'
|
||||
}
|
||||
},
|
||||
test_max: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'max'
|
||||
}
|
||||
}
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_table_overviews',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
source: { id: 'data-source' }
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
it("should expose a sum formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_sum', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose an avg formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_avg', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":3,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a count formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_count', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":5,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a max formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_max', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":5,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a min formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_min', { own_filter: 0 }, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should admit a bbox", function(done) {
|
||||
var params = {
|
||||
bbox: "-170,-80,170,80"
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_sum', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
|
||||
describe('category', function () {
|
||||
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: {test_categories: {accept: ['Hawai']}}
|
||||
}
|
||||
};
|
||||
|
||||
it("should expose a filtered sum formula", function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_sum', params, function (err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered avg formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_avg', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered count formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_count', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filterd max formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_max', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filterd min formula", function(done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_min', params, function(err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered sum formula with bbox", function (done) {
|
||||
var bboxparams = {
|
||||
filters: {
|
||||
dataviews: {test_categories: {accept: ['Hawai']}}
|
||||
},
|
||||
bbox: "-170,-80,170,80"
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_sum', bboxparams, function (err, formula_result) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
169
test/acceptance/dynamic-styling-named-maps.js
Normal file
169
test/acceptance/dynamic-styling-named-maps.js
Normal file
@@ -0,0 +1,169 @@
|
||||
var assert = require('../support/assert');
|
||||
var step = require('step');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var testHelper = require(__dirname + '/../support/test_helper');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
describe('dynamic styling for named maps', function() {
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var templateId = 'dynamic-styling-template-1';
|
||||
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateId,
|
||||
auth: { method: 'open' },
|
||||
placeholders: {
|
||||
color: {
|
||||
type: "css_color",
|
||||
default: "Reds"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [{
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: [
|
||||
'#layer {',
|
||||
' marker-fill: #000;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}, {
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: [
|
||||
'#layer {',
|
||||
' marker-fill: #000;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}, {
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: [
|
||||
'#layer {',
|
||||
' marker-fill: #000;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}]
|
||||
}
|
||||
};
|
||||
|
||||
var templateParams = {
|
||||
styles: {
|
||||
0: [
|
||||
'#layer {',
|
||||
' marker-fill: #fabada;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
2: [
|
||||
'#layer {',
|
||||
' marker-fill: #cebada;',
|
||||
'}'
|
||||
].join('\n')
|
||||
}
|
||||
};
|
||||
|
||||
it('should instantiate a template applying cartocss dynamicly', function (done) {
|
||||
step(
|
||||
function postTemplate() {
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: { host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template)
|
||||
}, {},
|
||||
function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(JSON.parse(res.body), {
|
||||
template_id: templateId
|
||||
});
|
||||
|
||||
return null;
|
||||
},
|
||||
function instantiateTemplate(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(templateParams)
|
||||
}, {},
|
||||
function (res, err) {
|
||||
return next(err, res);
|
||||
});
|
||||
},
|
||||
function checkInstanciation(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
assert.equal(parsedBody.metadata.layers[0].meta.cartocss, templateParams.styles['0']);
|
||||
assert.equal(
|
||||
parsedBody.metadata.layers[1].meta.cartocss,
|
||||
template.layergroup.layers[1].options.cartocss
|
||||
);
|
||||
assert.equal(parsedBody.metadata.layers[2].meta.cartocss, templateParams.styles['2']);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: { host: 'localhost' }
|
||||
}, {}, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkDeleteTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 204);
|
||||
assert.ok(!res.body);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
343
test/acceptance/geojson-renderer.js
Normal file
343
test/acceptance/geojson-renderer.js
Normal file
@@ -0,0 +1,343 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
|
||||
describe('use only needed columns', function() {
|
||||
|
||||
function getFeatureByCartodbId(features, cartodbId) {
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
if (features[i].properties.cartodb_id === cartodbId) {
|
||||
return features[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
var options = { format: 'geojson', layer: 0 };
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('with aggregation widget, interactivity and cartocss columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_min"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373,
|
||||
pop_min: 57586
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not duplicate columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: ['#layer0 {',
|
||||
'marker-fill: red;',
|
||||
'marker-width: 10;',
|
||||
'[name="Madrid"] { marker-fill: green; } ',
|
||||
'[pop_max>100000] { marker-fill: black; } ',
|
||||
'}'].join('\n'),
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_max"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with formula widget, no interactivity and no cartocss columns', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id',
|
||||
widgets: {
|
||||
pop_max_f: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('with cartocss with multiple expressions', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
|
||||
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max:71373,
|
||||
name:"Mardin",
|
||||
adm0name:"Turkey"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with mapnik substitution tokens', function(done) {
|
||||
var cartocss = [
|
||||
"#layer {",
|
||||
" line-width: 2;",
|
||||
" line-color: #3B3B58;",
|
||||
" line-opacity: 1;",
|
||||
" polygon-opacity: 0.7;",
|
||||
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var sql = [
|
||||
'WITH hgrid AS (',
|
||||
' SELECT CDB_HexagonGrid(',
|
||||
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
|
||||
' greatest(!pixel_width!,!pixel_height!) * 100',
|
||||
' ) as cell',
|
||||
')',
|
||||
'SELECT',
|
||||
' hgrid.cell as the_geom_webmercator,',
|
||||
' count(1) as points_count,',
|
||||
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
|
||||
' 1 as cartodb_id',
|
||||
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
|
||||
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
|
||||
'GROUP BY hgrid.cell'
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": sql,
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip empty and null columns for geojson tiles', function(done) {
|
||||
|
||||
var mapConfig = {
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"query": "SELECT * FROM test_table"
|
||||
},
|
||||
"type": "source"
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
|
||||
"options": {
|
||||
"aggregation": "sum",
|
||||
"aggregationColumn": "cartodb_id",
|
||||
"column": "name"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "aggregation"
|
||||
},
|
||||
"98a75757-3006-400a-b028-fb613a6c0b69": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "sum"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"bins": "12",
|
||||
"column": "cartodb_id"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "histogram"
|
||||
},
|
||||
"ebc0653f-3581-469c-8b31-c969e440a865": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"layer_name": "wadus",
|
||||
"source": {
|
||||
"id": "a0"
|
||||
}
|
||||
},
|
||||
"type": "cartodb"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
assert.deepEqual(Object.keys(geojson.features[0].properties), ['cartodb_id', 'name']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -7,7 +7,7 @@ var redis = require('redis');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
@@ -106,7 +106,7 @@ describe('render limits', function() {
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Render timed out' ] });
|
||||
assert.deepEqual(parsed.errors, [ 'Render timed out' ]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
@@ -171,7 +171,7 @@ describe('render limits', function() {
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: ['Render timed out'] });
|
||||
assert.deepEqual(parsed.errors, ['Render timed out']);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ var strftime = require('strftime');
|
||||
var redis_stats_db = 5;
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ var assert = require('../support/assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
@@ -228,7 +228,7 @@ describe('tests from old api translated to multilayer', function() {
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Unexpected token W' ] });
|
||||
assert.deepEqual(parsed.errors, [ 'Unexpected token W' ]);
|
||||
|
||||
done();
|
||||
}
|
||||
@@ -334,9 +334,7 @@ describe('tests from old api translated to multilayer', function() {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["fake error message"]
|
||||
});
|
||||
assert.deepEqual(parsed.errors, ["fake error message"]);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
@@ -181,7 +181,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] });
|
||||
assert.deepEqual(parsedBody.errors, ["Template 'nonexistent' of user 'localhost' not found"]);
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -234,10 +234,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(
|
||||
parsedBody,
|
||||
{ errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] }
|
||||
);
|
||||
assert.deepEqual(parsedBody.errors, [ "Unauthorized 'auth_valid_template' template instantiation" ]);
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -347,7 +344,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] });
|
||||
assert.deepEqual(parsedBody.errors, ['Nested named layers are not allowed' ]);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -169,8 +169,8 @@ describe('named maps authentication', function() {
|
||||
getNamedTile(nonexistentName, 0, 0, 0, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template '" + nonexistentName + "' of user '" + username + "' not found"]
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -179,7 +179,7 @@ describe('named maps authentication', function() {
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ['Unauthorized template instantiation']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -238,8 +238,8 @@ describe('named maps authentication', function() {
|
||||
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template '" + nonexistentName + "' of user '" + username + "' not found"]
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -248,7 +248,7 @@ describe('named maps authentication', function() {
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ['Unauthorized template instantiation']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,8 +124,8 @@ describe('named maps provider cache', function() {
|
||||
getNamedTile({ statusCode: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template 'template_with_color' of user 'localhost' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template 'template_with_color' of user 'localhost' not found"]
|
||||
);
|
||||
|
||||
// add template again so it's clean in afterEach
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
var qs = require('querystring');
|
||||
var testHelper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
@@ -53,10 +54,18 @@ describe('named maps static view', function() {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
|
||||
function getStaticMap(callback) {
|
||||
function getStaticMap(params, callback) {
|
||||
if (!callback) {
|
||||
callback = params;
|
||||
params = null;
|
||||
}
|
||||
|
||||
var url = '/api/v1/map/static/named/' + templateName + '/640/480.png';
|
||||
|
||||
if (params !== null) {
|
||||
url += '?' + qs.stringify(params);
|
||||
}
|
||||
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
@@ -163,4 +172,30 @@ describe('named maps static view', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return override zoom', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
},
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ zoom: 3 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
img.save('/tmp/static.png');
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
@@ -48,63 +48,179 @@ describe('overviews metadata', function() {
|
||||
|
||||
it("layers with and without overviews", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [overviews_layer, non_overviews_layer]
|
||||
};
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [overviews_layer, non_overviews_layer]
|
||||
};
|
||||
|
||||
var layergroup_url = '/api/v1/map';
|
||||
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 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);
|
||||
});
|
||||
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: {
|
||||
schema: 'public',
|
||||
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);
|
||||
}
|
||||
);
|
||||
next(err);
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('overviews metadata with filters', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
test_helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it("layers with overviews", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table_overviews',
|
||||
source: { id: 'with_overviews' },
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
test_names: {
|
||||
type: 'aggregation',
|
||||
source: {id: 'with_overviews'},
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'count'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{ id: 'with_overviews',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_table_overviews'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var filters = {
|
||||
dataviews: {
|
||||
test_names: { accept: ['Hawai'] }
|
||||
}
|
||||
};
|
||||
|
||||
var layergroup_url = '/api/v1/map';
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?filters=' + JSON.stringify(filters),
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
expected_token = parsedBody.layergroupid;
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_mapconfig(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: 500000
|
||||
});
|
||||
mapStore.load(LayergroupToken.parse(expected_token).token, function(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
assert.equal(mapConfig._cfg.layers[0].type, 'cartodb');
|
||||
assert.ok(mapConfig._cfg.layers[0].options.query_rewrite_data);
|
||||
var expected_data = {
|
||||
overviews: {
|
||||
test_table_overviews: {
|
||||
schema: 'public',
|
||||
1: { table: '_vovw_1_test_table_overviews' },
|
||||
2: { table: '_vovw_2_test_table_overviews' }
|
||||
}
|
||||
},
|
||||
filters: { test_names: { type: 'category', column: 'name', params: { accept: [ 'Hawai' ] } } },
|
||||
unfiltered_query: 'select * from test_table_overviews',
|
||||
filter_stats: { unfiltered_rows: 5, filtered_rows: 1 }
|
||||
};
|
||||
assert.deepEqual(mapConfig._cfg.layers[0].options.query_rewrite_data, expected_data);
|
||||
|
||||
});
|
||||
|
||||
next(err);
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(expected_token).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ 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 LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
@@ -59,116 +59,117 @@ describe('overviews metadata for named maps', function() {
|
||||
};
|
||||
|
||||
it("should add overviews data to layers", function(done) {
|
||||
step(
|
||||
function postTemplate()
|
||||
{
|
||||
var next = this;
|
||||
step(
|
||||
function postTemplate()
|
||||
{
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template)
|
||||
}, {}, function(res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(template)
|
||||
}, {}, function(res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(JSON.parse(res.body), {
|
||||
template_id: templateId
|
||||
});
|
||||
next(null);
|
||||
},
|
||||
function instantiateTemplate(err) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.deepEqual(JSON.parse(res.body), {
|
||||
template_id: templateId
|
||||
});
|
||||
next(null);
|
||||
},
|
||||
function instantiateTemplate(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, {},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
});
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, {},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
});
|
||||
|
||||
},
|
||||
function checkInstanciation(err, res) {
|
||||
assert.ifError(err);
|
||||
},
|
||||
function checkInstanciation(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
var next = this;
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.statusCode, 200);
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
next(null, parsedBody.layergroupid);
|
||||
},
|
||||
next(null, parsedBody.layergroupid);
|
||||
},
|
||||
|
||||
function checkMapconfig(err, layergroupId)
|
||||
{
|
||||
assert.ifError(err);
|
||||
function checkMapconfig(err, layergroupId)
|
||||
{
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
var next = this;
|
||||
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: 500000
|
||||
});
|
||||
mapStore.load(LayergroupToken.parse(layergroupId).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);
|
||||
});
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: 500000
|
||||
});
|
||||
mapStore.load(LayergroupToken.parse(layergroupId).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: {
|
||||
schema: 'public',
|
||||
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 deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
next(err);
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: { host: 'localhost' }
|
||||
}, {}, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkDeleteTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 204);
|
||||
assert.ok(!res.body);
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + templateId + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: { host: 'localhost' }
|
||||
}, {}, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkDeleteTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 204);
|
||||
assert.ok(!res.body);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
|
||||
describe('attributes', function() {
|
||||
@@ -231,7 +231,11 @@ describe('attributes', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
assert.equal(
|
||||
res.body,
|
||||
'/**/ typeof test === \'function\' && test({"errors":["Layer 0 has no exposed attributes"]});'
|
||||
'/**/ typeof test === \'function\' && ' +
|
||||
'test({"errors":["Layer 0 has no exposed attributes"],' +
|
||||
'"errors_with_context":[{' +
|
||||
'"type":"unknown","message":"Layer 0 has no exposed attributes"' +
|
||||
'}]});'
|
||||
);
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -138,11 +138,9 @@ describe('blend http fallback', function() {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, expectedResponse, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {
|
||||
errors: [
|
||||
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
|
||||
]
|
||||
});
|
||||
assert.deepEqual(parsedBody.errors, [
|
||||
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,12 +119,11 @@ describe('external resources', function() {
|
||||
var mapConfig = testClient.defaultTableMapConfig('test_table_3', style);
|
||||
|
||||
testClient.createLayergroup(mapConfig, { statusCode: 400 }, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), {
|
||||
errors: ["Unable to download '" + url + "' for 'style0' (server returned 404)"]
|
||||
});
|
||||
assert.deepEqual(JSON.parse(res.body).errors, [
|
||||
"Unable to download '" + url + "' for 'style0' (server returned 404)"]
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ var step = require('step');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('multilayer', function() {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('multilayer error cases', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {"errors":["layergroup POST data must be of type application/json"]});
|
||||
assert.deepEqual(parsedBody.errors, ["layergroup POST data must be of type application/json"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -44,7 +44,7 @@ describe('multilayer error cases', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {"errors":["Missing layers array from layergroup config"]});
|
||||
assert.deepEqual(parsedBody.errors, ["Missing layers array from layergroup config"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -58,7 +58,10 @@ describe('multilayer error cases', function() {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(
|
||||
res.body,
|
||||
'/**/ typeof test === \'function\' && test({"errors":["Missing layers array from layergroup config"]});'
|
||||
'/**/ typeof test === \'function\' && ' +
|
||||
'test({"errors":["Missing layers array from layergroup config"],' +
|
||||
'"errors_with_context":[{"type":"unknown",' +
|
||||
'"message":"Missing layers array from layergroup config"}]});'
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -83,7 +86,7 @@ describe('multilayer error cases', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {errors:["Missing cartocss_version for layer 0 options"]});
|
||||
assert.deepEqual(parsedBody.errors, ["Missing cartocss_version for layer 0 options"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -355,7 +358,7 @@ describe('multilayer error cases', function() {
|
||||
var mapConfig = testClient.singleLayerMapConfig('select * from test_table', null, null, 'name');
|
||||
|
||||
testClient.getGrid(mapConfig, 1, 13, 4011, 3088, defaultErrorExpectedResponse, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ["Layer '1' not found in layergroup"] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ["Layer '1' not found in layergroup"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -383,7 +386,7 @@ describe('multilayer error cases', function() {
|
||||
// FIXME: should be 404
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ':' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {"errors": ["Invalid or nonexistent map configuration token 'deadbeef'"]});
|
||||
assert.deepEqual(parsed.errors, ["Invalid or nonexistent map configuration token 'deadbeef'"]);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
|
||||
@@ -5,7 +5,7 @@ var _ = require('underscore');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
describe('raster', function() {
|
||||
|
||||
@@ -148,11 +148,10 @@ describe('raster', function() {
|
||||
assert.ok(!err);
|
||||
checkCORSHeaders(res);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Mapnik raster layers do not support interactivity' ] });
|
||||
assert.deepEqual(parsedBody.errors, [ 'Mapnik raster layers do not support interactivity' ]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('regressions', function() {
|
||||
contentType: 'application/json; charset=utf-8'
|
||||
};
|
||||
requestTile('/0/0/0.png?testUnexpectedError=1', options, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), { "errors": ["test unexpected error"] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ["test unexpected error"]);
|
||||
finish(done);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
describe('retina support', function() {
|
||||
|
||||
@@ -129,7 +129,7 @@ describe('retina support', function() {
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err, 'Failed to request 0/0/0' + scaleFactor + '.png tile');
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ["Tile with specified resolution not found"] } );
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ["Tile with specified resolution not found"]);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ describe('server', function() {
|
||||
}
|
||||
};
|
||||
testClient.getGrid(mapConfig, 0, 13, 4011, 3088, expectedResponse, function(err, res) {
|
||||
assert.deepEqual(JSON.parse(res.body), {"errors":["Tileset has no interactivity"]});
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ["Tileset has no interactivity"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 85;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user