Compare commits
647 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d45da0de | ||
|
|
289ffbbedc | ||
|
|
e0f0751b28 | ||
|
|
2a06405a58 | ||
|
|
9206b1a1b5 | ||
|
|
5989ab344d | ||
|
|
a1e024e228 | ||
|
|
8628d3b671 | ||
|
|
7ce4104d2f | ||
|
|
a26a5d6f5a | ||
|
|
e98a5aeff0 | ||
|
|
4c375780c7 | ||
|
|
48415fb1f3 | ||
|
|
8da7cf73c1 | ||
|
|
ba30f460ee | ||
|
|
e1aa0bc7ae | ||
|
|
3987e83b7a | ||
|
|
858d976637 | ||
|
|
48d2978997 | ||
|
|
1872fbd021 | ||
|
|
bbb1b4a7b9 | ||
|
|
aa0ddaae95 | ||
|
|
cb3706e5cf | ||
|
|
3d8f6576aa | ||
|
|
24f7bc6596 | ||
|
|
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 | ||
|
|
e3a68a6b4d | ||
|
|
01218c6ea1 | ||
|
|
83d27a8e29 | ||
|
|
fa2e884605 | ||
|
|
f4554f41d2 | ||
|
|
b97a67b844 | ||
|
|
3b13fad5e7 | ||
|
|
4d39d30f6e | ||
|
|
9a8964bd39 | ||
|
|
aae251b178 | ||
|
|
2db7a3d110 | ||
|
|
5fe96a618d | ||
|
|
499178319d | ||
|
|
7b7e9ffe59 | ||
|
|
43d1e5c613 | ||
|
|
8547cb836f | ||
|
|
2bd3e46a4d | ||
|
|
e44b5eaccd | ||
|
|
26512f6485 | ||
|
|
90b92f0180 | ||
|
|
ebe25761d2 | ||
|
|
f928147559 | ||
|
|
d5c5c7bdbb | ||
|
|
ff147ca3bf | ||
|
|
f745e915d3 | ||
|
|
1e239658d8 | ||
|
|
52f35d74b9 | ||
|
|
57e6e49749 | ||
|
|
b3bbb9d97a | ||
|
|
697749b204 | ||
|
|
a769545e39 | ||
|
|
df40302fd0 | ||
|
|
85e5a89298 | ||
|
|
5bd30b6b5f | ||
|
|
ed84ed8475 | ||
|
|
a444b80c96 | ||
|
|
fc94f3ad94 | ||
|
|
d43b766448 | ||
|
|
6ceca348cb | ||
|
|
bff6f056d0 | ||
|
|
366a66b331 | ||
|
|
43c56af0fc | ||
|
|
c0370c5703 | ||
|
|
8d5551343b | ||
|
|
4089e8537a | ||
|
|
cf06c6e974 | ||
|
|
2ebab4c89e | ||
|
|
8023fbae24 | ||
|
|
35cb652c6c | ||
|
|
c6085779a4 | ||
|
|
86232eb239 | ||
|
|
6db48a24b8 | ||
|
|
5cc3e914fa | ||
|
|
1da937d639 | ||
|
|
4924bcc298 | ||
|
|
a05f3d6ee9 | ||
|
|
d52d3d909f | ||
|
|
eec44dd62d | ||
|
|
e7da9a151b | ||
|
|
f75ba9d2c3 | ||
|
|
be61d41f5e | ||
|
|
f619a97f1a | ||
|
|
3f41f19ab9 | ||
|
|
e47449e357 | ||
|
|
178345ab12 | ||
|
|
a8340fef68 | ||
|
|
052b58ab90 | ||
|
|
cc5443152b | ||
|
|
d937d8970d | ||
|
|
dab4b6d56b | ||
|
|
1535820d4f | ||
|
|
b2378939c5 | ||
|
|
46b212b2cd | ||
|
|
f47842c96d | ||
|
|
050f90a07b | ||
|
|
c1642dfa73 | ||
|
|
bbfcc640d1 | ||
|
|
e9b8c512c9 | ||
|
|
15b54a2918 | ||
|
|
cf8ce42049 | ||
|
|
7fa8d1e0c9 | ||
|
|
eefa9f4222 | ||
|
|
a0073da4b3 | ||
|
|
c6fbb08c8f | ||
|
|
affa254b9d | ||
|
|
d5fbcfcecb | ||
|
|
ecd33e5561 | ||
|
|
9d77aea6be | ||
|
|
20609bc37e | ||
|
|
b56d110f50 | ||
|
|
3e0c19a669 | ||
|
|
ab6004f21e | ||
|
|
34863765ed | ||
|
|
32223baaef | ||
|
|
3cb007d147 | ||
|
|
2f038f006b | ||
|
|
634a4c2a01 | ||
|
|
f504807812 | ||
|
|
c7bdabfc65 | ||
|
|
16a7c4fa3d | ||
|
|
3979cda8c2 | ||
|
|
250d52f72c | ||
|
|
26e5b4f404 | ||
|
|
f19c1a34ec | ||
|
|
94c7bc41be | ||
|
|
df0597f12a | ||
|
|
52cb224225 | ||
|
|
baf87e90d7 | ||
|
|
6c3fde70e8 | ||
|
|
d9f6df9815 | ||
|
|
b2539f52b8 | ||
|
|
7c154dd405 | ||
|
|
47dfdf964e | ||
|
|
10a602a4f3 | ||
|
|
e3a5c52ebf | ||
|
|
66aea5e10f | ||
|
|
e0d18e3c20 | ||
|
|
4b79d06ae3 | ||
|
|
f9c0e29db0 | ||
|
|
e53d823b5a | ||
|
|
31dede5d06 | ||
|
|
69142964c6 | ||
|
|
2eac808e18 | ||
|
|
4e40a61795 | ||
|
|
011b60eeab | ||
|
|
16654c016a | ||
|
|
9b9e6b13b7 | ||
|
|
3b11525cfb | ||
|
|
6823fd8b03 | ||
|
|
ce032fcc96 | ||
|
|
a44477dddc | ||
|
|
2a789b5a5b | ||
|
|
3709d1f1d5 | ||
|
|
1789993467 | ||
|
|
604b50ffb5 | ||
|
|
2818413c5a | ||
|
|
e958f925d3 | ||
|
|
06164af17f | ||
|
|
6131c4a66a | ||
|
|
465dde7a51 | ||
|
|
7894acf830 | ||
|
|
d923b343fc | ||
|
|
86c6f6040d | ||
|
|
b79b2d4e7e | ||
|
|
b9d2e297b6 | ||
|
|
f2778a3292 | ||
|
|
f6f9f203d2 | ||
|
|
f6c519a9e7 | ||
|
|
0036056c07 | ||
|
|
dcf156ba21 | ||
|
|
f0a1e7a0e0 | ||
|
|
b931178e59 | ||
|
|
21f3c8a387 | ||
|
|
e491c0b825 | ||
|
|
2ac2974414 | ||
|
|
ce8c21261f | ||
|
|
dd8340b400 | ||
|
|
14f58e12bb | ||
|
|
b93d33e065 | ||
|
|
4c06c9ade4 | ||
|
|
2393a611a8 | ||
|
|
495fdaf8ec | ||
|
|
da680ec2a8 | ||
|
|
3cadf7f2a2 | ||
|
|
7c7bec6f31 | ||
|
|
0683f638ce | ||
|
|
ae9daed43f | ||
|
|
5301e748de | ||
|
|
37ae6b4fa0 | ||
|
|
6695e1128c | ||
|
|
37fcfe69c7 | ||
|
|
fb146f164c | ||
|
|
850f1cb7f4 | ||
|
|
e67f7b0d0e | ||
|
|
ba8e3d419e | ||
|
|
877425267e | ||
|
|
adefa8b819 | ||
|
|
36b7377662 | ||
|
|
2d6ee93448 | ||
|
|
f78c6fbc63 | ||
|
|
62e8868e4b | ||
|
|
aed0e03f7d | ||
|
|
3b67efeab1 | ||
|
|
dcfa38e29c | ||
|
|
cf06ff86c2 | ||
|
|
1c567ec455 | ||
|
|
842fa4dfd2 | ||
|
|
cfa2714dbb | ||
|
|
30f8234bd0 | ||
|
|
ae8e3f2ef8 | ||
|
|
4d6b4b1755 | ||
|
|
0994571895 | ||
|
|
3161939de9 | ||
|
|
4d8b341b6f | ||
|
|
b7fff960a2 | ||
|
|
b6273cfef3 | ||
|
|
b3f62e1631 | ||
|
|
65e539d4c8 | ||
|
|
6d91172630 | ||
|
|
e1732076fc | ||
|
|
587f66c23d | ||
|
|
3d0c0f34ad | ||
|
|
8d4ebc171b | ||
|
|
4d3c21f1bc | ||
|
|
6ece30fa2c | ||
|
|
dfcb3b6dc1 | ||
|
|
87b4a37f3f | ||
|
|
4db2c715a0 | ||
|
|
76653a4417 | ||
|
|
cd81a59418 | ||
|
|
19596245b8 | ||
|
|
0e83420e24 | ||
|
|
119846b56b | ||
|
|
121c146ef9 | ||
|
|
3ab94c7c91 | ||
|
|
a44ed65a0b | ||
|
|
d0f84b2440 | ||
|
|
33ba629c6d | ||
|
|
9e7b288f44 | ||
|
|
fe5c6faff1 | ||
|
|
2f51ad9c3f | ||
|
|
ed1f753690 | ||
|
|
bcf3ce71ef | ||
|
|
a656285001 | ||
|
|
39cb463fbd | ||
|
|
354c982ea0 | ||
|
|
f71d38fb79 | ||
|
|
b7ff554209 | ||
|
|
ba0cf1eddf | ||
|
|
b381e8bad7 | ||
|
|
700335062e | ||
|
|
cd2bc319d8 | ||
|
|
4f8534afb3 | ||
|
|
c5b7d400f5 | ||
|
|
ef58d7bcbd | ||
|
|
95ab99be4d | ||
|
|
bbb8841f5a | ||
|
|
5b50e784cd | ||
|
|
f0af107ffa | ||
|
|
0606fca484 | ||
|
|
e6812ef6c1 | ||
|
|
260e5ec25f | ||
|
|
097f68f98c | ||
|
|
45d72b2bc6 | ||
|
|
82d4c20586 | ||
|
|
03e3f7f13c | ||
|
|
b571b39b38 | ||
|
|
f42d20f2c3 | ||
|
|
74cb876771 | ||
|
|
d78e01b7a4 | ||
|
|
73478ed0e9 | ||
|
|
887d71a9ad | ||
|
|
56095926e0 | ||
|
|
13c3fbae70 | ||
|
|
0b6845235a | ||
|
|
d2558197d2 | ||
|
|
d005521aa4 | ||
|
|
336aaa3840 | ||
|
|
edbdd95f79 | ||
|
|
bb5a8fd0bf | ||
|
|
3284e709c3 | ||
|
|
d33ae29211 | ||
|
|
da51a173d7 | ||
|
|
425ec83209 | ||
|
|
971b77451d | ||
|
|
6407101709 | ||
|
|
0a218da835 | ||
|
|
89033d2cd4 | ||
|
|
870688309a | ||
|
|
a5070162c2 | ||
|
|
8348f74513 | ||
|
|
e1babd05c1 | ||
|
|
08d43a8620 | ||
|
|
8601a67e97 | ||
|
|
640500a0e3 | ||
|
|
6ee1f1a8bf | ||
|
|
a4041524a3 | ||
|
|
c3b17df3e7 | ||
|
|
5e77c50102 | ||
|
|
1620cbc8df | ||
|
|
912b8f6ff4 | ||
|
|
486a55ed7f | ||
|
|
af50af325d | ||
|
|
feef31d1bf | ||
|
|
f5b12d81ed | ||
|
|
81200b72b4 | ||
|
|
d6ecb8c793 | ||
|
|
5038ae6b1b | ||
|
|
37a4aaeeb4 | ||
|
|
6dbb0cb1c1 | ||
|
|
3b6abb5c9f | ||
|
|
ef9e9f8c78 | ||
|
|
2a819e559b | ||
|
|
8d691b2048 | ||
|
|
1f6d5cfd6d | ||
|
|
81cb75f821 | ||
|
|
8592136683 | ||
|
|
18246418a0 | ||
|
|
a6e3b07439 | ||
|
|
c8033700c3 | ||
|
|
77f529d519 | ||
|
|
6532024330 | ||
|
|
62cc53228c | ||
|
|
532654eea8 | ||
|
|
87bffb9657 | ||
|
|
094c9076be | ||
|
|
cc0385d614 | ||
|
|
ed9b3e1230 | ||
|
|
ffd89edaa7 | ||
|
|
5543fcb736 | ||
|
|
7c897a40bf | ||
|
|
528574c550 | ||
|
|
b9f8812c98 | ||
|
|
09568050d6 | ||
|
|
3dad225568 | ||
|
|
4ca8ecf64c | ||
|
|
2f2f6114e8 | ||
|
|
e4e7d6c840 | ||
|
|
d266d9a590 | ||
|
|
1e8525162a | ||
|
|
5fed5afe1f | ||
|
|
8a49e46626 | ||
|
|
9feae66173 | ||
|
|
f8e4deb4b9 | ||
|
|
9c2fe9eac4 | ||
|
|
6aa9515fd1 | ||
|
|
54854f0984 | ||
|
|
89590d32df | ||
|
|
d53a293f28 | ||
|
|
1dea84f9bf | ||
|
|
e72bd77265 | ||
|
|
8b50097a12 | ||
|
|
fbc1bccb48 | ||
|
|
b5b881e662 | ||
|
|
30e479094f | ||
|
|
2b3244440f | ||
|
|
ba4db870e4 | ||
|
|
d3f5b03f13 | ||
|
|
e00661aa34 | ||
|
|
3afb7a0eb3 | ||
|
|
1eb69ae3d1 | ||
|
|
5eeaa0272c | ||
|
|
3e601cc2e6 | ||
|
|
3e9f2a1319 | ||
|
|
45acdd9f39 | ||
|
|
62b1df970a | ||
|
|
1aac4316ab | ||
|
|
97d3bed1d2 | ||
|
|
19216eaa88 | ||
|
|
3780aed1b7 | ||
|
|
04a2d1d33c | ||
|
|
18278da4bb | ||
|
|
553c64bd8b | ||
|
|
74abee2700 | ||
|
|
8ea159e0a1 | ||
|
|
353919239d | ||
|
|
9e74e8633a | ||
|
|
98611be544 | ||
|
|
3b7ff2285c | ||
|
|
8203c878f4 | ||
|
|
216e7b7f1d | ||
|
|
4f4480dc9b | ||
|
|
16e1abe376 | ||
|
|
2d0ebc821f | ||
|
|
67e921017c | ||
|
|
7ed96ef0bb | ||
|
|
6f7bbe4ff5 | ||
|
|
74898e4261 | ||
|
|
c664d5392c | ||
|
|
76cbc2f863 | ||
|
|
69fdaca41f | ||
|
|
5ac327272f | ||
|
|
53a2d52523 | ||
|
|
cd847adfb3 | ||
|
|
e65dc2d790 | ||
|
|
e0d8f5afac | ||
|
|
7d2f543284 | ||
|
|
ad566d8ff0 | ||
|
|
67ba517a0e | ||
|
|
9a01c8b26e | ||
|
|
b40fd29228 | ||
|
|
152440e611 | ||
|
|
ea2a94be88 | ||
|
|
0a38549c55 | ||
|
|
e3b25f3080 | ||
|
|
1de4753daa | ||
|
|
d9614cc1c5 | ||
|
|
37ff13493c | ||
|
|
9264f4a668 | ||
|
|
e254379244 | ||
|
|
4c851a0d09 | ||
|
|
a84dd7cd29 | ||
|
|
86abc392a1 | ||
|
|
9e00cb3309 | ||
|
|
7d230cc15d | ||
|
|
663703abae | ||
|
|
4bc8c57729 | ||
|
|
2c4cb6d42e | ||
|
|
b2cd421e2e | ||
|
|
8a81828a3d | ||
|
|
8e568d0f20 | ||
|
|
3b9759d5e4 | ||
|
|
277c0ee818 | ||
|
|
a3c7b3fc35 | ||
|
|
034b492788 | ||
|
|
cd00680c80 | ||
|
|
cc0ebf70a7 | ||
|
|
8087f838ef | ||
|
|
3eda1750cc | ||
|
|
2c35e27095 | ||
|
|
90487819bf | ||
|
|
50a943c131 | ||
|
|
36b91180e4 | ||
|
|
df44a84bfc | ||
|
|
e5afdb1e04 | ||
|
|
cf2774c852 | ||
|
|
36369068e1 | ||
|
|
af0812e990 | ||
|
|
0285f015e2 | ||
|
|
473b20596a | ||
|
|
fda405f35d | ||
|
|
4047cb82b9 | ||
|
|
7b57d22444 | ||
|
|
50e63bf83d | ||
|
|
8a9e257cdb | ||
|
|
3a05d8c2e8 | ||
|
|
8b222914c5 | ||
|
|
6d9182aba8 | ||
|
|
f9d3e419a0 | ||
|
|
4b0ecb1251 | ||
|
|
36a6af3266 | ||
|
|
5e0c9377f2 | ||
|
|
8db1ad6f19 | ||
|
|
52dbe14af2 | ||
|
|
9bc95a6071 | ||
|
|
b80e80bd61 | ||
|
|
59fb1dea54 |
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"
|
||||
]
|
||||
}
|
||||
|
||||
18
.travis.yml
18
.travis.yml
@@ -1,11 +1,23 @@
|
||||
dist: precise
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
apt:
|
||||
packages:
|
||||
- postgresql-9.3-postgis-2.1
|
||||
- postgresql-plpython-9.3
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
- libpango1.0-dev
|
||||
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- sudo apt-get install postgresql-plpython-9.3
|
||||
- npm install -g npm@2
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
env:
|
||||
|
||||
53
INSTALL.md
Normal file
53
INSTALL.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Installing Windshaft-CartoDB #
|
||||
|
||||
## Requirements ##
|
||||
Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1 <2.0.0
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
On Ubuntu 14.04 the dependencies can be installed with
|
||||
|
||||
```shell
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y make g++ pkg-config git-core \
|
||||
libgif-dev libjpeg-dev libcairo2-dev \
|
||||
libhiredis-dev redis-server \
|
||||
nodejs nodejs-legacy npm \
|
||||
postgresql-9.3-postgis-2.1 postgresql-plpython-9.3 postgresql-server-dev-9.3
|
||||
```
|
||||
|
||||
On Ubuntu 12.04 the [cartodb/cairo PPA](https://launchpad.net/~cartodb/+archive/ubuntu/cairo) may be useful.
|
||||
|
||||
## PostGIS setup ##
|
||||
|
||||
A `template_postgis` database is expected. One can be set up with
|
||||
|
||||
```shell
|
||||
createdb --owner postgres --template template0 template_postgis
|
||||
psql -d template_postgis -c 'CREATE EXTENSION postgis;'
|
||||
```
|
||||
|
||||
## Build/install ##
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
Note that the ```npm install``` step will populate the node_modules/
|
||||
directory with modules, some of which being compiled on demand. If you
|
||||
happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
393
NEWS.md
393
NEWS.md
@@ -1,8 +1,387 @@
|
||||
# Changelog
|
||||
|
||||
## 2.43.2
|
||||
|
||||
Released 2017-09-01
|
||||
|
||||
Bug fixes:
|
||||
- Adding a new cartodb-psql fixing a pg vulnerability
|
||||
|
||||
|
||||
## 2.43.1
|
||||
|
||||
Released 2016-05-19
|
||||
|
||||
Bug fixes:
|
||||
- Dataview error when bbox present without query rewrite data #458
|
||||
|
||||
|
||||
## 2.43.0
|
||||
|
||||
Released 2016-05-18
|
||||
|
||||
New features:
|
||||
- Overviews now support dataviews and filtering #449
|
||||
|
||||
|
||||
## 2.42.2
|
||||
|
||||
Released 2016-05-17
|
||||
|
||||
New features:
|
||||
- turbo-carto: mapnik substitution tokens support #455
|
||||
|
||||
|
||||
## 2.42.1
|
||||
|
||||
Released 2016-05-17
|
||||
- Upgraded turbo-carto to fix reversed color scales
|
||||
|
||||
|
||||
## 2.42.0
|
||||
|
||||
Released 2016-05-16
|
||||
|
||||
Bug fixes:
|
||||
- Fix named maps with analysis #453
|
||||
|
||||
Enhancements:
|
||||
- Use split strategy for head/tails turbo-carto quantification
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.9.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.9.0)
|
||||
|
||||
|
||||
## 2.41.1
|
||||
|
||||
Released 2016-05-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.8.0](https://github.com/CartoDB/camshaft/releases/tag/0.8.0)
|
||||
|
||||
Bug fixes:
|
||||
- Nicer error message when missing sql from layer options #446
|
||||
|
||||
|
||||
## 2.41.0
|
||||
|
||||
Released 2016-05-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.7.0](https://github.com/CartoDB/camshaft/releases/tag/0.7.0)
|
||||
|
||||
|
||||
## 2.40.0
|
||||
|
||||
Released 2016-05-10
|
||||
|
||||
Enhancements:
|
||||
- Use original query from source nodes #444
|
||||
|
||||
New features:
|
||||
- Allow override zoom+center or bbox for static named maps previews #443
|
||||
- Analysis layers can have a sql_wrap option to wrap node queries #441
|
||||
|
||||
|
||||
## 2.39.0
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades step-profiler to 0.3.0 to avoid dots in json keys #438
|
||||
- Use a more aggressive cache control header for node status endpoint
|
||||
|
||||
|
||||
## 2.38.1
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Fixes problem in turbo-carto dependency
|
||||
- Removes console usages
|
||||
|
||||
|
||||
## 2.38.0
|
||||
|
||||
Released 2016-05-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.7.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.7.0)
|
||||
|
||||
|
||||
## 2.37.0
|
||||
|
||||
Released 2016-05-03
|
||||
|
||||
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
|
||||
|
||||
Bug fixes:
|
||||
- Overviews integration for named layers #400
|
||||
- Support wrapped queries in named layers #405
|
||||
|
||||
|
||||
## 2.31.1
|
||||
|
||||
Released 2016-03-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.16.1](https://github.com/CartoDB/Windshaft/releases/tag/1.16.1)
|
||||
|
||||
|
||||
## 2.31.0
|
||||
|
||||
Released 2016-03-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.16.0](https://github.com/CartoDB/Windshaft/releases/tag/1.16.0)
|
||||
|
||||
|
||||
## 2.30.0
|
||||
|
||||
Released 2016-03-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.15.0](https://github.com/CartoDB/Windshaft/releases/tag/1.15.0)
|
||||
|
||||
|
||||
## 2.29.0
|
||||
|
||||
Released 2016-03-14
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.14.0](https://github.com/CartoDB/Windshaft/releases/tag/1.14.0)
|
||||
|
||||
|
||||
## 2.28.0
|
||||
|
||||
Released 2016-03-14
|
||||
|
||||
New features:
|
||||
- Added [turbo-cartocss](https://github.com/CartoDB/turbo-cartocss) to preprocess CartoCSS.
|
||||
|
||||
|
||||
## 2.27.0
|
||||
|
||||
Released 2016-03-09
|
||||
|
||||
New features:
|
||||
- Add [Surrogate-Key](https://github.com/CartoDB/cartodb/wiki/CartoDB-Surrogate-Keys) headers to responses
|
||||
|
||||
Enhancements:
|
||||
- Use new `node-cartodb-query-tables` library to obtain affected tables in queries
|
||||
|
||||
Announcements:
|
||||
- Remove deprecated tools directory
|
||||
|
||||
|
||||
## 2.26.3
|
||||
|
||||
Released 2016-03-03
|
||||
|
||||
Improvements:
|
||||
- Optimize overviews queries for efficient spatial filtering in PostgreSQL
|
||||
|
||||
|
||||
## 2.26.2
|
||||
|
||||
Released 2016-02-25
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.13.2](https://github.com/CartoDB/Windshaft/releases/tag/1.13.2)
|
||||
|
||||
|
||||
## 2.26.1
|
||||
|
||||
Released 2016-02-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.13.1](https://github.com/CartoDB/Windshaft/releases/tag/1.13.1)
|
||||
|
||||
|
||||
## 2.26.0
|
||||
|
||||
Released 2016-02-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.13.0](https://github.com/CartoDB/Windshaft/releases/tag/1.13.0)
|
||||
|
||||
|
||||
## 2.25.2
|
||||
|
||||
Released 2016-02-22
|
||||
|
||||
Bug fixes:
|
||||
- Correct URLs for widgets in named maps #381
|
||||
|
||||
|
||||
## 2.25.1
|
||||
|
||||
Released 2016-02-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.11.1](https://github.com/CartoDB/Windshaft/releases/tag/1.11.1)
|
||||
|
||||
|
||||
## 2.25.0
|
||||
|
||||
Released 2016-02-18
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.11.0](https://github.com/CartoDB/Windshaft/releases/tag/1.11.0)
|
||||
|
||||
|
||||
## 2.24.0
|
||||
|
||||
Released 2016-02-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.10.1](https://github.com/CartoDB/Windshaft/releases/tag/1.10.1)
|
||||
|
||||
|
||||
## 2.23.0
|
||||
|
||||
Released 2016-02-10
|
||||
|
||||
Improvements:
|
||||
- Support for overviews
|
||||
|
||||
|
||||
## 2.22.0
|
||||
|
||||
Released 2016-02-08
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.8.3](https://github.com/CartoDB/Windshaft/releases/tag/1.8.3)
|
||||
|
||||
|
||||
## 2.21.1
|
||||
|
||||
Released 2016-02-05
|
||||
|
||||
Bug fixes:
|
||||
- Added default config for geojson renderer
|
||||
|
||||
|
||||
## 2.21.0
|
||||
|
||||
Released 2016-02-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.8.2](https://github.com/CartoDB/Windshaft/releases/tag/1.8.2)
|
||||
|
||||
|
||||
## 2.20.0
|
||||
|
||||
Released 2016-01-20
|
||||
|
||||
Bug fixes:
|
||||
- Change redis pool name to report with a valid statsd key #363
|
||||
|
||||
Improvements:
|
||||
- Query runner improvements #359
|
||||
|
||||
Unsupported:
|
||||
- Widgets endpoints
|
||||
- Layer filters
|
||||
|
||||
Note: API for unsupported list might change in the future, use at your own peril.
|
||||
|
||||
|
||||
## 2.19.1
|
||||
|
||||
Released 2015-11-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.6.1](https://github.com/CartoDB/Windshaft/releases/tag/1.6.1)
|
||||
|
||||
|
||||
## 2.19.0
|
||||
|
||||
Released 2015-11-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [1.6.0](https://github.com/CartoDB/Windshaft/releases/tag/1.6.0)
|
||||
|
||||
## 2.18.0
|
||||
@@ -792,7 +1171,7 @@ Released 2014-03-10
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Set statsd prefix for all endpoints
|
||||
- Set statsd prefix for all endpoints
|
||||
- Respond with a permission denied on attempt to access map tiles waiving
|
||||
signature of someone who had not left any (#170)
|
||||
- Do not log an error on GET / (#177)
|
||||
@@ -832,7 +1211,7 @@ Released 2014-02-27
|
||||
Enhancements:
|
||||
|
||||
- Upgrades windshaft to 0.19.1 with many performance improvements,
|
||||
See node_modules/windshaft/NEWS
|
||||
See node_modules/windshaft/NEWS
|
||||
- Improve speed of instanciating a map (#147, #159, #165)
|
||||
- Give meaningful error on attempts to use map tokens
|
||||
with attribute service (#156)
|
||||
@@ -921,7 +1300,7 @@ Bug fixes:
|
||||
|
||||
Released 2014-01-30
|
||||
|
||||
Bug fixes:
|
||||
Bug fixes:
|
||||
|
||||
* layergroup accept both map_key and api_key (#91)
|
||||
* Fix public instanciation of signed template accessing private data (#114)
|
||||
@@ -1044,7 +1423,7 @@ Released 2013-10-03
|
||||
"[ zoom > 3]" CartoCSS snippets (note the space)
|
||||
* Fix backward compatibility handling of sqlapi.host configuration (#82)
|
||||
* Fix error for invalid text-name in CartoCSS (#81)
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
|
||||
## 1.3.4
|
||||
|
||||
@@ -1091,7 +1470,7 @@ NOTE: configuration sqlapi.host renamed to sqlapi.domain
|
||||
* Multilayer API changes
|
||||
* Layers passed by index in grid fetching url
|
||||
* Interactivity only specified in layergroup config
|
||||
* Embed cache_buster within token
|
||||
* Embed cache_buster within token
|
||||
* Use ISO format for last_modified timestamp
|
||||
* Expected LZMA encoding changed to base64
|
||||
|
||||
@@ -1145,7 +1524,7 @@ Released DD//MM//YY
|
||||
|
||||
Released DD//MM//YY
|
||||
|
||||
* Reduce default extent to allow for consistent proj4 round-tripping
|
||||
* Reduce default extent to allow for consistent proj4 round-tripping
|
||||
* Enhance reset_styles script to use full configuration (#62)
|
||||
* Have reset_styles script also drop extended keys (#58)
|
||||
* Fix example postgis parameter for simplifying input geoms (#63)
|
||||
@@ -1195,7 +1574,7 @@ Released (30/10/12)
|
||||
* Autodetect target mapnik version and let config override it
|
||||
* Add tools/reset_styles script to batch-reset (and optionally convert) styles
|
||||
* Configurable logging format (#4)
|
||||
* Detailed error on missing user metadata
|
||||
* Detailed error on missing user metadata
|
||||
* Properly handle unauthenticated requests for metadata
|
||||
* Accept "api_key" in addition to "map_key",
|
||||
both in query_string and POST body (#38)
|
||||
|
||||
50
README.md
50
README.md
@@ -13,23 +13,9 @@ This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api
|
||||
* provides a link to varnish high speed cache
|
||||
* provides a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1 <2.0.0
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
- For cache control (optional)
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
- For running the testsuite
|
||||
- ImageMagick (http://www.imagemagick.org)
|
||||
Install
|
||||
-------
|
||||
See [INSTALL.md](INSTALL.md) for detailed installation instructions.
|
||||
|
||||
Configure
|
||||
---------
|
||||
@@ -40,22 +26,6 @@ see ```./configure --help``` to see available options.
|
||||
|
||||
Look at lib/cartodb/server_options.js for more on config
|
||||
|
||||
Build/install
|
||||
-------------
|
||||
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```
|
||||
git clone
|
||||
npm install
|
||||
```
|
||||
|
||||
Note that the ```npm install``` step will populate the node_modules/
|
||||
directory with modules, some of which being compiled on demand. If you
|
||||
happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
@@ -92,9 +62,21 @@ Examples
|
||||
--------
|
||||
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
m
|
||||
|
||||
Contributing
|
||||
---
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
### Developing with a custom windshaft version
|
||||
|
||||
If you plan or want to use a custom / not released yet version of windshaft (or any other dependency) the best option is
|
||||
to use `npm link`. You can read more about it at [npm-link: Symlink a package folder](https://docs.npmjs.com/cli/link).
|
||||
|
||||
**Quick start**:
|
||||
|
||||
```shell
|
||||
~/windshaft-directory $ npm install
|
||||
~/windshaft-directory $ npm link
|
||||
~/windshaft-cartodb-directory $ npm link windshaft
|
||||
```
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -33,7 +33,7 @@ var config = {
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
@@ -147,7 +147,28 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// 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: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -174,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'
|
||||
@@ -232,7 +269,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
@@ -250,7 +287,10 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
@@ -141,7 +141,28 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// 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: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -168,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/'
|
||||
@@ -226,7 +263,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:false
|
||||
,serverMetadata: {
|
||||
@@ -250,7 +287,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/maps/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
@@ -141,7 +141,28 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// 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: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -168,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/'
|
||||
@@ -226,7 +263,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
@@ -250,7 +287,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
@@ -34,7 +34,7 @@ var config = {
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
@@ -141,6 +141,26 @@ var config = {
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// 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: {
|
||||
@@ -170,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'
|
||||
@@ -228,7 +264,7 @@ var config = {
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
@@ -246,7 +282,9 @@ var config = {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerMetadata: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
982
docs/Map-API.md
982
docs/Map-API.md
@@ -1,977 +1,19 @@
|
||||
## Maps API
|
||||
# Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and you can apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles, using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
|
||||
You can create two types of maps with the Maps API:
|
||||
|
||||
- **Anonymous maps**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
|
||||
- **Anonymous Maps**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example](/cartodb-platform/cartodb-js/getting-started/).
|
||||
|
||||
- **Named maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
- **Named Maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
## Quickstart
|
||||
## Documentation
|
||||
|
||||
### Anonymous maps
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.3.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Named maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The map config needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type ``man curl`` in bash. Using `curl`, and storing the config from above in a file `mapconfig.json`, the call would look like:
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl -X POST 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
## General Concepts
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
|
||||
### Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a named map).
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
|
||||
### Errors
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
### CORS support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
|
||||
## Anonymous Maps
|
||||
|
||||
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
|
||||
### Instantiate
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md).
|
||||
|
||||
#### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
- **layergroupid**
|
||||
The ID for that map, used to compose the URL for the tiles. The final URL is:
|
||||
|
||||
```html
|
||||
https://{account}.cartodb.com/api/v1/map/:layergroupid/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- **updated_at**
|
||||
The ISO date of the last time the data involved in the query was updated.
|
||||
|
||||
- **metadata**
|
||||
Includes information about the layers.
|
||||
-
|
||||
|
||||
- **cdn_url**
|
||||
URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Retrieve resources from the layergroup
|
||||
|
||||
###### Mapnik tiles can be accessed using:
|
||||
|
||||
These tiles will get just the mapnik layers. To get individual layers see next section.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
###### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `:layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
###### Attributes defined in `attributes` section:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
###### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer_filter/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
|
||||
`:layer_filter` can be used to select some layers to be rendered together. `:layer_filter` supports two formats:
|
||||
|
||||
- `all` alias
|
||||
|
||||
Using `all` as `:layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/0,3,4/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Once a mapnik layer is selected, all mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all mapnik layers if you want to select at least one so you will get a consistent
|
||||
behavior in the future.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
### Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **config**
|
||||
Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
|
||||
- **lmza**
|
||||
This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
|
||||
- **callback**
|
||||
JSON callback name.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl "https://documentation.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## Named Maps
|
||||
|
||||
Named maps are essentially the same as anonymous maps except the MapConfig is stored on the server and the map is given a unique name. Two other big differences are: you can create named maps from private data and that users without an API Key can see them even though they are from that private data.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
- **auth layer**
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the MapConfig is static it can contain some variables so the client can modify the map's appearance using those variables.
|
||||
|
||||
Template maps are persistent with no preset expiration. They can only be created or deleted by a CartoDB user with a valid API_KEY (see auth section).
|
||||
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
<div class="code-title">template.json</div>
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"view": {
|
||||
"zoom": 4,
|
||||
"center": {
|
||||
"lng": 0,
|
||||
"lat": 0
|
||||
},
|
||||
"bounds": {
|
||||
"west": -45,
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-) or underscores (_).
|
||||
- **auth**:
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
- **placeholders**: Variables not listed here are not substituted. Variables not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md) for more info.
|
||||
- **view** (optional): extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
- **zoom** The zoom level to use
|
||||
- **center**
|
||||
- **lng** The longitude to use for the center
|
||||
- **lat** The latitude to use for the center
|
||||
- **bounds**
|
||||
- **west**: LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
- **south**: LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
- **east**: UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
|
||||
- **north**: UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
#### Template Format
|
||||
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced.
|
||||
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
#### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
- **sql_literal** internal single-quotes will be sql-escaped
|
||||
- **sql_ident** internal double-quotes will be sql-escaped
|
||||
- **number** can only contain numerical representation
|
||||
- **css_color** can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with new options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name",
|
||||
}
|
||||
```
|
||||
|
||||
### Instantiate
|
||||
|
||||
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400)
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
#### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
|
||||
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
<div class="code-title">Response</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">Error</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
### Using JSONP
|
||||
|
||||
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
- **callback:** JSON callback name
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
#### Other Info
|
||||
|
||||
Updating a named map removes all the named map instances so they need to be initialized again.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Delete the specified template map from the server and it disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request">REQUEST</div>
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Getting a Specific Template
|
||||
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Use with CartoDB.js
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) has methods for accessing your named maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
## Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
### Maps API endpoints
|
||||
|
||||
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
#### Zoom + center
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:z**: the zoom level of the map
|
||||
* **:lat**: the latitude for the center of the map
|
||||
* **:lng**: the longitude for the center of the map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
#### Bounding Box
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:bbox/:width/:height.:format`
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:bbox**: the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
- LowerCorner longitude, in decimal degrees (aka most western)
|
||||
- LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
- UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
- UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as:
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
|
||||
```
|
||||
|
||||
#### Named map
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/named/:name/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:name**: the name of the named map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
A named maps static image will get its constraints from the [view in the template](#Arguments), if `view` is not present it will estimate the extent based on the involved tables otherwise it fallback to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
####Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
As described in the [Mapconfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
#### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
#### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
#### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
* [Quickstart](quickstart.md)
|
||||
* [General Concepts](general_concepts.md)
|
||||
* [Anonymous Maps](anonymous_maps.md)
|
||||
* [Named Maps](named_maps.md)
|
||||
* [Static Maps API](static_maps_api.md)
|
||||
|
||||
@@ -6,7 +6,7 @@ This specification describes an extension for
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension introduces a new layer type so it's possible to use a named map by its name as a layer.
|
||||
This extension introduces a new layer type so it's possible to use a Named Map by its name as a layer.
|
||||
|
||||
## 2.1 Named layers definition
|
||||
|
||||
@@ -21,18 +21,18 @@ This extension introduces a new layer type so it's possible to use a named map b
|
||||
options: {
|
||||
|
||||
// REQUIRED
|
||||
// string, the name for the named map to use
|
||||
// string, the name for the Named Map to use
|
||||
name: "world_borders",
|
||||
|
||||
// OPTIONAL
|
||||
// object, the replacement values for the named map's template placeholders
|
||||
// object, the replacement values for the Named Map's template placeholders
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details
|
||||
config: {
|
||||
"color": "#000"
|
||||
},
|
||||
|
||||
// OPTIONAL
|
||||
// string array, the authorized tokens in case the named map has auth method set to `token`
|
||||
// string array, the authorized tokens in case the Named Map has auth method set to `token`
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details
|
||||
auth_tokens: [
|
||||
"token1",
|
||||
|
||||
@@ -17,8 +17,8 @@ Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
```json
|
||||
{
|
||||
http: 'http://cdn_url.com/'
|
||||
https: 'https://secure.cdn_url.com/'
|
||||
"http": "http://cdn_url.com/",
|
||||
"https": "https://secure.cdn_url.com/"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
194
docs/anonymous_maps.md
Normal file
194
docs/anonymous_maps.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# Anonymous Maps
|
||||
|
||||
Anonymous Maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
|
||||
|
||||
## Instantiate
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
See [MapConfig File Formats](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for details.
|
||||
|
||||
#### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
Attributes | Description
|
||||
--- | ---
|
||||
layergroupid | The ID for that map, used to compose the URL for the tiles. The final URL is: `https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png`
|
||||
updated_at | The ISO date of the last time the data involved in the query was updated.
|
||||
metadata | Includes information about the layers.
|
||||
cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieve resources from the layergroup
|
||||
|
||||
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
|
||||
|
||||
#### Mapnik tiles
|
||||
|
||||
These tiles will get just the Mapnik layers. To get individual layers, see the following section.
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
#### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer}/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
#### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
|
||||
`layer_filter` can be used to select some layers to be rendered together. `layer_filter` supports two formats:
|
||||
|
||||
- `all` alias
|
||||
|
||||
Using `all` as `layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/0,3,4/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Once a Mapnik layer is selected, all Mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all Mapnik layers if you want to select at least one so you will get a consistent
|
||||
behavior in the future.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
|
||||
## Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
config | Encoded JSON with the params for creating Named Maps (the variables defined in the template).
|
||||
lmza | This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
callback | JSON callback name.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl "https://{username}.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Remove
|
||||
|
||||
Anonymous Maps cannot be removed by an API call. They will expire after about five minutes, or sometimes longer. If an Anonymous Map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
27
docs/general_concepts.md
Normal file
27
docs/general_concepts.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# General Concepts
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
|
||||
## Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a Named Map).
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
|
||||
## Errors
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
## CORS support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
564
docs/named_maps.md
Normal file
564
docs/named_maps.md
Normal file
@@ -0,0 +1,564 @@
|
||||
# Named Maps
|
||||
|
||||
Named Maps are essentially the same as Anonymous Maps except the MapConfig is stored on the server, and the map is given a unique name. You can create Named Maps from private data, and users without an API Key can view your Named Map (while keeping your data private).
|
||||
|
||||
The Named Map workflow consists of uploading a MapConfig file to CartoDB servers, to select data from your CartoDB user database by using SQL, and specifying the CartoCSS for your map.
|
||||
|
||||
The response back from the API provides the template_id of your Named Map as the `name` (the identifier of your Named Map), which is the name that you specified in the MapConfig. You can which you can then use to create your Named Map details, or [fetch XYZ tiles](#fetching-xyz-tiles-for-named-maps) directly for Named Maps.
|
||||
|
||||
**Tip:** You can also 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.
|
||||
|
||||
The main differences, compared to Anonymous Maps, is that Named Maps include:
|
||||
|
||||
- **auth token**
|
||||
This allows you to control who is able to see the map based on an auth token, and create a secure Named Map with password-protection.
|
||||
|
||||
- **template map**
|
||||
The template map is static and may contain placeholders, enabling you to modify your maps appearance by using variables. Templates maps are persistent with no preset expiration. They can only be created, or deleted, by a CartoDB user with a valid API KEY (See [auth argument](#arguments)).
|
||||
|
||||
Uploading a MapConfig creates a Named Map. MapConfigs are uploaded to the server by sending the server a "template".json file, which contain the [MapConfig specifications](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/).
|
||||
|
||||
**Note:** There is a limit of 4,096 Named Maps allowed per account. If you need to create more Named Maps, it is recommended to use a single Named Map and change the variables using [placeholders](#placeholder-format), instead of uploading multiple [Named Map MapConfigs](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#named-map-layer-options).
|
||||
|
||||
## Create
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
MapConfig | a [Named Map MapConfig](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#named-map-layer-options) is required to create a Named Map
|
||||
|
||||
#### template.json
|
||||
|
||||
The `name` argument defines how to name this "template_name".json. Note that there are some requirements for how to name a Named Map template. See the [`name`](#arguments) argument description for details.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"view": {
|
||||
"zoom": 4,
|
||||
"center": {
|
||||
"lng": 0,
|
||||
"lat": 0
|
||||
},
|
||||
"bounds": {
|
||||
"west": -45,
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Arguments
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
name | There can only be _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-), or underscores (_). _This is specific to the name of your Named Map that is specified in the `name` property of the template file_.
|
||||
|
||||
auth |
|
||||
--- | ---
|
||||
|_ method | `"token"` or `"open"` (`"open"` is the default if no method is specified. Use `"token"` to password-protect your map)
|
||||
|_ valid_tokens | when `"method"` is set to `"token"`, the values listed here allow you to instantiate the Named Map. See this [example](http://docs.cartodb.com/faqs/manipulating-your-data/#how-to-create-a-password-protected-named-map) for how to create a password-protected map.
|
||||
placeholders | Placeholders are variables that can be placed in your template.json file's SQL or CartoCSS.
|
||||
layergroup | the layergroup configurations, as specified in the template. See [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for more information.
|
||||
view (optional) | extra keys to specify the view area for the map. It can be used to have a static preview of a Named Map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
--- | ---
|
||||
|_ zoom | The zoom level to use
|
||||
|
||||
|_ center |
|
||||
--- | ---
|
||||
|_ |_ lng | The longitude to use for the center
|
||||
|_ |_ lat | The latitude to use for the center
|
||||
|
||||
|_ bounds |
|
||||
--- | ---
|
||||
|_ |_ west | LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
|_ |_ south | LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
|_ |_ east | UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
|
||||
|_ |_ north | UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
|
||||
### Placeholder Format
|
||||
|
||||
Placeholders are variables that can be placed in your template.json file. Placeholders need to be defined with a `type` and a default value for MapConfigs. See details about defining a MapConfig `type` for [Layergoup configurations](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#layergroup-configurations).
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced inside the Named Maps API.
|
||||
|
||||
#### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template need to be explicitly defined with a specific type, and default value, for each placeholder.
|
||||
|
||||
### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
Types | Description
|
||||
--- | ---
|
||||
sql_literal | internal single-quotes will be sql-escaped
|
||||
sql_ident | internal double-quotes will be sql-escaped
|
||||
number | can only contain numerical representation
|
||||
css_color | can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options, at the time of creation on the client. They can also be used to test the template by creating a default version with new options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined loosely.
|
||||
|
||||
#### Call
|
||||
|
||||
This is the call for creating the Named Map. It is sending the template.json file to the service, and the server responds with the template id.
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
The response back from the API provides the name of your MapConfig as a template, enabling you to edit the Named Map details by inserting your variables into the template where placeholders are defined, and create custom queries using SQL.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name"
|
||||
}
|
||||
```
|
||||
|
||||
## Instantiate
|
||||
|
||||
Instantiating a Named Map allows you to fetch the map tiles. You can use the Maps API to instantiate, or use the CartoDB.js `createLayer()` function. The result is an Anonymous Map.
|
||||
|
||||
#### Definition
|
||||
|
||||
```html
|
||||
POST /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
auth_token | `"token"` or `"open"` (`"open"` is the default if not specified. Use `"token"` to password-protect your map)
|
||||
|
||||
```javascript
|
||||
// params.json, this is required if the Named Map allows variables (if placeholders were defined in the template.json by the user)
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the Named Map. If there are variables missing, it will raise an error (HTTP 400).
|
||||
|
||||
**Note:** It is required that you include a `params.json` file to instantiate a Named Map that contains variables, even if you have no fields to pass and the JSON is empty. (This is specific to when a Named Map allows variables (if placeholders were defined in the template.json by the user).
|
||||
|
||||
#### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/{template_name}`.
|
||||
|
||||
Valid auth token will be needed, if required by the template.
|
||||
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named/{template_name}?auth_token={auth_token}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see [Anonymous Maps](http://docs.cartodb.com/cartodb-platform/maps-api/anonymous-maps/)).
|
||||
|
||||
## Update
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
PUT /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
### Other Information
|
||||
|
||||
Updating a Named Map removes all the Named Map instances, so they need to be initialized again.
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## Delete
|
||||
|
||||
Deletes the specified template map from the server, and disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
DELETE /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X DELETE 'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response with an error will be returned.
|
||||
|
||||
## Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## Get Template Definition
|
||||
|
||||
This gets the definition of a requested template.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/{template_name}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
|
||||
### Example
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see [template.json](#templatejson)
|
||||
}
|
||||
```
|
||||
|
||||
#### Error
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
## JSONP for Named Maps
|
||||
|
||||
If using a [JSONP](https://en.wikipedia.org/wiki/JSONP) (for old browsers) request, there is a special endpoint used to initialize and create a Named Map.
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/named/{template_name}/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Params | Description
|
||||
--- | ---
|
||||
auth_token | `"token"` or `"open"` (`"open"` is the default if no method is specified. Use `"token"` to password-protect your map)
|
||||
params | Encoded JSON with the params (variables) needed for the Named Map
|
||||
lmza | You can use an LZMA compressed file instead of a params JSON file
|
||||
callback | JSON callback name
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map/named/{template_name}/jsonp?auth_token={auth_token}&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
## 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
|
||||
{
|
||||
user_name: '{username}', // Required
|
||||
type: 'namedmap', // Required
|
||||
named_map: {
|
||||
name: '{name_of_map}', // Required, the 'name' of the Named Map that you have created
|
||||
// Optional
|
||||
layers: [{
|
||||
layer_name: "sublayer0", // Optional
|
||||
interactivity: "column1, column2, ..." // Optional
|
||||
},
|
||||
{
|
||||
layer_name: "sublayer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
},
|
||||
...
|
||||
],
|
||||
// Optional
|
||||
params: {
|
||||
color: "hex_value",
|
||||
num: 2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**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. 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)
|
||||
|
||||
- [Named Map with interactivity](http://bl.ocks.org/ohasselblad/d1a45b8ff5e7bd90cd68)
|
||||
|
||||
- [Toggling sublayers in a Named Map](http://bl.ocks.org/ohasselblad/c1a0f4913610eec53cd3)
|
||||
|
||||
## Fetching XYZ Tiles for Named Maps
|
||||
|
||||
Optionally, authenticated users can fetch projected tiles (XYZ tiles or Mapnik Retina tiles) for your Named Map.
|
||||
|
||||
### Fetch XYZ Tiles Directly with a URL
|
||||
|
||||
Authenticated users, with an auth token, can use XYZ-based URLs to fetch tiles directly, and instantiate the Named Map as part of the request to your application. You do not have to do any other steps to initialize your map.
|
||||
|
||||
To call a template_id in a URL:
|
||||
|
||||
`/{template_id}/{layer}/{z}/{x}/{y}.{format}`
|
||||
|
||||
For example, a complete URL might appear as:
|
||||
|
||||
"https://{username}.cartodb.com/api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.png"
|
||||
|
||||
The placeholders indicate the following:
|
||||
|
||||
- [`template_id`](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#response) is the response of your Named Map.
|
||||
- layers can be a number (referring to the # layer of your map), all layers of your map, or a list of layers.
|
||||
- To show just the basemap layer, enter the number value `0` in the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/0/{z}/{x}/{y}.png"
|
||||
- To show the first layer, enter the number value `1` in the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/1/{z}/{x}/{y}.png"
|
||||
- To show all layers, enter the value `all` for the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/all/{z}/{x}/{y}.png"
|
||||
- To show a [list of layers](http://docs.cartodb.com/cartodb-platform/maps-api/anonymous-maps/#blending-and-layer-selection), enter the comma separated layer value as 0,1,2 in the layer placeholder. For example, to show the basemap and the first layer, "https://{username}.cartodb.com/api/v1/map/named/{template_id}/0,1/{z}/{x}/{y}.png"
|
||||
|
||||
|
||||
### Get Mapnik Retina Tiles
|
||||
|
||||
Mapnik Retina tiles are not directly supported for Named Maps, so you cannot use the Named Map template_id. To fetch Mapnik Retina tiles, get the [layergroupid](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#response-1) to initialize the map.
|
||||
|
||||
Instantiate the map by using your `layergroupid` in the token placeholder:
|
||||
|
||||
`{token}/{z}/{x}/{y}@{scale_factor}?{x}.{format}`
|
||||
100
docs/quickstart.md
Normal file
100
docs/quickstart.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Quickstart
|
||||
|
||||
## Anonymous Maps
|
||||
|
||||
Here is an example of how to create an Anonymous Map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.3.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://{username}.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://{username}.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Named Maps
|
||||
|
||||
Let's create a Named Map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The MapConfig needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type `man curl` in bash. Using `curl`, and storing the config from above in a file `MapConfig.json`, the call would look like:
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{username}.cartodb.com/api/v1/map/named/{template_id}' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
#### Response
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
221
docs/static_maps_api.md
Normal file
221
docs/static_maps_api.md
Normal file
@@ -0,0 +1,221 @@
|
||||
# Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both Named and Anonymous Maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
## Maps API endpoints
|
||||
|
||||
Begin by instantiating either a Named or Anonymous Map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
### Zoom + center
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
token | the layergroupid token from the map instantiation
|
||||
z | the zoom level of the map
|
||||
lat | the latitude for the center of the map
|
||||
|
||||
format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
### Bounding Box
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/{token}/{bbox}/{width}/{height}.{format}`
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
token | the layergroupid token from the map instantiation
|
||||
|
||||
bbox | the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
--- | ---
|
||||
| LowerCorner longitude, in decimal degrees (aka most western)
|
||||
| LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
| UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
| UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
width | the width in pixels for the output image
|
||||
height | the height in pixels for the output image
|
||||
format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format}`
|
||||
```
|
||||
|
||||
### Named Map
|
||||
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/named/{name}/{width}/{height}.{format}
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
Param | Description
|
||||
--- | ---
|
||||
name | the name of the Named Map
|
||||
width | the width in pixels for the output image
|
||||
height | the height in pixels for the output image
|
||||
|
||||
format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
A Named Maps static image will get its constraints from the [`view` argument of the Create Named Map function](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#arguments). If `view` is not defined, it will estimate the extent based on the involved tables, otherwise it fallbacks to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
#### Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
```javascript
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
```
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
As described in the [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
## Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/{layergroupid}/{z}/{x}/{y}/{width}/{height}.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
40
lib/cartodb/api/overviews_metadata_api.js
Normal file
40
lib/cartodb/api/overviews_metadata_api.js
Normal file
@@ -0,0 +1,40 @@
|
||||
var SubstitutionTokens = require('../utils/substitution-tokens');
|
||||
|
||||
function OverviewsMetadataApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = OverviewsMetadataApi;
|
||||
|
||||
function prepareSql(sql) {
|
||||
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) {
|
||||
// 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.reduce(function(metadata, row){
|
||||
var table = row.base_table;
|
||||
var schema = row._cdb_schema_name;
|
||||
if ( !metadata[table] ) {
|
||||
metadata[table] = {};
|
||||
}
|
||||
metadata[table][row.z] = { table: row.overview_table };
|
||||
metadata[table].schema = schema;
|
||||
return metadata;
|
||||
}, {});
|
||||
return callback(null, metadata);
|
||||
});
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
function QueryTablesApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
module.exports = QueryTablesApi;
|
||||
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesInQueryRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesInQueryRows(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = rows[0].cdb_querytablestext;
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, sql, callback) {
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
')',
|
||||
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m',
|
||||
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
var result = rows[0];
|
||||
|
||||
// This is an Array, so no need to split into parts
|
||||
var tableNames = result.tablenames;
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 1000
|
||||
});
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getLastUpdatedTime = function (username, tableNames, callback) {
|
||||
if (!Array.isArray(tableNames) || tableNames.length === 0) {
|
||||
return callback(null, 0);
|
||||
}
|
||||
|
||||
var query = [
|
||||
'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY[',
|
||||
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(','),
|
||||
'])'
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleLastUpdatedTimeRows, callback);
|
||||
};
|
||||
|
||||
function handleLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch affected tables or last updated time: ' + msg));
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var lastUpdated = 0;
|
||||
if (rows.length !== 0) {
|
||||
lastUpdated = rows[0].max || 0;
|
||||
}
|
||||
|
||||
return callback(null, lastUpdated*1000);
|
||||
}
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1')
|
||||
;
|
||||
}
|
||||
@@ -13,13 +13,9 @@ module.exports = TablesExtentApi;
|
||||
* `table_name` format as valid input
|
||||
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
|
||||
*/
|
||||
TablesExtentApi.prototype.getBounds = function (username, tableNames, callback) {
|
||||
var estimatedExtentSQLs = tableNames.map(function(tableName) {
|
||||
var schemaTable = tableName.split('.');
|
||||
if (schemaTable.length > 1) {
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', '" + schemaTable[1] + "', 'the_geom_webmercator')";
|
||||
}
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', 'the_geom_webmercator')";
|
||||
TablesExtentApi.prototype.getBounds = function (username, tables, callback) {
|
||||
var estimatedExtentSQLs = tables.map(function(table) {
|
||||
return "ST_EstimatedExtent('" + table.schema_name + "', '" + table.table_name + "', 'the_geom_webmercator')";
|
||||
});
|
||||
|
||||
var query = [
|
||||
@@ -35,19 +31,17 @@ TablesExtentApi.prototype.getBounds = function (username, tableNames, callback)
|
||||
"FROM ext"
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleBoundsResult, callback);
|
||||
this.pgQueryRunner.run(username, query, function handleBoundsResult (err, rows) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch source tables: ' + msg));
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
};
|
||||
|
||||
function handleBoundsResult(err, rows, callback) {
|
||||
if (err) {
|
||||
var msg = err.message ? err.message : err;
|
||||
return callback(new Error('could not fetch source tables: ' + msg));
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
|
||||
49
lib/cartodb/backends/analysis-status.js
Normal file
49
lib/cartodb/backends/analysis-status.js
Normal file
@@ -0,0 +1,49 @@
|
||||
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 FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\'';
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
pg.query(statusQuery, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
result = result || {};
|
||||
|
||||
var rows = result.rows || [];
|
||||
|
||||
return callback(null, rows[0] || {
|
||||
node_id: nodeId,
|
||||
status: 'unknown'
|
||||
});
|
||||
}, 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);
|
||||
};
|
||||
323
lib/cartodb/backends/dataview.js
Normal file
323
lib/cartodb/backends/dataview.js
Normal file
@@ -0,0 +1,323 @@
|
||||
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!)'
|
||||
});
|
||||
|
||||
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 = {
|
||||
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.getSortedNodes().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;
|
||||
|
||||
if (ownFilter) {
|
||||
query = node.getQuery();
|
||||
} else {
|
||||
var applyFilters = {};
|
||||
applyFilters[dataviewName] = false;
|
||||
query = node.getQuery(applyFilters);
|
||||
}
|
||||
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(
|
||||
mapConfig.obj().layers,
|
||||
function(l){ return l.options.source && (l.options.source.id === sourceId); }
|
||||
);
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if ( queryRewriteData ) {
|
||||
if ( node.type === 'source' ) {
|
||||
var filters = node.filters; // TODO: node.getFilters() when available in camshaft
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
var unfiltered_query = node.getQuery(filters_disabler);
|
||||
queryRewriteData = _.extend(
|
||||
{},
|
||||
queryRewriteData, { filters: filters, unfiltered_query: unfiltered_query }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
if ( queryRewriteData ) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom',
|
||||
srid: 4326,
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
}
|
||||
|
||||
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
|
||||
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
|
||||
);
|
||||
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
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 = {
|
||||
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.getSortedNodes().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;
|
||||
if (ownFilter) {
|
||||
query = node.getQuery();
|
||||
} else {
|
||||
var applyFilters = {};
|
||||
applyFilters[dataviewName] = false;
|
||||
query = node.getQuery(applyFilters);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
@@ -99,3 +100,37 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns a `cartodb-psql` object for a given username.
|
||||
* @param {String} username
|
||||
* @param {Function} callback function({Error}, {PSQL})
|
||||
*/
|
||||
|
||||
PgConnection.prototype.getConnection = function(username, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
|
||||
require('debug')('cachechan')("getConn1");
|
||||
step(
|
||||
function setAuth() {
|
||||
self.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.setDBConn(username, params, this);
|
||||
},
|
||||
function openConnection(err) {
|
||||
assert.ifError(err);
|
||||
return callback(err, new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
}));
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,8 +8,14 @@ function PgQueryRunner(pgConnection) {
|
||||
|
||||
module.exports = PgQueryRunner;
|
||||
|
||||
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
/**
|
||||
* Runs `query` with `username`'s PostgreSQL role, callback receives error and rows array.
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {String} query
|
||||
* @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array
|
||||
*/
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
@@ -33,8 +39,7 @@ PgQueryRunner.prototype.run = function(username, query, queryHandler, callback)
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
queryHandler(err, rows, callback);
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -21,7 +21,7 @@ var util = require('util');
|
||||
// @param opts TemplateMap options. Supported elements:
|
||||
// 'max_user_templates' limit on the number of per-user
|
||||
//
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) {
|
||||
return new TemplateMaps();
|
||||
@@ -41,7 +41,7 @@ function TemplateMaps(redis_pool, opts) {
|
||||
//
|
||||
// Map templates are owned by a user that specifies access permissions
|
||||
// for their instances.
|
||||
//
|
||||
//
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User templates: set of per-user map templates
|
||||
@@ -197,7 +197,7 @@ function templateDefaults(template) {
|
||||
// @param template layergroup template, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err, tpl_id)
|
||||
// @param callback function(err, tpl_id)
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
@@ -224,8 +224,10 @@ o.addTemplate = function(owner, template, callback) {
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
assert.ifError(err);
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
var limitReachedError = new Error("User '" + owner + "' reached limit on number of templates (" +
|
||||
numberOfTemplates + "/" + limit + ")");
|
||||
limitReachedError.http_status = 409;
|
||||
throw limitReachedError;
|
||||
}
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
@@ -294,7 +296,7 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
var self = this;
|
||||
|
||||
@@ -353,7 +355,7 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
//
|
||||
// @param callback function(err, tpl_id_list)
|
||||
// Returns a list of template identifiers
|
||||
//
|
||||
//
|
||||
o.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
};
|
||||
@@ -420,7 +422,7 @@ o.isAuthorized = function(template, authTokens) {
|
||||
// Only the ones found in the template's placeholders object
|
||||
// will be used, with missing ones taking default values.
|
||||
//
|
||||
// @returns a layergroup configuration
|
||||
// @returns a layergroup configuration
|
||||
//
|
||||
// @throws Error on malformed template or parameter
|
||||
//
|
||||
@@ -429,7 +431,7 @@ var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
function _replaceVars (str, params) {
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// Construct regular expressions for each param
|
||||
Object.keys(params).forEach(function(k) {
|
||||
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
|
||||
@@ -475,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) {
|
||||
|
||||
24
lib/cartodb/cache/model/database_tables_entry.js
vendored
24
lib/cartodb/cache/model/database_tables_entry.js
vendored
@@ -1,24 +0,0 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function DatabaseTables(dbName, tableNames) {
|
||||
this.namespace = 't';
|
||||
this.dbName = dbName;
|
||||
this.tableNames = tableNames;
|
||||
}
|
||||
|
||||
module.exports = DatabaseTables;
|
||||
|
||||
|
||||
DatabaseTables.prototype.key = function() {
|
||||
return this.tableNames.map(function(tableName) {
|
||||
return this.namespace + ':' + shortHashKey(this.dbName + ':' + tableName);
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
DatabaseTables.prototype.getCacheChannel = function() {
|
||||
return this.dbName + ':' + this.tableNames.join(',');
|
||||
};
|
||||
|
||||
function shortHashKey(target) {
|
||||
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
|
||||
}
|
||||
14
lib/cartodb/cache/named_map_provider_cache.js
vendored
14
lib/cartodb/cache/named_map_provider_cache.js
vendored
@@ -2,18 +2,23 @@ var _ = require('underscore');
|
||||
var dot = require('dot');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var AnalysisMapConfigAdapter = require('../models/analysis-mapconfig-adapter');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
var queue = require('queue-async');
|
||||
|
||||
var LruCache = require("lru-cache");
|
||||
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi) {
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, analysisBackend, userLimitsApi,
|
||||
overviewsAdapter, turboCartoAdapter) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
this.analysisMapConfigAdapter = new AnalysisMapConfigAdapter(analysisBackend);
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
this.turboCartoAdapter = turboCartoAdapter;
|
||||
|
||||
this.providerCache = new LruCache({ max: 2000 });
|
||||
}
|
||||
@@ -29,9 +34,12 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok
|
||||
namedMapProviders[providerKey] = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.queryTablesApi,
|
||||
this.namedLayersAdapter,
|
||||
this.overviewsAdapter,
|
||||
this.turboCartoAdapter,
|
||||
this.analysisMapConfigAdapter,
|
||||
user,
|
||||
templateId,
|
||||
config,
|
||||
|
||||
@@ -13,7 +13,20 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// widgets & filters
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'bins', // number
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
// widgets search
|
||||
'q'
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
@@ -187,18 +200,21 @@ 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);
|
||||
|
||||
debug('[%s ERROR] -- %d: %s', label, statusCode, err);
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = { errors: [errorMessage(err)] };
|
||||
var errorResponseBody = { errors: allErrors.map(errorMessage) };
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
@@ -229,6 +245,7 @@ module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
@@ -242,6 +259,8 @@ function statusFromErrorMessage(errMsg) {
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,12 @@ var BaseController = require('./base');
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
var DataviewBackend = require('../backends/dataview');
|
||||
var AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/map_store_provider');
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
@@ -17,24 +21,29 @@ var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {WidgetBackend} widgetBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, queryTablesApi, layergroupAffectedTables) {
|
||||
widgetBackend, surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.widgetBackend = widgetBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
@@ -66,6 +75,166 @@ LayergroupController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
|
||||
this.bbox.bind(this));
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:widgetName', cors(), userMiddleware,
|
||||
this.widget.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:widgetName/search', cors(), userMiddleware,
|
||||
this.widgetSearch.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/: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.analysisNodeStatus = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
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.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW');
|
||||
} else {
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function searchDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widget = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.getWidget(mapConfigProvider, req.params, this);
|
||||
},
|
||||
function finish(err, widget, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
} else {
|
||||
self.sendResponse(req, res, widget, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.widgetSearch = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveList(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
);
|
||||
self.widgetBackend.search(mapConfigProvider, req.params, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET WIDGET');
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res) {
|
||||
@@ -252,9 +421,8 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
|
||||
global.logger.warn('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!!affectedTables) {
|
||||
var tablesCacheEntry = new TablesCacheEntry(dbName, affectedTables);
|
||||
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, tablesCacheEntry);
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
self.send(req, res, body, status, headers);
|
||||
}
|
||||
@@ -298,17 +466,24 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
self.queryTablesApi.getAffectedTablesInQuery(user, sql, this); // in addCacheChannel
|
||||
step(
|
||||
function getConnection() {
|
||||
self.pgConnection.getConnection(user, this);
|
||||
},
|
||||
function getAffectedTables(err, connection) {
|
||||
assert.ifError(err);
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
function buildCacheChannel(err, tables) {
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
|
||||
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tableNames);
|
||||
|
||||
return tableNames;
|
||||
return tables;
|
||||
},
|
||||
function finish(err, affectedTables) {
|
||||
callback(err, affectedTables);
|
||||
}
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var windshaft = require('windshaft');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
@@ -13,9 +14,9 @@ var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
var MapConfigNamedLayersAdapter = require('../models/mapconfig_named_layers_adapter');
|
||||
var AnalysisMapConfigAdapter = require('../models/analysis-mapconfig-adapter');
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/named_map_provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_layergroup_provider');
|
||||
|
||||
@@ -25,14 +26,17 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/create_laye
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {QueryTablesApi} queryTablesApi
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigOverviewsAdapter} overviewsAdapter
|
||||
* @param {TurboCartoAdapter} turboCartoAdapter
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend, queryTablesApi,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables) {
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables,
|
||||
overviewsAdapter, turboCartoAdapter, analysisBackend) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
@@ -40,12 +44,14 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.turboCartoAdapter = turboCartoAdapter;
|
||||
|
||||
this.analysisMapConfigAdapter = new AnalysisMapConfigAdapter(analysisBackend);
|
||||
this.namedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
@@ -126,15 +132,43 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
var self = this;
|
||||
|
||||
var mapConfig;
|
||||
var analysesResults = [];
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function beforeLayergroupCreate(err, requestMapConfig) {
|
||||
function prepareAnalysisLayers(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var analysisConfiguration = {
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
var filters = {};
|
||||
if (req.params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(req.params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
self.analysisMapConfigAdapter.getMapConfig(analysisConfiguration, requestMapConfig, filters, this);
|
||||
},
|
||||
function beforeLayergroupCreate(err, requestMapConfig, _analysesResults) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
analysesResults = _analysesResults;
|
||||
self.namedLayersAdapter.getLayers(req.context.user, requestMapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
@@ -148,6 +182,40 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function addOverviewsInformation(err, requestMapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.overviewsAdapter.getLayers(
|
||||
req.context.user, requestMapConfig.layers, analysesResults,
|
||||
function(err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
requestMapConfig.layers = layers;
|
||||
}
|
||||
|
||||
return next(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
},
|
||||
function parseTurboCarto(err, requestMapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
self.turboCartoAdapter.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());
|
||||
@@ -159,12 +227,14 @@ 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, analysesResults, layergroup, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
addWidgetsUrl(req.context.user, layergroup);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
@@ -192,9 +262,12 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.metadataBackend,
|
||||
self.userLimitsApi,
|
||||
self.queryTablesApi,
|
||||
self.namedLayersAdapter,
|
||||
self.overviewsAdapter,
|
||||
self.turboCartoAdapter,
|
||||
self.analysisMapConfigAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
@@ -203,7 +276,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams/*, context*/) {
|
||||
function createLayergroup(err, mapConfig_, rendererParams) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
@@ -214,7 +287,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, this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
@@ -223,6 +296,10 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
addWidgetsUrl(cdbuser, layergroup);
|
||||
addDataviewsUrls(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()));
|
||||
|
||||
@@ -233,7 +310,7 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
};
|
||||
|
||||
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, analysesResults, layergroup, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
@@ -277,43 +354,36 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
|
||||
step(
|
||||
function checkCachedAffectedTables() {
|
||||
return self.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId);
|
||||
function getPgConnection() {
|
||||
self.pgConnection.getConnection(username, this);
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, hasCache) {
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
if (hasCache) {
|
||||
var next = this;
|
||||
var affectedTables = self.layergroupAffectedTables.get(dbName, layergroupId);
|
||||
self.queryTablesApi.getLastUpdatedTime(username, affectedTables, function(err, lastUpdatedTime) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return next(null, { affectedTables: affectedTables, lastUpdatedTime: lastUpdatedTime });
|
||||
});
|
||||
} else {
|
||||
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
|
||||
}
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result.affectedTables);
|
||||
// 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.lastUpdatedTime;
|
||||
layergroup.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + result.getLastUpdatedAt();
|
||||
layergroup.last_updated = new Date(result.getLastUpdatedAt()).toISOString();
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
addWidgetsUrl(username, layergroup);
|
||||
addDataviewsUrls(username, layergroup, mapconfig.obj());
|
||||
addAnalysesMetadata(username, layergroup, analysesResults, true);
|
||||
if (req.method === 'GET') {
|
||||
var tableCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
res.set('X-Cache-Channel', tableCacheEntry.getCacheChannel());
|
||||
if (result.affectedTables && result.affectedTables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, tableCacheEntry);
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables && result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,3 +394,72 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addAnalysesMetadata(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getSortedNodes();
|
||||
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;
|
||||
}, {})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (layer.widgets) {
|
||||
Object.keys(layer.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName].url = getUrls(username, resource);
|
||||
});
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getUrls(username, resource) {
|
||||
var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url;
|
||||
if (cdnUrl) {
|
||||
return {
|
||||
http: 'http://' + cdnUrl.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnUrl.https + '/' + username + '/api/v1/map/' + resource
|
||||
};
|
||||
} else {
|
||||
var port = global.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ var BaseController = require('./base');
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
var TablesCacheEntry = require('../cache/model/database_tables_entry');
|
||||
|
||||
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
@@ -44,7 +42,6 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
|
||||
|
||||
var self = this;
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime(this);
|
||||
@@ -54,22 +51,21 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
|
||||
if (err) {
|
||||
global.logger.log('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!result || !!result.affectedTables) {
|
||||
if (!result || !!result.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(result.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(result.lastUpdatedTime);
|
||||
lastModifiedDate = new Date(result.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
var tablesCacheEntry = new TablesCacheEntry(dbName, result.affectedTables);
|
||||
res.set('X-Cache-Channel', tablesCacheEntry.getCacheChannel());
|
||||
if (result.affectedTables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, tablesCacheEntry);
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
self.send(req, res, resource, 200);
|
||||
@@ -144,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);
|
||||
@@ -196,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);
|
||||
@@ -209,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;
|
||||
}
|
||||
|
||||
@@ -231,7 +258,7 @@ NamedMapsController.prototype.getStaticImageOptions = function(cdbUser, namedMap
|
||||
return next(null);
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.affectedTables || [];
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next(null);
|
||||
|
||||
@@ -65,6 +65,7 @@ NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
var cdbuser = req.context.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.authApi.authorizedByAPIKey(cdbuser, req, this);
|
||||
|
||||
228
lib/cartodb/models/analysis-mapconfig-adapter.js
Normal file
228
lib/cartodb/models/analysis-mapconfig-adapter.js
Normal file
@@ -0,0 +1,228 @@
|
||||
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;
|
||||
|
||||
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) &&
|
||||
Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0;
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
AnalysisMapConfigAdapter.prototype.getMapConfig = function(analysisConfiguration, requestMapConfig, filters, callback) {
|
||||
// jshint maxcomplexity:7
|
||||
var self = this;
|
||||
filters = filters || {};
|
||||
|
||||
if (!shouldAdaptLayers(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
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, done);
|
||||
}
|
||||
|
||||
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.getSortedNodes().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) {
|
||||
analysisSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, analysisSql);
|
||||
}
|
||||
layer.options.sql = analysisSql;
|
||||
var layerDataviews = getLayerDataviews(layer, dataviews);
|
||||
layer.options.columns = layerDataviews.reduce(function(columns, dataview) {
|
||||
return columns.concat(getDataviewColumns(dataview));
|
||||
}, []);
|
||||
} else {
|
||||
missingNodesErrors.push(
|
||||
new Error('Missing analysis node.id="' + layerSourceId +'" for layer='+layerIndex)
|
||||
);
|
||||
}
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
|
||||
debug('mapconfig output', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
if (missingNodesErrors.length > 0) {
|
||||
return callback(missingNodesErrors);
|
||||
}
|
||||
|
||||
return callback(null, requestMapConfig, analysesResults);
|
||||
});
|
||||
};
|
||||
|
||||
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 getDataviewColumns(dataview) {
|
||||
var columns = [];
|
||||
var options = dataview.options;
|
||||
['column', 'aggregationColumn'].forEach(function(opt) {
|
||||
if (options.hasOwnProperty(opt)) {
|
||||
columns.push(options[opt]);
|
||||
}
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
function getDataviewsList(dataviews) {
|
||||
return Object.keys(dataviews).map(function(dataviewKey) { return dataviews[dataviewKey]; });
|
||||
}
|
||||
|
||||
function getDataviewsErrors(dataviews) {
|
||||
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;
|
||||
}
|
||||
278
lib/cartodb/models/dataview/aggregation.js
Normal file
278
lib/cartodb/models/dataview/aggregation.js
Normal file
@@ -0,0 +1,278 @@
|
||||
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']
|
||||
};
|
||||
|
||||
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, filters, 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 {
|
||||
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, filters, 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
|
||||
});
|
||||
};
|
||||
297
lib/cartodb/models/dataview/histogram.js
Normal file
297
lib/cartodb/models/dataview/histogram.js
Normal file
@@ -0,0 +1,297 @@
|
||||
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: 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');
|
||||
|
||||
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') ? override.start : 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) {
|
||||
return override.start || 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, filters, 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(', ')
|
||||
});
|
||||
};
|
||||
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;
|
||||
104
lib/cartodb/models/dataview/overviews/formula.js
Normal file
104
lib/cartodb/models/dataview/overviews/formula.js
Normal file
@@ -0,0 +1,104 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('../base');
|
||||
var BaseDataview = require('../formula');
|
||||
|
||||
var debug = require('debug')('windshaft:widget:formula:overviews');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
};
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params) {
|
||||
this.base_dataview = new BaseDataview(query, options);
|
||||
this.query = query;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = params;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
var zoom_level_factor = 100.0;
|
||||
|
||||
// Compute zoom level so that the the resolution grid size of the
|
||||
// selected overview is smaller (zoom_level_factor times smaller at least)
|
||||
// than the bounding box size.
|
||||
function zoom_level_for_bbox(bbox) {
|
||||
var px_per_tile = 256.0;
|
||||
var earth_width = 360.0;
|
||||
// TODO: now we assume overviews are computed for 1-pixel tolerance;
|
||||
// should use extended overviews metadata to compute this properly.
|
||||
if ( bbox ) {
|
||||
var bbox_values = _.map(bbox.split(','), function(v) { return +v; });
|
||||
var w = Math.abs(bbox_values[2]-bbox_values[0]);
|
||||
var h = Math.abs(bbox_values[3]-bbox_values[1]);
|
||||
var max_dim = Math.min(w, h);
|
||||
|
||||
// Find minimum suitable z
|
||||
// note that the QueryRewirter will use the minimum level overview
|
||||
// of level >= z if it exists, and otherwise the base table
|
||||
var z = Math.ceil(-Math.log(max_dim*px_per_tile/earth_width/zoom_level_factor)/Math.log(2.0));
|
||||
return Math.max(z, 0);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
Formula.prototype.sql = function(psql, filters, override, callback) {
|
||||
var _query = this.query;
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if ( formulaQueryTpl ) {
|
||||
// supported formula for use with overviews
|
||||
var zoom_level = zoom_level_for_bbox(this.options.bbox);
|
||||
_query = this.queryRewriter.query(_query, this.queryRewriteData, { zoom_level: zoom_level });
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: _query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
debug(formulaSql);
|
||||
callback = callback || override;
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
// For non supported operations (min, max) we're not using overviews.
|
||||
return this.base_dataview.sql(psql, filters, override, callback);
|
||||
};
|
||||
|
||||
Formula.prototype.format = function(result) {
|
||||
return this.base_dataview.format(result);
|
||||
};
|
||||
|
||||
Formula.prototype.getType = function() {
|
||||
return this.base_dataview.getType();
|
||||
};
|
||||
|
||||
Formula.prototype.toString = function() {
|
||||
return this.base_dataview.toString();
|
||||
};
|
||||
3
lib/cartodb/models/dataview/overviews/index.js
Normal file
3
lib/cartodb/models/dataview/overviews/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
Formula: require('./formula')
|
||||
};
|
||||
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
|
||||
});
|
||||
};
|
||||
@@ -5,18 +5,23 @@ var dot = require('dot');
|
||||
var step = require('step');
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
var templateName = require('../../backends/template_maps').templateName;
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @type {NamedMapMapConfigProvider}
|
||||
*/
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi, queryTablesApi, namedLayersAdapter,
|
||||
function NamedMapMapConfigProvider(templateMaps, pgConnection, metadataBackend, userLimitsApi,
|
||||
namedLayersAdapter, overviewsAdapter, turboCartoAdapter, analysisMapConfigAdapter,
|
||||
owner, templateId, config, authToken, params) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.queryTablesApi = queryTablesApi;
|
||||
this.namedLayersAdapter = namedLayersAdapter;
|
||||
this.turboCartoAdapter = turboCartoAdapter;
|
||||
this.analysisMapConfigAdapter = analysisMapConfigAdapter;
|
||||
this.overviewsAdapter = overviewsAdapter;
|
||||
|
||||
this.owner = owner;
|
||||
this.templateName = templateName(templateId);
|
||||
@@ -36,6 +41,7 @@ function NamedMapMapConfigProvider(templateMaps, pgConnection, userLimitsApi, qu
|
||||
this.mapConfig = null;
|
||||
this.rendererParams = null;
|
||||
this.context = {};
|
||||
this.analysesResults = [];
|
||||
}
|
||||
|
||||
module.exports = NamedMapMapConfigProvider;
|
||||
@@ -50,15 +56,29 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var mapConfig = null;
|
||||
var datasource = null;
|
||||
var rendererParams;
|
||||
var apiKey;
|
||||
|
||||
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) {
|
||||
@@ -75,9 +95,36 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, _mapConfig) {
|
||||
function prepareAnalysisLayers(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var analysisConfiguration = {
|
||||
db: {
|
||||
host: rendererParams.dbhost,
|
||||
port: rendererParams.dbport,
|
||||
dbname: rendererParams.dbname,
|
||||
user: rendererParams.dbuser,
|
||||
pass: rendererParams.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: self.owner,
|
||||
apiKey: apiKey
|
||||
}
|
||||
};
|
||||
|
||||
var filters = {};
|
||||
if (self.params.filters) {
|
||||
try {
|
||||
filters = JSON.parse(self.params.filters);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
self.analysisMapConfigAdapter.getMapConfig(analysisConfiguration, requestMapConfig, filters, this);
|
||||
},
|
||||
function prepareLayergroup(err, _mapConfig, analysesResults) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.analysesResults = analysesResults || [];
|
||||
self.namedLayersAdapter.getLayers(self.owner, _mapConfig.layers, self.pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
@@ -91,17 +138,42 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
}
|
||||
);
|
||||
},
|
||||
function beforeLayergroupCreate(err, _mapConfig, _datasource) {
|
||||
function addOverviewsInformation(err, _mapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
self.overviewsAdapter.getLayers(self.owner, _mapConfig.layers, self.analysesResults, function(err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
|
||||
return next(null, _mapConfig, datasource);
|
||||
});
|
||||
},
|
||||
function parseTurboCarto(err, _mapConfig, datasource) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
self.turboCartoAdapter.getLayers(self.owner, _mapConfig.layers, function (err, layers) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layers) {
|
||||
_mapConfig.layers = layers;
|
||||
}
|
||||
|
||||
return next(null, _mapConfig, datasource);
|
||||
});
|
||||
},
|
||||
function prepareContextLimits(err, _mapConfig, _datasource) {
|
||||
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) {
|
||||
@@ -256,7 +328,16 @@ NamedMapMapConfigProvider.prototype.getAffectedTablesAndLastUpdatedTime = functi
|
||||
},
|
||||
function getAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
self.queryTablesApi.getAffectedTablesAndLastUpdatedTime(self.owner, sql, this);
|
||||
step(
|
||||
function getConnection() {
|
||||
self.pgConnection.getConnection(self.owner, this);
|
||||
},
|
||||
function getAffectedTables(err, connection) {
|
||||
assert.ifError(err);
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function finish(err, result) {
|
||||
self.affectedTablesAndLastUpdate = result;
|
||||
|
||||
96
lib/cartodb/models/mapconfig_overviews_adapter.js
Normal file
96
lib/cartodb/models/mapconfig_overviews_adapter.js
Normal file
@@ -0,0 +1,96 @@
|
||||
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.getLayers = function(username, layers, analysesResults, 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 {
|
||||
var query_rewrite_data = { overviews: metadata };
|
||||
step(
|
||||
function collectFiltersData() {
|
||||
var filters, unfiltered_query;
|
||||
if ( layer.options.source && analysesResults ) {
|
||||
var sourceId = layer.options.source.id;
|
||||
var node = _.find(analysesResults, function(a){ return a.rootNode.params.id === sourceId; });
|
||||
if ( node ) {
|
||||
node = node.rootNode;
|
||||
filters = node.filters; // TODO: node.getFilters() when available in camshaft
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
unfiltered_query = node.getQuery(filters_disabler);
|
||||
query_rewrite_data.filters = filters;
|
||||
query_rewrite_data.unfiltered_query = unfiltered_query;
|
||||
}
|
||||
}
|
||||
this(null, filters, unfiltered_query);
|
||||
},
|
||||
function collectStatsData(err, filters, unfiltered_query) {
|
||||
var next_step = this;
|
||||
if ( filters ) {
|
||||
self.filterStatsApi.getFilterStats(
|
||||
username,
|
||||
unfiltered_query, filters,
|
||||
function(err, stats) {
|
||||
if ( !err ) {
|
||||
query_rewrite_data.filter_stats = stats;
|
||||
}
|
||||
return next_step(err);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return next_step(null);
|
||||
}
|
||||
},
|
||||
function addDataToLayer(err) {
|
||||
if ( !err && !_.isEmpty(metadata) ) {
|
||||
layer = _.extend({}, layer);
|
||||
layer.options = _.extend({}, layer.options, { query_rewrite_data: query_rewrite_data });
|
||||
}
|
||||
done(err, layer);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
};
|
||||
@@ -3,6 +3,7 @@ var bodyParser = require('body-parser');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
@@ -19,7 +20,8 @@ var windshaft = require('windshaft');
|
||||
var mapnik = windshaft.mapnik;
|
||||
|
||||
var TemplateMaps = require('./backends/template_maps.js');
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var OverviewsMetadataApi = require('./api/overviews_metadata_api');
|
||||
var 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,16 +29,22 @@ 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 TurboCartoParser = require('./utils/style/turbo-carto-parser');
|
||||
var TurboCartoAdapter = require('./utils/style/turbo-carto-adapter');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
|
||||
var redisPool = new RedisPool(_.defaults(global.environment.redis, {
|
||||
name: 'windshaft:server',
|
||||
name: 'windshaft-server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}));
|
||||
@@ -51,7 +59,8 @@ module.exports = function(serverOptions) {
|
||||
var metadataBackend = cartodbRedis({pool: redisPool});
|
||||
var pgConnection = new PgConnection(metadataBackend);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var overviewsMetadataApi = new OverviewsMetadataApi(pgQueryRunner);
|
||||
var filterStatsApi = new FilterStatsApi(pgQueryRunner);
|
||||
var userLimitsApi = new UserLimitsApi(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
@@ -131,16 +140,31 @@ module.exports = function(serverOptions) {
|
||||
var rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
var attributesBackend = new windshaft.backend.Attributes(mapStore);
|
||||
var attributesBackend = new windshaft.backend.Attributes();
|
||||
var previewBackend = new windshaft.backend.Preview(rendererCache);
|
||||
var tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
|
||||
var analysisBackend = new AnalysisBackend(serverOptions.analysis);
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(templateMaps, pgConnection, userLimitsApi, queryTablesApi);
|
||||
var overviewsAdapter = new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi);
|
||||
|
||||
var turboCartoParser = new TurboCartoParser(pgQueryRunner);
|
||||
var turboCartoAdapter = new TurboCartoAdapter(turboCartoParser);
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
analysisBackend,
|
||||
userLimitsApi,
|
||||
overviewsAdapter,
|
||||
turboCartoAdapter
|
||||
);
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
|
||||
});
|
||||
@@ -161,10 +185,11 @@ module.exports = function(serverOptions) {
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
attributesBackend,
|
||||
new windshaft.backend.Widget(),
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
queryTablesApi,
|
||||
layergroupAffectedTablesCache
|
||||
layergroupAffectedTablesCache,
|
||||
analysisBackend
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
@@ -173,10 +198,12 @@ module.exports = function(serverOptions) {
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
queryTablesApi,
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache
|
||||
layergroupAffectedTablesCache,
|
||||
overviewsAdapter,
|
||||
turboCartoAdapter,
|
||||
analysisBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
@@ -208,7 +235,7 @@ function validateOptions(opts) {
|
||||
|
||||
// Be nice and warn if configured mapnik version is != instaled mapnik version
|
||||
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
|
||||
console.warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
debug('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
var os = require('os');
|
||||
var _ = require('underscore');
|
||||
var OverviewsQueryRewriter = require('./utils/overviews_query_rewriter');
|
||||
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
@@ -15,66 +20,93 @@ var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
http: {}
|
||||
});
|
||||
|
||||
rendererConfig.mapnik.queryRewriter = overviewsQueryRewriter;
|
||||
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
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: rendererConfig.mapnik,
|
||||
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;
|
||||
};
|
||||
313
lib/cartodb/utils/overviews_query_rewriter.js
Normal file
313
lib/cartodb/utils/overviews_query_rewriter.js
Normal file
@@ -0,0 +1,313 @@
|
||||
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;
|
||||
}
|
||||
|
||||
module.exports = OverviewsQueryRewriter;
|
||||
|
||||
// TODO: some names are introudced in the queries, and the
|
||||
// '_vovw_' (for vector overviews) is used in them, but no check
|
||||
// is performed for conflicts with existing identifiers in the query.
|
||||
|
||||
// Build UNION expression to replace table, using overviews metadata
|
||||
// overviews metadata: { 1: 'table_ov1', ... }
|
||||
// assume table and overview names include schema if necessary and are quoted as needed
|
||||
function overviews_view_for_table(table, overviews_metadata, indent) {
|
||||
var condition, i, len, ov_table, overview_layers, selects, z_hi, z_lo;
|
||||
var parsed_table = TableNameParser.parse(table);
|
||||
|
||||
var sorted_overviews = []; // [[1, 'table_ov1'], ...]
|
||||
|
||||
indent = indent || ' ';
|
||||
for (var z in overviews_metadata) {
|
||||
if (overviews_metadata.hasOwnProperty(z) && z !== 'schema') {
|
||||
sorted_overviews.push([z, overviews_metadata[z].table]);
|
||||
}
|
||||
}
|
||||
sorted_overviews.sort(function(a, b){ return a[0]-b[0]; });
|
||||
|
||||
overview_layers = [];
|
||||
z_lo = null;
|
||||
for (i = 0, len = sorted_overviews.length; i < len; i++) {
|
||||
z_hi = parseInt(sorted_overviews[i][0]);
|
||||
ov_table = sorted_overviews[i][1];
|
||||
overview_layers.push([overview_z_condition(z_lo, z_hi), ov_table]);
|
||||
z_lo = z_hi;
|
||||
}
|
||||
overview_layers.push(["_vovw_z > " + z_lo, table]);
|
||||
|
||||
selects = overview_layers.map(function(condition_table) {
|
||||
condition = condition_table[0];
|
||||
ov_table = TableNameParser.parse(condition_table[1]);
|
||||
ov_table.schema = ov_table.schema || parsed_table.schema;
|
||||
var ov_identifier = TableNameParser.table_identifier(ov_table);
|
||||
return indent + "SELECT * FROM " + ov_identifier + ", _vovw_scale WHERE " + condition;
|
||||
});
|
||||
|
||||
return selects.join("\n"+indent+"UNION ALL\n");
|
||||
}
|
||||
|
||||
function overview_z_condition(z_lo, z_hi) {
|
||||
if (z_lo !== null) {
|
||||
if (z_lo === z_hi - 1) {
|
||||
return "_vovw_z = " + z_hi;
|
||||
} else {
|
||||
return "_vovw_z > " + z_lo + " AND _vovw_z <= " + z_hi;
|
||||
}
|
||||
} else {
|
||||
if (z_hi === 0) {
|
||||
return "_vovw_z = " + z_hi;
|
||||
} else {
|
||||
return "_vovw_z <= " + z_hi;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// name to be used for the view of the table using overviews
|
||||
function overviews_view_name(table) {
|
||||
var parsed_table = TableNameParser.parse(table);
|
||||
parsed_table.table = '_vovw_' + parsed_table.table;
|
||||
parsed_table.schema = null;
|
||||
return TableNameParser.table_identifier(parsed_table);
|
||||
}
|
||||
|
||||
// replace a table name in a query by anoter name
|
||||
function replace_table_in_query(sql, old_table_name, replacement) {
|
||||
var old_table = TableNameParser.parse(old_table_name);
|
||||
var old_table_ident = TableNameParser.table_identifier(old_table);
|
||||
|
||||
// regular expression prefix (beginning) to match a table name
|
||||
function pattern_prefix(schema, identifier) {
|
||||
if ( schema ) {
|
||||
// to match a table name including schema prefix
|
||||
// name should not be part of another name, so we require
|
||||
// to start a at a word boundary
|
||||
if ( identifier[0] !== '"' ) {
|
||||
return '\\b';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
} else {
|
||||
// to match a table name without schema
|
||||
// name should not begin right after a dot (i.e. have a explicit schema)
|
||||
// nor be part of another name
|
||||
// since the pattern matches the first character of the table
|
||||
// it must be put back in the replacement text
|
||||
replacement = '$01'+replacement;
|
||||
return '([^\.a-z0-9_]|^)';
|
||||
}
|
||||
}
|
||||
|
||||
// regular expression suffix (ending) to match a table name
|
||||
function pattern_suffix(identifier) {
|
||||
// name shouldn't be the prefix of a longer name
|
||||
if ( identifier[identifier.length-1] !== '"' ) {
|
||||
return '\\b';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// regular expression to match a table name
|
||||
var regexp = pattern_prefix(old_table.schema, old_table_ident) +
|
||||
old_table_ident +
|
||||
pattern_suffix(old_table_ident);
|
||||
|
||||
// replace all occurrences of the table pattern
|
||||
return sql.replace(new RegExp(regexp, 'g'), replacement);
|
||||
}
|
||||
|
||||
|
||||
function 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;
|
||||
_.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;
|
||||
} else {
|
||||
sql = query;
|
||||
}
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
// SELECT * FROM -- in the query the table is replaced by:
|
||||
// ( ... ) AS _vovw_table -- a union of overviews and base table
|
||||
//
|
||||
// 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 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;
|
||||
};
|
||||
|
||||
function zoom_level_for_query(query, zoom_level_expression, options) {
|
||||
var zoom_level = null;
|
||||
if ( _.has(options, 'zoom_level') ) {
|
||||
zoom_level = options.zoom_level || '0';
|
||||
}
|
||||
if ( zoom_level === null && !zoom_level_expression ) {
|
||||
zoom_level = '0';
|
||||
}
|
||||
return zoom_level;
|
||||
}
|
||||
|
||||
function overviews_query(query, overviews, zoom_level, zoom_level_expression) {
|
||||
if ( zoom_level || zoom_level === '0' || zoom_level === 0 ) {
|
||||
return overviews_query_with_definite_zoom(query, overviews, zoom_level);
|
||||
} else {
|
||||
return overviews_query_with_zoom_expression(query, overviews, zoom_level_expression);
|
||||
}
|
||||
}
|
||||
|
||||
function should_use_overviews(query, data) {
|
||||
data = data || {};
|
||||
var use_overviews = data.overviews && is_supported_query(query);
|
||||
if ( use_overviews && data.filters && data.filter_stats ) {
|
||||
var filtered_rows = data.filter_stats.filtered_rows;
|
||||
var unfiltered_rows = data.filter_stats.unfiltered_rows;
|
||||
if ( unfiltered_rows && (filtered_rows || filtered_rows === 0) ) {
|
||||
use_overviews = filtered_rows >= FILTER_MIN_ROWS ||
|
||||
(filtered_rows/unfiltered_rows) > FILTER_MAX_FRACTION;
|
||||
}
|
||||
}
|
||||
return use_overviews;
|
||||
}
|
||||
|
||||
function is_supported_query(sql) {
|
||||
var basic_query =
|
||||
/\s*SELECT\s+[\*a-z0-9_,\s]+?\s+FROM\s+((\"[^"]+\"|[a-z0-9_]+)\.)?(\"[^"]+\"|[a-z0-9_]+)\s*;?\s*/i;
|
||||
var unwrapped_query = new RegExp("^"+basic_query.source+"$", 'i');
|
||||
// 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));
|
||||
}
|
||||
59
lib/cartodb/utils/style/postgres-datasource.js
Normal file
59
lib/cartodb/utils/style/postgres-datasource.js
Normal file
@@ -0,0 +1,59 @@
|
||||
'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;
|
||||
}, {});
|
||||
|
||||
var method2strategy = {
|
||||
headtails: 'split'
|
||||
};
|
||||
|
||||
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: ramp, strategy: method2strategy[methodName] });
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = PostgresDatasource;
|
||||
70
lib/cartodb/utils/style/turbo-carto-adapter.js
Normal file
70
lib/cartodb/utils/style/turbo-carto-adapter.js
Normal file
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
var queue = require('queue-async');
|
||||
var SubstitutionTokens = require('../substitution-tokens');
|
||||
|
||||
function TurboCartoAdapter(turboCartoParser) {
|
||||
this.turboCartoParser = turboCartoParser;
|
||||
}
|
||||
|
||||
module.exports = TurboCartoAdapter;
|
||||
|
||||
TurboCartoAdapter.prototype.getLayers = function (username, layers, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!layers || layers.length === 0) {
|
||||
return callback(null, layers);
|
||||
}
|
||||
|
||||
var parseCartoQueue = queue(layers.length);
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
parseCartoQueue.defer(self._parseCartoCss.bind(self), username, layer);
|
||||
});
|
||||
|
||||
parseCartoQueue.awaitAll(function (err, layers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, layers);
|
||||
});
|
||||
};
|
||||
|
||||
TurboCartoAdapter.prototype._parseCartoCss = function (username, layer, callback) {
|
||||
if (isNotLayerToParseCartocss(layer)) {
|
||||
return process.nextTick(function () {
|
||||
callback(null, layer);
|
||||
});
|
||||
}
|
||||
|
||||
var sql = SubstitutionTokens.replace(layer.options.sql, {
|
||||
bbox: 'ST_MakeEnvelope(-20037508.34,-20037508.34,20037508.34,20037508.34,3857)',
|
||||
scale_denominator: '500000001',
|
||||
pixel_width: '156412',
|
||||
pixel_height: '156412'
|
||||
});
|
||||
|
||||
this.turboCartoParser.process(username, layer.options.cartocss, sql, function (err, cartocss) {
|
||||
// Only return turbo-carto errors
|
||||
if (err && err.name === 'TurboCartoError') {
|
||||
err = new Error('turbo-carto: ' + err.message);
|
||||
err.http_status = 400;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Try to continue in the rest of the cases
|
||||
if (cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
return callback(null, layer);
|
||||
});
|
||||
};
|
||||
|
||||
function isNotLayerToParseCartocss(layer) {
|
||||
if (!layer || !layer.options || !layer.options.cartocss || !layer.options.sql) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
15
lib/cartodb/utils/style/turbo-carto-parser.js
Normal file
15
lib/cartodb/utils/style/turbo-carto-parser.js
Normal file
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
var turboCarto = require('turbo-carto');
|
||||
var PostgresDatasource = require('./postgres-datasource');
|
||||
|
||||
function TurboCartoParser (pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TurboCartoParser;
|
||||
|
||||
TurboCartoParser.prototype.process = function (username, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(this.pgQueryRunner, username, sql);
|
||||
turboCarto(cartocss, datasource, callback);
|
||||
};
|
||||
19
lib/cartodb/utils/substitution-tokens.js
Normal file
19
lib/cartodb/utils/substitution-tokens.js
Normal file
@@ -0,0 +1,19 @@
|
||||
var SUBSTITUTION_TOKENS = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
var SubstitutionTokens = {
|
||||
replace: function(sql, replaceValues) {
|
||||
Object.keys(replaceValues).forEach(function(token) {
|
||||
if (SUBSTITUTION_TOKENS[token]) {
|
||||
sql = sql.replace(SUBSTITUTION_TOKENS[token], replaceValues[token]);
|
||||
}
|
||||
});
|
||||
return sql;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SubstitutionTokens;
|
||||
106
lib/cartodb/utils/table_name_parser.js
Normal file
106
lib/cartodb/utils/table_name_parser.js
Normal file
@@ -0,0 +1,106 @@
|
||||
// Quote an PostgreSQL identifier if ncecessary
|
||||
function quote_identifier_if_needed(txt) {
|
||||
if ( txt && !txt.match(/^[a-z_][a-z_0-9]*$/)) {
|
||||
return '"' + txt.replace(/\"/g, '""') + '"';
|
||||
} else {
|
||||
return txt;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse PostgreSQL table name (possibly quoted and with optional schema).+
|
||||
// Returns { schema: 'schema_name', table: 'table_name' }
|
||||
function parse_table_name(table) {
|
||||
|
||||
function split_as_quoted_parts(table_name) {
|
||||
// parse table into 'parts' that may be quoted, each part
|
||||
// in the parts array being an object { part: 'text', quoted: false/true }
|
||||
var parts = [];
|
||||
var splitted = table_name.split(/\"/);
|
||||
for (var i=0; i<splitted.length; i++ ) {
|
||||
if ( splitted[i] === '' ) {
|
||||
if ( parts.length > 0 && i < splitted.length-1 ) {
|
||||
i++;
|
||||
parts[parts.length - 1].part += '"' + splitted[i];
|
||||
}
|
||||
}
|
||||
else {
|
||||
var is_quoted = (i > 0 && splitted[i-1] === '') ||
|
||||
(i < splitted.length - 1 && splitted[i+1] === '');
|
||||
parts.push({ part: splitted[i], quoted: is_quoted });
|
||||
}
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
|
||||
var parts = split_as_quoted_parts(table);
|
||||
|
||||
function split_single_part(part) {
|
||||
var schema_part = null;
|
||||
var table_part = null;
|
||||
if ( part.quoted ) {
|
||||
table_part = part.part;
|
||||
} else {
|
||||
var parts = part.part.split('.');
|
||||
if ( parts.length === 1 ) {
|
||||
schema_part = null;
|
||||
table_part = parts[0];
|
||||
} else if ( parts.length === 2 ) {
|
||||
schema_part = parts[0];
|
||||
table_part = parts[1];
|
||||
} // else invalid table name
|
||||
}
|
||||
return {
|
||||
schema: schema_part,
|
||||
table: table_part
|
||||
};
|
||||
}
|
||||
|
||||
function split_two_parts(part1, part2) {
|
||||
var schema_part = null;
|
||||
var table_part = null;
|
||||
if ( part1.quoted && !part2.quoted ) {
|
||||
if ( part2.part[0] === '.' ) {
|
||||
schema_part = part1.part;
|
||||
table_part = part2.part.slice(1);
|
||||
} // else invalid table name (missing dot)
|
||||
} else if ( !part1.quoted && part2.quoted ) {
|
||||
if ( part1.part[part1.part.length - 1] === '.' ) {
|
||||
schema_part = part1.part.slice(0, -1);
|
||||
table_part = part2.part;
|
||||
} // else invalid table name (missing dot)
|
||||
} // else invalid table name (missing dot)
|
||||
return {
|
||||
schema: schema_part,
|
||||
table: table_part
|
||||
};
|
||||
}
|
||||
|
||||
if ( parts.length === 1 ) {
|
||||
return split_single_part(parts[0]);
|
||||
} else if ( parts.length === 2 ) {
|
||||
return split_two_parts(parts[0], parts[1]);
|
||||
} else if ( parts.length === 3 && parts[1].part === '.' ) {
|
||||
return {
|
||||
schema: parts[0].part,
|
||||
table: parts[2].part
|
||||
};
|
||||
} // else invalid table name
|
||||
}
|
||||
|
||||
function table_identifier(parsed_name) {
|
||||
if ( parsed_name && parsed_name.table ) {
|
||||
if ( parsed_name.schema ) {
|
||||
return quote_identifier_if_needed(parsed_name.schema) + '.' + quote_identifier_if_needed(parsed_name.table);
|
||||
} else {
|
||||
return quote_identifier_if_needed(parsed_name.table);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parse: parse_table_name,
|
||||
quote: quote_identifier_if_needed,
|
||||
table_identifier: table_identifier
|
||||
};
|
||||
4533
npm-shrinkwrap.json
generated
4533
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
118
package.json
118
package.json
@@ -1,61 +1,61 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.19.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"licenses": [{
|
||||
"type": "BSD",
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb/blob/master/LICENCE"
|
||||
}],
|
||||
"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.6.0",
|
||||
"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"
|
||||
},
|
||||
"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": ">=1.2.1"
|
||||
}
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.43.2",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"license": "BSD-3-Clause",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
|
||||
},
|
||||
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.8.0",
|
||||
"cartodb-psql": "0.10.1",
|
||||
"cartodb-query-tables": "~0.1.0",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"debug": "~2.2.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.13.3",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~1.3.7",
|
||||
"node-statsd": "~0.0.7",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"request": "~2.62.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.9.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "1.19.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.4.3",
|
||||
"jshint": "~2.6.0",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.8.6",
|
||||
"semver": "~1.1.4",
|
||||
"strftime": "~0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=2.14.16"
|
||||
}
|
||||
}
|
||||
|
||||
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, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('query'), '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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
67
test/acceptance/analysis/error-cases.js
Normal file
67
test/acceptance/analysis/error-cases.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
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('../../../lib/cartodb/models/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, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
assert.ok(node.hasOwnProperty('status'), 'Missing "status" attribute in node');
|
||||
assert.ok(!node.hasOwnProperty('query'), 'Unexpected "query" attribute in node');
|
||||
});
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroup.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
return done();
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it('should be able to retrieve images from analysis', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupid + '/6/31/24.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to retrieve dataviews from analysis', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupid + '/dataview/pop_max_histogram',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var dataview = JSON.parse(res.body);
|
||||
assert.equal(dataview.type, 'histogram');
|
||||
assert.equal(dataview.bins_start, 0);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to retrieve static map preview via layergroup', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/static/center/' + layergroupid + '/4/42/-3/320/240.png',
|
||||
method: 'GET',
|
||||
encoding: 'binary',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer-layergroup-static-preview.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('auto-instantiation', function() {
|
||||
it('should be able to retrieve static map preview via fixed url', function(done) {
|
||||
TestClient.getStaticMap(widgetsTemplateName, function(err, image) {
|
||||
assert.ok(!err, err);
|
||||
var fixturePath = './test/fixtures/analysis/named-map-buffer-static-preview.png';
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
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('../../lib/cartodb/models/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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -111,6 +111,6 @@ describe('health checks', function () {
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -292,7 +292,8 @@ describe('render limits', function() {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/render-timeout-fallback.png', 25,
|
||||
var referenceImagePath = './test/fixtures/render-timeout-fallback.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var TablesCacheEntry = require('../../lib/cartodb/cache/model/database_tables_entry');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
['/api/v1/map', '/user/localhost/api/v1/map'].forEach(function(layergroup_url) {
|
||||
|
||||
@@ -52,14 +52,14 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2 offset 2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -104,7 +104,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
|
||||
@@ -122,7 +122,7 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, 'test/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
IMAGE_EQUALS_HIGHER_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
}
|
||||
@@ -190,38 +190,50 @@ describe(suiteName, function() {
|
||||
});
|
||||
|
||||
|
||||
it("should include serverMedata in the response", function(done) {
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
describe('server-metadata', function() {
|
||||
var serverMetadata;
|
||||
beforeEach(function() {
|
||||
serverMetadata = global.environment.serverMetadata;
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
global.environment.serverMetadata = serverMetadata;
|
||||
});
|
||||
|
||||
it("should include serverMedata in the response", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -245,7 +257,7 @@ describe(suiteName, function() {
|
||||
]
|
||||
};
|
||||
|
||||
var expected_token;
|
||||
var expected_token;
|
||||
step(
|
||||
function do_create_get()
|
||||
{
|
||||
@@ -253,7 +265,7 @@ describe(suiteName, function() {
|
||||
assert.response(server, {
|
||||
url: layergroup_url + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost'}
|
||||
headers: {host: 'localhost'}
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check_create(err, res) {
|
||||
@@ -262,9 +274,9 @@ describe(suiteName, function() {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
expected_token = parsedBody.layergroupid.split(':')[0];
|
||||
helper.checkCache(res);
|
||||
helper.checkSurrogateKey(res, new TablesCacheEntry('test_windshaft_cartodb_user_1_db', [
|
||||
'public.test_table',
|
||||
'public.test_table_2'
|
||||
helper.checkSurrogateKey(res, new QueryTables.DatabaseTablesEntry([
|
||||
{dbname: "test_windshaft_cartodb_user_1_db", table_name: "test_table", schema_name: "public"},
|
||||
{dbname: "test_windshaft_cartodb_user_1_db", table_name: "test_table_2", schema_name: "public"},
|
||||
]).key().join(' '));
|
||||
|
||||
|
||||
@@ -329,7 +341,7 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!))' +
|
||||
' as the_geom_webmercator from test_table limit 1',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -392,7 +404,8 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = 'test/fixtures/test_multilayer_bbox.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err/*, similarity*/) {
|
||||
next(err);
|
||||
});
|
||||
@@ -413,7 +426,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
if (!cdbQueryTablesFromPostgresEnabledValue) { // only test if it was using the SQL API
|
||||
@@ -431,7 +444,8 @@ describe(suiteName, function() {
|
||||
' WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])');
|
||||
}
|
||||
|
||||
assert.imageEqualsFile(res.body, 'test/fixtures/test_multilayer_bbox.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = 'test/fixtures/test_multilayer_bbox.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err/*, similarity*/) {
|
||||
next(err);
|
||||
});
|
||||
@@ -485,8 +499,8 @@ describe(suiteName, function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
' ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -576,10 +590,10 @@ describe(suiteName, function() {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h' +
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
'ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fit:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fit:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -606,8 +620,8 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select bogus(0,0) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -617,7 +631,7 @@ describe(suiteName, function() {
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ": " + res.body);
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ": " + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/bogus.*exist/), msg);
|
||||
@@ -633,13 +647,13 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table_private_1 where cartodb_id=1',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} },
|
||||
{ options: {
|
||||
sql: 'select * from test_table_private_1 where cartodb_id=2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -684,7 +698,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc);
|
||||
assert.ok(cc);
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
next(err);
|
||||
@@ -780,7 +794,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select * from test_table where cartodb_id=1',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -830,7 +844,7 @@ describe(suiteName, function() {
|
||||
|
||||
// Check X-Cache-Channel
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(cc, "Missing X-Cache-Channel");
|
||||
assert.ok(cc, "Missing X-Cache-Channel");
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
return null;
|
||||
@@ -965,7 +979,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator",
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1007,7 +1021,7 @@ describe(suiteName, function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
}
|
||||
@@ -1031,7 +1045,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1105,7 +1119,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: sql,
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
@@ -1194,7 +1208,7 @@ describe(suiteName, function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select *, 'SQLAPINOANSWER' from test_table",
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.1.0'
|
||||
} }
|
||||
]
|
||||
@@ -1318,7 +1332,40 @@ describe(suiteName, function() {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should response to empty layers mapconfig', function(done) {
|
||||
var layergroup = {
|
||||
layers: []
|
||||
};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsedBody.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ var _ = require('underscore');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
@@ -310,8 +311,8 @@ describe('tests from old api translated to multilayer', function() {
|
||||
it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('fake error message'), [], callback);
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
return callback(new Error('fake error message'), []);
|
||||
};
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
@@ -332,13 +333,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
// TODO when affected tables query makes the request to fail layergroup should be removed
|
||||
keysToDelete['map_cfg|4fb7bd7008322ce66f22d20aebba1ab0'] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["Error: could not fetch affected tables or last updated time: fake error message"]
|
||||
errors: ["fake error message"]
|
||||
});
|
||||
|
||||
done();
|
||||
@@ -364,9 +361,11 @@ describe('tests from old api translated to multilayer', function() {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
|
||||
var affectedFn = QueryTables.getAffectedTablesFromQuery;
|
||||
QueryTables.getAffectedTablesFromQuery = function(sql, username, query, callback) {
|
||||
affectedFn({query: function(query, callback) {
|
||||
return callback(new Error('fake error message'), []);
|
||||
}}, username, query, callback);
|
||||
};
|
||||
|
||||
// reset internal cacheChannel cache
|
||||
@@ -391,7 +390,7 @@ describe('tests from old api translated to multilayer', function() {
|
||||
},
|
||||
function(res) {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
PgQueryRunner.prototype.run = runQueryFn;
|
||||
QueryTables.getAffectedTablesFromQuery = affectedFn;
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
226
test/acceptance/overviews_metadata.js
Normal file
226
test/acceptance/overviews_metadata.js
Normal file
@@ -0,0 +1,226 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
|
||||
|
||||
describe('overviews metadata', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table_overviews',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var non_overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
test_helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
it("layers with and without overviews", function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [overviews_layer, non_overviews_layer]
|
||||
};
|
||||
|
||||
var layergroup_url = '/api/v1/map';
|
||||
|
||||
var expected_token;
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
expected_token = parsedBody.layergroupid;
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_mapconfig(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
|
||||
var mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: 500000
|
||||
});
|
||||
mapStore.load(LayergroupToken.parse(expected_token).token, function(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
assert.deepEqual(non_overviews_layer, mapConfig._cfg.layers[1]);
|
||||
assert.equal(mapConfig._cfg.layers[0].type, 'cartodb');
|
||||
assert.ok(mapConfig._cfg.layers[0].options.query_rewrite_data);
|
||||
var expected_data = {
|
||||
overviews: {
|
||||
test_table_overviews: {
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
175
test/acceptance/overviews_metadata_named_maps.js
Normal file
175
test/acceptance/overviews_metadata_named_maps.js
Normal file
@@ -0,0 +1,175 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup_token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
|
||||
|
||||
describe('overviews metadata for named maps', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table_overviews',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var non_overviews_layer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'SELECT * FROM test_table',
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
test_helper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var templateId = 'overviews-template-1';
|
||||
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateId,
|
||||
auth: { method: 'open' },
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [overviews_layer, non_overviews_layer]
|
||||
}
|
||||
};
|
||||
|
||||
it("should add overviews data to layers", 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);
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
},
|
||||
function checkInstanciation(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
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.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
next(null, parsedBody.layergroupid);
|
||||
},
|
||||
|
||||
function checkMapconfig(err, layergroupId)
|
||||
{
|
||||
assert.ifError(err);
|
||||
|
||||
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: {
|
||||
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);
|
||||
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
73
test/acceptance/overviews_queries.js
Normal file
73
test/acceptance/overviews_queries.js
Normal file
@@ -0,0 +1,73 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
var assert = require('../support/assert');
|
||||
|
||||
var cartodbServer = require('../../lib/cartodb/server');
|
||||
var ServerOptions = require('./ported/support/ported_server_options');
|
||||
var testClient = require('./ported/support/test_client');
|
||||
var BaseController = require('../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('overviews_queries', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
function imageCompareFn(fixture, done) {
|
||||
return function(err, tile) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
var referenceImagePath = './test/fixtures/' + fixture;
|
||||
assert.imageBufferIsSimilarToFile(tile.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
it("should not use overview for tables without overviews", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 1, 0, 0,
|
||||
imageCompareFn('test_table_1_0_0.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for tables without overviews at z=2", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 2, 1, 1,
|
||||
imageCompareFn('test_table_2_1_1.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for tables without overviews at z=2", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table'), 3, 3, 3,
|
||||
imageCompareFn('test_table_3_3_3.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should use overview for zoom level 1", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 1, 0, 0,
|
||||
imageCompareFn('_vovw_1_test_table_1_0_0.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should use overview for zoom level 1", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 2, 1, 1,
|
||||
imageCompareFn('_vovw_2_test_table_2_1_1.png', done)
|
||||
);
|
||||
});
|
||||
|
||||
it("should not use overview for zoom level 3", function(done){
|
||||
testClient.getTile(testClient.defaultTableMapConfig('test_table_overviews'), 3, 3, 3,
|
||||
imageCompareFn('test_table_3_3_3.png', done)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -95,10 +95,12 @@ describe('blend png renderer', function() {
|
||||
var zxy = [tileRequest.z, tileRequest.x, tileRequest.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(zxy), IMAGE_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,10 +156,12 @@ describe('blend layer filtering', function() {
|
||||
|
||||
it('should filter on ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('blend http fallback', function() {
|
||||
};
|
||||
|
||||
var filteredLayersSuite = [
|
||||
['all'], // layers displayed: 2 + 4, skipping 3 as it fails
|
||||
//['all'], // layers displayed: 2 + 4, skipping 3 as it fails
|
||||
[0, 4],
|
||||
[0, 3], // skips layer 3 as it fails
|
||||
[1, 2],
|
||||
@@ -111,10 +111,12 @@ describe('blend http fallback', function() {
|
||||
|
||||
it('should fallback on http error while blending layers ' + layerFilter + '/1/0/0.png', function (done) {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(filteredLayers), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err, err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,7 +57,8 @@ describe('external resources', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
var referenceImagePath = './test/fixtures/' + fixture;
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -84,11 +84,13 @@ describe.skip('render limits', function() {
|
||||
testClient.withLayergroup(slowQueryMapConfig, options, function(err, requestTile, finish) {
|
||||
var tileUrl = '/0/0/0.png';
|
||||
requestTile(tileUrl, options, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, fixtureImage, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,7 +117,8 @@ describe('multilayer', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
checkCORSHeaders(res);
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_bigpoint_red.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
var referenceImagePath = './test/fixtures/test_bigpoint_red.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -191,7 +192,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -302,7 +304,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -400,12 +403,11 @@ describe('multilayer', function() {
|
||||
function jsonp_test(body) {
|
||||
assert.ok(body.layergroupid);
|
||||
expected_token = LayergroupToken.parse(body.layergroupid).token;
|
||||
assert.deepEqual(body.metadata, {
|
||||
layers: [
|
||||
{ type: "mapnik", "meta":{} },
|
||||
{ type: "mapnik", "meta":{} }
|
||||
]
|
||||
});
|
||||
assert.ok(body.metadata.layers.length === 2);
|
||||
assert.ok(body.metadata.layers[0].type === 'mapnik');
|
||||
assert.ok(body.metadata.layers[0].meta);
|
||||
assert.ok(body.metadata.layers[1].type === 'mapnik');
|
||||
assert.ok(body.metadata.layers[1].meta);
|
||||
didRunJsonCallback = true;
|
||||
}
|
||||
eval(res.body);
|
||||
@@ -426,7 +428,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -542,7 +545,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer1.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -728,7 +732,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer2.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -762,7 +767,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer3.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -857,7 +863,8 @@ describe('multilayer', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageEqualsFile(res.body, './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png',
|
||||
var referenceImagePath = './test/acceptance/ported/fixtures/test_table_0_0_0_multilayer4.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath,
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -1260,7 +1267,7 @@ describe('multilayer', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
checkCORSHeaders(res);
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_bigpoint_red.png',
|
||||
assert.imageBufferIsSimilarToFile(res.body, './test/fixtures/test_bigpoint_red.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
next(err);
|
||||
});
|
||||
@@ -1278,4 +1285,3 @@ describe('multilayer', function() {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -64,46 +64,6 @@ describe('multilayer error cases', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft/issues/154
|
||||
it("mapnik tokens cannot be used with attributes service", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.1.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, 1 as n, the_geom, !bbox! as b from test_table limit 1',
|
||||
cartocss: '#layer { marker-fill:red }',
|
||||
cartocss_version: '2.0.1',
|
||||
attributes: { id:'cartodb_id', columns:['n'] },
|
||||
geom_column: 'the_geom'
|
||||
} }
|
||||
]
|
||||
};
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json; charset=utf-8' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res, err) { next(err, res); });
|
||||
},
|
||||
function do_check(err, res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
var msg = parsed.errors[0];
|
||||
assert.ok(msg.match(/Attribute service cannot be activated/), msg);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("layergroup with no cartocss_version", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
|
||||
@@ -374,4 +374,3 @@ describe('multilayer interactivity and layers order', function() {
|
||||
chaosScenarios.forEach(testInteractivityLayersOrderScenario);
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ describe('raster', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(res.headers['content-type'], "image/png");
|
||||
var next = this;
|
||||
assert.imageEqualsFile(res.body,
|
||||
assert.imageBufferIsSimilarToFile(res.body,
|
||||
'./test/fixtures/raster_gray_rect.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
try {
|
||||
|
||||
@@ -34,7 +34,9 @@ describe('server_gettile', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done);
|
||||
assert.imageBufferIsSimilarToFile(
|
||||
res.body, './test/fixtures/' + fixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, done
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,12 +115,13 @@ describe('server_gettile', function() {
|
||||
assert.ok(res.headers.hasOwnProperty('x-windshaft-cache'), "Did not hit renderer cache on second time");
|
||||
assert.ok(res.headers['x-windshaft-cache'] >= 0);
|
||||
|
||||
assert.imageEqualsFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err) {
|
||||
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, imageFixture, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
finish(function(finishErr) {
|
||||
done(err || finishErr);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('server_png8_format', function() {
|
||||
assert.equal(responsePng8.headers['content-type'], "image/png");
|
||||
bufferPng8 = responsePng8.body;
|
||||
assert.ok(bufferPng8.length < bufferPng32.length);
|
||||
assert.imageBuffersAreEqual(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
assert.imageBuffersAreSimilar(bufferPng32, bufferPng8, IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, imagePaths, similarity) {
|
||||
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
|
||||
@@ -2,6 +2,10 @@ var _ = require('underscore');
|
||||
var serverOptions = require('../../../../lib/cartodb/server_options');
|
||||
var LayergroupToken = require('../../../../lib/cartodb/models/layergroup_token');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var OverviewsQueryRewriter = require('../../../../lib/cartodb/utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
module.exports = _.extend({}, serverOptions, {
|
||||
base_url: '/database/:dbname/table/:table',
|
||||
@@ -26,7 +30,8 @@ module.exports = _.extend({}, serverOptions, {
|
||||
limits: {
|
||||
render: 0,
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
queryRewriter: overviewsQueryRewriter
|
||||
},
|
||||
http: {
|
||||
timeout: 5000,
|
||||
|
||||
@@ -216,8 +216,10 @@ describe('torque', function() {
|
||||
assert.ok(tm0,
|
||||
'No layer 0 in "torque" in metadata:' + JSON.stringify(tm));
|
||||
var expectedTorqueMetadata = {"start":0,"end":86400000,"data_steps":2,"column_type":"date"};
|
||||
assert.deepEqual(tm0, expectedTorqueMetadata);
|
||||
assert.deepEqual(meta.layers[0].meta, expectedTorqueMetadata);
|
||||
Object.keys(expectedTorqueMetadata).forEach(function(k) {
|
||||
assert.equal(tm0[k], expectedTorqueMetadata[k]);
|
||||
assert.equal(meta.layers[0].meta[k], expectedTorqueMetadata[k]);
|
||||
});
|
||||
return null;
|
||||
},
|
||||
function do_get_tile(err)
|
||||
@@ -416,7 +418,7 @@ describe('torque', function() {
|
||||
assert.response(server, {
|
||||
url: '/database/windshaft_test/layergroup',
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
headers: {'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
|
||||
@@ -83,10 +83,12 @@ describe('torque png renderer', function() {
|
||||
var zxy = [z, x, y];
|
||||
it('tile ' + zxy.join('/') + '.torque.png', function (done) {
|
||||
testClient.getTileLayer(torquePngPointsMapConfig, tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, torquePngFixture(zxy), IMAGE_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,10 +107,12 @@ describe('wrap x coordinate', function() {
|
||||
var fixtureZxy = [testScenario.fixture.z, testScenario.fixture.x, testScenario.fixture.y];
|
||||
it('tile all/' + zxy.join('/') + '.png', function (done) {
|
||||
testClient.getTileLayer(plainTorqueMapConfig(testScenario.plainColor), tileRequest, function(err, res) {
|
||||
assert.imageEqualsFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL, function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
});
|
||||
assert.imageBufferIsSimilarToFile(res.body, blendPngFixture(fixtureZxy), IMG_TOLERANCE_PER_MIL,
|
||||
function(err) {
|
||||
assert.ok(!err);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
40
test/acceptance/regressions.js
Normal file
40
test/acceptance/regressions.js
Normal file
@@ -0,0 +1,40 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
describe('regressions', function() {
|
||||
|
||||
var ERROR_RESPONSE = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
it('should expose a nice error when missing sql option', function(done) {
|
||||
var mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(layergroupResult.errors[0], 'Missing sql for layer 0 options');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,8 @@ var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
var step = require('step');
|
||||
var strftime = require('strftime');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var TablesCacheEntry = require('../../lib/cartodb/cache/model/database_tables_entry');
|
||||
var redis_stats_db = 5;
|
||||
|
||||
// Pollute the PG environment to make sure
|
||||
@@ -65,7 +65,7 @@ describe('template_api', function() {
|
||||
{ options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, -5e6, 0) as the_geom_webmercator' +
|
||||
' from test_table limit 2 offset 2',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -313,51 +313,63 @@ describe('template_api', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("instance endpoint should return server metadata", function(done){
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
var tmpl = _.clone(template_acceptance1);
|
||||
tmpl.name = "rambotemplate2";
|
||||
describe('server-metadata', function() {
|
||||
var serverMetadata;
|
||||
beforeEach(function() {
|
||||
serverMetadata = global.environment.serverMetadata;
|
||||
global.environment.serverMetadata = { cdn_url : { http:'test', https: 'tests' } };
|
||||
});
|
||||
|
||||
step(function postTemplate1() {
|
||||
var next = this;
|
||||
var post_request = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(tmpl)
|
||||
};
|
||||
assert.response(server, post_request, {}, function(res) {
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function testCORS() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + tmpl.name,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
},{
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
next(null);
|
||||
});
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
var del_request = {
|
||||
url: '/api/v1/map/named/' + tmpl.name + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
};
|
||||
assert.response(server, del_request, {}, function() {
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
afterEach(function() {
|
||||
global.environment.serverMetadata = serverMetadata;
|
||||
});
|
||||
|
||||
|
||||
it("instance endpoint should return server metadata", function(done){
|
||||
var tmpl = _.clone(template_acceptance1);
|
||||
tmpl.name = "rambotemplate2";
|
||||
|
||||
step(function postTemplate1() {
|
||||
var next = this;
|
||||
var post_request = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(tmpl)
|
||||
};
|
||||
assert.response(server, post_request, {}, function(res) {
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function testCORS() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/named/' + tmpl.name,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
},{
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.ok(_.isEqual(parsed.cdn_url, global.environment.serverMetadata.cdn_url));
|
||||
next(null);
|
||||
});
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
var del_request = {
|
||||
url: '/api/v1/map/named/' + tmpl.name + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' }
|
||||
};
|
||||
assert.response(server, del_request, {}, function() {
|
||||
done();
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -720,7 +732,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -848,7 +860,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkTile(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
return null;
|
||||
@@ -869,7 +881,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkForeignSignerError(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 403,
|
||||
assert.equal(res.statusCode, 403,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('errors'),
|
||||
@@ -1036,7 +1048,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkTile_fetchOnRestart(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
@@ -1056,7 +1068,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkCacheChannel(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
@@ -1112,7 +1124,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 where cartodb_id in ( 5,6 )",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
attributes: { id:'cartodb_id', columns: ['name', 'address'] }
|
||||
} }
|
||||
@@ -1224,7 +1236,7 @@ describe('template_api', function() {
|
||||
},
|
||||
function checkAttribute(err, res) {
|
||||
assert.ifError(err);
|
||||
assert.equal(res.statusCode, 200,
|
||||
assert.equal(res.statusCode, 200,
|
||||
'Unexpected error for authorized getAttributes: ' + res.statusCode + ' -- ' + res.body);
|
||||
assert.equal(res.headers['content-type'], "application/json; charset=utf-8");
|
||||
return null;
|
||||
@@ -1277,7 +1289,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1347,7 +1359,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1393,7 +1405,8 @@ describe('template_api', function() {
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
helper.checkCache(res);
|
||||
var expectedSurrogateKey = [
|
||||
new TablesCacheEntry('test_windshaft_cartodb_user_1_db', ['public.test_table_private_1']).key(),
|
||||
new QueryTables.DatabaseTablesEntry([{dbname: 'test_windshaft_cartodb_user_1_db', schema_name: 'public',
|
||||
table_name: 'test_table_private_1'}]).key(),
|
||||
new NamedMapsCacheEntry('localhost', template_acceptance_open.name).key()
|
||||
].join(' ');
|
||||
helper.checkSurrogateKey(res, expectedSurrogateKey);
|
||||
@@ -1430,7 +1443,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1476,7 +1489,8 @@ describe('template_api', function() {
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
helper.checkCache(res);
|
||||
var expectedSurrogateKey = [
|
||||
new TablesCacheEntry('test_windshaft_cartodb_user_1_db', ['public.test_table_private_1']).key(),
|
||||
new QueryTables.DatabaseTablesEntry([{dbname: 'test_windshaft_cartodb_user_1_db', schema_name: 'public',
|
||||
table_name: 'test_table_private_1'}]).key(),
|
||||
new NamedMapsCacheEntry('localhost', template_acceptance_open.name).key()
|
||||
].join(' ');
|
||||
helper.checkSurrogateKey(res, expectedSurrogateKey);
|
||||
@@ -1503,8 +1517,8 @@ describe('template_api', function() {
|
||||
{ options: {
|
||||
sql: 'select 1 as cartodb_id, !pixel_height! as h,' +
|
||||
' ST_Buffer(!bbox!, -32*greatest(!pixel_width!,!pixel_height!)) as the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
cartocss: '#layer { polygon-fill:red; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
@@ -1516,7 +1530,7 @@ describe('template_api', function() {
|
||||
};
|
||||
var statskey = "user:localhost:mapviews";
|
||||
var redis_stats_client = redis.createClient(global.environment.redis.port);
|
||||
var template_id; // will be set on template post
|
||||
var template_id; // will be set on template post
|
||||
var now = strftime("%Y%m%d", new Date());
|
||||
var errors = [];
|
||||
step(
|
||||
@@ -1621,7 +1635,7 @@ describe('template_api', function() {
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select * from test_table_private_1 LIMIT 0",
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
} }
|
||||
@@ -1947,4 +1961,140 @@ describe('template_api', function() {
|
||||
|
||||
});
|
||||
|
||||
var torqueParamsScenarios = [
|
||||
{
|
||||
templateParams: {},
|
||||
expectedTile: [{"x__uint8":125,"y__uint8":159,"vals__uint8":[2],"dates__uint16":[0]}]
|
||||
},
|
||||
{
|
||||
templateParams: { namesFilter: "'Hawai'" },
|
||||
expectedTile: [{"x__uint8":125,"y__uint8":159,"vals__uint8":[1],"dates__uint16":[0]}]
|
||||
}
|
||||
];
|
||||
torqueParamsScenarios.forEach(function(scenario) {
|
||||
it("can instantiate with torque layer and params=" + JSON.stringify(scenario.templateParams), function(done) {
|
||||
|
||||
var torqueParamsTemplate = {
|
||||
version: '0.0.1',
|
||||
name: 'acceptance_torque_params',
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
placeholders: {
|
||||
namesFilter: {
|
||||
"type": "sql_ident",
|
||||
"default": "'Hawai', 'El Estocolmo'"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.4.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "select * from test_table_private_1 where name in (<%= namesFilter %>)",
|
||||
cartocss: "Map { -torque-frame-count:1; -torque-resolution:1; " +
|
||||
"-torque-aggregation-function:'count(*)'; -torque-time-attribute:'cartodb_id'; }",
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroupIdToDelete;
|
||||
step(
|
||||
function createTemplate() {
|
||||
var next = this;
|
||||
var createTemplateRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(torqueParamsTemplate)
|
||||
};
|
||||
assert.response(
|
||||
server,
|
||||
createTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function instantiateTemplate(err, res) {
|
||||
assert.ifError(err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.hasOwnProperty('template_id'), "Missing 'template_id' from response: " + res.body);
|
||||
var templateId = parsed.template_id;
|
||||
var instantiatePostRequest = {
|
||||
url: '/api/v1/map/named/' + templateId,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(scenario.templateParams)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(
|
||||
server,
|
||||
instantiatePostRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function requestTile(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var layergroupId = JSON.parse(res.body).layergroupid;
|
||||
layergroupIdToDelete = LayergroupToken.parse(layergroupId).token;
|
||||
|
||||
var torqueTileRequest = {
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0/0.torque.json',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
var next = this;
|
||||
assert.response(
|
||||
server,
|
||||
torqueTileRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function validateTileAndFinish(err, res) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
keysToDelete['map_cfg|' + layergroupIdToDelete] = 0;
|
||||
keysToDelete['map_tpl|localhost'] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
scenario.expectedTile
|
||||
);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
128
test/acceptance/turbo-cartocss/anonymous-maps.js
Normal file
128
test/acceptance/turbo-cartocss/anonymous-maps.js
Normal file
@@ -0,0 +1,128 @@
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
function imageCompareFn(fixture, done) {
|
||||
return function(err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
assert.imageIsSimilarToFile(image, './test/fixtures/' + fixture, IMAGE_TOLERANCE_PER_MIL, done);
|
||||
};
|
||||
}
|
||||
|
||||
function makeMapconfig(cartocss) {
|
||||
return {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": [
|
||||
'SELECT test_table.*, _prices.price FROM test_table JOIN (' +
|
||||
' SELECT 1 AS cartodb_id, 10.00 AS price',
|
||||
' UNION',
|
||||
' SELECT 2, 10.50',
|
||||
' UNION',
|
||||
' SELECT 3, 11.00',
|
||||
' UNION',
|
||||
' SELECT 4, 12.00',
|
||||
' UNION',
|
||||
' SELECT 5, 21.00',
|
||||
') _prices ON _prices.cartodb_id = test_table.cartodb_id'
|
||||
].join('\n'),
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
describe('turbo-carto for anonymous maps', function() {
|
||||
describe('parsing ramp function with colorbrewer for greens and mapnik renderer', function () {
|
||||
beforeEach(function () {
|
||||
var turboCartocss = '#layer { marker-fill: ramp([price], colorbrewer(Greens)); }';
|
||||
this.testClient = new TestClient(makeMapconfig(turboCartocss));
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
it('should get a tile with turbo-carto parsed properly', function (done) {
|
||||
var fixturePath = 'test_turbo_carto_greens_13_4011_3088.png';
|
||||
this.testClient.getTile(13, 4011, 3088, imageCompareFn(fixturePath, done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsing ramp function with colorbrewer for reds and mapnik renderer', function () {
|
||||
beforeEach(function () {
|
||||
var turboCartocss = '#layer { marker-fill: ramp([price], colorbrewer(Reds)); }';
|
||||
this.testClient = new TestClient(makeMapconfig(turboCartocss));
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
it('should get a tile with turbo-carto parsed properly', function (done) {
|
||||
var fixtureFileName = 'test_turbo_carto_reds_13_4011_3088.png';
|
||||
this.testClient.getTile(13, 4011, 3088, imageCompareFn(fixtureFileName, done));
|
||||
});
|
||||
});
|
||||
|
||||
describe('parsing ramp function with colorbrewer for greens and toque renderer', function () {
|
||||
var mapConfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "SELECT * FROM populated_places_simple_reduced where the_geom" +
|
||||
" && ST_MakeEnvelope(-90, 0, 90, 65)",
|
||||
cartocss: [
|
||||
'Map {',
|
||||
' buffer-size:0;',
|
||||
' -torque-frame-count:1;',
|
||||
' -torque-animation-duration:30;',
|
||||
' -torque-time-attribute:"cartodb_id";',
|
||||
' -torque-aggregation-function:"count(cartodb_id)";',
|
||||
' -torque-resolution:1;',
|
||||
' -torque-data-aggregation:linear;',
|
||||
'};',
|
||||
'#populated_places_simple_reduced {',
|
||||
' comp-op: multiply;',
|
||||
' marker-fill-opacity: 1;',
|
||||
' marker-line-color: #FFF;',
|
||||
' marker-line-width: 0;',
|
||||
' marker-line-opacity: 1;',
|
||||
' marker-type: rectangle;',
|
||||
' marker-width: 3;',
|
||||
' marker-fill: ramp([pop_max], colorbrewer(Greens));',
|
||||
'};'
|
||||
].join(' '),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
it('should get a tile with turbo-carto parsed properly', function (done) {
|
||||
var z = 2;
|
||||
var x = 2;
|
||||
var y = 1;
|
||||
|
||||
var pngFixture = 'torque/populated_places_simple_reduced-turbo-carto-' + [z, x, y].join('.') + '.png';
|
||||
|
||||
this.testClient.getTile(z, x, y, { layers: 0, format: 'torque.png' }, imageCompareFn(pngFixture, done));
|
||||
});
|
||||
});
|
||||
});
|
||||
94
test/acceptance/turbo-cartocss/error-cases.js
Normal file
94
test/acceptance/turbo-cartocss/error-cases.js
Normal file
@@ -0,0 +1,94 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
function makeMapconfig(markerWidth, markerFill) {
|
||||
return {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": 'SELECT * FROM populated_places_simple_reduced',
|
||||
"cartocss": createCartocss(markerWidth, markerFill)
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createCartocss(markerWidth, markerFill) {
|
||||
return [
|
||||
"#populated_places_simple_reduced {",
|
||||
" marker-fill-opacity: 0.9;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 1;",
|
||||
" marker-line-opacity: 1;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-allow-overlap: true;",
|
||||
" marker-width: " + (markerWidth || '10') + ";",
|
||||
" marker-fill: " + (markerFill || 'red') + ";",
|
||||
"}"
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
var ERROR_RESPONSE = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
describe('turbo-carto error cases', function() {
|
||||
afterEach(function (done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return invalid number of ramp error', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([pop_max], (8,24,96), (8,24,96,128))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
assert.equal(layergroup.errors.length, 1);
|
||||
assert.ok(layergroup.errors[0].match(/^turbo-carto/));
|
||||
assert.ok(layergroup.errors[0].match(/invalid\sramp\slength/i));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return invalid column from datasource', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig(null, 'ramp([wadus_column], (red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
assert.equal(layergroup.errors.length, 1);
|
||||
assert.ok(layergroup.errors[0].match(/^turbo-carto/));
|
||||
assert.ok(layergroup.errors[0].match(/unable\sto\scompute\sramp/i));
|
||||
assert.ok(layergroup.errors[0].match(/wadus_column/));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail by falling back to normal carto parser', function(done) {
|
||||
this.testClient = new TestClient(makeMapconfig('ramp([price], (8,24,96), (8,24,96));//(red, green, blue))'));
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup.hasOwnProperty('errors'));
|
||||
assert.equal(layergroup.errors.length, 1);
|
||||
assert.ok(!layergroup.errors[0].match(/^turbo-carto/));
|
||||
assert.ok(layergroup.errors[0].match(/invalid\scode/i));
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
236
test/acceptance/turbo-cartocss/named-maps.js
Normal file
236
test/acceptance/turbo-cartocss/named-maps.js
Normal file
@@ -0,0 +1,236 @@
|
||||
var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup_token');
|
||||
var testHelper = require('../../support/test_helper');
|
||||
var CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var IMAGE_TOLERANCE_PER_MIL = 10;
|
||||
|
||||
describe('turbo-carto for named maps', function() {
|
||||
|
||||
var keysToDelete;
|
||||
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var templateId = 'turbo-carto-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 test_table.*, _prices.price FROM test_table JOIN (' +
|
||||
' SELECT 1 AS cartodb_id, 10.00 AS price',
|
||||
' UNION',
|
||||
' SELECT 2, 10.50',
|
||||
' UNION',
|
||||
' SELECT 3, 11.00',
|
||||
' UNION',
|
||||
' SELECT 4, 12.00',
|
||||
' UNION',
|
||||
' SELECT 5, 21.00',
|
||||
') _prices ON _prices.cartodb_id = test_table.cartodb_id'
|
||||
].join('\n'),
|
||||
cartocss: [
|
||||
'#layer {',
|
||||
' marker-fill: ramp([price], colorbrewer(<%= color %>));',
|
||||
' marker-allow-overlap:true;',
|
||||
'}'
|
||||
].join('\n'),
|
||||
cartocss_version: '2.0.2'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var templateParamsReds = { color: 'Reds' };
|
||||
var templateParamsBlues = { color: 'Blues' };
|
||||
|
||||
it('should create a template with turbo-carto parsed properly', 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 instantiateTemplateWithReds(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(templateParamsReds)
|
||||
}, {},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
});
|
||||
},
|
||||
function checkInstanciationWithReds(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.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTileReds(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: { host: 'localhost' },
|
||||
encoding: 'binary'
|
||||
}, {},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkTileReds(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.headers['content-type'], 'image/png');
|
||||
|
||||
var fixturePath = './test/fixtures/turbo-carto-named-maps-reds.png';
|
||||
var image = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, next);
|
||||
},
|
||||
function instantiateTemplateWithBlues(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(templateParamsBlues)
|
||||
}, {},
|
||||
function(res, err) {
|
||||
return next(err, res);
|
||||
});
|
||||
},
|
||||
function checkInstanciationWithBlues(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.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTileBlues(err, layergroupId) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: { host: 'localhost' },
|
||||
encoding: 'binary'
|
||||
}, {},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkTileBlues(err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.equal(res.statusCode, 200);
|
||||
assert.equal(res.headers['content-type'], 'image/png');
|
||||
|
||||
var fixturePath = './test/fixtures/turbo-carto-named-maps-blues.png';
|
||||
var image = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
|
||||
assert.imageIsSimilarToFile(image, fixturePath, IMAGE_TOLERANCE_PER_MIL, next);
|
||||
},
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user